1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/background/announcements/AnnouncementsService.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,292 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +package org.mozilla.gecko.background.announcements; 1.9 + 1.10 +import java.io.FileDescriptor; 1.11 +import java.io.PrintWriter; 1.12 +import java.net.URI; 1.13 +import java.util.List; 1.14 +import java.util.Locale; 1.15 + 1.16 +import org.mozilla.gecko.BrowserLocaleManager; 1.17 +import org.mozilla.gecko.background.BackgroundService; 1.18 +import org.mozilla.gecko.background.common.GlobalConstants; 1.19 +import org.mozilla.gecko.background.common.log.Logger; 1.20 + 1.21 +import android.content.Intent; 1.22 +import android.content.SharedPreferences; 1.23 +import android.os.IBinder; 1.24 + 1.25 +/** 1.26 + * A Service to periodically check for new published announcements, 1.27 + * presenting them to the user if local conditions permit. 1.28 + * 1.29 + * We extend IntentService, rather than just Service, because this gives us 1.30 + * a worker thread to avoid main-thread networking. 1.31 + * 1.32 + * Yes, even though we're in an alarm-triggered service, it still counts 1.33 + * as main-thread. 1.34 + * 1.35 + * The operation of this service is as follows: 1.36 + * 1.37 + * 0. Decide if a request should be made. 1.38 + * 1. Compute the arguments to the request. This includes enough 1.39 + * pertinent details to allow the server to pre-filter a message 1.40 + * set, recording enough tracking details to compute statistics. 1.41 + * 2. Issue the request. If this succeeds with a 200 or 204, great; 1.42 + * track that timestamp for the next run through Step 0. 1.43 + * 3. Process any received messages. 1.44 + * 1.45 + * Message processing is as follows: 1.46 + * 1.47 + * 0. Decide if message display should occur. This might involve 1.48 + * user preference or other kinds of environmental factors. 1.49 + * 1. Use the AnnouncementPresenter to open the announcement. 1.50 + * 1.51 + * Future: 1.52 + * * Persisting of multiple announcements. 1.53 + * * Prioritization. 1.54 + */ 1.55 +public class AnnouncementsService extends BackgroundService implements AnnouncementsFetchDelegate { 1.56 + private static final String WORKER_THREAD_NAME = "AnnouncementsServiceWorker"; 1.57 + private static final String LOG_TAG = "AnnounceService"; 1.58 + 1.59 + public AnnouncementsService() { 1.60 + super(WORKER_THREAD_NAME); 1.61 + Logger.setThreadLogTag(AnnouncementsConstants.GLOBAL_LOG_TAG); 1.62 + Logger.debug(LOG_TAG, "Creating AnnouncementsService."); 1.63 + } 1.64 + 1.65 + public boolean shouldFetchAnnouncements() { 1.66 + final long now = System.currentTimeMillis(); 1.67 + 1.68 + if (!backgroundDataIsEnabled()) { 1.69 + Logger.debug(LOG_TAG, "Background data not possible. Skipping."); 1.70 + return false; 1.71 + } 1.72 + 1.73 + // Don't fetch if we were told to back off. 1.74 + if (getEarliestNextFetch() > now) { 1.75 + return false; 1.76 + } 1.77 + 1.78 + // Don't do anything if we haven't waited long enough. 1.79 + final long lastFetch = getLastFetch(); 1.80 + 1.81 + // Just in case the alarm manager schedules us more frequently, or something 1.82 + // goes awry with relaunches. 1.83 + if ((now - lastFetch) < AnnouncementsConstants.MINIMUM_FETCH_INTERVAL_MSEC) { 1.84 + Logger.debug(LOG_TAG, "Returning: minimum fetch interval of " + AnnouncementsConstants.MINIMUM_FETCH_INTERVAL_MSEC + "ms not met."); 1.85 + return false; 1.86 + } 1.87 + 1.88 + return true; 1.89 + } 1.90 + 1.91 + /** 1.92 + * Display the first valid announcement in the list. 1.93 + */ 1.94 + protected void processAnnouncements(final List<Announcement> announcements) { 1.95 + if (announcements == null) { 1.96 + Logger.warn(LOG_TAG, "No announcements to present."); 1.97 + return; 1.98 + } 1.99 + 1.100 + boolean presented = false; 1.101 + for (Announcement an : announcements) { 1.102 + // Do this so we at least log, rather than just returning. 1.103 + if (presented) { 1.104 + Logger.warn(LOG_TAG, "Skipping announcement \"" + an.getTitle() + "\": one already shown."); 1.105 + continue; 1.106 + } 1.107 + if (Announcement.isValidAnnouncement(an)) { 1.108 + presented = true; 1.109 + AnnouncementPresenter.displayAnnouncement(this, an); 1.110 + } 1.111 + } 1.112 + } 1.113 + 1.114 + /** 1.115 + * If it's time to do a fetch -- we've waited long enough, 1.116 + * we're allowed to use background data, etc. -- then issue 1.117 + * a fetch. The subsequent background check is handled implicitly 1.118 + * by the AlarmManager. 1.119 + */ 1.120 + @Override 1.121 + public void onHandleIntent(Intent intent) { 1.122 + Logger.setThreadLogTag(AnnouncementsConstants.GLOBAL_LOG_TAG); 1.123 + 1.124 + // Intent can be null. Bug 1025937. 1.125 + if (intent == null) { 1.126 + Logger.debug(LOG_TAG, "Short-circuiting on null intent."); 1.127 + return; 1.128 + } 1.129 + 1.130 + Logger.debug(LOG_TAG, "Running AnnouncementsService."); 1.131 + 1.132 + if (AnnouncementsConstants.DISABLED) { 1.133 + Logger.debug(LOG_TAG, "Announcements disabled. Returning from AnnouncementsService."); 1.134 + return; 1.135 + } 1.136 + 1.137 + if (!shouldFetchAnnouncements()) { 1.138 + Logger.debug(LOG_TAG, "Not fetching."); 1.139 + return; 1.140 + } 1.141 + 1.142 + // Ensure that our locale is up to date, so that the fetcher's 1.143 + // Accept-Language header is, too. 1.144 + BrowserLocaleManager.getInstance().getAndApplyPersistedLocale(getApplicationContext()); 1.145 + 1.146 + // Otherwise, grab our announcements URL and process the contents. 1.147 + AnnouncementsFetcher.fetchAndProcessAnnouncements(getLastLaunch(), this); 1.148 + } 1.149 + 1.150 + @Override 1.151 + public IBinder onBind(Intent intent) { 1.152 + return null; 1.153 + } 1.154 + 1.155 + protected long getLastLaunch() { 1.156 + return getSharedPreferences().getLong(AnnouncementsConstants.PREF_LAST_LAUNCH, 0); 1.157 + } 1.158 + 1.159 + protected SharedPreferences getSharedPreferences() { 1.160 + return this.getSharedPreferences(AnnouncementsConstants.PREFS_BRANCH, GlobalConstants.SHARED_PREFERENCES_MODE); 1.161 + } 1.162 + 1.163 + @Override 1.164 + protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 1.165 + super.dump(fd, writer, args); 1.166 + 1.167 + final long lastFetch = getLastFetch(); 1.168 + final long lastLaunch = getLastLaunch(); 1.169 + writer.write("AnnouncementsService: last fetch " + lastFetch + 1.170 + ", last Firefox activity: " + lastLaunch + "\n"); 1.171 + } 1.172 + 1.173 + protected void setEarliestNextFetch(final long earliestInMsec) { 1.174 + this.getSharedPreferences().edit().putLong(AnnouncementsConstants.PREF_EARLIEST_NEXT_ANNOUNCE_FETCH, earliestInMsec).commit(); 1.175 + } 1.176 + 1.177 + protected long getEarliestNextFetch() { 1.178 + return this.getSharedPreferences().getLong(AnnouncementsConstants.PREF_EARLIEST_NEXT_ANNOUNCE_FETCH, 0L); 1.179 + } 1.180 + 1.181 + protected void setLastFetch(final long fetch) { 1.182 + this.getSharedPreferences().edit().putLong(AnnouncementsConstants.PREF_LAST_FETCH_LOCAL_TIME, fetch).commit(); 1.183 + } 1.184 + 1.185 + @Override 1.186 + public long getLastFetch() { 1.187 + return this.getSharedPreferences().getLong(AnnouncementsConstants.PREF_LAST_FETCH_LOCAL_TIME, 0L); 1.188 + } 1.189 + 1.190 + protected String setLastDate(final String fetch) { 1.191 + if (fetch == null) { 1.192 + this.getSharedPreferences().edit().remove(AnnouncementsConstants.PREF_LAST_FETCH_SERVER_DATE).commit(); 1.193 + return null; 1.194 + } 1.195 + this.getSharedPreferences().edit().putString(AnnouncementsConstants.PREF_LAST_FETCH_SERVER_DATE, fetch).commit(); 1.196 + return fetch; 1.197 + } 1.198 + 1.199 + @Override 1.200 + public String getLastDate() { 1.201 + return this.getSharedPreferences().getString(AnnouncementsConstants.PREF_LAST_FETCH_SERVER_DATE, null); 1.202 + } 1.203 + 1.204 + /** 1.205 + * Use this to write the persisted server URL, overriding 1.206 + * the default value. 1.207 + * @param url a URI identifying the full request path, e.g., 1.208 + * "http://foo.com:1234/announce/" 1.209 + */ 1.210 + public void setAnnouncementsServerBaseURL(final URI url) { 1.211 + if (url == null) { 1.212 + throw new IllegalArgumentException("url cannot be null."); 1.213 + } 1.214 + final String scheme = url.getScheme(); 1.215 + if (scheme == null) { 1.216 + throw new IllegalArgumentException("url must have a scheme."); 1.217 + } 1.218 + if (!scheme.equalsIgnoreCase("http") && !scheme.equalsIgnoreCase("https")) { 1.219 + throw new IllegalArgumentException("url must be http or https."); 1.220 + } 1.221 + SharedPreferences p = this.getSharedPreferences(); 1.222 + p.edit().putString(AnnouncementsConstants.PREF_ANNOUNCE_SERVER_BASE_URL, url.toASCIIString()).commit(); 1.223 + } 1.224 + 1.225 + /** 1.226 + * Return the service URL, including protocol version and application identifier. E.g., 1.227 + * 1.228 + * "https://campaigns.services.mozilla.com/announce/1/android/" 1.229 + */ 1.230 + @Override 1.231 + public String getServiceURL() { 1.232 + SharedPreferences p = this.getSharedPreferences(); 1.233 + String base = p.getString(AnnouncementsConstants.PREF_ANNOUNCE_SERVER_BASE_URL, AnnouncementsConstants.DEFAULT_ANNOUNCE_SERVER_BASE_URL); 1.234 + return base + AnnouncementsConstants.ANNOUNCE_PATH_SUFFIX; 1.235 + } 1.236 + 1.237 + @Override 1.238 + public Locale getLocale() { 1.239 + return Locale.getDefault(); 1.240 + } 1.241 + 1.242 + @Override 1.243 + public String getUserAgent() { 1.244 + return AnnouncementsConstants.USER_AGENT; 1.245 + } 1.246 + 1.247 + protected void persistTimes(long fetched, String date) { 1.248 + setLastFetch(fetched); 1.249 + if (date != null) { 1.250 + setLastDate(date); 1.251 + } 1.252 + } 1.253 + 1.254 + @Override 1.255 + public void onNoNewAnnouncements(long fetched, String date) { 1.256 + Logger.info(LOG_TAG, "No new announcements to display."); 1.257 + persistTimes(fetched, date); 1.258 + } 1.259 + 1.260 + @Override 1.261 + public void onNewAnnouncements(List<Announcement> announcements, long fetched, String date) { 1.262 + Logger.info(LOG_TAG, "Processing announcements: " + announcements.size()); 1.263 + persistTimes(fetched, date); 1.264 + processAnnouncements(announcements); 1.265 + } 1.266 + 1.267 + @Override 1.268 + public void onRemoteFailure(int status) { 1.269 + // Bump our fetch timestamp. 1.270 + Logger.warn(LOG_TAG, "Got remote fetch status " + status + "; bumping fetch time."); 1.271 + setLastFetch(System.currentTimeMillis()); 1.272 + } 1.273 + 1.274 + @Override 1.275 + public void onRemoteError(Exception e) { 1.276 + // Bump our fetch timestamp. 1.277 + Logger.warn(LOG_TAG, "Error processing response.", e); 1.278 + setLastFetch(System.currentTimeMillis()); 1.279 + } 1.280 + 1.281 + @Override 1.282 + public void onLocalError(Exception e) { 1.283 + Logger.error(LOG_TAG, "Got exception in fetch.", e); 1.284 + // Do nothing yet, so we'll retry. 1.285 + } 1.286 + 1.287 + @Override 1.288 + public void onBackoff(int retryAfterInSeconds) { 1.289 + Logger.info(LOG_TAG, "Got retry after: " + retryAfterInSeconds); 1.290 + final long delayInMsec = Math.max(retryAfterInSeconds * 1000, AnnouncementsConstants.DEFAULT_BACKOFF_MSEC); 1.291 + final long fuzzedBackoffInMsec = delayInMsec + Math.round(((double) delayInMsec * 0.25d * Math.random())); 1.292 + Logger.debug(LOG_TAG, "Fuzzed backoff: " + fuzzedBackoffInMsec + "ms."); 1.293 + setEarliestNextFetch(fuzzedBackoffInMsec + System.currentTimeMillis()); 1.294 + } 1.295 +}