Bug 1224708 : Update SyncPreference asynchronously using Loaders r=nalexander
authorvivek <vivekb.balakrishnan@gmail.com>
Tue, 17 Nov 2015 21:24:18 +0200
changeset 277640 d5193b7ae7c54aea4deaa8bfbc27a07aa45783de
parent 277639 b92cd30cf34f0f4fee029c6ce21e727625aa7543
child 277641 a25f919304364b6ac19e7a52ea935070a372b65f
push id29825
push userryanvm@gmail.com
push dateSat, 26 Dec 2015 01:49:53 +0000
treeherdermozilla-central@4a559a618d67 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnalexander
bugs1224708
milestone46.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1224708 : Update SyncPreference asynchronously using Loaders r=nalexander
mobile/android/base/android-services.mozbuild
mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferenceFragment.java
mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
mobile/android/base/java/org/mozilla/gecko/preferences/SyncPreference.java
mobile/android/services/src/main/java/org/mozilla/gecko/fxa/AccountLoader.java
mobile/android/services/src/main/java/org/mozilla/gecko/fxa/AccountLoaderNative.java
--- a/mobile/android/base/android-services.mozbuild
+++ b/mobile/android/base/android-services.mozbuild
@@ -844,16 +844,17 @@ sync_java_files = [TOPSRCDIR + '/mobile/
     'browserid/verifier/AbstractBrowserIDRemoteVerifierClient.java',
     'browserid/verifier/BrowserIDRemoteVerifierClient10.java',
     'browserid/verifier/BrowserIDRemoteVerifierClient20.java',
     'browserid/verifier/BrowserIDVerifierClient.java',
     'browserid/verifier/BrowserIDVerifierDelegate.java',
     'browserid/verifier/BrowserIDVerifierException.java',
     'browserid/VerifyingPublicKey.java',
     'fxa/AccountLoader.java',
+    'fxa/AccountLoaderNative.java',
     'fxa/activities/CustomColorPreference.java',
     'fxa/activities/FxAccountAbstractActivity.java',
     'fxa/activities/FxAccountConfirmAccountActivityWeb.java',
     'fxa/activities/FxAccountFinishMigratingActivityWeb.java',
     'fxa/activities/FxAccountGetStartedActivityWeb.java',
     'fxa/activities/FxAccountStatusActivity.java',
     'fxa/activities/FxAccountStatusFragment.java',
     'fxa/activities/FxAccountUpdateCredentialsActivityWeb.java',
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferenceFragment.java
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferenceFragment.java
@@ -11,36 +11,45 @@ import org.mozilla.gecko.AppConstants.Ve
 import org.mozilla.gecko.BrowserLocaleManager;
 import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.LocaleManager;
 import org.mozilla.gecko.PrefsHelper;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.TelemetryContract.Method;
+import org.mozilla.gecko.fxa.AccountLoaderNative;
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
 
+import android.accounts.Account;
 import android.app.ActionBar;
 import android.app.Activity;
+import android.app.LoaderManager;
 import android.content.Context;
+import android.content.Loader;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.os.Bundle;
 import android.preference.PreferenceActivity;
 import android.preference.PreferenceFragment;
 import android.preference.PreferenceScreen;
 import android.util.Log;
 import android.view.Menu;
 import android.view.MenuInflater;
 
 /* A simple implementation of PreferenceFragment for large screen devices
  * This will strip category headers (so that they aren't shown to the user twice)
  * as well as initializing Gecko prefs when a fragment is shown.
 */
 public class GeckoPreferenceFragment extends PreferenceFragment {
 
+    public static final int ACCOUNT_LOADER_ID = 1;
+    private AccountLoaderCallbacks accountLoaderCallbacks;
+    private SyncPreference syncPreference;
+
     @Override
     public void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
         Log.d(LOGTAG, "onConfigurationChanged: " + newConfig.locale);
 
         final Activity context = getActivity();
 
         final LocaleManager localeManager = BrowserLocaleManager.getInstance();
@@ -74,16 +83,17 @@ public class GeckoPreferenceFragment ext
             setHasOptionsMenu(true);
         }
 
         addPreferencesFromResource(res);
 
         PreferenceScreen screen = getPreferenceScreen();
         setPreferenceScreen(screen);
         mPrefsRequestId = ((GeckoPreferences)getActivity()).setupPreferences(screen);
+        syncPreference = (SyncPreference) findPreference(GeckoPreferences.PREFS_SYNC);
     }
 
     /**
      * Return the title to use for this preference fragment.
      *
      * We only return titles for the preference screens that are
      * launched directly, and thus might need to be redisplayed.
      *
@@ -158,21 +168,31 @@ public class GeckoPreferenceFragment ext
             final ActionBar actionBar = activity.getActionBar();
             if (actionBar != null) {
                 actionBar.setTitle(newTitle);
             }
         }
     }
 
     @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+        accountLoaderCallbacks = new AccountLoaderCallbacks();
+        getLoaderManager().initLoader(ACCOUNT_LOADER_ID, null, accountLoaderCallbacks);
+    }
+
+    @Override
     public void onResume() {
         // This is a little delicate. Ensure that you do nothing prior to
         // super.onResume that you wouldn't do in onCreate.
         applyLocale(Locale.getDefault());
         super.onResume();
+
+        // Force reload as the account may have been deleted while the app was in background.
+        getLoaderManager().restartLoader(ACCOUNT_LOADER_ID, null, accountLoaderCallbacks);
     }
 
     private void applyLocale(final Locale currentLocale) {
         final Context context = getActivity().getApplicationContext();
 
         BrowserLocaleManager.getInstance().updateConfiguration(context, currentLocale);
 
         if (!currentLocale.equals(lastLocale)) {
@@ -232,9 +252,37 @@ public class GeckoPreferenceFragment ext
             PrefsHelper.removeObserver(mPrefsRequestId);
         }
 
         final int res = getResource();
         if (res == R.xml.preferences) {
             Telemetry.stopUISession(TelemetryContract.Session.SETTINGS);
         }
     }
+
+    private class AccountLoaderCallbacks implements LoaderManager.LoaderCallbacks<Account> {
+        @Override
+        public Loader<Account> onCreateLoader(int id, Bundle args) {
+            return new AccountLoaderNative(getActivity());
+        }
+
+        @Override
+        public void onLoadFinished(Loader<Account> loader, Account account) {
+            if (syncPreference == null) {
+                return;
+            }
+
+            if (account == null) {
+                syncPreference.update(null);
+                return;
+            }
+
+            syncPreference.update(new AndroidFxAccount(getActivity(), account));
+        }
+
+        @Override
+        public void onLoaderReset(Loader<Account> loader) {
+            if (syncPreference != null) {
+                syncPreference.update(null);
+            }
+        }
+    }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
@@ -102,16 +102,17 @@ OnSharedPreferenceChangeListener
     // some devices look bad. Don't use transitions on those
     // devices.
     private static final boolean NO_TRANSITIONS = HardwareUtils.IS_KINDLE_DEVICE;
 
     public static final String NON_PREF_PREFIX = "android.not_a_preference.";
     public static final String INTENT_EXTRA_RESOURCES = "resource";
     public static final String PREFS_TRACKING_PROTECTION_PROMPT_SHOWN = NON_PREF_PREFIX + "trackingProtectionPromptShown";
     public static String PREFS_HEALTHREPORT_UPLOAD_ENABLED = NON_PREF_PREFIX + "healthreport.uploadEnabled";
+    public static final String PREFS_SYNC = NON_PREF_PREFIX + "sync";
 
     private static boolean sIsCharEncodingEnabled;
     private boolean mInitialized;
     private int mPrefsRequestId;
     private List<Header> mHeaders;
 
     // These match keys in resources/xml*/preferences*.xml
     private static final String PREFS_SEARCH_RESTORE_DEFAULTS = NON_PREF_PREFIX + "search.restore_defaults";
