TwoPaneController.java revision dd6a7ce32c4003bd0941e2f18fcf5b80b5cd43c5
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.text.Html;
27import android.text.TextUtils;
28import android.view.Gravity;
29import android.widget.FrameLayout;
30
31import com.android.mail.ConversationListContext;
32import com.android.mail.R;
33import com.android.mail.providers.Account;
34import com.android.mail.providers.Conversation;
35import com.android.mail.providers.Folder;
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 */
43
44// Called TwoPaneActivityController in Gmail.
45public final class TwoPaneController extends AbstractActivityController {
46    private TwoPaneLayout mLayout;
47    private Conversation mConversationToShow;
48
49    /**
50     * @param activity
51     * @param viewMode
52     */
53    public TwoPaneController(MailActivity activity, ViewMode viewMode) {
54        super(activity, viewMode);
55    }
56
57    /**
58     * Display the conversation list fragment.
59     * @param show
60     */
61    private void initializeConversationListFragment(boolean show) {
62        if (show) {
63            if (Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())) {
64                if (Utils.showTwoPaneSearchResults(mActivity.getActivityContext())) {
65                    mViewMode.enterSearchResultsConversationMode();
66                } else {
67                    mViewMode.enterSearchResultsListMode();
68                }
69            } else {
70                mViewMode.enterConversationListMode();
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        Fragment conversationListFragment = ConversationListFragment.newInstance(mConvListContext);
87        fragmentTransaction.replace(R.id.conversation_list_pane, conversationListFragment,
88                TAG_CONVERSATION_LIST);
89        fragmentTransaction.commitAllowingStateLoss();
90    }
91
92    /**
93     * Render the folder list in the correct pane.
94     */
95    private void renderFolderList() {
96        if (mActivity == null) {
97            return;
98        }
99        createFolderListFragment(null, mAccount.folderListUri);
100    }
101
102    private void createFolderListFragment(Folder parent, Uri uri) {
103        setHierarchyFolder(parent);
104        // Create a sectioned FolderListFragment.
105        FolderListFragment folderListFragment = FolderListFragment.newInstance(parent, uri, true);
106        FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction();
107        if (Utils.useFolderListFragmentTransition(mActivity.getActivityContext())) {
108            fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
109        }
110        fragmentTransaction.replace(R.id.content_pane, folderListFragment, TAG_FOLDER_LIST);
111        fragmentTransaction.commitAllowingStateLoss();
112        // Since we are showing the folder list, we are at the start of the view
113        // stack.
114        resetActionBarIcon();
115    }
116
117    @Override
118    protected boolean isConversationListVisible() {
119        return !mLayout.isConversationListCollapsed();
120    }
121
122    @Override
123    public void showConversationList(ConversationListContext listContext) {
124        super.showConversationList(listContext);
125        initializeConversationListFragment(true);
126    }
127
128    @Override
129    public void showFolderList() {
130        // On two-pane layouts, showing the folder list takes you to the top level of the
131        // application, which is the same as pressing the Up button
132        onUpPressed();
133    }
134
135    @Override
136    public boolean onCreate(Bundle savedState) {
137        mActivity.setContentView(R.layout.two_pane_activity);
138        mLayout = (TwoPaneLayout) mActivity.findViewById(R.id.two_pane_activity);
139        if (mLayout == null) {
140            // We need the layout for everything. Crash early if it is null.
141            LogUtils.wtf(LOG_TAG, "mLayout is null!");
142        }
143        mLayout.setController(this, Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction()));
144
145        // 2-pane layout is the main listener of view mode changes, and issues secondary
146        // notifications upon animation completion:
147        // (onConversationVisibilityChanged, onConversationListVisibilityChanged)
148        mViewMode.addListener(mLayout);
149        final boolean isParentInitialized = super.onCreate(savedState);
150        return isParentInitialized;
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 onAccountChanged(Account account) {
163        super.onAccountChanged(account);
164        renderFolderList();
165    }
166
167    @Override
168    public void onFolderChanged(Folder folder) {
169        super.onFolderChanged(folder);
170        exitCabMode();
171        final FolderListFragment folderList = getFolderListFragment();
172        if (folderList == null && mViewMode.getMode() == ViewMode.CONVERSATION_LIST) {
173            // Create a folder list fragment if none exists.
174            renderFolderList();
175        }
176    }
177
178    @Override
179    public void onFolderSelected(Folder folder) {
180        if (folder.hasChildren && !folder.equals(getHierarchyFolder())) {
181            // Replace this fragment with a new FolderListFragment
182            // showing this folder's children if we are not already looking
183            // at the child view for this folder.
184            createFolderListFragment(folder, folder.childFoldersListUri);
185            // Show the up affordance when digging into child folders.
186            mActionBarView.setBackButton();
187            super.onFolderSelected(folder);
188        } else {
189            setHierarchyFolder(folder);
190            super.onFolderSelected(folder);
191        }
192    }
193
194    private void goUpFolderHierarchy(Folder current) {
195        Folder parent = current.parent;
196        if (parent.parent != null) {
197            createFolderListFragment(parent.parent, parent.parent.childFoldersListUri);
198            // Show the up affordance when digging into child folders.
199            mActionBarView.setBackButton();
200        } else {
201            onFolderSelected(parent);
202        }
203    }
204
205    @Override
206    public void onViewModeChanged(int newMode) {
207        super.onViewModeChanged(newMode);
208        if (newMode != ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION) {
209            // Clear the wait fragment
210            hideWaitForInitialization();
211        }
212        // In conversation mode, if the conversation list is not visible, then the user cannot
213        // see the selected conversations. Disable the CAB mode while leaving the selected set
214        // untouched.
215        // When the conversation list is made visible again, try to enable the CAB
216        // mode if any conversations are selected.
217        if (newMode == ViewMode.CONVERSATION || newMode == ViewMode.CONVERSATION_LIST){
218            enableOrDisableCab();
219        }
220        resetActionBarIcon();
221    }
222
223    @Override
224    public void onConversationVisibilityChanged(boolean visible) {
225        super.onConversationVisibilityChanged(visible);
226        if (!visible) {
227            mPagerController.hide(false /* changeVisibility */);
228        } else if (mConversationToShow != null) {
229            mPagerController.show(mAccount, mFolder, mConversationToShow,
230                    false /* changeVisibility */);
231            mConversationToShow = null;
232        }
233    }
234
235    @Override
236    public void onConversationListVisibilityChanged(boolean visible) {
237        super.onConversationListVisibilityChanged(visible);
238        enableOrDisableCab();
239    }
240
241    @Override
242    public void resetActionBarIcon() {
243        // If the viewmode is not set, preserve existing icon.
244        if (mViewMode.getMode() == ViewMode.UNKNOWN) {
245            return;
246        }
247        if (mViewMode.isListMode()) {
248            mActionBarView.removeBackButton();
249        } else {
250            mActionBarView.setBackButton();
251        }
252    }
253
254    /**
255     * Enable or disable the CAB mode based on the visibility of the conversation list fragment.
256     */
257    private final void enableOrDisableCab() {
258        if (mLayout.isConversationListCollapsed()) {
259            disableCabMode();
260        } else {
261            enableCabMode();
262        }
263    }
264
265    @Override
266    protected void showConversation(Conversation conversation, boolean inLoaderCallbacks) {
267        super.showConversation(conversation, inLoaderCallbacks);
268
269        // 2-pane can ignore inLoaderCallbacks because it doesn't use
270        // FragmentManager.popBackStack().
271
272        if (mActivity == null) {
273            return;
274        }
275        if (conversation == null) {
276            onBackPressed();
277            return;
278        }
279        // If conversation list is not visible, then the user cannot see the CAB mode, so exit it.
280        // This is needed here (in addition to during viewmode changes) because orientation changes
281        // while viewing a conversation don't change the viewmode: the mode stays
282        // ViewMode.CONVERSATION and yet the conversation list goes in and out of visibility.
283        enableOrDisableCab();
284
285        // When a mode change is required, wait for onConversationVisibilityChanged(), the signal
286        // that the mode change animation has finished, before rendering the conversation.
287        mConversationToShow = conversation;
288
289        final int mode = mViewMode.getMode();
290        boolean changedMode = false;
291        if (mode == ViewMode.SEARCH_RESULTS_LIST || mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
292            changedMode = mViewMode.enterSearchResultsConversationMode();
293        } else {
294            changedMode = mViewMode.enterConversationMode();
295        }
296        // load the conversation immediately if we're already in conversation mode
297        if (!changedMode) {
298            onConversationVisibilityChanged(true);
299        }
300    }
301
302    @Override
303    public void setCurrentConversation(Conversation conversation) {
304        long oldId = mCurrentConversation != null ? mCurrentConversation.id : -1;
305        long newId = conversation != null ? conversation.id : -1;
306        boolean different = oldId != newId;
307        super.setCurrentConversation(conversation);
308        final ConversationListFragment convList = getConversationListFragment();
309        if (convList != null && conversation != null) {
310            convList.setSelected(conversation.position, different);
311        }
312    }
313
314    @Override
315    public void showWaitForInitialization() {
316        super.showWaitForInitialization();
317
318        final Fragment waitFragment = WaitFragment.newInstance(mAccount);
319        FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction();
320        fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
321        fragmentTransaction.replace(R.id.wait, waitFragment, TAG_WAIT);
322        fragmentTransaction.commitAllowingStateLoss();
323    }
324
325    @Override
326    protected void hideWaitForInitialization() {
327        final FragmentManager manager = mActivity.getFragmentManager();
328        final WaitFragment waitFragment = (WaitFragment)manager.findFragmentByTag(TAG_WAIT);
329        if (waitFragment != null) {
330            FragmentTransaction fragmentTransaction =
331                    mActivity.getFragmentManager().beginTransaction();
332            fragmentTransaction.remove(waitFragment);
333            fragmentTransaction.commitAllowingStateLoss();
334        }
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 onUpPressed() {
349        int mode = mViewMode.getMode();
350        if (mode == ViewMode.CONVERSATION) {
351            mActivity.onBackPressed();
352        } else if (mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
353            if (mLayout.isConversationListCollapsed()
354                    || (ConversationListContext.isSearchResult(mConvListContext) && !Utils.
355                            showTwoPaneSearchResults(mActivity.getApplicationContext()))) {
356                onBackPressed();
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 onBackPressed() {
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        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                && Utils.showTwoPaneSearchResults(mActivity.getApplicationContext());
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()) {
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 FrameLayout.LayoutParams params =
458                (FrameLayout.LayoutParams) mToastBar.getLayoutParams();
459        final ConversationListFragment convList = getConversationListFragment();
460        int undoBarWidth = getUndoBarWidth(mode, op);
461        switch (mode) {
462            case ViewMode.SEARCH_RESULTS_LIST:
463            case ViewMode.CONVERSATION_LIST:
464                params.width = undoBarWidth - params.leftMargin - params.rightMargin;
465                params.gravity = Gravity.BOTTOM | Gravity.RIGHT;
466                mToastBar.setLayoutParams(params);
467                mToastBar.setConversationMode(false);
468                if (convList != null) {
469                    mToastBar.show(
470                            getUndoClickedListener(convList.getAnimatedAdapter()),
471                            0,
472                            Html.fromHtml(op.getDescription(mActivity.getActivityContext(),
473                                    mFolder)),
474                            true, /* showActionIcon */
475                            R.string.undo,
476                            true,  /* replaceVisibleToast */
477                            op);
478                }
479                break;
480            case ViewMode.SEARCH_RESULTS_CONVERSATION:
481            case ViewMode.CONVERSATION:
482                if (op.isBatchUndo()) {
483                    // Show undo bar in the conversation list.
484                    params.gravity = Gravity.BOTTOM | Gravity.LEFT;
485                    params.width = undoBarWidth - params.leftMargin - params.rightMargin;
486                    mToastBar.setLayoutParams(params);
487                    mToastBar.setConversationMode(false);
488                } else {
489                    // Show undo bar in the conversation.
490                    params.gravity = Gravity.BOTTOM | Gravity.RIGHT;
491                    params.width = undoBarWidth - params.leftMargin - params.rightMargin;
492                    mToastBar.setLayoutParams(params);
493                    mToastBar.setConversationMode(true);
494                }
495                if (convList != null) {
496                    mToastBar.show(getUndoClickedListener(convList.getAnimatedAdapter()), 0, Html
497                            .fromHtml(op.getDescription(mActivity.getActivityContext(), mFolder)),
498                            true, /* showActionIcon */
499                            R.string.undo, true, /* replaceVisibleToast */
500                            op);
501                }
502                break;
503        }
504    }
505
506    @Override
507    public void onError(final Folder folder, boolean replaceVisibleToast) {
508        final int mode = mViewMode.getMode();
509        final FrameLayout.LayoutParams params =
510                (FrameLayout.LayoutParams) mToastBar.getLayoutParams();
511        switch (mode) {
512            case ViewMode.SEARCH_RESULTS_LIST:
513            case ViewMode.CONVERSATION_LIST:
514                params.width = mLayout.computeConversationListWidth()
515                        - params.leftMargin - params.rightMargin;
516                params.gravity = Gravity.BOTTOM | Gravity.RIGHT;
517                mToastBar.setLayoutParams(params);
518                break;
519            case ViewMode.SEARCH_RESULTS_CONVERSATION:
520            case ViewMode.CONVERSATION:
521                // Show error bar in the conversation list.
522                params.gravity = Gravity.BOTTOM | Gravity.LEFT;
523                params.width = mLayout.computeConversationListWidth()
524                        - params.leftMargin - params.rightMargin;
525                mToastBar.setLayoutParams(params);
526                break;
527        }
528
529        showErrorToast(folder, replaceVisibleToast);
530    }
531}
532