Home | History | Annotate | Download | only in browse
      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 
     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 
    114     public boolean onActionItemClicked(MenuItem item) {
    115         return onActionItemClicked(mActionMode, item);
    116     }
    117 
    118     @Override
    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      */
    244     private void clearChecked() {
    245         mCheckedSet.clear();
    246     }
    247 
    248     /**
    249      * Update the underlying list adapter and redraw the menus if necessary.
    250      */
    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 
    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      */
    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      */
    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      */
    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      */
    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
    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
    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
    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
    503     public void onSetPopulated(ConversationCheckedSet set) {
    504         // Noop. This object can only exist while the set is non-empty.
    505     }
    506 
    507     @Override
    508     public void onSetEmpty() {
    509         LogUtils.d(LOG_TAG, "onSetEmpty called.");
    510         destroy();
    511     }
    512 
    513     @Override
    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      */
    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      */
    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      */
    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      */
    564     public boolean isActivated() {
    565         return mActivated;
    566     }
    567 
    568     /**
    569      * Destroys and cleans up the resources associated with this menu.
    570      */
    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