|
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/. */ |
|
4 |
|
5 package org.mozilla.gecko.background.healthreport; |
|
6 |
|
7 import org.mozilla.gecko.background.BackgroundService; |
|
8 import org.mozilla.gecko.background.common.GlobalConstants; |
|
9 import org.mozilla.gecko.background.common.log.Logger; |
|
10 import org.mozilla.gecko.background.healthreport.prune.HealthReportPruneService; |
|
11 import org.mozilla.gecko.background.healthreport.upload.HealthReportUploadService; |
|
12 import org.mozilla.gecko.background.healthreport.upload.ObsoleteDocumentTracker; |
|
13 |
|
14 import android.app.AlarmManager; |
|
15 import android.app.PendingIntent; |
|
16 import android.content.Context; |
|
17 import android.content.Intent; |
|
18 import android.content.SharedPreferences; |
|
19 import android.content.SharedPreferences.Editor; |
|
20 |
|
21 /** |
|
22 * A service which listens to broadcast intents from the system and from the |
|
23 * browser, registering or unregistering the background health report services with the |
|
24 * {@link AlarmManager}. |
|
25 */ |
|
26 public class HealthReportBroadcastService extends BackgroundService { |
|
27 public static final String LOG_TAG = HealthReportBroadcastService.class.getSimpleName(); |
|
28 public static final String WORKER_THREAD_NAME = LOG_TAG + "Worker"; |
|
29 |
|
30 public HealthReportBroadcastService() { |
|
31 super(WORKER_THREAD_NAME); |
|
32 } |
|
33 |
|
34 protected SharedPreferences getSharedPreferences() { |
|
35 return this.getSharedPreferences(HealthReportConstants.PREFS_BRANCH, GlobalConstants.SHARED_PREFERENCES_MODE); |
|
36 } |
|
37 |
|
38 public long getSubmissionPollInterval() { |
|
39 return getSharedPreferences().getLong(HealthReportConstants.PREF_SUBMISSION_INTENT_INTERVAL_MSEC, HealthReportConstants.DEFAULT_SUBMISSION_INTENT_INTERVAL_MSEC); |
|
40 } |
|
41 |
|
42 public void setSubmissionPollInterval(final long interval) { |
|
43 getSharedPreferences().edit().putLong(HealthReportConstants.PREF_SUBMISSION_INTENT_INTERVAL_MSEC, interval).commit(); |
|
44 } |
|
45 |
|
46 public long getPrunePollInterval() { |
|
47 return getSharedPreferences().getLong(HealthReportConstants.PREF_PRUNE_INTENT_INTERVAL_MSEC, |
|
48 HealthReportConstants.DEFAULT_PRUNE_INTENT_INTERVAL_MSEC); |
|
49 } |
|
50 |
|
51 public void setPrunePollInterval(final long interval) { |
|
52 getSharedPreferences().edit().putLong(HealthReportConstants.PREF_PRUNE_INTENT_INTERVAL_MSEC, |
|
53 interval).commit(); |
|
54 } |
|
55 |
|
56 /** |
|
57 * Set or cancel an alarm to submit data for a profile. |
|
58 * |
|
59 * @param context |
|
60 * Android context. |
|
61 * @param profileName |
|
62 * to submit data for. |
|
63 * @param profilePath |
|
64 * to submit data for. |
|
65 * @param enabled |
|
66 * whether the user has enabled submitting health report data for |
|
67 * this profile. |
|
68 * @param serviceEnabled |
|
69 * whether submitting should be scheduled. If the user turns off |
|
70 * submitting, <code>enabled</code> could be false but we could need |
|
71 * to delete so <code>serviceEnabled</code> could be true. |
|
72 */ |
|
73 protected void toggleSubmissionAlarm(final Context context, String profileName, String profilePath, |
|
74 boolean enabled, boolean serviceEnabled) { |
|
75 final Class<?> serviceClass = HealthReportUploadService.class; |
|
76 Logger.info(LOG_TAG, (serviceEnabled ? "R" : "Unr") + "egistering " + |
|
77 serviceClass.getSimpleName() + "."); |
|
78 |
|
79 // PendingIntents are compared without reference to their extras. Therefore |
|
80 // even though we pass the profile details to the action, different |
|
81 // profiles will share the *same* pending intent. In a multi-profile future, |
|
82 // this will need to be addressed. See Bug 882182. |
|
83 final Intent service = new Intent(context, serviceClass); |
|
84 service.setAction("upload"); // PendingIntents "lose" their extras if no action is set. |
|
85 service.putExtra("uploadEnabled", enabled); |
|
86 service.putExtra("profileName", profileName); |
|
87 service.putExtra("profilePath", profilePath); |
|
88 final PendingIntent pending = PendingIntent.getService(context, 0, service, PendingIntent.FLAG_CANCEL_CURRENT); |
|
89 |
|
90 if (!serviceEnabled) { |
|
91 cancelAlarm(pending); |
|
92 return; |
|
93 } |
|
94 |
|
95 final long pollInterval = getSubmissionPollInterval(); |
|
96 scheduleAlarm(pollInterval, pending); |
|
97 } |
|
98 |
|
99 @Override |
|
100 protected void onHandleIntent(Intent intent) { |
|
101 Logger.setThreadLogTag(HealthReportConstants.GLOBAL_LOG_TAG); |
|
102 |
|
103 // Intent can be null. Bug 1025937. |
|
104 if (intent == null) { |
|
105 Logger.debug(LOG_TAG, "Short-circuiting on null intent."); |
|
106 return; |
|
107 } |
|
108 |
|
109 // The same intent can be handled by multiple methods so do not short-circuit evaluate. |
|
110 boolean handled = attemptHandleIntentForUpload(intent); |
|
111 handled = attemptHandleIntentForPrune(intent) ? true : handled; |
|
112 |
|
113 if (!handled) { |
|
114 Logger.warn(LOG_TAG, "Unhandled intent with action " + intent.getAction() + "."); |
|
115 } |
|
116 } |
|
117 |
|
118 /** |
|
119 * Attempts to handle the given intent for FHR document upload. If it cannot, false is returned. |
|
120 * |
|
121 * @param intent must be non-null. |
|
122 */ |
|
123 private boolean attemptHandleIntentForUpload(final Intent intent) { |
|
124 if (HealthReportConstants.UPLOAD_FEATURE_DISABLED) { |
|
125 Logger.debug(LOG_TAG, "Health report upload feature is compile-time disabled; not handling intent."); |
|
126 return false; |
|
127 } |
|
128 |
|
129 final String action = intent.getAction(); |
|
130 Logger.debug(LOG_TAG, "Health report upload feature is compile-time enabled; attempting to " + |
|
131 "handle intent with action " + action + "."); |
|
132 |
|
133 if (HealthReportConstants.ACTION_HEALTHREPORT_UPLOAD_PREF.equals(action)) { |
|
134 handleUploadPrefIntent(intent); |
|
135 return true; |
|
136 } |
|
137 |
|
138 if (Intent.ACTION_BOOT_COMPLETED.equals(action) || |
|
139 Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action)) { |
|
140 BackgroundService.reflectContextToFennec(this, |
|
141 GlobalConstants.GECKO_PREFERENCES_CLASS, |
|
142 GlobalConstants.GECKO_BROADCAST_HEALTHREPORT_UPLOAD_PREF_METHOD); |
|
143 return true; |
|
144 } |
|
145 |
|
146 return false; |
|
147 } |
|
148 |
|
149 /** |
|
150 * Handle the intent sent by the browser when it wishes to notify us |
|
151 * of the value of the user preference. Look at the value and toggle the |
|
152 * alarm service accordingly. |
|
153 * |
|
154 * @param intent must be non-null. |
|
155 */ |
|
156 private void handleUploadPrefIntent(Intent intent) { |
|
157 if (!intent.hasExtra("enabled")) { |
|
158 Logger.warn(LOG_TAG, "Got " + HealthReportConstants.ACTION_HEALTHREPORT_UPLOAD_PREF + " intent without enabled. Ignoring."); |
|
159 return; |
|
160 } |
|
161 |
|
162 final boolean enabled = intent.getBooleanExtra("enabled", true); |
|
163 Logger.debug(LOG_TAG, intent.getStringExtra("branch") + "/" + |
|
164 intent.getStringExtra("pref") + " = " + |
|
165 (intent.hasExtra("enabled") ? enabled : "")); |
|
166 |
|
167 String profileName = intent.getStringExtra("profileName"); |
|
168 String profilePath = intent.getStringExtra("profilePath"); |
|
169 |
|
170 if (profileName == null || profilePath == null) { |
|
171 Logger.warn(LOG_TAG, "Got " + HealthReportConstants.ACTION_HEALTHREPORT_UPLOAD_PREF + " intent without profilePath or profileName. Ignoring."); |
|
172 return; |
|
173 } |
|
174 |
|
175 Logger.pii(LOG_TAG, "Updating health report upload alarm for profile " + profileName + " at " + |
|
176 profilePath + "."); |
|
177 |
|
178 final SharedPreferences sharedPrefs = getSharedPreferences(); |
|
179 final ObsoleteDocumentTracker tracker = new ObsoleteDocumentTracker(sharedPrefs); |
|
180 final boolean hasObsoleteIds = tracker.hasObsoleteIds(); |
|
181 |
|
182 if (!enabled) { |
|
183 final Editor editor = sharedPrefs.edit(); |
|
184 editor.remove(HealthReportConstants.PREF_LAST_UPLOAD_DOCUMENT_ID); |
|
185 |
|
186 if (hasObsoleteIds) { |
|
187 Logger.debug(LOG_TAG, "Health report upload disabled; scheduling deletion of " + tracker.numberOfObsoleteIds() + " documents."); |
|
188 tracker.limitObsoleteIds(); |
|
189 } else { |
|
190 // Primarily intended for debugging and testing. |
|
191 Logger.debug(LOG_TAG, "Health report upload disabled and no deletes to schedule: clearing prefs."); |
|
192 editor.remove(HealthReportConstants.PREF_FIRST_RUN); |
|
193 editor.remove(HealthReportConstants.PREF_NEXT_SUBMISSION); |
|
194 } |
|
195 |
|
196 editor.commit(); |
|
197 } |
|
198 |
|
199 // The user can toggle us off or on, or we can have obsolete documents to |
|
200 // remove. |
|
201 final boolean serviceEnabled = hasObsoleteIds || enabled; |
|
202 toggleSubmissionAlarm(this, profileName, profilePath, enabled, serviceEnabled); |
|
203 } |
|
204 |
|
205 /** |
|
206 * Attempts to handle the given intent for FHR data pruning. If it cannot, false is returned. |
|
207 * |
|
208 * @param intent must be non-null. |
|
209 */ |
|
210 private boolean attemptHandleIntentForPrune(final Intent intent) { |
|
211 final String action = intent.getAction(); |
|
212 Logger.debug(LOG_TAG, "Prune: Attempting to handle intent with action, " + action + "."); |
|
213 |
|
214 if (HealthReportConstants.ACTION_HEALTHREPORT_PRUNE.equals(action)) { |
|
215 handlePruneIntent(intent); |
|
216 return true; |
|
217 } |
|
218 |
|
219 if (Intent.ACTION_BOOT_COMPLETED.equals(action) || |
|
220 Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action)) { |
|
221 BackgroundService.reflectContextToFennec(this, |
|
222 GlobalConstants.GECKO_PREFERENCES_CLASS, |
|
223 GlobalConstants.GECKO_BROADCAST_HEALTHREPORT_PRUNE_METHOD); |
|
224 return true; |
|
225 } |
|
226 |
|
227 return false; |
|
228 } |
|
229 |
|
230 /** |
|
231 * @param intent must be non-null. |
|
232 */ |
|
233 private void handlePruneIntent(final Intent intent) { |
|
234 final String profileName = intent.getStringExtra("profileName"); |
|
235 final String profilePath = intent.getStringExtra("profilePath"); |
|
236 |
|
237 if (profileName == null || profilePath == null) { |
|
238 Logger.warn(LOG_TAG, "Got " + HealthReportConstants.ACTION_HEALTHREPORT_PRUNE + " intent " + |
|
239 "without profilePath or profileName. Ignoring."); |
|
240 return; |
|
241 } |
|
242 |
|
243 final Class<?> serviceClass = HealthReportPruneService.class; |
|
244 final Intent service = new Intent(this, serviceClass); |
|
245 service.setAction("prune"); // Intents without actions have their extras removed. |
|
246 service.putExtra("profileName", profileName); |
|
247 service.putExtra("profilePath", profilePath); |
|
248 final PendingIntent pending = PendingIntent.getService(this, 0, service, |
|
249 PendingIntent.FLAG_CANCEL_CURRENT); |
|
250 |
|
251 // Set a regular alarm to start PruneService. Since the various actions that PruneService can |
|
252 // take occur on irregular intervals, we can be more efficient by only starting the Service |
|
253 // when one of these time limits runs out. However, subsequent Service invocations must then |
|
254 // be registered by the PruneService itself, which would fail if the PruneService crashes. |
|
255 // Thus, we set this regular (and slightly inefficient) alarm. |
|
256 Logger.info(LOG_TAG, "Registering " + serviceClass.getSimpleName() + "."); |
|
257 final long pollInterval = getPrunePollInterval(); |
|
258 scheduleAlarm(pollInterval, pending); |
|
259 } |
|
260 } |