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