/******************************************************************************* * Copyright (C) 2012 Google Inc. * Licensed to The Android Open Source Project. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. *******************************************************************************/ package com.android.mail.ui; import android.app.Activity; import android.app.Fragment; import android.app.FragmentManager; import android.app.FragmentTransaction; import android.content.Intent; import android.os.Bundle; import android.support.annotation.IdRes; import android.support.annotation.LayoutRes; import android.support.v7.app.ActionBar; import android.view.KeyEvent; import android.view.Menu; import android.view.View; import android.widget.ImageView; import android.widget.ListView; import com.android.mail.ConversationListContext; import com.android.mail.R; import com.android.mail.providers.Account; import com.android.mail.providers.Conversation; import com.android.mail.providers.Folder; import com.android.mail.providers.UIProvider.AutoAdvance; import com.android.mail.providers.UIProvider.ConversationListIcon; import com.android.mail.utils.EmptyStateUtils; import com.android.mail.utils.LogUtils; import com.android.mail.utils.Utils; import com.google.common.base.Objects; import com.google.common.collect.Lists; import java.util.Collection; import java.util.List; /** * Controller for two-pane Mail activity. Two Pane is used for tablets, where screen real estate * abounds. */ public final class TwoPaneController extends AbstractActivityController implements ConversationViewFrame.DownEventListener { private static final String SAVED_MISCELLANEOUS_VIEW = "saved-miscellaneous-view"; private static final String SAVED_MISCELLANEOUS_VIEW_TRANSACTION_ID = "saved-miscellaneous-view-transaction-id"; private static final String SAVED_PEEK_MODE = "saved-peeking"; private static final String SAVED_PEEKING_CONVERSATION = "saved-peeking-conv"; private TwoPaneLayout mLayout; private ImageView mEmptyCvView; private List mConversationListLayoutListeners = Lists.newArrayList(); /** * 2-pane, in wider configurations, allows peeking at a conversation view without having the * conversation marked-as-read as far as read/unread state goes.
*
* This flag applies to {@link AbstractActivityController#mCurrentConversation} and indicates * that the current conversation, if set, is in a 'peeking' state. If there is no current * conversation, peeking is implied (in certain view configurations) and this value is * meaningless. */ private boolean mCurrentConversationJustPeeking; /** * When rotating from land->port->back to land while peeking at a conversation, typically we * would lose the pointer to the conversation being seen in portrait (because in port, we're in * TL mode so conv=null). This is bad if we ever want to go back to landscape, since the user * expectation is that the original peek conversation should appear. *
*

So save the previous peeking conversation (if any) when restoring in portrait so that a * future landscape restore can load it up. */ private Conversation mSavedPeekingConversation; /** * The conversation to show (and any extra information about its presentation, like how it was * triggered). Kept here during a transition animation to take effect afterwards. */ private ToShow mToShow; // For keyboard-focused conversations, we'll put it in a separate runnable. private static final int FOCUSED_CONVERSATION_DELAY_MS = 500; private final Runnable mFocusedConversationRunnable = new Runnable() { @Override public void run() { if (!mActivity.isFinishing()) { showCurrentConversationInPager(); } } }; /** * Used to determine whether onViewModeChanged should skip a potential * fragment transaction that would remove a miscellaneous view. */ private boolean mSavedMiscellaneousView = false; private boolean mIsTabletLandscape; public TwoPaneController(MailActivity activity, ViewMode viewMode) { super(activity, viewMode); } @Override protected void appendToString(StringBuilder sb) { sb.append(" mPeeking="); sb.append(mCurrentConversationJustPeeking); sb.append(" mSavedPeekConv="); sb.append(mSavedPeekingConversation); if (mToShow != null) { sb.append(" mToShow.conv="); sb.append(mToShow.conversation); sb.append(" mToShow.dueToKeyboard="); sb.append(mToShow.dueToKeyboard); } sb.append(" mLayout="); sb.append(mLayout); } @Override public boolean isCurrentConversationJustPeeking() { return mCurrentConversationJustPeeking; } private boolean isHidingConversationList() { return (mViewMode.isConversationMode() || mViewMode.isAdMode()) && !mLayout.shouldShowPreviewPanel(); } /** * Display the conversation list fragment. */ private void initializeConversationListFragment() { if (Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())) { if (shouldEnterSearchConvMode()) { mViewMode.enterSearchResultsConversationMode(); } else { mViewMode.enterSearchResultsListMode(); } } renderConversationList(); } /** * Render the conversation list in the correct pane. */ private void renderConversationList() { if (mActivity == null) { return; } FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction(); // Use cross fading animation. fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); final ConversationListFragment conversationListFragment = ConversationListFragment.newInstance(mConvListContext); fragmentTransaction.replace(R.id.conversation_list_place_holder, conversationListFragment, TAG_CONVERSATION_LIST); fragmentTransaction.commitAllowingStateLoss(); // Set default navigation here once the ConversationListFragment is created. conversationListFragment.setNextFocusStartId( getClfNextFocusStartId()); } @Override public boolean doesActionChangeConversationListVisibility(final int action) { if (action == R.id.settings || action == R.id.compose || action == R.id.help_info_menu_item || action == R.id.feedback_menu_item) { return true; } return false; } @Override protected boolean isConversationListVisible() { return !mLayout.isConversationListCollapsed(); } @Override protected void showConversationList(ConversationListContext listContext) { initializeConversationListFragment(); } @Override public @LayoutRes int getContentViewResource() { return R.layout.two_pane_activity; } @Override public void onCreate(Bundle savedState) { mLayout = (TwoPaneLayout) mActivity.findViewById(R.id.two_pane_activity); mEmptyCvView = (ImageView) mActivity.findViewById(R.id.conversation_pane_no_message_view); if (mLayout == null) { // We need the layout for everything. Crash/Return early if it is null. LogUtils.wtf(LOG_TAG, "mLayout is null!"); return; } mLayout.setController(this); mActivity.getWindow().setBackgroundDrawable(null); mIsTabletLandscape = mActivity.getResources().getBoolean(R.bool.is_tablet_landscape); final FolderListFragment flf = getFolderListFragment(); flf.setMiniDrawerEnabled(true); flf.setMinimized(true); if (savedState != null) { mSavedMiscellaneousView = savedState.getBoolean(SAVED_MISCELLANEOUS_VIEW, false); mMiscellaneousViewTransactionId = savedState.getInt(SAVED_MISCELLANEOUS_VIEW_TRANSACTION_ID, -1); } // 2-pane layout is the main listener of view mode changes, and issues secondary // notifications upon animation completion: // (onConversationVisibilityChanged, onConversationListVisibilityChanged) mViewMode.addListener(mLayout); super.onCreate(savedState); // Restore peek-related state *after* the super-implementation naively restores view mode. if (savedState != null) { mCurrentConversationJustPeeking = savedState.getBoolean(SAVED_PEEK_MODE, false /* defaultValue */); mSavedPeekingConversation = savedState.getParcelable(SAVED_PEEKING_CONVERSATION); // do the remaining restore work in restoreConversation() } } @Override public void onDestroy() { super.onDestroy(); mHandler.removeCallbacks(mFocusedConversationRunnable); } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putBoolean(SAVED_MISCELLANEOUS_VIEW, mMiscellaneousViewTransactionId >= 0); outState.putInt(SAVED_MISCELLANEOUS_VIEW_TRANSACTION_ID, mMiscellaneousViewTransactionId); outState.putBoolean(SAVED_PEEK_MODE, mCurrentConversationJustPeeking); outState.putParcelable(SAVED_PEEKING_CONVERSATION, mSavedPeekingConversation); } @Override public void onWindowFocusChanged(boolean hasFocus) { if (hasFocus && !mLayout.isConversationListCollapsed()) { // The conversation list is visible. informCursorVisiblity(true); } } @Override protected void restoreConversation(Conversation conversation) { // When handling restoration as part of rotation, if the destination orientation doesn't // support peek (i.e. portrait), remap the view mode to list-mode if previously peeking. // We still want to keep the peek state around in case the user rotates back to // landscape, in which case the app should remember that peek mode was on and which // conversation to peek at. if (mCurrentConversationJustPeeking && !mIsTabletLandscape && mViewMode.isConversationMode()) { LogUtils.i(LOG_TAG, "restoring peek to port orientation"); // Restore the pager saved state, extract the Fragments out of it, kill each one // manually, and finally tear down the pager and go back to the list. // // Need to tear down the restored CV fragments or else they will leak since the // fragment manager will have a reference to them but nobody else does. // normally, CPC.show() connects the new pager to the restored fragments, so a future // CPC.hide() correctly clears them. mPagerController.show(mAccount, mFolder, conversation, false /* changeVisibility */, null /* pagerAnimationListener */); mPagerController.killRestoredFragments(); mPagerController.hide(false /* changeVisibility */); // but first, save off the conversation in a separate slot for later restoration if // we then end up back in peek mode mSavedPeekingConversation = conversation; mViewMode.enterConversationListMode(); return; } super.restoreConversation(conversation); } @Override public void switchToDefaultInboxOrChangeAccount(Account account) { if (mViewMode.isSearchMode()) { // We are in an activity on top of the main navigation activity. // We need to return to it with a result code that indicates it should navigate to // a different folder. final Intent intent = new Intent(); intent.putExtra(AbstractActivityController.EXTRA_ACCOUNT, account); mActivity.setResult(Activity.RESULT_OK, intent); mActivity.finish(); return; } if (mViewMode.getMode() != ViewMode.CONVERSATION_LIST) { mViewMode.enterConversationListMode(); } super.switchToDefaultInboxOrChangeAccount(account); } @Override public void onFolderSelected(Folder folder) { // It's possible that we are not in conversation list mode if (mViewMode.isSearchMode()) { // We are in an activity on top of the main navigation activity. // We need to return to it with a result code that indicates it should navigate to // a different folder. final Intent intent = new Intent(); intent.putExtra(AbstractActivityController.EXTRA_FOLDER, folder); mActivity.setResult(Activity.RESULT_OK, intent); mActivity.finish(); return; } else if (mViewMode.getMode() != ViewMode.CONVERSATION_LIST) { mViewMode.enterConversationListMode(); } setHierarchyFolder(folder); super.onFolderSelected(folder); } public boolean isDrawerOpen() { final FolderListFragment flf = getFolderListFragment(); return flf != null && !flf.isMinimized(); } @Override protected void toggleDrawerState() { final FolderListFragment flf = getFolderListFragment(); if (flf == null) { LogUtils.w(LOG_TAG, "no drawer to toggle open/closed"); return; } setDrawerState(!flf.isMinimized()); } protected void setDrawerState(boolean minimized) { final FolderListFragment flf = getFolderListFragment(); if (flf == null) { LogUtils.w(LOG_TAG, "no drawer to toggle open/closed"); return; } flf.animateMinimized(minimized); mLayout.animateDrawer(minimized); resetActionBarIcon(); final ConversationListFragment clf = getConversationListFragment(); if (clf != null) { clf.setNextFocusStartId(getClfNextFocusStartId()); final SwipeableListView list = clf.getListView(); if (list != null) { if (minimized) { list.stopPreventingSwipes(); } else { list.preventSwipesEntirely(); } } } } /** START TPL DRAWER DRAG CALLBACKS **/ protected void onDrawerDragStarted() { final FolderListFragment flf = getFolderListFragment(); if (flf == null) { LogUtils.w(LOG_TAG, "no drawer to toggle open/closed"); return; } flf.onDrawerDragStarted(); } protected void onDrawerDrag(float percent) { final FolderListFragment flf = getFolderListFragment(); if (flf == null) { LogUtils.w(LOG_TAG, "no drawer to toggle open/closed"); return; } flf.onDrawerDrag(percent); } protected void onDrawerDragEnded(boolean minimized) { // On drag completion animate the drawer to the final state. setDrawerState(minimized); } /** END TPL DRAWER DRAG CALLBACKS **/ @Override public boolean shouldPreventListSwipesEntirely() { return isDrawerOpen(); } @Override public void onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); if (mCurrentConversation != null) { if (mCurrentConversationJustPeeking) { Utils.setMenuItemPresent(menu, R.id.read, !mCurrentConversation.read); Utils.setMenuItemPresent(menu, R.id.inside_conversation_unread, mCurrentConversation.read); } else { // in normal conv mode, always hide the extra 'mark-read' item Utils.setMenuItemPresent(menu, R.id.read, false); } } } @Override public void onViewModeChanged(int newMode) { if (!mSavedMiscellaneousView && mMiscellaneousViewTransactionId >= 0) { final FragmentManager fragmentManager = mActivity.getFragmentManager(); fragmentManager.popBackStackImmediate(mMiscellaneousViewTransactionId, FragmentManager.POP_BACK_STACK_INCLUSIVE); mMiscellaneousViewTransactionId = -1; } mSavedMiscellaneousView = false; super.onViewModeChanged(newMode); if (newMode != ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION) { // Clear the wait fragment hideWaitForInitialization(); } // In conversation mode, if the conversation list is not visible, then the user cannot // see the selected conversations. Disable the CAB mode while leaving the selected set // untouched. // When the conversation list is made visible again, try to enable the CAB // mode if any conversations are selected. if (newMode == ViewMode.CONVERSATION || newMode == ViewMode.CONVERSATION_LIST || ViewMode.isAdMode(newMode)) { enableOrDisableCab(); } } private @IdRes int getClfNextFocusStartId() { return (isDrawerOpen()) ? android.R.id.list : R.id.mini_drawer; } @Override public void onConversationVisibilityChanged(boolean visible) { super.onConversationVisibilityChanged(visible); if (!visible) { mPagerController.hide(false /* changeVisibility */); } else if (mToShow != null) { if (mToShow.dueToKeyboard) { mHandler.removeCallbacks(mFocusedConversationRunnable); mHandler.postDelayed(mFocusedConversationRunnable, FOCUSED_CONVERSATION_DELAY_MS); } else { showCurrentConversationInPager(); } } // Change visibility of the empty view if (mIsTabletLandscape) { mEmptyCvView.setVisibility(visible ? View.GONE : View.VISIBLE); } } private void showCurrentConversationInPager() { if (mToShow != null) { mPagerController.show(mAccount, mFolder, mToShow.conversation, false /* changeVisibility */, null /* pagerAnimationListener */); mToShow = null; } } @Override public void onConversationListVisibilityChanged(boolean visible) { super.onConversationListVisibilityChanged(visible); enableOrDisableCab(); } @Override public void resetActionBarIcon() { final ActionBar ab = mActivity.getSupportActionBar(); final boolean isChildFolder = getFolder() != null && !Utils.isEmpty(getFolder().parent); if (isHidingConversationList() || isChildFolder) { ab.setHomeAsUpIndicator(R.drawable.ic_arrow_back_wht_24dp_with_rtl); ab.setHomeActionContentDescription(0 /* system default */); } else { ab.setHomeAsUpIndicator(R.drawable.ic_menu_wht_24dp); ab.setHomeActionContentDescription( isDrawerOpen() ? R.string.drawer_close : R.string.drawer_open); } } /** * Enable or disable the CAB mode based on the visibility of the conversation list fragment. */ private void enableOrDisableCab() { if (mLayout.isConversationListCollapsed()) { disableCabMode(); } else { enableCabMode(); } } @Override public void onSetPopulated(ConversationCheckedSet set) { super.onSetPopulated(set); boolean showSenderImage = (mAccount.settings.convListIcon == ConversationListIcon.SENDER_IMAGE); if (!showSenderImage && mViewMode.isListMode()) { getConversationListFragment().setChoiceNone(); } } @Override public void onSetEmpty() { super.onSetEmpty(); boolean showSenderImage = (mAccount.settings.convListIcon == ConversationListIcon.SENDER_IMAGE); if (!showSenderImage && mViewMode.isListMode()) { getConversationListFragment().revertChoiceMode(); } } @Override protected void showConversationWithPeek(Conversation conversation, boolean peek) { showConversation(conversation, peek, false /* fromKeyboard */); } private boolean isCurrentlyPeeking() { return mViewMode.isConversationMode() && mCurrentConversationJustPeeking && mCurrentConversation != null; } private void showConversation(Conversation conversation, boolean peek, boolean fromKeyboard) { // transition from peek mode to normal mode if we're already peeking at this convo // and this was a request to switch to normal mode if (!peek && conversation != null && conversation.equals(mCurrentConversation) && transitionFromPeekToNormalMode()) { LogUtils.i(LOG_TAG, "peek->normal: marking current CV seen. conv=%s", mCurrentConversation); return; } // Make sure that we set the peeking flag before calling super (since some functionality // in super depends on the flag. mCurrentConversationJustPeeking = peek; super.showConversationWithPeek(conversation, peek); // 2-pane can ignore inLoaderCallbacks because it doesn't use // FragmentManager.popBackStack(). if (mActivity == null) { return; } if (conversation == null) { handleBackPress(true /* preventClose */); return; } // If conversation list is not visible, then the user cannot see the CAB mode, so exit it. // This is needed here (in addition to during viewmode changes) because orientation changes // while viewing a conversation don't change the viewmode: the mode stays // ViewMode.CONVERSATION and yet the conversation list goes in and out of visibility. enableOrDisableCab(); // When a mode change is required, wait for onConversationVisibilityChanged(), the signal // that the mode change animation has finished, before rendering the conversation. mToShow = new ToShow(conversation, fromKeyboard); final int mode = mViewMode.getMode(); LogUtils.i(LOG_TAG, "IN TPC.showConv, oldMode=%s conv=%s", mViewMode, mToShow.conversation); if (mode == ViewMode.SEARCH_RESULTS_LIST || mode == ViewMode.SEARCH_RESULTS_CONVERSATION) { mViewMode.enterSearchResultsConversationMode(); } else { mViewMode.enterConversationMode(); } // load the conversation immediately if we're already in conversation mode if (!mLayout.isModeChangePending()) { onConversationVisibilityChanged(true); } else { LogUtils.i(LOG_TAG, "TPC.showConversation will wait for TPL.animationEnd to show!"); } } /** * @return success=true, else false if we aren't peeking */ private boolean transitionFromPeekToNormalMode() { final boolean shouldTransition = isCurrentlyPeeking(); if (shouldTransition) { mCurrentConversationJustPeeking = false; markConversationSeen(mCurrentConversation); } return shouldTransition; } @Override public void onConversationSelected(Conversation conversation, boolean inLoaderCallbacks) { // close the drawer when the user opens CV from the list if (isDrawerOpen()) { toggleDrawerState(); } super.onConversationSelected(conversation, inLoaderCallbacks); if (!mCurrentConversationJustPeeking) { // Shift the focus to the conversation in landscape mode. mPagerController.focusPager(); } } @Override public void onConversationFocused(Conversation conversation) { if (mIsTabletLandscape) { showConversation(conversation, true /* peek */, true /* fromKeyboard */); } } @Override public void setCurrentConversation(Conversation conversation) { // Order is important! We want to calculate different *before* the superclass changes // mCurrentConversation, so before super.setCurrentConversation(). final long oldId = mCurrentConversation != null ? mCurrentConversation.id : -1; final long newId = conversation != null ? conversation.id : -1; final boolean different = oldId != newId; if (different) { LogUtils.i(LOG_TAG, "TPC.setCurrentConv w/ new conv. new=%s old=%s newPeek=%s", conversation, mCurrentConversation, mCurrentConversationJustPeeking); } // This call might change mCurrentConversation. super.setCurrentConversation(conversation); final ConversationListFragment convList = getConversationListFragment(); if (convList != null && conversation != null) { if (mCurrentConversationJustPeeking) { convList.clearChoicesAndActivated(); // TODO: set the list highlight to this new item } else { convList.setActivated(conversation.position, different); } } } @Override public void onConversationViewSwitched(Conversation conversation) { // swiping on CV to flip through CV pages should reset the peeking flag; the next // conversation should be marked read when visible // // it's also possible to get here when the dataset changes and the current CV is // repositioned in the dataset, so make sure the current conv is actually being switched // before clearing the peek state if (!Objects.equal(conversation, mCurrentConversation)) { LogUtils.i(LOG_TAG, "CPA reported a page change. resetting peek to false. new conv=%s", conversation); mCurrentConversationJustPeeking = false; } super.onConversationViewSwitched(conversation); } @Override protected void doShowNextConversation(Collection target, int autoAdvance) { // in portrait, and in landscape when auto-advance is set, do the regular thing if (!isTwoPaneLandscape() || autoAdvance != AutoAdvance.LIST) { super.doShowNextConversation(target, autoAdvance); return; } // special case for two-pane landscape with LIST auto-advance: prefer to peek at the // next-oldest conversation instead. showConversation() will resort to an empty CV pane when // destroying the very last conversation. final Conversation next = mTracker.getNextConversation(AutoAdvance.OLDER, target); LogUtils.i(LOG_TAG, "showNextConversation(2P-land): showing %s next.", next); showConversationWithPeek(next, true /* peek */); } @Override protected void showWaitForInitialization() { super.showWaitForInitialization(); FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction(); fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); fragmentTransaction.replace(R.id.conversation_list_place_holder, getWaitFragment(), TAG_WAIT); fragmentTransaction.commitAllowingStateLoss(); } @Override protected void hideWaitForInitialization() { final WaitFragment waitFragment = getWaitFragment(); if (waitFragment == null) { // We aren't showing a wait fragment: nothing to do return; } // Remove the existing wait fragment from the back stack. final FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction(); fragmentTransaction.remove(waitFragment); fragmentTransaction.commitAllowingStateLoss(); super.hideWaitForInitialization(); if (mViewMode.isWaitingForSync()) { // We should come out of wait mode and display the account inbox. loadAccountInbox(); } } /** * Up works as follows: * 1) If the user is in a conversation and: * a) the conversation list is hidden (portrait mode), shows the conv list and * stays in conversation view mode. * b) the conversation list is shown, goes back to conversation list mode. * 2) If the user is in search results, up exits search. * mode and returns the user to whatever view they were in when they began search. * 3) If the user is in conversation list mode, there is no up. */ @Override public boolean handleUpPress() { if (isHidingConversationList()) { handleBackPress(); } else { toggleDrawerState(); } return true; } @Override public boolean handleBackPress() { return handleBackPress(false /* preventClose */); } private boolean handleBackPress(boolean preventClose) { // Clear any visible undo bars. mToastBar.hide(false, false /* actionClicked */); if (isDrawerOpen()) { toggleDrawerState(); } else { popView(preventClose); } return true; } /** * Pops the "view stack" to the last screen the user was viewing. * * @param preventClose Whether to prevent closing the app if the stack is empty. */ protected void popView(boolean preventClose) { // If the user is in search query entry mode, or the user is viewing // search results, exit // the mode. int mode = mViewMode.getMode(); if (mode == ViewMode.SEARCH_RESULTS_LIST) { mActivity.finish(); } else if (ViewMode.isConversationMode(mode) || mViewMode.isAdMode()) { // die if in two-pane landscape and the back button was pressed if (isTwoPaneLandscape() && !preventClose) { mActivity.finish(); } else if (mode == ViewMode.SEARCH_RESULTS_CONVERSATION) { mViewMode.enterSearchResultsListMode(); } else { mViewMode.enterConversationListMode(); } } else { // The Folder List fragment can be null for monkeys where we get a back before the // folder list has had a chance to initialize. final FolderListFragment folderList = getFolderListFragment(); if (mode == ViewMode.CONVERSATION_LIST && folderList != null && !Folder.isRoot(mFolder)) { // If the user navigated via the left folders list into a child folder, // back should take the user up to the parent folder's conversation list. navigateUpFolderHierarchy(); // Otherwise, if we are in the conversation list but not in the default // inbox and not on expansive layouts, we want to switch back to the default // inbox. This fixes b/9006969 so that on smaller tablets where we have this // hybrid one and two-pane mode, we will return to the inbox. On larger tablets, // we will instead exit the app. } else if (!preventClose) { // There is nothing else to pop off the stack. mActivity.finish(); } } } @Override protected void onPreMarkUnread() { // stay in CV when marking unread in two-pane mode if (isTwoPaneLandscape()) { // TODO: need to update the list item state to switch from activated to peeking mCurrentConversationJustPeeking = true; mActivity.supportInvalidateOptionsMenu(); } else { super.onPreMarkUnread(); } } @Override protected void perhapsShowFirstConversation() { super.perhapsShowFirstConversation(); if (!mViewMode.isAdMode() && mCurrentConversation == null && isTwoPaneLandscape() && mConversationListCursor.getCount() > 0) { final Conversation conv; // restore the saved peeking conversation if present from the previous rotation if (mCurrentConversationJustPeeking && mSavedPeekingConversation != null) { conv = mSavedPeekingConversation; mSavedPeekingConversation = null; LogUtils.i(LOG_TAG, "peeking at saved conv=%s", conv); } else { mConversationListCursor.moveToPosition(0); conv = mConversationListCursor.getConversation(); conv.position = 0; LogUtils.i(LOG_TAG, "peeking at default/zeroth conv=%s", conv); } showConversationWithPeek(conv, true /* peek */); } } @Override public boolean shouldShowFirstConversation() { return mLayout.shouldShowPreviewPanel(); } @Override public void onUndoAvailable(ToastBarOperation op) { final int mode = mViewMode.getMode(); final ConversationListFragment convList = getConversationListFragment(); switch (mode) { case ViewMode.SEARCH_RESULTS_LIST: case ViewMode.CONVERSATION_LIST: case ViewMode.SEARCH_RESULTS_CONVERSATION: case ViewMode.CONVERSATION: if (convList != null) { mToastBar.show(getUndoClickedListener(convList.getAnimatedAdapter()), Utils.convertHtmlToPlainText (op.getDescription(mActivity.getActivityContext())), R.string.undo, true /* replaceVisibleToast */, true /* autohide */, op); } } } @Override public void onError(final Folder folder, boolean replaceVisibleToast) { showErrorToast(folder, replaceVisibleToast); } @Override public boolean isDrawerEnabled() { // two-pane has its own drawer-like thing that expands inline from a minimized state. return false; } @Override public int getFolderListViewChoiceMode() { // By default, we want to allow one item to be selected in the folder list return ListView.CHOICE_MODE_SINGLE; } private int mMiscellaneousViewTransactionId = -1; @Override public void launchFragment(final Fragment fragment, final int selectPosition) { final int containerViewId = TwoPaneLayout.MISCELLANEOUS_VIEW_ID; final FragmentManager fragmentManager = mActivity.getFragmentManager(); if (fragmentManager.findFragmentByTag(TAG_CUSTOM_FRAGMENT) == null) { final FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); fragmentTransaction.addToBackStack(null); fragmentTransaction.replace(containerViewId, fragment, TAG_CUSTOM_FRAGMENT); mMiscellaneousViewTransactionId = fragmentTransaction.commitAllowingStateLoss(); fragmentManager.executePendingTransactions(); } if (selectPosition >= 0) { getConversationListFragment().setRawActivated(selectPosition, true); } } @Override public boolean shouldBlockTouchEvents() { return isDrawerOpen(); } @Override public void onConversationViewFrameTapped() { // handle a tap on CV by closing the drawer if open if (isDrawerOpen()) { toggleDrawerState(); } } @Override public void onConversationViewTouchDown() { final boolean handled = transitionFromPeekToNormalMode(); if (handled) { LogUtils.i(LOG_TAG, "TPC: tap on CV triggered peek->normal, marking seen. conv=%s", mCurrentConversation); } } @Override public boolean onInterceptKeyFromCV(int keyCode, KeyEvent keyEvent, boolean navigateAway) { // Override left/right key presses in landscape mode. if (navigateAway) { if (keyEvent.getAction() == KeyEvent.ACTION_UP) { ConversationListFragment clf = getConversationListFragment(); if (clf != null) { clf.getListView().requestFocus(); } } return true; } return false; } @Override public boolean isTwoPaneLandscape() { return mIsTabletLandscape; } @Override public boolean shouldShowSearchBarByDefault(int viewMode) { return viewMode == ViewMode.SEARCH_RESULTS_LIST || (mIsTabletLandscape && viewMode == ViewMode.SEARCH_RESULTS_CONVERSATION); } @Override public boolean shouldShowSearchMenuItem() { final int mode = mViewMode.getMode(); return mode == ViewMode.CONVERSATION_LIST || (mIsTabletLandscape && mode == ViewMode.CONVERSATION); } @Override public void addConversationListLayoutListener( TwoPaneLayout.ConversationListLayoutListener listener) { mConversationListLayoutListeners.add(listener); } public List getConversationListLayoutListeners() { return mConversationListLayoutListeners; } @Override public boolean setupEmptyIconView(Folder folder, boolean isEmpty) { if (mIsTabletLandscape) { if (!isEmpty) { mEmptyCvView.setImageResource(R.drawable.ic_empty_default); } else { EmptyStateUtils.bindEmptyFolderIcon(mEmptyCvView, folder); } return true; } return false; } /** * The conversation to show (and other associated bits) when performing a TL->CV transition. * */ private static class ToShow { public final Conversation conversation; public final boolean dueToKeyboard; public ToShow(Conversation c, boolean fromKeyboard) { conversation = c; dueToKeyboard = fromKeyboard; } } }