1 /*
2  * Copyright (C) 2010 Google Inc.
3  * Licensed to The Android Open Source Project.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 package com.android.mail.browse;
19 
20 import android.content.Context;
21 import android.net.Uri;
22 import android.os.AsyncTask;
23 import android.support.v7.view.ActionMode;
24 import android.view.Menu;
25 import android.view.MenuInflater;
26 import android.view.MenuItem;
27 import android.widget.Toast;
28 
29 import com.android.mail.R;
30 import com.android.mail.analytics.Analytics;
31 import com.android.mail.providers.Account;
32 import com.android.mail.providers.AccountObserver;
33 import com.android.mail.providers.Conversation;
34 import com.android.mail.providers.Folder;
35 import com.android.mail.providers.MailAppProvider;
36 import com.android.mail.providers.Settings;
37 import com.android.mail.providers.UIProvider;
38 import com.android.mail.providers.UIProvider.AccountCapabilities;
39 import com.android.mail.providers.UIProvider.ConversationColumns;
40 import com.android.mail.providers.UIProvider.FolderCapabilities;
41 import com.android.mail.providers.UIProvider.FolderType;
42 import com.android.mail.ui.ControllableActivity;
43 import com.android.mail.ui.ConversationCheckedSet;
44 import com.android.mail.ui.ConversationListCallbacks;
45 import com.android.mail.ui.ConversationSetObserver;
46 import com.android.mail.ui.ConversationUpdater;
47 import com.android.mail.ui.DestructiveAction;
48 import com.android.mail.ui.FolderOperation;
49 import com.android.mail.ui.FolderSelectionDialog;
50 import com.android.mail.utils.LogTag;
51 import com.android.mail.utils.LogUtils;
52 import com.android.mail.utils.Utils;
53 import com.google.common.annotations.VisibleForTesting;
54 import com.google.common.collect.Lists;
55 
56 import java.util.Collection;
57 import java.util.List;
58 
59 /**
60  * A component that displays a custom view for an {@code ActionBar}'s {@code
61  * ContextMode} specific to operating on a set of conversations.
62  */
63 public class SelectedConversationsActionMenu implements ActionMode.Callback,
64         ConversationSetObserver {
65 
66     private static final String LOG_TAG = LogTag.getLogTag();
67 
68     /**
69      * The set of conversations to display the menu for.
70      */
71     protected final ConversationCheckedSet mCheckedSet;
72 
73     private final ControllableActivity mActivity;
74     private final ConversationListCallbacks mListController;
75     /**
76      * Context of the activity. A dialog requires the context of an activity rather than the global
77      * root context of the process. So mContext = mActivity.getApplicationContext() will fail.
78      */
79     private final Context mContext;
80 
81     @VisibleForTesting
82     private ActionMode mActionMode;
83 
84     private boolean mActivated = false;
85 
86     /** Object that can update conversation state on our behalf. */
87     private final ConversationUpdater mUpdater;
88 
89     private Account mAccount;
90 
91     private final Folder mFolder;
92 
93     private AccountObserver mAccountObserver;
94 
95     private MenuItem mDiscardOutboxMenuItem;
96 
SelectedConversationsActionMenu( ControllableActivity activity, ConversationCheckedSet checkedSet, Folder folder)97     public SelectedConversationsActionMenu(
98             ControllableActivity activity, ConversationCheckedSet checkedSet, Folder folder) {
99         mActivity = activity;
100         mListController = activity.getListHandler();
101         mCheckedSet = checkedSet;
102         mAccountObserver = new AccountObserver() {
103             @Override
104             public void onChanged(Account newAccount) {
105                 mAccount = newAccount;
106             }
107         };
108         mAccount = mAccountObserver.initialize(activity.getAccountController());
109         mFolder = folder;
110         mContext = mActivity.getActivityContext();
111         mUpdater = activity.getConversationUpdater();
112     }
113 
onActionItemClicked(MenuItem item)114     public boolean onActionItemClicked(MenuItem item) {
115         return onActionItemClicked(mActionMode, item);
116     }
117 
118     @Override
onActionItemClicked(ActionMode mode, MenuItem item)119     public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
120         boolean handled = true;
121         // If the user taps a new menu item, commit any existing destructive actions.
122         mListController.commitDestructiveActions(true);
123         final int itemId = item.getItemId();
124 
125         Analytics.getInstance().sendMenuItemEvent(Analytics.EVENT_CATEGORY_MENU_ITEM, itemId,
126                 "cab_mode", 0);
127 
128         UndoCallback undoCallback = null;   // not applicable here (yet)
129         if (itemId == R.id.delete) {
130             LogUtils.i(LOG_TAG, "Delete selected from CAB menu");
131             performDestructiveAction(R.id.delete, undoCallback);
132         } else if (itemId == R.id.discard_drafts) {
133             LogUtils.i(LOG_TAG, "Discard drafts selected from CAB menu");
134             performDestructiveAction(R.id.discard_drafts, undoCallback);
135         } else if (itemId == R.id.discard_outbox) {
136             LogUtils.i(LOG_TAG, "Discard outbox selected from CAB menu");
137             performDestructiveAction(R.id.discard_outbox, undoCallback);
138         } else if (itemId == R.id.archive) {
139             LogUtils.i(LOG_TAG, "Archive selected from CAB menu");
140             performDestructiveAction(R.id.archive, undoCallback);
141         } else if (itemId == R.id.remove_folder) {
142             destroy(R.id.remove_folder, mCheckedSet.values(),
143                     mUpdater.getDeferredRemoveFolder(mCheckedSet.values(), mFolder, true,
144                             true, true, undoCallback));
145         } else if (itemId == R.id.mute) {
146             destroy(R.id.mute, mCheckedSet.values(), mUpdater.getBatchAction(R.id.mute,
147                     undoCallback));
148         } else if (itemId == R.id.report_spam) {
149             destroy(R.id.report_spam, mCheckedSet.values(),
150                     mUpdater.getBatchAction(R.id.report_spam, undoCallback));
151         } else if (itemId == R.id.mark_not_spam) {
152             // Currently, since spam messages are only shown in list with other spam messages,
153             // marking a message not as spam is a destructive action
154             destroy (R.id.mark_not_spam,
155                     mCheckedSet.values(), mUpdater.getBatchAction(R.id.mark_not_spam,
156                             undoCallback)) ;
157         } else if (itemId == R.id.report_phishing) {
158             destroy(R.id.report_phishing,
159                     mCheckedSet.values(), mUpdater.getBatchAction(R.id.report_phishing,
160                             undoCallback));
161         } else if (itemId == R.id.read) {
162             markConversationsRead(true);
163         } else if (itemId == R.id.unread) {
164             markConversationsRead(false);
165         } else if (itemId == R.id.star) {
166             starConversations(true);
167         } else if (itemId == R.id.toggle_read_unread) {
168             if (mActionMode != null) {
169                 markConversationsRead(mActionMode.getMenu().findItem(R.id.read).isVisible());
170             }
171         } else if (itemId == R.id.remove_star) {
172             if (mFolder.isType(UIProvider.FolderType.STARRED)) {
173                 LogUtils.d(LOG_TAG, "We are in a starred folder, removing the star");
174                 performDestructiveAction(R.id.remove_star, undoCallback);
175             } else {
176                 LogUtils.d(LOG_TAG, "Not in a starred folder.");
177                 starConversations(false);
178             }
179         } else if (itemId == R.id.move_to || itemId == R.id.change_folders) {
180             boolean cantMove = false;
181             Account acct = mAccount;
182             // Special handling for virtual folders
183             if (mFolder.supportsCapability(FolderCapabilities.IS_VIRTUAL)) {
184                 Uri accountUri = null;
185                 for (Conversation conv: mCheckedSet.values()) {
186                     if (accountUri == null) {
187                         accountUri = conv.accountUri;
188                     } else if (!accountUri.equals(conv.accountUri)) {
189                         // Tell the user why we can't do this
190                         Toast.makeText(mContext, R.string.cant_move_or_change_labels,
191                                 Toast.LENGTH_LONG).show();
192                         cantMove = true;
193                         return handled;
194                     }
195                 }
196                 if (!cantMove) {
197                     // Get the actual account here, so that we display its folders in the dialog
198                     acct = MailAppProvider.getAccountFromAccountUri(accountUri);
199                 }
200             }
201             if (!cantMove) {
202                 final FolderSelectionDialog dialog = FolderSelectionDialog.getInstance(
203                         acct, mCheckedSet.values(), true, mFolder,
204                         item.getItemId() == R.id.move_to);
205                 if (dialog != null) {
206                     dialog.show(mActivity.getFragmentManager(), null);
207                 }
208             }
209         } else if (itemId == R.id.move_to_inbox) {
210             new AsyncTask<Void, Void, Folder>() {
211                 @Override
212                 protected Folder doInBackground(final Void... params) {
213                     // Get the "move to" inbox
214                     return Utils.getFolder(mContext, mAccount.settings.moveToInbox,
215                             true /* allowHidden */);
216                 }
217 
218                 @Override
219                 protected void onPostExecute(final Folder moveToInbox) {
220                     final List<FolderOperation> ops = Lists.newArrayListWithCapacity(1);
221                     // Add inbox
222                     ops.add(new FolderOperation(moveToInbox, true));
223                     mUpdater.assignFolder(ops, mCheckedSet.values(), true,
224                             true /* showUndo */, false /* isMoveTo */);
225                 }
226             }.execute((Void[]) null);
227         } else if (itemId == R.id.mark_important) {
228             markConversationsImportant(true);
229         } else if (itemId == R.id.mark_not_important) {
230             if (mFolder.supportsCapability(UIProvider.FolderCapabilities.ONLY_IMPORTANT)) {
231                 performDestructiveAction(R.id.mark_not_important, undoCallback);
232             } else {
233                 markConversationsImportant(false);
234             }
235         } else {
236             handled = false;
237         }
238         return handled;
239     }
240 
241     /**
242      * Clear the selection and perform related UI changes to keep the state consistent.
243      */
clearChecked()244     private void clearChecked() {
245         mCheckedSet.clear();
246     }
247 
248     /**
249      * Update the underlying list adapter and redraw the menus if necessary.
250      */
updateSelection()251     private void updateSelection() {
252         mUpdater.refreshConversationList();
253         if (mActionMode != null) {
254             // Calling mActivity.invalidateOptionsMenu doesn't have the correct behavior, since
255             // the action mode is not refreshed when activity's options menu is invalidated.
256             // Since we need to refresh our own menu, it is easy to call onPrepareActionMode
257             // directly.
258             onPrepareActionMode(mActionMode, mActionMode.getMenu());
259         }
260     }
261 
performDestructiveAction(final int action, UndoCallback undoCallback)262     private void performDestructiveAction(final int action, UndoCallback undoCallback) {
263         final Collection<Conversation> conversations = mCheckedSet.values();
264         final Settings settings = mAccount.settings;
265         final boolean showDialog;
266         // no confirmation dialog by default unless user preference or common sense dictates one
267         if (action == R.id.discard_drafts) {
268             // drafts are lost forever, so always confirm
269             showDialog = true;
270         } else if (settings != null && (action == R.id.archive || action == R.id.delete)) {
271             showDialog = (action == R.id.delete) ? settings.confirmDelete : settings.confirmArchive;
272         } else {
273             showDialog = false;
274         }
275         if (showDialog) {
276             mUpdater.makeDialogListener(action, true /* fromSelectedSet */, null /* undoCallback */);
277             final int resId;
278             if (action == R.id.delete) {
279                 resId = R.plurals.confirm_delete_conversation;
280             } else if (action == R.id.discard_drafts) {
281                 resId = R.plurals.confirm_discard_drafts_conversation;
282             } else {
283                 resId = R.plurals.confirm_archive_conversation;
284             }
285             final CharSequence message = Utils.formatPlural(mContext, resId, conversations.size());
286             final ConfirmDialogFragment c = ConfirmDialogFragment.newInstance(message);
287             c.displayDialog(mActivity.getFragmentManager());
288         } else {
289             // No need to show the dialog, just make a destructive action and destroy the
290             // selected set immediately.
291             // TODO(viki): Stop using the deferred action here. Use the registered action.
292             destroy(action, conversations, mUpdater.getDeferredBatchAction(action, undoCallback));
293         }
294     }
295 
296     /**
297      * Destroy these conversations through the conversation updater
298      * @param actionId the ID of the action: R.id.archive, R.id.delete, ...
299      * @param target conversations to destroy
300      * @param action the action that performs the destruction
301      */
destroy(int actionId, final Collection<Conversation> target, final DestructiveAction action)302     private void destroy(int actionId, final Collection<Conversation> target,
303             final DestructiveAction action) {
304         LogUtils.i(LOG_TAG, "About to remove %d converations", target.size());
305         mUpdater.delete(actionId, target, action, true);
306     }
307 
308     /**
309      * Marks the read state of currently selected conversations (<b>and</b> the backing storage)
310      * to the value provided here.
311      * @param read is true if the conversations are to be marked as read, false if they are to be
312      * marked unread.
313      */
markConversationsRead(boolean read)314     private void markConversationsRead(boolean read) {
315         final Collection<Conversation> targets = mCheckedSet.values();
316         // The conversations are marked read but not viewed.
317         mUpdater.markConversationsRead(targets, read, false);
318         updateSelection();
319     }
320 
321     /**
322      * Marks the important state of currently selected conversations (<b>and</b> the backing
323      * storage) to the value provided here.
324      * @param important is true if the conversations are to be marked as important, false if they
325      * are to be marked not important.
326      */
markConversationsImportant(boolean important)327     private void markConversationsImportant(boolean important) {
328         final Collection<Conversation> target = mCheckedSet.values();
329         final int priority = important ? UIProvider.ConversationPriority.HIGH
330                 : UIProvider.ConversationPriority.LOW;
331         mUpdater.updateConversation(target, ConversationColumns.PRIORITY, priority);
332         // Update the conversations in the selection too.
333         for (final Conversation c : target) {
334             c.priority = priority;
335         }
336         updateSelection();
337     }
338 
339     /**
340      * Marks the selected conversations with the star setting provided here.
341      * @param star true if you want all the conversations to have stars, false if you want to remove
342      * stars from all conversations
343      */
starConversations(boolean star)344     private void starConversations(boolean star) {
345         final Collection<Conversation> target = mCheckedSet.values();
346         mUpdater.updateConversation(target, ConversationColumns.STARRED, star);
347         // Update the conversations in the selection too.
348         for (final Conversation c : target) {
349             c.starred = star;
350         }
351         updateSelection();
352     }
353 
354     @Override
onCreateActionMode(ActionMode mode, Menu menu)355     public boolean onCreateActionMode(ActionMode mode, Menu menu) {
356         mCheckedSet.addObserver(this);
357         final MenuInflater inflater = mActivity.getMenuInflater();
358         inflater.inflate(R.menu.conversation_list_selection_actions_menu, menu);
359         mActionMode = mode;
360         updateCount();
361         return true;
362     }
363 
364     @Override
onPrepareActionMode(ActionMode mode, Menu menu)365     public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
366         // Update the actionbar to select operations available on the current conversation.
367         final Collection<Conversation> conversations = mCheckedSet.values();
368         boolean showStar = false;
369         boolean showMarkUnread = false;
370         boolean showMarkImportant = false;
371         boolean showMarkNotSpam = false;
372         boolean showMarkAsPhishing = false;
373 
374         // TODO(shahrk): Clean up these dirty calls using Utils.setMenuItemPresent(...) or
375         // in another way
376 
377         for (Conversation conversation : conversations) {
378             if (!conversation.starred) {
379                 showStar = true;
380             }
381             if (conversation.read) {
382                 showMarkUnread = true;
383             }
384             if (!conversation.isImportant()) {
385                 showMarkImportant = true;
386             }
387             if (conversation.spam) {
388                 showMarkNotSpam = true;
389             }
390             if (!conversation.phishing) {
391                 showMarkAsPhishing = true;
392             }
393             if (showStar && showMarkUnread && showMarkImportant && showMarkNotSpam &&
394                     showMarkAsPhishing) {
395                 break;
396             }
397         }
398         final boolean canStar = mFolder != null && !mFolder.isTrash();
399         final MenuItem star = menu.findItem(R.id.star);
400         star.setVisible(showStar && canStar);
401         final MenuItem unstar = menu.findItem(R.id.remove_star);
402         unstar.setVisible(!showStar && canStar);
403         final MenuItem read = menu.findItem(R.id.read);
404         read.setVisible(!showMarkUnread);
405         final MenuItem unread = menu.findItem(R.id.unread);
406         unread.setVisible(showMarkUnread);
407 
408         // We only ever show one of:
409         // 1) remove folder
410         // 2) archive
411         final MenuItem removeFolder = menu.findItem(R.id.remove_folder);
412         final MenuItem moveTo = menu.findItem(R.id.move_to);
413         final MenuItem moveToInbox = menu.findItem(R.id.move_to_inbox);
414         final boolean showRemoveFolder = mFolder != null && mFolder.isType(FolderType.DEFAULT)
415                 && mFolder.supportsCapability(FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES)
416                 && !mFolder.isProviderFolder()
417                 && mAccount.supportsCapability(AccountCapabilities.ARCHIVE);
418         final boolean showMoveTo = mFolder != null
419                 && mFolder.supportsCapability(FolderCapabilities.ALLOWS_REMOVE_CONVERSATION);
420         final boolean showMoveToInbox = mFolder != null
421                 && mFolder.supportsCapability(FolderCapabilities.ALLOWS_MOVE_TO_INBOX);
422         removeFolder.setVisible(showRemoveFolder);
423         moveTo.setVisible(showMoveTo);
424         moveToInbox.setVisible(showMoveToInbox);
425 
426         final MenuItem changeFolders = menu.findItem(R.id.change_folders);
427         changeFolders.setVisible(mAccount.supportsCapability(
428                 UIProvider.AccountCapabilities.MULTIPLE_FOLDERS_PER_CONV));
429 
430         if (mFolder != null && showRemoveFolder) {
431             removeFolder.setTitle(mActivity.getActivityContext().getString(R.string.remove_folder,
432                     mFolder.name));
433         }
434         final MenuItem archive = menu.findItem(R.id.archive);
435         if (archive != null) {
436             archive.setVisible(
437                     mAccount.supportsCapability(UIProvider.AccountCapabilities.ARCHIVE) &&
438                     mFolder.supportsCapability(FolderCapabilities.ARCHIVE));
439         }
440         final MenuItem spam = menu.findItem(R.id.report_spam);
441         spam.setVisible(!showMarkNotSpam
442                 && mAccount.supportsCapability(UIProvider.AccountCapabilities.REPORT_SPAM)
443                 && mFolder.supportsCapability(FolderCapabilities.REPORT_SPAM));
444         final MenuItem notSpam = menu.findItem(R.id.mark_not_spam);
445         notSpam.setVisible(showMarkNotSpam &&
446                 mAccount.supportsCapability(UIProvider.AccountCapabilities.REPORT_SPAM) &&
447                 mFolder.supportsCapability(FolderCapabilities.MARK_NOT_SPAM));
448         final MenuItem phishing = menu.findItem(R.id.report_phishing);
449         phishing.setVisible(showMarkAsPhishing &&
450                 mAccount.supportsCapability(UIProvider.AccountCapabilities.REPORT_PHISHING) &&
451                 mFolder.supportsCapability(FolderCapabilities.REPORT_PHISHING));
452 
453         final MenuItem mute = menu.findItem(R.id.mute);
454         if (mute != null) {
455             mute.setVisible(mAccount.supportsCapability(UIProvider.AccountCapabilities.MUTE)
456                     && (mFolder != null && mFolder.isInbox()));
457         }
458         final MenuItem markImportant = menu.findItem(R.id.mark_important);
459         markImportant.setVisible(showMarkImportant
460                 && mAccount.supportsCapability(UIProvider.AccountCapabilities.MARK_IMPORTANT));
461         final MenuItem markNotImportant = menu.findItem(R.id.mark_not_important);
462         markNotImportant.setVisible(!showMarkImportant
463                 && mAccount.supportsCapability(UIProvider.AccountCapabilities.MARK_IMPORTANT));
464 
465         boolean shouldShowDiscardOutbox = mFolder != null && mFolder.isType(FolderType.OUTBOX);
466         mDiscardOutboxMenuItem = menu.findItem(R.id.discard_outbox);
467         if (mDiscardOutboxMenuItem != null) {
468             mDiscardOutboxMenuItem.setVisible(shouldShowDiscardOutbox);
469         }
470         final boolean showDelete = mFolder != null && !mFolder.isType(FolderType.OUTBOX)
471                 && mFolder.supportsCapability(UIProvider.FolderCapabilities.DELETE);
472         final MenuItem trash = menu.findItem(R.id.delete);
473         trash.setVisible(showDelete);
474         // We only want to show the discard drafts menu item if we are not showing the delete menu
475         // item, and the current folder is a draft folder and the account supports discarding
476         // drafts for a conversation
477         final boolean showDiscardDrafts = !showDelete && mFolder != null && mFolder.isDraft() &&
478                 mAccount.supportsCapability(AccountCapabilities.DISCARD_CONVERSATION_DRAFTS);
479         final MenuItem discardDrafts = menu.findItem(R.id.discard_drafts);
480         if (discardDrafts != null) {
481             discardDrafts.setVisible(showDiscardDrafts);
482         }
483 
484         return true;
485     }
486 
487     @Override
onDestroyActionMode(ActionMode mode)488     public void onDestroyActionMode(ActionMode mode) {
489         mActionMode = null;
490         // The action mode may have been destroyed due to this menu being deactivated, in which
491         // case resources need not be cleaned up. However, if it was destroyed while this menu is
492         // active, that implies the user hit "Done" in the top right, and resources need cleaning.
493         if (mActivated) {
494             destroy();
495             // Only commit destructive actions if the user actually pressed
496             // done; otherwise, this was handled when we toggled conversation
497             // selection state.
498             mActivity.getListHandler().commitDestructiveActions(true);
499         }
500     }
501 
502     @Override
onSetPopulated(ConversationCheckedSet set)503     public void onSetPopulated(ConversationCheckedSet set) {
504         // Noop. This object can only exist while the set is non-empty.
505     }
506 
507     @Override
onSetEmpty()508     public void onSetEmpty() {
509         LogUtils.d(LOG_TAG, "onSetEmpty called.");
510         destroy();
511     }
512 
513     @Override
onSetChanged(ConversationCheckedSet set)514     public void onSetChanged(ConversationCheckedSet set) {
515         // If the set is empty, the menu buttons are invalid and most like the menu will be cleaned
516         // up. Avoid making any changes to stop flickering ("Add Star" -> "Remove Star") just
517         // before hiding the menu.
518         if (set.isEmpty()) {
519             return;
520         }
521         updateCount();
522     }
523 
524     /**
525      * Updates the visible count of how many conversations are selected.
526      */
updateCount()527     private void updateCount() {
528         if (mActionMode != null) {
529             mActionMode.setTitle(String.format("%d", mCheckedSet.size()));
530         }
531     }
532 
533     /**
534      * Activates and shows this menu (essentially starting an {@link ActionMode}) if the selected
535      * set is non-empty.
536      */
activate()537     public void activate() {
538         if (mCheckedSet.isEmpty()) {
539             return;
540         }
541         mListController.onCabModeEntered();
542         mActivated = true;
543         if (mActionMode == null) {
544             mActivity.startSupportActionMode(this);
545         }
546     }
547 
548     /**
549      * De-activates and hides the menu (essentially disabling the {@link ActionMode}), but maintains
550      * the selection conversation set, and internally updates state as necessary.
551      */
deactivate()552     public void deactivate() {
553         mListController.onCabModeExited();
554         mActivated = false;
555         if (mActionMode != null) {
556             mActionMode.finish();
557         }
558     }
559 
560     @VisibleForTesting
561     /**
562      * Returns true if CAB mode is active.
563      */
isActivated()564     public boolean isActivated() {
565         return mActivated;
566     }
567 
568     /**
569      * Destroys and cleans up the resources associated with this menu.
570      */
destroy()571     private void destroy() {
572         deactivate();
573         mCheckedSet.removeObserver(this);
574         clearChecked();
575         mUpdater.refreshConversationList();
576         if (mAccountObserver != null) {
577             mAccountObserver.unregisterAndDestroy();
578             mAccountObserver = null;
579         }
580     }
581 }
582