OnePaneController.java revision 0f7ae7a2d244463f75b3d4e1f79e27305a4dcb38
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.net.Uri;
24import android.os.Bundle;
25import android.text.Html;
26
27import com.android.mail.ConversationListContext;
28import com.android.mail.R;
29import com.android.mail.providers.Account;
30import com.android.mail.providers.Conversation;
31import com.android.mail.providers.Folder;
32import com.android.mail.providers.Settings;
33import com.android.mail.providers.UIProvider;
34import com.android.mail.utils.LogUtils;
35
36/**
37 * Controller for one-pane Mail activity. One Pane is used for phones, where screen real estate is
38 * limited. This controller also does the layout, since the layout is simpler in the one pane case.
39 */
40
41// Called OnePaneActivityController in Gmail.
42public final class OnePaneController extends AbstractActivityController {
43    // Used for saving transaction IDs in bundles
44    private static final String FOLDER_LIST_TRANSACTION_KEY = "folder-list-transaction";
45    private static final String INBOX_CONVERSATION_LIST_TRANSACTION_KEY =
46            "inbox_conversation-list-transaction";
47    private static final String CONVERSATION_LIST_TRANSACTION_KEY = "conversation-list-transaction";
48    private static final String CONVERSATION_TRANSACTION_KEY = "conversation-transaction";
49
50    private static final int INVALID_ID = -1;
51    private boolean mConversationListVisible = false;
52    private int mLastInboxConversationListTransactionId = INVALID_ID;
53    private int mLastConversationListTransactionId = INVALID_ID;
54    private int mLastConversationTransactionId = INVALID_ID;
55    private int mLastFolderListTransactionId = INVALID_ID;
56    private Folder mInbox;
57    /** Whether a conversation list for this account has ever been shown.*/
58    private boolean mConversationListNeverShown = true;
59
60    /**
61     * @param activity
62     * @param viewMode
63     */
64    public OnePaneController(MailActivity activity, ViewMode viewMode) {
65        super(activity, viewMode);
66    }
67
68    @Override
69    public void onRestoreInstanceState(Bundle inState) {
70        super.onRestoreInstanceState(inState);
71        // TODO(mindyp) handle saved state.
72        if (inState != null) {
73            mLastFolderListTransactionId = inState.getInt(FOLDER_LIST_TRANSACTION_KEY, INVALID_ID);
74            mLastInboxConversationListTransactionId =
75                    inState.getInt(INBOX_CONVERSATION_LIST_TRANSACTION_KEY, INVALID_ID);
76            mLastConversationListTransactionId = inState.getInt(CONVERSATION_LIST_TRANSACTION_KEY,
77                    INVALID_ID);
78            mLastConversationTransactionId = inState.getInt(CONVERSATION_TRANSACTION_KEY,
79                    INVALID_ID);
80        }
81    }
82
83    @Override
84    public void onSaveInstanceState(Bundle outState) {
85        super.onSaveInstanceState(outState);
86        // TODO(mindyp) handle saved state.
87        outState.putInt(FOLDER_LIST_TRANSACTION_KEY, mLastFolderListTransactionId);
88        outState.putInt(INBOX_CONVERSATION_LIST_TRANSACTION_KEY,
89                mLastInboxConversationListTransactionId);
90        outState.putInt(CONVERSATION_LIST_TRANSACTION_KEY, mLastConversationListTransactionId);
91        outState.putInt(CONVERSATION_TRANSACTION_KEY, mLastConversationTransactionId);
92    }
93
94    @Override
95    public void resetActionBarIcon() {
96        final int mode = mViewMode.getMode();
97        if (!inInbox(mAccount, mConvListContext)
98                || mode == ViewMode.SEARCH_RESULTS_LIST
99                || mode == ViewMode.SEARCH_RESULTS_CONVERSATION
100                || mode == ViewMode.CONVERSATION
101                || mode == ViewMode.FOLDER_LIST) {
102            mActionBarView.setBackButton();
103        } else {
104            mActionBarView.removeBackButton();
105        }
106    }
107
108    /**
109     * Returns true if the user is currently in the conversation list view, viewing the default
110     * inbox.
111     * @return
112     */
113    private static boolean inInbox(final Account account, final ConversationListContext context) {
114        // If we don't have valid state, then we are not in the inbox.
115        if (account == null || context == null || context.folder == null
116                || account.settings == null) {
117            return false;
118        }
119        final Uri inboxUri = Settings.getDefaultInboxUri(account.settings);
120        return !context.isSearchResult() && context.folder.uri.equals(inboxUri);
121    }
122
123    @Override
124    public void onAccountChanged(Account account) {
125        super.onAccountChanged(account);
126        mConversationListNeverShown = true;
127    }
128
129    @Override
130    public boolean onCreate(Bundle savedInstanceState) {
131        mActivity.setContentView(R.layout.one_pane_activity);
132        // The parent class sets the correct viewmode and starts the application off.
133        return super.onCreate(savedInstanceState);
134    }
135
136    @Override
137    protected boolean isConversationListVisible() {
138        return mConversationListVisible;
139    }
140
141    @Override
142    public void onViewModeChanged(int newMode) {
143        super.onViewModeChanged(newMode);
144
145        // When entering conversation list mode, hide and clean up any currently visible
146        // conversation.
147        // TODO: improve this transition
148        if (newMode == ViewMode.CONVERSATION_LIST || newMode == ViewMode.SEARCH_RESULTS_LIST) {
149            mPagerController.hide();
150        }
151    }
152
153    @Override
154    public void showConversationList(ConversationListContext listContext) {
155        super.showConversationList(listContext);
156        enableCabMode();
157        // TODO(viki): Check if the account has been changed since the previous
158        // time.
159        if (listContext != null && listContext.isSearchResult()) {
160            mViewMode.enterSearchResultsListMode();
161        } else {
162            mViewMode.enterConversationListMode();
163        }
164        // TODO(viki): This account transition looks strange in two pane mode.
165        // Revisit as the app is coming together and improve the look and feel.
166        final int transition = mConversationListNeverShown
167                ? FragmentTransaction.TRANSIT_FRAGMENT_FADE
168                : FragmentTransaction.TRANSIT_FRAGMENT_OPEN;
169        Fragment conversationListFragment = ConversationListFragment.newInstance(listContext);
170
171        if (!inInbox(mAccount, mConvListContext)) {
172            // Maintain fragment transaction history so we can get back to the
173            // fragment used to launch this list.
174            mLastConversationListTransactionId = replaceFragment(conversationListFragment,
175                    transition, TAG_CONVERSATION_LIST);
176        } else {
177            // If going to the inbox, clear the folder list transaction history.
178            mInbox = listContext.folder;
179            mLastInboxConversationListTransactionId = replaceFragment(conversationListFragment,
180                    transition, TAG_CONVERSATION_LIST);
181            mLastFolderListTransactionId = INVALID_ID;
182
183            // If we ever to to the inbox, we want to unset the transation id for any other
184            // non-inbox folder.
185            mLastConversationListTransactionId = INVALID_ID;
186        }
187        mConversationListVisible = true;
188        onConversationVisibilityChanged(false);
189        onConversationListVisibilityChanged(true);
190        mConversationListNeverShown = false;
191    }
192
193    @Override
194    public void showConversation(Conversation conversation) {
195        super.showConversation(conversation);
196        if (conversation == null) {
197            // This is a request to remove the conversation view, and pop back the view stack.
198            // If we are in conversation list view already, this should be a safe thing to do, so
199            // we don't check viewmode.
200            transitionBackToConversationListMode();
201            return;
202        }
203        disableCabMode();
204        if (mConvListContext != null && mConvListContext.isSearchResult()) {
205            mViewMode.enterSearchResultsConversationMode();
206        } else {
207            mViewMode.enterConversationMode();
208        }
209
210        // Switching to conversation view is an incongruous transition: we are not replacing a
211        // fragment with another fragment as usual. Instead, reveal the heretofore inert
212        // conversation ViewPager and just remove the previously visible fragment
213        // (e.g. conversation list, or possibly label list?).
214        final FragmentManager fm = mActivity.getFragmentManager();
215        final Fragment f = fm.findFragmentById(R.id.content_pane);
216        if (f != null) {
217            final FragmentTransaction ft = fm.beginTransaction();
218            ft.addToBackStack(null);
219            ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
220            ft.remove(f);
221            ft.commitAllowingStateLoss();
222        }
223
224        // TODO: improve this transition
225        mPagerController.show(mAccount, mFolder, conversation);
226        onConversationVisibilityChanged(true);
227        resetActionBarIcon();
228
229        mConversationListVisible = false;
230        onConversationListVisibilityChanged(false);
231    }
232
233    @Override
234    public void showWaitForInitialization() {
235        super.showWaitForInitialization();
236
237        replaceFragment(WaitFragment.newInstance(mAccount),
238                FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_WAIT);
239    }
240
241    @Override
242    public void hideWaitForInitialization() {
243        transitionToInbox();
244    }
245
246    @Override
247    public void showFolderList() {
248        if (mAccount == null) {
249            LogUtils.e(LOG_TAG, "Null account in showFolderList");
250            return;
251        }
252        // Null out the currently selected folder; we have nothing selected the
253        // first time the user enters the folder list
254        setHierarchyFolder(null);
255        mViewMode.enterFolderListMode();
256        enableCabMode();
257        mLastFolderListTransactionId = replaceFragment(
258                FolderListFragment.newInstance(null, mAccount.folderListUri),
259                FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_FOLDER_LIST);
260        mConversationListVisible = false;
261        onConversationVisibilityChanged(false);
262        onConversationListVisibilityChanged(false);
263    }
264
265    /**
266     * Replace the content_pane with the fragment specified here. The tag is specified so that
267     * the {@link ActivityController} can look up the fragments through the
268     * {@link android.app.FragmentManager}.
269     * @param fragment
270     * @param transition
271     * @param tag
272     * @return transaction ID returned when the transition is committed.
273     */
274    private int replaceFragment(Fragment fragment, int transition, String tag) {
275        FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction();
276        fragmentTransaction.addToBackStack(null);
277        fragmentTransaction.setTransition(transition);
278        fragmentTransaction.replace(R.id.content_pane, fragment, tag);
279        final int transactionId = fragmentTransaction.commitAllowingStateLoss();
280        resetActionBarIcon();
281        return transactionId;
282    }
283
284    /**
285     * Back works as follows:
286     * 1) If the user is in the folder list view, go back
287     * to the account default inbox.
288     * 2) If the user is in a conversation list
289     * that is not the inbox AND:
290     *  a) they got there by going through the folder
291     *  list view, go back to the folder list view.
292     *  b) they got there by using some other means (account dropdown), go back to the inbox.
293     * 3) If the user is in a conversation, go back to the conversation list they were last in.
294     * 4) If the user is in the conversation list for the default account inbox,
295     * back exits the app.
296     */
297    @Override
298    public boolean onBackPressed() {
299        final int mode = mViewMode.getMode();
300        if (mode == ViewMode.FOLDER_LIST) {
301            if (getFolderListFragment().showingHierarchy() && mFolder != null) {
302                // If we are showing the folder list and the user is exploring
303                // the children of a single parent folder,
304                // back should display the parent folder's parent and siblings.
305                goUpFolderHierarchy(getHierarchyFolder());
306            } else {
307                // We are at the topmost list of folders; just go back to
308                // whatever conv list we were viewing before.
309                mLastFolderListTransactionId = INVALID_ID;
310                transitionToInbox();
311            }
312        } else if (mode == ViewMode.SEARCH_RESULTS_LIST) {
313            mActivity.finish();
314        } else if (mViewMode.isListMode() && !inInbox(mAccount, mConvListContext)) {
315            if (mLastFolderListTransactionId != INVALID_ID) {
316                // If the user got here by navigating via the folder list, back
317                // should bring them back to the folder list.
318                mViewMode.enterFolderListMode();
319                if (mFolder != null && mFolder.parent != null) {
320                    // If there was a parent folder, show the parent and
321                    // siblings of the current folder for which we are viewing
322                    // the conversation list.
323                    setHierarchyFolder(mFolder.parent);
324                } else {
325                    setHierarchyFolder(null);
326                }
327                mActivity.getFragmentManager().popBackStack(mLastFolderListTransactionId, 0);
328            } else {
329                mLastFolderListTransactionId = INVALID_ID;
330                transitionToInbox();
331            }
332        } else if (mode == ViewMode.CONVERSATION || mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
333            transitionBackToConversationListMode();
334        } else {
335            mActivity.finish();
336        }
337        mToastBar.hide(false);
338        return true;
339    }
340
341    private void goUpFolderHierarchy(Folder current) {
342        Folder top = current.parent;
343        if (top != null) {
344            setHierarchyFolder(top);
345            // Replace this fragment with a new FolderListFragment
346            // showing this folder's children if we are not already
347            // looking at the child view for this folder.
348            mLastFolderListTransactionId = replaceFragment(FolderListFragment.newInstance(
349                    top, top.childFoldersListUri),
350                    FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_FOLDER_LIST);
351            // Show the up affordance when digging into child folders.
352            mActionBarView.setBackButton();
353        } else {
354            // Otherwise, clear the selected folder and go back to whatever the
355            // last folder list displayed was.
356            showFolderList();
357        }
358    }
359
360    private void transitionToInbox() {
361        mViewMode.enterConversationListMode();
362        if (mInbox == null) {
363            loadAccountInbox();
364        } else {
365            ConversationListContext listContext = ConversationListContext.forFolder(mContext,
366                    mAccount, mInbox);
367            // Set the correct context for what the conversation view will be
368            // now.
369            onFolderChanged(mInbox);
370            showConversationList(listContext);
371        }
372    }
373
374    @Override
375    public void onFolderSelected(Folder folder) {
376        if (folder.hasChildren && !folder.equals(getHierarchyFolder())) {
377            mViewMode.enterFolderListMode();
378            setHierarchyFolder(folder);
379            // Replace this fragment with a new FolderListFragment
380            // showing this folder's children if we are not already
381            // looking at the child view for this folder.
382            mLastFolderListTransactionId = replaceFragment(
383                    FolderListFragment.newInstance(folder, folder.childFoldersListUri),
384                    FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_FOLDER_LIST);
385            // Show the up affordance when digging into child folders.
386            mActionBarView.setBackButton();
387        } else {
388            super.onFolderSelected(folder);
389        }
390    }
391
392    private boolean isTransactionIdValid(int id) {
393        return id >= 0;
394    }
395
396    /**
397     * Up works as follows:
398     * 1) If the user is in a conversation list that is not the default account inbox,
399     * a conversation, or the folder list, up follows the rules of back.
400     * 2) If the user is in search results, up exits search
401     * mode and returns the user to whatever view they were in when they began search.
402     * 3) If the user is in the inbox, there is no up.
403     */
404    @Override
405    public boolean onUpPressed() {
406        final int mode = mViewMode.getMode();
407        if (mode == ViewMode.SEARCH_RESULTS_LIST) {
408            mActivity.finish();
409        } else if ((!inInbox(mAccount, mConvListContext) && mViewMode.isListMode())
410                || mode == ViewMode.CONVERSATION
411                || mode == ViewMode.FOLDER_LIST
412                || mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
413            // Same as go back.
414            mActivity.onBackPressed();
415        }
416        return true;
417    }
418
419    private void transitionBackToConversationListMode() {
420        final int mode = mViewMode.getMode();
421        enableCabMode();
422        if (mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
423            mViewMode.enterSearchResultsListMode();
424        } else {
425            mViewMode.enterConversationListMode();
426        }
427        if (isTransactionIdValid(mLastConversationListTransactionId)) {
428            mActivity.getFragmentManager().popBackStack(mLastConversationListTransactionId, 0);
429            resetActionBarIcon();
430        } else if (isTransactionIdValid(mLastInboxConversationListTransactionId)) {
431            mActivity.getFragmentManager().popBackStack(mLastInboxConversationListTransactionId, 0);
432            resetActionBarIcon();
433            onFolderChanged(mInbox);
434        } else {
435            // TODO: revisit if this block is necessary
436            final ConversationListContext listContext = ConversationListContext.forFolder(mContext,
437                    mAccount, mInbox);
438            // Set the correct context for what the conversation view will be now.
439            onFolderChanged(mInbox);
440            showConversationList(listContext);
441        }
442        resetActionBarIcon();
443
444        mConversationListVisible = true;
445        onConversationVisibilityChanged(false);
446        onConversationListVisibilityChanged(true);
447    }
448
449    @Override
450    public boolean shouldShowFirstConversation() {
451        return false;
452    }
453
454    @Override
455    public void onUndoAvailable(ToastBarOperation op) {
456        if (op != null && mAccount.supportsCapability(UIProvider.AccountCapabilities.UNDO)) {
457            final int mode = mViewMode.getMode();
458            final ConversationListFragment convList = getConversationListFragment();
459            switch (mode) {
460                case ViewMode.SEARCH_RESULTS_CONVERSATION:
461                case ViewMode.CONVERSATION:
462                    mToastBar.setConversationMode(true);
463                    mToastBar.show(
464                            getUndoClickedListener(
465                                    convList != null ? convList.getAnimatedAdapter() : null),
466                            0,
467                            Html.fromHtml(op.getDescription(mActivity.getActivityContext())),
468                            true, /* showActionIcon */
469                            R.string.undo,
470                            true,  /* replaceVisibleToast */
471                            op);
472                    break;
473                case ViewMode.SEARCH_RESULTS_LIST:
474                case ViewMode.CONVERSATION_LIST:
475                    if (convList != null) {
476                        mToastBar.setConversationMode(false);
477                        mToastBar.show(
478                                getUndoClickedListener(convList.getAnimatedAdapter()),
479                                0,
480                                Html.fromHtml(op.getDescription(mActivity.getActivityContext())),
481                                true, /* showActionIcon */
482                                R.string.undo,
483                                true,  /* replaceVisibleToast */
484                                op);
485                    }
486                    break;
487            }
488        }
489    }
490
491    @Override
492    public void onError(final Folder folder, boolean replaceVisibleToast) {
493        final int mode = mViewMode.getMode();
494        switch (mode) {
495            case ViewMode.SEARCH_RESULTS_LIST:
496            case ViewMode.CONVERSATION_LIST:
497                showErrorToast(folder, replaceVisibleToast);
498                break;
499            default:
500                break;
501        }
502    }
503}
504