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