mobile/android/base/background/announcements/AnnouncementsService.java

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

Correct previous dual key logic pending first delivery installment.

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

mercurial