michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko.fxa.sync; michael@0: michael@0: import java.net.URI; michael@0: import java.net.URISyntaxException; michael@0: import java.security.NoSuchAlgorithmException; michael@0: import java.util.Collection; michael@0: import java.util.Collections; michael@0: import java.util.EnumSet; michael@0: import java.util.concurrent.CountDownLatch; michael@0: import java.util.concurrent.ExecutorService; michael@0: import java.util.concurrent.Executors; michael@0: michael@0: import org.mozilla.gecko.background.common.log.Logger; michael@0: import org.mozilla.gecko.background.fxa.FxAccountClient; michael@0: import org.mozilla.gecko.background.fxa.FxAccountClient20; michael@0: import org.mozilla.gecko.background.fxa.SkewHandler; michael@0: import org.mozilla.gecko.browserid.BrowserIDKeyPair; michael@0: import org.mozilla.gecko.browserid.JSONWebTokenUtils; michael@0: import org.mozilla.gecko.fxa.FirefoxAccounts; michael@0: import org.mozilla.gecko.fxa.FxAccountConstants; michael@0: import org.mozilla.gecko.fxa.authenticator.AccountPickler; michael@0: import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; michael@0: import org.mozilla.gecko.fxa.authenticator.FxAccountAuthenticator; michael@0: import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine; michael@0: import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.LoginStateMachineDelegate; michael@0: import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition; michael@0: import org.mozilla.gecko.fxa.login.Married; michael@0: import org.mozilla.gecko.fxa.login.State; michael@0: import org.mozilla.gecko.fxa.login.State.StateLabel; michael@0: import org.mozilla.gecko.fxa.login.StateFactory; michael@0: import org.mozilla.gecko.sync.BackoffHandler; michael@0: import org.mozilla.gecko.sync.GlobalSession; michael@0: import org.mozilla.gecko.sync.PrefsBackoffHandler; michael@0: import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate; michael@0: import org.mozilla.gecko.sync.SyncConfiguration; michael@0: import org.mozilla.gecko.sync.ThreadPool; michael@0: import org.mozilla.gecko.sync.Utils; michael@0: import org.mozilla.gecko.sync.crypto.KeyBundle; michael@0: import org.mozilla.gecko.sync.delegates.BaseGlobalSessionCallback; michael@0: import org.mozilla.gecko.sync.delegates.ClientsDataDelegate; michael@0: import org.mozilla.gecko.sync.net.AuthHeaderProvider; michael@0: import org.mozilla.gecko.sync.net.HawkAuthHeaderProvider; michael@0: import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage; michael@0: import org.mozilla.gecko.tokenserver.TokenServerClient; michael@0: import org.mozilla.gecko.tokenserver.TokenServerClientDelegate; michael@0: import org.mozilla.gecko.tokenserver.TokenServerException; michael@0: import org.mozilla.gecko.tokenserver.TokenServerToken; michael@0: michael@0: import android.accounts.Account; michael@0: import android.content.AbstractThreadedSyncAdapter; michael@0: import android.content.ContentProviderClient; michael@0: import android.content.ContentResolver; michael@0: import android.content.Context; michael@0: import android.content.SharedPreferences; michael@0: import android.content.SyncResult; michael@0: import android.os.Bundle; michael@0: import android.os.SystemClock; michael@0: michael@0: public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter { michael@0: private static final String LOG_TAG = FxAccountSyncAdapter.class.getSimpleName(); michael@0: michael@0: public static final String SYNC_EXTRAS_RESPECT_LOCAL_RATE_LIMIT = "respect_local_rate_limit"; michael@0: public static final String SYNC_EXTRAS_RESPECT_REMOTE_SERVER_BACKOFF = "respect_remote_server_backoff"; michael@0: michael@0: protected static final int NOTIFICATION_ID = LOG_TAG.hashCode(); michael@0: michael@0: // Tracks the last seen storage hostname for backoff purposes. michael@0: private static final String PREF_BACKOFF_STORAGE_HOST = "backoffStorageHost"; michael@0: michael@0: // Used to do cheap in-memory rate limiting. Don't sync again if we michael@0: // successfully synced within this duration. michael@0: private static final int MINIMUM_SYNC_DELAY_MILLIS = 15 * 1000; // 15 seconds. michael@0: private volatile long lastSyncRealtimeMillis = 0L; michael@0: michael@0: protected final ExecutorService executor; michael@0: protected final FxAccountNotificationManager notificationManager; michael@0: michael@0: public FxAccountSyncAdapter(Context context, boolean autoInitialize) { michael@0: super(context, autoInitialize); michael@0: this.executor = Executors.newSingleThreadExecutor(); michael@0: this.notificationManager = new FxAccountNotificationManager(NOTIFICATION_ID); michael@0: } michael@0: michael@0: protected static class SyncDelegate { michael@0: protected final CountDownLatch latch; michael@0: protected final SyncResult syncResult; michael@0: protected final AndroidFxAccount fxAccount; michael@0: protected final Collection stageNamesToSync; michael@0: michael@0: public SyncDelegate(CountDownLatch latch, SyncResult syncResult, AndroidFxAccount fxAccount, Collection stageNamesToSync) { michael@0: if (latch == null) { michael@0: throw new IllegalArgumentException("latch must not be null"); michael@0: } michael@0: if (syncResult == null) { michael@0: throw new IllegalArgumentException("syncResult must not be null"); michael@0: } michael@0: if (fxAccount == null) { michael@0: throw new IllegalArgumentException("fxAccount must not be null"); michael@0: } michael@0: this.latch = latch; michael@0: this.syncResult = syncResult; michael@0: this.fxAccount = fxAccount; michael@0: this.stageNamesToSync = Collections.unmodifiableCollection(stageNamesToSync); michael@0: } michael@0: michael@0: /** michael@0: * No error! Say that we made progress. michael@0: */ michael@0: protected void setSyncResultSuccess() { michael@0: syncResult.stats.numUpdates += 1; michael@0: } michael@0: michael@0: /** michael@0: * Soft error. Say that we made progress, so that Android will sync us again michael@0: * after exponential backoff. michael@0: */ michael@0: protected void setSyncResultSoftError() { michael@0: syncResult.stats.numUpdates += 1; michael@0: syncResult.stats.numIoExceptions += 1; michael@0: } michael@0: michael@0: /** michael@0: * Hard error. We don't want Android to sync us again, even if we make michael@0: * progress, until the user intervenes. michael@0: */ michael@0: protected void setSyncResultHardError() { michael@0: syncResult.stats.numAuthExceptions += 1; michael@0: } michael@0: michael@0: public void handleSuccess() { michael@0: Logger.info(LOG_TAG, "Sync succeeded."); michael@0: setSyncResultSuccess(); michael@0: latch.countDown(); michael@0: } michael@0: michael@0: public void handleError(Exception e) { michael@0: Logger.error(LOG_TAG, "Got exception syncing.", e); michael@0: setSyncResultSoftError(); michael@0: // This is awful, but we need to propagate bad assertions back up the michael@0: // chain somehow, and this will do for now. michael@0: if (e instanceof TokenServerException) { michael@0: // We should only get here *after* we're locked into the married state. michael@0: State state = fxAccount.getState(); michael@0: if (state.getStateLabel() == StateLabel.Married) { michael@0: Married married = (Married) state; michael@0: fxAccount.setState(married.makeCohabitingState()); michael@0: } michael@0: } michael@0: latch.countDown(); michael@0: } michael@0: michael@0: /** michael@0: * When the login machine terminates, we might not be in the michael@0: * Married state, and therefore we can't sync. This method michael@0: * messages as much to the user. michael@0: *

michael@0: * To avoid stopping us syncing altogether, we set a soft error rather than michael@0: * a hard error. In future, we would like to set a hard error if we are in, michael@0: * for example, the Separated state, and then have some user michael@0: * initiated activity mark the Android account as ready to sync again. This michael@0: * is tricky, though, so we play it safe for now. michael@0: * michael@0: * @param finalState michael@0: * that login machine ended in. michael@0: */ michael@0: public void handleCannotSync(State finalState) { michael@0: Logger.warn(LOG_TAG, "Cannot sync from state: " + finalState.getStateLabel()); michael@0: setSyncResultSoftError(); michael@0: latch.countDown(); michael@0: } michael@0: michael@0: public void postponeSync(long millis) { michael@0: if (millis <= 0) { michael@0: Logger.debug(LOG_TAG, "Asked to postpone sync, but zero delay. Short-circuiting."); michael@0: } else { michael@0: // delayUntil is broken: https://code.google.com/p/android/issues/detail?id=65669 michael@0: // So we don't bother doing this. Instead, we rely on the periodic sync michael@0: // we schedule, and the backoff handler for the rest. michael@0: /* michael@0: Logger.warn(LOG_TAG, "Postponing sync by " + millis + "ms."); michael@0: syncResult.delayUntil = millis / 1000; michael@0: */ michael@0: } michael@0: setSyncResultSoftError(); michael@0: latch.countDown(); michael@0: } michael@0: michael@0: /** michael@0: * Simply don't sync, without setting any error flags. michael@0: * This is the appropriate behavior when a routine backoff has not yet michael@0: * been met. michael@0: */ michael@0: public void rejectSync() { michael@0: latch.countDown(); michael@0: } michael@0: michael@0: public Collection getStageNamesToSync() { michael@0: return this.stageNamesToSync; michael@0: } michael@0: } michael@0: michael@0: protected static class SessionCallback implements BaseGlobalSessionCallback { michael@0: protected final SyncDelegate syncDelegate; michael@0: protected final SchedulePolicy schedulePolicy; michael@0: protected volatile BackoffHandler storageBackoffHandler; michael@0: michael@0: public SessionCallback(SyncDelegate syncDelegate, SchedulePolicy schedulePolicy) { michael@0: this.syncDelegate = syncDelegate; michael@0: this.schedulePolicy = schedulePolicy; michael@0: } michael@0: michael@0: public void setBackoffHandler(BackoffHandler backoffHandler) { michael@0: this.storageBackoffHandler = backoffHandler; michael@0: } michael@0: michael@0: @Override michael@0: public boolean shouldBackOffStorage() { michael@0: return storageBackoffHandler.delayMilliseconds() > 0; michael@0: } michael@0: michael@0: @Override michael@0: public void requestBackoff(long backoffMillis) { michael@0: final boolean onlyExtend = true; // Because we trust what the storage server says. michael@0: schedulePolicy.configureBackoffMillisOnBackoff(storageBackoffHandler, backoffMillis, onlyExtend); michael@0: } michael@0: michael@0: @Override michael@0: public void informUpgradeRequiredResponse(GlobalSession session) { michael@0: schedulePolicy.onUpgradeRequired(); michael@0: } michael@0: michael@0: @Override michael@0: public void informUnauthorizedResponse(GlobalSession globalSession, URI oldClusterURL) { michael@0: schedulePolicy.onUnauthorized(); michael@0: } michael@0: michael@0: @Override michael@0: public void handleStageCompleted(Stage currentState, GlobalSession globalSession) { michael@0: } michael@0: michael@0: @Override michael@0: public void handleSuccess(GlobalSession globalSession) { michael@0: Logger.info(LOG_TAG, "Global session succeeded."); michael@0: michael@0: // Get the number of clients, so we can schedule the sync interval accordingly. michael@0: try { michael@0: int otherClientsCount = globalSession.getClientsDelegate().getClientsCount(); michael@0: Logger.debug(LOG_TAG, "" + otherClientsCount + " other client(s)."); michael@0: this.schedulePolicy.onSuccessfulSync(otherClientsCount); michael@0: } finally { michael@0: // Continue with the usual success flow. michael@0: syncDelegate.handleSuccess(); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void handleError(GlobalSession globalSession, Exception e) { michael@0: Logger.warn(LOG_TAG, "Global session failed."); // Exception will be dumped by delegate below. michael@0: syncDelegate.handleError(e); michael@0: // TODO: should we reduce the periodic sync interval? michael@0: } michael@0: michael@0: @Override michael@0: public void handleAborted(GlobalSession globalSession, String reason) { michael@0: Logger.warn(LOG_TAG, "Global session aborted: " + reason); michael@0: syncDelegate.handleError(null); michael@0: // TODO: should we reduce the periodic sync interval? michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Return true if the provided {@link BackoffHandler} isn't reporting that we're in michael@0: * a backoff state, or the provided {@link Bundle} contains flags that indicate michael@0: * we should force a sync. michael@0: */ michael@0: private boolean shouldPerformSync(final BackoffHandler backoffHandler, final String kind, final Bundle extras) { michael@0: final long delay = backoffHandler.delayMilliseconds(); michael@0: if (delay <= 0) { michael@0: return true; michael@0: } michael@0: michael@0: if (extras == null) { michael@0: return false; michael@0: } michael@0: michael@0: final boolean forced = extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false); michael@0: if (forced) { michael@0: Logger.info(LOG_TAG, "Forced sync (" + kind + "): overruling remaining backoff of " + delay + "ms."); michael@0: } else { michael@0: Logger.info(LOG_TAG, "Not syncing (" + kind + "): must wait another " + delay + "ms."); michael@0: } michael@0: return forced; michael@0: } michael@0: michael@0: protected void syncWithAssertion(final String audience, michael@0: final String assertion, michael@0: final URI tokenServerEndpointURI, michael@0: final BackoffHandler tokenBackoffHandler, michael@0: final SharedPreferences sharedPrefs, michael@0: final KeyBundle syncKeyBundle, michael@0: final String clientState, michael@0: final SessionCallback callback, michael@0: final Bundle extras) { michael@0: final TokenServerClientDelegate delegate = new TokenServerClientDelegate() { michael@0: private boolean didReceiveBackoff = false; michael@0: michael@0: @Override michael@0: public String getUserAgent() { michael@0: return FxAccountConstants.USER_AGENT; michael@0: } michael@0: michael@0: @Override michael@0: public void handleSuccess(final TokenServerToken token) { michael@0: FxAccountConstants.pii(LOG_TAG, "Got token! uid is " + token.uid + " and endpoint is " + token.endpoint + "."); michael@0: michael@0: if (!didReceiveBackoff) { michael@0: // We must be OK to touch this token server. michael@0: tokenBackoffHandler.setEarliestNextRequest(0L); michael@0: } michael@0: michael@0: final URI storageServerURI; michael@0: try { michael@0: storageServerURI = new URI(token.endpoint); michael@0: } catch (URISyntaxException e) { michael@0: handleError(e); michael@0: return; michael@0: } michael@0: final String storageHostname = storageServerURI.getHost(); michael@0: michael@0: // We back off on a per-host basis. When we have an endpoint URI from a token, we michael@0: // can check on the backoff status for that host. michael@0: // If we're supposed to be backing off, we abort the not-yet-started session. michael@0: final BackoffHandler storageBackoffHandler = new PrefsBackoffHandler(sharedPrefs, "sync.storage"); michael@0: callback.setBackoffHandler(storageBackoffHandler); michael@0: michael@0: String lastStorageHost = sharedPrefs.getString(PREF_BACKOFF_STORAGE_HOST, null); michael@0: final boolean storageHostIsUnchanged = lastStorageHost != null && michael@0: lastStorageHost.equalsIgnoreCase(storageHostname); michael@0: if (storageHostIsUnchanged) { michael@0: Logger.debug(LOG_TAG, "Storage host is unchanged."); michael@0: if (!shouldPerformSync(storageBackoffHandler, "storage", extras)) { michael@0: Logger.info(LOG_TAG, "Not syncing: storage server requested backoff."); michael@0: callback.handleAborted(null, "Storage backoff"); michael@0: return; michael@0: } michael@0: } else { michael@0: Logger.debug(LOG_TAG, "Received new storage host."); michael@0: } michael@0: michael@0: // Invalidate the previous backoff, because our storage host has changed, michael@0: // or we never had one at all, or we're OK to sync. michael@0: storageBackoffHandler.setEarliestNextRequest(0L); michael@0: michael@0: FxAccountGlobalSession globalSession = null; michael@0: try { michael@0: ClientsDataDelegate clientsDataDelegate = new SharedPreferencesClientsDataDelegate(sharedPrefs); michael@0: michael@0: // We compute skew over time using SkewHandler. This yields an unchanging michael@0: // skew adjustment that the HawkAuthHeaderProvider uses to adjust its michael@0: // timestamps. Eventually we might want this to adapt within the scope of a michael@0: // global session. michael@0: final SkewHandler storageServerSkewHandler = SkewHandler.getSkewHandlerForHostname(storageHostname); michael@0: final long storageServerSkew = storageServerSkewHandler.getSkewInSeconds(); michael@0: // We expect Sync to upload large sets of records. Calculating the michael@0: // payload verification hash for these record sets could be expensive, michael@0: // so we explicitly do not send payload verification hashes to the michael@0: // Sync storage endpoint. michael@0: final boolean includePayloadVerificationHash = false; michael@0: final AuthHeaderProvider authHeaderProvider = new HawkAuthHeaderProvider(token.id, token.key.getBytes("UTF-8"), includePayloadVerificationHash, storageServerSkew); michael@0: michael@0: final Context context = getContext(); michael@0: final SyncConfiguration syncConfig = new SyncConfiguration(token.uid, authHeaderProvider, sharedPrefs, syncKeyBundle); michael@0: michael@0: Collection knownStageNames = SyncConfiguration.validEngineNames(); michael@0: syncConfig.stagesToSync = Utils.getStagesToSyncFromBundle(knownStageNames, extras); michael@0: syncConfig.setClusterURL(storageServerURI); michael@0: michael@0: globalSession = new FxAccountGlobalSession(syncConfig, callback, context, clientsDataDelegate); michael@0: globalSession.start(); michael@0: } catch (Exception e) { michael@0: callback.handleError(globalSession, e); michael@0: return; michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void handleFailure(TokenServerException e) { michael@0: handleError(e); michael@0: } michael@0: michael@0: @Override michael@0: public void handleError(Exception e) { michael@0: Logger.error(LOG_TAG, "Failed to get token.", e); michael@0: callback.handleError(null, e); michael@0: } michael@0: michael@0: @Override michael@0: public void handleBackoff(int backoffSeconds) { michael@0: // This is the token server telling us to back off. michael@0: Logger.info(LOG_TAG, "Token server requesting backoff of " + backoffSeconds + "s. Backoff handler: " + tokenBackoffHandler); michael@0: didReceiveBackoff = true; michael@0: michael@0: // If we've already stored a backoff, overrule it: we only use the server michael@0: // value for token server scheduling. michael@0: tokenBackoffHandler.setEarliestNextRequest(delay(backoffSeconds * 1000)); michael@0: } michael@0: michael@0: private long delay(long delay) { michael@0: return System.currentTimeMillis() + delay; michael@0: } michael@0: }; michael@0: michael@0: TokenServerClient tokenServerclient = new TokenServerClient(tokenServerEndpointURI, executor); michael@0: tokenServerclient.getTokenFromBrowserIDAssertion(assertion, true, clientState, delegate); michael@0: } michael@0: michael@0: /** michael@0: * A trivial Sync implementation that does not cache client keys, michael@0: * certificates, or tokens. michael@0: * michael@0: * This should be replaced with a full {@link FxAccountAuthenticator}-based michael@0: * token implementation. michael@0: */ michael@0: @Override michael@0: public void onPerformSync(final Account account, final Bundle extras, final String authority, ContentProviderClient provider, final SyncResult syncResult) { michael@0: Logger.setThreadLogTag(FxAccountConstants.GLOBAL_LOG_TAG); michael@0: Logger.resetLogging(); michael@0: michael@0: Logger.info(LOG_TAG, "Syncing FxAccount" + michael@0: " account named like " + Utils.obfuscateEmail(account.name) + michael@0: " for authority " + authority + michael@0: " with instance " + this + "."); michael@0: michael@0: final EnumSet syncHints = FirefoxAccounts.getHintsToSyncFromBundle(extras); michael@0: FirefoxAccounts.logSyncHints(syncHints); michael@0: michael@0: // This applies even to forced syncs, but only on success. michael@0: if (this.lastSyncRealtimeMillis > 0L && michael@0: (this.lastSyncRealtimeMillis + MINIMUM_SYNC_DELAY_MILLIS) > SystemClock.elapsedRealtime()) { michael@0: Logger.info(LOG_TAG, "Not syncing FxAccount " + Utils.obfuscateEmail(account.name) + michael@0: ": minimum interval not met."); michael@0: return; michael@0: } michael@0: michael@0: final Context context = getContext(); michael@0: final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account); michael@0: if (FxAccountConstants.LOG_PERSONAL_INFORMATION) { michael@0: fxAccount.dump(); michael@0: } michael@0: michael@0: // Pickle in a background thread to avoid strict mode warnings. michael@0: ThreadPool.run(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: try { michael@0: AccountPickler.pickle(fxAccount, FxAccountConstants.ACCOUNT_PICKLE_FILENAME); michael@0: } catch (Exception e) { michael@0: // Should never happen, but we really don't want to die in a background thread. michael@0: Logger.warn(LOG_TAG, "Got exception pickling current account details; ignoring.", e); michael@0: } michael@0: } michael@0: }); michael@0: michael@0: final CountDownLatch latch = new CountDownLatch(1); michael@0: michael@0: Collection knownStageNames = SyncConfiguration.validEngineNames(); michael@0: Collection stageNamesToSync = Utils.getStagesToSyncFromBundle(knownStageNames, extras); michael@0: michael@0: final SyncDelegate syncDelegate = new SyncDelegate(latch, syncResult, fxAccount, stageNamesToSync); michael@0: michael@0: try { michael@0: final State state; michael@0: try { michael@0: state = fxAccount.getState(); michael@0: } catch (Exception e) { michael@0: syncDelegate.handleError(e); michael@0: return; michael@0: } michael@0: michael@0: // This will be the same chunk of SharedPreferences that we pass through to GlobalSession/SyncConfiguration. michael@0: final SharedPreferences sharedPrefs = fxAccount.getSyncPrefs(); michael@0: michael@0: final BackoffHandler backgroundBackoffHandler = new PrefsBackoffHandler(sharedPrefs, "background"); michael@0: final BackoffHandler rateLimitBackoffHandler = new PrefsBackoffHandler(sharedPrefs, "rate"); michael@0: michael@0: // If this sync was triggered by user action, this will be true. michael@0: final boolean isImmediate = (extras != null) && michael@0: (extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, false) || michael@0: extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false)); michael@0: michael@0: // If it's not an immediate sync, it must be either periodic or tickled. michael@0: // Check our background rate limiter. michael@0: if (!isImmediate) { michael@0: if (!shouldPerformSync(backgroundBackoffHandler, "background", extras)) { michael@0: syncDelegate.rejectSync(); michael@0: return; michael@0: } michael@0: } michael@0: michael@0: // Regardless, let's make sure we're not syncing too often. michael@0: if (!shouldPerformSync(rateLimitBackoffHandler, "rate", extras)) { michael@0: syncDelegate.postponeSync(rateLimitBackoffHandler.delayMilliseconds()); michael@0: return; michael@0: } michael@0: michael@0: final SchedulePolicy schedulePolicy = new FxAccountSchedulePolicy(context, fxAccount); michael@0: michael@0: // Set a small scheduled 'backoff' to rate-limit the next sync, michael@0: // and extend the background delay even further into the future. michael@0: schedulePolicy.configureBackoffMillisBeforeSyncing(rateLimitBackoffHandler, backgroundBackoffHandler); michael@0: michael@0: final String audience = fxAccount.getAudience(); michael@0: final String authServerEndpoint = fxAccount.getAccountServerURI(); michael@0: final String tokenServerEndpoint = fxAccount.getTokenServerURI(); michael@0: final URI tokenServerEndpointURI = new URI(tokenServerEndpoint); michael@0: michael@0: // TODO: why doesn't the loginPolicy extract the audience from the account? michael@0: final FxAccountClient client = new FxAccountClient20(authServerEndpoint, executor); michael@0: final FxAccountLoginStateMachine stateMachine = new FxAccountLoginStateMachine(); michael@0: stateMachine.advance(state, StateLabel.Married, new LoginStateMachineDelegate() { michael@0: @Override michael@0: public FxAccountClient getClient() { michael@0: return client; michael@0: } michael@0: michael@0: @Override michael@0: public long getCertificateDurationInMilliseconds() { michael@0: return 12 * 60 * 60 * 1000; michael@0: } michael@0: michael@0: @Override michael@0: public long getAssertionDurationInMilliseconds() { michael@0: return 15 * 60 * 1000; michael@0: } michael@0: michael@0: @Override michael@0: public BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException { michael@0: return StateFactory.generateKeyPair(); michael@0: } michael@0: michael@0: @Override michael@0: public void handleTransition(Transition transition, State state) { michael@0: Logger.info(LOG_TAG, "handleTransition: " + transition + " to " + state.getStateLabel()); michael@0: } michael@0: michael@0: private boolean shouldRequestToken(final BackoffHandler tokenBackoffHandler, final Bundle extras) { michael@0: return shouldPerformSync(tokenBackoffHandler, "token", extras); michael@0: } michael@0: michael@0: @Override michael@0: public void handleFinal(State state) { michael@0: Logger.info(LOG_TAG, "handleFinal: in " + state.getStateLabel()); michael@0: fxAccount.setState(state); michael@0: schedulePolicy.onHandleFinal(state.getNeededAction()); michael@0: notificationManager.update(context, fxAccount); michael@0: try { michael@0: if (state.getStateLabel() != StateLabel.Married) { michael@0: syncDelegate.handleCannotSync(state); michael@0: return; michael@0: } michael@0: michael@0: final Married married = (Married) state; michael@0: final String assertion = married.generateAssertion(audience, JSONWebTokenUtils.DEFAULT_ASSERTION_ISSUER); michael@0: michael@0: /* michael@0: * At this point we're in the correct state to sync, and we're ready to fetch michael@0: * a token and do some work. michael@0: * michael@0: * But first we need to do two things: michael@0: * 1. Check to see whether we're in a backoff situation for the token server. michael@0: * If we are, but we're not forcing a sync, then we go no further. michael@0: * 2. Clear an existing backoff (if we're syncing it doesn't matter, and if michael@0: * we're forcing we'll get a new backoff if things are still bad). michael@0: * michael@0: * Note that we don't check the storage backoff before the token dance: the token michael@0: * server tells us which server we're syncing to! michael@0: * michael@0: * That logic lives in the TokenServerClientDelegate elsewhere in this file. michael@0: */ michael@0: michael@0: // Strictly speaking this backoff check could be done prior to walking through michael@0: // the login state machine, allowing us to short-circuit sooner. michael@0: // We don't expect many token server backoffs, and most users will be sitting michael@0: // in the Married state, so instead we simply do this here, once. michael@0: final BackoffHandler tokenBackoffHandler = new PrefsBackoffHandler(sharedPrefs, "token"); michael@0: if (!shouldRequestToken(tokenBackoffHandler, extras)) { michael@0: Logger.info(LOG_TAG, "Not syncing (token server)."); michael@0: syncDelegate.postponeSync(tokenBackoffHandler.delayMilliseconds()); michael@0: return; michael@0: } michael@0: michael@0: final SessionCallback sessionCallback = new SessionCallback(syncDelegate, schedulePolicy); michael@0: final KeyBundle syncKeyBundle = married.getSyncKeyBundle(); michael@0: final String clientState = married.getClientState(); michael@0: syncWithAssertion(audience, assertion, tokenServerEndpointURI, tokenBackoffHandler, sharedPrefs, syncKeyBundle, clientState, sessionCallback, extras); michael@0: } catch (Exception e) { michael@0: syncDelegate.handleError(e); michael@0: return; michael@0: } michael@0: } michael@0: }); michael@0: michael@0: latch.await(); michael@0: } catch (Exception e) { michael@0: Logger.error(LOG_TAG, "Got error syncing.", e); michael@0: syncDelegate.handleError(e); michael@0: } michael@0: michael@0: Logger.info(LOG_TAG, "Syncing done."); michael@0: lastSyncRealtimeMillis = SystemClock.elapsedRealtime(); michael@0: } michael@0: }