mobile/android/base/sync/GlobalSession.java

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

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.sync;
     7 import java.io.IOException;
     8 import java.net.URI;
     9 import java.net.URISyntaxException;
    10 import java.util.ArrayList;
    11 import java.util.Collection;
    12 import java.util.Collections;
    13 import java.util.HashMap;
    14 import java.util.HashSet;
    15 import java.util.List;
    16 import java.util.Map;
    17 import java.util.Map.Entry;
    18 import java.util.Set;
    19 import java.util.concurrent.atomic.AtomicLong;
    21 import org.json.simple.JSONArray;
    22 import org.json.simple.parser.ParseException;
    23 import org.mozilla.gecko.background.common.log.Logger;
    24 import org.mozilla.gecko.sync.crypto.CryptoException;
    25 import org.mozilla.gecko.sync.crypto.KeyBundle;
    26 import org.mozilla.gecko.sync.delegates.BaseGlobalSessionCallback;
    27 import org.mozilla.gecko.sync.delegates.ClientsDataDelegate;
    28 import org.mozilla.gecko.sync.delegates.FreshStartDelegate;
    29 import org.mozilla.gecko.sync.delegates.JSONRecordFetchDelegate;
    30 import org.mozilla.gecko.sync.delegates.KeyUploadDelegate;
    31 import org.mozilla.gecko.sync.delegates.MetaGlobalDelegate;
    32 import org.mozilla.gecko.sync.delegates.NodeAssignmentCallback;
    33 import org.mozilla.gecko.sync.delegates.WipeServerDelegate;
    34 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
    35 import org.mozilla.gecko.sync.net.BaseResource;
    36 import org.mozilla.gecko.sync.net.HttpResponseObserver;
    37 import org.mozilla.gecko.sync.net.SyncResponse;
    38 import org.mozilla.gecko.sync.net.SyncStorageRecordRequest;
    39 import org.mozilla.gecko.sync.net.SyncStorageRequest;
    40 import org.mozilla.gecko.sync.net.SyncStorageRequestDelegate;
    41 import org.mozilla.gecko.sync.net.SyncStorageResponse;
    42 import org.mozilla.gecko.sync.stage.AndroidBrowserBookmarksServerSyncStage;
    43 import org.mozilla.gecko.sync.stage.AndroidBrowserHistoryServerSyncStage;
    44 import org.mozilla.gecko.sync.stage.CheckPreconditionsStage;
    45 import org.mozilla.gecko.sync.stage.CompletedStage;
    46 import org.mozilla.gecko.sync.stage.EnsureClusterURLStage;
    47 import org.mozilla.gecko.sync.stage.EnsureCrypto5KeysStage;
    48 import org.mozilla.gecko.sync.stage.FennecTabsServerSyncStage;
    49 import org.mozilla.gecko.sync.stage.FetchInfoCollectionsStage;
    50 import org.mozilla.gecko.sync.stage.FetchMetaGlobalStage;
    51 import org.mozilla.gecko.sync.stage.FormHistoryServerSyncStage;
    52 import org.mozilla.gecko.sync.stage.GlobalSyncStage;
    53 import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
    54 import org.mozilla.gecko.sync.stage.NoSuchStageException;
    55 import org.mozilla.gecko.sync.stage.PasswordsServerSyncStage;
    56 import org.mozilla.gecko.sync.stage.SyncClientsEngineStage;
    57 import org.mozilla.gecko.sync.stage.UploadMetaGlobalStage;
    59 import android.content.Context;
    60 import ch.boye.httpclientandroidlib.HttpResponse;
    62 public class GlobalSession implements HttpResponseObserver {
    63   private static final String LOG_TAG = "GlobalSession";
    65   public static final long STORAGE_VERSION = 5;
    67   public SyncConfiguration config = null;
    69   protected Map<Stage, GlobalSyncStage> stages;
    70   public Stage currentState = Stage.idle;
    72   public final BaseGlobalSessionCallback callback;
    73   protected final Context context;
    74   protected final ClientsDataDelegate clientsDelegate;
    75   protected final NodeAssignmentCallback nodeAssignmentCallback;
    77   /**
    78    * Map from engine name to new settings for an updated meta/global record.
    79    * Engines to remove will have <code>null</code> EngineSettings.
    80    */
    81   public final Map<String, EngineSettings> enginesToUpdate = new HashMap<String, EngineSettings>();
    83    /*
    84    * Key accessors.
    85    */
    86   public KeyBundle keyBundleForCollection(String collection) throws NoCollectionKeysSetException {
    87     return config.getCollectionKeys().keyBundleForCollection(collection);
    88   }
    90   /*
    91    * Config passthrough for convenience.
    92    */
    93   public AuthHeaderProvider getAuthHeaderProvider() {
    94     return config.getAuthHeaderProvider();
    95   }
    97   public URI wboURI(String collection, String id) throws URISyntaxException {
    98     return config.wboURI(collection, id);
    99   }
   101   public GlobalSession(SyncConfiguration config,
   102                        BaseGlobalSessionCallback callback,
   103                        Context context,
   104                        ClientsDataDelegate clientsDelegate, NodeAssignmentCallback nodeAssignmentCallback)
   105     throws SyncConfigurationException, IllegalArgumentException, IOException, ParseException, NonObjectJSONException {
   107     if (callback == null) {
   108       throw new IllegalArgumentException("Must provide a callback to GlobalSession constructor.");
   109     }
   111     this.callback        = callback;
   112     this.context         = context;
   113     this.clientsDelegate = clientsDelegate;
   114     this.nodeAssignmentCallback = nodeAssignmentCallback;
   116     this.config = config;
   117     registerCommands();
   118     prepareStages();
   120     if (config.stagesToSync == null) {
   121       Logger.info(LOG_TAG, "No stages to sync specified; defaulting to all valid engine names.");
   122       config.stagesToSync = Collections.unmodifiableCollection(SyncConfiguration.validEngineNames());
   123     }
   125     // TODO: data-driven plan for the sync, referring to prepareStages.
   126   }
   128   /**
   129    * Register commands this global session knows how to process.
   130    * <p>
   131    * Re-registering a command overwrites any existing registration.
   132    */
   133   protected static void registerCommands() {
   134     final CommandProcessor processor = CommandProcessor.getProcessor();
   136     processor.registerCommand("resetEngine", new CommandRunner(1) {
   137       @Override
   138       public void executeCommand(final GlobalSession session, List<String> args) {
   139         HashSet<String> names = new HashSet<String>();
   140         names.add(args.get(0));
   141         session.resetStagesByName(names);
   142       }
   143     });
   145     processor.registerCommand("resetAll", new CommandRunner(0) {
   146       @Override
   147       public void executeCommand(final GlobalSession session, List<String> args) {
   148         session.resetAllStages();
   149       }
   150     });
   152     processor.registerCommand("wipeEngine", new CommandRunner(1) {
   153       @Override
   154       public void executeCommand(final GlobalSession session, List<String> args) {
   155         HashSet<String> names = new HashSet<String>();
   156         names.add(args.get(0));
   157         session.wipeStagesByName(names);
   158       }
   159     });
   161     processor.registerCommand("wipeAll", new CommandRunner(0) {
   162       @Override
   163       public void executeCommand(final GlobalSession session, List<String> args) {
   164         session.wipeAllStages();
   165       }
   166     });
   168     processor.registerCommand("displayURI", new CommandRunner(3) {
   169       @Override
   170       public void executeCommand(final GlobalSession session, List<String> args) {
   171         CommandProcessor.displayURI(args, session.getContext());
   172       }
   173     });
   174   }
   176   protected void prepareStages() {
   177     HashMap<Stage, GlobalSyncStage> stages = new HashMap<Stage, GlobalSyncStage>();
   179     stages.put(Stage.checkPreconditions,      new CheckPreconditionsStage());
   180     stages.put(Stage.ensureClusterURL,        new EnsureClusterURLStage(nodeAssignmentCallback));
   181     stages.put(Stage.fetchInfoCollections,    new FetchInfoCollectionsStage());
   182     stages.put(Stage.fetchMetaGlobal,         new FetchMetaGlobalStage());
   183     stages.put(Stage.ensureKeysStage,         new EnsureCrypto5KeysStage());
   184     stages.put(Stage.syncClientsEngine,       new SyncClientsEngineStage());
   186     stages.put(Stage.syncTabs,                new FennecTabsServerSyncStage());
   187     stages.put(Stage.syncPasswords,           new PasswordsServerSyncStage());
   188     stages.put(Stage.syncBookmarks,           new AndroidBrowserBookmarksServerSyncStage());
   189     stages.put(Stage.syncHistory,             new AndroidBrowserHistoryServerSyncStage());
   190     stages.put(Stage.syncFormHistory,         new FormHistoryServerSyncStage());
   192     stages.put(Stage.uploadMetaGlobal,        new UploadMetaGlobalStage());
   193     stages.put(Stage.completed,               new CompletedStage());
   195     this.stages = Collections.unmodifiableMap(stages);
   196   }
   198   public GlobalSyncStage getSyncStageByName(String name) throws NoSuchStageException {
   199     return getSyncStageByName(Stage.byName(name));
   200   }
   202   public GlobalSyncStage getSyncStageByName(Stage next) throws NoSuchStageException {
   203     GlobalSyncStage stage = stages.get(next);
   204     if (stage == null) {
   205       throw new NoSuchStageException(next);
   206     }
   207     return stage;
   208   }
   210   public Collection<GlobalSyncStage> getSyncStagesByEnum(Collection<Stage> enums) {
   211     ArrayList<GlobalSyncStage> out = new ArrayList<GlobalSyncStage>();
   212     for (Stage name : enums) {
   213       try {
   214         GlobalSyncStage stage = this.getSyncStageByName(name);
   215         out.add(stage);
   216       } catch (NoSuchStageException e) {
   217         Logger.warn(LOG_TAG, "Unable to find stage with name " + name);
   218       }
   219     }
   220     return out;
   221   }
   223   public Collection<GlobalSyncStage> getSyncStagesByName(Collection<String> names) {
   224     ArrayList<GlobalSyncStage> out = new ArrayList<GlobalSyncStage>();
   225     for (String name : names) {
   226       try {
   227         GlobalSyncStage stage = this.getSyncStageByName(name);
   228         out.add(stage);
   229       } catch (NoSuchStageException e) {
   230         Logger.warn(LOG_TAG, "Unable to find stage with name " + name);
   231       }
   232     }
   233     return out;
   234   }
   236   /**
   237    * Advance and loop around the stages of a sync.
   238    * @param current
   239    * @return
   240    *        The next stage to execute.
   241    */
   242   public static Stage nextStage(Stage current) {
   243     int index = current.ordinal() + 1;
   244     int max   = Stage.completed.ordinal() + 1;
   245     return Stage.values()[index % max];
   246   }
   248   /**
   249    * Move to the next stage in the syncing process.
   250    */
   251   public void advance() {
   252     // If we have a backoff, request a backoff and don't advance to next stage.
   253     long existingBackoff = largestBackoffObserved.get();
   254     if (existingBackoff > 0) {
   255       this.abort(null, "Aborting sync because of backoff of " + existingBackoff + " milliseconds.");
   256       return;
   257     }
   259     this.callback.handleStageCompleted(this.currentState, this);
   260     Stage next = nextStage(this.currentState);
   261     GlobalSyncStage nextStage;
   262     try {
   263       nextStage = this.getSyncStageByName(next);
   264     } catch (NoSuchStageException e) {
   265       this.abort(e, "No such stage " + next);
   266       return;
   267     }
   268     this.currentState = next;
   269     Logger.info(LOG_TAG, "Running next stage " + next + " (" + nextStage + ")...");
   270     try {
   271       nextStage.execute(this);
   272     } catch (Exception ex) {
   273       Logger.warn(LOG_TAG, "Caught exception " + ex + " running stage " + next);
   274       this.abort(ex, "Uncaught exception in stage.");
   275       return;
   276     }
   277   }
   279   public Context getContext() {
   280     return this.context;
   281   }
   283   /**
   284    * Begin a sync.
   285    * <p>
   286    * The caller is responsible for:
   287    * <ul>
   288    * <li>Verifying that any backoffs/minimum next sync requests are respected.</li>
   289    * <li>Ensuring that the device is online.</li>
   290    * <li>Ensuring that dependencies are ready.</li>
   291    * </ul>
   292    *
   293    * @throws AlreadySyncingException
   294    */
   295   public void start() throws AlreadySyncingException {
   296     if (this.currentState != GlobalSyncStage.Stage.idle) {
   297       throw new AlreadySyncingException(this.currentState);
   298     }
   299     installAsHttpResponseObserver(); // Uninstalled by completeSync or abort.
   300     this.advance();
   301   }
   303   /**
   304    * Stop this sync and start again.
   305    * @throws AlreadySyncingException
   306    */
   307   protected void restart() throws AlreadySyncingException {
   308     this.currentState = GlobalSyncStage.Stage.idle;
   309     if (callback.shouldBackOffStorage()) {
   310       this.callback.handleAborted(this, "Told to back off.");
   311       return;
   312     }
   313     this.start();
   314   }
   316   /**
   317    * We're finished (aborted or succeeded): release resources.
   318    */
   319   protected void cleanUp() {
   320     uninstallAsHttpResponseObserver();
   321     this.stages = null;
   322   }
   324   public void completeSync() {
   325     cleanUp();
   326     this.currentState = GlobalSyncStage.Stage.idle;
   327     this.callback.handleSuccess(this);
   328   }
   330   /**
   331    * Record that an updated meta/global record should be uploaded with the given
   332    * settings for the given engine.
   333    *
   334    * @param engineName engine to update.
   335    * @param engineSettings new syncID and version.
   336    */
   337   public void recordForMetaGlobalUpdate(String engineName, EngineSettings engineSettings) {
   338     enginesToUpdate.put(engineName, engineSettings);
   339   }
   341   /**
   342    * Record that an updated meta/global record should be uploaded without the
   343    * given engine name.
   344    *
   345    * @param engineName
   346    *          engine to remove.
   347    */
   348   public void removeEngineFromMetaGlobal(String engineName) {
   349     enginesToUpdate.put(engineName, null);
   350   }
   352   public boolean hasUpdatedMetaGlobal() {
   353     if (enginesToUpdate.isEmpty()) {
   354       Logger.info(LOG_TAG, "Not uploading updated meta/global record since there are no engines requesting upload.");
   355       return false;
   356     }
   358     if (Logger.shouldLogVerbose(LOG_TAG)) {
   359       Logger.trace(LOG_TAG, "Uploading updated meta/global record since there are engine changes to meta/global.");
   360       Logger.trace(LOG_TAG, "Engines requesting update [" + Utils.toCommaSeparatedString(enginesToUpdate.keySet()) + "]");
   361     }
   363     return true;
   364   }
   366   public void updateMetaGlobalInPlace() {
   367     config.metaGlobal.declined = this.declinedEngineNames();
   368     ExtendedJSONObject engines = config.metaGlobal.getEngines();
   369     for (Entry<String, EngineSettings> pair : enginesToUpdate.entrySet()) {
   370       if (pair.getValue() == null) {
   371         engines.remove(pair.getKey());
   372       } else {
   373         engines.put(pair.getKey(), pair.getValue().toJSONObject());
   374       }
   375     }
   377     enginesToUpdate.clear();
   378   }
   380   /**
   381    * Synchronously upload an updated meta/global.
   382    * <p>
   383    * All problems are logged and ignored.
   384    */
   385   public void uploadUpdatedMetaGlobal() {
   386     updateMetaGlobalInPlace();
   388     Logger.debug(LOG_TAG, "Uploading updated meta/global record.");
   389     final Object monitor = new Object();
   391     Runnable doUpload = new Runnable() {
   392       @Override
   393       public void run() {
   394         config.metaGlobal.upload(new MetaGlobalDelegate() {
   395           @Override
   396           public void handleSuccess(MetaGlobal global, SyncStorageResponse response) {
   397             Logger.info(LOG_TAG, "Successfully uploaded updated meta/global record.");
   398             // Engine changes are stored as diffs, so update enabled engines in config to match uploaded meta/global.
   399             config.enabledEngineNames = config.metaGlobal.getEnabledEngineNames();
   400             // Clear userSelectedEngines because they are updated in config and meta/global.
   401             config.userSelectedEngines = null;
   403             synchronized (monitor) {
   404               monitor.notify();
   405             }
   406           }
   408           @Override
   409           public void handleMissing(MetaGlobal global, SyncStorageResponse response) {
   410             Logger.warn(LOG_TAG, "Got 404 missing uploading updated meta/global record; shouldn't happen.  Ignoring.");
   411             synchronized (monitor) {
   412               monitor.notify();
   413             }
   414           }
   416           @Override
   417           public void handleFailure(SyncStorageResponse response) {
   418             Logger.warn(LOG_TAG, "Failed to upload updated meta/global record; ignoring.");
   419             synchronized (monitor) {
   420               monitor.notify();
   421             }
   422           }
   424           @Override
   425           public void handleError(Exception e) {
   426             Logger.warn(LOG_TAG, "Got exception trying to upload updated meta/global record; ignoring.", e);
   427             synchronized (monitor) {
   428               monitor.notify();
   429             }
   430           }
   431         });
   432       }
   433     };
   435     final Thread upload = new Thread(doUpload);
   436     synchronized (monitor) {
   437       try {
   438         upload.start();
   439         monitor.wait();
   440         Logger.debug(LOG_TAG, "Uploaded updated meta/global record.");
   441       } catch (InterruptedException e) {
   442         Logger.error(LOG_TAG, "Uploading updated meta/global interrupted; continuing.");
   443       }
   444     }
   445   }
   448   public void abort(Exception e, String reason) {
   449     Logger.warn(LOG_TAG, "Aborting sync: " + reason, e);
   450     cleanUp();
   451     long existingBackoff = largestBackoffObserved.get();
   452     if (existingBackoff > 0) {
   453       callback.requestBackoff(existingBackoff);
   454     }
   455     if (!(e instanceof HTTPFailureException)) {
   456       //  e is null, or we aborted for a non-HTTP reason; okay to upload new meta/global record.
   457       if (this.hasUpdatedMetaGlobal()) {
   458         this.uploadUpdatedMetaGlobal(); // Only logs errors; does not call abort.
   459       }
   460     }
   461     this.callback.handleError(this, e);
   462   }
   464   public void handleHTTPError(SyncStorageResponse response, String reason) {
   465     // TODO: handling of 50x (backoff), 401 (node reassignment or auth error).
   466     // Fall back to aborting.
   467     Logger.warn(LOG_TAG, "Aborting sync due to HTTP " + response.getStatusCode());
   468     this.interpretHTTPFailure(response.httpResponse());
   469     this.abort(new HTTPFailureException(response), reason);
   470   }
   472   /**
   473    * Perform appropriate backoff etc. extraction.
   474    */
   475   public void interpretHTTPFailure(HttpResponse response) {
   476     // TODO: handle permanent rejection.
   477     long responseBackoff = (new SyncResponse(response)).totalBackoffInMilliseconds();
   478     if (responseBackoff > 0) {
   479       callback.requestBackoff(responseBackoff);
   480     }
   482     if (response.getStatusLine() != null) {
   483       final int statusCode = response.getStatusLine().getStatusCode();
   484       switch(statusCode) {
   486       case 400:
   487         SyncStorageResponse storageResponse = new SyncStorageResponse(response);
   488         this.interpretHTTPBadRequestBody(storageResponse);
   489         break;
   491       case 401:
   492         /*
   493          * Alert our callback we have a 401 on a cluster URL. This GlobalSession
   494          * will fail, but the next one will fetch a new cluster URL and will
   495          * distinguish between "node reassignment" and "user password changed".
   496          */
   497         callback.informUnauthorizedResponse(this, config.getClusterURL());
   498         break;
   499       }
   500     }
   501   }
   503   protected void interpretHTTPBadRequestBody(final SyncStorageResponse storageResponse) {
   504     try {
   505       final String body = storageResponse.body();
   506       if (body == null) {
   507         return;
   508       }
   509       if (SyncStorageResponse.RESPONSE_CLIENT_UPGRADE_REQUIRED.equals(body)) {
   510         callback.informUpgradeRequiredResponse(this);
   511         return;
   512       }
   513     } catch (Exception e) {
   514       Logger.warn(LOG_TAG, "Exception parsing HTTP 400 body.", e);
   515     }
   516   }
   518   public void fetchInfoCollections(JSONRecordFetchDelegate callback) throws URISyntaxException {
   519     final JSONRecordFetcher fetcher = new JSONRecordFetcher(config.infoCollectionsURL(), getAuthHeaderProvider());
   520     fetcher.fetch(callback);
   521   }
   523   /**
   524    * Upload new crypto/keys.
   525    *
   526    * @param keys
   527    *          new keys.
   528    * @param keyUploadDelegate
   529    *          a delegate.
   530    */
   531   public void uploadKeys(final CollectionKeys keys,
   532                          final KeyUploadDelegate keyUploadDelegate) {
   533     SyncStorageRecordRequest request;
   534     try {
   535       request = new SyncStorageRecordRequest(this.config.keysURI());
   536     } catch (URISyntaxException e) {
   537       keyUploadDelegate.onKeyUploadFailed(e);
   538       return;
   539     }
   541     request.delegate = new SyncStorageRequestDelegate() {
   543       @Override
   544       public String ifUnmodifiedSince() {
   545         return null;
   546       }
   548       @Override
   549       public void handleRequestSuccess(SyncStorageResponse response) {
   550         Logger.debug(LOG_TAG, "Keys uploaded.");
   551         BaseResource.consumeEntity(response); // We don't need the response at all.
   552         keyUploadDelegate.onKeysUploaded();
   553       }
   555       @Override
   556       public void handleRequestFailure(SyncStorageResponse response) {
   557         Logger.debug(LOG_TAG, "Failed to upload keys.");
   558         GlobalSession.this.interpretHTTPFailure(response.httpResponse());
   559         BaseResource.consumeEntity(response); // The exception thrown should not need the body of the response.
   560         keyUploadDelegate.onKeyUploadFailed(new HTTPFailureException(response));
   561       }
   563       @Override
   564       public void handleRequestError(Exception ex) {
   565         Logger.warn(LOG_TAG, "Got exception trying to upload keys", ex);
   566         keyUploadDelegate.onKeyUploadFailed(ex);
   567       }
   569       @Override
   570       public AuthHeaderProvider getAuthHeaderProvider() {
   571         return GlobalSession.this.getAuthHeaderProvider();
   572       }
   573     };
   575     // Convert keys to an encrypted crypto record.
   576     CryptoRecord keysRecord;
   577     try {
   578       keysRecord = keys.asCryptoRecord();
   579       keysRecord.setKeyBundle(config.syncKeyBundle);
   580       keysRecord.encrypt();
   581     } catch (Exception e) {
   582       Logger.warn(LOG_TAG, "Got exception trying creating crypto record from keys", e);
   583       keyUploadDelegate.onKeyUploadFailed(e);
   584       return;
   585     }
   587     request.put(keysRecord);
   588   }
   590   /*
   591    * meta/global callbacks.
   592    */
   593   public void processMetaGlobal(MetaGlobal global) {
   594     config.metaGlobal = global;
   596     Long storageVersion = global.getStorageVersion();
   597     if (storageVersion == null) {
   598       Logger.warn(LOG_TAG, "Malformed remote meta/global: could not retrieve remote storage version.");
   599       freshStart();
   600       return;
   601     }
   602     if (storageVersion < STORAGE_VERSION) {
   603       Logger.warn(LOG_TAG, "Outdated server: reported " +
   604           "remote storage version " + storageVersion + " < " +
   605           "local storage version " + STORAGE_VERSION);
   606       freshStart();
   607       return;
   608     }
   609     if (storageVersion > STORAGE_VERSION) {
   610       Logger.warn(LOG_TAG, "Outdated client: reported " +
   611           "remote storage version " + storageVersion + " > " +
   612           "local storage version " + STORAGE_VERSION);
   613       requiresUpgrade();
   614       return;
   615     }
   616     String remoteSyncID = global.getSyncID();
   617     if (remoteSyncID == null) {
   618       Logger.warn(LOG_TAG, "Malformed remote meta/global: could not retrieve remote syncID.");
   619       freshStart();
   620       return;
   621     }
   622     String localSyncID = config.syncID;
   623     if (!remoteSyncID.equals(localSyncID)) {
   624       Logger.warn(LOG_TAG, "Remote syncID different from local syncID: resetting client and assuming remote syncID.");
   625       resetAllStages();
   626       config.purgeCryptoKeys();
   627       config.syncID = remoteSyncID;
   628     }
   629     // Compare lastModified timestamps for remote/local engine selection times.
   630     Logger.debug(LOG_TAG, "Comparing local engine selection timestamp [" + config.userSelectedEnginesTimestamp + "] to server meta/global timestamp [" + config.persistedMetaGlobal().lastModified() + "].");
   631     if (config.userSelectedEnginesTimestamp < config.persistedMetaGlobal().lastModified()) {
   632       // Remote has later meta/global timestamp. Don't upload engine changes.
   633       config.userSelectedEngines = null;
   634     }
   635     // Persist enabled engine names.
   636     config.enabledEngineNames = global.getEnabledEngineNames();
   637     if (config.enabledEngineNames == null) {
   638       Logger.warn(LOG_TAG, "meta/global reported no enabled engine names!");
   639     } else {
   640       if (Logger.shouldLogVerbose(LOG_TAG)) {
   641         Logger.trace(LOG_TAG, "Persisting enabled engine names '" +
   642             Utils.toCommaSeparatedString(config.enabledEngineNames) + "' from meta/global.");
   643       }
   644     }
   646     // Persist declined.
   647     // Our declined engines at any point are:
   648     // Whatever they were remotely, plus whatever they were locally, less any
   649     // engines that were just enabled locally or remotely.
   650     // If remote just 'won', our recently enabled list just got cleared.
   651     final HashSet<String> allDeclined = new HashSet<String>();
   653     final Set<String> newRemoteDeclined = global.getDeclinedEngineNames();
   654     final Set<String> oldLocalDeclined = config.declinedEngineNames;
   656     allDeclined.addAll(newRemoteDeclined);
   657     allDeclined.addAll(oldLocalDeclined);
   659     if (config.userSelectedEngines != null) {
   660       for (Entry<String, Boolean> selection : config.userSelectedEngines.entrySet()) {
   661         if (selection.getValue()) {
   662           allDeclined.remove(selection.getKey());
   663         }
   664       }
   665     }
   667     config.declinedEngineNames = allDeclined;
   668     if (config.declinedEngineNames.isEmpty()) {
   669       Logger.debug(LOG_TAG, "meta/global reported no declined engine names, and we have none declined locally.");
   670     } else {
   671       if (Logger.shouldLogVerbose(LOG_TAG)) {
   672         Logger.trace(LOG_TAG, "Persisting declined engine names '" +
   673             Utils.toCommaSeparatedString(config.declinedEngineNames) + "' from meta/global.");
   674       }
   675     }
   677     config.persistToPrefs();
   678     advance();
   679   }
   681   public void processMissingMetaGlobal(MetaGlobal global) {
   682     freshStart();
   683   }
   685   /**
   686    * Do a fresh start then quietly finish the sync, starting another.
   687    */
   688   public void freshStart() {
   689     final GlobalSession globalSession = this;
   690     freshStart(this, new FreshStartDelegate() {
   692       @Override
   693       public void onFreshStartFailed(Exception e) {
   694         globalSession.abort(e, "Fresh start failed.");
   695       }
   697       @Override
   698       public void onFreshStart() {
   699         try {
   700           Logger.warn(LOG_TAG, "Fresh start succeeded; restarting global session.");
   701           globalSession.config.persistToPrefs();
   702           globalSession.restart();
   703         } catch (Exception e) {
   704           Logger.warn(LOG_TAG, "Got exception when restarting sync after freshStart.", e);
   705           globalSession.abort(e, "Got exception after freshStart.");
   706         }
   707       }
   708     });
   709   }
   711   /**
   712    * Clean the server, aborting the current sync.
   713    * <p>
   714    * <ol>
   715    * <li>Wipe the server storage.</li>
   716    * <li>Reset all stages and purge cached state: (meta/global and crypto/keys records).</li>
   717    * <li>Upload fresh meta/global record.</li>
   718    * <li>Upload fresh crypto/keys record.</li>
   719    * <li>Restart the sync entirely in order to re-download meta/global and crypto/keys record.</li>
   720    * </ol>
   721    * @param session the current session.
   722    * @param freshStartDelegate delegate to notify on fresh start or failure.
   723    */
   724   protected static void freshStart(final GlobalSession session, final FreshStartDelegate freshStartDelegate) {
   725     Logger.debug(LOG_TAG, "Fresh starting.");
   727     final MetaGlobal mg = session.generateNewMetaGlobal();
   729     session.wipeServer(session.getAuthHeaderProvider(), new WipeServerDelegate() {
   731       @Override
   732       public void onWiped(long timestamp) {
   733         Logger.debug(LOG_TAG, "Successfully wiped server.  Resetting all stages and purging cached meta/global and crypto/keys records.");
   735         session.resetAllStages();
   736         session.config.purgeMetaGlobal();
   737         session.config.purgeCryptoKeys();
   738         session.config.persistToPrefs();
   740         Logger.info(LOG_TAG, "Uploading new meta/global with sync ID " + mg.syncID + ".");
   742         // It would be good to set the X-If-Unmodified-Since header to `timestamp`
   743         // for this PUT to ensure at least some level of transactionality.
   744         // Unfortunately, the servers don't support it after a wipe right now
   745         // (bug 693893), so we're going to defer this until bug 692700.
   746         mg.upload(new MetaGlobalDelegate() {
   747           @Override
   748           public void handleSuccess(MetaGlobal uploadedGlobal, SyncStorageResponse uploadResponse) {
   749             Logger.info(LOG_TAG, "Uploaded new meta/global with sync ID " + uploadedGlobal.syncID + ".");
   751             // Generate new keys.
   752             CollectionKeys keys = null;
   753             try {
   754               keys = session.generateNewCryptoKeys();
   755             } catch (CryptoException e) {
   756               Logger.warn(LOG_TAG, "Got exception generating new keys; failing fresh start.", e);
   757               freshStartDelegate.onFreshStartFailed(e);
   758             }
   759             if (keys == null) {
   760               Logger.warn(LOG_TAG, "Got null keys from generateNewKeys; failing fresh start.");
   761               freshStartDelegate.onFreshStartFailed(null);
   762             }
   764             // Upload new keys.
   765             Logger.info(LOG_TAG, "Uploading new crypto/keys.");
   766             session.uploadKeys(keys, new KeyUploadDelegate() {
   767               @Override
   768               public void onKeysUploaded() {
   769                 Logger.info(LOG_TAG, "Uploaded new crypto/keys.");
   770                 freshStartDelegate.onFreshStart();
   771               }
   773               @Override
   774               public void onKeyUploadFailed(Exception e) {
   775                 Logger.warn(LOG_TAG, "Got exception uploading new keys.", e);
   776                 freshStartDelegate.onFreshStartFailed(e);
   777               }
   778             });
   779           }
   781           @Override
   782           public void handleMissing(MetaGlobal global, SyncStorageResponse response) {
   783             // Shouldn't happen on upload.
   784             Logger.warn(LOG_TAG, "Got 'missing' response uploading new meta/global.");
   785             freshStartDelegate.onFreshStartFailed(new Exception("meta/global missing while uploading."));
   786           }
   788           @Override
   789           public void handleFailure(SyncStorageResponse response) {
   790             Logger.warn(LOG_TAG, "Got failure " + response.getStatusCode() + " uploading new meta/global.");
   791             session.interpretHTTPFailure(response.httpResponse());
   792             freshStartDelegate.onFreshStartFailed(new HTTPFailureException(response));
   793           }
   795           @Override
   796           public void handleError(Exception e) {
   797             Logger.warn(LOG_TAG, "Got error uploading new meta/global.", e);
   798             freshStartDelegate.onFreshStartFailed(e);
   799           }
   800         });
   801       }
   803       @Override
   804       public void onWipeFailed(Exception e) {
   805         Logger.warn(LOG_TAG, "Wipe failed.");
   806         freshStartDelegate.onFreshStartFailed(e);
   807       }
   808     });
   809   }
   811   // Note that we do not yet implement wipeRemote: it's only necessary for
   812   // first sync options.
   813   // -- reset local stages, wipe server for each stage *except* clients
   814   //    (stages only, not whole server!), send wipeEngine commands to each client.
   815   //
   816   // Similarly for startOver (because we don't receive that notification).
   817   // -- remove client data from server, reset local stages, clear keys, reset
   818   //    backoff, clear all prefs, discard credentials.
   819   //
   820   // Change passphrase: wipe entire server, reset client to force upload, sync.
   821   //
   822   // When an engine is disabled: wipe its collections on the server, reupload
   823   // meta/global.
   824   //
   825   // On syncing each stage: if server has engine version 0 or old, wipe server,
   826   // reset client to prompt reupload.
   827   // If sync ID mismatch: take that syncID and reset client.
   829   protected void wipeServer(final AuthHeaderProvider authHeaderProvider, final WipeServerDelegate wipeDelegate) {
   830     SyncStorageRequest request;
   831     final GlobalSession self = this;
   833     try {
   834       request = new SyncStorageRequest(config.storageURL());
   835     } catch (URISyntaxException ex) {
   836       Logger.warn(LOG_TAG, "Invalid URI in wipeServer.");
   837       wipeDelegate.onWipeFailed(ex);
   838       return;
   839     }
   841     request.delegate = new SyncStorageRequestDelegate() {
   843       @Override
   844       public String ifUnmodifiedSince() {
   845         return null;
   846       }
   848       @Override
   849       public void handleRequestSuccess(SyncStorageResponse response) {
   850         BaseResource.consumeEntity(response);
   851         wipeDelegate.onWiped(response.normalizedWeaveTimestamp());
   852       }
   854       @Override
   855       public void handleRequestFailure(SyncStorageResponse response) {
   856         Logger.warn(LOG_TAG, "Got request failure " + response.getStatusCode() + " in wipeServer.");
   857         // Process HTTP failures here to pick up backoffs, etc.
   858         self.interpretHTTPFailure(response.httpResponse());
   859         BaseResource.consumeEntity(response); // The exception thrown should not need the body of the response.
   860         wipeDelegate.onWipeFailed(new HTTPFailureException(response));
   861       }
   863       @Override
   864       public void handleRequestError(Exception ex) {
   865         Logger.warn(LOG_TAG, "Got exception in wipeServer.", ex);
   866         wipeDelegate.onWipeFailed(ex);
   867       }
   869       @Override
   870       public AuthHeaderProvider getAuthHeaderProvider() {
   871         return GlobalSession.this.getAuthHeaderProvider();
   872       }
   873     };
   874     request.delete();
   875   }
   877   public void wipeAllStages() {
   878     Logger.info(LOG_TAG, "Wiping all stages.");
   879     // Includes "clients".
   880     this.wipeStagesByEnum(Stage.getNamedStages());
   881   }
   883   public void wipeStages(Collection<GlobalSyncStage> stages) {
   884     for (GlobalSyncStage stage : stages) {
   885       try {
   886         Logger.info(LOG_TAG, "Wiping " + stage);
   887         stage.wipeLocal(this);
   888       } catch (Exception e) {
   889         Logger.error(LOG_TAG, "Ignoring wipe failure for stage " + stage, e);
   890       }
   891     }
   892   }
   894   public void wipeStagesByEnum(Collection<Stage> stages) {
   895     wipeStages(this.getSyncStagesByEnum(stages));
   896   }
   898   public void wipeStagesByName(Collection<String> names) {
   899     wipeStages(this.getSyncStagesByName(names));
   900   }
   902   public void resetAllStages() {
   903     Logger.info(LOG_TAG, "Resetting all stages.");
   904     // Includes "clients".
   905     this.resetStagesByEnum(Stage.getNamedStages());
   906   }
   908   public void resetStages(Collection<GlobalSyncStage> stages) {
   909     for (GlobalSyncStage stage : stages) {
   910       try {
   911         Logger.info(LOG_TAG, "Resetting " + stage);
   912         stage.resetLocal(this);
   913       } catch (Exception e) {
   914         Logger.error(LOG_TAG, "Ignoring reset failure for stage " + stage, e);
   915       }
   916     }
   917   }
   919   public void resetStagesByEnum(Collection<Stage> stages) {
   920     resetStages(this.getSyncStagesByEnum(stages));
   921   }
   923   public void resetStagesByName(Collection<String> names) {
   924     resetStages(this.getSyncStagesByName(names));
   925   }
   927   /**
   928    * Engines to explicitly mark as declined in a fresh meta/global record.
   929    * <p>
   930    * Returns an empty array if the user hasn't elected to customize data types,
   931    * or an array of engines that the user un-checked during customization.
   932    * <p>
   933    * Engines that Android Sync doesn't recognize are <b>not</b> included in
   934    * the returned array.
   935    *
   936    * @return a new JSONArray of engine names.
   937    */
   938   @SuppressWarnings("unchecked")
   939   protected JSONArray declinedEngineNames() {
   940     final JSONArray declined = new JSONArray();
   941     for (String engine : config.declinedEngineNames) {
   942       declined.add(engine);
   943     };
   945     return declined;
   946   }
   948   /**
   949    * Engines to include in a fresh meta/global record.
   950    * <p>
   951    * Returns either the persisted engine names (perhaps we have been node
   952    * re-assigned and are initializing a clean server: we want to upload the
   953    * persisted engine names so that we don't accidentally disable engines that
   954    * Android Sync doesn't recognize), or the set of engines names that Android
   955    * Sync implements.
   956    *
   957    * @return set of engine names.
   958    */
   959   protected Set<String> enabledEngineNames() {
   960     if (config.enabledEngineNames != null) {
   961       return config.enabledEngineNames;
   962     }
   964     // These are the default set of engine names.
   965     Set<String> validEngineNames = SyncConfiguration.validEngineNames();
   967     // If the user hasn't set any selected engines, that's okay -- default to
   968     // everything.
   969     if (config.userSelectedEngines == null) {
   970       return validEngineNames;
   971     }
   973     // userSelectedEngines has keys that are engine names, and boolean values
   974     // corresponding to whether the user asked for the engine to sync or not. If
   975     // an engine is not present, that means the user didn't change its sync
   976     // setting. Since we default to everything on, that means the user didn't
   977     // turn it off; therefore, it's included in the set of engines to sync.
   978     Set<String> validAndSelectedEngineNames = new HashSet<String>();
   979     for (String engineName : validEngineNames) {
   980       if (config.userSelectedEngines.containsKey(engineName) &&
   981           !config.userSelectedEngines.get(engineName)) {
   982         continue;
   983       }
   984       validAndSelectedEngineNames.add(engineName);
   985     }
   986     return validAndSelectedEngineNames;
   987   }
   989   /**
   990    * Generate fresh crypto/keys collection.
   991    * @return crypto/keys collection.
   992    * @throws CryptoException
   993    */
   994   @SuppressWarnings("static-method")
   995   public CollectionKeys generateNewCryptoKeys() throws CryptoException {
   996     return CollectionKeys.generateCollectionKeys();
   997   }
   999   /**
  1000    * Generate a fresh meta/global record.
  1001    * @return meta/global record.
  1002    */
  1003   public MetaGlobal generateNewMetaGlobal() {
  1004     final String newSyncID   = Utils.generateGuid();
  1005     final String metaURL     = this.config.metaURL();
  1007     ExtendedJSONObject engines = new ExtendedJSONObject();
  1008     for (String engineName : enabledEngineNames()) {
  1009       EngineSettings engineSettings = null;
  1010       try {
  1011         GlobalSyncStage globalStage = this.getSyncStageByName(engineName);
  1012         Integer version = globalStage.getStorageVersion();
  1013         if (version == null) {
  1014           continue; // Don't want this stage to be included in meta/global.
  1016         engineSettings = new EngineSettings(Utils.generateGuid(), version.intValue());
  1017       } catch (NoSuchStageException e) {
  1018         // No trouble; Android Sync might not recognize this engine yet.
  1019         // By default, version 0.  Other clients will see the 0 version and reset/wipe accordingly.
  1020         engineSettings = new EngineSettings(Utils.generateGuid(), 0);
  1022       engines.put(engineName, engineSettings.toJSONObject());
  1025     MetaGlobal metaGlobal = new MetaGlobal(metaURL, this.getAuthHeaderProvider());
  1026     metaGlobal.setSyncID(newSyncID);
  1027     metaGlobal.setStorageVersion(STORAGE_VERSION);
  1028     metaGlobal.setEngines(engines);
  1030     // We assume that the config's declined engines have been updated
  1031     // according to the user's selections.
  1032     metaGlobal.setDeclinedEngineNames(this.declinedEngineNames());
  1034     return metaGlobal;
  1037   /**
  1038    * Suggest that your Sync client needs to be upgraded to work
  1039    * with this server.
  1040    */
  1041   public void requiresUpgrade() {
  1042     Logger.info(LOG_TAG, "Client outdated storage version; requires update.");
  1043     // TODO: notify UI.
  1044     this.abort(null, "Requires upgrade");
  1047   /**
  1048    * If meta/global is missing or malformed, throws a MetaGlobalException.
  1049    * Otherwise, returns true if there is an entry for this engine in the
  1050    * meta/global "engines" object.
  1051    * <p>
  1052    * This is a global/permanent setting, not a local/temporary setting. For the
  1053    * latter, see {@link GlobalSession#isEngineLocallyEnabled(String)}.
  1055    * @param engineName the name to check (e.g., "bookmarks").
  1056    * @param engineSettings
  1057    *        if non-null, verify that the server engine settings are congruent
  1058    *        with this, throwing the appropriate MetaGlobalException if not.
  1059    * @return
  1060    *        true if the engine with the provided name is present in the
  1061    *        meta/global "engines" object, and verification passed.
  1063    * @throws MetaGlobalException
  1064    */
  1065   public boolean isEngineRemotelyEnabled(String engineName, EngineSettings engineSettings) throws MetaGlobalException {
  1066     if (this.config.metaGlobal == null) {
  1067       throw new MetaGlobalNotSetException();
  1070     // This should not occur.
  1071     if (this.config.enabledEngineNames == null) {
  1072       Logger.error(LOG_TAG, "No enabled engines in config. Giving up.");
  1073       throw new MetaGlobalMissingEnginesException();
  1076     if (!(this.config.enabledEngineNames.contains(engineName))) {
  1077       Logger.debug(LOG_TAG, "Engine " + engineName + " not enabled: no meta/global entry.");
  1078       return false;
  1081     // If we have a meta/global, check that it's safe for us to sync.
  1082     // (If we don't, we'll create one later, which is why we return `true` above.)
  1083     if (engineSettings != null) {
  1084       // Throws if there's a problem.
  1085       this.config.metaGlobal.verifyEngineSettings(engineName, engineSettings);
  1088     return true;
  1092   /**
  1093    * Return true if the named stage should be synced this session.
  1094    * <p>
  1095    * This is a local/temporary setting, in contrast to the meta/global record,
  1096    * which is a global/permanent setting. For the latter, see
  1097    * {@link GlobalSession#isEngineRemotelyEnabled(String, EngineSettings)}.
  1099    * @param stageName
  1100    *          to query.
  1101    * @return true if named stage is enabled for this sync.
  1102    */
  1103   public boolean isEngineLocallyEnabled(String stageName) {
  1104     if (config.stagesToSync == null) {
  1105       return true;
  1107     return config.stagesToSync.contains(stageName);
  1110   public ClientsDataDelegate getClientsDelegate() {
  1111     return this.clientsDelegate;
  1114   /**
  1115    * The longest backoff observed to date; -1 means no backoff observed.
  1116    */
  1117   protected final AtomicLong largestBackoffObserved = new AtomicLong(-1);
  1119   /**
  1120    * Reset any observed backoff and start observing HTTP responses for backoff
  1121    * requests.
  1122    */
  1123   protected void installAsHttpResponseObserver() {
  1124     Logger.debug(LOG_TAG, "Installing " + this + " as BaseResource HttpResponseObserver.");
  1125     BaseResource.setHttpResponseObserver(this);
  1126     largestBackoffObserved.set(-1);
  1129   /**
  1130    * Stop observing HttpResponses for backoff requests.
  1131    */
  1132   protected void uninstallAsHttpResponseObserver() {
  1133     Logger.debug(LOG_TAG, "Uninstalling " + this + " as BaseResource HttpResponseObserver.");
  1134     BaseResource.setHttpResponseObserver(null);
  1137   /**
  1138    * Observe all HTTP response for backoff requests on all status codes, not just errors.
  1139    */
  1140   @Override
  1141   public void observeHttpResponse(HttpResponse response) {
  1142     long responseBackoff = (new SyncResponse(response)).totalBackoffInMilliseconds(); // TODO: don't allocate object?
  1143     if (responseBackoff <= 0) {
  1144       return;
  1147     Logger.debug(LOG_TAG, "Observed " + responseBackoff + " millisecond backoff request.");
  1148     while (true) {
  1149       long existingBackoff = largestBackoffObserved.get();
  1150       if (existingBackoff >= responseBackoff) {
  1151         return;
  1153       if (largestBackoffObserved.compareAndSet(existingBackoff, responseBackoff)) {
  1154         return;

mercurial