AbstractActivityController.java revision d503df4f0c31bbf842c6a1d3cba18df8c074bf67
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.Fragment; 26import android.app.FragmentManager; 27import android.app.LoaderManager; 28import android.app.SearchManager; 29import android.content.ContentResolver; 30import android.content.Context; 31import android.content.CursorLoader; 32import android.content.DialogInterface; 33import android.content.Intent; 34import android.content.Loader; 35import android.database.Cursor; 36import android.database.DataSetObservable; 37import android.database.DataSetObserver; 38import android.net.Uri; 39import android.os.Bundle; 40import android.os.Handler; 41import android.provider.SearchRecentSuggestions; 42import android.view.DragEvent; 43import android.view.KeyEvent; 44import android.view.LayoutInflater; 45import android.view.Menu; 46import android.view.MenuInflater; 47import android.view.MenuItem; 48import android.view.MotionEvent; 49import android.widget.AbsListView; 50import android.widget.AbsListView.OnScrollListener; 51import android.widget.Toast; 52 53import com.android.mail.ConversationListContext; 54import com.android.mail.R; 55import com.android.mail.browse.ConversationCursor; 56import com.android.mail.browse.ConversationCursor.ConversationListener; 57import com.android.mail.browse.ConversationPagerController; 58import com.android.mail.browse.SelectedConversationsActionMenu; 59import com.android.mail.compose.ComposeActivity; 60import com.android.mail.providers.Account; 61import com.android.mail.providers.Conversation; 62import com.android.mail.providers.Folder; 63import com.android.mail.providers.MailAppProvider; 64import com.android.mail.providers.Settings; 65import com.android.mail.providers.SuggestionsProvider; 66import com.android.mail.providers.UIProvider; 67import com.android.mail.providers.UIProvider.AccountCursorExtraKeys; 68import com.android.mail.providers.UIProvider.ConversationColumns; 69import com.android.mail.providers.UIProvider.FolderCapabilities; 70import com.android.mail.utils.LogUtils; 71import com.android.mail.utils.Utils; 72import com.google.common.collect.Lists; 73import com.google.common.collect.Sets; 74 75import java.util.ArrayList; 76import java.util.Collection; 77import java.util.Collections; 78import java.util.Set; 79import java.util.Timer; 80import java.util.TimerTask; 81 82 83/** 84 * This is an abstract implementation of the Activity Controller. This class 85 * knows how to respond to menu items, state changes, layout changes, etc. It 86 * weaves together the views and listeners, dispatching actions to the 87 * respective underlying classes. 88 * <p> 89 * Even though this class is abstract, it should provide default implementations 90 * for most, if not all the methods in the ActivityController interface. This 91 * makes the task of the subclasses easier: OnePaneActivityController and 92 * TwoPaneActivityController can be concise when the common functionality is in 93 * AbstractActivityController. 94 * </p> 95 * <p> 96 * In the Gmail codebase, this was called BaseActivityController 97 * </p> 98 */ 99public abstract class AbstractActivityController implements ActivityController, 100 ConversationListener, OnScrollListener { 101 // Keys for serialization of various information in Bundles. 102 /** Tag for {@link #mAccount} */ 103 private static final String SAVED_ACCOUNT = "saved-account"; 104 /** Tag for {@link #mFolder} */ 105 private static final String SAVED_FOLDER = "saved-folder"; 106 /** Tag for {@link #mCurrentConversation} */ 107 private static final String SAVED_CONVERSATION = "saved-conversation"; 108 /** Tag for {@link #mSelectedSet} */ 109 private static final String SAVED_SELECTED_SET = "saved-selected-set"; 110 111 /** Tag used when loading a wait fragment */ 112 protected static final String TAG_WAIT = "wait-fragment"; 113 /** Tag used when loading a conversation list fragment. */ 114 public static final String TAG_CONVERSATION_LIST = "tag-conversation-list"; 115 /** Tag used when loading a conversation fragment. */ 116 public static final String TAG_CONVERSATION = "tag-conversation"; 117 /** Tag used when loading a folder list fragment. */ 118 protected static final String TAG_FOLDER_LIST = "tag-folder-list"; 119 120 private static final long CONVERSATION_LIST_THROTTLE_MS = 4000L; 121 122 /** Are we on a tablet device or not. */ 123 public final boolean IS_TABLET_DEVICE; 124 125 protected Account mAccount; 126 protected Folder mFolder; 127 protected ActionBarView mActionBarView; 128 protected final RestrictedActivity mActivity; 129 protected final Context mContext; 130 private final FragmentManager mFragmentManager; 131 protected final RecentFolderList mRecentFolderList; 132 protected ConversationListContext mConvListContext; 133 protected Conversation mCurrentConversation; 134 135 /** A {@link android.content.BroadcastReceiver} that suppresses new e-mail notifications. */ 136 private SuppressNotificationReceiver mNewEmailReceiver = null; 137 138 protected Handler mHandler = new Handler(); 139 /** 140 * The current mode of the application. All changes in mode are initiated by 141 * the activity controller. View mode changes are propagated to classes that 142 * attach themselves as listeners of view mode changes. 143 */ 144 protected final ViewMode mViewMode; 145 protected ContentResolver mResolver; 146 protected boolean isLoaderInitialized = false; 147 private AsyncRefreshTask mAsyncRefreshTask; 148 149 private final Set<Uri> mCurrentAccountUris = Sets.newHashSet(); 150 protected ConversationCursor mConversationListCursor; 151 private final DataSetObservable mConversationListObservable = new DataSetObservable() { 152 @Override 153 public void registerObserver(DataSetObserver observer) { 154 final int count = mObservers.size(); 155 super.registerObserver(observer); 156 LogUtils.d(LOG_TAG, "IN AAC.registerListObserver: %s before=%d after=%d", observer, 157 count, mObservers.size()); 158 } 159 @Override 160 public void unregisterObserver(DataSetObserver observer) { 161 final int count = mObservers.size(); 162 super.unregisterObserver(observer); 163 LogUtils.d(LOG_TAG, "IN AAC.unregisterListObserver: %s before=%d after=%d", observer, 164 count, mObservers.size()); 165 } 166 }; 167 protected boolean mConversationListenerAdded = false; 168 169 private boolean mIsConversationListScrolling = false; 170 private long mConversationListRefreshTime = 0; 171 private Timer mConversationListTimer = new Timer(); 172 private RefreshTimerTask mConversationListRefreshTask; 173 174 /** Listeners that are interested in changes to current account settings. */ 175 private final ArrayList<Settings.ChangeListener> mSettingsListeners = Lists.newArrayList(); 176 177 /** 178 * Selected conversations, if any. 179 */ 180 private final ConversationSelectionSet mSelectedSet = new ConversationSelectionSet(); 181 182 private final int mFolderItemUpdateDelayMs; 183 184 /** Keeps track of selected and unselected conversations */ 185 final protected ConversationPositionTracker mTracker = 186 new ConversationPositionTracker(mSelectedSet); 187 188 /** 189 * Action menu associated with the selected set. 190 */ 191 SelectedConversationsActionMenu mCabActionMenu; 192 protected UndoBarView mUndoBarView; 193 protected ConversationPagerController mPagerController; 194 195 // this is split out from the general loader dispatcher because its loader doesn't return a 196 // basic Cursor 197 private final ConversationListLoaderCallbacks mListCursorCallbacks = 198 new ConversationListLoaderCallbacks(); 199 200 protected static final String LOG_TAG = new LogUtils().getLogTag(); 201 /** Constants used to differentiate between the types of loaders. */ 202 private static final int LOADER_ACCOUNT_CURSOR = 0; 203 private static final int LOADER_FOLDER_CURSOR = 2; 204 private static final int LOADER_RECENT_FOLDERS = 3; 205 private static final int LOADER_CONVERSATION_LIST = 4; 206 private static final int LOADER_ACCOUNT_INBOX = 5; 207 private static final int LOADER_SEARCH = 6; 208 private static final int LOADER_ACCOUNT_UPDATE_CURSOR = 7; 209 210 private static final int ADD_ACCOUNT_REQUEST_CODE = 1; 211 212 /** The pending destructive action to be carried out before swapping the conversation cursor.*/ 213 private DestructiveAction mPendingDestruction; 214 215 public AbstractActivityController(MailActivity activity, ViewMode viewMode) { 216 mActivity = activity; 217 mFragmentManager = mActivity.getFragmentManager(); 218 mViewMode = viewMode; 219 mContext = activity.getApplicationContext(); 220 IS_TABLET_DEVICE = Utils.useTabletUI(mContext); 221 mRecentFolderList = new RecentFolderList(mContext); 222 // Allow the fragment to observe changes to its own selection set. No other object is 223 // aware of the selected set. 224 mSelectedSet.addObserver(this); 225 226 mFolderItemUpdateDelayMs = 227 mContext.getResources().getInteger(R.integer.folder_item_refresh_delay_ms); 228 } 229 230 @Override 231 public void clearSubject() { 232 // TODO(viki): Auto-generated method stub 233 } 234 235 @Override 236 public Account getCurrentAccount() { 237 return mAccount; 238 } 239 240 @Override 241 public ConversationListContext getCurrentListContext() { 242 return mConvListContext; 243 } 244 245 @Override 246 public String getHelpContext() { 247 return "Mail"; 248 } 249 250 @Override 251 public int getMode() { 252 return mViewMode.getMode(); 253 } 254 255 @Override 256 public String getUnshownSubject(String subject) { 257 // Calculate how much of the subject is shown, and return the remaining. 258 return null; 259 } 260 261 @Override 262 public void handleConversationLoadError() { 263 // TODO(viki): Auto-generated method stub 264 } 265 266 @Override 267 public final ConversationCursor getConversationListCursor() { 268 return mConversationListCursor; 269 } 270 271 /** 272 * Check if the fragment is attached to an activity and has a root view. 273 * @param in 274 * @return true if the fragment is valid, false otherwise 275 */ 276 private static final boolean isValidFragment(Fragment in) { 277 if (in == null || in.getActivity() == null || in.getView() == null) { 278 return false; 279 } 280 return true; 281 } 282 283 /** 284 * Get the conversation list fragment for this activity. If the conversation list fragment 285 * is not attached, this method returns null 286 * @return 287 */ 288 protected ConversationListFragment getConversationListFragment() { 289 final Fragment fragment = mFragmentManager.findFragmentByTag(TAG_CONVERSATION_LIST); 290 if (isValidFragment(fragment)) { 291 return (ConversationListFragment) fragment; 292 } 293 return null; 294 } 295 296 /** 297 * Get the conversation view fragment for this activity. If the conversation view fragment 298 * is not attached, this method returns null 299 * @return 300 */ 301 protected ConversationViewFragment getConversationViewFragment() { 302 final Fragment fragment = mFragmentManager.findFragmentByTag(TAG_CONVERSATION); 303 if (isValidFragment(fragment)) { 304 return (ConversationViewFragment) fragment; 305 } 306 return null; 307 } 308 309 /** 310 * Returns the folder list fragment attached with this activity. If no such fragment is attached 311 * this method returns null. 312 * @return 313 */ 314 protected FolderListFragment getFolderListFragment() { 315 final Fragment fragment = mFragmentManager.findFragmentByTag(TAG_FOLDER_LIST); 316 if (isValidFragment(fragment)) { 317 return (FolderListFragment) fragment; 318 } 319 return null; 320 } 321 322 /** 323 * Initialize the action bar. This is not visible to OnePaneController and 324 * TwoPaneController so they cannot override this behavior. 325 */ 326 private void initializeActionBar() { 327 final ActionBar actionBar = mActivity.getActionBar(); 328 mActionBarView = (ActionBarView) LayoutInflater.from(mContext).inflate( 329 R.layout.actionbar_view, null); 330 if (actionBar != null && mActionBarView != null) { 331 // Why have a different variable for the same thing? We should apply 332 // the same actions 333 // on mActionBarView instead. 334 mActionBarView.initialize(mActivity, this, mViewMode, actionBar, mRecentFolderList); 335 } 336 } 337 338 /** 339 * Attach the action bar to the activity. 340 */ 341 private void attachActionBar() { 342 final ActionBar actionBar = mActivity.getActionBar(); 343 if (actionBar != null && mActionBarView != null) { 344 actionBar.setCustomView(mActionBarView, new ActionBar.LayoutParams( 345 LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); 346 actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM, 347 ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_SHOW_TITLE); 348 mActionBarView.attach(); 349 } 350 mViewMode.addListener(mActionBarView); 351 } 352 353 /** 354 * Returns whether the conversation list fragment is visible or not. 355 * Different layouts will have their own notion on the visibility of 356 * fragments, so this method needs to be overriden. 357 * 358 * @return 359 */ 360 protected abstract boolean isConversationListVisible(); 361 362 /** 363 * Switch the current account to the one provided as an argument to the method. 364 * @param account 365 */ 366 private void switchAccount(Account account){ 367 // Current account is different from the new account, restart loaders and show 368 // the account Inbox. 369 mAccount = account; 370 LogUtils.d(LOG_TAG, "AbstractActivityController.switchAccount(): mAccount = %s", 371 mAccount.uri); 372 cancelRefreshTask(); 373 onSettingsChanged(mAccount.settings); 374 mActionBarView.setAccount(mAccount); 375 loadAccountInbox(); 376 377 mRecentFolderList.setCurrentAccount(account); 378 restartOptionalLoader(LOADER_RECENT_FOLDERS); 379 mActivity.invalidateOptionsMenu(); 380 disableNotificationsOnAccountChange(mAccount); 381 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR); 382 MailAppProvider.getInstance().setLastViewedAccount(mAccount.uri.toString()); 383 } 384 385 @Override 386 public void onAccountChanged(Account account) { 387 LogUtils.d(LOG_TAG, "onAccountChanged (%s) called.", account.uri); 388 final boolean accountChanged = (mAccount == null) || !account.uri.equals(mAccount.uri); 389 if (accountChanged) { 390 switchAccount(account); 391 return; 392 } 393 // Current account is the same as the new account, but the settings might be different. 394 if (!account.settings.equals(mAccount.settings)){ 395 onSettingsChanged(account.settings); 396 return; 397 } 398 } 399 400 /** 401 * Changes the settings for the current account. The new settings are provided as a parameter. 402 * @param settings 403 */ 404 public void onSettingsChanged(Settings settings) { 405 dispatchSettingsChange(settings); 406 resetActionBarIcon(); 407 mActivity.invalidateOptionsMenu(); 408 // If the user was viewing the default Inbox here, and the new setting contains a different 409 // default Inbox, we don't want to load a different folder here. 410 } 411 412 @Override 413 public Settings getSettings() { 414 return mAccount.settings; 415 } 416 417 /** 418 * Adds a listener interested in change in settings. If a class is storing a reference to 419 * Settings, it should listen on changes, so it can receive updates to settings. 420 * Must happen in the UI thread. 421 */ 422 public void addSettingsListener(Settings.ChangeListener listener) { 423 mSettingsListeners.add(listener); 424 } 425 426 /** 427 * Removes a listener from receiving settings changes. 428 * Must happen in the UI thread. 429 */ 430 public void removeSettingsListener(Settings.ChangeListener listener) { 431 mSettingsListeners.remove(listener); 432 } 433 434 /** 435 * Method that lets the settings listeners know when the settings got changed. 436 */ 437 private void dispatchSettingsChange(Settings updatedSettings) { 438 // Copy the list of current listeners so that 439 final ArrayList<Settings.ChangeListener> allListeners = 440 new ArrayList<Settings.ChangeListener>(mSettingsListeners); 441 for (Settings.ChangeListener listener : allListeners) { 442 if (listener != null) { 443 listener.onSettingsChanged(updatedSettings); 444 } 445 } 446 // And we know that the ConversationListFragment is interested in changes to settings, 447 // though it hasn't registered itself with us. 448 final ConversationListFragment convList = getConversationListFragment(); 449 if (convList != null) { 450 convList.onSettingsChanged(updatedSettings); 451 } 452 } 453 454 private void fetchSearchFolder(Intent intent) { 455 Bundle args = new Bundle(); 456 args.putString(ConversationListContext.EXTRA_SEARCH_QUERY, intent 457 .getStringExtra(ConversationListContext.EXTRA_SEARCH_QUERY)); 458 mActivity.getLoaderManager().restartLoader(LOADER_SEARCH, args, this); 459 } 460 461 @Override 462 public void onFolderChanged(Folder folder) { 463 if (folder != null && !folder.equals(mFolder)) { 464 setFolder(folder); 465 mConvListContext = ConversationListContext.forFolder(mContext, mAccount, mFolder); 466 showConversationList(mConvListContext); 467 468 // Add the folder that we were viewing to the recent folders list. 469 // TODO: this may need to be fine tuned. If this is the signal that is indicating that 470 // the list is shown to the user, this could fire in one pane if the user goes directly 471 // to a conversation 472 updateRecentFolderList(); 473 cancelRefreshTask(); 474 } 475 } 476 477 /** 478 * Update the recent folders. This only needs to be done once when accessing a new folder. 479 */ 480 private void updateRecentFolderList() { 481 if (mFolder != null) { 482 mRecentFolderList.touchFolder(mFolder, mAccount); 483 } 484 } 485 486 // TODO(mindyp): set this up to store a copy of the folder as a transient 487 // field in the account. 488 @Override 489 public void loadAccountInbox() { 490 restartOptionalLoader(LOADER_ACCOUNT_INBOX); 491 } 492 493 /** Set the current folder */ 494 private void setFolder(Folder folder) { 495 // Start watching folder for sync status. 496 if (folder != null && !folder.equals(mFolder)) { 497 LogUtils.d(LOG_TAG, "AbstractActivityController.setFolder(%s)", folder.name); 498 final boolean folderWasNull = (mFolder == null); 499 final LoaderManager lm = mActivity.getLoaderManager(); 500 mActionBarView.setRefreshInProgress(false); 501 mFolder = folder; 502 mActionBarView.setFolder(mFolder); 503 504 // Only when we switch from one folder to another do we want to restart the 505 // folder and conversation list loaders (to trigger onCreateLoader). 506 // The first time this runs when the activity is [re-]initialized, we want to re-use the 507 // previous loader's instance and data upon configuration change (e.g. rotation). 508 if (folderWasNull) { 509 lm.initLoader(LOADER_FOLDER_CURSOR, null, this); 510 lm.initLoader(LOADER_CONVERSATION_LIST, null, mListCursorCallbacks); 511 } else { 512 lm.restartLoader(LOADER_FOLDER_CURSOR, null, this); 513 lm.restartLoader(LOADER_CONVERSATION_LIST, null, mListCursorCallbacks); 514 } 515 } else if (folder == null) { 516 LogUtils.wtf(LOG_TAG, "Folder in setFolder is null"); 517 } 518 } 519 520 @Override 521 public void onActivityResult(int requestCode, int resultCode, Intent data) { 522 if (requestCode == ADD_ACCOUNT_REQUEST_CODE) { 523 // We were waiting for the user to create an account 524 if (resultCode == Activity.RESULT_OK) { 525 // restart the loader to get the updated list of accounts 526 mActivity.getLoaderManager().initLoader( 527 LOADER_ACCOUNT_CURSOR, null, this); 528 } else { 529 // The user failed to create an account, just exit the app 530 mActivity.finish(); 531 } 532 } 533 } 534 535 @Override 536 public void onConversationListVisibilityChanged(boolean visible) { 537 if (mConversationListCursor != null) { 538 // The conversation list is visible. 539 Utils.setConversationCursorVisibility(mConversationListCursor, visible); 540 } 541 } 542 543 /** 544 * By default, doing nothing is right. A two-pane controller will need to 545 * override this. 546 */ 547 @Override 548 public void onConversationVisibilityChanged(boolean visible) { 549 // Do nothing. 550 return; 551 } 552 553 @Override 554 public boolean onCreate(Bundle savedState) { 555 initializeActionBar(); 556 // Allow shortcut keys to function for the ActionBar and menus. 557 mActivity.setDefaultKeyMode(Activity.DEFAULT_KEYS_SHORTCUT); 558 mResolver = mActivity.getContentResolver(); 559 mNewEmailReceiver = new SuppressNotificationReceiver(); 560 561 // All the individual UI components listen for ViewMode changes. This 562 // simplifies the amount of logic in the AbstractActivityController, but increases the 563 // possibility of timing-related bugs. 564 mViewMode.addListener(this); 565 mPagerController = new ConversationPagerController(mActivity, this); 566 mUndoBarView = (UndoBarView) mActivity.findViewById(R.id.undo_view); 567 attachActionBar(); 568 569 final Intent intent = mActivity.getIntent(); 570 // Immediately handle a clean launch with intent, and any state restoration 571 // that does not rely on restored fragments or loader data 572 // any state restoration that relies on those can be done later in 573 // onRestoreInstanceState, once fragments are up and loader data is re-delivered 574 if (savedState != null) { 575 if (savedState.containsKey(SAVED_ACCOUNT)) { 576 setAccount((Account) savedState.getParcelable(SAVED_ACCOUNT)); 577 mActivity.invalidateOptionsMenu(); 578 } 579 if (savedState.containsKey(SAVED_FOLDER)) { 580 // Open the folder. 581 onFolderChanged((Folder) savedState.getParcelable(SAVED_FOLDER)); 582 } 583 } else if (intent != null) { 584 handleIntent(intent); 585 } 586 // Create the accounts loader; this loads the account switch spinner. 587 mActivity.getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this); 588 return true; 589 } 590 591 @Override 592 public Dialog onCreateDialog(int id, Bundle bundle) { 593 return null; 594 } 595 596 @Override 597 public boolean onCreateOptionsMenu(Menu menu) { 598 MenuInflater inflater = mActivity.getMenuInflater(); 599 inflater.inflate(mActionBarView.getOptionsMenuId(), menu); 600 mActionBarView.onCreateOptionsMenu(menu); 601 return true; 602 } 603 604 @Override 605 public boolean onKeyDown(int keyCode, KeyEvent event) { 606 // TODO(viki): Auto-generated method stub 607 return false; 608 } 609 610 @Override 611 public boolean onOptionsItemSelected(MenuItem item) { 612 final int id = item.getItemId(); 613 boolean handled = true; 614 switch (id) { 615 case android.R.id.home: 616 onUpPressed(); 617 break; 618 case R.id.compose: 619 ComposeActivity.compose(mActivity.getActivityContext(), mAccount); 620 break; 621 case R.id.show_all_folders: 622 showFolderList(); 623 break; 624 case R.id.refresh: 625 requestFolderRefresh(); 626 break; 627 case R.id.settings: 628 Utils.showSettings(mActivity.getActivityContext(), mAccount); 629 break; 630 case R.id.folder_options: 631 Utils.showFolderSettings(mActivity.getActivityContext(), mAccount, mFolder); 632 break; 633 case R.id.help_info_menu_item: 634 // TODO: enable context sensitive help 635 Utils.showHelp(mActivity.getActivityContext(), mAccount, null); 636 break; 637 case R.id.feedback_menu_item: 638 Utils.sendFeedback(mActivity.getActivityContext(), mAccount); 639 break; 640 case R.id.manage_folders_item: 641 Utils.showManageFolder(mActivity.getActivityContext(), mAccount); 642 break; 643 case R.id.change_folder: 644 new FoldersSelectionDialog(mActivity.getActivityContext(), mAccount, this, 645 Conversation.listOf(mCurrentConversation)).show(); 646 break; 647 default: 648 handled = false; 649 break; 650 } 651 return handled; 652 } 653 654 /** 655 * Update the specified column name in conversation for a boolean value. 656 * @param columnName 657 * @param value 658 */ 659 protected void updateCurrentConversation(String columnName, boolean value) { 660 mConversationListCursor.updateBoolean(mContext, Conversation.listOf(mCurrentConversation), 661 columnName, value); 662 refreshConversationList(); 663 } 664 665 /** 666 * Update the specified column name in conversation for an integer value. 667 * @param columnName 668 * @param value 669 */ 670 protected void updateCurrentConversation(String columnName, int value) { 671 mConversationListCursor.updateInt(mContext, Conversation.listOf(mCurrentConversation), 672 columnName, value); 673 refreshConversationList(); 674 } 675 676 protected void updateCurrentConversation(String columnName, String value) { 677 mConversationListCursor.updateString(mContext, Conversation.listOf(mCurrentConversation), 678 columnName, value); 679 refreshConversationList(); 680 } 681 682 private void requestFolderRefresh() { 683 if (mFolder != null) { 684 if (mAsyncRefreshTask != null) { 685 mAsyncRefreshTask.cancel(true); 686 } 687 mAsyncRefreshTask = new AsyncRefreshTask(mContext, mFolder); 688 mAsyncRefreshTask.execute(); 689 } 690 } 691 692 /** 693 * Confirm (based on user's settings) and delete a conversation from the conversation list and 694 * from the database. 695 * @param showDialog 696 * @param confirmResource 697 * @param action 698 */ 699 protected void confirmAndDelete(final Collection<Conversation> target, boolean showDialog, 700 int confirmResource, final DestructiveAction action) { 701 if (showDialog) { 702 final AlertDialog.OnClickListener onClick = new AlertDialog.OnClickListener() { 703 @Override 704 public void onClick(DialogInterface dialog, int which) { 705 requestDelete(target, action); 706 } 707 }; 708 final CharSequence message = Utils.formatPlural(mContext, confirmResource, 1); 709 new AlertDialog.Builder(mActivity.getActivityContext()).setMessage(message) 710 .setPositiveButton(R.string.ok, onClick) 711 .setNegativeButton(R.string.cancel, null) 712 .create().show(); 713 } else { 714 requestDelete(target, action); 715 } 716 } 717 718 /** 719 * Requests the removal of the current conversation with the specified destructive action. 720 * @param action 721 */ 722 protected void requestDelete(final Collection<Conversation> target, 723 final DestructiveAction action) { 724 // The conversation list handles deletion if it exists. 725 final ConversationListFragment convList = getConversationListFragment(); 726 if (convList != null) { 727 convList.requestDelete(target, action); 728 return; 729 } 730 // Update the conversation fragment if the current conversation is deleted. 731 if (getConversationViewFragment() != null && 732 !Conversation.contains(target, mCurrentConversation)) { 733 final Conversation next = mTracker.getNextConversation( 734 Settings.getAutoAdvanceSetting(mAccount.settings)); 735 if (next != null) { 736 showConversation(next); 737 // TODO(viki): Change showConversation to allow for null inputs. 738 } 739 } 740 // No visible UI element handled it on our behalf. Perform the action ourself. 741 action.performAction(); 742 } 743 744 /** 745 * Requests that the action be performed and the UI state is updated to reflect the new change. 746 * @param target 747 * @param action 748 */ 749 protected void requestUpdate(final Collection<Conversation> target, 750 final DestructiveAction action) { 751 action.performAction(); 752 refreshConversationList(); 753 } 754 755 @Override 756 public void onPrepareDialog(int id, Dialog dialog, Bundle bundle) { 757 // TODO(viki): Auto-generated method stub 758 759 } 760 761 @Override 762 public boolean onPrepareOptionsMenu(Menu menu) { 763 mActionBarView.onPrepareOptionsMenu(menu); 764 return true; 765 } 766 767 @Override 768 public void onPause() { 769 isLoaderInitialized = false; 770 771 enableNotifications(); 772 commitLeaveBehindItems(); 773 } 774 775 @Override 776 public void onResume() { 777 // Register the receiver that will prevent the status receiver from 778 // displaying its notification icon as long as we're running. 779 // The SupressNotificationReceiver will block the broadcast if we're looking at the folder 780 // that the notification was received for. 781 disableNotifications(); 782 783 if (mActionBarView != null) { 784 mActionBarView.onResume(); 785 } 786 787 } 788 789 @Override 790 public void onSaveInstanceState(Bundle outState) { 791 if (mAccount != null) { 792 LogUtils.d(LOG_TAG, "Saving the account now"); 793 outState.putParcelable(SAVED_ACCOUNT, mAccount); 794 } 795 if (mFolder != null) { 796 outState.putParcelable(SAVED_FOLDER, mFolder); 797 } 798 if (mCurrentConversation != null && mViewMode.getMode() == ViewMode.CONVERSATION) { 799 outState.putParcelable(SAVED_CONVERSATION, mCurrentConversation); 800 } 801 if (!mSelectedSet.isEmpty()) { 802 outState.putParcelable(SAVED_SELECTED_SET, mSelectedSet); 803 } 804 } 805 806 @Override 807 public void onSearchRequested(String query) { 808 Intent intent = new Intent(); 809 intent.setAction(Intent.ACTION_SEARCH); 810 intent.putExtra(ConversationListContext.EXTRA_SEARCH_QUERY, query); 811 intent.putExtra(Utils.EXTRA_ACCOUNT, mAccount); 812 intent.setComponent(mActivity.getComponentName()); 813 mActionBarView.collapseSearch(); 814 mActivity.startActivity(intent); 815 } 816 817 @Override 818 public void onStartDragMode() { 819 // TODO(viki): Auto-generated method stub 820 } 821 822 @Override 823 public void onStop() { 824 // TODO(viki): Auto-generated method stub 825 } 826 827 @Override 828 public void onStopDragMode() { 829 // TODO(viki): Auto-generated method stub 830 } 831 832 @Override 833 public void onDestroy() { 834 // unregister the ViewPager's observer on the conversation cursor 835 mPagerController.onDestroy(); 836 } 837 838 /** 839 * {@inheritDoc} Subclasses must override this to listen to mode changes 840 * from the ViewMode. Subclasses <b>must</b> call the parent's 841 * onViewModeChanged since the parent will handle common state changes. 842 */ 843 @Override 844 public void onViewModeChanged(int newMode) { 845 // Perform any mode specific work here. 846 // reset the action bar icon based on the mode. Why don't the individual 847 // controllers do 848 // this themselves? 849 850 // We don't want to invalidate the options menu when switching to 851 // conversation 852 // mode, as it will happen when the conversation finishes loading. 853 if (newMode != ViewMode.CONVERSATION) { 854 mActivity.invalidateOptionsMenu(); 855 } 856 } 857 858 protected void commitLeaveBehindItems() { 859 ConversationListFragment fragment = getConversationListFragment(); 860 if (fragment != null) { 861 fragment.commitLeaveBehindItems(); 862 } 863 } 864 865 @Override 866 public void onWindowFocusChanged(boolean hasFocus) { 867 ConversationListFragment convList = getConversationListFragment(); 868 if (hasFocus && convList != null && convList.isVisible()) { 869 // The conversation list is visible. 870 Utils.setConversationCursorVisibility(mConversationListCursor, true); 871 } 872 } 873 874 private void setAccount(Account account) { 875 mAccount = account; 876 LogUtils.d(LOG_TAG, "AbstractActivityController.setAccount(): mAccount = %s", mAccount.uri); 877 dispatchSettingsChange(mAccount.settings); 878 mActionBarView.setAccount(mAccount); 879 } 880 881 /** 882 * Restore the state from the previous bundle. Subclasses should call this 883 * method from the parent class, since it performs important UI 884 * initialization. 885 * 886 * @param savedState 887 */ 888 @Override 889 public void onRestoreInstanceState(Bundle savedState) { 890 LogUtils.d(LOG_TAG, "IN AAC.onRestoreInstanceState"); 891 if (savedState.containsKey(SAVED_CONVERSATION)) { 892 // Open the conversation. 893 final Conversation conversation = 894 (Conversation)savedState.getParcelable(SAVED_CONVERSATION); 895 if (conversation != null && conversation.position < 0) { 896 // Set the position to 0 on this conversation, as we don't know where it is 897 // in the list 898 conversation.position = 0; 899 } 900 setCurrentConversation(conversation); 901 showConversation(mCurrentConversation); 902 } 903 904 /** 905 * Restore the state of selected conversations. This needs to be done after the correct mode 906 * is set and the action bar is fully initialized. If not, several key pieces of state 907 * information will be missing, and the split views may not be initialized correctly. 908 * @param savedState 909 */ 910 restoreSelectedConversations(savedState); 911 } 912 913 private void handleIntent(Intent intent) { 914 boolean handled = false; 915 if (Intent.ACTION_VIEW.equals(intent.getAction())) { 916 if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) { 917 setAccount((Account) intent.getParcelableExtra(Utils.EXTRA_ACCOUNT)); 918 } else if (intent.hasExtra(Utils.EXTRA_ACCOUNT_STRING)) { 919 setAccount(Account.newinstance(intent 920 .getStringExtra(Utils.EXTRA_ACCOUNT_STRING))); 921 } 922 if (mAccount != null) { 923 mActivity.invalidateOptionsMenu(); 924 } 925 926 Folder folder = null; 927 if (intent.hasExtra(Utils.EXTRA_FOLDER)) { 928 // Open the folder. 929 LogUtils.d(LOG_TAG, "SHOW THE FOLDER at %s", 930 intent.getParcelableExtra(Utils.EXTRA_FOLDER)); 931 folder = (Folder) intent.getParcelableExtra(Utils.EXTRA_FOLDER); 932 933 } else if (intent.hasExtra(Utils.EXTRA_FOLDER_STRING)) { 934 // Open the folder. 935 folder = new Folder(intent.getStringExtra(Utils.EXTRA_FOLDER_STRING)); 936 } 937 if (folder != null) { 938 onFolderChanged(folder); 939 handled = true; 940 } 941 942 if (intent.hasExtra(Utils.EXTRA_CONVERSATION)) { 943 // Open the conversation. 944 LogUtils.d(LOG_TAG, "SHOW THE CONVERSATION at %s", 945 intent.getParcelableExtra(Utils.EXTRA_CONVERSATION)); 946 final Conversation conversation = 947 (Conversation)intent.getParcelableExtra(Utils.EXTRA_CONVERSATION); 948 if (conversation != null && conversation.position < 0) { 949 // Set the position to 0 on this conversation, as we don't know where it is 950 // in the list 951 conversation.position = 0; 952 } 953 setCurrentConversation(conversation); 954 showConversation(mCurrentConversation); 955 handled = true; 956 } 957 958 if (!handled) { 959 // Nothing was saved; just load the account inbox. 960 loadAccountInbox(); 961 } 962 } else if (Intent.ACTION_SEARCH.equals(intent.getAction())) { 963 if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) { 964 // Save this search query for future suggestions. 965 final String query = intent.getStringExtra(SearchManager.QUERY); 966 final String authority = mContext.getString(R.string.suggestions_authority); 967 SearchRecentSuggestions suggestions = new SearchRecentSuggestions( 968 mContext, authority, SuggestionsProvider.MODE); 969 suggestions.saveRecentQuery(query, null); 970 971 mViewMode.enterSearchResultsListMode(); 972 setAccount((Account) intent.getParcelableExtra(Utils.EXTRA_ACCOUNT)); 973 mActivity.invalidateOptionsMenu(); 974 restartOptionalLoader(LOADER_RECENT_FOLDERS); 975 mRecentFolderList.setCurrentAccount(mAccount); 976 fetchSearchFolder(intent); 977 } else { 978 LogUtils.e(LOG_TAG, "Missing account extra from search intent. Finishing"); 979 mActivity.finish(); 980 } 981 } 982 if (mAccount != null) { 983 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR); 984 } 985 } 986 987 /** 988 * Copy any selected conversations stored in the saved bundle into our selection set, 989 * triggering {@link ConversationSetObserver} callbacks as our selection set changes. 990 * 991 */ 992 private void restoreSelectedConversations(Bundle savedState) { 993 if (savedState == null) { 994 mSelectedSet.clear(); 995 return; 996 } 997 final ConversationSelectionSet selectedSet = savedState.getParcelable(SAVED_SELECTED_SET); 998 if (selectedSet == null || selectedSet.isEmpty()) { 999 mSelectedSet.clear(); 1000 return; 1001 } 1002 1003 // putAll will take care of calling our registered onSetPopulated method 1004 mSelectedSet.putAll(selectedSet); 1005 } 1006 1007 @Override 1008 public void setSubject(String subject) { 1009 // Do something useful with the subject. This requires changing the 1010 // conversation view's subject text. 1011 } 1012 1013 /** 1014 * Children can override this method, but they must call super.showConversation(). 1015 * {@inheritDoc} 1016 */ 1017 @Override 1018 public void showConversation(Conversation conversation) { 1019 // Set the current conversation just in case it wasn't already set. 1020 setCurrentConversation(conversation); 1021 } 1022 1023 /** 1024 * Children can override this method, but they must call super.showWaitForInitialization(). 1025 * {@inheritDoc} 1026 */ 1027 @Override 1028 public void showWaitForInitialization() { 1029 mViewMode.enterWaitingForInitializationMode(); 1030 } 1031 1032 @Override 1033 public void hideWaitForInitialization() { 1034 } 1035 1036 @Override 1037 public void updateWaitMode() { 1038 final FragmentManager manager = mActivity.getFragmentManager(); 1039 final WaitFragment waitFragment = 1040 (WaitFragment)manager.findFragmentByTag(TAG_WAIT); 1041 if (waitFragment != null) { 1042 waitFragment.updateAccount(mAccount); 1043 } 1044 } 1045 1046 @Override 1047 public boolean inWaitMode() { 1048 final FragmentManager manager = mActivity.getFragmentManager(); 1049 final WaitFragment waitFragment = 1050 (WaitFragment)manager.findFragmentByTag(TAG_WAIT); 1051 if (waitFragment != null) { 1052 final Account fragmentAccount = waitFragment.getAccount(); 1053 return fragmentAccount.uri.equals(mAccount.uri) && 1054 mViewMode.getMode() == ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION; 1055 } 1056 return false; 1057 } 1058 1059 /** 1060 * Children can override this method, but they must call super.showConversationList(). 1061 * {@inheritDoc} 1062 */ 1063 @Override 1064 public void showConversationList(ConversationListContext listContext) { 1065 } 1066 1067 @Override 1068 public void onConversationSelected(Conversation conversation) { 1069 showConversation(conversation); 1070 if (mConvListContext != null && mConvListContext.isSearchResult()) { 1071 mViewMode.enterSearchResultsConversationMode(); 1072 } else { 1073 mViewMode.enterConversationMode(); 1074 } 1075 } 1076 1077 /** 1078 * Set the current conversation. This is the conversation on which all actions are performed. 1079 * Do not modify mCurrentConversation except through this method, which makes it easy to 1080 * perform common actions associated with changing the current conversation. 1081 * @param conversation 1082 */ 1083 @Override 1084 public void setCurrentConversation(Conversation conversation) { 1085 mCurrentConversation = conversation; 1086 mTracker.initialize(mCurrentConversation); 1087 } 1088 1089 /** 1090 * {@inheritDoc} 1091 */ 1092 @Override 1093 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 1094 // Create a loader to listen in on account changes. 1095 switch (id) { 1096 case LOADER_ACCOUNT_CURSOR: 1097 return new CursorLoader(mContext, MailAppProvider.getAccountsUri(), 1098 UIProvider.ACCOUNTS_PROJECTION, null, null, null); 1099 case LOADER_FOLDER_CURSOR: 1100 final CursorLoader loader = new CursorLoader(mContext, mFolder.uri, 1101 UIProvider.FOLDERS_PROJECTION, null, null, null); 1102 loader.setUpdateThrottle(mFolderItemUpdateDelayMs); 1103 return loader; 1104 case LOADER_RECENT_FOLDERS: 1105 if (mAccount != null && mAccount.recentFolderListUri != null) { 1106 return new CursorLoader(mContext, mAccount.recentFolderListUri, 1107 UIProvider.FOLDERS_PROJECTION, null, null, null); 1108 } 1109 break; 1110 case LOADER_ACCOUNT_INBOX: 1111 final Uri defaultInbox = Settings.getDefaultInboxUri(mAccount.settings); 1112 final Uri inboxUri = defaultInbox.equals(Uri.EMPTY) ? 1113 mAccount.folderListUri : defaultInbox; 1114 LogUtils.d(LOG_TAG, "Loading the default inbox: %s", inboxUri); 1115 if (inboxUri != null) { 1116 return new CursorLoader(mContext, inboxUri, UIProvider.FOLDERS_PROJECTION, null, 1117 null, null); 1118 } 1119 break; 1120 case LOADER_SEARCH: 1121 return Folder.forSearchResults(mAccount, 1122 args.getString(ConversationListContext.EXTRA_SEARCH_QUERY), 1123 mActivity.getActivityContext()); 1124 case LOADER_ACCOUNT_UPDATE_CURSOR: 1125 return new CursorLoader(mContext, mAccount.uri, UIProvider.ACCOUNTS_PROJECTION, 1126 null, null, null); 1127 default: 1128 LogUtils.wtf(LOG_TAG, "Loader returned unexpected id: %d", id); 1129 } 1130 return null; 1131 } 1132 1133 @Override 1134 public void onLoaderReset(Loader<Cursor> loader) { 1135 1136 } 1137 1138 /** 1139 * {@link LoaderManager} currently has a bug in 1140 * {@link LoaderManager#restartLoader(int, Bundle, android.app.LoaderManager.LoaderCallbacks)} 1141 * where, if a previous onCreateLoader returned a null loader, this method will NPE. Work around 1142 * this bug by destroying any loaders that may have been created as null (essentially because 1143 * they are optional loads, and may not apply to a particular account). 1144 * <p> 1145 * A simple null check before restarting a loader will not work, because that would not 1146 * give the controller a chance to invalidate UI corresponding the prior loader result. 1147 * 1148 * @param id loader ID to safely restart 1149 */ 1150 private void restartOptionalLoader(int id) { 1151 final LoaderManager lm = mActivity.getLoaderManager(); 1152 lm.destroyLoader(id); 1153 lm.restartLoader(id, Bundle.EMPTY, this); 1154 } 1155 1156 /** 1157 * Start a loader with the given id. This should be called when we know that the previous 1158 * state of the application matches this state, and we are happy if we get the previously 1159 * created loader with this id. If that is not true, consider calling 1160 * {@link #restartOptionalLoader(int)} instead. 1161 * @param id 1162 */ 1163 private void startLoader(int id) { 1164 final LoaderManager lm = mActivity.getLoaderManager(); 1165 lm.initLoader(id, Bundle.EMPTY, this); 1166 } 1167 1168 @Override 1169 public void registerConversationListObserver(DataSetObserver observer) { 1170 mConversationListObservable.registerObserver(observer); 1171 } 1172 1173 @Override 1174 public void unregisterConversationListObserver(DataSetObserver observer) { 1175 mConversationListObservable.unregisterObserver(observer); 1176 } 1177 1178 private boolean accountsUpdated(Cursor accountCursor) { 1179 // Check to see if the current account hasn't been set, or the account cursor is empty 1180 if (mAccount == null || !accountCursor.moveToFirst()) { 1181 return true; 1182 } 1183 1184 // Check to see if the number of accounts are different, from the number we saw on the last 1185 // updated 1186 if (mCurrentAccountUris.size() != accountCursor.getCount()) { 1187 return true; 1188 } 1189 1190 // Check to see if the account list is different or if the current account is not found in 1191 // the cursor. 1192 boolean foundCurrentAccount = false; 1193 do { 1194 final Uri accountUri = 1195 Uri.parse(accountCursor.getString(UIProvider.ACCOUNT_URI_COLUMN)); 1196 if (!foundCurrentAccount && mAccount.uri.equals(accountUri)) { 1197 foundCurrentAccount = true; 1198 } 1199 1200 if (!mCurrentAccountUris.contains(accountUri)) { 1201 return true; 1202 } 1203 } while (accountCursor.moveToNext()); 1204 1205 // As long as we found the current account, the list hasn't been updated 1206 return !foundCurrentAccount; 1207 } 1208 1209 /** 1210 * Update the accounts on the device. This currently loads the first account 1211 * in the list. 1212 * 1213 * @param loader 1214 * @param accounts cursor into the AccountCache 1215 * @return true if the update was successful, false otherwise 1216 */ 1217 private boolean updateAccounts(Loader<Cursor> loader, Cursor accounts) { 1218 if (accounts == null || !accounts.moveToFirst()) { 1219 return false; 1220 } 1221 1222 final Account[] allAccounts = Account.getAllAccounts(accounts); 1223 1224 // Save the uris for the accounts 1225 mCurrentAccountUris.clear(); 1226 for (Account account : allAccounts) { 1227 mCurrentAccountUris.add(account.uri); 1228 } 1229 1230 // 1. current account is already set and is in allAccounts -> no-op 1231 // 2. current account is set and is not in allAccounts -> pick first (acct was deleted?) 1232 // 3. saved pref has an account -> pick that one 1233 // 4. otherwise just pick first 1234 1235 Account newAccount = null; 1236 1237 if (mAccount != null) { 1238 if (!mCurrentAccountUris.contains(mAccount.uri)) { 1239 newAccount = allAccounts[0]; 1240 } else { 1241 newAccount = mAccount; 1242 } 1243 } else { 1244 final String lastAccountUri = MailAppProvider.getInstance().getLastViewedAccount(); 1245 if (lastAccountUri != null) { 1246 for (int i = 0; i < allAccounts.length; i++) { 1247 final Account acct = allAccounts[i]; 1248 if (lastAccountUri.equals(acct.uri.toString())) { 1249 newAccount = acct; 1250 break; 1251 } 1252 } 1253 } 1254 if (newAccount == null) { 1255 newAccount = allAccounts[0]; 1256 } 1257 } 1258 1259 onAccountChanged(newAccount); 1260 mActionBarView.setAccounts(allAccounts); 1261 return (allAccounts.length > 0); 1262 } 1263 1264 private void disableNotifications() { 1265 mNewEmailReceiver.activate(mContext, this); 1266 } 1267 1268 private void enableNotifications() { 1269 mNewEmailReceiver.deactivate(); 1270 } 1271 1272 private void disableNotificationsOnAccountChange(Account account) { 1273 // If the new mail suppression receiver is activated for a different account, we want to 1274 // activate it for the new account. 1275 if (mNewEmailReceiver.activated() && 1276 !mNewEmailReceiver.notificationsDisabledForAccount(account)) { 1277 // Deactivate the current receiver, otherwise multiple receivers may be registered. 1278 mNewEmailReceiver.deactivate(); 1279 mNewEmailReceiver.activate(mContext, this); 1280 } 1281 } 1282 1283 /** 1284 * {@inheritDoc} 1285 */ 1286 @Override 1287 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 1288 // We want to reinitialize only if we haven't ever been initialized, or 1289 // if the current account has vanished. 1290 if (data == null) { 1291 LogUtils.e(LOG_TAG, "Received null cursor from loader id: %d", loader.getId()); 1292 } 1293 switch (loader.getId()) { 1294 case LOADER_ACCOUNT_CURSOR: 1295 // If the account list is not not null, and the account list cursor is empty, 1296 // we need to start the specified activity. 1297 if (data != null && data.getCount() == 0) { 1298 // If an empty cursor is returned, the MailAppProvider is indicating that 1299 // no accounts have been specified. We want to navigate to the "add account" 1300 // activity that will handle the intent returned by the MailAppProvider 1301 1302 // If the MailAppProvider believes that all accounts have been loaded, and the 1303 // account list is still empty, we want to prompt the user to add an account 1304 final Bundle extras = data.getExtras(); 1305 final boolean accountsLoaded = 1306 extras.getInt(AccountCursorExtraKeys.ACCOUNTS_LOADED) != 0; 1307 1308 if (accountsLoaded) { 1309 final Intent noAccountIntent = MailAppProvider.getNoAccountIntent(mContext); 1310 if (noAccountIntent != null) { 1311 mActivity.startActivityForResult(noAccountIntent, 1312 ADD_ACCOUNT_REQUEST_CODE); 1313 } 1314 } 1315 } else { 1316 final boolean accountListUpdated = accountsUpdated(data); 1317 if (!isLoaderInitialized || accountListUpdated) { 1318 isLoaderInitialized = updateAccounts(loader, data); 1319 } 1320 } 1321 break; 1322 case LOADER_ACCOUNT_UPDATE_CURSOR: 1323 // We have gotten an update for current account. 1324 1325 // Make sure that this is an update for what is the current account 1326 if (data != null && data.moveToFirst()) { 1327 final Account updatedAccount = new Account(data); 1328 1329 if (updatedAccount.uri.equals(mAccount.uri)) { 1330 // Update the controller's reference to the current account 1331 mAccount = updatedAccount; 1332 LogUtils.d(LOG_TAG, "AbstractActivityController.onLoadFinished(): " 1333 + "mAccount = %s", mAccount.uri); 1334 dispatchSettingsChange(mAccount.settings); 1335 1336 // Got an update for the current account 1337 final boolean inWaitingMode = inWaitMode(); 1338 if (!updatedAccount.isAccountIntialized() && !inWaitingMode) { 1339 // Transition to waiting mode 1340 showWaitForInitialization(); 1341 } else if (updatedAccount.isAccountIntialized() && inWaitingMode) { 1342 // Dismiss waiting mode 1343 hideWaitForInitialization(); 1344 } else if (!updatedAccount.isAccountIntialized() && inWaitingMode) { 1345 // Update the WaitFragment's account object 1346 updateWaitMode(); 1347 } 1348 } else { 1349 LogUtils.e(LOG_TAG, "Got update for account: %s with current account: %s", 1350 updatedAccount.uri, mAccount.uri); 1351 // We need to restart the loader, so the correct account information will 1352 // be returned 1353 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR); 1354 } 1355 } 1356 break; 1357 case LOADER_FOLDER_CURSOR: 1358 // Check status of the cursor. 1359 if (data != null && data.moveToFirst()) { 1360 Folder folder = new Folder(data); 1361 if (folder.isSyncInProgress()) { 1362 mActionBarView.onRefreshStarted(); 1363 } else { 1364 // Stop the spinner here. 1365 mActionBarView.onRefreshStopped(folder.lastSyncResult); 1366 } 1367 mActionBarView.onFolderUpdated(folder); 1368 final ConversationListFragment convList = getConversationListFragment(); 1369 if (convList != null) { 1370 convList.onFolderUpdated(folder); 1371 } 1372 LogUtils.d(LOG_TAG, "FOLDER STATUS = %d", folder.syncStatus); 1373 } else { 1374 LogUtils.d(LOG_TAG, "Unable to get the folder %s", 1375 mFolder != null ? mAccount.name : ""); 1376 } 1377 break; 1378 case LOADER_RECENT_FOLDERS: 1379 mRecentFolderList.loadFromUiProvider(data); 1380 mActionBarView.requestRecentFoldersAndRedraw(); 1381 break; 1382 case LOADER_ACCOUNT_INBOX: 1383 if (data != null && !data.isClosed() && data.moveToFirst()) { 1384 Folder inbox = new Folder(data); 1385 onFolderChanged(inbox); 1386 // Just want to get the inbox, don't care about updates to it 1387 // as this will be tracked by the folder change listener. 1388 mActivity.getLoaderManager().destroyLoader(LOADER_ACCOUNT_INBOX); 1389 } else { 1390 LogUtils.d(LOG_TAG, "Unable to get the account inbox for account %s", 1391 mAccount != null ? mAccount.name : ""); 1392 } 1393 break; 1394 case LOADER_SEARCH: 1395 data.moveToFirst(); 1396 Folder search = new Folder(data); 1397 setFolder(search); 1398 mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder, 1399 mActivity.getIntent() 1400 .getStringExtra(UIProvider.SearchQueryParameters.QUERY)); 1401 showConversationList(mConvListContext); 1402 mActivity.invalidateOptionsMenu(); 1403 mActivity.getLoaderManager().destroyLoader(LOADER_SEARCH); 1404 break; 1405 } 1406 } 1407 1408 /** 1409 * Destructive actions on Conversations. This class should only be created by controllers, and 1410 * clients should only require {@link DestructiveAction}s, not specific implementations of the. 1411 * Only the controllers should know what kind of destructive actions are being created. 1412 * 1413 * These Destructive Actions should not be used by themselves. These must be used by 1414 * classes that update the UI state like {@link OnePaneController.OnePaneDestructiveAction}, 1415 * {@link TwoPaneController.TwoPaneDestructiveAction} and others. 1416 */ 1417 protected class ConversationAction implements DestructiveAction { 1418 /** 1419 * The action to be performed. This is specified as the resource ID of the menu item 1420 * corresponding to this action: R.id.delete, R.id.report_spam, etc. 1421 */ 1422 private final int mAction; 1423 /** The action will act upon these conversations */ 1424 private final Collection<Conversation> mTarget = new ArrayList<Conversation>(); 1425 /** Whether this destructive action has already been performed */ 1426 private boolean mCompleted; 1427 /** Whether this is an action on the currently selected set. */ 1428 private final boolean mIsSelectedSet; 1429 1430 /** 1431 * Create a listener object. action is one of four constants: R.id.y_button (archive), 1432 * R.id.delete , R.id.mute, and R.id.report_spam. 1433 * @param action 1434 * @param target Conversation that we want to apply the action to. 1435 */ 1436 public ConversationAction(int action, Collection<Conversation> target) { 1437 this(action, target, false); 1438 } 1439 /** 1440 * Create a listener object. action is one of four constants: R.id.y_button (archive), 1441 * R.id.delete , R.id.mute, and R.id.report_spam. 1442 * @param action 1443 * @param target Conversation that we want to apply the action to. 1444 * @param isBatch whether the conversations are in the currently selected batch set. 1445 */ 1446 public ConversationAction(int action, Collection<Conversation> target, boolean isBatch) { 1447 mAction = action; 1448 mTarget.addAll(target); 1449 mIsSelectedSet = isBatch; 1450 } 1451 1452 /** 1453 * The action common to child classes. This performs the action specified in the constructor 1454 * on the conversations given here. 1455 */ 1456 @Override 1457 public void performAction() { 1458 if (isPerformed()) { 1459 return; 1460 } 1461 LogUtils.d(LOG_TAG, "Target is: %s", mTarget); 1462 switch (mAction) { 1463 case R.id.archive: 1464 LogUtils.d(LOG_TAG, "Archiving: %s", mTarget); 1465 mConversationListCursor.archive(mContext, mTarget); 1466 break; 1467 case R.id.delete: 1468 LogUtils.d(LOG_TAG, "Deleting: %s", mTarget); 1469 mConversationListCursor.delete(mContext, mTarget); 1470 break; 1471 case R.id.mute: 1472 LogUtils.d(LOG_TAG, "Muting: %s", mTarget); 1473 if (mFolder.supportsCapability(FolderCapabilities.DESTRUCTIVE_MUTE)) { 1474 for (Conversation c : mTarget) { 1475 c.localDeleteOnUpdate = true; 1476 } 1477 } 1478 mConversationListCursor.mute(mContext, mTarget); 1479 break; 1480 case R.id.report_spam: 1481 LogUtils.d(LOG_TAG, "Reporting spam: %s", mTarget); 1482 mConversationListCursor.reportSpam(mContext, mTarget); 1483 break; 1484 case R.id.remove_star: 1485 LogUtils.d(LOG_TAG, "Removing star: %s", mTarget); 1486 // Star removal is destructive in the Starred folder. 1487 mConversationListCursor.updateBoolean(mContext, mTarget, 1488 ConversationColumns.STARRED, false); 1489 break; 1490 case R.id.mark_not_important: 1491 LogUtils.d(LOG_TAG, "Marking not-important: %s", mTarget); 1492 // Marking not important is destructive in a mailbox containing only important 1493 // messages 1494 mConversationListCursor.updateInt(mContext, mTarget, 1495 ConversationColumns.PRIORITY, UIProvider.ConversationPriority.LOW); 1496 break; 1497 } 1498 refreshConversationList(); 1499 if (mIsSelectedSet) { 1500 onUndoAvailable(new UndoOperation(mTarget.size(), mAction)); 1501 mSelectedSet.clear(); 1502 } 1503 } 1504 1505 /** 1506 * Returns true if this action has been performed, false otherwise. 1507 * @return 1508 */ 1509 private synchronized boolean isPerformed() { 1510 if (mCompleted) { 1511 return true; 1512 } 1513 mCompleted = true; 1514 return false; 1515 } 1516 } 1517 1518 // Called from the FolderSelectionDialog after a user is done selecting folders to assign the 1519 // conversations to. 1520 @Override 1521 public final void onFolderChangesCommit( 1522 Collection<Folder> folders, Collection<Conversation> target) { 1523 final boolean isDestructive = !Folder.contains(folders, mFolder); 1524 LogUtils.d(LOG_TAG, "onFolderChangesCommit: isDestructive = %b", isDestructive); 1525 if (isDestructive) { 1526 for (final Conversation c : target) { 1527 c.localDeleteOnUpdate = true; 1528 } 1529 } 1530 final DestructiveAction folderChange = getFolderChange(target, folders, isDestructive); 1531 // Update the UI elements depending no their visibility and availability 1532 // TODO(viki): Consolidate this into requestDelete. 1533 if (isDestructive) { 1534 requestDelete(target, folderChange); 1535 } else { 1536 requestUpdate(target, folderChange); 1537 } 1538 } 1539 1540 @Override 1541 public void onRefreshRequired() { 1542 if (mIsConversationListScrolling) { 1543 LogUtils.d(LOG_TAG, "onRefreshRequired: delay until scrolling done"); 1544 return; 1545 } 1546 // Refresh the query in the background 1547 long now = System.currentTimeMillis(); 1548 long sinceLastRefresh = now - mConversationListRefreshTime; 1549// if (sinceLastRefresh > CONVERSATION_LIST_THROTTLE_MS) { 1550 if (mConversationListCursor.isRefreshRequired()) { 1551 mConversationListCursor.refresh(); 1552 mTracker.updateCursor(mConversationListCursor); 1553 mConversationListRefreshTime = now; 1554 } 1555// } else { 1556// long delay = CONVERSATION_LIST_THROTTLE_MS - sinceLastRefresh; 1557// LogUtils.d(LOG_TAG, "onRefreshRequired: delay for %sms", delay); 1558// mConversationListRefreshTask = new RefreshTimerTask(this, mHandler); 1559// mConversationListTimer.schedule(mConversationListRefreshTask, delay); 1560// } 1561 } 1562 1563 /** 1564 * Called when the {@link ConversationCursor} is changed or has new data in it. 1565 * <p> 1566 * {@inheritDoc} 1567 */ 1568 @Override 1569 public void onRefreshReady() { 1570 if (!mIsConversationListScrolling) { 1571 // Swap cursors 1572 mConversationListCursor.sync(); 1573 } 1574 mTracker.updateCursor(mConversationListCursor); 1575 } 1576 1577 @Override 1578 public void onDataSetChanged() { 1579 updateConversationListFragment(); 1580 mConversationListObservable.notifyChanged(); 1581 } 1582 1583 private void updateConversationListFragment() { 1584 final ConversationListFragment convList = getConversationListFragment(); 1585 if (convList != null) { 1586 refreshConversationList(); 1587 if (convList.isVisible()) { 1588 Utils.setConversationCursorVisibility(mConversationListCursor, true); 1589 } 1590 } 1591 } 1592 1593 /** 1594 * This class handles throttled refresh of the conversation list 1595 */ 1596 static class RefreshTimerTask extends TimerTask { 1597 final Handler mHandler; 1598 final AbstractActivityController mController; 1599 1600 RefreshTimerTask(AbstractActivityController controller, Handler handler) { 1601 mHandler = handler; 1602 mController = controller; 1603 } 1604 1605 @Override 1606 public void run() { 1607 mHandler.post(new Runnable() { 1608 @Override 1609 public void run() { 1610 LogUtils.d(LOG_TAG, "Delay done... calling onRefreshRequired"); 1611 mController.onRefreshRequired(); 1612 }}); 1613 } 1614 } 1615 1616 /** 1617 * Cancel the refresh task, if it's running 1618 */ 1619 private void cancelRefreshTask () { 1620 if (mConversationListRefreshTask != null) { 1621 mConversationListRefreshTask.cancel(); 1622 mConversationListRefreshTask = null; 1623 } 1624 } 1625 1626 @Override 1627 public void onScrollStateChanged(AbsListView view, int scrollState) { 1628 boolean isScrolling = (scrollState != OnScrollListener.SCROLL_STATE_IDLE); 1629 if (!isScrolling) { 1630 if (mConversationListCursor.isRefreshRequired()) { 1631 LogUtils.d(LOG_TAG, "Stop scrolling: refresh"); 1632 mConversationListCursor.refresh(); 1633 } else if (mConversationListCursor.isRefreshReady()) { 1634 LogUtils.d(LOG_TAG, "Stop scrolling: try sync"); 1635 onRefreshReady(); 1636 } 1637 } 1638 mIsConversationListScrolling = isScrolling; 1639 } 1640 1641 @Override 1642 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 1643 int totalItemCount) { 1644 } 1645 1646 @Override 1647 public void onSetEmpty() { 1648 } 1649 1650 @Override 1651 public void onSetPopulated(ConversationSelectionSet set) { 1652 final ConversationListFragment convList = getConversationListFragment(); 1653 if (convList == null) { 1654 return; 1655 } 1656 mCabActionMenu = new SelectedConversationsActionMenu(mActivity, set, 1657 convList.getAnimatedAdapter(), this, 1658 mAccount, mFolder, (SwipeableListView) convList.getListView()); 1659 enableCabMode(); 1660 } 1661 1662 @Override 1663 public void onSetChanged(ConversationSelectionSet set) { 1664 // Do nothing. We don't care about changes to the set. 1665 } 1666 1667 @Override 1668 public ConversationSelectionSet getSelectedSet() { 1669 return mSelectedSet; 1670 } 1671 1672 /** 1673 * Disable the Contextual Action Bar (CAB). The selected set is not changed. 1674 */ 1675 protected void disableCabMode() { 1676 if (mCabActionMenu != null) { 1677 mCabActionMenu.deactivate(); 1678 } 1679 } 1680 1681 /** 1682 * Re-enable the CAB menu if required. The selection set is not changed. 1683 */ 1684 protected void enableCabMode() { 1685 if (mCabActionMenu != null) { 1686 mCabActionMenu.activate(); 1687 } 1688 } 1689 1690 @Override 1691 public void startSearch() { 1692 if (mAccount == null) { 1693 // We cannot search if there is no account. Drop the request to the floor. 1694 LogUtils.d(LOG_TAG, "AbstractActivityController.startSearch(): null account"); 1695 return; 1696 } 1697 if (mAccount.supportsCapability(UIProvider.AccountCapabilities.LOCAL_SEARCH) 1698 | mAccount.supportsCapability(UIProvider.AccountCapabilities.SERVER_SEARCH)) { 1699 onSearchRequested(mActionBarView.getQuery()); 1700 } else { 1701 Toast.makeText(mActivity.getActivityContext(), mActivity.getActivityContext() 1702 .getString(R.string.search_unsupported), Toast.LENGTH_SHORT).show(); 1703 } 1704 } 1705 1706 @Override 1707 public void exitSearchMode() { 1708 if (mViewMode.getMode() == ViewMode.SEARCH_RESULTS_LIST) { 1709 mActivity.finish(); 1710 } 1711 } 1712 1713 /** 1714 * Supports dragging conversations to a folder. 1715 */ 1716 @Override 1717 public boolean supportsDrag(DragEvent event, Folder folder) { 1718 return (folder != null 1719 && event != null 1720 && event.getClipDescription() != null 1721 && folder.supportsCapability 1722 (UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES) 1723 && folder.supportsCapability 1724 (UIProvider.FolderCapabilities.CAN_HOLD_MAIL) 1725 && !mFolder.uri.equals(folder.uri)); 1726 } 1727 1728 /** 1729 * Handles dropping conversations to a label. 1730 */ 1731 @Override 1732 public void handleDrop(DragEvent event, final Folder folder) { 1733 if (!supportsDrag(event, folder)) { 1734 return; 1735 } 1736 final Collection<Conversation> conversations = mSelectedSet.values(); 1737 final Collection<Folder> dropTarget = Folder.listOf(folder); 1738 // Drag and drop is destructive: we remove conversations from the current folder. 1739 final DestructiveAction action = getFolderChange(conversations, dropTarget, true); 1740 requestDelete(conversations, action); 1741 } 1742 1743 @Override 1744 public void onUndoCancel() { 1745 mUndoBarView.hide(false); 1746 } 1747 1748 1749 @Override 1750 public void onTouchEvent(MotionEvent event) { 1751 if (event.getAction() == MotionEvent.ACTION_DOWN) { 1752 if (mUndoBarView != null && !mUndoBarView.isEventInUndo(event)) { 1753 mUndoBarView.hide(true); 1754 } 1755 } 1756 } 1757 1758 @Override 1759 public void onConversationSeen(Conversation conv) { 1760 mPagerController.onConversationSeen(conv); 1761 } 1762 1763 private class ConversationListLoaderCallbacks implements 1764 LoaderManager.LoaderCallbacks<ConversationCursor> { 1765 1766 @Override 1767 public Loader<ConversationCursor> onCreateLoader(int id, Bundle args) { 1768 Loader<ConversationCursor> result = new ConversationCursorLoader((Activity) mActivity, 1769 mAccount, mFolder.conversationListUri, mFolder.name); 1770 return result; 1771 } 1772 1773 @Override 1774 public void onLoadFinished(Loader<ConversationCursor> loader, ConversationCursor data) { 1775 LogUtils.d(LOG_TAG, "IN AAC.ConversationCursor.onLoadFinished, data=%s loader=%s", 1776 data, loader); 1777 // Clear our all pending destructive actions before swapping the conversation cursor 1778 destroyPending(null); 1779 mConversationListCursor = data; 1780 mConversationListCursor.addListener(AbstractActivityController.this); 1781 1782 mConversationListObservable.notifyChanged(); 1783 1784 // Register the AbstractActivityController as a listener to changes in 1785 // data in the cursor. 1786 final ConversationListFragment convList = getConversationListFragment(); 1787 if (convList != null) { 1788 convList.onCursorUpdated(); 1789 convList.getListView().setOnScrollListener(AbstractActivityController.this); 1790 1791 if (convList.isVisible()) { 1792 // The conversation list is visible. 1793 Utils.setConversationCursorVisibility(mConversationListCursor, true); 1794 } 1795 } 1796 // Shown for search results in two-pane mode only. 1797 if (shouldShowFirstConversation()) { 1798 if (mConversationListCursor.getCount() > 0) { 1799 mConversationListCursor.moveToPosition(0); 1800 if (convList != null) { 1801 convList.getListView().setItemChecked(0, true); 1802 } 1803 final Conversation conv = new Conversation(mConversationListCursor); 1804 conv.position = 0; 1805 onConversationSelected(conv); 1806 } 1807 } 1808 } 1809 1810 @Override 1811 public void onLoaderReset(Loader<ConversationCursor> loader) { 1812 final ConversationListFragment convList = getConversationListFragment(); 1813 if (convList == null) { 1814 return; 1815 } 1816 convList.onCursorUpdated(); 1817 } 1818 } 1819 1820 @Override 1821 public void sendConversationRead(String toFragment, Conversation conversation, boolean state, 1822 boolean local) { 1823 if (toFragment.equals(TAG_CONVERSATION_LIST)) { 1824 if (mConversationListCursor != null) { 1825 if (local) { 1826 mConversationListCursor.setConversationColumn(conversation.uri.toString(), ConversationColumns.READ, 1827 state); 1828 } else { 1829 mConversationListCursor.markRead(mContext, state, conversation); 1830 } 1831 } 1832 } else if (toFragment.equals(TAG_CONVERSATION)) { 1833 // TODO Handle setting read in conversation view 1834 } 1835 } 1836 1837 @Override 1838 public void sendConversationUriStarred(String toFragment, String conversationUri, 1839 boolean state, boolean local) { 1840 if (toFragment.equals(TAG_CONVERSATION_LIST)) { 1841 if (mConversationListCursor != null) { 1842 if (local) { 1843 mConversationListCursor.setConversationColumn(conversationUri, ConversationColumns.STARRED, state); 1844 } else { 1845 mConversationListCursor.updateBoolean(mContext, conversationUri, ConversationColumns.STARRED, state); 1846 } 1847 } 1848 } else if (toFragment.equals(TAG_CONVERSATION)) { 1849 // TODO Handle setting starred in conversation view 1850 } 1851 } 1852 1853 /** 1854 * Destroy the pending {@link DestructiveAction} till now and assign the given action as the 1855 * next destructive action.. 1856 * @param nextAction the next destructive action to be performed. This can be null. 1857 */ 1858 private final void destroyPending(DestructiveAction nextAction) { 1859 // If there is a pending action, perform that first. 1860 if (mPendingDestruction != null) { 1861 mPendingDestruction.performAction(); 1862 } 1863 mPendingDestruction = nextAction; 1864 } 1865 1866 /** 1867 * Register a destructive action with the controller. This performs the previous destructive 1868 * action as a side effect. This method is final because we don't want the child classes to 1869 * embellish this method any more. This is a temporary workaround to reduce the number of 1870 * {@link DestructiveAction} classes. Please do not copy this paradigm. 1871 * @param action 1872 */ 1873 protected final void registerDestructiveAction(DestructiveAction action) { 1874 // TODO(viki): This is not a good idea. The best solution is for clients to request a 1875 // destructive action from the controller and for the controller to own the action. This is 1876 // a half-way solution while refactoring DestructiveAction. 1877 destroyPending(action); 1878 return; 1879 } 1880 1881 /** 1882 * Get a destructive action for selected conversations. 1883 * @param action 1884 * @return 1885 */ 1886 public final DestructiveAction getBatchDestruction(int action) { 1887 final DestructiveAction da = new ConversationAction(action, mSelectedSet.values(), true); 1888 registerDestructiveAction(da); 1889 return da; 1890 } 1891 1892 /** 1893 * Class to change the folders that are assigned to a set of conversations. This is destructive 1894 * because the user can remove the current folder from the conversation, in which case it has 1895 * to be animated away from the current folder. 1896 */ 1897 private class FolderDestruction implements DestructiveAction { 1898 private final Collection<Conversation> mTarget = new ArrayList<Conversation>(); 1899 private final ArrayList<Folder> mFolderList = new ArrayList<Folder>(); 1900 private final boolean mIsDestructive; 1901 /** Whether this destructive action has already been performed */ 1902 private boolean mCompleted; 1903 1904 /** 1905 * Create a new folder destruction object to act on the given conversations. 1906 * @param target 1907 */ 1908 private FolderDestruction(final Collection<Conversation> target, 1909 final Collection<Folder> folders, boolean isDestructive) { 1910 mTarget.addAll(target); 1911 mFolderList.addAll(folders); 1912 mIsDestructive = isDestructive; 1913 } 1914 1915 @Override 1916 public void performAction() { 1917 if (isPerformed()) { 1918 return; 1919 } 1920 if (mIsDestructive) { 1921 UndoOperation undoOp = new UndoOperation(mTarget.size(), R.id.change_folder); 1922 onUndoAvailable(undoOp); 1923 } 1924 mConversationListCursor.updateString(mContext, mTarget, 1925 ConversationColumns.FOLDER_LIST, Folder.getUriString(mFolderList)); 1926 mConversationListCursor.updateString(mContext, mTarget, 1927 ConversationColumns.RAW_FOLDERS, 1928 Folder.getSerializedFolderString(mFolder, mFolderList)); 1929 refreshConversationList(); 1930 } 1931 /** 1932 * Returns true if this action has been performed, false otherwise. 1933 * @return 1934 */ 1935 private synchronized boolean isPerformed() { 1936 if (mCompleted) { 1937 return true; 1938 } 1939 mCompleted = true; 1940 return false; 1941 } 1942 } 1943 1944 private final DestructiveAction getFolderChange(Collection<Conversation> target, 1945 Collection<Folder> folders, boolean isDestructive){ 1946 final DestructiveAction da = new FolderDestruction(target, folders, isDestructive); 1947 registerDestructiveAction(da); 1948 return da; 1949 } 1950 1951 /** 1952 * Safely refresh the conversation list if it exists. 1953 */ 1954 protected final void refreshConversationList() { 1955 final ConversationListFragment convList = getConversationListFragment(); 1956 if (convList == null) { 1957 return; 1958 } 1959 convList.requestListRefresh(); 1960 } 1961} 1962