Wed, 31 Dec 2014 07:22:50 +0100
Correct previous dual key logic pending first delivery installment.
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 package org.mozilla.gecko.background.announcements;
7 import java.io.FileDescriptor;
8 import java.io.PrintWriter;
9 import java.net.URI;
10 import java.util.List;
11 import java.util.Locale;
13 import org.mozilla.gecko.BrowserLocaleManager;
14 import org.mozilla.gecko.background.BackgroundService;
15 import org.mozilla.gecko.background.common.GlobalConstants;
16 import org.mozilla.gecko.background.common.log.Logger;
18 import android.content.Intent;
19 import android.content.SharedPreferences;
20 import android.os.IBinder;
22 /**
23 * A Service to periodically check for new published announcements,
24 * presenting them to the user if local conditions permit.
25 *
26 * We extend IntentService, rather than just Service, because this gives us
27 * a worker thread to avoid main-thread networking.
28 *
29 * Yes, even though we're in an alarm-triggered service, it still counts
30 * as main-thread.
31 *
32 * The operation of this service is as follows:
33 *
34 * 0. Decide if a request should be made.
35 * 1. Compute the arguments to the request. This includes enough
36 * pertinent details to allow the server to pre-filter a message
37 * set, recording enough tracking details to compute statistics.
38 * 2. Issue the request. If this succeeds with a 200 or 204, great;
39 * track that timestamp for the next run through Step 0.
40 * 3. Process any received messages.
41 *
42 * Message processing is as follows:
43 *
44 * 0. Decide if message display should occur. This might involve
45 * user preference or other kinds of environmental factors.
46 * 1. Use the AnnouncementPresenter to open the announcement.
47 *
48 * Future:
49 * * Persisting of multiple announcements.
50 * * Prioritization.
51 */
52 public class AnnouncementsService extends BackgroundService implements AnnouncementsFetchDelegate {
53 private static final String WORKER_THREAD_NAME = "AnnouncementsServiceWorker";
54 private static final String LOG_TAG = "AnnounceService";
56 public AnnouncementsService() {
57 super(WORKER_THREAD_NAME);
58 Logger.setThreadLogTag(AnnouncementsConstants.GLOBAL_LOG_TAG);
59 Logger.debug(LOG_TAG, "Creating AnnouncementsService.");
60 }
62 public boolean shouldFetchAnnouncements() {
63 final long now = System.currentTimeMillis();
65 if (!backgroundDataIsEnabled()) {
66 Logger.debug(LOG_TAG, "Background data not possible. Skipping.");
67 return false;
68 }
70 // Don't fetch if we were told to back off.
71 if (getEarliestNextFetch() > now) {
72 return false;
73 }
75 // Don't do anything if we haven't waited long enough.
76 final long lastFetch = getLastFetch();
78 // Just in case the alarm manager schedules us more frequently, or something
79 // goes awry with relaunches.
80 if ((now - lastFetch) < AnnouncementsConstants.MINIMUM_FETCH_INTERVAL_MSEC) {
81 Logger.debug(LOG_TAG, "Returning: minimum fetch interval of " + AnnouncementsConstants.MINIMUM_FETCH_INTERVAL_MSEC + "ms not met.");
82 return false;
83 }
85 return true;
86 }
88 /**
89 * Display the first valid announcement in the list.
90 */
91 protected void processAnnouncements(final List<Announcement> announcements) {
92 if (announcements == null) {
93 Logger.warn(LOG_TAG, "No announcements to present.");
94 return;
95 }
97 boolean presented = false;
98 for (Announcement an : announcements) {
99 // Do this so we at least log, rather than just returning.
100 if (presented) {
101 Logger.warn(LOG_TAG, "Skipping announcement \"" + an.getTitle() + "\": one already shown.");
102 continue;
103 }
104 if (Announcement.isValidAnnouncement(an)) {
105 presented = true;
106 AnnouncementPresenter.displayAnnouncement(this, an);
107 }
108 }
109 }
111 /**
112 * If it's time to do a fetch -- we've waited long enough,
113 * we're allowed to use background data, etc. -- then issue
114 * a fetch. The subsequent background check is handled implicitly
115 * by the AlarmManager.
116 */
117 @Override
118 public void onHandleIntent(Intent intent) {
119 Logger.setThreadLogTag(AnnouncementsConstants.GLOBAL_LOG_TAG);
121 // Intent can be null. Bug 1025937.
122 if (intent == null) {
123 Logger.debug(LOG_TAG, "Short-circuiting on null intent.");
124 return;
125 }
127 Logger.debug(LOG_TAG, "Running AnnouncementsService.");
129 if (AnnouncementsConstants.DISABLED) {
130 Logger.debug(LOG_TAG, "Announcements disabled. Returning from AnnouncementsService.");
131 return;
132 }
134 if (!shouldFetchAnnouncements()) {
135 Logger.debug(LOG_TAG, "Not fetching.");
136 return;
137 }
139 // Ensure that our locale is up to date, so that the fetcher's
140 // Accept-Language header is, too.
141 BrowserLocaleManager.getInstance().getAndApplyPersistedLocale(getApplicationContext());
143 // Otherwise, grab our announcements URL and process the contents.
144 AnnouncementsFetcher.fetchAndProcessAnnouncements(getLastLaunch(), this);
145 }
147 @Override
148 public IBinder onBind(Intent intent) {
149 return null;
150 }
152 protected long getLastLaunch() {
153 return getSharedPreferences().getLong(AnnouncementsConstants.PREF_LAST_LAUNCH, 0);
154 }
156 protected SharedPreferences getSharedPreferences() {
157 return this.getSharedPreferences(AnnouncementsConstants.PREFS_BRANCH, GlobalConstants.SHARED_PREFERENCES_MODE);
158 }
160 @Override
161 protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
162 super.dump(fd, writer, args);
164 final long lastFetch = getLastFetch();
165 final long lastLaunch = getLastLaunch();
166 writer.write("AnnouncementsService: last fetch " + lastFetch +
167 ", last Firefox activity: " + lastLaunch + "\n");
168 }
170 protected void setEarliestNextFetch(final long earliestInMsec) {
171 this.getSharedPreferences().edit().putLong(AnnouncementsConstants.PREF_EARLIEST_NEXT_ANNOUNCE_FETCH, earliestInMsec).commit();
172 }
174 protected long getEarliestNextFetch() {
175 return this.getSharedPreferences().getLong(AnnouncementsConstants.PREF_EARLIEST_NEXT_ANNOUNCE_FETCH, 0L);
176 }
178 protected void setLastFetch(final long fetch) {
179 this.getSharedPreferences().edit().putLong(AnnouncementsConstants.PREF_LAST_FETCH_LOCAL_TIME, fetch).commit();
180 }
182 @Override
183 public long getLastFetch() {
184 return this.getSharedPreferences().getLong(AnnouncementsConstants.PREF_LAST_FETCH_LOCAL_TIME, 0L);
185 }
187 protected String setLastDate(final String fetch) {
188 if (fetch == null) {
189 this.getSharedPreferences().edit().remove(AnnouncementsConstants.PREF_LAST_FETCH_SERVER_DATE).commit();
190 return null;
191 }
192 this.getSharedPreferences().edit().putString(AnnouncementsConstants.PREF_LAST_FETCH_SERVER_DATE, fetch).commit();
193 return fetch;
194 }
196 @Override
197 public String getLastDate() {
198 return this.getSharedPreferences().getString(AnnouncementsConstants.PREF_LAST_FETCH_SERVER_DATE, null);
199 }
201 /**
202 * Use this to write the persisted server URL, overriding
203 * the default value.
204 * @param url a URI identifying the full request path, e.g.,
205 * "http://foo.com:1234/announce/"
206 */
207 public void setAnnouncementsServerBaseURL(final URI url) {
208 if (url == null) {
209 throw new IllegalArgumentException("url cannot be null.");
210 }
211 final String scheme = url.getScheme();
212 if (scheme == null) {
213 throw new IllegalArgumentException("url must have a scheme.");
214 }
215 if (!scheme.equalsIgnoreCase("http") && !scheme.equalsIgnoreCase("https")) {
216 throw new IllegalArgumentException("url must be http or https.");
217 }
218 SharedPreferences p = this.getSharedPreferences();
219 p.edit().putString(AnnouncementsConstants.PREF_ANNOUNCE_SERVER_BASE_URL, url.toASCIIString()).commit();
220 }
222 /**
223 * Return the service URL, including protocol version and application identifier. E.g.,
224 *
225 * "https://campaigns.services.mozilla.com/announce/1/android/"
226 */
227 @Override
228 public String getServiceURL() {
229 SharedPreferences p = this.getSharedPreferences();
230 String base = p.getString(AnnouncementsConstants.PREF_ANNOUNCE_SERVER_BASE_URL, AnnouncementsConstants.DEFAULT_ANNOUNCE_SERVER_BASE_URL);
231 return base + AnnouncementsConstants.ANNOUNCE_PATH_SUFFIX;
232 }
234 @Override
235 public Locale getLocale() {
236 return Locale.getDefault();
237 }
239 @Override
240 public String getUserAgent() {
241 return AnnouncementsConstants.USER_AGENT;
242 }
244 protected void persistTimes(long fetched, String date) {
245 setLastFetch(fetched);
246 if (date != null) {
247 setLastDate(date);
248 }
249 }
251 @Override
252 public void onNoNewAnnouncements(long fetched, String date) {
253 Logger.info(LOG_TAG, "No new announcements to display.");
254 persistTimes(fetched, date);
255 }
257 @Override
258 public void onNewAnnouncements(List<Announcement> announcements, long fetched, String date) {
259 Logger.info(LOG_TAG, "Processing announcements: " + announcements.size());
260 persistTimes(fetched, date);
261 processAnnouncements(announcements);
262 }
264 @Override
265 public void onRemoteFailure(int status) {
266 // Bump our fetch timestamp.
267 Logger.warn(LOG_TAG, "Got remote fetch status " + status + "; bumping fetch time.");
268 setLastFetch(System.currentTimeMillis());
269 }
271 @Override
272 public void onRemoteError(Exception e) {
273 // Bump our fetch timestamp.
274 Logger.warn(LOG_TAG, "Error processing response.", e);
275 setLastFetch(System.currentTimeMillis());
276 }
278 @Override
279 public void onLocalError(Exception e) {
280 Logger.error(LOG_TAG, "Got exception in fetch.", e);
281 // Do nothing yet, so we'll retry.
282 }
284 @Override
285 public void onBackoff(int retryAfterInSeconds) {
286 Logger.info(LOG_TAG, "Got retry after: " + retryAfterInSeconds);
287 final long delayInMsec = Math.max(retryAfterInSeconds * 1000, AnnouncementsConstants.DEFAULT_BACKOFF_MSEC);
288 final long fuzzedBackoffInMsec = delayInMsec + Math.round(((double) delayInMsec * 0.25d * Math.random()));
289 Logger.debug(LOG_TAG, "Fuzzed backoff: " + fuzzedBackoffInMsec + "ms.");
290 setEarliestNextFetch(fuzzedBackoffInMsec + System.currentTimeMillis());
291 }
292 }