1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,616 @@ 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.fxa.sync; 1.9 + 1.10 +import java.net.URI; 1.11 +import java.net.URISyntaxException; 1.12 +import java.security.NoSuchAlgorithmException; 1.13 +import java.util.Collection; 1.14 +import java.util.Collections; 1.15 +import java.util.EnumSet; 1.16 +import java.util.concurrent.CountDownLatch; 1.17 +import java.util.concurrent.ExecutorService; 1.18 +import java.util.concurrent.Executors; 1.19 + 1.20 +import org.mozilla.gecko.background.common.log.Logger; 1.21 +import org.mozilla.gecko.background.fxa.FxAccountClient; 1.22 +import org.mozilla.gecko.background.fxa.FxAccountClient20; 1.23 +import org.mozilla.gecko.background.fxa.SkewHandler; 1.24 +import org.mozilla.gecko.browserid.BrowserIDKeyPair; 1.25 +import org.mozilla.gecko.browserid.JSONWebTokenUtils; 1.26 +import org.mozilla.gecko.fxa.FirefoxAccounts; 1.27 +import org.mozilla.gecko.fxa.FxAccountConstants; 1.28 +import org.mozilla.gecko.fxa.authenticator.AccountPickler; 1.29 +import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; 1.30 +import org.mozilla.gecko.fxa.authenticator.FxAccountAuthenticator; 1.31 +import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine; 1.32 +import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.LoginStateMachineDelegate; 1.33 +import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition; 1.34 +import org.mozilla.gecko.fxa.login.Married; 1.35 +import org.mozilla.gecko.fxa.login.State; 1.36 +import org.mozilla.gecko.fxa.login.State.StateLabel; 1.37 +import org.mozilla.gecko.fxa.login.StateFactory; 1.38 +import org.mozilla.gecko.sync.BackoffHandler; 1.39 +import org.mozilla.gecko.sync.GlobalSession; 1.40 +import org.mozilla.gecko.sync.PrefsBackoffHandler; 1.41 +import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate; 1.42 +import org.mozilla.gecko.sync.SyncConfiguration; 1.43 +import org.mozilla.gecko.sync.ThreadPool; 1.44 +import org.mozilla.gecko.sync.Utils; 1.45 +import org.mozilla.gecko.sync.crypto.KeyBundle; 1.46 +import org.mozilla.gecko.sync.delegates.BaseGlobalSessionCallback; 1.47 +import org.mozilla.gecko.sync.delegates.ClientsDataDelegate; 1.48 +import org.mozilla.gecko.sync.net.AuthHeaderProvider; 1.49 +import org.mozilla.gecko.sync.net.HawkAuthHeaderProvider; 1.50 +import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage; 1.51 +import org.mozilla.gecko.tokenserver.TokenServerClient; 1.52 +import org.mozilla.gecko.tokenserver.TokenServerClientDelegate; 1.53 +import org.mozilla.gecko.tokenserver.TokenServerException; 1.54 +import org.mozilla.gecko.tokenserver.TokenServerToken; 1.55 + 1.56 +import android.accounts.Account; 1.57 +import android.content.AbstractThreadedSyncAdapter; 1.58 +import android.content.ContentProviderClient; 1.59 +import android.content.ContentResolver; 1.60 +import android.content.Context; 1.61 +import android.content.SharedPreferences; 1.62 +import android.content.SyncResult; 1.63 +import android.os.Bundle; 1.64 +import android.os.SystemClock; 1.65 + 1.66 +public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter { 1.67 + private static final String LOG_TAG = FxAccountSyncAdapter.class.getSimpleName(); 1.68 + 1.69 + public static final String SYNC_EXTRAS_RESPECT_LOCAL_RATE_LIMIT = "respect_local_rate_limit"; 1.70 + public static final String SYNC_EXTRAS_RESPECT_REMOTE_SERVER_BACKOFF = "respect_remote_server_backoff"; 1.71 + 1.72 + protected static final int NOTIFICATION_ID = LOG_TAG.hashCode(); 1.73 + 1.74 + // Tracks the last seen storage hostname for backoff purposes. 1.75 + private static final String PREF_BACKOFF_STORAGE_HOST = "backoffStorageHost"; 1.76 + 1.77 + // Used to do cheap in-memory rate limiting. Don't sync again if we 1.78 + // successfully synced within this duration. 1.79 + private static final int MINIMUM_SYNC_DELAY_MILLIS = 15 * 1000; // 15 seconds. 1.80 + private volatile long lastSyncRealtimeMillis = 0L; 1.81 + 1.82 + protected final ExecutorService executor; 1.83 + protected final FxAccountNotificationManager notificationManager; 1.84 + 1.85 + public FxAccountSyncAdapter(Context context, boolean autoInitialize) { 1.86 + super(context, autoInitialize); 1.87 + this.executor = Executors.newSingleThreadExecutor(); 1.88 + this.notificationManager = new FxAccountNotificationManager(NOTIFICATION_ID); 1.89 + } 1.90 + 1.91 + protected static class SyncDelegate { 1.92 + protected final CountDownLatch latch; 1.93 + protected final SyncResult syncResult; 1.94 + protected final AndroidFxAccount fxAccount; 1.95 + protected final Collection<String> stageNamesToSync; 1.96 + 1.97 + public SyncDelegate(CountDownLatch latch, SyncResult syncResult, AndroidFxAccount fxAccount, Collection<String> stageNamesToSync) { 1.98 + if (latch == null) { 1.99 + throw new IllegalArgumentException("latch must not be null"); 1.100 + } 1.101 + if (syncResult == null) { 1.102 + throw new IllegalArgumentException("syncResult must not be null"); 1.103 + } 1.104 + if (fxAccount == null) { 1.105 + throw new IllegalArgumentException("fxAccount must not be null"); 1.106 + } 1.107 + this.latch = latch; 1.108 + this.syncResult = syncResult; 1.109 + this.fxAccount = fxAccount; 1.110 + this.stageNamesToSync = Collections.unmodifiableCollection(stageNamesToSync); 1.111 + } 1.112 + 1.113 + /** 1.114 + * No error! Say that we made progress. 1.115 + */ 1.116 + protected void setSyncResultSuccess() { 1.117 + syncResult.stats.numUpdates += 1; 1.118 + } 1.119 + 1.120 + /** 1.121 + * Soft error. Say that we made progress, so that Android will sync us again 1.122 + * after exponential backoff. 1.123 + */ 1.124 + protected void setSyncResultSoftError() { 1.125 + syncResult.stats.numUpdates += 1; 1.126 + syncResult.stats.numIoExceptions += 1; 1.127 + } 1.128 + 1.129 + /** 1.130 + * Hard error. We don't want Android to sync us again, even if we make 1.131 + * progress, until the user intervenes. 1.132 + */ 1.133 + protected void setSyncResultHardError() { 1.134 + syncResult.stats.numAuthExceptions += 1; 1.135 + } 1.136 + 1.137 + public void handleSuccess() { 1.138 + Logger.info(LOG_TAG, "Sync succeeded."); 1.139 + setSyncResultSuccess(); 1.140 + latch.countDown(); 1.141 + } 1.142 + 1.143 + public void handleError(Exception e) { 1.144 + Logger.error(LOG_TAG, "Got exception syncing.", e); 1.145 + setSyncResultSoftError(); 1.146 + // This is awful, but we need to propagate bad assertions back up the 1.147 + // chain somehow, and this will do for now. 1.148 + if (e instanceof TokenServerException) { 1.149 + // We should only get here *after* we're locked into the married state. 1.150 + State state = fxAccount.getState(); 1.151 + if (state.getStateLabel() == StateLabel.Married) { 1.152 + Married married = (Married) state; 1.153 + fxAccount.setState(married.makeCohabitingState()); 1.154 + } 1.155 + } 1.156 + latch.countDown(); 1.157 + } 1.158 + 1.159 + /** 1.160 + * When the login machine terminates, we might not be in the 1.161 + * <code>Married</code> state, and therefore we can't sync. This method 1.162 + * messages as much to the user. 1.163 + * <p> 1.164 + * To avoid stopping us syncing altogether, we set a soft error rather than 1.165 + * a hard error. In future, we would like to set a hard error if we are in, 1.166 + * for example, the <code>Separated</code> state, and then have some user 1.167 + * initiated activity mark the Android account as ready to sync again. This 1.168 + * is tricky, though, so we play it safe for now. 1.169 + * 1.170 + * @param finalState 1.171 + * that login machine ended in. 1.172 + */ 1.173 + public void handleCannotSync(State finalState) { 1.174 + Logger.warn(LOG_TAG, "Cannot sync from state: " + finalState.getStateLabel()); 1.175 + setSyncResultSoftError(); 1.176 + latch.countDown(); 1.177 + } 1.178 + 1.179 + public void postponeSync(long millis) { 1.180 + if (millis <= 0) { 1.181 + Logger.debug(LOG_TAG, "Asked to postpone sync, but zero delay. Short-circuiting."); 1.182 + } else { 1.183 + // delayUntil is broken: https://code.google.com/p/android/issues/detail?id=65669 1.184 + // So we don't bother doing this. Instead, we rely on the periodic sync 1.185 + // we schedule, and the backoff handler for the rest. 1.186 + /* 1.187 + Logger.warn(LOG_TAG, "Postponing sync by " + millis + "ms."); 1.188 + syncResult.delayUntil = millis / 1000; 1.189 + */ 1.190 + } 1.191 + setSyncResultSoftError(); 1.192 + latch.countDown(); 1.193 + } 1.194 + 1.195 + /** 1.196 + * Simply don't sync, without setting any error flags. 1.197 + * This is the appropriate behavior when a routine backoff has not yet 1.198 + * been met. 1.199 + */ 1.200 + public void rejectSync() { 1.201 + latch.countDown(); 1.202 + } 1.203 + 1.204 + public Collection<String> getStageNamesToSync() { 1.205 + return this.stageNamesToSync; 1.206 + } 1.207 + } 1.208 + 1.209 + protected static class SessionCallback implements BaseGlobalSessionCallback { 1.210 + protected final SyncDelegate syncDelegate; 1.211 + protected final SchedulePolicy schedulePolicy; 1.212 + protected volatile BackoffHandler storageBackoffHandler; 1.213 + 1.214 + public SessionCallback(SyncDelegate syncDelegate, SchedulePolicy schedulePolicy) { 1.215 + this.syncDelegate = syncDelegate; 1.216 + this.schedulePolicy = schedulePolicy; 1.217 + } 1.218 + 1.219 + public void setBackoffHandler(BackoffHandler backoffHandler) { 1.220 + this.storageBackoffHandler = backoffHandler; 1.221 + } 1.222 + 1.223 + @Override 1.224 + public boolean shouldBackOffStorage() { 1.225 + return storageBackoffHandler.delayMilliseconds() > 0; 1.226 + } 1.227 + 1.228 + @Override 1.229 + public void requestBackoff(long backoffMillis) { 1.230 + final boolean onlyExtend = true; // Because we trust what the storage server says. 1.231 + schedulePolicy.configureBackoffMillisOnBackoff(storageBackoffHandler, backoffMillis, onlyExtend); 1.232 + } 1.233 + 1.234 + @Override 1.235 + public void informUpgradeRequiredResponse(GlobalSession session) { 1.236 + schedulePolicy.onUpgradeRequired(); 1.237 + } 1.238 + 1.239 + @Override 1.240 + public void informUnauthorizedResponse(GlobalSession globalSession, URI oldClusterURL) { 1.241 + schedulePolicy.onUnauthorized(); 1.242 + } 1.243 + 1.244 + @Override 1.245 + public void handleStageCompleted(Stage currentState, GlobalSession globalSession) { 1.246 + } 1.247 + 1.248 + @Override 1.249 + public void handleSuccess(GlobalSession globalSession) { 1.250 + Logger.info(LOG_TAG, "Global session succeeded."); 1.251 + 1.252 + // Get the number of clients, so we can schedule the sync interval accordingly. 1.253 + try { 1.254 + int otherClientsCount = globalSession.getClientsDelegate().getClientsCount(); 1.255 + Logger.debug(LOG_TAG, "" + otherClientsCount + " other client(s)."); 1.256 + this.schedulePolicy.onSuccessfulSync(otherClientsCount); 1.257 + } finally { 1.258 + // Continue with the usual success flow. 1.259 + syncDelegate.handleSuccess(); 1.260 + } 1.261 + } 1.262 + 1.263 + @Override 1.264 + public void handleError(GlobalSession globalSession, Exception e) { 1.265 + Logger.warn(LOG_TAG, "Global session failed."); // Exception will be dumped by delegate below. 1.266 + syncDelegate.handleError(e); 1.267 + // TODO: should we reduce the periodic sync interval? 1.268 + } 1.269 + 1.270 + @Override 1.271 + public void handleAborted(GlobalSession globalSession, String reason) { 1.272 + Logger.warn(LOG_TAG, "Global session aborted: " + reason); 1.273 + syncDelegate.handleError(null); 1.274 + // TODO: should we reduce the periodic sync interval? 1.275 + } 1.276 + }; 1.277 + 1.278 + /** 1.279 + * Return true if the provided {@link BackoffHandler} isn't reporting that we're in 1.280 + * a backoff state, or the provided {@link Bundle} contains flags that indicate 1.281 + * we should force a sync. 1.282 + */ 1.283 + private boolean shouldPerformSync(final BackoffHandler backoffHandler, final String kind, final Bundle extras) { 1.284 + final long delay = backoffHandler.delayMilliseconds(); 1.285 + if (delay <= 0) { 1.286 + return true; 1.287 + } 1.288 + 1.289 + if (extras == null) { 1.290 + return false; 1.291 + } 1.292 + 1.293 + final boolean forced = extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false); 1.294 + if (forced) { 1.295 + Logger.info(LOG_TAG, "Forced sync (" + kind + "): overruling remaining backoff of " + delay + "ms."); 1.296 + } else { 1.297 + Logger.info(LOG_TAG, "Not syncing (" + kind + "): must wait another " + delay + "ms."); 1.298 + } 1.299 + return forced; 1.300 + } 1.301 + 1.302 + protected void syncWithAssertion(final String audience, 1.303 + final String assertion, 1.304 + final URI tokenServerEndpointURI, 1.305 + final BackoffHandler tokenBackoffHandler, 1.306 + final SharedPreferences sharedPrefs, 1.307 + final KeyBundle syncKeyBundle, 1.308 + final String clientState, 1.309 + final SessionCallback callback, 1.310 + final Bundle extras) { 1.311 + final TokenServerClientDelegate delegate = new TokenServerClientDelegate() { 1.312 + private boolean didReceiveBackoff = false; 1.313 + 1.314 + @Override 1.315 + public String getUserAgent() { 1.316 + return FxAccountConstants.USER_AGENT; 1.317 + } 1.318 + 1.319 + @Override 1.320 + public void handleSuccess(final TokenServerToken token) { 1.321 + FxAccountConstants.pii(LOG_TAG, "Got token! uid is " + token.uid + " and endpoint is " + token.endpoint + "."); 1.322 + 1.323 + if (!didReceiveBackoff) { 1.324 + // We must be OK to touch this token server. 1.325 + tokenBackoffHandler.setEarliestNextRequest(0L); 1.326 + } 1.327 + 1.328 + final URI storageServerURI; 1.329 + try { 1.330 + storageServerURI = new URI(token.endpoint); 1.331 + } catch (URISyntaxException e) { 1.332 + handleError(e); 1.333 + return; 1.334 + } 1.335 + final String storageHostname = storageServerURI.getHost(); 1.336 + 1.337 + // We back off on a per-host basis. When we have an endpoint URI from a token, we 1.338 + // can check on the backoff status for that host. 1.339 + // If we're supposed to be backing off, we abort the not-yet-started session. 1.340 + final BackoffHandler storageBackoffHandler = new PrefsBackoffHandler(sharedPrefs, "sync.storage"); 1.341 + callback.setBackoffHandler(storageBackoffHandler); 1.342 + 1.343 + String lastStorageHost = sharedPrefs.getString(PREF_BACKOFF_STORAGE_HOST, null); 1.344 + final boolean storageHostIsUnchanged = lastStorageHost != null && 1.345 + lastStorageHost.equalsIgnoreCase(storageHostname); 1.346 + if (storageHostIsUnchanged) { 1.347 + Logger.debug(LOG_TAG, "Storage host is unchanged."); 1.348 + if (!shouldPerformSync(storageBackoffHandler, "storage", extras)) { 1.349 + Logger.info(LOG_TAG, "Not syncing: storage server requested backoff."); 1.350 + callback.handleAborted(null, "Storage backoff"); 1.351 + return; 1.352 + } 1.353 + } else { 1.354 + Logger.debug(LOG_TAG, "Received new storage host."); 1.355 + } 1.356 + 1.357 + // Invalidate the previous backoff, because our storage host has changed, 1.358 + // or we never had one at all, or we're OK to sync. 1.359 + storageBackoffHandler.setEarliestNextRequest(0L); 1.360 + 1.361 + FxAccountGlobalSession globalSession = null; 1.362 + try { 1.363 + ClientsDataDelegate clientsDataDelegate = new SharedPreferencesClientsDataDelegate(sharedPrefs); 1.364 + 1.365 + // We compute skew over time using SkewHandler. This yields an unchanging 1.366 + // skew adjustment that the HawkAuthHeaderProvider uses to adjust its 1.367 + // timestamps. Eventually we might want this to adapt within the scope of a 1.368 + // global session. 1.369 + final SkewHandler storageServerSkewHandler = SkewHandler.getSkewHandlerForHostname(storageHostname); 1.370 + final long storageServerSkew = storageServerSkewHandler.getSkewInSeconds(); 1.371 + // We expect Sync to upload large sets of records. Calculating the 1.372 + // payload verification hash for these record sets could be expensive, 1.373 + // so we explicitly do not send payload verification hashes to the 1.374 + // Sync storage endpoint. 1.375 + final boolean includePayloadVerificationHash = false; 1.376 + final AuthHeaderProvider authHeaderProvider = new HawkAuthHeaderProvider(token.id, token.key.getBytes("UTF-8"), includePayloadVerificationHash, storageServerSkew); 1.377 + 1.378 + final Context context = getContext(); 1.379 + final SyncConfiguration syncConfig = new SyncConfiguration(token.uid, authHeaderProvider, sharedPrefs, syncKeyBundle); 1.380 + 1.381 + Collection<String> knownStageNames = SyncConfiguration.validEngineNames(); 1.382 + syncConfig.stagesToSync = Utils.getStagesToSyncFromBundle(knownStageNames, extras); 1.383 + syncConfig.setClusterURL(storageServerURI); 1.384 + 1.385 + globalSession = new FxAccountGlobalSession(syncConfig, callback, context, clientsDataDelegate); 1.386 + globalSession.start(); 1.387 + } catch (Exception e) { 1.388 + callback.handleError(globalSession, e); 1.389 + return; 1.390 + } 1.391 + } 1.392 + 1.393 + @Override 1.394 + public void handleFailure(TokenServerException e) { 1.395 + handleError(e); 1.396 + } 1.397 + 1.398 + @Override 1.399 + public void handleError(Exception e) { 1.400 + Logger.error(LOG_TAG, "Failed to get token.", e); 1.401 + callback.handleError(null, e); 1.402 + } 1.403 + 1.404 + @Override 1.405 + public void handleBackoff(int backoffSeconds) { 1.406 + // This is the token server telling us to back off. 1.407 + Logger.info(LOG_TAG, "Token server requesting backoff of " + backoffSeconds + "s. Backoff handler: " + tokenBackoffHandler); 1.408 + didReceiveBackoff = true; 1.409 + 1.410 + // If we've already stored a backoff, overrule it: we only use the server 1.411 + // value for token server scheduling. 1.412 + tokenBackoffHandler.setEarliestNextRequest(delay(backoffSeconds * 1000)); 1.413 + } 1.414 + 1.415 + private long delay(long delay) { 1.416 + return System.currentTimeMillis() + delay; 1.417 + } 1.418 + }; 1.419 + 1.420 + TokenServerClient tokenServerclient = new TokenServerClient(tokenServerEndpointURI, executor); 1.421 + tokenServerclient.getTokenFromBrowserIDAssertion(assertion, true, clientState, delegate); 1.422 + } 1.423 + 1.424 + /** 1.425 + * A trivial Sync implementation that does not cache client keys, 1.426 + * certificates, or tokens. 1.427 + * 1.428 + * This should be replaced with a full {@link FxAccountAuthenticator}-based 1.429 + * token implementation. 1.430 + */ 1.431 + @Override 1.432 + public void onPerformSync(final Account account, final Bundle extras, final String authority, ContentProviderClient provider, final SyncResult syncResult) { 1.433 + Logger.setThreadLogTag(FxAccountConstants.GLOBAL_LOG_TAG); 1.434 + Logger.resetLogging(); 1.435 + 1.436 + Logger.info(LOG_TAG, "Syncing FxAccount" + 1.437 + " account named like " + Utils.obfuscateEmail(account.name) + 1.438 + " for authority " + authority + 1.439 + " with instance " + this + "."); 1.440 + 1.441 + final EnumSet<FirefoxAccounts.SyncHint> syncHints = FirefoxAccounts.getHintsToSyncFromBundle(extras); 1.442 + FirefoxAccounts.logSyncHints(syncHints); 1.443 + 1.444 + // This applies even to forced syncs, but only on success. 1.445 + if (this.lastSyncRealtimeMillis > 0L && 1.446 + (this.lastSyncRealtimeMillis + MINIMUM_SYNC_DELAY_MILLIS) > SystemClock.elapsedRealtime()) { 1.447 + Logger.info(LOG_TAG, "Not syncing FxAccount " + Utils.obfuscateEmail(account.name) + 1.448 + ": minimum interval not met."); 1.449 + return; 1.450 + } 1.451 + 1.452 + final Context context = getContext(); 1.453 + final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account); 1.454 + if (FxAccountConstants.LOG_PERSONAL_INFORMATION) { 1.455 + fxAccount.dump(); 1.456 + } 1.457 + 1.458 + // Pickle in a background thread to avoid strict mode warnings. 1.459 + ThreadPool.run(new Runnable() { 1.460 + @Override 1.461 + public void run() { 1.462 + try { 1.463 + AccountPickler.pickle(fxAccount, FxAccountConstants.ACCOUNT_PICKLE_FILENAME); 1.464 + } catch (Exception e) { 1.465 + // Should never happen, but we really don't want to die in a background thread. 1.466 + Logger.warn(LOG_TAG, "Got exception pickling current account details; ignoring.", e); 1.467 + } 1.468 + } 1.469 + }); 1.470 + 1.471 + final CountDownLatch latch = new CountDownLatch(1); 1.472 + 1.473 + Collection<String> knownStageNames = SyncConfiguration.validEngineNames(); 1.474 + Collection<String> stageNamesToSync = Utils.getStagesToSyncFromBundle(knownStageNames, extras); 1.475 + 1.476 + final SyncDelegate syncDelegate = new SyncDelegate(latch, syncResult, fxAccount, stageNamesToSync); 1.477 + 1.478 + try { 1.479 + final State state; 1.480 + try { 1.481 + state = fxAccount.getState(); 1.482 + } catch (Exception e) { 1.483 + syncDelegate.handleError(e); 1.484 + return; 1.485 + } 1.486 + 1.487 + // This will be the same chunk of SharedPreferences that we pass through to GlobalSession/SyncConfiguration. 1.488 + final SharedPreferences sharedPrefs = fxAccount.getSyncPrefs(); 1.489 + 1.490 + final BackoffHandler backgroundBackoffHandler = new PrefsBackoffHandler(sharedPrefs, "background"); 1.491 + final BackoffHandler rateLimitBackoffHandler = new PrefsBackoffHandler(sharedPrefs, "rate"); 1.492 + 1.493 + // If this sync was triggered by user action, this will be true. 1.494 + final boolean isImmediate = (extras != null) && 1.495 + (extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, false) || 1.496 + extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false)); 1.497 + 1.498 + // If it's not an immediate sync, it must be either periodic or tickled. 1.499 + // Check our background rate limiter. 1.500 + if (!isImmediate) { 1.501 + if (!shouldPerformSync(backgroundBackoffHandler, "background", extras)) { 1.502 + syncDelegate.rejectSync(); 1.503 + return; 1.504 + } 1.505 + } 1.506 + 1.507 + // Regardless, let's make sure we're not syncing too often. 1.508 + if (!shouldPerformSync(rateLimitBackoffHandler, "rate", extras)) { 1.509 + syncDelegate.postponeSync(rateLimitBackoffHandler.delayMilliseconds()); 1.510 + return; 1.511 + } 1.512 + 1.513 + final SchedulePolicy schedulePolicy = new FxAccountSchedulePolicy(context, fxAccount); 1.514 + 1.515 + // Set a small scheduled 'backoff' to rate-limit the next sync, 1.516 + // and extend the background delay even further into the future. 1.517 + schedulePolicy.configureBackoffMillisBeforeSyncing(rateLimitBackoffHandler, backgroundBackoffHandler); 1.518 + 1.519 + final String audience = fxAccount.getAudience(); 1.520 + final String authServerEndpoint = fxAccount.getAccountServerURI(); 1.521 + final String tokenServerEndpoint = fxAccount.getTokenServerURI(); 1.522 + final URI tokenServerEndpointURI = new URI(tokenServerEndpoint); 1.523 + 1.524 + // TODO: why doesn't the loginPolicy extract the audience from the account? 1.525 + final FxAccountClient client = new FxAccountClient20(authServerEndpoint, executor); 1.526 + final FxAccountLoginStateMachine stateMachine = new FxAccountLoginStateMachine(); 1.527 + stateMachine.advance(state, StateLabel.Married, new LoginStateMachineDelegate() { 1.528 + @Override 1.529 + public FxAccountClient getClient() { 1.530 + return client; 1.531 + } 1.532 + 1.533 + @Override 1.534 + public long getCertificateDurationInMilliseconds() { 1.535 + return 12 * 60 * 60 * 1000; 1.536 + } 1.537 + 1.538 + @Override 1.539 + public long getAssertionDurationInMilliseconds() { 1.540 + return 15 * 60 * 1000; 1.541 + } 1.542 + 1.543 + @Override 1.544 + public BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException { 1.545 + return StateFactory.generateKeyPair(); 1.546 + } 1.547 + 1.548 + @Override 1.549 + public void handleTransition(Transition transition, State state) { 1.550 + Logger.info(LOG_TAG, "handleTransition: " + transition + " to " + state.getStateLabel()); 1.551 + } 1.552 + 1.553 + private boolean shouldRequestToken(final BackoffHandler tokenBackoffHandler, final Bundle extras) { 1.554 + return shouldPerformSync(tokenBackoffHandler, "token", extras); 1.555 + } 1.556 + 1.557 + @Override 1.558 + public void handleFinal(State state) { 1.559 + Logger.info(LOG_TAG, "handleFinal: in " + state.getStateLabel()); 1.560 + fxAccount.setState(state); 1.561 + schedulePolicy.onHandleFinal(state.getNeededAction()); 1.562 + notificationManager.update(context, fxAccount); 1.563 + try { 1.564 + if (state.getStateLabel() != StateLabel.Married) { 1.565 + syncDelegate.handleCannotSync(state); 1.566 + return; 1.567 + } 1.568 + 1.569 + final Married married = (Married) state; 1.570 + final String assertion = married.generateAssertion(audience, JSONWebTokenUtils.DEFAULT_ASSERTION_ISSUER); 1.571 + 1.572 + /* 1.573 + * At this point we're in the correct state to sync, and we're ready to fetch 1.574 + * a token and do some work. 1.575 + * 1.576 + * But first we need to do two things: 1.577 + * 1. Check to see whether we're in a backoff situation for the token server. 1.578 + * If we are, but we're not forcing a sync, then we go no further. 1.579 + * 2. Clear an existing backoff (if we're syncing it doesn't matter, and if 1.580 + * we're forcing we'll get a new backoff if things are still bad). 1.581 + * 1.582 + * Note that we don't check the storage backoff before the token dance: the token 1.583 + * server tells us which server we're syncing to! 1.584 + * 1.585 + * That logic lives in the TokenServerClientDelegate elsewhere in this file. 1.586 + */ 1.587 + 1.588 + // Strictly speaking this backoff check could be done prior to walking through 1.589 + // the login state machine, allowing us to short-circuit sooner. 1.590 + // We don't expect many token server backoffs, and most users will be sitting 1.591 + // in the Married state, so instead we simply do this here, once. 1.592 + final BackoffHandler tokenBackoffHandler = new PrefsBackoffHandler(sharedPrefs, "token"); 1.593 + if (!shouldRequestToken(tokenBackoffHandler, extras)) { 1.594 + Logger.info(LOG_TAG, "Not syncing (token server)."); 1.595 + syncDelegate.postponeSync(tokenBackoffHandler.delayMilliseconds()); 1.596 + return; 1.597 + } 1.598 + 1.599 + final SessionCallback sessionCallback = new SessionCallback(syncDelegate, schedulePolicy); 1.600 + final KeyBundle syncKeyBundle = married.getSyncKeyBundle(); 1.601 + final String clientState = married.getClientState(); 1.602 + syncWithAssertion(audience, assertion, tokenServerEndpointURI, tokenBackoffHandler, sharedPrefs, syncKeyBundle, clientState, sessionCallback, extras); 1.603 + } catch (Exception e) { 1.604 + syncDelegate.handleError(e); 1.605 + return; 1.606 + } 1.607 + } 1.608 + }); 1.609 + 1.610 + latch.await(); 1.611 + } catch (Exception e) { 1.612 + Logger.error(LOG_TAG, "Got error syncing.", e); 1.613 + syncDelegate.handleError(e); 1.614 + } 1.615 + 1.616 + Logger.info(LOG_TAG, "Syncing done."); 1.617 + lastSyncRealtimeMillis = SystemClock.elapsedRealtime(); 1.618 + } 1.619 +}