mobile/android/base/fxa/sync/FxAccountSyncAdapter.java

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

Correct previous dual key logic pending first delivery installment.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 package org.mozilla.gecko.fxa.sync;
michael@0 6
michael@0 7 import java.net.URI;
michael@0 8 import java.net.URISyntaxException;
michael@0 9 import java.security.NoSuchAlgorithmException;
michael@0 10 import java.util.Collection;
michael@0 11 import java.util.Collections;
michael@0 12 import java.util.EnumSet;
michael@0 13 import java.util.concurrent.CountDownLatch;
michael@0 14 import java.util.concurrent.ExecutorService;
michael@0 15 import java.util.concurrent.Executors;
michael@0 16
michael@0 17 import org.mozilla.gecko.background.common.log.Logger;
michael@0 18 import org.mozilla.gecko.background.fxa.FxAccountClient;
michael@0 19 import org.mozilla.gecko.background.fxa.FxAccountClient20;
michael@0 20 import org.mozilla.gecko.background.fxa.SkewHandler;
michael@0 21 import org.mozilla.gecko.browserid.BrowserIDKeyPair;
michael@0 22 import org.mozilla.gecko.browserid.JSONWebTokenUtils;
michael@0 23 import org.mozilla.gecko.fxa.FirefoxAccounts;
michael@0 24 import org.mozilla.gecko.fxa.FxAccountConstants;
michael@0 25 import org.mozilla.gecko.fxa.authenticator.AccountPickler;
michael@0 26 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
michael@0 27 import org.mozilla.gecko.fxa.authenticator.FxAccountAuthenticator;
michael@0 28 import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine;
michael@0 29 import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.LoginStateMachineDelegate;
michael@0 30 import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition;
michael@0 31 import org.mozilla.gecko.fxa.login.Married;
michael@0 32 import org.mozilla.gecko.fxa.login.State;
michael@0 33 import org.mozilla.gecko.fxa.login.State.StateLabel;
michael@0 34 import org.mozilla.gecko.fxa.login.StateFactory;
michael@0 35 import org.mozilla.gecko.sync.BackoffHandler;
michael@0 36 import org.mozilla.gecko.sync.GlobalSession;
michael@0 37 import org.mozilla.gecko.sync.PrefsBackoffHandler;
michael@0 38 import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate;
michael@0 39 import org.mozilla.gecko.sync.SyncConfiguration;
michael@0 40 import org.mozilla.gecko.sync.ThreadPool;
michael@0 41 import org.mozilla.gecko.sync.Utils;
michael@0 42 import org.mozilla.gecko.sync.crypto.KeyBundle;
michael@0 43 import org.mozilla.gecko.sync.delegates.BaseGlobalSessionCallback;
michael@0 44 import org.mozilla.gecko.sync.delegates.ClientsDataDelegate;
michael@0 45 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
michael@0 46 import org.mozilla.gecko.sync.net.HawkAuthHeaderProvider;
michael@0 47 import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
michael@0 48 import org.mozilla.gecko.tokenserver.TokenServerClient;
michael@0 49 import org.mozilla.gecko.tokenserver.TokenServerClientDelegate;
michael@0 50 import org.mozilla.gecko.tokenserver.TokenServerException;
michael@0 51 import org.mozilla.gecko.tokenserver.TokenServerToken;
michael@0 52
michael@0 53 import android.accounts.Account;
michael@0 54 import android.content.AbstractThreadedSyncAdapter;
michael@0 55 import android.content.ContentProviderClient;
michael@0 56 import android.content.ContentResolver;
michael@0 57 import android.content.Context;
michael@0 58 import android.content.SharedPreferences;
michael@0 59 import android.content.SyncResult;
michael@0 60 import android.os.Bundle;
michael@0 61 import android.os.SystemClock;
michael@0 62
michael@0 63 public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter {
michael@0 64 private static final String LOG_TAG = FxAccountSyncAdapter.class.getSimpleName();
michael@0 65
michael@0 66 public static final String SYNC_EXTRAS_RESPECT_LOCAL_RATE_LIMIT = "respect_local_rate_limit";
michael@0 67 public static final String SYNC_EXTRAS_RESPECT_REMOTE_SERVER_BACKOFF = "respect_remote_server_backoff";
michael@0 68
michael@0 69 protected static final int NOTIFICATION_ID = LOG_TAG.hashCode();
michael@0 70
michael@0 71 // Tracks the last seen storage hostname for backoff purposes.
michael@0 72 private static final String PREF_BACKOFF_STORAGE_HOST = "backoffStorageHost";
michael@0 73
michael@0 74 // Used to do cheap in-memory rate limiting. Don't sync again if we
michael@0 75 // successfully synced within this duration.
michael@0 76 private static final int MINIMUM_SYNC_DELAY_MILLIS = 15 * 1000; // 15 seconds.
michael@0 77 private volatile long lastSyncRealtimeMillis = 0L;
michael@0 78
michael@0 79 protected final ExecutorService executor;
michael@0 80 protected final FxAccountNotificationManager notificationManager;
michael@0 81
michael@0 82 public FxAccountSyncAdapter(Context context, boolean autoInitialize) {
michael@0 83 super(context, autoInitialize);
michael@0 84 this.executor = Executors.newSingleThreadExecutor();
michael@0 85 this.notificationManager = new FxAccountNotificationManager(NOTIFICATION_ID);
michael@0 86 }
michael@0 87
michael@0 88 protected static class SyncDelegate {
michael@0 89 protected final CountDownLatch latch;
michael@0 90 protected final SyncResult syncResult;
michael@0 91 protected final AndroidFxAccount fxAccount;
michael@0 92 protected final Collection<String> stageNamesToSync;
michael@0 93
michael@0 94 public SyncDelegate(CountDownLatch latch, SyncResult syncResult, AndroidFxAccount fxAccount, Collection<String> stageNamesToSync) {
michael@0 95 if (latch == null) {
michael@0 96 throw new IllegalArgumentException("latch must not be null");
michael@0 97 }
michael@0 98 if (syncResult == null) {
michael@0 99 throw new IllegalArgumentException("syncResult must not be null");
michael@0 100 }
michael@0 101 if (fxAccount == null) {
michael@0 102 throw new IllegalArgumentException("fxAccount must not be null");
michael@0 103 }
michael@0 104 this.latch = latch;
michael@0 105 this.syncResult = syncResult;
michael@0 106 this.fxAccount = fxAccount;
michael@0 107 this.stageNamesToSync = Collections.unmodifiableCollection(stageNamesToSync);
michael@0 108 }
michael@0 109
michael@0 110 /**
michael@0 111 * No error! Say that we made progress.
michael@0 112 */
michael@0 113 protected void setSyncResultSuccess() {
michael@0 114 syncResult.stats.numUpdates += 1;
michael@0 115 }
michael@0 116
michael@0 117 /**
michael@0 118 * Soft error. Say that we made progress, so that Android will sync us again
michael@0 119 * after exponential backoff.
michael@0 120 */
michael@0 121 protected void setSyncResultSoftError() {
michael@0 122 syncResult.stats.numUpdates += 1;
michael@0 123 syncResult.stats.numIoExceptions += 1;
michael@0 124 }
michael@0 125
michael@0 126 /**
michael@0 127 * Hard error. We don't want Android to sync us again, even if we make
michael@0 128 * progress, until the user intervenes.
michael@0 129 */
michael@0 130 protected void setSyncResultHardError() {
michael@0 131 syncResult.stats.numAuthExceptions += 1;
michael@0 132 }
michael@0 133
michael@0 134 public void handleSuccess() {
michael@0 135 Logger.info(LOG_TAG, "Sync succeeded.");
michael@0 136 setSyncResultSuccess();
michael@0 137 latch.countDown();
michael@0 138 }
michael@0 139
michael@0 140 public void handleError(Exception e) {
michael@0 141 Logger.error(LOG_TAG, "Got exception syncing.", e);
michael@0 142 setSyncResultSoftError();
michael@0 143 // This is awful, but we need to propagate bad assertions back up the
michael@0 144 // chain somehow, and this will do for now.
michael@0 145 if (e instanceof TokenServerException) {
michael@0 146 // We should only get here *after* we're locked into the married state.
michael@0 147 State state = fxAccount.getState();
michael@0 148 if (state.getStateLabel() == StateLabel.Married) {
michael@0 149 Married married = (Married) state;
michael@0 150 fxAccount.setState(married.makeCohabitingState());
michael@0 151 }
michael@0 152 }
michael@0 153 latch.countDown();
michael@0 154 }
michael@0 155
michael@0 156 /**
michael@0 157 * When the login machine terminates, we might not be in the
michael@0 158 * <code>Married</code> state, and therefore we can't sync. This method
michael@0 159 * messages as much to the user.
michael@0 160 * <p>
michael@0 161 * To avoid stopping us syncing altogether, we set a soft error rather than
michael@0 162 * a hard error. In future, we would like to set a hard error if we are in,
michael@0 163 * for example, the <code>Separated</code> state, and then have some user
michael@0 164 * initiated activity mark the Android account as ready to sync again. This
michael@0 165 * is tricky, though, so we play it safe for now.
michael@0 166 *
michael@0 167 * @param finalState
michael@0 168 * that login machine ended in.
michael@0 169 */
michael@0 170 public void handleCannotSync(State finalState) {
michael@0 171 Logger.warn(LOG_TAG, "Cannot sync from state: " + finalState.getStateLabel());
michael@0 172 setSyncResultSoftError();
michael@0 173 latch.countDown();
michael@0 174 }
michael@0 175
michael@0 176 public void postponeSync(long millis) {
michael@0 177 if (millis <= 0) {
michael@0 178 Logger.debug(LOG_TAG, "Asked to postpone sync, but zero delay. Short-circuiting.");
michael@0 179 } else {
michael@0 180 // delayUntil is broken: https://code.google.com/p/android/issues/detail?id=65669
michael@0 181 // So we don't bother doing this. Instead, we rely on the periodic sync
michael@0 182 // we schedule, and the backoff handler for the rest.
michael@0 183 /*
michael@0 184 Logger.warn(LOG_TAG, "Postponing sync by " + millis + "ms.");
michael@0 185 syncResult.delayUntil = millis / 1000;
michael@0 186 */
michael@0 187 }
michael@0 188 setSyncResultSoftError();
michael@0 189 latch.countDown();
michael@0 190 }
michael@0 191
michael@0 192 /**
michael@0 193 * Simply don't sync, without setting any error flags.
michael@0 194 * This is the appropriate behavior when a routine backoff has not yet
michael@0 195 * been met.
michael@0 196 */
michael@0 197 public void rejectSync() {
michael@0 198 latch.countDown();
michael@0 199 }
michael@0 200
michael@0 201 public Collection<String> getStageNamesToSync() {
michael@0 202 return this.stageNamesToSync;
michael@0 203 }
michael@0 204 }
michael@0 205
michael@0 206 protected static class SessionCallback implements BaseGlobalSessionCallback {
michael@0 207 protected final SyncDelegate syncDelegate;
michael@0 208 protected final SchedulePolicy schedulePolicy;
michael@0 209 protected volatile BackoffHandler storageBackoffHandler;
michael@0 210
michael@0 211 public SessionCallback(SyncDelegate syncDelegate, SchedulePolicy schedulePolicy) {
michael@0 212 this.syncDelegate = syncDelegate;
michael@0 213 this.schedulePolicy = schedulePolicy;
michael@0 214 }
michael@0 215
michael@0 216 public void setBackoffHandler(BackoffHandler backoffHandler) {
michael@0 217 this.storageBackoffHandler = backoffHandler;
michael@0 218 }
michael@0 219
michael@0 220 @Override
michael@0 221 public boolean shouldBackOffStorage() {
michael@0 222 return storageBackoffHandler.delayMilliseconds() > 0;
michael@0 223 }
michael@0 224
michael@0 225 @Override
michael@0 226 public void requestBackoff(long backoffMillis) {
michael@0 227 final boolean onlyExtend = true; // Because we trust what the storage server says.
michael@0 228 schedulePolicy.configureBackoffMillisOnBackoff(storageBackoffHandler, backoffMillis, onlyExtend);
michael@0 229 }
michael@0 230
michael@0 231 @Override
michael@0 232 public void informUpgradeRequiredResponse(GlobalSession session) {
michael@0 233 schedulePolicy.onUpgradeRequired();
michael@0 234 }
michael@0 235
michael@0 236 @Override
michael@0 237 public void informUnauthorizedResponse(GlobalSession globalSession, URI oldClusterURL) {
michael@0 238 schedulePolicy.onUnauthorized();
michael@0 239 }
michael@0 240
michael@0 241 @Override
michael@0 242 public void handleStageCompleted(Stage currentState, GlobalSession globalSession) {
michael@0 243 }
michael@0 244
michael@0 245 @Override
michael@0 246 public void handleSuccess(GlobalSession globalSession) {
michael@0 247 Logger.info(LOG_TAG, "Global session succeeded.");
michael@0 248
michael@0 249 // Get the number of clients, so we can schedule the sync interval accordingly.
michael@0 250 try {
michael@0 251 int otherClientsCount = globalSession.getClientsDelegate().getClientsCount();
michael@0 252 Logger.debug(LOG_TAG, "" + otherClientsCount + " other client(s).");
michael@0 253 this.schedulePolicy.onSuccessfulSync(otherClientsCount);
michael@0 254 } finally {
michael@0 255 // Continue with the usual success flow.
michael@0 256 syncDelegate.handleSuccess();
michael@0 257 }
michael@0 258 }
michael@0 259
michael@0 260 @Override
michael@0 261 public void handleError(GlobalSession globalSession, Exception e) {
michael@0 262 Logger.warn(LOG_TAG, "Global session failed."); // Exception will be dumped by delegate below.
michael@0 263 syncDelegate.handleError(e);
michael@0 264 // TODO: should we reduce the periodic sync interval?
michael@0 265 }
michael@0 266
michael@0 267 @Override
michael@0 268 public void handleAborted(GlobalSession globalSession, String reason) {
michael@0 269 Logger.warn(LOG_TAG, "Global session aborted: " + reason);
michael@0 270 syncDelegate.handleError(null);
michael@0 271 // TODO: should we reduce the periodic sync interval?
michael@0 272 }
michael@0 273 };
michael@0 274
michael@0 275 /**
michael@0 276 * Return true if the provided {@link BackoffHandler} isn't reporting that we're in
michael@0 277 * a backoff state, or the provided {@link Bundle} contains flags that indicate
michael@0 278 * we should force a sync.
michael@0 279 */
michael@0 280 private boolean shouldPerformSync(final BackoffHandler backoffHandler, final String kind, final Bundle extras) {
michael@0 281 final long delay = backoffHandler.delayMilliseconds();
michael@0 282 if (delay <= 0) {
michael@0 283 return true;
michael@0 284 }
michael@0 285
michael@0 286 if (extras == null) {
michael@0 287 return false;
michael@0 288 }
michael@0 289
michael@0 290 final boolean forced = extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false);
michael@0 291 if (forced) {
michael@0 292 Logger.info(LOG_TAG, "Forced sync (" + kind + "): overruling remaining backoff of " + delay + "ms.");
michael@0 293 } else {
michael@0 294 Logger.info(LOG_TAG, "Not syncing (" + kind + "): must wait another " + delay + "ms.");
michael@0 295 }
michael@0 296 return forced;
michael@0 297 }
michael@0 298
michael@0 299 protected void syncWithAssertion(final String audience,
michael@0 300 final String assertion,
michael@0 301 final URI tokenServerEndpointURI,
michael@0 302 final BackoffHandler tokenBackoffHandler,
michael@0 303 final SharedPreferences sharedPrefs,
michael@0 304 final KeyBundle syncKeyBundle,
michael@0 305 final String clientState,
michael@0 306 final SessionCallback callback,
michael@0 307 final Bundle extras) {
michael@0 308 final TokenServerClientDelegate delegate = new TokenServerClientDelegate() {
michael@0 309 private boolean didReceiveBackoff = false;
michael@0 310
michael@0 311 @Override
michael@0 312 public String getUserAgent() {
michael@0 313 return FxAccountConstants.USER_AGENT;
michael@0 314 }
michael@0 315
michael@0 316 @Override
michael@0 317 public void handleSuccess(final TokenServerToken token) {
michael@0 318 FxAccountConstants.pii(LOG_TAG, "Got token! uid is " + token.uid + " and endpoint is " + token.endpoint + ".");
michael@0 319
michael@0 320 if (!didReceiveBackoff) {
michael@0 321 // We must be OK to touch this token server.
michael@0 322 tokenBackoffHandler.setEarliestNextRequest(0L);
michael@0 323 }
michael@0 324
michael@0 325 final URI storageServerURI;
michael@0 326 try {
michael@0 327 storageServerURI = new URI(token.endpoint);
michael@0 328 } catch (URISyntaxException e) {
michael@0 329 handleError(e);
michael@0 330 return;
michael@0 331 }
michael@0 332 final String storageHostname = storageServerURI.getHost();
michael@0 333
michael@0 334 // We back off on a per-host basis. When we have an endpoint URI from a token, we
michael@0 335 // can check on the backoff status for that host.
michael@0 336 // If we're supposed to be backing off, we abort the not-yet-started session.
michael@0 337 final BackoffHandler storageBackoffHandler = new PrefsBackoffHandler(sharedPrefs, "sync.storage");
michael@0 338 callback.setBackoffHandler(storageBackoffHandler);
michael@0 339
michael@0 340 String lastStorageHost = sharedPrefs.getString(PREF_BACKOFF_STORAGE_HOST, null);
michael@0 341 final boolean storageHostIsUnchanged = lastStorageHost != null &&
michael@0 342 lastStorageHost.equalsIgnoreCase(storageHostname);
michael@0 343 if (storageHostIsUnchanged) {
michael@0 344 Logger.debug(LOG_TAG, "Storage host is unchanged.");
michael@0 345 if (!shouldPerformSync(storageBackoffHandler, "storage", extras)) {
michael@0 346 Logger.info(LOG_TAG, "Not syncing: storage server requested backoff.");
michael@0 347 callback.handleAborted(null, "Storage backoff");
michael@0 348 return;
michael@0 349 }
michael@0 350 } else {
michael@0 351 Logger.debug(LOG_TAG, "Received new storage host.");
michael@0 352 }
michael@0 353
michael@0 354 // Invalidate the previous backoff, because our storage host has changed,
michael@0 355 // or we never had one at all, or we're OK to sync.
michael@0 356 storageBackoffHandler.setEarliestNextRequest(0L);
michael@0 357
michael@0 358 FxAccountGlobalSession globalSession = null;
michael@0 359 try {
michael@0 360 ClientsDataDelegate clientsDataDelegate = new SharedPreferencesClientsDataDelegate(sharedPrefs);
michael@0 361
michael@0 362 // We compute skew over time using SkewHandler. This yields an unchanging
michael@0 363 // skew adjustment that the HawkAuthHeaderProvider uses to adjust its
michael@0 364 // timestamps. Eventually we might want this to adapt within the scope of a
michael@0 365 // global session.
michael@0 366 final SkewHandler storageServerSkewHandler = SkewHandler.getSkewHandlerForHostname(storageHostname);
michael@0 367 final long storageServerSkew = storageServerSkewHandler.getSkewInSeconds();
michael@0 368 // We expect Sync to upload large sets of records. Calculating the
michael@0 369 // payload verification hash for these record sets could be expensive,
michael@0 370 // so we explicitly do not send payload verification hashes to the
michael@0 371 // Sync storage endpoint.
michael@0 372 final boolean includePayloadVerificationHash = false;
michael@0 373 final AuthHeaderProvider authHeaderProvider = new HawkAuthHeaderProvider(token.id, token.key.getBytes("UTF-8"), includePayloadVerificationHash, storageServerSkew);
michael@0 374
michael@0 375 final Context context = getContext();
michael@0 376 final SyncConfiguration syncConfig = new SyncConfiguration(token.uid, authHeaderProvider, sharedPrefs, syncKeyBundle);
michael@0 377
michael@0 378 Collection<String> knownStageNames = SyncConfiguration.validEngineNames();
michael@0 379 syncConfig.stagesToSync = Utils.getStagesToSyncFromBundle(knownStageNames, extras);
michael@0 380 syncConfig.setClusterURL(storageServerURI);
michael@0 381
michael@0 382 globalSession = new FxAccountGlobalSession(syncConfig, callback, context, clientsDataDelegate);
michael@0 383 globalSession.start();
michael@0 384 } catch (Exception e) {
michael@0 385 callback.handleError(globalSession, e);
michael@0 386 return;
michael@0 387 }
michael@0 388 }
michael@0 389
michael@0 390 @Override
michael@0 391 public void handleFailure(TokenServerException e) {
michael@0 392 handleError(e);
michael@0 393 }
michael@0 394
michael@0 395 @Override
michael@0 396 public void handleError(Exception e) {
michael@0 397 Logger.error(LOG_TAG, "Failed to get token.", e);
michael@0 398 callback.handleError(null, e);
michael@0 399 }
michael@0 400
michael@0 401 @Override
michael@0 402 public void handleBackoff(int backoffSeconds) {
michael@0 403 // This is the token server telling us to back off.
michael@0 404 Logger.info(LOG_TAG, "Token server requesting backoff of " + backoffSeconds + "s. Backoff handler: " + tokenBackoffHandler);
michael@0 405 didReceiveBackoff = true;
michael@0 406
michael@0 407 // If we've already stored a backoff, overrule it: we only use the server
michael@0 408 // value for token server scheduling.
michael@0 409 tokenBackoffHandler.setEarliestNextRequest(delay(backoffSeconds * 1000));
michael@0 410 }
michael@0 411
michael@0 412 private long delay(long delay) {
michael@0 413 return System.currentTimeMillis() + delay;
michael@0 414 }
michael@0 415 };
michael@0 416
michael@0 417 TokenServerClient tokenServerclient = new TokenServerClient(tokenServerEndpointURI, executor);
michael@0 418 tokenServerclient.getTokenFromBrowserIDAssertion(assertion, true, clientState, delegate);
michael@0 419 }
michael@0 420
michael@0 421 /**
michael@0 422 * A trivial Sync implementation that does not cache client keys,
michael@0 423 * certificates, or tokens.
michael@0 424 *
michael@0 425 * This should be replaced with a full {@link FxAccountAuthenticator}-based
michael@0 426 * token implementation.
michael@0 427 */
michael@0 428 @Override
michael@0 429 public void onPerformSync(final Account account, final Bundle extras, final String authority, ContentProviderClient provider, final SyncResult syncResult) {
michael@0 430 Logger.setThreadLogTag(FxAccountConstants.GLOBAL_LOG_TAG);
michael@0 431 Logger.resetLogging();
michael@0 432
michael@0 433 Logger.info(LOG_TAG, "Syncing FxAccount" +
michael@0 434 " account named like " + Utils.obfuscateEmail(account.name) +
michael@0 435 " for authority " + authority +
michael@0 436 " with instance " + this + ".");
michael@0 437
michael@0 438 final EnumSet<FirefoxAccounts.SyncHint> syncHints = FirefoxAccounts.getHintsToSyncFromBundle(extras);
michael@0 439 FirefoxAccounts.logSyncHints(syncHints);
michael@0 440
michael@0 441 // This applies even to forced syncs, but only on success.
michael@0 442 if (this.lastSyncRealtimeMillis > 0L &&
michael@0 443 (this.lastSyncRealtimeMillis + MINIMUM_SYNC_DELAY_MILLIS) > SystemClock.elapsedRealtime()) {
michael@0 444 Logger.info(LOG_TAG, "Not syncing FxAccount " + Utils.obfuscateEmail(account.name) +
michael@0 445 ": minimum interval not met.");
michael@0 446 return;
michael@0 447 }
michael@0 448
michael@0 449 final Context context = getContext();
michael@0 450 final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
michael@0 451 if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
michael@0 452 fxAccount.dump();
michael@0 453 }
michael@0 454
michael@0 455 // Pickle in a background thread to avoid strict mode warnings.
michael@0 456 ThreadPool.run(new Runnable() {
michael@0 457 @Override
michael@0 458 public void run() {
michael@0 459 try {
michael@0 460 AccountPickler.pickle(fxAccount, FxAccountConstants.ACCOUNT_PICKLE_FILENAME);
michael@0 461 } catch (Exception e) {
michael@0 462 // Should never happen, but we really don't want to die in a background thread.
michael@0 463 Logger.warn(LOG_TAG, "Got exception pickling current account details; ignoring.", e);
michael@0 464 }
michael@0 465 }
michael@0 466 });
michael@0 467
michael@0 468 final CountDownLatch latch = new CountDownLatch(1);
michael@0 469
michael@0 470 Collection<String> knownStageNames = SyncConfiguration.validEngineNames();
michael@0 471 Collection<String> stageNamesToSync = Utils.getStagesToSyncFromBundle(knownStageNames, extras);
michael@0 472
michael@0 473 final SyncDelegate syncDelegate = new SyncDelegate(latch, syncResult, fxAccount, stageNamesToSync);
michael@0 474
michael@0 475 try {
michael@0 476 final State state;
michael@0 477 try {
michael@0 478 state = fxAccount.getState();
michael@0 479 } catch (Exception e) {
michael@0 480 syncDelegate.handleError(e);
michael@0 481 return;
michael@0 482 }
michael@0 483
michael@0 484 // This will be the same chunk of SharedPreferences that we pass through to GlobalSession/SyncConfiguration.
michael@0 485 final SharedPreferences sharedPrefs = fxAccount.getSyncPrefs();
michael@0 486
michael@0 487 final BackoffHandler backgroundBackoffHandler = new PrefsBackoffHandler(sharedPrefs, "background");
michael@0 488 final BackoffHandler rateLimitBackoffHandler = new PrefsBackoffHandler(sharedPrefs, "rate");
michael@0 489
michael@0 490 // If this sync was triggered by user action, this will be true.
michael@0 491 final boolean isImmediate = (extras != null) &&
michael@0 492 (extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, false) ||
michael@0 493 extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false));
michael@0 494
michael@0 495 // If it's not an immediate sync, it must be either periodic or tickled.
michael@0 496 // Check our background rate limiter.
michael@0 497 if (!isImmediate) {
michael@0 498 if (!shouldPerformSync(backgroundBackoffHandler, "background", extras)) {
michael@0 499 syncDelegate.rejectSync();
michael@0 500 return;
michael@0 501 }
michael@0 502 }
michael@0 503
michael@0 504 // Regardless, let's make sure we're not syncing too often.
michael@0 505 if (!shouldPerformSync(rateLimitBackoffHandler, "rate", extras)) {
michael@0 506 syncDelegate.postponeSync(rateLimitBackoffHandler.delayMilliseconds());
michael@0 507 return;
michael@0 508 }
michael@0 509
michael@0 510 final SchedulePolicy schedulePolicy = new FxAccountSchedulePolicy(context, fxAccount);
michael@0 511
michael@0 512 // Set a small scheduled 'backoff' to rate-limit the next sync,
michael@0 513 // and extend the background delay even further into the future.
michael@0 514 schedulePolicy.configureBackoffMillisBeforeSyncing(rateLimitBackoffHandler, backgroundBackoffHandler);
michael@0 515
michael@0 516 final String audience = fxAccount.getAudience();
michael@0 517 final String authServerEndpoint = fxAccount.getAccountServerURI();
michael@0 518 final String tokenServerEndpoint = fxAccount.getTokenServerURI();
michael@0 519 final URI tokenServerEndpointURI = new URI(tokenServerEndpoint);
michael@0 520
michael@0 521 // TODO: why doesn't the loginPolicy extract the audience from the account?
michael@0 522 final FxAccountClient client = new FxAccountClient20(authServerEndpoint, executor);
michael@0 523 final FxAccountLoginStateMachine stateMachine = new FxAccountLoginStateMachine();
michael@0 524 stateMachine.advance(state, StateLabel.Married, new LoginStateMachineDelegate() {
michael@0 525 @Override
michael@0 526 public FxAccountClient getClient() {
michael@0 527 return client;
michael@0 528 }
michael@0 529
michael@0 530 @Override
michael@0 531 public long getCertificateDurationInMilliseconds() {
michael@0 532 return 12 * 60 * 60 * 1000;
michael@0 533 }
michael@0 534
michael@0 535 @Override
michael@0 536 public long getAssertionDurationInMilliseconds() {
michael@0 537 return 15 * 60 * 1000;
michael@0 538 }
michael@0 539
michael@0 540 @Override
michael@0 541 public BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException {
michael@0 542 return StateFactory.generateKeyPair();
michael@0 543 }
michael@0 544
michael@0 545 @Override
michael@0 546 public void handleTransition(Transition transition, State state) {
michael@0 547 Logger.info(LOG_TAG, "handleTransition: " + transition + " to " + state.getStateLabel());
michael@0 548 }
michael@0 549
michael@0 550 private boolean shouldRequestToken(final BackoffHandler tokenBackoffHandler, final Bundle extras) {
michael@0 551 return shouldPerformSync(tokenBackoffHandler, "token", extras);
michael@0 552 }
michael@0 553
michael@0 554 @Override
michael@0 555 public void handleFinal(State state) {
michael@0 556 Logger.info(LOG_TAG, "handleFinal: in " + state.getStateLabel());
michael@0 557 fxAccount.setState(state);
michael@0 558 schedulePolicy.onHandleFinal(state.getNeededAction());
michael@0 559 notificationManager.update(context, fxAccount);
michael@0 560 try {
michael@0 561 if (state.getStateLabel() != StateLabel.Married) {
michael@0 562 syncDelegate.handleCannotSync(state);
michael@0 563 return;
michael@0 564 }
michael@0 565
michael@0 566 final Married married = (Married) state;
michael@0 567 final String assertion = married.generateAssertion(audience, JSONWebTokenUtils.DEFAULT_ASSERTION_ISSUER);
michael@0 568
michael@0 569 /*
michael@0 570 * At this point we're in the correct state to sync, and we're ready to fetch
michael@0 571 * a token and do some work.
michael@0 572 *
michael@0 573 * But first we need to do two things:
michael@0 574 * 1. Check to see whether we're in a backoff situation for the token server.
michael@0 575 * If we are, but we're not forcing a sync, then we go no further.
michael@0 576 * 2. Clear an existing backoff (if we're syncing it doesn't matter, and if
michael@0 577 * we're forcing we'll get a new backoff if things are still bad).
michael@0 578 *
michael@0 579 * Note that we don't check the storage backoff before the token dance: the token
michael@0 580 * server tells us which server we're syncing to!
michael@0 581 *
michael@0 582 * That logic lives in the TokenServerClientDelegate elsewhere in this file.
michael@0 583 */
michael@0 584
michael@0 585 // Strictly speaking this backoff check could be done prior to walking through
michael@0 586 // the login state machine, allowing us to short-circuit sooner.
michael@0 587 // We don't expect many token server backoffs, and most users will be sitting
michael@0 588 // in the Married state, so instead we simply do this here, once.
michael@0 589 final BackoffHandler tokenBackoffHandler = new PrefsBackoffHandler(sharedPrefs, "token");
michael@0 590 if (!shouldRequestToken(tokenBackoffHandler, extras)) {
michael@0 591 Logger.info(LOG_TAG, "Not syncing (token server).");
michael@0 592 syncDelegate.postponeSync(tokenBackoffHandler.delayMilliseconds());
michael@0 593 return;
michael@0 594 }
michael@0 595
michael@0 596 final SessionCallback sessionCallback = new SessionCallback(syncDelegate, schedulePolicy);
michael@0 597 final KeyBundle syncKeyBundle = married.getSyncKeyBundle();
michael@0 598 final String clientState = married.getClientState();
michael@0 599 syncWithAssertion(audience, assertion, tokenServerEndpointURI, tokenBackoffHandler, sharedPrefs, syncKeyBundle, clientState, sessionCallback, extras);
michael@0 600 } catch (Exception e) {
michael@0 601 syncDelegate.handleError(e);
michael@0 602 return;
michael@0 603 }
michael@0 604 }
michael@0 605 });
michael@0 606
michael@0 607 latch.await();
michael@0 608 } catch (Exception e) {
michael@0 609 Logger.error(LOG_TAG, "Got error syncing.", e);
michael@0 610 syncDelegate.handleError(e);
michael@0 611 }
michael@0 612
michael@0 613 Logger.info(LOG_TAG, "Syncing done.");
michael@0 614 lastSyncRealtimeMillis = SystemClock.elapsedRealtime();
michael@0 615 }
michael@0 616 }

mercurial