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