TwoPaneController.java revision 7b6d03db55338cbf9717896f99eb20d02bf371e4
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 48 /** 49 * @param activity 50 * @param viewMode 51 */ 52 public TwoPaneController(MailActivity activity, ViewMode viewMode) { 53 super(activity, viewMode); 54 } 55 56 /** 57 * Display the conversation list fragment. 58 * @param show 59 */ 60 private void initializeConversationListFragment(boolean show) { 61 if (show) { 62 if (Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())) { 63 if (Utils.showTwoPaneSearchResults(mActivity.getActivityContext())) { 64 mViewMode.enterSearchResultsConversationMode(); 65 } else { 66 mViewMode.enterSearchResultsListMode(); 67 } 68 } else { 69 mViewMode.enterConversationListMode(); 70 } 71 } 72 renderConversationList(); 73 } 74 75 /** 76 * Render the conversation list in the correct pane. 77 */ 78 private void renderConversationList() { 79 if (mActivity == null) { 80 return; 81 } 82 FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction(); 83 // Use cross fading animation. 84 fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); 85 Fragment conversationListFragment = ConversationListFragment.newInstance(mConvListContext); 86 fragmentTransaction.replace(R.id.conversation_list_pane, conversationListFragment, 87 TAG_CONVERSATION_LIST); 88 fragmentTransaction.commitAllowingStateLoss(); 89 } 90 91 /** 92 * Render the folder list in the correct pane. 93 */ 94 private void renderFolderList() { 95 if (mActivity == null) { 96 return; 97 } 98 createFolderListFragment(null, mAccount.folderListUri); 99 } 100 101 private void createFolderListFragment(Folder parent, Uri uri) { 102 setHierarchyFolder(parent); 103 FolderListFragment folderListFragment = FolderListFragment.newInstance(parent, uri); 104 FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction(); 105 fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); 106 fragmentTransaction.replace(R.id.content_pane, folderListFragment, TAG_FOLDER_LIST); 107 fragmentTransaction.commitAllowingStateLoss(); 108 // Since we are showing the folder list, we are at the start of the view 109 // stack. 110 resetActionBarIcon(); 111 } 112 113 @Override 114 protected boolean isConversationListVisible() { 115 return !mLayout.isConversationListCollapsed(); 116 } 117 118 @Override 119 public void showConversationList(ConversationListContext listContext) { 120 super.showConversationList(listContext); 121 initializeConversationListFragment(true); 122 } 123 124 @Override 125 public void showFolderList() { 126 // On two-pane layouts, showing the folder list takes you to the top level of the 127 // application, which is the same as pressing the Up button 128 onUpPressed(); 129 } 130 131 @Override 132 public boolean onCreate(Bundle savedState) { 133 mActivity.setContentView(R.layout.two_pane_activity); 134 mLayout = (TwoPaneLayout) mActivity.findViewById(R.id.two_pane_activity); 135 if (mLayout == null) { 136 // We need the layout for everything. Crash early if it is null. 137 LogUtils.wtf(LOG_TAG, "mLayout is null!"); 138 } 139 mLayout.initializeLayout(mActivity.getApplicationContext(), 140 Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())); 141 142 // The tablet layout needs to refer to mode changes. 143 mViewMode.addListener(mLayout); 144 // The activity controller needs to listen to layout changes. 145 mLayout.setListener(this); 146 final boolean isParentInitialized = super.onCreate(savedState); 147 return isParentInitialized; 148 } 149 150 @Override 151 public void onWindowFocusChanged(boolean hasFocus) { 152 if (hasFocus && !mLayout.isConversationListCollapsed()) { 153 // The conversation list is visible. 154 Utils.setConversationCursorVisibility(mConversationListCursor, true); 155 } 156 } 157 158 @Override 159 public void onAccountChanged(Account account) { 160 super.onAccountChanged(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) { 170 folderList.selectInitialFolder(folder); 171 } 172 } 173 174 @Override 175 public void onFolderSelected(Folder folder) { 176 if (folder.hasChildren && !folder.equals(getHierarchyFolder())) { 177 // Replace this fragment with a new FolderListFragment 178 // showing this folder's children if we are not already looking 179 // at the child view for this folder. 180 createFolderListFragment(folder, folder.childFoldersListUri); 181 // Show the up affordance when digging into child folders. 182 mActionBarView.setBackButton(); 183 super.onFolderSelected(folder); 184 } else { 185 setHierarchyFolder(folder); 186 super.onFolderSelected(folder); 187 } 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 // Otherwise, the conversation list is guaranteed to be visible. Try to enable the CAB 212 // mode if any conversations are selected. 213 if (newMode == ViewMode.CONVERSATION){ 214 enableOrDisableCab(); 215 } 216 resetActionBarIcon(); 217 } 218 219 @Override 220 public void onConversationVisibilityChanged(boolean visible) { 221 super.onConversationVisibilityChanged(visible); 222 if (!visible) { 223 mPagerController.hide(); 224 } 225 } 226 227 @Override 228 public void onConversationListVisibilityChanged(boolean visible) { 229 super.onConversationListVisibilityChanged(visible); 230 } 231 232 @Override 233 public void resetActionBarIcon() { 234 if (mViewMode.isListMode()) { 235 mActionBarView.removeBackButton(); 236 } else { 237 mActionBarView.setBackButton(); 238 } 239 } 240 241 /** 242 * Enable or disable the CAB mode based on the visibility of the conversation list fragment. 243 */ 244 private final void enableOrDisableCab() { 245 if (mLayout.isConversationListCollapsed()) { 246 disableCabMode(); 247 } else { 248 enableCabMode(); 249 } 250 } 251 252 @Override 253 public void showConversation(Conversation conversation) { 254 super.showConversation(conversation); 255 if (mActivity == null) { 256 return; 257 } 258 if (conversation == null) { 259 // This is a request to remove the conversation view and show the conversation list 260 // fragment instead. 261 mPagerController.stopListening(); 262 onBackPressed(); 263 return; 264 } 265 // If conversation list is not visible, then the user cannot see the CAB mode, so exit it. 266 // This is needed here (in addition to during viewmode changes) because orientation changes 267 // while viewing a conversation don't change the viewmode: the mode stays 268 // ViewMode.CONVERSATION and yet the conversation list goes in and out of visibility. 269 enableOrDisableCab(); 270 271 final int mode = mViewMode.getMode(); 272 if (mode == ViewMode.SEARCH_RESULTS_LIST || mode == ViewMode.SEARCH_RESULTS_CONVERSATION) { 273 mViewMode.enterSearchResultsConversationMode(); 274 } else { 275 mViewMode.enterConversationMode(); 276 } 277 mPagerController.show(mAccount, mFolder, conversation); 278 } 279 280 @Override 281 public void setCurrentConversation(Conversation conversation) { 282 super.setCurrentConversation(conversation); 283 284 final ConversationListFragment convList = getConversationListFragment(); 285 if (convList != null && conversation != null) { 286 LogUtils.d(LOG_TAG, "showConversation: Selecting position %d.", conversation.position); 287 convList.setSelected(conversation.position); 288 } 289 } 290 291 @Override 292 public void showWaitForInitialization() { 293 super.showWaitForInitialization(); 294 295 Fragment waitFragment = WaitFragment.newInstance(mAccount); 296 FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction(); 297 fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); 298 fragmentTransaction.replace(R.id.two_pane_activity, waitFragment, TAG_WAIT); 299 fragmentTransaction.commitAllowingStateLoss(); 300 } 301 302 @Override 303 public void hideWaitForInitialization() { 304 final FragmentManager manager = mActivity.getFragmentManager(); 305 final WaitFragment waitFragment = (WaitFragment)manager.findFragmentByTag(TAG_WAIT); 306 if (waitFragment != null) { 307 FragmentTransaction fragmentTransaction = 308 mActivity.getFragmentManager().beginTransaction(); 309 fragmentTransaction.remove(waitFragment); 310 fragmentTransaction.commitAllowingStateLoss(); 311 } 312 } 313 314 /** 315 * Up works as follows: 316 * 1) If the user is in a conversation and: 317 * a) the conversation list is hidden (portrait mode), shows the conv list and 318 * stays in conversation view mode. 319 * b) the conversation list is shown, goes back to conversation list mode. 320 * 2) If the user is in search results, up exits search. 321 * mode and returns the user to whatever view they were in when they began search. 322 * 3) If the user is in conversation list mode, there is no up. 323 */ 324 @Override 325 public boolean onUpPressed() { 326 int mode = mViewMode.getMode(); 327 if (mode == ViewMode.CONVERSATION) { 328 mActivity.onBackPressed(); 329 } else if (mode == ViewMode.SEARCH_RESULTS_CONVERSATION) { 330 if (mLayout.isConversationListCollapsed() 331 || (mConvListContext.isSearchResult() && !Utils. 332 showTwoPaneSearchResults(mActivity.getApplicationContext()))) { 333 onBackPressed(); 334 } else { 335 mActivity.finish(); 336 } 337 } else if (mode == ViewMode.SEARCH_RESULTS_LIST) { 338 mActivity.finish(); 339 } else if (mode == ViewMode.CONVERSATION_LIST) { 340 popView(true); 341 } 342 return true; 343 } 344 345 @Override 346 public boolean onBackPressed() { 347 // Clear any visible undo bars. 348 mToastBar.hide(false); 349 popView(false); 350 return true; 351 } 352 353 /** 354 * Pops the "view stack" to the last screen the user was viewing. 355 * 356 * @param preventClose Whether to prevent closing the app if the stack is empty. 357 */ 358 protected void popView(boolean preventClose) { 359 // If the user is in search query entry mode, or the user is viewing 360 // search results, exit 361 // the mode. 362 int mode = mViewMode.getMode(); 363 if (mode == ViewMode.SEARCH_RESULTS_LIST) { 364 mActivity.finish(); 365 } else if (mode == ViewMode.CONVERSATION) { 366 // Go to conversation list. 367 mViewMode.enterConversationListMode(); 368 } else if (mode == ViewMode.SEARCH_RESULTS_CONVERSATION) { 369 mViewMode.enterSearchResultsListMode(); 370 } else { 371 if (mode == ViewMode.CONVERSATION_LIST && getFolderListFragment().showingHierarchy()) { 372 // If the user navigated via the left folders list into a child folder, 373 // back should take the user up to the parent folder's conversation list. 374 Folder hierarchyFolder = getHierarchyFolder(); 375 if (hierarchyFolder.parent != null) { 376 goUpFolderHierarchy(hierarchyFolder); 377 } else { 378 // Show inbox; we are at the top of the hierarchy we were 379 // showing, and it doesn't have a parent, so we must want to 380 // the basic account folder list. 381 createFolderListFragment(null, mAccount.folderListUri); 382 loadAccountInbox(); 383 } 384 } else if (!preventClose) { 385 // There is nothing else to pop off the stack. 386 mActivity.finish(); 387 } 388 } 389 } 390 391 @Override 392 public void onRestoreInstanceState(Bundle inState) { 393 super.onRestoreInstanceState(inState); 394 if (inState.containsKey(SAVED_HIERARCHICAL_FOLDER)) { 395 String folderString = inState.getString(SAVED_HIERARCHICAL_FOLDER); 396 if (!TextUtils.isEmpty(folderString)) { 397 Folder folder = Folder.fromString(inState.getString(SAVED_HIERARCHICAL_FOLDER)); 398 mViewMode.enterConversationListMode(); 399 if (folder.hasChildren) { 400 onFolderSelected(folder); 401 } else if (folder.parent != null) { 402 onFolderSelected(folder.parent); 403 setHierarchyFolder(folder); 404 } 405 } 406 } 407 } 408 409 @Override 410 public void onSaveInstanceState(Bundle outState) { 411 super.onSaveInstanceState(outState); 412 Folder hierarchyFolder = getHierarchyFolder(); 413 outState.putString(SAVED_HIERARCHICAL_FOLDER, 414 hierarchyFolder != null ? Folder.toString(hierarchyFolder) : null); 415 } 416 417 @Override 418 public void exitSearchMode() { 419 int mode = mViewMode.getMode(); 420 if (mode == ViewMode.SEARCH_RESULTS_LIST 421 || (mode == ViewMode.SEARCH_RESULTS_CONVERSATION 422 && Utils.showTwoPaneSearchResults(mActivity.getApplicationContext()))) { 423 mActivity.finish(); 424 } 425 } 426 427 @Override 428 public boolean shouldShowFirstConversation() { 429 return Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction()) 430 && Utils.showTwoPaneSearchResults(mActivity.getApplicationContext()); 431 } 432 433 @Override 434 public void onUndoAvailable(ToastBarOperation op) { 435 final int mode = mViewMode.getMode(); 436 final FrameLayout.LayoutParams params = 437 (FrameLayout.LayoutParams) mToastBar.getLayoutParams(); 438 final ConversationListFragment convList = getConversationListFragment(); 439 switch (mode) { 440 case ViewMode.SEARCH_RESULTS_LIST: 441 case ViewMode.CONVERSATION_LIST: 442 params.width = mLayout.computeConversationListWidth() 443 - params.leftMargin - params.rightMargin; 444 params.gravity = Gravity.BOTTOM | Gravity.RIGHT; 445 mToastBar.setLayoutParams(params); 446 mToastBar.setConversationMode(false); 447 if (convList != null) { 448 mToastBar.show( 449 getUndoClickedListener(convList.getAnimatedAdapter()), 450 0, 451 Html.fromHtml(op.getDescription(mActivity.getActivityContext())), 452 true, /* showActionIcon */ 453 R.string.undo, 454 true, /* replaceVisibleToast */ 455 op); 456 } 457 break; 458 case ViewMode.SEARCH_RESULTS_CONVERSATION: 459 case ViewMode.CONVERSATION: 460 if (op.isBatchUndo()) { 461 // Show undo bar in the conversation list. 462 params.gravity = Gravity.BOTTOM | Gravity.LEFT; 463 params.width = mLayout.computeConversationListWidth() 464 - params.leftMargin - params.rightMargin; 465 mToastBar.setLayoutParams(params); 466 mToastBar.setConversationMode(false); 467 } else { 468 // Show undo bar in the conversation. 469 params.gravity = Gravity.BOTTOM | Gravity.RIGHT; 470 params.width = mLayout.getConversationView().getWidth() 471 - params.leftMargin - params.rightMargin; 472 mToastBar.setLayoutParams(params); 473 mToastBar.setConversationMode(true); 474 } 475 mToastBar.show( 476 getUndoClickedListener(convList.getAnimatedAdapter()), 477 0, 478 Html.fromHtml(op.getDescription(mActivity.getActivityContext())), 479 true, /* showActionIcon */ 480 R.string.undo, 481 true, /* replaceVisibleToast */ 482 op); 483 break; 484 } 485 } 486 487 @Override 488 public void onError(final Folder folder, boolean replaceVisibleToast) { 489 final int mode = mViewMode.getMode(); 490 final FrameLayout.LayoutParams params = 491 (FrameLayout.LayoutParams) mToastBar.getLayoutParams(); 492 switch (mode) { 493 case ViewMode.SEARCH_RESULTS_LIST: 494 case ViewMode.CONVERSATION_LIST: 495 params.width = mLayout.computeConversationListWidth() 496 - params.leftMargin - params.rightMargin; 497 params.gravity = Gravity.BOTTOM | Gravity.RIGHT; 498 mToastBar.setLayoutParams(params); 499 break; 500 case ViewMode.SEARCH_RESULTS_CONVERSATION: 501 case ViewMode.CONVERSATION: 502 // Show error bar in the conversation list. 503 params.gravity = Gravity.BOTTOM | Gravity.LEFT; 504 params.width = mLayout.computeConversationListWidth() 505 - params.leftMargin - params.rightMargin; 506 mToastBar.setLayoutParams(params); 507 break; 508 } 509 510 showErrorToast(folder, replaceVisibleToast); 511 } 512} 513