mobile/android/base/background/healthreport/upload/AndroidSubmissionClient.java

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

Correct previous dual key logic pending first delivery installment.

     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 }

mercurial