michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko.fxa.activities; michael@0: michael@0: import java.util.HashMap; michael@0: import java.util.Map; michael@0: import java.util.Set; michael@0: michael@0: import org.mozilla.gecko.R; michael@0: import org.mozilla.gecko.background.common.log.Logger; michael@0: import org.mozilla.gecko.background.preferences.PreferenceFragment; michael@0: import org.mozilla.gecko.fxa.FirefoxAccounts; michael@0: import org.mozilla.gecko.fxa.FxAccountConstants; michael@0: import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; michael@0: import org.mozilla.gecko.fxa.login.Married; michael@0: import org.mozilla.gecko.fxa.login.State; michael@0: import org.mozilla.gecko.fxa.sync.FxAccountSyncStatusHelper; michael@0: import org.mozilla.gecko.sync.SyncConfiguration; michael@0: michael@0: import android.accounts.Account; michael@0: import android.content.ContentResolver; michael@0: import android.content.Context; michael@0: import android.content.Intent; michael@0: import android.content.SharedPreferences; michael@0: import android.os.Bundle; michael@0: import android.os.Handler; michael@0: import android.preference.CheckBoxPreference; michael@0: import android.preference.Preference; michael@0: import android.preference.Preference.OnPreferenceClickListener; michael@0: import android.preference.PreferenceCategory; michael@0: import android.preference.PreferenceScreen; michael@0: michael@0: /** michael@0: * A fragment that displays the status of an AndroidFxAccount. michael@0: *
michael@0: * The owning activity is responsible for providing an AndroidFxAccount at michael@0: * appropriate times. michael@0: */ michael@0: public class FxAccountStatusFragment extends PreferenceFragment implements OnPreferenceClickListener { michael@0: private static final String LOG_TAG = FxAccountStatusFragment.class.getSimpleName(); michael@0: michael@0: // When a checkbox is toggled, wait 5 seconds (for other checkbox actions) michael@0: // before trying to sync. Should we kill off the fragment before the sync michael@0: // request happens, that's okay: the runnable will run if the UI thread is michael@0: // still around to service it, and since we're not updating any UI, we'll just michael@0: // schedule the sync as usual. See also comment below about garbage michael@0: // collection. michael@0: private static final long DELAY_IN_MILLISECONDS_BEFORE_REQUESTING_SYNC = 5 * 1000; michael@0: michael@0: protected Preference emailPreference; michael@0: michael@0: protected Preference needsPasswordPreference; michael@0: protected Preference needsUpgradePreference; michael@0: protected Preference needsVerificationPreference; michael@0: protected Preference needsMasterSyncAutomaticallyEnabledPreference; michael@0: protected Preference needsAccountEnabledPreference; michael@0: michael@0: protected PreferenceCategory syncCategory; michael@0: michael@0: protected CheckBoxPreference bookmarksPreference; michael@0: protected CheckBoxPreference historyPreference; michael@0: protected CheckBoxPreference tabsPreference; michael@0: protected CheckBoxPreference passwordsPreference; michael@0: michael@0: protected volatile AndroidFxAccount fxAccount; michael@0: michael@0: // Used to post delayed sync requests. michael@0: protected Handler handler; michael@0: michael@0: // Member variable so that re-posting pushes back the already posted instance. michael@0: // This Runnable references the fxAccount above, but it is not specific to a michael@0: // single account. (That is, it does not capture a single account instance.) michael@0: protected Runnable requestSyncRunnable; michael@0: michael@0: protected final InnerSyncStatusDelegate syncStatusDelegate = new InnerSyncStatusDelegate(); michael@0: michael@0: protected Preference ensureFindPreference(String key) { michael@0: Preference preference = findPreference(key); michael@0: if (preference == null) { michael@0: throw new IllegalStateException("Could not find preference with key: " + key); michael@0: } michael@0: return preference; michael@0: } michael@0: michael@0: @Override michael@0: public void onCreate(Bundle savedInstanceState) { michael@0: super.onCreate(savedInstanceState); michael@0: addPreferencesFromResource(R.xml.fxaccount_status_prefscreen); michael@0: michael@0: emailPreference = ensureFindPreference("email"); michael@0: michael@0: needsPasswordPreference = ensureFindPreference("needs_credentials"); michael@0: needsUpgradePreference = ensureFindPreference("needs_upgrade"); michael@0: needsVerificationPreference = ensureFindPreference("needs_verification"); michael@0: needsMasterSyncAutomaticallyEnabledPreference = ensureFindPreference("needs_master_sync_automatically_enabled"); michael@0: needsAccountEnabledPreference = ensureFindPreference("needs_account_enabled"); michael@0: michael@0: syncCategory = (PreferenceCategory) ensureFindPreference("sync_category"); michael@0: michael@0: bookmarksPreference = (CheckBoxPreference) ensureFindPreference("bookmarks"); michael@0: historyPreference = (CheckBoxPreference) ensureFindPreference("history"); michael@0: tabsPreference = (CheckBoxPreference) ensureFindPreference("tabs"); michael@0: passwordsPreference = (CheckBoxPreference) ensureFindPreference("passwords"); michael@0: michael@0: if (!FxAccountConstants.LOG_PERSONAL_INFORMATION) { michael@0: removeDebugButtons(); michael@0: } else { michael@0: connectDebugButtons(); michael@0: } michael@0: michael@0: needsPasswordPreference.setOnPreferenceClickListener(this); michael@0: needsVerificationPreference.setOnPreferenceClickListener(this); michael@0: needsAccountEnabledPreference.setOnPreferenceClickListener(this); michael@0: michael@0: bookmarksPreference.setOnPreferenceClickListener(this); michael@0: historyPreference.setOnPreferenceClickListener(this); michael@0: tabsPreference.setOnPreferenceClickListener(this); michael@0: passwordsPreference.setOnPreferenceClickListener(this); michael@0: } michael@0: michael@0: /** michael@0: * We intentionally don't refresh here. Our owning activity is responsible for michael@0: * providing an AndroidFxAccount to our refresh method in its onResume method. michael@0: */ michael@0: @Override michael@0: public void onResume() { michael@0: super.onResume(); michael@0: } michael@0: michael@0: @Override michael@0: public boolean onPreferenceClick(Preference preference) { michael@0: if (preference == needsPasswordPreference) { michael@0: Intent intent = new Intent(getActivity(), FxAccountUpdateCredentialsActivity.class); michael@0: // Per http://stackoverflow.com/a/8992365, this triggers a known bug with michael@0: // the soft keyboard not being shown for the started activity. Why, Android, why? michael@0: intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); michael@0: startActivity(intent); michael@0: michael@0: return true; michael@0: } michael@0: michael@0: if (preference == needsVerificationPreference) { michael@0: FxAccountConfirmAccountActivity.resendCode(getActivity().getApplicationContext(), fxAccount); michael@0: michael@0: Intent intent = new Intent(getActivity(), FxAccountConfirmAccountActivity.class); michael@0: // Per http://stackoverflow.com/a/8992365, this triggers a known bug with michael@0: // the soft keyboard not being shown for the started activity. Why, Android, why? michael@0: intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); michael@0: startActivity(intent); michael@0: michael@0: return true; michael@0: } michael@0: michael@0: if (preference == needsAccountEnabledPreference) { michael@0: fxAccount.enableSyncing(); michael@0: refresh(); michael@0: michael@0: return true; michael@0: } michael@0: michael@0: if (preference == bookmarksPreference || michael@0: preference == historyPreference || michael@0: preference == passwordsPreference || michael@0: preference == tabsPreference) { michael@0: saveEngineSelections(); michael@0: return true; michael@0: } michael@0: michael@0: return false; michael@0: } michael@0: michael@0: protected void setCheckboxesEnabled(boolean enabled) { michael@0: bookmarksPreference.setEnabled(enabled); michael@0: historyPreference.setEnabled(enabled); michael@0: tabsPreference.setEnabled(enabled); michael@0: passwordsPreference.setEnabled(enabled); michael@0: } michael@0: michael@0: /** michael@0: * Show at most one error preference, hiding all others. michael@0: * michael@0: * @param errorPreferenceToShow michael@0: * single error preference to show; if null, hide all error preferences michael@0: */ michael@0: protected void showOnlyOneErrorPreference(Preference errorPreferenceToShow) { michael@0: final Preference[] errorPreferences = new Preference[] { michael@0: this.needsPasswordPreference, michael@0: this.needsUpgradePreference, michael@0: this.needsVerificationPreference, michael@0: this.needsMasterSyncAutomaticallyEnabledPreference, michael@0: this.needsAccountEnabledPreference, michael@0: }; michael@0: for (Preference errorPreference : errorPreferences) { michael@0: final boolean currentlyShown = null != findPreference(errorPreference.getKey()); michael@0: final boolean shouldBeShown = errorPreference == errorPreferenceToShow; michael@0: if (currentlyShown == shouldBeShown) { michael@0: continue; michael@0: } michael@0: if (shouldBeShown) { michael@0: syncCategory.addPreference(errorPreference); michael@0: } else { michael@0: syncCategory.removePreference(errorPreference); michael@0: } michael@0: } michael@0: } michael@0: michael@0: protected void showNeedsPassword() { michael@0: syncCategory.setTitle(R.string.fxaccount_status_sync); michael@0: showOnlyOneErrorPreference(needsPasswordPreference); michael@0: setCheckboxesEnabled(false); michael@0: } michael@0: michael@0: protected void showNeedsUpgrade() { michael@0: syncCategory.setTitle(R.string.fxaccount_status_sync); michael@0: showOnlyOneErrorPreference(needsUpgradePreference); michael@0: setCheckboxesEnabled(false); michael@0: } michael@0: michael@0: protected void showNeedsVerification() { michael@0: syncCategory.setTitle(R.string.fxaccount_status_sync); michael@0: showOnlyOneErrorPreference(needsVerificationPreference); michael@0: setCheckboxesEnabled(false); michael@0: } michael@0: michael@0: protected void showNeedsMasterSyncAutomaticallyEnabled() { michael@0: syncCategory.setTitle(R.string.fxaccount_status_sync); michael@0: showOnlyOneErrorPreference(needsMasterSyncAutomaticallyEnabledPreference); michael@0: setCheckboxesEnabled(false); michael@0: } michael@0: michael@0: protected void showNeedsAccountEnabled() { michael@0: syncCategory.setTitle(R.string.fxaccount_status_sync); michael@0: showOnlyOneErrorPreference(needsAccountEnabledPreference); michael@0: setCheckboxesEnabled(false); michael@0: } michael@0: michael@0: protected void showConnected() { michael@0: syncCategory.setTitle(R.string.fxaccount_status_sync_enabled); michael@0: showOnlyOneErrorPreference(null); michael@0: setCheckboxesEnabled(true); michael@0: } michael@0: michael@0: protected class InnerSyncStatusDelegate implements FirefoxAccounts.SyncStatusListener { michael@0: protected final Runnable refreshRunnable = new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: refresh(); michael@0: } michael@0: }; michael@0: michael@0: @Override michael@0: public Context getContext() { michael@0: return FxAccountStatusFragment.this.getActivity(); michael@0: } michael@0: michael@0: @Override michael@0: public Account getAccount() { michael@0: return fxAccount.getAndroidAccount(); michael@0: } michael@0: michael@0: @Override michael@0: public void onSyncStarted() { michael@0: if (fxAccount == null) { michael@0: return; michael@0: } michael@0: Logger.info(LOG_TAG, "Got sync started message; refreshing."); michael@0: getActivity().runOnUiThread(refreshRunnable); michael@0: } michael@0: michael@0: @Override michael@0: public void onSyncFinished() { michael@0: if (fxAccount == null) { michael@0: return; michael@0: } michael@0: Logger.info(LOG_TAG, "Got sync finished message; refreshing."); michael@0: getActivity().runOnUiThread(refreshRunnable); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Notify the fragment that a new AndroidFxAccount instance is current. michael@0: *
michael@0: * Important: call this method on the UI thread! michael@0: *
michael@0: * In future, this might be a Loader. michael@0: * michael@0: * @param fxAccount new instance. michael@0: */ michael@0: public void refresh(AndroidFxAccount fxAccount) { michael@0: if (fxAccount == null) { michael@0: throw new IllegalArgumentException("fxAccount must not be null"); michael@0: } michael@0: this.fxAccount = fxAccount; michael@0: michael@0: handler = new Handler(); // Attached to current (assumed to be UI) thread. michael@0: michael@0: // Runnable is not specific to one Firefox Account. This runnable will keep michael@0: // a reference to this fragment alive, but we expect posted runnables to be michael@0: // serviced very quickly, so this is not an issue. michael@0: requestSyncRunnable = new RequestSyncRunnable(); michael@0: michael@0: // We would very much like register these status observers in bookended michael@0: // onResume/onPause calls, but because the Fragment gets onResume during the michael@0: // Activity's super.onResume, it hasn't yet been told its Firefox Account. michael@0: // So we register the observer here (and remove it in onPause), and open michael@0: // ourselves to the possibility that we don't have properly paired michael@0: // register/unregister calls. michael@0: FxAccountSyncStatusHelper.getInstance().startObserving(syncStatusDelegate); michael@0: michael@0: refresh(); michael@0: } michael@0: michael@0: @Override michael@0: public void onPause() { michael@0: super.onPause(); michael@0: FxAccountSyncStatusHelper.getInstance().stopObserving(syncStatusDelegate); michael@0: } michael@0: michael@0: protected void refresh() { michael@0: // refresh is called from our onResume, which can happen before the owning michael@0: // Activity tells us about an account (via our public michael@0: // refresh(AndroidFxAccount) method). michael@0: if (fxAccount == null) { michael@0: throw new IllegalArgumentException("fxAccount must not be null"); michael@0: } michael@0: michael@0: emailPreference.setTitle(fxAccount.getEmail()); michael@0: michael@0: try { michael@0: // There are error states determined by Android, not the login state michael@0: // machine, and we have a chance to present these states here. We handle michael@0: // them specially, since we can't surface these states as part of syncing, michael@0: // because they generally stop syncs from happening regularly. michael@0: michael@0: // The action to enable syncing the Firefox Account doesn't require michael@0: // leaving this activity, so let's present it first. michael@0: final boolean isSyncing = fxAccount.isSyncing(); michael@0: if (!isSyncing) { michael@0: showNeedsAccountEnabled(); michael@0: return; michael@0: } michael@0: michael@0: // Interrogate the Firefox Account's state. michael@0: State state = fxAccount.getState(); michael@0: switch (state.getNeededAction()) { michael@0: case NeedsUpgrade: michael@0: showNeedsUpgrade(); michael@0: break; michael@0: case NeedsPassword: michael@0: showNeedsPassword(); michael@0: break; michael@0: case NeedsVerification: michael@0: showNeedsVerification(); michael@0: break; michael@0: default: michael@0: showConnected(); michael@0: } michael@0: michael@0: // We check for the master setting last, since it is not strictly michael@0: // necessary for the user to address this error state: it's really a michael@0: // warning state. We surface it for the user's convenience, and to prevent michael@0: // confused folks wondering why Sync is not working at all. michael@0: final boolean masterSyncAutomatically = ContentResolver.getMasterSyncAutomatically(); michael@0: if (!masterSyncAutomatically) { michael@0: showNeedsMasterSyncAutomaticallyEnabled(); michael@0: return; michael@0: } michael@0: } finally { michael@0: // No matter our state, we should update the checkboxes. michael@0: updateSelectedEngines(); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Query shared prefs for the current engine state, and update the UI michael@0: * accordingly. michael@0: *
michael@0: * In future, we might want this to be on a background thread, or implemented
michael@0: * as a Loader.
michael@0: */
michael@0: protected void updateSelectedEngines() {
michael@0: try {
michael@0: SharedPreferences syncPrefs = fxAccount.getSyncPrefs();
michael@0: Map
michael@0: * References the member
michael@0: * References the member fxAccount
and is specific to the Android
michael@0: * account associated to that account.
michael@0: */
michael@0: protected class PersistEngineSelectionsRunnable implements Runnable {
michael@0: private final MapfxAccount
, but is not specific to the
michael@0: * Android account associated to that account.
michael@0: */
michael@0: protected class RequestSyncRunnable implements Runnable {
michael@0: @Override
michael@0: public void run() {
michael@0: // Name shadowing -- do you like it, or do you love it?
michael@0: AndroidFxAccount fxAccount = FxAccountStatusFragment.this.fxAccount;
michael@0: if (fxAccount == null) {
michael@0: return;
michael@0: }
michael@0: Logger.info(LOG_TAG, "Requesting a sync sometime soon.");
michael@0: fxAccount.requestSync();
michael@0: }
michael@0: }
michael@0:
michael@0: /**
michael@0: * A separate listener to separate debug logic from main code paths.
michael@0: */
michael@0: protected class DebugPreferenceClickListener implements OnPreferenceClickListener {
michael@0: @Override
michael@0: public boolean onPreferenceClick(Preference preference) {
michael@0: final String key = preference.getKey();
michael@0: if ("debug_refresh".equals(key)) {
michael@0: Logger.info(LOG_TAG, "Refreshing.");
michael@0: refresh();
michael@0: } else if ("debug_dump".equals(key)) {
michael@0: fxAccount.dump();
michael@0: } else if ("debug_force_sync".equals(key)) {
michael@0: Logger.info(LOG_TAG, "Force syncing.");
michael@0: fxAccount.requestSync(FirefoxAccounts.FORCE);
michael@0: // No sense refreshing, since the sync will complete in the future.
michael@0: } else if ("debug_forget_certificate".equals(key)) {
michael@0: State state = fxAccount.getState();
michael@0: try {
michael@0: Married married = (Married) state;
michael@0: Logger.info(LOG_TAG, "Moving to Cohabiting state: Forgetting certificate.");
michael@0: fxAccount.setState(married.makeCohabitingState());
michael@0: refresh();
michael@0: } catch (ClassCastException e) {
michael@0: Logger.info(LOG_TAG, "Not in Married state; can't forget certificate.");
michael@0: // Ignore.
michael@0: }
michael@0: } else if ("debug_require_password".equals(key)) {
michael@0: Logger.info(LOG_TAG, "Moving to Separated state: Forgetting password.");
michael@0: State state = fxAccount.getState();
michael@0: fxAccount.setState(state.makeSeparatedState());
michael@0: refresh();
michael@0: } else if ("debug_require_upgrade".equals(key)) {
michael@0: Logger.info(LOG_TAG, "Moving to Doghouse state: Requiring upgrade.");
michael@0: State state = fxAccount.getState();
michael@0: fxAccount.setState(state.makeDoghouseState());
michael@0: refresh();
michael@0: } else {
michael@0: return false;
michael@0: }
michael@0: return true;
michael@0: }
michael@0: }
michael@0:
michael@0: /**
michael@0: * Iterate through debug buttons, adding a special deubg preference click
michael@0: * listener to each of them.
michael@0: */
michael@0: protected void connectDebugButtons() {
michael@0: // Separate listener to really separate debug logic from main code paths.
michael@0: final OnPreferenceClickListener listener = new DebugPreferenceClickListener();
michael@0:
michael@0: // We don't want to use Android resource strings for debug UI, so we just
michael@0: // use the keys throughout.
michael@0: final Preference debugCategory = ensureFindPreference("debug_category");
michael@0: debugCategory.setTitle(debugCategory.getKey());
michael@0:
michael@0: String[] debugKeys = new String[] {
michael@0: "debug_refresh",
michael@0: "debug_dump",
michael@0: "debug_force_sync",
michael@0: "debug_forget_certificate",
michael@0: "debug_require_password",
michael@0: "debug_require_upgrade" };
michael@0: for (String debugKey : debugKeys) {
michael@0: final Preference button = ensureFindPreference(debugKey);
michael@0: button.setTitle(debugKey); // Not very friendly, but this is for debugging only!
michael@0: button.setOnPreferenceClickListener(listener);
michael@0: }
michael@0: }
michael@0: }