diff -r 000000000000 -r 6474c204b198 mobile/android/base/sync/syncadapter/SyncAdapter.java
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mobile/android/base/sync/syncadapter/SyncAdapter.java Wed Dec 31 06:09:35 2014 +0100
@@ -0,0 +1,569 @@
+/* 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.sync.syncadapter;
+
+import java.io.IOException;
+import java.net.URI;
+import java.security.NoSuchAlgorithmException;
+import java.util.Collection;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.json.simple.parser.ParseException;
+import org.mozilla.gecko.background.common.GlobalConstants;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.sync.AlreadySyncingException;
+import org.mozilla.gecko.sync.BackoffHandler;
+import org.mozilla.gecko.sync.CredentialException;
+import org.mozilla.gecko.sync.GlobalSession;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.PrefsBackoffHandler;
+import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate;
+import org.mozilla.gecko.sync.SharedPreferencesNodeAssignmentCallback;
+import org.mozilla.gecko.sync.Sync11Configuration;
+import org.mozilla.gecko.sync.SyncConfiguration;
+import org.mozilla.gecko.sync.SyncConfigurationException;
+import org.mozilla.gecko.sync.SyncConstants;
+import org.mozilla.gecko.sync.SyncException;
+import org.mozilla.gecko.sync.ThreadPool;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.config.AccountPickler;
+import org.mozilla.gecko.sync.crypto.CryptoException;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+import org.mozilla.gecko.sync.delegates.BaseGlobalSessionCallback;
+import org.mozilla.gecko.sync.delegates.ClientsDataDelegate;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
+import org.mozilla.gecko.sync.net.ConnectionMonitorThread;
+import org.mozilla.gecko.sync.setup.Constants;
+import org.mozilla.gecko.sync.setup.SyncAccounts;
+import org.mozilla.gecko.sync.setup.SyncAccounts.SyncAccountParameters;
+import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AuthenticatorException;
+import android.accounts.OperationCanceledException;
+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.database.sqlite.SQLiteConstraintException;
+import android.database.sqlite.SQLiteException;
+import android.os.Bundle;
+
+public class SyncAdapter extends AbstractThreadedSyncAdapter implements BaseGlobalSessionCallback {
+ private static final String LOG_TAG = "SyncAdapter";
+
+ private static final int BACKOFF_PAD_SECONDS = 5;
+ public static final int MULTI_DEVICE_INTERVAL_MILLISECONDS = 5 * 60 * 1000; // 5 minutes.
+ public static final int SINGLE_DEVICE_INTERVAL_MILLISECONDS = 24 * 60 * 60 * 1000; // 24 hours.
+
+ private final Context mContext;
+
+ protected long syncStartTimestamp;
+
+ protected volatile BackoffHandler backoffHandler;
+
+ public SyncAdapter(Context context, boolean autoInitialize) {
+ super(context, autoInitialize);
+ mContext = context;
+ }
+
+ /**
+ * Handle an exception: update stats, log errors, etc.
+ * Wakes up sleeping threads by calling notifyMonitor().
+ *
+ * @param globalSession
+ * current global session, or null.
+ * @param e
+ * Exception to handle.
+ */
+ protected void processException(final GlobalSession globalSession, final Exception e) {
+ try {
+ if (e instanceof SQLiteConstraintException) {
+ Logger.error(LOG_TAG, "Constraint exception. Aborting sync.", e);
+ syncResult.stats.numParseExceptions++; // This is as good as we can do.
+ return;
+ }
+ if (e instanceof SQLiteException) {
+ Logger.error(LOG_TAG, "Couldn't open database (locked?). Aborting sync.", e);
+ syncResult.stats.numIoExceptions++;
+ return;
+ }
+ if (e instanceof OperationCanceledException) {
+ Logger.error(LOG_TAG, "Operation canceled. Aborting sync.", e);
+ return;
+ }
+ if (e instanceof AuthenticatorException) {
+ syncResult.stats.numParseExceptions++;
+ Logger.error(LOG_TAG, "AuthenticatorException. Aborting sync.", e);
+ return;
+ }
+ if (e instanceof IOException) {
+ syncResult.stats.numIoExceptions++;
+ Logger.error(LOG_TAG, "IOException. Aborting sync.", e);
+ e.printStackTrace();
+ return;
+ }
+
+ // Blanket stats updating for SyncException subclasses.
+ if (e instanceof SyncException) {
+ ((SyncException) e).updateStats(globalSession, syncResult);
+ } else {
+ // Generic exception.
+ syncResult.stats.numIoExceptions++;
+ }
+
+ if (e instanceof CredentialException.MissingAllCredentialsException) {
+ // This is bad: either we couldn't fetch credentials, or the credentials
+ // were totally blank. Most likely the user has two copies of Firefox
+ // installed, and something is misbehaving.
+ // Either way, disable this account.
+ if (localAccount == null) {
+ // Should not happen, but be safe.
+ Logger.error(LOG_TAG, "No credentials attached to account. Aborting sync.");
+ return;
+ }
+
+ Logger.error(LOG_TAG, "No credentials attached to account " + localAccount.name + ". Aborting sync.");
+ try {
+ SyncAccounts.setSyncAutomatically(localAccount, false);
+ } catch (Exception ex) {
+ Logger.error(LOG_TAG, "Unable to disable account " + localAccount.name + ".", ex);
+ }
+ return;
+ }
+
+ if (e instanceof CredentialException.MissingCredentialException) {
+ Logger.error(LOG_TAG, "Credentials attached to account, but missing " +
+ ((CredentialException.MissingCredentialException) e).missingCredential + ". Aborting sync.");
+ return;
+ }
+
+ if (e instanceof CredentialException) {
+ Logger.error(LOG_TAG, "Credentials attached to account were bad.");
+ return;
+ }
+
+ // Bug 755638 - Uncaught SecurityException when attempting to sync multiple Fennecs
+ // to the same Sync account.
+ // Uncheck Sync checkbox because we cannot sync this instance.
+ if (e instanceof SecurityException) {
+ Logger.error(LOG_TAG, "SecurityException, multiple Fennecs. Disabling this instance.", e);
+ SyncAccounts.backgroundSetSyncAutomatically(localAccount, false);
+ return;
+ }
+ // Generic exception.
+ Logger.error(LOG_TAG, "Unknown exception. Aborting sync.", e);
+ } finally {
+ notifyMonitor();
+ }
+ }
+
+ @Override
+ public void onSyncCanceled() {
+ super.onSyncCanceled();
+ // TODO: cancel the sync!
+ // From the docs: "This will be invoked on a separate thread than the sync
+ // thread and so you must consider the multi-threaded implications of the
+ // work that you do in this method."
+ }
+
+ public Object syncMonitor = new Object();
+ private SyncResult syncResult;
+
+ protected Account localAccount;
+ protected boolean thisSyncIsForced = false;
+ protected SharedPreferences accountSharedPreferences;
+ protected SharedPreferencesClientsDataDelegate clientsDataDelegate;
+ protected SharedPreferencesNodeAssignmentCallback nodeAssignmentDelegate;
+
+ /**
+ * Request that no sync start right away. A new sync won't start until
+ * at least backoff
milliseconds from now.
+ *
+ * Don't call this unless you are inside `run`.
+ *
+ * @param backoff time to wait in milliseconds.
+ */
+ @Override
+ public void requestBackoff(final long backoff) {
+ if (this.backoffHandler == null) {
+ throw new IllegalStateException("No backoff handler: requesting backoff outside run()?");
+ }
+ if (backoff > 0) {
+ // Fuzz the backoff time (up to 25% more) to prevent client lock-stepping; agrees with desktop.
+ final long fuzzedBackoff = backoff + Math.round((double) backoff * 0.25d * Math.random());
+ this.backoffHandler.extendEarliestNextRequest(System.currentTimeMillis() + fuzzedBackoff);
+ }
+ }
+
+ @Override
+ public boolean shouldBackOffStorage() {
+ if (thisSyncIsForced) {
+ /*
+ * If the user asks us to sync, we should sync regardless. This path is
+ * hit if the user force syncs and we restart a session after a
+ * freshStart.
+ */
+ return false;
+ }
+
+ if (nodeAssignmentDelegate.wantNodeAssignment()) {
+ /*
+ * We recently had a 401 and we aborted the last sync. We should kick off
+ * another sync to fetch a new node/weave cluster URL, since ours is
+ * stale. If we have a user authentication error, the next sync will
+ * determine that and will stop requesting node assignment, so this will
+ * only force one abnormally scheduled sync.
+ */
+ return false;
+ }
+
+ if (this.backoffHandler == null) {
+ throw new IllegalStateException("No backoff handler: checking backoff outside run()?");
+ }
+ return this.backoffHandler.delayMilliseconds() > 0;
+ }
+
+ /**
+ * Asynchronously request an immediate sync, optionally syncing only the given
+ * named stages.
+ *
+ * Returns immediately.
+ *
+ * @param account
+ * the Android Account
instance to sync.
+ * @param stageNames
+ * stage names to sync, or null
to sync all known stages.
+ */
+ public static void requestImmediateSync(final Account account, final String[] stageNames) {
+ requestImmediateSync(account, stageNames, null);
+ }
+
+ /**
+ * Asynchronously request an immediate sync, optionally syncing only the given
+ * named stages.
+ *
+ * Returns immediately.
+ *
+ * @param account
+ * the Android Account
instance to sync.
+ * @param stageNames
+ * stage names to sync, or null
to sync all known stages.
+ * @param moreExtras
+ * bundle of extras to give to the sync, or null
+ */
+ public static void requestImmediateSync(final Account account, final String[] stageNames, Bundle moreExtras) {
+ if (account == null) {
+ Logger.warn(LOG_TAG, "Not requesting immediate sync because Android Account is null.");
+ return;
+ }
+
+ final Bundle extras = new Bundle();
+ Utils.putStageNamesToSync(extras, stageNames, null);
+ extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
+
+ if (moreExtras != null) {
+ extras.putAll(moreExtras);
+ }
+
+ ContentResolver.requestSync(account, BrowserContract.AUTHORITY, extras);
+ }
+
+ @Override
+ public void onPerformSync(final Account account,
+ final Bundle extras,
+ final String authority,
+ final ContentProviderClient provider,
+ final SyncResult syncResult) {
+ syncStartTimestamp = System.currentTimeMillis();
+
+ Logger.setThreadLogTag(SyncConstants.GLOBAL_LOG_TAG);
+ Logger.resetLogging();
+ Utils.reseedSharedRandom(); // Make sure we don't work with the same random seed for too long.
+
+ // Set these so that we don't need to thread them through assorted calls and callbacks.
+ this.syncResult = syncResult;
+ this.localAccount = account;
+
+ SyncAccountParameters params;
+ try {
+ params = SyncAccounts.blockingFromAndroidAccountV0(mContext, AccountManager.get(mContext), this.localAccount);
+ } catch (Exception e) {
+ // Updates syncResult and (harmlessly) calls notifyMonitor().
+ processException(null, e);
+ return;
+ }
+
+ // params and the following fields are non-null at this point.
+ final String username = params.username; // Encoded with Utils.usernameFromAccount.
+ final String password = params.password;
+ final String serverURL = params.serverURL;
+ final String syncKey = params.syncKey;
+
+ final AtomicBoolean setNextSync = new AtomicBoolean(true);
+ final SyncAdapter self = this;
+ final Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ Logger.trace(LOG_TAG, "AccountManagerCallback invoked.");
+ // TODO: N.B.: Future must not be used on the main thread.
+ try {
+ if (Logger.LOG_PERSONAL_INFORMATION) {
+ Logger.pii(LOG_TAG, "Syncing account named " + account.name +
+ " for authority " + authority + ".");
+ } else {
+ // Replace "foo@bar.com" with "XXX@XXX.XXX".
+ Logger.info(LOG_TAG, "Syncing account named like " + Utils.obfuscateEmail(account.name) +
+ " for authority " + authority + ".");
+ }
+
+ // We dump this information right away to help with debugging.
+ Logger.debug(LOG_TAG, "Username: " + username);
+ Logger.debug(LOG_TAG, "Server: " + serverURL);
+ if (Logger.LOG_PERSONAL_INFORMATION) {
+ Logger.debug(LOG_TAG, "Password: " + password);
+ Logger.debug(LOG_TAG, "Sync key: " + syncKey);
+ } else {
+ Logger.debug(LOG_TAG, "Password? " + (password != null));
+ Logger.debug(LOG_TAG, "Sync key? " + (syncKey != null));
+ }
+
+ // Support multiple accounts by mapping each server/account pair to a branch of the
+ // shared preferences space.
+ final String product = GlobalConstants.BROWSER_INTENT_PACKAGE;
+ final String profile = Constants.DEFAULT_PROFILE;
+ final long version = SyncConfiguration.CURRENT_PREFS_VERSION;
+ self.accountSharedPreferences = Utils.getSharedPreferences(mContext, product, username, serverURL, profile, version);
+ self.clientsDataDelegate = new SharedPreferencesClientsDataDelegate(accountSharedPreferences);
+ self.backoffHandler = new PrefsBackoffHandler(accountSharedPreferences, SyncConstants.BACKOFF_PREF_SUFFIX_11);
+ final String nodeWeaveURL = Utils.nodeWeaveURL(serverURL, username);
+ self.nodeAssignmentDelegate = new SharedPreferencesNodeAssignmentCallback(accountSharedPreferences, nodeWeaveURL);
+
+ Logger.info(LOG_TAG,
+ "Client is named '" + clientsDataDelegate.getClientName() + "'" +
+ ", has client guid " + clientsDataDelegate.getAccountGUID() +
+ ", and has " + clientsDataDelegate.getClientsCount() + " clients.");
+
+ final boolean thisSyncIsForced = (extras != null) && (extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false));
+ final long delayMillis = backoffHandler.delayMilliseconds();
+ boolean shouldSync = thisSyncIsForced || (delayMillis <= 0L);
+ if (!shouldSync) {
+ long remainingSeconds = delayMillis / 1000;
+ syncResult.delayUntil = remainingSeconds + BACKOFF_PAD_SECONDS;
+ setNextSync.set(false);
+ self.notifyMonitor();
+ return;
+ }
+
+ final String prefsPath = Utils.getPrefsPath(product, username, serverURL, profile, version);
+ self.performSync(account, extras, authority, provider, syncResult,
+ username, password, prefsPath, serverURL, syncKey);
+ } catch (Exception e) {
+ self.processException(null, e);
+ return;
+ }
+ }
+ };
+
+ synchronized (syncMonitor) {
+ // Perform the work in a new thread from within this synchronized block,
+ // which allows us to be waiting on the monitor before the callback can
+ // notify us in a failure case. Oh, concurrent programming.
+ new Thread(runnable).start();
+
+ // Start our stale connection monitor thread.
+ ConnectionMonitorThread stale = new ConnectionMonitorThread();
+ stale.start();
+
+ Logger.trace(LOG_TAG, "Waiting on sync monitor.");
+ try {
+ syncMonitor.wait();
+
+ if (setNextSync.get()) {
+ long interval = getSyncInterval(clientsDataDelegate);
+ long next = System.currentTimeMillis() + interval;
+
+ if (thisSyncIsForced) {
+ Logger.info(LOG_TAG, "Setting minimum next sync time to " + next + " (" + interval + "ms from now).");
+ self.backoffHandler.setEarliestNextRequest(next);
+ } else {
+ Logger.info(LOG_TAG, "Extending minimum next sync time to " + next + " (" + interval + "ms from now).");
+ self.backoffHandler.extendEarliestNextRequest(next);
+ }
+ }
+ Logger.info(LOG_TAG, "Sync took " + Utils.formatDuration(syncStartTimestamp, System.currentTimeMillis()) + ".");
+ } catch (InterruptedException e) {
+ Logger.warn(LOG_TAG, "Waiting on sync monitor interrupted.", e);
+ } finally {
+ // And we're done with HTTP stuff.
+ stale.shutdown();
+ }
+ }
+ }
+
+ public int getSyncInterval(ClientsDataDelegate clientsDataDelegate) {
+ // Must have been a problem that means we can't access the Account.
+ if (this.localAccount == null) {
+ return SINGLE_DEVICE_INTERVAL_MILLISECONDS;
+ }
+
+ int clientsCount = clientsDataDelegate.getClientsCount();
+ if (clientsCount <= 1) {
+ return SINGLE_DEVICE_INTERVAL_MILLISECONDS;
+ }
+
+ return MULTI_DEVICE_INTERVAL_MILLISECONDS;
+ }
+
+ /**
+ * Now that we have a sync key and password, go ahead and do the work.
+ * @throws NoSuchAlgorithmException
+ * @throws IllegalArgumentException
+ * @throws SyncConfigurationException
+ * @throws AlreadySyncingException
+ * @throws NonObjectJSONException
+ * @throws ParseException
+ * @throws IOException
+ * @throws CryptoException
+ */
+ protected void performSync(final Account account,
+ final Bundle extras,
+ final String authority,
+ final ContentProviderClient provider,
+ final SyncResult syncResult,
+ final String username,
+ final String password,
+ final String prefsPath,
+ final String serverURL,
+ final String syncKey)
+ throws NoSuchAlgorithmException,
+ SyncConfigurationException,
+ IllegalArgumentException,
+ AlreadySyncingException,
+ IOException, ParseException,
+ NonObjectJSONException, CryptoException {
+ Logger.trace(LOG_TAG, "Performing sync.");
+
+ /**
+ * Bug 769745: pickle Sync account parameters to JSON file. Un-pickle in
+ * SyncAccounts.syncAccountsExist
.
+ */
+ try {
+ // Constructor can throw on nulls, which should not happen -- but let's be safe.
+ final SyncAccountParameters params = new SyncAccountParameters(mContext, null,
+ account.name, // Un-encoded, like "test@mozilla.com".
+ syncKey,
+ password,
+ serverURL,
+ null, // We'll re-fetch cluster URL; not great, but not harmful.
+ clientsDataDelegate.getClientName(),
+ clientsDataDelegate.getAccountGUID());
+
+ // Bug 772971: pickle Sync account parameters on background thread to
+ // avoid strict mode warnings.
+ ThreadPool.run(new Runnable() {
+ @Override
+ public void run() {
+ final boolean syncAutomatically = ContentResolver.getSyncAutomatically(account, authority);
+ try {
+ AccountPickler.pickle(mContext, Constants.ACCOUNT_PICKLE_FILENAME, params, syncAutomatically);
+ } 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);
+ }
+ }
+ });
+ } catch (IllegalArgumentException e) {
+ // Do nothing.
+ }
+
+ if (username == null) {
+ throw new IllegalArgumentException("username must not be null.");
+ }
+
+ if (syncKey == null) {
+ throw new SyncConfigurationException();
+ }
+
+ final KeyBundle keyBundle = new KeyBundle(username, syncKey);
+
+ if (keyBundle == null ||
+ keyBundle.getEncryptionKey() == null ||
+ keyBundle.getHMACKey() == null) {
+ throw new SyncConfigurationException();
+ }
+
+ final AuthHeaderProvider authHeaderProvider = new BasicAuthHeaderProvider(username, password);
+ final SharedPreferences prefs = getContext().getSharedPreferences(prefsPath, Utils.SHARED_PREFERENCES_MODE);
+ final SyncConfiguration config = new Sync11Configuration(username, authHeaderProvider, prefs, keyBundle);
+
+ Collection