@@ -123,17 +124,16 @@ OnSharedPreferenceChangeListener
     private static final String PREFS_UPDATER_AUTODOWNLOAD = "app.update.autodownload";
     private static final String PREFS_UPDATER_URL = "app.update.url.android";
     private static final String PREFS_GEO_REPORTING = NON_PREF_PREFIX + "app.geo.reportdata";
     private static final String PREFS_GEO_LEARN_MORE = NON_PREF_PREFIX + "geo.learn_more";
     private static final String PREFS_HEALTHREPORT_LINK = NON_PREF_PREFIX + "healthreport.link";
     private static final String PREFS_DEVTOOLS_REMOTE_USB_ENABLED = "devtools.remote.usb.enabled";
     private static final String PREFS_DEVTOOLS_REMOTE_WIFI_ENABLED = "devtools.remote.wifi.enabled";
     private static final String PREFS_DEVTOOLS_REMOTE_LINK = NON_PREF_PREFIX + "remote_debugging.link";
-    private static final String PREFS_SYNC = NON_PREF_PREFIX + "sync";
     private static final String PREFS_TRACKING_PROTECTION = "privacy.trackingprotection.state";
     private static final String PREFS_TRACKING_PROTECTION_PB = "privacy.trackingprotection.pbmode.enabled";
     public static final String PREFS_VOICE_INPUT_ENABLED = NON_PREF_PREFIX + "voice_input_enabled";
     public static final String PREFS_QRCODE_ENABLED = NON_PREF_PREFIX + "qrcode_enabled";
     private static final String PREFS_ADVANCED = NON_PREF_PREFIX + "advanced.enabled";
     private static final String PREFS_ACCESSIBILITY = NON_PREF_PREFIX + "accessibility.enabled";
     private static final String PREFS_CUSTOMIZE_HOME = NON_PREF_PREFIX + "customize_home";
     private static final String PREFS_TRACKING_PROTECTION_PRIVATE_BROWSING = "privacy.trackingprotection.pbmode.enabled";
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/SyncPreference.java
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/SyncPreference.java
@@ -25,16 +25,17 @@ import org.mozilla.gecko.TelemetryContra
 import org.mozilla.gecko.fxa.FirefoxAccounts;
 import org.mozilla.gecko.fxa.FxAccountConstants;
 import org.mozilla.gecko.fxa.activities.FxAccountWebFlowActivity;
 import org.mozilla.gecko.fxa.activities.PicassoPreferenceIconTarget;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.setup.SyncAccounts;
 import org.mozilla.gecko.sync.setup.activities.SetupSyncActivity;
