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