AbstractActivityController.java revision 69b5c304f9572f18c0fcb456d81cccf2e560c72f
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.DialogFragment; 26import android.app.Fragment; 27import android.app.FragmentManager; 28import android.app.LoaderManager; 29import android.app.SearchManager; 30import android.content.ContentProviderOperation; 31import android.content.ContentResolver; 32import android.content.ContentValues; 33import android.content.Context; 34import android.content.CursorLoader; 35import android.content.DialogInterface; 36import android.content.Intent; 37import android.content.Loader; 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.view.View; 55import android.widget.Toast; 56 57import com.android.mail.ConversationListContext; 58import com.android.mail.R; 59import com.android.mail.browse.ConversationCursor; 60import com.android.mail.browse.ConversationPagerController; 61import com.android.mail.browse.ConversationCursor.ConversationOperation; 62import com.android.mail.browse.MessageCursor.ConversationMessage; 63import com.android.mail.browse.SelectedConversationsActionMenu; 64import com.android.mail.browse.SyncErrorDialogFragment; 65import com.android.mail.compose.ComposeActivity; 66import com.android.mail.providers.Account; 67import com.android.mail.providers.Conversation; 68import com.android.mail.providers.ConversationInfo; 69import com.android.mail.providers.Folder; 70import com.android.mail.providers.FolderWatcher; 71import com.android.mail.providers.MailAppProvider; 72import com.android.mail.providers.Settings; 73import com.android.mail.providers.SuggestionsProvider; 74import com.android.mail.providers.UIProvider; 75import com.android.mail.providers.UIProvider.AccountCapabilities; 76import com.android.mail.providers.UIProvider.AccountCursorExtraKeys; 77import com.android.mail.providers.UIProvider.ConversationColumns; 78import com.android.mail.providers.UIProvider.FolderCapabilities; 79import com.android.mail.ui.ActionableToastBar.ActionClickedListener; 80import com.android.mail.utils.ContentProviderTask; 81import com.android.mail.utils.LogTag; 82import com.android.mail.utils.LogUtils; 83import com.android.mail.utils.Utils; 84import com.google.common.base.Objects; 85import com.google.common.collect.ImmutableList; 86import com.google.common.collect.Lists; 87import com.google.common.collect.Sets; 88 89import java.util.ArrayList; 90import java.util.Collection; 91import java.util.Collections; 92import java.util.HashMap; 93import java.util.List; 94import java.util.Set; 95import java.util.TimerTask; 96 97 98/** 99 * This is an abstract implementation of the Activity Controller. This class 100 * knows how to respond to menu items, state changes, layout changes, etc. It 101 * weaves together the views and listeners, dispatching actions to the 102 * respective underlying classes. 103 * <p> 104 * Even though this class is abstract, it should provide default implementations 105 * for most, if not all the methods in the ActivityController interface. This 106 * makes the task of the subclasses easier: OnePaneActivityController and 107 * TwoPaneActivityController can be concise when the common functionality is in 108 * AbstractActivityController. 109 * </p> 110 * <p> 111 * In the Gmail codebase, this was called BaseActivityController 112 * </p> 113 */ 114public abstract class AbstractActivityController implements ActivityController { 115 // Keys for serialization of various information in Bundles. 116 /** Tag for {@link #mAccount} */ 117 private static final String SAVED_ACCOUNT = "saved-account"; 118 /** Tag for {@link #mFolder} */ 119 private static final String SAVED_FOLDER = "saved-folder"; 120 /** Tag for {@link #mCurrentConversation} */ 121 private static final String SAVED_CONVERSATION = "saved-conversation"; 122 /** Tag for {@link #mSelectedSet} */ 123 private static final String SAVED_SELECTED_SET = "saved-selected-set"; 124 /** Tag for {@link ActionableToastBar#getOperation()} */ 125 private static final String SAVED_TOAST_BAR_OP = "saved-toast-bar-op"; 126 /** Tag for {@link #mFolderListFolder} */ 127 private static final String SAVED_HIERARCHICAL_FOLDER = "saved-hierarchical-folder"; 128 /** Tag for {@link ConversationListContext#searchQuery} */ 129 private static final String SAVED_QUERY = "saved-query"; 130 131 /** Tag used when loading a wait fragment */ 132 protected static final String TAG_WAIT = "wait-fragment"; 133 /** Tag used when loading a conversation list fragment. */ 134 public static final String TAG_CONVERSATION_LIST = "tag-conversation-list"; 135 /** Tag used when loading a folder list fragment. */ 136 protected static final String TAG_FOLDER_LIST = "tag-folder-list"; 137 138 protected Account mAccount; 139 protected Folder mFolder; 140 /** True when {@link #mFolder} is first shown to the user. */ 141 private boolean mFolderChanged = false; 142 protected MailActionBarView mActionBarView; 143 protected final ControllableActivity mActivity; 144 protected final Context mContext; 145 private final FragmentManager mFragmentManager; 146 protected final RecentFolderList mRecentFolderList; 147 protected ConversationListContext mConvListContext; 148 protected Conversation mCurrentConversation; 149 150 /** A {@link android.content.BroadcastReceiver} that suppresses new e-mail notifications. */ 151 private SuppressNotificationReceiver mNewEmailReceiver = null; 152 153 protected Handler mHandler = new Handler(); 154 155 /** 156 * The current mode of the application. All changes in mode are initiated by 157 * the activity controller. View mode changes are propagated to classes that 158 * attach themselves as listeners of view mode changes. 159 */ 160 protected final ViewMode mViewMode; 161 protected ContentResolver mResolver; 162 protected boolean isLoaderInitialized = false; 163 private AsyncRefreshTask mAsyncRefreshTask; 164 165 private boolean mDestroyed; 166 167 /** 168 * Are we in a point in the Activity/Fragment lifecycle where it's safe to execute fragment 169 * transactions? (including back stack manipulation) 170 * <p> 171 * Per docs in {@link FragmentManager#beginTransaction()}, this flag starts out true, switches 172 * to false after {@link Activity#onSaveInstanceState}, and becomes true again in both onStart 173 * and onResume. 174 */ 175 private boolean mSafeToModifyFragments = true; 176 177 private final Set<Uri> mCurrentAccountUris = Sets.newHashSet(); 178 protected ConversationCursor mConversationListCursor; 179 private final DataSetObservable mConversationListObservable = new DataSetObservable() { 180 @Override 181 public void registerObserver(DataSetObserver observer) { 182 final int count = mObservers.size(); 183 super.registerObserver(observer); 184 LogUtils.d(LOG_TAG, "IN AAC.register(List)Observer: %s before=%d after=%d", observer, 185 count, mObservers.size()); 186 } 187 @Override 188 public void unregisterObserver(DataSetObserver observer) { 189 final int count = mObservers.size(); 190 super.unregisterObserver(observer); 191 LogUtils.d(LOG_TAG, "IN AAC.unregister(List)Observer: %s before=%d after=%d", observer, 192 count, mObservers.size()); 193 } 194 }; 195 196 private RefreshTimerTask mConversationListRefreshTask; 197 198 /** Listeners that are interested in changes to the current account. */ 199 private final DataSetObservable mAccountObservers = new DataSetObservable() { 200 @Override 201 public void registerObserver(DataSetObserver observer) { 202 final int count = mObservers.size(); 203 super.registerObserver(observer); 204 LogUtils.d(LOG_TAG, "IN AAC.register(Account)Observer: %s before=%d after=%d", 205 observer, count, mObservers.size()); 206 } 207 @Override 208 public void unregisterObserver(DataSetObserver observer) { 209 final int count = mObservers.size(); 210 super.unregisterObserver(observer); 211 LogUtils.d(LOG_TAG, "IN AAC.unregister(Account)Observer: %s before=%d after=%d", 212 observer, count, mObservers.size()); 213 } 214 }; 215 216 /** Listeners that are interested in changes to the recent folders. */ 217 private final DataSetObservable mRecentFolderObservers = new DataSetObservable() { 218 @Override 219 public void registerObserver(DataSetObserver observer) { 220 final int count = mObservers.size(); 221 super.registerObserver(observer); 222 LogUtils.d(LOG_TAG, "IN AAC.register(RecentFolder)Observer: %s before=%d after=%d", 223 observer, count, mObservers.size()); 224 } 225 @Override 226 public void unregisterObserver(DataSetObserver observer) { 227 final int count = mObservers.size(); 228 super.unregisterObserver(observer); 229 LogUtils.d(LOG_TAG, "IN AAC.unregister(RecentFolder)Observer: %s before=%d after=%d", 230 observer, count, mObservers.size()); 231 } 232 }; 233 234 /** 235 * Selected conversations, if any. 236 */ 237 private final ConversationSelectionSet mSelectedSet = new ConversationSelectionSet(); 238 239 private final int mFolderItemUpdateDelayMs; 240 241 /** Keeps track of selected and unselected conversations */ 242 final protected ConversationPositionTracker mTracker; 243 244 /** 245 * Action menu associated with the selected set. 246 */ 247 SelectedConversationsActionMenu mCabActionMenu; 248 protected ActionableToastBar mToastBar; 249 protected ConversationPagerController mPagerController; 250 251 // this is split out from the general loader dispatcher because its loader doesn't return a 252 // basic Cursor 253 private final ConversationListLoaderCallbacks mListCursorCallbacks = 254 new ConversationListLoaderCallbacks(); 255 256 private final DataSetObservable mFolderObservable = new DataSetObservable(); 257 258 protected static final String LOG_TAG = LogTag.getLogTag(); 259 /** Constants used to differentiate between the types of loaders. */ 260 private static final int LOADER_ACCOUNT_CURSOR = 0; 261 private static final int LOADER_FOLDER_CURSOR = 2; 262 private static final int LOADER_RECENT_FOLDERS = 3; 263 private static final int LOADER_CONVERSATION_LIST = 4; 264 private static final int LOADER_ACCOUNT_INBOX = 5; 265 private static final int LOADER_SEARCH = 6; 266 private static final int LOADER_ACCOUNT_UPDATE_CURSOR = 7; 267 /** 268 * Guaranteed to be the last loader ID used by the activity. Loaders are owned by Activity or 269 * fragments, and within an activity, loader IDs need to be unique. A hack to ensure that the 270 * {@link FolderWatcher} can create its folder loaders without clashing with the IDs of those 271 * of the {@link AbstractActivityController}. Currently, the {@link FolderWatcher} is the only 272 * other class that uses this activity's LoaderManager. If another class needs activity-level 273 * loaders, consider consolidating the loaders in a central location: a UI-less fragment 274 * perhaps. 275 */ 276 public static final int LAST_LOADER_ID = 100; 277 278 private static final int ADD_ACCOUNT_REQUEST_CODE = 1; 279 private static final int REAUTHENTICATE_REQUEST_CODE = 2; 280 281 /** The pending destructive action to be carried out before swapping the conversation cursor.*/ 282 private DestructiveAction mPendingDestruction; 283 protected AsyncRefreshTask mFolderSyncTask; 284 // Task for setting any share intents for the account to enabled. 285 // This gets cancelled if the user kills the app before it finishes, and 286 // will just run the next time the user opens the app. 287 private AsyncTask<String, Void, Void> mEnableShareIntents; 288 private Folder mFolderListFolder; 289 private boolean mIsDragHappening; 290 private int mShowUndoBarDelay; 291 public static final String SYNC_ERROR_DIALOG_FRAGMENT_TAG = "SyncErrorDialogFragment"; 292 293 public AbstractActivityController(MailActivity activity, ViewMode viewMode) { 294 mActivity = activity; 295 mFragmentManager = mActivity.getFragmentManager(); 296 mViewMode = viewMode; 297 mContext = activity.getApplicationContext(); 298 mRecentFolderList = new RecentFolderList(mContext); 299 mTracker = new ConversationPositionTracker(this); 300 // Allow the fragment to observe changes to its own selection set. No other object is 301 // aware of the selected set. 302 mSelectedSet.addObserver(this); 303 304 mFolderItemUpdateDelayMs = 305 mContext.getResources().getInteger(R.integer.folder_item_refresh_delay_ms); 306 mShowUndoBarDelay = 307 mContext.getResources().getInteger(R.integer.show_undo_bar_delay_ms); 308 } 309 310 @Override 311 public Account getCurrentAccount() { 312 return mAccount; 313 } 314 315 @Override 316 public ConversationListContext getCurrentListContext() { 317 return mConvListContext; 318 } 319 320 @Override 321 public String getHelpContext() { 322 final int mode = mViewMode.getMode(); 323 final int helpContextResId; 324 switch (mode) { 325 case ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION: 326 helpContextResId = R.string.wait_help_context; 327 break; 328 default: 329 helpContextResId = R.string.main_help_context; 330 } 331 return mContext.getString(helpContextResId); 332 } 333 334 @Override 335 public final ConversationCursor getConversationListCursor() { 336 return mConversationListCursor; 337 } 338 339 /** 340 * Check if the fragment is attached to an activity and has a root view. 341 * @param in 342 * @return true if the fragment is valid, false otherwise 343 */ 344 private static final boolean isValidFragment(Fragment in) { 345 if (in == null || in.getActivity() == null || in.getView() == null) { 346 return false; 347 } 348 return true; 349 } 350 351 /** 352 * Get the conversation list fragment for this activity. If the conversation list fragment 353 * is not attached, this method returns null 354 * 355 */ 356 protected ConversationListFragment getConversationListFragment() { 357 final Fragment fragment = mFragmentManager.findFragmentByTag(TAG_CONVERSATION_LIST); 358 if (isValidFragment(fragment)) { 359 return (ConversationListFragment) fragment; 360 } 361 return null; 362 } 363 364 /** 365 * Returns the folder list fragment attached with this activity. If no such fragment is attached 366 * this method returns null. 367 * 368 */ 369 protected FolderListFragment getFolderListFragment() { 370 final Fragment fragment = mFragmentManager.findFragmentByTag(TAG_FOLDER_LIST); 371 if (isValidFragment(fragment)) { 372 return (FolderListFragment) fragment; 373 } 374 return null; 375 } 376 377 /** 378 * Initialize the action bar. This is not visible to OnePaneController and 379 * TwoPaneController so they cannot override this behavior. 380 */ 381 private void initializeActionBar() { 382 final ActionBar actionBar = mActivity.getActionBar(); 383 if (actionBar == null) { 384 return; 385 } 386 387 // be sure to inherit from the ActionBar theme when inflating 388 final LayoutInflater inflater = LayoutInflater.from(actionBar.getThemedContext()); 389 final boolean isSearch = mActivity.getIntent() != null 390 && Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction()); 391 mActionBarView = (MailActionBarView) inflater.inflate( 392 isSearch ? R.layout.search_actionbar_view : R.layout.actionbar_view, null); 393 mActionBarView.initialize(mActivity, this, mViewMode, actionBar, mRecentFolderList); 394 } 395 396 /** 397 * Attach the action bar to the activity. 398 */ 399 private void attachActionBar() { 400 final ActionBar actionBar = mActivity.getActionBar(); 401 if (actionBar != null && mActionBarView != null) { 402 actionBar.setCustomView(mActionBarView, new ActionBar.LayoutParams( 403 LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); 404 // Show a custom view and home icon, but remove the title 405 final int mask = ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_SHOW_TITLE 406 | ActionBar.DISPLAY_SHOW_HOME; 407 final int enabled = ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_SHOW_HOME; 408 actionBar.setDisplayOptions(enabled, mask); 409 mActionBarView.attach(); 410 } 411 mViewMode.addListener(mActionBarView); 412 } 413 414 /** 415 * Returns whether the conversation list fragment is visible or not. 416 * Different layouts will have their own notion on the visibility of 417 * fragments, so this method needs to be overriden. 418 * 419 */ 420 protected abstract boolean isConversationListVisible(); 421 422 /** 423 * Switch the current account to the one provided as an argument to the method. 424 * @param account new account 425 * @param shouldReloadInbox whether the default inbox should be reloaded. 426 */ 427 private void switchAccount(Account account, boolean shouldReloadInbox){ 428 // Current account is different from the new account, restart loaders and show 429 // the account Inbox. 430 mAccount = account; 431 LogUtils.d(LOG_TAG, "AbstractActivityController.switchAccount(): mAccount = %s", 432 mAccount.uri); 433 cancelRefreshTask(); 434 mAccountObservers.notifyChanged(); 435 if (shouldReloadInbox) { 436 loadAccountInbox(); 437 } 438 restartOptionalLoader(LOADER_RECENT_FOLDERS); 439 mActivity.invalidateOptionsMenu(); 440 disableNotificationsOnAccountChange(mAccount); 441 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR); 442 MailAppProvider.getInstance().setLastViewedAccount(mAccount.uri.toString()); 443 } 444 445 @Override 446 public void onAccountChanged(Account account) { 447 // Is the account or account settings different from the existing account? 448 final boolean firstLoad = mAccount == null; 449 LogUtils.d(LOG_TAG, "onAccountChanged (%s) called. firstLoad=%s", account, firstLoad); 450 final boolean accountChanged = firstLoad || !account.uri.equals(mAccount.uri); 451 final boolean settingsChanged = firstLoad || !account.settings.equals(mAccount.settings); 452 if (accountChanged || settingsChanged) { 453 if (account != null) { 454 final String accountName = account.name; 455 mHandler.post(new Runnable() { 456 @Override 457 public void run() { 458 MailActivity.setForegroundNdef(MailActivity.getMailtoNdef(accountName)); 459 } 460 }); 461 } 462 if (accountChanged) { 463 commitDestructiveActions(false); 464 } 465 switchAccount(account, accountChanged); 466 } 467 } 468 469 /** 470 * Adds a listener interested in change in the current account. If a class is storing a 471 * reference to the current account, it should listen on changes, so it can receive updates to 472 * settings. Must happen in the UI thread. 473 */ 474 @Override 475 public void registerAccountObserver(DataSetObserver obs) { 476 mAccountObservers.registerObserver(obs); 477 } 478 479 /** 480 * Removes a listener from receiving current account changes. 481 * Must happen in the UI thread. 482 */ 483 @Override 484 public void unregisterAccountObserver(DataSetObserver obs) { 485 mAccountObservers.unregisterObserver(obs); 486 } 487 488 @Override 489 public Account getAccount() { 490 return mAccount; 491 } 492 493 private void fetchSearchFolder(Intent intent) { 494 Bundle args = new Bundle(); 495 args.putString(ConversationListContext.EXTRA_SEARCH_QUERY, intent 496 .getStringExtra(ConversationListContext.EXTRA_SEARCH_QUERY)); 497 mActivity.getLoaderManager().restartLoader(LOADER_SEARCH, args, this); 498 } 499 500 @Override 501 public void onFolderChanged(Folder folder) { 502 changeFolder(folder, null); 503 } 504 505 /** 506 * Sets the folder state without changing view mode and without creating a list fragment, if 507 * possible. 508 * @param folder 509 */ 510 private void setListContext(Folder folder, String query) { 511 updateFolder(folder); 512 if (query != null) { 513 mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder, query); 514 } else { 515 mConvListContext = ConversationListContext.forFolder(mAccount, mFolder); 516 } 517 cancelRefreshTask(); 518 } 519 520 /** 521 * Changes the folder to the value provided here. This causes the view mode to change. 522 * @param folder the folder to change to 523 * @param query if non-null, this represents the search string that the folder represents. 524 */ 525 private void changeFolder(Folder folder, String query) { 526 if (!Objects.equal(mFolder, folder)) { 527 commitDestructiveActions(false); 528 } 529 if (folder != null && !folder.equals(mFolder) 530 || (mViewMode.getMode() != ViewMode.CONVERSATION_LIST)) { 531 setListContext(folder, query); 532 showConversationList(mConvListContext); 533 } 534 } 535 536 @Override 537 public void onFolderSelected(Folder folder) { 538 onFolderChanged(folder); 539 } 540 541 /** 542 * Update the recent folders. This only needs to be done once when accessing a new folder. 543 */ 544 private void updateRecentFolderList() { 545 if (mFolder != null) { 546 mRecentFolderList.touchFolder(mFolder, mAccount); 547 } 548 } 549 550 /** 551 * Adds a listener interested in change in the recent folders. If a class is storing a 552 * reference to the recent folders, it should listen on changes, so it can receive updates. 553 * Must happen in the UI thread. 554 */ 555 @Override 556 public void registerRecentFolderObserver(DataSetObserver obs) { 557 mRecentFolderObservers.registerObserver(obs); 558 } 559 560 /** 561 * Removes a listener from receiving recent folder changes. 562 * Must happen in the UI thread. 563 */ 564 @Override 565 public void unregisterRecentFolderObserver(DataSetObserver obs) { 566 mRecentFolderObservers.unregisterObserver(obs); 567 } 568 569 @Override 570 public RecentFolderList getRecentFolders() { 571 return mRecentFolderList; 572 } 573 574 // TODO(mindyp): set this up to store a copy of the folder as a transient 575 // field in the account. 576 @Override 577 public void loadAccountInbox() { 578 restartOptionalLoader(LOADER_ACCOUNT_INBOX); 579 } 580 581 /** 582 * Sets the current folder if it is different from the object provided here. This method does 583 * NOT notify the folder observers that a change has happened. Observers are notified when we 584 * get an updated folder from the loaders, which will happen as a consequence of this method 585 * (since this method starts/restarts the loaders). 586 * @param folder The folder to assign 587 */ 588 private void updateFolder(Folder folder) { 589 boolean wasNull = mFolder == null; 590 if (folder != null && !folder.equals(mFolder) && folder.isInitialized()) { 591 LogUtils.d(LOG_TAG, "AbstractActivityController.setFolder(%s)", folder.name); 592 final LoaderManager lm = mActivity.getLoaderManager(); 593 mFolder = folder; 594 mFolderChanged = true; 595 596 // We do not need to notify folder observers yet. Instead we start the loaders and 597 // when the load finishes, we will get an updated folder. Then, we notify the 598 // folderObservers in onLoadFinished. 599 mActionBarView.setFolder(mFolder); 600 601 // Only when we switch from one folder to another do we want to restart the 602 // folder and conversation list loaders (to trigger onCreateLoader). 603 // The first time this runs when the activity is [re-]initialized, we want to re-use the 604 // previous loader's instance and data upon configuration change (e.g. rotation). 605 // If there was not already an instance of the loader, init it. 606 if (lm.getLoader(LOADER_FOLDER_CURSOR) == null) { 607 lm.initLoader(LOADER_FOLDER_CURSOR, null, this); 608 } else { 609 lm.restartLoader(LOADER_FOLDER_CURSOR, null, this); 610 } 611 // In this case, we are starting from no folder, which would occur 612 // the first time the app was launched or on orientation changes. 613 // We want to attach to an existing loader, if available. 614 if (wasNull || lm.getLoader(LOADER_CONVERSATION_LIST) == null) { 615 lm.initLoader(LOADER_CONVERSATION_LIST, null, mListCursorCallbacks); 616 } else { 617 // However, if there was an existing folder AND we have changed 618 // folders, we want to restart the loader to get the information 619 // for the newly selected folder 620 lm.destroyLoader(LOADER_CONVERSATION_LIST); 621 lm.initLoader(LOADER_CONVERSATION_LIST, null, mListCursorCallbacks); 622 } 623 } else if (!folder.isInitialized()) { 624 LogUtils.e(LOG_TAG, new Error(), "Uninitialized Folder %s in setFolder.", folder); 625 } 626 } 627 628 @Override 629 public Folder getFolder() { 630 return mFolder; 631 } 632 633 @Override 634 public Folder getHierarchyFolder() { 635 return mFolderListFolder; 636 } 637 638 @Override 639 public void setHierarchyFolder(Folder folder) { 640 mFolderListFolder = folder; 641 } 642 643 @Override 644 public void onActivityResult(int requestCode, int resultCode, Intent data) { 645 switch (requestCode) { 646 case ADD_ACCOUNT_REQUEST_CODE: 647 // We were waiting for the user to create an account 648 if (resultCode == Activity.RESULT_OK) { 649 // restart the loader to get the updated list of accounts 650 mActivity.getLoaderManager().initLoader( 651 LOADER_ACCOUNT_CURSOR, null, this); 652 } else { 653 // The user failed to create an account, just exit the app 654 mActivity.finish(); 655 } 656 break; 657 case REAUTHENTICATE_REQUEST_CODE: 658 if (resultCode == Activity.RESULT_OK) { 659 // The user successfully authenticated, attempt to refresh the list 660 final Uri refreshUri = mFolder != null ? mFolder.refreshUri : null; 661 if (refreshUri != null) { 662 startAsyncRefreshTask(refreshUri); 663 } 664 } 665 break; 666 } 667 } 668 669 /** 670 * Inform the conversation cursor that there has been a visibility change. 671 * @param visible 672 */ 673 protected synchronized void informCursorVisiblity(boolean visible) { 674 if (mConversationListCursor != null) { 675 Utils.setConversationCursorVisibility(mConversationListCursor, visible, mFolderChanged); 676 // We have informed the cursor. Subsequent visibility changes should not tell it that 677 // the folder has changed. 678 mFolderChanged = false; 679 } 680 } 681 682 @Override 683 public void onConversationListVisibilityChanged(boolean visible) { 684 informCursorVisiblity(visible); 685 } 686 687 /** 688 * Called when a conversation is visible. Child classes must call the super class implementation 689 * before performing local computation. 690 */ 691 @Override 692 public void onConversationVisibilityChanged(boolean visible) { 693 return; 694 } 695 696 @Override 697 public boolean onCreate(Bundle savedState) { 698 initializeActionBar(); 699 // Allow shortcut keys to function for the ActionBar and menus. 700 mActivity.setDefaultKeyMode(Activity.DEFAULT_KEYS_SHORTCUT); 701 mResolver = mActivity.getContentResolver(); 702 mNewEmailReceiver = new SuppressNotificationReceiver(); 703 mRecentFolderList.initialize(mActivity); 704 705 // All the individual UI components listen for ViewMode changes. This 706 // simplifies the amount of logic in the AbstractActivityController, but increases the 707 // possibility of timing-related bugs. 708 mViewMode.addListener(this); 709 mPagerController = new ConversationPagerController(mActivity, this); 710 mToastBar = (ActionableToastBar) mActivity.findViewById(R.id.toast_bar); 711 attachActionBar(); 712 713 final Intent intent = mActivity.getIntent(); 714 // Immediately handle a clean launch with intent, and any state restoration 715 // that does not rely on restored fragments or loader data 716 // any state restoration that relies on those can be done later in 717 // onRestoreInstanceState, once fragments are up and loader data is re-delivered 718 if (savedState != null) { 719 if (savedState.containsKey(SAVED_ACCOUNT)) { 720 setAccount((Account) savedState.getParcelable(SAVED_ACCOUNT)); 721 } 722 if (savedState.containsKey(SAVED_FOLDER)) { 723 final Folder folder = (Folder) savedState.getParcelable(SAVED_FOLDER); 724 final String query = savedState.getString(SAVED_QUERY, null); 725 setListContext(folder, query); 726 } 727 mViewMode.handleRestore(savedState); 728 } else if (intent != null) { 729 handleIntent(intent); 730 } 731 // Create the accounts loader; this loads the account switch spinner. 732 mActivity.getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this); 733 return true; 734 } 735 736 @Override 737 public void onStart() { 738 mSafeToModifyFragments = true; 739 } 740 741 @Override 742 public void onRestart() { 743 DialogFragment fragment = (DialogFragment) 744 mFragmentManager.findFragmentByTag(SYNC_ERROR_DIALOG_FRAGMENT_TAG); 745 if (fragment != null) { 746 fragment.dismiss(); 747 } 748 // When the user places the app in the background by pressing "home", 749 // dismiss the toast bar. However, since there is no way to determine if 750 // home was pressed, just dismiss any existing toast bar when restarting 751 // the app. 752 if (mToastBar != null) { 753 mToastBar.hide(false); 754 } 755 } 756 757 @Override 758 public Dialog onCreateDialog(int id, Bundle bundle) { 759 return null; 760 } 761 762 @Override 763 public final boolean onCreateOptionsMenu(Menu menu) { 764 MenuInflater inflater = mActivity.getMenuInflater(); 765 inflater.inflate(mActionBarView.getOptionsMenuId(), menu); 766 mActionBarView.onCreateOptionsMenu(menu); 767 return true; 768 } 769 770 @Override 771 public final boolean onKeyDown(int keyCode, KeyEvent event) { 772 // TODO(viki): Auto-generated method stub 773 return false; 774 } 775 776 @Override 777 public final boolean onOptionsItemSelected(MenuItem item) { 778 final int id = item.getItemId(); 779 LogUtils.d(LOG_TAG, "AbstractController.onOptionsItemSelected(%d) called.", id); 780 boolean handled = true; 781 final Collection<Conversation> target = Conversation.listOf(mCurrentConversation); 782 final Settings settings = (mAccount == null) ? null : mAccount.settings; 783 // The user is choosing a new action; commit whatever they had been doing before. 784 commitDestructiveActions(true); 785 switch (id) { 786 case R.id.archive: { 787 final boolean showDialog = (settings != null && settings.confirmArchive); 788 confirmAndDelete(target, showDialog, R.plurals.confirm_archive_conversation, 789 getAction(R.id.archive, target)); 790 break; 791 } 792 case R.id.remove_folder: 793 delete(target, getRemoveFolder(target, mFolder, true, false, true)); 794 break; 795 case R.id.delete: { 796 final boolean showDialog = (settings != null && settings.confirmDelete); 797 confirmAndDelete(target, showDialog, R.plurals.confirm_delete_conversation, 798 getAction(R.id.delete, target)); 799 break; 800 } 801 case R.id.discard_drafts: 802 final boolean showDialog = (settings != null && settings.confirmDelete); 803 confirmAndDelete(target, showDialog, R.plurals.confirm_discard_drafts_conversation, 804 getAction(R.id.discard_drafts, target)); 805 break; 806 case R.id.mark_important: 807 updateConversation(Conversation.listOf(mCurrentConversation), 808 ConversationColumns.PRIORITY, UIProvider.ConversationPriority.HIGH); 809 break; 810 case R.id.mark_not_important: 811 if (mFolder != null && mFolder.isImportantOnly()) { 812 delete(target, getAction(R.id.mark_not_important, target)); 813 } else { 814 updateConversation(Conversation.listOf(mCurrentConversation), 815 ConversationColumns.PRIORITY, UIProvider.ConversationPriority.LOW); 816 } 817 break; 818 case R.id.mute: 819 delete(target, getAction(R.id.mute, target)); 820 break; 821 case R.id.report_spam: 822 delete(target, getAction(R.id.report_spam, target)); 823 break; 824 case R.id.mark_not_spam: 825 // Currently, since spam messages are only shown in list with other spam messages, 826 // marking a message not as spam is a destructive action 827 delete(target, getAction(R.id.mark_not_spam, target)); 828 break; 829 case R.id.report_phishing: 830 delete(target, getAction(R.id.report_phishing, target)); 831 break; 832 case android.R.id.home: 833 onUpPressed(); 834 break; 835 case R.id.compose: 836 ComposeActivity.compose(mActivity.getActivityContext(), mAccount); 837 break; 838 case R.id.show_all_folders: 839 showFolderList(); 840 break; 841 case R.id.refresh: 842 requestFolderRefresh(); 843 break; 844 case R.id.settings: 845 Utils.showSettings(mActivity.getActivityContext(), mAccount); 846 break; 847 case R.id.folder_options: 848 Utils.showFolderSettings(mActivity.getActivityContext(), mAccount, mFolder); 849 break; 850 case R.id.help_info_menu_item: 851 Utils.showHelp(mActivity.getActivityContext(), mAccount, getHelpContext()); 852 break; 853 case R.id.feedback_menu_item: 854 Utils.sendFeedback(mActivity.getActivityContext(), mAccount, false); 855 break; 856 case R.id.manage_folders_item: 857 Utils.showManageFolder(mActivity.getActivityContext(), mAccount); 858 break; 859 case R.id.change_folder: 860 new FoldersSelectionDialog(mActivity.getActivityContext(), mAccount, this, 861 Conversation.listOf(mCurrentConversation), false, mFolder).show(); 862 break; 863 default: 864 handled = false; 865 break; 866 } 867 return handled; 868 } 869 870 @Override 871 public void updateConversation(Collection<Conversation> target, ContentValues values) { 872 mConversationListCursor.updateValues(mContext, target, values); 873 refreshConversationList(); 874 } 875 876 @Override 877 public void updateConversation(Collection <Conversation> target, String columnName, 878 boolean value) { 879 mConversationListCursor.updateBoolean(mContext, target, columnName, value); 880 refreshConversationList(); 881 } 882 883 @Override 884 public void updateConversation(Collection <Conversation> target, String columnName, 885 int value) { 886 mConversationListCursor.updateInt(mContext, target, columnName, value); 887 refreshConversationList(); 888 } 889 890 @Override 891 public void updateConversation(Collection <Conversation> target, String columnName, 892 String value) { 893 mConversationListCursor.updateString(mContext, target, columnName, value); 894 refreshConversationList(); 895 } 896 897 @Override 898 public void markConversationMessagesUnread(Conversation conv, Set<Uri> unreadMessageUris, 899 String originalConversationInfo) { 900 // The only caller of this method is the conversation view, from where marking unread should 901 // *always* take you back to list mode. 902 showConversation(null); 903 904 // locally mark conversation unread (the provider is supposed to propagate message unread 905 // to conversation unread) 906 conv.read = false; 907 908 // only do a granular 'mark unread' if a subset of messages are unread 909 final int unreadCount = (unreadMessageUris == null) ? 0 : unreadMessageUris.size(); 910 final int numMessages = conv.getNumMessages(); 911 final boolean subsetIsUnread = (numMessages > 1 && unreadCount > 0 912 && unreadCount < numMessages); 913 914 if (!subsetIsUnread) { 915 // Conversations are neither marked read, nor viewed, and we don't want to show 916 // the next conversation. 917 markConversationsRead(Collections.singletonList(conv), false, false, false); 918 } else { 919 mConversationListCursor.setConversationColumn(conv.uri, ConversationColumns.READ, 0); 920 921 // locally update conversation's conversationInfo to revert to original version 922 if (originalConversationInfo != null) { 923 mConversationListCursor.setConversationColumn(conv.uri, 924 ConversationColumns.CONVERSATION_INFO, originalConversationInfo); 925 } 926 927 // applyBatch with each CPO as an UPDATE op on each affected message uri 928 final ArrayList<ContentProviderOperation> ops = Lists.newArrayList(); 929 String authority = null; 930 for (Uri messageUri : unreadMessageUris) { 931 if (authority == null) { 932 authority = messageUri.getAuthority(); 933 } 934 ops.add(ContentProviderOperation.newUpdate(messageUri) 935 .withValue(UIProvider.MessageColumns.READ, 0) 936 .build()); 937 } 938 939 new ContentProviderTask() { 940 @Override 941 protected void onPostExecute(Result result) { 942 // TODO: handle errors? 943 } 944 }.run(mResolver, authority, ops); 945 } 946 } 947 948 @Override 949 public void markConversationsRead(Collection<Conversation> targets, boolean read, 950 boolean viewed) { 951 // We want to show the next conversation if we are marking unread. 952 markConversationsRead(targets, read, viewed, true); 953 } 954 955 private void markConversationsRead(Collection<Conversation> targets, boolean read, 956 boolean markViewed, boolean showNext) { 957 // Auto-advance if requested and the current conversation is being marked unread 958 if (showNext && !read) { 959 showNextConversation(targets); 960 } 961 final int size = targets.size(); 962 final List<ConversationOperation> opList = new ArrayList<ConversationOperation>(size); 963 for (final Conversation target : targets) { 964 final ContentValues value = new ContentValues(); 965 value.put(ConversationColumns.READ, read); 966 if (markViewed) { 967 value.put(ConversationColumns.VIEWED, true); 968 } 969 final ConversationInfo info = target.conversationInfo; 970 if (info != null) { 971 info.markRead(read); 972 value.put(ConversationColumns.CONVERSATION_INFO, ConversationInfo.toString(info)); 973 } 974 opList.add(mConversationListCursor.getOperationForConversation( 975 target, ConversationOperation.UPDATE, value)); 976 // Update the local conversation objects so they immediately change state. 977 target.read = read; 978 if (markViewed) { 979 target.markViewed(); 980 } 981 } 982 mConversationListCursor.updateBulkValues(mContext, opList); 983 } 984 985 /** 986 * Auto-advance to a different conversation if the currently visible conversation in 987 * conversation mode is affected (deleted, marked unread, etc.). 988 * 989 * <p>Does nothing if outside of conversation mode. 990 * 991 * @param target the set of conversations being deleted/marked unread 992 */ 993 private void showNextConversation(Collection<Conversation> target) { 994 final boolean currentConversationInView = (mViewMode.getMode() == ViewMode.CONVERSATION) 995 && Conversation.contains(target, mCurrentConversation); 996 if (currentConversationInView) { 997 final Conversation next = mTracker.getNextConversation( 998 Settings.getAutoAdvanceSetting(mAccount.settings), target); 999 LogUtils.d(LOG_TAG, "showNextConversation: showing %s next.", next); 1000 showConversation(next); 1001 } 1002 } 1003 1004 @Override 1005 public void starMessage(ConversationMessage msg, boolean starred) { 1006 if (msg.starred == starred) { 1007 return; 1008 } 1009 1010 msg.starred = starred; 1011 1012 // locally propagate the change to the owning conversation 1013 // (figure the provider will properly propagate the change when it commits it) 1014 // 1015 // when unstarring, only propagate the change if this was the only message starred 1016 final boolean conversationStarred = starred || msg.isConversationStarred(); 1017 if (conversationStarred != msg.conversation.starred) { 1018 msg.conversation.starred = conversationStarred; 1019 mConversationListCursor.setConversationColumn(msg.conversation.uri, 1020 ConversationColumns.STARRED, conversationStarred); 1021 } 1022 1023 final ContentValues values = new ContentValues(1); 1024 values.put(UIProvider.MessageColumns.STARRED, starred ? 1 : 0); 1025 1026 new ContentProviderTask.UpdateTask() { 1027 @Override 1028 protected void onPostExecute(Result result) { 1029 // TODO: handle errors? 1030 } 1031 }.run(mResolver, msg.uri, values, null /* selection*/, null /* selectionArgs */); 1032 } 1033 1034 private void requestFolderRefresh() { 1035 if (mFolder != null) { 1036 if (mAsyncRefreshTask != null) { 1037 mAsyncRefreshTask.cancel(true); 1038 } 1039 mAsyncRefreshTask = new AsyncRefreshTask(mContext, mFolder.refreshUri); 1040 mAsyncRefreshTask.execute(); 1041 } 1042 } 1043 1044 /** 1045 * Confirm (based on user's settings) and delete a conversation from the conversation list and 1046 * from the database. 1047 * @param target the conversations to act upon 1048 * @param showDialog true if a confirmation dialog is to be shown, false otherwise. 1049 * @param confirmResource the resource ID of the string that is shown in the confirmation dialog 1050 * @param action the action to perform after animating the deletion of the conversations. 1051 */ 1052 protected void confirmAndDelete(final Collection<Conversation> target, boolean showDialog, 1053 int confirmResource, final DestructiveAction action) { 1054 if (showDialog) { 1055 final AlertDialog.OnClickListener onClick = new AlertDialog.OnClickListener() { 1056 @Override 1057 public void onClick(DialogInterface dialog, int which) { 1058 delete(target, action); 1059 } 1060 }; 1061 final CharSequence message = Utils.formatPlural(mContext, confirmResource, 1062 target.size()); 1063 new AlertDialog.Builder(mActivity.getActivityContext()).setMessage(message) 1064 .setPositiveButton(R.string.ok, onClick) 1065 .setNegativeButton(R.string.cancel, null) 1066 .create().show(); 1067 } else { 1068 delete(target, action); 1069 } 1070 } 1071 1072 @Override 1073 public void delete(final Collection<Conversation> target, final DestructiveAction action) { 1074 // Order of events is critical! The Conversation View Fragment must be notified 1075 // of the next conversation with showConversation(next) *before* the conversation list 1076 // fragment has a chance to delete the conversation, animating it away. 1077 1078 // Update the conversation fragment if the current conversation is deleted. 1079 showNextConversation(target); 1080 // The conversation list deletes and performs the action if it exists. 1081 final ConversationListFragment convListFragment = getConversationListFragment(); 1082 if (convListFragment != null) { 1083 LogUtils.d(LOG_TAG, "AAC.requestDelete: ListFragment is handling delete."); 1084 convListFragment.requestDelete(target, action); 1085 return; 1086 } 1087 // No visible UI element handled it on our behalf. Perform the action ourself. 1088 action.performAction(); 1089 } 1090 1091 /** 1092 * Requests that the action be performed and the UI state is updated to reflect the new change. 1093 * @param target 1094 * @param action 1095 */ 1096 private void requestUpdate(final Collection<Conversation> target, 1097 final DestructiveAction action) { 1098 action.performAction(); 1099 refreshConversationList(); 1100 } 1101 1102 @Override 1103 public void onPrepareDialog(int id, Dialog dialog, Bundle bundle) { 1104 // TODO(viki): Auto-generated method stub 1105 } 1106 1107 @Override 1108 public boolean onPrepareOptionsMenu(Menu menu) { 1109 return mActionBarView.onPrepareOptionsMenu(menu); 1110 } 1111 1112 @Override 1113 public void onPause() { 1114 isLoaderInitialized = false; 1115 enableNotifications(); 1116 } 1117 1118 @Override 1119 public void onResume() { 1120 // Register the receiver that will prevent the status receiver from 1121 // displaying its notification icon as long as we're running. 1122 // The SupressNotificationReceiver will block the broadcast if we're looking at the folder 1123 // that the notification was received for. 1124 disableNotifications(); 1125 1126 mSafeToModifyFragments = true; 1127 } 1128 1129 @Override 1130 public void onSaveInstanceState(Bundle outState) { 1131 mViewMode.handleSaveInstanceState(outState); 1132 if (mAccount != null) { 1133 LogUtils.d(LOG_TAG, "Saving the account now"); 1134 outState.putParcelable(SAVED_ACCOUNT, mAccount); 1135 } 1136 if (mFolder != null) { 1137 outState.putParcelable(SAVED_FOLDER, mFolder); 1138 } 1139 // If this is a search activity, let's store the search query term as well. 1140 if (ConversationListContext.isSearchResult(mConvListContext)) { 1141 outState.putString(SAVED_QUERY, mConvListContext.searchQuery); 1142 } 1143 final int mode = mViewMode.getMode(); 1144 if (mCurrentConversation != null 1145 && (mode == ViewMode.CONVERSATION || 1146 mViewMode.getMode() == ViewMode.SEARCH_RESULTS_CONVERSATION)) { 1147 outState.putParcelable(SAVED_CONVERSATION, mCurrentConversation); 1148 } 1149 if (!mSelectedSet.isEmpty()) { 1150 outState.putParcelable(SAVED_SELECTED_SET, mSelectedSet); 1151 } 1152 if (mToastBar.getVisibility() == View.VISIBLE) { 1153 outState.putParcelable(SAVED_TOAST_BAR_OP, mToastBar.getOperation()); 1154 } 1155 final ConversationListFragment convListFragment = getConversationListFragment(); 1156 if (convListFragment != null) { 1157 convListFragment.getAnimatedAdapter().onSaveInstanceState(outState); 1158 } 1159 mSafeToModifyFragments = false; 1160 outState.putString(SAVED_HIERARCHICAL_FOLDER, 1161 (mFolderListFolder != null) ? Folder.toString(mFolderListFolder) : null); 1162 } 1163 1164 /** 1165 * @see #mSafeToModifyFragments 1166 */ 1167 protected boolean safeToModifyFragments() { 1168 return mSafeToModifyFragments; 1169 } 1170 1171 @Override 1172 public void onSearchRequested(String query) { 1173 Intent intent = new Intent(); 1174 intent.setAction(Intent.ACTION_SEARCH); 1175 intent.putExtra(ConversationListContext.EXTRA_SEARCH_QUERY, query); 1176 intent.putExtra(Utils.EXTRA_ACCOUNT, mAccount); 1177 intent.setComponent(mActivity.getComponentName()); 1178 mActionBarView.collapseSearch(); 1179 mActivity.startActivity(intent); 1180 } 1181 1182 @Override 1183 public void onStop() { 1184 if (mEnableShareIntents != null) { 1185 mEnableShareIntents.cancel(true); 1186 } 1187 } 1188 1189 @Override 1190 public void onDestroy() { 1191 // unregister the ViewPager's observer on the conversation cursor 1192 mPagerController.onDestroy(); 1193 mActionBarView.onDestroy(); 1194 mRecentFolderList.destroy(); 1195 mDestroyed = true; 1196 } 1197 1198 /** 1199 * {@inheritDoc} Subclasses must override this to listen to mode changes 1200 * from the ViewMode. Subclasses <b>must</b> call the parent's 1201 * onViewModeChanged since the parent will handle common state changes. 1202 */ 1203 @Override 1204 public void onViewModeChanged(int newMode) { 1205 // Perform any mode specific work here. 1206 // reset the action bar icon based on the mode. Why don't the individual 1207 // controllers do 1208 // this themselves? 1209 1210 // Commit any destructive undoable actions the user may have performed. 1211 commitDestructiveActions(true); 1212 } 1213 1214 public void disablePagerUpdates() { 1215 mPagerController.stopListening(); 1216 } 1217 1218 public boolean isDestroyed() { 1219 return mDestroyed; 1220 } 1221 1222 @Override 1223 public void commitDestructiveActions(boolean animate) { 1224 ConversationListFragment fragment = getConversationListFragment(); 1225 if (fragment != null) { 1226 fragment.commitDestructiveActions(animate); 1227 } 1228 } 1229 1230 @Override 1231 public void onWindowFocusChanged(boolean hasFocus) { 1232 final ConversationListFragment convList = getConversationListFragment(); 1233 if (hasFocus && convList != null && convList.isVisible()) { 1234 // The conversation list is visible. 1235 informCursorVisiblity(true); 1236 } 1237 } 1238 1239 /** 1240 * Set the account, and carry out all the account-related changes that rely on this. 1241 * @param account 1242 */ 1243 // TODO(viki): Two different methods do the same thing. Resolve 1244 // {@link #setAccount(Account)} and {@link #switchAccount(Account, boolean)} 1245 private void setAccount(Account account) { 1246 if (account == null) { 1247 LogUtils.w(LOG_TAG, new Error(), 1248 "AAC ignoring null (presumably invalid) account restoration"); 1249 return; 1250 } 1251 LogUtils.d(LOG_TAG, "AbstractActivityController.setAccount(): account = %s", account.uri); 1252 mAccount = account; 1253 // Only change AAC state here. Do *not* modify any other object's state. The object 1254 // should listen on account changes. 1255 restartOptionalLoader(LOADER_RECENT_FOLDERS); 1256 mActivity.invalidateOptionsMenu(); 1257 disableNotificationsOnAccountChange(mAccount); 1258 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR); 1259 MailAppProvider.getInstance().setLastViewedAccount(mAccount.uri.toString()); 1260 1261 if (account.settings == null) { 1262 LogUtils.w(LOG_TAG, new Error(), "AAC ignoring account with null settings."); 1263 return; 1264 } 1265 mAccountObservers.notifyChanged(); 1266 } 1267 1268 /** 1269 * Restore the state from the previous bundle. Subclasses should call this 1270 * method from the parent class, since it performs important UI 1271 * initialization. 1272 * 1273 * @param savedState 1274 */ 1275 @Override 1276 public void onRestoreInstanceState(Bundle savedState) { 1277 LogUtils.d(LOG_TAG, "IN AAC.onRestoreInstanceState"); 1278 if (savedState.containsKey(SAVED_CONVERSATION)) { 1279 // Open the conversation. 1280 final Conversation conversation = 1281 (Conversation)savedState.getParcelable(SAVED_CONVERSATION); 1282 if (conversation != null && conversation.position < 0) { 1283 // Set the position to 0 on this conversation, as we don't know where it is 1284 // in the list 1285 conversation.position = 0; 1286 } 1287 showConversation(conversation); 1288 } 1289 1290 if (savedState.containsKey(SAVED_TOAST_BAR_OP)) { 1291 ToastBarOperation op = ((ToastBarOperation) savedState 1292 .getParcelable(SAVED_TOAST_BAR_OP)); 1293 if (op != null) { 1294 if (op.getType() == ToastBarOperation.UNDO) { 1295 onUndoAvailable(op); 1296 } else if (op.getType() == ToastBarOperation.ERROR) { 1297 onError(mFolder, true); 1298 } 1299 } 1300 } 1301 final String folderString = savedState.getString(SAVED_HIERARCHICAL_FOLDER, null); 1302 if (!TextUtils.isEmpty(folderString)) { 1303 mFolderListFolder = Folder.fromString(folderString); 1304 } 1305 final ConversationListFragment convListFragment = getConversationListFragment(); 1306 if (convListFragment != null) { 1307 convListFragment.getAnimatedAdapter().onRestoreInstanceState(savedState); 1308 } 1309 /** 1310 * Restore the state of selected conversations. This needs to be done after the correct mode 1311 * is set and the action bar is fully initialized. If not, several key pieces of state 1312 * information will be missing, and the split views may not be initialized correctly. 1313 * @param savedState 1314 */ 1315 restoreSelectedConversations(savedState); 1316 } 1317 1318 /** 1319 * Handle an intent to open the app. This method is called only when there is no saved state, 1320 * so we need to set state that wasn't set before. It is correct to change the viewmode here 1321 * since it has not been previously set. 1322 * @param intent 1323 */ 1324 private void handleIntent(Intent intent) { 1325 boolean handled = false; 1326 if (Intent.ACTION_VIEW.equals(intent.getAction())) { 1327 if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) { 1328 setAccount(Account.newinstance(intent.getStringExtra(Utils.EXTRA_ACCOUNT))); 1329 } 1330 if (mAccount == null) { 1331 return; 1332 } 1333 final boolean isConversationMode = intent.hasExtra(Utils.EXTRA_CONVERSATION); 1334 if (isConversationMode && mViewMode.getMode() == ViewMode.UNKNOWN) { 1335 mViewMode.enterConversationMode(); 1336 } else { 1337 mViewMode.enterConversationListMode(); 1338 } 1339 1340 Folder folder = null; 1341 if (intent.hasExtra(Utils.EXTRA_FOLDER)) { 1342 // Open the folder. 1343 folder = Folder.fromString(intent.getStringExtra(Utils.EXTRA_FOLDER)); 1344 } 1345 if (folder != null) { 1346 onFolderChanged(folder); 1347 handled = true; 1348 } 1349 1350 if (isConversationMode) { 1351 // Open the conversation. 1352 LogUtils.d(LOG_TAG, "SHOW THE CONVERSATION at %s", 1353 intent.getParcelableExtra(Utils.EXTRA_CONVERSATION)); 1354 final Conversation conversation = 1355 (Conversation)intent.getParcelableExtra(Utils.EXTRA_CONVERSATION); 1356 if (conversation != null && conversation.position < 0) { 1357 // Set the position to 0 on this conversation, as we don't know where it is 1358 // in the list 1359 conversation.position = 0; 1360 } 1361 showConversation(conversation); 1362 handled = true; 1363 } 1364 1365 if (!handled) { 1366 // Nothing was saved; just load the account inbox. 1367 loadAccountInbox(); 1368 } 1369 } else if (Intent.ACTION_SEARCH.equals(intent.getAction())) { 1370 if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) { 1371 // Save this search query for future suggestions. 1372 final String query = intent.getStringExtra(SearchManager.QUERY); 1373 final String authority = mContext.getString(R.string.suggestions_authority); 1374 SearchRecentSuggestions suggestions = new SearchRecentSuggestions( 1375 mContext, authority, SuggestionsProvider.MODE); 1376 suggestions.saveRecentQuery(query, null); 1377 if (Utils.showTwoPaneSearchResults(mActivity.getActivityContext())) { 1378 mViewMode.enterSearchResultsConversationMode(); 1379 } else { 1380 mViewMode.enterSearchResultsListMode(); 1381 } 1382 setAccount((Account) intent.getParcelableExtra(Utils.EXTRA_ACCOUNT)); 1383 restartOptionalLoader(LOADER_RECENT_FOLDERS); 1384 fetchSearchFolder(intent); 1385 } else { 1386 LogUtils.e(LOG_TAG, "Missing account extra from search intent. Finishing"); 1387 mActivity.finish(); 1388 } 1389 } 1390 if (mAccount != null) { 1391 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR); 1392 } 1393 } 1394 1395 /** 1396 * Copy any selected conversations stored in the saved bundle into our selection set, 1397 * triggering {@link ConversationSetObserver} callbacks as our selection set changes. 1398 * 1399 */ 1400 private final void restoreSelectedConversations(Bundle savedState) { 1401 if (savedState == null) { 1402 mSelectedSet.clear(); 1403 return; 1404 } 1405 final ConversationSelectionSet selectedSet = savedState.getParcelable(SAVED_SELECTED_SET); 1406 if (selectedSet == null || selectedSet.isEmpty()) { 1407 mSelectedSet.clear(); 1408 return; 1409 } 1410 1411 // putAll will take care of calling our registered onSetPopulated method 1412 mSelectedSet.putAll(selectedSet); 1413 } 1414 1415 @Override 1416 public SubjectDisplayChanger getSubjectDisplayChanger() { 1417 return mActionBarView; 1418 } 1419 1420 private void showConversation(Conversation conversation) { 1421 showConversation(conversation, false /* inLoaderCallbacks */); 1422 } 1423 1424 /** 1425 * Children can override this method, but they must call super.showConversation(). 1426 * 1427 */ 1428 protected void showConversation(Conversation conversation, boolean inLoaderCallbacks) { 1429 // Set the current conversation just in case it wasn't already set. 1430 setCurrentConversation(conversation); 1431 // Add the folder that we were viewing to the recent folders list. 1432 // TODO: this may need to be fine tuned. If this is the signal that is indicating that 1433 // the list is shown to the user, this could fire in one pane if the user goes directly 1434 // to a conversation 1435 updateRecentFolderList(); 1436 } 1437 1438 /** 1439 * Children can override this method, but they must call super.showWaitForInitialization(). 1440 * {@inheritDoc} 1441 */ 1442 @Override 1443 public void showWaitForInitialization() { 1444 mViewMode.enterWaitingForInitializationMode(); 1445 } 1446 1447 @Override 1448 public void hideWaitForInitialization() { 1449 } 1450 1451 @Override 1452 public void updateWaitMode() { 1453 final FragmentManager manager = mActivity.getFragmentManager(); 1454 final WaitFragment waitFragment = 1455 (WaitFragment)manager.findFragmentByTag(TAG_WAIT); 1456 if (waitFragment != null) { 1457 waitFragment.updateAccount(mAccount); 1458 } 1459 } 1460 1461 /** 1462 * Returns true if we are waiting for the account to sync, and cannot show any folders or 1463 * conversation for the current account yet. 1464 * 1465 */ 1466 public boolean inWaitMode() { 1467 final FragmentManager manager = mActivity.getFragmentManager(); 1468 final WaitFragment waitFragment = 1469 (WaitFragment)manager.findFragmentByTag(TAG_WAIT); 1470 if (waitFragment != null) { 1471 final Account fragmentAccount = waitFragment.getAccount(); 1472 return fragmentAccount.uri.equals(mAccount.uri) && 1473 mViewMode.getMode() == ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION; 1474 } 1475 return false; 1476 } 1477 1478 /** 1479 * Children can override this method, but they must call super.showConversationList(). 1480 * {@inheritDoc} 1481 */ 1482 @Override 1483 public void showConversationList(ConversationListContext listContext) { 1484 } 1485 1486 @Override 1487 public void onConversationSelected(Conversation conversation, boolean inLoaderCallbacks) { 1488 // Only animate destructive actions if we are going to be showing the 1489 // conversation list when we show the next conversation. 1490 commitDestructiveActions(Utils.useTabletUI(mContext)); 1491 showConversation(conversation, inLoaderCallbacks); 1492 } 1493 1494 @Override 1495 public Conversation getCurrentConversation() { 1496 return mCurrentConversation; 1497 } 1498 1499 /** 1500 * Set the current conversation. This is the conversation on which all actions are performed. 1501 * Do not modify mCurrentConversation except through this method, which makes it easy to 1502 * perform common actions associated with changing the current conversation. 1503 * @param conversation 1504 */ 1505 @Override 1506 public void setCurrentConversation(Conversation conversation) { 1507 mCurrentConversation = conversation; 1508 mTracker.initialize(mCurrentConversation); 1509 } 1510 1511 /** 1512 * {@inheritDoc} 1513 */ 1514 @Override 1515 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 1516 // Create a loader to listen in on account changes. 1517 switch (id) { 1518 case LOADER_ACCOUNT_CURSOR: 1519 return new CursorLoader(mContext, MailAppProvider.getAccountsUri(), 1520 UIProvider.ACCOUNTS_PROJECTION, null, null, null); 1521 case LOADER_FOLDER_CURSOR: 1522 final CursorLoader loader = new CursorLoader(mContext, mFolder.uri, 1523 UIProvider.FOLDERS_PROJECTION, null, null, null); 1524 loader.setUpdateThrottle(mFolderItemUpdateDelayMs); 1525 return loader; 1526 case LOADER_RECENT_FOLDERS: 1527 if (mAccount != null && mAccount.recentFolderListUri != null) { 1528 return new CursorLoader(mContext, mAccount.recentFolderListUri, 1529 UIProvider.FOLDERS_PROJECTION, null, null, null); 1530 } 1531 break; 1532 case LOADER_ACCOUNT_INBOX: 1533 final Uri defaultInbox = Settings.getDefaultInboxUri(mAccount.settings); 1534 final Uri inboxUri = defaultInbox.equals(Uri.EMPTY) ? 1535 mAccount.folderListUri : defaultInbox; 1536 LogUtils.d(LOG_TAG, "Loading the default inbox: %s", inboxUri); 1537 if (inboxUri != null) { 1538 return new CursorLoader(mContext, inboxUri, UIProvider.FOLDERS_PROJECTION, null, 1539 null, null); 1540 } 1541 break; 1542 case LOADER_SEARCH: 1543 return Folder.forSearchResults(mAccount, 1544 args.getString(ConversationListContext.EXTRA_SEARCH_QUERY), 1545 mActivity.getActivityContext()); 1546 case LOADER_ACCOUNT_UPDATE_CURSOR: 1547 return new CursorLoader(mContext, mAccount.uri, UIProvider.ACCOUNTS_PROJECTION, 1548 null, null, null); 1549 default: 1550 LogUtils.wtf(LOG_TAG, "Loader returned unexpected id: %d", id); 1551 } 1552 return null; 1553 } 1554 1555 @Override 1556 public void onLoaderReset(Loader<Cursor> loader) { 1557 1558 } 1559 1560 /** 1561 * {@link LoaderManager} currently has a bug in 1562 * {@link LoaderManager#restartLoader(int, Bundle, android.app.LoaderManager.LoaderCallbacks)} 1563 * where, if a previous onCreateLoader returned a null loader, this method will NPE. Work around 1564 * this bug by destroying any loaders that may have been created as null (essentially because 1565 * they are optional loads, and may not apply to a particular account). 1566 * <p> 1567 * A simple null check before restarting a loader will not work, because that would not 1568 * give the controller a chance to invalidate UI corresponding the prior loader result. 1569 * 1570 * @param id loader ID to safely restart 1571 */ 1572 private void restartOptionalLoader(int id) { 1573 final LoaderManager lm = mActivity.getLoaderManager(); 1574 lm.destroyLoader(id); 1575 lm.restartLoader(id, Bundle.EMPTY, this); 1576 } 1577 1578 @Override 1579 public void registerConversationListObserver(DataSetObserver observer) { 1580 mConversationListObservable.registerObserver(observer); 1581 } 1582 1583 @Override 1584 public void unregisterConversationListObserver(DataSetObserver observer) { 1585 mConversationListObservable.unregisterObserver(observer); 1586 } 1587 1588 @Override 1589 public void registerFolderObserver(DataSetObserver observer) { 1590 mFolderObservable.registerObserver(observer); 1591 } 1592 1593 @Override 1594 public void unregisterFolderObserver(DataSetObserver observer) { 1595 mFolderObservable.unregisterObserver(observer); 1596 } 1597 1598 /** 1599 * Returns true if the number of accounts is different, or if the current account has been 1600 * removed from the device 1601 * @param accountCursor 1602 * @return 1603 */ 1604 private boolean accountsUpdated(Cursor accountCursor) { 1605 // Check to see if the current account hasn't been set, or the account cursor is empty 1606 if (mAccount == null || !accountCursor.moveToFirst()) { 1607 return true; 1608 } 1609 1610 // Check to see if the number of accounts are different, from the number we saw on the last 1611 // updated 1612 if (mCurrentAccountUris.size() != accountCursor.getCount()) { 1613 return true; 1614 } 1615 1616 // Check to see if the account list is different or if the current account is not found in 1617 // the cursor. 1618 boolean foundCurrentAccount = false; 1619 do { 1620 final Uri accountUri = 1621 Uri.parse(accountCursor.getString(UIProvider.ACCOUNT_URI_COLUMN)); 1622 if (!foundCurrentAccount && mAccount.uri.equals(accountUri)) { 1623 foundCurrentAccount = true; 1624 } 1625 // Is there a new account that we do not know about? 1626 if (!mCurrentAccountUris.contains(accountUri)) { 1627 return true; 1628 } 1629 } while (accountCursor.moveToNext()); 1630 1631 // As long as we found the current account, the list hasn't been updated 1632 return !foundCurrentAccount; 1633 } 1634 1635 /** 1636 * Updates accounts for the app. If the current account is missing, the first 1637 * account in the list is set to the current account (we <em>have</em> to choose something). 1638 * 1639 * @param accounts cursor into the AccountCache 1640 * @return true if the update was successful, false otherwise 1641 */ 1642 private boolean updateAccounts(Cursor accounts) { 1643 if (accounts == null || !accounts.moveToFirst()) { 1644 return false; 1645 } 1646 1647 final Account[] allAccounts = Account.getAllAccounts(accounts); 1648 // A match for the current account's URI in the list of accounts. 1649 Account currentFromList = null; 1650 1651 // Save the uris for the accounts 1652 mCurrentAccountUris.clear(); 1653 for (Account account : allAccounts) { 1654 LogUtils.d(LOG_TAG, "updateAccounts(%s)", account); 1655 mCurrentAccountUris.add(account.uri); 1656 if (mAccount != null && account.uri.equals(mAccount.uri)) { 1657 currentFromList = account; 1658 } 1659 } 1660 1661 // 1. current account is already set and is in allAccounts: 1662 // 1a. It has changed -> load the updated account. 1663 // 2b. It is unchanged -> no-op 1664 // 2. current account is set and is not in allAccounts -> pick first (acct was deleted?) 1665 // 3. saved preference has an account -> pick that one 1666 // 4. otherwise just pick first 1667 1668 boolean accountChanged = false; 1669 /// Assume case 4, initialize to first account, and see if we can find anything better. 1670 Account newAccount = allAccounts[0]; 1671 if (currentFromList != null) { 1672 // Case 1: Current account exists but has changed 1673 if (!currentFromList.equals(mAccount)) { 1674 newAccount = currentFromList; 1675 accountChanged = true; 1676 } 1677 // Case 1b: else, current account is unchanged: nothing to do. 1678 } else { 1679 // Case 2: Current account is not in allAccounts, the account needs to change. 1680 accountChanged = true; 1681 if (mAccount == null) { 1682 // Case 3: Check for last viewed account, and check if it exists in the list. 1683 final String lastAccountUri = MailAppProvider.getInstance().getLastViewedAccount(); 1684 if (lastAccountUri != null) { 1685 for (final Account account : allAccounts) { 1686 if (lastAccountUri.equals(account.uri.toString())) { 1687 newAccount = account; 1688 break; 1689 } 1690 } 1691 } 1692 } 1693 } 1694 if (accountChanged) { 1695 onAccountChanged(newAccount); 1696 } 1697 // Whether we have updated the current account or not, we need to update the list of 1698 // accounts in the ActionBar. 1699 mActionBarView.setAccounts(allAccounts); 1700 return (allAccounts.length > 0); 1701 } 1702 1703 private void disableNotifications() { 1704 mNewEmailReceiver.activate(mContext, this); 1705 } 1706 1707 private void enableNotifications() { 1708 mNewEmailReceiver.deactivate(); 1709 } 1710 1711 private void disableNotificationsOnAccountChange(Account account) { 1712 // If the new mail suppression receiver is activated for a different account, we want to 1713 // activate it for the new account. 1714 if (mNewEmailReceiver.activated() && 1715 !mNewEmailReceiver.notificationsDisabledForAccount(account)) { 1716 // Deactivate the current receiver, otherwise multiple receivers may be registered. 1717 mNewEmailReceiver.deactivate(); 1718 mNewEmailReceiver.activate(mContext, this); 1719 } 1720 } 1721 1722 /** 1723 * {@inheritDoc} 1724 */ 1725 @Override 1726 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 1727 // We want to reinitialize only if we haven't ever been initialized, or 1728 // if the current account has vanished. 1729 if (data == null) { 1730 LogUtils.e(LOG_TAG, "Received null cursor from loader id: %d", loader.getId()); 1731 } 1732 switch (loader.getId()) { 1733 case LOADER_ACCOUNT_CURSOR: 1734 // If the account list is not null, and the account list cursor is empty, 1735 // we need to start the specified activity. 1736 if (data != null && data.getCount() == 0) { 1737 // If an empty cursor is returned, the MailAppProvider is indicating that 1738 // no accounts have been specified. We want to navigate to the "add account" 1739 // activity that will handle the intent returned by the MailAppProvider 1740 1741 // If the MailAppProvider believes that all accounts have been loaded, and the 1742 // account list is still empty, we want to prompt the user to add an account 1743 final Bundle extras = data.getExtras(); 1744 final boolean accountsLoaded = 1745 extras.getInt(AccountCursorExtraKeys.ACCOUNTS_LOADED) != 0; 1746 1747 if (accountsLoaded) { 1748 final Intent noAccountIntent = MailAppProvider.getNoAccountIntent(mContext); 1749 if (noAccountIntent != null) { 1750 mActivity.startActivityForResult(noAccountIntent, 1751 ADD_ACCOUNT_REQUEST_CODE); 1752 } 1753 } 1754 } else { 1755 final boolean accountListUpdated = accountsUpdated(data); 1756 if (!isLoaderInitialized || accountListUpdated) { 1757 isLoaderInitialized = updateAccounts(data); 1758 } 1759 } 1760 break; 1761 case LOADER_ACCOUNT_UPDATE_CURSOR: 1762 // We have gotten an update for current account. 1763 1764 // Make sure that this is an update for the current account 1765 if (data != null && data.moveToFirst()) { 1766 final Account updatedAccount = new Account(data); 1767 1768 if (updatedAccount.uri.equals(mAccount.uri)) { 1769 // Keep a reference to the previous settings object 1770 final Settings previousSettings = mAccount.settings; 1771 1772 // Update the controller's reference to the current account 1773 mAccount = updatedAccount; 1774 LogUtils.d(LOG_TAG, "AbstractActivityController.onLoadFinished(): " 1775 + "mAccount = %s", mAccount.uri); 1776 1777 // Only notify about a settings change if something differs 1778 if (!Objects.equal(mAccount.settings, previousSettings)) { 1779 mAccountObservers.notifyChanged(); 1780 } 1781 1782 // Got an update for the current account 1783 final boolean inWaitingMode = inWaitMode(); 1784 if (!updatedAccount.isAccountIntialized() && !inWaitingMode) { 1785 // Transition to waiting mode 1786 showWaitForInitialization(); 1787 } else if (updatedAccount.isAccountIntialized()) { 1788 if (inWaitingMode) { 1789 // Dismiss waiting mode 1790 hideWaitForInitialization(); 1791 } 1792 } else if (!updatedAccount.isAccountIntialized() && inWaitingMode) { 1793 // Update the WaitFragment's account object 1794 updateWaitMode(); 1795 } 1796 } else { 1797 LogUtils.e(LOG_TAG, "Got update for account: %s with current account: %s", 1798 updatedAccount.uri, mAccount.uri); 1799 // We need to restart the loader, so the correct account information will 1800 // be returned 1801 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR); 1802 } 1803 } 1804 break; 1805 case LOADER_FOLDER_CURSOR: 1806 // Check status of the cursor. 1807 if (data != null && data.moveToFirst()) { 1808 final Folder folder = new Folder(data); 1809 LogUtils.d(LOG_TAG, "FOLDER STATUS = %d", folder.syncStatus); 1810 1811 mFolder = folder; 1812 mFolderObservable.notifyChanged(); 1813 1814 } else { 1815 LogUtils.d(LOG_TAG, "Unable to get the folder %s", 1816 mFolder != null ? mAccount.name : ""); 1817 } 1818 break; 1819 case LOADER_RECENT_FOLDERS: 1820 // No recent folders and we are running on a phone? Populate the default recents. 1821 if (data != null && data.getCount() == 0 && !Utils.useTabletUI(mContext)) { 1822 final class PopulateDefault extends AsyncTask<Uri, Void, Void> { 1823 @Override 1824 protected Void doInBackground(Uri... uri) { 1825 // Asking for an update on the URI and ignore the result. 1826 final ContentResolver resolver = mContext.getContentResolver(); 1827 resolver.update(uri[0], null, null, null); 1828 return null; 1829 } 1830 } 1831 final Uri uri = mAccount.defaultRecentFolderListUri; 1832 LogUtils.v(LOG_TAG, "Default recents at %s", uri); 1833 new PopulateDefault().execute(uri); 1834 break; 1835 } 1836 LogUtils.v(LOG_TAG, "Reading recent folders from the cursor."); 1837 mRecentFolderList.loadFromUiProvider(data); 1838 mRecentFolderObservers.notifyChanged(); 1839 break; 1840 case LOADER_ACCOUNT_INBOX: 1841 if (data != null && !data.isClosed() && data.moveToFirst()) { 1842 Folder inbox = new Folder(data); 1843 onFolderChanged(inbox); 1844 // Just want to get the inbox, don't care about updates to it 1845 // as this will be tracked by the folder change listener. 1846 mActivity.getLoaderManager().destroyLoader(LOADER_ACCOUNT_INBOX); 1847 } else { 1848 LogUtils.d(LOG_TAG, "Unable to get the account inbox for account %s", 1849 mAccount != null ? mAccount.name : ""); 1850 } 1851 break; 1852 case LOADER_SEARCH: 1853 if (data != null && data.getCount() > 0) { 1854 data.moveToFirst(); 1855 Folder search = new Folder(data); 1856 updateFolder(search); 1857 mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder, 1858 mActivity.getIntent() 1859 .getStringExtra(UIProvider.SearchQueryParameters.QUERY)); 1860 showConversationList(mConvListContext); 1861 mActivity.invalidateOptionsMenu(); 1862 mActivity.getLoaderManager().destroyLoader(LOADER_SEARCH); 1863 } else { 1864 LogUtils.e(LOG_TAG, "Null or empty cursor returned by LOADER_SEARCH loader"); 1865 } 1866 break; 1867 } 1868 } 1869 1870 /** 1871 * Destructive actions on Conversations. This class should only be created by controllers, and 1872 * clients should only require {@link DestructiveAction}s, not specific implementations of the. 1873 * Only the controllers should know what kind of destructive actions are being created. 1874 */ 1875 public class ConversationAction implements DestructiveAction { 1876 /** 1877 * The action to be performed. This is specified as the resource ID of the menu item 1878 * corresponding to this action: R.id.delete, R.id.report_spam, etc. 1879 */ 1880 private final int mAction; 1881 /** The action will act upon these conversations */ 1882 private final Collection<Conversation> mTarget; 1883 /** Whether this destructive action has already been performed */ 1884 private boolean mCompleted; 1885 /** Whether this is an action on the currently selected set. */ 1886 private final boolean mIsSelectedSet; 1887 1888 /** 1889 * Create a listener object. action is one of four constants: R.id.y_button (archive), 1890 * R.id.delete , R.id.mute, and R.id.report_spam. 1891 * @param action 1892 * @param target Conversation that we want to apply the action to. 1893 * @param isBatch whether the conversations are in the currently selected batch set. 1894 */ 1895 public ConversationAction(int action, Collection<Conversation> target, boolean isBatch) { 1896 mAction = action; 1897 mTarget = ImmutableList.copyOf(target); 1898 mIsSelectedSet = isBatch; 1899 } 1900 1901 /** 1902 * The action common to child classes. This performs the action specified in the constructor 1903 * on the conversations given here. 1904 */ 1905 @Override 1906 public void performAction() { 1907 if (isPerformed()) { 1908 return; 1909 } 1910 boolean undoEnabled = mAccount.supportsCapability(AccountCapabilities.UNDO); 1911 1912 // Are we destroying the currently shown conversation? Show the next one. 1913 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)){ 1914 LogUtils.d(LOG_TAG, "ConversationAction.performAction():" 1915 + "\nmTarget=%s\nCurrent=%s", 1916 Conversation.toString(mTarget), mCurrentConversation); 1917 } 1918 1919 if (mConversationListCursor == null) { 1920 LogUtils.e(LOG_TAG, "null ConversationCursor in ConversationAction.performAction():" 1921 + "\nmTarget=%s\nCurrent=%s", 1922 Conversation.toString(mTarget), mCurrentConversation); 1923 return; 1924 } 1925 1926 switch (mAction) { 1927 case R.id.archive: 1928 LogUtils.d(LOG_TAG, "Archiving"); 1929 mConversationListCursor.archive(mContext, mTarget); 1930 break; 1931 case R.id.delete: 1932 LogUtils.d(LOG_TAG, "Deleting"); 1933 mConversationListCursor.delete(mContext, mTarget); 1934 if (mFolder.supportsCapability(FolderCapabilities.DELETE_ACTION_FINAL)) { 1935 undoEnabled = false; 1936 } 1937 break; 1938 case R.id.mute: 1939 LogUtils.d(LOG_TAG, "Muting"); 1940 if (mFolder.supportsCapability(FolderCapabilities.DESTRUCTIVE_MUTE)) { 1941 for (Conversation c : mTarget) { 1942 c.localDeleteOnUpdate = true; 1943 } 1944 } 1945 mConversationListCursor.mute(mContext, mTarget); 1946 break; 1947 case R.id.report_spam: 1948 LogUtils.d(LOG_TAG, "Reporting spam"); 1949 mConversationListCursor.reportSpam(mContext, mTarget); 1950 break; 1951 case R.id.mark_not_spam: 1952 LogUtils.d(LOG_TAG, "Marking not spam"); 1953 mConversationListCursor.reportNotSpam(mContext, mTarget); 1954 break; 1955 case R.id.report_phishing: 1956 LogUtils.d(LOG_TAG, "Reporting phishing"); 1957 mConversationListCursor.reportPhishing(mContext, mTarget); 1958 break; 1959 case R.id.remove_star: 1960 LogUtils.d(LOG_TAG, "Removing star"); 1961 // Star removal is destructive in the Starred folder. 1962 mConversationListCursor.updateBoolean(mContext, mTarget, 1963 ConversationColumns.STARRED, false); 1964 break; 1965 case R.id.mark_not_important: 1966 LogUtils.d(LOG_TAG, "Marking not-important"); 1967 // Marking not important is destructive in a mailbox 1968 // containing only important messages 1969 if (mFolder != null && mFolder.isImportantOnly()) { 1970 for (Conversation conv : mTarget) { 1971 conv.localDeleteOnUpdate = true; 1972 } 1973 } 1974 mConversationListCursor.updateInt(mContext, mTarget, 1975 ConversationColumns.PRIORITY, UIProvider.ConversationPriority.LOW); 1976 break; 1977 case R.id.discard_drafts: 1978 LogUtils.d(LOG_TAG, "Discarding draft messages"); 1979 // Discarding draft messages is destructive in a "draft" mailbox 1980 if (mFolder != null && mFolder.isDraft()) { 1981 for (Conversation conv : mTarget) { 1982 conv.localDeleteOnUpdate = true; 1983 } 1984 } 1985 mConversationListCursor.discardDrafts(mContext, mTarget); 1986 // We don't support undoing discarding drafts 1987 undoEnabled = false; 1988 break; 1989 } 1990 if (undoEnabled) { 1991 mHandler.postDelayed(new Runnable() { 1992 @Override 1993 public void run() { 1994 onUndoAvailable(new ToastBarOperation(mTarget.size(), mAction, 1995 ToastBarOperation.UNDO)); 1996 } 1997 }, mShowUndoBarDelay); 1998 } 1999 refreshConversationList(); 2000 if (mIsSelectedSet) { 2001 mSelectedSet.clear(); 2002 } 2003 } 2004 2005 /** 2006 * Returns true if this action has been performed, false otherwise. 2007 * 2008 */ 2009 private synchronized boolean isPerformed() { 2010 if (mCompleted) { 2011 return true; 2012 } 2013 mCompleted = true; 2014 return false; 2015 } 2016 } 2017 2018 /** 2019 * Get a destructive action for a menu action. 2020 * This is a temporary method, to control the profusion of {@link DestructiveAction} classes 2021 * that are created. Please do not copy this paradigm. 2022 * @param action the resource ID of the menu action: R.id.delete, for example 2023 * @param target the conversations to act upon. 2024 * @return a {@link DestructiveAction} that performs the specified action. 2025 */ 2026 private final DestructiveAction getAction(int action, Collection<Conversation> target) { 2027 final DestructiveAction da = new ConversationAction(action, target, false); 2028 registerDestructiveAction(da); 2029 return da; 2030 } 2031 2032 // Called from the FolderSelectionDialog after a user is done selecting folders to assign the 2033 // conversations to. 2034 @Override 2035 public final void assignFolder(Collection<FolderOperation> folderOps, 2036 Collection<Conversation> target, boolean batch, boolean showUndo) { 2037 // Actions are destructive only when the current folder can be assigned 2038 // to (which is the same as being able to un-assign a conversation from the folder) and 2039 // when the list of folders contains the current folder. 2040 final boolean isDestructive = mFolder 2041 .supportsCapability(FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES) 2042 && FolderOperation.isDestructive(folderOps, mFolder); 2043 LogUtils.d(LOG_TAG, "onFolderChangesCommit: isDestructive = %b", isDestructive); 2044 if (isDestructive) { 2045 for (final Conversation c : target) { 2046 c.localDeleteOnUpdate = true; 2047 } 2048 } 2049 final DestructiveAction folderChange; 2050 // Update the UI elements depending no their visibility and availability 2051 // TODO(viki): Consolidate this into a single method requestDelete. 2052 if (isDestructive) { 2053 folderChange = getDeferredFolderChange(target, folderOps, isDestructive, 2054 batch, showUndo); 2055 delete(target, folderChange); 2056 } else { 2057 folderChange = getFolderChange(target, folderOps, isDestructive, 2058 batch, showUndo); 2059 requestUpdate(target, folderChange); 2060 } 2061 } 2062 2063 @Override 2064 public final void onRefreshRequired() { 2065 if (isAnimating() || isDragging()) { 2066 LogUtils.d(LOG_TAG, "onRefreshRequired: delay until animating done"); 2067 return; 2068 } 2069 // Refresh the query in the background 2070 if (mConversationListCursor.isRefreshRequired()) { 2071 mConversationListCursor.refresh(); 2072 } 2073 } 2074 2075 @Override 2076 public void startDragMode() { 2077 mIsDragHappening = true; 2078 } 2079 2080 @Override 2081 public void stopDragMode() { 2082 mIsDragHappening = false; 2083 if (mConversationListCursor.isRefreshReady()) { 2084 LogUtils.d(LOG_TAG, "Stopped animating: try sync"); 2085 onRefreshReady(); 2086 } 2087 2088 if (mConversationListCursor.isRefreshRequired()) { 2089 LogUtils.d(LOG_TAG, "Stopped animating: refresh"); 2090 mConversationListCursor.refresh(); 2091 } 2092 } 2093 2094 private boolean isDragging() { 2095 return mIsDragHappening; 2096 } 2097 2098 private boolean isAnimating() { 2099 boolean isAnimating = false; 2100 ConversationListFragment convListFragment = getConversationListFragment(); 2101 if (convListFragment != null) { 2102 AnimatedAdapter adapter = convListFragment.getAnimatedAdapter(); 2103 if (adapter != null) { 2104 isAnimating = adapter.isAnimating(); 2105 } 2106 } 2107 return isAnimating; 2108 } 2109 2110 /** 2111 * Called when the {@link ConversationCursor} is changed or has new data in it. 2112 * <p> 2113 * {@inheritDoc} 2114 */ 2115 @Override 2116 public final void onRefreshReady() { 2117 if (!isAnimating()) { 2118 // Swap cursors 2119 mConversationListCursor.sync(); 2120 } 2121 mTracker.onCursorUpdated(); 2122 } 2123 2124 @Override 2125 public final void onDataSetChanged() { 2126 updateConversationListFragment(); 2127 mConversationListObservable.notifyChanged(); 2128 } 2129 2130 /** 2131 * If the Conversation List Fragment is visible, updates the fragment. 2132 */ 2133 private final void updateConversationListFragment() { 2134 final ConversationListFragment convList = getConversationListFragment(); 2135 if (convList != null) { 2136 refreshConversationList(); 2137 if (convList.isVisible()) { 2138 informCursorVisiblity(true); 2139 } 2140 } 2141 } 2142 2143 /** 2144 * This class handles throttled refresh of the conversation list 2145 */ 2146 static class RefreshTimerTask extends TimerTask { 2147 final Handler mHandler; 2148 final AbstractActivityController mController; 2149 2150 RefreshTimerTask(AbstractActivityController controller, Handler handler) { 2151 mHandler = handler; 2152 mController = controller; 2153 } 2154 2155 @Override 2156 public void run() { 2157 mHandler.post(new Runnable() { 2158 @Override 2159 public void run() { 2160 LogUtils.d(LOG_TAG, "Delay done... calling onRefreshRequired"); 2161 mController.onRefreshRequired(); 2162 }}); 2163 } 2164 } 2165 2166 /** 2167 * Cancel the refresh task, if it's running 2168 */ 2169 private void cancelRefreshTask () { 2170 if (mConversationListRefreshTask != null) { 2171 mConversationListRefreshTask.cancel(); 2172 mConversationListRefreshTask = null; 2173 } 2174 } 2175 2176 @Override 2177 public void onAnimationEnd(AnimatedAdapter animatedAdapter) { 2178 if (mConversationListCursor.isRefreshReady()) { 2179 LogUtils.d(LOG_TAG, "Stopped animating: try sync"); 2180 onRefreshReady(); 2181 } 2182 2183 if (mConversationListCursor.isRefreshRequired()) { 2184 LogUtils.d(LOG_TAG, "Stopped animating: refresh"); 2185 mConversationListCursor.refresh(); 2186 } 2187 } 2188 2189 @Override 2190 public void onSetEmpty() { 2191 } 2192 2193 @Override 2194 public void onSetPopulated(ConversationSelectionSet set) { 2195 final ConversationListFragment convList = getConversationListFragment(); 2196 if (convList == null) { 2197 return; 2198 } 2199 mCabActionMenu = new SelectedConversationsActionMenu(mActivity, set, mFolder, 2200 (SwipeableListView) convList.getListView()); 2201 enableCabMode(); 2202 } 2203 2204 @Override 2205 public void onSetChanged(ConversationSelectionSet set) { 2206 // Do nothing. We don't care about changes to the set. 2207 } 2208 2209 @Override 2210 public ConversationSelectionSet getSelectedSet() { 2211 return mSelectedSet; 2212 } 2213 2214 /** 2215 * Disable the Contextual Action Bar (CAB). The selected set is not changed. 2216 */ 2217 protected void disableCabMode() { 2218 // Commit any previous destructive actions when entering/ exiting CAB mode. 2219 commitDestructiveActions(true); 2220 if (mCabActionMenu != null) { 2221 mCabActionMenu.deactivate(); 2222 } 2223 } 2224 2225 /** 2226 * Re-enable the CAB menu if required. The selection set is not changed. 2227 */ 2228 protected void enableCabMode() { 2229 // Commit any previous destructive actions when entering/ exiting CAB mode. 2230 commitDestructiveActions(true); 2231 if (mCabActionMenu != null) { 2232 mCabActionMenu.activate(); 2233 } 2234 } 2235 2236 /** 2237 * Unselect conversations and exit CAB mode. 2238 */ 2239 protected final void exitCabMode() { 2240 mSelectedSet.clear(); 2241 } 2242 2243 @Override 2244 public void startSearch() { 2245 if (mAccount == null) { 2246 // We cannot search if there is no account. Drop the request to the floor. 2247 LogUtils.d(LOG_TAG, "AbstractActivityController.startSearch(): null account"); 2248 return; 2249 } 2250 if (mAccount.supportsCapability(UIProvider.AccountCapabilities.LOCAL_SEARCH) 2251 | mAccount.supportsCapability(UIProvider.AccountCapabilities.SERVER_SEARCH)) { 2252 onSearchRequested(mActionBarView.getQuery()); 2253 } else { 2254 Toast.makeText(mActivity.getActivityContext(), mActivity.getActivityContext() 2255 .getString(R.string.search_unsupported), Toast.LENGTH_SHORT).show(); 2256 } 2257 } 2258 2259 @Override 2260 public void exitSearchMode() { 2261 if (mViewMode.getMode() == ViewMode.SEARCH_RESULTS_LIST) { 2262 mActivity.finish(); 2263 } 2264 } 2265 2266 /** 2267 * Supports dragging conversations to a folder. 2268 */ 2269 @Override 2270 public boolean supportsDrag(DragEvent event, Folder folder) { 2271 return (folder != null 2272 && event != null 2273 && event.getClipDescription() != null 2274 && folder.supportsCapability 2275 (UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES) 2276 && folder.supportsCapability 2277 (UIProvider.FolderCapabilities.CAN_HOLD_MAIL) 2278 && !mFolder.uri.equals(folder.uri)); 2279 } 2280 2281 /** 2282 * Handles dropping conversations to a folder. 2283 */ 2284 @Override 2285 public void handleDrop(DragEvent event, final Folder folder) { 2286 if (!supportsDrag(event, folder)) { 2287 return; 2288 } 2289 final Collection<Conversation> conversations = mSelectedSet.values(); 2290 final Collection<FolderOperation> dropTarget = FolderOperation.listOf(new FolderOperation( 2291 folder, true)); 2292 // Drag and drop is destructive: we remove conversations from the 2293 // current folder. 2294 final DestructiveAction action = getFolderChange(conversations, dropTarget, true, true, 2295 true); 2296 delete(conversations, action); 2297 } 2298 2299 @Override 2300 public void onTouchEvent(MotionEvent event) { 2301 if (event.getAction() == MotionEvent.ACTION_DOWN) { 2302 if (mToastBar != null && !mToastBar.isEventInToastBar(event)) { 2303 mToastBar.hide(true); 2304 } 2305 } 2306 } 2307 2308 @Override 2309 public void onConversationSeen(Conversation conv) { 2310 mPagerController.onConversationSeen(conv); 2311 } 2312 2313 private class ConversationListLoaderCallbacks implements 2314 LoaderManager.LoaderCallbacks<ConversationCursor> { 2315 2316 @Override 2317 public Loader<ConversationCursor> onCreateLoader(int id, Bundle args) { 2318 Loader<ConversationCursor> result = new ConversationCursorLoader((Activity) mActivity, 2319 mAccount, mFolder.conversationListUri, mFolder.name); 2320 return result; 2321 } 2322 2323 @Override 2324 public void onLoadFinished(Loader<ConversationCursor> loader, ConversationCursor data) { 2325 LogUtils.d(LOG_TAG, "IN AAC.ConversationCursor.onLoadFinished, data=%s loader=%s", 2326 data, loader); 2327 // Clear our all pending destructive actions before swapping the conversation cursor 2328 destroyPending(null); 2329 mConversationListCursor = data; 2330 mConversationListCursor.addListener(AbstractActivityController.this); 2331 2332 mTracker.onCursorUpdated(); 2333 2334 mConversationListObservable.notifyChanged(); 2335 // Register the AbstractActivityController as a listener to changes in 2336 // data in the cursor. 2337 final ConversationListFragment convList = getConversationListFragment(); 2338 if (convList != null) { 2339 convList.onCursorUpdated(); 2340 2341 if (convList.isVisible()) { 2342 // The conversation list is visible. 2343 informCursorVisiblity(true); 2344 } 2345 } 2346 // Shown for search results in two-pane mode only. 2347 if (shouldShowFirstConversation()) { 2348 if (mConversationListCursor.getCount() > 0) { 2349 mConversationListCursor.moveToPosition(0); 2350 final Conversation conv = new Conversation(mConversationListCursor); 2351 conv.position = 0; 2352 onConversationSelected(conv, true /* checkSafeToModifyFragments */); 2353 } 2354 } 2355 } 2356 2357 @Override 2358 public void onLoaderReset(Loader<ConversationCursor> loader) { 2359 LogUtils.d(LOG_TAG, "IN AAC.ConversationCursor.onLoaderReset, data=%s loader=%s", 2360 mConversationListCursor, loader); 2361 2362 if (mConversationListCursor != null) { 2363 // Unregister the listener 2364 mConversationListCursor.removeListener(AbstractActivityController.this); 2365 mConversationListCursor = null; 2366 2367 // Inform anyone who is interested about the change 2368 mTracker.onCursorUpdated(); 2369 mConversationListObservable.notifyChanged(); 2370 2371 final ConversationListFragment convList = getConversationListFragment(); 2372 if (convList != null) { 2373 convList.onCursorUpdated(); 2374 } 2375 } 2376 } 2377 } 2378 2379 /** 2380 * Destroy the pending {@link DestructiveAction} till now and assign the given action as the 2381 * next destructive action.. 2382 * @param nextAction the next destructive action to be performed. This can be null. 2383 */ 2384 private final void destroyPending(DestructiveAction nextAction) { 2385 // If there is a pending action, perform that first. 2386 if (mPendingDestruction != null) { 2387 mPendingDestruction.performAction(); 2388 } 2389 mPendingDestruction = nextAction; 2390 } 2391 2392 /** 2393 * Register a destructive action with the controller. This performs the previous destructive 2394 * action as a side effect. This method is final because we don't want the child classes to 2395 * embellish this method any more. 2396 * @param action 2397 */ 2398 private final void registerDestructiveAction(DestructiveAction action) { 2399 // TODO(viki): This is not a good idea. The best solution is for clients to request a 2400 // destructive action from the controller and for the controller to own the action. This is 2401 // a half-way solution while refactoring DestructiveAction. 2402 destroyPending(action); 2403 return; 2404 } 2405 2406 @Override 2407 public final DestructiveAction getBatchAction(int action) { 2408 final DestructiveAction da = new ConversationAction(action, mSelectedSet.values(), true); 2409 registerDestructiveAction(da); 2410 return da; 2411 } 2412 2413 @Override 2414 public final DestructiveAction getDeferredBatchAction(int action) { 2415 final DestructiveAction da = new ConversationAction(action, mSelectedSet.values(), true); 2416 return da; 2417 } 2418 2419 /** 2420 * Class to change the folders that are assigned to a set of conversations. This is destructive 2421 * because the user can remove the current folder from the conversation, in which case it has 2422 * to be animated away from the current folder. 2423 */ 2424 private class FolderDestruction implements DestructiveAction { 2425 private final Collection<Conversation> mTarget; 2426 private final ArrayList<FolderOperation> mFolderOps = new ArrayList<FolderOperation>(); 2427 private final boolean mIsDestructive; 2428 /** Whether this destructive action has already been performed */ 2429 private boolean mCompleted; 2430 private boolean mIsSelectedSet; 2431 private boolean mShowUndo; 2432 private int mAction; 2433 2434 /** 2435 * Create a new folder destruction object to act on the given conversations. 2436 * @param target 2437 */ 2438 private FolderDestruction(final Collection<Conversation> target, 2439 final Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch, 2440 boolean showUndo, int action) { 2441 mTarget = ImmutableList.copyOf(target); 2442 mFolderOps.addAll(folders); 2443 mIsDestructive = isDestructive; 2444 mIsSelectedSet = isBatch; 2445 mShowUndo = showUndo; 2446 mAction = action; 2447 } 2448 2449 @Override 2450 public void performAction() { 2451 if (isPerformed()) { 2452 return; 2453 } 2454 if (mIsDestructive && mShowUndo) { 2455 ToastBarOperation undoOp = new ToastBarOperation(mTarget.size(), 2456 mAction, ToastBarOperation.UNDO); 2457 onUndoAvailable(undoOp); 2458 } 2459 // For each conversation, for each operation, add/ remove the 2460 // appropriate folders. 2461 ArrayList<String> updatedTargetFolders = new ArrayList<String>(mTarget.size()); 2462 for (Conversation target : mTarget) { 2463 HashMap<Uri, Folder> targetFolders = Folder 2464 .hashMapForFolders(target.getRawFolders()); 2465 if (mIsDestructive) { 2466 target.localDeleteOnUpdate = true; 2467 } 2468 for (FolderOperation op : mFolderOps) { 2469 if (op.mAdd) { 2470 targetFolders.put(op.mFolder.uri, op.mFolder); 2471 } else { 2472 targetFolders.remove(op.mFolder.uri); 2473 } 2474 } 2475 updatedTargetFolders.add(Folder.getSerializedFolderString(targetFolders.values())); 2476 } 2477 if (mConversationListCursor != null) { 2478 mConversationListCursor.updateStrings(mContext, mTarget, 2479 Conversation.UPDATE_FOLDER_COLUMN, updatedTargetFolders); 2480 } 2481 refreshConversationList(); 2482 if (mIsSelectedSet) { 2483 mSelectedSet.clear(); 2484 } 2485 } 2486 2487 /** 2488 * Returns true if this action has been performed, false otherwise. 2489 * 2490 */ 2491 private synchronized boolean isPerformed() { 2492 if (mCompleted) { 2493 return true; 2494 } 2495 mCompleted = true; 2496 return false; 2497 } 2498 } 2499 2500 public final DestructiveAction getFolderChange(Collection<Conversation> target, 2501 Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch, 2502 boolean showUndo) { 2503 final DestructiveAction da = getDeferredFolderChange(target, folders, isDestructive, 2504 isBatch, showUndo); 2505 registerDestructiveAction(da); 2506 return da; 2507 } 2508 2509 public final DestructiveAction getDeferredFolderChange(Collection<Conversation> target, 2510 Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch, 2511 boolean showUndo) { 2512 final DestructiveAction da = new FolderDestruction(target, folders, isDestructive, isBatch, 2513 showUndo, R.id.change_folder); 2514 return da; 2515 } 2516 2517 @Override 2518 public final DestructiveAction getDeferredRemoveFolder(Collection<Conversation> target, 2519 Folder toRemove, boolean isDestructive, boolean isBatch, 2520 boolean showUndo) { 2521 Collection<FolderOperation> folderOps = new ArrayList<FolderOperation>(); 2522 folderOps.add(new FolderOperation(toRemove, false)); 2523 return new FolderDestruction(target, folderOps, isDestructive, isBatch, 2524 showUndo, R.id.remove_folder); 2525 } 2526 2527 private final DestructiveAction getRemoveFolder(Collection<Conversation> target, 2528 Folder toRemove, boolean isDestructive, boolean isBatch, boolean showUndo) { 2529 DestructiveAction da = getDeferredRemoveFolder(target, toRemove, isDestructive, isBatch, 2530 showUndo); 2531 registerDestructiveAction(da); 2532 return da; 2533 } 2534 2535 @Override 2536 public final void refreshConversationList() { 2537 final ConversationListFragment convList = getConversationListFragment(); 2538 if (convList == null) { 2539 return; 2540 } 2541 convList.requestListRefresh(); 2542 } 2543 2544 protected final ActionClickedListener getUndoClickedListener( 2545 final AnimatedAdapter listAdapter) { 2546 return new ActionClickedListener() { 2547 @Override 2548 public void onActionClicked() { 2549 if (mAccount.undoUri != null) { 2550 // NOTE: We might want undo to return the messages affected, in which case 2551 // the resulting cursor might be interesting... 2552 // TODO: Use UIProvider.SEQUENCE_QUERY_PARAMETER to indicate the set of 2553 // commands to undo 2554 if (mConversationListCursor != null) { 2555 mConversationListCursor.undo( 2556 mActivity.getActivityContext(), mAccount.undoUri); 2557 } 2558 if (listAdapter != null) { 2559 listAdapter.setUndo(true); 2560 } 2561 } 2562 } 2563 }; 2564 } 2565 2566 protected final void showErrorToast(final Folder folder, boolean replaceVisibleToast) { 2567 mToastBar.setConversationMode(false); 2568 2569 ActionClickedListener listener = null; 2570 int actionTextResourceId; 2571 final int lastSyncResult = folder.lastSyncResult; 2572 switch (lastSyncResult) { 2573 case UIProvider.LastSyncResult.CONNECTION_ERROR: 2574 listener = getRetryClickedListener(folder); 2575 actionTextResourceId = R.string.retry; 2576 break; 2577 case UIProvider.LastSyncResult.AUTH_ERROR: 2578 listener = getSignInClickedListener(); 2579 actionTextResourceId = R.string.signin; 2580 break; 2581 case UIProvider.LastSyncResult.SECURITY_ERROR: 2582 return; // Currently we do nothing for security errors. 2583 case UIProvider.LastSyncResult.STORAGE_ERROR: 2584 listener = getStorageErrorClickedListener(); 2585 actionTextResourceId = R.string.info; 2586 break; 2587 case UIProvider.LastSyncResult.INTERNAL_ERROR: 2588 listener = getInternalErrorClickedListener(); 2589 actionTextResourceId = R.string.report; 2590 break; 2591 default: 2592 return; 2593 } 2594 mToastBar.show( 2595 listener, 2596 R.drawable.ic_alert_white, 2597 Utils.getSyncStatusText(mActivity.getActivityContext(), 2598 lastSyncResult), 2599 false, /* showActionIcon */ 2600 actionTextResourceId, 2601 replaceVisibleToast, 2602 new ToastBarOperation(1, 0, ToastBarOperation.ERROR)); 2603 } 2604 2605 private ActionClickedListener getRetryClickedListener(final Folder folder) { 2606 return new ActionClickedListener() { 2607 @Override 2608 public void onActionClicked() { 2609 final Uri uri = folder.refreshUri; 2610 2611 if (uri != null) { 2612 startAsyncRefreshTask(uri); 2613 } 2614 } 2615 }; 2616 } 2617 2618 private ActionClickedListener getSignInClickedListener() { 2619 return new ActionClickedListener() { 2620 @Override 2621 public void onActionClicked() { 2622 promptUserForAuthentication(mAccount); 2623 } 2624 }; 2625 } 2626 2627 private ActionClickedListener getStorageErrorClickedListener() { 2628 return new ActionClickedListener() { 2629 @Override 2630 public void onActionClicked() { 2631 showStorageErrorDialog(); 2632 } 2633 }; 2634 } 2635 2636 private void showStorageErrorDialog() { 2637 DialogFragment fragment = (DialogFragment) 2638 mFragmentManager.findFragmentByTag(SYNC_ERROR_DIALOG_FRAGMENT_TAG); 2639 if (fragment == null) { 2640 fragment = SyncErrorDialogFragment.newInstance(); 2641 } 2642 fragment.show(mFragmentManager, SYNC_ERROR_DIALOG_FRAGMENT_TAG); 2643 } 2644 2645 private ActionClickedListener getInternalErrorClickedListener() { 2646 return new ActionClickedListener() { 2647 @Override 2648 public void onActionClicked() { 2649 Utils.sendFeedback( 2650 mActivity.getActivityContext(), mAccount, true /* reportingProblem */); 2651 } 2652 }; 2653 } 2654 2655 @Override 2656 public void onFooterViewErrorActionClick(Folder folder, int errorStatus) { 2657 Uri uri = null; 2658 switch (errorStatus) { 2659 case UIProvider.LastSyncResult.CONNECTION_ERROR: 2660 if (folder != null && folder.refreshUri != null) { 2661 uri = folder.refreshUri; 2662 } 2663 break; 2664 case UIProvider.LastSyncResult.AUTH_ERROR: 2665 promptUserForAuthentication(mAccount); 2666 return; 2667 case UIProvider.LastSyncResult.SECURITY_ERROR: 2668 return; // Currently we do nothing for security errors. 2669 case UIProvider.LastSyncResult.STORAGE_ERROR: 2670 showStorageErrorDialog(); 2671 return; 2672 case UIProvider.LastSyncResult.INTERNAL_ERROR: 2673 Utils.sendFeedback( 2674 mActivity.getActivityContext(), mAccount, true /* reportingProblem */); 2675 return; 2676 default: 2677 return; 2678 } 2679 2680 if (uri != null) { 2681 startAsyncRefreshTask(uri); 2682 } 2683 } 2684 2685 @Override 2686 public void onFooterViewLoadMoreClick(Folder folder) { 2687 if (folder != null && folder.loadMoreUri != null) { 2688 startAsyncRefreshTask(folder.loadMoreUri); 2689 } 2690 } 2691 2692 private void startAsyncRefreshTask(Uri uri) { 2693 if (mFolderSyncTask != null) { 2694 mFolderSyncTask.cancel(true); 2695 } 2696 mFolderSyncTask = new AsyncRefreshTask(mActivity.getActivityContext(), uri); 2697 mFolderSyncTask.execute(); 2698 } 2699 2700 private void promptUserForAuthentication(Account account) { 2701 if (account != null && !Utils.isEmpty(account.reauthenticationIntentUri)) { 2702 final Intent authenticationIntent = 2703 new Intent(Intent.ACTION_VIEW, account.reauthenticationIntentUri); 2704 mActivity.startActivityForResult(authenticationIntent, REAUTHENTICATE_REQUEST_CODE); 2705 } 2706 } 2707} 2708