TwoPaneController.java revision 9860f5ebcd77bce62f76ccf46cee939fe4f2858a
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.FragmentTransaction;
22import android.content.Intent;
23import android.net.Uri;
24import android.os.Bundle;
25import android.view.Gravity;
26import android.widget.FrameLayout;
27
28import com.android.mail.ConversationListContext;
29import com.android.mail.R;
30import com.android.mail.providers.Account;
31import com.android.mail.providers.Conversation;
32import com.android.mail.providers.Folder;
33import com.android.mail.utils.LogUtils;
34import com.android.mail.utils.Utils;
35
36/**
37 * Controller for two-pane Mail activity. Two Pane is used for tablets, where screen real estate
38 * abounds.
39 */
40public final class TwoPaneController extends AbstractActivityController {
41    private TwoPaneLayout mLayout;
42    private Conversation mConversationToShow;
43
44    public TwoPaneController(MailActivity activity, ViewMode viewMode) {
45        super(activity, viewMode);
46    }
47
48    /**
49     * Display the conversation list fragment.
50     */
51    private void initializeConversationListFragment() {
52        if (Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())) {
53            if (shouldEnterSearchConvMode()) {
54                mViewMode.enterSearchResultsConversationMode();
55            } else {
56                mViewMode.enterSearchResultsListMode();
57            }
58        } else {
59            mViewMode.enterConversationListMode();
60        }
61        renderConversationList();
62    }
63
64    /**
65     * Render the conversation list in the correct pane.
66     */
67    private void renderConversationList() {
68        if (mActivity == null) {
69            return;
70        }
71        FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction();
72        // Use cross fading animation.
73        fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
74        Fragment conversationListFragment = ConversationListFragment.newInstance(mConvListContext);
75        fragmentTransaction.replace(R.id.conversation_list_pane, conversationListFragment,
76                TAG_CONVERSATION_LIST);
77        fragmentTransaction.commitAllowingStateLoss();
78    }
79
80    @Override
81    public boolean doesActionChangeConversationListVisibility(int action) {
82        switch (action) {
83            case R.id.settings:
84            case R.id.compose:
85            case R.id.help_info_menu_item:
86            case R.id.manage_folders_item:
87            case R.id.folder_options:
88            case R.id.feedback_menu_item:
89                return true;
90            default:
91                return false;
92        }
93    }
94
95    /**
96     * Render the folder list in the correct pane.
97     */
98    private void renderFolderList() {
99        if (mActivity == null) {
100            return;
101        }
102        createFolderListFragment(null, mAccount.folderListUri);
103    }
104
105    private void createFolderListFragment(Folder parent, Uri uri) {
106        setHierarchyFolder(parent);
107        // Create a sectioned FolderListFragment.
108        FolderListFragment folderListFragment = FolderListFragment.newInstance(parent, true);
109        FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction();
110        if (Utils.useFolderListFragmentTransition(mActivity.getActivityContext())) {
111            fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
112        }
113        fragmentTransaction.replace(R.id.content_pane, folderListFragment, TAG_FOLDER_LIST);
114        fragmentTransaction.commitAllowingStateLoss();
115        // We only set the action bar if the viewmode has been set previously. Otherwise, we leave
116        // the action bar in the state it is currently in.
117        if (mViewMode.getMode() != ViewMode.UNKNOWN) {
118            resetActionBarIcon();
119        }
120    }
121
122    @Override
123    protected boolean isConversationListVisible() {
124        return !mLayout.isConversationListCollapsed();
125    }
126
127    @Override
128    public void showConversationList(ConversationListContext listContext) {
129        super.showConversationList(listContext);
130        initializeConversationListFragment();
131    }
132
133    @Override
134    public boolean onCreate(Bundle savedState) {
135        mActivity.setContentView(R.layout.two_pane_activity);
136        mLayout = (TwoPaneLayout) mActivity.findViewById(R.id.two_pane_activity);
137        if (mLayout == null) {
138            // We need the layout for everything. Crash early if it is null.
139            LogUtils.wtf(LOG_TAG, "mLayout is null!");
140        }
141        mLayout.setController(this, Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction()));
142
143        // 2-pane layout is the main listener of view mode changes, and issues secondary
144        // notifications upon animation completion:
145        // (onConversationVisibilityChanged, onConversationListVisibilityChanged)
146        mViewMode.addListener(mLayout);
147        return super.onCreate(savedState);
148    }
149
150    @Override
151    public void onWindowFocusChanged(boolean hasFocus) {
152        if (hasFocus && !mLayout.isConversationListCollapsed()) {
153            // The conversation list is visible.
154            informCursorVisiblity(true);
155        }
156    }
157
158    @Override
159    public void changeAccount(Account account) {
160        super.changeAccount(account);
161        renderFolderList();
162    }
163
164    @Override
165    public void onFolderChanged(Folder folder) {
166        super.onFolderChanged(folder);
167        exitCabMode();
168        final FolderListFragment folderList = getFolderListFragment();
169        if (folderList == null && mViewMode.getMode() == ViewMode.CONVERSATION_LIST) {
170            // Create a folder list fragment if none exists.
171            renderFolderList();
172        }
173    }
174
175    @Override
176    public void onFolderSelected(Folder folder) {
177        if (folder.hasChildren && !folder.equals(getHierarchyFolder())) {
178            // Replace this fragment with a new FolderListFragment
179            // showing this folder's children if we are not already looking
180            // at the child view for this folder.
181            createFolderListFragment(folder, folder.childFoldersListUri);
182            // Show the up affordance when digging into child folders.
183            mActionBarView.setBackButton();
184        } else {
185            setHierarchyFolder(folder);
186        }
187        super.onFolderSelected(folder);
188    }
189
190    private void goUpFolderHierarchy(Folder current) {
191        Folder parent = current.parent;
192        if (parent.parent != null) {
193            createFolderListFragment(parent.parent, parent.parent.childFoldersListUri);
194            // Show the up affordance when digging into child folders.
195            mActionBarView.setBackButton();
196        } else {
197            onFolderSelected(parent);
198        }
199    }
200
201    @Override
202    public void onViewModeChanged(int newMode) {
203        super.onViewModeChanged(newMode);
204        if (newMode != ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION) {
205            // Clear the wait fragment
206            hideWaitForInitialization();
207        }
208        // In conversation mode, if the conversation list is not visible, then the user cannot
209        // see the selected conversations. Disable the CAB mode while leaving the selected set
210        // untouched.
211        // When the conversation list is made visible again, try to enable the CAB
212        // mode if any conversations are selected.
213        if (newMode == ViewMode.CONVERSATION || newMode == ViewMode.CONVERSATION_LIST){
214            enableOrDisableCab();
215        }
216    }
217
218    @Override
219    public void onConversationVisibilityChanged(boolean visible) {
220        super.onConversationVisibilityChanged(visible);
221        if (!visible) {
222            mPagerController.hide(false /* changeVisibility */);
223        } else if (mConversationToShow != null) {
224            mPagerController.show(mAccount, mFolder, mConversationToShow,
225                    false /* changeVisibility */);
226            mConversationToShow = null;
227        }
228    }
229
230    @Override
231    public void onConversationListVisibilityChanged(boolean visible) {
232        super.onConversationListVisibilityChanged(visible);
233        enableOrDisableCab();
234    }
235
236    @Override
237    public void resetActionBarIcon() {
238        // On two-pane, the back button is only removed in the conversation list mode, and shown
239        // for every other condition.
240        if (mViewMode.isListMode() || mViewMode.isWaitingForSync()) {
241            mActionBarView.removeBackButton();
242        } else {
243            mActionBarView.setBackButton();
244        }
245    }
246
247    /**
248     * Enable or disable the CAB mode based on the visibility of the conversation list fragment.
249     */
250    private void enableOrDisableCab() {
251        if (mLayout.isConversationListCollapsed()) {
252            disableCabMode();
253        } else {
254            enableCabMode();
255        }
256    }
257
258    @Override
259    protected void showConversation(Conversation conversation, boolean inLoaderCallbacks) {
260        super.showConversation(conversation, inLoaderCallbacks);
261
262        // 2-pane can ignore inLoaderCallbacks because it doesn't use
263        // FragmentManager.popBackStack().
264
265        if (mActivity == null) {
266            return;
267        }
268        if (conversation == null) {
269            handleBackPress();
270            return;
271        }
272        // If conversation list is not visible, then the user cannot see the CAB mode, so exit it.
273        // This is needed here (in addition to during viewmode changes) because orientation changes
274        // while viewing a conversation don't change the viewmode: the mode stays
275        // ViewMode.CONVERSATION and yet the conversation list goes in and out of visibility.
276        enableOrDisableCab();
277
278        // When a mode change is required, wait for onConversationVisibilityChanged(), the signal
279        // that the mode change animation has finished, before rendering the conversation.
280        mConversationToShow = conversation;
281
282        final int mode = mViewMode.getMode();
283        final boolean changedMode;
284        if (mode == ViewMode.SEARCH_RESULTS_LIST || mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
285            changedMode = mViewMode.enterSearchResultsConversationMode();
286        } else {
287            changedMode = mViewMode.enterConversationMode();
288        }
289        // load the conversation immediately if we're already in conversation mode
290        if (!changedMode) {
291            onConversationVisibilityChanged(true);
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.wait, 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    }
336
337    /**
338     * Up works as follows:
339     * 1) If the user is in a conversation and:
340     *  a) the conversation list is hidden (portrait mode), shows the conv list and
341     *  stays in conversation view mode.
342     *  b) the conversation list is shown, goes back to conversation list mode.
343     * 2) If the user is in search results, up exits search.
344     * mode and returns the user to whatever view they were in when they began search.
345     * 3) If the user is in conversation list mode, there is no up.
346     */
347    @Override
348    public boolean handleUpPress() {
349        int mode = mViewMode.getMode();
350        if (mode == ViewMode.CONVERSATION) {
351            handleBackPress();
352        } else if (mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
353            if (mLayout.isConversationListCollapsed()
354                    || (ConversationListContext.isSearchResult(mConvListContext) && !Utils.
355                            showTwoPaneSearchResults(mActivity.getApplicationContext()))) {
356                handleBackPress();
357            } else {
358                mActivity.finish();
359            }
360        } else if (mode == ViewMode.SEARCH_RESULTS_LIST) {
361            mActivity.finish();
362        } else if (mode == ViewMode.CONVERSATION_LIST) {
363            popView(true);
364        }
365        return true;
366    }
367
368    @Override
369    public boolean handleBackPress() {
370        // Clear any visible undo bars.
371        mToastBar.hide(false);
372        popView(false);
373        return true;
374    }
375
376    /**
377     * Pops the "view stack" to the last screen the user was viewing.
378     *
379     * @param preventClose Whether to prevent closing the app if the stack is empty.
380     */
381    protected void popView(boolean preventClose) {
382        // If the user is in search query entry mode, or the user is viewing
383        // search results, exit
384        // the mode.
385        int mode = mViewMode.getMode();
386        if (mode == ViewMode.SEARCH_RESULTS_LIST) {
387            mActivity.finish();
388        } else if (mode == ViewMode.CONVERSATION) {
389            // Go to conversation list.
390            mViewMode.enterConversationListMode();
391        } else if (mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
392            mViewMode.enterSearchResultsListMode();
393        } else {
394            // The Folder List fragment can be null for monkeys where we get a back before the
395            // folder list has had a chance to initialize.
396            final FolderListFragment folderList = getFolderListFragment();
397            if (mode == ViewMode.CONVERSATION_LIST && folderList != null
398                    && folderList.showingHierarchy()) {
399                // If the user navigated via the left folders list into a child folder,
400                // back should take the user up to the parent folder's conversation list.
401                final Folder hierarchyFolder = getHierarchyFolder();
402                if (hierarchyFolder.parent != null) {
403                    goUpFolderHierarchy(hierarchyFolder);
404                } else  {
405                    // Show inbox; we are at the top of the hierarchy we were
406                    // showing, and it doesn't have a parent, so we must want to
407                    // the basic account folder list.
408                    createFolderListFragment(null, mAccount.folderListUri);
409                    loadAccountInbox();
410                }
411            } else if (!preventClose) {
412                // There is nothing else to pop off the stack.
413                mActivity.finish();
414            }
415        }
416    }
417
418    @Override
419    public void exitSearchMode() {
420        final int mode = mViewMode.getMode();
421        if (mode == ViewMode.SEARCH_RESULTS_LIST
422                || (mode == ViewMode.SEARCH_RESULTS_CONVERSATION
423                        && Utils.showTwoPaneSearchResults(mActivity.getApplicationContext()))) {
424            mActivity.finish();
425        }
426    }
427
428    @Override
429    public boolean shouldShowFirstConversation() {
430        return Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())
431                && shouldEnterSearchConvMode();
432    }
433
434    private int getUndoBarWidth(int mode, ToastBarOperation op) {
435        int resId = -1;
436        switch (mode) {
437            case ViewMode.SEARCH_RESULTS_LIST:
438                resId = R.dimen.undo_bar_width_search_list;
439                break;
440            case ViewMode.CONVERSATION_LIST:
441                resId = R.dimen.undo_bar_width_list;
442                break;
443            case ViewMode.SEARCH_RESULTS_CONVERSATION:
444            case ViewMode.CONVERSATION:
445                if (op.isBatchUndo() && !mLayout.isConversationListCollapsed()) {
446                    resId = R.dimen.undo_bar_width_conv_list;
447                } else {
448                    resId = R.dimen.undo_bar_width_conv;
449                }
450        }
451        return resId != -1 ? (int) mContext.getResources().getDimension(resId) : -1;
452    }
453
454    @Override
455    public void onUndoAvailable(ToastBarOperation op) {
456        final int mode = mViewMode.getMode();
457        final ConversationListFragment convList = getConversationListFragment();
458
459        repositionToastBar(op);
460
461        switch (mode) {
462            case ViewMode.SEARCH_RESULTS_LIST:
463            case ViewMode.CONVERSATION_LIST:
464                if (convList != null) {
465                    mToastBar.show(
466                            getUndoClickedListener(convList.getAnimatedAdapter()),
467                            0,
468                            Utils.convertHtmlToPlainText
469                                (op.getDescription(mActivity.getActivityContext())),
470                            true, /* showActionIcon */
471                            R.string.undo,
472                            true,  /* replaceVisibleToast */
473                            op);
474                }
475                break;
476            case ViewMode.SEARCH_RESULTS_CONVERSATION:
477            case ViewMode.CONVERSATION:
478                if (convList != null) {
479                    mToastBar.show(getUndoClickedListener(convList.getAnimatedAdapter()), 0,
480                            Utils.convertHtmlToPlainText
481                                (op.getDescription(mActivity.getActivityContext())),
482                            true, /* showActionIcon */
483                            R.string.undo, true, /* replaceVisibleToast */
484                            op);
485                }
486                break;
487        }
488    }
489
490    public void repositionToastBar(ToastBarOperation op) {
491        final int mode = mViewMode.getMode();
492        final FrameLayout.LayoutParams params =
493                (FrameLayout.LayoutParams) mToastBar.getLayoutParams();
494        int undoBarWidth = getUndoBarWidth(mode, op);
495        switch (mode) {
496            case ViewMode.SEARCH_RESULTS_LIST:
497            case ViewMode.CONVERSATION_LIST:
498                params.width = undoBarWidth - params.leftMargin - params.rightMargin;
499                params.gravity = Gravity.BOTTOM | Gravity.RIGHT;
500                mToastBar.setLayoutParams(params);
501                mToastBar.setConversationMode(false);
502                break;
503            case ViewMode.SEARCH_RESULTS_CONVERSATION:
504            case ViewMode.CONVERSATION:
505                if (op.isBatchUndo() && !mLayout.isConversationListCollapsed()) {
506                    // Show undo bar in the conversation list.
507                    params.gravity = Gravity.BOTTOM | Gravity.LEFT;
508                    params.width = undoBarWidth - params.leftMargin - params.rightMargin;
509                    mToastBar.setLayoutParams(params);
510                    mToastBar.setConversationMode(false);
511                } else {
512                    // Show undo bar in the conversation.
513                    params.gravity = Gravity.BOTTOM | Gravity.RIGHT;
514                    params.width = undoBarWidth - params.leftMargin - params.rightMargin;
515                    mToastBar.setLayoutParams(params);
516                    mToastBar.setConversationMode(true);
517                }
518                break;
519        }
520    }
521
522    @Override
523    protected void hideOrRepositionToastBar(final boolean animated) {
524        final int oldViewMode = mViewMode.getMode();
525        mLayout.postDelayed(new Runnable() {
526                @Override
527            public void run() {
528                if (/* the touch did not open a conversation */oldViewMode == mViewMode.getMode() ||
529                /* animation has ended */!mToastBar.isAnimating()) {
530                    mToastBar.hide(animated);
531                } else {
532                    // the touch opened a conversation, reposition undo bar
533                    repositionToastBar(mToastBar.getOperation());
534                }
535            }
536        },
537        /* Give time for ViewMode to change from the touch */
538        mContext.getResources().getInteger(R.integer.dismiss_undo_bar_delay_ms));
539    }
540
541    @Override
542    public void onError(final Folder folder, boolean replaceVisibleToast) {
543        final int mode = mViewMode.getMode();
544        final FrameLayout.LayoutParams params =
545                (FrameLayout.LayoutParams) mToastBar.getLayoutParams();
546        switch (mode) {
547            case ViewMode.SEARCH_RESULTS_LIST:
548            case ViewMode.CONVERSATION_LIST:
549                params.width = mLayout.computeConversationListWidth()
550                        - params.leftMargin - params.rightMargin;
551                params.gravity = Gravity.BOTTOM | Gravity.RIGHT;
552                mToastBar.setLayoutParams(params);
553                break;
554            case ViewMode.SEARCH_RESULTS_CONVERSATION:
555            case ViewMode.CONVERSATION:
556                // Show error bar in the conversation list.
557                params.gravity = Gravity.BOTTOM | Gravity.LEFT;
558                params.width = mLayout.computeConversationListWidth()
559                        - params.leftMargin - params.rightMargin;
560                mToastBar.setLayoutParams(params);
561                break;
562        }
563
564        showErrorToast(folder, replaceVisibleToast);
565    }
566}
567