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