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
18package com.android.mail.ui;
19
20import android.content.ContentResolver;
21import android.content.Context;
22import android.net.Uri;
23import android.os.AsyncTask;
24import android.os.Bundle;
25import android.support.v7.app.ActionBar;
26import android.text.TextUtils;
27import android.view.Menu;
28import android.view.MenuItem;
29
30import com.android.mail.R;
31import com.android.mail.providers.Account;
32import com.android.mail.providers.AccountObserver;
33import com.android.mail.providers.Conversation;
34import com.android.mail.providers.Folder;
35import com.android.mail.providers.FolderObserver;
36import com.android.mail.providers.UIProvider;
37import com.android.mail.providers.UIProvider.AccountCapabilities;
38import com.android.mail.providers.UIProvider.FolderCapabilities;
39import com.android.mail.providers.UIProvider.FolderType;
40import com.android.mail.utils.LogTag;
41import com.android.mail.utils.LogUtils;
42import com.android.mail.utils.Utils;
43
44/**
45 * Controller to manage the various states of the {@link android.app.ActionBar}.
46 */
47public 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