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.sync.syncadapter; michael@0: michael@0: import java.io.IOException; michael@0: import java.net.URI; michael@0: import java.security.NoSuchAlgorithmException; michael@0: import java.util.Collection; michael@0: import java.util.concurrent.atomic.AtomicBoolean; michael@0: michael@0: import org.json.simple.parser.ParseException; michael@0: import org.mozilla.gecko.background.common.GlobalConstants; michael@0: import org.mozilla.gecko.background.common.log.Logger; michael@0: import org.mozilla.gecko.db.BrowserContract; michael@0: import org.mozilla.gecko.sync.AlreadySyncingException; michael@0: import org.mozilla.gecko.sync.BackoffHandler; michael@0: import org.mozilla.gecko.sync.CredentialException; michael@0: import org.mozilla.gecko.sync.GlobalSession; michael@0: import org.mozilla.gecko.sync.NonObjectJSONException; michael@0: import org.mozilla.gecko.sync.PrefsBackoffHandler; michael@0: import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate; michael@0: import org.mozilla.gecko.sync.SharedPreferencesNodeAssignmentCallback; michael@0: import org.mozilla.gecko.sync.Sync11Configuration; michael@0: import org.mozilla.gecko.sync.SyncConfiguration; michael@0: import org.mozilla.gecko.sync.SyncConfigurationException; michael@0: import org.mozilla.gecko.sync.SyncConstants; michael@0: import org.mozilla.gecko.sync.SyncException; michael@0: import org.mozilla.gecko.sync.ThreadPool; michael@0: import org.mozilla.gecko.sync.Utils; michael@0: import org.mozilla.gecko.sync.config.AccountPickler; michael@0: import org.mozilla.gecko.sync.crypto.CryptoException; 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.BasicAuthHeaderProvider; michael@0: import org.mozilla.gecko.sync.net.ConnectionMonitorThread; michael@0: import org.mozilla.gecko.sync.setup.Constants; michael@0: import org.mozilla.gecko.sync.setup.SyncAccounts; michael@0: import org.mozilla.gecko.sync.setup.SyncAccounts.SyncAccountParameters; michael@0: import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage; michael@0: michael@0: import android.accounts.Account; michael@0: import android.accounts.AccountManager; michael@0: import android.accounts.AuthenticatorException; michael@0: import android.accounts.OperationCanceledException; 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.database.sqlite.SQLiteConstraintException; michael@0: import android.database.sqlite.SQLiteException; michael@0: import android.os.Bundle; michael@0: michael@0: public class SyncAdapter extends AbstractThreadedSyncAdapter implements BaseGlobalSessionCallback { michael@0: private static final String LOG_TAG = "SyncAdapter"; michael@0: michael@0: private static final int BACKOFF_PAD_SECONDS = 5; michael@0: public static final int MULTI_DEVICE_INTERVAL_MILLISECONDS = 5 * 60 * 1000; // 5 minutes. michael@0: public static final int SINGLE_DEVICE_INTERVAL_MILLISECONDS = 24 * 60 * 60 * 1000; // 24 hours. michael@0: michael@0: private final Context mContext; michael@0: michael@0: protected long syncStartTimestamp; michael@0: michael@0: protected volatile BackoffHandler backoffHandler; michael@0: michael@0: public SyncAdapter(Context context, boolean autoInitialize) { michael@0: super(context, autoInitialize); michael@0: mContext = context; michael@0: } michael@0: michael@0: /** michael@0: * Handle an exception: update stats, log errors, etc. michael@0: * Wakes up sleeping threads by calling notifyMonitor(). michael@0: * michael@0: * @param globalSession michael@0: * current global session, or null. michael@0: * @param e michael@0: * Exception to handle. michael@0: */ michael@0: protected void processException(final GlobalSession globalSession, final Exception e) { michael@0: try { michael@0: if (e instanceof SQLiteConstraintException) { michael@0: Logger.error(LOG_TAG, "Constraint exception. Aborting sync.", e); michael@0: syncResult.stats.numParseExceptions++; // This is as good as we can do. michael@0: return; michael@0: } michael@0: if (e instanceof SQLiteException) { michael@0: Logger.error(LOG_TAG, "Couldn't open database (locked?). Aborting sync.", e); michael@0: syncResult.stats.numIoExceptions++; michael@0: return; michael@0: } michael@0: if (e instanceof OperationCanceledException) { michael@0: Logger.error(LOG_TAG, "Operation canceled. Aborting sync.", e); michael@0: return; michael@0: } michael@0: if (e instanceof AuthenticatorException) { michael@0: syncResult.stats.numParseExceptions++; michael@0: Logger.error(LOG_TAG, "AuthenticatorException. Aborting sync.", e); michael@0: return; michael@0: } michael@0: if (e instanceof IOException) { michael@0: syncResult.stats.numIoExceptions++; michael@0: Logger.error(LOG_TAG, "IOException. Aborting sync.", e); michael@0: e.printStackTrace(); michael@0: return; michael@0: } michael@0: michael@0: // Blanket stats updating for SyncException subclasses. michael@0: if (e instanceof SyncException) { michael@0: ((SyncException) e).updateStats(globalSession, syncResult); michael@0: } else { michael@0: // Generic exception. michael@0: syncResult.stats.numIoExceptions++; michael@0: } michael@0: michael@0: if (e instanceof CredentialException.MissingAllCredentialsException) { michael@0: // This is bad: either we couldn't fetch credentials, or the credentials michael@0: // were totally blank. Most likely the user has two copies of Firefox michael@0: // installed, and something is misbehaving. michael@0: // Either way, disable this account. michael@0: if (localAccount == null) { michael@0: // Should not happen, but be safe. michael@0: Logger.error(LOG_TAG, "No credentials attached to account. Aborting sync."); michael@0: return; michael@0: } michael@0: michael@0: Logger.error(LOG_TAG, "No credentials attached to account " + localAccount.name + ". Aborting sync."); michael@0: try { michael@0: SyncAccounts.setSyncAutomatically(localAccount, false); michael@0: } catch (Exception ex) { michael@0: Logger.error(LOG_TAG, "Unable to disable account " + localAccount.name + ".", ex); michael@0: } michael@0: return; michael@0: } michael@0: michael@0: if (e instanceof CredentialException.MissingCredentialException) { michael@0: Logger.error(LOG_TAG, "Credentials attached to account, but missing " + michael@0: ((CredentialException.MissingCredentialException) e).missingCredential + ". Aborting sync."); michael@0: return; michael@0: } michael@0: michael@0: if (e instanceof CredentialException) { michael@0: Logger.error(LOG_TAG, "Credentials attached to account were bad."); michael@0: return; michael@0: } michael@0: michael@0: // Bug 755638 - Uncaught SecurityException when attempting to sync multiple Fennecs michael@0: // to the same Sync account. michael@0: // Uncheck Sync checkbox because we cannot sync this instance. michael@0: if (e instanceof SecurityException) { michael@0: Logger.error(LOG_TAG, "SecurityException, multiple Fennecs. Disabling this instance.", e); michael@0: SyncAccounts.backgroundSetSyncAutomatically(localAccount, false); michael@0: return; michael@0: } michael@0: // Generic exception. michael@0: Logger.error(LOG_TAG, "Unknown exception. Aborting sync.", e); michael@0: } finally { michael@0: notifyMonitor(); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void onSyncCanceled() { michael@0: super.onSyncCanceled(); michael@0: // TODO: cancel the sync! michael@0: // From the docs: "This will be invoked on a separate thread than the sync michael@0: // thread and so you must consider the multi-threaded implications of the michael@0: // work that you do in this method." michael@0: } michael@0: michael@0: public Object syncMonitor = new Object(); michael@0: private SyncResult syncResult; michael@0: michael@0: protected Account localAccount; michael@0: protected boolean thisSyncIsForced = false; michael@0: protected SharedPreferences accountSharedPreferences; michael@0: protected SharedPreferencesClientsDataDelegate clientsDataDelegate; michael@0: protected SharedPreferencesNodeAssignmentCallback nodeAssignmentDelegate; michael@0: michael@0: /** michael@0: * Request that no sync start right away. A new sync won't start until michael@0: * at least backoff milliseconds from now. michael@0: * michael@0: * Don't call this unless you are inside `run`. michael@0: * michael@0: * @param backoff time to wait in milliseconds. michael@0: */ michael@0: @Override michael@0: public void requestBackoff(final long backoff) { michael@0: if (this.backoffHandler == null) { michael@0: throw new IllegalStateException("No backoff handler: requesting backoff outside run()?"); michael@0: } michael@0: if (backoff > 0) { michael@0: // Fuzz the backoff time (up to 25% more) to prevent client lock-stepping; agrees with desktop. michael@0: final long fuzzedBackoff = backoff + Math.round((double) backoff * 0.25d * Math.random()); michael@0: this.backoffHandler.extendEarliestNextRequest(System.currentTimeMillis() + fuzzedBackoff); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public boolean shouldBackOffStorage() { michael@0: if (thisSyncIsForced) { michael@0: /* michael@0: * If the user asks us to sync, we should sync regardless. This path is michael@0: * hit if the user force syncs and we restart a session after a michael@0: * freshStart. michael@0: */ michael@0: return false; michael@0: } michael@0: michael@0: if (nodeAssignmentDelegate.wantNodeAssignment()) { michael@0: /* michael@0: * We recently had a 401 and we aborted the last sync. We should kick off michael@0: * another sync to fetch a new node/weave cluster URL, since ours is michael@0: * stale. If we have a user authentication error, the next sync will michael@0: * determine that and will stop requesting node assignment, so this will michael@0: * only force one abnormally scheduled sync. michael@0: */ michael@0: return false; michael@0: } michael@0: michael@0: if (this.backoffHandler == null) { michael@0: throw new IllegalStateException("No backoff handler: checking backoff outside run()?"); michael@0: } michael@0: return this.backoffHandler.delayMilliseconds() > 0; michael@0: } michael@0: michael@0: /** michael@0: * Asynchronously request an immediate sync, optionally syncing only the given michael@0: * named stages. michael@0: *

michael@0: * Returns immediately. michael@0: * michael@0: * @param account michael@0: * the Android Account instance to sync. michael@0: * @param stageNames michael@0: * stage names to sync, or null to sync all known stages. michael@0: */ michael@0: public static void requestImmediateSync(final Account account, final String[] stageNames) { michael@0: requestImmediateSync(account, stageNames, null); michael@0: } michael@0: michael@0: /** michael@0: * Asynchronously request an immediate sync, optionally syncing only the given michael@0: * named stages. michael@0: *

michael@0: * Returns immediately. michael@0: * michael@0: * @param account michael@0: * the Android Account instance to sync. michael@0: * @param stageNames michael@0: * stage names to sync, or null to sync all known stages. michael@0: * @param moreExtras michael@0: * bundle of extras to give to the sync, or null michael@0: */ michael@0: public static void requestImmediateSync(final Account account, final String[] stageNames, Bundle moreExtras) { michael@0: if (account == null) { michael@0: Logger.warn(LOG_TAG, "Not requesting immediate sync because Android Account is null."); michael@0: return; michael@0: } michael@0: michael@0: final Bundle extras = new Bundle(); michael@0: Utils.putStageNamesToSync(extras, stageNames, null); michael@0: extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); michael@0: michael@0: if (moreExtras != null) { michael@0: extras.putAll(moreExtras); michael@0: } michael@0: michael@0: ContentResolver.requestSync(account, BrowserContract.AUTHORITY, extras); michael@0: } michael@0: michael@0: @Override michael@0: public void onPerformSync(final Account account, michael@0: final Bundle extras, michael@0: final String authority, michael@0: final ContentProviderClient provider, michael@0: final SyncResult syncResult) { michael@0: syncStartTimestamp = System.currentTimeMillis(); michael@0: michael@0: Logger.setThreadLogTag(SyncConstants.GLOBAL_LOG_TAG); michael@0: Logger.resetLogging(); michael@0: Utils.reseedSharedRandom(); // Make sure we don't work with the same random seed for too long. michael@0: michael@0: // Set these so that we don't need to thread them through assorted calls and callbacks. michael@0: this.syncResult = syncResult; michael@0: this.localAccount = account; michael@0: michael@0: SyncAccountParameters params; michael@0: try { michael@0: params = SyncAccounts.blockingFromAndroidAccountV0(mContext, AccountManager.get(mContext), this.localAccount); michael@0: } catch (Exception e) { michael@0: // Updates syncResult and (harmlessly) calls notifyMonitor(). michael@0: processException(null, e); michael@0: return; michael@0: } michael@0: michael@0: // params and the following fields are non-null at this point. michael@0: final String username = params.username; // Encoded with Utils.usernameFromAccount. michael@0: final String password = params.password; michael@0: final String serverURL = params.serverURL; michael@0: final String syncKey = params.syncKey; michael@0: michael@0: final AtomicBoolean setNextSync = new AtomicBoolean(true); michael@0: final SyncAdapter self = this; michael@0: final Runnable runnable = new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: Logger.trace(LOG_TAG, "AccountManagerCallback invoked."); michael@0: // TODO: N.B.: Future must not be used on the main thread. michael@0: try { michael@0: if (Logger.LOG_PERSONAL_INFORMATION) { michael@0: Logger.pii(LOG_TAG, "Syncing account named " + account.name + michael@0: " for authority " + authority + "."); michael@0: } else { michael@0: // Replace "foo@bar.com" with "XXX@XXX.XXX". michael@0: Logger.info(LOG_TAG, "Syncing account named like " + Utils.obfuscateEmail(account.name) + michael@0: " for authority " + authority + "."); michael@0: } michael@0: michael@0: // We dump this information right away to help with debugging. michael@0: Logger.debug(LOG_TAG, "Username: " + username); michael@0: Logger.debug(LOG_TAG, "Server: " + serverURL); michael@0: if (Logger.LOG_PERSONAL_INFORMATION) { michael@0: Logger.debug(LOG_TAG, "Password: " + password); michael@0: Logger.debug(LOG_TAG, "Sync key: " + syncKey); michael@0: } else { michael@0: Logger.debug(LOG_TAG, "Password? " + (password != null)); michael@0: Logger.debug(LOG_TAG, "Sync key? " + (syncKey != null)); michael@0: } michael@0: michael@0: // Support multiple accounts by mapping each server/account pair to a branch of the michael@0: // shared preferences space. michael@0: final String product = GlobalConstants.BROWSER_INTENT_PACKAGE; michael@0: final String profile = Constants.DEFAULT_PROFILE; michael@0: final long version = SyncConfiguration.CURRENT_PREFS_VERSION; michael@0: self.accountSharedPreferences = Utils.getSharedPreferences(mContext, product, username, serverURL, profile, version); michael@0: self.clientsDataDelegate = new SharedPreferencesClientsDataDelegate(accountSharedPreferences); michael@0: self.backoffHandler = new PrefsBackoffHandler(accountSharedPreferences, SyncConstants.BACKOFF_PREF_SUFFIX_11); michael@0: final String nodeWeaveURL = Utils.nodeWeaveURL(serverURL, username); michael@0: self.nodeAssignmentDelegate = new SharedPreferencesNodeAssignmentCallback(accountSharedPreferences, nodeWeaveURL); michael@0: michael@0: Logger.info(LOG_TAG, michael@0: "Client is named '" + clientsDataDelegate.getClientName() + "'" + michael@0: ", has client guid " + clientsDataDelegate.getAccountGUID() + michael@0: ", and has " + clientsDataDelegate.getClientsCount() + " clients."); michael@0: michael@0: final boolean thisSyncIsForced = (extras != null) && (extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false)); michael@0: final long delayMillis = backoffHandler.delayMilliseconds(); michael@0: boolean shouldSync = thisSyncIsForced || (delayMillis <= 0L); michael@0: if (!shouldSync) { michael@0: long remainingSeconds = delayMillis / 1000; michael@0: syncResult.delayUntil = remainingSeconds + BACKOFF_PAD_SECONDS; michael@0: setNextSync.set(false); michael@0: self.notifyMonitor(); michael@0: return; michael@0: } michael@0: michael@0: final String prefsPath = Utils.getPrefsPath(product, username, serverURL, profile, version); michael@0: self.performSync(account, extras, authority, provider, syncResult, michael@0: username, password, prefsPath, serverURL, syncKey); michael@0: } catch (Exception e) { michael@0: self.processException(null, e); michael@0: return; michael@0: } michael@0: } michael@0: }; michael@0: michael@0: synchronized (syncMonitor) { michael@0: // Perform the work in a new thread from within this synchronized block, michael@0: // which allows us to be waiting on the monitor before the callback can michael@0: // notify us in a failure case. Oh, concurrent programming. michael@0: new Thread(runnable).start(); michael@0: michael@0: // Start our stale connection monitor thread. michael@0: ConnectionMonitorThread stale = new ConnectionMonitorThread(); michael@0: stale.start(); michael@0: michael@0: Logger.trace(LOG_TAG, "Waiting on sync monitor."); michael@0: try { michael@0: syncMonitor.wait(); michael@0: michael@0: if (setNextSync.get()) { michael@0: long interval = getSyncInterval(clientsDataDelegate); michael@0: long next = System.currentTimeMillis() + interval; michael@0: michael@0: if (thisSyncIsForced) { michael@0: Logger.info(LOG_TAG, "Setting minimum next sync time to " + next + " (" + interval + "ms from now)."); michael@0: self.backoffHandler.setEarliestNextRequest(next); michael@0: } else { michael@0: Logger.info(LOG_TAG, "Extending minimum next sync time to " + next + " (" + interval + "ms from now)."); michael@0: self.backoffHandler.extendEarliestNextRequest(next); michael@0: } michael@0: } michael@0: Logger.info(LOG_TAG, "Sync took " + Utils.formatDuration(syncStartTimestamp, System.currentTimeMillis()) + "."); michael@0: } catch (InterruptedException e) { michael@0: Logger.warn(LOG_TAG, "Waiting on sync monitor interrupted.", e); michael@0: } finally { michael@0: // And we're done with HTTP stuff. michael@0: stale.shutdown(); michael@0: } michael@0: } michael@0: } michael@0: michael@0: public int getSyncInterval(ClientsDataDelegate clientsDataDelegate) { michael@0: // Must have been a problem that means we can't access the Account. michael@0: if (this.localAccount == null) { michael@0: return SINGLE_DEVICE_INTERVAL_MILLISECONDS; michael@0: } michael@0: michael@0: int clientsCount = clientsDataDelegate.getClientsCount(); michael@0: if (clientsCount <= 1) { michael@0: return SINGLE_DEVICE_INTERVAL_MILLISECONDS; michael@0: } michael@0: michael@0: return MULTI_DEVICE_INTERVAL_MILLISECONDS; michael@0: } michael@0: michael@0: /** michael@0: * Now that we have a sync key and password, go ahead and do the work. michael@0: * @throws NoSuchAlgorithmException michael@0: * @throws IllegalArgumentException michael@0: * @throws SyncConfigurationException michael@0: * @throws AlreadySyncingException michael@0: * @throws NonObjectJSONException michael@0: * @throws ParseException michael@0: * @throws IOException michael@0: * @throws CryptoException michael@0: */ michael@0: protected void performSync(final Account account, michael@0: final Bundle extras, michael@0: final String authority, michael@0: final ContentProviderClient provider, michael@0: final SyncResult syncResult, michael@0: final String username, michael@0: final String password, michael@0: final String prefsPath, michael@0: final String serverURL, michael@0: final String syncKey) michael@0: throws NoSuchAlgorithmException, michael@0: SyncConfigurationException, michael@0: IllegalArgumentException, michael@0: AlreadySyncingException, michael@0: IOException, ParseException, michael@0: NonObjectJSONException, CryptoException { michael@0: Logger.trace(LOG_TAG, "Performing sync."); michael@0: michael@0: /** michael@0: * Bug 769745: pickle Sync account parameters to JSON file. Un-pickle in michael@0: * SyncAccounts.syncAccountsExist. michael@0: */ michael@0: try { michael@0: // Constructor can throw on nulls, which should not happen -- but let's be safe. michael@0: final SyncAccountParameters params = new SyncAccountParameters(mContext, null, michael@0: account.name, // Un-encoded, like "test@mozilla.com". michael@0: syncKey, michael@0: password, michael@0: serverURL, michael@0: null, // We'll re-fetch cluster URL; not great, but not harmful. michael@0: clientsDataDelegate.getClientName(), michael@0: clientsDataDelegate.getAccountGUID()); michael@0: michael@0: // Bug 772971: pickle Sync account parameters on background thread to michael@0: // avoid strict mode warnings. michael@0: ThreadPool.run(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: final boolean syncAutomatically = ContentResolver.getSyncAutomatically(account, authority); michael@0: try { michael@0: AccountPickler.pickle(mContext, Constants.ACCOUNT_PICKLE_FILENAME, params, syncAutomatically); 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: } catch (IllegalArgumentException e) { michael@0: // Do nothing. michael@0: } michael@0: michael@0: if (username == null) { michael@0: throw new IllegalArgumentException("username must not be null."); michael@0: } michael@0: michael@0: if (syncKey == null) { michael@0: throw new SyncConfigurationException(); michael@0: } michael@0: michael@0: final KeyBundle keyBundle = new KeyBundle(username, syncKey); michael@0: michael@0: if (keyBundle == null || michael@0: keyBundle.getEncryptionKey() == null || michael@0: keyBundle.getHMACKey() == null) { michael@0: throw new SyncConfigurationException(); michael@0: } michael@0: michael@0: final AuthHeaderProvider authHeaderProvider = new BasicAuthHeaderProvider(username, password); michael@0: final SharedPreferences prefs = getContext().getSharedPreferences(prefsPath, Utils.SHARED_PREFERENCES_MODE); michael@0: final SyncConfiguration config = new Sync11Configuration(username, authHeaderProvider, prefs, keyBundle); michael@0: michael@0: Collection knownStageNames = SyncConfiguration.validEngineNames(); michael@0: config.stagesToSync = Utils.getStagesToSyncFromBundle(knownStageNames, extras); michael@0: michael@0: GlobalSession globalSession = new GlobalSession(config, this, this.mContext, clientsDataDelegate, nodeAssignmentDelegate); michael@0: globalSession.start(); michael@0: } michael@0: michael@0: private void notifyMonitor() { michael@0: synchronized (syncMonitor) { michael@0: Logger.trace(LOG_TAG, "Notifying sync monitor."); michael@0: syncMonitor.notifyAll(); michael@0: } michael@0: } michael@0: michael@0: // Implementing GlobalSession callbacks. michael@0: @Override michael@0: public void handleError(GlobalSession globalSession, Exception ex) { michael@0: Logger.info(LOG_TAG, "GlobalSession indicated error."); michael@0: this.processException(globalSession, ex); michael@0: } michael@0: michael@0: @Override michael@0: public void handleAborted(GlobalSession globalSession, String reason) { michael@0: Logger.warn(LOG_TAG, "Sync aborted: " + reason); michael@0: notifyMonitor(); michael@0: } michael@0: michael@0: @Override michael@0: public void handleSuccess(GlobalSession globalSession) { michael@0: Logger.info(LOG_TAG, "GlobalSession indicated success."); michael@0: globalSession.config.persistToPrefs(); michael@0: notifyMonitor(); michael@0: } michael@0: michael@0: @Override michael@0: public void handleStageCompleted(Stage currentState, michael@0: GlobalSession globalSession) { michael@0: Logger.trace(LOG_TAG, "Stage completed: " + currentState); michael@0: } michael@0: michael@0: @Override michael@0: public void informUnauthorizedResponse(GlobalSession session, URI oldClusterURL) { michael@0: nodeAssignmentDelegate.setClusterURLIsStale(true); michael@0: } michael@0: michael@0: @Override michael@0: public void informUpgradeRequiredResponse(final GlobalSession session) { michael@0: final AccountManager manager = AccountManager.get(mContext); michael@0: final Account toDisable = localAccount; michael@0: if (toDisable == null || manager == null) { michael@0: Logger.warn(LOG_TAG, "Attempting to disable account, but null found."); michael@0: return; michael@0: } michael@0: // Sync needs to be upgraded. Don't automatically sync anymore. michael@0: ThreadPool.run(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: manager.setUserData(toDisable, Constants.DATA_ENABLE_ON_UPGRADE, "1"); michael@0: SyncAccounts.setSyncAutomatically(toDisable, false); michael@0: } michael@0: }); michael@0: } michael@0: }