Bug 966104 - Add Sync status listener to Android FxA activities. r=rnewman, a=sylvestre
authorNick Alexander <nalexander@mozilla.com>
Fri, 04 Apr 2014 16:24:00 -0700
changeset 192751 ce9a983cdfbae6767161add1bff48380c3be1f2d
parent 192750 5fcfd5ac40663d92c6fcac055f2859a60d545393
child 192752 84931e0fd466905d6d52b5b79a3e8760b7e86644
push id474
push userasasaki@mozilla.com
push dateMon, 02 Jun 2014 21:01:02 +0000
treeherdermozilla-release@967f4cf1b31c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrnewman, sylvestre
bugs966104
milestone30.0a2
Bug 966104 - Add Sync status listener to Android FxA activities. r=rnewman, a=sylvestre
mobile/android/base/android-services.mozbuild
mobile/android/base/fxa/FirefoxAccounts.java
mobile/android/base/fxa/activities/FxAccountConfirmAccountActivity.java
mobile/android/base/fxa/activities/FxAccountStatusFragment.java
mobile/android/base/fxa/activities/FxAccountUpdateCredentialsActivity.java
mobile/android/base/fxa/authenticator/AccountPickler.java
mobile/android/base/fxa/authenticator/AndroidFxAccount.java
mobile/android/base/fxa/sync/FxAccountSyncAdapter.java
mobile/android/base/fxa/sync/FxAccountSyncStatusHelper.java
mobile/android/base/sync/setup/activities/SendTabActivity.java
mobile/android/tests/background/junit3/android-services-files.mk
mobile/android/tests/background/junit3/src/fxa/TestFirefoxAccounts.java
mobile/android/tests/background/junit3/src/fxa/authenticator/TestAccountPickler.java
--- a/mobile/android/base/android-services.mozbuild
+++ b/mobile/android/base/android-services.mozbuild
@@ -581,16 +581,17 @@ sync_java_files = [
     'fxa/login/TokensAndKeysState.java',
     'fxa/receivers/FxAccountDeletedReceiver.java',
     'fxa/receivers/FxAccountDeletedService.java',
     'fxa/sync/FxAccountGlobalSession.java',
     'fxa/sync/FxAccountNotificationManager.java',
     'fxa/sync/FxAccountSchedulePolicy.java',
     'fxa/sync/FxAccountSyncAdapter.java',
     'fxa/sync/FxAccountSyncService.java',
+    'fxa/sync/FxAccountSyncStatusHelper.java',
     'fxa/sync/SchedulePolicy.java',
     'sync/AlreadySyncingException.java',
     'sync/BackoffHandler.java',
     'sync/BadRequiredFieldJSONException.java',
     'sync/CollectionKeys.java',
     'sync/CommandProcessor.java',
     'sync/CommandRunner.java',
     'sync/config/AccountPickler.java',
--- a/mobile/android/base/fxa/FirefoxAccounts.java
+++ b/mobile/android/base/fxa/FirefoxAccounts.java
@@ -1,32 +1,71 @@
 /* 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;
 
 import java.io.File;
+import java.util.EnumSet;
 import java.util.concurrent.CountDownLatch;
 
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.fxa.authenticator.AccountPickler;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+import org.mozilla.gecko.fxa.sync.FxAccountSyncAdapter;
 import org.mozilla.gecko.sync.ThreadPool;
+import org.mozilla.gecko.sync.Utils;
 
 import android.accounts.Account;
 import android.accounts.AccountManager;
+import android.content.ContentResolver;
 import android.content.Context;
+import android.os.Bundle;
 
 /**
  * Simple public accessors for Firefox account objects.
  */
 public class FirefoxAccounts {
   private static final String LOG_TAG = FirefoxAccounts.class.getSimpleName();
 
+  public enum SyncHint {
+    /**
+     * Hint that a requested sync is preferred immediately.
+     * <p>
+     * On many devices, not including <code>SCHEDULE_NOW</code> means a delay of
+     * at least 30 seconds.
+     */
+    SCHEDULE_NOW,
+
+    /**
+     * Hint that a requested sync may ignore local rate limiting.
+     * <p>
+     * This is just a hint; the actual requested sync may not obey the hint.
+     */
+    IGNORE_LOCAL_RATE_LIMIT,
+
+    /**
+     * Hint that a requested sync may ignore remote server backoffs.
+     * <p>
+     * This is just a hint; the actual requested sync may not obey the hint.
+     */
+    IGNORE_REMOTE_SERVER_BACKOFF,
+  }
+
+  public static final EnumSet<SyncHint> SOON = EnumSet.noneOf(SyncHint.class);
+
+  public static final EnumSet<SyncHint> NOW = EnumSet.of(
+      SyncHint.SCHEDULE_NOW);
+
+  public static final EnumSet<SyncHint> FORCE = EnumSet.of(
+      SyncHint.SCHEDULE_NOW,
+      SyncHint.IGNORE_LOCAL_RATE_LIMIT,
+      SyncHint.IGNORE_REMOTE_SERVER_BACKOFF);
+
   /**
    * Returns true if a FirefoxAccount exists, false otherwise.
    *
    * @param context Android context.
    * @return true if at least one Firefox account exists.
    */
   public static boolean firefoxAccountsExist(final Context context) {
     return getFirefoxAccounts(context).length > 0;
@@ -99,9 +138,91 @@ public class FirefoxAccounts {
    */
   public static Account getFirefoxAccount(final Context context) {
     Account[] accounts = getFirefoxAccounts(context);
     if (accounts.length > 0) {
       return accounts[0];
     }
     return null;
   }
+
+  protected static void putHintsToSync(final Bundle extras, EnumSet<SyncHint> syncHints) {
+    // stagesToSync and stagesToSkip are allowed to be null.
+    if (syncHints == null) {
+      throw new IllegalArgumentException("syncHints must not be null");
+    }
+
+    final boolean scheduleNow = syncHints.contains(SyncHint.SCHEDULE_NOW);
+    final boolean ignoreLocalRateLimit = syncHints.contains(SyncHint.IGNORE_LOCAL_RATE_LIMIT);
+    final boolean ignoreRemoteServerBackoff = syncHints.contains(SyncHint.IGNORE_REMOTE_SERVER_BACKOFF);
+
+    extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, scheduleNow);
+    // The default when manually syncing is to ignore the local rate limit and
+    // any remote server backoff requests. Since we can't add flags to a manual
+    // sync instigated by the user, we have to reverse the natural conditionals.
+    // See also the FORCE EnumSet.
+    extras.putBoolean(FxAccountSyncAdapter.SYNC_EXTRAS_RESPECT_LOCAL_RATE_LIMIT, !ignoreLocalRateLimit);
+    extras.putBoolean(FxAccountSyncAdapter.SYNC_EXTRAS_RESPECT_REMOTE_SERVER_BACKOFF, !ignoreRemoteServerBackoff);
+  }
+
+  public static EnumSet<SyncHint> getHintsToSyncFromBundle(final Bundle extras) {
+    final EnumSet<SyncHint> syncHints = EnumSet.noneOf(SyncHint.class);
+
+    final boolean scheduleNow = extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false);
+    final boolean ignoreLocalRateLimit = !extras.getBoolean(FxAccountSyncAdapter.SYNC_EXTRAS_RESPECT_LOCAL_RATE_LIMIT, false);
+    final boolean ignoreRemoteServerBackoff = !extras.getBoolean(FxAccountSyncAdapter.SYNC_EXTRAS_RESPECT_REMOTE_SERVER_BACKOFF, false);
+
+    if (scheduleNow) {
+      syncHints.add(SyncHint.SCHEDULE_NOW);
+    }
+    if (ignoreLocalRateLimit) {
+      syncHints.add(SyncHint.IGNORE_LOCAL_RATE_LIMIT);
+    }
+    if (ignoreRemoteServerBackoff) {
+      syncHints.add(SyncHint.IGNORE_REMOTE_SERVER_BACKOFF);
+    }
+
+    return syncHints;
+  }
+
+  public static void logSyncHints(EnumSet<SyncHint> syncHints) {
+    final boolean scheduleNow = syncHints.contains(SyncHint.SCHEDULE_NOW);
+    final boolean ignoreLocalRateLimit = syncHints.contains(SyncHint.IGNORE_LOCAL_RATE_LIMIT);
+    final boolean ignoreRemoteServerBackoff = syncHints.contains(SyncHint.IGNORE_REMOTE_SERVER_BACKOFF);
+
+    Logger.info(LOG_TAG, "Sync hints" +
+        "; scheduling now: " + scheduleNow +
+        "; ignoring local rate limit: " + ignoreLocalRateLimit +
+        "; ignoring remote server backoff: " + ignoreRemoteServerBackoff + ".");
+  }
+
+  /**
+   * Request a sync for the given Android Account.
+   * <p>
+   * Any hints are strictly optional: the actual requested sync is scheduled by
+   * the Android sync scheduler, and the sync mechanism may ignore hints as it
+   * sees fit.
+   *
+   * @param account to sync.
+   * @param syncHints to pass to sync.
+   * @param stagesToSync stage names to sync.
+   * @param stagesToSkip stage names to skip.
+   */
+  public static void requestSync(Account account, EnumSet<SyncHint> syncHints, String[] stagesToSync, String[] stagesToSkip) {
+    if (account == null) {
+      throw new IllegalArgumentException("account must not be null");
+    }
+    if (syncHints == null) {
+      throw new IllegalArgumentException("syncHints must not be null");
+    }
+
+    final Bundle extras = new Bundle();
+    putHintsToSync(extras, syncHints);
+    Utils.putStageNamesToSync(extras, stagesToSync, stagesToSkip);
+
+    Logger.info(LOG_TAG, "Requesting sync.");
+    logSyncHints(syncHints);
+
+    for (String authority : AndroidFxAccount.getAndroidAuthorities()) {
+      ContentResolver.requestSync(account, authority, extras);
+    }
+  }
 }
--- a/mobile/android/base/fxa/activities/FxAccountConfirmAccountActivity.java
+++ b/mobile/android/base/fxa/activities/FxAccountConfirmAccountActivity.java
@@ -11,17 +11,18 @@ import org.mozilla.gecko.R;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.fxa.FxAccountClient;
 import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
 import org.mozilla.gecko.background.fxa.FxAccountClient20;
 import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
 import org.mozilla.gecko.fxa.login.Engaged;
 import org.mozilla.gecko.fxa.login.State;
-import org.mozilla.gecko.fxa.login.State.StateLabel;
+import org.mozilla.gecko.fxa.login.State.Action;
+import org.mozilla.gecko.fxa.sync.FxAccountSyncStatusHelper;
 import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
 
 import android.content.Context;
 import android.os.Bundle;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.widget.TextView;
 import android.widget.Toast;
@@ -35,16 +36,18 @@ public class FxAccountConfirmAccountActi
 
   // Set in onCreate.
   protected TextView verificationLinkTextView;
   protected View resendLink;
 
   // Set in onResume.
   protected AndroidFxAccount fxAccount;
 
+  protected final SyncStatusDelegate syncStatusDelegate = new SyncStatusDelegate();
+
   public FxAccountConfirmAccountActivity() {
     super(CANNOT_RESUME_WHEN_NO_ACCOUNTS_EXIST);
   }
 
   /**
    * {@inheritDoc}
    */
   @Override
@@ -72,21 +75,72 @@ public class FxAccountConfirmAccountActi
     super.onResume();
     this.fxAccount = getAndroidFxAccount();
     if (fxAccount == null) {
       Logger.warn(LOG_TAG, "Could not get Firefox Account.");
       setResult(RESULT_CANCELED);
       finish();
       return;
     }
-    State state = fxAccount.getState();
-    if (state.getStateLabel() != StateLabel.Engaged) {
-      Logger.warn(LOG_TAG, "Cannot confirm Firefox Account in state: " + state.getStateLabel());
+
+    FxAccountSyncStatusHelper.getInstance().startObserving(syncStatusDelegate);
+
+    refresh();
+  }
+
+  @Override
+  public void onPause() {
+    super.onPause();
+    FxAccountSyncStatusHelper.getInstance().stopObserving(syncStatusDelegate);
+  }
+
+  protected class SyncStatusDelegate implements FxAccountSyncStatusHelper.Delegate {
+    protected final Runnable refreshRunnable = new Runnable() {
+      @Override
+      public void run() {
+        refresh();
+      }
+    };
+
+    @Override
+    public AndroidFxAccount getAccount() {
+      return fxAccount;
+    }
+
+    @Override
+    public void handleSyncStarted() {
+      Logger.info(LOG_TAG, "Got sync started message; ignoring.");
+    }
+
+    @Override
+    public void handleSyncFinished() {
+      if (fxAccount == null) {
+        return;
+      }
+      Logger.info(LOG_TAG, "Got sync finished message; refreshing.");
+      runOnUiThread(refreshRunnable);
+    }
+  }
+
+  protected void refresh() {
+    final State state = fxAccount.getState();
+    final Action neededAction = state.getNeededAction();
+    switch (neededAction) {
+    case NeedsVerification:
+      // This is what we're here to handle.
+      break;
+    case NeedsPassword:
+    case NeedsUpgrade:
+    case None:
+    default:
+      // We're not in the right place!  Redirect to status.
+      Logger.warn(LOG_TAG, "No need to verifiy Firefox Account that needs action " + neededAction.toString() +
+          " (in state " + state.getStateLabel() + ").");
       setResult(RESULT_CANCELED);
-      finish();
+      this.redirectToActivity(FxAccountStatusActivity.class);
       return;
     }
 
     final String email = fxAccount.getEmail();
     final String text = getResources().getString(R.string.fxaccount_confirm_account_verification_link, email);
     verificationLinkTextView.setText(text);
 
     boolean resendLinkShouldBeEnabled = ((Engaged) state).getSessionToken() != null;
--- a/mobile/android/base/fxa/activities/FxAccountStatusFragment.java
+++ b/mobile/android/base/fxa/activities/FxAccountStatusFragment.java
@@ -6,21 +6,22 @@ package org.mozilla.gecko.fxa.activities
 
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Set;
 
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.preferences.PreferenceFragment;
-import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.fxa.FirefoxAccounts;
 import org.mozilla.gecko.fxa.FxAccountConstants;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
 import org.mozilla.gecko.fxa.login.Married;
 import org.mozilla.gecko.fxa.login.State;
+import org.mozilla.gecko.fxa.sync.FxAccountSyncStatusHelper;
 import org.mozilla.gecko.sync.SyncConfiguration;
 
 import android.content.ContentResolver;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.os.Bundle;
 import android.os.Handler;
 import android.preference.CheckBoxPreference;
@@ -64,16 +65,18 @@ public class FxAccountStatusFragment ext
   // Used to post delayed sync requests.
   protected Handler handler;
 
   // Member variable so that re-posting pushes back the already posted instance.
   // This Runnable references the fxAccount above, but it is not specific to a
   // single account. (That is, it does not capture a single account instance.)
   protected Runnable requestSyncRunnable;
 
+  protected final SyncStatusDelegate syncStatusDelegate = new SyncStatusDelegate();
+
   protected Preference ensureFindPreference(String key) {
     Preference preference = findPreference(key);
     if (preference == null) {
       throw new IllegalStateException("Could not find preference with key: " + key);
     }
     return preference;
   }
 
@@ -205,16 +208,48 @@ public class FxAccountStatusFragment ext
   }
 
   protected void showConnected() {
     syncCategory.setTitle(R.string.fxaccount_status_sync_enabled);
     showOnlyOneErrorPreference(null);
     setCheckboxesEnabled(true);
   }
 
+  protected class SyncStatusDelegate implements FxAccountSyncStatusHelper.Delegate {
+    protected final Runnable refreshRunnable = new Runnable() {
+      @Override
+      public void run() {
+        refresh();
+      }
+    };
+
+    @Override
+    public AndroidFxAccount getAccount() {
+      return fxAccount;
+    }
+
+    @Override
+    public void handleSyncStarted() {
+      if (fxAccount == null) {
+        return;
+      }
+      Logger.info(LOG_TAG, "Got sync started message; refreshing.");
+      getActivity().runOnUiThread(refreshRunnable);
+    }
+
+    @Override
+    public void handleSyncFinished() {
+      if (fxAccount == null) {
+        return;
+      }
+      Logger.info(LOG_TAG, "Got sync finished message; refreshing.");
+      getActivity().runOnUiThread(refreshRunnable);
+    }
+  }
+
   /**
    * Notify the fragment that a new AndroidFxAccount instance is current.
    * <p>
    * <b>Important:</b> call this method on the UI thread!
    * <p>
    * In future, this might be a Loader.
    *
    * @param fxAccount new instance.
@@ -227,19 +262,33 @@ public class FxAccountStatusFragment ext
 
     handler = new Handler(); // Attached to current (assumed to be UI) thread.
 
     // Runnable is not specific to one Firefox Account. This runnable will keep
     // a reference to this fragment alive, but we expect posted runnables to be
     // serviced very quickly, so this is not an issue.
     requestSyncRunnable = new RequestSyncRunnable();
 
+    // We would very much like register these status observers in bookended
+    // onResume/onPause calls, but because the Fragment gets onResume during the
+    // Activity's super.onResume, it hasn't yet been told its Firefox Account.
+    // So we register the observer here (and remove it in onPause), and open
+    // ourselves to the possibility that we don't have properly paired
+    // register/unregister calls.
+    FxAccountSyncStatusHelper.getInstance().startObserving(syncStatusDelegate);
+
     refresh();
   }
 
+  @Override
+  public void onPause() {
+    super.onPause();
+    FxAccountSyncStatusHelper.getInstance().stopObserving(syncStatusDelegate);
+  }
+
   protected void refresh() {
     // refresh is called from our onResume, which can happen before the owning
     // Activity tells us about an account (via our public
     // refresh(AndroidFxAccount) method).
     if (fxAccount == null) {
       throw new IllegalArgumentException("fxAccount must not be null");
     }
 
@@ -382,39 +431,35 @@ public class FxAccountStatusFragment ext
     @Override
     public void run() {
       // Name shadowing -- do you like it, or do you love it?
       AndroidFxAccount fxAccount = FxAccountStatusFragment.this.fxAccount;
       if (fxAccount == null) {
         return;
       }
       Logger.info(LOG_TAG, "Requesting a sync sometime soon.");
-      // Request a sync, but not necessarily an immediate sync.
-      ContentResolver.requestSync(fxAccount.getAndroidAccount(), BrowserContract.AUTHORITY, Bundle.EMPTY);
-      // SyncAdapter.requestImmediateSync(fxAccount.getAndroidAccount(), null);
+      fxAccount.requestSync();
     }
   }
 
   /**
    * A separate listener to separate debug logic from main code paths.
    */
   protected class DebugPreferenceClickListener implements OnPreferenceClickListener {
     @Override
     public boolean onPreferenceClick(Preference preference) {
       final String key = preference.getKey();
       if ("debug_refresh".equals(key)) {
         Logger.info(LOG_TAG, "Refreshing.");
         refresh();
       } else if ("debug_dump".equals(key)) {
         fxAccount.dump();
       } else if ("debug_force_sync".equals(key)) {
-        Logger.info(LOG_TAG, "Syncing.");
-        final Bundle extras = new Bundle();
-        extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
-        fxAccount.requestSync(extras);
+        Logger.info(LOG_TAG, "Force syncing.");
+        fxAccount.requestSync(FirefoxAccounts.FORCE);
         // No sense refreshing, since the sync will complete in the future.
       } else if ("debug_forget_certificate".equals(key)) {
         State state = fxAccount.getState();
         try {
           Married married = (Married) state;
           Logger.info(LOG_TAG, "Moving to Cohabiting state: Forgetting certificate.");
           fxAccount.setState(married.makeCohabitingState());
           refresh();
--- a/mobile/android/base/fxa/activities/FxAccountUpdateCredentialsActivity.java
+++ b/mobile/android/base/fxa/activities/FxAccountUpdateCredentialsActivity.java
@@ -11,16 +11,17 @@ import org.mozilla.gecko.R;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.fxa.FxAccountClient;
 import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
 import org.mozilla.gecko.background.fxa.FxAccountClient20;
 import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
 import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
 import org.mozilla.gecko.background.fxa.FxAccountUtils;
 import org.mozilla.gecko.background.fxa.PasswordStretcher;
+import org.mozilla.gecko.fxa.FirefoxAccounts;
 import org.mozilla.gecko.fxa.FxAccountConstants;
 import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.FxAccountSignInTask;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
 import org.mozilla.gecko.fxa.login.Engaged;
 import org.mozilla.gecko.fxa.login.State;
 import org.mozilla.gecko.fxa.login.State.StateLabel;
 import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
 
@@ -137,16 +138,17 @@ public class FxAccountUpdateCredentialsA
         // entered locally as much as possible.
         byte[] quickStretchedPW = passwordStretcher.getQuickStretchedPW(result.remoteEmail.getBytes("UTF-8"));
         unwrapkB = FxAccountUtils.generateUnwrapBKey(quickStretchedPW);
       } catch (Exception e) {
         this.handleError(e);
         return;
       }
       fxAccount.setState(new Engaged(email, result.uid, result.verified, unwrapkB, result.sessionToken, result.keyFetchToken));
+      fxAccount.requestSync(FirefoxAccounts.FORCE);
 
       // For great debugging.
       if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
         fxAccount.dump();
       }
 
       redirectToActivity(FxAccountStatusActivity.class);
     }
--- a/mobile/android/base/fxa/authenticator/AccountPickler.java
+++ b/mobile/android/base/fxa/authenticator/AccountPickler.java
@@ -92,17 +92,17 @@ public class AccountPickler {
     o.put(KEY_PICKLE_TIMESTAMP, Long.valueOf(System.currentTimeMillis()));
 
     o.put(KEY_ACCOUNT_VERSION, AndroidFxAccount.CURRENT_ACCOUNT_VERSION);
     o.put(KEY_ACCOUNT_TYPE, FxAccountConstants.ACCOUNT_TYPE);
     o.put(KEY_EMAIL, account.getEmail());
     o.put(KEY_PROFILE, account.getProfile());
     o.put(KEY_IDP_SERVER_URI, account.getAccountServerURI());
     o.put(KEY_TOKEN_SERVER_URI, account.getTokenServerURI());
-    o.put(KEY_IS_SYNCING_ENABLED, account.isSyncingEnabled());
+    o.put(KEY_IS_SYNCING_ENABLED, account.isSyncing());
 
     // TODO: If prefs version changes under us, SyncPrefsPath will change, "clearing" prefs.
 
     final ExtendedJSONObject bundle = account.unbundle();
     if (bundle == null) {
       Logger.warn(LOG_TAG, "Unable to obtain account bundle; aborting.");
       return;
     }
--- a/mobile/android/base/fxa/authenticator/AndroidFxAccount.java
+++ b/mobile/android/base/fxa/authenticator/AndroidFxAccount.java
@@ -3,22 +3,26 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.fxa.authenticator;
 
 import java.io.UnsupportedEncodingException;
 import java.net.URISyntaxException;
 import java.security.GeneralSecurityException;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.EnumSet;
+import java.util.List;
 
 import org.mozilla.gecko.background.common.GlobalConstants;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.fxa.FxAccountUtils;
 import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.fxa.FirefoxAccounts;
 import org.mozilla.gecko.fxa.FxAccountConstants;
 import org.mozilla.gecko.fxa.login.State;
 import org.mozilla.gecko.fxa.login.State.StateLabel;
 import org.mozilla.gecko.fxa.login.StateFactory;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.Utils;
 
 import android.accounts.Account;
@@ -53,16 +57,19 @@ public class AndroidFxAccount {
   public static final String ACCOUNT_KEY_TOKEN_SERVER = "tokenServerURI";       // Sync-specific.
   public static final String ACCOUNT_KEY_DESCRIPTOR = "descriptor";
 
   public static final int CURRENT_BUNDLE_VERSION = 2;
   public static final String BUNDLE_KEY_BUNDLE_VERSION = "version";
   public static final String BUNDLE_KEY_STATE_LABEL = "stateLabel";
   public static final String BUNDLE_KEY_STATE = "state";
 
+  protected static final List<String> ANDROID_AUTHORITIES = Collections.unmodifiableList(Arrays.asList(
+      new String[] { BrowserContract.AUTHORITY }));
+
   protected final Context context;
   protected final AccountManager accountManager;
   protected final Account account;
 
   /**
    * Create an Android Firefox Account instance backed by an Android Account
    * instance.
    * <p>
@@ -375,50 +382,90 @@ public class AndroidFxAccount {
 
     return fxAccount;
   }
 
   public void clearSyncPrefs() throws UnsupportedEncodingException, GeneralSecurityException {
     getSyncPrefs().edit().clear().commit();
   }
 
-  public boolean isSyncingEnabled() {
-    // TODO: Authority will be static in PR 426.
-    final int result = ContentResolver.getIsSyncable(account, BrowserContract.AUTHORITY);
-    if (result > 0) {
-      return true;
-    } else if (result == 0) {
-      return false;
-    } else {
-      // This should not happen.
-      throw new IllegalStateException("Sync enabled state unknown.");
+  public static Iterable<String> getAndroidAuthorities() {
+    return ANDROID_AUTHORITIES;
+  }
+
+  /**
+   * Return true if the underlying Android account is currently set to sync automatically.
+   * <p>
+   * This is, confusingly, not the same thing as "being syncable": that refers
+   * to whether this account can be synced, ever; this refers to whether Android
+   * will try to sync the account at appropriate times.
+   *
+   * @return true if the account is set to sync automatically.
+   */
+  public boolean isSyncing() {
+    boolean isSyncEnabled = true;
+    for (String authority : getAndroidAuthorities()) {
+      isSyncEnabled &= ContentResolver.getSyncAutomatically(account, authority);
     }
+    return isSyncEnabled;
   }
 
   public void enableSyncing() {
-    Logger.info(LOG_TAG, "Disabling sync for account named like " + Utils.obfuscateEmail(getEmail()));
-    for (String authority : new String[] { BrowserContract.AUTHORITY }) {
+    Logger.info(LOG_TAG, "Enabling sync for account named like " + getObfuscatedEmail());
+    for (String authority : getAndroidAuthorities()) {
       ContentResolver.setSyncAutomatically(account, authority, true);
       ContentResolver.setIsSyncable(account, authority, 1);
     }
   }
 
   public void disableSyncing() {
-    Logger.info(LOG_TAG, "Disabling sync for account named like " + Utils.obfuscateEmail(getEmail()));
-    for (String authority : new String[] { BrowserContract.AUTHORITY }) {
+    Logger.info(LOG_TAG, "Disabling sync for account named like " + getObfuscatedEmail());
+    for (String authority : getAndroidAuthorities()) {
       ContentResolver.setSyncAutomatically(account, authority, false);
     }
   }
 
-  public void requestSync(Bundle extras) {
-    Logger.info(LOG_TAG, "Requesting sync for account named like " + Utils.obfuscateEmail(getEmail()) +
-        (extras.isEmpty() ? "." : "; has extras."));
-    for (String authority : new String[] { BrowserContract.AUTHORITY }) {
-      ContentResolver.requestSync(account, authority, extras);
+  /**
+   * Is a sync currently in progress?
+   *
+   * @return true if Android is currently syncing the underlying Android Account.
+   */
+  public boolean isCurrentlySyncing() {
+    boolean active = false;
+    for (String authority : AndroidFxAccount.getAndroidAuthorities()) {
+      active |= ContentResolver.isSyncActive(account, authority);
     }
+    return active;
+  }
+
+  /**
+   * Request a sync.  See {@link FirefoxAccounts#requestSync(Account, EnumSet, String[], String[])}.
+   */
+  public void requestSync() {
+    requestSync(FirefoxAccounts.SOON, null, null);
+  }
+
+  /**
+   * Request a sync.  See {@link FirefoxAccounts#requestSync(Account, EnumSet, String[], String[])}.
+   *
+   * @param syncHints to pass to sync.
+   */
+  public void requestSync(EnumSet<FirefoxAccounts.SyncHint> syncHints) {
+    requestSync(syncHints, null, null);
+  }
+
+  /**
+   * Request a sync.  See {@link FirefoxAccounts#requestSync(Account, EnumSet, String[], String[])}.
+   *
+   * @param syncHints to pass to sync.
+   * @param stagesToSync stage names to sync.
+   * @param stagesToSkip stage names to skip.
+   */
+  public void requestSync(EnumSet<FirefoxAccounts.SyncHint> syncHints, String[] stagesToSync, String[] stagesToSkip) {
+    FirefoxAccounts.requestSync(getAndroidAccount(), syncHints, stagesToSync, stagesToSkip);
   }
 
   public synchronized void setState(State state) {
     if (state == null) {
       throw new IllegalArgumentException("state must not be null");
     }
     Logger.info(LOG_TAG, "Moving account named like " + Utils.obfuscateEmail(getEmail()) +
         " to state " + state.getStateLabel().toString());
@@ -467,16 +514,27 @@ public class AndroidFxAccount {
    *
    * @return local email address.
    */
   public String getEmail() {
     return account.name;
   }
 
   /**
+   * Return the Firefox Account's local email address, obfuscated.
+   * <p>
+   * Use this when logging.
+   *
+   * @return local email address, obfuscated.
+   */
+  public String getObfuscatedEmail() {
+    return Utils.obfuscateEmail(account.name);
+  }
+
+  /**
    * Create an intent announcing that a Firefox account will be deleted.
    *
    * @param context
    *          Android context.
    * @param account
    *          Android account being removed.
    * @return <code>Intent</code> to broadcast.
    */
--- a/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java
+++ b/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java
@@ -2,29 +2,31 @@
  * 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 java.net.URI;
 import java.net.URISyntaxException;
 import java.security.NoSuchAlgorithmException;
+import java.util.EnumSet;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.fxa.FxAccountClient;
 import org.mozilla.gecko.background.fxa.FxAccountClient20;
 import org.mozilla.gecko.background.fxa.SkewHandler;
 import org.mozilla.gecko.browserid.BrowserIDKeyPair;
 import org.mozilla.gecko.browserid.JSONWebTokenUtils;
 import org.mozilla.gecko.browserid.RSACryptoImplementation;
 import org.mozilla.gecko.browserid.verifier.BrowserIDRemoteVerifierClient;
 import org.mozilla.gecko.browserid.verifier.BrowserIDVerifierDelegate;
+import org.mozilla.gecko.fxa.FirefoxAccounts;
 import org.mozilla.gecko.fxa.FxAccountConstants;
 import org.mozilla.gecko.fxa.authenticator.AccountPickler;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
 import org.mozilla.gecko.fxa.authenticator.FxAccountAuthenticator;
 import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine;
 import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.LoginStateMachineDelegate;
 import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition;
 import org.mozilla.gecko.fxa.login.Married;
@@ -57,17 +59,20 @@ import android.content.Context;
 import android.content.SharedPreferences;
 import android.content.SyncResult;
 import android.os.Bundle;
 import android.os.SystemClock;
 
 public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter {
   private static final String LOG_TAG = FxAccountSyncAdapter.class.getSimpleName();
 
-  public static final int NOTIFICATION_ID = LOG_TAG.hashCode();
+  public static final String SYNC_EXTRAS_RESPECT_LOCAL_RATE_LIMIT = "respect_local_rate_limit";
+  public static final String SYNC_EXTRAS_RESPECT_REMOTE_SERVER_BACKOFF = "respect_remote_server_backoff";
+
+  protected 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 = 0L;
@@ -418,29 +423,32 @@ public class FxAccountSyncAdapter extend
    * 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();
 
+    Logger.info(LOG_TAG, "Syncing FxAccount" +
+        " account named like " + Utils.obfuscateEmail(account.name) +
+        " for authority " + authority +
+        " with instance " + this + ".");
+
+    final EnumSet<FirefoxAccounts.SyncHint> syncHints = FirefoxAccounts.getHintsToSyncFromBundle(extras);
+    FirefoxAccounts.logSyncHints(syncHints);
+
     // This applies even to forced syncs, but only on success.
     if (this.lastSyncRealtimeMillis > 0L &&
         (this.lastSyncRealtimeMillis + MINIMUM_SYNC_DELAY_MILLIS) > SystemClock.elapsedRealtime()) {
       Logger.info(LOG_TAG, "Not syncing FxAccount " + Utils.obfuscateEmail(account.name) +
                            ": minimum interval not met.");
       return;
     }
 
-    Logger.info(LOG_TAG, "Syncing FxAccount" +
-        " account named like " + Utils.obfuscateEmail(account.name) +
-        " for authority " + authority +
-        " with instance " + this + ".");
-
     final Context context = getContext();
     final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
     if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
       fxAccount.dump();
     }
 
     // Pickle in a background thread to avoid strict mode warnings.
     ThreadPool.run(new Runnable() {
@@ -471,17 +479,17 @@ public class FxAccountSyncAdapter extend
       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_FORCE, false));
+                                   extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, 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;
         }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/fxa/sync/FxAccountSyncStatusHelper.java
@@ -0,0 +1,113 @@
+/* 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 java.util.Map;
+import java.util.Map.Entry;
+import java.util.WeakHashMap;
+
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+
+import android.content.ContentResolver;
+import android.content.SyncStatusObserver;
+
+/**
+ * Abstract away some details of Android's SyncStatusObserver.
+ * <p>
+ * Provides a simplified sync started/sync finished delegate.
+ * <p>
+ * We would prefer to register multiple observers, but it's of limited value
+ * right now, so we support only a single observer, and we are as tolerant as
+ * possible of non-paired add/remove calls.
+ */
+public class FxAccountSyncStatusHelper implements SyncStatusObserver {
+  @SuppressWarnings("unused")
+  private static final String LOG_TAG = FxAccountSyncStatusHelper.class.getSimpleName();
+
+  protected static FxAccountSyncStatusHelper sInstance = null;
+
+  public synchronized static FxAccountSyncStatusHelper getInstance() {
+    if (sInstance == null) {
+      sInstance = new FxAccountSyncStatusHelper();
+    }
+    return sInstance;
+  }
+
+  public interface Delegate {
+    public AndroidFxAccount getAccount();
+    public void handleSyncStarted();
+    public void handleSyncFinished();
+  }
+
+  // Used to unregister this as a listener.
+  protected Object handle = null;
+
+  // Maps delegates to whether their underlying Android account was syncing the
+  // last time we observed a status change.
+  protected Map<Delegate, Boolean> delegates = new WeakHashMap<Delegate, Boolean>();
+
+  @Override
+  public synchronized void onStatusChanged(int which) {
+    for (Entry<Delegate, Boolean> entry : delegates.entrySet()) {
+      final Delegate delegate = entry.getKey();
+      final AndroidFxAccount fxAccount = delegate.getAccount();
+      if (fxAccount == null) {
+        continue;
+      }
+      final boolean active = fxAccount.isCurrentlySyncing();
+      // Remember for later.
+      boolean wasActiveLastTime = entry.getValue();
+      // It's okay to update the value of an entry while iterating the entrySet.
+      entry.setValue(active);
+
+      if (active && !wasActiveLastTime) {
+        // We've started a sync.
+        delegate.handleSyncStarted();
+      }
+      if (!active && wasActiveLastTime) {
+        // We've finished a sync.
+        delegate.handleSyncFinished();
+      }
+    }
+  }
+
+  protected void addListener() {
+    final int mask = ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE;
+    if (this.handle != null) {
+      throw new IllegalStateException("Already registered this as an observer?");
+    }
+    this.handle = ContentResolver.addStatusChangeListener(mask, this);
+  }
+
+  protected void removeListener() {
+    Object handle = this.handle;
+    this.handle = null;
+    if (handle != null) {
+      ContentResolver.removeStatusChangeListener(handle);
+    }
+  }
+
+  public synchronized void startObserving(Delegate delegate) {
+    if (delegate == null) {
+      throw new IllegalArgumentException("delegate must not be null");
+    }
+    if (delegates.containsKey(delegate)) {
+      return;
+    }
+    // If we are the first delegate to the party, start listening.
+    if (delegates.isEmpty()) {
+      addListener();
+    }
+    delegates.put(delegate, Boolean.FALSE);
+  }
+
+  public synchronized void stopObserving(Delegate delegate) {
+    delegates.remove(delegate);
+    // If we are the last delegate leaving the party, stop listening.
+    if (delegates.isEmpty()) {
+      removeListener();
+    }
+  }
+}
--- a/mobile/android/base/sync/setup/activities/SendTabActivity.java
+++ b/mobile/android/base/sync/setup/activities/SendTabActivity.java
@@ -7,36 +7,35 @@ package org.mozilla.gecko.sync.setup.act
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.fxa.FirefoxAccounts;
 import org.mozilla.gecko.fxa.FxAccountConstants;
 import org.mozilla.gecko.fxa.activities.FxAccountGetStartedActivity;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
 import org.mozilla.gecko.sync.CommandProcessor;
 import org.mozilla.gecko.sync.CommandRunner;
 import org.mozilla.gecko.sync.GlobalSession;
 import org.mozilla.gecko.sync.SyncConfiguration;
 import org.mozilla.gecko.sync.SyncConstants;
-import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.sync.repositories.NullCursorException;
 import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor;
 import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
 import org.mozilla.gecko.sync.setup.SyncAccounts;
 import org.mozilla.gecko.sync.stage.SyncClientsEngineStage;
 import org.mozilla.gecko.sync.syncadapter.SyncAdapter;
 
 import android.accounts.Account;
 import android.accounts.AccountManager;
 import android.app.Activity;
-import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.os.AsyncTask;
 import android.os.Bundle;
 import android.view.View;
 import android.widget.ListView;
 import android.widget.TextView;
@@ -73,20 +72,17 @@ public class SendTabActivity extends Act
       } catch (Exception e) {
         Logger.warn(LOG_TAG, "Could not get Firefox Account parameters or preferences; aborting.");
         return null;
       }
     }
 
     @Override
     public void syncClientsStage() {
-      final Bundle extras = new Bundle();
-      Utils.putStageNamesToSync(extras, CLIENTS_STAGE, null);
-      extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
-      this.account.requestSync(extras);
+      account.requestSync(FirefoxAccounts.FORCE, CLIENTS_STAGE, null);
     }
   }
 
   private static class Sync11TabSender implements TabSender {
     private final Account account;
     private final AccountManager accountManager;
     private final Context context;
     private Sync11TabSender(Context context, Account syncAccount, AccountManager accountManager) {
--- a/mobile/android/tests/background/junit3/android-services-files.mk
+++ b/mobile/android/tests/background/junit3/android-services-files.mk
@@ -19,16 +19,17 @@ BACKGROUND_TESTS_JAVA_FILES := \
   src/db/TestClientsDatabase.java \
   src/db/TestClientsDatabaseAccessor.java \
   src/db/TestFennecTabsRepositorySession.java \
   src/db/TestFennecTabsStorage.java \
   src/db/TestFormHistoryRepositorySession.java \
   src/db/TestPasswordsRepository.java \
   src/fxa/authenticator/TestAccountPickler.java \
   src/fxa/TestBrowserIDKeyPairGeneration.java \
+  src/fxa/TestFirefoxAccounts.java \
   src/healthreport/MockDatabaseEnvironment.java \
   src/healthreport/MockHealthReportDatabaseStorage.java \
   src/healthreport/MockHealthReportSQLiteOpenHelper.java \
   src/healthreport/MockProfileInformationCache.java \
   src/healthreport/prune/TestHealthReportPruneService.java \
   src/healthreport/prune/TestPrunePolicyDatabaseStorage.java \
   src/healthreport/TestEnvironmentBuilder.java \
   src/healthreport/TestEnvironmentV1HashAppender.java \
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/fxa/TestFirefoxAccounts.java
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.fxa;
+
+import java.util.EnumSet;
+
+import junit.framework.Assert;
+
+import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
+import org.mozilla.gecko.fxa.FirefoxAccounts;
+import org.mozilla.gecko.fxa.sync.FxAccountSyncAdapter;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.setup.Constants;
+
+import android.content.ContentResolver;
+import android.os.Bundle;
+
+public class TestFirefoxAccounts extends AndroidSyncTestCase {
+  private static class InnerFirefoxAccounts extends FirefoxAccounts {
+    // For testing, since putHintsToSync is not public.
+    public static Bundle makeTestBundle(EnumSet<SyncHint> syncHints, String[] stagesToSync, String[] stagesToSkip) {
+      final Bundle bundle = new Bundle();
+      FirefoxAccounts.putHintsToSync(bundle, syncHints);
+      Utils.putStageNamesToSync(bundle, stagesToSync, stagesToSkip);
+      return bundle;
+    }
+  }
+
+  protected void assertBundle(Bundle bundle, boolean manual, boolean respectLocal, boolean respectRemote, String stagesToSync, String stagesToSkip) {
+    Assert.assertEquals(manual, bundle.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL));
+    Assert.assertEquals(respectLocal, bundle.getBoolean(FxAccountSyncAdapter.SYNC_EXTRAS_RESPECT_LOCAL_RATE_LIMIT));
+    Assert.assertEquals(respectRemote, bundle.getBoolean(FxAccountSyncAdapter.SYNC_EXTRAS_RESPECT_REMOTE_SERVER_BACKOFF));
+    Assert.assertEquals(stagesToSync, bundle.getString(Constants.EXTRAS_KEY_STAGES_TO_SYNC));
+    Assert.assertEquals(stagesToSkip, bundle.getString(Constants.EXTRAS_KEY_STAGES_TO_SKIP));
+  }
+
+  public void testMakeTestBundle() {
+    Bundle bundle;
+    bundle = InnerFirefoxAccounts.makeTestBundle(FirefoxAccounts.FORCE, new String[] { "clients" }, null);
+    assertBundle(bundle, true, false, false, "{\"clients\":0}", null);
+    assertEquals(FirefoxAccounts.FORCE, FirefoxAccounts.getHintsToSyncFromBundle(bundle));
+
+    bundle = InnerFirefoxAccounts.makeTestBundle(FirefoxAccounts.NOW, null, new String[] { "bookmarks" });
+    assertBundle(bundle, true, true, true, null, "{\"bookmarks\":0}");
+    assertEquals(FirefoxAccounts.NOW, FirefoxAccounts.getHintsToSyncFromBundle(bundle));
+
+    bundle = InnerFirefoxAccounts.makeTestBundle(FirefoxAccounts.SOON, null, null);
+    assertBundle(bundle, false, true, true, null, null);
+    assertEquals(FirefoxAccounts.SOON, FirefoxAccounts.getHintsToSyncFromBundle(bundle));
+  }
+}
--- a/mobile/android/tests/background/junit3/src/fxa/authenticator/TestAccountPickler.java
+++ b/mobile/android/tests/background/junit3/src/fxa/authenticator/TestAccountPickler.java
@@ -99,17 +99,17 @@ public class TestAccountPickler extends 
     // TODO: Write and use AndroidFxAccount.equals
     // TODO: protected.
     //assertEquals(expected.getAccountVersion(), actual.getAccountVersion());
     assertEquals(expected.getProfile(), actual.getProfile());
     assertEquals(expected.getAccountServerURI(), actual.getAccountServerURI());
     assertEquals(expected.getAudience(), actual.getAudience());
     assertEquals(expected.getTokenServerURI(), actual.getTokenServerURI());
     assertEquals(expected.getSyncPrefsPath(), actual.getSyncPrefsPath());
-    assertEquals(expected.isSyncingEnabled(), actual.isSyncingEnabled());
+    assertEquals(expected.isSyncing(), actual.isSyncing());
     assertEquals(expected.getEmail(), actual.getEmail());
     assertStateEquals(expected.getState(), actual.getState());
   }
 
   private void assertStateEquals(final State expected, final State actual) throws Exception {
     // TODO: Write and use State.equals. Thus, this is only thorough for the State base class.
     assertEquals(expected.getStateLabel(), actual.getStateLabel());
     assertEquals(expected.email, actual.email);