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