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