DXR is a code search and navigation tool aimed at making sense of large projects. It supports full-text and regex searches as well as structural queries.

Mercurial (237e4c0633fd)

VCS Links

Line Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963
/* 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.*;
import android.preference.Preference.OnPreferenceChangeListener;
import android.preference.Preference.OnPreferenceClickListener;
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.FxAccountSyncAdapter;
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" };

  protected PreferenceCategory additionalSettingsCategory;
  protected PreferenceCategory errorStatesCategory;

  protected Preference profilePreference;
  protected Preference authServerPreference;
  protected Preference removeAccountPreference;

  protected Preference needsPasswordPreference;
  protected Preference needsUpgradePreference;
  protected Preference needsVerificationPreference;
  protected Preference needsMasterSyncAutomaticallyEnabledPreference;
  protected Preference needsFinishMigratingPreference;

  protected CheckBoxPreference bookmarksPreference;
  protected CheckBoxPreference historyPreference;
  protected CheckBoxPreference tabsPreference;
  protected CheckBoxPreference passwordsPreference;

  protected EditTextPreference deviceNamePreference;
  protected SwitchPreference syncOverMeteredPreference;
  protected Preference syncServerPreference;
  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);

    errorStatesCategory = (PreferenceCategory) ensureFindPreference("error_state");
    additionalSettingsCategory = (PreferenceCategory) ensureFindPreference("additional_settings");

    profilePreference = ensureFindPreference("profile");
    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");

    bookmarksPreference = (CheckBoxPreference) ensureFindPreference("bookmarks");
    historyPreference = (CheckBoxPreference) ensureFindPreference("history");
    tabsPreference = (CheckBoxPreference) ensureFindPreference("tabs");
    passwordsPreference = (CheckBoxPreference) ensureFindPreference("passwords");

    profilePreference.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);

    syncOverMeteredPreference = (SwitchPreference) ensureFindPreference(FxAccountSyncAdapter.PREFS_SYNC_RESTRICT_METERED);
    syncOverMeteredPreference.setOnPreferenceChangeListener(this);

    deviceNamePreference = (EditTextPreference) ensureFindPreference("device_name");
    deviceNamePreference.setOnPreferenceChangeListener(this);

    syncServerPreference = ensureFindPreference("sync_server");

    syncNowPreference = ensureFindPreference("sync_now");
    syncNowPreference.setEnabled(true);
    syncNowPreference.setOnPreferenceClickListener(this);

    if (!FxAccountUtils.LOG_PERSONAL_INFORMATION) {
      removeDebugButtons();
    } else {
      connectDebugButtons();
    }

    updateAdditionalPreferences();

    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=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 == syncNowPreference) {
      if (fxAccount != null) {
        fxAccount.requestImmediateSync(null, null, true);
      }
      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 PreferenceScreen statusScreen = (PreferenceScreen) ensureFindPreference("status_screen");
    final boolean showShowErrorState = errorPreferenceToShow != null;
    final boolean currentlyShowingErrorState = null != findPreference(errorStatesCategory.getKey());

    if (currentlyShowingErrorState != showShowErrorState) {
      if (showShowErrorState) {
        statusScreen.addPreference(errorStatesCategory);
      } else {
        statusScreen.removePreference(errorStatesCategory);
      }
    }

    if (!showShowErrorState) {
      return;
    }

    final Preference[] errorPreferences = new Preference[] {
        this.needsPasswordPreference,
        this.needsUpgradePreference,
        this.needsVerificationPreference,
        this.needsMasterSyncAutomaticallyEnabledPreference,
        this.needsFinishMigratingPreference,
    };
    for (Preference errorPreference : errorPreferences) {
      final boolean currentlyShown = null != errorStatesCategory.findPreference(errorPreference.getKey());
      final boolean shouldBeShown = errorPreference == errorPreferenceToShow;
      if (currentlyShown == shouldBeShown) {
        continue;
      }
      if (shouldBeShown) {
        errorStatesCategory.addPreference(errorPreference);
      } else {
        errorStatesCategory.removePreference(errorPreference);
      }
    }
  }

  protected void showNeedsPassword() {
    showOnlyOneErrorPreference(needsPasswordPreference);
    setCheckboxesEnabled(false);
  }

  protected void showNeedsUpgrade() {
    showOnlyOneErrorPreference(needsUpgradePreference);
    setCheckboxesEnabled(false);
  }

  protected void showNeedsVerification() {
    showOnlyOneErrorPreference(needsVerificationPreference);
    setCheckboxesEnabled(false);
  }

  protected void showNeedsMasterSyncAutomaticallyEnabled() {
    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() {
    showOnlyOneErrorPreference(needsFinishMigratingPreference);
    setCheckboxesEnabled(false);
  }

  protected void showConnected() {
    showOnlyOneErrorPreference(null);
    setCheckboxesEnabled(true);
  }

  private class InnerSyncStatusDelegate implements SyncStatusListener {
    /* package-private */ 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);

    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");
    }

    // 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);

    updateProfileInformation();
    updateAdditionalPreferences();

    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);
  }

  private void updateAdditionalPreferences() {
    // Ensure we have fxAccount; it's set in refresh().
    if (fxAccount == null) {
      return;
    }

    // In debug mode, everything is shown. Otherwise, we show those that have been customized.
    final String authServer = fxAccount.getAccountServerURI();
    final String syncServer = fxAccount.getTokenServerURI();
    final boolean inDebugMode = FxAccountUtils.LOG_PERSONAL_INFORMATION;
    final boolean authServerCustomized = !FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT.equals(authServer);
    final boolean syncServerCustomized = !FxAccountConstants.DEFAULT_TOKEN_SERVER_ENDPOINT.equals(syncServer);

    final boolean shouldBeShown = inDebugMode || authServerCustomized || syncServerCustomized;

    final boolean additionalSettingsCategoryCurrentlyShown = null != findPreference(additionalSettingsCategory.getKey());
    if (shouldBeShown != additionalSettingsCategoryCurrentlyShown) {
      final PreferenceScreen statusScreen = (PreferenceScreen) ensureFindPreference("status_screen");
      if (shouldBeShown) {
        statusScreen.addPreference(additionalSettingsCategory);
      } else {
        statusScreen.removePreference(additionalSettingsCategory);
        return;
      }
    }

    final boolean showAuthServerPref = authServerCustomized || inDebugMode;
    final boolean authServerPrefCurrentlyShown = null != findPreference(authServerPreference.getKey());
    if (authServerPrefCurrentlyShown != showAuthServerPref) {
      if (showAuthServerPref) {
        additionalSettingsCategory.addPreference(authServerPreference);
      } else {
        additionalSettingsCategory.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);

    final boolean showSyncServerPref = syncServerCustomized || inDebugMode;
    final boolean syncServerPrefCurrentlyShown = null != findPreference(syncServerPreference.getKey());
    if (syncServerPrefCurrentlyShown != showSyncServerPref) {
      if (showSyncServerPref) {
        additionalSettingsCategory.addPreference(syncServerPreference);
      } else {
        additionalSettingsCategory.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, true);
        // 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, true);
      hardRefresh(); // Updates the value displayed to the user, among other things.
      return true;
    }

    if (preference == syncOverMeteredPreference) {
      try {
        fxAccount.getSyncPrefs().edit().putBoolean(FxAccountSyncAdapter.PREFS_SYNC_RESTRICT_METERED, (Boolean) newValue).apply();
      } catch (Exception e) {
        Logger.error(LOG_TAG, "Failed to save the new for syncMeteredPreference");
      }
    }

    // For everything else, accept the change.
    return true;
  }
}