summaryrefslogtreecommitdiffstats
path: root/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncAdapter.java
diff options
context:
space:
mode:
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.java568
1 files changed, 568 insertions, 0 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
new file mode 100644
index 000000000..30990cf7f
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncAdapter.java
@@ -0,0 +1,568 @@
+/* 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();
+ }
+}