mobile/android/base/sync/GlobalSession.java

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/mobile/android/base/sync/GlobalSession.java	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,1158 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +package org.mozilla.gecko.sync;
     1.9 +
    1.10 +import java.io.IOException;
    1.11 +import java.net.URI;
    1.12 +import java.net.URISyntaxException;
    1.13 +import java.util.ArrayList;
    1.14 +import java.util.Collection;
    1.15 +import java.util.Collections;
    1.16 +import java.util.HashMap;
    1.17 +import java.util.HashSet;
    1.18 +import java.util.List;
    1.19 +import java.util.Map;
    1.20 +import java.util.Map.Entry;
    1.21 +import java.util.Set;
    1.22 +import java.util.concurrent.atomic.AtomicLong;
    1.23 +
    1.24 +import org.json.simple.JSONArray;
    1.25 +import org.json.simple.parser.ParseException;
    1.26 +import org.mozilla.gecko.background.common.log.Logger;
    1.27 +import org.mozilla.gecko.sync.crypto.CryptoException;
    1.28 +import org.mozilla.gecko.sync.crypto.KeyBundle;
    1.29 +import org.mozilla.gecko.sync.delegates.BaseGlobalSessionCallback;
    1.30 +import org.mozilla.gecko.sync.delegates.ClientsDataDelegate;
    1.31 +import org.mozilla.gecko.sync.delegates.FreshStartDelegate;
    1.32 +import org.mozilla.gecko.sync.delegates.JSONRecordFetchDelegate;
    1.33 +import org.mozilla.gecko.sync.delegates.KeyUploadDelegate;
    1.34 +import org.mozilla.gecko.sync.delegates.MetaGlobalDelegate;
    1.35 +import org.mozilla.gecko.sync.delegates.NodeAssignmentCallback;
    1.36 +import org.mozilla.gecko.sync.delegates.WipeServerDelegate;
    1.37 +import org.mozilla.gecko.sync.net.AuthHeaderProvider;
    1.38 +import org.mozilla.gecko.sync.net.BaseResource;
    1.39 +import org.mozilla.gecko.sync.net.HttpResponseObserver;
    1.40 +import org.mozilla.gecko.sync.net.SyncResponse;
    1.41 +import org.mozilla.gecko.sync.net.SyncStorageRecordRequest;
    1.42 +import org.mozilla.gecko.sync.net.SyncStorageRequest;
    1.43 +import org.mozilla.gecko.sync.net.SyncStorageRequestDelegate;
    1.44 +import org.mozilla.gecko.sync.net.SyncStorageResponse;
    1.45 +import org.mozilla.gecko.sync.stage.AndroidBrowserBookmarksServerSyncStage;
    1.46 +import org.mozilla.gecko.sync.stage.AndroidBrowserHistoryServerSyncStage;
    1.47 +import org.mozilla.gecko.sync.stage.CheckPreconditionsStage;
    1.48 +import org.mozilla.gecko.sync.stage.CompletedStage;
    1.49 +import org.mozilla.gecko.sync.stage.EnsureClusterURLStage;
    1.50 +import org.mozilla.gecko.sync.stage.EnsureCrypto5KeysStage;
    1.51 +import org.mozilla.gecko.sync.stage.FennecTabsServerSyncStage;
    1.52 +import org.mozilla.gecko.sync.stage.FetchInfoCollectionsStage;
    1.53 +import org.mozilla.gecko.sync.stage.FetchMetaGlobalStage;
    1.54 +import org.mozilla.gecko.sync.stage.FormHistoryServerSyncStage;
    1.55 +import org.mozilla.gecko.sync.stage.GlobalSyncStage;
    1.56 +import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
    1.57 +import org.mozilla.gecko.sync.stage.NoSuchStageException;
    1.58 +import org.mozilla.gecko.sync.stage.PasswordsServerSyncStage;
    1.59 +import org.mozilla.gecko.sync.stage.SyncClientsEngineStage;
    1.60 +import org.mozilla.gecko.sync.stage.UploadMetaGlobalStage;
    1.61 +
    1.62 +import android.content.Context;
    1.63 +import ch.boye.httpclientandroidlib.HttpResponse;
    1.64 +
    1.65 +public class GlobalSession implements HttpResponseObserver {
    1.66 +  private static final String LOG_TAG = "GlobalSession";
    1.67 +
    1.68 +  public static final long STORAGE_VERSION = 5;
    1.69 +
    1.70 +  public SyncConfiguration config = null;
    1.71 +
    1.72 +  protected Map<Stage, GlobalSyncStage> stages;
    1.73 +  public Stage currentState = Stage.idle;
    1.74 +
    1.75 +  public final BaseGlobalSessionCallback callback;
    1.76 +  protected final Context context;
    1.77 +  protected final ClientsDataDelegate clientsDelegate;
    1.78 +  protected final NodeAssignmentCallback nodeAssignmentCallback;
    1.79 +
    1.80 +  /**
    1.81 +   * Map from engine name to new settings for an updated meta/global record.
    1.82 +   * Engines to remove will have <code>null</code> EngineSettings.
    1.83 +   */
    1.84 +  public final Map<String, EngineSettings> enginesToUpdate = new HashMap<String, EngineSettings>();
    1.85 +
    1.86 +   /*
    1.87 +   * Key accessors.
    1.88 +   */
    1.89 +  public KeyBundle keyBundleForCollection(String collection) throws NoCollectionKeysSetException {
    1.90 +    return config.getCollectionKeys().keyBundleForCollection(collection);
    1.91 +  }
    1.92 +
    1.93 +  /*
    1.94 +   * Config passthrough for convenience.
    1.95 +   */
    1.96 +  public AuthHeaderProvider getAuthHeaderProvider() {
    1.97 +    return config.getAuthHeaderProvider();
    1.98 +  }
    1.99 +
   1.100 +  public URI wboURI(String collection, String id) throws URISyntaxException {
   1.101 +    return config.wboURI(collection, id);
   1.102 +  }
   1.103 +
   1.104 +  public GlobalSession(SyncConfiguration config,
   1.105 +                       BaseGlobalSessionCallback callback,
   1.106 +                       Context context,
   1.107 +                       ClientsDataDelegate clientsDelegate, NodeAssignmentCallback nodeAssignmentCallback)
   1.108 +    throws SyncConfigurationException, IllegalArgumentException, IOException, ParseException, NonObjectJSONException {
   1.109 +
   1.110 +    if (callback == null) {
   1.111 +      throw new IllegalArgumentException("Must provide a callback to GlobalSession constructor.");
   1.112 +    }
   1.113 +
   1.114 +    this.callback        = callback;
   1.115 +    this.context         = context;
   1.116 +    this.clientsDelegate = clientsDelegate;
   1.117 +    this.nodeAssignmentCallback = nodeAssignmentCallback;
   1.118 +
   1.119 +    this.config = config;
   1.120 +    registerCommands();
   1.121 +    prepareStages();
   1.122 +
   1.123 +    if (config.stagesToSync == null) {
   1.124 +      Logger.info(LOG_TAG, "No stages to sync specified; defaulting to all valid engine names.");
   1.125 +      config.stagesToSync = Collections.unmodifiableCollection(SyncConfiguration.validEngineNames());
   1.126 +    }
   1.127 +
   1.128 +    // TODO: data-driven plan for the sync, referring to prepareStages.
   1.129 +  }
   1.130 +
   1.131 +  /**
   1.132 +   * Register commands this global session knows how to process.
   1.133 +   * <p>
   1.134 +   * Re-registering a command overwrites any existing registration.
   1.135 +   */
   1.136 +  protected static void registerCommands() {
   1.137 +    final CommandProcessor processor = CommandProcessor.getProcessor();
   1.138 +
   1.139 +    processor.registerCommand("resetEngine", new CommandRunner(1) {
   1.140 +      @Override
   1.141 +      public void executeCommand(final GlobalSession session, List<String> args) {
   1.142 +        HashSet<String> names = new HashSet<String>();
   1.143 +        names.add(args.get(0));
   1.144 +        session.resetStagesByName(names);
   1.145 +      }
   1.146 +    });
   1.147 +
   1.148 +    processor.registerCommand("resetAll", new CommandRunner(0) {
   1.149 +      @Override
   1.150 +      public void executeCommand(final GlobalSession session, List<String> args) {
   1.151 +        session.resetAllStages();
   1.152 +      }
   1.153 +    });
   1.154 +
   1.155 +    processor.registerCommand("wipeEngine", new CommandRunner(1) {
   1.156 +      @Override
   1.157 +      public void executeCommand(final GlobalSession session, List<String> args) {
   1.158 +        HashSet<String> names = new HashSet<String>();
   1.159 +        names.add(args.get(0));
   1.160 +        session.wipeStagesByName(names);
   1.161 +      }
   1.162 +    });
   1.163 +
   1.164 +    processor.registerCommand("wipeAll", new CommandRunner(0) {
   1.165 +      @Override
   1.166 +      public void executeCommand(final GlobalSession session, List<String> args) {
   1.167 +        session.wipeAllStages();
   1.168 +      }
   1.169 +    });
   1.170 +
   1.171 +    processor.registerCommand("displayURI", new CommandRunner(3) {
   1.172 +      @Override
   1.173 +      public void executeCommand(final GlobalSession session, List<String> args) {
   1.174 +        CommandProcessor.displayURI(args, session.getContext());
   1.175 +      }
   1.176 +    });
   1.177 +  }
   1.178 +
   1.179 +  protected void prepareStages() {
   1.180 +    HashMap<Stage, GlobalSyncStage> stages = new HashMap<Stage, GlobalSyncStage>();
   1.181 +
   1.182 +    stages.put(Stage.checkPreconditions,      new CheckPreconditionsStage());
   1.183 +    stages.put(Stage.ensureClusterURL,        new EnsureClusterURLStage(nodeAssignmentCallback));
   1.184 +    stages.put(Stage.fetchInfoCollections,    new FetchInfoCollectionsStage());
   1.185 +    stages.put(Stage.fetchMetaGlobal,         new FetchMetaGlobalStage());
   1.186 +    stages.put(Stage.ensureKeysStage,         new EnsureCrypto5KeysStage());
   1.187 +    stages.put(Stage.syncClientsEngine,       new SyncClientsEngineStage());
   1.188 +
   1.189 +    stages.put(Stage.syncTabs,                new FennecTabsServerSyncStage());
   1.190 +    stages.put(Stage.syncPasswords,           new PasswordsServerSyncStage());
   1.191 +    stages.put(Stage.syncBookmarks,           new AndroidBrowserBookmarksServerSyncStage());
   1.192 +    stages.put(Stage.syncHistory,             new AndroidBrowserHistoryServerSyncStage());
   1.193 +    stages.put(Stage.syncFormHistory,         new FormHistoryServerSyncStage());
   1.194 +
   1.195 +    stages.put(Stage.uploadMetaGlobal,        new UploadMetaGlobalStage());
   1.196 +    stages.put(Stage.completed,               new CompletedStage());
   1.197 +
   1.198 +    this.stages = Collections.unmodifiableMap(stages);
   1.199 +  }
   1.200 +
   1.201 +  public GlobalSyncStage getSyncStageByName(String name) throws NoSuchStageException {
   1.202 +    return getSyncStageByName(Stage.byName(name));
   1.203 +  }
   1.204 +
   1.205 +  public GlobalSyncStage getSyncStageByName(Stage next) throws NoSuchStageException {
   1.206 +    GlobalSyncStage stage = stages.get(next);
   1.207 +    if (stage == null) {
   1.208 +      throw new NoSuchStageException(next);
   1.209 +    }
   1.210 +    return stage;
   1.211 +  }
   1.212 +
   1.213 +  public Collection<GlobalSyncStage> getSyncStagesByEnum(Collection<Stage> enums) {
   1.214 +    ArrayList<GlobalSyncStage> out = new ArrayList<GlobalSyncStage>();
   1.215 +    for (Stage name : enums) {
   1.216 +      try {
   1.217 +        GlobalSyncStage stage = this.getSyncStageByName(name);
   1.218 +        out.add(stage);
   1.219 +      } catch (NoSuchStageException e) {
   1.220 +        Logger.warn(LOG_TAG, "Unable to find stage with name " + name);
   1.221 +      }
   1.222 +    }
   1.223 +    return out;
   1.224 +  }
   1.225 +
   1.226 +  public Collection<GlobalSyncStage> getSyncStagesByName(Collection<String> names) {
   1.227 +    ArrayList<GlobalSyncStage> out = new ArrayList<GlobalSyncStage>();
   1.228 +    for (String name : names) {
   1.229 +      try {
   1.230 +        GlobalSyncStage stage = this.getSyncStageByName(name);
   1.231 +        out.add(stage);
   1.232 +      } catch (NoSuchStageException e) {
   1.233 +        Logger.warn(LOG_TAG, "Unable to find stage with name " + name);
   1.234 +      }
   1.235 +    }
   1.236 +    return out;
   1.237 +  }
   1.238 +
   1.239 +  /**
   1.240 +   * Advance and loop around the stages of a sync.
   1.241 +   * @param current
   1.242 +   * @return
   1.243 +   *        The next stage to execute.
   1.244 +   */
   1.245 +  public static Stage nextStage(Stage current) {
   1.246 +    int index = current.ordinal() + 1;
   1.247 +    int max   = Stage.completed.ordinal() + 1;
   1.248 +    return Stage.values()[index % max];
   1.249 +  }
   1.250 +
   1.251 +  /**
   1.252 +   * Move to the next stage in the syncing process.
   1.253 +   */
   1.254 +  public void advance() {
   1.255 +    // If we have a backoff, request a backoff and don't advance to next stage.
   1.256 +    long existingBackoff = largestBackoffObserved.get();
   1.257 +    if (existingBackoff > 0) {
   1.258 +      this.abort(null, "Aborting sync because of backoff of " + existingBackoff + " milliseconds.");
   1.259 +      return;
   1.260 +    }
   1.261 +
   1.262 +    this.callback.handleStageCompleted(this.currentState, this);
   1.263 +    Stage next = nextStage(this.currentState);
   1.264 +    GlobalSyncStage nextStage;
   1.265 +    try {
   1.266 +      nextStage = this.getSyncStageByName(next);
   1.267 +    } catch (NoSuchStageException e) {
   1.268 +      this.abort(e, "No such stage " + next);
   1.269 +      return;
   1.270 +    }
   1.271 +    this.currentState = next;
   1.272 +    Logger.info(LOG_TAG, "Running next stage " + next + " (" + nextStage + ")...");
   1.273 +    try {
   1.274 +      nextStage.execute(this);
   1.275 +    } catch (Exception ex) {
   1.276 +      Logger.warn(LOG_TAG, "Caught exception " + ex + " running stage " + next);
   1.277 +      this.abort(ex, "Uncaught exception in stage.");
   1.278 +      return;
   1.279 +    }
   1.280 +  }
   1.281 +
   1.282 +  public Context getContext() {
   1.283 +    return this.context;
   1.284 +  }
   1.285 +
   1.286 +  /**
   1.287 +   * Begin a sync.
   1.288 +   * <p>
   1.289 +   * The caller is responsible for:
   1.290 +   * <ul>
   1.291 +   * <li>Verifying that any backoffs/minimum next sync requests are respected.</li>
   1.292 +   * <li>Ensuring that the device is online.</li>
   1.293 +   * <li>Ensuring that dependencies are ready.</li>
   1.294 +   * </ul>
   1.295 +   *
   1.296 +   * @throws AlreadySyncingException
   1.297 +   */
   1.298 +  public void start() throws AlreadySyncingException {
   1.299 +    if (this.currentState != GlobalSyncStage.Stage.idle) {
   1.300 +      throw new AlreadySyncingException(this.currentState);
   1.301 +    }
   1.302 +    installAsHttpResponseObserver(); // Uninstalled by completeSync or abort.
   1.303 +    this.advance();
   1.304 +  }
   1.305 +
   1.306 +  /**
   1.307 +   * Stop this sync and start again.
   1.308 +   * @throws AlreadySyncingException
   1.309 +   */
   1.310 +  protected void restart() throws AlreadySyncingException {
   1.311 +    this.currentState = GlobalSyncStage.Stage.idle;
   1.312 +    if (callback.shouldBackOffStorage()) {
   1.313 +      this.callback.handleAborted(this, "Told to back off.");
   1.314 +      return;
   1.315 +    }
   1.316 +    this.start();
   1.317 +  }
   1.318 +
   1.319 +  /**
   1.320 +   * We're finished (aborted or succeeded): release resources.
   1.321 +   */
   1.322 +  protected void cleanUp() {
   1.323 +    uninstallAsHttpResponseObserver();
   1.324 +    this.stages = null;
   1.325 +  }
   1.326 +
   1.327 +  public void completeSync() {
   1.328 +    cleanUp();
   1.329 +    this.currentState = GlobalSyncStage.Stage.idle;
   1.330 +    this.callback.handleSuccess(this);
   1.331 +  }
   1.332 +
   1.333 +  /**
   1.334 +   * Record that an updated meta/global record should be uploaded with the given
   1.335 +   * settings for the given engine.
   1.336 +   *
   1.337 +   * @param engineName engine to update.
   1.338 +   * @param engineSettings new syncID and version.
   1.339 +   */
   1.340 +  public void recordForMetaGlobalUpdate(String engineName, EngineSettings engineSettings) {
   1.341 +    enginesToUpdate.put(engineName, engineSettings);
   1.342 +  }
   1.343 +
   1.344 +  /**
   1.345 +   * Record that an updated meta/global record should be uploaded without the
   1.346 +   * given engine name.
   1.347 +   *
   1.348 +   * @param engineName
   1.349 +   *          engine to remove.
   1.350 +   */
   1.351 +  public void removeEngineFromMetaGlobal(String engineName) {
   1.352 +    enginesToUpdate.put(engineName, null);
   1.353 +  }
   1.354 +
   1.355 +  public boolean hasUpdatedMetaGlobal() {
   1.356 +    if (enginesToUpdate.isEmpty()) {
   1.357 +      Logger.info(LOG_TAG, "Not uploading updated meta/global record since there are no engines requesting upload.");
   1.358 +      return false;
   1.359 +    }
   1.360 +
   1.361 +    if (Logger.shouldLogVerbose(LOG_TAG)) {
   1.362 +      Logger.trace(LOG_TAG, "Uploading updated meta/global record since there are engine changes to meta/global.");
   1.363 +      Logger.trace(LOG_TAG, "Engines requesting update [" + Utils.toCommaSeparatedString(enginesToUpdate.keySet()) + "]");
   1.364 +    }
   1.365 +
   1.366 +    return true;
   1.367 +  }
   1.368 +
   1.369 +  public void updateMetaGlobalInPlace() {
   1.370 +    config.metaGlobal.declined = this.declinedEngineNames();
   1.371 +    ExtendedJSONObject engines = config.metaGlobal.getEngines();
   1.372 +    for (Entry<String, EngineSettings> pair : enginesToUpdate.entrySet()) {
   1.373 +      if (pair.getValue() == null) {
   1.374 +        engines.remove(pair.getKey());
   1.375 +      } else {
   1.376 +        engines.put(pair.getKey(), pair.getValue().toJSONObject());
   1.377 +      }
   1.378 +    }
   1.379 +
   1.380 +    enginesToUpdate.clear();
   1.381 +  }
   1.382 +
   1.383 +  /**
   1.384 +   * Synchronously upload an updated meta/global.
   1.385 +   * <p>
   1.386 +   * All problems are logged and ignored.
   1.387 +   */
   1.388 +  public void uploadUpdatedMetaGlobal() {
   1.389 +    updateMetaGlobalInPlace();
   1.390 +
   1.391 +    Logger.debug(LOG_TAG, "Uploading updated meta/global record.");
   1.392 +    final Object monitor = new Object();
   1.393 +
   1.394 +    Runnable doUpload = new Runnable() {
   1.395 +      @Override
   1.396 +      public void run() {
   1.397 +        config.metaGlobal.upload(new MetaGlobalDelegate() {
   1.398 +          @Override
   1.399 +          public void handleSuccess(MetaGlobal global, SyncStorageResponse response) {
   1.400 +            Logger.info(LOG_TAG, "Successfully uploaded updated meta/global record.");
   1.401 +            // Engine changes are stored as diffs, so update enabled engines in config to match uploaded meta/global.
   1.402 +            config.enabledEngineNames = config.metaGlobal.getEnabledEngineNames();
   1.403 +            // Clear userSelectedEngines because they are updated in config and meta/global.
   1.404 +            config.userSelectedEngines = null;
   1.405 +
   1.406 +            synchronized (monitor) {
   1.407 +              monitor.notify();
   1.408 +            }
   1.409 +          }
   1.410 +
   1.411 +          @Override
   1.412 +          public void handleMissing(MetaGlobal global, SyncStorageResponse response) {
   1.413 +            Logger.warn(LOG_TAG, "Got 404 missing uploading updated meta/global record; shouldn't happen.  Ignoring.");
   1.414 +            synchronized (monitor) {
   1.415 +              monitor.notify();
   1.416 +            }
   1.417 +          }
   1.418 +
   1.419 +          @Override
   1.420 +          public void handleFailure(SyncStorageResponse response) {
   1.421 +            Logger.warn(LOG_TAG, "Failed to upload updated meta/global record; ignoring.");
   1.422 +            synchronized (monitor) {
   1.423 +              monitor.notify();
   1.424 +            }
   1.425 +          }
   1.426 +
   1.427 +          @Override
   1.428 +          public void handleError(Exception e) {
   1.429 +            Logger.warn(LOG_TAG, "Got exception trying to upload updated meta/global record; ignoring.", e);
   1.430 +            synchronized (monitor) {
   1.431 +              monitor.notify();
   1.432 +            }
   1.433 +          }
   1.434 +        });
   1.435 +      }
   1.436 +    };
   1.437 +
   1.438 +    final Thread upload = new Thread(doUpload);
   1.439 +    synchronized (monitor) {
   1.440 +      try {
   1.441 +        upload.start();
   1.442 +        monitor.wait();
   1.443 +        Logger.debug(LOG_TAG, "Uploaded updated meta/global record.");
   1.444 +      } catch (InterruptedException e) {
   1.445 +        Logger.error(LOG_TAG, "Uploading updated meta/global interrupted; continuing.");
   1.446 +      }
   1.447 +    }
   1.448 +  }
   1.449 +
   1.450 +
   1.451 +  public void abort(Exception e, String reason) {
   1.452 +    Logger.warn(LOG_TAG, "Aborting sync: " + reason, e);
   1.453 +    cleanUp();
   1.454 +    long existingBackoff = largestBackoffObserved.get();
   1.455 +    if (existingBackoff > 0) {
   1.456 +      callback.requestBackoff(existingBackoff);
   1.457 +    }
   1.458 +    if (!(e instanceof HTTPFailureException)) {
   1.459 +      //  e is null, or we aborted for a non-HTTP reason; okay to upload new meta/global record.
   1.460 +      if (this.hasUpdatedMetaGlobal()) {
   1.461 +        this.uploadUpdatedMetaGlobal(); // Only logs errors; does not call abort.
   1.462 +      }
   1.463 +    }
   1.464 +    this.callback.handleError(this, e);
   1.465 +  }
   1.466 +
   1.467 +  public void handleHTTPError(SyncStorageResponse response, String reason) {
   1.468 +    // TODO: handling of 50x (backoff), 401 (node reassignment or auth error).
   1.469 +    // Fall back to aborting.
   1.470 +    Logger.warn(LOG_TAG, "Aborting sync due to HTTP " + response.getStatusCode());
   1.471 +    this.interpretHTTPFailure(response.httpResponse());
   1.472 +    this.abort(new HTTPFailureException(response), reason);
   1.473 +  }
   1.474 +
   1.475 +  /**
   1.476 +   * Perform appropriate backoff etc. extraction.
   1.477 +   */
   1.478 +  public void interpretHTTPFailure(HttpResponse response) {
   1.479 +    // TODO: handle permanent rejection.
   1.480 +    long responseBackoff = (new SyncResponse(response)).totalBackoffInMilliseconds();
   1.481 +    if (responseBackoff > 0) {
   1.482 +      callback.requestBackoff(responseBackoff);
   1.483 +    }
   1.484 +
   1.485 +    if (response.getStatusLine() != null) {
   1.486 +      final int statusCode = response.getStatusLine().getStatusCode();
   1.487 +      switch(statusCode) {
   1.488 +
   1.489 +      case 400:
   1.490 +        SyncStorageResponse storageResponse = new SyncStorageResponse(response);
   1.491 +        this.interpretHTTPBadRequestBody(storageResponse);
   1.492 +        break;
   1.493 +
   1.494 +      case 401:
   1.495 +        /*
   1.496 +         * Alert our callback we have a 401 on a cluster URL. This GlobalSession
   1.497 +         * will fail, but the next one will fetch a new cluster URL and will
   1.498 +         * distinguish between "node reassignment" and "user password changed".
   1.499 +         */
   1.500 +        callback.informUnauthorizedResponse(this, config.getClusterURL());
   1.501 +        break;
   1.502 +      }
   1.503 +    }
   1.504 +  }
   1.505 +
   1.506 +  protected void interpretHTTPBadRequestBody(final SyncStorageResponse storageResponse) {
   1.507 +    try {
   1.508 +      final String body = storageResponse.body();
   1.509 +      if (body == null) {
   1.510 +        return;
   1.511 +      }
   1.512 +      if (SyncStorageResponse.RESPONSE_CLIENT_UPGRADE_REQUIRED.equals(body)) {
   1.513 +        callback.informUpgradeRequiredResponse(this);
   1.514 +        return;
   1.515 +      }
   1.516 +    } catch (Exception e) {
   1.517 +      Logger.warn(LOG_TAG, "Exception parsing HTTP 400 body.", e);
   1.518 +    }
   1.519 +  }
   1.520 +
   1.521 +  public void fetchInfoCollections(JSONRecordFetchDelegate callback) throws URISyntaxException {
   1.522 +    final JSONRecordFetcher fetcher = new JSONRecordFetcher(config.infoCollectionsURL(), getAuthHeaderProvider());
   1.523 +    fetcher.fetch(callback);
   1.524 +  }
   1.525 +
   1.526 +  /**
   1.527 +   * Upload new crypto/keys.
   1.528 +   *
   1.529 +   * @param keys
   1.530 +   *          new keys.
   1.531 +   * @param keyUploadDelegate
   1.532 +   *          a delegate.
   1.533 +   */
   1.534 +  public void uploadKeys(final CollectionKeys keys,
   1.535 +                         final KeyUploadDelegate keyUploadDelegate) {
   1.536 +    SyncStorageRecordRequest request;
   1.537 +    try {
   1.538 +      request = new SyncStorageRecordRequest(this.config.keysURI());
   1.539 +    } catch (URISyntaxException e) {
   1.540 +      keyUploadDelegate.onKeyUploadFailed(e);
   1.541 +      return;
   1.542 +    }
   1.543 +
   1.544 +    request.delegate = new SyncStorageRequestDelegate() {
   1.545 +
   1.546 +      @Override
   1.547 +      public String ifUnmodifiedSince() {
   1.548 +        return null;
   1.549 +      }
   1.550 +
   1.551 +      @Override
   1.552 +      public void handleRequestSuccess(SyncStorageResponse response) {
   1.553 +        Logger.debug(LOG_TAG, "Keys uploaded.");
   1.554 +        BaseResource.consumeEntity(response); // We don't need the response at all.
   1.555 +        keyUploadDelegate.onKeysUploaded();
   1.556 +      }
   1.557 +
   1.558 +      @Override
   1.559 +      public void handleRequestFailure(SyncStorageResponse response) {
   1.560 +        Logger.debug(LOG_TAG, "Failed to upload keys.");
   1.561 +        GlobalSession.this.interpretHTTPFailure(response.httpResponse());
   1.562 +        BaseResource.consumeEntity(response); // The exception thrown should not need the body of the response.
   1.563 +        keyUploadDelegate.onKeyUploadFailed(new HTTPFailureException(response));
   1.564 +      }
   1.565 +
   1.566 +      @Override
   1.567 +      public void handleRequestError(Exception ex) {
   1.568 +        Logger.warn(LOG_TAG, "Got exception trying to upload keys", ex);
   1.569 +        keyUploadDelegate.onKeyUploadFailed(ex);
   1.570 +      }
   1.571 +
   1.572 +      @Override
   1.573 +      public AuthHeaderProvider getAuthHeaderProvider() {
   1.574 +        return GlobalSession.this.getAuthHeaderProvider();
   1.575 +      }
   1.576 +    };
   1.577 +
   1.578 +    // Convert keys to an encrypted crypto record.
   1.579 +    CryptoRecord keysRecord;
   1.580 +    try {
   1.581 +      keysRecord = keys.asCryptoRecord();
   1.582 +      keysRecord.setKeyBundle(config.syncKeyBundle);
   1.583 +      keysRecord.encrypt();
   1.584 +    } catch (Exception e) {
   1.585 +      Logger.warn(LOG_TAG, "Got exception trying creating crypto record from keys", e);
   1.586 +      keyUploadDelegate.onKeyUploadFailed(e);
   1.587 +      return;
   1.588 +    }
   1.589 +
   1.590 +    request.put(keysRecord);
   1.591 +  }
   1.592 +
   1.593 +  /*
   1.594 +   * meta/global callbacks.
   1.595 +   */
   1.596 +  public void processMetaGlobal(MetaGlobal global) {
   1.597 +    config.metaGlobal = global;
   1.598 +
   1.599 +    Long storageVersion = global.getStorageVersion();
   1.600 +    if (storageVersion == null) {
   1.601 +      Logger.warn(LOG_TAG, "Malformed remote meta/global: could not retrieve remote storage version.");
   1.602 +      freshStart();
   1.603 +      return;
   1.604 +    }
   1.605 +    if (storageVersion < STORAGE_VERSION) {
   1.606 +      Logger.warn(LOG_TAG, "Outdated server: reported " +
   1.607 +          "remote storage version " + storageVersion + " < " +
   1.608 +          "local storage version " + STORAGE_VERSION);
   1.609 +      freshStart();
   1.610 +      return;
   1.611 +    }
   1.612 +    if (storageVersion > STORAGE_VERSION) {
   1.613 +      Logger.warn(LOG_TAG, "Outdated client: reported " +
   1.614 +          "remote storage version " + storageVersion + " > " +
   1.615 +          "local storage version " + STORAGE_VERSION);
   1.616 +      requiresUpgrade();
   1.617 +      return;
   1.618 +    }
   1.619 +    String remoteSyncID = global.getSyncID();
   1.620 +    if (remoteSyncID == null) {
   1.621 +      Logger.warn(LOG_TAG, "Malformed remote meta/global: could not retrieve remote syncID.");
   1.622 +      freshStart();
   1.623 +      return;
   1.624 +    }
   1.625 +    String localSyncID = config.syncID;
   1.626 +    if (!remoteSyncID.equals(localSyncID)) {
   1.627 +      Logger.warn(LOG_TAG, "Remote syncID different from local syncID: resetting client and assuming remote syncID.");
   1.628 +      resetAllStages();
   1.629 +      config.purgeCryptoKeys();
   1.630 +      config.syncID = remoteSyncID;
   1.631 +    }
   1.632 +    // Compare lastModified timestamps for remote/local engine selection times.
   1.633 +    Logger.debug(LOG_TAG, "Comparing local engine selection timestamp [" + config.userSelectedEnginesTimestamp + "] to server meta/global timestamp [" + config.persistedMetaGlobal().lastModified() + "].");
   1.634 +    if (config.userSelectedEnginesTimestamp < config.persistedMetaGlobal().lastModified()) {
   1.635 +      // Remote has later meta/global timestamp. Don't upload engine changes.
   1.636 +      config.userSelectedEngines = null;
   1.637 +    }
   1.638 +    // Persist enabled engine names.
   1.639 +    config.enabledEngineNames = global.getEnabledEngineNames();
   1.640 +    if (config.enabledEngineNames == null) {
   1.641 +      Logger.warn(LOG_TAG, "meta/global reported no enabled engine names!");
   1.642 +    } else {
   1.643 +      if (Logger.shouldLogVerbose(LOG_TAG)) {
   1.644 +        Logger.trace(LOG_TAG, "Persisting enabled engine names '" +
   1.645 +            Utils.toCommaSeparatedString(config.enabledEngineNames) + "' from meta/global.");
   1.646 +      }
   1.647 +    }
   1.648 +
   1.649 +    // Persist declined.
   1.650 +    // Our declined engines at any point are:
   1.651 +    // Whatever they were remotely, plus whatever they were locally, less any
   1.652 +    // engines that were just enabled locally or remotely.
   1.653 +    // If remote just 'won', our recently enabled list just got cleared.
   1.654 +    final HashSet<String> allDeclined = new HashSet<String>();
   1.655 +
   1.656 +    final Set<String> newRemoteDeclined = global.getDeclinedEngineNames();
   1.657 +    final Set<String> oldLocalDeclined = config.declinedEngineNames;
   1.658 +
   1.659 +    allDeclined.addAll(newRemoteDeclined);
   1.660 +    allDeclined.addAll(oldLocalDeclined);
   1.661 +
   1.662 +    if (config.userSelectedEngines != null) {
   1.663 +      for (Entry<String, Boolean> selection : config.userSelectedEngines.entrySet()) {
   1.664 +        if (selection.getValue()) {
   1.665 +          allDeclined.remove(selection.getKey());
   1.666 +        }
   1.667 +      }
   1.668 +    }
   1.669 +
   1.670 +    config.declinedEngineNames = allDeclined;
   1.671 +    if (config.declinedEngineNames.isEmpty()) {
   1.672 +      Logger.debug(LOG_TAG, "meta/global reported no declined engine names, and we have none declined locally.");
   1.673 +    } else {
   1.674 +      if (Logger.shouldLogVerbose(LOG_TAG)) {
   1.675 +        Logger.trace(LOG_TAG, "Persisting declined engine names '" +
   1.676 +            Utils.toCommaSeparatedString(config.declinedEngineNames) + "' from meta/global.");
   1.677 +      }
   1.678 +    }
   1.679 +
   1.680 +    config.persistToPrefs();
   1.681 +    advance();
   1.682 +  }
   1.683 +
   1.684 +  public void processMissingMetaGlobal(MetaGlobal global) {
   1.685 +    freshStart();
   1.686 +  }
   1.687 +
   1.688 +  /**
   1.689 +   * Do a fresh start then quietly finish the sync, starting another.
   1.690 +   */
   1.691 +  public void freshStart() {
   1.692 +    final GlobalSession globalSession = this;
   1.693 +    freshStart(this, new FreshStartDelegate() {
   1.694 +
   1.695 +      @Override
   1.696 +      public void onFreshStartFailed(Exception e) {
   1.697 +        globalSession.abort(e, "Fresh start failed.");
   1.698 +      }
   1.699 +
   1.700 +      @Override
   1.701 +      public void onFreshStart() {
   1.702 +        try {
   1.703 +          Logger.warn(LOG_TAG, "Fresh start succeeded; restarting global session.");
   1.704 +          globalSession.config.persistToPrefs();
   1.705 +          globalSession.restart();
   1.706 +        } catch (Exception e) {
   1.707 +          Logger.warn(LOG_TAG, "Got exception when restarting sync after freshStart.", e);
   1.708 +          globalSession.abort(e, "Got exception after freshStart.");
   1.709 +        }
   1.710 +      }
   1.711 +    });
   1.712 +  }
   1.713 +
   1.714 +  /**
   1.715 +   * Clean the server, aborting the current sync.
   1.716 +   * <p>
   1.717 +   * <ol>
   1.718 +   * <li>Wipe the server storage.</li>
   1.719 +   * <li>Reset all stages and purge cached state: (meta/global and crypto/keys records).</li>
   1.720 +   * <li>Upload fresh meta/global record.</li>
   1.721 +   * <li>Upload fresh crypto/keys record.</li>
   1.722 +   * <li>Restart the sync entirely in order to re-download meta/global and crypto/keys record.</li>
   1.723 +   * </ol>
   1.724 +   * @param session the current session.
   1.725 +   * @param freshStartDelegate delegate to notify on fresh start or failure.
   1.726 +   */
   1.727 +  protected static void freshStart(final GlobalSession session, final FreshStartDelegate freshStartDelegate) {
   1.728 +    Logger.debug(LOG_TAG, "Fresh starting.");
   1.729 +
   1.730 +    final MetaGlobal mg = session.generateNewMetaGlobal();
   1.731 +
   1.732 +    session.wipeServer(session.getAuthHeaderProvider(), new WipeServerDelegate() {
   1.733 +
   1.734 +      @Override
   1.735 +      public void onWiped(long timestamp) {
   1.736 +        Logger.debug(LOG_TAG, "Successfully wiped server.  Resetting all stages and purging cached meta/global and crypto/keys records.");
   1.737 +
   1.738 +        session.resetAllStages();
   1.739 +        session.config.purgeMetaGlobal();
   1.740 +        session.config.purgeCryptoKeys();
   1.741 +        session.config.persistToPrefs();
   1.742 +
   1.743 +        Logger.info(LOG_TAG, "Uploading new meta/global with sync ID " + mg.syncID + ".");
   1.744 +
   1.745 +        // It would be good to set the X-If-Unmodified-Since header to `timestamp`
   1.746 +        // for this PUT to ensure at least some level of transactionality.
   1.747 +        // Unfortunately, the servers don't support it after a wipe right now
   1.748 +        // (bug 693893), so we're going to defer this until bug 692700.
   1.749 +        mg.upload(new MetaGlobalDelegate() {
   1.750 +          @Override
   1.751 +          public void handleSuccess(MetaGlobal uploadedGlobal, SyncStorageResponse uploadResponse) {
   1.752 +            Logger.info(LOG_TAG, "Uploaded new meta/global with sync ID " + uploadedGlobal.syncID + ".");
   1.753 +
   1.754 +            // Generate new keys.
   1.755 +            CollectionKeys keys = null;
   1.756 +            try {
   1.757 +              keys = session.generateNewCryptoKeys();
   1.758 +            } catch (CryptoException e) {
   1.759 +              Logger.warn(LOG_TAG, "Got exception generating new keys; failing fresh start.", e);
   1.760 +              freshStartDelegate.onFreshStartFailed(e);
   1.761 +            }
   1.762 +            if (keys == null) {
   1.763 +              Logger.warn(LOG_TAG, "Got null keys from generateNewKeys; failing fresh start.");
   1.764 +              freshStartDelegate.onFreshStartFailed(null);
   1.765 +            }
   1.766 +
   1.767 +            // Upload new keys.
   1.768 +            Logger.info(LOG_TAG, "Uploading new crypto/keys.");
   1.769 +            session.uploadKeys(keys, new KeyUploadDelegate() {
   1.770 +              @Override
   1.771 +              public void onKeysUploaded() {
   1.772 +                Logger.info(LOG_TAG, "Uploaded new crypto/keys.");
   1.773 +                freshStartDelegate.onFreshStart();
   1.774 +              }
   1.775 +
   1.776 +              @Override
   1.777 +              public void onKeyUploadFailed(Exception e) {
   1.778 +                Logger.warn(LOG_TAG, "Got exception uploading new keys.", e);
   1.779 +                freshStartDelegate.onFreshStartFailed(e);
   1.780 +              }
   1.781 +            });
   1.782 +          }
   1.783 +
   1.784 +          @Override
   1.785 +          public void handleMissing(MetaGlobal global, SyncStorageResponse response) {
   1.786 +            // Shouldn't happen on upload.
   1.787 +            Logger.warn(LOG_TAG, "Got 'missing' response uploading new meta/global.");
   1.788 +            freshStartDelegate.onFreshStartFailed(new Exception("meta/global missing while uploading."));
   1.789 +          }
   1.790 +
   1.791 +          @Override
   1.792 +          public void handleFailure(SyncStorageResponse response) {
   1.793 +            Logger.warn(LOG_TAG, "Got failure " + response.getStatusCode() + " uploading new meta/global.");
   1.794 +            session.interpretHTTPFailure(response.httpResponse());
   1.795 +            freshStartDelegate.onFreshStartFailed(new HTTPFailureException(response));
   1.796 +          }
   1.797 +
   1.798 +          @Override
   1.799 +          public void handleError(Exception e) {
   1.800 +            Logger.warn(LOG_TAG, "Got error uploading new meta/global.", e);
   1.801 +            freshStartDelegate.onFreshStartFailed(e);
   1.802 +          }
   1.803 +        });
   1.804 +      }
   1.805 +
   1.806 +      @Override
   1.807 +      public void onWipeFailed(Exception e) {
   1.808 +        Logger.warn(LOG_TAG, "Wipe failed.");
   1.809 +        freshStartDelegate.onFreshStartFailed(e);
   1.810 +      }
   1.811 +    });
   1.812 +  }
   1.813 +
   1.814 +  // Note that we do not yet implement wipeRemote: it's only necessary for
   1.815 +  // first sync options.
   1.816 +  // -- reset local stages, wipe server for each stage *except* clients
   1.817 +  //    (stages only, not whole server!), send wipeEngine commands to each client.
   1.818 +  //
   1.819 +  // Similarly for startOver (because we don't receive that notification).
   1.820 +  // -- remove client data from server, reset local stages, clear keys, reset
   1.821 +  //    backoff, clear all prefs, discard credentials.
   1.822 +  //
   1.823 +  // Change passphrase: wipe entire server, reset client to force upload, sync.
   1.824 +  //
   1.825 +  // When an engine is disabled: wipe its collections on the server, reupload
   1.826 +  // meta/global.
   1.827 +  //
   1.828 +  // On syncing each stage: if server has engine version 0 or old, wipe server,
   1.829 +  // reset client to prompt reupload.
   1.830 +  // If sync ID mismatch: take that syncID and reset client.
   1.831 +
   1.832 +  protected void wipeServer(final AuthHeaderProvider authHeaderProvider, final WipeServerDelegate wipeDelegate) {
   1.833 +    SyncStorageRequest request;
   1.834 +    final GlobalSession self = this;
   1.835 +
   1.836 +    try {
   1.837 +      request = new SyncStorageRequest(config.storageURL());
   1.838 +    } catch (URISyntaxException ex) {
   1.839 +      Logger.warn(LOG_TAG, "Invalid URI in wipeServer.");
   1.840 +      wipeDelegate.onWipeFailed(ex);
   1.841 +      return;
   1.842 +    }
   1.843 +
   1.844 +    request.delegate = new SyncStorageRequestDelegate() {
   1.845 +
   1.846 +      @Override
   1.847 +      public String ifUnmodifiedSince() {
   1.848 +        return null;
   1.849 +      }
   1.850 +
   1.851 +      @Override
   1.852 +      public void handleRequestSuccess(SyncStorageResponse response) {
   1.853 +        BaseResource.consumeEntity(response);
   1.854 +        wipeDelegate.onWiped(response.normalizedWeaveTimestamp());
   1.855 +      }
   1.856 +
   1.857 +      @Override
   1.858 +      public void handleRequestFailure(SyncStorageResponse response) {
   1.859 +        Logger.warn(LOG_TAG, "Got request failure " + response.getStatusCode() + " in wipeServer.");
   1.860 +        // Process HTTP failures here to pick up backoffs, etc.
   1.861 +        self.interpretHTTPFailure(response.httpResponse());
   1.862 +        BaseResource.consumeEntity(response); // The exception thrown should not need the body of the response.
   1.863 +        wipeDelegate.onWipeFailed(new HTTPFailureException(response));
   1.864 +      }
   1.865 +
   1.866 +      @Override
   1.867 +      public void handleRequestError(Exception ex) {
   1.868 +        Logger.warn(LOG_TAG, "Got exception in wipeServer.", ex);
   1.869 +        wipeDelegate.onWipeFailed(ex);
   1.870 +      }
   1.871 +
   1.872 +      @Override
   1.873 +      public AuthHeaderProvider getAuthHeaderProvider() {
   1.874 +        return GlobalSession.this.getAuthHeaderProvider();
   1.875 +      }
   1.876 +    };
   1.877 +    request.delete();
   1.878 +  }
   1.879 +
   1.880 +  public void wipeAllStages() {
   1.881 +    Logger.info(LOG_TAG, "Wiping all stages.");
   1.882 +    // Includes "clients".
   1.883 +    this.wipeStagesByEnum(Stage.getNamedStages());
   1.884 +  }
   1.885 +
   1.886 +  public void wipeStages(Collection<GlobalSyncStage> stages) {
   1.887 +    for (GlobalSyncStage stage : stages) {
   1.888 +      try {
   1.889 +        Logger.info(LOG_TAG, "Wiping " + stage);
   1.890 +        stage.wipeLocal(this);
   1.891 +      } catch (Exception e) {
   1.892 +        Logger.error(LOG_TAG, "Ignoring wipe failure for stage " + stage, e);
   1.893 +      }
   1.894 +    }
   1.895 +  }
   1.896 +
   1.897 +  public void wipeStagesByEnum(Collection<Stage> stages) {
   1.898 +    wipeStages(this.getSyncStagesByEnum(stages));
   1.899 +  }
   1.900 +
   1.901 +  public void wipeStagesByName(Collection<String> names) {
   1.902 +    wipeStages(this.getSyncStagesByName(names));
   1.903 +  }
   1.904 +
   1.905 +  public void resetAllStages() {
   1.906 +    Logger.info(LOG_TAG, "Resetting all stages.");
   1.907 +    // Includes "clients".
   1.908 +    this.resetStagesByEnum(Stage.getNamedStages());
   1.909 +  }
   1.910 +
   1.911 +  public void resetStages(Collection<GlobalSyncStage> stages) {
   1.912 +    for (GlobalSyncStage stage : stages) {
   1.913 +      try {
   1.914 +        Logger.info(LOG_TAG, "Resetting " + stage);
   1.915 +        stage.resetLocal(this);
   1.916 +      } catch (Exception e) {
   1.917 +        Logger.error(LOG_TAG, "Ignoring reset failure for stage " + stage, e);
   1.918 +      }
   1.919 +    }
   1.920 +  }
   1.921 +
   1.922 +  public void resetStagesByEnum(Collection<Stage> stages) {
   1.923 +    resetStages(this.getSyncStagesByEnum(stages));
   1.924 +  }
   1.925 +
   1.926 +  public void resetStagesByName(Collection<String> names) {
   1.927 +    resetStages(this.getSyncStagesByName(names));
   1.928 +  }
   1.929 +
   1.930 +  /**
   1.931 +   * Engines to explicitly mark as declined in a fresh meta/global record.
   1.932 +   * <p>
   1.933 +   * Returns an empty array if the user hasn't elected to customize data types,
   1.934 +   * or an array of engines that the user un-checked during customization.
   1.935 +   * <p>
   1.936 +   * Engines that Android Sync doesn't recognize are <b>not</b> included in
   1.937 +   * the returned array.
   1.938 +   *
   1.939 +   * @return a new JSONArray of engine names.
   1.940 +   */
   1.941 +  @SuppressWarnings("unchecked")
   1.942 +  protected JSONArray declinedEngineNames() {
   1.943 +    final JSONArray declined = new JSONArray();
   1.944 +    for (String engine : config.declinedEngineNames) {
   1.945 +      declined.add(engine);
   1.946 +    };
   1.947 +
   1.948 +    return declined;
   1.949 +  }
   1.950 +
   1.951 +  /**
   1.952 +   * Engines to include in a fresh meta/global record.
   1.953 +   * <p>
   1.954 +   * Returns either the persisted engine names (perhaps we have been node
   1.955 +   * re-assigned and are initializing a clean server: we want to upload the
   1.956 +   * persisted engine names so that we don't accidentally disable engines that
   1.957 +   * Android Sync doesn't recognize), or the set of engines names that Android
   1.958 +   * Sync implements.
   1.959 +   *
   1.960 +   * @return set of engine names.
   1.961 +   */
   1.962 +  protected Set<String> enabledEngineNames() {
   1.963 +    if (config.enabledEngineNames != null) {
   1.964 +      return config.enabledEngineNames;
   1.965 +    }
   1.966 +
   1.967 +    // These are the default set of engine names.
   1.968 +    Set<String> validEngineNames = SyncConfiguration.validEngineNames();
   1.969 +
   1.970 +    // If the user hasn't set any selected engines, that's okay -- default to
   1.971 +    // everything.
   1.972 +    if (config.userSelectedEngines == null) {
   1.973 +      return validEngineNames;
   1.974 +    }
   1.975 +
   1.976 +    // userSelectedEngines has keys that are engine names, and boolean values
   1.977 +    // corresponding to whether the user asked for the engine to sync or not. If
   1.978 +    // an engine is not present, that means the user didn't change its sync
   1.979 +    // setting. Since we default to everything on, that means the user didn't
   1.980 +    // turn it off; therefore, it's included in the set of engines to sync.
   1.981 +    Set<String> validAndSelectedEngineNames = new HashSet<String>();
   1.982 +    for (String engineName : validEngineNames) {
   1.983 +      if (config.userSelectedEngines.containsKey(engineName) &&
   1.984 +          !config.userSelectedEngines.get(engineName)) {
   1.985 +        continue;
   1.986 +      }
   1.987 +      validAndSelectedEngineNames.add(engineName);
   1.988 +    }
   1.989 +    return validAndSelectedEngineNames;
   1.990 +  }
   1.991 +
   1.992 +  /**
   1.993 +   * Generate fresh crypto/keys collection.
   1.994 +   * @return crypto/keys collection.
   1.995 +   * @throws CryptoException
   1.996 +   */
   1.997 +  @SuppressWarnings("static-method")
   1.998 +  public CollectionKeys generateNewCryptoKeys() throws CryptoException {
   1.999 +    return CollectionKeys.generateCollectionKeys();
  1.1000 +  }
  1.1001 +
  1.1002 +  /**
  1.1003 +   * Generate a fresh meta/global record.
  1.1004 +   * @return meta/global record.
  1.1005 +   */
  1.1006 +  public MetaGlobal generateNewMetaGlobal() {
  1.1007 +    final String newSyncID   = Utils.generateGuid();
  1.1008 +    final String metaURL     = this.config.metaURL();
  1.1009 +
  1.1010 +    ExtendedJSONObject engines = new ExtendedJSONObject();
  1.1011 +    for (String engineName : enabledEngineNames()) {
  1.1012 +      EngineSettings engineSettings = null;
  1.1013 +      try {
  1.1014 +        GlobalSyncStage globalStage = this.getSyncStageByName(engineName);
  1.1015 +        Integer version = globalStage.getStorageVersion();
  1.1016 +        if (version == null) {
  1.1017 +          continue; // Don't want this stage to be included in meta/global.
  1.1018 +        }
  1.1019 +        engineSettings = new EngineSettings(Utils.generateGuid(), version.intValue());
  1.1020 +      } catch (NoSuchStageException e) {
  1.1021 +        // No trouble; Android Sync might not recognize this engine yet.
  1.1022 +        // By default, version 0.  Other clients will see the 0 version and reset/wipe accordingly.
  1.1023 +        engineSettings = new EngineSettings(Utils.generateGuid(), 0);
  1.1024 +      }
  1.1025 +      engines.put(engineName, engineSettings.toJSONObject());
  1.1026 +    }
  1.1027 +
  1.1028 +    MetaGlobal metaGlobal = new MetaGlobal(metaURL, this.getAuthHeaderProvider());
  1.1029 +    metaGlobal.setSyncID(newSyncID);
  1.1030 +    metaGlobal.setStorageVersion(STORAGE_VERSION);
  1.1031 +    metaGlobal.setEngines(engines);
  1.1032 +
  1.1033 +    // We assume that the config's declined engines have been updated
  1.1034 +    // according to the user's selections.
  1.1035 +    metaGlobal.setDeclinedEngineNames(this.declinedEngineNames());
  1.1036 +
  1.1037 +    return metaGlobal;
  1.1038 +  }
  1.1039 +
  1.1040 +  /**
  1.1041 +   * Suggest that your Sync client needs to be upgraded to work
  1.1042 +   * with this server.
  1.1043 +   */
  1.1044 +  public void requiresUpgrade() {
  1.1045 +    Logger.info(LOG_TAG, "Client outdated storage version; requires update.");
  1.1046 +    // TODO: notify UI.
  1.1047 +    this.abort(null, "Requires upgrade");
  1.1048 +  }
  1.1049 +
  1.1050 +  /**
  1.1051 +   * If meta/global is missing or malformed, throws a MetaGlobalException.
  1.1052 +   * Otherwise, returns true if there is an entry for this engine in the
  1.1053 +   * meta/global "engines" object.
  1.1054 +   * <p>
  1.1055 +   * This is a global/permanent setting, not a local/temporary setting. For the
  1.1056 +   * latter, see {@link GlobalSession#isEngineLocallyEnabled(String)}.
  1.1057 +   *
  1.1058 +   * @param engineName the name to check (e.g., "bookmarks").
  1.1059 +   * @param engineSettings
  1.1060 +   *        if non-null, verify that the server engine settings are congruent
  1.1061 +   *        with this, throwing the appropriate MetaGlobalException if not.
  1.1062 +   * @return
  1.1063 +   *        true if the engine with the provided name is present in the
  1.1064 +   *        meta/global "engines" object, and verification passed.
  1.1065 +   *
  1.1066 +   * @throws MetaGlobalException
  1.1067 +   */
  1.1068 +  public boolean isEngineRemotelyEnabled(String engineName, EngineSettings engineSettings) throws MetaGlobalException {
  1.1069 +    if (this.config.metaGlobal == null) {
  1.1070 +      throw new MetaGlobalNotSetException();
  1.1071 +    }
  1.1072 +
  1.1073 +    // This should not occur.
  1.1074 +    if (this.config.enabledEngineNames == null) {
  1.1075 +      Logger.error(LOG_TAG, "No enabled engines in config. Giving up.");
  1.1076 +      throw new MetaGlobalMissingEnginesException();
  1.1077 +    }
  1.1078 +
  1.1079 +    if (!(this.config.enabledEngineNames.contains(engineName))) {
  1.1080 +      Logger.debug(LOG_TAG, "Engine " + engineName + " not enabled: no meta/global entry.");
  1.1081 +      return false;
  1.1082 +    }
  1.1083 +
  1.1084 +    // If we have a meta/global, check that it's safe for us to sync.
  1.1085 +    // (If we don't, we'll create one later, which is why we return `true` above.)
  1.1086 +    if (engineSettings != null) {
  1.1087 +      // Throws if there's a problem.
  1.1088 +      this.config.metaGlobal.verifyEngineSettings(engineName, engineSettings);
  1.1089 +    }
  1.1090 +
  1.1091 +    return true;
  1.1092 +  }
  1.1093 +
  1.1094 +
  1.1095 +  /**
  1.1096 +   * Return true if the named stage should be synced this session.
  1.1097 +   * <p>
  1.1098 +   * This is a local/temporary setting, in contrast to the meta/global record,
  1.1099 +   * which is a global/permanent setting. For the latter, see
  1.1100 +   * {@link GlobalSession#isEngineRemotelyEnabled(String, EngineSettings)}.
  1.1101 +   *
  1.1102 +   * @param stageName
  1.1103 +   *          to query.
  1.1104 +   * @return true if named stage is enabled for this sync.
  1.1105 +   */
  1.1106 +  public boolean isEngineLocallyEnabled(String stageName) {
  1.1107 +    if (config.stagesToSync == null) {
  1.1108 +      return true;
  1.1109 +    }
  1.1110 +    return config.stagesToSync.contains(stageName);
  1.1111 +  }
  1.1112 +
  1.1113 +  public ClientsDataDelegate getClientsDelegate() {
  1.1114 +    return this.clientsDelegate;
  1.1115 +  }
  1.1116 +
  1.1117 +  /**
  1.1118 +   * The longest backoff observed to date; -1 means no backoff observed.
  1.1119 +   */
  1.1120 +  protected final AtomicLong largestBackoffObserved = new AtomicLong(-1);
  1.1121 +
  1.1122 +  /**
  1.1123 +   * Reset any observed backoff and start observing HTTP responses for backoff
  1.1124 +   * requests.
  1.1125 +   */
  1.1126 +  protected void installAsHttpResponseObserver() {
  1.1127 +    Logger.debug(LOG_TAG, "Installing " + this + " as BaseResource HttpResponseObserver.");
  1.1128 +    BaseResource.setHttpResponseObserver(this);
  1.1129 +    largestBackoffObserved.set(-1);
  1.1130 +  }
  1.1131 +
  1.1132 +  /**
  1.1133 +   * Stop observing HttpResponses for backoff requests.
  1.1134 +   */
  1.1135 +  protected void uninstallAsHttpResponseObserver() {
  1.1136 +    Logger.debug(LOG_TAG, "Uninstalling " + this + " as BaseResource HttpResponseObserver.");
  1.1137 +    BaseResource.setHttpResponseObserver(null);
  1.1138 +  }
  1.1139 +
  1.1140 +  /**
  1.1141 +   * Observe all HTTP response for backoff requests on all status codes, not just errors.
  1.1142 +   */
  1.1143 +  @Override
  1.1144 +  public void observeHttpResponse(HttpResponse response) {
  1.1145 +    long responseBackoff = (new SyncResponse(response)).totalBackoffInMilliseconds(); // TODO: don't allocate object?
  1.1146 +    if (responseBackoff <= 0) {
  1.1147 +      return;
  1.1148 +    }
  1.1149 +
  1.1150 +    Logger.debug(LOG_TAG, "Observed " + responseBackoff + " millisecond backoff request.");
  1.1151 +    while (true) {
  1.1152 +      long existingBackoff = largestBackoffObserved.get();
  1.1153 +      if (existingBackoff >= responseBackoff) {
  1.1154 +        return;
  1.1155 +      }
  1.1156 +      if (largestBackoffObserved.compareAndSet(existingBackoff, responseBackoff)) {
  1.1157 +        return;
  1.1158 +      }
  1.1159 +    }
  1.1160 +  }
  1.1161 +}

mercurial