Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
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/. */
5 package org.mozilla.gecko.background.healthreport.upload;
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;
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;
31 import android.content.ContentProviderClient;
32 import android.content.Context;
33 import android.content.SharedPreferences;
34 import ch.boye.httpclientandroidlib.HttpResponse;
36 public class AndroidSubmissionClient implements SubmissionClient {
37 protected static final String LOG_TAG = AndroidSubmissionClient.class.getSimpleName();
39 private static final String MEASUREMENT_NAME_SUBMISSIONS = "org.mozilla.healthreport.submissions";
40 private static final int MEASUREMENT_VERSION_SUBMISSIONS = 1;
42 protected final Context context;
43 protected final SharedPreferences sharedPreferences;
44 protected final String profilePath;
46 public AndroidSubmissionClient(Context context, SharedPreferences sharedPreferences, String profilePath) {
47 this.context = context;
48 this.sharedPreferences = sharedPreferences;
49 this.profilePath = profilePath;
50 }
52 public SharedPreferences getSharedPreferences() {
53 return sharedPreferences;
54 }
56 public String getDocumentServerURI() {
57 return getSharedPreferences().getString(HealthReportConstants.PREF_DOCUMENT_SERVER_URI, HealthReportConstants.DEFAULT_DOCUMENT_SERVER_URI);
58 }
60 public String getDocumentServerNamespace() {
61 return getSharedPreferences().getString(HealthReportConstants.PREF_DOCUMENT_SERVER_NAMESPACE, HealthReportConstants.DEFAULT_DOCUMENT_SERVER_NAMESPACE);
62 }
64 public long getLastUploadLocalTime() {
65 return getSharedPreferences().getLong(HealthReportConstants.PREF_LAST_UPLOAD_LOCAL_TIME, 0L);
66 }
68 public String getLastUploadDocumentId() {
69 return getSharedPreferences().getString(HealthReportConstants.PREF_LAST_UPLOAD_DOCUMENT_ID, null);
70 }
72 public boolean hasUploadBeenRequested() {
73 return getSharedPreferences().contains(HealthReportConstants.PREF_LAST_UPLOAD_REQUESTED);
74 }
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 }
83 protected HealthReportDatabaseStorage getStorage(final ContentProviderClient client) {
84 return EnvironmentBuilder.getStorage(client, profilePath);
85 }
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 }
94 protected void uploadPayload(String id, String payload, Collection<String> oldIds, BagheeraRequestDelegate uploadDelegate) {
95 final BagheeraClient client = new BagheeraClient(getDocumentServerURI());
97 Logger.pii(LOG_TAG, "New health report has id " + id +
98 "and obsoletes " + (oldIds != null ? Integer.toString(oldIds.size()) : "no") + " old ids.");
100 try {
101 client.uploadJSONDocument(getDocumentServerNamespace(),
102 id,
103 payload,
104 oldIds,
105 uploadDelegate);
106 } catch (Exception e) {
107 uploadDelegate.handleError(e);
108 }
109 }
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 }
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 }
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 }
143 initializeStorageForUploadProviders(storage);
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 }
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 }
174 protected SubmissionsTracker getSubmissionsTracker(final HealthReportStorage storage,
175 final long localTime, final boolean hasUploadBeenRequested) {
176 return new SubmissionsTracker(storage, localTime, hasUploadBeenRequested);
177 }
179 @Override
180 public void delete(final long localTime, final String id, Delegate delegate) {
181 final BagheeraClient client = new BagheeraClient(getDocumentServerURI());
183 Logger.pii(LOG_TAG, "Deleting health report with id " + id + ".");
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 }
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;
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 }
208 @Override
209 public String getUserAgent() {
210 return HealthReportConstants.USER_AGENT;
211 }
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 }
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 }
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 };
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 }
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 }
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");
295 private final String name;
297 SubmissionsFieldName(String name) {
298 this.name = name;
299 }
301 public String getName() {
302 return name;
303 }
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 }
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;
323 private boolean isUploadStatusCountIncremented;
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();
332 this.isUploadStatusCountIncremented = false;
334 if (!hasUploadBeenRequested) {
335 incrementFirstUploadAttemptCount();
336 } else {
337 incrementContinuationAttemptCount();
338 }
339 }
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 }
350 protected int registerCurrentEnvironment() {
351 return EnvironmentBuilder.registerCurrentEnvironment(storage, profileCache);
352 }
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 }
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 }
364 public void incrementUploadSuccessCount() {
365 incrementStatusCount(SubmissionsFieldName.SUCCESS.getID(storage), "success");
366 }
368 public void incrementUploadClientFailureCount() {
369 incrementStatusCount(SubmissionsFieldName.CLIENT_FAILURE.getID(storage), "client failure");
370 }
372 public void incrementUploadTransportFailureCount() {
373 incrementStatusCount(SubmissionsFieldName.TRANSPORT_FAILURE.getID(storage), "transport failure");
374 }
376 public void incrementUploadServerFailureCount() {
377 incrementStatusCount(SubmissionsFieldName.SERVER_FAILURE.getID(storage), "server failure");
378 }
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 }
391 public TrackingGenerator getGenerator() {
392 return new TrackingGenerator();
393 }
395 public class TrackingGenerator extends HealthReportGenerator {
396 public TrackingGenerator() {
397 super(storage);
398 }
400 @Override
401 public JSONObject generateDocument(long since, long lastPingTime,
402 String generationProfilePath) throws JSONException {
404 // Let's make sure we have an accurate locale.
405 BrowserLocaleManager.getInstance().getAndApplyPersistedLocale(context);
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 }
416 if (document == null) {
417 incrementUploadClientFailureCount();
418 }
419 return document;
420 }
422 protected Environment getCurrentEnvironment() {
423 return EnvironmentBuilder.getCurrentEnvironment(profileCache);
424 }
425 }
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 }
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 }
438 @Override
439 public void handleSuccess(int status, String namespace, String id, HttpResponse response) {
440 super.handleSuccess(status, namespace, id, response);
441 incrementUploadSuccessCount();
442 }
444 @Override
445 public void handleFailure(int status, String namespace, HttpResponse response) {
446 super.handleFailure(status, namespace, response);
447 incrementUploadServerFailureCount();
448 }
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 }