diff options
Diffstat (limited to 'mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncAdapter.java')
-rw-r--r-- | mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncAdapter.java | 568 |
1 files changed, 0 insertions, 568 deletions
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncAdapter.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncAdapter.java deleted file mode 100644 index 30990cf7f..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncAdapter.java +++ /dev/null @@ -1,568 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa.sync; - -import android.accounts.Account; -import android.content.AbstractThreadedSyncAdapter; -import android.content.ContentProviderClient; -import android.content.ContentResolver; -import android.content.Context; -import android.content.SharedPreferences; -import android.content.SyncResult; -import android.os.Bundle; -import android.os.SystemClock; -import android.text.TextUtils; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.background.common.telemetry.TelemetryWrapper; -import org.mozilla.gecko.background.fxa.FxAccountUtils; -import org.mozilla.gecko.background.fxa.SkewHandler; -import org.mozilla.gecko.browserid.JSONWebTokenUtils; -import org.mozilla.gecko.fxa.FirefoxAccounts; -import org.mozilla.gecko.fxa.FxAccountConstants; -import org.mozilla.gecko.fxa.FxAccountDeviceRegistrator; -import org.mozilla.gecko.fxa.authenticator.AccountPickler; -import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; -import org.mozilla.gecko.fxa.authenticator.FxADefaultLoginStateMachineDelegate; -import org.mozilla.gecko.fxa.authenticator.FxAccountAuthenticator; -import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine; -import org.mozilla.gecko.fxa.login.Married; -import org.mozilla.gecko.fxa.login.State; -import org.mozilla.gecko.fxa.login.State.StateLabel; -import org.mozilla.gecko.fxa.sync.FxAccountSyncDelegate.Result; -import org.mozilla.gecko.sync.BackoffHandler; -import org.mozilla.gecko.sync.GlobalSession; -import org.mozilla.gecko.sync.PrefsBackoffHandler; -import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate; -import org.mozilla.gecko.sync.SyncConfiguration; -import org.mozilla.gecko.sync.ThreadPool; -import org.mozilla.gecko.sync.Utils; -import org.mozilla.gecko.sync.crypto.KeyBundle; -import org.mozilla.gecko.sync.delegates.GlobalSessionCallback; -import org.mozilla.gecko.sync.delegates.ClientsDataDelegate; -import org.mozilla.gecko.sync.net.AuthHeaderProvider; -import org.mozilla.gecko.sync.net.HawkAuthHeaderProvider; -import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage; -import org.mozilla.gecko.sync.telemetry.TelemetryContract; -import org.mozilla.gecko.tokenserver.TokenServerClient; -import org.mozilla.gecko.tokenserver.TokenServerClientDelegate; -import org.mozilla.gecko.tokenserver.TokenServerException; -import org.mozilla.gecko.tokenserver.TokenServerToken; - -import java.net.URI; -import java.net.URISyntaxException; -import java.util.Collection; -import java.util.Collections; -import java.util.EnumSet; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.LinkedBlockingQueue; - -public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter { - private static final String LOG_TAG = FxAccountSyncAdapter.class.getSimpleName(); - - public static final int NOTIFICATION_ID = LOG_TAG.hashCode(); - - // Tracks the last seen storage hostname for backoff purposes. - private static final String PREF_BACKOFF_STORAGE_HOST = "backoffStorageHost"; - - // Used to do cheap in-memory rate limiting. Don't sync again if we - // successfully synced within this duration. - private static final int MINIMUM_SYNC_DELAY_MILLIS = 15 * 1000; // 15 seconds. - private volatile long lastSyncRealtimeMillis; - - protected final ExecutorService executor; - protected final FxAccountNotificationManager notificationManager; - - public FxAccountSyncAdapter(Context context, boolean autoInitialize) { - super(context, autoInitialize); - this.executor = Executors.newSingleThreadExecutor(); - this.notificationManager = new FxAccountNotificationManager(NOTIFICATION_ID); - } - - protected static class SyncDelegate extends FxAccountSyncDelegate { - @Override - public void handleSuccess() { - Logger.info(LOG_TAG, "Sync succeeded."); - super.handleSuccess(); - TelemetryWrapper.addToHistogram(TelemetryContract.SYNC_COMPLETED, 1); - } - - @Override - public void handleError(Exception e) { - Logger.error(LOG_TAG, "Got exception syncing.", e); - super.handleError(e); - TelemetryWrapper.addToHistogram(TelemetryContract.SYNC_FAILED, 1); - } - - @Override - public void handleCannotSync(State finalState) { - Logger.warn(LOG_TAG, "Cannot sync from state: " + finalState.getStateLabel()); - super.handleCannotSync(finalState); - } - - @Override - public void postponeSync(long millis) { - if (millis <= 0) { - Logger.debug(LOG_TAG, "Asked to postpone sync, but zero delay."); - } - super.postponeSync(millis); - } - - @Override - public void rejectSync() { - super.rejectSync(); - } - - protected final Collection<String> stageNamesToSync; - - public SyncDelegate(BlockingQueue<Result> latch, SyncResult syncResult, AndroidFxAccount fxAccount, Collection<String> stageNamesToSync) { - super(latch, syncResult); - this.stageNamesToSync = Collections.unmodifiableCollection(stageNamesToSync); - } - - public Collection<String> getStageNamesToSync() { - return this.stageNamesToSync; - } - } - - protected static class SessionCallback implements GlobalSessionCallback { - protected final SyncDelegate syncDelegate; - protected final SchedulePolicy schedulePolicy; - protected volatile BackoffHandler storageBackoffHandler; - - public SessionCallback(SyncDelegate syncDelegate, SchedulePolicy schedulePolicy) { - this.syncDelegate = syncDelegate; - this.schedulePolicy = schedulePolicy; - } - - public void setBackoffHandler(BackoffHandler backoffHandler) { - this.storageBackoffHandler = backoffHandler; - } - - @Override - public boolean shouldBackOffStorage() { - return storageBackoffHandler.delayMilliseconds() > 0; - } - - @Override - public void requestBackoff(long backoffMillis) { - final boolean onlyExtend = true; // Because we trust what the storage server says. - schedulePolicy.configureBackoffMillisOnBackoff(storageBackoffHandler, backoffMillis, onlyExtend); - } - - @Override - public void informUpgradeRequiredResponse(GlobalSession session) { - schedulePolicy.onUpgradeRequired(); - } - - @Override - public void informUnauthorizedResponse(GlobalSession globalSession, URI oldClusterURL) { - schedulePolicy.onUnauthorized(); - } - - @Override - public void informMigrated(GlobalSession globalSession) { - // It's not possible to migrate a Firefox Account to another Account type - // yet. Yell loudly but otherwise ignore. - Logger.error(LOG_TAG, - "Firefox Account informMigrated called, but it's not yet possible to migrate. " + - "Ignoring even though something is terribly wrong."); - } - - @Override - public void handleStageCompleted(Stage currentState, GlobalSession globalSession) { - } - - @Override - public void handleSuccess(GlobalSession globalSession) { - Logger.info(LOG_TAG, "Global session succeeded."); - - // Get the number of clients, so we can schedule the sync interval accordingly. - try { - int otherClientsCount = globalSession.getClientsDelegate().getClientsCount(); - Logger.debug(LOG_TAG, "" + otherClientsCount + " other client(s)."); - this.schedulePolicy.onSuccessfulSync(otherClientsCount); - } finally { - // Continue with the usual success flow. - syncDelegate.handleSuccess(); - } - } - - @Override - public void handleError(GlobalSession globalSession, Exception e) { - Logger.warn(LOG_TAG, "Global session failed."); // Exception will be dumped by delegate below. - syncDelegate.handleError(e); - // TODO: should we reduce the periodic sync interval? - } - - @Override - public void handleAborted(GlobalSession globalSession, String reason) { - Logger.warn(LOG_TAG, "Global session aborted: " + reason); - syncDelegate.handleError(null); - // TODO: should we reduce the periodic sync interval? - } - }; - - /** - * Return true if the provided {@link BackoffHandler} isn't reporting that we're in - * a backoff state, or the provided {@link Bundle} contains flags that indicate - * we should force a sync. - */ - private boolean shouldPerformSync(final BackoffHandler backoffHandler, final String kind, final Bundle extras) { - final long delay = backoffHandler.delayMilliseconds(); - if (delay <= 0) { - return true; - } - - if (extras == null) { - return false; - } - - final boolean forced = extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false); - if (forced) { - Logger.info(LOG_TAG, "Forced sync (" + kind + "): overruling remaining backoff of " + delay + "ms."); - } else { - Logger.info(LOG_TAG, "Not syncing (" + kind + "): must wait another " + delay + "ms."); - } - return forced; - } - - protected void syncWithAssertion(final String audience, - final String assertion, - final URI tokenServerEndpointURI, - final BackoffHandler tokenBackoffHandler, - final SharedPreferences sharedPrefs, - final KeyBundle syncKeyBundle, - final String clientState, - final SessionCallback callback, - final Bundle extras, - final AndroidFxAccount fxAccount) { - final TokenServerClientDelegate delegate = new TokenServerClientDelegate() { - private boolean didReceiveBackoff = false; - - @Override - public String getUserAgent() { - return FxAccountConstants.USER_AGENT; - } - - @Override - public void handleSuccess(final TokenServerToken token) { - FxAccountUtils.pii(LOG_TAG, "Got token! uid is " + token.uid + " and endpoint is " + token.endpoint + "."); - fxAccount.releaseSharedAccountStateLock(); - - if (!didReceiveBackoff) { - // We must be OK to touch this token server. - tokenBackoffHandler.setEarliestNextRequest(0L); - } - - final URI storageServerURI; - try { - storageServerURI = new URI(token.endpoint); - } catch (URISyntaxException e) { - handleError(e); - return; - } - final String storageHostname = storageServerURI.getHost(); - - // We back off on a per-host basis. When we have an endpoint URI from a token, we - // can check on the backoff status for that host. - // If we're supposed to be backing off, we abort the not-yet-started session. - final BackoffHandler storageBackoffHandler = new PrefsBackoffHandler(sharedPrefs, "sync.storage"); - callback.setBackoffHandler(storageBackoffHandler); - - String lastStorageHost = sharedPrefs.getString(PREF_BACKOFF_STORAGE_HOST, null); - final boolean storageHostIsUnchanged = lastStorageHost != null && - lastStorageHost.equalsIgnoreCase(storageHostname); - if (storageHostIsUnchanged) { - Logger.debug(LOG_TAG, "Storage host is unchanged."); - if (!shouldPerformSync(storageBackoffHandler, "storage", extras)) { - Logger.info(LOG_TAG, "Not syncing: storage server requested backoff."); - callback.handleAborted(null, "Storage backoff"); - return; - } - } else { - Logger.debug(LOG_TAG, "Received new storage host."); - } - - // Invalidate the previous backoff, because our storage host has changed, - // or we never had one at all, or we're OK to sync. - storageBackoffHandler.setEarliestNextRequest(0L); - - GlobalSession globalSession = null; - try { - final ClientsDataDelegate clientsDataDelegate = new SharedPreferencesClientsDataDelegate(sharedPrefs, getContext()); - if (FxAccountUtils.LOG_PERSONAL_INFORMATION) { - FxAccountUtils.pii(LOG_TAG, "Client device name is: '" + clientsDataDelegate.getClientName() + "'."); - FxAccountUtils.pii(LOG_TAG, "Client device data last modified: " + clientsDataDelegate.getLastModifiedTimestamp()); - } - - // We compute skew over time using SkewHandler. This yields an unchanging - // skew adjustment that the HawkAuthHeaderProvider uses to adjust its - // timestamps. Eventually we might want this to adapt within the scope of a - // global session. - final SkewHandler storageServerSkewHandler = SkewHandler.getSkewHandlerForHostname(storageHostname); - final long storageServerSkew = storageServerSkewHandler.getSkewInSeconds(); - // We expect Sync to upload large sets of records. Calculating the - // payload verification hash for these record sets could be expensive, - // so we explicitly do not send payload verification hashes to the - // Sync storage endpoint. - final boolean includePayloadVerificationHash = false; - final AuthHeaderProvider authHeaderProvider = new HawkAuthHeaderProvider(token.id, token.key.getBytes("UTF-8"), includePayloadVerificationHash, storageServerSkew); - - final Context context = getContext(); - final SyncConfiguration syncConfig = new SyncConfiguration(token.uid, authHeaderProvider, sharedPrefs, syncKeyBundle); - - Collection<String> knownStageNames = SyncConfiguration.validEngineNames(); - syncConfig.stagesToSync = Utils.getStagesToSyncFromBundle(knownStageNames, extras); - syncConfig.setClusterURL(storageServerURI); - - globalSession = new GlobalSession(syncConfig, callback, context, clientsDataDelegate); - globalSession.start(); - } catch (Exception e) { - callback.handleError(globalSession, e); - return; - } - } - - @Override - public void handleFailure(TokenServerException e) { - Logger.error(LOG_TAG, "Failed to get token.", e); - try { - // We should only get here *after* we're locked into the married state. - State state = fxAccount.getState(); - if (state.getStateLabel() == StateLabel.Married) { - Married married = (Married) state; - fxAccount.setState(married.makeCohabitingState()); - } - } finally { - fxAccount.releaseSharedAccountStateLock(); - } - callback.handleError(null, e); - } - - @Override - public void handleError(Exception e) { - Logger.error(LOG_TAG, "Failed to get token.", e); - fxAccount.releaseSharedAccountStateLock(); - callback.handleError(null, e); - } - - @Override - public void handleBackoff(int backoffSeconds) { - // This is the token server telling us to back off. - Logger.info(LOG_TAG, "Token server requesting backoff of " + backoffSeconds + "s. Backoff handler: " + tokenBackoffHandler); - didReceiveBackoff = true; - - // If we've already stored a backoff, overrule it: we only use the server - // value for token server scheduling. - tokenBackoffHandler.setEarliestNextRequest(delay(backoffSeconds * 1000)); - } - - private long delay(long delay) { - return System.currentTimeMillis() + delay; - } - }; - - TokenServerClient tokenServerclient = new TokenServerClient(tokenServerEndpointURI, executor); - tokenServerclient.getTokenFromBrowserIDAssertion(assertion, true, clientState, delegate); - } - - /** - * A trivial Sync implementation that does not cache client keys, - * certificates, or tokens. - * - * This should be replaced with a full {@link FxAccountAuthenticator}-based - * token implementation. - */ - @Override - public void onPerformSync(final Account account, final Bundle extras, final String authority, ContentProviderClient provider, final SyncResult syncResult) { - Logger.setThreadLogTag(FxAccountConstants.GLOBAL_LOG_TAG); - Logger.resetLogging(); - - final Context context = getContext(); - final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account); - - Logger.info(LOG_TAG, "Syncing FxAccount" + - " account named like " + Utils.obfuscateEmail(account.name) + - " for authority " + authority + - " with instance " + this + "."); - - Logger.info(LOG_TAG, "Account last synced at: " + fxAccount.getLastSyncedTimestamp()); - - if (FxAccountUtils.LOG_PERSONAL_INFORMATION) { - fxAccount.dump(); - } - - FirefoxAccounts.logSyncOptions(extras); - - if (this.lastSyncRealtimeMillis > 0L && - (this.lastSyncRealtimeMillis + MINIMUM_SYNC_DELAY_MILLIS) > SystemClock.elapsedRealtime() && - !extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false)) { - Logger.info(LOG_TAG, "Not syncing FxAccount " + Utils.obfuscateEmail(account.name) + - ": minimum interval not met."); - TelemetryWrapper.addToHistogram(TelemetryContract.SYNC_FAILED_BACKOFF, 1); - return; - } - - // Pickle in a background thread to avoid strict mode warnings. - ThreadPool.run(new Runnable() { - @Override - public void run() { - try { - AccountPickler.pickle(fxAccount, FxAccountConstants.ACCOUNT_PICKLE_FILENAME); - } catch (Exception e) { - // Should never happen, but we really don't want to die in a background thread. - Logger.warn(LOG_TAG, "Got exception pickling current account details; ignoring.", e); - } - } - }); - - final BlockingQueue<Result> latch = new LinkedBlockingQueue<>(1); - - Collection<String> knownStageNames = SyncConfiguration.validEngineNames(); - Collection<String> stageNamesToSync = Utils.getStagesToSyncFromBundle(knownStageNames, extras); - - final SyncDelegate syncDelegate = new SyncDelegate(latch, syncResult, fxAccount, stageNamesToSync); - - try { - // This will be the same chunk of SharedPreferences that we pass through to GlobalSession/SyncConfiguration. - final SharedPreferences sharedPrefs = fxAccount.getSyncPrefs(); - - final BackoffHandler backgroundBackoffHandler = new PrefsBackoffHandler(sharedPrefs, "background"); - final BackoffHandler rateLimitBackoffHandler = new PrefsBackoffHandler(sharedPrefs, "rate"); - - // If this sync was triggered by user action, this will be true. - final boolean isImmediate = (extras != null) && - (extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, false) || - extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false)); - - // If it's not an immediate sync, it must be either periodic or tickled. - // Check our background rate limiter. - if (!isImmediate) { - if (!shouldPerformSync(backgroundBackoffHandler, "background", extras)) { - syncDelegate.rejectSync(); - return; - } - } - - // Regardless, let's make sure we're not syncing too often. - if (!shouldPerformSync(rateLimitBackoffHandler, "rate", extras)) { - syncDelegate.postponeSync(rateLimitBackoffHandler.delayMilliseconds()); - return; - } - - final SchedulePolicy schedulePolicy = new FxAccountSchedulePolicy(context, fxAccount); - - // Set a small scheduled 'backoff' to rate-limit the next sync, - // and extend the background delay even further into the future. - schedulePolicy.configureBackoffMillisBeforeSyncing(rateLimitBackoffHandler, backgroundBackoffHandler); - - final String tokenServerEndpoint = fxAccount.getTokenServerURI(); - final URI tokenServerEndpointURI = new URI(tokenServerEndpoint); - final String audience = FxAccountUtils.getAudienceForURL(tokenServerEndpoint); - - try { - // The clock starts... now! - fxAccount.acquireSharedAccountStateLock(FxAccountSyncAdapter.LOG_TAG); - } catch (InterruptedException e) { - // OK, skip this sync. - syncDelegate.handleError(e); - return; - } - - final State state; - try { - state = fxAccount.getState(); - } catch (Exception e) { - fxAccount.releaseSharedAccountStateLock(); - syncDelegate.handleError(e); - return; - } - - TelemetryWrapper.addToHistogram(TelemetryContract.SYNC_STARTED, 1); - - final FxAccountLoginStateMachine stateMachine = new FxAccountLoginStateMachine(); - stateMachine.advance(state, StateLabel.Married, new FxADefaultLoginStateMachineDelegate(context, fxAccount) { - @Override - public void handleNotMarried(State notMarried) { - Logger.info(LOG_TAG, "handleNotMarried: in " + notMarried.getStateLabel()); - schedulePolicy.onHandleFinal(notMarried.getNeededAction()); - syncDelegate.handleCannotSync(notMarried); - } - - private boolean shouldRequestToken(final BackoffHandler tokenBackoffHandler, final Bundle extras) { - return shouldPerformSync(tokenBackoffHandler, "token", extras); - } - - @Override - public void handleMarried(Married married) { - schedulePolicy.onHandleFinal(married.getNeededAction()); - Logger.info(LOG_TAG, "handleMarried: in " + married.getStateLabel()); - - try { - final String assertion = married.generateAssertion(audience, JSONWebTokenUtils.DEFAULT_ASSERTION_ISSUER); - - /* - * At this point we're in the correct state to sync, and we're ready to fetch - * a token and do some work. - * - * But first we need to do two things: - * 1. Check to see whether we're in a backoff situation for the token server. - * If we are, but we're not forcing a sync, then we go no further. - * 2. Clear an existing backoff (if we're syncing it doesn't matter, and if - * we're forcing we'll get a new backoff if things are still bad). - * - * Note that we don't check the storage backoff before the token dance: the token - * server tells us which server we're syncing to! - * - * That logic lives in the TokenServerClientDelegate elsewhere in this file. - */ - - // Strictly speaking this backoff check could be done prior to walking through - // the login state machine, allowing us to short-circuit sooner. - // We don't expect many token server backoffs, and most users will be sitting - // in the Married state, so instead we simply do this here, once. - final BackoffHandler tokenBackoffHandler = new PrefsBackoffHandler(sharedPrefs, "token"); - if (!shouldRequestToken(tokenBackoffHandler, extras)) { - Logger.info(LOG_TAG, "Not syncing (token server)."); - syncDelegate.postponeSync(tokenBackoffHandler.delayMilliseconds()); - return; - } - - final SessionCallback sessionCallback = new SessionCallback(syncDelegate, schedulePolicy); - final KeyBundle syncKeyBundle = married.getSyncKeyBundle(); - final String clientState = married.getClientState(); - syncWithAssertion(audience, assertion, tokenServerEndpointURI, tokenBackoffHandler, sharedPrefs, syncKeyBundle, clientState, sessionCallback, extras, fxAccount); - - // Register the device if necessary (asynchronous, in another thread) - if (fxAccount.getDeviceRegistrationVersion() != FxAccountDeviceRegistrator.DEVICE_REGISTRATION_VERSION - || TextUtils.isEmpty(fxAccount.getDeviceId())) { - FxAccountDeviceRegistrator.register(context); - } - - // Force fetch the profile avatar information. (asynchronous, in another thread) - Logger.info(LOG_TAG, "Fetching profile avatar information."); - fxAccount.fetchProfileJSON(); - } catch (Exception e) { - syncDelegate.handleError(e); - return; - } - } - }); - - latch.take(); - } catch (Exception e) { - Logger.error(LOG_TAG, "Got error syncing.", e); - syncDelegate.handleError(e); - } finally { - fxAccount.releaseSharedAccountStateLock(); - } - - Logger.info(LOG_TAG, "Syncing done."); - lastSyncRealtimeMillis = SystemClock.elapsedRealtime(); - } -} |