|
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/. */ |
|
4 |
|
5 package org.mozilla.gecko.background.announcements; |
|
6 |
|
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; |
|
12 |
|
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; |
|
17 |
|
18 import android.content.Intent; |
|
19 import android.content.SharedPreferences; |
|
20 import android.os.IBinder; |
|
21 |
|
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"; |
|
55 |
|
56 public AnnouncementsService() { |
|
57 super(WORKER_THREAD_NAME); |
|
58 Logger.setThreadLogTag(AnnouncementsConstants.GLOBAL_LOG_TAG); |
|
59 Logger.debug(LOG_TAG, "Creating AnnouncementsService."); |
|
60 } |
|
61 |
|
62 public boolean shouldFetchAnnouncements() { |
|
63 final long now = System.currentTimeMillis(); |
|
64 |
|
65 if (!backgroundDataIsEnabled()) { |
|
66 Logger.debug(LOG_TAG, "Background data not possible. Skipping."); |
|
67 return false; |
|
68 } |
|
69 |
|
70 // Don't fetch if we were told to back off. |
|
71 if (getEarliestNextFetch() > now) { |
|
72 return false; |
|
73 } |
|
74 |
|
75 // Don't do anything if we haven't waited long enough. |
|
76 final long lastFetch = getLastFetch(); |
|
77 |
|
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 } |
|
84 |
|
85 return true; |
|
86 } |
|
87 |
|
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 } |
|
96 |
|
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 } |
|
110 |
|
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); |
|
120 |
|
121 // Intent can be null. Bug 1025937. |
|
122 if (intent == null) { |
|
123 Logger.debug(LOG_TAG, "Short-circuiting on null intent."); |
|
124 return; |
|
125 } |
|
126 |
|
127 Logger.debug(LOG_TAG, "Running AnnouncementsService."); |
|
128 |
|
129 if (AnnouncementsConstants.DISABLED) { |
|
130 Logger.debug(LOG_TAG, "Announcements disabled. Returning from AnnouncementsService."); |
|
131 return; |
|
132 } |
|
133 |
|
134 if (!shouldFetchAnnouncements()) { |
|
135 Logger.debug(LOG_TAG, "Not fetching."); |
|
136 return; |
|
137 } |
|
138 |
|
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()); |
|
142 |
|
143 // Otherwise, grab our announcements URL and process the contents. |
|
144 AnnouncementsFetcher.fetchAndProcessAnnouncements(getLastLaunch(), this); |
|
145 } |
|
146 |
|
147 @Override |
|
148 public IBinder onBind(Intent intent) { |
|
149 return null; |
|
150 } |
|
151 |
|
152 protected long getLastLaunch() { |
|
153 return getSharedPreferences().getLong(AnnouncementsConstants.PREF_LAST_LAUNCH, 0); |
|
154 } |
|
155 |
|
156 protected SharedPreferences getSharedPreferences() { |
|
157 return this.getSharedPreferences(AnnouncementsConstants.PREFS_BRANCH, GlobalConstants.SHARED_PREFERENCES_MODE); |
|
158 } |
|
159 |
|
160 @Override |
|
161 protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { |
|
162 super.dump(fd, writer, args); |
|
163 |
|
164 final long lastFetch = getLastFetch(); |
|
165 final long lastLaunch = getLastLaunch(); |
|
166 writer.write("AnnouncementsService: last fetch " + lastFetch + |
|
167 ", last Firefox activity: " + lastLaunch + "\n"); |
|
168 } |
|
169 |
|
170 protected void setEarliestNextFetch(final long earliestInMsec) { |
|
171 this.getSharedPreferences().edit().putLong(AnnouncementsConstants.PREF_EARLIEST_NEXT_ANNOUNCE_FETCH, earliestInMsec).commit(); |
|
172 } |
|
173 |
|
174 protected long getEarliestNextFetch() { |
|
175 return this.getSharedPreferences().getLong(AnnouncementsConstants.PREF_EARLIEST_NEXT_ANNOUNCE_FETCH, 0L); |
|
176 } |
|
177 |
|
178 protected void setLastFetch(final long fetch) { |
|
179 this.getSharedPreferences().edit().putLong(AnnouncementsConstants.PREF_LAST_FETCH_LOCAL_TIME, fetch).commit(); |
|
180 } |
|
181 |
|
182 @Override |
|
183 public long getLastFetch() { |
|
184 return this.getSharedPreferences().getLong(AnnouncementsConstants.PREF_LAST_FETCH_LOCAL_TIME, 0L); |
|
185 } |
|
186 |
|
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 } |
|
195 |
|
196 @Override |
|
197 public String getLastDate() { |
|
198 return this.getSharedPreferences().getString(AnnouncementsConstants.PREF_LAST_FETCH_SERVER_DATE, null); |
|
199 } |
|
200 |
|
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 } |
|
221 |
|
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 } |
|
233 |
|
234 @Override |
|
235 public Locale getLocale() { |
|
236 return Locale.getDefault(); |
|
237 } |
|
238 |
|
239 @Override |
|
240 public String getUserAgent() { |
|
241 return AnnouncementsConstants.USER_AGENT; |
|
242 } |
|
243 |
|
244 protected void persistTimes(long fetched, String date) { |
|
245 setLastFetch(fetched); |
|
246 if (date != null) { |
|
247 setLastDate(date); |
|
248 } |
|
249 } |
|
250 |
|
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 } |
|
256 |
|
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 } |
|
263 |
|
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 } |
|
270 |
|
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 } |
|
277 |
|
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 } |
|
283 |
|
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 } |