TwoPaneController.java revision dd6a7ce32c4003bd0941e2f18fcf5b80b5cd43c5
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.text.Html; 27import android.text.TextUtils; 28import android.view.Gravity; 29import android.widget.FrameLayout; 30 31import com.android.mail.ConversationListContext; 32import com.android.mail.R; 33import com.android.mail.providers.Account; 34import com.android.mail.providers.Conversation; 35import com.android.mail.providers.Folder; 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 */ 43 44// Called TwoPaneActivityController in Gmail. 45public final class TwoPaneController extends AbstractActivityController { 46 private TwoPaneLayout mLayout; 47 private Conversation mConversationToShow; 48 49 /** 50 * @param activity 51 * @param viewMode 52 */ 53 public TwoPaneController(MailActivity activity, ViewMode viewMode) { 54 super(activity, viewMode); 55 } 56 57 /** 58 * Display the conversation list fragment. 59 * @param show 60 */ 61 private void initializeConversationListFragment(boolean show) { 62 if (show) { 63 if (Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())) { 64 if (Utils.showTwoPaneSearchResults(mActivity.getActivityContext())) { 65 mViewMode.enterSearchResultsConversationMode(); 66 } else { 67 mViewMode.enterSearchResultsListMode(); 68 } 69 } else { 70 mViewMode.enterConversationListMode(); 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 Fragment conversationListFragment = ConversationListFragment.newInstance(mConvListContext); 87 fragmentTransaction.replace(R.id.conversation_list_pane, conversationListFragment, 88 TAG_CONVERSATION_LIST); 89 fragmentTransaction.commitAllowingStateLoss(); 90 } 91 92 /** 93 * Render the folder list in the correct pane. 94 */ 95 private void renderFolderList() { 96 if (mActivity == null) { 97 return; 98 } 99 createFolderListFragment(null, mAccount.folderListUri); 100 } 101 102 private void createFolderListFragment(Folder parent, Uri uri) { 103 setHierarchyFolder(parent); 104 // Create a sectioned FolderListFragment. 105 FolderListFragment folderListFragment = FolderListFragment.newInstance(parent, uri, true); 106 FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction(); 107 if (Utils.useFolderListFragmentTransition(mActivity.getActivityContext())) { 108 fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); 109 } 110 fragmentTransaction.replace(R.id.content_pane, folderListFragment, TAG_FOLDER_LIST); 111 fragmentTransaction.commitAllowingStateLoss(); 112 // Since we are showing the folder list, we are at the start of the view 113 // stack. 114 resetActionBarIcon(); 115 } 116 117 @Override 118 protected boolean isConversationListVisible() { 119 return !mLayout.isConversationListCollapsed(); 120 } 121 122 @Override 123 public void showConversationList(ConversationListContext listContext) { 124 super.showConversationList(listContext); 125 initializeConversationListFragment(true); 126 } 127 128 @Override 129 public void showFolderList() { 130 // On two-pane layouts, showing the folder list takes you to the top level of the 131 // application, which is the same as pressing the Up button 132 onUpPressed(); 133 } 134 135 @Override 136 public boolean onCreate(Bundle savedState) { 137 mActivity.setContentView(R.layout.two_pane_activity); 138 mLayout = (TwoPaneLayout) mActivity.findViewById(R.id.two_pane_activity); 139 if (mLayout == null) { 140 // We need the layout for everything. Crash early if it is null. 141 LogUtils.wtf(LOG_TAG, "mLayout is null!"); 142 } 143 mLayout.setController(this, Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())); 144 145 // 2-pane layout is the main listener of view mode changes, and issues secondary 146 // notifications upon animation completion: 147 // (onConversationVisibilityChanged, onConversationListVisibilityChanged) 148 mViewMode.addListener(mLayout); 149 final boolean isParentInitialized = super.onCreate(savedState); 150 return isParentInitialized; 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 onAccountChanged(Account account) { 163 super.onAccountChanged(account); 164 renderFolderList(); 165 } 166 167 @Override 168 public void onFolderChanged(Folder folder) { 169 super.onFolderChanged(folder); 170 exitCabMode(); 171 final FolderListFragment folderList = getFolderListFragment(); 172 if (folderList == null && mViewMode.getMode() == ViewMode.CONVERSATION_LIST) { 173 // Create a folder list fragment if none exists. 174 renderFolderList(); 175 } 176 } 177 178 @Override 179 public void onFolderSelected(Folder folder) { 180 if (folder.hasChildren && !folder.equals(getHierarchyFolder())) { 181 // Replace this fragment with a new FolderListFragment 182 // showing this folder's children if we are not already looking 183 // at the child view for this folder. 184 createFolderListFragment(folder, folder.childFoldersListUri); 185 // Show the up affordance when digging into child folders. 186 mActionBarView.setBackButton(); 187 super.onFolderSelected(folder); 188 } else { 189 setHierarchyFolder(folder); 190 super.onFolderSelected(folder); 191 } 192 } 193 194 private void goUpFolderHierarchy(Folder current) { 195 Folder parent = current.parent; 196 if (parent.parent != null) { 197 createFolderListFragment(parent.parent, parent.parent.childFoldersListUri); 198 // Show the up affordance when digging into child folders. 199 mActionBarView.setBackButton(); 200 } else { 201 onFolderSelected(parent); 202 } 203 } 204 205 @Override 206 public void onViewModeChanged(int newMode) { 207 super.onViewModeChanged(newMode); 208 if (newMode != ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION) { 209 // Clear the wait fragment 210 hideWaitForInitialization(); 211 } 212 // In conversation mode, if the conversation list is not visible, then the user cannot 213 // see the selected conversations. Disable the CAB mode while leaving the selected set 214 // untouched. 215 // When the conversation list is made visible again, try to enable the CAB 216 // mode if any conversations are selected. 217 if (newMode == ViewMode.CONVERSATION || newMode == ViewMode.CONVERSATION_LIST){ 218 enableOrDisableCab(); 219 } 220 resetActionBarIcon(); 221 } 222 223 @Override 224 public void onConversationVisibilityChanged(boolean visible) { 225 super.onConversationVisibilityChanged(visible); 226 if (!visible) { 227 mPagerController.hide(false /* changeVisibility */); 228 } else if (mConversationToShow != null) { 229 mPagerController.show(mAccount, mFolder, mConversationToShow, 230 false /* changeVisibility */); 231 mConversationToShow = null; 232 } 233 } 234 235 @Override 236 public void onConversationListVisibilityChanged(boolean visible) { 237 super.onConversationListVisibilityChanged(visible); 238 enableOrDisableCab(); 239 } 240 241 @Override 242 public void resetActionBarIcon() { 243 // If the viewmode is not set, preserve existing icon. 244 if (mViewMode.getMode() == ViewMode.UNKNOWN) { 245 return; 246 } 247 if (mViewMode.isListMode()) { 248 mActionBarView.removeBackButton(); 249 } else { 250 mActionBarView.setBackButton(); 251 } 252 } 253 254 /** 255 * Enable or disable the CAB mode based on the visibility of the conversation list fragment. 256 */ 257 private final void enableOrDisableCab() { 258 if (mLayout.isConversationListCollapsed()) { 259 disableCabMode(); 260 } else { 261 enableCabMode(); 262 } 263 } 264 265 @Override 266 protected void showConversation(Conversation conversation, boolean inLoaderCallbacks) { 267 super.showConversation(conversation, inLoaderCallbacks); 268 269 // 2-pane can ignore inLoaderCallbacks because it doesn't use 270 // FragmentManager.popBackStack(). 271 272 if (mActivity == null) { 273 return; 274 } 275 if (conversation == null) { 276 onBackPressed(); 277 return; 278 } 279 // If conversation list is not visible, then the user cannot see the CAB mode, so exit it. 280 // This is needed here (in addition to during viewmode changes) because orientation changes 281 // while viewing a conversation don't change the viewmode: the mode stays 282 // ViewMode.CONVERSATION and yet the conversation list goes in and out of visibility. 283 enableOrDisableCab(); 284 285 // When a mode change is required, wait for onConversationVisibilityChanged(), the signal 286 // that the mode change animation has finished, before rendering the conversation. 287 mConversationToShow = conversation; 288 289 final int mode = mViewMode.getMode(); 290 boolean changedMode = false; 291 if (mode == ViewMode.SEARCH_RESULTS_LIST || mode == ViewMode.SEARCH_RESULTS_CONVERSATION) { 292 changedMode = mViewMode.enterSearchResultsConversationMode(); 293 } else { 294 changedMode = mViewMode.enterConversationMode(); 295 } 296 // load the conversation immediately if we're already in conversation mode 297 if (!changedMode) { 298 onConversationVisibilityChanged(true); 299 } 300 } 301 302 @Override 303 public void setCurrentConversation(Conversation conversation) { 304 long oldId = mCurrentConversation != null ? mCurrentConversation.id : -1; 305 long newId = conversation != null ? conversation.id : -1; 306 boolean different = oldId != newId; 307 super.setCurrentConversation(conversation); 308 final ConversationListFragment convList = getConversationListFragment(); 309 if (convList != null && conversation != null) { 310 convList.setSelected(conversation.position, different); 311 } 312 } 313 314 @Override 315 public void showWaitForInitialization() { 316 super.showWaitForInitialization(); 317 318 final Fragment waitFragment = WaitFragment.newInstance(mAccount); 319 FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction(); 320 fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); 321 fragmentTransaction.replace(R.id.wait, waitFragment, TAG_WAIT); 322 fragmentTransaction.commitAllowingStateLoss(); 323 } 324 325 @Override 326 protected void hideWaitForInitialization() { 327 final FragmentManager manager = mActivity.getFragmentManager(); 328 final WaitFragment waitFragment = (WaitFragment)manager.findFragmentByTag(TAG_WAIT); 329 if (waitFragment != null) { 330 FragmentTransaction fragmentTransaction = 331 mActivity.getFragmentManager().beginTransaction(); 332 fragmentTransaction.remove(waitFragment); 333 fragmentTransaction.commitAllowingStateLoss(); 334 } 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 onUpPressed() { 349 int mode = mViewMode.getMode(); 350 if (mode == ViewMode.CONVERSATION) { 351 mActivity.onBackPressed(); 352 } else if (mode == ViewMode.SEARCH_RESULTS_CONVERSATION) { 353 if (mLayout.isConversationListCollapsed() 354 || (ConversationListContext.isSearchResult(mConvListContext) && !Utils. 355 showTwoPaneSearchResults(mActivity.getApplicationContext()))) { 356 onBackPressed(); 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 onBackPressed() { 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 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 && Utils.showTwoPaneSearchResults(mActivity.getApplicationContext()); 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()) { 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 FrameLayout.LayoutParams params = 458 (FrameLayout.LayoutParams) mToastBar.getLayoutParams(); 459 final ConversationListFragment convList = getConversationListFragment(); 460 int undoBarWidth = getUndoBarWidth(mode, op); 461 switch (mode) { 462 case ViewMode.SEARCH_RESULTS_LIST: 463 case ViewMode.CONVERSATION_LIST: 464 params.width = undoBarWidth - params.leftMargin - params.rightMargin; 465 params.gravity = Gravity.BOTTOM | Gravity.RIGHT; 466 mToastBar.setLayoutParams(params); 467 mToastBar.setConversationMode(false); 468 if (convList != null) { 469 mToastBar.show( 470 getUndoClickedListener(convList.getAnimatedAdapter()), 471 0, 472 Html.fromHtml(op.getDescription(mActivity.getActivityContext(), 473 mFolder)), 474 true, /* showActionIcon */ 475 R.string.undo, 476 true, /* replaceVisibleToast */ 477 op); 478 } 479 break; 480 case ViewMode.SEARCH_RESULTS_CONVERSATION: 481 case ViewMode.CONVERSATION: 482 if (op.isBatchUndo()) { 483 // Show undo bar in the conversation list. 484 params.gravity = Gravity.BOTTOM | Gravity.LEFT; 485 params.width = undoBarWidth - params.leftMargin - params.rightMargin; 486 mToastBar.setLayoutParams(params); 487 mToastBar.setConversationMode(false); 488 } else { 489 // Show undo bar in the conversation. 490 params.gravity = Gravity.BOTTOM | Gravity.RIGHT; 491 params.width = undoBarWidth - params.leftMargin - params.rightMargin; 492 mToastBar.setLayoutParams(params); 493 mToastBar.setConversationMode(true); 494 } 495 if (convList != null) { 496 mToastBar.show(getUndoClickedListener(convList.getAnimatedAdapter()), 0, Html 497 .fromHtml(op.getDescription(mActivity.getActivityContext(), mFolder)), 498 true, /* showActionIcon */ 499 R.string.undo, true, /* replaceVisibleToast */ 500 op); 501 } 502 break; 503 } 504 } 505 506 @Override 507 public void onError(final Folder folder, boolean replaceVisibleToast) { 508 final int mode = mViewMode.getMode(); 509 final FrameLayout.LayoutParams params = 510 (FrameLayout.LayoutParams) mToastBar.getLayoutParams(); 511 switch (mode) { 512 case ViewMode.SEARCH_RESULTS_LIST: 513 case ViewMode.CONVERSATION_LIST: 514 params.width = mLayout.computeConversationListWidth() 515 - params.leftMargin - params.rightMargin; 516 params.gravity = Gravity.BOTTOM | Gravity.RIGHT; 517 mToastBar.setLayoutParams(params); 518 break; 519 case ViewMode.SEARCH_RESULTS_CONVERSATION: 520 case ViewMode.CONVERSATION: 521 // Show error bar in the conversation list. 522 params.gravity = Gravity.BOTTOM | Gravity.LEFT; 523 params.width = mLayout.computeConversationListWidth() 524 - params.leftMargin - params.rightMargin; 525 mToastBar.setLayoutParams(params); 526 break; 527 } 528 529 showErrorToast(folder, replaceVisibleToast); 530 } 531} 532