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