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