+import org.mozilla.gecko.util.ThreadUtils;
 
 class SyncPreference extends Preference {
     private static final boolean DEFAULT_TO_FXA = true;
 
     private final Context mContext;
     private final Target profileAvatarTarget;
 
     public SyncPreference(Context context, AttributeSet attrs) {
@@ -59,30 +60,43 @@ class SyncPreference extends Preference 
     private void launchFxASetup() {
         final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_GET_STARTED);
         intent.putExtra(FxAccountWebFlowActivity.EXTRA_ENDPOINT, FxAccountConstants.ENDPOINT_PREFERENCES);
         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
         mContext.startActivity(intent);
     }
 
-    @Override
-    protected void onBindView(View view) {
-        super.onBindView(view);
+    public void update(final AndroidFxAccount fxAccount) {
+        if (fxAccount == null) {
+            ThreadUtils.postToUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    setTitle(R.string.pref_sync);
+                    setSummary(R.string.pref_sync_summary);
+                    if (AppConstants.Versions.feature11Plus) {
+                        // Cancel any pending task.
+                        Picasso.with(mContext).cancelRequest(profileAvatarTarget);
+                        // Clear previously set icon.
+                        setIcon(R.drawable.sync_avatar_default);
+                    }
 
-        Account account = FirefoxAccounts.getFirefoxAccount(mContext);
-        if (account == null) {
+                }
+            });
             return;
         }
 
-        final AndroidFxAccount fxAccount = new AndroidFxAccount(mContext, account);
-        final TextView title = (TextView) view.findViewById(android.R.id.title);
-        final TextView summary = (TextView) view.findViewById(android.R.id.summary);
-        title.setText(fxAccount.getEmail());
-        summary.setVisibility(View.GONE);
+        // Update title from account email.
+        ThreadUtils.postToUiThread(new Runnable() {
+            @Override
+            public void run() {
+                setTitle(fxAccount.getEmail());
+                setSummary("");
+            }
+        });
 
         // Updating icons from Java is not supported prior to API 11.
         if (!AppConstants.Versions.feature11Plus) {
             return;
         }
 
         final ExtendedJSONObject profileJSON = fxAccount.getProfileJSON();
         if (profileJSON == null) {
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/AccountLoader.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/AccountLoader.java
@@ -166,16 +166,18 @@ public class AccountLoader extends Async
   }
 
   protected void registerObserver(BroadcastReceiver observer) {
     final IntentFilter intentFilter = new IntentFilter();
     // Android Account added or removed.
     intentFilter.addAction(AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION);
     // Firefox Account internal state changed.
     intentFilter.addAction(FxAccountConstants.ACCOUNT_STATE_CHANGED_ACTION);
+    // Firefox Account profile state changed.
+    intentFilter.addAction(FxAccountConstants.ACCOUNT_PROFILE_JSON_UPDATED_ACTION);
 
     // null means: "the main thread of the process will be used." We must call
     // onContentChanged on the main thread of the process; this ensures we do.
     final Handler handler = null;
     getContext().registerReceiver(observer, intentFilter, FxAccountConstants.PER_ACCOUNT_TYPE_PERMISSION, handler);
   }
 
   protected void unregisterObserver(BroadcastReceiver observer) {
new file mode 100644
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/AccountLoaderNative.java
@@ -0,0 +1,182 @@
+/* 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 android.accounts.Account;
+import android.accounts.AccountManager;
+import android.annotation.TargetApi;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.content.AsyncTaskLoader;
+
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+
+/**
+ * A Loader that queries and updates based on the existence of only Firefox Android Account.
+ *
+ * The loader is similar to @Link{AccountLoader} that is intended to be used with native LoaderManager.
+ * Note: This loader available only on devices running Honeycomb or later Android version.
+ *
+ * The loader returns an Android Account (of either Account type) if an account
+ * exists, and null to indicate no Account is present.
+ *
+ * The loader listens for Accounts added and deleted, and also Accounts being
+ * updated by Sync or another Activity, via the use of
+ * {@link AndroidFxAccount#setState(org.mozilla.gecko.fxa.login.State)}.
+ * Be careful of message loops if you update the account state from an activity
+ * that uses this loader.
+ *
+ * This implementation is based on
+ * <a href="http://www.androiddesignpatterns.com/2012/08/implementing-loaders.html">http://www.androiddesignpatterns.com/2012/08/implementing-loaders.html</a>.
+ */
+public class AccountLoaderNative extends AsyncTaskLoader<Account> {
+  protected Account account = null;
+  protected BroadcastReceiver broadcastReceiver = null;
+
+  @TargetApi(11)
+  public AccountLoaderNative(Context context) {
+    super(context);
+  }
+
+  // Task that performs the asynchronous load **/
+  @Override
+  public Account loadInBackground() {
+    final Context context = getContext();
+    return FirefoxAccounts.getFirefoxAccount(context);
+  }
+
+  // Deliver the results to the registered listener.
+  @Override
+  public void deliverResult(Account data) {
+    if (isReset()) {
+      // The Loader has been reset; ignore the result and invalidate the data.
+      releaseResources(data);
+      return;
+    }
+
+    // Hold a reference to the old data so it doesn't get garbage collected.
+    // We must protect it until the new data has been delivered.
+    Account oldData = account;
+    account = data;
+
+    if (isStarted()) {
+      // If the Loader is in a started state, deliver the results to the
+      // client. The superclass method does this for us.
+      super.deliverResult(data);
+    }
+
+    // Invalidate the old data as we don't need it any more.
+    if (oldData != null && oldData != data) {
+      releaseResources(oldData);
+    }
+  }
+
+  // The Loader’s state-dependent behavior.
+  @Override
+  protected void onStartLoading() {
+    if (account != null) {
+      // Deliver any previously loaded data immediately.
+      deliverResult(account);
+    }
+
+    // Begin monitoring the underlying data source.
+    if (broadcastReceiver == null) {
+      broadcastReceiver = makeNewObserver();
+      registerObserver(broadcastReceiver);
+    }
+
+    if (takeContentChanged() || account == null) {
+      // When the observer detects a change, it should call onContentChanged()
+      // on the Loader, which will cause the next call to takeContentChanged()
+      // to return true. If this is ever the case (or if the current data is
+      // null), we force a new load.
+      forceLoad();
+    }
+  }
+
+  @Override
+  protected void onStopLoading() {
+    // The Loader is in a stopped state, so we should attempt to cancel the
+    // current load (if there is one).
+    cancelLoad();
+
+    // Note that we leave the observer as is. Loaders in a stopped state
+    // should still monitor the data source for changes so that the Loader
+    // will know to force a new load if it is ever started again.
+  }
+
+  @Override
+  protected void onReset() {
+    // Ensure the loader has been stopped.  In CursorLoader and the template
+    // this code follows (see the class comment), this is onStopLoading, which
+    // appears to not set the started flag (see Loader itself).
+    stopLoading();
+
+    // At this point we can release the resources associated with 'mData'.
+    if (account != null) {
+      releaseResources(account);
+      account = null;
+    }
+
+    // The Loader is being reset, so we should stop monitoring for changes.
+    if (broadcastReceiver != null) {
+      final BroadcastReceiver observer = broadcastReceiver;
+      broadcastReceiver = null;
+      unregisterObserver(observer);
+    }
+  }
+
+  @Override
+  public void onCanceled(Account data) {
+    // Attempt to cancel the current asynchronous load.
+    super.onCanceled(data);
+
+    // The load has been canceled, so we should release the resources
+    // associated with 'data'.
+    releaseResources(data);
+  }
+
+  private void releaseResources(Account data) {
+    // For a simple List, there is nothing to do. For something like a Cursor, we
+    // would close it in this method. All resources associated with the Loader
+    // should be released here.
+  }
+
+  // Observer which receives notifications when the data changes.
+  protected BroadcastReceiver makeNewObserver() {
+    final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
+      @Override
+      public void onReceive(Context context, Intent intent) {
+        // Must be called on the main thread of the process. We register the
+        // broadcast receiver with a null Handler (see registerObserver), which
+        // ensures we're on the main thread when we receive this intent.
+        onContentChanged();
+      }
+    };
+    return broadcastReceiver;
+  }
+
+  protected void registerObserver(BroadcastReceiver observer) {
+    final IntentFilter intentFilter = new IntentFilter();
+    // Android Account added or removed.
+    intentFilter.addAction(AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION);
+    // Firefox Account internal state changed.
+    intentFilter.addAction(FxAccountConstants.ACCOUNT_STATE_CHANGED_ACTION);
+    // Firefox Account profile state changed.
+    intentFilter.addAction(FxAccountConstants.ACCOUNT_PROFILE_JSON_UPDATED_ACTION);
+
+    // null means: "the main thread of the process will be used." We must call
+    // onContentChanged on the main thread of the process; this ensures we do.
+    final Handler handler = null;
+    getContext().registerReceiver(observer, intentFilter, FxAccountConstants.PER_ACCOUNT_TYPE_PERMISSION, handler);
+  }
+
+  protected void unregisterObserver(BroadcastReceiver observer) {
+    getContext().unregisterReceiver(observer);
+  }
+}