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.v7.app.ActionBar;
29import android.view.KeyEvent;
30import android.view.Menu;
31import android.view.View;
32import android.widget.ImageView;
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.AutoAdvance;
41import com.android.mail.providers.UIProvider.ConversationListIcon;
42import com.android.mail.utils.EmptyStateUtils;
43import com.android.mail.utils.LogUtils;
44import com.android.mail.utils.Utils;
45import com.google.common.base.Objects;
46import com.google.common.collect.Lists;
47
48import java.util.Collection;
49import java.util.List;
50
51/**
52 * Controller for two-pane Mail activity. Two Pane is used for tablets, where screen real estate
53 * abounds.
54 */
55public final class TwoPaneController extends AbstractActivityController implements
56        ConversationViewFrame.DownEventListener {
57
58    private static final String SAVED_MISCELLANEOUS_VIEW = "saved-miscellaneous-view";
59    private static final String SAVED_MISCELLANEOUS_VIEW_TRANSACTION_ID =
60            "saved-miscellaneous-view-transaction-id";
61    private static final String SAVED_PEEK_MODE = "saved-peeking";
62    private static final String SAVED_PEEKING_CONVERSATION = "saved-peeking-conv";
63
64    private TwoPaneLayout mLayout;
65    private ImageView mEmptyCvView;
66    private List<TwoPaneLayout.ConversationListLayoutListener> mConversationListLayoutListeners =
67            Lists.newArrayList();
68
69    /**
70     * 2-pane, in wider configurations, allows peeking at a conversation view without having the
71     * conversation marked-as-read as far as read/unread state goes.<br>
72     * <br>
73     * This flag applies to {@link AbstractActivityController#mCurrentConversation} and indicates
74     * that the current conversation, if set, is in a 'peeking' state. If there is no current
75     * conversation, peeking is implied (in certain view configurations) and this value is
76     * meaningless.
77     */
78    private boolean mCurrentConversationJustPeeking;
79
80    /**
81     * When rotating from land->port->back to land while peeking at a conversation, typically we
82     * would lose the pointer to the conversation being seen in portrait (because in port, we're in
83     * TL mode so conv=null). This is bad if we ever want to go back to landscape, since the user
84     * expectation is that the original peek conversation should appear.
85     * <br>
86     * <p>So save the previous peeking conversation (if any) when restoring in portrait so that a
87     * future landscape restore can load it up.
88     */
89    private Conversation mSavedPeekingConversation;
90
91    /**
92     * The conversation to show (and any extra information about its presentation, like how it was
93     * triggered). Kept here during a transition animation to take effect afterwards.
94     */
95    private ToShow mToShow;
96
97    // For keyboard-focused conversations, we'll put it in a separate runnable.
98    private static final int FOCUSED_CONVERSATION_DELAY_MS = 500;
99    private final Runnable mFocusedConversationRunnable = new Runnable() {
100        @Override
101        public void run() {
102            if (!mActivity.isFinishing()) {
103                showCurrentConversationInPager();
104            }
105        }
106    };
107
108    /**
109     * Used to determine whether onViewModeChanged should skip a potential
110     * fragment transaction that would remove a miscellaneous view.
111     */
112    private boolean mSavedMiscellaneousView = false;
113
114    private boolean mIsTabletLandscape;
115
116    public TwoPaneController(MailActivity activity, ViewMode viewMode) {
117        super(activity, viewMode);
118    }
119
120    @Override
121    protected void appendToString(StringBuilder sb) {
122        sb.append(" mPeeking=");
123        sb.append(mCurrentConversationJustPeeking);
124        sb.append(" mSavedPeekConv=");
125        sb.append(mSavedPeekingConversation);
126        if (mToShow != null) {
127            sb.append(" mToShow.conv=");
128            sb.append(mToShow.conversation);
129            sb.append(" mToShow.dueToKeyboard=");
130            sb.append(mToShow.dueToKeyboard);
131        }
132        sb.append(" mLayout=");
133        sb.append(mLayout);
134    }
135
136    @Override
137    public boolean isCurrentConversationJustPeeking() {
138        return mCurrentConversationJustPeeking;
139    }
140
141    private boolean isHidingConversationList() {
142        return (mViewMode.isConversationMode() || mViewMode.isAdMode()) &&
143                !mLayout.shouldShowPreviewPanel();
144    }
145
146    /**
147     * Display the conversation list fragment.
148     */
149    private void initializeConversationListFragment() {
150        if (Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())) {
151            if (shouldEnterSearchConvMode()) {
152                mViewMode.enterSearchResultsConversationMode();
153            } else {
154                mViewMode.enterSearchResultsListMode();
155            }
156        }
157        renderConversationList();
158    }
159
160    /**
161     * Render the conversation list in the correct pane.
162     */
163    private void renderConversationList() {
164        if (mActivity == null) {
165            return;
166        }
167        FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction();
168        // Use cross fading animation.
169        fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
170        final ConversationListFragment conversationListFragment =
171                ConversationListFragment.newInstance(mConvListContext);
172        fragmentTransaction.replace(R.id.conversation_list_place_holder, conversationListFragment,
173                TAG_CONVERSATION_LIST);
174        fragmentTransaction.commitAllowingStateLoss();
175        // Set default navigation here once the ConversationListFragment is created.
176        conversationListFragment.setNextFocusStartId(
177                getClfNextFocusStartId());
178    }
179
180    @Override
181    public boolean doesActionChangeConversationListVisibility(final int action) {
182        if (action == R.id.settings
183                || action == R.id.compose
184                || action == R.id.help_info_menu_item
185                || action == R.id.feedback_menu_item) {
186            return true;
187        }
188
189        return false;
190    }
191
192    @Override
193    protected boolean isConversationListVisible() {
194        return !mLayout.isConversationListCollapsed();
195    }
196
197    @Override
198    protected void showConversationList(ConversationListContext listContext) {
199        initializeConversationListFragment();
200    }
201
202    @Override
203    public @LayoutRes int getContentViewResource() {
204        return R.layout.two_pane_activity;
205    }
206
207    @Override
208    public void onCreate(Bundle savedState) {
209        mLayout = (TwoPaneLayout) mActivity.findViewById(R.id.two_pane_activity);
210        mEmptyCvView = (ImageView) mActivity.findViewById(R.id.conversation_pane_no_message_view);
211        if (mLayout == null) {
212            // We need the layout for everything. Crash/Return early if it is null.
213            LogUtils.wtf(LOG_TAG, "mLayout is null!");
214            return;
215        }
216        mLayout.setController(this);
217        mActivity.getWindow().setBackgroundDrawable(null);
218        mIsTabletLandscape = mActivity.getResources().getBoolean(R.bool.is_tablet_landscape);
219
220        final FolderListFragment flf = getFolderListFragment();
221        flf.setMiniDrawerEnabled(true);
222        flf.setMinimized(true);
223
224        if (savedState != null) {
225            mSavedMiscellaneousView = savedState.getBoolean(SAVED_MISCELLANEOUS_VIEW, false);
226            mMiscellaneousViewTransactionId =
227                    savedState.getInt(SAVED_MISCELLANEOUS_VIEW_TRANSACTION_ID, -1);
228        }
229
230        // 2-pane layout is the main listener of view mode changes, and issues secondary
231        // notifications upon animation completion:
232        // (onConversationVisibilityChanged, onConversationListVisibilityChanged)
233        mViewMode.addListener(mLayout);
234
235        super.onCreate(savedState);
236
237        // Restore peek-related state *after* the super-implementation naively restores view mode.
238        if (savedState != null) {
239            mCurrentConversationJustPeeking = savedState.getBoolean(SAVED_PEEK_MODE,
240                    false /* defaultValue */);
241            mSavedPeekingConversation = savedState.getParcelable(SAVED_PEEKING_CONVERSATION);
242            // do the remaining restore work in restoreConversation()
243        }
244    }
245
246    @Override
247    public void onDestroy() {
248        super.onDestroy();
249        mHandler.removeCallbacks(mFocusedConversationRunnable);
250    }
251
252    @Override
253    public void onSaveInstanceState(Bundle outState) {
254        super.onSaveInstanceState(outState);
255
256        outState.putBoolean(SAVED_MISCELLANEOUS_VIEW, mMiscellaneousViewTransactionId >= 0);
257        outState.putInt(SAVED_MISCELLANEOUS_VIEW_TRANSACTION_ID, mMiscellaneousViewTransactionId);
258        outState.putBoolean(SAVED_PEEK_MODE, mCurrentConversationJustPeeking);
259        outState.putParcelable(SAVED_PEEKING_CONVERSATION, mSavedPeekingConversation);
260    }
261
262    @Override
263    public void onWindowFocusChanged(boolean hasFocus) {
264        if (hasFocus && !mLayout.isConversationListCollapsed()) {
265            // The conversation list is visible.
266            informCursorVisiblity(true);
267        }
268    }
269
270    @Override
271    protected void restoreConversation(Conversation conversation) {
272        // When handling restoration as part of rotation, if the destination orientation doesn't
273        // support peek (i.e. portrait), remap the view mode to list-mode if previously peeking.
274        // We still want to keep the peek state around in case the user rotates back to
275        // landscape, in which case the app should remember that peek mode was on and which
276        // conversation to peek at.
277        if (mCurrentConversationJustPeeking && !mIsTabletLandscape
278                && mViewMode.isConversationMode()) {
279            LogUtils.i(LOG_TAG, "restoring peek to port orientation");
280
281            // Restore the pager saved state, extract the Fragments out of it, kill each one
282            // manually, and finally tear down the pager and go back to the list.
283            //
284            // Need to tear down the restored CV fragments or else they will leak since the
285            // fragment manager will have a reference to them but nobody else does.
286            // normally, CPC.show() connects the new pager to the restored fragments, so a future
287            // CPC.hide() correctly clears them.
288
289            mPagerController.show(mAccount, mFolder, conversation, false /* changeVisibility */,
290                    null /* pagerAnimationListener */);
291            mPagerController.killRestoredFragments();
292            mPagerController.hide(false /* changeVisibility */);
293
294            // but first, save off the conversation in a separate slot for later restoration if
295            // we then end up back in peek mode
296            mSavedPeekingConversation = conversation;
297
298            mViewMode.enterConversationListMode();
299        } else if (mCurrentConversationJustPeeking && mIsTabletLandscape) {
300            showConversationWithPeek(conversation, true /* peek */);
301        } else {
302            super.restoreConversation(conversation);
303        }
304    }
305
306    @Override
307    public void switchToDefaultInboxOrChangeAccount(Account account) {
308        if (mViewMode.isSearchMode()) {
309            // We are in an activity on top of the main navigation activity.
310            // We need to return to it with a result code that indicates it should navigate to
311            // a different folder.
312            final Intent intent = new Intent();
313            intent.putExtra(AbstractActivityController.EXTRA_ACCOUNT, account);
314            mActivity.setResult(Activity.RESULT_OK, intent);
315            mActivity.finish();
316            return;
317        }
318        if (mViewMode.getMode() != ViewMode.CONVERSATION_LIST) {
319            mViewMode.enterConversationListMode();
320        }
321        super.switchToDefaultInboxOrChangeAccount(account);
322    }
323
324    @Override
325    public void onFolderSelected(Folder folder) {
326        // It's possible that we are not in conversation list mode
327        if (mViewMode.isSearchMode()) {
328            // We are in an activity on top of the main navigation activity.
329            // We need to return to it with a result code that indicates it should navigate to
330            // a different folder.
331            final Intent intent = new Intent();
332            intent.putExtra(AbstractActivityController.EXTRA_FOLDER, folder);
333            mActivity.setResult(Activity.RESULT_OK, intent);
334            mActivity.finish();
335            return;
336        } else if (mViewMode.getMode() != ViewMode.CONVERSATION_LIST) {
337            mViewMode.enterConversationListMode();
338        }
339
340        setHierarchyFolder(folder);
341        super.onFolderSelected(folder);
342    }
343
344    public boolean isDrawerOpen() {
345        final FolderListFragment flf = getFolderListFragment();
346        return flf != null && !flf.isMinimized();
347    }
348
349    @Override
350    protected void toggleDrawerState() {
351        final FolderListFragment flf = getFolderListFragment();
352        if (flf == null) {
353            LogUtils.w(LOG_TAG, "no drawer to toggle open/closed");
354            return;
355        }
356
357        setDrawerState(!flf.isMinimized());
358    }
359
360    protected void setDrawerState(boolean minimized) {
361        final FolderListFragment flf = getFolderListFragment();
362        if (flf == null) {
363            LogUtils.w(LOG_TAG, "no drawer to toggle open/closed");
364            return;
365        }
366
367        flf.animateMinimized(minimized);
368        mLayout.animateDrawer(minimized);
369        resetActionBarIcon();
370
371        final ConversationListFragment clf = getConversationListFragment();
372        if (clf != null) {
373            clf.setNextFocusStartId(getClfNextFocusStartId());
374
375            final SwipeableListView list = clf.getListView();
376            if (list != null) {
377                if (minimized) {
378                    list.stopPreventingSwipes();
379                } else {
380                    list.preventSwipesEntirely();
381                }
382            }
383        }
384    }
385
386    /** START TPL DRAWER DRAG CALLBACKS **/
387    protected void onDrawerDragStarted() {
388        final FolderListFragment flf = getFolderListFragment();
389        if (flf == null) {
390            LogUtils.w(LOG_TAG, "no drawer to toggle open/closed");
391            return;
392        }
393
394        flf.onDrawerDragStarted();
395    }
396
397    protected void onDrawerDrag(float percent) {
398        final FolderListFragment flf = getFolderListFragment();
399        if (flf == null) {
400            LogUtils.w(LOG_TAG, "no drawer to toggle open/closed");
401            return;
402        }
403
404        flf.onDrawerDrag(percent);
405    }
406
407    protected void onDrawerDragEnded(boolean minimized) {
408        // On drag completion animate the drawer to the final state.
409        setDrawerState(minimized);
410    }
411    /** END TPL DRAWER DRAG CALLBACKS **/
412
413    @Override
414    public boolean shouldPreventListSwipesEntirely() {
415        return isDrawerOpen();
416    }
417
418    @Override
419    public void onPrepareOptionsMenu(Menu menu) {
420        super.onPrepareOptionsMenu(menu);
421        if (mCurrentConversation != null) {
422            if (mCurrentConversationJustPeeking) {
423                Utils.setMenuItemPresent(menu, R.id.read, !mCurrentConversation.read);
424                Utils.setMenuItemPresent(menu, R.id.inside_conversation_unread,
425                        mCurrentConversation.read);
426            } else {
427                // in normal conv mode, always hide the extra 'mark-read' item
428                Utils.setMenuItemPresent(menu, R.id.read, false);
429            }
430        }
431    }
432
433    @Override
434    public void onViewModeChanged(int newMode) {
435        if (!mSavedMiscellaneousView && mMiscellaneousViewTransactionId >= 0) {
436            final FragmentManager fragmentManager = mActivity.getFragmentManager();
437            fragmentManager.popBackStackImmediate(mMiscellaneousViewTransactionId,
438                    FragmentManager.POP_BACK_STACK_INCLUSIVE);
439            mMiscellaneousViewTransactionId = -1;
440        }
441        mSavedMiscellaneousView = false;
442
443        super.onViewModeChanged(newMode);
444        if (newMode != ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION) {
445            // Clear the wait fragment
446            hideWaitForInitialization();
447        }
448        // In conversation mode, if the conversation list is not visible, then the user cannot
449        // see the selected conversations. Disable the CAB mode while leaving the selected set
450        // untouched.
451        // When the conversation list is made visible again, try to enable the CAB
452        // mode if any conversations are selected.
453        if (newMode == ViewMode.CONVERSATION || newMode == ViewMode.CONVERSATION_LIST
454                || ViewMode.isAdMode(newMode)) {
455            enableOrDisableCab();
456        }
457    }
458
459    private @IdRes int getClfNextFocusStartId() {
460        return (isDrawerOpen()) ? android.R.id.list : R.id.mini_drawer;
461    }
462
463    @Override
464    public void onConversationVisibilityChanged(boolean visible) {
465        super.onConversationVisibilityChanged(visible);
466        if (!visible) {
467            mPagerController.hide(false /* changeVisibility */);
468        } else if (mToShow != null) {
469            if (mToShow.dueToKeyboard) {
470                mHandler.removeCallbacks(mFocusedConversationRunnable);
471                mHandler.postDelayed(mFocusedConversationRunnable, FOCUSED_CONVERSATION_DELAY_MS);
472            } else {
473                showCurrentConversationInPager();
474            }
475        }
476
477        // Change visibility of the empty view
478        if (mIsTabletLandscape) {
479            mEmptyCvView.setVisibility(visible ? View.GONE : View.VISIBLE);
480        }
481    }
482
483    private void showCurrentConversationInPager() {
484        if (mToShow != null) {
485            mPagerController.show(mAccount, mFolder, mToShow.conversation,
486                    false /* changeVisibility */, null /* pagerAnimationListener */);
487            mToShow = null;
488        }
489    }
490
491    @Override
492    public void onConversationListVisibilityChanged(boolean visible) {
493        super.onConversationListVisibilityChanged(visible);
494        enableOrDisableCab();
495    }
496
497    @Override
498    public void resetActionBarIcon() {
499        final ActionBar ab = mActivity.getSupportActionBar();
500        final boolean isChildFolder = getFolder() != null && !Utils.isEmpty(getFolder().parent);
501        if (isHidingConversationList() || isChildFolder) {
502            ab.setHomeAsUpIndicator(R.drawable.ic_arrow_back_wht_24dp_with_rtl);
503            ab.setHomeActionContentDescription(0 /* system default */);
504        } else {
505            ab.setHomeAsUpIndicator(R.drawable.ic_menu_wht_24dp);
506            ab.setHomeActionContentDescription(
507                    isDrawerOpen() ? R.string.drawer_close : R.string.drawer_open);
508        }
509    }
510
511    /**
512     * Enable or disable the CAB mode based on the visibility of the conversation list fragment.
513     */
514    private void enableOrDisableCab() {
515        if (mLayout.isConversationListCollapsed()) {
516            disableCabMode();
517        } else {
518            enableCabMode();
519        }
520    }
521
522    @Override
523    public void onSetPopulated(ConversationCheckedSet set) {
524        super.onSetPopulated(set);
525
526        boolean showSenderImage =
527                (mAccount.settings.convListIcon == ConversationListIcon.SENDER_IMAGE);
528        if (!showSenderImage && mViewMode.isListMode()) {
529            getConversationListFragment().setChoiceNone();
530        }
531    }
532
533    @Override
534    public void onSetEmpty() {
535        super.onSetEmpty();
536
537        boolean showSenderImage =
538                (mAccount.settings.convListIcon == ConversationListIcon.SENDER_IMAGE);
539        if (!showSenderImage && mViewMode.isListMode()) {
540            getConversationListFragment().revertChoiceMode();
541        }
542    }
543
544    @Override
545    protected void showConversationWithPeek(Conversation conversation, boolean peek) {
546        showConversation(conversation, peek, false /* fromKeyboard */);
547    }
548
549    private boolean isCurrentlyPeeking() {
550        return mViewMode.isConversationMode() && mCurrentConversationJustPeeking
551                && mCurrentConversation != null;
552    }
553
554    private void showConversation(Conversation conversation, boolean peek, boolean fromKeyboard) {
555        // transition from peek mode to normal mode if we're already peeking at this convo
556        // and this was a request to switch to normal mode
557        if (!peek && conversation != null && conversation.equals(mCurrentConversation)
558                && transitionFromPeekToNormalMode()) {
559            LogUtils.i(LOG_TAG, "peek->normal: marking current CV seen. conv=%s",
560                    mCurrentConversation);
561            return;
562        }
563
564        // Make sure that we set the peeking flag before calling super (since some functionality
565        // in super depends on the flag.
566        mCurrentConversationJustPeeking = peek;
567        super.showConversationWithPeek(conversation, peek);
568
569        // 2-pane can ignore inLoaderCallbacks because it doesn't use
570        // FragmentManager.popBackStack().
571
572        if (mActivity == null) {
573            return;
574        }
575        if (conversation == null) {
576            handleBackPress(true /* preventClose */);
577            return;
578        }
579        // If conversation list is not visible, then the user cannot see the CAB mode, so exit it.
580        // This is needed here (in addition to during viewmode changes) because orientation changes
581        // while viewing a conversation don't change the viewmode: the mode stays
582        // ViewMode.CONVERSATION and yet the conversation list goes in and out of visibility.
583        enableOrDisableCab();
584
585        // When a mode change is required, wait for onConversationVisibilityChanged(), the signal
586        // that the mode change animation has finished, before rendering the conversation.
587        mToShow = new ToShow(conversation, fromKeyboard);
588
589        final int mode = mViewMode.getMode();
590        LogUtils.i(LOG_TAG, "IN TPC.showConv, oldMode=%s conv=%s", mViewMode, mToShow.conversation);
591        if (mode == ViewMode.SEARCH_RESULTS_LIST || mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
592            mViewMode.enterSearchResultsConversationMode();
593        } else {
594            mViewMode.enterConversationMode();
595        }
596        // load the conversation immediately if we're already in conversation mode
597        if (!mLayout.isModeChangePending()) {
598            onConversationVisibilityChanged(true);
599        } else {
600            LogUtils.i(LOG_TAG, "TPC.showConversation will wait for TPL.animationEnd to show!");
601        }
602    }
603
604    /**
605     * @return success=true, else false if we aren't peeking
606     */
607    private boolean transitionFromPeekToNormalMode() {
608        final boolean shouldTransition = isCurrentlyPeeking();
609        if (shouldTransition) {
610            mCurrentConversationJustPeeking = false;
611            markConversationSeen(mCurrentConversation);
612        }
613        return shouldTransition;
614    }
615
616    @Override
617    public void onConversationSelected(Conversation conversation, boolean inLoaderCallbacks) {
618        // close the drawer when the user opens CV from the list
619        if (isDrawerOpen()) {
620            toggleDrawerState();
621        }
622        super.onConversationSelected(conversation, inLoaderCallbacks);
623        if (!mCurrentConversationJustPeeking) {
624            // Shift the focus to the conversation in landscape mode.
625            mPagerController.focusPager();
626        }
627    }
628
629    @Override
630    public void onConversationFocused(Conversation conversation) {
631        if (mIsTabletLandscape) {
632            showConversation(conversation, true /* peek */, true /* fromKeyboard */);
633        }
634    }
635
636    @Override
637    public void setCurrentConversation(Conversation conversation) {
638        // Order is important! We want to calculate different *before* the superclass changes
639        // mCurrentConversation, so before super.setCurrentConversation().
640        final long oldId = mCurrentConversation != null ? mCurrentConversation.id : -1;
641        final long newId = conversation != null ? conversation.id : -1;
642        final boolean different = oldId != newId;
643
644        if (different) {
645            LogUtils.i(LOG_TAG, "TPC.setCurrentConv w/ new conv. new=%s old=%s newPeek=%s",
646                    conversation, mCurrentConversation, mCurrentConversationJustPeeking);
647        }
648
649        // This call might change mCurrentConversation.
650        super.setCurrentConversation(conversation);
651
652        final ConversationListFragment convList = getConversationListFragment();
653        if (different && convList != null && conversation != null) {
654            if (mCurrentConversationJustPeeking) {
655                convList.clearChoicesAndActivated();
656                convList.setSelected(conversation);
657            } else {
658                convList.setActivated(conversation, different);
659            }
660        }
661    }
662
663    @Override
664    public void onConversationViewSwitched(Conversation conversation) {
665        // swiping on CV to flip through CV pages should reset the peeking flag; the next
666        // conversation should be marked read when visible
667        //
668        // it's also possible to get here when the dataset changes and the current CV is
669        // repositioned in the dataset, so make sure the current conv is actually being switched
670        // before clearing the peek state
671        if (!Objects.equal(conversation, mCurrentConversation)) {
672            LogUtils.i(LOG_TAG, "CPA reported a page change. resetting peek to false. new conv=%s",
673                    conversation);
674            mCurrentConversationJustPeeking = false;
675        }
676        super.onConversationViewSwitched(conversation);
677    }
678
679    @Override
680    protected void doShowNextConversation(Collection<Conversation> target, int autoAdvance) {
681        // in portrait, and in landscape when auto-advance is set, do the regular thing
682        if (!isTwoPaneLandscape() || autoAdvance != AutoAdvance.LIST) {
683            super.doShowNextConversation(target, autoAdvance);
684            return;
685        }
686
687        // special case for two-pane landscape with LIST auto-advance: prefer to peek at the
688        // next-oldest conversation instead. showConversation() will resort to an empty CV pane when
689        // destroying the very last conversation.
690        final Conversation next = mTracker.getNextConversation(AutoAdvance.OLDER, target);
691        LogUtils.i(LOG_TAG, "showNextConversation(2P-land): showing %s next.", next);
692        showConversationWithPeek(next, true /* peek */);
693    }
694
695    @Override
696    protected void showWaitForInitialization() {
697        super.showWaitForInitialization();
698
699        FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction();
700        fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
701        fragmentTransaction.replace(R.id.conversation_list_place_holder, getWaitFragment(), TAG_WAIT);
702        fragmentTransaction.commitAllowingStateLoss();
703    }
704
705    @Override
706    protected void hideWaitForInitialization() {
707        final WaitFragment waitFragment = getWaitFragment();
708        if (waitFragment == null) {
709            // We aren't showing a wait fragment: nothing to do
710            return;
711        }
712        // Remove the existing wait fragment from the back stack.
713        final FragmentTransaction fragmentTransaction =
714                mActivity.getFragmentManager().beginTransaction();
715        fragmentTransaction.remove(waitFragment);
716        fragmentTransaction.commitAllowingStateLoss();
717        super.hideWaitForInitialization();
718        if (mViewMode.isWaitingForSync()) {
719            // We should come out of wait mode and display the account inbox.
720            loadAccountInbox();
721        }
722    }
723
724    /**
725     * Up works as follows:
726     * 1) If the user is in a conversation and:
727     *  a) the conversation list is hidden (portrait mode), shows the conv list and
728     *  stays in conversation view mode.
729     *  b) the conversation list is shown, goes back to conversation list mode.
730     * 2) If the user is in search results, up exits search.
731     * mode and returns the user to whatever view they were in when they began search.
732     * 3) If the user is in conversation list mode, there is no up.
733     */
734    @Override
735    public boolean handleUpPress() {
736        if (isHidingConversationList()) {
737            handleBackPress();
738        } else {
739            final boolean isTopLevel = Folder.isRoot(mFolder);
740
741            if (isTopLevel) {
742                // Show the drawer.
743                toggleDrawerState();
744            } else {
745                navigateUpFolderHierarchy();
746            }
747        }
748
749        return true;
750    }
751
752    @Override
753    public boolean handleBackPress() {
754        return handleBackPress(false /* preventClose */);
755    }
756
757    private boolean handleBackPress(boolean preventClose) {
758        // Clear any visible undo bars.
759        mToastBar.hide(false, false /* actionClicked */);
760        if (isDrawerOpen()) {
761            toggleDrawerState();
762        } else {
763            popView(preventClose);
764        }
765        return true;
766    }
767
768    /**
769     * Pops the "view stack" to the last screen the user was viewing.
770     *
771     * @param preventClose Whether to prevent closing the app if the stack is empty.
772     */
773    protected void popView(boolean preventClose) {
774        // If the user is in search query entry mode, or the user is viewing
775        // search results, exit
776        // the mode.
777        int mode = mViewMode.getMode();
778        if (mode == ViewMode.SEARCH_RESULTS_LIST) {
779            mActivity.finish();
780        } else if (ViewMode.isConversationMode(mode) || mViewMode.isAdMode()) {
781            // die if in two-pane landscape and the back button was pressed
782            if (isTwoPaneLandscape() && !preventClose) {
783                mActivity.finish();
784            } else if (mode == ViewMode.SEARCH_RESULTS_CONVERSATION) {
785                mViewMode.enterSearchResultsListMode();
786            } else {
787                mViewMode.enterConversationListMode();
788            }
789        } else {
790            // The Folder List fragment can be null for monkeys where we get a back before the
791            // folder list has had a chance to initialize.
792            final FolderListFragment folderList = getFolderListFragment();
793            if (mode == ViewMode.CONVERSATION_LIST && folderList != null
794                    && !Folder.isRoot(mFolder)) {
795                // If the user navigated via the left folders list into a child folder,
796                // back should take the user up to the parent folder's conversation list.
797                navigateUpFolderHierarchy();
798            // Otherwise, if we are in the conversation list but not in the default
799            // inbox and not on expansive layouts, we want to switch back to the default
800            // inbox. This fixes b/9006969 so that on smaller tablets where we have this
801            // hybrid one and two-pane mode, we will return to the inbox. On larger tablets,
802            // we will instead exit the app.
803            } else if (!preventClose) {
804                // There is nothing else to pop off the stack.
805                mActivity.finish();
806            }
807        }
808    }
809
810    @Override
811    protected void onPreMarkUnread() {
812        // stay in CV when marking unread in two-pane mode
813        if (isTwoPaneLandscape()) {
814            // TODO: need to update the list item state to switch from activated to peeking
815            mCurrentConversationJustPeeking = true;
816            mActivity.supportInvalidateOptionsMenu();
817        } else {
818            super.onPreMarkUnread();
819        }
820    }
821
822    @Override
823    protected void perhapsShowFirstConversation() {
824        super.perhapsShowFirstConversation();
825        if (!mViewMode.isAdMode() && mCurrentConversation == null && isTwoPaneLandscape()
826                && mConversationListCursor.getCount() > 0) {
827            final Conversation conv;
828
829            // restore the saved peeking conversation if present from the previous rotation
830            if (mCurrentConversationJustPeeking && mSavedPeekingConversation != null) {
831                conv = mSavedPeekingConversation;
832                mSavedPeekingConversation = null;
833                LogUtils.i(LOG_TAG, "peeking at saved conv=%s", conv);
834            } else {
835                mConversationListCursor.moveToPosition(0);
836                conv = mConversationListCursor.getConversation();
837                conv.position = 0;
838                LogUtils.i(LOG_TAG, "peeking at default/zeroth conv=%s", conv);
839            }
840
841            showConversationWithPeek(conv, true /* peek */);
842        }
843    }
844
845    @Override
846    public boolean shouldShowFirstConversation() {
847        return mLayout.shouldShowPreviewPanel();
848    }
849
850    @Override
851    public void onUndoAvailable(ToastBarOperation op) {
852        final int mode = mViewMode.getMode();
853        final ConversationListFragment convList = getConversationListFragment();
854
855        switch (mode) {
856            case ViewMode.SEARCH_RESULTS_LIST:
857            case ViewMode.CONVERSATION_LIST:
858            case ViewMode.SEARCH_RESULTS_CONVERSATION:
859            case ViewMode.CONVERSATION:
860                if (convList != null) {
861                    mToastBar.show(getUndoClickedListener(convList.getAnimatedAdapter()),
862                            Utils.convertHtmlToPlainText
863                                (op.getDescription(mActivity.getActivityContext())),
864                            R.string.undo,
865                            true /* replaceVisibleToast */,
866                            true /* autohide */,
867                            op);
868                }
869        }
870    }
871
872    @Override
873    public void onError(final Folder folder, boolean replaceVisibleToast) {
874        showErrorToast(folder, replaceVisibleToast);
875    }
876
877    @Override
878    public boolean isDrawerEnabled() {
879        // two-pane has its own drawer-like thing that expands inline from a minimized state.
880        return false;
881    }
882
883    @Override
884    public int getFolderListViewChoiceMode() {
885        // By default, we want to allow one item to be selected in the folder list
886        return ListView.CHOICE_MODE_SINGLE;
887    }
888
889    private int mMiscellaneousViewTransactionId = -1;
890
891    @Override
892    public void launchFragment(final Fragment fragment, final int selectPosition) {
893        final int containerViewId = TwoPaneLayout.MISCELLANEOUS_VIEW_ID;
894
895        final FragmentManager fragmentManager = mActivity.getFragmentManager();
896        if (fragmentManager.findFragmentByTag(TAG_CUSTOM_FRAGMENT) == null) {
897            final FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
898            fragmentTransaction.addToBackStack(null);
899            fragmentTransaction.replace(containerViewId, fragment, TAG_CUSTOM_FRAGMENT);
900            mMiscellaneousViewTransactionId = fragmentTransaction.commitAllowingStateLoss();
901            fragmentManager.executePendingTransactions();
902        }
903
904        if (selectPosition >= 0) {
905            getConversationListFragment().setRawActivated(selectPosition, true);
906        }
907    }
908
909    @Override
910    public boolean shouldBlockTouchEvents() {
911        return isDrawerOpen();
912    }
913
914    @Override
915    public void onConversationViewFrameTapped() {
916        // handle a tap on CV by closing the drawer if open
917        if (isDrawerOpen()) {
918            toggleDrawerState();
919        }
920    }
921
922    @Override
923    public void onConversationViewTouchDown() {
924        final boolean handled = transitionFromPeekToNormalMode();
925        if (handled) {
926            LogUtils.i(LOG_TAG, "TPC: tap on CV triggered peek->normal, marking seen. conv=%s",
927                    mCurrentConversation);
928        }
929    }
930
931    @Override
932    public boolean onInterceptKeyFromCV(int keyCode, KeyEvent keyEvent, boolean navigateAway) {
933        // Override left/right key presses in landscape mode.
934        if (navigateAway) {
935            if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
936                ConversationListFragment clf = getConversationListFragment();
937                if (clf != null) {
938                    clf.getListView().requestFocus();
939                }
940            }
941            return true;
942        }
943        return false;
944    }
945
946    @Override
947    public boolean isTwoPaneLandscape() {
948        return mIsTabletLandscape;
949    }
950
951    @Override
952    public boolean shouldShowSearchBarByDefault(int viewMode) {
953        return viewMode == ViewMode.SEARCH_RESULTS_LIST ||
954                (mIsTabletLandscape && viewMode == ViewMode.SEARCH_RESULTS_CONVERSATION);
955    }
956
957    @Override
958    public boolean shouldShowSearchMenuItem() {
959        final int mode = mViewMode.getMode();
960        return mode == ViewMode.CONVERSATION_LIST ||
961                (mIsTabletLandscape && mode == ViewMode.CONVERSATION);
962    }
963
964    @Override
965    public void addConversationListLayoutListener(
966            TwoPaneLayout.ConversationListLayoutListener listener) {
967        mConversationListLayoutListeners.add(listener);
968    }
969
970    public List<TwoPaneLayout.ConversationListLayoutListener> getConversationListLayoutListeners() {
971        return mConversationListLayoutListeners;
972    }
973
974    @Override
975    public boolean setupEmptyIconView(Folder folder, boolean isEmpty) {
976        if (mIsTabletLandscape) {
977            if (!isEmpty) {
978                mEmptyCvView.setImageResource(R.drawable.ic_empty_default);
979            } else {
980                EmptyStateUtils.bindEmptyFolderIcon(mEmptyCvView, folder);
981            }
982            return true;
983        }
984        return false;
985    }
986
987    /**
988     * The conversation to show (and other associated bits) when performing a TL->CV transition.
989     *
990     */
991    private static class ToShow {
992        public final Conversation conversation;
993        public final boolean dueToKeyboard;
994
995        public ToShow(Conversation c, boolean fromKeyboard) {
996            conversation = c;
997            dueToKeyboard = fromKeyboard;
998        }
999
1000    }
1001
1002}
1003