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 
18 package com.android.mail.ui;
19 
20 import android.app.Activity;
21 import android.app.Fragment;
22 import android.app.FragmentManager;
23 import android.app.FragmentTransaction;
24 import android.content.Intent;
25 import android.os.Bundle;
26 import android.support.annotation.IdRes;
27 import android.support.annotation.LayoutRes;
28 import android.support.v7.app.ActionBar;
29 import android.view.KeyEvent;
30 import android.view.Menu;
31 import android.view.View;
32 import android.widget.ImageView;
33 import android.widget.ListView;
34 
35 import com.android.mail.ConversationListContext;
36 import com.android.mail.R;
37 import com.android.mail.providers.Account;
38 import com.android.mail.providers.Conversation;
39 import com.android.mail.providers.Folder;
40 import com.android.mail.providers.UIProvider.AutoAdvance;
41 import com.android.mail.providers.UIProvider.ConversationListIcon;
42 import com.android.mail.utils.EmptyStateUtils;
43 import com.android.mail.utils.LogUtils;
44 import com.android.mail.utils.Utils;
45 import com.google.common.base.Objects;
46 import com.google.common.collect.Lists;
47 
48 import java.util.Collection;
49 import 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  */
55 public 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 
TwoPaneController(MailActivity activity, ViewMode viewMode)116     public TwoPaneController(MailActivity activity, ViewMode viewMode) {
117         super(activity, viewMode);
118     }
119 
120     @Override
appendToString(StringBuilder sb)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
isCurrentConversationJustPeeking()137     public boolean isCurrentConversationJustPeeking() {
138         return mCurrentConversationJustPeeking;
139     }
140 
isHidingConversationList()141     private boolean isHidingConversationList() {
142         return (mViewMode.isConversationMode() || mViewMode.isAdMode()) &&
143                 !mLayout.shouldShowPreviewPanel();
144     }
145 
146     /**
147      * Display the conversation list fragment.
148      */
initializeConversationListFragment()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      */
renderConversationList()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
doesActionChangeConversationListVisibility(final int action)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
isConversationListVisible()193     protected boolean isConversationListVisible() {
194         return !mLayout.isConversationListCollapsed();
195     }
196 
197     @Override
showConversationList(ConversationListContext listContext)198     protected void showConversationList(ConversationListContext listContext) {
199         initializeConversationListFragment();
200     }
201 
202     @Override
getContentViewResource()203     public @LayoutRes int getContentViewResource() {
204         return R.layout.two_pane_activity;
205     }
206 
207     @Override
onCreate(Bundle savedState)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
onDestroy()247     public void onDestroy() {
248         super.onDestroy();
249         mHandler.removeCallbacks(mFocusedConversationRunnable);
250     }
251 
252     @Override
onSaveInstanceState(Bundle outState)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
onWindowFocusChanged(boolean hasFocus)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
restoreConversation(Conversation conversation)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
switchToDefaultInboxOrChangeAccount(Account account)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
onFolderSelected(Folder folder)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 
isDrawerOpen()344     public boolean isDrawerOpen() {
345         final FolderListFragment flf = getFolderListFragment();
346         return flf != null && !flf.isMinimized();
347     }
348 
349     @Override
toggleDrawerState()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 
setDrawerState(boolean minimized)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 **/
onDrawerDragStarted()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 
onDrawerDrag(float percent)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 
onDrawerDragEnded(boolean minimized)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
shouldPreventListSwipesEntirely()414     public boolean shouldPreventListSwipesEntirely() {
415         return isDrawerOpen();
416     }
417 
418     @Override
onPrepareOptionsMenu(Menu menu)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
onViewModeChanged(int newMode)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 
getClfNextFocusStartId()459     private @IdRes int getClfNextFocusStartId() {
460         return (isDrawerOpen()) ? android.R.id.list : R.id.mini_drawer;
461     }
462 
463     @Override
onConversationVisibilityChanged(boolean visible)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 
showCurrentConversationInPager()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
onConversationListVisibilityChanged(boolean visible)492     public void onConversationListVisibilityChanged(boolean visible) {
493         super.onConversationListVisibilityChanged(visible);
494         enableOrDisableCab();
495     }
496 
497     @Override
resetActionBarIcon()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      */
enableOrDisableCab()514     private void enableOrDisableCab() {
515         if (mLayout.isConversationListCollapsed()) {
516             disableCabMode();
517         } else {
518             enableCabMode();
519         }
520     }
521 
522     @Override
onSetPopulated(ConversationCheckedSet set)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
onSetEmpty()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
showConversationWithPeek(Conversation conversation, boolean peek)545     protected void showConversationWithPeek(Conversation conversation, boolean peek) {
546         showConversation(conversation, peek, false /* fromKeyboard */);
547     }
548 
isCurrentlyPeeking()549     private boolean isCurrentlyPeeking() {
550         return mViewMode.isConversationMode() && mCurrentConversationJustPeeking
551                 && mCurrentConversation != null;
552     }
553 
showConversation(Conversation conversation, boolean peek, boolean fromKeyboard)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      */
transitionFromPeekToNormalMode()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
onConversationSelected(Conversation conversation, boolean inLoaderCallbacks)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
onConversationFocused(Conversation conversation)630     public void onConversationFocused(Conversation conversation) {
631         if (mIsTabletLandscape) {
632             showConversation(conversation, true /* peek */, true /* fromKeyboard */);
633         }
634     }
635 
636     @Override
setCurrentConversation(Conversation conversation)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
onConversationViewSwitched(Conversation conversation)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
doShowNextConversation(Collection<Conversation> target, int autoAdvance)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
showWaitForInitialization()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
hideWaitForInitialization()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
handleUpPress()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
handleBackPress()753     public boolean handleBackPress() {
754         return handleBackPress(false /* preventClose */);
755     }
756 
handleBackPress(boolean preventClose)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      */
popView(boolean preventClose)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
onPreMarkUnread()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
perhapsShowFirstConversation()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
shouldShowFirstConversation()846     public boolean shouldShowFirstConversation() {
847         return mLayout.shouldShowPreviewPanel();
848     }
849 
850     @Override
onUndoAvailable(ToastBarOperation op)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
onError(final Folder folder, boolean replaceVisibleToast)873     public void onError(final Folder folder, boolean replaceVisibleToast) {
874         showErrorToast(folder, replaceVisibleToast);
875     }
876 
877     @Override
isDrawerEnabled()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
getFolderListViewChoiceMode()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
launchFragment(final Fragment fragment, final int selectPosition)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
shouldBlockTouchEvents()910     public boolean shouldBlockTouchEvents() {
911         return isDrawerOpen();
912     }
913 
914     @Override
onConversationViewFrameTapped()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
onConversationViewTouchDown()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
onInterceptKeyFromCV(int keyCode, KeyEvent keyEvent, boolean navigateAway)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
isTwoPaneLandscape()947     public boolean isTwoPaneLandscape() {
948         return mIsTabletLandscape;
949     }
950 
951     @Override
shouldShowSearchBarByDefault(int viewMode)952     public boolean shouldShowSearchBarByDefault(int viewMode) {
953         return viewMode == ViewMode.SEARCH_RESULTS_LIST ||
954                 (mIsTabletLandscape && viewMode == ViewMode.SEARCH_RESULTS_CONVERSATION);
955     }
956 
957     @Override
shouldShowSearchMenuItem()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
addConversationListLayoutListener( TwoPaneLayout.ConversationListLayoutListener listener)965     public void addConversationListLayoutListener(
966             TwoPaneLayout.ConversationListLayoutListener listener) {
967         mConversationListLayoutListeners.add(listener);
968     }
969 
getConversationListLayoutListeners()970     public List<TwoPaneLayout.ConversationListLayoutListener> getConversationListLayoutListeners() {
971         return mConversationListLayoutListeners;
972     }
973 
974     @Override
setupEmptyIconView(Folder folder, boolean isEmpty)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 
ToShow(Conversation c, boolean fromKeyboard)995         public ToShow(Conversation c, boolean fromKeyboard) {
996             conversation = c;
997             dueToKeyboard = fromKeyboard;
998         }
999 
1000     }
1001 
1002 }
1003