summaryrefslogtreecommitdiffstats
path: root/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountStatusFragment.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountStatusFragment.java')
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountStatusFragment.java949
1 files changed, 949 insertions, 0 deletions
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountStatusFragment.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountStatusFragment.java
new file mode 100644
index 000000000..a30b92e5f
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountStatusFragment.java
@@ -0,0 +1,949 @@
+/* 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 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;
+
+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.FxAccountConstants;
+import org.mozilla.gecko.fxa.SyncStatusListener;
+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.ExtendedJSONObject;
+import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate;
+import org.mozilla.gecko.sync.SyncConfiguration;
+import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
+import org.mozilla.gecko.util.HardwareUtils;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * 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;
+
+ private static final String[] STAGES_TO_SYNC_ON_DEVICE_NAME_CHANGE = new String[] { "clients" };
+
+ // 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 manageAccountPreference;
+ protected Preference authServerPreference;
+ protected Preference removeAccountPreference;
+
+ 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");
+ manageAccountPreference = ensureFindPreference("manage_account");
+ authServerPreference = ensureFindPreference("auth_server");
+ removeAccountPreference = ensureFindPreference("remove_account");
+
+ 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;
+ }
+
+ profilePreference.setOnPreferenceClickListener(this);
+ manageAccountPreference.setOnPreferenceClickListener(this);
+ removeAccountPreference.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);
+
+ ensureFindPreference("linktos").setOnPreferenceClickListener(this);
+ ensureFindPreference("linkprivacy").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 == profilePreference) {
+ ActivityUtils.openURLInFennec(getActivity().getApplicationContext(), "about:accounts?action=avatar");
+ return true;
+ }
+
+ if (preference == manageAccountPreference) {
+ ActivityUtils.openURLInFennec(getActivity().getApplicationContext(), "about:accounts?action=manage");
+ return true;
+ }
+
+ if (preference == removeAccountPreference) {
+ FxAccountStatusActivity.maybeDeleteAndroidAccount(getActivity(), fxAccount.getAndroidAccount(), null);
+ return true;
+ }
+
+ if (preference == needsPasswordPreference) {
+ final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_UPDATE_CREDENTIALS);
+ intent.putExtra(FxAccountWebFlowActivity.EXTRA_ENDPOINT, FxAccountConstants.ENDPOINT_PREFERENCES);
+ // 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(FxAccountConstants.ACTION_FXA_FINISH_MIGRATING);
+ intent.putExtra(FxAccountWebFlowActivity.EXTRA_ENDPOINT, FxAccountConstants.ENDPOINT_PREFERENCES);
+ // 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) {
+ final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_CONFIRM_ACCOUNT);
+ // 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);
+ intent.putExtra(FxAccountWebFlowActivity.EXTRA_ENDPOINT, FxAccountConstants.ENDPOINT_PREFERENCES);
+ 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) {
+ fxAccount.requestImmediateSync(null, null);
+ }
+ return true;
+ }
+
+ if (TextUtils.equals("linktos", preference.getKey())) {
+ ActivityUtils.openURLInFennec(getActivity().getApplicationContext(), getResources().getString(R.string.fxaccount_link_tos));
+ return true;
+ }
+
+ if (TextUtils.equals("linkprivacy", preference.getKey())) {
+ ActivityUtils.openURLInFennec(getActivity().getApplicationContext(), getResources().getString(R.string.fxaccount_link_pn));
+ return true;
+ }
+
+ return false;
+ }
+
+ 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 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);
+
+ // 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() {
+
+ 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());
+ }
+
+ // 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.requestEventualSync(null, null);
+ }
+ }
+
+ /**
+ * 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.requestImmediateSync(null, null);
+ // 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);
+ // Force sync the client record, we want the user to see the device name change immediately
+ // on the FxA Device Manager if possible ( = we are online) to avoid confusion
+ // ("I changed my Android's device name but I don't see it on my computer").
+ fxAccount.requestImmediateSync(STAGES_TO_SYNC_ON_DEVICE_NAME_CHANGE, null);
+ hardRefresh(); // Updates the value displayed to the user, among other things.
+ return true;
+ }
+
+ // For everything else, accept the change.
+ return true;
+ }
+}