Wed, 31 Dec 2014 07:22:50 +0100
Correct previous dual key logic pending first delivery installment.
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 package org.mozilla.gecko.fxa.activities;
7 import java.util.HashMap;
8 import java.util.Map;
9 import java.util.Set;
11 import org.mozilla.gecko.R;
12 import org.mozilla.gecko.background.common.log.Logger;
13 import org.mozilla.gecko.background.preferences.PreferenceFragment;
14 import org.mozilla.gecko.fxa.FirefoxAccounts;
15 import org.mozilla.gecko.fxa.FxAccountConstants;
16 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
17 import org.mozilla.gecko.fxa.login.Married;
18 import org.mozilla.gecko.fxa.login.State;
19 import org.mozilla.gecko.fxa.sync.FxAccountSyncStatusHelper;
20 import org.mozilla.gecko.sync.SyncConfiguration;
22 import android.accounts.Account;
23 import android.content.ContentResolver;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.SharedPreferences;
27 import android.os.Bundle;
28 import android.os.Handler;
29 import android.preference.CheckBoxPreference;
30 import android.preference.Preference;
31 import android.preference.Preference.OnPreferenceClickListener;
32 import android.preference.PreferenceCategory;
33 import android.preference.PreferenceScreen;
35 /**
36 * A fragment that displays the status of an AndroidFxAccount.
37 * <p>
38 * The owning activity is responsible for providing an AndroidFxAccount at
39 * appropriate times.
40 */
41 public class FxAccountStatusFragment extends PreferenceFragment implements OnPreferenceClickListener {
42 private static final String LOG_TAG = FxAccountStatusFragment.class.getSimpleName();
44 // When a checkbox is toggled, wait 5 seconds (for other checkbox actions)
45 // before trying to sync. Should we kill off the fragment before the sync
46 // request happens, that's okay: the runnable will run if the UI thread is
47 // still around to service it, and since we're not updating any UI, we'll just
48 // schedule the sync as usual. See also comment below about garbage
49 // collection.
50 private static final long DELAY_IN_MILLISECONDS_BEFORE_REQUESTING_SYNC = 5 * 1000;
52 protected Preference emailPreference;
54 protected Preference needsPasswordPreference;
55 protected Preference needsUpgradePreference;
56 protected Preference needsVerificationPreference;
57 protected Preference needsMasterSyncAutomaticallyEnabledPreference;
58 protected Preference needsAccountEnabledPreference;
60 protected PreferenceCategory syncCategory;
62 protected CheckBoxPreference bookmarksPreference;
63 protected CheckBoxPreference historyPreference;
64 protected CheckBoxPreference tabsPreference;
65 protected CheckBoxPreference passwordsPreference;
67 protected volatile AndroidFxAccount fxAccount;
69 // Used to post delayed sync requests.
70 protected Handler handler;
72 // Member variable so that re-posting pushes back the already posted instance.
73 // This Runnable references the fxAccount above, but it is not specific to a
74 // single account. (That is, it does not capture a single account instance.)
75 protected Runnable requestSyncRunnable;
77 protected final InnerSyncStatusDelegate syncStatusDelegate = new InnerSyncStatusDelegate();
79 protected Preference ensureFindPreference(String key) {
80 Preference preference = findPreference(key);
81 if (preference == null) {
82 throw new IllegalStateException("Could not find preference with key: " + key);
83 }
84 return preference;
85 }
87 @Override
88 public void onCreate(Bundle savedInstanceState) {
89 super.onCreate(savedInstanceState);
90 addPreferencesFromResource(R.xml.fxaccount_status_prefscreen);
92 emailPreference = ensureFindPreference("email");
94 needsPasswordPreference = ensureFindPreference("needs_credentials");
95 needsUpgradePreference = ensureFindPreference("needs_upgrade");
96 needsVerificationPreference = ensureFindPreference("needs_verification");
97 needsMasterSyncAutomaticallyEnabledPreference = ensureFindPreference("needs_master_sync_automatically_enabled");
98 needsAccountEnabledPreference = ensureFindPreference("needs_account_enabled");
100 syncCategory = (PreferenceCategory) ensureFindPreference("sync_category");
102 bookmarksPreference = (CheckBoxPreference) ensureFindPreference("bookmarks");
103 historyPreference = (CheckBoxPreference) ensureFindPreference("history");
104 tabsPreference = (CheckBoxPreference) ensureFindPreference("tabs");
105 passwordsPreference = (CheckBoxPreference) ensureFindPreference("passwords");
107 if (!FxAccountConstants.LOG_PERSONAL_INFORMATION) {
108 removeDebugButtons();
109 } else {
110 connectDebugButtons();
111 }
113 needsPasswordPreference.setOnPreferenceClickListener(this);
114 needsVerificationPreference.setOnPreferenceClickListener(this);
115 needsAccountEnabledPreference.setOnPreferenceClickListener(this);
117 bookmarksPreference.setOnPreferenceClickListener(this);
118 historyPreference.setOnPreferenceClickListener(this);
119 tabsPreference.setOnPreferenceClickListener(this);
120 passwordsPreference.setOnPreferenceClickListener(this);
121 }
123 /**
124 * We intentionally don't refresh here. Our owning activity is responsible for
125 * providing an AndroidFxAccount to our refresh method in its onResume method.
126 */
127 @Override
128 public void onResume() {
129 super.onResume();
130 }
132 @Override
133 public boolean onPreferenceClick(Preference preference) {
134 if (preference == needsPasswordPreference) {
135 Intent intent = new Intent(getActivity(), FxAccountUpdateCredentialsActivity.class);
136 // Per http://stackoverflow.com/a/8992365, this triggers a known bug with
137 // the soft keyboard not being shown for the started activity. Why, Android, why?
138 intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
139 startActivity(intent);
141 return true;
142 }
144 if (preference == needsVerificationPreference) {
145 FxAccountConfirmAccountActivity.resendCode(getActivity().getApplicationContext(), fxAccount);
147 Intent intent = new Intent(getActivity(), FxAccountConfirmAccountActivity.class);
148 // Per http://stackoverflow.com/a/8992365, this triggers a known bug with
149 // the soft keyboard not being shown for the started activity. Why, Android, why?
150 intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
151 startActivity(intent);
153 return true;
154 }
156 if (preference == needsAccountEnabledPreference) {
157 fxAccount.enableSyncing();
158 refresh();
160 return true;
161 }
163 if (preference == bookmarksPreference ||
164 preference == historyPreference ||
165 preference == passwordsPreference ||
166 preference == tabsPreference) {
167 saveEngineSelections();
168 return true;
169 }
171 return false;
172 }
174 protected void setCheckboxesEnabled(boolean enabled) {
175 bookmarksPreference.setEnabled(enabled);
176 historyPreference.setEnabled(enabled);
177 tabsPreference.setEnabled(enabled);
178 passwordsPreference.setEnabled(enabled);
179 }
181 /**
182 * Show at most one error preference, hiding all others.
183 *
184 * @param errorPreferenceToShow
185 * single error preference to show; if null, hide all error preferences
186 */
187 protected void showOnlyOneErrorPreference(Preference errorPreferenceToShow) {
188 final Preference[] errorPreferences = new Preference[] {
189 this.needsPasswordPreference,
190 this.needsUpgradePreference,
191 this.needsVerificationPreference,
192 this.needsMasterSyncAutomaticallyEnabledPreference,
193 this.needsAccountEnabledPreference,
194 };
195 for (Preference errorPreference : errorPreferences) {
196 final boolean currentlyShown = null != findPreference(errorPreference.getKey());
197 final boolean shouldBeShown = errorPreference == errorPreferenceToShow;
198 if (currentlyShown == shouldBeShown) {
199 continue;
200 }
201 if (shouldBeShown) {
202 syncCategory.addPreference(errorPreference);
203 } else {
204 syncCategory.removePreference(errorPreference);
205 }
206 }
207 }
209 protected void showNeedsPassword() {
210 syncCategory.setTitle(R.string.fxaccount_status_sync);
211 showOnlyOneErrorPreference(needsPasswordPreference);
212 setCheckboxesEnabled(false);
213 }
215 protected void showNeedsUpgrade() {
216 syncCategory.setTitle(R.string.fxaccount_status_sync);
217 showOnlyOneErrorPreference(needsUpgradePreference);
218 setCheckboxesEnabled(false);
219 }
221 protected void showNeedsVerification() {
222 syncCategory.setTitle(R.string.fxaccount_status_sync);
223 showOnlyOneErrorPreference(needsVerificationPreference);
224 setCheckboxesEnabled(false);
225 }
227 protected void showNeedsMasterSyncAutomaticallyEnabled() {
228 syncCategory.setTitle(R.string.fxaccount_status_sync);
229 showOnlyOneErrorPreference(needsMasterSyncAutomaticallyEnabledPreference);
230 setCheckboxesEnabled(false);
231 }
233 protected void showNeedsAccountEnabled() {
234 syncCategory.setTitle(R.string.fxaccount_status_sync);
235 showOnlyOneErrorPreference(needsAccountEnabledPreference);
236 setCheckboxesEnabled(false);
237 }
239 protected void showConnected() {
240 syncCategory.setTitle(R.string.fxaccount_status_sync_enabled);
241 showOnlyOneErrorPreference(null);
242 setCheckboxesEnabled(true);
243 }
245 protected class InnerSyncStatusDelegate implements FirefoxAccounts.SyncStatusListener {
246 protected final Runnable refreshRunnable = new Runnable() {
247 @Override
248 public void run() {
249 refresh();
250 }
251 };
253 @Override
254 public Context getContext() {
255 return FxAccountStatusFragment.this.getActivity();
256 }
258 @Override
259 public Account getAccount() {
260 return fxAccount.getAndroidAccount();
261 }
263 @Override
264 public void onSyncStarted() {
265 if (fxAccount == null) {
266 return;
267 }
268 Logger.info(LOG_TAG, "Got sync started message; refreshing.");
269 getActivity().runOnUiThread(refreshRunnable);
270 }
272 @Override
273 public void onSyncFinished() {
274 if (fxAccount == null) {
275 return;
276 }
277 Logger.info(LOG_TAG, "Got sync finished message; refreshing.");
278 getActivity().runOnUiThread(refreshRunnable);
279 }
280 }
282 /**
283 * Notify the fragment that a new AndroidFxAccount instance is current.
284 * <p>
285 * <b>Important:</b> call this method on the UI thread!
286 * <p>
287 * In future, this might be a Loader.
288 *
289 * @param fxAccount new instance.
290 */
291 public void refresh(AndroidFxAccount fxAccount) {
292 if (fxAccount == null) {
293 throw new IllegalArgumentException("fxAccount must not be null");
294 }
295 this.fxAccount = fxAccount;
297 handler = new Handler(); // Attached to current (assumed to be UI) thread.
299 // Runnable is not specific to one Firefox Account. This runnable will keep
300 // a reference to this fragment alive, but we expect posted runnables to be
301 // serviced very quickly, so this is not an issue.
302 requestSyncRunnable = new RequestSyncRunnable();
304 // We would very much like register these status observers in bookended
305 // onResume/onPause calls, but because the Fragment gets onResume during the
306 // Activity's super.onResume, it hasn't yet been told its Firefox Account.
307 // So we register the observer here (and remove it in onPause), and open
308 // ourselves to the possibility that we don't have properly paired
309 // register/unregister calls.
310 FxAccountSyncStatusHelper.getInstance().startObserving(syncStatusDelegate);
312 refresh();
313 }
315 @Override
316 public void onPause() {
317 super.onPause();
318 FxAccountSyncStatusHelper.getInstance().stopObserving(syncStatusDelegate);
319 }
321 protected void refresh() {
322 // refresh is called from our onResume, which can happen before the owning
323 // Activity tells us about an account (via our public
324 // refresh(AndroidFxAccount) method).
325 if (fxAccount == null) {
326 throw new IllegalArgumentException("fxAccount must not be null");
327 }
329 emailPreference.setTitle(fxAccount.getEmail());
331 try {
332 // There are error states determined by Android, not the login state
333 // machine, and we have a chance to present these states here. We handle
334 // them specially, since we can't surface these states as part of syncing,
335 // because they generally stop syncs from happening regularly.
337 // The action to enable syncing the Firefox Account doesn't require
338 // leaving this activity, so let's present it first.
339 final boolean isSyncing = fxAccount.isSyncing();
340 if (!isSyncing) {
341 showNeedsAccountEnabled();
342 return;
343 }
345 // Interrogate the Firefox Account's state.
346 State state = fxAccount.getState();
347 switch (state.getNeededAction()) {
348 case NeedsUpgrade:
349 showNeedsUpgrade();
350 break;
351 case NeedsPassword:
352 showNeedsPassword();
353 break;
354 case NeedsVerification:
355 showNeedsVerification();
356 break;
357 default:
358 showConnected();
359 }
361 // We check for the master setting last, since it is not strictly
362 // necessary for the user to address this error state: it's really a
363 // warning state. We surface it for the user's convenience, and to prevent
364 // confused folks wondering why Sync is not working at all.
365 final boolean masterSyncAutomatically = ContentResolver.getMasterSyncAutomatically();
366 if (!masterSyncAutomatically) {
367 showNeedsMasterSyncAutomaticallyEnabled();
368 return;
369 }
370 } finally {
371 // No matter our state, we should update the checkboxes.
372 updateSelectedEngines();
373 }
374 }
376 /**
377 * Query shared prefs for the current engine state, and update the UI
378 * accordingly.
379 * <p>
380 * In future, we might want this to be on a background thread, or implemented
381 * as a Loader.
382 */
383 protected void updateSelectedEngines() {
384 try {
385 SharedPreferences syncPrefs = fxAccount.getSyncPrefs();
386 Map<String, Boolean> engines = SyncConfiguration.getUserSelectedEngines(syncPrefs);
387 if (engines != null) {
388 bookmarksPreference.setChecked(engines.containsKey("bookmarks") && engines.get("bookmarks"));
389 historyPreference.setChecked(engines.containsKey("history") && engines.get("history"));
390 passwordsPreference.setChecked(engines.containsKey("passwords") && engines.get("passwords"));
391 tabsPreference.setChecked(engines.containsKey("tabs") && engines.get("tabs"));
392 return;
393 }
395 // We don't have user specified preferences. Perhaps we have seen a meta/global?
396 Set<String> enabledNames = SyncConfiguration.getEnabledEngineNames(syncPrefs);
397 if (enabledNames != null) {
398 bookmarksPreference.setChecked(enabledNames.contains("bookmarks"));
399 historyPreference.setChecked(enabledNames.contains("history"));
400 passwordsPreference.setChecked(enabledNames.contains("passwords"));
401 tabsPreference.setChecked(enabledNames.contains("tabs"));
402 return;
403 }
405 // Okay, we don't have userSelectedEngines or enabledEngines. That means
406 // the user hasn't specified to begin with, we haven't specified here, and
407 // we haven't already seen, Sync engines. We don't know our state, so
408 // let's check everything (the default) and disable everything.
409 bookmarksPreference.setChecked(true);
410 historyPreference.setChecked(true);
411 passwordsPreference.setChecked(true);
412 tabsPreference.setChecked(true);
413 setCheckboxesEnabled(false);
414 } catch (Exception e) {
415 Logger.warn(LOG_TAG, "Got exception getting engines to select; ignoring.", e);
416 return;
417 }
418 }
420 /**
421 * Persist engine selections to local shared preferences, and request a sync
422 * to persist selections to remote storage.
423 */
424 protected void saveEngineSelections() {
425 final Map<String, Boolean> engineSelections = new HashMap<String, Boolean>();
426 engineSelections.put("bookmarks", bookmarksPreference.isChecked());
427 engineSelections.put("history", historyPreference.isChecked());
428 engineSelections.put("passwords", passwordsPreference.isChecked());
429 engineSelections.put("tabs", tabsPreference.isChecked());
431 // No GlobalSession.config, so store directly to shared prefs. We do this on
432 // a background thread to avoid IO on the main thread and strict mode
433 // warnings.
434 new Thread(new PersistEngineSelectionsRunnable(engineSelections)).start();
435 }
437 protected void requestDelayedSync() {
438 Logger.info(LOG_TAG, "Posting a delayed request for a sync sometime soon.");
439 handler.removeCallbacks(requestSyncRunnable);
440 handler.postDelayed(requestSyncRunnable, DELAY_IN_MILLISECONDS_BEFORE_REQUESTING_SYNC);
441 }
443 /**
444 * Remove all traces of debug buttons. By default, no debug buttons are shown.
445 */
446 protected void removeDebugButtons() {
447 final PreferenceScreen statusScreen = (PreferenceScreen) ensureFindPreference("status_screen");
448 final PreferenceCategory debugCategory = (PreferenceCategory) ensureFindPreference("debug_category");
449 statusScreen.removePreference(debugCategory);
450 }
452 /**
453 * A Runnable that persists engine selections to shared prefs, and then
454 * requests a delayed sync.
455 * <p>
456 * References the member <code>fxAccount</code> and is specific to the Android
457 * account associated to that account.
458 */
459 protected class PersistEngineSelectionsRunnable implements Runnable {
460 private final Map<String, Boolean> engineSelections;
462 protected PersistEngineSelectionsRunnable(Map<String, Boolean> engineSelections) {
463 this.engineSelections = engineSelections;
464 }
466 @Override
467 public void run() {
468 try {
469 // Name shadowing -- do you like it, or do you love it?
470 AndroidFxAccount fxAccount = FxAccountStatusFragment.this.fxAccount;
471 if (fxAccount == null) {
472 return;
473 }
474 Logger.info(LOG_TAG, "Persisting engine selections: " + engineSelections.toString());
475 SyncConfiguration.storeSelectedEnginesToPrefs(fxAccount.getSyncPrefs(), engineSelections);
476 requestDelayedSync();
477 } catch (Exception e) {
478 Logger.warn(LOG_TAG, "Got exception persisting selected engines; ignoring.", e);
479 return;
480 }
481 }
482 }
484 /**
485 * A Runnable that requests a sync.
486 * <p>
487 * References the member <code>fxAccount</code>, but is not specific to the
488 * Android account associated to that account.
489 */
490 protected class RequestSyncRunnable implements Runnable {
491 @Override
492 public void run() {
493 // Name shadowing -- do you like it, or do you love it?
494 AndroidFxAccount fxAccount = FxAccountStatusFragment.this.fxAccount;
495 if (fxAccount == null) {
496 return;
497 }
498 Logger.info(LOG_TAG, "Requesting a sync sometime soon.");
499 fxAccount.requestSync();
500 }
501 }
503 /**
504 * A separate listener to separate debug logic from main code paths.
505 */
506 protected class DebugPreferenceClickListener implements OnPreferenceClickListener {
507 @Override
508 public boolean onPreferenceClick(Preference preference) {
509 final String key = preference.getKey();
510 if ("debug_refresh".equals(key)) {
511 Logger.info(LOG_TAG, "Refreshing.");
512 refresh();
513 } else if ("debug_dump".equals(key)) {
514 fxAccount.dump();
515 } else if ("debug_force_sync".equals(key)) {
516 Logger.info(LOG_TAG, "Force syncing.");
517 fxAccount.requestSync(FirefoxAccounts.FORCE);
518 // No sense refreshing, since the sync will complete in the future.
519 } else if ("debug_forget_certificate".equals(key)) {
520 State state = fxAccount.getState();
521 try {
522 Married married = (Married) state;
523 Logger.info(LOG_TAG, "Moving to Cohabiting state: Forgetting certificate.");
524 fxAccount.setState(married.makeCohabitingState());
525 refresh();
526 } catch (ClassCastException e) {
527 Logger.info(LOG_TAG, "Not in Married state; can't forget certificate.");
528 // Ignore.
529 }
530 } else if ("debug_require_password".equals(key)) {
531 Logger.info(LOG_TAG, "Moving to Separated state: Forgetting password.");
532 State state = fxAccount.getState();
533 fxAccount.setState(state.makeSeparatedState());
534 refresh();
535 } else if ("debug_require_upgrade".equals(key)) {
536 Logger.info(LOG_TAG, "Moving to Doghouse state: Requiring upgrade.");
537 State state = fxAccount.getState();
538 fxAccount.setState(state.makeDoghouseState());
539 refresh();
540 } else {
541 return false;
542 }
543 return true;
544 }
545 }
547 /**
548 * Iterate through debug buttons, adding a special deubg preference click
549 * listener to each of them.
550 */
551 protected void connectDebugButtons() {
552 // Separate listener to really separate debug logic from main code paths.
553 final OnPreferenceClickListener listener = new DebugPreferenceClickListener();
555 // We don't want to use Android resource strings for debug UI, so we just
556 // use the keys throughout.
557 final Preference debugCategory = ensureFindPreference("debug_category");
558 debugCategory.setTitle(debugCategory.getKey());
560 String[] debugKeys = new String[] {
561 "debug_refresh",
562 "debug_dump",
563 "debug_force_sync",
564 "debug_forget_certificate",
565 "debug_require_password",
566 "debug_require_upgrade" };
567 for (String debugKey : debugKeys) {
568 final Preference button = ensureFindPreference(debugKey);
569 button.setTitle(debugKey); // Not very friendly, but this is for debugging only!
570 button.setOnPreferenceClickListener(listener);
571 }
572 }
573 }