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 18 package com.android.mail.ui; 19 20 import android.app.Fragment; 21 import android.app.FragmentManager; 22 import android.app.FragmentTransaction; 23 import android.net.Uri; 24 import android.os.Bundle; 25 import android.support.v4.widget.DrawerLayout; 26 import android.widget.ListView; 27 28 import com.android.mail.ConversationListContext; 29 import com.android.mail.R; 30 import com.android.mail.providers.Account; 31 import com.android.mail.providers.Conversation; 32 import com.android.mail.providers.Folder; 33 import com.android.mail.providers.UIProvider; 34 import com.android.mail.utils.FolderUri; 35 import com.android.mail.utils.Utils; 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 public final class OnePaneController extends AbstractActivityController { 43 /** Key used to store {@link #mLastConversationListTransactionId} */ 44 private static final String CONVERSATION_LIST_TRANSACTION_KEY = "conversation-list-transaction"; 45 /** Key used to store {@link #mLastConversationTransactionId}. */ 46 private static final String CONVERSATION_TRANSACTION_KEY = "conversation-transaction"; 47 /** Key used to store {@link #mConversationListVisible}. */ 48 private static final String CONVERSATION_LIST_VISIBLE_KEY = "conversation-list-visible"; 49 /** Key used to store {@link #mConversationListNeverShown}. */ 50 private static final String CONVERSATION_LIST_NEVER_SHOWN_KEY = "conversation-list-never-shown"; 51 52 private static final int INVALID_ID = -1; 53 private boolean mConversationListVisible = false; 54 private int mLastConversationListTransactionId = INVALID_ID; 55 private int mLastConversationTransactionId = INVALID_ID; 56 /** Whether a conversation list for this account has ever been shown.*/ 57 private boolean mConversationListNeverShown = true; 58 59 public OnePaneController(MailActivity activity, ViewMode viewMode) { 60 super(activity, viewMode); 61 } 62 63 @Override 64 public void onRestoreInstanceState(Bundle inState) { 65 super.onRestoreInstanceState(inState); 66 if (inState == null) { 67 return; 68 } 69 mLastConversationListTransactionId = 70 inState.getInt(CONVERSATION_LIST_TRANSACTION_KEY, INVALID_ID); 71 mLastConversationTransactionId = inState.getInt(CONVERSATION_TRANSACTION_KEY, INVALID_ID); 72 mConversationListVisible = inState.getBoolean(CONVERSATION_LIST_VISIBLE_KEY); 73 mConversationListNeverShown = inState.getBoolean(CONVERSATION_LIST_NEVER_SHOWN_KEY); 74 } 75 76 @Override 77 public void onSaveInstanceState(Bundle outState) { 78 super.onSaveInstanceState(outState); 79 outState.putInt(CONVERSATION_LIST_TRANSACTION_KEY, mLastConversationListTransactionId); 80 outState.putInt(CONVERSATION_TRANSACTION_KEY, mLastConversationTransactionId); 81 outState.putBoolean(CONVERSATION_LIST_VISIBLE_KEY, mConversationListVisible); 82 outState.putBoolean(CONVERSATION_LIST_NEVER_SHOWN_KEY, mConversationListNeverShown); 83 } 84 85 @Override 86 public void resetActionBarIcon() { 87 // Calling resetActionBarIcon should never remove the up affordance 88 // even when waiting for sync (Folder list should still show with one 89 // account. Currently this method is blank to avoid any changes. 90 } 91 92 /** 93 * Returns true if the candidate URI is the URI for the default inbox for the given account. 94 * @param candidate the URI to check 95 * @param account the account whose default Inbox the candidate might be 96 * @return true if the candidate is indeed the default inbox for the given account. 97 */ 98 private static boolean isDefaultInbox(FolderUri candidate, Account account) { 99 return (candidate != null && account != null) 100 && candidate.equals(account.settings.defaultInbox); 101 } 102 103 /** 104 * Returns true if the user is currently in the conversation list view, viewing the default 105 * inbox. 106 * @return true if user is in conversation list mode, viewing the default inbox. 107 */ 108 private static boolean inInbox(final Account account, final ConversationListContext context) { 109 // If we don't have valid state, then we are not in the inbox. 110 return !(account == null || context == null || context.folder == null 111 || account.settings == null) && !ConversationListContext.isSearchResult(context) 112 && isDefaultInbox(context.folder.folderUri, account); 113 } 114 115 /** 116 * On account change, carry out super implementation, load FolderListFragment 117 * into drawer (to avoid repetitive calls to replaceFragment). 118 */ 119 @Override 120 public void changeAccount(Account account) { 121 super.changeAccount(account); 122 mConversationListNeverShown = true; 123 closeDrawerIfOpen(); 124 } 125 126 @Override 127 public boolean onCreate(Bundle savedInstanceState) { 128 mActivity.setContentView(R.layout.one_pane_activity); 129 mDrawerContainer = (DrawerLayout) mActivity.findViewById(R.id.drawer_container); 130 mDrawerPullout = mDrawerContainer.findViewById(R.id.drawer_pullout); 131 mDrawerPullout.setBackgroundResource(R.color.list_background_color); 132 133 // The parent class sets the correct viewmode and starts the application off. 134 return super.onCreate(savedInstanceState); 135 } 136 137 @Override 138 protected boolean isConversationListVisible() { 139 return mConversationListVisible; 140 } 141 142 @Override 143 public void onViewModeChanged(int newMode) { 144 super.onViewModeChanged(newMode); 145 146 // When entering conversation list mode, hide and clean up any currently visible 147 // conversation. 148 if (ViewMode.isListMode(newMode)) { 149 mPagerController.hide(true /* changeVisibility */); 150 } 151 // When we step away from the conversation mode, we don't have a current conversation 152 // anymore. Let's blank it out so clients calling getCurrentConversation are not misled. 153 if (!ViewMode.isConversationMode(newMode)) { 154 setCurrentConversation(null); 155 } 156 } 157 158 @Override 159 public String toString() { 160 final StringBuilder sb = new StringBuilder(super.toString()); 161 sb.append(" lastConvListTransId="); 162 sb.append(mLastConversationListTransactionId); 163 sb.append("}"); 164 return sb.toString(); 165 } 166 167 @Override 168 public void showConversationList(ConversationListContext listContext) { 169 super.showConversationList(listContext); 170 enableCabMode(); 171 mConversationListVisible = true; 172 if (ConversationListContext.isSearchResult(listContext)) { 173 mViewMode.enterSearchResultsListMode(); 174 } else { 175 mViewMode.enterConversationListMode(); 176 } 177 final int transition = mConversationListNeverShown 178 ? FragmentTransaction.TRANSIT_FRAGMENT_FADE 179 : FragmentTransaction.TRANSIT_FRAGMENT_OPEN; 180 final Fragment conversationListFragment = 181 ConversationListFragment.newInstance(listContext); 182 183 if (!inInbox(mAccount, listContext)) { 184 // Maintain fragment transaction history so we can get back to the 185 // fragment used to launch this list. 186 mLastConversationListTransactionId = replaceFragment(conversationListFragment, 187 transition, TAG_CONVERSATION_LIST, R.id.content_pane); 188 } else { 189 // If going to the inbox, clear the folder list transaction history. 190 mInbox = listContext.folder; 191 replaceFragment(conversationListFragment, transition, TAG_CONVERSATION_LIST, 192 R.id.content_pane); 193 194 // If we ever to to the inbox, we want to unset the transation id for any other 195 // non-inbox folder. 196 mLastConversationListTransactionId = INVALID_ID; 197 } 198 199 mActivity.getFragmentManager().executePendingTransactions(); 200 201 onConversationVisibilityChanged(false); 202 onConversationListVisibilityChanged(true); 203 mConversationListNeverShown = false; 204 } 205 206 @Override 207 protected void showConversation(Conversation conversation, boolean inLoaderCallbacks) { 208 super.showConversation(conversation, inLoaderCallbacks); 209 mConversationListVisible = false; 210 if (conversation == null) { 211 transitionBackToConversationListMode(); 212 return; 213 } 214 disableCabMode(); 215 if (ConversationListContext.isSearchResult(mConvListContext)) { 216 mViewMode.enterSearchResultsConversationMode(); 217 } else { 218 mViewMode.enterConversationMode(); 219 } 220 final FragmentManager fm = mActivity.getFragmentManager(); 221 final FragmentTransaction ft = fm.beginTransaction(); 222 // Switching to conversation view is an incongruous transition: 223 // we are not replacing a fragment with another fragment as 224 // usual. Instead, reveal the heretofore inert conversation 225 // ViewPager and just remove the previously visible fragment 226 // e.g. conversation list, or possibly label list?). 227 final Fragment f = fm.findFragmentById(R.id.content_pane); 228 // FragmentManager#findFragmentById can return fragments that are not added to the activity. 229 // We want to make sure that we don't attempt to remove fragments that are not added to the 230 // activity, as when the transaction is popped off, the FragmentManager will attempt to 231 // readd the same fragment twice 232 if (f != null && f.isAdded()) { 233 ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); 234 ft.remove(f); 235 ft.commitAllowingStateLoss(); 236 fm.executePendingTransactions(); 237 } 238 mPagerController.show(mAccount, mFolder, conversation, true /* changeVisibility */); 239 onConversationVisibilityChanged(true); 240 onConversationListVisibilityChanged(false); 241 } 242 243 @Override 244 public void showWaitForInitialization() { 245 super.showWaitForInitialization(); 246 replaceFragment(getWaitFragment(), FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_WAIT, 247 R.id.content_pane); 248 } 249 250 @Override 251 protected void hideWaitForInitialization() { 252 transitionToInbox(); 253 super.hideWaitForInitialization(); 254 } 255 256 @Override 257 public boolean doesActionChangeConversationListVisibility(final int action) { 258 if (action == R.id.archive 259 || action == R.id.remove_folder 260 || action == R.id.delete 261 || action == R.id.discard_drafts 262 || action == R.id.mark_important 263 || action == R.id.mark_not_important 264 || action == R.id.mute 265 || action == R.id.report_spam 266 || action == R.id.mark_not_spam 267 || action == R.id.report_phishing 268 || action == R.id.refresh 269 || action == R.id.change_folders) { 270 return false; 271 } else { 272 return true; 273 } 274 } 275 276 /** 277 * Replace the content_pane with the fragment specified here. The tag is specified so that 278 * the {@link ActivityController} can look up the fragments through the 279 * {@link android.app.FragmentManager}. 280 * @param fragment the new fragment to put 281 * @param transition the transition to show 282 * @param tag a tag for the fragment manager. 283 * @param anchor ID of view to replace fragment in 284 * @return transaction ID returned when the transition is committed. 285 */ 286 private int replaceFragment(Fragment fragment, int transition, String tag, int anchor) { 287 final FragmentManager fm = mActivity.getFragmentManager(); 288 FragmentTransaction fragmentTransaction = fm.beginTransaction(); 289 fragmentTransaction.setTransition(transition); 290 fragmentTransaction.replace(anchor, fragment, tag); 291 final int id = fragmentTransaction.commitAllowingStateLoss(); 292 fm.executePendingTransactions(); 293 return id; 294 } 295 296 /** 297 * Back works as follows: 298 * 1) If the drawer is pulled out (Or mid-drag), close it - handled. 299 * 2) If the user is in the folder list view, go back 300 * to the account default inbox. 301 * 3) If the user is in a conversation list 302 * that is not the inbox AND: 303 * a) they got there by going through the folder 304 * list view, go back to the folder list view. 305 * b) they got there by using some other means (account dropdown), go back to the inbox. 306 * 4) If the user is in a conversation, go back to the conversation list they were last in. 307 * 5) If the user is in the conversation list for the default account inbox, 308 * back exits the app. 309 */ 310 @Override 311 public boolean handleBackPress() { 312 final int mode = mViewMode.getMode(); 313 314 if (mode == ViewMode.SEARCH_RESULTS_LIST) { 315 mActivity.finish(); 316 } else if (mViewMode.isListMode() && !inInbox(mAccount, mConvListContext)) { 317 navigateUpFolderHierarchy(); 318 } else if (mViewMode.isConversationMode() || mViewMode.isAdMode()) { 319 transitionBackToConversationListMode(); 320 } else { 321 mActivity.finish(); 322 } 323 mToastBar.hide(false, false /* actionClicked */); 324 return true; 325 } 326 327 /** 328 * Switch to the Inbox by creating a new conversation list context that loads the inbox. 329 */ 330 private void transitionToInbox() { 331 // The inbox could have changed, in which case we should load it again. 332 if (mInbox == null || !isDefaultInbox(mInbox.folderUri, mAccount)) { 333 loadAccountInbox(); 334 } else { 335 onFolderChanged(mInbox, false /* force */); 336 } 337 } 338 339 @Override 340 public void onFolderSelected(Folder folder) { 341 setHierarchyFolder(folder); 342 super.onFolderSelected(folder); 343 } 344 345 /** 346 * Up works as follows: 347 * 1) If the user is in a conversation list that is not the default account inbox, 348 * a conversation, or the folder list, up follows the rules of back. 349 * 2) If the user is in search results, up exits search 350 * mode and returns the user to whatever view they were in when they began search. 351 * 3) If the user is in the inbox, there is no up. 352 */ 353 @Override 354 public boolean handleUpPress() { 355 final int mode = mViewMode.getMode(); 356 if (mode == ViewMode.SEARCH_RESULTS_LIST) { 357 mActivity.finish(); 358 // Not needed, the activity is going away anyway. 359 } else if (mode == ViewMode.CONVERSATION_LIST 360 || mode == ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION) { 361 final boolean isTopLevel = (mFolder == null) || (mFolder.parent == Uri.EMPTY); 362 363 if (isTopLevel) { 364 // Show the drawer. 365 toggleDrawerState(); 366 } else { 367 navigateUpFolderHierarchy(); 368 } 369 } else if (mode == ViewMode.CONVERSATION || mode == ViewMode.SEARCH_RESULTS_CONVERSATION 370 || mode == ViewMode.AD) { 371 // Same as go back. 372 handleBackPress(); 373 } 374 return true; 375 } 376 377 private void transitionBackToConversationListMode() { 378 final int mode = mViewMode.getMode(); 379 enableCabMode(); 380 mConversationListVisible = true; 381 if (mode == ViewMode.SEARCH_RESULTS_CONVERSATION) { 382 mViewMode.enterSearchResultsListMode(); 383 } else { 384 mViewMode.enterConversationListMode(); 385 } 386 387 final Folder folder = mFolder != null ? mFolder : mInbox; 388 onFolderChanged(folder, true /* force */); 389 390 onConversationVisibilityChanged(false); 391 onConversationListVisibilityChanged(true); 392 } 393 394 @Override 395 public boolean shouldShowFirstConversation() { 396 return false; 397 } 398 399 @Override 400 public void onUndoAvailable(ToastBarOperation op) { 401 if (op != null && mAccount.supportsCapability(UIProvider.AccountCapabilities.UNDO)) { 402 final int mode = mViewMode.getMode(); 403 final ConversationListFragment convList = getConversationListFragment(); 404 switch (mode) { 405 case ViewMode.SEARCH_RESULTS_CONVERSATION: 406 case ViewMode.CONVERSATION: 407 mToastBar.show(getUndoClickedListener( 408 convList != null ? convList.getAnimatedAdapter() : null), 409 0, 410 Utils.convertHtmlToPlainText 411 (op.getDescription(mActivity.getActivityContext())), 412 true, /* showActionIcon */ 413 R.string.undo, 414 true, /* replaceVisibleToast */ 415 op); 416 break; 417 case ViewMode.SEARCH_RESULTS_LIST: 418 case ViewMode.CONVERSATION_LIST: 419 if (convList != null) { 420 mToastBar.show( 421 getUndoClickedListener(convList.getAnimatedAdapter()), 422 0, 423 Utils.convertHtmlToPlainText 424 (op.getDescription(mActivity.getActivityContext())), 425 true, /* showActionIcon */ 426 R.string.undo, 427 true, /* replaceVisibleToast */ 428 op); 429 } else { 430 mActivity.setPendingToastOperation(op); 431 } 432 break; 433 } 434 } 435 } 436 437 @Override 438 protected void hideOrRepositionToastBar(boolean animated) { 439 mToastBar.hide(animated, false /* actionClicked */); 440 } 441 442 @Override 443 public void onError(final Folder folder, boolean replaceVisibleToast) { 444 final int mode = mViewMode.getMode(); 445 switch (mode) { 446 case ViewMode.SEARCH_RESULTS_LIST: 447 case ViewMode.CONVERSATION_LIST: 448 showErrorToast(folder, replaceVisibleToast); 449 break; 450 default: 451 break; 452 } 453 } 454 455 @Override 456 public boolean isDrawerEnabled() { 457 // The drawer is enabled for one pane mode 458 return true; 459 } 460 461 @Override 462 public int getFolderListViewChoiceMode() { 463 // By default, we do not want to allow any item to be selected in the folder list 464 return ListView.CHOICE_MODE_NONE; 465 } 466 467 @Override 468 public void launchFragment(final Fragment fragment, final int selectPosition) { 469 replaceFragment(fragment, FragmentTransaction.TRANSIT_FRAGMENT_OPEN, 470 TAG_CUSTOM_FRAGMENT, R.id.content_pane); 471 } 472 } 473