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.

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

mercurial