TwoPaneController.java revision 516b31665599f35d6845c5ffcaaab547ceb66640
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.Fragment; 21import android.app.FragmentManager; 22import android.app.FragmentTransaction; 23import android.content.Intent; 24import android.net.Uri; 25import android.os.Bundle; 26import android.support.v4.widget.DrawerLayout; 27import android.view.Gravity; 28import android.widget.FrameLayout; 29import android.widget.ListView; 30 31import com.android.mail.ConversationListContext; 32import com.android.mail.R; 33import com.android.mail.providers.Conversation; 34import com.android.mail.providers.Folder; 35import com.android.mail.providers.UIProvider.ConversationListIcon; 36import com.android.mail.utils.LogUtils; 37import com.android.mail.utils.Utils; 38 39/** 40 * Controller for two-pane Mail activity. Two Pane is used for tablets, where screen real estate 41 * abounds. 42 */ 43public final class TwoPaneController extends AbstractActivityController { 44 private TwoPaneLayout mLayout; 45 private Conversation mConversationToShow; 46 47 public TwoPaneController(MailActivity activity, ViewMode viewMode) { 48 super(activity, viewMode); 49 } 50 51 /** 52 * Display the conversation list fragment. 53 */ 54 private void initializeConversationListFragment() { 55 if (Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())) { 56 if (shouldEnterSearchConvMode()) { 57 mViewMode.enterSearchResultsConversationMode(); 58 } else { 59 mViewMode.enterSearchResultsListMode(); 60 } 61 } 62 renderConversationList(); 63 } 64 65 /** 66 * Render the conversation list in the correct pane. 67 */ 68 private void renderConversationList() { 69 if (mActivity == null) { 70 return; 71 } 72 FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction(); 73 // Use cross fading animation. 74 fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); 75 final Fragment conversationListFragment = 76 ConversationListFragment.newInstance(mConvListContext); 77 fragmentTransaction.replace(R.id.conversation_list_pane, conversationListFragment, 78 TAG_CONVERSATION_LIST); 79 fragmentTransaction.commitAllowingStateLoss(); 80 } 81 82 @Override 83 public boolean doesActionChangeConversationListVisibility(final int action) { 84 if (action == R.id.settings 85 || action == R.id.compose 86 || action == R.id.help_info_menu_item 87 || action == R.id.manage_folders_item 88 || action == R.id.folder_options 89 || action == R.id.feedback_menu_item) { 90 return true; 91 } 92 93 return false; 94 } 95 96 @Override 97 protected boolean isConversationListVisible() { 98 return !mLayout.isConversationListCollapsed(); 99 } 100 101 @Override 102 public void showConversationList(ConversationListContext listContext) { 103 super.showConversationList(listContext); 104 initializeConversationListFragment(); 105 } 106 107 @Override 108 public boolean onCreate(Bundle savedState) { 109 mActivity.setContentView(R.layout.two_pane_activity); 110 mDrawerContainer = (DrawerLayout) mActivity.findViewById(R.id.drawer_container); 111 mDrawerPullout = mDrawerContainer.findViewById(R.id.content_pane); 112 mLayout = (TwoPaneLayout) mActivity.findViewById(R.id.two_pane_activity); 113 if (mLayout == null) { 114 // We need the layout for everything. Crash/Return early if it is null. 115 LogUtils.wtf(LOG_TAG, "mLayout is null!"); 116 return false; 117 } 118 mLayout.setController(this, Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())); 119 mLayout.setDrawerLayout(mDrawerContainer); 120 121 // 2-pane layout is the main listener of view mode changes, and issues secondary 122 // notifications upon animation completion: 123 // (onConversationVisibilityChanged, onConversationListVisibilityChanged) 124 mViewMode.addListener(mLayout); 125 return super.onCreate(savedState); 126 } 127 128 @Override 129 public void onWindowFocusChanged(boolean hasFocus) { 130 if (hasFocus && !mLayout.isConversationListCollapsed()) { 131 // The conversation list is visible. 132 informCursorVisiblity(true); 133 } 134 } 135 136 @Override 137 public void onFolderChanged(Folder folder) { 138 super.onFolderChanged(folder); 139 exitCabMode(); 140 } 141 142 @Override 143 public void onFolderSelected(Folder folder) { 144 // It's possible that we are not in conversation list mode 145 if (mViewMode.getMode() != ViewMode.CONVERSATION_LIST) { 146 mViewMode.enterConversationListMode(); 147 } 148 149 if (folder.hasChildren) { 150 // Show the up affordance when digging into child folders. 151 mActionBarView.setBackButton(); 152 } 153 setHierarchyFolder(folder); 154 super.onFolderSelected(folder); 155 } 156 157 private void goUpFolderHierarchy(Folder current) { 158 // If the current folder is a child, up should show the parent folder. 159 // Fix this to load the parent folder: http://b/9694899 160// final Folder parent = current.parent; 161// if (parent != null) { 162// onFolderSelected(parent); 163// } 164 } 165 166 @Override 167 public void onViewModeChanged(int newMode) { 168 if (mMiscellaneousViewTransactionId >= 0) { 169 final FragmentManager fragmentManager = mActivity.getFragmentManager(); 170 fragmentManager.popBackStackImmediate(mMiscellaneousViewTransactionId, 171 FragmentManager.POP_BACK_STACK_INCLUSIVE); 172 mMiscellaneousViewTransactionId = -1; 173 } 174 175 super.onViewModeChanged(newMode); 176 if (newMode != ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION) { 177 // Clear the wait fragment 178 hideWaitForInitialization(); 179 } 180 // In conversation mode, if the conversation list is not visible, then the user cannot 181 // see the selected conversations. Disable the CAB mode while leaving the selected set 182 // untouched. 183 // When the conversation list is made visible again, try to enable the CAB 184 // mode if any conversations are selected. 185 if (newMode == ViewMode.CONVERSATION || newMode == ViewMode.CONVERSATION_LIST 186 || ViewMode.isAdMode(newMode)) { 187 enableOrDisableCab(); 188 } 189 } 190 191 @Override 192 public void onConversationVisibilityChanged(boolean visible) { 193 super.onConversationVisibilityChanged(visible); 194 if (!visible) { 195 mPagerController.hide(false /* changeVisibility */); 196 } else if (mConversationToShow != null) { 197 mPagerController.show(mAccount, mFolder, mConversationToShow, 198 false /* changeVisibility */); 199 mConversationToShow = null; 200 } 201 } 202 203 @Override 204 public void onConversationListVisibilityChanged(boolean visible) { 205 super.onConversationListVisibilityChanged(visible); 206 enableOrDisableCab(); 207 } 208 209 @Override 210 public void resetActionBarIcon() { 211 if (isDrawerEnabled()) { 212 return; 213 } 214 // On two-pane, the back button is only removed in the conversation list mode, and shown 215 // for every other condition. 216 if (mViewMode.isListMode() || mViewMode.isWaitingForSync()) { 217 mActionBarView.removeBackButton(); 218 } else { 219 mActionBarView.setBackButton(); 220 } 221 } 222 223 /** 224 * Enable or disable the CAB mode based on the visibility of the conversation list fragment. 225 */ 226 private void enableOrDisableCab() { 227 if (mLayout.isConversationListCollapsed()) { 228 disableCabMode(); 229 } else { 230 enableCabMode(); 231 } 232 } 233 234 @Override 235 public void onSetPopulated(ConversationSelectionSet set) { 236 super.onSetPopulated(set); 237 238 boolean showSenderImage = 239 (mAccount.settings.convListIcon == ConversationListIcon.SENDER_IMAGE); 240 if (!showSenderImage && mViewMode.isListMode()) { 241 getConversationListFragment().setChoiceNone(); 242 } 243 } 244 245 @Override 246 public void onSetEmpty() { 247 super.onSetEmpty(); 248 249 boolean showSenderImage = 250 (mAccount.settings.convListIcon == ConversationListIcon.SENDER_IMAGE); 251 if (!showSenderImage && mViewMode.isListMode()) { 252 getConversationListFragment().revertChoiceMode(); 253 } 254 } 255 256 @Override 257 protected void showConversation(Conversation conversation, boolean inLoaderCallbacks) { 258 super.showConversation(conversation, inLoaderCallbacks); 259 260 // 2-pane can ignore inLoaderCallbacks because it doesn't use 261 // FragmentManager.popBackStack(). 262 263 if (mActivity == null) { 264 return; 265 } 266 if (conversation == null) { 267 handleBackPress(); 268 return; 269 } 270 // If conversation list is not visible, then the user cannot see the CAB mode, so exit it. 271 // This is needed here (in addition to during viewmode changes) because orientation changes 272 // while viewing a conversation don't change the viewmode: the mode stays 273 // ViewMode.CONVERSATION and yet the conversation list goes in and out of visibility. 274 enableOrDisableCab(); 275 276 // When a mode change is required, wait for onConversationVisibilityChanged(), the signal 277 // that the mode change animation has finished, before rendering the conversation. 278 mConversationToShow = conversation; 279 280 final int mode = mViewMode.getMode(); 281 LogUtils.i(LOG_TAG, "IN TPC.showConv, oldMode=%s conv=%s", mode, mConversationToShow); 282 if (mode == ViewMode.SEARCH_RESULTS_LIST || mode == ViewMode.SEARCH_RESULTS_CONVERSATION) { 283 mViewMode.enterSearchResultsConversationMode(); 284 } else { 285 mViewMode.enterConversationMode(); 286 } 287 // load the conversation immediately if we're already in conversation mode 288 if (!mLayout.isModeChangePending()) { 289 onConversationVisibilityChanged(true); 290 } else { 291 LogUtils.i(LOG_TAG, "TPC.showConversation will wait for TPL.animationEnd to show!"); 292 } 293 } 294 295 @Override 296 public void setCurrentConversation(Conversation conversation) { 297 // Order is important! We want to calculate different *before* the superclass changes 298 // mCurrentConversation, so before super.setCurrentConversation(). 299 final long oldId = mCurrentConversation != null ? mCurrentConversation.id : -1; 300 final long newId = conversation != null ? conversation.id : -1; 301 final boolean different = oldId != newId; 302 303 // This call might change mCurrentConversation. 304 super.setCurrentConversation(conversation); 305 306 final ConversationListFragment convList = getConversationListFragment(); 307 if (convList != null && conversation != null) { 308 convList.setSelected(conversation.position, different); 309 } 310 } 311 312 @Override 313 public void showWaitForInitialization() { 314 super.showWaitForInitialization(); 315 316 FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction(); 317 fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); 318 fragmentTransaction.replace(R.id.conversation_list_pane, getWaitFragment(), TAG_WAIT); 319 fragmentTransaction.commitAllowingStateLoss(); 320 } 321 322 @Override 323 protected void hideWaitForInitialization() { 324 final WaitFragment waitFragment = getWaitFragment(); 325 if (waitFragment == null) { 326 // We aren't showing a wait fragment: nothing to do 327 return; 328 } 329 // Remove the existing wait fragment from the back stack. 330 final FragmentTransaction fragmentTransaction = 331 mActivity.getFragmentManager().beginTransaction(); 332 fragmentTransaction.remove(waitFragment); 333 fragmentTransaction.commitAllowingStateLoss(); 334 super.hideWaitForInitialization(); 335 if (mViewMode.isWaitingForSync()) { 336 // We should come out of wait mode and display the account inbox. 337 loadAccountInbox(); 338 } 339 } 340 341 /** 342 * Up works as follows: 343 * 1) If the user is in a conversation and: 344 * a) the conversation list is hidden (portrait mode), shows the conv list and 345 * stays in conversation view mode. 346 * b) the conversation list is shown, goes back to conversation list mode. 347 * 2) If the user is in search results, up exits search. 348 * mode and returns the user to whatever view they were in when they began search. 349 * 3) If the user is in conversation list mode, there is no up. 350 */ 351 @Override 352 public boolean handleUpPress() { 353 int mode = mViewMode.getMode(); 354 if (mode == ViewMode.CONVERSATION || mViewMode.isAdMode()) { 355 handleBackPress(); 356 } else if (mode == ViewMode.SEARCH_RESULTS_CONVERSATION) { 357 if (mLayout.isConversationListCollapsed() 358 || (ConversationListContext.isSearchResult(mConvListContext) && !Utils. 359 showTwoPaneSearchResults(mActivity.getApplicationContext()))) { 360 handleBackPress(); 361 } else { 362 mActivity.finish(); 363 } 364 } else if (mode == ViewMode.SEARCH_RESULTS_LIST) { 365 mActivity.finish(); 366 } else if (mode == ViewMode.CONVERSATION_LIST 367 || mode == ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION) { 368 final boolean isTopLevel = (mFolder == null) || (mFolder.parent == Uri.EMPTY); 369 370 if (isTopLevel) { 371 // Show the drawer 372 toggleDrawerState(); 373 } else { 374 popView(true); 375 } 376 } 377 return true; 378 } 379 380 @Override 381 public boolean handleBackPress() { 382 // Clear any visible undo bars. 383 mToastBar.hide(false, false /* actionClicked */); 384 popView(false); 385 return true; 386 } 387 388 /** 389 * Pops the "view stack" to the last screen the user was viewing. 390 * 391 * @param preventClose Whether to prevent closing the app if the stack is empty. 392 */ 393 protected void popView(boolean preventClose) { 394 // If the user is in search query entry mode, or the user is viewing 395 // search results, exit 396 // the mode. 397 int mode = mViewMode.getMode(); 398 if (mode == ViewMode.SEARCH_RESULTS_LIST) { 399 mActivity.finish(); 400 } else if (mode == ViewMode.CONVERSATION || mViewMode.isAdMode()) { 401 // Go to conversation list. 402 mViewMode.enterConversationListMode(); 403 } else if (mode == ViewMode.SEARCH_RESULTS_CONVERSATION) { 404 mViewMode.enterSearchResultsListMode(); 405 } else { 406 // The Folder List fragment can be null for monkeys where we get a back before the 407 // folder list has had a chance to initialize. 408 final FolderListFragment folderList = getFolderListFragment(); 409 if (mode == ViewMode.CONVERSATION_LIST && folderList != null 410 && folderList.showingHierarchy()) { 411 // If the user navigated via the left folders list into a child folder, 412 // back should take the user up to the parent folder's conversation list. 413 // TODO: Clean this code up: http://b/9694899 414 final Folder hierarchyFolder = getHierarchyFolder(); 415 if (hierarchyFolder.parent != Uri.EMPTY) { 416 goUpFolderHierarchy(hierarchyFolder); 417 } else { 418 // Show inbox; we are at the top of the hierarchy we were 419 // showing, and it doesn't have a parent, so we must want to 420 // the basic account folder list. 421 loadAccountInbox(); 422 } 423 // Otherwise, if we are in the conversation list but not in the default 424 // inbox and not on expansive layouts, we want to switch back to the default 425 // inbox. This fixes b/9006969 so that on smaller tablets where we have this 426 // hybrid one and two-pane mode, we will return to the inbox. On larger tablets, 427 // we will instead exit the app. 428 } else { 429 // Don't think mLayout could be null but checking just in case 430 if (mLayout == null) { 431 LogUtils.wtf(LOG_TAG, new Throwable(), "mLayout is null"); 432 } 433 // mFolder could be null if back is pressed while account is waiting for sync 434 final boolean shouldLoadInbox = mode == ViewMode.CONVERSATION_LIST && 435 mFolder != null && 436 !mFolder.folderUri.equals(mAccount.settings.defaultInbox) && 437 mLayout != null && !mLayout.isExpansiveLayout(); 438 if (shouldLoadInbox) { 439 loadAccountInbox(); 440 } else if (!preventClose) { 441 // There is nothing else to pop off the stack. 442 mActivity.finish(); 443 } 444 } 445 } 446 } 447 448 @Override 449 public void exitSearchMode() { 450 final int mode = mViewMode.getMode(); 451 if (mode == ViewMode.SEARCH_RESULTS_LIST 452 || (mode == ViewMode.SEARCH_RESULTS_CONVERSATION 453 && Utils.showTwoPaneSearchResults(mActivity.getApplicationContext()))) { 454 mActivity.finish(); 455 } 456 } 457 458 @Override 459 public boolean shouldShowFirstConversation() { 460 return Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction()) 461 && shouldEnterSearchConvMode(); 462 } 463 464 @Override 465 public void onUndoAvailable(ToastBarOperation op) { 466 final int mode = mViewMode.getMode(); 467 final ConversationListFragment convList = getConversationListFragment(); 468 469 repositionToastBar(op); 470 471 switch (mode) { 472 case ViewMode.SEARCH_RESULTS_LIST: 473 case ViewMode.CONVERSATION_LIST: 474 case ViewMode.SEARCH_RESULTS_CONVERSATION: 475 case ViewMode.CONVERSATION: 476 if (convList != null) { 477 mToastBar.show(getUndoClickedListener(convList.getAnimatedAdapter()), 478 0, 479 Utils.convertHtmlToPlainText 480 (op.getDescription(mActivity.getActivityContext())), 481 true, /* showActionIcon */ 482 R.string.undo, 483 true, /* replaceVisibleToast */ 484 op); 485 } 486 } 487 } 488 489 public void repositionToastBar(ToastBarOperation op) { 490 repositionToastBar(op.isBatchUndo()); 491 } 492 493 /** 494 * Set the toast bar's layout params to position it in the right place 495 * depending the current view mode. 496 * 497 * @param convModeShowInList if we're in conversation mode, should the toast 498 * bar appear over the list? no effect when not in conversation mode. 499 */ 500 private void repositionToastBar(boolean convModeShowInList) { 501 final int mode = mViewMode.getMode(); 502 final FrameLayout.LayoutParams params = 503 (FrameLayout.LayoutParams) mToastBar.getLayoutParams(); 504 switch (mode) { 505 case ViewMode.SEARCH_RESULTS_LIST: 506 case ViewMode.CONVERSATION_LIST: 507 params.width = mLayout.computeConversationListWidth() - params.leftMargin 508 - params.rightMargin; 509 params.gravity = Gravity.BOTTOM | Gravity.RIGHT; 510 mToastBar.setLayoutParams(params); 511 mToastBar.setConversationMode(false); 512 break; 513 case ViewMode.SEARCH_RESULTS_CONVERSATION: 514 case ViewMode.CONVERSATION: 515 if (convModeShowInList && !mLayout.isConversationListCollapsed()) { 516 // Show undo bar in the conversation list. 517 params.gravity = Gravity.BOTTOM | Gravity.LEFT; 518 params.width = mLayout.computeConversationListWidth() - params.leftMargin 519 - params.rightMargin; 520 mToastBar.setLayoutParams(params); 521 mToastBar.setConversationMode(false); 522 } else { 523 // Show undo bar in the conversation. 524 params.gravity = Gravity.BOTTOM | Gravity.RIGHT; 525 params.width = mLayout.computeConversationWidth() - params.leftMargin 526 - params.rightMargin; 527 mToastBar.setLayoutParams(params); 528 mToastBar.setConversationMode(true); 529 } 530 break; 531 } 532 } 533 534 @Override 535 protected void hideOrRepositionToastBar(final boolean animated) { 536 final int oldViewMode = mViewMode.getMode(); 537 mLayout.postDelayed(new Runnable() { 538 @Override 539 public void run() { 540 if (/* the touch did not open a conversation */oldViewMode == mViewMode.getMode() || 541 /* animation has ended */!mToastBar.isAnimating()) { 542 mToastBar.hide(animated, false /* actionClicked */); 543 } else { 544 // the touch opened a conversation, reposition undo bar 545 repositionToastBar(mToastBar.getOperation()); 546 } 547 } 548 }, 549 /* Give time for ViewMode to change from the touch */ 550 mContext.getResources().getInteger(R.integer.dismiss_undo_bar_delay_ms)); 551 } 552 553 @Override 554 public void onError(final Folder folder, boolean replaceVisibleToast) { 555 repositionToastBar(true /* convModeShowInList */); 556 showErrorToast(folder, replaceVisibleToast); 557 } 558 559 @Override 560 public boolean isDrawerEnabled() { 561 return mLayout.isDrawerEnabled(); 562 } 563 564 @Override 565 public int getFolderListViewChoiceMode() { 566 // By default, we want to allow one item to be selected in the folder list 567 return ListView.CHOICE_MODE_SINGLE; 568 } 569 570 private int mMiscellaneousViewTransactionId = -1; 571 572 @Override 573 public void launchFragment(final Fragment fragment) { 574 final int containerViewId = TwoPaneLayout.MISCELLANEOUS_VIEW_ID; 575 576 final FragmentManager fragmentManager = mActivity.getFragmentManager(); 577 final FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); 578 fragmentTransaction.addToBackStack(null); 579 fragmentTransaction.replace(containerViewId, fragment, TAG_CUSTOM_FRAGMENT); 580 mMiscellaneousViewTransactionId = fragmentTransaction.commitAllowingStateLoss(); 581 fragmentManager.executePendingTransactions(); 582 } 583} 584