mobile/android/base/fxa/activities/FxAccountStatusFragment.java

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

Correct previous dual key logic pending first delivery installment.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 package org.mozilla.gecko.fxa.activities;
     7 import java.util.HashMap;
     8 import java.util.Map;
     9 import java.util.Set;
    11 import org.mozilla.gecko.R;
    12 import org.mozilla.gecko.background.common.log.Logger;
    13 import org.mozilla.gecko.background.preferences.PreferenceFragment;
    14 import org.mozilla.gecko.fxa.FirefoxAccounts;
    15 import org.mozilla.gecko.fxa.FxAccountConstants;
    16 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
    17 import org.mozilla.gecko.fxa.login.Married;
    18 import org.mozilla.gecko.fxa.login.State;
    19 import org.mozilla.gecko.fxa.sync.FxAccountSyncStatusHelper;
    20 import org.mozilla.gecko.sync.SyncConfiguration;
    22 import android.accounts.Account;
    23 import android.content.ContentResolver;
    24 import android.content.Context;
    25 import android.content.Intent;
    26 import android.content.SharedPreferences;
    27 import android.os.Bundle;
    28 import android.os.Handler;
    29 import android.preference.CheckBoxPreference;
    30 import android.preference.Preference;
    31 import android.preference.Preference.OnPreferenceClickListener;
    32 import android.preference.PreferenceCategory;
    33 import android.preference.PreferenceScreen;
    35 /**
    36  * A fragment that displays the status of an AndroidFxAccount.
    37  * <p>
    38  * The owning activity is responsible for providing an AndroidFxAccount at
    39  * appropriate times.
    40  */
    41 public class FxAccountStatusFragment extends PreferenceFragment implements OnPreferenceClickListener {
    42   private static final String LOG_TAG = FxAccountStatusFragment.class.getSimpleName();
    44   // When a checkbox is toggled, wait 5 seconds (for other checkbox actions)
    45   // before trying to sync. Should we kill off the fragment before the sync
    46   // request happens, that's okay: the runnable will run if the UI thread is
    47   // still around to service it, and since we're not updating any UI, we'll just
    48   // schedule the sync as usual. See also comment below about garbage
    49   // collection.
    50   private static final long DELAY_IN_MILLISECONDS_BEFORE_REQUESTING_SYNC = 5 * 1000;
    52   protected Preference emailPreference;
    54   protected Preference needsPasswordPreference;
    55   protected Preference needsUpgradePreference;
    56   protected Preference needsVerificationPreference;
    57   protected Preference needsMasterSyncAutomaticallyEnabledPreference;
    58   protected Preference needsAccountEnabledPreference;
    60   protected PreferenceCategory syncCategory;
    62   protected CheckBoxPreference bookmarksPreference;
    63   protected CheckBoxPreference historyPreference;
    64   protected CheckBoxPreference tabsPreference;
    65   protected CheckBoxPreference passwordsPreference;
    67   protected volatile AndroidFxAccount fxAccount;
    69   // Used to post delayed sync requests.
    70   protected Handler handler;
    72   // Member variable so that re-posting pushes back the already posted instance.
    73   // This Runnable references the fxAccount above, but it is not specific to a
    74   // single account. (That is, it does not capture a single account instance.)
    75   protected Runnable requestSyncRunnable;
    77   protected final InnerSyncStatusDelegate syncStatusDelegate = new InnerSyncStatusDelegate();
    79   protected Preference ensureFindPreference(String key) {
    80     Preference preference = findPreference(key);
    81     if (preference == null) {
    82       throw new IllegalStateException("Could not find preference with key: " + key);
    83     }
    84     return preference;
    85   }
    87   @Override
    88   public void onCreate(Bundle savedInstanceState) {
    89     super.onCreate(savedInstanceState);
    90     addPreferencesFromResource(R.xml.fxaccount_status_prefscreen);
    92     emailPreference = ensureFindPreference("email");
    94     needsPasswordPreference = ensureFindPreference("needs_credentials");
    95     needsUpgradePreference = ensureFindPreference("needs_upgrade");
    96     needsVerificationPreference = ensureFindPreference("needs_verification");
    97     needsMasterSyncAutomaticallyEnabledPreference = ensureFindPreference("needs_master_sync_automatically_enabled");
    98     needsAccountEnabledPreference = ensureFindPreference("needs_account_enabled");
   100     syncCategory = (PreferenceCategory) ensureFindPreference("sync_category");
   102     bookmarksPreference = (CheckBoxPreference) ensureFindPreference("bookmarks");
   103     historyPreference = (CheckBoxPreference) ensureFindPreference("history");
   104     tabsPreference = (CheckBoxPreference) ensureFindPreference("tabs");
   105     passwordsPreference = (CheckBoxPreference) ensureFindPreference("passwords");
   107     if (!FxAccountConstants.LOG_PERSONAL_INFORMATION) {
   108       removeDebugButtons();
   109     } else {
   110       connectDebugButtons();
   111     }
   113     needsPasswordPreference.setOnPreferenceClickListener(this);
   114     needsVerificationPreference.setOnPreferenceClickListener(this);
   115     needsAccountEnabledPreference.setOnPreferenceClickListener(this);
   117     bookmarksPreference.setOnPreferenceClickListener(this);
   118     historyPreference.setOnPreferenceClickListener(this);
   119     tabsPreference.setOnPreferenceClickListener(this);
   120     passwordsPreference.setOnPreferenceClickListener(this);
   121   }
   123   /**
   124    * We intentionally don't refresh here. Our owning activity is responsible for
   125    * providing an AndroidFxAccount to our refresh method in its onResume method.
   126    */
   127   @Override
   128   public void onResume() {
   129     super.onResume();
   130   }
   132   @Override
   133   public boolean onPreferenceClick(Preference preference) {
   134     if (preference == needsPasswordPreference) {
   135       Intent intent = new Intent(getActivity(), FxAccountUpdateCredentialsActivity.class);
   136       // Per http://stackoverflow.com/a/8992365, this triggers a known bug with
   137       // the soft keyboard not being shown for the started activity. Why, Android, why?
   138       intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
   139       startActivity(intent);
   141       return true;
   142     }
   144     if (preference == needsVerificationPreference) {
   145       FxAccountConfirmAccountActivity.resendCode(getActivity().getApplicationContext(), fxAccount);
   147       Intent intent = new Intent(getActivity(), FxAccountConfirmAccountActivity.class);
   148       // Per http://stackoverflow.com/a/8992365, this triggers a known bug with
   149       // the soft keyboard not being shown for the started activity. Why, Android, why?
   150       intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
   151       startActivity(intent);
   153       return true;
   154     }
   156     if (preference == needsAccountEnabledPreference) {
   157       fxAccount.enableSyncing();
   158       refresh();
   160       return true;
   161     }
   163     if (preference == bookmarksPreference ||
   164         preference == historyPreference ||
   165         preference == passwordsPreference ||
   166         preference == tabsPreference) {
   167       saveEngineSelections();
   168       return true;
   169     }
   171     return false;
   172   }
   174   protected void setCheckboxesEnabled(boolean enabled) {
   175     bookmarksPreference.setEnabled(enabled);
   176     historyPreference.setEnabled(enabled);
   177     tabsPreference.setEnabled(enabled);
   178     passwordsPreference.setEnabled(enabled);
   179   }
   181   /**
   182    * Show at most one error preference, hiding all others.
   183    *
   184    * @param errorPreferenceToShow
   185    *          single error preference to show; if null, hide all error preferences
   186    */
   187   protected void showOnlyOneErrorPreference(Preference errorPreferenceToShow) {
   188     final Preference[] errorPreferences = new Preference[] {
   189         this.needsPasswordPreference,
   190         this.needsUpgradePreference,
   191         this.needsVerificationPreference,
   192         this.needsMasterSyncAutomaticallyEnabledPreference,
   193         this.needsAccountEnabledPreference,
   194     };
   195     for (Preference errorPreference : errorPreferences) {
   196       final boolean currentlyShown = null != findPreference(errorPreference.getKey());
   197       final boolean shouldBeShown = errorPreference == errorPreferenceToShow;
   198       if (currentlyShown == shouldBeShown) {
   199         continue;
   200       }
   201       if (shouldBeShown) {
   202         syncCategory.addPreference(errorPreference);
   203       } else {
   204         syncCategory.removePreference(errorPreference);
   205       }
   206     }
   207   }
   209   protected void showNeedsPassword() {
   210     syncCategory.setTitle(R.string.fxaccount_status_sync);
   211     showOnlyOneErrorPreference(needsPasswordPreference);
   212     setCheckboxesEnabled(false);
   213   }
   215   protected void showNeedsUpgrade() {
   216     syncCategory.setTitle(R.string.fxaccount_status_sync);
   217     showOnlyOneErrorPreference(needsUpgradePreference);
   218     setCheckboxesEnabled(false);
   219   }
   221   protected void showNeedsVerification() {
   222     syncCategory.setTitle(R.string.fxaccount_status_sync);
   223     showOnlyOneErrorPreference(needsVerificationPreference);
   224     setCheckboxesEnabled(false);
   225   }
   227   protected void showNeedsMasterSyncAutomaticallyEnabled() {
   228     syncCategory.setTitle(R.string.fxaccount_status_sync);
   229     showOnlyOneErrorPreference(needsMasterSyncAutomaticallyEnabledPreference);
   230     setCheckboxesEnabled(false);
   231   }
   233   protected void showNeedsAccountEnabled() {
   234     syncCategory.setTitle(R.string.fxaccount_status_sync);
   235     showOnlyOneErrorPreference(needsAccountEnabledPreference);
   236     setCheckboxesEnabled(false);
   237   }
   239   protected void showConnected() {
   240     syncCategory.setTitle(R.string.fxaccount_status_sync_enabled);
   241     showOnlyOneErrorPreference(null);
   242     setCheckboxesEnabled(true);
   243   }
   245   protected class InnerSyncStatusDelegate implements FirefoxAccounts.SyncStatusListener {
   246     protected final Runnable refreshRunnable = new Runnable() {
   247       @Override
   248       public void run() {
   249         refresh();
   250       }
   251     };
   253     @Override
   254     public Context getContext() {
   255       return FxAccountStatusFragment.this.getActivity();
   256     }
   258     @Override
   259     public Account getAccount() {
   260       return fxAccount.getAndroidAccount();
   261     }
   263     @Override
   264     public void onSyncStarted() {
   265       if (fxAccount == null) {
   266         return;
   267       }
   268       Logger.info(LOG_TAG, "Got sync started message; refreshing.");
   269       getActivity().runOnUiThread(refreshRunnable);
   270     }
   272     @Override
   273     public void onSyncFinished() {
   274       if (fxAccount == null) {
   275         return;
   276       }
   277       Logger.info(LOG_TAG, "Got sync finished message; refreshing.");
   278       getActivity().runOnUiThread(refreshRunnable);
   279     }
   280   }
   282   /**
   283    * Notify the fragment that a new AndroidFxAccount instance is current.
   284    * <p>
   285    * <b>Important:</b> call this method on the UI thread!
   286    * <p>
   287    * In future, this might be a Loader.
   288    *
   289    * @param fxAccount new instance.
   290    */
   291   public void refresh(AndroidFxAccount fxAccount) {
   292     if (fxAccount == null) {
   293       throw new IllegalArgumentException("fxAccount must not be null");
   294     }
   295     this.fxAccount = fxAccount;
   297     handler = new Handler(); // Attached to current (assumed to be UI) thread.
   299     // Runnable is not specific to one Firefox Account. This runnable will keep
   300     // a reference to this fragment alive, but we expect posted runnables to be
   301     // serviced very quickly, so this is not an issue.
   302     requestSyncRunnable = new RequestSyncRunnable();
   304     // We would very much like register these status observers in bookended
   305     // onResume/onPause calls, but because the Fragment gets onResume during the
   306     // Activity's super.onResume, it hasn't yet been told its Firefox Account.
   307     // So we register the observer here (and remove it in onPause), and open
   308     // ourselves to the possibility that we don't have properly paired
   309     // register/unregister calls.
   310     FxAccountSyncStatusHelper.getInstance().startObserving(syncStatusDelegate);
   312     refresh();
   313   }
   315   @Override
   316   public void onPause() {
   317     super.onPause();
   318     FxAccountSyncStatusHelper.getInstance().stopObserving(syncStatusDelegate);
   319   }
   321   protected void refresh() {
   322     // refresh is called from our onResume, which can happen before the owning
   323     // Activity tells us about an account (via our public
   324     // refresh(AndroidFxAccount) method).
   325     if (fxAccount == null) {
   326       throw new IllegalArgumentException("fxAccount must not be null");
   327     }
   329     emailPreference.setTitle(fxAccount.getEmail());
   331     try {
   332       // There are error states determined by Android, not the login state
   333       // machine, and we have a chance to present these states here.  We handle
   334       // them specially, since we can't surface these states as part of syncing,
   335       // because they generally stop syncs from happening regularly.
   337       // The action to enable syncing the Firefox Account doesn't require
   338       // leaving this activity, so let's present it first.
   339       final boolean isSyncing = fxAccount.isSyncing();
   340       if (!isSyncing) {
   341         showNeedsAccountEnabled();
   342         return;
   343       }
   345       // Interrogate the Firefox Account's state.
   346       State state = fxAccount.getState();
   347       switch (state.getNeededAction()) {
   348       case NeedsUpgrade:
   349         showNeedsUpgrade();
   350         break;
   351       case NeedsPassword:
   352         showNeedsPassword();
   353         break;
   354       case NeedsVerification:
   355         showNeedsVerification();
   356         break;
   357       default:
   358         showConnected();
   359       }
   361       // We check for the master setting last, since it is not strictly
   362       // necessary for the user to address this error state: it's really a
   363       // warning state. We surface it for the user's convenience, and to prevent
   364       // confused folks wondering why Sync is not working at all.
   365       final boolean masterSyncAutomatically = ContentResolver.getMasterSyncAutomatically();
   366       if (!masterSyncAutomatically) {
   367         showNeedsMasterSyncAutomaticallyEnabled();
   368         return;
   369       }
   370     } finally {
   371       // No matter our state, we should update the checkboxes.
   372       updateSelectedEngines();
   373     }
   374   }
   376   /**
   377    * Query shared prefs for the current engine state, and update the UI
   378    * accordingly.
   379    * <p>
   380    * In future, we might want this to be on a background thread, or implemented
   381    * as a Loader.
   382    */
   383   protected void updateSelectedEngines() {
   384     try {
   385       SharedPreferences syncPrefs = fxAccount.getSyncPrefs();
   386       Map<String, Boolean> engines = SyncConfiguration.getUserSelectedEngines(syncPrefs);
   387       if (engines != null) {
   388         bookmarksPreference.setChecked(engines.containsKey("bookmarks") && engines.get("bookmarks"));
   389         historyPreference.setChecked(engines.containsKey("history") && engines.get("history"));
   390         passwordsPreference.setChecked(engines.containsKey("passwords") && engines.get("passwords"));
   391         tabsPreference.setChecked(engines.containsKey("tabs") && engines.get("tabs"));
   392         return;
   393       }
   395       // We don't have user specified preferences.  Perhaps we have seen a meta/global?
   396       Set<String> enabledNames = SyncConfiguration.getEnabledEngineNames(syncPrefs);
   397       if (enabledNames != null) {
   398         bookmarksPreference.setChecked(enabledNames.contains("bookmarks"));
   399         historyPreference.setChecked(enabledNames.contains("history"));
   400         passwordsPreference.setChecked(enabledNames.contains("passwords"));
   401         tabsPreference.setChecked(enabledNames.contains("tabs"));
   402         return;
   403       }
   405       // Okay, we don't have userSelectedEngines or enabledEngines. That means
   406       // the user hasn't specified to begin with, we haven't specified here, and
   407       // we haven't already seen, Sync engines. We don't know our state, so
   408       // let's check everything (the default) and disable everything.
   409       bookmarksPreference.setChecked(true);
   410       historyPreference.setChecked(true);
   411       passwordsPreference.setChecked(true);
   412       tabsPreference.setChecked(true);
   413       setCheckboxesEnabled(false);
   414     } catch (Exception e) {
   415       Logger.warn(LOG_TAG, "Got exception getting engines to select; ignoring.", e);
   416       return;
   417     }
   418   }
   420   /**
   421    * Persist engine selections to local shared preferences, and request a sync
   422    * to persist selections to remote storage.
   423    */
   424   protected void saveEngineSelections() {
   425     final Map<String, Boolean> engineSelections = new HashMap<String, Boolean>();
   426     engineSelections.put("bookmarks", bookmarksPreference.isChecked());
   427     engineSelections.put("history", historyPreference.isChecked());
   428     engineSelections.put("passwords", passwordsPreference.isChecked());
   429     engineSelections.put("tabs", tabsPreference.isChecked());
   431     // No GlobalSession.config, so store directly to shared prefs. We do this on
   432     // a background thread to avoid IO on the main thread and strict mode
   433     // warnings.
   434     new Thread(new PersistEngineSelectionsRunnable(engineSelections)).start();
   435   }
   437   protected void requestDelayedSync() {
   438     Logger.info(LOG_TAG, "Posting a delayed request for a sync sometime soon.");
   439     handler.removeCallbacks(requestSyncRunnable);
   440     handler.postDelayed(requestSyncRunnable, DELAY_IN_MILLISECONDS_BEFORE_REQUESTING_SYNC);
   441   }
   443   /**
   444    * Remove all traces of debug buttons. By default, no debug buttons are shown.
   445    */
   446   protected void removeDebugButtons() {
   447     final PreferenceScreen statusScreen = (PreferenceScreen) ensureFindPreference("status_screen");
   448     final PreferenceCategory debugCategory = (PreferenceCategory) ensureFindPreference("debug_category");
   449     statusScreen.removePreference(debugCategory);
   450   }
   452   /**
   453    * A Runnable that persists engine selections to shared prefs, and then
   454    * requests a delayed sync.
   455    * <p>
   456    * References the member <code>fxAccount</code> and is specific to the Android
   457    * account associated to that account.
   458    */
   459   protected class PersistEngineSelectionsRunnable implements Runnable {
   460     private final Map<String, Boolean> engineSelections;
   462     protected PersistEngineSelectionsRunnable(Map<String, Boolean> engineSelections) {
   463       this.engineSelections = engineSelections;
   464     }
   466     @Override
   467     public void run() {
   468       try {
   469         // Name shadowing -- do you like it, or do you love it?
   470         AndroidFxAccount fxAccount = FxAccountStatusFragment.this.fxAccount;
   471         if (fxAccount == null) {
   472           return;
   473         }
   474         Logger.info(LOG_TAG, "Persisting engine selections: " + engineSelections.toString());
   475         SyncConfiguration.storeSelectedEnginesToPrefs(fxAccount.getSyncPrefs(), engineSelections);
   476         requestDelayedSync();
   477       } catch (Exception e) {
   478         Logger.warn(LOG_TAG, "Got exception persisting selected engines; ignoring.", e);
   479         return;
   480       }
   481     }
   482   }
   484   /**
   485    * A Runnable that requests a sync.
   486    * <p>
   487    * References the member <code>fxAccount</code>, but is not specific to the
   488    * Android account associated to that account.
   489    */
   490   protected class RequestSyncRunnable implements Runnable {
   491     @Override
   492     public void run() {
   493       // Name shadowing -- do you like it, or do you love it?
   494       AndroidFxAccount fxAccount = FxAccountStatusFragment.this.fxAccount;
   495       if (fxAccount == null) {
   496         return;
   497       }
   498       Logger.info(LOG_TAG, "Requesting a sync sometime soon.");
   499       fxAccount.requestSync();
   500     }
   501   }
   503   /**
   504    * A separate listener to separate debug logic from main code paths.
   505    */
   506   protected class DebugPreferenceClickListener implements OnPreferenceClickListener {
   507     @Override
   508     public boolean onPreferenceClick(Preference preference) {
   509       final String key = preference.getKey();
   510       if ("debug_refresh".equals(key)) {
   511         Logger.info(LOG_TAG, "Refreshing.");
   512         refresh();
   513       } else if ("debug_dump".equals(key)) {
   514         fxAccount.dump();
   515       } else if ("debug_force_sync".equals(key)) {
   516         Logger.info(LOG_TAG, "Force syncing.");
   517         fxAccount.requestSync(FirefoxAccounts.FORCE);
   518         // No sense refreshing, since the sync will complete in the future.
   519       } else if ("debug_forget_certificate".equals(key)) {
   520         State state = fxAccount.getState();
   521         try {
   522           Married married = (Married) state;
   523           Logger.info(LOG_TAG, "Moving to Cohabiting state: Forgetting certificate.");
   524           fxAccount.setState(married.makeCohabitingState());
   525           refresh();
   526         } catch (ClassCastException e) {
   527           Logger.info(LOG_TAG, "Not in Married state; can't forget certificate.");
   528           // Ignore.
   529         }
   530       } else if ("debug_require_password".equals(key)) {
   531         Logger.info(LOG_TAG, "Moving to Separated state: Forgetting password.");
   532         State state = fxAccount.getState();
   533         fxAccount.setState(state.makeSeparatedState());
   534         refresh();
   535       } else if ("debug_require_upgrade".equals(key)) {
   536         Logger.info(LOG_TAG, "Moving to Doghouse state: Requiring upgrade.");
   537         State state = fxAccount.getState();
   538         fxAccount.setState(state.makeDoghouseState());
   539         refresh();
   540       } else {
   541         return false;
   542       }
   543       return true;
   544     }
   545   }
   547   /**
   548    * Iterate through debug buttons, adding a special deubg preference click
   549    * listener to each of them.
   550    */
   551   protected void connectDebugButtons() {
   552     // Separate listener to really separate debug logic from main code paths.
   553     final OnPreferenceClickListener listener = new DebugPreferenceClickListener();
   555     // We don't want to use Android resource strings for debug UI, so we just
   556     // use the keys throughout.
   557     final Preference debugCategory = ensureFindPreference("debug_category");
   558     debugCategory.setTitle(debugCategory.getKey());
   560     String[] debugKeys = new String[] {
   561         "debug_refresh",
   562         "debug_dump",
   563         "debug_force_sync",
   564         "debug_forget_certificate",
   565         "debug_require_password",
   566         "debug_require_upgrade" };
   567     for (String debugKey : debugKeys) {
   568       final Preference button = ensureFindPreference(debugKey);
   569       button.setTitle(debugKey); // Not very friendly, but this is for debugging only!
   570       button.setOnPreferenceClickListener(listener);
   571     }
   572   }
   573 }

mercurial