TwoPaneController.java revision 0b69338a45faa422ccba8faf64c9816c55d33e4a
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.app.Activity;
21import android.app.Fragment;
22import android.app.FragmentManager;
23import android.app.FragmentTransaction;
24import android.app.ListFragment;
25import android.content.Intent;
26import android.os.Bundle;
27import android.support.annotation.IdRes;
28import android.support.annotation.LayoutRes;
29import android.support.v7.app.ActionBar;
30import android.view.KeyEvent;
31import android.view.View;
32import android.widget.ListView;
33
34import com.android.mail.ConversationListContext;
35import com.android.mail.R;
36import com.android.mail.providers.Account;
37import com.android.mail.providers.Conversation;
38import com.android.mail.providers.Folder;
39import com.android.mail.providers.UIProvider.ConversationListIcon;
40import com.android.mail.utils.LogUtils;
41import com.android.mail.utils.Utils;
42
43/**
44 * Controller for two-pane Mail activity. Two Pane is used for tablets, where screen real estate
45 * abounds.
46 */
47public final class TwoPaneController extends AbstractActivityController implements
48        ConversationViewFrame.DownEventListener {
49
50    private static final String SAVED_MISCELLANEOUS_VIEW = "saved-miscellaneous-view";
51    private static final String SAVED_MISCELLANEOUS_VIEW_TRANSACTION_ID =
52            "saved-miscellaneous-view-transaction-id";
53
54    private TwoPaneLayout mLayout;
55    @Deprecated
56    private Conversation mConversationToShow;
57
58    /**
59     * 2-pane, in wider configurations, allows peeking at a conversation view without having the
60     * conversation marked-as-read as far as read/unread state goes.<br>
61     * <br>
62     * This flag applies to {@link AbstractActivityController#mCurrentConversation} and indicates
63     * that the current conversation, if set, is in a 'peeking' state. If there is no current
64     * conversation, peeking is implied (in certain view configurations) and this value is
65     * meaningless.
66     */
67    // TODO: save in instance state
68    private boolean mCurrentConversationJustPeeking;
69
70    /**
71     * Used to determine whether onViewModeChanged should skip a potential
72     * fragment transaction that would remove a miscellaneous view.
73     */
74    private boolean mSavedMiscellaneousView = false;
75
76    private boolean mIsTabletLandscape;
77
78    public TwoPaneController(MailActivity activity, ViewMode viewMode) {
79        super(activity, viewMode);
80    }
81
82    public boolean isCurrentConversationJustPeeking() {
83        return mCurrentConversationJustPeeking;
84    }
85
86    private boolean isConversationOnlyMode() {
87        return getCurrentConversation() != null && !isCurrentConversationJustPeeking()
88                && !mLayout.shouldShowPreviewPanel();
89    }
90
91    /**
92     * Display the conversation list fragment.
93     */
94    private void initializeConversationListFragment() {
95        if (Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())) {
96            if (shouldEnterSearchConvMode()) {
97                mViewMode.enterSearchResultsConversationMode();
98            } else {
99                mViewMode.enterSearchResultsListMode();
100            }
101        }
102        renderConversationList();
103    }
104
105    /**
106     * Render the conversation list in the correct pane.
107     */
108    private void renderConversationList() {
109        if (mActivity == null) {
110            return;
111        }
112        FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction();
113        // Use cross fading animation.
114        fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
115        final ConversationListFragment conversationListFragment =
116                ConversationListFragment.newInstance(mConvListContext);
117        fragmentTransaction.replace(R.id.conversation_list_pane, conversationListFragment,
118                TAG_CONVERSATION_LIST);
119        fragmentTransaction.commitAllowingStateLoss();
120        // Set default navigation here once the ConversationListFragment is created.
121        conversationListFragment.setNextFocusLeftId(
122                getClfNextFocusLeftId(getFolderListFragment().isMinimized()));
123    }
124
125    @Override
126    public boolean doesActionChangeConversationListVisibility(final int action) {
127        if (action == R.id.settings
128                || action == R.id.compose
129                || action == R.id.help_info_menu_item
130                || action == R.id.feedback_menu_item) {
131            return true;
132        }
133
134        return false;
135    }
136
137    @Override
138    protected boolean isConversationListVisible() {
139        return !mLayout.isConversationListCollapsed();
140    }
141
142    @Override
143    public void showConversationList(ConversationListContext listContext) {
144        super.showConversationList(listContext);
145        initializeConversationListFragment();
146    }
147
148    @Override
149    public @LayoutRes int getContentViewResource() {
150        return R.layout.two_pane_activity;
151    }
152
153    @Override
154    public boolean onCreate(Bundle savedState) {
155        mLayout = (TwoPaneLayout) mActivity.findViewById(R.id.two_pane_activity);
156            if (mLayout == null) {
157            // We need the layout for everything. Crash/Return early if it is null.
158            LogUtils.wtf(LOG_TAG, "mLayout is null!");
159            return false;
160        }
161        mLayout.setController(this, Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction()));
162        mActivity.getWindow().setBackgroundDrawable(null);
163        mIsTabletLandscape = !mActivity.getResources().getBoolean(R.bool.list_collapsible);
164
165        final FolderListFragment flf = getFolderListFragment();
166        flf.setMiniDrawerEnabled(true);
167        flf.setMinimized(true);
168
169        if (savedState != null) {
170            mSavedMiscellaneousView = savedState.getBoolean(SAVED_MISCELLANEOUS_VIEW, false);
171            mMiscellaneousViewTransactionId =
172                    savedState.getInt(SAVED_MISCELLANEOUS_VIEW_TRANSACTION_ID, -1);
173        }
174
175        // 2-pane layout is the main listener of view mode changes, and issues secondary
176        // notifications upon animation completion:
177        // (onConversationVisibilityChanged, onConversationListVisibilityChanged)
178        mViewMode.addListener(mLayout);
179        return super.onCreate(savedState);
180    }
181
182    @Override
183    public void onSaveInstanceState(Bundle outState) {
184        super.onSaveInstanceState(outState);
185
186        outState.putBoolean(SAVED_MISCELLANEOUS_VIEW, mMiscellaneousViewTransactionId >= 0);
187        outState.putInt(SAVED_MISCELLANEOUS_VIEW_TRANSACTION_ID, mMiscellaneousViewTransactionId);
188    }
189
190    @Override
191    public void onWindowFocusChanged(boolean hasFocus) {
192        if (hasFocus && !mLayout.isConversationListCollapsed()) {
193            // The conversation list is visible.
194            informCursorVisiblity(true);
195        }
196    }
197
198    @Override
199    public void switchToDefaultInboxOrChangeAccount(Account account) {
200        if (mViewMode.isSearchMode()) {
201            // We are in an activity on top of the main navigation activity.
202            // We need to return to it with a result code that indicates it should navigate to
203            // a different folder.
204            final Intent intent = new Intent();
205            intent.putExtra(AbstractActivityController.EXTRA_ACCOUNT, account);
206            mActivity.setResult(Activity.RESULT_OK, intent);
207            mActivity.finish();
208            return;
209        }
210        if (mViewMode.getMode() != ViewMode.CONVERSATION_LIST) {
211            mViewMode.enterConversationListMode();
212        }
213        super.switchToDefaultInboxOrChangeAccount(account);
214    }
215
216    @Override
217    public void onFolderSelected(Folder folder) {
218        // It's possible that we are not in conversation list mode
219        if (mViewMode.isSearchMode()) {
220            // We are in an activity on top of the main navigation activity.
221            // We need to return to it with a result code that indicates it should navigate to
222            // a different folder.
223            final Intent intent = new Intent();
224            intent.putExtra(AbstractActivityController.EXTRA_FOLDER, folder);
225            mActivity.setResult(Activity.RESULT_OK, intent);
226            mActivity.finish();
227            return;
228        } else if (mViewMode.getMode() != ViewMode.CONVERSATION_LIST) {
229            mViewMode.enterConversationListMode();
230        }
231
232        setHierarchyFolder(folder);
233        super.onFolderSelected(folder);
234    }
235
236    public boolean isDrawerOpen() {
237        final FolderListFragment flf = getFolderListFragment();
238        return flf != null && !flf.isMinimized();
239    }
240
241    @Override
242    protected void toggleDrawerState() {
243        final FolderListFragment flf = getFolderListFragment();
244        if (flf == null) {
245            LogUtils.w(LOG_TAG, "no drawer to toggle open/closed");
246            return;
247        }
248        flf.setMinimized(!flf.isMinimized());
249        mLayout.requestLayout();
250        resetActionBarIcon();
251
252        final ConversationListFragment clf = getConversationListFragment();
253        if (clf != null) {
254            clf.setNextFocusLeftId(getClfNextFocusLeftId(flf.isMinimized()));
255        }
256    }
257
258    @Override
259    public void onViewModeChanged(int newMode) {
260        if (!mSavedMiscellaneousView && mMiscellaneousViewTransactionId >= 0) {
261            final FragmentManager fragmentManager = mActivity.getFragmentManager();
262            fragmentManager.popBackStackImmediate(mMiscellaneousViewTransactionId,
263                    FragmentManager.POP_BACK_STACK_INCLUSIVE);
264            mMiscellaneousViewTransactionId = -1;
265        }
266        mSavedMiscellaneousView = false;
267
268        super.onViewModeChanged(newMode);
269        if (!isConversationOnlyMode()) {
270            mFloatingComposeButton.setVisibility(View.VISIBLE);
271        }
272        if (newMode != ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION) {
273            // Clear the wait fragment
274            hideWaitForInitialization();
275        }
276        // In conversation mode, if the conversation list is not visible, then the user cannot
277        // see the selected conversations. Disable the CAB mode while leaving the selected set
278        // untouched.
279        // When the conversation list is made visible again, try to enable the CAB
280        // mode if any conversations are selected.
281        if (newMode == ViewMode.CONVERSATION || newMode == ViewMode.CONVERSATION_LIST
282                || ViewMode.isAdMode(newMode)) {
283            enableOrDisableCab();
284        }
285    }
286
287    private @IdRes int getClfNextFocusLeftId(boolean drawerMinimized) {
288        return (drawerMinimized) ? R.id.current_account_avatar : android.R.id.list;
289    }
290
291    @Override
292    public void onConversationVisibilityChanged(boolean visible) {
293        super.onConversationVisibilityChanged(visible);
294        if (!visible) {
295            mPagerController.hide(false /* changeVisibility */);
296        } else if (mConversationToShow != null) {
297            mPagerController.show(mAccount, mFolder, mConversationToShow,
298                    false /* changeVisibility */);
299            mConversationToShow = null;
300        }
301    }
302
303    @Override
304    public void onConversationListVisibilityChanged(boolean visible) {
305        super.onConversationListVisibilityChanged(visible);
306        enableOrDisableCab();
307    }
308
309    @Override
310    public void resetActionBarIcon() {
311        final ActionBar ab = mActivity.getSupportActionBar();
312        final boolean isChildFolder = getFolder() != null && !Utils.isEmpty(getFolder().parent);
313        if (isConversationOnlyMode() || isChildFolder) {
314            ab.setHomeAsUpIndicator(R.drawable.ic_arrow_back_wht_24dp);
315            ab.setHomeActionContentDescription(0 /* system default */);
316        } else {
317            ab.setHomeAsUpIndicator(R.drawable.ic_drawer);
318            ab.setHomeActionContentDescription(
319                    isDrawerOpen() ? R.string.drawer_close : R.string.drawer_open);
320        }
321    }
322
323    /**
324     * Enable or disable the CAB mode based on the visibility of the conversation list fragment.
325     */
326    private void enableOrDisableCab() {
327        if (mLayout.isConversationListCollapsed()) {
328            disableCabMode();
329        } else {
330            enableCabMode();
331        }
332    }
333
334    @Override
335    public void onSetPopulated(ConversationSelectionSet set) {
336        super.onSetPopulated(set);
337
338        boolean showSenderImage =
339                (mAccount.settings.convListIcon == ConversationListIcon.SENDER_IMAGE);
340        if (!showSenderImage && mViewMode.isListMode()) {
341            getConversationListFragment().setChoiceNone();
342        }
343    }
344
345    @Override
346    public void onSetEmpty() {
347        super.onSetEmpty();
348
349        boolean showSenderImage =
350                (mAccount.settings.convListIcon == ConversationListIcon.SENDER_IMAGE);
351        if (!showSenderImage && mViewMode.isListMode()) {
352            getConversationListFragment().revertChoiceMode();
353        }
354    }
355
356    @Override
357    protected void showConversation(Conversation conversation, boolean markAsRead) {
358        super.showConversation(conversation, markAsRead);
359
360        // 2-pane can ignore inLoaderCallbacks because it doesn't use
361        // FragmentManager.popBackStack().
362
363        if (mActivity == null) {
364            return;
365        }
366        if (conversation == null) {
367            handleBackPress();
368            return;
369        }
370        // If conversation list is not visible, then the user cannot see the CAB mode, so exit it.
371        // This is needed here (in addition to during viewmode changes) because orientation changes
372        // while viewing a conversation don't change the viewmode: the mode stays
373        // ViewMode.CONVERSATION and yet the conversation list goes in and out of visibility.
374        enableOrDisableCab();
375
376        // close the drawer, if open
377        if (isDrawerOpen()) {
378            toggleDrawerState();
379        }
380
381        // When a mode change is required, wait for onConversationVisibilityChanged(), the signal
382        // that the mode change animation has finished, before rendering the conversation.
383        mConversationToShow = conversation;
384        mCurrentConversationJustPeeking = !markAsRead;
385
386        final int mode = mViewMode.getMode();
387        LogUtils.i(LOG_TAG, "IN TPC.showConv, oldMode=%s conv=%s", mode, mConversationToShow);
388        if (mode == ViewMode.SEARCH_RESULTS_LIST || mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
389            mViewMode.enterSearchResultsConversationMode();
390        } else {
391            mViewMode.enterConversationMode();
392        }
393        // load the conversation immediately if we're already in conversation mode
394        if (!mLayout.isModeChangePending()) {
395            onConversationVisibilityChanged(true);
396        } else {
397            LogUtils.i(LOG_TAG, "TPC.showConversation will wait for TPL.animationEnd to show!");
398        }
399    }
400
401    @Override
402    public final void onConversationSelected(Conversation conversation, boolean inLoaderCallbacks) {
403        super.onConversationSelected(conversation, inLoaderCallbacks);
404        // Shift the focus to the conversation in landscape mode
405        mPagerController.focusPager();
406    }
407
408    @Override
409    public void onConversationFocused(Conversation conversation) {
410        if (mIsTabletLandscape) {
411            showConversation(conversation, false /* markAsRead */);
412        }
413    }
414
415    @Override
416    public void setCurrentConversation(Conversation conversation) {
417        // Order is important! We want to calculate different *before* the superclass changes
418        // mCurrentConversation, so before super.setCurrentConversation().
419        final long oldId = mCurrentConversation != null ? mCurrentConversation.id : -1;
420        final long newId = conversation != null ? conversation.id : -1;
421        final boolean different = oldId != newId;
422
423        // This call might change mCurrentConversation.
424        super.setCurrentConversation(conversation);
425
426        final ConversationListFragment convList = getConversationListFragment();
427        if (convList != null && conversation != null) {
428            convList.setSelected(conversation.position, different);
429        }
430    }
431
432    @Override
433    public void showWaitForInitialization() {
434        super.showWaitForInitialization();
435
436        FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction();
437        fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
438        fragmentTransaction.replace(R.id.conversation_list_pane, getWaitFragment(), TAG_WAIT);
439        fragmentTransaction.commitAllowingStateLoss();
440    }
441
442    @Override
443    protected void hideWaitForInitialization() {
444        final WaitFragment waitFragment = getWaitFragment();
445        if (waitFragment == null) {
446            // We aren't showing a wait fragment: nothing to do
447            return;
448        }
449        // Remove the existing wait fragment from the back stack.
450        final FragmentTransaction fragmentTransaction =
451                mActivity.getFragmentManager().beginTransaction();
452        fragmentTransaction.remove(waitFragment);
453        fragmentTransaction.commitAllowingStateLoss();
454        super.hideWaitForInitialization();
455        if (mViewMode.isWaitingForSync()) {
456            // We should come out of wait mode and display the account inbox.
457            loadAccountInbox();
458        }
459    }
460
461    /**
462     * Up works as follows:
463     * 1) If the user is in a conversation and:
464     *  a) the conversation list is hidden (portrait mode), shows the conv list and
465     *  stays in conversation view mode.
466     *  b) the conversation list is shown, goes back to conversation list mode.
467     * 2) If the user is in search results, up exits search.
468     * mode and returns the user to whatever view they were in when they began search.
469     * 3) If the user is in conversation list mode, there is no up.
470     */
471    @Override
472    public boolean handleUpPress() {
473        if (isConversationOnlyMode()) {
474            handleBackPress();
475        } else {
476            toggleDrawerState();
477        }
478
479        return true;
480    }
481
482    @Override
483    public boolean handleBackPress() {
484        // Clear any visible undo bars.
485        mToastBar.hide(false, false /* actionClicked */);
486        if (isDrawerOpen()) {
487            toggleDrawerState();
488        } else {
489            popView(false);
490        }
491        return true;
492    }
493
494    /**
495     * Pops the "view stack" to the last screen the user was viewing.
496     *
497     * @param preventClose Whether to prevent closing the app if the stack is empty.
498     */
499    protected void popView(boolean preventClose) {
500        // If the user is in search query entry mode, or the user is viewing
501        // search results, exit
502        // the mode.
503        int mode = mViewMode.getMode();
504        if (mode == ViewMode.SEARCH_RESULTS_LIST) {
505            mActivity.finish();
506        } else if (mode == ViewMode.CONVERSATION || mViewMode.isAdMode()) {
507            // Go to conversation list.
508            mViewMode.enterConversationListMode();
509        } else if (mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
510            mViewMode.enterSearchResultsListMode();
511        } else {
512            // The Folder List fragment can be null for monkeys where we get a back before the
513            // folder list has had a chance to initialize.
514            final FolderListFragment folderList = getFolderListFragment();
515            if (mode == ViewMode.CONVERSATION_LIST && folderList != null
516                    && !Folder.isRoot(mFolder)) {
517                // If the user navigated via the left folders list into a child folder,
518                // back should take the user up to the parent folder's conversation list.
519                navigateUpFolderHierarchy();
520            // Otherwise, if we are in the conversation list but not in the default
521            // inbox and not on expansive layouts, we want to switch back to the default
522            // inbox. This fixes b/9006969 so that on smaller tablets where we have this
523            // hybrid one and two-pane mode, we will return to the inbox. On larger tablets,
524            // we will instead exit the app.
525            } else if (!preventClose) {
526                // There is nothing else to pop off the stack.
527                mActivity.finish();
528            }
529        }
530    }
531
532    @Override
533    public void exitSearchMode() {
534        final int mode = mViewMode.getMode();
535        if (mode == ViewMode.SEARCH_RESULTS_LIST
536                || (mode == ViewMode.SEARCH_RESULTS_CONVERSATION
537                        && Utils.showTwoPaneSearchResults(mActivity.getApplicationContext()))) {
538            mActivity.finish();
539        }
540    }
541
542    @Override
543    public boolean shouldShowFirstConversation() {
544        return Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())
545                && shouldEnterSearchConvMode();
546    }
547
548    @Override
549    public void onUndoAvailable(ToastBarOperation op) {
550        final int mode = mViewMode.getMode();
551        final ConversationListFragment convList = getConversationListFragment();
552
553        switch (mode) {
554            case ViewMode.SEARCH_RESULTS_LIST:
555            case ViewMode.CONVERSATION_LIST:
556            case ViewMode.SEARCH_RESULTS_CONVERSATION:
557            case ViewMode.CONVERSATION:
558                if (convList != null) {
559                    mToastBar.show(getUndoClickedListener(convList.getAnimatedAdapter()),
560                            Utils.convertHtmlToPlainText
561                                (op.getDescription(mActivity.getActivityContext())),
562                            R.string.undo,
563                            true,  /* replaceVisibleToast */
564                            op);
565                }
566        }
567    }
568
569    @Override
570    public void onError(final Folder folder, boolean replaceVisibleToast) {
571        showErrorToast(folder, replaceVisibleToast);
572    }
573
574    @Override
575    public boolean isDrawerEnabled() {
576        // two-pane has its own drawer-like thing that expands inline from a minimized state.
577        return false;
578    }
579
580    @Override
581    public int getFolderListViewChoiceMode() {
582        // By default, we want to allow one item to be selected in the folder list
583        return ListView.CHOICE_MODE_SINGLE;
584    }
585
586    private int mMiscellaneousViewTransactionId = -1;
587
588    @Override
589    public void launchFragment(final Fragment fragment, final int selectPosition) {
590        final int containerViewId = TwoPaneLayout.MISCELLANEOUS_VIEW_ID;
591
592        final FragmentManager fragmentManager = mActivity.getFragmentManager();
593        if (fragmentManager.findFragmentByTag(TAG_CUSTOM_FRAGMENT) == null) {
594            final FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
595            fragmentTransaction.addToBackStack(null);
596            fragmentTransaction.replace(containerViewId, fragment, TAG_CUSTOM_FRAGMENT);
597            mMiscellaneousViewTransactionId = fragmentTransaction.commitAllowingStateLoss();
598            fragmentManager.executePendingTransactions();
599        }
600
601        if (selectPosition >= 0) {
602            getConversationListFragment().setRawSelected(selectPosition, true);
603        }
604    }
605
606    @Override
607    public boolean onInterceptCVDownEvent() {
608        // handle a down event on CV by closing the drawer if open
609        if (isDrawerOpen()) {
610            toggleDrawerState();
611            return true;
612        }
613        return false;
614    }
615
616    @Override
617    public boolean onInterceptKeyFromCV(int keyCode, KeyEvent keyEvent, boolean navigateAway) {
618        // Override left/right key presses in landscape mode.
619        if (navigateAway) {
620            if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
621                ConversationListFragment clf = getConversationListFragment();
622                if (clf != null) {
623                    clf.getListView().requestFocus();
624                }
625            }
626            return true;
627        }
628        return false;
629    }
630
631    @Override
632    public boolean isTwoPaneLandscape() {
633        return mIsTabletLandscape;
634    }
635}
636