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