1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/background/healthreport/HealthReportBroadcastService.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,260 @@ 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.healthreport; 1.9 + 1.10 +import org.mozilla.gecko.background.BackgroundService; 1.11 +import org.mozilla.gecko.background.common.GlobalConstants; 1.12 +import org.mozilla.gecko.background.common.log.Logger; 1.13 +import org.mozilla.gecko.background.healthreport.prune.HealthReportPruneService; 1.14 +import org.mozilla.gecko.background.healthreport.upload.HealthReportUploadService; 1.15 +import org.mozilla.gecko.background.healthreport.upload.ObsoleteDocumentTracker; 1.16 + 1.17 +import android.app.AlarmManager; 1.18 +import android.app.PendingIntent; 1.19 +import android.content.Context; 1.20 +import android.content.Intent; 1.21 +import android.content.SharedPreferences; 1.22 +import android.content.SharedPreferences.Editor; 1.23 + 1.24 +/** 1.25 + * A service which listens to broadcast intents from the system and from the 1.26 + * browser, registering or unregistering the background health report services with the 1.27 + * {@link AlarmManager}. 1.28 + */ 1.29 +public class HealthReportBroadcastService extends BackgroundService { 1.30 + public static final String LOG_TAG = HealthReportBroadcastService.class.getSimpleName(); 1.31 + public static final String WORKER_THREAD_NAME = LOG_TAG + "Worker"; 1.32 + 1.33 + public HealthReportBroadcastService() { 1.34 + super(WORKER_THREAD_NAME); 1.35 + } 1.36 + 1.37 + protected SharedPreferences getSharedPreferences() { 1.38 + return this.getSharedPreferences(HealthReportConstants.PREFS_BRANCH, GlobalConstants.SHARED_PREFERENCES_MODE); 1.39 + } 1.40 + 1.41 + public long getSubmissionPollInterval() { 1.42 + return getSharedPreferences().getLong(HealthReportConstants.PREF_SUBMISSION_INTENT_INTERVAL_MSEC, HealthReportConstants.DEFAULT_SUBMISSION_INTENT_INTERVAL_MSEC); 1.43 + } 1.44 + 1.45 + public void setSubmissionPollInterval(final long interval) { 1.46 + getSharedPreferences().edit().putLong(HealthReportConstants.PREF_SUBMISSION_INTENT_INTERVAL_MSEC, interval).commit(); 1.47 + } 1.48 + 1.49 + public long getPrunePollInterval() { 1.50 + return getSharedPreferences().getLong(HealthReportConstants.PREF_PRUNE_INTENT_INTERVAL_MSEC, 1.51 + HealthReportConstants.DEFAULT_PRUNE_INTENT_INTERVAL_MSEC); 1.52 + } 1.53 + 1.54 + public void setPrunePollInterval(final long interval) { 1.55 + getSharedPreferences().edit().putLong(HealthReportConstants.PREF_PRUNE_INTENT_INTERVAL_MSEC, 1.56 + interval).commit(); 1.57 + } 1.58 + 1.59 + /** 1.60 + * Set or cancel an alarm to submit data for a profile. 1.61 + * 1.62 + * @param context 1.63 + * Android context. 1.64 + * @param profileName 1.65 + * to submit data for. 1.66 + * @param profilePath 1.67 + * to submit data for. 1.68 + * @param enabled 1.69 + * whether the user has enabled submitting health report data for 1.70 + * this profile. 1.71 + * @param serviceEnabled 1.72 + * whether submitting should be scheduled. If the user turns off 1.73 + * submitting, <code>enabled</code> could be false but we could need 1.74 + * to delete so <code>serviceEnabled</code> could be true. 1.75 + */ 1.76 + protected void toggleSubmissionAlarm(final Context context, String profileName, String profilePath, 1.77 + boolean enabled, boolean serviceEnabled) { 1.78 + final Class<?> serviceClass = HealthReportUploadService.class; 1.79 + Logger.info(LOG_TAG, (serviceEnabled ? "R" : "Unr") + "egistering " + 1.80 + serviceClass.getSimpleName() + "."); 1.81 + 1.82 + // PendingIntents are compared without reference to their extras. Therefore 1.83 + // even though we pass the profile details to the action, different 1.84 + // profiles will share the *same* pending intent. In a multi-profile future, 1.85 + // this will need to be addressed. See Bug 882182. 1.86 + final Intent service = new Intent(context, serviceClass); 1.87 + service.setAction("upload"); // PendingIntents "lose" their extras if no action is set. 1.88 + service.putExtra("uploadEnabled", enabled); 1.89 + service.putExtra("profileName", profileName); 1.90 + service.putExtra("profilePath", profilePath); 1.91 + final PendingIntent pending = PendingIntent.getService(context, 0, service, PendingIntent.FLAG_CANCEL_CURRENT); 1.92 + 1.93 + if (!serviceEnabled) { 1.94 + cancelAlarm(pending); 1.95 + return; 1.96 + } 1.97 + 1.98 + final long pollInterval = getSubmissionPollInterval(); 1.99 + scheduleAlarm(pollInterval, pending); 1.100 + } 1.101 + 1.102 + @Override 1.103 + protected void onHandleIntent(Intent intent) { 1.104 + Logger.setThreadLogTag(HealthReportConstants.GLOBAL_LOG_TAG); 1.105 + 1.106 + // Intent can be null. Bug 1025937. 1.107 + if (intent == null) { 1.108 + Logger.debug(LOG_TAG, "Short-circuiting on null intent."); 1.109 + return; 1.110 + } 1.111 + 1.112 + // The same intent can be handled by multiple methods so do not short-circuit evaluate. 1.113 + boolean handled = attemptHandleIntentForUpload(intent); 1.114 + handled = attemptHandleIntentForPrune(intent) ? true : handled; 1.115 + 1.116 + if (!handled) { 1.117 + Logger.warn(LOG_TAG, "Unhandled intent with action " + intent.getAction() + "."); 1.118 + } 1.119 + } 1.120 + 1.121 + /** 1.122 + * Attempts to handle the given intent for FHR document upload. If it cannot, false is returned. 1.123 + * 1.124 + * @param intent must be non-null. 1.125 + */ 1.126 + private boolean attemptHandleIntentForUpload(final Intent intent) { 1.127 + if (HealthReportConstants.UPLOAD_FEATURE_DISABLED) { 1.128 + Logger.debug(LOG_TAG, "Health report upload feature is compile-time disabled; not handling intent."); 1.129 + return false; 1.130 + } 1.131 + 1.132 + final String action = intent.getAction(); 1.133 + Logger.debug(LOG_TAG, "Health report upload feature is compile-time enabled; attempting to " + 1.134 + "handle intent with action " + action + "."); 1.135 + 1.136 + if (HealthReportConstants.ACTION_HEALTHREPORT_UPLOAD_PREF.equals(action)) { 1.137 + handleUploadPrefIntent(intent); 1.138 + return true; 1.139 + } 1.140 + 1.141 + if (Intent.ACTION_BOOT_COMPLETED.equals(action) || 1.142 + Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action)) { 1.143 + BackgroundService.reflectContextToFennec(this, 1.144 + GlobalConstants.GECKO_PREFERENCES_CLASS, 1.145 + GlobalConstants.GECKO_BROADCAST_HEALTHREPORT_UPLOAD_PREF_METHOD); 1.146 + return true; 1.147 + } 1.148 + 1.149 + return false; 1.150 + } 1.151 + 1.152 + /** 1.153 + * Handle the intent sent by the browser when it wishes to notify us 1.154 + * of the value of the user preference. Look at the value and toggle the 1.155 + * alarm service accordingly. 1.156 + * 1.157 + * @param intent must be non-null. 1.158 + */ 1.159 + private void handleUploadPrefIntent(Intent intent) { 1.160 + if (!intent.hasExtra("enabled")) { 1.161 + Logger.warn(LOG_TAG, "Got " + HealthReportConstants.ACTION_HEALTHREPORT_UPLOAD_PREF + " intent without enabled. Ignoring."); 1.162 + return; 1.163 + } 1.164 + 1.165 + final boolean enabled = intent.getBooleanExtra("enabled", true); 1.166 + Logger.debug(LOG_TAG, intent.getStringExtra("branch") + "/" + 1.167 + intent.getStringExtra("pref") + " = " + 1.168 + (intent.hasExtra("enabled") ? enabled : "")); 1.169 + 1.170 + String profileName = intent.getStringExtra("profileName"); 1.171 + String profilePath = intent.getStringExtra("profilePath"); 1.172 + 1.173 + if (profileName == null || profilePath == null) { 1.174 + Logger.warn(LOG_TAG, "Got " + HealthReportConstants.ACTION_HEALTHREPORT_UPLOAD_PREF + " intent without profilePath or profileName. Ignoring."); 1.175 + return; 1.176 + } 1.177 + 1.178 + Logger.pii(LOG_TAG, "Updating health report upload alarm for profile " + profileName + " at " + 1.179 + profilePath + "."); 1.180 + 1.181 + final SharedPreferences sharedPrefs = getSharedPreferences(); 1.182 + final ObsoleteDocumentTracker tracker = new ObsoleteDocumentTracker(sharedPrefs); 1.183 + final boolean hasObsoleteIds = tracker.hasObsoleteIds(); 1.184 + 1.185 + if (!enabled) { 1.186 + final Editor editor = sharedPrefs.edit(); 1.187 + editor.remove(HealthReportConstants.PREF_LAST_UPLOAD_DOCUMENT_ID); 1.188 + 1.189 + if (hasObsoleteIds) { 1.190 + Logger.debug(LOG_TAG, "Health report upload disabled; scheduling deletion of " + tracker.numberOfObsoleteIds() + " documents."); 1.191 + tracker.limitObsoleteIds(); 1.192 + } else { 1.193 + // Primarily intended for debugging and testing. 1.194 + Logger.debug(LOG_TAG, "Health report upload disabled and no deletes to schedule: clearing prefs."); 1.195 + editor.remove(HealthReportConstants.PREF_FIRST_RUN); 1.196 + editor.remove(HealthReportConstants.PREF_NEXT_SUBMISSION); 1.197 + } 1.198 + 1.199 + editor.commit(); 1.200 + } 1.201 + 1.202 + // The user can toggle us off or on, or we can have obsolete documents to 1.203 + // remove. 1.204 + final boolean serviceEnabled = hasObsoleteIds || enabled; 1.205 + toggleSubmissionAlarm(this, profileName, profilePath, enabled, serviceEnabled); 1.206 + } 1.207 + 1.208 + /** 1.209 + * Attempts to handle the given intent for FHR data pruning. If it cannot, false is returned. 1.210 + * 1.211 + * @param intent must be non-null. 1.212 + */ 1.213 + private boolean attemptHandleIntentForPrune(final Intent intent) { 1.214 + final String action = intent.getAction(); 1.215 + Logger.debug(LOG_TAG, "Prune: Attempting to handle intent with action, " + action + "."); 1.216 + 1.217 + if (HealthReportConstants.ACTION_HEALTHREPORT_PRUNE.equals(action)) { 1.218 + handlePruneIntent(intent); 1.219 + return true; 1.220 + } 1.221 + 1.222 + if (Intent.ACTION_BOOT_COMPLETED.equals(action) || 1.223 + Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action)) { 1.224 + BackgroundService.reflectContextToFennec(this, 1.225 + GlobalConstants.GECKO_PREFERENCES_CLASS, 1.226 + GlobalConstants.GECKO_BROADCAST_HEALTHREPORT_PRUNE_METHOD); 1.227 + return true; 1.228 + } 1.229 + 1.230 + return false; 1.231 + } 1.232 + 1.233 + /** 1.234 + * @param intent must be non-null. 1.235 + */ 1.236 + private void handlePruneIntent(final Intent intent) { 1.237 + final String profileName = intent.getStringExtra("profileName"); 1.238 + final String profilePath = intent.getStringExtra("profilePath"); 1.239 + 1.240 + if (profileName == null || profilePath == null) { 1.241 + Logger.warn(LOG_TAG, "Got " + HealthReportConstants.ACTION_HEALTHREPORT_PRUNE + " intent " + 1.242 + "without profilePath or profileName. Ignoring."); 1.243 + return; 1.244 + } 1.245 + 1.246 + final Class<?> serviceClass = HealthReportPruneService.class; 1.247 + final Intent service = new Intent(this, serviceClass); 1.248 + service.setAction("prune"); // Intents without actions have their extras removed. 1.249 + service.putExtra("profileName", profileName); 1.250 + service.putExtra("profilePath", profilePath); 1.251 + final PendingIntent pending = PendingIntent.getService(this, 0, service, 1.252 + PendingIntent.FLAG_CANCEL_CURRENT); 1.253 + 1.254 + // Set a regular alarm to start PruneService. Since the various actions that PruneService can 1.255 + // take occur on irregular intervals, we can be more efficient by only starting the Service 1.256 + // when one of these time limits runs out. However, subsequent Service invocations must then 1.257 + // be registered by the PruneService itself, which would fail if the PruneService crashes. 1.258 + // Thus, we set this regular (and slightly inefficient) alarm. 1.259 + Logger.info(LOG_TAG, "Registering " + serviceClass.getSimpleName() + "."); 1.260 + final long pollInterval = getPrunePollInterval(); 1.261 + scheduleAlarm(pollInterval, pending); 1.262 + } 1.263 +}