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.

     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 }

mercurial