Home | History | Annotate | Download | only in ui
      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 
    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