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