TwoPaneController.java revision 7b6d03db55338cbf9717896f99eb20d02bf371e4
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
48    /**
49     * @param activity
50     * @param viewMode
51     */
52    public TwoPaneController(MailActivity activity, ViewMode viewMode) {
53        super(activity, viewMode);
54    }
55
56    /**
57     * Display the conversation list fragment.
58     * @param show
59     */
60    private void initializeConversationListFragment(boolean show) {
61        if (show) {
62            if (Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())) {
63                if (Utils.showTwoPaneSearchResults(mActivity.getActivityContext())) {
64                    mViewMode.enterSearchResultsConversationMode();
65                } else {
66                    mViewMode.enterSearchResultsListMode();
67                }
68            } else {
69                mViewMode.enterConversationListMode();
70            }
71        }
72        renderConversationList();
73    }
74
75    /**
76     * Render the conversation list in the correct pane.
77     */
78    private void renderConversationList() {
79        if (mActivity == null) {
80            return;
81        }
82        FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction();
83        // Use cross fading animation.
84        fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
85        Fragment conversationListFragment = ConversationListFragment.newInstance(mConvListContext);
86        fragmentTransaction.replace(R.id.conversation_list_pane, conversationListFragment,
87                TAG_CONVERSATION_LIST);
88        fragmentTransaction.commitAllowingStateLoss();
89    }
90
91    /**
92     * Render the folder list in the correct pane.
93     */
94    private void renderFolderList() {
95        if (mActivity == null) {
96            return;
97        }
98        createFolderListFragment(null, mAccount.folderListUri);
99    }
100
101    private void createFolderListFragment(Folder parent, Uri uri) {
102        setHierarchyFolder(parent);
103        FolderListFragment folderListFragment = FolderListFragment.newInstance(parent, uri);
104        FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction();
105        fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
106        fragmentTransaction.replace(R.id.content_pane, folderListFragment, TAG_FOLDER_LIST);
107        fragmentTransaction.commitAllowingStateLoss();
108        // Since we are showing the folder list, we are at the start of the view
109        // stack.
110        resetActionBarIcon();
111    }
112
113    @Override
114    protected boolean isConversationListVisible() {
115        return !mLayout.isConversationListCollapsed();
116    }
117
118    @Override
119    public void showConversationList(ConversationListContext listContext) {
120        super.showConversationList(listContext);
121        initializeConversationListFragment(true);
122    }
123
124    @Override
125    public void showFolderList() {
126        // On two-pane layouts, showing the folder list takes you to the top level of the
127        // application, which is the same as pressing the Up button
128        onUpPressed();
129    }
130
131    @Override
132    public boolean onCreate(Bundle savedState) {
133        mActivity.setContentView(R.layout.two_pane_activity);
134        mLayout = (TwoPaneLayout) mActivity.findViewById(R.id.two_pane_activity);
135        if (mLayout == null) {
136            // We need the layout for everything. Crash early if it is null.
137            LogUtils.wtf(LOG_TAG, "mLayout is null!");
138        }
139        mLayout.initializeLayout(mActivity.getApplicationContext(),
140                Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction()));
141
142        // The tablet layout needs to refer to mode changes.
143        mViewMode.addListener(mLayout);
144        // The activity controller needs to listen to layout changes.
145        mLayout.setListener(this);
146        final boolean isParentInitialized = super.onCreate(savedState);
147        return isParentInitialized;
148    }
149
150    @Override
151    public void onWindowFocusChanged(boolean hasFocus) {
152        if (hasFocus && !mLayout.isConversationListCollapsed()) {
153            // The conversation list is visible.
154            Utils.setConversationCursorVisibility(mConversationListCursor, true);
155        }
156    }
157
158    @Override
159    public void onAccountChanged(Account account) {
160        super.onAccountChanged(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) {
170            folderList.selectInitialFolder(folder);
171        }
172    }
173
174    @Override
175    public void onFolderSelected(Folder folder) {
176        if (folder.hasChildren && !folder.equals(getHierarchyFolder())) {
177            // Replace this fragment with a new FolderListFragment
178            // showing this folder's children if we are not already looking
179            // at the child view for this folder.
180            createFolderListFragment(folder, folder.childFoldersListUri);
181            // Show the up affordance when digging into child folders.
182            mActionBarView.setBackButton();
183            super.onFolderSelected(folder);
184        } else {
185            setHierarchyFolder(folder);
186            super.onFolderSelected(folder);
187        }
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        // Otherwise, the conversation list is guaranteed to be visible. Try to enable the CAB
212        // mode if any conversations are selected.
213        if (newMode == ViewMode.CONVERSATION){
214            enableOrDisableCab();
215        }
216        resetActionBarIcon();
217    }
218
219    @Override
220    public void onConversationVisibilityChanged(boolean visible) {
221        super.onConversationVisibilityChanged(visible);
222        if (!visible) {
223            mPagerController.hide();
224        }
225    }
226
227    @Override
228    public void onConversationListVisibilityChanged(boolean visible) {
229        super.onConversationListVisibilityChanged(visible);
230    }
231
232    @Override
233    public void resetActionBarIcon() {
234        if (mViewMode.isListMode()) {
235            mActionBarView.removeBackButton();
236        } else {
237            mActionBarView.setBackButton();
238        }
239    }
240
241    /**
242     * Enable or disable the CAB mode based on the visibility of the conversation list fragment.
243     */
244    private final void enableOrDisableCab() {
245        if (mLayout.isConversationListCollapsed()) {
246            disableCabMode();
247        } else {
248            enableCabMode();
249        }
250    }
251
252    @Override
253    public void showConversation(Conversation conversation) {
254        super.showConversation(conversation);
255        if (mActivity == null) {
256            return;
257        }
258        if (conversation == null) {
259            // This is a request to remove the conversation view and show the conversation list
260            // fragment instead.
261            mPagerController.stopListening();
262            onBackPressed();
263            return;
264        }
265        // If conversation list is not visible, then the user cannot see the CAB mode, so exit it.
266        // This is needed here (in addition to during viewmode changes) because orientation changes
267        // while viewing a conversation don't change the viewmode: the mode stays
268        // ViewMode.CONVERSATION and yet the conversation list goes in and out of visibility.
269        enableOrDisableCab();
270
271        final int mode = mViewMode.getMode();
272        if (mode == ViewMode.SEARCH_RESULTS_LIST || mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
273            mViewMode.enterSearchResultsConversationMode();
274        } else {
275            mViewMode.enterConversationMode();
276        }
277        mPagerController.show(mAccount, mFolder, conversation);
278    }
279
280    @Override
281    public void setCurrentConversation(Conversation conversation) {
282        super.setCurrentConversation(conversation);
283
284        final ConversationListFragment convList = getConversationListFragment();
285        if (convList != null && conversation != null) {
286            LogUtils.d(LOG_TAG, "showConversation: Selecting position %d.", conversation.position);
287            convList.setSelected(conversation.position);
288        }
289    }
290
291    @Override
292    public void showWaitForInitialization() {
293        super.showWaitForInitialization();
294
295        Fragment waitFragment = WaitFragment.newInstance(mAccount);
296        FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction();
297        fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
298        fragmentTransaction.replace(R.id.two_pane_activity, waitFragment, TAG_WAIT);
299        fragmentTransaction.commitAllowingStateLoss();
300    }
301
302    @Override
303    public void hideWaitForInitialization() {
304        final FragmentManager manager = mActivity.getFragmentManager();
305        final WaitFragment waitFragment = (WaitFragment)manager.findFragmentByTag(TAG_WAIT);
306        if (waitFragment != null) {
307            FragmentTransaction fragmentTransaction =
308                    mActivity.getFragmentManager().beginTransaction();
309            fragmentTransaction.remove(waitFragment);
310            fragmentTransaction.commitAllowingStateLoss();
311        }
312    }
313
314    /**
315     * Up works as follows:
316     * 1) If the user is in a conversation and:
317     *  a) the conversation list is hidden (portrait mode), shows the conv list and
318     *  stays in conversation view mode.
319     *  b) the conversation list is shown, goes back to conversation list mode.
320     * 2) If the user is in search results, up exits search.
321     * mode and returns the user to whatever view they were in when they began search.
322     * 3) If the user is in conversation list mode, there is no up.
323     */
324    @Override
325    public boolean onUpPressed() {
326        int mode = mViewMode.getMode();
327        if (mode == ViewMode.CONVERSATION) {
328            mActivity.onBackPressed();
329        } else if (mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
330            if (mLayout.isConversationListCollapsed()
331                    || (mConvListContext.isSearchResult() && !Utils.
332                            showTwoPaneSearchResults(mActivity.getApplicationContext()))) {
333                onBackPressed();
334            } else {
335                mActivity.finish();
336            }
337        } else if (mode == ViewMode.SEARCH_RESULTS_LIST) {
338            mActivity.finish();
339        } else if (mode == ViewMode.CONVERSATION_LIST) {
340            popView(true);
341        }
342        return true;
343    }
344
345    @Override
346    public boolean onBackPressed() {
347        // Clear any visible undo bars.
348        mToastBar.hide(false);
349        popView(false);
350        return true;
351    }
352
353    /**
354     * Pops the "view stack" to the last screen the user was viewing.
355     *
356     * @param preventClose Whether to prevent closing the app if the stack is empty.
357     */
358    protected void popView(boolean preventClose) {
359        // If the user is in search query entry mode, or the user is viewing
360        // search results, exit
361        // the mode.
362        int mode = mViewMode.getMode();
363        if (mode == ViewMode.SEARCH_RESULTS_LIST) {
364            mActivity.finish();
365        } else if (mode == ViewMode.CONVERSATION) {
366            // Go to conversation list.
367            mViewMode.enterConversationListMode();
368        } else if (mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
369            mViewMode.enterSearchResultsListMode();
370        } else {
371            if (mode == ViewMode.CONVERSATION_LIST && getFolderListFragment().showingHierarchy()) {
372                // If the user navigated via the left folders list into a child folder,
373                // back should take the user up to the parent folder's conversation list.
374                Folder hierarchyFolder = getHierarchyFolder();
375                if (hierarchyFolder.parent != null) {
376                    goUpFolderHierarchy(hierarchyFolder);
377                } else  {
378                    // Show inbox; we are at the top of the hierarchy we were
379                    // showing, and it doesn't have a parent, so we must want to
380                    // the basic account folder list.
381                    createFolderListFragment(null, mAccount.folderListUri);
382                    loadAccountInbox();
383                }
384            } else if (!preventClose) {
385                // There is nothing else to pop off the stack.
386                mActivity.finish();
387            }
388        }
389    }
390
391    @Override
392    public void onRestoreInstanceState(Bundle inState) {
393        super.onRestoreInstanceState(inState);
394        if (inState.containsKey(SAVED_HIERARCHICAL_FOLDER)) {
395            String folderString = inState.getString(SAVED_HIERARCHICAL_FOLDER);
396            if (!TextUtils.isEmpty(folderString)) {
397                Folder folder = Folder.fromString(inState.getString(SAVED_HIERARCHICAL_FOLDER));
398                mViewMode.enterConversationListMode();
399                if (folder.hasChildren) {
400                    onFolderSelected(folder);
401                } else if (folder.parent != null) {
402                    onFolderSelected(folder.parent);
403                    setHierarchyFolder(folder);
404                }
405            }
406        }
407    }
408
409    @Override
410    public void onSaveInstanceState(Bundle outState) {
411        super.onSaveInstanceState(outState);
412        Folder hierarchyFolder = getHierarchyFolder();
413        outState.putString(SAVED_HIERARCHICAL_FOLDER,
414                hierarchyFolder != null ? Folder.toString(hierarchyFolder) : null);
415    }
416
417    @Override
418    public void exitSearchMode() {
419        int mode = mViewMode.getMode();
420        if (mode == ViewMode.SEARCH_RESULTS_LIST
421                || (mode == ViewMode.SEARCH_RESULTS_CONVERSATION
422                        && Utils.showTwoPaneSearchResults(mActivity.getApplicationContext()))) {
423            mActivity.finish();
424        }
425    }
426
427    @Override
428    public boolean shouldShowFirstConversation() {
429        return Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())
430                && Utils.showTwoPaneSearchResults(mActivity.getApplicationContext());
431    }
432
433    @Override
434    public void onUndoAvailable(ToastBarOperation op) {
435        final int mode = mViewMode.getMode();
436        final FrameLayout.LayoutParams params =
437                (FrameLayout.LayoutParams) mToastBar.getLayoutParams();
438        final ConversationListFragment convList = getConversationListFragment();
439        switch (mode) {
440            case ViewMode.SEARCH_RESULTS_LIST:
441            case ViewMode.CONVERSATION_LIST:
442                params.width = mLayout.computeConversationListWidth()
443                        - params.leftMargin - params.rightMargin;
444                params.gravity = Gravity.BOTTOM | Gravity.RIGHT;
445                mToastBar.setLayoutParams(params);
446                mToastBar.setConversationMode(false);
447                if (convList != null) {
448                    mToastBar.show(
449                            getUndoClickedListener(convList.getAnimatedAdapter()),
450                            0,
451                            Html.fromHtml(op.getDescription(mActivity.getActivityContext())),
452                            true, /* showActionIcon */
453                            R.string.undo,
454                            true,  /* replaceVisibleToast */
455                            op);
456                }
457                break;
458            case ViewMode.SEARCH_RESULTS_CONVERSATION:
459            case ViewMode.CONVERSATION:
460                if (op.isBatchUndo()) {
461                    // Show undo bar in the conversation list.
462                    params.gravity = Gravity.BOTTOM | Gravity.LEFT;
463                    params.width = mLayout.computeConversationListWidth()
464                            - params.leftMargin - params.rightMargin;
465                    mToastBar.setLayoutParams(params);
466                    mToastBar.setConversationMode(false);
467                } else {
468                    // Show undo bar in the conversation.
469                    params.gravity = Gravity.BOTTOM | Gravity.RIGHT;
470                    params.width = mLayout.getConversationView().getWidth()
471                            - params.leftMargin - params.rightMargin;
472                    mToastBar.setLayoutParams(params);
473                    mToastBar.setConversationMode(true);
474                }
475                mToastBar.show(
476                        getUndoClickedListener(convList.getAnimatedAdapter()),
477                        0,
478                        Html.fromHtml(op.getDescription(mActivity.getActivityContext())),
479                        true, /* showActionIcon */
480                        R.string.undo,
481                        true,  /* replaceVisibleToast */
482                        op);
483                break;
484        }
485    }
486
487    @Override
488    public void onError(final Folder folder, boolean replaceVisibleToast) {
489        final int mode = mViewMode.getMode();
490        final FrameLayout.LayoutParams params =
491                (FrameLayout.LayoutParams) mToastBar.getLayoutParams();
492        switch (mode) {
493            case ViewMode.SEARCH_RESULTS_LIST:
494            case ViewMode.CONVERSATION_LIST:
495                params.width = mLayout.computeConversationListWidth()
496                        - params.leftMargin - params.rightMargin;
497                params.gravity = Gravity.BOTTOM | Gravity.RIGHT;
498                mToastBar.setLayoutParams(params);
499                break;
500            case ViewMode.SEARCH_RESULTS_CONVERSATION:
501            case ViewMode.CONVERSATION:
502                // Show error bar in the conversation list.
503                params.gravity = Gravity.BOTTOM | Gravity.LEFT;
504                params.width = mLayout.computeConversationListWidth()
505                        - params.leftMargin - params.rightMargin;
506                mToastBar.setLayoutParams(params);
507                break;
508        }
509
510        showErrorToast(folder, replaceVisibleToast);
511    }
512}
513