|
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.upload; |
|
6 |
|
7 import java.io.IOException; |
|
8 import java.io.UnsupportedEncodingException; |
|
9 import java.net.URISyntaxException; |
|
10 import java.util.ArrayList; |
|
11 import java.util.Collection; |
|
12 |
|
13 import org.json.JSONException; |
|
14 import org.json.JSONObject; |
|
15 import org.mozilla.gecko.BrowserLocaleManager; |
|
16 import org.mozilla.gecko.background.bagheera.BagheeraClient; |
|
17 import org.mozilla.gecko.background.bagheera.BagheeraRequestDelegate; |
|
18 import org.mozilla.gecko.background.common.GlobalConstants; |
|
19 import org.mozilla.gecko.background.common.log.Logger; |
|
20 import org.mozilla.gecko.background.healthreport.Environment; |
|
21 import org.mozilla.gecko.background.healthreport.EnvironmentBuilder; |
|
22 import org.mozilla.gecko.background.healthreport.HealthReportConstants; |
|
23 import org.mozilla.gecko.background.healthreport.HealthReportDatabaseStorage; |
|
24 import org.mozilla.gecko.background.healthreport.HealthReportGenerator; |
|
25 import org.mozilla.gecko.background.healthreport.HealthReportStorage; |
|
26 import org.mozilla.gecko.background.healthreport.HealthReportStorage.Field; |
|
27 import org.mozilla.gecko.background.healthreport.HealthReportStorage.MeasurementFields; |
|
28 import org.mozilla.gecko.background.healthreport.ProfileInformationCache; |
|
29 import org.mozilla.gecko.sync.net.BaseResource; |
|
30 |
|
31 import android.content.ContentProviderClient; |
|
32 import android.content.Context; |
|
33 import android.content.SharedPreferences; |
|
34 import ch.boye.httpclientandroidlib.HttpResponse; |
|
35 |
|
36 public class AndroidSubmissionClient implements SubmissionClient { |
|
37 protected static final String LOG_TAG = AndroidSubmissionClient.class.getSimpleName(); |
|
38 |
|
39 private static final String MEASUREMENT_NAME_SUBMISSIONS = "org.mozilla.healthreport.submissions"; |
|
40 private static final int MEASUREMENT_VERSION_SUBMISSIONS = 1; |
|
41 |
|
42 protected final Context context; |
|
43 protected final SharedPreferences sharedPreferences; |
|
44 protected final String profilePath; |
|
45 |
|
46 public AndroidSubmissionClient(Context context, SharedPreferences sharedPreferences, String profilePath) { |
|
47 this.context = context; |
|
48 this.sharedPreferences = sharedPreferences; |
|
49 this.profilePath = profilePath; |
|
50 } |
|
51 |
|
52 public SharedPreferences getSharedPreferences() { |
|
53 return sharedPreferences; |
|
54 } |
|
55 |
|
56 public String getDocumentServerURI() { |
|
57 return getSharedPreferences().getString(HealthReportConstants.PREF_DOCUMENT_SERVER_URI, HealthReportConstants.DEFAULT_DOCUMENT_SERVER_URI); |
|
58 } |
|
59 |
|
60 public String getDocumentServerNamespace() { |
|
61 return getSharedPreferences().getString(HealthReportConstants.PREF_DOCUMENT_SERVER_NAMESPACE, HealthReportConstants.DEFAULT_DOCUMENT_SERVER_NAMESPACE); |
|
62 } |
|
63 |
|
64 public long getLastUploadLocalTime() { |
|
65 return getSharedPreferences().getLong(HealthReportConstants.PREF_LAST_UPLOAD_LOCAL_TIME, 0L); |
|
66 } |
|
67 |
|
68 public String getLastUploadDocumentId() { |
|
69 return getSharedPreferences().getString(HealthReportConstants.PREF_LAST_UPLOAD_DOCUMENT_ID, null); |
|
70 } |
|
71 |
|
72 public boolean hasUploadBeenRequested() { |
|
73 return getSharedPreferences().contains(HealthReportConstants.PREF_LAST_UPLOAD_REQUESTED); |
|
74 } |
|
75 |
|
76 public void setLastUploadLocalTimeAndDocumentId(long localTime, String id) { |
|
77 getSharedPreferences().edit() |
|
78 .putLong(HealthReportConstants.PREF_LAST_UPLOAD_LOCAL_TIME, localTime) |
|
79 .putString(HealthReportConstants.PREF_LAST_UPLOAD_DOCUMENT_ID, id) |
|
80 .commit(); |
|
81 } |
|
82 |
|
83 protected HealthReportDatabaseStorage getStorage(final ContentProviderClient client) { |
|
84 return EnvironmentBuilder.getStorage(client, profilePath); |
|
85 } |
|
86 |
|
87 protected JSONObject generateDocument(final long localTime, final long last, |
|
88 final SubmissionsTracker tracker) throws JSONException { |
|
89 final long since = localTime - GlobalConstants.MILLISECONDS_PER_SIX_MONTHS; |
|
90 final HealthReportGenerator generator = tracker.getGenerator(); |
|
91 return generator.generateDocument(since, last, profilePath); |
|
92 } |
|
93 |
|
94 protected void uploadPayload(String id, String payload, Collection<String> oldIds, BagheeraRequestDelegate uploadDelegate) { |
|
95 final BagheeraClient client = new BagheeraClient(getDocumentServerURI()); |
|
96 |
|
97 Logger.pii(LOG_TAG, "New health report has id " + id + |
|
98 "and obsoletes " + (oldIds != null ? Integer.toString(oldIds.size()) : "no") + " old ids."); |
|
99 |
|
100 try { |
|
101 client.uploadJSONDocument(getDocumentServerNamespace(), |
|
102 id, |
|
103 payload, |
|
104 oldIds, |
|
105 uploadDelegate); |
|
106 } catch (Exception e) { |
|
107 uploadDelegate.handleError(e); |
|
108 } |
|
109 } |
|
110 |
|
111 @Override |
|
112 public void upload(long localTime, String id, Collection<String> oldIds, Delegate delegate) { |
|
113 // We abuse the life-cycle of an Android ContentProvider slightly by holding |
|
114 // onto a ContentProviderClient while we generate a payload. This keeps our |
|
115 // database storage alive, and may also allow us to share a database |
|
116 // connection with a BrowserHealthRecorder from Fennec. The ContentProvider |
|
117 // owns all underlying Storage instances, so we don't need to explicitly |
|
118 // close them. |
|
119 ContentProviderClient client = EnvironmentBuilder.getContentProviderClient(context); |
|
120 if (client == null) { |
|
121 // TODO: Bug 910898 - Store client failure in SharedPrefs so we can increment next time with storage. |
|
122 delegate.onHardFailure(localTime, null, "Could not fetch content provider client.", null); |
|
123 return; |
|
124 } |
|
125 |
|
126 try { |
|
127 // Storage instance is owned by HealthReportProvider, so we don't need to |
|
128 // close it. It's worth noting that this call will fail if called |
|
129 // out-of-process. |
|
130 final HealthReportDatabaseStorage storage = getStorage(client); |
|
131 if (storage == null) { |
|
132 // TODO: Bug 910898 - Store error in SharedPrefs so we can increment next time with storage. |
|
133 delegate.onHardFailure(localTime, null, "No storage when generating report.", null); |
|
134 return; |
|
135 } |
|
136 |
|
137 long last = Math.max(getLastUploadLocalTime(), HealthReportConstants.EARLIEST_LAST_PING); |
|
138 if (!storage.hasEventSince(last)) { |
|
139 delegate.onHardFailure(localTime, null, "No new events in storage.", null); |
|
140 return; |
|
141 } |
|
142 |
|
143 initializeStorageForUploadProviders(storage); |
|
144 |
|
145 final SubmissionsTracker tracker = |
|
146 getSubmissionsTracker(storage, localTime, hasUploadBeenRequested()); |
|
147 try { |
|
148 // TODO: Bug 910898 - Add errors from sharedPrefs to tracker. |
|
149 final JSONObject document = generateDocument(localTime, last, tracker); |
|
150 if (document == null) { |
|
151 delegate.onHardFailure(localTime, null, "Generator returned null document.", null); |
|
152 return; |
|
153 } |
|
154 |
|
155 final BagheeraRequestDelegate uploadDelegate = tracker.getDelegate(delegate, localTime, |
|
156 true, id); |
|
157 this.uploadPayload(id, document.toString(), oldIds, uploadDelegate); |
|
158 } catch (Exception e) { |
|
159 // Incrementing the failure count here could potentially cause the failure count to be |
|
160 // incremented twice, but this helper class checks and prevents this. |
|
161 tracker.incrementUploadClientFailureCount(); |
|
162 throw e; |
|
163 } |
|
164 } catch (Exception e) { |
|
165 // TODO: Bug 910898 - Store client failure in SharedPrefs so we can increment next time with storage. |
|
166 Logger.warn(LOG_TAG, "Got exception generating document.", e); |
|
167 delegate.onHardFailure(localTime, null, "Got exception uploading.", e); |
|
168 return; |
|
169 } finally { |
|
170 client.release(); |
|
171 } |
|
172 } |
|
173 |
|
174 protected SubmissionsTracker getSubmissionsTracker(final HealthReportStorage storage, |
|
175 final long localTime, final boolean hasUploadBeenRequested) { |
|
176 return new SubmissionsTracker(storage, localTime, hasUploadBeenRequested); |
|
177 } |
|
178 |
|
179 @Override |
|
180 public void delete(final long localTime, final String id, Delegate delegate) { |
|
181 final BagheeraClient client = new BagheeraClient(getDocumentServerURI()); |
|
182 |
|
183 Logger.pii(LOG_TAG, "Deleting health report with id " + id + "."); |
|
184 |
|
185 BagheeraRequestDelegate deleteDelegate = new RequestDelegate(delegate, localTime, false, id); |
|
186 try { |
|
187 client.deleteDocument(getDocumentServerNamespace(), id, deleteDelegate); |
|
188 } catch (Exception e) { |
|
189 deleteDelegate.handleError(e); |
|
190 } |
|
191 } |
|
192 |
|
193 protected class RequestDelegate implements BagheeraRequestDelegate { |
|
194 protected final Delegate delegate; |
|
195 protected final boolean isUpload; |
|
196 protected final String methodString; |
|
197 protected final long localTime; |
|
198 protected final String id; |
|
199 |
|
200 public RequestDelegate(Delegate delegate, long localTime, boolean isUpload, String id) { |
|
201 this.delegate = delegate; |
|
202 this.localTime = localTime; |
|
203 this.isUpload = isUpload; |
|
204 this.methodString = this.isUpload ? "upload" : "delete"; |
|
205 this.id = id; |
|
206 } |
|
207 |
|
208 @Override |
|
209 public String getUserAgent() { |
|
210 return HealthReportConstants.USER_AGENT; |
|
211 } |
|
212 |
|
213 @Override |
|
214 public void handleSuccess(int status, String namespace, String id, HttpResponse response) { |
|
215 BaseResource.consumeEntity(response); |
|
216 if (isUpload) { |
|
217 setLastUploadLocalTimeAndDocumentId(localTime, id); |
|
218 } |
|
219 Logger.debug(LOG_TAG, "Successful " + methodString + " at " + localTime + "."); |
|
220 delegate.onSuccess(localTime, id); |
|
221 } |
|
222 |
|
223 /** |
|
224 * Bagheera status codes: |
|
225 * |
|
226 * 403 Forbidden - Violated access restrictions. Most likely because of the method used. |
|
227 * 413 Request Too Large - Request payload was larger than the configured maximum. |
|
228 * 400 Bad Request - Returned if the POST/PUT failed validation in some manner. |
|
229 * 404 Not Found - Returned if the URI path doesn't exist or if the URI was not in the proper format. |
|
230 * 500 Server Error - General server error. Someone with access should look at the logs for more details. |
|
231 */ |
|
232 @Override |
|
233 public void handleFailure(int status, String namespace, HttpResponse response) { |
|
234 BaseResource.consumeEntity(response); |
|
235 Logger.debug(LOG_TAG, "Failed " + methodString + " at " + localTime + "."); |
|
236 if (status >= 500) { |
|
237 delegate.onSoftFailure(localTime, id, "Got status " + status + " from server.", null); |
|
238 return; |
|
239 } |
|
240 // Things are either bad locally (bad payload format, too much data) or |
|
241 // bad remotely (badly configured server, temporarily unavailable). Try |
|
242 // again tomorrow. |
|
243 delegate.onHardFailure(localTime, id, "Got status " + status + " from server.", null); |
|
244 } |
|
245 |
|
246 @Override |
|
247 public void handleError(Exception e) { |
|
248 Logger.debug(LOG_TAG, "Exception during " + methodString + " at " + localTime + ".", e); |
|
249 if (e instanceof IOException) { |
|
250 // Let's assume IO exceptions are Android dropping the network. |
|
251 delegate.onSoftFailure(localTime, id, "Got exception during " + methodString + ".", e); |
|
252 return; |
|
253 } |
|
254 delegate.onHardFailure(localTime, id, "Got exception during " + methodString + ".", e); |
|
255 } |
|
256 }; |
|
257 |
|
258 private void initializeStorageForUploadProviders(HealthReportDatabaseStorage storage) { |
|
259 storage.beginInitialization(); |
|
260 try { |
|
261 initializeSubmissionsProvider(storage); |
|
262 storage.finishInitialization(); |
|
263 } catch (Exception e) { |
|
264 // TODO: Bug 910898 - Store error in SharedPrefs so we can increment next time with storage. |
|
265 storage.abortInitialization(); |
|
266 throw new IllegalStateException("Could not initialize storage for upload provider.", e); |
|
267 } |
|
268 } |
|
269 |
|
270 private void initializeSubmissionsProvider(HealthReportDatabaseStorage storage) { |
|
271 storage.ensureMeasurementInitialized( |
|
272 MEASUREMENT_NAME_SUBMISSIONS, |
|
273 MEASUREMENT_VERSION_SUBMISSIONS, |
|
274 new MeasurementFields() { |
|
275 @Override |
|
276 public Iterable<FieldSpec> getFields() { |
|
277 final ArrayList<FieldSpec> out = new ArrayList<FieldSpec>(); |
|
278 for (SubmissionsFieldName fieldName : SubmissionsFieldName.values()) { |
|
279 FieldSpec spec = new FieldSpec(fieldName.getName(), Field.TYPE_INTEGER_COUNTER); |
|
280 out.add(spec); |
|
281 } |
|
282 return out; |
|
283 } |
|
284 }); |
|
285 } |
|
286 |
|
287 public static enum SubmissionsFieldName { |
|
288 FIRST_ATTEMPT("firstDocumentUploadAttempt"), |
|
289 CONTINUATION_ATTEMPT("continuationDocumentUploadAttempt"), |
|
290 SUCCESS("uploadSuccess"), |
|
291 TRANSPORT_FAILURE("uploadTransportFailure"), |
|
292 SERVER_FAILURE("uploadServerFailure"), |
|
293 CLIENT_FAILURE("uploadClientFailure"); |
|
294 |
|
295 private final String name; |
|
296 |
|
297 SubmissionsFieldName(String name) { |
|
298 this.name = name; |
|
299 } |
|
300 |
|
301 public String getName() { |
|
302 return name; |
|
303 } |
|
304 |
|
305 public int getID(HealthReportStorage storage) { |
|
306 final Field field = storage.getField(MEASUREMENT_NAME_SUBMISSIONS, |
|
307 MEASUREMENT_VERSION_SUBMISSIONS, |
|
308 name); |
|
309 return field.getID(); |
|
310 } |
|
311 } |
|
312 |
|
313 /** |
|
314 * Encapsulates the counting mechanisms for submissions status counts. Ensures multiple failures |
|
315 * and successes are not recorded for a single instance. |
|
316 */ |
|
317 public class SubmissionsTracker { |
|
318 private final HealthReportStorage storage; |
|
319 private final ProfileInformationCache profileCache; |
|
320 private final int day; |
|
321 private final int envID; |
|
322 |
|
323 private boolean isUploadStatusCountIncremented; |
|
324 |
|
325 public SubmissionsTracker(final HealthReportStorage storage, final long localTime, |
|
326 final boolean hasUploadBeenRequested) throws IllegalStateException { |
|
327 this.storage = storage; |
|
328 this.profileCache = getProfileInformationCache(); |
|
329 this.day = storage.getDay(localTime); |
|
330 this.envID = registerCurrentEnvironment(); |
|
331 |
|
332 this.isUploadStatusCountIncremented = false; |
|
333 |
|
334 if (!hasUploadBeenRequested) { |
|
335 incrementFirstUploadAttemptCount(); |
|
336 } else { |
|
337 incrementContinuationAttemptCount(); |
|
338 } |
|
339 } |
|
340 |
|
341 protected ProfileInformationCache getProfileInformationCache() { |
|
342 final ProfileInformationCache profileCache = new ProfileInformationCache(profilePath); |
|
343 if (!profileCache.restoreUnlessInitialized()) { |
|
344 Logger.warn(LOG_TAG, "Not enough profile information to compute current environment."); |
|
345 throw new IllegalStateException("Could not retrieve current environment."); |
|
346 } |
|
347 return profileCache; |
|
348 } |
|
349 |
|
350 protected int registerCurrentEnvironment() { |
|
351 return EnvironmentBuilder.registerCurrentEnvironment(storage, profileCache); |
|
352 } |
|
353 |
|
354 protected void incrementFirstUploadAttemptCount() { |
|
355 Logger.debug(LOG_TAG, "Incrementing first upload attempt field."); |
|
356 storage.incrementDailyCount(envID, day, SubmissionsFieldName.FIRST_ATTEMPT.getID(storage)); |
|
357 } |
|
358 |
|
359 protected void incrementContinuationAttemptCount() { |
|
360 Logger.debug(LOG_TAG, "Incrementing continuation upload attempt field."); |
|
361 storage.incrementDailyCount(envID, day, SubmissionsFieldName.CONTINUATION_ATTEMPT.getID(storage)); |
|
362 } |
|
363 |
|
364 public void incrementUploadSuccessCount() { |
|
365 incrementStatusCount(SubmissionsFieldName.SUCCESS.getID(storage), "success"); |
|
366 } |
|
367 |
|
368 public void incrementUploadClientFailureCount() { |
|
369 incrementStatusCount(SubmissionsFieldName.CLIENT_FAILURE.getID(storage), "client failure"); |
|
370 } |
|
371 |
|
372 public void incrementUploadTransportFailureCount() { |
|
373 incrementStatusCount(SubmissionsFieldName.TRANSPORT_FAILURE.getID(storage), "transport failure"); |
|
374 } |
|
375 |
|
376 public void incrementUploadServerFailureCount() { |
|
377 incrementStatusCount(SubmissionsFieldName.SERVER_FAILURE.getID(storage), "server failure"); |
|
378 } |
|
379 |
|
380 private void incrementStatusCount(final int fieldID, final String countType) { |
|
381 if (!isUploadStatusCountIncremented) { |
|
382 Logger.debug(LOG_TAG, "Incrementing upload attempt " + countType + " count."); |
|
383 storage.incrementDailyCount(envID, day, fieldID); |
|
384 isUploadStatusCountIncremented = true; |
|
385 } else { |
|
386 Logger.warn(LOG_TAG, "Upload status count already incremented - not incrementing " + |
|
387 countType + " count."); |
|
388 } |
|
389 } |
|
390 |
|
391 public TrackingGenerator getGenerator() { |
|
392 return new TrackingGenerator(); |
|
393 } |
|
394 |
|
395 public class TrackingGenerator extends HealthReportGenerator { |
|
396 public TrackingGenerator() { |
|
397 super(storage); |
|
398 } |
|
399 |
|
400 @Override |
|
401 public JSONObject generateDocument(long since, long lastPingTime, |
|
402 String generationProfilePath) throws JSONException { |
|
403 |
|
404 // Let's make sure we have an accurate locale. |
|
405 BrowserLocaleManager.getInstance().getAndApplyPersistedLocale(context); |
|
406 |
|
407 final JSONObject document; |
|
408 // If the given profilePath matches the one we cached for the tracker, use the cached env. |
|
409 if (profilePath != null && profilePath.equals(generationProfilePath)) { |
|
410 final Environment environment = getCurrentEnvironment(); |
|
411 document = super.generateDocument(since, lastPingTime, environment); |
|
412 } else { |
|
413 document = super.generateDocument(since, lastPingTime, generationProfilePath); |
|
414 } |
|
415 |
|
416 if (document == null) { |
|
417 incrementUploadClientFailureCount(); |
|
418 } |
|
419 return document; |
|
420 } |
|
421 |
|
422 protected Environment getCurrentEnvironment() { |
|
423 return EnvironmentBuilder.getCurrentEnvironment(profileCache); |
|
424 } |
|
425 } |
|
426 |
|
427 public TrackingRequestDelegate getDelegate(final Delegate delegate, final long localTime, |
|
428 final boolean isUpload, final String id) { |
|
429 return new TrackingRequestDelegate(delegate, localTime, isUpload, id); |
|
430 } |
|
431 |
|
432 public class TrackingRequestDelegate extends RequestDelegate { |
|
433 public TrackingRequestDelegate(final Delegate delegate, final long localTime, |
|
434 final boolean isUpload, final String id) { |
|
435 super(delegate, localTime, isUpload, id); |
|
436 } |
|
437 |
|
438 @Override |
|
439 public void handleSuccess(int status, String namespace, String id, HttpResponse response) { |
|
440 super.handleSuccess(status, namespace, id, response); |
|
441 incrementUploadSuccessCount(); |
|
442 } |
|
443 |
|
444 @Override |
|
445 public void handleFailure(int status, String namespace, HttpResponse response) { |
|
446 super.handleFailure(status, namespace, response); |
|
447 incrementUploadServerFailureCount(); |
|
448 } |
|
449 |
|
450 @Override |
|
451 public void handleError(Exception e) { |
|
452 super.handleError(e); |
|
453 if (e instanceof IllegalArgumentException || |
|
454 e instanceof UnsupportedEncodingException || |
|
455 e instanceof URISyntaxException) { |
|
456 incrementUploadClientFailureCount(); |
|
457 } else { |
|
458 incrementUploadTransportFailureCount(); |
|
459 } |
|
460 } |
|
461 } |
|
462 } |
|
463 } |