TwoPaneController.java revision 405a344937675f57fc9c6988b2b124410a270f13
1/******************************************************************************* 2 * Copyright (C) 2012 Google Inc. 3 * Licensed to The Android Open Source Project. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 *******************************************************************************/ 17 18package com.android.mail.ui; 19 20import android.app.Activity; 21import android.app.Fragment; 22import android.app.FragmentManager; 23import android.app.FragmentTransaction; 24import android.content.Intent; 25import android.os.Bundle; 26import android.support.annotation.IdRes; 27import android.support.annotation.LayoutRes; 28import android.support.v7.app.ActionBar; 29import android.view.KeyEvent; 30import android.view.View; 31import android.widget.ListView; 32 33import com.android.mail.ConversationListContext; 34import com.android.mail.R; 35import com.android.mail.providers.Account; 36import com.android.mail.providers.Conversation; 37import com.android.mail.providers.Folder; 38import com.android.mail.providers.UIProvider.ConversationListIcon; 39import com.android.mail.utils.LogUtils; 40import com.android.mail.utils.Utils; 41 42/** 43 * Controller for two-pane Mail activity. Two Pane is used for tablets, where screen real estate 44 * abounds. 45 */ 46public final class TwoPaneController extends AbstractActivityController implements 47 ConversationViewFrame.DownEventListener { 48 49 private static final String SAVED_MISCELLANEOUS_VIEW = "saved-miscellaneous-view"; 50 private static final String SAVED_MISCELLANEOUS_VIEW_TRANSACTION_ID = 51 "saved-miscellaneous-view-transaction-id"; 52 53 private TwoPaneLayout mLayout; 54 @Deprecated 55 private Conversation mConversationToShow; 56 57 /** 58 * 2-pane, in wider configurations, allows peeking at a conversation view without having the 59 * conversation marked-as-read as far as read/unread state goes.<br> 60 * <br> 61 * This flag applies to {@link AbstractActivityController#mCurrentConversation} and indicates 62 * that the current conversation, if set, is in a 'peeking' state. If there is no current 63 * conversation, peeking is implied (in certain view configurations) and this value is 64 * meaningless. 65 */ 66 // TODO: save in instance state 67 private boolean mCurrentConversationJustPeeking; 68 69 /** 70 * Used to determine whether onViewModeChanged should skip a potential 71 * fragment transaction that would remove a miscellaneous view. 72 */ 73 private boolean mSavedMiscellaneousView = false; 74 75 private boolean mIsTabletLandscape; 76 77 public TwoPaneController(MailActivity activity, ViewMode viewMode) { 78 super(activity, viewMode); 79 } 80 81 public boolean isCurrentConversationJustPeeking() { 82 return mCurrentConversationJustPeeking; 83 } 84 85 private boolean isConversationOnlyMode() { 86 return getCurrentConversation() != null && !isCurrentConversationJustPeeking() 87 && !mLayout.shouldShowPreviewPanel(); 88 } 89 90 /** 91 * Display the conversation list fragment. 92 */ 93 private void initializeConversationListFragment() { 94 if (Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())) { 95 if (shouldEnterSearchConvMode()) { 96 mViewMode.enterSearchResultsConversationMode(); 97 } else { 98 mViewMode.enterSearchResultsListMode(); 99 } 100 } 101 renderConversationList(); 102 } 103 104 /** 105 * Render the conversation list in the correct pane. 106 */ 107 private void renderConversationList() { 108 if (mActivity == null) { 109 return; 110 } 111 FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction(); 112 // Use cross fading animation. 113 fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); 114 final ConversationListFragment conversationListFragment = 115 ConversationListFragment.newInstance(mConvListContext); 116 fragmentTransaction.replace(R.id.conversation_list_pane, conversationListFragment, 117 TAG_CONVERSATION_LIST); 118 fragmentTransaction.commitAllowingStateLoss(); 119 // Set default navigation here once the ConversationListFragment is created. 120 conversationListFragment.setNextFocusLeftId( 121 getClfNextFocusLeftId(getFolderListFragment().isMinimized())); 122 } 123 124 @Override 125 public boolean doesActionChangeConversationListVisibility(final int action) { 126 if (action == R.id.settings 127 || action == R.id.compose 128 || action == R.id.help_info_menu_item 129 || action == R.id.feedback_menu_item) { 130 return true; 131 } 132 133 return false; 134 } 135 136 @Override 137 protected boolean isConversationListVisible() { 138 return !mLayout.isConversationListCollapsed(); 139 } 140 141 @Override 142 protected void showConversationList(ConversationListContext listContext) { 143 initializeConversationListFragment(); 144 } 145 146 @Override 147 public @LayoutRes int getContentViewResource() { 148 return R.layout.two_pane_activity; 149 } 150 151 @Override 152 public boolean onCreate(Bundle savedState) { 153 mLayout = (TwoPaneLayout) mActivity.findViewById(R.id.two_pane_activity); 154 if (mLayout == null) { 155 // We need the layout for everything. Crash/Return early if it is null. 156 LogUtils.wtf(LOG_TAG, "mLayout is null!"); 157 return false; 158 } 159 mLayout.setController(this, Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())); 160 mActivity.getWindow().setBackgroundDrawable(null); 161 mIsTabletLandscape = !mActivity.getResources().getBoolean(R.bool.list_collapsible); 162 163 final FolderListFragment flf = getFolderListFragment(); 164 flf.setMiniDrawerEnabled(true); 165 flf.setMinimized(true); 166 167 if (savedState != null) { 168 mSavedMiscellaneousView = savedState.getBoolean(SAVED_MISCELLANEOUS_VIEW, false); 169 mMiscellaneousViewTransactionId = 170 savedState.getInt(SAVED_MISCELLANEOUS_VIEW_TRANSACTION_ID, -1); 171 } 172 173 // 2-pane layout is the main listener of view mode changes, and issues secondary 174 // notifications upon animation completion: 175 // (onConversationVisibilityChanged, onConversationListVisibilityChanged) 176 mViewMode.addListener(mLayout); 177 return super.onCreate(savedState); 178 } 179 180 @Override 181 public void onSaveInstanceState(Bundle outState) { 182 super.onSaveInstanceState(outState); 183 184 outState.putBoolean(SAVED_MISCELLANEOUS_VIEW, mMiscellaneousViewTransactionId >= 0); 185 outState.putInt(SAVED_MISCELLANEOUS_VIEW_TRANSACTION_ID, mMiscellaneousViewTransactionId); 186 } 187 188 @Override 189 public void onWindowFocusChanged(boolean hasFocus) { 190 if (hasFocus && !mLayout.isConversationListCollapsed()) { 191 // The conversation list is visible. 192 informCursorVisiblity(true); 193 } 194 } 195 196 @Override 197 public void switchToDefaultInboxOrChangeAccount(Account account) { 198 if (mViewMode.isSearchMode()) { 199 // We are in an activity on top of the main navigation activity. 200 // We need to return to it with a result code that indicates it should navigate to 201 // a different folder. 202 final Intent intent = new Intent(); 203 intent.putExtra(AbstractActivityController.EXTRA_ACCOUNT, account); 204 mActivity.setResult(Activity.RESULT_OK, intent); 205 mActivity.finish(); 206 return; 207 } 208 if (mViewMode.getMode() != ViewMode.CONVERSATION_LIST) { 209 mViewMode.enterConversationListMode(); 210 } 211 super.switchToDefaultInboxOrChangeAccount(account); 212 } 213 214 @Override 215 public void onFolderSelected(Folder folder) { 216 // It's possible that we are not in conversation list mode 217 if (mViewMode.isSearchMode()) { 218 // We are in an activity on top of the main navigation activity. 219 // We need to return to it with a result code that indicates it should navigate to 220 // a different folder. 221 final Intent intent = new Intent(); 222 intent.putExtra(AbstractActivityController.EXTRA_FOLDER, folder); 223 mActivity.setResult(Activity.RESULT_OK, intent); 224 mActivity.finish(); 225 return; 226 } else if (mViewMode.getMode() != ViewMode.CONVERSATION_LIST) { 227 mViewMode.enterConversationListMode(); 228 } 229 230 setHierarchyFolder(folder); 231 super.onFolderSelected(folder); 232 } 233 234 public boolean isDrawerOpen() { 235 final FolderListFragment flf = getFolderListFragment(); 236 return flf != null && !flf.isMinimized(); 237 } 238 239 @Override 240 protected void toggleDrawerState() { 241 final FolderListFragment flf = getFolderListFragment(); 242 if (flf == null) { 243 LogUtils.w(LOG_TAG, "no drawer to toggle open/closed"); 244 return; 245 } 246 flf.setMinimized(!flf.isMinimized()); 247 mLayout.requestLayout(); 248 resetActionBarIcon(); 249 250 final ConversationListFragment clf = getConversationListFragment(); 251 if (clf != null) { 252 clf.setNextFocusLeftId(getClfNextFocusLeftId(flf.isMinimized())); 253 254 final SwipeableListView list = clf.getListView(); 255 if (list != null) { 256 if (flf.isMinimized()) { 257 list.stopPreventingSwipes(); 258 } else { 259 list.preventSwipesEntirely(); 260 } 261 } 262 } 263 } 264 265 @Override 266 public boolean shouldPreventListSwipesEntirely() { 267 return isDrawerOpen(); 268 } 269 270 @Override 271 public void onViewModeChanged(int newMode) { 272 if (!mSavedMiscellaneousView && mMiscellaneousViewTransactionId >= 0) { 273 final FragmentManager fragmentManager = mActivity.getFragmentManager(); 274 fragmentManager.popBackStackImmediate(mMiscellaneousViewTransactionId, 275 FragmentManager.POP_BACK_STACK_INCLUSIVE); 276 mMiscellaneousViewTransactionId = -1; 277 } 278 mSavedMiscellaneousView = false; 279 280 super.onViewModeChanged(newMode); 281 if (!isConversationOnlyMode()) { 282 mFloatingComposeButton.setVisibility(View.VISIBLE); 283 } 284 if (newMode != ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION) { 285 // Clear the wait fragment 286 hideWaitForInitialization(); 287 } 288 // In conversation mode, if the conversation list is not visible, then the user cannot 289 // see the selected conversations. Disable the CAB mode while leaving the selected set 290 // untouched. 291 // When the conversation list is made visible again, try to enable the CAB 292 // mode if any conversations are selected. 293 if (newMode == ViewMode.CONVERSATION || newMode == ViewMode.CONVERSATION_LIST 294 || ViewMode.isAdMode(newMode)) { 295 enableOrDisableCab(); 296 } 297 } 298 299 private @IdRes int getClfNextFocusLeftId(boolean drawerMinimized) { 300 return (drawerMinimized) ? R.id.current_account_avatar : android.R.id.list; 301 } 302 303 @Override 304 public void onConversationVisibilityChanged(boolean visible) { 305 super.onConversationVisibilityChanged(visible); 306 if (!visible) { 307 mPagerController.hide(false /* changeVisibility */); 308 } else if (mConversationToShow != null) { 309 mPagerController.show(mAccount, mFolder, mConversationToShow, 310 false /* changeVisibility */); 311 mConversationToShow = null; 312 } 313 } 314 315 @Override 316 public void onConversationListVisibilityChanged(boolean visible) { 317 super.onConversationListVisibilityChanged(visible); 318 enableOrDisableCab(); 319 } 320 321 @Override 322 public void resetActionBarIcon() { 323 final ActionBar ab = mActivity.getSupportActionBar(); 324 final boolean isChildFolder = getFolder() != null && !Utils.isEmpty(getFolder().parent); 325 if (isConversationOnlyMode() || isChildFolder) { 326 ab.setHomeAsUpIndicator(R.drawable.ic_arrow_back_wht_24dp); 327 ab.setHomeActionContentDescription(0 /* system default */); 328 } else { 329 ab.setHomeAsUpIndicator(R.drawable.ic_drawer); 330 ab.setHomeActionContentDescription( 331 isDrawerOpen() ? R.string.drawer_close : R.string.drawer_open); 332 } 333 } 334 335 /** 336 * Enable or disable the CAB mode based on the visibility of the conversation list fragment. 337 */ 338 private void enableOrDisableCab() { 339 if (mLayout.isConversationListCollapsed()) { 340 disableCabMode(); 341 } else { 342 enableCabMode(); 343 } 344 } 345 346 @Override 347 public void onSetPopulated(ConversationSelectionSet set) { 348 super.onSetPopulated(set); 349 350 boolean showSenderImage = 351 (mAccount.settings.convListIcon == ConversationListIcon.SENDER_IMAGE); 352 if (!showSenderImage && mViewMode.isListMode()) { 353 getConversationListFragment().setChoiceNone(); 354 } 355 } 356 357 @Override 358 public void onSetEmpty() { 359 super.onSetEmpty(); 360 361 boolean showSenderImage = 362 (mAccount.settings.convListIcon == ConversationListIcon.SENDER_IMAGE); 363 if (!showSenderImage && mViewMode.isListMode()) { 364 getConversationListFragment().revertChoiceMode(); 365 } 366 } 367 368 @Override 369 protected void showConversation(Conversation conversation, boolean markAsRead) { 370 super.showConversation(conversation, markAsRead); 371 372 // 2-pane can ignore inLoaderCallbacks because it doesn't use 373 // FragmentManager.popBackStack(). 374 375 if (mActivity == null) { 376 return; 377 } 378 if (conversation == null) { 379 handleBackPress(); 380 return; 381 } 382 // If conversation list is not visible, then the user cannot see the CAB mode, so exit it. 383 // This is needed here (in addition to during viewmode changes) because orientation changes 384 // while viewing a conversation don't change the viewmode: the mode stays 385 // ViewMode.CONVERSATION and yet the conversation list goes in and out of visibility. 386 enableOrDisableCab(); 387 388 // close the drawer, if open 389 if (isDrawerOpen()) { 390 toggleDrawerState(); 391 } 392 393 // When a mode change is required, wait for onConversationVisibilityChanged(), the signal 394 // that the mode change animation has finished, before rendering the conversation. 395 mConversationToShow = conversation; 396 mCurrentConversationJustPeeking = !markAsRead; 397 398 final int mode = mViewMode.getMode(); 399 LogUtils.i(LOG_TAG, "IN TPC.showConv, oldMode=%s conv=%s", mode, mConversationToShow); 400 if (mode == ViewMode.SEARCH_RESULTS_LIST || mode == ViewMode.SEARCH_RESULTS_CONVERSATION) { 401 mViewMode.enterSearchResultsConversationMode(); 402 } else { 403 mViewMode.enterConversationMode(); 404 } 405 // load the conversation immediately if we're already in conversation mode 406 if (!mLayout.isModeChangePending()) { 407 onConversationVisibilityChanged(true); 408 } else { 409 LogUtils.i(LOG_TAG, "TPC.showConversation will wait for TPL.animationEnd to show!"); 410 } 411 } 412 413 @Override 414 public void onConversationSelected(Conversation conversation, boolean inLoaderCallbacks) { 415 super.onConversationSelected(conversation, inLoaderCallbacks); 416 if (!mCurrentConversationJustPeeking) { 417 // Shift the focus to the conversation in landscape mode. 418 mPagerController.focusPager(); 419 } 420 } 421 422 @Override 423 public void onConversationFocused(Conversation conversation) { 424 if (mIsTabletLandscape) { 425 showConversation(conversation, false /* markAsRead */); 426 } 427 } 428 429 @Override 430 public void setCurrentConversation(Conversation conversation) { 431 // Order is important! We want to calculate different *before* the superclass changes 432 // mCurrentConversation, so before super.setCurrentConversation(). 433 final long oldId = mCurrentConversation != null ? mCurrentConversation.id : -1; 434 final long newId = conversation != null ? conversation.id : -1; 435 final boolean different = oldId != newId; 436 437 // This call might change mCurrentConversation. 438 super.setCurrentConversation(conversation); 439 440 final ConversationListFragment convList = getConversationListFragment(); 441 if (convList != null && conversation != null) { 442 convList.setSelected(conversation.position, different); 443 } 444 } 445 446 @Override 447 protected void showWaitForInitialization() { 448 super.showWaitForInitialization(); 449 450 FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction(); 451 fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); 452 fragmentTransaction.replace(R.id.conversation_list_pane, getWaitFragment(), TAG_WAIT); 453 fragmentTransaction.commitAllowingStateLoss(); 454 } 455 456 @Override 457 protected void hideWaitForInitialization() { 458 final WaitFragment waitFragment = getWaitFragment(); 459 if (waitFragment == null) { 460 // We aren't showing a wait fragment: nothing to do 461 return; 462 } 463 // Remove the existing wait fragment from the back stack. 464 final FragmentTransaction fragmentTransaction = 465 mActivity.getFragmentManager().beginTransaction(); 466 fragmentTransaction.remove(waitFragment); 467 fragmentTransaction.commitAllowingStateLoss(); 468 super.hideWaitForInitialization(); 469 if (mViewMode.isWaitingForSync()) { 470 // We should come out of wait mode and display the account inbox. 471 loadAccountInbox(); 472 } 473 } 474 475 /** 476 * Up works as follows: 477 * 1) If the user is in a conversation and: 478 * a) the conversation list is hidden (portrait mode), shows the conv list and 479 * stays in conversation view mode. 480 * b) the conversation list is shown, goes back to conversation list mode. 481 * 2) If the user is in search results, up exits search. 482 * mode and returns the user to whatever view they were in when they began search. 483 * 3) If the user is in conversation list mode, there is no up. 484 */ 485 @Override 486 public boolean handleUpPress() { 487 if (isConversationOnlyMode()) { 488 handleBackPress(); 489 } else { 490 toggleDrawerState(); 491 } 492 493 return true; 494 } 495 496 @Override 497 public boolean handleBackPress() { 498 // Clear any visible undo bars. 499 mToastBar.hide(false, false /* actionClicked */); 500 if (isDrawerOpen()) { 501 toggleDrawerState(); 502 } else { 503 popView(false); 504 } 505 return true; 506 } 507 508 /** 509 * Pops the "view stack" to the last screen the user was viewing. 510 * 511 * @param preventClose Whether to prevent closing the app if the stack is empty. 512 */ 513 protected void popView(boolean preventClose) { 514 // If the user is in search query entry mode, or the user is viewing 515 // search results, exit 516 // the mode. 517 int mode = mViewMode.getMode(); 518 if (mode == ViewMode.SEARCH_RESULTS_LIST) { 519 mActivity.finish(); 520 } else if (mode == ViewMode.CONVERSATION || mViewMode.isAdMode()) { 521 // Go to conversation list. 522 mViewMode.enterConversationListMode(); 523 } else if (mode == ViewMode.SEARCH_RESULTS_CONVERSATION) { 524 mViewMode.enterSearchResultsListMode(); 525 } else { 526 // The Folder List fragment can be null for monkeys where we get a back before the 527 // folder list has had a chance to initialize. 528 final FolderListFragment folderList = getFolderListFragment(); 529 if (mode == ViewMode.CONVERSATION_LIST && folderList != null 530 && !Folder.isRoot(mFolder)) { 531 // If the user navigated via the left folders list into a child folder, 532 // back should take the user up to the parent folder's conversation list. 533 navigateUpFolderHierarchy(); 534 // Otherwise, if we are in the conversation list but not in the default 535 // inbox and not on expansive layouts, we want to switch back to the default 536 // inbox. This fixes b/9006969 so that on smaller tablets where we have this 537 // hybrid one and two-pane mode, we will return to the inbox. On larger tablets, 538 // we will instead exit the app. 539 } else if (!preventClose) { 540 // There is nothing else to pop off the stack. 541 mActivity.finish(); 542 } 543 } 544 } 545 546 @Override 547 public boolean shouldShowFirstConversation() { 548 return Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction()) 549 && shouldEnterSearchConvMode(); 550 } 551 552 @Override 553 public void onUndoAvailable(ToastBarOperation op) { 554 final int mode = mViewMode.getMode(); 555 final ConversationListFragment convList = getConversationListFragment(); 556 557 switch (mode) { 558 case ViewMode.SEARCH_RESULTS_LIST: 559 case ViewMode.CONVERSATION_LIST: 560 case ViewMode.SEARCH_RESULTS_CONVERSATION: 561 case ViewMode.CONVERSATION: 562 if (convList != null) { 563 mToastBar.show(getUndoClickedListener(convList.getAnimatedAdapter()), 564 Utils.convertHtmlToPlainText 565 (op.getDescription(mActivity.getActivityContext())), 566 R.string.undo, 567 true, /* replaceVisibleToast */ 568 op); 569 } 570 } 571 } 572 573 @Override 574 public void onError(final Folder folder, boolean replaceVisibleToast) { 575 showErrorToast(folder, replaceVisibleToast); 576 } 577 578 @Override 579 public boolean isDrawerEnabled() { 580 // two-pane has its own drawer-like thing that expands inline from a minimized state. 581 return false; 582 } 583 584 @Override 585 public int getFolderListViewChoiceMode() { 586 // By default, we want to allow one item to be selected in the folder list 587 return ListView.CHOICE_MODE_SINGLE; 588 } 589 590 private int mMiscellaneousViewTransactionId = -1; 591 592 @Override 593 public void launchFragment(final Fragment fragment, final int selectPosition) { 594 final int containerViewId = TwoPaneLayout.MISCELLANEOUS_VIEW_ID; 595 596 final FragmentManager fragmentManager = mActivity.getFragmentManager(); 597 if (fragmentManager.findFragmentByTag(TAG_CUSTOM_FRAGMENT) == null) { 598 final FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); 599 fragmentTransaction.addToBackStack(null); 600 fragmentTransaction.replace(containerViewId, fragment, TAG_CUSTOM_FRAGMENT); 601 mMiscellaneousViewTransactionId = fragmentTransaction.commitAllowingStateLoss(); 602 fragmentManager.executePendingTransactions(); 603 } 604 605 if (selectPosition >= 0) { 606 getConversationListFragment().setRawSelected(selectPosition, true); 607 } 608 } 609 610 @Override 611 public boolean onInterceptCVDownEvent() { 612 // handle a down event on CV by closing the drawer if open 613 if (isDrawerOpen()) { 614 toggleDrawerState(); 615 return true; 616 } 617 return false; 618 } 619 620 @Override 621 public boolean onInterceptKeyFromCV(int keyCode, KeyEvent keyEvent, boolean navigateAway) { 622 // Override left/right key presses in landscape mode. 623 if (navigateAway) { 624 if (keyEvent.getAction() == KeyEvent.ACTION_UP) { 625 ConversationListFragment clf = getConversationListFragment(); 626 if (clf != null) { 627 clf.getListView().requestFocus(); 628 } 629 } 630 return true; 631 } 632 return false; 633 } 634 635 @Override 636 public boolean isTwoPaneLandscape() { 637 return mIsTabletLandscape; 638 } 639 640 @Override 641 public boolean shouldShowSearchBarByDefault() { 642 final int mode = mViewMode.getMode(); 643 return mode == ViewMode.SEARCH_RESULTS_LIST || 644 (mIsTabletLandscape && mode == ViewMode.SEARCH_RESULTS_CONVERSATION); 645 } 646} 647