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.content.ContentResolver;
     21 import android.content.Context;
     22 import android.net.Uri;
     23 import android.os.AsyncTask;
     24 import android.os.Bundle;
     25 import android.support.v7.app.ActionBar;
     26 import android.text.TextUtils;
     27 import android.view.Menu;
     28 import android.view.MenuItem;
     29 
     30 import com.android.mail.R;
     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.FolderObserver;
     36 import com.android.mail.providers.UIProvider;
     37 import com.android.mail.providers.UIProvider.AccountCapabilities;
     38 import com.android.mail.providers.UIProvider.FolderCapabilities;
     39 import com.android.mail.providers.UIProvider.FolderType;
     40 import com.android.mail.utils.LogTag;
     41 import com.android.mail.utils.LogUtils;
     42 import com.android.mail.utils.Utils;
     43 
     44 /**
     45  * Controller to manage the various states of the {@link android.app.ActionBar}.
     46  */
     47 public class ActionBarController implements ViewMode.ModeChangeListener {
     48 
     49     private final Context mContext;
     50 
     51     protected ActionBar mActionBar;
     52     protected ControllableActivity mActivity;
     53     protected ActivityController mController;
     54     /**
     55      * The current mode of the ActionBar and Activity
     56      */
     57     private ViewMode mViewModeController;
     58 
     59     /**
     60      * The account currently being shown
     61      */
     62     private Account mAccount;
     63     /**
     64      * The folder currently being shown
     65      */
     66     private Folder mFolder;
     67 
     68     private MenuItem mEmptyTrashItem;
     69     private MenuItem mEmptySpamItem;
     70 
     71     /** True if the current device is a tablet, false otherwise. */
     72     protected final boolean mIsOnTablet;
     73     private Conversation mCurrentConversation;
     74 
     75     public static final String LOG_TAG = LogTag.getLogTag();
     76 
     77     private FolderObserver mFolderObserver;
     78 
     79     /** Updates the resolver and tells it the most recent account. */
     80     private final class UpdateProvider extends AsyncTask<Bundle, Void, Void> {
     81         final Uri mAccount;
     82         final ContentResolver mResolver;
     83         public UpdateProvider(Uri account, ContentResolver resolver) {
     84             mAccount = account;
     85             mResolver = resolver;
     86         }
     87 
     88         @Override
     89         protected Void doInBackground(Bundle... params) {
     90             mResolver.call(mAccount, UIProvider.AccountCallMethods.SET_CURRENT_ACCOUNT,
     91                     mAccount.toString(), params[0]);
     92             return null;
     93         }
     94     }
     95 
     96     private final AccountObserver mAccountObserver = new AccountObserver() {
     97         @Override
     98         public void onChanged(Account newAccount) {
     99             updateAccount(newAccount);
    100         }
    101     };
    102 
    103     public ActionBarController(Context context) {
    104         mContext = context;
    105         mIsOnTablet = Utils.useTabletUI(context.getResources());
    106     }
    107 
    108     public boolean onCreateOptionsMenu(Menu menu) {
    109         mEmptyTrashItem = menu.findItem(R.id.empty_trash);
    110         mEmptySpamItem = menu.findItem(R.id.empty_spam);
    111 
    112         // the menu should be displayed if the mode is known
    113         return getMode() != ViewMode.UNKNOWN;
    114     }
    115 
    116     public int getOptionsMenuId() {
    117         switch (getMode()) {
    118             case ViewMode.UNKNOWN:
    119                 return R.menu.conversation_list_menu;
    120             case ViewMode.CONVERSATION:
    121                 return R.menu.conversation_actions;
    122             case ViewMode.CONVERSATION_LIST:
    123                 return R.menu.conversation_list_menu;
    124             case ViewMode.SEARCH_RESULTS_LIST:
    125                 return R.menu.conversation_list_search_results_actions;
    126             case ViewMode.SEARCH_RESULTS_CONVERSATION:
    127                 return R.menu.conversation_actions;
    128             case ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION:
    129                 return R.menu.wait_mode_actions;
    130         }
    131         LogUtils.wtf(LOG_TAG, "Menu requested for unknown view mode");
    132         return R.menu.conversation_list_menu;
    133     }
    134 
    135     public void initialize(ControllableActivity activity, ActivityController callback,
    136             ActionBar actionBar) {
    137         mActionBar = actionBar;
    138         mController = callback;
    139         mActivity = activity;
    140 
    141         mFolderObserver = new FolderObserver() {
    142             @Override
    143             public void onChanged(Folder newFolder) {
    144                 onFolderUpdated(newFolder);
    145             }
    146         };
    147         // Return values are purposely discarded. Initialization happens quite early, and we don't
    148         // have a valid folder, or a valid list of accounts.
    149         mFolderObserver.initialize(mController);
    150         updateAccount(mAccountObserver.initialize(activity.getAccountController()));
    151     }
    152 
    153     private void updateAccount(Account account) {
    154         final boolean accountChanged = mAccount == null || !mAccount.uri.equals(account.uri);
    155         mAccount = account;
    156         if (mAccount != null && accountChanged) {
    157             final ContentResolver resolver = mActivity.getActivityContext().getContentResolver();
    158             final Bundle bundle = new Bundle(1);
    159             bundle.putParcelable(UIProvider.SetCurrentAccountColumns.ACCOUNT, account);
    160             final UpdateProvider updater = new UpdateProvider(mAccount.uri, resolver);
    161             updater.execute(bundle);
    162             setFolderAndAccount();
    163         }
    164     }
    165 
    166     /**
    167      * Called by the owner of the ActionBar to change the current folder.
    168      */
    169     public void setFolder(Folder folder) {
    170         mFolder = folder;
    171         setFolderAndAccount();
    172     }
    173 
    174     public void onDestroy() {
    175         if (mFolderObserver != null) {
    176             mFolderObserver.unregisterAndDestroy();
    177             mFolderObserver = null;
    178         }
    179         mAccountObserver.unregisterAndDestroy();
    180     }
    181 
    182     @Override
    183     public void onViewModeChanged(int newMode) {
    184         final boolean mIsTabletLandscape =
    185                 mContext.getResources().getBoolean(R.bool.is_tablet_landscape);
    186 
    187         mActivity.supportInvalidateOptionsMenu();
    188         // Check if we are either on a phone, or in Conversation mode on tablet. For these, the
    189         // recent folders is enabled.
    190         switch (getMode()) {
    191             case ViewMode.UNKNOWN:
    192                 break;
    193             case ViewMode.CONVERSATION_LIST:
    194                 showNavList();
    195                 break;
    196             case ViewMode.SEARCH_RESULTS_CONVERSATION:
    197                 mActionBar.setDisplayHomeAsUpEnabled(true);
    198                 setEmptyMode();
    199                 break;
    200             case ViewMode.CONVERSATION:
    201                 // If on tablet landscape, show current folder instead of emptying the action bar
    202                 if (mIsTabletLandscape) {
    203                     mActionBar.setDisplayHomeAsUpEnabled(true);
    204                     showNavList();
    205                     break;
    206                 }
    207                 // Otherwise, fall through to default behavior, shared with Ads ViewMode.
    208             case ViewMode.AD:
    209                 mActionBar.setDisplayHomeAsUpEnabled(true);
    210                 setEmptyMode();
    211                 break;
    212             case ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION:
    213                 // We want the user to be able to switch accounts while waiting for an account
    214                 // to sync.
    215                 showNavList();
    216                 break;
    217         }
    218     }
    219 
    220     protected int getMode() {
    221         if (mViewModeController != null) {
    222             return mViewModeController.getMode();
    223         } else {
    224             return ViewMode.UNKNOWN;
    225         }
    226     }
    227 
    228     /**
    229      * Helper function to ensure that the menu items that are prone to variable changes and race
    230      * conditions are properly set to the correct visibility
    231      */
    232     public void validateVolatileMenuOptionVisibility() {
    233         Utils.setMenuItemPresent(mEmptyTrashItem, mAccount != null && mFolder != null
    234                 && mAccount.supportsCapability(AccountCapabilities.EMPTY_TRASH)
    235                 && mFolder.isTrash() && mFolder.totalCount > 0
    236                 && (mController.getConversationListCursor() == null
    237                 || mController.getConversationListCursor().getCount() > 0));
    238         Utils.setMenuItemPresent(mEmptySpamItem, mAccount != null && mFolder != null
    239                 && mAccount.supportsCapability(AccountCapabilities.EMPTY_SPAM)
    240                 && mFolder.isType(FolderType.SPAM) && mFolder.totalCount > 0
    241                 && (mController.getConversationListCursor() == null
    242                 || mController.getConversationListCursor().getCount() > 0));
    243     }
    244 
    245     public void onPrepareOptionsMenu(Menu menu) {
    246         menu.setQwertyMode(true);
    247         // We start out with every option enabled. Based on the current view, we disable actions
    248         // that are possible.
    249         LogUtils.d(LOG_TAG, "ActionBarView.onPrepareOptionsMenu().");
    250 
    251         if (mController.shouldHideMenuItems()) {
    252             // Shortcut: hide all menu items if the drawer is shown
    253             final int size = menu.size();
    254 
    255             for (int i = 0; i < size; i++) {
    256                 final MenuItem item = menu.getItem(i);
    257                 item.setVisible(false);
    258             }
    259             return;
    260         }
    261         validateVolatileMenuOptionVisibility();
    262 
    263         switch (getMode()) {
    264             case ViewMode.CONVERSATION:
    265             case ViewMode.SEARCH_RESULTS_CONVERSATION:
    266                 // We update the ActionBar options when we are entering conversation view because
    267                 // waiting for the AbstractConversationViewFragment to do it causes duplicate icons
    268                 // to show up during the time between the conversation is selected and the fragment
    269                 // is added.
    270                 setConversationModeOptions(menu);
    271                 break;
    272             case ViewMode.CONVERSATION_LIST:
    273             case ViewMode.SEARCH_RESULTS_LIST:
    274                 // The search menu item should only be visible for non-tablet devices
    275                 Utils.setMenuItemPresent(menu, R.id.search,
    276                         mAccount.supportsSearch() && !mIsOnTablet);
    277         }
    278 
    279         return;
    280     }
    281 
    282     /**
    283      * Put the ActionBar in List navigation mode.
    284      */
    285     private void showNavList() {
    286         setTitleModeFlags(ActionBar.DISPLAY_SHOW_TITLE);
    287         setFolderAndAccount();
    288     }
    289 
    290     private void setTitle(String title) {
    291         if (!TextUtils.equals(title, mActionBar.getTitle())) {
    292             mActionBar.setTitle(title);
    293         }
    294     }
    295 
    296     /**
    297      * Set the actionbar mode to empty: no title, no subtitle, no custom view.
    298      */
    299     protected void setEmptyMode() {
    300         // Disable title/subtitle and the custom view by setting the bitmask to all off.
    301         setTitleModeFlags(0);
    302     }
    303 
    304     /**
    305      * Removes the back button from being shown
    306      */
    307     public void removeBackButton() {
    308         if (mActionBar == null) {
    309             return;
    310         }
    311         // Remove the back button but continue showing an icon.
    312         final int mask = ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME;
    313         mActionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_HOME, mask);
    314         mActionBar.setHomeButtonEnabled(false);
    315     }
    316 
    317     public void setBackButton() {
    318         if (mActionBar == null) {
    319             return;
    320         }
    321         // Show home as up, and show an icon.
    322         final int mask = ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME;
    323         mActionBar.setDisplayOptions(mask, mask);
    324         mActionBar.setHomeButtonEnabled(true);
    325     }
    326 
    327     /**
    328      * Uses the current state to update the current folder {@link #mFolder} and the current
    329      * account {@link #mAccount} shown in the actionbar. Also updates the actionbar subtitle to
    330      * momentarily display the unread count if it has changed.
    331      */
    332     private void setFolderAndAccount() {
    333         // Very little can be done if the actionbar or activity is null.
    334         if (mActionBar == null || mActivity == null) {
    335             return;
    336         }
    337         if (ViewMode.isWaitingForSync(getMode())) {
    338             // Account is not synced: clear title and update the subtitle.
    339             setTitle("");
    340             return;
    341         }
    342         // Check if we should be changing the actionbar at all, and back off if not.
    343         final boolean isShowingFolder = mIsOnTablet || ViewMode.isListMode(getMode());
    344         if (!isShowingFolder) {
    345             // It isn't necessary to set the title in this case, as the title view will
    346             // be hidden
    347             return;
    348         }
    349         if (mFolder == null) {
    350             // Clear the action bar title.  We don't want the app name to be shown while
    351             // waiting for the folder query to finish
    352             setTitle("");
    353             return;
    354         }
    355         setTitle(mFolder.name);
    356     }
    357 
    358 
    359     /**
    360      * Notify that the folder has changed.
    361      */
    362     public void onFolderUpdated(Folder folder) {
    363         if (folder == null) {
    364             return;
    365         }
    366         /** True if we are changing folders. */
    367         mFolder = folder;
    368         setFolderAndAccount();
    369         // make sure that we re-validate the optional menu items
    370         validateVolatileMenuOptionVisibility();
    371     }
    372 
    373     /**
    374      * Sets the actionbar mode: Pass it an integer which contains each of these values, perhaps
    375      * OR'd together: {@link ActionBar#DISPLAY_SHOW_CUSTOM} and
    376      * {@link ActionBar#DISPLAY_SHOW_TITLE}. To disable all, pass a zero.
    377      * @param enabledFlags
    378      */
    379     private void setTitleModeFlags(int enabledFlags) {
    380         final int mask = ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_CUSTOM;
    381         mActionBar.setDisplayOptions(enabledFlags, mask);
    382     }
    383 
    384     public void setCurrentConversation(Conversation conversation) {
    385         mCurrentConversation = conversation;
    386     }
    387 
    388     //We need to do this here instead of in the fragment
    389     public void setConversationModeOptions(Menu menu) {
    390         if (mCurrentConversation == null) {
    391             return;
    392         }
    393         final boolean showMarkImportant = !mCurrentConversation.isImportant();
    394         Utils.setMenuItemPresent(menu, R.id.mark_important, showMarkImportant
    395                 && mAccount.supportsCapability(UIProvider.AccountCapabilities.MARK_IMPORTANT));
    396         Utils.setMenuItemPresent(menu, R.id.mark_not_important, !showMarkImportant
    397                 && mAccount.supportsCapability(UIProvider.AccountCapabilities.MARK_IMPORTANT));
    398         final boolean isOutbox = mFolder.isType(FolderType.OUTBOX);
    399         final boolean showDiscardOutbox = mFolder != null && isOutbox;
    400         Utils.setMenuItemPresent(menu, R.id.discard_outbox, showDiscardOutbox);
    401         final boolean showDelete = !isOutbox && mFolder != null &&
    402                 mFolder.supportsCapability(UIProvider.FolderCapabilities.DELETE);
    403         Utils.setMenuItemPresent(menu, R.id.delete, showDelete);
    404         // We only want to show the discard drafts menu item if we are not showing the delete menu
    405         // item, and the current folder is a draft folder and the account supports discarding
    406         // drafts for a conversation
    407         final boolean showDiscardDrafts = !showDelete && mFolder != null && mFolder.isDraft() &&
    408                 mAccount.supportsCapability(AccountCapabilities.DISCARD_CONVERSATION_DRAFTS);
    409         Utils.setMenuItemPresent(menu, R.id.discard_drafts, showDiscardDrafts);
    410         final boolean archiveVisible = mAccount.supportsCapability(AccountCapabilities.ARCHIVE)
    411                 && mFolder != null && mFolder.supportsCapability(FolderCapabilities.ARCHIVE)
    412                 && !mFolder.isTrash();
    413         Utils.setMenuItemPresent(menu, R.id.archive, archiveVisible);
    414         Utils.setMenuItemPresent(menu, R.id.remove_folder, !archiveVisible && mFolder != null
    415                 && mFolder.supportsCapability(FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES)
    416                 && !mFolder.isProviderFolder()
    417                 && mAccount.supportsCapability(AccountCapabilities.ARCHIVE));
    418         Utils.setMenuItemPresent(menu, R.id.move_to, mFolder != null
    419                 && mFolder.supportsCapability(FolderCapabilities.ALLOWS_REMOVE_CONVERSATION));
    420         Utils.setMenuItemPresent(menu, R.id.move_to_inbox, mFolder != null
    421                 && mFolder.supportsCapability(FolderCapabilities.ALLOWS_MOVE_TO_INBOX));
    422         Utils.setMenuItemPresent(menu, R.id.change_folders, mAccount.supportsCapability(
    423                 UIProvider.AccountCapabilities.MULTIPLE_FOLDERS_PER_CONV));
    424 
    425         final MenuItem removeFolder = menu.findItem(R.id.remove_folder);
    426         if (mFolder != null && removeFolder != null) {
    427             removeFolder.setTitle(mActivity.getApplicationContext().getString(
    428                     R.string.remove_folder, mFolder.name));
    429         }
    430         Utils.setMenuItemPresent(menu, R.id.report_spam,
    431                 mAccount.supportsCapability(AccountCapabilities.REPORT_SPAM) && mFolder != null
    432                         && mFolder.supportsCapability(FolderCapabilities.REPORT_SPAM)
    433                         && !mCurrentConversation.spam);
    434         Utils.setMenuItemPresent(menu, R.id.mark_not_spam,
    435                 mAccount.supportsCapability(AccountCapabilities.REPORT_SPAM) && mFolder != null
    436                         && mFolder.supportsCapability(FolderCapabilities.MARK_NOT_SPAM)
    437                         && mCurrentConversation.spam);
    438         Utils.setMenuItemPresent(menu, R.id.report_phishing,
    439                 mAccount.supportsCapability(AccountCapabilities.REPORT_PHISHING) && mFolder != null
    440                         && mFolder.supportsCapability(FolderCapabilities.REPORT_PHISHING)
    441                         && !mCurrentConversation.phishing);
    442         Utils.setMenuItemPresent(menu, R.id.mute,
    443                 mAccount.supportsCapability(AccountCapabilities.MUTE) && mFolder != null
    444                         && mFolder.supportsCapability(FolderCapabilities.DESTRUCTIVE_MUTE)
    445                         && !mCurrentConversation.muted);
    446     }
    447 
    448     public void setViewModeController(ViewMode viewModeController) {
    449         mViewModeController = viewModeController;
    450         mViewModeController.addListener(this);
    451     }
    452 
    453     public Context getContext() {
    454         return mContext;
    455     }
    456 }
    457