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.ConversationListCallbacks;
     44 import com.android.mail.ui.ConversationSelectionSet;
     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 ConversationSelectionSet mSelectionSet;
     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, ConversationSelectionSet selectionSet, Folder folder) {
     99         mActivity = activity;
    100         mListController = activity.getListHandler();
    101         mSelectionSet = selectionSet;
    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     @Override
    115     public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
    116         boolean handled = true;
    117         // If the user taps a new menu item, commit any existing destructive actions.
    118         mListController.commitDestructiveActions(true);
    119         final int itemId = item.getItemId();
    120 
    121         Analytics.getInstance().sendMenuItemEvent(Analytics.EVENT_CATEGORY_MENU_ITEM, itemId,
    122                 "cab_mode", 0);
    123 
    124         UndoCallback undoCallback = null;   // not applicable here (yet)
    125         if (itemId == R.id.delete) {
    126             LogUtils.i(LOG_TAG, "Delete selected from CAB menu");
    127             performDestructiveAction(R.id.delete, undoCallback);
    128         } else if (itemId == R.id.discard_drafts) {
    129             LogUtils.i(LOG_TAG, "Discard drafts selected from CAB menu");
    130             performDestructiveAction(R.id.discard_drafts, undoCallback);
    131         } else if (itemId == R.id.discard_outbox) {
    132             LogUtils.i(LOG_TAG, "Discard outbox selected from CAB menu");
    133             performDestructiveAction(R.id.discard_outbox, undoCallback);
    134         } else if (itemId == R.id.archive) {
    135             LogUtils.i(LOG_TAG, "Archive selected from CAB menu");
    136             performDestructiveAction(R.id.archive, undoCallback);
    137         } else if (itemId == R.id.remove_folder) {
    138             destroy(R.id.remove_folder, mSelectionSet.values(),
    139                     mUpdater.getDeferredRemoveFolder(mSelectionSet.values(), mFolder, true,
    140                             true, true, undoCallback));
    141         } else if (itemId == R.id.mute) {
    142             destroy(R.id.mute, mSelectionSet.values(), mUpdater.getBatchAction(R.id.mute,
    143                     undoCallback));
    144         } else if (itemId == R.id.report_spam) {
    145             destroy(R.id.report_spam, mSelectionSet.values(),
    146                     mUpdater.getBatchAction(R.id.report_spam, undoCallback));
    147         } else if (itemId == R.id.mark_not_spam) {
    148             // Currently, since spam messages are only shown in list with other spam messages,
    149             // marking a message not as spam is a destructive action
    150             destroy (R.id.mark_not_spam,
    151                     mSelectionSet.values(), mUpdater.getBatchAction(R.id.mark_not_spam,
    152                             undoCallback)) ;
    153         } else if (itemId == R.id.report_phishing) {
    154             destroy(R.id.report_phishing,
    155                     mSelectionSet.values(), mUpdater.getBatchAction(R.id.report_phishing,
    156                             undoCallback));
    157         } else if (itemId == R.id.read) {
    158             markConversationsRead(true);
    159         } else if (itemId == R.id.unread) {
    160             markConversationsRead(false);
    161         } else if (itemId == R.id.star) {
    162             starConversations(true);
    163         } else if (itemId == R.id.remove_star) {
    164             if (mFolder.isType(UIProvider.FolderType.STARRED)) {
    165                 LogUtils.d(LOG_TAG, "We are in a starred folder, removing the star");
    166                 performDestructiveAction(R.id.remove_star, undoCallback);
    167             } else {
    168                 LogUtils.d(LOG_TAG, "Not in a starred folder.");
    169                 starConversations(false);
    170             }
    171         } else if (itemId == R.id.move_to || itemId == R.id.change_folders) {
    172             boolean cantMove = false;
    173             Account acct = mAccount;
    174             // Special handling for virtual folders
    175             if (mFolder.supportsCapability(FolderCapabilities.IS_VIRTUAL)) {
    176                 Uri accountUri = null;
    177                 for (Conversation conv: mSelectionSet.values()) {
    178                     if (accountUri == null) {
    179                         accountUri = conv.accountUri;
    180                     } else if (!accountUri.equals(conv.accountUri)) {
    181                         // Tell the user why we can't do this
    182                         Toast.makeText(mContext, R.string.cant_move_or_change_labels,
    183                                 Toast.LENGTH_LONG).show();
    184                         cantMove = true;
    185                         return handled;
    186                     }
    187                 }
    188                 if (!cantMove) {
    189                     // Get the actual account here, so that we display its folders in the dialog
    190                     acct = MailAppProvider.getAccountFromAccountUri(accountUri);
    191                 }
    192             }
    193             if (!cantMove) {
    194                 final FolderSelectionDialog dialog = FolderSelectionDialog.getInstance(
    195                         acct, mSelectionSet.values(), true, mFolder,
    196                         item.getItemId() == R.id.move_to);
    197                 if (dialog != null) {
    198                     dialog.show(mActivity.getFragmentManager(), null);
    199                 }
    200             }
    201         } else if (itemId == R.id.move_to_inbox) {
    202             new AsyncTask<Void, Void, Folder>() {
    203                 @Override
    204                 protected Folder doInBackground(final Void... params) {
    205                     // Get the "move to" inbox
    206                     return Utils.getFolder(mContext, mAccount.settings.moveToInbox,
    207                             true /* allowHidden */);
    208                 }
    209 
    210                 @Override
    211                 protected void onPostExecute(final Folder moveToInbox) {
    212                     final List<FolderOperation> ops = Lists.newArrayListWithCapacity(1);
    213                     // Add inbox
    214                     ops.add(new FolderOperation(moveToInbox, true));
    215                     mUpdater.assignFolder(ops, mSelectionSet.values(), true,
    216                             true /* showUndo */, false /* isMoveTo */);
    217                 }
    218             }.execute((Void[]) null);
    219         } else if (itemId == R.id.mark_important) {
    220             markConversationsImportant(true);
    221         } else if (itemId == R.id.mark_not_important) {
    222             if (mFolder.supportsCapability(UIProvider.FolderCapabilities.ONLY_IMPORTANT)) {
    223                 performDestructiveAction(R.id.mark_not_important, undoCallback);
    224             } else {
    225                 markConversationsImportant(false);
    226             }
    227         } else {
    228             handled = false;
    229         }
    230         return handled;
    231     }
    232 
    233     /**
    234      * Clear the selection and perform related UI changes to keep the state consistent.
    235      */
    236     private void clearSelection() {
    237         mSelectionSet.clear();
    238     }
    239 
    240     /**
    241      * Update the underlying list adapter and redraw the menus if necessary.
    242      */
    243     private void updateSelection() {
    244         mUpdater.refreshConversationList();
    245         if (mActionMode != null) {
    246             // Calling mActivity.invalidateOptionsMenu doesn't have the correct behavior, since
    247             // the action mode is not refreshed when activity's options menu is invalidated.
    248             // Since we need to refresh our own menu, it is easy to call onPrepareActionMode
    249             // directly.
    250             onPrepareActionMode(mActionMode, mActionMode.getMenu());
    251         }
    252     }
    253 
    254     private void performDestructiveAction(final int action, UndoCallback undoCallback) {
    255         final Collection<Conversation> conversations = mSelectionSet.values();
    256         final Settings settings = mAccount.settings;
    257         final boolean showDialog;
    258         // no confirmation dialog by default unless user preference or common sense dictates one
    259         if (action == R.id.discard_drafts) {
    260             // drafts are lost forever, so always confirm
    261             showDialog = true;
    262         } else if (settings != null && (action == R.id.archive || action == R.id.delete)) {
    263             showDialog = (action == R.id.delete) ? settings.confirmDelete : settings.confirmArchive;
    264         } else {
    265             showDialog = false;
    266         }
    267         if (showDialog) {
    268             mUpdater.makeDialogListener(action, true /* fromSelectedSet */, null /* undoCallback */);
    269             final int resId;
    270             if (action == R.id.delete) {
    271                 resId = R.plurals.confirm_delete_conversation;
    272             } else if (action == R.id.discard_drafts) {
    273                 resId = R.plurals.confirm_discard_drafts_conversation;
    274             } else {
    275                 resId = R.plurals.confirm_archive_conversation;
    276             }
    277             final CharSequence message = Utils.formatPlural(mContext, resId, conversations.size());
    278             final ConfirmDialogFragment c = ConfirmDialogFragment.newInstance(message);
    279             c.displayDialog(mActivity.getFragmentManager());
    280         } else {
    281             // No need to show the dialog, just make a destructive action and destroy the
    282             // selected set immediately.
    283             // TODO(viki): Stop using the deferred action here. Use the registered action.
    284             destroy(action, conversations, mUpdater.getDeferredBatchAction(action, undoCallback));
    285         }
    286     }
    287 
    288     /**
    289      * Destroy these conversations through the conversation updater
    290      * @param actionId the ID of the action: R.id.archive, R.id.delete, ...
    291      * @param target conversations to destroy
    292      * @param action the action that performs the destruction
    293      */
    294     private void destroy(int actionId, final Collection<Conversation> target,
    295             final DestructiveAction action) {
    296         LogUtils.i(LOG_TAG, "About to remove %d converations", target.size());
    297         mUpdater.delete(actionId, target, action, true);
    298     }
    299 
    300     /**
    301      * Marks the read state of currently selected conversations (<b>and</b> the backing storage)
    302      * to the value provided here.
    303      * @param read is true if the conversations are to be marked as read, false if they are to be
    304      * marked unread.
    305      */
    306     private void markConversationsRead(boolean read) {
    307         final Collection<Conversation> targets = mSelectionSet.values();
    308         // The conversations are marked read but not viewed.
    309         mUpdater.markConversationsRead(targets, read, false);
    310         updateSelection();
    311     }
    312 
    313     /**
    314      * Marks the important state of currently selected conversations (<b>and</b> the backing
    315      * storage) to the value provided here.
    316      * @param important is true if the conversations are to be marked as important, false if they
    317      * are to be marked not important.
    318      */
    319     private void markConversationsImportant(boolean important) {
    320         final Collection<Conversation> target = mSelectionSet.values();
    321         final int priority = important ? UIProvider.ConversationPriority.HIGH
    322                 : UIProvider.ConversationPriority.LOW;
    323         mUpdater.updateConversation(target, ConversationColumns.PRIORITY, priority);
    324         // Update the conversations in the selection too.
    325         for (final Conversation c : target) {
    326             c.priority = priority;
    327         }
    328         updateSelection();
    329     }
    330 
    331     /**
    332      * Marks the selected conversations with the star setting provided here.
    333      * @param star true if you want all the conversations to have stars, false if you want to remove
    334      * stars from all conversations
    335      */
    336     private void starConversations(boolean star) {
    337         final Collection<Conversation> target = mSelectionSet.values();
    338         mUpdater.updateConversation(target, ConversationColumns.STARRED, star);
    339         // Update the conversations in the selection too.
    340         for (final Conversation c : target) {
    341             c.starred = star;
    342         }
    343         updateSelection();
    344     }
    345 
    346     @Override
    347     public boolean onCreateActionMode(ActionMode mode, Menu menu) {
    348         mSelectionSet.addObserver(this);
    349         final MenuInflater inflater = mActivity.getMenuInflater();
    350         inflater.inflate(R.menu.conversation_list_selection_actions_menu, menu);
    351         mActionMode = mode;
    352         return true;
    353     }
    354 
    355     @Override
    356     public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
    357         // Update the actionbar to select operations available on the current conversation.
    358         final Collection<Conversation> conversations = mSelectionSet.values();
    359         boolean showStar = false;
    360         boolean showMarkUnread = false;
    361         boolean showMarkImportant = false;
    362         boolean showMarkNotSpam = false;
    363         boolean showMarkAsPhishing = false;
    364 
    365         // TODO(shahrk): Clean up these dirty calls using Utils.setMenuItemVisibility(...) or
    366         // in another way
    367 
    368         for (Conversation conversation : conversations) {
    369             if (!conversation.starred) {
    370                 showStar = true;
    371             }
    372             if (conversation.read) {
    373                 showMarkUnread = true;
    374             }
    375             if (!conversation.isImportant()) {
    376                 showMarkImportant = true;
    377             }
    378             if (conversation.spam) {
    379                 showMarkNotSpam = true;
    380             }
    381             if (!conversation.phishing) {
    382                 showMarkAsPhishing = true;
    383             }
    384             if (showStar && showMarkUnread && showMarkImportant && showMarkNotSpam &&
    385                     showMarkAsPhishing) {
    386                 break;
    387             }
    388         }
    389         final MenuItem star = menu.findItem(R.id.star);
    390         star.setVisible(showStar);
    391         final MenuItem unstar = menu.findItem(R.id.remove_star);
    392         unstar.setVisible(!showStar);
    393         final MenuItem read = menu.findItem(R.id.read);
    394         read.setVisible(!showMarkUnread);
    395         final MenuItem unread = menu.findItem(R.id.unread);
    396         unread.setVisible(showMarkUnread);
    397         // We only ever show one of:
    398         // 1) remove folder
    399         // 2) archive
    400         final MenuItem removeFolder = menu.findItem(R.id.remove_folder);
    401         final MenuItem moveTo = menu.findItem(R.id.move_to);
    402         final MenuItem moveToInbox = menu.findItem(R.id.move_to_inbox);
    403         final boolean showRemoveFolder = mFolder != null && mFolder.isType(FolderType.DEFAULT)
    404                 && mFolder.supportsCapability(FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES)
    405                 && !mFolder.isProviderFolder()
    406                 && mAccount.supportsCapability(AccountCapabilities.ARCHIVE);
    407         final boolean showMoveTo = mFolder != null
    408                 && mFolder.supportsCapability(FolderCapabilities.ALLOWS_REMOVE_CONVERSATION);
    409         final boolean showMoveToInbox = mFolder != null
    410                 && mFolder.supportsCapability(FolderCapabilities.ALLOWS_MOVE_TO_INBOX);
    411         removeFolder.setVisible(showRemoveFolder);
    412         moveTo.setVisible(showMoveTo);
    413         moveToInbox.setVisible(showMoveToInbox);
    414 
    415         final MenuItem changeFolders = menu.findItem(R.id.change_folders);
    416         changeFolders.setVisible(mAccount.supportsCapability(
    417                 UIProvider.AccountCapabilities.MULTIPLE_FOLDERS_PER_CONV));
    418 
    419         if (mFolder != null && showRemoveFolder) {
    420             removeFolder.setTitle(mActivity.getActivityContext().getString(R.string.remove_folder,
    421                     mFolder.name));
    422         }
    423         final MenuItem archive = menu.findItem(R.id.archive);
    424         if (archive != null) {
    425             archive.setVisible(
    426                     mAccount.supportsCapability(UIProvider.AccountCapabilities.ARCHIVE) &&
    427                     mFolder.supportsCapability(FolderCapabilities.ARCHIVE));
    428         }
    429         final MenuItem spam = menu.findItem(R.id.report_spam);
    430         spam.setVisible(!showMarkNotSpam
    431                 && mAccount.supportsCapability(UIProvider.AccountCapabilities.REPORT_SPAM)
    432                 && mFolder.supportsCapability(FolderCapabilities.REPORT_SPAM));
    433         final MenuItem notSpam = menu.findItem(R.id.mark_not_spam);
    434         notSpam.setVisible(showMarkNotSpam &&
    435                 mAccount.supportsCapability(UIProvider.AccountCapabilities.REPORT_SPAM) &&
    436                 mFolder.supportsCapability(FolderCapabilities.MARK_NOT_SPAM));
    437         final MenuItem phishing = menu.findItem(R.id.report_phishing);
    438         phishing.setVisible(showMarkAsPhishing &&
    439                 mAccount.supportsCapability(UIProvider.AccountCapabilities.REPORT_PHISHING) &&
    440                 mFolder.supportsCapability(FolderCapabilities.REPORT_PHISHING));
    441 
    442         final MenuItem mute = menu.findItem(R.id.mute);
    443         if (mute != null) {
    444             mute.setVisible(mAccount.supportsCapability(UIProvider.AccountCapabilities.MUTE)
    445                     && (mFolder != null && mFolder.isInbox()));
    446         }
    447         final MenuItem markImportant = menu.findItem(R.id.mark_important);
    448         markImportant.setVisible(showMarkImportant
    449                 && mAccount.supportsCapability(UIProvider.AccountCapabilities.MARK_IMPORTANT));
    450         final MenuItem markNotImportant = menu.findItem(R.id.mark_not_important);
    451         markNotImportant.setVisible(!showMarkImportant
    452                 && mAccount.supportsCapability(UIProvider.AccountCapabilities.MARK_IMPORTANT));
    453 
    454         boolean shouldShowDiscardOutbox = mFolder != null && mFolder.isType(FolderType.OUTBOX);
    455         mDiscardOutboxMenuItem = menu.findItem(R.id.discard_outbox);
    456         if (mDiscardOutboxMenuItem != null) {
    457             mDiscardOutboxMenuItem.setVisible(shouldShowDiscardOutbox);
    458             mDiscardOutboxMenuItem.setEnabled(shouldEnableDiscardOutbox(conversations));
    459         }
    460         final boolean showDelete = mFolder != null && !mFolder.isType(FolderType.OUTBOX)
    461                 && mFolder.supportsCapability(UIProvider.FolderCapabilities.DELETE);
    462         final MenuItem trash = menu.findItem(R.id.delete);
    463         trash.setVisible(showDelete);
    464         // We only want to show the discard drafts menu item if we are not showing the delete menu
    465         // item, and the current folder is a draft folder and the account supports discarding
    466         // drafts for a conversation
    467         final boolean showDiscardDrafts = !showDelete && mFolder != null && mFolder.isDraft() &&
    468                 mAccount.supportsCapability(AccountCapabilities.DISCARD_CONVERSATION_DRAFTS);
    469         final MenuItem discardDrafts = menu.findItem(R.id.discard_drafts);
    470         if (discardDrafts != null) {
    471             discardDrafts.setVisible(showDiscardDrafts);
    472         }
    473 
    474         return true;
    475     }
    476 
    477     private boolean shouldEnableDiscardOutbox(Collection<Conversation> conversations) {
    478         boolean shouldEnableDiscardOutbox = true;
    479         // Java should be smart enough to realize that once showDiscardOutbox becomes false it can
    480         // just skip everything remaining in the for-loop..
    481         for (Conversation conv : conversations) {
    482             shouldEnableDiscardOutbox &=
    483                     conv.sendingState != UIProvider.ConversationSendingState.SENDING &&
    484                     conv.sendingState != UIProvider.ConversationSendingState.RETRYING;
    485         }
    486         return shouldEnableDiscardOutbox;
    487     }
    488 
    489     @Override
    490     public void onDestroyActionMode(ActionMode mode) {
    491         mActionMode = null;
    492         // The action mode may have been destroyed due to this menu being deactivated, in which
    493         // case resources need not be cleaned up. However, if it was destroyed while this menu is
    494         // active, that implies the user hit "Done" in the top right, and resources need cleaning.
    495         if (mActivated) {
    496             destroy();
    497             // Only commit destructive actions if the user actually pressed
    498             // done; otherwise, this was handled when we toggled conversation
    499             // selection state.
    500             mActivity.getListHandler().commitDestructiveActions(true);
    501         }
    502     }
    503 
    504     @Override
    505     public void onSetPopulated(ConversationSelectionSet set) {
    506         // Noop. This object can only exist while the set is non-empty.
    507     }
    508 
    509     @Override
    510     public void onSetEmpty() {
    511         LogUtils.d(LOG_TAG, "onSetEmpty called.");
    512         destroy();
    513     }
    514 
    515     @Override
    516     public void onSetChanged(ConversationSelectionSet set) {
    517         // If the set is empty, the menu buttons are invalid and most like the menu will be cleaned
    518         // up. Avoid making any changes to stop flickering ("Add Star" -> "Remove Star") just
    519         // before hiding the menu.
    520         if (set.isEmpty()) {
    521             return;
    522         }
    523 
    524         if (mFolder.isType(FolderType.OUTBOX) && mDiscardOutboxMenuItem != null) {
    525             mDiscardOutboxMenuItem.setEnabled(shouldEnableDiscardOutbox(set.values()));
    526         }
    527     }
    528 
    529     /**
    530      * Activates and shows this menu (essentially starting an {@link ActionMode}) if the selected
    531      * set is non-empty.
    532      */
    533     public void activate() {
    534         if (mSelectionSet.isEmpty()) {
    535             return;
    536         }
    537         mListController.onCabModeEntered();
    538         mActivated = true;
    539         if (mActionMode == null) {
    540             mActivity.startSupportActionMode(this);
    541         }
    542     }
    543 
    544     /**
    545      * De-activates and hides the menu (essentially disabling the {@link ActionMode}), but maintains
    546      * the selection conversation set, and internally updates state as necessary.
    547      */
    548     public void deactivate() {
    549         mListController.onCabModeExited();
    550 
    551         if (mActionMode != null) {
    552             mActivated = false;
    553             mActionMode.finish();
    554         }
    555     }
    556 
    557     @VisibleForTesting
    558     public boolean isActivated() {
    559         return mActivated;
    560     }
    561 
    562     /**
    563      * Destroys and cleans up the resources associated with this menu.
    564      */
    565     private void destroy() {
    566         deactivate();
    567         mSelectionSet.removeObserver(this);
    568         clearSelection();
    569         mUpdater.refreshConversationList();
    570         if (mAccountObserver != null) {
    571             mAccountObserver.unregisterAndDestroy();
    572             mAccountObserver = null;
    573         }
    574     }
    575 }
    576