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