Home | History | Annotate | Download | only in ui
      1 /*
      2  * Copyright (C) 2012 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.ui;
     19 
     20 import android.app.ActionBar;
     21 import android.app.SearchManager;
     22 import android.app.SearchableInfo;
     23 import android.content.ContentResolver;
     24 import android.content.Context;
     25 import android.content.res.Resources;
     26 import android.database.Cursor;
     27 import android.net.Uri;
     28 import android.os.AsyncTask;
     29 import android.os.Build;
     30 import android.os.Bundle;
     31 import android.os.Handler;
     32 import android.os.Message;
     33 import android.text.TextUtils;
     34 import android.util.AttributeSet;
     35 import android.view.Menu;
     36 import android.view.MenuItem;
     37 import android.view.View;
     38 import android.view.ViewConfiguration;
     39 import android.widget.LinearLayout;
     40 import android.widget.SearchView;
     41 import android.widget.SearchView.OnQueryTextListener;
     42 import android.widget.SearchView.OnSuggestionListener;
     43 import android.widget.TextView;
     44 
     45 import com.android.mail.ConversationListContext;
     46 import com.android.mail.R;
     47 import com.android.mail.preferences.MailPrefs;
     48 import com.android.mail.providers.Account;
     49 import com.android.mail.providers.AccountObserver;
     50 import com.android.mail.providers.Conversation;
     51 import com.android.mail.providers.Folder;
     52 import com.android.mail.providers.FolderObserver;
     53 import com.android.mail.providers.SearchRecentSuggestionsProvider;
     54 import com.android.mail.providers.UIProvider;
     55 import com.android.mail.providers.UIProvider.AccountCapabilities;
     56 import com.android.mail.providers.UIProvider.FolderCapabilities;
     57 import com.android.mail.providers.UIProvider.FolderType;
     58 import com.android.mail.utils.LogTag;
     59 import com.android.mail.utils.LogUtils;
     60 import com.android.mail.utils.Utils;
     61 
     62 /**
     63  * View to manage the various states of the Mail Action Bar.
     64  * <p>
     65  * This also happens to be the custom view we supply to ActionBar.
     66  *
     67  */
     68 public class MailActionBarView extends LinearLayout implements ViewMode.ModeChangeListener,
     69         OnQueryTextListener, OnSuggestionListener, MenuItem.OnActionExpandListener,
     70         View.OnClickListener {
     71 
     72     protected ActionBar mActionBar;
     73     protected ControllableActivity mActivity;
     74     protected ActivityController mController;
     75     /**
     76      * The current mode of the ActionBar. This references constants in {@link ViewMode}
     77      */
     78     private int mMode = ViewMode.UNKNOWN;
     79 
     80     private MenuItem mSearch;
     81     /**
     82      * The account currently being shown
     83      */
     84     private Account mAccount;
     85     /**
     86      * The folder currently being shown
     87      */
     88     private Folder mFolder;
     89 
     90     private SearchView mSearchWidget;
     91     private MenuItem mHelpItem;
     92     private MenuItem mSendFeedbackItem;
     93     private MenuItem mRefreshItem;
     94     private MenuItem mFolderSettingsItem;
     95     private MenuItem mEmptyTrashItem;
     96     private MenuItem mEmptySpamItem;
     97     private boolean mUseLegacyTitle;
     98     private View mLegacyTitleContainer;
     99     private TextView mLegacyTitle;
    100     private TextView mLegacySubTitle;
    101 
    102     /** True if the current device is a tablet, false otherwise. */
    103     protected final boolean mIsOnTablet;
    104     private Conversation mCurrentConversation;
    105 
    106     public static final String LOG_TAG = LogTag.getLogTag();
    107 
    108     private FolderObserver mFolderObserver;
    109 
    110     /** A handler that changes the subtitle when it receives a message. */
    111     private final class SubtitleHandler extends Handler {
    112         /** Message sent to display the account email address in the subtitle. */
    113         private static final int EMAIL = 0;
    114 
    115         @Override
    116         public void handleMessage(Message message) {
    117             assert (message.what == EMAIL);
    118             final String subtitleText;
    119             if (mAccount != null) {
    120                 // Display the account name (email address).
    121                 subtitleText = mAccount.name;
    122             } else {
    123                 subtitleText = null;
    124                 LogUtils.wtf(LOG_TAG, "MABV.handleMessage() has a null account!");
    125             }
    126             setSubtitle(subtitleText);
    127             super.handleMessage(message);
    128         }
    129     }
    130 
    131     /** Changes the subtitle to display the account name */
    132     private final SubtitleHandler mHandler = new SubtitleHandler();
    133     /** Unread count for the current folder. */
    134     private int mUnreadCount = 0;
    135     /** We show the email address after this delay: 5 seconds currently */
    136     private static final int ACCOUNT_DELAY_MS = 5 * 1000;
    137     /** At what point do we stop showing the unread count: 999+ currently */
    138     private final int UNREAD_LIMIT;
    139 
    140     /** Updates the resolver and tells it the most recent account. */
    141     private final class UpdateProvider extends AsyncTask<Bundle, Void, Void> {
    142         final Uri mAccount;
    143         final ContentResolver mResolver;
    144         public UpdateProvider(Uri account, ContentResolver resolver) {
    145             mAccount = account;
    146             mResolver = resolver;
    147         }
    148 
    149         @Override
    150         protected Void doInBackground(Bundle... params) {
    151             mResolver.call(mAccount, UIProvider.AccountCallMethods.SET_CURRENT_ACCOUNT,
    152                     mAccount.toString(), params[0]);
    153             return null;
    154         }
    155     }
    156 
    157     private final AccountObserver mAccountObserver = new AccountObserver() {
    158         @Override
    159         public void onChanged(Account newAccount) {
    160             updateAccount(newAccount);
    161         }
    162     };
    163 
    164     public MailActionBarView(Context context) {
    165         this(context, null);
    166     }
    167 
    168     public MailActionBarView(Context context, AttributeSet attrs) {
    169         this(context, attrs, 0);
    170     }
    171 
    172     public MailActionBarView(Context context, AttributeSet attrs, int defStyle) {
    173         super(context, attrs, defStyle);
    174         final Resources r = getResources();
    175         mIsOnTablet = Utils.useTabletUI(r);
    176         UNREAD_LIMIT = r.getInteger(R.integer.maxUnreadCount);
    177     }
    178 
    179     private void initializeTitleViews() {
    180         mLegacyTitleContainer = findViewById(R.id.legacy_title_container);
    181         if (mLegacyTitleContainer != null) {
    182             // Determine if this device is running on MR1.1 or later
    183             final boolean runningMR11OrLater = actionBarSupportsNewMethods(mActionBar);
    184             if (runningMR11OrLater || !mController.isDrawerEnabled()) {
    185                 // We don't need the legacy view, just hide it
    186                 mLegacyTitleContainer.setVisibility(View.GONE);
    187                 mUseLegacyTitle = false;
    188             } else {
    189                 mUseLegacyTitle = true;
    190                 // We need to show the legacy title/subtitle.  Set the click listener
    191                 mLegacyTitleContainer.setOnClickListener(this);
    192 
    193                 mLegacyTitle = (TextView)mLegacyTitleContainer.findViewById(R.id.legacy_title);
    194                 mLegacySubTitle =
    195                         (TextView)mLegacyTitleContainer.findViewById(R.id.legacy_subtitle);
    196             }
    197         }
    198     }
    199 
    200     public void expandSearch() {
    201         if (mSearch != null) {
    202             mSearch.expandActionView();
    203         }
    204     }
    205 
    206     /**
    207      * Close the search view if it is expanded.
    208      */
    209     public void collapseSearch() {
    210         if (mSearch != null) {
    211             mSearch.collapseActionView();
    212         }
    213     }
    214 
    215     /**
    216      * Get the search menu item.
    217      */
    218     protected MenuItem getSearch() {
    219         return mSearch;
    220     }
    221 
    222     public boolean onCreateOptionsMenu(Menu menu) {
    223         // If the mode is valid, then set the initial menu
    224         if (mMode == ViewMode.UNKNOWN) {
    225             return false;
    226         }
    227         mSearch = menu.findItem(R.id.search);
    228         if (mSearch != null) {
    229             mSearchWidget = (SearchView) mSearch.getActionView();
    230             mSearch.setOnActionExpandListener(this);
    231             SearchManager searchManager = (SearchManager) mActivity.getActivityContext()
    232                     .getSystemService(Context.SEARCH_SERVICE);
    233             if (searchManager != null && mSearchWidget != null) {
    234                 SearchableInfo info = searchManager.getSearchableInfo(mActivity.getComponentName());
    235                 mSearchWidget.setSearchableInfo(info);
    236                 mSearchWidget.setOnQueryTextListener(this);
    237                 mSearchWidget.setOnSuggestionListener(this);
    238                 mSearchWidget.setIconifiedByDefault(true);
    239             }
    240         }
    241         mHelpItem = menu.findItem(R.id.help_info_menu_item);
    242         mSendFeedbackItem = menu.findItem(R.id.feedback_menu_item);
    243         mRefreshItem = menu.findItem(R.id.refresh);
    244         mFolderSettingsItem = menu.findItem(R.id.folder_options);
    245         mEmptyTrashItem = menu.findItem(R.id.empty_trash);
    246         mEmptySpamItem = menu.findItem(R.id.empty_spam);
    247         return true;
    248     }
    249 
    250     public int getOptionsMenuId() {
    251         switch (mMode) {
    252             case ViewMode.UNKNOWN:
    253                 return R.menu.conversation_list_menu;
    254             case ViewMode.CONVERSATION:
    255                 return R.menu.conversation_actions;
    256             case ViewMode.CONVERSATION_LIST:
    257                 return R.menu.conversation_list_menu;
    258             case ViewMode.SEARCH_RESULTS_LIST:
    259                 return R.menu.conversation_list_search_results_actions;
    260             case ViewMode.SEARCH_RESULTS_CONVERSATION:
    261                 return R.menu.conversation_actions;
    262             case ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION:
    263                 return R.menu.wait_mode_actions;
    264         }
    265         LogUtils.wtf(LOG_TAG, "Menu requested for unknown view mode");
    266         return R.menu.conversation_list_menu;
    267     }
    268 
    269     public void initialize(ControllableActivity activity, ActivityController callback,
    270             ActionBar actionBar) {
    271         mActionBar = actionBar;
    272         mController = callback;
    273         mActivity = activity;
    274         initializeTitleViews();
    275 
    276         mFolderObserver = new FolderObserver() {
    277             @Override
    278             public void onChanged(Folder newFolder) {
    279                 onFolderUpdated(newFolder);
    280             }
    281         };
    282         // Return values are purposely discarded. Initialization happens quite early, and we don't
    283         // have a valid folder, or a valid list of accounts.
    284         mFolderObserver.initialize(mController);
    285         updateAccount(mAccountObserver.initialize(activity.getAccountController()));
    286     }
    287 
    288     private void updateAccount(Account account) {
    289         final boolean accountChanged = mAccount == null || !mAccount.uri.equals(account.uri);
    290         mAccount = account;
    291         if (mAccount != null && accountChanged) {
    292             final ContentResolver resolver = mActivity.getActivityContext().getContentResolver();
    293             final Bundle bundle = new Bundle(1);
    294             bundle.putParcelable(UIProvider.SetCurrentAccountColumns.ACCOUNT, account);
    295             final UpdateProvider updater = new UpdateProvider(mAccount.uri, resolver);
    296             updater.execute(bundle);
    297             setFolderAndAccount(false /* folderChanged */);
    298         }
    299     }
    300 
    301     /**
    302      * Called by the owner of the ActionBar to change the current folder.
    303      */
    304     public void setFolder(Folder folder) {
    305         mFolder = folder;
    306         setFolderAndAccount(true);
    307     }
    308 
    309     public void onDestroy() {
    310         if (mFolderObserver != null) {
    311             mFolderObserver.unregisterAndDestroy();
    312             mFolderObserver = null;
    313         }
    314         mAccountObserver.unregisterAndDestroy();
    315         mHandler.removeMessages(SubtitleHandler.EMAIL);
    316     }
    317 
    318     @Override
    319     public void onViewModeChanged(int newMode) {
    320         mMode = newMode;
    321         mActivity.invalidateOptionsMenu();
    322         mHandler.removeMessages(SubtitleHandler.EMAIL);
    323         // Check if we are either on a phone, or in Conversation mode on tablet. For these, the
    324         // recent folders is enabled.
    325         switch (mMode) {
    326             case ViewMode.UNKNOWN:
    327                 break;
    328             case ViewMode.CONVERSATION_LIST:
    329                 showNavList();
    330                 break;
    331             case ViewMode.SEARCH_RESULTS_CONVERSATION:
    332                 mActionBar.setDisplayHomeAsUpEnabled(true);
    333                 setEmptyMode();
    334                 break;
    335             case ViewMode.CONVERSATION:
    336             case ViewMode.AD:
    337                 closeSearchField();
    338                 mActionBar.setDisplayHomeAsUpEnabled(true);
    339                 setEmptyMode();
    340                 break;
    341             case ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION:
    342                 // We want the user to be able to switch accounts while waiting for an account
    343                 // to sync.
    344                 showNavList();
    345                 break;
    346         }
    347     }
    348 
    349     /**
    350      * Close the search query entry field to avoid keyboard events, and to restore the actionbar
    351      * to non-search mode.
    352      */
    353     private void closeSearchField() {
    354         if (mSearch == null) {
    355             return;
    356         }
    357         mSearch.collapseActionView();
    358     }
    359 
    360     protected int getMode() {
    361         return mMode;
    362     }
    363 
    364     public boolean onPrepareOptionsMenu(Menu menu) {
    365         // We start out with every option enabled. Based on the current view, we disable actions
    366         // that are possible.
    367         LogUtils.d(LOG_TAG, "ActionBarView.onPrepareOptionsMenu().");
    368 
    369         if (mHelpItem != null) {
    370             mHelpItem.setVisible(mAccount != null
    371                     && mAccount.supportsCapability(AccountCapabilities.HELP_CONTENT));
    372         }
    373         if (mSendFeedbackItem != null) {
    374             mSendFeedbackItem.setVisible(mAccount != null
    375                     && mAccount.supportsCapability(AccountCapabilities.SEND_FEEDBACK));
    376         }
    377         if (mController.shouldHideMenuItems()) {
    378             // Shortcut: hide all remaining menu items if the drawer is shown
    379             final int size = menu.size();
    380 
    381             for (int i = 0; i < size; i++) {
    382                 final MenuItem item = menu.getItem(i);
    383                 final int id = item.getItemId();
    384                 if (id != R.id.settings
    385                         && id != R.id.feedback_menu_item
    386                         && id != R.id.help_info_menu_item) {
    387                     item.setVisible(false);
    388                 }
    389             }
    390             return false;
    391         }
    392 
    393         if (mRefreshItem != null) {
    394             // See b/11158759
    395             // Disable refresh on drafts folders.
    396             mRefreshItem.setVisible(mFolder != null &&
    397                     !mFolder.isDraft() &&
    398                     !mFolder.supportsCapability(FolderCapabilities.IS_VIRTUAL));
    399         }
    400 
    401         if (mFolderSettingsItem != null) {
    402             mFolderSettingsItem.setVisible(mFolder != null
    403                     && mFolder.supportsCapability(FolderCapabilities.SUPPORTS_SETTINGS));
    404         }
    405         if (mEmptyTrashItem != null) {
    406             mEmptyTrashItem.setVisible(mAccount != null && mFolder != null
    407                     && mAccount.supportsCapability(AccountCapabilities.EMPTY_TRASH)
    408                     && mFolder.isTrash() && mFolder.totalCount > 0);
    409         }
    410         if (mEmptySpamItem != null) {
    411             mEmptySpamItem.setVisible(mAccount != null && mFolder != null
    412                     && mAccount.supportsCapability(AccountCapabilities.EMPTY_SPAM)
    413                     && mFolder.isType(FolderType.SPAM) && mFolder.totalCount > 0);
    414         }
    415 
    416         switch (mMode) {
    417             case ViewMode.CONVERSATION:
    418             case ViewMode.SEARCH_RESULTS_CONVERSATION:
    419                 // We update the ActionBar options when we are entering conversation view because
    420                 // waiting for the AbstractConversationViewFragment to do it causes duplicate icons
    421                 // to show up during the time between the conversation is selected and the fragment
    422                 // is added.
    423                 setConversationModeOptions(menu);
    424                 // We want to use the user's preferred menu items here
    425                 final Resources resources = getResources();
    426                 final int maxItems = resources.getInteger(R.integer.actionbar_max_items);
    427                 final int hiddenItems = resources.getInteger(
    428                         R.integer.actionbar_hidden_non_cab_items_no_physical_button);
    429                 final int totalItems = maxItems
    430                         - (ViewConfiguration.get(getContext()).hasPermanentMenuKey()
    431                                 ? 0 : hiddenItems);
    432                 reorderMenu(getContext(), mAccount, menu, totalItems);
    433                 break;
    434             case ViewMode.CONVERSATION_LIST:
    435                 // Show compose and search based on the account
    436                 // The only option that needs to be disabled is search
    437                 Utils.setMenuItemVisibility(menu, R.id.search,
    438                         mAccount.supportsCapability(AccountCapabilities.FOLDER_SERVER_SEARCH));
    439                 break;
    440             case ViewMode.SEARCH_RESULTS_LIST:
    441                 // Hide compose and search
    442                 Utils.setMenuItemVisibility(menu, R.id.compose, false);
    443                 Utils.setMenuItemVisibility(menu, R.id.search, false);
    444                 break;
    445         }
    446 
    447         return false;
    448     }
    449 
    450     /**
    451      * Reorders the specified {@link Menu}, taking into account the user's Archive/Delete
    452      * preference.
    453      */
    454     public static void reorderMenu(final Context context, final Account account, final Menu menu,
    455             final int maxItems) {
    456         final String removalAction = MailPrefs.get(context).getRemovalAction(
    457                 account.supportsCapability(AccountCapabilities.ARCHIVE));
    458         final boolean showArchive = MailPrefs.RemovalActions.ARCHIVE.equals(removalAction) ||
    459                 MailPrefs.RemovalActions.ARCHIVE_AND_DELETE.equals(removalAction);
    460         final boolean showDelete = MailPrefs.RemovalActions.DELETE.equals(removalAction) ||
    461                 MailPrefs.RemovalActions.ARCHIVE_AND_DELETE.equals(removalAction);
    462 
    463         // Do a first pass to extract necessary information on what is safe to display
    464         boolean archiveVisibleEnabled = false;
    465         boolean deleteVisibleEnabled = false;
    466         for (int i = 0; i < menu.size(); i++) {
    467             final MenuItem menuItem = menu.getItem(i);
    468             final int itemId = menuItem.getItemId();
    469             final boolean visible = menuItem.isVisible();
    470             final boolean enabled = menuItem.isEnabled();
    471 
    472             if (itemId == R.id.archive || itemId == R.id.remove_folder) {
    473                 archiveVisibleEnabled |= (visible & enabled);
    474             } else if (itemId == R.id.delete || itemId == R.id.discard_drafts) {
    475                 deleteVisibleEnabled |= (visible & enabled);
    476             }
    477         }
    478 
    479         int actionItems = 0;
    480 
    481         for (int i = 0; i < menu.size(); i++) {
    482             final MenuItem menuItem = menu.getItem(i);
    483             final int itemId = menuItem.getItemId();
    484 
    485             // We only want to promote it if it's visible and has an icon
    486             if (menuItem.isVisible() && menuItem.getIcon() != null) {
    487                 if (itemId == R.id.archive || itemId == R.id.remove_folder) {
    488                     /*
    489                      * If this is disabled, and we want to show both archive and delete, we will
    490                      * hide archive (rather than showing it disabled), and take up one of our
    491                      * spaces. If we only want to show archive, we'll hide it, but not take up
    492                      * a space.
    493                      */
    494                     if (!menuItem.isEnabled() && showArchive) {
    495                         menuItem.setVisible(false);
    496 
    497                         if (showDelete) {
    498                             actionItems++;
    499                         }
    500                     } else {
    501                         /*
    502                          * We show this if the following are all true:
    503                          * 1. The user wants to display archive, or delete is not visible
    504                          * 2. We have room for it
    505                          */
    506                         if ((showArchive || !deleteVisibleEnabled) && actionItems < maxItems) {
    507                             menuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
    508                             actionItems++;
    509                         }
    510                     }
    511                 } else if (itemId == R.id.delete || itemId == R.id.discard_drafts) {
    512                     /*
    513                      * We show this if the following are all true:
    514                      * 1. The user wants to display delete, or archive is not visible
    515                      * 2. We have room for it
    516                      */
    517                     if ((showDelete || !archiveVisibleEnabled) && actionItems < maxItems) {
    518                         menuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
    519                         actionItems++;
    520                     }
    521                 } else if (itemId == R.id.change_folders) {
    522                     final boolean showChangeFolder = account
    523                             .supportsCapability(AccountCapabilities.MULTIPLE_FOLDERS_PER_CONV);
    524                     menuItem.setVisible(showChangeFolder);
    525 
    526                     if (showChangeFolder && actionItems < maxItems) {
    527                         menuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
    528                         actionItems++;
    529                     }
    530                 } else if (itemId == R.id.search) {
    531                     menuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS
    532                             | MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW);
    533                     actionItems++;
    534                 } else {
    535                     if (actionItems < maxItems) {
    536                         menuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
    537                         actionItems++;
    538                     }
    539                 }
    540             }
    541         }
    542     }
    543 
    544     /**
    545      * Put the ActionBar in List navigation mode.
    546      */
    547     private void showNavList() {
    548         setTitleModeFlags(getActionBarTitleModeFlag());
    549         setFolderAndAccount(false);
    550     }
    551 
    552     private void setSubtitle(CharSequence subtitle) {
    553         if (!TextUtils.equals(subtitle, mActionBar.getSubtitle())) {
    554             mActionBar.setSubtitle(subtitle);
    555         }
    556         if (mLegacySubTitle != null) {
    557             mLegacySubTitle.setText(subtitle);
    558         }
    559     }
    560 
    561     private void setTitle(CharSequence title) {
    562         if (!TextUtils.equals(title, mActionBar.getTitle())) {
    563             mActionBar.setTitle(title);
    564         }
    565         if (mLegacyTitle != null) {
    566             mLegacyTitle.setText(title);
    567         }
    568     }
    569 
    570     private int getActionBarTitleModeFlag() {
    571         return mUseLegacyTitle ? ActionBar.DISPLAY_SHOW_CUSTOM : ActionBar.DISPLAY_SHOW_TITLE;
    572     }
    573 
    574     /**
    575      * Set the actionbar mode to empty: no title, no subtitle, no custom view.
    576      */
    577     protected void setEmptyMode() {
    578         // Disable title/subtitle and the custom view by setting the bitmask to all off.
    579         setTitleModeFlags(0);
    580     }
    581 
    582     /**
    583      * Removes the back button from being shown
    584      */
    585     public void removeBackButton() {
    586         if (mActionBar == null) {
    587             return;
    588         }
    589         // Remove the back button but continue showing an icon.
    590         final int mask = ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME;
    591         mActionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_HOME, mask);
    592         mActivity.getActionBar().setHomeButtonEnabled(false);
    593     }
    594 
    595     public void setBackButton() {
    596         if (mActionBar == null) {
    597             return;
    598         }
    599         // Show home as up, and show an icon.
    600         final int mask = ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME;
    601         mActionBar.setDisplayOptions(mask, mask);
    602         mActivity.getActionBar().setHomeButtonEnabled(true);
    603     }
    604 
    605     @Override
    606     public boolean onQueryTextSubmit(String query) {
    607         if (mSearch != null) {
    608             mSearch.collapseActionView();
    609             mSearchWidget.setQuery("", false);
    610         }
    611         mController.executeSearch(query.trim());
    612         return true;
    613     }
    614 
    615     @Override
    616     public boolean onQueryTextChange(String newText) {
    617         return false;
    618     }
    619 
    620     // Next two methods are called when search suggestions are clicked.
    621     @Override
    622     public boolean onSuggestionSelect(int position) {
    623         return onSuggestionClick(position);
    624     }
    625 
    626     @Override
    627     public boolean onSuggestionClick(int position) {
    628         final Cursor c = mSearchWidget.getSuggestionsAdapter().getCursor();
    629         final boolean haveValidQuery = (c != null) && c.moveToPosition(position);
    630         if (!haveValidQuery) {
    631             LogUtils.d(LOG_TAG, "onSuggestionClick: Couldn't get a search query");
    632             // We haven't handled this query, but the default behavior will
    633             // leave EXTRA_ACCOUNT un-populated, leading to a crash. So claim
    634             // that we have handled the event.
    635             return true;
    636         }
    637         collapseSearch();
    638         // what is in the text field
    639         String queryText = mSearchWidget.getQuery().toString();
    640         // What the suggested query is
    641         String query = c.getString(c.getColumnIndex(SearchManager.SUGGEST_COLUMN_QUERY));
    642         // If the text the user typed in is a prefix of what is in the search
    643         // widget suggestion query, just take the search widget suggestion
    644         // query. Otherwise, it is a suffix and we want to remove matching
    645         // prefix portions.
    646         if (!TextUtils.isEmpty(queryText) && query.indexOf(queryText) != 0) {
    647             final int queryTokenIndex = queryText
    648                     .lastIndexOf(SearchRecentSuggestionsProvider.QUERY_TOKEN_SEPARATOR);
    649             if (queryTokenIndex > -1) {
    650                 queryText = queryText.substring(0, queryTokenIndex);
    651             }
    652             // Since we auto-complete on each token in a query, if the query the
    653             // user typed up until the last token is a substring of the
    654             // suggestion they click, make sure we don't double include the
    655             // query text. For example:
    656             // user types john, that matches john palo alto
    657             // User types john p, that matches john john palo alto
    658             // Remove the first john
    659             // Only do this if we have multiple query tokens.
    660             if (queryTokenIndex > -1 && !TextUtils.isEmpty(query) && query.contains(queryText)
    661                     && queryText.length() < query.length()) {
    662                 int start = query.indexOf(queryText);
    663                 query = query.substring(0, start) + query.substring(start + queryText.length());
    664             }
    665         }
    666         mController.executeSearch(query.trim());
    667         return true;
    668     }
    669 
    670     /**
    671      * Uses the current state to update the current folder {@link #mFolder} and the current
    672      * account {@link #mAccount} shown in the actionbar. Also updates the actionbar subtitle to
    673      * momentarily display the unread count if it has changed.
    674      * @param folderChanged true if folder changed in terms of URI
    675      */
    676     private void setFolderAndAccount(final boolean folderChanged) {
    677         // Very little can be done if the actionbar or activity is null.
    678         if (mActionBar == null || mActivity == null) {
    679             return;
    680         }
    681         if (ViewMode.isWaitingForSync(mMode)) {
    682             // Account is not synced: clear title and update the subtitle.
    683             setTitle("");
    684             removeUnreadCount(true);
    685             return;
    686         }
    687         // Check if we should be changing the actionbar at all, and back off if not.
    688         final boolean isShowingFolder = mIsOnTablet || ViewMode.isListMode(mMode);
    689         if (!isShowingFolder) {
    690             // It isn't necessary to set the title in this case, as the title view will
    691             // be hidden
    692             return;
    693         }
    694         if (mFolder == null) {
    695             // Clear the action bar title.  We don't want the app name to be shown while
    696             // waiting for the folder query to finish
    697             setTitle("");
    698             return;
    699         }
    700         setTitle(mFolder.name);
    701 
    702         final int folderUnreadCount = mFolder.isUnreadCountHidden() ? 0 : mFolder.unreadCount;
    703         // The user shouldn't see "999+ unread messages", and then a short while later: "999+
    704         // unread messages". So we set our unread count just past the limit. This way we can
    705         // change the subtitle the first time around but not for subsequent changes as far as the
    706         // unread count remains over the limit.
    707         final int toDisplay = (folderUnreadCount > UNREAD_LIMIT)
    708                 ? (UNREAD_LIMIT + 1) : folderUnreadCount;
    709         if ((mUnreadCount != toDisplay || folderChanged) && toDisplay != 0) {
    710             setSubtitle(Utils.getUnreadMessageString(mActivity.getApplicationContext(), toDisplay));
    711         }
    712         // Schedule a removal of unread count for the future, if there isn't one already. If the
    713         // unread count dropped to zero, remove it and show the account name right away.
    714         removeUnreadCount(toDisplay == 0);
    715         // Remember the new value for the next run
    716         mUnreadCount = toDisplay;
    717     }
    718 
    719     /**
    720      * Remove the unread count and show the account name, if required.
    721      * @param now true if you want the change to happen immediately. False if you want to enforce
    722      *            it happens later.
    723      */
    724     private void removeUnreadCount(boolean now) {
    725         if (now) {
    726             // Remove all previous messages which might change the subtitle
    727             mHandler.removeMessages(SubtitleHandler.EMAIL);
    728             // Update the subtitle: clear it or show account name.
    729             mHandler.sendEmptyMessage(SubtitleHandler.EMAIL);
    730         } else {
    731             if (!mHandler.hasMessages(SubtitleHandler.EMAIL)) {
    732                 // In a short while, show the account name in its place.
    733                 mHandler.sendEmptyMessageDelayed(SubtitleHandler.EMAIL, ACCOUNT_DELAY_MS);
    734             }
    735         }
    736     }
    737 
    738     /**
    739      * Notify that the folder has changed.
    740      */
    741     public void onFolderUpdated(Folder folder) {
    742         if (folder == null) {
    743             return;
    744         }
    745         /** True if we are changing folders. */
    746         final boolean changingFolders = (mFolder == null || !mFolder.equals(folder));
    747         mFolder = folder;
    748         setFolderAndAccount(changingFolders);
    749         final ConversationListContext listContext = mController == null ? null :
    750                 mController.getCurrentListContext();
    751         if (changingFolders && !ConversationListContext.isSearchResult(listContext)) {
    752             closeSearchField();
    753         }
    754     }
    755 
    756     @Override
    757     public boolean onMenuItemActionExpand(MenuItem item) {
    758         // Do nothing. Required as part of the interface, we ar only interested in
    759         // onMenuItemActionCollapse(MenuItem).
    760         // Have to return true here. Unlike other callbacks, the return value here is whether
    761         // we want to suppress the action (rather than consume the action). We don't want to
    762         // suppress the action.
    763         return true;
    764     }
    765 
    766     @Override
    767     public boolean onMenuItemActionCollapse(MenuItem item) {
    768         // Work around b/6664203 by manually forcing this view to be VISIBLE
    769         // upon ActionView collapse. DISPLAY_SHOW_CUSTOM will still control its final
    770         // visibility.
    771         setVisibility(VISIBLE);
    772         // Have to return true here. Unlike other callbacks, the return value
    773         // here is whether we want to suppress the action (rather than consume the action). We
    774         // don't want to suppress the action.
    775         return true;
    776     }
    777 
    778     /**
    779      * Sets the actionbar mode: Pass it an integer which contains each of these values, perhaps
    780      * OR'd together: {@link ActionBar#DISPLAY_SHOW_CUSTOM} and
    781      * {@link ActionBar#DISPLAY_SHOW_TITLE}. To disable all, pass a zero.
    782      * @param enabledFlags
    783      */
    784     private void setTitleModeFlags(int enabledFlags) {
    785         final int mask = ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_CUSTOM;
    786         mActionBar.setDisplayOptions(enabledFlags, mask);
    787     }
    788 
    789     public void setCurrentConversation(Conversation conversation) {
    790         mCurrentConversation = conversation;
    791     }
    792 
    793     //We need to do this here instead of in the fragment
    794     public void setConversationModeOptions(Menu menu) {
    795         if (mCurrentConversation == null) {
    796             return;
    797         }
    798         final boolean showMarkImportant = !mCurrentConversation.isImportant();
    799         Utils.setMenuItemVisibility(menu, R.id.mark_important, showMarkImportant
    800                 && mAccount.supportsCapability(UIProvider.AccountCapabilities.MARK_IMPORTANT));
    801         Utils.setMenuItemVisibility(menu, R.id.mark_not_important, !showMarkImportant
    802                 && mAccount.supportsCapability(UIProvider.AccountCapabilities.MARK_IMPORTANT));
    803         final boolean showDelete = mFolder != null &&
    804                 mFolder.supportsCapability(UIProvider.FolderCapabilities.DELETE);
    805         Utils.setMenuItemVisibility(menu, R.id.delete, showDelete);
    806         // We only want to show the discard drafts menu item if we are not showing the delete menu
    807         // item, and the current folder is a draft folder and the account supports discarding
    808         // drafts for a conversation
    809         final boolean showDiscardDrafts = !showDelete && mFolder != null && mFolder.isDraft() &&
    810                 mAccount.supportsCapability(AccountCapabilities.DISCARD_CONVERSATION_DRAFTS);
    811         Utils.setMenuItemVisibility(menu, R.id.discard_drafts, showDiscardDrafts);
    812         final boolean archiveVisible = mAccount.supportsCapability(AccountCapabilities.ARCHIVE)
    813                 && mFolder != null && mFolder.supportsCapability(FolderCapabilities.ARCHIVE)
    814                 && !mFolder.isTrash();
    815         Utils.setMenuItemVisibility(menu, R.id.archive, archiveVisible);
    816         Utils.setMenuItemVisibility(menu, R.id.remove_folder, !archiveVisible && mFolder != null
    817                 && mFolder.supportsCapability(FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES)
    818                 && !mFolder.isProviderFolder()
    819                 && mAccount.supportsCapability(AccountCapabilities.ARCHIVE));
    820         Utils.setMenuItemVisibility(menu, R.id.move_to, mFolder != null
    821                 && mFolder.supportsCapability(FolderCapabilities.ALLOWS_REMOVE_CONVERSATION));
    822         Utils.setMenuItemVisibility(menu, R.id.move_to_inbox, mFolder != null
    823                 && mFolder.supportsCapability(FolderCapabilities.ALLOWS_MOVE_TO_INBOX));
    824 
    825         final MenuItem removeFolder = menu.findItem(R.id.remove_folder);
    826         if (mFolder != null && removeFolder != null) {
    827             removeFolder.setTitle(mActivity.getApplicationContext().getString(
    828                     R.string.remove_folder, mFolder.name));
    829         }
    830         Utils.setMenuItemVisibility(menu, R.id.report_spam,
    831                 mAccount.supportsCapability(AccountCapabilities.REPORT_SPAM) && mFolder != null
    832                         && mFolder.supportsCapability(FolderCapabilities.REPORT_SPAM)
    833                         && !mCurrentConversation.spam);
    834         Utils.setMenuItemVisibility(menu, R.id.mark_not_spam,
    835                 mAccount.supportsCapability(AccountCapabilities.REPORT_SPAM) && mFolder != null
    836                         && mFolder.supportsCapability(FolderCapabilities.MARK_NOT_SPAM)
    837                         && mCurrentConversation.spam);
    838         Utils.setMenuItemVisibility(menu, R.id.report_phishing,
    839                 mAccount.supportsCapability(AccountCapabilities.REPORT_PHISHING) && mFolder != null
    840                         && mFolder.supportsCapability(FolderCapabilities.REPORT_PHISHING)
    841                         && !mCurrentConversation.phishing);
    842         Utils.setMenuItemVisibility(menu, R.id.mute,
    843                         mAccount.supportsCapability(AccountCapabilities.MUTE) && mFolder != null
    844                         && mFolder.supportsCapability(FolderCapabilities.DESTRUCTIVE_MUTE)
    845                         && !mCurrentConversation.muted);
    846     }
    847 
    848     private static boolean actionBarSupportsNewMethods(ActionBar bar) {
    849         // TODO(pwestbro) switch this to
    850         // (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) when we switch to the
    851         // latest SDK
    852         if (Build.VERSION.SDK_INT > 17) {
    853             return true;
    854         }
    855         if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN) {
    856             return false;
    857         }
    858         boolean supportsNewApi = false;
    859         try {
    860             if (bar != null) {
    861                 supportsNewApi = (ActionBar.class.getField("DISPLAY_TITLE_MULTIPLE_LINES") != null);
    862             }
    863         } catch (NoSuchFieldException e) {
    864             // stay false
    865         }
    866         return supportsNewApi;
    867     }
    868 
    869     @Override
    870     public void onClick (View v) {
    871         if (v.getId() == R.id.legacy_title_container) {
    872             mController.onUpPressed();
    873         }
    874     }
    875 }
    876