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