OnePaneController.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.app.LoaderManager.LoaderCallbacks; 24import android.net.Uri; 25import android.os.Bundle; 26import android.text.Html; 27import com.android.mail.ConversationListContext; 28import com.android.mail.R; 29import com.android.mail.browse.SecureConversationViewFragment; 30import com.android.mail.providers.Account; 31import com.android.mail.providers.Conversation; 32import com.android.mail.providers.Folder; 33import com.android.mail.providers.Settings; 34import com.android.mail.providers.UIProvider; 35import com.android.mail.utils.LogUtils; 36 37/** 38 * Controller for one-pane Mail activity. One Pane is used for phones, where screen real estate is 39 * limited. This controller also does the layout, since the layout is simpler in the one pane case. 40 */ 41 42// Called OnePaneActivityController in Gmail. 43public final class OnePaneController extends AbstractActivityController { 44 /** Key used to store {@link #mLastFolderListTransactionId}. */ 45 private static final String FOLDER_LIST_TRANSACTION_KEY = "folder-list-transaction"; 46 /** Key used to store {@link #mLastInboxConversationListTransactionId} */ 47 private static final String INBOX_CONVERSATION_LIST_TRANSACTION_KEY = 48 "inbox_conversation-list-transaction"; 49 /** Key used to store {@link #mLastConversationListTransactionId} */ 50 private static final String CONVERSATION_LIST_TRANSACTION_KEY = "conversation-list-transaction"; 51 /** Key used to store {@link #mLastConversationTransactionId}. */ 52 private static final String CONVERSATION_TRANSACTION_KEY = "conversation-transaction"; 53 /** Key used to store {@link #mConversationListVisible}. */ 54 private static final String CONVERSATION_LIST_VISIBLE_KEY = "conversation-list-visible"; 55 /** Key used to store {@link #mConversationListNeverShown}. */ 56 private static final String CONVERSATION_LIST_NEVER_SHOWN_KEY = "conversation-list-never-shown"; 57 /** Key to store {@link #mInbox}. */ 58 private final static String SAVED_INBOX_KEY = "m-inbox"; 59 60 private static final int INVALID_ID = -1; 61 private boolean mConversationListVisible = false; 62 private int mLastInboxConversationListTransactionId = INVALID_ID; 63 private int mLastConversationListTransactionId = INVALID_ID; 64 private int mLastConversationTransactionId = INVALID_ID; 65 private int mLastFolderListTransactionId = INVALID_ID; 66 private Folder mInbox; 67 /** Whether a conversation list for this account has ever been shown.*/ 68 private boolean mConversationListNeverShown = true; 69 70 /** 71 * @param activity 72 * @param viewMode 73 */ 74 public OnePaneController(MailActivity activity, ViewMode viewMode) { 75 super(activity, viewMode); 76 } 77 78 @Override 79 public void onRestoreInstanceState(Bundle inState) { 80 super.onRestoreInstanceState(inState); 81 // TODO(mindyp) handle saved state. 82 if (inState == null) { 83 return; 84 } 85 mLastFolderListTransactionId = inState.getInt(FOLDER_LIST_TRANSACTION_KEY, INVALID_ID); 86 mLastInboxConversationListTransactionId = 87 inState.getInt(INBOX_CONVERSATION_LIST_TRANSACTION_KEY, INVALID_ID); 88 mLastConversationListTransactionId = 89 inState.getInt(CONVERSATION_LIST_TRANSACTION_KEY, INVALID_ID); 90 mLastConversationTransactionId = inState.getInt(CONVERSATION_TRANSACTION_KEY, INVALID_ID); 91 mConversationListVisible = inState.getBoolean(CONVERSATION_LIST_VISIBLE_KEY); 92 mConversationListNeverShown = inState.getBoolean(CONVERSATION_LIST_NEVER_SHOWN_KEY); 93 mInbox = inState.getParcelable(SAVED_INBOX_KEY); 94 } 95 96 @Override 97 public void onSaveInstanceState(Bundle outState) { 98 super.onSaveInstanceState(outState); 99 // TODO(mindyp) handle saved state. 100 outState.putInt(FOLDER_LIST_TRANSACTION_KEY, mLastFolderListTransactionId); 101 outState.putInt(INBOX_CONVERSATION_LIST_TRANSACTION_KEY, 102 mLastInboxConversationListTransactionId); 103 outState.putInt(CONVERSATION_LIST_TRANSACTION_KEY, mLastConversationListTransactionId); 104 outState.putInt(CONVERSATION_TRANSACTION_KEY, mLastConversationTransactionId); 105 outState.putBoolean(CONVERSATION_LIST_VISIBLE_KEY, mConversationListVisible); 106 outState.putBoolean(CONVERSATION_LIST_NEVER_SHOWN_KEY, mConversationListNeverShown); 107 outState.putParcelable(SAVED_INBOX_KEY, mInbox); 108 } 109 110 @Override 111 public void resetActionBarIcon() { 112 final int mode = mViewMode.getMode(); 113 // If the viewmode is not set, preserve existing icon. 114 if (mode == ViewMode.UNKNOWN) { 115 return; 116 } 117 if (!inInbox(mAccount, mConvListContext) 118 || mode == ViewMode.SEARCH_RESULTS_LIST 119 || mode == ViewMode.SEARCH_RESULTS_CONVERSATION 120 || mode == ViewMode.CONVERSATION 121 || mode == ViewMode.FOLDER_LIST) { 122 mActionBarView.setBackButton(); 123 } else { 124 mActionBarView.removeBackButton(); 125 } 126 } 127 128 /** 129 * Returns true if the user is currently in the conversation list view, viewing the default 130 * inbox. 131 * @return 132 */ 133 private static boolean inInbox(final Account account, final ConversationListContext context) { 134 // If we don't have valid state, then we are not in the inbox. 135 if (account == null || context == null || context.folder == null 136 || account.settings == null) { 137 return false; 138 } 139 final Uri inboxUri = Settings.getDefaultInboxUri(account.settings); 140 return !ConversationListContext.isSearchResult(context) 141 && context.folder.uri.equals(inboxUri); 142 } 143 144 @Override 145 public void onAccountChanged(Account account) { 146 super.onAccountChanged(account); 147 mConversationListNeverShown = true; 148 } 149 150 @Override 151 public boolean onCreate(Bundle savedInstanceState) { 152 mActivity.setContentView(R.layout.one_pane_activity); 153 // The parent class sets the correct viewmode and starts the application off. 154 return super.onCreate(savedInstanceState); 155 } 156 157 @Override 158 protected boolean isConversationListVisible() { 159 return mConversationListVisible; 160 } 161 162 @Override 163 public void onViewModeChanged(int newMode) { 164 super.onViewModeChanged(newMode); 165 166 // When entering conversation list mode, hide and clean up any currently visible 167 // conversation. 168 // TODO: improve this transition 169 if (ViewMode.isListMode(newMode)) { 170 mPagerController.hide(true /* changeVisibility */); 171 } 172 // When we step away from the conversation mode, we don't have a current conversation 173 // anymore. Let's blank it out so clients calling getCurrentConversation are not misled. 174 if (!ViewMode.isConversationMode(newMode)) { 175 setCurrentConversation(null); 176 } 177 resetActionBarIcon(); 178 } 179 180 @Override 181 public void showConversationList(ConversationListContext listContext) { 182 super.showConversationList(listContext); 183 enableCabMode(); 184 // TODO(viki): Check if the account has been changed since the previous 185 // time. 186 if (ConversationListContext.isSearchResult(listContext)) { 187 mViewMode.enterSearchResultsListMode(); 188 } else { 189 mViewMode.enterConversationListMode(); 190 } 191 // TODO(viki): This account transition looks strange in two pane mode. 192 // Revisit as the app is coming together and improve the look and feel. 193 final int transition = mConversationListNeverShown 194 ? FragmentTransaction.TRANSIT_FRAGMENT_FADE 195 : FragmentTransaction.TRANSIT_FRAGMENT_OPEN; 196 Fragment conversationListFragment = ConversationListFragment.newInstance(listContext); 197 198 if (!inInbox(mAccount, mConvListContext)) { 199 // Maintain fragment transaction history so we can get back to the 200 // fragment used to launch this list. 201 mLastConversationListTransactionId = replaceFragment(conversationListFragment, 202 transition, TAG_CONVERSATION_LIST); 203 } else { 204 // If going to the inbox, clear the folder list transaction history. 205 mInbox = listContext.folder; 206 mLastInboxConversationListTransactionId = replaceFragment(conversationListFragment, 207 transition, TAG_CONVERSATION_LIST); 208 mLastFolderListTransactionId = INVALID_ID; 209 210 // If we ever to to the inbox, we want to unset the transation id for any other 211 // non-inbox folder. 212 mLastConversationListTransactionId = INVALID_ID; 213 } 214 mConversationListVisible = true; 215 onConversationVisibilityChanged(false); 216 onConversationListVisibilityChanged(true); 217 mConversationListNeverShown = false; 218 } 219 220 @Override 221 protected void showConversation(Conversation conversation, boolean inLoaderCallbacks) { 222 super.showConversation(conversation, inLoaderCallbacks); 223 if (conversation == null) { 224 transitionBackToConversationListMode(inLoaderCallbacks); 225 return; 226 } 227 disableCabMode(); 228 if (ConversationListContext.isSearchResult(mConvListContext)) { 229 mViewMode.enterSearchResultsConversationMode(); 230 } else { 231 mViewMode.enterConversationMode(); 232 } 233 final FragmentManager fm = mActivity.getFragmentManager(); 234 final FragmentTransaction ft = fm.beginTransaction(); 235 ft.addToBackStack(null); 236 // Switching to conversation view is an incongruous transition: 237 // we are not replacing a fragment with another fragment as 238 // usual. Instead, reveal the heretofore inert conversation 239 // ViewPager and just remove the previously visible fragment 240 // e.g. conversation list, or possibly label list?). 241 final Fragment f = fm.findFragmentById(R.id.content_pane); 242 // FragmentManager#findFragmentById can return fragments that are not added to the activity. 243 // We want to make sure that we don't attempt to remove fragments that are not added to the 244 // activity, as when the transaction is popped off, the FragmentManager will attempt to 245 // readd the same fragment twice 246 if (f != null && f.isAdded()) { 247 // TODO: improve this transition 248 ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); 249 ft.remove(f); 250 ft.commitAllowingStateLoss(); 251 } 252 mPagerController.show(mAccount, mFolder, conversation, true /* changeVisibility */); 253 onConversationVisibilityChanged(true); 254 mConversationListVisible = false; 255 onConversationListVisibilityChanged(false); 256 } 257 258 @Override 259 public void showWaitForInitialization() { 260 super.showWaitForInitialization(); 261 262 replaceFragment(WaitFragment.newInstance(mAccount), 263 FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_WAIT); 264 } 265 266 protected void hideWaitForInitialization() { 267 transitionToInbox(); 268 } 269 270 @Override 271 public void showFolderList() { 272 if (mAccount == null) { 273 LogUtils.e(LOG_TAG, "Null account in showFolderList"); 274 return; 275 } 276 // Null out the currently selected folder; we have nothing selected the 277 // first time the user enters the folder list 278 setHierarchyFolder(null); 279 mViewMode.enterFolderListMode(); 280 enableCabMode(); 281 mLastFolderListTransactionId = replaceFragment( 282 FolderListFragment.newInstance(null, mAccount.folderListUri, false), 283 FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_FOLDER_LIST); 284 mConversationListVisible = false; 285 onConversationVisibilityChanged(false); 286 onConversationListVisibilityChanged(false); 287 } 288 289 /** 290 * Replace the content_pane with the fragment specified here. The tag is specified so that 291 * the {@link ActivityController} can look up the fragments through the 292 * {@link android.app.FragmentManager}. 293 * @param fragment 294 * @param transition 295 * @param tag 296 * @return transaction ID returned when the transition is committed. 297 */ 298 private int replaceFragment(Fragment fragment, int transition, String tag) { 299 FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction(); 300 fragmentTransaction.addToBackStack(null); 301 fragmentTransaction.setTransition(transition); 302 fragmentTransaction.replace(R.id.content_pane, fragment, tag); 303 return fragmentTransaction.commitAllowingStateLoss(); 304 } 305 306 /** 307 * Back works as follows: 308 * 1) If the user is in the folder list view, go back 309 * to the account default inbox. 310 * 2) If the user is in a conversation list 311 * that is not the inbox AND: 312 * a) they got there by going through the folder 313 * list view, go back to the folder list view. 314 * b) they got there by using some other means (account dropdown), go back to the inbox. 315 * 3) If the user is in a conversation, go back to the conversation list they were last in. 316 * 4) If the user is in the conversation list for the default account inbox, 317 * back exits the app. 318 */ 319 @Override 320 public boolean onBackPressed() { 321 final int mode = mViewMode.getMode(); 322 if (mode == ViewMode.FOLDER_LIST) { 323 final Folder hierarchyFolder = getHierarchyFolder(); 324 final FolderListFragment folderListFragment = getFolderListFragment(); 325 if (folderListFragment != null && 326 folderListFragment.showingHierarchy() && hierarchyFolder != null) { 327 // If we are showing the folder list and the user is exploring 328 // the children of a single parent folder, 329 // back should display the parent folder's parent and siblings. 330 goUpFolderHierarchy(hierarchyFolder); 331 } else { 332 // We are at the topmost list of folders: go back 333 mLastFolderListTransactionId = INVALID_ID; 334 transitionToInbox(); 335 } 336 } else if (mode == ViewMode.SEARCH_RESULTS_LIST) { 337 mActivity.finish(); 338 } else if (mViewMode.isListMode() && !inInbox(mAccount, mConvListContext)) { 339 if (mLastFolderListTransactionId != INVALID_ID) { 340 // If the user got here by navigating via the folder list, back 341 // should bring them back to the folder list. 342 mViewMode.enterFolderListMode(); 343 mActivity.getFragmentManager().popBackStack(mLastFolderListTransactionId, 0); 344 } else { 345 transitionToInbox(); 346 } 347 } else if (mode == ViewMode.CONVERSATION || mode == ViewMode.SEARCH_RESULTS_CONVERSATION) { 348 transitionBackToConversationListMode(false /* inLoaderCallbacks */); 349 } else { 350 mActivity.finish(); 351 } 352 mToastBar.hide(false); 353 return true; 354 } 355 356 private void goUpFolderHierarchy(Folder current) { 357 Folder top = current.parent; 358 if (top != null) { 359 setHierarchyFolder(top); 360 // Replace this fragment with a new FolderListFragment 361 // showing this folder's children if we are not already 362 // looking at the child view for this folder. 363 mLastFolderListTransactionId = replaceFragment(FolderListFragment.newInstance( 364 top, top.childFoldersListUri, false), 365 FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_FOLDER_LIST); 366 // Show the up affordance when digging into child folders. 367 mActionBarView.setBackButton(); 368 } else { 369 // Otherwise, clear the selected folder and go back to whatever the 370 // last folder list displayed was. 371 showFolderList(); 372 } 373 } 374 375 /** 376 * Switch to the Inbox by creating a new conversation list context that loads the inbox. 377 */ 378 private void transitionToInbox() { 379 mViewMode.enterConversationListMode(); 380 if (mInbox == null) { 381 loadAccountInbox(); 382 } else { 383 final ConversationListContext listContext = 384 ConversationListContext.forFolder(mAccount, mInbox); 385 // Set the correct context for what the conversation view will be now. 386 onFolderChanged(mInbox); 387 showConversationList(listContext); 388 } 389 } 390 391 @Override 392 public void onFolderSelected(Folder folder) { 393 if (folder.hasChildren && !folder.equals(getHierarchyFolder())) { 394 mViewMode.enterFolderListMode(); 395 setHierarchyFolder(folder); 396 // Replace this fragment with a new FolderListFragment 397 // showing this folder's children if we are not already 398 // looking at the child view for this folder. 399 mLastFolderListTransactionId = replaceFragment( 400 FolderListFragment.newInstance(folder, folder.childFoldersListUri, false), 401 FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_FOLDER_LIST); 402 // Show the up affordance when digging into child folders. 403 mActionBarView.setBackButton(); 404 } else { 405 super.onFolderSelected(folder); 406 } 407 } 408 409 private boolean isTransactionIdValid(int id) { 410 return id >= 0; 411 } 412 413 /** 414 * Up works as follows: 415 * 1) If the user is in a conversation list that is not the default account inbox, 416 * a conversation, or the folder list, up follows the rules of back. 417 * 2) If the user is in search results, up exits search 418 * mode and returns the user to whatever view they were in when they began search. 419 * 3) If the user is in the inbox, there is no up. 420 */ 421 @Override 422 public boolean onUpPressed() { 423 final int mode = mViewMode.getMode(); 424 if (mode == ViewMode.SEARCH_RESULTS_LIST) { 425 mActivity.finish(); 426 } else if ((!inInbox(mAccount, mConvListContext) && mViewMode.isListMode()) 427 || mode == ViewMode.CONVERSATION 428 || mode == ViewMode.FOLDER_LIST 429 || mode == ViewMode.SEARCH_RESULTS_CONVERSATION) { 430 // Same as go back. 431 mActivity.onBackPressed(); 432 } 433 return true; 434 } 435 436 private void transitionBackToConversationListMode(boolean inLoaderCallbacks) { 437 final int mode = mViewMode.getMode(); 438 enableCabMode(); 439 if (mode == ViewMode.SEARCH_RESULTS_CONVERSATION) { 440 mViewMode.enterSearchResultsListMode(); 441 } else { 442 mViewMode.enterConversationListMode(); 443 } 444 if (isTransactionIdValid(mLastConversationListTransactionId)) { 445 safelyPopBackStack(mLastConversationListTransactionId, inLoaderCallbacks); 446 } else if (isTransactionIdValid(mLastInboxConversationListTransactionId)) { 447 safelyPopBackStack(mLastInboxConversationListTransactionId, inLoaderCallbacks); 448 onFolderChanged(mInbox); 449 } else { 450 // TODO: revisit if this block is necessary 451 final ConversationListContext listContext = ConversationListContext.forFolder( 452 mAccount, mInbox); 453 // Set the correct context for what the conversation view will be now. 454 onFolderChanged(mInbox); 455 showConversationList(listContext); 456 } 457 mConversationListVisible = true; 458 onConversationVisibilityChanged(false); 459 onConversationListVisibilityChanged(true); 460 } 461 462 /** 463 * Pop to a specified point in the fragment back stack without causing IllegalStateExceptions 464 * from committing a fragment transaction "at the wrong time". 465 * <p> 466 * If the caller specifies that we are in 467 * the scope of an {@link LoaderCallbacks#onLoadFinished(android.content.Loader, Object)}, 468 * this method will pop back in a Handler. The deferred job will also check that the Activity 469 * is in a valid state for fragment transactions, using {@link #safeToModifyFragments()}. 470 * Otherwise, this method will pop back immediately if safe. Finally, if we are not in 471 * onLoadFinished and it's not safe, this method will just ignore the request. 472 * 473 * @param transactionId back stack destination to pop to 474 * @param inLoaderCallbacks whether we are in the scope of an onLoadFinished (when fragment 475 * transactions are disallowed) 476 */ 477 private void safelyPopBackStack(int transactionId, boolean inLoaderCallbacks) { 478 final PopBackStackRunnable r = new PopBackStackRunnable(transactionId); 479 if (inLoaderCallbacks) { 480 // always run deferred. ensure deferred job checks safety. 481 mHandler.post(r); 482 } else if (safeToModifyFragments()) { 483 // run now 484 r.popBackStack(); 485 } else { 486 // ignore 487 LogUtils.i(LOG_TAG, "Activity has been saved; ignoring unsafe immediate request" 488 + " to pop back stack"); 489 } 490 } 491 492 @Override 493 public boolean shouldShowFirstConversation() { 494 return false; 495 } 496 497 @Override 498 public void onUndoAvailable(ToastBarOperation op) { 499 if (op != null && mAccount.supportsCapability(UIProvider.AccountCapabilities.UNDO)) { 500 final int mode = mViewMode.getMode(); 501 final ConversationListFragment convList = getConversationListFragment(); 502 switch (mode) { 503 case ViewMode.SEARCH_RESULTS_CONVERSATION: 504 case ViewMode.CONVERSATION: 505 mToastBar.setConversationMode(true); 506 mToastBar.show( 507 getUndoClickedListener( 508 convList != null ? convList.getAnimatedAdapter() : null), 509 0, 510 Html.fromHtml(op.getDescription(mActivity.getActivityContext(), 511 mFolder)), 512 true, /* showActionIcon */ 513 R.string.undo, 514 true, /* replaceVisibleToast */ 515 op); 516 break; 517 case ViewMode.SEARCH_RESULTS_LIST: 518 case ViewMode.CONVERSATION_LIST: 519 if (convList != null) { 520 mToastBar.setConversationMode(false); 521 mToastBar.show( 522 getUndoClickedListener(convList.getAnimatedAdapter()), 523 0, 524 Html.fromHtml(op.getDescription(mActivity.getActivityContext(), 525 mFolder)), 526 true, /* showActionIcon */ 527 R.string.undo, 528 true, /* replaceVisibleToast */ 529 op); 530 } else { 531 mActivity.setPendingToastOperation(op); 532 } 533 break; 534 } 535 } 536 } 537 538 @Override 539 public void onError(final Folder folder, boolean replaceVisibleToast) { 540 final int mode = mViewMode.getMode(); 541 switch (mode) { 542 case ViewMode.SEARCH_RESULTS_LIST: 543 case ViewMode.CONVERSATION_LIST: 544 showErrorToast(folder, replaceVisibleToast); 545 break; 546 default: 547 break; 548 } 549 } 550 551 @Override 552 public String getHelpContext() { 553 final int mode = mViewMode.getMode(); 554 switch (mode) { 555 case ViewMode.FOLDER_LIST: 556 return mContext.getString(R.string.one_pane_folder_list_help_context); 557 } 558 return super.getHelpContext(); 559 } 560 561 private final class PopBackStackRunnable implements Runnable { 562 563 private final int mTransactionId; 564 565 public PopBackStackRunnable(int transactionId) { 566 mTransactionId = transactionId; 567 } 568 569 public void popBackStack() { 570 mActivity.getFragmentManager().popBackStack(mTransactionId, 0); 571 } 572 573 @Override 574 public void run() { 575 if (safeToModifyFragments()) { 576 popBackStack(); 577 } else { 578 LogUtils.i(LOG_TAG, "Activity has been saved; ignoring unsafe deferred request" 579 + " to pop back stack"); 580 } 581 } 582 } 583 584} 585