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