michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko.background.announcements; michael@0: michael@0: import java.io.FileDescriptor; michael@0: import java.io.PrintWriter; michael@0: import java.net.URI; michael@0: import java.util.List; michael@0: import java.util.Locale; michael@0: michael@0: import org.mozilla.gecko.BrowserLocaleManager; michael@0: import org.mozilla.gecko.background.BackgroundService; michael@0: import org.mozilla.gecko.background.common.GlobalConstants; michael@0: import org.mozilla.gecko.background.common.log.Logger; michael@0: michael@0: import android.content.Intent; michael@0: import android.content.SharedPreferences; michael@0: import android.os.IBinder; michael@0: michael@0: /** michael@0: * A Service to periodically check for new published announcements, michael@0: * presenting them to the user if local conditions permit. michael@0: * michael@0: * We extend IntentService, rather than just Service, because this gives us michael@0: * a worker thread to avoid main-thread networking. michael@0: * michael@0: * Yes, even though we're in an alarm-triggered service, it still counts michael@0: * as main-thread. michael@0: * michael@0: * The operation of this service is as follows: michael@0: * michael@0: * 0. Decide if a request should be made. michael@0: * 1. Compute the arguments to the request. This includes enough michael@0: * pertinent details to allow the server to pre-filter a message michael@0: * set, recording enough tracking details to compute statistics. michael@0: * 2. Issue the request. If this succeeds with a 200 or 204, great; michael@0: * track that timestamp for the next run through Step 0. michael@0: * 3. Process any received messages. michael@0: * michael@0: * Message processing is as follows: michael@0: * michael@0: * 0. Decide if message display should occur. This might involve michael@0: * user preference or other kinds of environmental factors. michael@0: * 1. Use the AnnouncementPresenter to open the announcement. michael@0: * michael@0: * Future: michael@0: * * Persisting of multiple announcements. michael@0: * * Prioritization. michael@0: */ michael@0: public class AnnouncementsService extends BackgroundService implements AnnouncementsFetchDelegate { michael@0: private static final String WORKER_THREAD_NAME = "AnnouncementsServiceWorker"; michael@0: private static final String LOG_TAG = "AnnounceService"; michael@0: michael@0: public AnnouncementsService() { michael@0: super(WORKER_THREAD_NAME); michael@0: Logger.setThreadLogTag(AnnouncementsConstants.GLOBAL_LOG_TAG); michael@0: Logger.debug(LOG_TAG, "Creating AnnouncementsService."); michael@0: } michael@0: michael@0: public boolean shouldFetchAnnouncements() { michael@0: final long now = System.currentTimeMillis(); michael@0: michael@0: if (!backgroundDataIsEnabled()) { michael@0: Logger.debug(LOG_TAG, "Background data not possible. Skipping."); michael@0: return false; michael@0: } michael@0: michael@0: // Don't fetch if we were told to back off. michael@0: if (getEarliestNextFetch() > now) { michael@0: return false; michael@0: } michael@0: michael@0: // Don't do anything if we haven't waited long enough. michael@0: final long lastFetch = getLastFetch(); michael@0: michael@0: // Just in case the alarm manager schedules us more frequently, or something michael@0: // goes awry with relaunches. michael@0: if ((now - lastFetch) < AnnouncementsConstants.MINIMUM_FETCH_INTERVAL_MSEC) { michael@0: Logger.debug(LOG_TAG, "Returning: minimum fetch interval of " + AnnouncementsConstants.MINIMUM_FETCH_INTERVAL_MSEC + "ms not met."); michael@0: return false; michael@0: } michael@0: michael@0: return true; michael@0: } michael@0: michael@0: /** michael@0: * Display the first valid announcement in the list. michael@0: */ michael@0: protected void processAnnouncements(final List announcements) { michael@0: if (announcements == null) { michael@0: Logger.warn(LOG_TAG, "No announcements to present."); michael@0: return; michael@0: } michael@0: michael@0: boolean presented = false; michael@0: for (Announcement an : announcements) { michael@0: // Do this so we at least log, rather than just returning. michael@0: if (presented) { michael@0: Logger.warn(LOG_TAG, "Skipping announcement \"" + an.getTitle() + "\": one already shown."); michael@0: continue; michael@0: } michael@0: if (Announcement.isValidAnnouncement(an)) { michael@0: presented = true; michael@0: AnnouncementPresenter.displayAnnouncement(this, an); michael@0: } michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * If it's time to do a fetch -- we've waited long enough, michael@0: * we're allowed to use background data, etc. -- then issue michael@0: * a fetch. The subsequent background check is handled implicitly michael@0: * by the AlarmManager. michael@0: */ michael@0: @Override michael@0: public void onHandleIntent(Intent intent) { michael@0: Logger.setThreadLogTag(AnnouncementsConstants.GLOBAL_LOG_TAG); michael@0: michael@0: // Intent can be null. Bug 1025937. michael@0: if (intent == null) { michael@0: Logger.debug(LOG_TAG, "Short-circuiting on null intent."); michael@0: return; michael@0: } michael@0: michael@0: Logger.debug(LOG_TAG, "Running AnnouncementsService."); michael@0: michael@0: if (AnnouncementsConstants.DISABLED) { michael@0: Logger.debug(LOG_TAG, "Announcements disabled. Returning from AnnouncementsService."); michael@0: return; michael@0: } michael@0: michael@0: if (!shouldFetchAnnouncements()) { michael@0: Logger.debug(LOG_TAG, "Not fetching."); michael@0: return; michael@0: } michael@0: michael@0: // Ensure that our locale is up to date, so that the fetcher's michael@0: // Accept-Language header is, too. michael@0: BrowserLocaleManager.getInstance().getAndApplyPersistedLocale(getApplicationContext()); michael@0: michael@0: // Otherwise, grab our announcements URL and process the contents. michael@0: AnnouncementsFetcher.fetchAndProcessAnnouncements(getLastLaunch(), this); michael@0: } michael@0: michael@0: @Override michael@0: public IBinder onBind(Intent intent) { michael@0: return null; michael@0: } michael@0: michael@0: protected long getLastLaunch() { michael@0: return getSharedPreferences().getLong(AnnouncementsConstants.PREF_LAST_LAUNCH, 0); michael@0: } michael@0: michael@0: protected SharedPreferences getSharedPreferences() { michael@0: return this.getSharedPreferences(AnnouncementsConstants.PREFS_BRANCH, GlobalConstants.SHARED_PREFERENCES_MODE); michael@0: } michael@0: michael@0: @Override michael@0: protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { michael@0: super.dump(fd, writer, args); michael@0: michael@0: final long lastFetch = getLastFetch(); michael@0: final long lastLaunch = getLastLaunch(); michael@0: writer.write("AnnouncementsService: last fetch " + lastFetch + michael@0: ", last Firefox activity: " + lastLaunch + "\n"); michael@0: } michael@0: michael@0: protected void setEarliestNextFetch(final long earliestInMsec) { michael@0: this.getSharedPreferences().edit().putLong(AnnouncementsConstants.PREF_EARLIEST_NEXT_ANNOUNCE_FETCH, earliestInMsec).commit(); michael@0: } michael@0: michael@0: protected long getEarliestNextFetch() { michael@0: return this.getSharedPreferences().getLong(AnnouncementsConstants.PREF_EARLIEST_NEXT_ANNOUNCE_FETCH, 0L); michael@0: } michael@0: michael@0: protected void setLastFetch(final long fetch) { michael@0: this.getSharedPreferences().edit().putLong(AnnouncementsConstants.PREF_LAST_FETCH_LOCAL_TIME, fetch).commit(); michael@0: } michael@0: michael@0: @Override michael@0: public long getLastFetch() { michael@0: return this.getSharedPreferences().getLong(AnnouncementsConstants.PREF_LAST_FETCH_LOCAL_TIME, 0L); michael@0: } michael@0: michael@0: protected String setLastDate(final String fetch) { michael@0: if (fetch == null) { michael@0: this.getSharedPreferences().edit().remove(AnnouncementsConstants.PREF_LAST_FETCH_SERVER_DATE).commit(); michael@0: return null; michael@0: } michael@0: this.getSharedPreferences().edit().putString(AnnouncementsConstants.PREF_LAST_FETCH_SERVER_DATE, fetch).commit(); michael@0: return fetch; michael@0: } michael@0: michael@0: @Override michael@0: public String getLastDate() { michael@0: return this.getSharedPreferences().getString(AnnouncementsConstants.PREF_LAST_FETCH_SERVER_DATE, null); michael@0: } michael@0: michael@0: /** michael@0: * Use this to write the persisted server URL, overriding michael@0: * the default value. michael@0: * @param url a URI identifying the full request path, e.g., michael@0: * "http://foo.com:1234/announce/" michael@0: */ michael@0: public void setAnnouncementsServerBaseURL(final URI url) { michael@0: if (url == null) { michael@0: throw new IllegalArgumentException("url cannot be null."); michael@0: } michael@0: final String scheme = url.getScheme(); michael@0: if (scheme == null) { michael@0: throw new IllegalArgumentException("url must have a scheme."); michael@0: } michael@0: if (!scheme.equalsIgnoreCase("http") && !scheme.equalsIgnoreCase("https")) { michael@0: throw new IllegalArgumentException("url must be http or https."); michael@0: } michael@0: SharedPreferences p = this.getSharedPreferences(); michael@0: p.edit().putString(AnnouncementsConstants.PREF_ANNOUNCE_SERVER_BASE_URL, url.toASCIIString()).commit(); michael@0: } michael@0: michael@0: /** michael@0: * Return the service URL, including protocol version and application identifier. E.g., michael@0: * michael@0: * "https://campaigns.services.mozilla.com/announce/1/android/" michael@0: */ michael@0: @Override michael@0: public String getServiceURL() { michael@0: SharedPreferences p = this.getSharedPreferences(); michael@0: String base = p.getString(AnnouncementsConstants.PREF_ANNOUNCE_SERVER_BASE_URL, AnnouncementsConstants.DEFAULT_ANNOUNCE_SERVER_BASE_URL); michael@0: return base + AnnouncementsConstants.ANNOUNCE_PATH_SUFFIX; michael@0: } michael@0: michael@0: @Override michael@0: public Locale getLocale() { michael@0: return Locale.getDefault(); michael@0: } michael@0: michael@0: @Override michael@0: public String getUserAgent() { michael@0: return AnnouncementsConstants.USER_AGENT; michael@0: } michael@0: michael@0: protected void persistTimes(long fetched, String date) { michael@0: setLastFetch(fetched); michael@0: if (date != null) { michael@0: setLastDate(date); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void onNoNewAnnouncements(long fetched, String date) { michael@0: Logger.info(LOG_TAG, "No new announcements to display."); michael@0: persistTimes(fetched, date); michael@0: } michael@0: michael@0: @Override michael@0: public void onNewAnnouncements(List announcements, long fetched, String date) { michael@0: Logger.info(LOG_TAG, "Processing announcements: " + announcements.size()); michael@0: persistTimes(fetched, date); michael@0: processAnnouncements(announcements); michael@0: } michael@0: michael@0: @Override michael@0: public void onRemoteFailure(int status) { michael@0: // Bump our fetch timestamp. michael@0: Logger.warn(LOG_TAG, "Got remote fetch status " + status + "; bumping fetch time."); michael@0: setLastFetch(System.currentTimeMillis()); michael@0: } michael@0: michael@0: @Override michael@0: public void onRemoteError(Exception e) { michael@0: // Bump our fetch timestamp. michael@0: Logger.warn(LOG_TAG, "Error processing response.", e); michael@0: setLastFetch(System.currentTimeMillis()); michael@0: } michael@0: michael@0: @Override michael@0: public void onLocalError(Exception e) { michael@0: Logger.error(LOG_TAG, "Got exception in fetch.", e); michael@0: // Do nothing yet, so we'll retry. michael@0: } michael@0: michael@0: @Override michael@0: public void onBackoff(int retryAfterInSeconds) { michael@0: Logger.info(LOG_TAG, "Got retry after: " + retryAfterInSeconds); michael@0: final long delayInMsec = Math.max(retryAfterInSeconds * 1000, AnnouncementsConstants.DEFAULT_BACKOFF_MSEC); michael@0: final long fuzzedBackoffInMsec = delayInMsec + Math.round(((double) delayInMsec * 0.25d * Math.random())); michael@0: Logger.debug(LOG_TAG, "Fuzzed backoff: " + fuzzedBackoffInMsec + "ms."); michael@0: setEarliestNextFetch(fuzzedBackoffInMsec + System.currentTimeMillis()); michael@0: } michael@0: }