Java tutorial
/* 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.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.Map; import java.util.Set; import org.mozilla.gecko.AppConstants; import org.mozilla.gecko.R; import org.mozilla.gecko.background.common.log.Logger; import org.mozilla.gecko.background.fxa.FxAccountUtils; 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.fxa.tasks.FxAccountCodeResender; import org.mozilla.gecko.sync.ExtendedJSONObject; import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate; import org.mozilla.gecko.sync.SyncConfiguration; import org.mozilla.gecko.util.HardwareUtils; import org.mozilla.gecko.util.ThreadUtils; import android.accounts.Account; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.os.Bundle; import android.os.Handler; import android.preference.CheckBoxPreference; import android.preference.EditTextPreference; import android.preference.Preference; import android.preference.Preference.OnPreferenceChangeListener; import android.preference.Preference.OnPreferenceClickListener; import android.preference.PreferenceCategory; import android.preference.PreferenceScreen; import android.support.v4.content.LocalBroadcastManager; import android.text.TextUtils; import android.text.format.DateUtils; import com.squareup.picasso.Picasso; import com.squareup.picasso.Target; /** * A fragment that displays the status of an AndroidFxAccount. * <p> * The owning activity is responsible for providing an AndroidFxAccount at * appropriate times. */ public class FxAccountStatusFragment extends PreferenceFragment implements OnPreferenceClickListener, OnPreferenceChangeListener { private static final String LOG_TAG = FxAccountStatusFragment.class.getSimpleName(); /** * If a device claims to have synced before this date, we will assume it has never synced. */ private static final Date EARLIEST_VALID_SYNCED_DATE; static { final Calendar c = GregorianCalendar.getInstance(); c.set(2000, Calendar.JANUARY, 1, 0, 0, 0); EARLIEST_VALID_SYNCED_DATE = c.getTime(); } // 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; private static final long LAST_SYNCED_TIME_UPDATE_INTERVAL_IN_MILLISECONDS = 60 * 1000; private static final long PROFILE_FETCH_RETRY_INTERVAL_IN_MILLISECONDS = 60 * 1000; // By default, the auth/account server preference is only shown when the // account is configured to use a custom server. In debug mode, this is set. private static boolean ALWAYS_SHOW_AUTH_SERVER = false; // By default, the Sync server preference is only shown when the account is // configured to use a custom Sync server. In debug mode, this is set. private static boolean ALWAYS_SHOW_SYNC_SERVER = false; protected PreferenceCategory accountCategory; protected Preference profilePreference; protected Preference emailPreference; protected Preference authServerPreference; protected Preference needsPasswordPreference; protected Preference needsUpgradePreference; protected Preference needsVerificationPreference; protected Preference needsMasterSyncAutomaticallyEnabledPreference; protected Preference needsFinishMigratingPreference; protected PreferenceCategory syncCategory; protected CheckBoxPreference bookmarksPreference; protected CheckBoxPreference historyPreference; protected CheckBoxPreference tabsPreference; protected CheckBoxPreference passwordsPreference; protected CheckBoxPreference readingListPreference; protected EditTextPreference deviceNamePreference; protected Preference syncServerPreference; protected Preference morePreference; protected Preference syncNowPreference; protected volatile AndroidFxAccount fxAccount; // The contract is: when fxAccount is non-null, then clientsDataDelegate is // non-null. If violated then an IllegalStateException is thrown. protected volatile SharedPreferencesClientsDataDelegate clientsDataDelegate; // 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; // Runnable to update last synced time. protected Runnable lastSyncedTimeUpdateRunnable; // Broadcast Receiver to update profile Information. protected FxAccountProfileInformationReceiver accountProfileInformationReceiver; protected final InnerSyncStatusDelegate syncStatusDelegate = new InnerSyncStatusDelegate(); private Target profileAvatarTarget; 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); // We need to do this before we can query the hardware menu button state. // We're guaranteed to have an activity at this point (onAttach is called // before onCreate). It's okay to call this multiple times (with different // contexts). HardwareUtils.init(getActivity()); addPreferences(); } protected void addPreferences() { addPreferencesFromResource(R.xml.fxaccount_status_prefscreen); accountCategory = (PreferenceCategory) ensureFindPreference("signed_in_as_category"); profilePreference = ensureFindPreference("profile"); emailPreference = ensureFindPreference("email"); if (AppConstants.MOZ_ANDROID_FIREFOX_ACCOUNT_PROFILES) { accountCategory.removePreference(emailPreference); } else { accountCategory.removePreference(profilePreference); } authServerPreference = ensureFindPreference("auth_server"); needsPasswordPreference = ensureFindPreference("needs_credentials"); needsUpgradePreference = ensureFindPreference("needs_upgrade"); needsVerificationPreference = ensureFindPreference("needs_verification"); needsMasterSyncAutomaticallyEnabledPreference = ensureFindPreference( "needs_master_sync_automatically_enabled"); needsFinishMigratingPreference = ensureFindPreference("needs_finish_migrating"); syncCategory = (PreferenceCategory) ensureFindPreference("sync_category"); bookmarksPreference = (CheckBoxPreference) ensureFindPreference("bookmarks"); historyPreference = (CheckBoxPreference) ensureFindPreference("history"); tabsPreference = (CheckBoxPreference) ensureFindPreference("tabs"); passwordsPreference = (CheckBoxPreference) ensureFindPreference("passwords"); if (!FxAccountUtils.LOG_PERSONAL_INFORMATION) { removeDebugButtons(); } else { connectDebugButtons(); ALWAYS_SHOW_AUTH_SERVER = true; ALWAYS_SHOW_SYNC_SERVER = true; } if (AppConstants.MOZ_ANDROID_FIREFOX_ACCOUNT_PROFILES) { profilePreference.setOnPreferenceClickListener(this); } else { emailPreference.setOnPreferenceClickListener(this); } needsPasswordPreference.setOnPreferenceClickListener(this); needsVerificationPreference.setOnPreferenceClickListener(this); needsFinishMigratingPreference.setOnPreferenceClickListener(this); bookmarksPreference.setOnPreferenceClickListener(this); historyPreference.setOnPreferenceClickListener(this); tabsPreference.setOnPreferenceClickListener(this); passwordsPreference.setOnPreferenceClickListener(this); deviceNamePreference = (EditTextPreference) ensureFindPreference("device_name"); deviceNamePreference.setOnPreferenceChangeListener(this); syncServerPreference = ensureFindPreference("sync_server"); morePreference = ensureFindPreference("more"); morePreference.setOnPreferenceClickListener(this); syncNowPreference = ensureFindPreference("sync_now"); syncNowPreference.setEnabled(true); syncNowPreference.setOnPreferenceClickListener(this); if (HardwareUtils.hasMenuButton()) { syncCategory.removePreference(morePreference); } } /** * 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); final Bundle extras = getExtrasForAccount(); if (extras != null) { intent.putExtras(extras); } // 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 == needsFinishMigratingPreference) { final Intent intent = new Intent(getActivity(), FxAccountFinishMigratingActivity.class); final Bundle extras = getExtrasForAccount(); if (extras != null) { intent.putExtras(extras); } // 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) { FxAccountCodeResender.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 == bookmarksPreference || preference == historyPreference || preference == passwordsPreference || preference == tabsPreference) { saveEngineSelections(); return true; } if (preference == morePreference) { getActivity().openOptionsMenu(); return true; } if (preference == syncNowPreference) { if (fxAccount != null) { FirefoxAccounts.requestSync(fxAccount.getAndroidAccount(), FirefoxAccounts.FORCE, null, null); } return true; } return false; } protected Bundle getExtrasForAccount() { final Bundle extras = new Bundle(); final ExtendedJSONObject o = new ExtendedJSONObject(); o.put(FxAccountAbstractSetupActivity.JSON_KEY_AUTH, fxAccount.getAccountServerURI()); final ExtendedJSONObject services = new ExtendedJSONObject(); services.put(FxAccountAbstractSetupActivity.JSON_KEY_SYNC, fxAccount.getTokenServerURI()); services.put(FxAccountAbstractSetupActivity.JSON_KEY_PROFILE, fxAccount.getProfileServerURI()); o.put(FxAccountAbstractSetupActivity.JSON_KEY_SERVICES, services); extras.putString(FxAccountAbstractSetupActivity.EXTRA_EXTRAS, o.toJSONString()); return extras; } protected void setCheckboxesEnabled(boolean enabled) { bookmarksPreference.setEnabled(enabled); historyPreference.setEnabled(enabled); tabsPreference.setEnabled(enabled); passwordsPreference.setEnabled(enabled); // Since we can't sync, we can't update our remote client record. deviceNamePreference.setEnabled(enabled); syncNowPreference.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.needsFinishMigratingPreference, }; 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); needsMasterSyncAutomaticallyEnabledPreference.setTitle(AppConstants.Versions.preLollipop ? R.string.fxaccount_status_needs_master_sync_automatically_enabled : R.string.fxaccount_status_needs_master_sync_automatically_enabled_v21); showOnlyOneErrorPreference(needsMasterSyncAutomaticallyEnabledPreference); setCheckboxesEnabled(false); } protected void showNeedsFinishMigrating() { syncCategory.setTitle(R.string.fxaccount_status_sync); showOnlyOneErrorPreference(needsFinishMigratingPreference); 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. * <p> * <b>Important:</b> call this method on the UI thread! * <p> * 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; try { this.clientsDataDelegate = new SharedPreferencesClientsDataDelegate(fxAccount.getSyncPrefs(), getActivity().getApplicationContext()); } catch (Exception e) { Logger.error(LOG_TAG, "Got exception fetching Sync prefs associated to Firefox Account; aborting.", e); // Something is terribly wrong; best to get a stack trace rather than // continue with a null clients delegate. throw new IllegalStateException(e); } 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(); lastSyncedTimeUpdateRunnable = new LastSyncTimeUpdateRunnable(); // 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); if (AppConstants.MOZ_ANDROID_FIREFOX_ACCOUNT_PROFILES) { // Register a local broadcast receiver to get profile cached notification. final IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(FxAccountConstants.ACCOUNT_PROFILE_JSON_UPDATED_ACTION); accountProfileInformationReceiver = new FxAccountProfileInformationReceiver(); LocalBroadcastManager.getInstance(getActivity()).registerReceiver(accountProfileInformationReceiver, intentFilter); // profilePreference is set during onCreate, so it's definitely not null here. final float cornerRadius = getResources().getDimension(R.dimen.fxaccount_profile_image_width) / 2; profileAvatarTarget = new PicassoPreferenceIconTarget(getResources(), profilePreference, cornerRadius); } refresh(); } @Override public void onPause() { super.onPause(); FxAccountSyncStatusHelper.getInstance().stopObserving(syncStatusDelegate); // Focus lost, remove scheduled update if any. if (lastSyncedTimeUpdateRunnable != null) { handler.removeCallbacks(lastSyncedTimeUpdateRunnable); } // Focus lost, unregister broadcast receiver. if (accountProfileInformationReceiver != null) { LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(accountProfileInformationReceiver); } if (profileAvatarTarget != null) { Picasso.with(getActivity()).cancelRequest(profileAvatarTarget); profileAvatarTarget = null; } } protected void hardRefresh() { // This is the only way to guarantee that the EditText dialogs created by // EditTextPreferences are re-created. This works around the issue described // at http://androiddev.orkitra.com/?p=112079. final PreferenceScreen statusScreen = (PreferenceScreen) ensureFindPreference("status_screen"); statusScreen.removeAll(); addPreferences(); refresh(); } 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"); } updateProfileInformation(); updateAuthServerPreference(); updateSyncServerPreference(); 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. Right now // there are no such states. // 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; case NeedsFinishMigrating: showNeedsFinishMigrating(); break; case None: showConnected(); break; } // 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(); } final String clientName = clientsDataDelegate.getClientName(); deviceNamePreference.setSummary(clientName); deviceNamePreference.setText(clientName); updateSyncNowPreference(); } // This is a helper function similar to TabsAccessor.getLastSyncedString() to calculate relative "Last synced" time span. private String getLastSyncedString(final long startTime) { if (new Date(startTime).before(EARLIEST_VALID_SYNCED_DATE)) { return getActivity().getString(R.string.fxaccount_status_never_synced); } final CharSequence relativeTimeSpanString = DateUtils.getRelativeTimeSpanString(startTime); return getActivity().getResources().getString(R.string.fxaccount_status_last_synced, relativeTimeSpanString); } protected void updateSyncNowPreference() { final boolean currentlySyncing = fxAccount.isCurrentlySyncing(); syncNowPreference.setEnabled(!currentlySyncing); if (currentlySyncing) { syncNowPreference.setTitle(R.string.fxaccount_status_syncing); } else { syncNowPreference.setTitle(R.string.fxaccount_status_sync_now); } scheduleAndUpdateLastSyncedTime(); } private void updateProfileInformation() { if (!AppConstants.MOZ_ANDROID_FIREFOX_ACCOUNT_PROFILES) { // Life is so much simpler. emailPreference.setTitle(fxAccount.getEmail()); return; } final ExtendedJSONObject profileJSON = fxAccount.getProfileJSON(); if (profileJSON == null) { // Update the profile title with email as the fallback. // Profile icon by default use the default avatar as the fallback. profilePreference.setTitle(fxAccount.getEmail()); return; } updateProfileInformation(profileJSON); } /** * Update profile information from json on UI thread. * * @param profileJSON json fetched from server. */ protected void updateProfileInformation(final ExtendedJSONObject profileJSON) { // View changes must always be done on UI thread. ThreadUtils.assertOnUiThread(); FxAccountUtils.pii(LOG_TAG, "Profile JSON is: " + profileJSON.toJSONString()); final String userName = profileJSON.getString(FxAccountConstants.KEY_PROFILE_JSON_USERNAME); // Update the profile username and email if available. if (!TextUtils.isEmpty(userName)) { profilePreference.setTitle(userName); profilePreference.setSummary(fxAccount.getEmail()); } else { profilePreference.setTitle(fxAccount.getEmail()); } // Icon update from java is not supported prior to API 11, skip the avatar image fetch and update for older device. if (!AppConstants.Versions.feature11Plus) { Logger.info(LOG_TAG, "Skipping profile image fetch for older pre-API 11 devices."); return; } // Avatar URI empty, skip profile image fetch. final String avatarURI = profileJSON.getString(FxAccountConstants.KEY_PROFILE_JSON_AVATAR); if (TextUtils.isEmpty(avatarURI)) { Logger.info(LOG_TAG, "AvatarURI is empty, skipping profile image fetch."); return; } // Using noPlaceholder would avoid a pop of the default image, but it's not available in the version of Picasso // we ship in the tree. Picasso.with(getActivity()).load(avatarURI).centerInside() .resizeDimen(R.dimen.fxaccount_profile_image_width, R.dimen.fxaccount_profile_image_height) .placeholder(R.drawable.sync_avatar_default).error(R.drawable.sync_avatar_default) .into(profileAvatarTarget); } private void scheduleAndUpdateLastSyncedTime() { final String lastSynced = getLastSyncedString(fxAccount.getLastSyncedTimestamp()); syncNowPreference.setSummary(lastSynced); handler.postDelayed(lastSyncedTimeUpdateRunnable, LAST_SYNCED_TIME_UPDATE_INTERVAL_IN_MILLISECONDS); } protected void updateAuthServerPreference() { final String authServer = fxAccount.getAccountServerURI(); final boolean shouldBeShown = ALWAYS_SHOW_AUTH_SERVER || !FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT.equals(authServer); final boolean currentlyShown = null != findPreference(authServerPreference.getKey()); if (currentlyShown != shouldBeShown) { if (shouldBeShown) { accountCategory.addPreference(authServerPreference); } else { accountCategory.removePreference(authServerPreference); } } // Always set the summary, because on first run, the preference is visible, // and the above block will be skipped if there is a custom value. authServerPreference.setSummary(authServer); } protected void updateSyncServerPreference() { final String syncServer = fxAccount.getTokenServerURI(); final boolean shouldBeShown = ALWAYS_SHOW_SYNC_SERVER || !FxAccountConstants.DEFAULT_TOKEN_SERVER_ENDPOINT.equals(syncServer); final boolean currentlyShown = null != findPreference(syncServerPreference.getKey()); if (currentlyShown != shouldBeShown) { if (shouldBeShown) { syncCategory.addPreference(syncServerPreference); } else { syncCategory.removePreference(syncServerPreference); } } // Always set the summary, because on first run, the preference is visible, // and the above block will be skipped if there is a custom value. syncServerPreference.setSummary(syncServer); } /** * Query shared prefs for the current engine state, and update the UI * accordingly. * <p> * 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<String, Boolean> engines = SyncConfiguration.getUserSelectedEngines(syncPrefs); if (engines != null) { bookmarksPreference.setChecked(engines.containsKey("bookmarks") && engines.get("bookmarks")); historyPreference.setChecked(engines.containsKey("history") && engines.get("history")); passwordsPreference.setChecked(engines.containsKey("passwords") && engines.get("passwords")); tabsPreference.setChecked(engines.containsKey("tabs") && engines.get("tabs")); return; } // We don't have user specified preferences. Perhaps we have seen a meta/global? Set<String> enabledNames = SyncConfiguration.getEnabledEngineNames(syncPrefs); if (enabledNames != null) { bookmarksPreference.setChecked(enabledNames.contains("bookmarks")); historyPreference.setChecked(enabledNames.contains("history")); passwordsPreference.setChecked(enabledNames.contains("passwords")); tabsPreference.setChecked(enabledNames.contains("tabs")); return; } // Okay, we don't have userSelectedEngines or enabledEngines. That means // the user hasn't specified to begin with, we haven't specified here, and // we haven't already seen, Sync engines. We don't know our state, so // let's check everything (the default) and disable everything. bookmarksPreference.setChecked(true); historyPreference.setChecked(true); passwordsPreference.setChecked(true); tabsPreference.setChecked(true); setCheckboxesEnabled(false); } catch (Exception e) { Logger.warn(LOG_TAG, "Got exception getting engines to select; ignoring.", e); return; } } /** * Persist engine selections to local shared preferences, and request a sync * to persist selections to remote storage. */ protected void saveEngineSelections() { final Map<String, Boolean> engineSelections = new HashMap<String, Boolean>(); engineSelections.put("bookmarks", bookmarksPreference.isChecked()); engineSelections.put("history", historyPreference.isChecked()); engineSelections.put("passwords", passwordsPreference.isChecked()); engineSelections.put("tabs", tabsPreference.isChecked()); // No GlobalSession.config, so store directly to shared prefs. We do this on // a background thread to avoid IO on the main thread and strict mode // warnings. new Thread(new PersistEngineSelectionsRunnable(engineSelections)).start(); } protected void requestDelayedSync() { Logger.info(LOG_TAG, "Posting a delayed request for a sync sometime soon."); handler.removeCallbacks(requestSyncRunnable); handler.postDelayed(requestSyncRunnable, DELAY_IN_MILLISECONDS_BEFORE_REQUESTING_SYNC); } /** * Remove all traces of debug buttons. By default, no debug buttons are shown. */ protected void removeDebugButtons() { final PreferenceScreen statusScreen = (PreferenceScreen) ensureFindPreference("status_screen"); final PreferenceCategory debugCategory = (PreferenceCategory) ensureFindPreference("debug_category"); statusScreen.removePreference(debugCategory); } /** * A Runnable that persists engine selections to shared prefs, and then * requests a delayed sync. * <p> * References the member <code>fxAccount</code> and is specific to the Android * account associated to that account. */ protected class PersistEngineSelectionsRunnable implements Runnable { private final Map<String, Boolean> engineSelections; protected PersistEngineSelectionsRunnable(Map<String, Boolean> engineSelections) { this.engineSelections = engineSelections; } @Override public void run() { try { // Name shadowing -- do you like it, or do you love it? AndroidFxAccount fxAccount = FxAccountStatusFragment.this.fxAccount; if (fxAccount == null) { return; } Logger.info(LOG_TAG, "Persisting engine selections: " + engineSelections.toString()); SyncConfiguration.storeSelectedEnginesToPrefs(fxAccount.getSyncPrefs(), engineSelections); requestDelayedSync(); } catch (Exception e) { Logger.warn(LOG_TAG, "Got exception persisting selected engines; ignoring.", e); return; } } } /** * A Runnable that requests a sync. * <p> * References the member <code>fxAccount</code>, 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(); } } /** * The Runnable that schedules a future update and updates the last synced time. */ protected class LastSyncTimeUpdateRunnable implements Runnable { @Override public void run() { scheduleAndUpdateLastSyncedTime(); } } /** * Broadcast receiver to receive updates for the cached profile action. */ public class FxAccountProfileInformationReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (!intent.getAction().equals(FxAccountConstants.ACCOUNT_PROFILE_JSON_UPDATED_ACTION)) { return; } Logger.info(LOG_TAG, "Profile avatar cache update action broadcast received."); // Update the UI from cached profile json on the main thread. getActivity().runOnUiThread(new Runnable() { @Override public void run() { updateProfileInformation(); } }); } } /** * 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_invalidate_certificate".equals(key)) { State state = fxAccount.getState(); try { Married married = (Married) state; Logger.info(LOG_TAG, "Invalidating certificate."); fxAccount.setState(married.makeCohabitingState().withCertificate("INVALID CERTIFICATE")); refresh(); } catch (ClassCastException e) { Logger.info(LOG_TAG, "Not in Married state; can't invalidate 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 if ("debug_migrated_from_sync11".equals(key)) { Logger.info(LOG_TAG, "Moving to MigratedFromSync11 state: Requiring password."); State state = fxAccount.getState(); fxAccount.setState(state.makeMigratedFromSync11State(null)); refresh(); } else if ("debug_make_account_stage".equals(key)) { Logger.info(LOG_TAG, "Moving Account endpoints, in place, to stage. Deleting Sync and RL prefs and requiring password."); fxAccount.unsafeTransitionToStageEndpoints(); refresh(); } else if ("debug_make_account_default".equals(key)) { Logger.info(LOG_TAG, "Moving Account endpoints, in place, to default (production). Deleting Sync and RL prefs and requiring password."); fxAccount.unsafeTransitionToDefaultEndpoints(); refresh(); } else { return false; } return true; } } /** * Iterate through debug buttons, adding a special debug 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 PreferenceCategory debugCategory = (PreferenceCategory) ensureFindPreference("debug_category"); debugCategory.setTitle(debugCategory.getKey()); for (int i = 0; i < debugCategory.getPreferenceCount(); i++) { final Preference button = debugCategory.getPreference(i); button.setTitle(button.getKey()); // Not very friendly, but this is for debugging only! button.setOnPreferenceClickListener(listener); } } @Override public boolean onPreferenceChange(Preference preference, Object newValue) { if (preference == deviceNamePreference) { String newClientName = (String) newValue; if (TextUtils.isEmpty(newClientName)) { newClientName = clientsDataDelegate.getDefaultClientName(); } final long now = System.currentTimeMillis(); clientsDataDelegate.setClientName(newClientName, now); requestDelayedSync(); // Try to update our remote client record. hardRefresh(); // Updates the value displayed to the user, among other things. return true; } // For everything else, accept the change. return true; } }