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