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;
UpdateProvider(Uri account, ContentResolver resolver)83         public UpdateProvider(Uri account, ContentResolver resolver) {
84             mAccount = account;
85             mResolver = resolver;
86         }
87 
88         @Override
doInBackground(Bundle... params)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 
ActionBarController(Context context)103     public ActionBarController(Context context) {
104         mContext = context;
105         mIsOnTablet = Utils.useTabletUI(context.getResources());
106     }
107 
onCreateOptionsMenu(Menu menu)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 
getOptionsMenuId()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 
initialize(ControllableActivity activity, ActivityController callback, ActionBar actionBar)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 
updateAccount(Account account)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      */
setFolder(Folder folder)169     public void setFolder(Folder folder) {
170         mFolder = folder;
171         setFolderAndAccount();
172     }
173 
onDestroy()174     public void onDestroy() {
175         if (mFolderObserver != null) {
176             mFolderObserver.unregisterAndDestroy();
177             mFolderObserver = null;
178         }
179         mAccountObserver.unregisterAndDestroy();
180     }
181 
182     @Override
onViewModeChanged(int newMode)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 
getMode()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      */
validateVolatileMenuOptionVisibility()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 
onPrepareOptionsMenu(Menu menu)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      */
showNavList()285     private void showNavList() {
286         setTitleModeFlags(ActionBar.DISPLAY_SHOW_TITLE);
287         setFolderAndAccount();
288     }
289 
setTitle(String title)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      */
setEmptyMode()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      */
removeBackButton()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 
setBackButton()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      */
setFolderAndAccount()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      */
onFolderUpdated(Folder folder)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      */
setTitleModeFlags(int enabledFlags)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 
setCurrentConversation(Conversation conversation)384     public void setCurrentConversation(Conversation conversation) {
385         mCurrentConversation = conversation;
386     }
387 
388     //We need to do this here instead of in the fragment
setConversationModeOptions(Menu menu)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 
setViewModeController(ViewMode viewModeController)448     public void setViewModeController(ViewMode viewModeController) {
449         mViewModeController = viewModeController;
450         mViewModeController.addListener(this);
451     }
452 
getContext()453     public Context getContext() {
454         return mContext;
455     }
456 }
457