TwoPaneController.java revision 1a7884b6e516220ace1ab74196df445fee10592b
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.Fragment;
21import android.app.FragmentManager;
22import android.app.FragmentTransaction;
23import android.content.Intent;
24import android.net.Uri;
25import android.os.Bundle;
26import android.support.v4.widget.DrawerLayout;
27import android.view.Gravity;
28import android.widget.FrameLayout;
29import android.widget.ListView;
30
31import com.android.mail.ConversationListContext;
32import com.android.mail.R;
33import com.android.mail.providers.Conversation;
34import com.android.mail.providers.Folder;
35import com.android.mail.providers.UIProvider.ConversationListIcon;
36import com.android.mail.utils.LogUtils;
37import com.android.mail.utils.Utils;
38
39/**
40 * Controller for two-pane Mail activity. Two Pane is used for tablets, where screen real estate
41 * abounds.
42 */
43public final class TwoPaneController extends AbstractActivityController {
44
45    private static final String SAVED_MISCELLANEOUS_VIEW = "saved-miscellaneous-view";
46    private static final String SAVED_MISCELLANEOUS_VIEW_TRANSACTION_ID =
47            "saved-miscellaneous-view-transaction-id";
48
49    private TwoPaneLayout mLayout;
50    private Conversation mConversationToShow;
51
52    /**
53     * Used to determine whether onViewModeChanged should skip a potential
54     * fragment transaction that would remove a miscellaneous view.
55     */
56    private boolean mSavedMiscellaneousView = false;
57
58    public TwoPaneController(MailActivity activity, ViewMode viewMode) {
59        super(activity, viewMode);
60    }
61
62    /**
63     * Display the conversation list fragment.
64     */
65    private void initializeConversationListFragment() {
66        if (Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())) {
67            if (shouldEnterSearchConvMode()) {
68                mViewMode.enterSearchResultsConversationMode();
69            } else {
70                mViewMode.enterSearchResultsListMode();
71            }
72        }
73        renderConversationList();
74    }
75
76    /**
77     * Render the conversation list in the correct pane.
78     */
79    private void renderConversationList() {
80        if (mActivity == null) {
81            return;
82        }
83        FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction();
84        // Use cross fading animation.
85        fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
86        final Fragment conversationListFragment =
87                ConversationListFragment.newInstance(mConvListContext);
88        fragmentTransaction.replace(R.id.conversation_list_pane, conversationListFragment,
89                TAG_CONVERSATION_LIST);
90        fragmentTransaction.commitAllowingStateLoss();
91    }
92
93    @Override
94    public boolean doesActionChangeConversationListVisibility(final int action) {
95        if (action == R.id.settings
96                || action == R.id.compose
97                || action == R.id.help_info_menu_item
98                || action == R.id.manage_folders_item
99                || action == R.id.folder_options
100                || action == R.id.feedback_menu_item) {
101            return true;
102        }
103
104        return false;
105    }
106
107    @Override
108    protected boolean isConversationListVisible() {
109        return !mLayout.isConversationListCollapsed();
110    }
111
112    @Override
113    public void showConversationList(ConversationListContext listContext) {
114        super.showConversationList(listContext);
115        initializeConversationListFragment();
116    }
117
118    @Override
119    public boolean onCreate(Bundle savedState) {
120        mActivity.setContentView(R.layout.two_pane_activity);
121        mDrawerContainer = (DrawerLayout) mActivity.findViewById(R.id.drawer_container);
122        mDrawerPullout = mDrawerContainer.findViewById(R.id.content_pane);
123        mLayout = (TwoPaneLayout) mActivity.findViewById(R.id.two_pane_activity);
124        if (mLayout == null) {
125            // We need the layout for everything. Crash/Return early if it is null.
126            LogUtils.wtf(LOG_TAG, "mLayout is null!");
127            return false;
128        }
129        mLayout.setController(this, Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction()));
130        mLayout.setDrawerLayout(mDrawerContainer);
131
132        if (savedState != null) {
133            mSavedMiscellaneousView = savedState.getBoolean(SAVED_MISCELLANEOUS_VIEW, false);
134            mMiscellaneousViewTransactionId =
135                    savedState.getInt(SAVED_MISCELLANEOUS_VIEW_TRANSACTION_ID, -1);
136        }
137
138        // 2-pane layout is the main listener of view mode changes, and issues secondary
139        // notifications upon animation completion:
140        // (onConversationVisibilityChanged, onConversationListVisibilityChanged)
141        mViewMode.addListener(mLayout);
142        return super.onCreate(savedState);
143    }
144
145    @Override
146    public void onSaveInstanceState(Bundle outState) {
147        super.onSaveInstanceState(outState);
148
149        outState.putBoolean(SAVED_MISCELLANEOUS_VIEW, mMiscellaneousViewTransactionId >= 0);
150        outState.putInt(SAVED_MISCELLANEOUS_VIEW_TRANSACTION_ID, mMiscellaneousViewTransactionId);
151    }
152
153    @Override
154    public void onWindowFocusChanged(boolean hasFocus) {
155        if (hasFocus && !mLayout.isConversationListCollapsed()) {
156            // The conversation list is visible.
157            informCursorVisiblity(true);
158        }
159    }
160
161    @Override
162    public void onFolderSelected(Folder folder) {
163        // It's possible that we are not in conversation list mode
164        if (mViewMode.getMode() != ViewMode.CONVERSATION_LIST) {
165            mViewMode.enterConversationListMode();
166        }
167
168        if (folder.parent != Uri.EMPTY) {
169            // Show the up affordance when digging into child folders.
170            mActionBarView.setBackButton();
171        }
172        setHierarchyFolder(folder);
173        super.onFolderSelected(folder);
174    }
175
176    @Override
177    public void onViewModeChanged(int newMode) {
178        if (!mSavedMiscellaneousView && mMiscellaneousViewTransactionId >= 0) {
179            final FragmentManager fragmentManager = mActivity.getFragmentManager();
180            fragmentManager.popBackStackImmediate(mMiscellaneousViewTransactionId,
181                    FragmentManager.POP_BACK_STACK_INCLUSIVE);
182            mMiscellaneousViewTransactionId = -1;
183        }
184        mSavedMiscellaneousView = false;
185
186        super.onViewModeChanged(newMode);
187        if (newMode != ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION) {
188            // Clear the wait fragment
189            hideWaitForInitialization();
190        }
191        // In conversation mode, if the conversation list is not visible, then the user cannot
192        // see the selected conversations. Disable the CAB mode while leaving the selected set
193        // untouched.
194        // When the conversation list is made visible again, try to enable the CAB
195        // mode if any conversations are selected.
196        if (newMode == ViewMode.CONVERSATION || newMode == ViewMode.CONVERSATION_LIST
197                || ViewMode.isAdMode(newMode)) {
198            enableOrDisableCab();
199        }
200    }
201
202    @Override
203    public void onConversationVisibilityChanged(boolean visible) {
204        super.onConversationVisibilityChanged(visible);
205        if (!visible) {
206            mPagerController.hide(false /* changeVisibility */);
207        } else if (mConversationToShow != null) {
208            mPagerController.show(mAccount, mFolder, mConversationToShow,
209                    false /* changeVisibility */);
210            mConversationToShow = null;
211        }
212    }
213
214    @Override
215    public void onConversationListVisibilityChanged(boolean visible) {
216        super.onConversationListVisibilityChanged(visible);
217        enableOrDisableCab();
218    }
219
220    @Override
221    public void resetActionBarIcon() {
222        if (isDrawerEnabled()) {
223            return;
224        }
225        // On two-pane, the back button is only removed in the conversation list mode for top level
226        // folders, and shown for every other condition.
227        if ((mViewMode.isListMode() && (mFolder == null || mFolder.parent == null
228                || mFolder.parent == Uri.EMPTY)) || mViewMode.isWaitingForSync()) {
229            mActionBarView.removeBackButton();
230        } else {
231            mActionBarView.setBackButton();
232        }
233    }
234
235    /**
236     * Enable or disable the CAB mode based on the visibility of the conversation list fragment.
237     */
238    private void enableOrDisableCab() {
239        if (mLayout.isConversationListCollapsed()) {
240            disableCabMode();
241        } else {
242            enableCabMode();
243        }
244    }
245
246    @Override
247    public void onSetPopulated(ConversationSelectionSet set) {
248        super.onSetPopulated(set);
249
250        boolean showSenderImage =
251                (mAccount.settings.convListIcon == ConversationListIcon.SENDER_IMAGE);
252        if (!showSenderImage && mViewMode.isListMode()) {
253            getConversationListFragment().setChoiceNone();
254        }
255    }
256
257    @Override
258    public void onSetEmpty() {
259        super.onSetEmpty();
260
261        boolean showSenderImage =
262                (mAccount.settings.convListIcon == ConversationListIcon.SENDER_IMAGE);
263        if (!showSenderImage && mViewMode.isListMode()) {
264            getConversationListFragment().revertChoiceMode();
265        }
266    }
267
268    @Override
269    protected void showConversation(Conversation conversation, boolean inLoaderCallbacks) {
270        super.showConversation(conversation, inLoaderCallbacks);
271
272        // 2-pane can ignore inLoaderCallbacks because it doesn't use
273        // FragmentManager.popBackStack().
274
275        if (mActivity == null) {
276            return;
277        }
278        if (conversation == null) {
279            handleBackPress();
280            return;
281        }
282        // If conversation list is not visible, then the user cannot see the CAB mode, so exit it.
283        // This is needed here (in addition to during viewmode changes) because orientation changes
284        // while viewing a conversation don't change the viewmode: the mode stays
285        // ViewMode.CONVERSATION and yet the conversation list goes in and out of visibility.
286        enableOrDisableCab();
287
288        // When a mode change is required, wait for onConversationVisibilityChanged(), the signal
289        // that the mode change animation has finished, before rendering the conversation.
290        mConversationToShow = conversation;
291
292        final int mode = mViewMode.getMode();
293        LogUtils.i(LOG_TAG, "IN TPC.showConv, oldMode=%s conv=%s", mode, mConversationToShow);
294        if (mode == ViewMode.SEARCH_RESULTS_LIST || mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
295            mViewMode.enterSearchResultsConversationMode();
296        } else {
297            mViewMode.enterConversationMode();
298        }
299        // load the conversation immediately if we're already in conversation mode
300        if (!mLayout.isModeChangePending()) {
301            onConversationVisibilityChanged(true);
302        } else {
303            LogUtils.i(LOG_TAG, "TPC.showConversation will wait for TPL.animationEnd to show!");
304        }
305    }
306
307    @Override
308    public void setCurrentConversation(Conversation conversation) {
309        // Order is important! We want to calculate different *before* the superclass changes
310        // mCurrentConversation, so before super.setCurrentConversation().
311        final long oldId = mCurrentConversation != null ? mCurrentConversation.id : -1;
312        final long newId = conversation != null ? conversation.id : -1;
313        final boolean different = oldId != newId;
314
315        // This call might change mCurrentConversation.
316        super.setCurrentConversation(conversation);
317
318        final ConversationListFragment convList = getConversationListFragment();
319        if (convList != null && conversation != null) {
320            convList.setSelected(conversation.position, different);
321        }
322    }
323
324    @Override
325    public void showWaitForInitialization() {
326        super.showWaitForInitialization();
327
328        FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction();
329        fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
330        fragmentTransaction.replace(R.id.conversation_list_pane, getWaitFragment(), TAG_WAIT);
331        fragmentTransaction.commitAllowingStateLoss();
332    }
333
334    @Override
335    protected void hideWaitForInitialization() {
336        final WaitFragment waitFragment = getWaitFragment();
337        if (waitFragment == null) {
338            // We aren't showing a wait fragment: nothing to do
339            return;
340        }
341        // Remove the existing wait fragment from the back stack.
342        final FragmentTransaction fragmentTransaction =
343                mActivity.getFragmentManager().beginTransaction();
344        fragmentTransaction.remove(waitFragment);
345        fragmentTransaction.commitAllowingStateLoss();
346        super.hideWaitForInitialization();
347        if (mViewMode.isWaitingForSync()) {
348            // We should come out of wait mode and display the account inbox.
349            loadAccountInbox();
350        }
351    }
352
353    /**
354     * Up works as follows:
355     * 1) If the user is in a conversation and:
356     *  a) the conversation list is hidden (portrait mode), shows the conv list and
357     *  stays in conversation view mode.
358     *  b) the conversation list is shown, goes back to conversation list mode.
359     * 2) If the user is in search results, up exits search.
360     * mode and returns the user to whatever view they were in when they began search.
361     * 3) If the user is in conversation list mode, there is no up.
362     */
363    @Override
364    public boolean handleUpPress() {
365        int mode = mViewMode.getMode();
366        if (mode == ViewMode.CONVERSATION || mViewMode.isAdMode()) {
367            handleBackPress();
368        } else if (mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
369            if (mLayout.isConversationListCollapsed()
370                    || (ConversationListContext.isSearchResult(mConvListContext) && !Utils.
371                            showTwoPaneSearchResults(mActivity.getApplicationContext()))) {
372                handleBackPress();
373            } else {
374                mActivity.finish();
375            }
376        } else if (mode == ViewMode.SEARCH_RESULTS_LIST) {
377            mActivity.finish();
378        } else if (mode == ViewMode.CONVERSATION_LIST
379                || mode == ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION) {
380            final boolean isTopLevel = (mFolder == null) || (mFolder.parent == Uri.EMPTY);
381
382            if (isTopLevel) {
383                // Show the drawer
384                toggleDrawerState();
385            } else {
386                popView(true);
387            }
388        }
389        return true;
390    }
391
392    @Override
393    public boolean handleBackPress() {
394        // Clear any visible undo bars.
395        mToastBar.hide(false, false /* actionClicked */);
396        popView(false);
397        return true;
398    }
399
400    /**
401     * Pops the "view stack" to the last screen the user was viewing.
402     *
403     * @param preventClose Whether to prevent closing the app if the stack is empty.
404     */
405    protected void popView(boolean preventClose) {
406        // If the user is in search query entry mode, or the user is viewing
407        // search results, exit
408        // the mode.
409        int mode = mViewMode.getMode();
410        if (mode == ViewMode.SEARCH_RESULTS_LIST) {
411            mActivity.finish();
412        } else if (mode == ViewMode.CONVERSATION || mViewMode.isAdMode()) {
413            // Go to conversation list.
414            mViewMode.enterConversationListMode();
415        } else if (mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
416            mViewMode.enterSearchResultsListMode();
417        } else {
418            // The Folder List fragment can be null for monkeys where we get a back before the
419            // folder list has had a chance to initialize.
420            final FolderListFragment folderList = getFolderListFragment();
421            if (mode == ViewMode.CONVERSATION_LIST && folderList != null
422                    && mFolder != null && mFolder.parent != Uri.EMPTY) {
423                // If the user navigated via the left folders list into a child folder,
424                // back should take the user up to the parent folder's conversation list.
425                navigateUpFolderHierarchy();
426            // Otherwise, if we are in the conversation list but not in the default
427            // inbox and not on expansive layouts, we want to switch back to the default
428            // inbox. This fixes b/9006969 so that on smaller tablets where we have this
429            // hybrid one and two-pane mode, we will return to the inbox. On larger tablets,
430            // we will instead exit the app.
431            } else {
432                // Don't think mLayout could be null but checking just in case
433                if (mLayout == null) {
434                    LogUtils.wtf(LOG_TAG, new Throwable(), "mLayout is null");
435                }
436                // mFolder could be null if back is pressed while account is waiting for sync
437                final boolean shouldLoadInbox = mode == ViewMode.CONVERSATION_LIST &&
438                        mFolder != null &&
439                        !mFolder.folderUri.equals(mAccount.settings.defaultInbox) &&
440                        mLayout != null && !mLayout.isExpansiveLayout();
441                if (shouldLoadInbox) {
442                    loadAccountInbox();
443                } else if (!preventClose) {
444                    // There is nothing else to pop off the stack.
445                    mActivity.finish();
446                }
447            }
448        }
449    }
450
451    @Override
452    public void exitSearchMode() {
453        final int mode = mViewMode.getMode();
454        if (mode == ViewMode.SEARCH_RESULTS_LIST
455                || (mode == ViewMode.SEARCH_RESULTS_CONVERSATION
456                        && Utils.showTwoPaneSearchResults(mActivity.getApplicationContext()))) {
457            mActivity.finish();
458        }
459    }
460
461    @Override
462    public boolean shouldShowFirstConversation() {
463        return Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())
464                && shouldEnterSearchConvMode();
465    }
466
467    @Override
468    public void onUndoAvailable(ToastBarOperation op) {
469        final int mode = mViewMode.getMode();
470        final ConversationListFragment convList = getConversationListFragment();
471
472        repositionToastBar(op);
473
474        switch (mode) {
475            case ViewMode.SEARCH_RESULTS_LIST:
476            case ViewMode.CONVERSATION_LIST:
477            case ViewMode.SEARCH_RESULTS_CONVERSATION:
478            case ViewMode.CONVERSATION:
479                if (convList != null) {
480                    mToastBar.show(getUndoClickedListener(convList.getAnimatedAdapter()),
481                            0,
482                            Utils.convertHtmlToPlainText
483                                (op.getDescription(mActivity.getActivityContext())),
484                            true, /* showActionIcon */
485                            R.string.undo,
486                            true,  /* replaceVisibleToast */
487                            op);
488                }
489        }
490    }
491
492    public void repositionToastBar(ToastBarOperation op) {
493        repositionToastBar(op.isBatchUndo());
494    }
495
496    /**
497     * Set the toast bar's layout params to position it in the right place
498     * depending the current view mode.
499     *
500     * @param convModeShowInList if we're in conversation mode, should the toast
501     *            bar appear over the list? no effect when not in conversation mode.
502     */
503    private void repositionToastBar(boolean convModeShowInList) {
504        final int mode = mViewMode.getMode();
505        final FrameLayout.LayoutParams params =
506                (FrameLayout.LayoutParams) mToastBar.getLayoutParams();
507        switch (mode) {
508            case ViewMode.SEARCH_RESULTS_LIST:
509            case ViewMode.CONVERSATION_LIST:
510                params.width = mLayout.computeConversationListWidth() - params.leftMargin
511                        - params.rightMargin;
512                params.gravity = Gravity.BOTTOM | Gravity.RIGHT;
513                mToastBar.setLayoutParams(params);
514                break;
515            case ViewMode.SEARCH_RESULTS_CONVERSATION:
516            case ViewMode.CONVERSATION:
517                if (convModeShowInList && !mLayout.isConversationListCollapsed()) {
518                    // Show undo bar in the conversation list.
519                    params.gravity = Gravity.BOTTOM | Gravity.LEFT;
520                    params.width = mLayout.computeConversationListWidth() - params.leftMargin
521                            - params.rightMargin;
522                    mToastBar.setLayoutParams(params);
523                } else {
524                    // Show undo bar in the conversation.
525                    params.gravity = Gravity.BOTTOM | Gravity.RIGHT;
526                    params.width = mLayout.computeConversationWidth() - params.leftMargin
527                            - params.rightMargin;
528                    mToastBar.setLayoutParams(params);
529                }
530                break;
531        }
532    }
533
534    @Override
535    protected void hideOrRepositionToastBar(final boolean animated) {
536        final int oldViewMode = mViewMode.getMode();
537        mLayout.postDelayed(new Runnable() {
538                @Override
539            public void run() {
540                if (/* the touch did not open a conversation */oldViewMode == mViewMode.getMode() ||
541                /* animation has ended */!mToastBar.isAnimating()) {
542                    mToastBar.hide(animated, false /* actionClicked */);
543                } else {
544                    // the touch opened a conversation, reposition undo bar
545                    repositionToastBar(mToastBar.getOperation());
546                }
547            }
548        },
549        /* Give time for ViewMode to change from the touch */
550        mContext.getResources().getInteger(R.integer.dismiss_undo_bar_delay_ms));
551    }
552
553    @Override
554    public void onError(final Folder folder, boolean replaceVisibleToast) {
555        repositionToastBar(true /* convModeShowInList */);
556        showErrorToast(folder, replaceVisibleToast);
557    }
558
559    @Override
560    public boolean isDrawerEnabled() {
561        return mLayout.isDrawerEnabled();
562    }
563
564    @Override
565    public int getFolderListViewChoiceMode() {
566        // By default, we want to allow one item to be selected in the folder list
567        return ListView.CHOICE_MODE_SINGLE;
568    }
569
570    private int mMiscellaneousViewTransactionId = -1;
571
572    @Override
573    public void launchFragment(final Fragment fragment, final int selectPosition) {
574        final int containerViewId = TwoPaneLayout.MISCELLANEOUS_VIEW_ID;
575
576        final FragmentManager fragmentManager = mActivity.getFragmentManager();
577        final FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
578        fragmentTransaction.addToBackStack(null);
579        fragmentTransaction.replace(containerViewId, fragment, TAG_CUSTOM_FRAGMENT);
580        mMiscellaneousViewTransactionId = fragmentTransaction.commitAllowingStateLoss();
581        fragmentManager.executePendingTransactions();
582
583        if (selectPosition >= 0) {
584            getConversationListFragment().setRawSelected(selectPosition, true);
585        }
586    }
587}
588