AbstractActivityController.java revision 08935f5787ac90d3cb2be84454665a99e77d024f
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.ActionBar; 21import android.app.ActionBar.LayoutParams; 22import android.app.Activity; 23import android.app.AlertDialog; 24import android.app.Dialog; 25import android.app.LoaderManager; 26import android.content.ContentResolver; 27import android.content.Context; 28import android.content.CursorLoader; 29import android.content.DialogInterface; 30import android.content.Intent; 31import android.content.Loader; 32import android.database.Cursor; 33import android.net.Uri; 34import android.os.Bundle; 35import android.os.Handler; 36import android.text.TextUtils; 37import android.view.KeyEvent; 38import android.view.LayoutInflater; 39import android.view.Menu; 40import android.view.MenuInflater; 41import android.view.MenuItem; 42import android.view.MotionEvent; 43 44import com.android.mail.ConversationListContext; 45import com.android.mail.R; 46import com.android.mail.browse.ConversationCursor; 47import com.android.mail.browse.SelectedConversationsActionMenu; 48import com.android.mail.browse.ConversationCursor.ConversationListener; 49import com.android.mail.compose.ComposeActivity; 50import com.android.mail.providers.Account; 51import com.android.mail.providers.Conversation; 52import com.android.mail.providers.Folder; 53import com.android.mail.providers.MailAppProvider; 54import com.android.mail.providers.Settings; 55import com.android.mail.providers.UIProvider; 56import com.android.mail.providers.UIProvider.AutoAdvance; 57import com.android.mail.providers.UIProvider.ConversationColumns; 58import com.android.mail.providers.UIProvider.FolderCapabilities; 59import com.android.mail.utils.LogUtils; 60import com.android.mail.utils.Utils; 61import com.google.common.collect.ImmutableList; 62import com.google.common.collect.Sets; 63 64import java.util.ArrayList; 65import java.util.Collection; 66import java.util.HashSet; 67import java.util.Set; 68 69 70/** 71 * This is an abstract implementation of the Activity Controller. This class 72 * knows how to respond to menu items, state changes, layout changes, etc. It 73 * weaves together the views and listeners, dispatching actions to the 74 * respective underlying classes. 75 * <p> 76 * Even though this class is abstract, it should provide default implementations 77 * for most, if not all the methods in the ActivityController interface. This 78 * makes the task of the subclasses easier: OnePaneActivityController and 79 * TwoPaneActivityController can be concise when the common functionality is in 80 * AbstractActivityController. 81 * </p> 82 * <p> 83 * In the Gmail codebase, this was called BaseActivityController 84 * </p> 85 */ 86public abstract class AbstractActivityController implements ActivityController, ConversationListener { 87 // Keys for serialization of various information in Bundles. 88 private static final String SAVED_ACCOUNT = "saved-account"; 89 private static final String SAVED_FOLDER = "saved-folder"; 90 private static final String SAVED_CONVERSATION = "saved-conversation"; 91 // Batch conversations stored in the Bundle using this key. 92 private static final String SAVED_CONVERSATIONS = "saved-conversations"; 93 94 /** Are we on a tablet device or not. */ 95 public final boolean IS_TABLET_DEVICE; 96 97 protected Account mAccount; 98 protected Folder mFolder; 99 protected ActionBarView mActionBarView; 100 protected final RestrictedActivity mActivity; 101 protected final Context mContext; 102 protected final RecentFolderList mRecentFolderList; 103 protected ConversationListContext mConvListContext; 104 protected Conversation mCurrentConversation; 105 106 /** A {@link android.content.BroadcastReceiver} that suppresses new e-mail notifications. */ 107 private SuppressNotificationReceiver mNewEmailReceiver = null; 108 109 protected Handler mHandler = new Handler(); 110 protected ConversationListFragment mConversationListFragment; 111 private SelectedConversationsActionMenu mSelectedConversationsActionMenu; 112 /** 113 * The current mode of the application. All changes in mode are initiated by 114 * the activity controller. View mode changes are propagated to classes that 115 * attach themselves as listeners of view mode changes. 116 */ 117 protected final ViewMode mViewMode; 118 protected ContentResolver mResolver; 119 protected FolderListFragment mFolderListFragment; 120 protected ConversationViewFragment mConversationViewFragment; 121 protected boolean isLoaderInitialized = false; 122 private AsyncRefreshTask mAsyncRefreshTask; 123 124 private final Set<Uri> mCurrentAccountUris = Sets.newHashSet(); 125 protected Settings mCachedSettings; 126 protected ConversationCursor mConversationListCursor; 127 protected boolean mConversationListenerAdded = false; 128 /** 129 * Selected conversations, if any. 130 */ 131 private ConversationSelectionSet mSelectedSet = new ConversationSelectionSet(); 132 133 134 protected static final String LOG_TAG = new LogUtils().getLogTag(); 135 /** Constants used to differentiate between the types of loaders. */ 136 private static final int LOADER_ACCOUNT_CURSOR = 0; 137 private static final int LOADER_ACCOUNT_SETTINGS = 1; 138 private static final int LOADER_FOLDER_CURSOR = 2; 139 private static final int LOADER_RECENT_FOLDERS = 3; 140 private static final int LOADER_CONVERSATION_LIST = 4; 141 private static final int LOADER_ACCOUNT_INBOX = 5; 142 private static final int LOADER_SEARCH = 6; 143 144 public AbstractActivityController(MailActivity activity, ViewMode viewMode) { 145 mActivity = activity; 146 mViewMode = viewMode; 147 mContext = activity.getApplicationContext(); 148 IS_TABLET_DEVICE = Utils.useTabletUI(mContext); 149 mRecentFolderList = new RecentFolderList(mContext, this); 150 // Allow the fragment to observe changes to its own selection set. No other object is 151 // aware of the selected set. 152 mSelectedSet.addObserver(this); 153 } 154 155 @Override 156 public synchronized void attachConversationList(ConversationListFragment fragment) { 157 // If there is an existing fragment, unregister it 158 if (mConversationListFragment != null) { 159 mViewMode.removeListener(mConversationListFragment); 160 } 161 mConversationListFragment = fragment; 162 // If the current fragment is non-null, add it as a listener. 163 if (fragment != null) { 164 mViewMode.addListener(mConversationListFragment); 165 } 166 } 167 168 @Override 169 public synchronized void attachFolderList(FolderListFragment fragment) { 170 // If there is an existing fragment, unregister it 171 if (mFolderListFragment != null) { 172 mViewMode.removeListener(mFolderListFragment); 173 } 174 mFolderListFragment = fragment; 175 if (fragment != null) { 176 mViewMode.addListener(mFolderListFragment); 177 } 178 } 179 180 @Override 181 public void attachConversationView(ConversationViewFragment conversationViewFragment) { 182 mConversationViewFragment = conversationViewFragment; 183 } 184 185 @Override 186 public void clearSubject() { 187 // TODO(viki): Auto-generated method stub 188 } 189 190 @Override 191 public Account getCurrentAccount() { 192 return mAccount; 193 } 194 195 @Override 196 public ConversationListContext getCurrentListContext() { 197 return mConvListContext; 198 } 199 200 @Override 201 public String getHelpContext() { 202 return "Mail"; 203 } 204 205 @Override 206 public int getMode() { 207 return mViewMode.getMode(); 208 } 209 210 @Override 211 public String getUnshownSubject(String subject) { 212 // Calculate how much of the subject is shown, and return the remaining. 213 return null; 214 } 215 216 @Override 217 public void handleConversationLoadError() { 218 // TODO(viki): Auto-generated method stub 219 } 220 221 @Override 222 public ConversationCursor getConversationListCursor() { 223 return mConversationListCursor; 224 } 225 226 @Override 227 public void initConversationListCursor() { 228 mActivity.getLoaderManager().restartLoader(LOADER_CONVERSATION_LIST, Bundle.EMPTY, 229 new LoaderManager.LoaderCallbacks<ConversationCursor>() { 230 231 @Override 232 public void onLoadFinished(Loader<ConversationCursor> loader, 233 ConversationCursor data) { 234 mConversationListCursor = data; 235 if (mConversationListCursor.isRefreshReady()) { 236 mConversationListCursor.sync(); 237 } 238 if (mConversationListFragment != null) { 239 mConversationListFragment.onCursorUpdated(); 240 if (!mConversationListenerAdded) { 241 // TODO(mindyp): when we move to the cursor loader, we need 242 // to add/remove the listener when we create/ destroy loaders. 243 mConversationListCursor 244 .addListener(AbstractActivityController.this); 245 mConversationListenerAdded = true; 246 } 247 } 248 if (shouldShowFirstConversation()) { 249 if (mConversationListCursor.getCount() > 0) { 250 mConversationListCursor.moveToPosition(0); 251 mConversationListFragment.getListView().setItemChecked(0, true); 252 Conversation conv = new Conversation(mConversationListCursor); 253 conv.position = 0; 254 onConversationSelected(conv); 255 } 256 } 257 258 } 259 260 @Override 261 public void onLoaderReset(Loader<ConversationCursor> loader) { 262 if (mConversationListFragment == null) { 263 return; 264 } 265 mConversationListFragment.onCursorUpdated(); 266 } 267 268 @Override 269 public Loader<ConversationCursor> onCreateLoader(int id, Bundle args) { 270 if (mConversationListFragment != null) { 271 mConversationListFragment.configureSearchResultHeader(); 272 AnimatedAdapter adapter = mConversationListFragment 273 .getAnimatedAdapter(); 274 if (adapter != null) { 275 adapter.hideFooter(); 276 } 277 } 278 return new ConversationCursorLoader((Activity) mActivity, mAccount, 279 UIProvider.CONVERSATION_PROJECTION, mFolder.conversationListUri); 280 } 281 282 }); 283 } 284 285 /** 286 * Initialize the action bar. This is not visible to OnePaneController and 287 * TwoPaneController so they cannot override this behavior. 288 */ 289 private void initCustomActionBarView() { 290 ActionBar actionBar = mActivity.getActionBar(); 291 mActionBarView = (ActionBarView) LayoutInflater.from(mContext).inflate( 292 R.layout.actionbar_view, null); 293 if (actionBar != null && mActionBarView != null) { 294 // Why have a different variable for the same thing? We should apply 295 // the same actions 296 // on mActionBarView instead. 297 mActionBarView.initialize(mActivity, this, mViewMode, actionBar, mRecentFolderList); 298 actionBar.setCustomView(mActionBarView, new ActionBar.LayoutParams( 299 LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); 300 actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM, 301 ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_SHOW_TITLE); 302 } 303 } 304 305 /** 306 * Returns whether the conversation list fragment is visible or not. 307 * Different layouts will have their own notion on the visibility of 308 * fragments, so this method needs to be overriden. 309 * 310 * @return 311 */ 312 protected abstract boolean isConversationListVisible(); 313 314 @Override 315 public void onAccountChanged(Account account) { 316 if (!account.equals(mAccount)) { 317 // Current account is different from the new account, restart loaders and show 318 // the account Inbox. 319 mAccount = account; 320 mFolder = null; 321 // Reset settings; they are no longer valid. 322 onSettingsChanged(null); 323 mActionBarView.setAccount(mAccount); 324 restartOptionalLoader(LOADER_ACCOUNT_SETTINGS, null /* args */); 325 loadAccountInbox(); 326 327 mRecentFolderList.setCurrentAccount(account); 328 restartOptionalLoader(LOADER_RECENT_FOLDERS, null /* args */); 329 mActivity.invalidateOptionsMenu(); 330 331 disableNotificationsOnAccountChange(mAccount); 332 333 MailAppProvider.getInstance().setLastViewedAccount(mAccount.uri.toString()); 334 } else { 335 // Current account is the same as the new account. Load the default inbox if the 336 // current inbox is not the same as the default inbox. 337 final Uri oldUri = mFolder != null ? mFolder.uri : Uri.EMPTY; 338 final Uri newUri = getDefaultInboxUri(mCachedSettings); 339 if (!oldUri.equals(newUri)) { 340 loadAccountInbox(); 341 } 342 } 343 } 344 345 /** 346 * Returns the URI of the current account's default inbox if available, otherwise 347 * returns the empty URI {@link Uri#EMPTY} 348 * @return 349 */ 350 private Uri getDefaultInboxUri(Settings settings) { 351 if (settings != null && settings.defaultInbox != null) { 352 return settings.defaultInbox; 353 } 354 return Uri.EMPTY; 355 } 356 357 public void onSettingsChanged(Settings settings) { 358 final Uri oldUri = getDefaultInboxUri(mCachedSettings); 359 final Uri newUri = getDefaultInboxUri(settings); 360 mCachedSettings = settings; 361 resetActionBarIcon(); 362 // Only restart the loader if the defaultInboxUri is not the same as 363 // the folder we are already loading. 364 final boolean changed = !oldUri.equals(newUri); 365 if (settings != null && settings.defaultInbox != null 366 && (mFolder == null || mFolder.type == UIProvider.FolderType.INBOX) && changed) { 367 loadAccountInbox(); 368 } 369 } 370 371 @Override 372 public Settings getSettings() { 373 return mCachedSettings; 374 } 375 376 private void fetchSearchFolder(Intent intent) { 377 Bundle args = new Bundle(); 378 args.putString(ConversationListContext.EXTRA_SEARCH_QUERY, intent 379 .getStringExtra(ConversationListContext.EXTRA_SEARCH_QUERY)); 380 mActivity.getLoaderManager().restartLoader(LOADER_SEARCH, args, this); 381 } 382 383 @Override 384 public void onFolderChanged(Folder folder) { 385 if (folder != null && !folder.equals(mFolder)) { 386 setFolder(folder); 387 mConvListContext = ConversationListContext.forFolder(mContext, mAccount, mFolder); 388 showConversationList(mConvListContext); 389 390 // Add the folder that we were viewing to the recent folders list. 391 // TODO: this may need to be fine tuned. If this is the signal that is indicating that 392 // the list is shown to the user, this could fire in one pane if the user goes directly 393 // to a conversation 394 updateRecentFolderList(); 395 } 396 } 397 398 private void updateRecentFolderList() { 399 if (mFolder != null) { 400 mRecentFolderList.setCurrentAccount(mAccount); 401 mRecentFolderList.touchFolder(mFolder); 402 } 403 } 404 405 // TODO(mindyp): set this up to store a copy of the folder as a transient 406 // field in the account. 407 public void loadAccountInbox() { 408 restartOptionalLoader(LOADER_ACCOUNT_INBOX, null); 409 } 410 411 /** Set the current folder */ 412 private void setFolder(Folder folder) { 413 // Start watching folder for sync status. 414 if (folder != null && !folder.equals(mFolder)) { 415 mActionBarView.setRefreshInProgress(false); 416 mFolder = folder; 417 mActionBarView.setFolder(mFolder); 418 mActivity.getLoaderManager().restartLoader(LOADER_FOLDER_CURSOR, null, this); 419 initConversationListCursor(); 420 } else if (folder == null) { 421 LogUtils.wtf(LOG_TAG, "Folder in setFolder is null"); 422 } 423 } 424 425 @Override 426 public void onActivityResult(int requestCode, int resultCode, Intent data) { 427 // TODO(viki): Auto-generated method stub 428 } 429 430 @Override 431 public void onConversationListVisibilityChanged(boolean visible) { 432 // TODO(viki): Auto-generated method stub 433 } 434 435 /** 436 * By default, doing nothing is right. A two-pane controller will need to 437 * override this. 438 */ 439 @Override 440 public void onConversationVisibilityChanged(boolean visible) { 441 // Do nothing. 442 return; 443 } 444 445 @Override 446 public boolean onCreate(Bundle savedState) { 447 // Initialize the action bar view. 448 initCustomActionBarView(); 449 // Allow shortcut keys to function for the ActionBar and menus. 450 mActivity.setDefaultKeyMode(Activity.DEFAULT_KEYS_SHORTCUT); 451 mResolver = mActivity.getContentResolver(); 452 453 mNewEmailReceiver = new SuppressNotificationReceiver(); 454 455 // All the individual UI components listen for ViewMode changes. This 456 // simplifies the amount of logic in the AbstractActivityController, but increases the 457 // possibility of timing-related bugs. 458 mViewMode.addListener(this); 459 assert (mActionBarView != null); 460 mViewMode.addListener(mActionBarView); 461 462 restoreState(savedState); 463 return true; 464 } 465 466 @Override 467 public Dialog onCreateDialog(int id, Bundle bundle) { 468 return null; 469 } 470 471 @Override 472 public boolean onCreateOptionsMenu(Menu menu) { 473 MenuInflater inflater = mActivity.getMenuInflater(); 474 inflater.inflate(mActionBarView.getOptionsMenuId(), menu); 475 mActionBarView.onCreateOptionsMenu(menu); 476 return true; 477 } 478 479 @Override 480 public boolean onKeyDown(int keyCode, KeyEvent event) { 481 // TODO(viki): Auto-generated method stub 482 return false; 483 } 484 485 @Override 486 public boolean onOptionsItemSelected(MenuItem item) { 487 final int id = item.getItemId(); 488 boolean handled = true; 489 switch (id) { 490 case android.R.id.home: 491 onUpPressed(); 492 break; 493 case R.id.compose: 494 ComposeActivity.compose(mActivity.getActivityContext(), mAccount); 495 break; 496 case R.id.show_all_folders: 497 showFolderList(); 498 break; 499 case R.id.refresh: 500 requestFolderRefresh(); 501 break; 502 case R.id.settings: 503 Utils.showSettings(mActivity.getActivityContext(), mAccount); 504 break; 505 case R.id.help_info_menu_item: 506 // TODO: enable context sensitive help 507 Utils.showHelp(mActivity.getActivityContext(), mAccount.helpIntentUri, null); 508 break; 509 case R.id.feedback_menu_item: 510 Utils.sendFeedback(mActivity.getActivityContext(), mAccount); 511 break; 512 default: 513 handled = false; 514 break; 515 } 516 return handled; 517 } 518 519 /** 520 * Return the auto advance setting for the current account. 521 * @param activity 522 * @return the autoadvance setting, a constant from {@link AutoAdvance} 523 */ 524 static int getAutoAdvanceSetting(RestrictedActivity activity) { 525 final Settings settings = activity.getSettings(); 526 // TODO(mindyp): if this isn't set, then show the dialog telling the user to set it. 527 // Remove defaulting to AutoAdvance.LIST. 528 final int autoAdvance = (settings != null) ? 529 (settings.autoAdvance == AutoAdvance.UNSET ? 530 AutoAdvance.LIST : settings.autoAdvance) 531 : AutoAdvance.LIST; 532 return autoAdvance; 533 } 534 535 /** 536 * Implements folder changes. This class is a listener because folder changes need to be 537 * performed <b>after</b> the ConversationListFragment has finished animating away the 538 * removal of the conversation. 539 * 540 */ 541 protected abstract class FolderChangeListener implements ActionCompleteListener { 542 protected final String mFolderChangeList; 543 protected final boolean mDestructiveChange; 544 545 public FolderChangeListener(String changeList, boolean destructive) { 546 mFolderChangeList = changeList; 547 mDestructiveChange = destructive; 548 } 549 550 @Override 551 public abstract void onActionComplete(); 552 } 553 554 /** 555 * Update the specified column name in conversation for a boolean value. 556 * @param columnName 557 * @param value 558 */ 559 protected void updateCurrentConversation(String columnName, boolean value) { 560 Conversation.updateBoolean(mContext, ImmutableList.of(mCurrentConversation), columnName, 561 value); 562 if (mConversationListFragment != null) { 563 mConversationListFragment.requestListRefresh(); 564 } 565 } 566 567 /** 568 * Update the specified column name in conversation for an integer value. 569 * @param columnName 570 * @param value 571 */ 572 protected void updateCurrentConversation(String columnName, int value) { 573 Conversation.updateInt(mContext, ImmutableList.of(mCurrentConversation), columnName, value); 574 if (mConversationListFragment != null) { 575 mConversationListFragment.requestListRefresh(); 576 } 577 } 578 579 protected void updateCurrentConversation(String columnName, String value) { 580 Conversation.updateString(mContext, ImmutableList.of(mCurrentConversation), columnName, 581 value); 582 if (mConversationListFragment != null) { 583 mConversationListFragment.requestListRefresh(); 584 } 585 } 586 587 private void requestFolderRefresh() { 588 if (mFolder != null) { 589 if (mAsyncRefreshTask != null) { 590 mAsyncRefreshTask.cancel(true); 591 } 592 mAsyncRefreshTask = new AsyncRefreshTask(mContext, mFolder); 593 mAsyncRefreshTask.execute(); 594 } 595 } 596 597 /** 598 * Confirm (based on user's settings) and delete a conversation from the conversation list and 599 * from the database. 600 * @param showDialog 601 * @param confirmResource 602 * @param listener 603 */ 604 protected void confirmAndDelete(boolean showDialog, int confirmResource, 605 final ActionCompleteListener listener) { 606 final ArrayList<Conversation> single = new ArrayList<Conversation>(); 607 single.add(mCurrentConversation); 608 if (showDialog) { 609 final AlertDialog.OnClickListener onClick = new AlertDialog.OnClickListener() { 610 @Override 611 public void onClick(DialogInterface dialog, int which) { 612 requestDelete(listener); 613 } 614 }; 615 final CharSequence message = Utils.formatPlural(mContext, confirmResource, 1); 616 new AlertDialog.Builder(mActivity.getActivityContext()).setMessage(message) 617 .setPositiveButton(R.string.ok, onClick) 618 .setNegativeButton(R.string.cancel, null) 619 .create().show(); 620 } else { 621 requestDelete(listener); 622 } 623 } 624 625 626 protected abstract void requestDelete(ActionCompleteListener listener); 627 628 629 @Override 630 public void onPrepareDialog(int id, Dialog dialog, Bundle bundle) { 631 // TODO(viki): Auto-generated method stub 632 633 } 634 635 @Override 636 public boolean onPrepareOptionsMenu(Menu menu) { 637 mActionBarView.onPrepareOptionsMenu(menu); 638 return true; 639 } 640 641 @Override 642 public void onPause() { 643 isLoaderInitialized = false; 644 645 enableNotifications(); 646 } 647 648 @Override 649 public void onResume() { 650 // Register the receiver that will prevent the status receiver from 651 // displaying its notification icon as long as we're running. 652 // The SupressNotificationReceiver will block the broadcast if we're looking at the folder 653 // that the notification was received for. 654 disableNotifications(); 655 656 if (mActionBarView != null) { 657 mActionBarView.onResume(); 658 } 659 660 } 661 662 @Override 663 public void onSaveInstanceState(Bundle outState) { 664 if (mAccount != null) { 665 LogUtils.d(LOG_TAG, "Saving the account now"); 666 outState.putParcelable(SAVED_ACCOUNT, mAccount); 667 } 668 if (mFolder != null) { 669 outState.putParcelable(SAVED_FOLDER, mFolder); 670 } 671 if (mCurrentConversation != null && mViewMode.getMode() == ViewMode.CONVERSATION) { 672 outState.putParcelable(SAVED_CONVERSATION, mCurrentConversation); 673 } 674 } 675 676 @Override 677 public void onSearchRequested(String query) { 678 Intent intent = new Intent(); 679 intent.setAction(Intent.ACTION_SEARCH); 680 intent.putExtra(ConversationListContext.EXTRA_SEARCH_QUERY, query); 681 intent.putExtra(Utils.EXTRA_ACCOUNT, mAccount); 682 intent.setComponent(mActivity.getComponentName()); 683 mActivity.startActivity(intent); 684 } 685 686 @Override 687 public void onStartDragMode() { 688 // TODO(viki): Auto-generated method stub 689 } 690 691 @Override 692 public void onStop() { 693 // TODO(viki): Auto-generated method stub 694 } 695 696 @Override 697 public void onStopDragMode() { 698 // TODO(viki): Auto-generated method stub 699 } 700 701 /** 702 * {@inheritDoc} Subclasses must override this to listen to mode changes 703 * from the ViewMode. Subclasses <b>must</b> call the parent's 704 * onViewModeChanged since the parent will handle common state changes. 705 */ 706 @Override 707 public void onViewModeChanged(int newMode) { 708 // Perform any mode specific work here. 709 // reset the action bar icon based on the mode. Why don't the individual 710 // controllers do 711 // this themselves? 712 713 // In conversation list mode, clean up the conversation. 714 if (newMode == ViewMode.CONVERSATION_LIST) { 715 // Clean up the conversation here. 716 } 717 718 // We don't want to invalidate the options menu when switching to 719 // conversation 720 // mode, as it will happen when the conversation finishes loading. 721 if (newMode != ViewMode.CONVERSATION) { 722 mActivity.invalidateOptionsMenu(); 723 } 724 } 725 726 @Override 727 public void onWindowFocusChanged(boolean hasFocus) { 728 // TODO(viki): Auto-generated method stub 729 } 730 731 /** 732 * Restore the state from the previous bundle. Subclasses should call this 733 * method from the parent class, since it performs important UI 734 * initialization. 735 * 736 * @param savedState 737 */ 738 protected void restoreState(Bundle savedState) { 739 final Intent intent = mActivity.getIntent(); 740 if (savedState != null) { 741 boolean handled = false; 742 if (savedState.containsKey(SAVED_ACCOUNT)) { 743 mAccount = ((Account) savedState.getParcelable(SAVED_ACCOUNT)); 744 mActionBarView.setAccount(mAccount); 745 mActivity.invalidateOptionsMenu(); 746 } 747 if (savedState.containsKey(SAVED_FOLDER)) { 748 // Open the folder. 749 LogUtils.d(LOG_TAG, "SHOW THE FOLDER at %s", 750 intent.getParcelableExtra(Utils.EXTRA_FOLDER)); 751 onFolderChanged((Folder) savedState.getParcelable(SAVED_FOLDER)); 752 handled = true; 753 } 754 if (savedState.containsKey(SAVED_CONVERSATION)) { 755 // Open the conversation. 756 setCurrentConversation((Conversation) savedState.getParcelable(SAVED_CONVERSATION)); 757 showConversation(mCurrentConversation); 758 handled = true; 759 } 760 if (!handled) { 761 // Nothing was saved; just load the account inbox. 762 loadAccountInbox(); 763 } 764 restartOptionalLoader(LOADER_ACCOUNT_SETTINGS, null /* args */); 765 } 766 else if (intent != null) { 767 if (Intent.ACTION_VIEW.equals(intent.getAction())) { 768 if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) { 769 mAccount = ((Account) intent.getParcelableExtra(Utils.EXTRA_ACCOUNT)); 770 mActionBarView.setAccount(mAccount); 771 restartOptionalLoader(LOADER_ACCOUNT_SETTINGS, null /* args */); 772 mActivity.invalidateOptionsMenu(); 773 } 774 if (intent.hasExtra(Utils.EXTRA_FOLDER)) { 775 // Open the folder. 776 LogUtils.d(LOG_TAG, "SHOW THE FOLDER at %s", 777 intent.getParcelableExtra(Utils.EXTRA_FOLDER)); 778 onFolderChanged((Folder) intent.getParcelableExtra(Utils.EXTRA_FOLDER)); 779 } 780 if (intent.hasExtra(Utils.EXTRA_CONVERSATION)) { 781 // Open the conversation. 782 LogUtils.d(LOG_TAG, "SHOW THE CONVERSATION at %s", 783 intent.getParcelableExtra(Utils.EXTRA_CONVERSATION)); 784 setCurrentConversation((Conversation) intent 785 .getParcelableExtra(Utils.EXTRA_CONVERSATION)); 786 showConversation(mCurrentConversation); 787 } 788 } else if (Intent.ACTION_SEARCH.equals(intent.getAction())) { 789 mViewMode.enterSearchResultsListMode(); 790 mAccount = ((Account) intent.getParcelableExtra(Utils.EXTRA_ACCOUNT)); 791 mActionBarView.setAccount(mAccount); 792 fetchSearchFolder(intent); 793 } 794 } 795 796 /** 797 * Restore the state of selected conversations. This needs to be done after the correct mode 798 * is set and the action bar is fully initialized. If not, several key pieces of state 799 * information will be missing, and the split views may not be initialized correctly. 800 * @param savedState 801 */ 802 restoreSelectedConversations(intent.getExtras()); 803 // Create the accounts loader; this loads the account switch spinner. 804 mActivity.getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this); 805 } 806 807 private void restoreSelectedConversations(Bundle savedState) { 808 if (savedState == null) { 809 onSetEmpty(); 810 return; 811 } 812 mSelectedSet = savedState.getParcelable(SAVED_CONVERSATIONS); 813 if (mSelectedSet == null) { 814 mSelectedSet = new ConversationSelectionSet(); 815 } 816 if (mSelectedSet.isEmpty()) { 817 onSetEmpty(); 818 return; 819 } 820 // We have some selected conversations. Perform all the actions needed. 821 onSetPopulated(mSelectedSet); 822 } 823 824 @Override 825 public void setSubject(String subject) { 826 // Do something useful with the subject. This requires changing the 827 // conversation view's subject text. 828 } 829 830 /** 831 * Children can override this method, but they must call super.showConversation(). 832 * {@inheritDoc} 833 */ 834 @Override 835 public void showConversation(Conversation conversation) { 836 } 837 838 @Override 839 public void onConversationSelected(Conversation conversation) { 840 setCurrentConversation(conversation); 841 showConversation(mCurrentConversation); 842 if (mConvListContext != null && mConvListContext.isSearchResult()) { 843 mViewMode.enterSearchResultsConversationMode(); 844 } else { 845 mViewMode.enterConversationMode(); 846 } 847 } 848 849 public void setCurrentConversation(Conversation conversation) { 850 mCurrentConversation = conversation; 851 } 852 853 /** 854 * {@inheritDoc} 855 */ 856 @Override 857 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 858 // Create a loader to listen in on account changes. 859 switch (id) { 860 case LOADER_ACCOUNT_CURSOR: 861 return new CursorLoader(mContext, MailAppProvider.getAccountsUri(), 862 UIProvider.ACCOUNTS_PROJECTION, null, null, null); 863 case LOADER_FOLDER_CURSOR: 864 return new CursorLoader(mContext, mFolder.uri, UIProvider.FOLDERS_PROJECTION, null, 865 null, null); 866 case LOADER_ACCOUNT_SETTINGS: 867 if (mAccount.settingsQueryUri != null) { 868 return new CursorLoader(mContext, mAccount.settingsQueryUri, 869 UIProvider.SETTINGS_PROJECTION, null, null, null); 870 } 871 break; 872 case LOADER_RECENT_FOLDERS: 873 if (mAccount.recentFolderListUri != null) { 874 return new CursorLoader(mContext, mAccount.recentFolderListUri, 875 UIProvider.FOLDERS_PROJECTION, null, null, null); 876 } 877 break; 878 case LOADER_ACCOUNT_INBOX: 879 Settings settings = getSettings(); 880 Uri inboxUri; 881 if (settings != null) { 882 inboxUri = settings.defaultInbox; 883 } else { 884 inboxUri = mAccount.folderListUri; 885 } 886 return new CursorLoader(mContext, inboxUri, UIProvider.FOLDERS_PROJECTION, null, 887 null, null); 888 case LOADER_SEARCH: 889 return Folder.forSearchResults(mAccount, 890 args.getString(ConversationListContext.EXTRA_SEARCH_QUERY), 891 mActivity.getActivityContext()); 892 default: 893 LogUtils.wtf(LOG_TAG, "Loader returned unexpected id: " + id); 894 } 895 return null; 896 } 897 898 /** 899 * {@link LoaderManager} currently has a bug in 900 * {@link LoaderManager#restartLoader(int, Bundle, android.app.LoaderManager.LoaderCallbacks)} 901 * where, if a previous onCreateLoader returned a null loader, this method will NPE. Work around 902 * this bug by destroying any loaders that may have been created as null (essentially because 903 * they are optional loads, and may not apply to a particular account). 904 * <p> 905 * A simple null check before restarting a loader will not work, because that would not 906 * give the controller a chance to invalidate UI corresponding the prior loader result. 907 * 908 * @param id loader ID to safely restart 909 * @param args arguments to pass to the restarted loader 910 */ 911 private void restartOptionalLoader(int id, Bundle args) { 912 final LoaderManager lm = mActivity.getLoaderManager(); 913 lm.destroyLoader(id); 914 lm.restartLoader(id, args, this); 915 } 916 917 private boolean accountsUpdated(Cursor accountCursor) { 918 // Check to see if the current account hasn't been set, or the account cursor is empty 919 if (mAccount == null || !accountCursor.moveToFirst()) { 920 return true; 921 } 922 923 // Check to see if the number of accounts are different, from the number we saw on the last 924 // updated 925 if (mCurrentAccountUris.size() != accountCursor.getCount()) { 926 return true; 927 } 928 929 // Check to see if the account list is different or if the current account is not found in 930 // the cursor. 931 boolean foundCurrentAccount = false; 932 do { 933 final Uri accountUri = 934 Uri.parse(accountCursor.getString(UIProvider.ACCOUNT_URI_COLUMN)); 935 if (!foundCurrentAccount && mAccount.uri.equals(accountUri)) { 936 foundCurrentAccount = true; 937 } 938 939 if (!mCurrentAccountUris.contains(accountUri)) { 940 return true; 941 } 942 } while (accountCursor.moveToNext()); 943 944 // As long as we found the current account, the list hasn't been updated 945 return !foundCurrentAccount; 946 } 947 948 /** 949 * Update the accounts on the device. This currently loads the first account 950 * in the list. 951 * 952 * @param loader 953 * @param accounts cursor into the AccountCache 954 * @return true if the update was successful, false otherwise 955 */ 956 private boolean updateAccounts(Loader<Cursor> loader, Cursor accounts) { 957 if (accounts == null || !accounts.moveToFirst()) { 958 return false; 959 } 960 961 final Account[] allAccounts = Account.getAllAccounts(accounts); 962 963 // Save the uris for the accounts 964 mCurrentAccountUris.clear(); 965 for (Account account : allAccounts) { 966 mCurrentAccountUris.add(account.uri); 967 } 968 969 // 1. current account is already set and is in allAccounts -> no-op 970 // 2. current account is set and is not in allAccounts -> pick first (acct was deleted?) 971 // 3. saved pref has an account -> pick that one 972 // 4. otherwise just pick first 973 974 Account newAccount = null; 975 976 if (mAccount != null) { 977 if (!mCurrentAccountUris.contains(mAccount.uri)) { 978 newAccount = allAccounts[0]; 979 } else { 980 newAccount = mAccount; 981 } 982 } else { 983 final String lastAccountUri = MailAppProvider.getInstance() 984 .getLastViewedAccount(); 985 if (lastAccountUri != null) { 986 for (int i = 0; i < allAccounts.length; i++) { 987 final Account acct = allAccounts[i]; 988 if (lastAccountUri.equals(acct.uri.toString())) { 989 newAccount = acct; 990 break; 991 } 992 } 993 } 994 if (newAccount == null) { 995 newAccount = allAccounts[0]; 996 } 997 } 998 999 onAccountChanged(newAccount); 1000 1001 mActionBarView.setAccounts(allAccounts); 1002 return (allAccounts.length > 0); 1003 } 1004 1005 private void disableNotifications() { 1006 mNewEmailReceiver.activate(mContext, this); 1007 } 1008 1009 private void enableNotifications() { 1010 mNewEmailReceiver.deactivate(); 1011 } 1012 1013 private void disableNotificationsOnAccountChange(Account account) { 1014 // If the new mail suppression receiver is activated for a different account, we want to 1015 // activate it for the new account. 1016 if (mNewEmailReceiver.activated() && 1017 !mNewEmailReceiver.notificationsDisabledForAccount(account)) { 1018 // Deactivate the current receiver, otherwise multiple receivers may be registered. 1019 mNewEmailReceiver.deactivate(); 1020 mNewEmailReceiver.activate(mContext, this); 1021 } 1022 } 1023 1024 /** 1025 * {@inheritDoc} 1026 */ 1027 @Override 1028 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 1029 // We want to reinitialize only if we haven't ever been initialized, or 1030 // if the current account has vanished. 1031 if (data == null) { 1032 LogUtils.e(LOG_TAG, "Received null cursor from loader id: %d", loader.getId()); 1033 } 1034 switch (loader.getId()) { 1035 case LOADER_ACCOUNT_CURSOR: 1036 final boolean accountListUpdated = accountsUpdated(data); 1037 if (!isLoaderInitialized || accountListUpdated) { 1038 isLoaderInitialized = updateAccounts(loader, data); 1039 } 1040 break; 1041 case LOADER_FOLDER_CURSOR: 1042 // Check status of the cursor. 1043 data.moveToFirst(); 1044 Folder folder = new Folder(data); 1045 if (folder.isSyncInProgress()) { 1046 mActionBarView.onRefreshStarted(); 1047 } else { 1048 // Stop the spinner here. 1049 mActionBarView.onRefreshStopped(folder.lastSyncResult); 1050 } 1051 if (mConversationListFragment != null) { 1052 mConversationListFragment.onFolderUpdated(folder); 1053 } 1054 LogUtils.v(LOG_TAG, "FOLDER STATUS = " + folder.syncStatus); 1055 break; 1056 case LOADER_ACCOUNT_SETTINGS: 1057 // An account may actually have no settings if it is one of the 1058 // special combined accounts. 1059 Settings settings = null; 1060 if (data.moveToFirst()) { 1061 settings = new Settings(data); 1062 } 1063 onSettingsChanged(settings); 1064 break; 1065 case LOADER_RECENT_FOLDERS: 1066 mRecentFolderList.loadFromUiProvider(data); 1067 break; 1068 case LOADER_ACCOUNT_INBOX: 1069 if (data.moveToFirst() && !data.isClosed()) { 1070 Folder inbox = new Folder(data); 1071 onFolderChanged(inbox); 1072 // Just want to get the inbox, don't care about updates to it 1073 // as this will be tracked by the folder change listener. 1074 mActivity.getLoaderManager().destroyLoader(LOADER_ACCOUNT_INBOX); 1075 } else { 1076 LogUtils.d(LOG_TAG, "Unable to get the account inbox for account %s", 1077 mAccount != null ? mAccount.name : ""); 1078 } 1079 break; 1080 case LOADER_SEARCH: 1081 data.moveToFirst(); 1082 Folder search = new Folder(data); 1083 setFolder(search); 1084 mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder, 1085 mActivity.getIntent() 1086 .getStringExtra(UIProvider.SearchQueryParameters.QUERY)); 1087 showConversationList(mConvListContext); 1088 mActivity.invalidateOptionsMenu(); 1089 break; 1090 } 1091 } 1092 1093 /** 1094 * {@inheritDoc} 1095 */ 1096 @Override 1097 public void onLoaderReset(Loader<Cursor> loader) { 1098 switch (loader.getId()) { 1099 case LOADER_ACCOUNT_SETTINGS: 1100 onSettingsChanged(null); 1101 break; 1102 } 1103 } 1104 1105 @Override 1106 public void onTouchEvent(MotionEvent event) { 1107 if (event.getAction() == MotionEvent.ACTION_DOWN) { 1108 int mode = mViewMode.getMode(); 1109 if (mode == ViewMode.CONVERSATION_LIST) { 1110 mConversationListFragment.onTouchEvent(event); 1111 } else if (mode == ViewMode.CONVERSATION) { 1112 mConversationViewFragment.onTouchEvent(event); 1113 } 1114 } 1115 } 1116 1117 protected abstract class DestructiveActionListener implements ActionCompleteListener { 1118 protected final int mAction; 1119 1120 /** 1121 * Create a listener object. action is one of four constants: R.id.y_button (archive), 1122 * R.id.delete , R.id.mute, and R.id.report_spam. 1123 * @param action 1124 */ 1125 public DestructiveActionListener(int action) { 1126 mAction = action; 1127 } 1128 1129 public void performConversationAction(Collection<Conversation> single) { 1130 switch (mAction) { 1131 case R.id.archive: 1132 LogUtils.d(LOG_TAG, "Archiving conversation " + mCurrentConversation); 1133 Conversation.archive(mContext, single); 1134 break; 1135 case R.id.delete: 1136 LogUtils.d(LOG_TAG, "Deleting conversation " + mCurrentConversation); 1137 Conversation.delete(mContext, single); 1138 break; 1139 case R.id.mute: 1140 LogUtils.d(LOG_TAG, "Muting conversation " + mCurrentConversation); 1141 if (mFolder.supportsCapability(FolderCapabilities.DESTRUCTIVE_MUTE)) 1142 mCurrentConversation.localDeleteOnUpdate = true; 1143 Conversation.mute(mContext, single); 1144 break; 1145 case R.id.report_spam: 1146 LogUtils.d(LOG_TAG, "reporting spam conversation " + mCurrentConversation); 1147 Conversation.reportSpam(mContext, single); 1148 break; 1149 } 1150 } 1151 1152 public Conversation getNextConversation() { 1153 Conversation next = null; 1154 int pref = getAutoAdvanceSetting(mActivity); 1155 Cursor c = mConversationListCursor; 1156 if (c != null) { 1157 c.moveToPosition(mCurrentConversation.position); 1158 } 1159 switch (pref) { 1160 case AutoAdvance.NEWER: 1161 if (c.moveToPrevious()) { 1162 next = new Conversation(c); 1163 } 1164 break; 1165 case AutoAdvance.OLDER: 1166 if (c.moveToNext()) { 1167 next = new Conversation(c); 1168 } 1169 break; 1170 } 1171 return next; 1172 } 1173 1174 @Override 1175 public abstract void onActionComplete(); 1176 } 1177 1178 // Called from the FolderSelectionDialog after a user is done changing 1179 // folders. 1180 @Override 1181 public void onFolderChangesCommit(ArrayList<Folder> folderChangeList) { 1182 // Get currently active folder info and compare it to the list 1183 // these conversations have been given; if they no longer contain 1184 // the selected folder, delete them from the list. 1185 HashSet<String> folderUris = new HashSet<String>(); 1186 if (folderChangeList != null && !folderChangeList.isEmpty()) { 1187 for (Folder f : folderChangeList) { 1188 folderUris.add(f.uri.toString()); 1189 } 1190 } 1191 final boolean destructiveChange = !folderUris.contains(mFolder.uri.toString()); 1192 DestructiveActionListener listener = getFolderDestructiveActionListener(); 1193 StringBuilder foldersUrisString = new StringBuilder(); 1194 boolean first = true; 1195 for (Folder f : folderChangeList) { 1196 if (first) { 1197 first = false; 1198 } else { 1199 foldersUrisString.append(','); 1200 } 1201 foldersUrisString.append(f.uri.toString()); 1202 } 1203 updateCurrentConversation(ConversationColumns.FOLDER_LIST, foldersUrisString.toString()); 1204 updateCurrentConversation(ConversationColumns.RAW_FOLDERS, 1205 Folder.getSerializedFolderString(mFolder, folderChangeList)); 1206 // TODO: (mindyp): set ConversationColumns.RAW_FOLDERS like in 1207 // SelectedConversationsActionMenu 1208 if (destructiveChange) { 1209 mCurrentConversation.localDeleteOnUpdate = true; 1210 requestDelete(listener); 1211 } else if (mConversationListFragment != null) { 1212 mConversationListFragment.requestListRefresh(); 1213 } 1214 } 1215 1216 protected abstract DestructiveActionListener getFolderDestructiveActionListener(); 1217 1218 @Override 1219 public void onRefreshRequired() { 1220 // Refresh the query in the background 1221 getConversationListCursor().refresh(); 1222 } 1223 1224 @Override 1225 public void onRefreshReady() { 1226 ArrayList<Integer> deletedRows = mConversationListCursor.getRefreshDeletions(); 1227 // If we have any deletions from the server, animate them away 1228 if (!deletedRows.isEmpty() && mConversationListFragment != null) { 1229 AnimatedAdapter adapter = mConversationListFragment.getAnimatedAdapter(); 1230 if (adapter != null) { 1231 mConversationListFragment.getAnimatedAdapter().delete(deletedRows, 1232 this); 1233 } 1234 } else { 1235 // Swap cursors 1236 getConversationListCursor().sync(); 1237 refreshAdapter(); 1238 } 1239 } 1240 1241 @Override 1242 public void onDataSetChanged() { 1243 refreshAdapter(); 1244 } 1245 1246 private void refreshAdapter() { 1247 if (mConversationListFragment != null) { 1248 AnimatedAdapter adapter = mConversationListFragment.getAnimatedAdapter(); 1249 if (adapter != null) { 1250 adapter.notifyDataSetChanged(); 1251 } 1252 } 1253 } 1254 1255 @Override 1256 public void onSetEmpty() { 1257 mSelectedConversationsActionMenu = null; 1258 } 1259 1260 @Override 1261 public void onSetPopulated(ConversationSelectionSet set) { 1262 mSelectedConversationsActionMenu = new SelectedConversationsActionMenu(mActivity, 1263 mSelectedSet, mConversationListFragment.getAnimatedAdapter(), this, 1264 mConversationListFragment, mAccount, mFolder); 1265 mSelectedConversationsActionMenu.activate(); 1266 } 1267 1268 1269 @Override 1270 public void onSetChanged(ConversationSelectionSet set) { 1271 // Do nothing. We don't care about changes to the set. 1272 } 1273 1274 @Override 1275 public ConversationSelectionSet getSelectedSet() { 1276 return mSelectedSet; 1277 } 1278 1279 @Override 1280 public void onActionComplete() { 1281 if (getConversationListCursor().isRefreshReady()) { 1282 refreshAdapter(); 1283 } 1284 } 1285} 1286