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