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.Activity;
21import android.app.Fragment;
22import android.app.FragmentManager;
23import android.app.FragmentTransaction;
24import android.content.Intent;
25import android.os.Bundle;
26import android.support.annotation.IdRes;
27import android.support.annotation.LayoutRes;
28import android.support.v4.widget.DrawerLayout;
29import android.view.Gravity;
30import android.view.KeyEvent;
31import android.view.View;
32import android.widget.ListView;
33
34import com.android.mail.ConversationListContext;
35import com.android.mail.R;
36import com.android.mail.providers.Account;
37import com.android.mail.providers.Conversation;
38import com.android.mail.providers.Folder;
39import com.android.mail.providers.UIProvider;
40import com.android.mail.utils.FolderUri;
41import com.android.mail.utils.Utils;
42
43/**
44 * Controller for one-pane Mail activity. One Pane is used for phones, where screen real estate is
45 * limited. This controller also does the layout, since the layout is simpler in the one pane case.
46 */
47
48public final class OnePaneController extends AbstractActivityController {
49    /** Key used to store {@link #mLastConversationListTransactionId} */
50    private static final String CONVERSATION_LIST_TRANSACTION_KEY = "conversation-list-transaction";
51    /** Key used to store {@link #mLastConversationTransactionId}. */
52    private static final String CONVERSATION_TRANSACTION_KEY = "conversation-transaction";
53    /** Key used to store {@link #mConversationListVisible}. */
54    private static final String CONVERSATION_LIST_VISIBLE_KEY = "conversation-list-visible";
55    /** Key used to store {@link #mConversationListNeverShown}. */
56    private static final String CONVERSATION_LIST_NEVER_SHOWN_KEY = "conversation-list-never-shown";
57
58    private static final int INVALID_ID = -1;
59    private boolean mConversationListVisible = false;
60    private int mLastConversationListTransactionId = INVALID_ID;
61    private int mLastConversationTransactionId = INVALID_ID;
62    /** Whether a conversation list for this account has ever been shown.*/
63    private boolean mConversationListNeverShown = true;
64
65    public OnePaneController(MailActivity activity, ViewMode viewMode) {
66        super(activity, viewMode);
67    }
68
69    @Override
70    public void onRestoreInstanceState(Bundle inState) {
71        super.onRestoreInstanceState(inState);
72        if (inState == null) {
73            return;
74        }
75        mLastConversationListTransactionId =
76                inState.getInt(CONVERSATION_LIST_TRANSACTION_KEY, INVALID_ID);
77        mLastConversationTransactionId = inState.getInt(CONVERSATION_TRANSACTION_KEY, INVALID_ID);
78        mConversationListVisible = inState.getBoolean(CONVERSATION_LIST_VISIBLE_KEY);
79        mConversationListNeverShown = inState.getBoolean(CONVERSATION_LIST_NEVER_SHOWN_KEY);
80    }
81
82    @Override
83    public void onSaveInstanceState(Bundle outState) {
84        super.onSaveInstanceState(outState);
85        outState.putInt(CONVERSATION_LIST_TRANSACTION_KEY, mLastConversationListTransactionId);
86        outState.putInt(CONVERSATION_TRANSACTION_KEY, mLastConversationTransactionId);
87        outState.putBoolean(CONVERSATION_LIST_VISIBLE_KEY, mConversationListVisible);
88        outState.putBoolean(CONVERSATION_LIST_NEVER_SHOWN_KEY, mConversationListNeverShown);
89    }
90
91    @Override
92    public void resetActionBarIcon() {
93        // Calling resetActionBarIcon should never remove the up affordance
94        // even when waiting for sync (Folder list should still show with one
95        // account. Currently this method is blank to avoid any changes.
96    }
97
98    /**
99     * Returns true if the candidate URI is the URI for the default inbox for the given account.
100     * @param candidate the URI to check
101     * @param account the account whose default Inbox the candidate might be
102     * @return true if the candidate is indeed the default inbox for the given account.
103     */
104    private static boolean isDefaultInbox(FolderUri candidate, Account account) {
105        return (candidate != null && account != null)
106                && candidate.equals(account.settings.defaultInbox);
107    }
108
109    /**
110     * Returns true if the user is currently in the conversation list view, viewing the default
111     * inbox.
112     * @return true if user is in conversation list mode, viewing the default inbox.
113     */
114    private static boolean inInbox(final Account account, final ConversationListContext context) {
115        // If we don't have valid state, then we are not in the inbox.
116        return !(account == null || context == null || context.folder == null
117                || account.settings == null) && !ConversationListContext.isSearchResult(context)
118                && isDefaultInbox(context.folder.folderUri, account);
119    }
120
121    /**
122     * On account change, carry out super implementation, load FolderListFragment
123     * into drawer (to avoid repetitive calls to replaceFragment).
124     */
125    @Override
126    public void changeAccount(Account account) {
127        super.changeAccount(account);
128        mConversationListNeverShown = true;
129        closeDrawerIfOpen();
130    }
131
132    @Override
133    public @LayoutRes int getContentViewResource() {
134        return R.layout.one_pane_activity;
135    }
136
137    @Override
138    public boolean onCreate(Bundle savedInstanceState) {
139        mDrawerContainer = (DrawerLayout) mActivity.findViewById(R.id.drawer_container);
140        mDrawerContainer.setDrawerTitle(Gravity.START,
141                mActivity.getActivityContext().getString(R.string.drawer_title));
142        final String drawerPulloutTag = mActivity.getString(R.string.drawer_pullout_tag);
143        mDrawerPullout = mDrawerContainer.findViewWithTag(drawerPulloutTag);
144        mDrawerPullout.setBackgroundResource(R.color.list_background_color);
145
146        // CV is initially GONE on 1-pane (mode changes trigger visibility changes)
147        mActivity.findViewById(R.id.conversation_pager).setVisibility(View.GONE);
148
149        // The parent class sets the correct viewmode and starts the application off.
150        return super.onCreate(savedInstanceState);
151    }
152
153    @Override
154    protected ActionableToastBar findActionableToastBar(MailActivity activity) {
155        final ActionableToastBar tb = super.findActionableToastBar(activity);
156
157        // notify the toast bar of its sibling floating action button so it can move them together
158        // as they animate
159        tb.setFloatingActionButton(activity.findViewById(R.id.compose_button));
160        return tb;
161    }
162
163    @Override
164    protected boolean isConversationListVisible() {
165        return mConversationListVisible;
166    }
167
168    @Override
169    public void onViewModeChanged(int newMode) {
170        super.onViewModeChanged(newMode);
171
172        // When entering conversation list mode, hide and clean up any currently visible
173        // conversation.
174        if (ViewMode.isListMode(newMode)) {
175            mPagerController.hide(true /* changeVisibility */);
176        }
177        // When we step away from the conversation mode, we don't have a current conversation
178        // anymore. Let's blank it out so clients calling getCurrentConversation are not misled.
179        if (!ViewMode.isConversationMode(newMode)) {
180            setCurrentConversation(null);
181        }
182    }
183
184    @Override
185    public String toString() {
186        final StringBuilder sb = new StringBuilder(super.toString());
187        sb.append(" lastConvListTransId=");
188        sb.append(mLastConversationListTransactionId);
189        sb.append("}");
190        return sb.toString();
191    }
192
193    @Override
194    public void showConversationList(ConversationListContext listContext) {
195        super.showConversationList(listContext);
196        enableCabMode();
197        mConversationListVisible = true;
198        if (ConversationListContext.isSearchResult(listContext)) {
199            mViewMode.enterSearchResultsListMode();
200        } else {
201            mViewMode.enterConversationListMode();
202        }
203        final int transition = mConversationListNeverShown
204                ? FragmentTransaction.TRANSIT_FRAGMENT_FADE
205                : FragmentTransaction.TRANSIT_FRAGMENT_OPEN;
206        final Fragment conversationListFragment =
207                ConversationListFragment.newInstance(listContext);
208
209        if (!inInbox(mAccount, listContext)) {
210            // Maintain fragment transaction history so we can get back to the
211            // fragment used to launch this list.
212            mLastConversationListTransactionId = replaceFragment(conversationListFragment,
213                    transition, TAG_CONVERSATION_LIST, R.id.content_pane);
214        } else {
215            // If going to the inbox, clear the folder list transaction history.
216            mInbox = listContext.folder;
217            replaceFragment(conversationListFragment, transition, TAG_CONVERSATION_LIST,
218                    R.id.content_pane);
219
220            // If we ever to to the inbox, we want to unset the transation id for any other
221            // non-inbox folder.
222            mLastConversationListTransactionId = INVALID_ID;
223        }
224
225        mActivity.getFragmentManager().executePendingTransactions();
226
227        onConversationVisibilityChanged(false);
228        onConversationListVisibilityChanged(true);
229        mConversationListNeverShown = false;
230    }
231
232    @Override
233    protected void showConversation(Conversation conversation) {
234        super.showConversation(conversation);
235        mConversationListVisible = false;
236        if (conversation == null) {
237            transitionBackToConversationListMode();
238            return;
239        }
240        disableCabMode();
241        if (ConversationListContext.isSearchResult(mConvListContext)) {
242            mViewMode.enterSearchResultsConversationMode();
243        } else {
244            mViewMode.enterConversationMode();
245        }
246        final FragmentManager fm = mActivity.getFragmentManager();
247        final FragmentTransaction ft = fm.beginTransaction();
248        // Switching to conversation view is an incongruous transition:
249        // we are not replacing a fragment with another fragment as
250        // usual. Instead, reveal the heretofore inert conversation
251        // ViewPager and just remove the previously visible fragment
252        // e.g. conversation list, or possibly label list?).
253        final Fragment f = fm.findFragmentById(R.id.content_pane);
254        // FragmentManager#findFragmentById can return fragments that are not added to the activity.
255        // We want to make sure that we don't attempt to remove fragments that are not added to the
256        // activity, as when the transaction is popped off, the FragmentManager will attempt to
257        // readd the same fragment twice
258        if (f != null && f.isAdded()) {
259            ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
260            ft.remove(f);
261            ft.commitAllowingStateLoss();
262            fm.executePendingTransactions();
263        }
264        mPagerController.show(mAccount, mFolder, conversation, true /* changeVisibility */);
265        onConversationVisibilityChanged(true);
266        onConversationListVisibilityChanged(false);
267    }
268
269    @Override
270    public void onConversationFocused(Conversation conversation) {
271        // Do nothing
272    }
273
274    @Override
275    public void showWaitForInitialization() {
276        super.showWaitForInitialization();
277        replaceFragment(getWaitFragment(), FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_WAIT,
278                R.id.content_pane);
279    }
280
281    @Override
282    protected void hideWaitForInitialization() {
283        transitionToInbox();
284        super.hideWaitForInitialization();
285    }
286
287    /**
288     * Switch to the Inbox by creating a new conversation list context that loads the inbox.
289     */
290    private void transitionToInbox() {
291        // The inbox could have changed, in which case we should load it again.
292        if (mInbox == null || !isDefaultInbox(mInbox.folderUri, mAccount)) {
293            loadAccountInbox();
294        } else {
295            onFolderChanged(mInbox, false /* force */);
296        }
297    }
298
299    @Override
300    public boolean doesActionChangeConversationListVisibility(final int action) {
301        if (action == R.id.archive
302                || action == R.id.remove_folder
303                || action == R.id.delete
304                || action == R.id.discard_drafts
305                || action == R.id.discard_outbox
306                || action == R.id.mark_important
307                || action == R.id.mark_not_important
308                || action == R.id.mute
309                || action == R.id.report_spam
310                || action == R.id.mark_not_spam
311                || action == R.id.report_phishing
312                || action == R.id.refresh
313                || action == R.id.change_folders) {
314            return false;
315        } else {
316            return true;
317        }
318    }
319
320    /**
321     * Replace the content_pane with the fragment specified here. The tag is specified so that
322     * the {@link ActivityController} can look up the fragments through the
323     * {@link android.app.FragmentManager}.
324     * @param fragment the new fragment to put
325     * @param transition the transition to show
326     * @param tag a tag for the fragment manager.
327     * @param anchor ID of view to replace fragment in
328     * @return transaction ID returned when the transition is committed.
329     */
330    private int replaceFragment(Fragment fragment, int transition, String tag, int anchor) {
331        final FragmentManager fm = mActivity.getFragmentManager();
332        FragmentTransaction fragmentTransaction = fm.beginTransaction();
333        fragmentTransaction.setTransition(transition);
334        fragmentTransaction.replace(anchor, fragment, tag);
335        final int id = fragmentTransaction.commitAllowingStateLoss();
336        fm.executePendingTransactions();
337        return id;
338    }
339
340    /**
341     * Back works as follows:
342     * 1) If the drawer is pulled out (Or mid-drag), close it - handled.
343     * 2) If the user is in the folder list view, go back
344     * to the account default inbox.
345     * 3) If the user is in a conversation list
346     * that is not the inbox AND:
347     *  a) they got there by going through the folder
348     *  list view, go back to the folder list view.
349     *  b) they got there by using some other means (account dropdown), go back to the inbox.
350     * 4) If the user is in a conversation, go back to the conversation list they were last in.
351     * 5) If the user is in the conversation list for the default account inbox,
352     * back exits the app.
353     */
354    @Override
355    public boolean handleBackPress() {
356        final int mode = mViewMode.getMode();
357
358        if (mode == ViewMode.SEARCH_RESULTS_LIST) {
359            mActivity.finish();
360        } else if (mViewMode.isListMode() && !inInbox(mAccount, mConvListContext)) {
361            navigateUpFolderHierarchy();
362        } else if (mViewMode.isConversationMode() || mViewMode.isAdMode()) {
363            transitionBackToConversationListMode();
364        } else {
365            mActivity.finish();
366        }
367        mToastBar.hide(false, false /* actionClicked */);
368        return true;
369    }
370
371    @Override
372    public void onFolderSelected(Folder folder) {
373        if (mViewMode.isSearchMode()) {
374            // We are in an activity on top of the main navigation activity.
375            // We need to return to it with a result code that indicates it should navigate to
376            // a different folder.
377            final Intent intent = new Intent();
378            intent.putExtra(AbstractActivityController.EXTRA_FOLDER, folder);
379            mActivity.setResult(Activity.RESULT_OK, intent);
380            mActivity.finish();
381            return;
382        }
383        setHierarchyFolder(folder);
384        super.onFolderSelected(folder);
385    }
386
387    /**
388     * Up works as follows:
389     * 1) If the user is in a conversation list that is not the default account inbox,
390     * a conversation, or the folder list, up follows the rules of back.
391     * 2) If the user is in search results, up exits search
392     * mode and returns the user to whatever view they were in when they began search.
393     * 3) If the user is in the inbox, there is no up.
394     */
395    @Override
396    public boolean handleUpPress() {
397        final int mode = mViewMode.getMode();
398        if (mode == ViewMode.SEARCH_RESULTS_LIST) {
399            mActivity.finish();
400            // Not needed, the activity is going away anyway.
401        } else if (mode == ViewMode.CONVERSATION_LIST
402                || mode == ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION) {
403            final boolean isTopLevel = Folder.isRoot(mFolder);
404
405            if (isTopLevel) {
406                // Show the drawer.
407                toggleDrawerState();
408            } else {
409                navigateUpFolderHierarchy();
410            }
411        } else if (mode == ViewMode.CONVERSATION || mode == ViewMode.SEARCH_RESULTS_CONVERSATION
412                || mode == ViewMode.AD) {
413            // Same as go back.
414            handleBackPress();
415        }
416        return true;
417    }
418
419    private void transitionBackToConversationListMode() {
420        final int mode = mViewMode.getMode();
421        enableCabMode();
422        mConversationListVisible = true;
423        if (mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
424            mViewMode.enterSearchResultsListMode();
425        } else {
426            mViewMode.enterConversationListMode();
427        }
428
429        final Folder folder = mFolder != null ? mFolder : mInbox;
430        onFolderChanged(folder, true /* force */);
431
432        onConversationVisibilityChanged(false);
433        onConversationListVisibilityChanged(true);
434    }
435
436    @Override
437    public boolean shouldShowFirstConversation() {
438        return false;
439    }
440
441    @Override
442    public void onUndoAvailable(ToastBarOperation op) {
443        if (op != null && mAccount.supportsCapability(UIProvider.AccountCapabilities.UNDO)) {
444            final int mode = mViewMode.getMode();
445            final ConversationListFragment convList = getConversationListFragment();
446            switch (mode) {
447                case ViewMode.SEARCH_RESULTS_CONVERSATION:
448                case ViewMode.CONVERSATION:
449                    mToastBar.show(getUndoClickedListener(
450                            convList != null ? convList.getAnimatedAdapter() : null),
451                            Utils.convertHtmlToPlainText
452                                (op.getDescription(mActivity.getActivityContext())),
453                            R.string.undo,
454                            true,  /* replaceVisibleToast */
455                            op);
456                    break;
457                case ViewMode.SEARCH_RESULTS_LIST:
458                case ViewMode.CONVERSATION_LIST:
459                    if (convList != null) {
460                        mToastBar.show(
461                                getUndoClickedListener(convList.getAnimatedAdapter()),
462                                Utils.convertHtmlToPlainText
463                                    (op.getDescription(mActivity.getActivityContext())),
464                                R.string.undo,
465                                true,  /* replaceVisibleToast */
466                                op);
467                    } else {
468                        mActivity.setPendingToastOperation(op);
469                    }
470                    break;
471            }
472        }
473    }
474
475    @Override
476    public void onError(final Folder folder, boolean replaceVisibleToast) {
477        final int mode = mViewMode.getMode();
478        switch (mode) {
479            case ViewMode.SEARCH_RESULTS_LIST:
480            case ViewMode.CONVERSATION_LIST:
481                showErrorToast(folder, replaceVisibleToast);
482                break;
483            default:
484                break;
485        }
486    }
487
488    @Override
489    public boolean isDrawerEnabled() {
490        // The drawer is enabled for one pane mode
491        return true;
492    }
493
494    @Override
495    public int getFolderListViewChoiceMode() {
496        // By default, we do not want to allow any item to be selected in the folder list
497        return ListView.CHOICE_MODE_NONE;
498    }
499
500    @Override
501    public void launchFragment(final Fragment fragment, final int selectPosition) {
502        replaceFragment(fragment, FragmentTransaction.TRANSIT_FRAGMENT_OPEN,
503                TAG_CUSTOM_FRAGMENT, R.id.content_pane);
504    }
505
506    @Override
507    public boolean onInterceptKeyFromCV(int keyCode, KeyEvent keyEvent, boolean navigateAway) {
508        // Not applicable
509        return false;
510    }
511
512    @Override
513    public boolean isTwoPaneLandscape() {
514        return false;
515    }
516}
517