diff -r 000000000000 -r 6474c204b198 mobile/android/base/fxa/activities/FxAccountStatusFragment.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mobile/android/base/fxa/activities/FxAccountStatusFragment.java Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,573 @@ +/* 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.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.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.accounts.Account; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.os.Handler; +import android.preference.CheckBoxPreference; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceClickListener; +import android.preference.PreferenceCategory; +import android.preference.PreferenceScreen; + +/** + * A fragment that displays the status of an AndroidFxAccount. + *
+ * The owning activity is responsible for providing an AndroidFxAccount at + * appropriate times. + */ +public class FxAccountStatusFragment extends PreferenceFragment implements OnPreferenceClickListener { + private static final String LOG_TAG = FxAccountStatusFragment.class.getSimpleName(); + + // When a checkbox is toggled, wait 5 seconds (for other checkbox actions) + // before trying to sync. Should we kill off the fragment before the sync + // request happens, that's okay: the runnable will run if the UI thread is + // still around to service it, and since we're not updating any UI, we'll just + // schedule the sync as usual. See also comment below about garbage + // collection. + private static final long DELAY_IN_MILLISECONDS_BEFORE_REQUESTING_SYNC = 5 * 1000; + + protected Preference emailPreference; + + protected Preference needsPasswordPreference; + protected Preference needsUpgradePreference; + protected Preference needsVerificationPreference; + protected Preference needsMasterSyncAutomaticallyEnabledPreference; + protected Preference needsAccountEnabledPreference; + + protected PreferenceCategory syncCategory; + + protected CheckBoxPreference bookmarksPreference; + protected CheckBoxPreference historyPreference; + protected CheckBoxPreference tabsPreference; + protected CheckBoxPreference passwordsPreference; + + protected volatile AndroidFxAccount fxAccount; + + // 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 InnerSyncStatusDelegate syncStatusDelegate = new InnerSyncStatusDelegate(); + + protected Preference ensureFindPreference(String key) { + Preference preference = findPreference(key); + if (preference == null) { + throw new IllegalStateException("Could not find preference with key: " + key); + } + return preference; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.fxaccount_status_prefscreen); + + emailPreference = ensureFindPreference("email"); + + needsPasswordPreference = ensureFindPreference("needs_credentials"); + needsUpgradePreference = ensureFindPreference("needs_upgrade"); + needsVerificationPreference = ensureFindPreference("needs_verification"); + needsMasterSyncAutomaticallyEnabledPreference = ensureFindPreference("needs_master_sync_automatically_enabled"); + needsAccountEnabledPreference = ensureFindPreference("needs_account_enabled"); + + syncCategory = (PreferenceCategory) ensureFindPreference("sync_category"); + + bookmarksPreference = (CheckBoxPreference) ensureFindPreference("bookmarks"); + historyPreference = (CheckBoxPreference) ensureFindPreference("history"); + tabsPreference = (CheckBoxPreference) ensureFindPreference("tabs"); + passwordsPreference = (CheckBoxPreference) ensureFindPreference("passwords"); + + if (!FxAccountConstants.LOG_PERSONAL_INFORMATION) { + removeDebugButtons(); + } else { + connectDebugButtons(); + } + + needsPasswordPreference.setOnPreferenceClickListener(this); + needsVerificationPreference.setOnPreferenceClickListener(this); + needsAccountEnabledPreference.setOnPreferenceClickListener(this); + + bookmarksPreference.setOnPreferenceClickListener(this); + historyPreference.setOnPreferenceClickListener(this); + tabsPreference.setOnPreferenceClickListener(this); + passwordsPreference.setOnPreferenceClickListener(this); + } + + /** + * We intentionally don't refresh here. Our owning activity is responsible for + * providing an AndroidFxAccount to our refresh method in its onResume method. + */ + @Override + public void onResume() { + super.onResume(); + } + + @Override + public boolean onPreferenceClick(Preference preference) { + if (preference == needsPasswordPreference) { + Intent intent = new Intent(getActivity(), FxAccountUpdateCredentialsActivity.class); + // Per http://stackoverflow.com/a/8992365, this triggers a known bug with + // the soft keyboard not being shown for the started activity. Why, Android, why? + intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); + startActivity(intent); + + return true; + } + + if (preference == needsVerificationPreference) { + FxAccountConfirmAccountActivity.resendCode(getActivity().getApplicationContext(), fxAccount); + + Intent intent = new Intent(getActivity(), FxAccountConfirmAccountActivity.class); + // Per http://stackoverflow.com/a/8992365, this triggers a known bug with + // the soft keyboard not being shown for the started activity. Why, Android, why? + intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); + startActivity(intent); + + return true; + } + + if (preference == needsAccountEnabledPreference) { + fxAccount.enableSyncing(); + refresh(); + + return true; + } + + if (preference == bookmarksPreference || + preference == historyPreference || + preference == passwordsPreference || + preference == tabsPreference) { + saveEngineSelections(); + return true; + } + + return false; + } + + protected void setCheckboxesEnabled(boolean enabled) { + bookmarksPreference.setEnabled(enabled); + historyPreference.setEnabled(enabled); + tabsPreference.setEnabled(enabled); + passwordsPreference.setEnabled(enabled); + } + + /** + * Show at most one error preference, hiding all others. + * + * @param errorPreferenceToShow + * single error preference to show; if null, hide all error preferences + */ + protected void showOnlyOneErrorPreference(Preference errorPreferenceToShow) { + final Preference[] errorPreferences = new Preference[] { + this.needsPasswordPreference, + this.needsUpgradePreference, + this.needsVerificationPreference, + this.needsMasterSyncAutomaticallyEnabledPreference, + this.needsAccountEnabledPreference, + }; + for (Preference errorPreference : errorPreferences) { + final boolean currentlyShown = null != findPreference(errorPreference.getKey()); + final boolean shouldBeShown = errorPreference == errorPreferenceToShow; + if (currentlyShown == shouldBeShown) { + continue; + } + if (shouldBeShown) { + syncCategory.addPreference(errorPreference); + } else { + syncCategory.removePreference(errorPreference); + } + } + } + + protected void showNeedsPassword() { + syncCategory.setTitle(R.string.fxaccount_status_sync); + showOnlyOneErrorPreference(needsPasswordPreference); + setCheckboxesEnabled(false); + } + + protected void showNeedsUpgrade() { + syncCategory.setTitle(R.string.fxaccount_status_sync); + showOnlyOneErrorPreference(needsUpgradePreference); + setCheckboxesEnabled(false); + } + + protected void showNeedsVerification() { + syncCategory.setTitle(R.string.fxaccount_status_sync); + showOnlyOneErrorPreference(needsVerificationPreference); + setCheckboxesEnabled(false); + } + + protected void showNeedsMasterSyncAutomaticallyEnabled() { + syncCategory.setTitle(R.string.fxaccount_status_sync); + showOnlyOneErrorPreference(needsMasterSyncAutomaticallyEnabledPreference); + setCheckboxesEnabled(false); + } + + protected void showNeedsAccountEnabled() { + syncCategory.setTitle(R.string.fxaccount_status_sync); + showOnlyOneErrorPreference(needsAccountEnabledPreference); + setCheckboxesEnabled(false); + } + + protected void showConnected() { + syncCategory.setTitle(R.string.fxaccount_status_sync_enabled); + showOnlyOneErrorPreference(null); + setCheckboxesEnabled(true); + } + + protected class InnerSyncStatusDelegate implements FirefoxAccounts.SyncStatusListener { + protected final Runnable refreshRunnable = new Runnable() { + @Override + public void run() { + refresh(); + } + }; + + @Override + public Context getContext() { + return FxAccountStatusFragment.this.getActivity(); + } + + @Override + public Account getAccount() { + return fxAccount.getAndroidAccount(); + } + + @Override + public void onSyncStarted() { + if (fxAccount == null) { + return; + } + Logger.info(LOG_TAG, "Got sync started message; refreshing."); + getActivity().runOnUiThread(refreshRunnable); + } + + @Override + public void onSyncFinished() { + 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. + *
+ * Important: call this method on the UI thread! + *
+ * In future, this might be a Loader. + * + * @param fxAccount new instance. + */ + public void refresh(AndroidFxAccount fxAccount) { + if (fxAccount == null) { + throw new IllegalArgumentException("fxAccount must not be null"); + } + this.fxAccount = fxAccount; + + 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"); + } + + emailPreference.setTitle(fxAccount.getEmail()); + + try { + // There are error states determined by Android, not the login state + // machine, and we have a chance to present these states here. We handle + // them specially, since we can't surface these states as part of syncing, + // because they generally stop syncs from happening regularly. + + // The action to enable syncing the Firefox Account doesn't require + // leaving this activity, so let's present it first. + final boolean isSyncing = fxAccount.isSyncing(); + if (!isSyncing) { + showNeedsAccountEnabled(); + return; + } + + // Interrogate the Firefox Account's state. + State state = fxAccount.getState(); + switch (state.getNeededAction()) { + case NeedsUpgrade: + showNeedsUpgrade(); + break; + case NeedsPassword: + showNeedsPassword(); + break; + case NeedsVerification: + showNeedsVerification(); + break; + default: + showConnected(); + } + + // We check for the master setting last, since it is not strictly + // necessary for the user to address this error state: it's really a + // warning state. We surface it for the user's convenience, and to prevent + // confused folks wondering why Sync is not working at all. + final boolean masterSyncAutomatically = ContentResolver.getMasterSyncAutomatically(); + if (!masterSyncAutomatically) { + showNeedsMasterSyncAutomaticallyEnabled(); + return; + } + } finally { + // No matter our state, we should update the checkboxes. + updateSelectedEngines(); + } + } + + /** + * Query shared prefs for the current engine state, and update the UI + * accordingly. + *
+ * In future, we might want this to be on a background thread, or implemented
+ * as a Loader.
+ */
+ protected void updateSelectedEngines() {
+ try {
+ SharedPreferences syncPrefs = fxAccount.getSyncPrefs();
+ Map
+ * References the member
+ * References the member fxAccount
and is specific to the Android
+ * account associated to that account.
+ */
+ protected class PersistEngineSelectionsRunnable implements Runnable {
+ private final MapfxAccount
, but is not specific to the
+ * Android account associated to that account.
+ */
+ protected class RequestSyncRunnable implements Runnable {
+ @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.");
+ 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, "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();
+ } catch (ClassCastException e) {
+ Logger.info(LOG_TAG, "Not in Married state; can't forget certificate.");
+ // Ignore.
+ }
+ } else if ("debug_require_password".equals(key)) {
+ Logger.info(LOG_TAG, "Moving to Separated state: Forgetting password.");
+ State state = fxAccount.getState();
+ fxAccount.setState(state.makeSeparatedState());
+ refresh();
+ } else if ("debug_require_upgrade".equals(key)) {
+ Logger.info(LOG_TAG, "Moving to Doghouse state: Requiring upgrade.");
+ State state = fxAccount.getState();
+ fxAccount.setState(state.makeDoghouseState());
+ refresh();
+ } else {
+ return false;
+ }
+ return true;
+ }
+ }
+
+ /**
+ * Iterate through debug buttons, adding a special deubg preference click
+ * listener to each of them.
+ */
+ protected void connectDebugButtons() {
+ // Separate listener to really separate debug logic from main code paths.
+ final OnPreferenceClickListener listener = new DebugPreferenceClickListener();
+
+ // We don't want to use Android resource strings for debug UI, so we just
+ // use the keys throughout.
+ final Preference debugCategory = ensureFindPreference("debug_category");
+ debugCategory.setTitle(debugCategory.getKey());
+
+ String[] debugKeys = new String[] {
+ "debug_refresh",
+ "debug_dump",
+ "debug_force_sync",
+ "debug_forget_certificate",
+ "debug_require_password",
+ "debug_require_upgrade" };
+ for (String debugKey : debugKeys) {
+ final Preference button = ensureFindPreference(debugKey);
+ button.setTitle(debugKey); // Not very friendly, but this is for debugging only!
+ button.setOnPreferenceClickListener(listener);
+ }
+ }
+}