AbstractActivityController.java revision 5db676608eb00d3b1830344bf979a1bb5c093b8c
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.animation.ValueAnimator; 21import android.app.ActionBar; 22import android.app.ActionBar.LayoutParams; 23import android.app.Activity; 24import android.app.AlertDialog; 25import android.app.Dialog; 26import android.app.DialogFragment; 27import android.app.Fragment; 28import android.app.FragmentManager; 29import android.app.LoaderManager; 30import android.app.SearchManager; 31import android.content.ContentProviderOperation; 32import android.content.ContentResolver; 33import android.content.ContentValues; 34import android.content.Context; 35import android.content.DialogInterface; 36import android.content.DialogInterface.OnClickListener; 37import android.content.Intent; 38import android.content.Loader; 39import android.content.res.Configuration; 40import android.content.res.Resources; 41import android.database.Cursor; 42import android.database.DataSetObservable; 43import android.database.DataSetObserver; 44import android.database.Observable; 45import android.net.Uri; 46import android.os.AsyncTask; 47import android.os.Bundle; 48import android.os.Handler; 49import android.os.Parcelable; 50import android.provider.SearchRecentSuggestions; 51import android.support.v4.app.ActionBarDrawerToggle; 52import android.support.v4.widget.DrawerLayout; 53import android.view.DragEvent; 54import android.view.Gravity; 55import android.view.KeyEvent; 56import android.view.LayoutInflater; 57import android.view.Menu; 58import android.view.MenuInflater; 59import android.view.MenuItem; 60import android.view.MotionEvent; 61import android.view.View; 62import android.widget.ListView; 63import android.widget.Toast; 64 65import com.android.mail.ConversationListContext; 66import com.android.mail.MailLogService; 67import com.android.mail.R; 68import com.android.mail.analytics.Analytics; 69import com.android.mail.analytics.AnalyticsTimer; 70import com.android.mail.analytics.AnalyticsUtils; 71import com.android.mail.browse.ConfirmDialogFragment; 72import com.android.mail.browse.ConversationCursor; 73import com.android.mail.browse.ConversationCursor.ConversationOperation; 74import com.android.mail.browse.ConversationItemViewModel; 75import com.android.mail.browse.ConversationMessage; 76import com.android.mail.browse.ConversationPagerController; 77import com.android.mail.browse.SelectedConversationsActionMenu; 78import com.android.mail.browse.SyncErrorDialogFragment; 79import com.android.mail.browse.UndoCallback; 80import com.android.mail.compose.ComposeActivity; 81import com.android.mail.content.CursorCreator; 82import com.android.mail.content.ObjectCursor; 83import com.android.mail.content.ObjectCursorLoader; 84import com.android.mail.providers.Account; 85import com.android.mail.providers.Conversation; 86import com.android.mail.providers.ConversationInfo; 87import com.android.mail.providers.Folder; 88import com.android.mail.providers.FolderWatcher; 89import com.android.mail.providers.MailAppProvider; 90import com.android.mail.providers.Settings; 91import com.android.mail.providers.SuggestionsProvider; 92import com.android.mail.providers.UIProvider; 93import com.android.mail.providers.UIProvider.AccountCapabilities; 94import com.android.mail.providers.UIProvider.AccountColumns; 95import com.android.mail.providers.UIProvider.AccountCursorExtraKeys; 96import com.android.mail.providers.UIProvider.AutoAdvance; 97import com.android.mail.providers.UIProvider.ConversationColumns; 98import com.android.mail.providers.UIProvider.ConversationOperations; 99import com.android.mail.providers.UIProvider.FolderCapabilities; 100import com.android.mail.providers.UIProvider.FolderType; 101import com.android.mail.ui.ActionableToastBar.ActionClickedListener; 102import com.android.mail.utils.ContentProviderTask; 103import com.android.mail.utils.DrawIdler; 104import com.android.mail.utils.LogTag; 105import com.android.mail.utils.LogUtils; 106import com.android.mail.utils.MailObservable; 107import com.android.mail.utils.NotificationActionUtils; 108import com.android.mail.utils.Utils; 109import com.android.mail.utils.VeiledAddressMatcher; 110import com.google.common.base.Objects; 111import com.google.common.collect.ImmutableList; 112import com.google.common.collect.Lists; 113import com.google.common.collect.Sets; 114 115import java.util.ArrayList; 116import java.util.Arrays; 117import java.util.Collection; 118import java.util.Collections; 119import java.util.Deque; 120import java.util.HashMap; 121import java.util.List; 122import java.util.Set; 123import java.util.TimerTask; 124 125 126/** 127 * This is an abstract implementation of the Activity Controller. This class 128 * knows how to respond to menu items, state changes, layout changes, etc. It 129 * weaves together the views and listeners, dispatching actions to the 130 * respective underlying classes. 131 * <p> 132 * Even though this class is abstract, it should provide default implementations 133 * for most, if not all the methods in the ActivityController interface. This 134 * makes the task of the subclasses easier: OnePaneActivityController and 135 * TwoPaneActivityController can be concise when the common functionality is in 136 * AbstractActivityController. 137 * </p> 138 * <p> 139 * In the Gmail codebase, this was called BaseActivityController 140 * </p> 141 */ 142public abstract class AbstractActivityController implements ActivityController, 143 EmptyFolderDialogFragment.EmptyFolderDialogFragmentListener { 144 // Keys for serialization of various information in Bundles. 145 /** Tag for {@link #mAccount} */ 146 private static final String SAVED_ACCOUNT = "saved-account"; 147 /** Tag for {@link #mFolder} */ 148 private static final String SAVED_FOLDER = "saved-folder"; 149 /** Tag for {@link #mCurrentConversation} */ 150 private static final String SAVED_CONVERSATION = "saved-conversation"; 151 /** Tag for {@link #mSelectedSet} */ 152 private static final String SAVED_SELECTED_SET = "saved-selected-set"; 153 /** Tag for {@link ActionableToastBar#getOperation()} */ 154 private static final String SAVED_TOAST_BAR_OP = "saved-toast-bar-op"; 155 /** Tag for {@link #mFolderListFolder} */ 156 private static final String SAVED_HIERARCHICAL_FOLDER = "saved-hierarchical-folder"; 157 /** Tag for {@link ConversationListContext#searchQuery} */ 158 private static final String SAVED_QUERY = "saved-query"; 159 /** Tag for {@link #mDialogAction} */ 160 private static final String SAVED_ACTION = "saved-action"; 161 /** Tag for {@link #mDialogFromSelectedSet} */ 162 private static final String SAVED_ACTION_FROM_SELECTED = "saved-action-from-selected"; 163 /** Tag for {@link #mDetachedConvUri} */ 164 private static final String SAVED_DETACHED_CONV_URI = "saved-detached-conv-uri"; 165 /** Key to store {@link #mInbox}. */ 166 private static final String SAVED_INBOX_KEY = "m-inbox"; 167 /** Key to store {@link #mConversationListScrollPositions} */ 168 private static final String SAVED_CONVERSATION_LIST_SCROLL_POSITIONS = 169 "saved-conversation-list-scroll-positions"; 170 171 /** Tag used when loading a wait fragment */ 172 protected static final String TAG_WAIT = "wait-fragment"; 173 /** Tag used when loading a conversation list fragment. */ 174 public static final String TAG_CONVERSATION_LIST = "tag-conversation-list"; 175 /** Tag used when loading a custom fragment. */ 176 protected static final String TAG_CUSTOM_FRAGMENT = "tag-custom-fragment"; 177 178 /** Key to store an account in a bundle */ 179 private final String BUNDLE_ACCOUNT_KEY = "account"; 180 /** Key to store a folder in a bundle */ 181 private final String BUNDLE_FOLDER_KEY = "folder"; 182 /** 183 * Key to set a flag for the ConversationCursorLoader to ignore any 184 * initial load limit that may be set by the Account. Instead, 185 * perform a full load instead of the full-stage load. 186 */ 187 private final String BUNDLE_IGNORE_INITIAL_CONVERSATION_LIMIT_KEY = 188 "ignore-initial-conversation-limit"; 189 190 protected Account mAccount; 191 protected Folder mFolder; 192 protected Folder mInbox; 193 /** True when {@link #mFolder} is first shown to the user. */ 194 private boolean mFolderChanged = false; 195 protected MailActionBarView mActionBarView; 196 protected final MailActivity mActivity; 197 protected final Context mContext; 198 private final FragmentManager mFragmentManager; 199 protected final RecentFolderList mRecentFolderList; 200 protected ConversationListContext mConvListContext; 201 protected Conversation mCurrentConversation; 202 /** 203 * The hash of {@link #mCurrentConversation} in detached mode. 0 if we are not in detached mode. 204 */ 205 private Uri mDetachedConvUri; 206 207 /** A map of {@link Folder} {@link Uri} to scroll position in the conversation list. */ 208 private final Bundle mConversationListScrollPositions = new Bundle(); 209 210 /** A {@link android.content.BroadcastReceiver} that suppresses new e-mail notifications. */ 211 private SuppressNotificationReceiver mNewEmailReceiver = null; 212 213 /** Handler for all our local runnables. */ 214 protected Handler mHandler = new Handler(); 215 216 /** 217 * The current mode of the application. All changes in mode are initiated by 218 * the activity controller. View mode changes are propagated to classes that 219 * attach themselves as listeners of view mode changes. 220 */ 221 protected final ViewMode mViewMode; 222 protected ContentResolver mResolver; 223 protected boolean mHaveAccountList = false; 224 private AsyncRefreshTask mAsyncRefreshTask; 225 226 private boolean mDestroyed; 227 228 /** True if running on tablet */ 229 private final boolean mIsTablet; 230 231 /** 232 * Are we in a point in the Activity/Fragment lifecycle where it's safe to execute fragment 233 * transactions? (including back stack manipulation) 234 * <p> 235 * Per docs in {@link FragmentManager#beginTransaction()}, this flag starts out true, switches 236 * to false after {@link Activity#onSaveInstanceState}, and becomes true again in both onStart 237 * and onResume. 238 */ 239 private boolean mSafeToModifyFragments = true; 240 241 private final Set<Uri> mCurrentAccountUris = Sets.newHashSet(); 242 protected ConversationCursor mConversationListCursor; 243 private final DataSetObservable mConversationListObservable = new MailObservable("List"); 244 245 /** Runnable that checks the logging level to enable/disable the logging service. */ 246 private Runnable mLogServiceChecker = null; 247 /** List of all accounts currently known to the controller. This is never null. */ 248 private Account[] mAllAccounts = new Account[0]; 249 250 private FolderWatcher mFolderWatcher; 251 252 private boolean mIgnoreInitialConversationLimit; 253 254 /** 255 * Interface for actions that are deferred until after a load completes. This is for handling 256 * user actions which affect cursors (e.g. marking messages read or unread) that happen before 257 * that cursor is loaded. 258 */ 259 private interface LoadFinishedCallback { 260 void onLoadFinished(); 261 } 262 263 /** The deferred actions to execute when mConversationListCursor load completes. */ 264 private final ArrayList<LoadFinishedCallback> mConversationListLoadFinishedCallbacks = 265 new ArrayList<LoadFinishedCallback>(); 266 267 private RefreshTimerTask mConversationListRefreshTask; 268 269 /** Listeners that are interested in changes to the current account. */ 270 private final DataSetObservable mAccountObservers = new MailObservable("Account"); 271 /** Listeners that are interested in changes to the recent folders. */ 272 private final DataSetObservable mRecentFolderObservers = new MailObservable("RecentFolder"); 273 /** Listeners that are interested in changes to the list of all accounts. */ 274 private final DataSetObservable mAllAccountObservers = new MailObservable("AllAccounts"); 275 /** Listeners that are interested in changes to the current folder. */ 276 private final DataSetObservable mFolderObservable = new MailObservable("CurrentFolder"); 277 /** Listeners that are interested in changes to the drawer state. */ 278 private final DataSetObservable mDrawerObservers = new MailObservable("Drawer"); 279 280 /** 281 * Selected conversations, if any. 282 */ 283 private final ConversationSelectionSet mSelectedSet = new ConversationSelectionSet(); 284 285 private final int mFolderItemUpdateDelayMs; 286 287 /** Keeps track of selected and unselected conversations */ 288 final protected ConversationPositionTracker mTracker; 289 290 /** 291 * Action menu associated with the selected set. 292 */ 293 SelectedConversationsActionMenu mCabActionMenu; 294 protected ActionableToastBar mToastBar; 295 protected ConversationPagerController mPagerController; 296 297 // This is split out from the general loader dispatcher because its loader doesn't return a 298 // basic Cursor 299 /** Handles loader callbacks to create a convesation cursor. */ 300 private final ConversationListLoaderCallbacks mListCursorCallbacks = 301 new ConversationListLoaderCallbacks(); 302 303 /** Object that listens to all LoaderCallbacks that result in {@link Folder} creation. */ 304 private final FolderLoads mFolderCallbacks = new FolderLoads(); 305 /** Object that listens to all LoaderCallbacks that result in {@link Account} creation. */ 306 private final AccountLoads mAccountCallbacks = new AccountLoads(); 307 308 /** 309 * Matched addresses that must be shielded from users because they are temporary. Even though 310 * this is instantiated from settings, this matcher is valid for all accounts, and is expected 311 * to live past the life of an account. 312 */ 313 private final VeiledAddressMatcher mVeiledMatcher; 314 315 protected static final String LOG_TAG = LogTag.getLogTag(); 316 317 // Loader constants: Accounts 318 /** 319 * The list of accounts. This loader is started early in the application life-cycle since 320 * the list of accounts is central to all other data the application needs: unread counts for 321 * folders, critical UI settings like show/hide checkboxes, ... 322 * The loader is started when the application is created: both in 323 * {@link #onCreate(Bundle)} and in {@link #onActivityResult(int, int, Intent)}. It is never 324 * destroyed since the cursor is needed through the life of the application. When the list of 325 * accounts changes, we notify {@link #mAllAccountObservers}. 326 */ 327 private static final int LOADER_ACCOUNT_CURSOR = 0; 328 329 /** 330 * The current account. This loader is started when we have an account. The mail application 331 * <b>needs</b> a valid account to function. As soon as we set {@link #mAccount}, 332 * we start a loader to observe for changes on the current account. 333 * The loader is always restarted when an account is set in {@link #setAccount(Account)}. 334 * When the current account object changes, we notify {@link #mAccountObservers}. 335 * A possible performance improvement would be to listen purely on 336 * {@link #LOADER_ACCOUNT_CURSOR}. The current account is guaranteed to be in the list, 337 * and would avoid two updates when a single setting on the current account changes. 338 */ 339 private static final int LOADER_ACCOUNT_UPDATE_CURSOR = 7; 340 341 // Loader constants: Folders 342 /** The current folder. This loader watches for updates to the current folder in a manner 343 * analogous to the {@link #LOADER_ACCOUNT_UPDATE_CURSOR}. Updates to the current folder 344 * might be due to server-side changes (unread count), or local changes (sync window or sync 345 * status change). 346 * The change of current folder calls {@link #updateFolder(Folder)}. 347 * This is responsible for restarting a loader using the URI of the provided folder. When the 348 * loader returns, the current folder is updated and consumers, if any, are notified. 349 * When the current folder changes, we notify {@link #mFolderObservable} 350 */ 351 private static final int LOADER_FOLDER_CURSOR = 2; 352 /** 353 * The list of recent folders. Recent folders are shown in the DrawerFragment. The recent 354 * folders are tied to the current account being viewed. When the account is changed, 355 * we restart this loader to retrieve the recent accounts. Recents are pre-populated for 356 * phones historically, when they were displayed in the spinner. On the tablet, 357 * they showed in the {@link FolderListFragment} and were not-populated. The code to 358 * pre-populate the recents is somewhat convoluted: when the loader returns a short list of 359 * recent folders, it issues an update on the Recent Folder URI. The underlying provider then 360 * does the appropriate thing to populate recent folders, and notify of a change on the cursor. 361 * Recent folders are needed for the life of the current account. 362 * When the recent folders change, we notify {@link #mRecentFolderObservers}. 363 */ 364 private static final int LOADER_RECENT_FOLDERS = 3; 365 /** 366 * The primary inbox for the current account. The mechanism to load the default inbox for the 367 * current account is (sadly) different from loading other folders. The method 368 * {@link #loadAccountInbox()} is called, and it restarts this loader. When the loader returns 369 * a valid cursor, we create a folder, call {@link #onFolderChanged{Folder)} eventually 370 * calling {@link #updateFolder(Folder)} which starts a loader {@link #LOADER_FOLDER_CURSOR} 371 * over the current folder. 372 * When we have a valid cursor, we destroy this loader, This convoluted flow is historical. 373 */ 374 private static final int LOADER_ACCOUNT_INBOX = 5; 375 /** 376 * The fake folder of search results for a term. When we search for a term, 377 * a new activity is created with {@link Intent#ACTION_SEARCH}. For this new activity, 378 * we start a loader which returns conversations that match the user-provided query. 379 * We destroy the loader when we obtain a valid cursor since subsequent searches will create 380 * a new activity. 381 */ 382 private static final int LOADER_SEARCH = 6; 383 /** 384 * The initial folder at app start. When the application is launched from an intent that 385 * specifies the initial folder (notifications/widgets/shortcuts), 386 * then we extract the folder URI from the intent, but we cannot trust the folder object. Since 387 * shortcuts and widgets persist past application update, they might have incorrect 388 * information encoded in them. So, to obtain a {@link Folder} object from a {@link Uri}, 389 * we need to start another loader. Upon obtaining a valid cursor, the loader is destroyed. 390 * An additional complication arises if we have to view a specific conversation within this 391 * folder. This is the case when launching the app from a single conversation notification 392 * or tapping on a specific conversation in the widget. In these cases, the conversation is 393 * saved in {@link #mConversationToShow} and is retrieved when the loader returns. 394 */ 395 public static final int LOADER_FIRST_FOLDER = 8; 396 397 // Loader constants: Conversations 398 /** The conversation cursor over the current conversation list. This loader provides 399 * a cursor over conversation entries from a folder to display a conversation 400 * list. 401 * This loader is started when the user switches folders (in {@link #updateFolder(Folder)}, 402 * or when the controller is told that a folder/account change is imminent 403 * (in {@link #preloadConvList(Account, Folder)}. The loader is maintained for the life of 404 * the current folder. When the user switches folders, the old loader is destroyed and a new 405 * one is created. 406 * 407 * When the conversation list changes, we notify {@link #mConversationListObservable}. 408 */ 409 private static final int LOADER_CONVERSATION_LIST = 4; 410 411 /** 412 * Guaranteed to be the last loader ID used by the activity. Loaders are owned by Activity or 413 * fragments, and within an activity, loader IDs need to be unique. A hack to ensure that the 414 * {@link FolderWatcher} can create its folder loaders without clashing with the IDs of those 415 * of the {@link AbstractActivityController}. Currently, the {@link FolderWatcher} is the only 416 * other class that uses this activity's LoaderManager. If another class needs activity-level 417 * loaders, consider consolidating the loaders in a central location: a UI-less fragment 418 * perhaps. 419 */ 420 public static final int LAST_LOADER_ID = 100; 421 /** 422 * Guaranteed to be the last loader ID used by the Fragment. Loaders are owned by Activity or 423 * fragments, and within an activity, loader IDs need to be unique. Currently, 424 * {@link SectionedInboxTeaserView} is the only class that uses the 425 * {@link ConversationListFragment}'s LoaderManager. 426 */ 427 public static final int LAST_FRAGMENT_LOADER_ID = 1000; 428 429 /** Code returned after an account has been added. */ 430 private static final int ADD_ACCOUNT_REQUEST_CODE = 1; 431 /** Code returned when the user has to enter the new password on an existing account. */ 432 private static final int REAUTHENTICATE_REQUEST_CODE = 2; 433 /** Code returned when the previous activity needs to navigate to a different folder 434 * or account */ 435 private static final int CHANGE_NAVIGATION_REQUEST_CODE = 3; 436 437 public static final String EXTRA_FOLDER = "extra-folder"; 438 public static final String EXTRA_ACCOUNT = "extra-account"; 439 440 /** The pending destructive action to be carried out before swapping the conversation cursor.*/ 441 private DestructiveAction mPendingDestruction; 442 protected AsyncRefreshTask mFolderSyncTask; 443 private Folder mFolderListFolder; 444 private boolean mIsDragHappening; 445 private final int mShowUndoBarDelay; 446 private boolean mRecentsDataUpdated; 447 /** A wait fragment we added, if any. */ 448 private WaitFragment mWaitFragment; 449 /** True if we have results from a search query */ 450 private boolean mHaveSearchResults = false; 451 /** If a confirmation dialog is being show, the listener for the positive action. */ 452 private OnClickListener mDialogListener; 453 /** 454 * If a confirmation dialog is being show, the resource of the action: R.id.delete, etc. This 455 * is used to create a new {@link #mDialogListener} on orientation changes. 456 */ 457 private int mDialogAction = -1; 458 /** 459 * If a confirmation dialog is being shown, this is true if the dialog acts on the selected set 460 * and false if it acts on the currently selected conversation 461 */ 462 private boolean mDialogFromSelectedSet; 463 464 /** Which conversation to show, if started from widget/notification. */ 465 private Conversation mConversationToShow = null; 466 467 /** 468 * A temporary reference to the pending destructive action that was deferred due to an 469 * auto-advance transition in progress. 470 * <p> 471 * In detail: when auto-advance triggers a mode change, we must wait until the transition 472 * completes before executing the destructive action to ensure a smooth mode change transition. 473 * This member variable houses the pending destructive action work to be run upon completion. 474 */ 475 private Runnable mAutoAdvanceOp = null; 476 477 private final Deque<UpOrBackHandler> mUpOrBackHandlers = Lists.newLinkedList(); 478 479 protected DrawerLayout mDrawerContainer; 480 protected View mDrawerPullout; 481 protected ActionBarDrawerToggle mDrawerToggle; 482 protected ListView mListViewForAnimating; 483 protected boolean mHasNewAccountOrFolder; 484 private boolean mConversationListLoadFinishedIgnored; 485 private final MailDrawerListener mDrawerListener = new MailDrawerListener(); 486 private boolean mHideMenuItems; 487 488 private final DrawIdler mDrawIdler = new DrawIdler(); 489 490 public static final String SYNC_ERROR_DIALOG_FRAGMENT_TAG = "SyncErrorDialogFragment"; 491 492 private final DataSetObserver mUndoNotificationObserver = new DataSetObserver() { 493 @Override 494 public void onChanged() { 495 super.onChanged(); 496 497 if (mConversationListCursor != null) { 498 mConversationListCursor.handleNotificationActions(); 499 } 500 } 501 }; 502 503 public AbstractActivityController(MailActivity activity, ViewMode viewMode) { 504 mActivity = activity; 505 mFragmentManager = mActivity.getFragmentManager(); 506 mViewMode = viewMode; 507 mContext = activity.getApplicationContext(); 508 mRecentFolderList = new RecentFolderList(mContext); 509 mTracker = new ConversationPositionTracker(this); 510 // Allow the fragment to observe changes to its own selection set. No other object is 511 // aware of the selected set. 512 mSelectedSet.addObserver(this); 513 514 final Resources r = mContext.getResources(); 515 mFolderItemUpdateDelayMs = r.getInteger(R.integer.folder_item_refresh_delay_ms); 516 mShowUndoBarDelay = r.getInteger(R.integer.show_undo_bar_delay_ms); 517 mVeiledMatcher = VeiledAddressMatcher.newInstance(activity.getResources()); 518 mIsTablet = Utils.useTabletUI(r); 519 mConversationListLoadFinishedIgnored = false; 520 } 521 522 @Override 523 public Account getCurrentAccount() { 524 return mAccount; 525 } 526 527 @Override 528 public ConversationListContext getCurrentListContext() { 529 return mConvListContext; 530 } 531 532 @Override 533 public String getHelpContext() { 534 final int mode = mViewMode.getMode(); 535 final int helpContextResId; 536 switch (mode) { 537 case ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION: 538 helpContextResId = R.string.wait_help_context; 539 break; 540 default: 541 helpContextResId = R.string.main_help_context; 542 } 543 return mContext.getString(helpContextResId); 544 } 545 546 @Override 547 public final ConversationCursor getConversationListCursor() { 548 return mConversationListCursor; 549 } 550 551 /** 552 * Check if the fragment is attached to an activity and has a root view. 553 * @param in fragment to be checked 554 * @return true if the fragment is valid, false otherwise 555 */ 556 private static boolean isValidFragment(Fragment in) { 557 return !(in == null || in.getActivity() == null || in.getView() == null); 558 } 559 560 /** 561 * Get the conversation list fragment for this activity. If the conversation list fragment is 562 * not attached, this method returns null. 563 * 564 * Caution! This method returns the {@link ConversationListFragment} after the fragment has been 565 * added, <b>and</b> after the {@link FragmentManager} has run through its queue to add the 566 * fragment. There is a non-trivial amount of time after the fragment is instantiated and before 567 * this call returns a non-null value, depending on the {@link FragmentManager}. If you 568 * need the fragment immediately after adding it, consider making the fragment an observer of 569 * the controller and perform the task immediately on {@link Fragment#onActivityCreated(Bundle)} 570 */ 571 protected ConversationListFragment getConversationListFragment() { 572 final Fragment fragment = mFragmentManager.findFragmentByTag(TAG_CONVERSATION_LIST); 573 if (isValidFragment(fragment)) { 574 return (ConversationListFragment) fragment; 575 } 576 return null; 577 } 578 579 /** 580 * Returns the folder list fragment attached with this activity. If no such fragment is attached 581 * this method returns null. 582 * 583 * Caution! This method returns the {@link FolderListFragment} after the fragment has been 584 * added, <b>and</b> after the {@link FragmentManager} has run through its queue to add the 585 * fragment. There is a non-trivial amount of time after the fragment is instantiated and before 586 * this call returns a non-null value, depending on the {@link FragmentManager}. If you 587 * need the fragment immediately after adding it, consider making the fragment an observer of 588 * the controller and perform the task immediately on {@link Fragment#onActivityCreated(Bundle)} 589 */ 590 protected FolderListFragment getFolderListFragment() { 591 final String drawerPulloutTag = mActivity.getString(R.string.drawer_pullout_tag); 592 final Fragment fragment = mFragmentManager.findFragmentByTag(drawerPulloutTag); 593 if (isValidFragment(fragment)) { 594 return (FolderListFragment) fragment; 595 } 596 return null; 597 } 598 599 /** 600 * Initialize the action bar. This is not visible to OnePaneController and 601 * TwoPaneController so they cannot override this behavior. 602 */ 603 private void initializeActionBar() { 604 final ActionBar actionBar = mActivity.getActionBar(); 605 if (actionBar == null) { 606 return; 607 } 608 609 // be sure to inherit from the ActionBar theme when inflating 610 final LayoutInflater inflater = LayoutInflater.from(actionBar.getThemedContext()); 611 final boolean isSearch = mActivity.getIntent() != null 612 && Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction()); 613 mActionBarView = (MailActionBarView) inflater.inflate( 614 isSearch ? R.layout.search_actionbar_view : R.layout.actionbar_view, null); 615 mActionBarView.initialize(mActivity, this, actionBar); 616 617 // init the action bar to allow the 'up' affordance. 618 // any configurations that disallow 'up' should do that later. 619 mActionBarView.setBackButton(); 620 } 621 622 /** 623 * Attach the action bar to the activity. 624 */ 625 private void attachActionBar() { 626 final ActionBar actionBar = mActivity.getActionBar(); 627 if (actionBar != null) { 628 actionBar.setCustomView(mActionBarView, new ActionBar.LayoutParams( 629 LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)); 630 // Show a custom view and home icon, keep the title and subttitle 631 final int mask = ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_SHOW_TITLE 632 | ActionBar.DISPLAY_SHOW_HOME; 633 actionBar.setDisplayOptions(mask, mask); 634 } 635 mActionBarView.setViewModeController(mViewMode); 636 } 637 638 /** 639 * Returns whether the conversation list fragment is visible or not. 640 * Different layouts will have their own notion on the visibility of 641 * fragments, so this method needs to be overriden. 642 * 643 */ 644 protected abstract boolean isConversationListVisible(); 645 646 /** 647 * If required, starts wait mode for the current account. 648 */ 649 final void perhapsEnterWaitMode() { 650 // If the account is not initialized, then show the wait fragment, since nothing can be 651 // shown. 652 if (mAccount.isAccountInitializationRequired()) { 653 showWaitForInitialization(); 654 return; 655 } 656 657 final boolean inWaitingMode = inWaitMode(); 658 final boolean isSyncRequired = mAccount.isAccountSyncRequired(); 659 if (isSyncRequired) { 660 if (inWaitingMode) { 661 // Update the WaitFragment's account object 662 updateWaitMode(); 663 } else { 664 // Transition to waiting mode 665 showWaitForInitialization(); 666 } 667 } else if (inWaitingMode) { 668 // Dismiss waiting mode 669 hideWaitForInitialization(); 670 } 671 } 672 673 @Override 674 public void switchToDefaultInboxOrChangeAccount(Account account) { 675 LogUtils.d(LOG_TAG, "AAC.switchToDefaultAccount(%s)", account); 676 if (mViewMode.isSearchMode()) { 677 // We are in an activity on top of the main navigation activity. 678 // We need to return to it with a result code that indicates it should navigate to 679 // a different folder. 680 final Intent intent = new Intent(); 681 intent.putExtra(AbstractActivityController.EXTRA_ACCOUNT, account); 682 mActivity.setResult(Activity.RESULT_OK, intent); 683 mActivity.finish(); 684 return; 685 } 686 final boolean firstLoad = mAccount == null; 687 final boolean switchToDefaultInbox = !firstLoad && account.uri.equals(mAccount.uri); 688 // If the active account has been clicked in the drawer, go to default inbox 689 if (switchToDefaultInbox) { 690 loadAccountInbox(); 691 return; 692 } 693 changeAccount(account); 694 } 695 696 @Override 697 public void changeAccount(Account account) { 698 LogUtils.d(LOG_TAG, "AAC.changeAccount(%s)", account); 699 // Is the account or account settings different from the existing account? 700 final boolean firstLoad = mAccount == null; 701 final boolean accountChanged = firstLoad || !account.uri.equals(mAccount.uri); 702 703 // If nothing has changed, return early without wasting any more time. 704 if (!accountChanged && !account.settingsDiffer(mAccount)) { 705 return; 706 } 707 // We also don't want to do anything if the new account is null 708 if (account == null) { 709 LogUtils.e(LOG_TAG, "AAC.changeAccount(null) called."); 710 return; 711 } 712 final String emailAddress = account.getEmailAddress(); 713 mHandler.post(new Runnable() { 714 @Override 715 public void run() { 716 MailActivity.setNfcMessage(emailAddress); 717 } 718 }); 719 if (accountChanged) { 720 commitDestructiveActions(false); 721 } 722 Analytics.getInstance().setCustomDimension(Analytics.CD_INDEX_ACCOUNT_TYPE, 723 AnalyticsUtils.getAccountTypeForAccount(emailAddress)); 724 // Change the account here 725 setAccount(account); 726 // And carry out associated actions. 727 cancelRefreshTask(); 728 if (accountChanged) { 729 loadAccountInbox(); 730 } 731 // Check if we need to force setting up an account before proceeding. 732 if (mAccount != null && !Uri.EMPTY.equals(mAccount.settings.setupIntentUri)) { 733 // Launch the intent! 734 final Intent intent = new Intent(Intent.ACTION_EDIT); 735 intent.setData(mAccount.settings.setupIntentUri); 736 mActivity.startActivity(intent); 737 } 738 } 739 740 /** 741 * Adds a listener interested in change in the current account. If a class is storing a 742 * reference to the current account, it should listen on changes, so it can receive updates to 743 * settings. Must happen in the UI thread. 744 */ 745 @Override 746 public void registerAccountObserver(DataSetObserver obs) { 747 mAccountObservers.registerObserver(obs); 748 } 749 750 /** 751 * Removes a listener from receiving current account changes. 752 * Must happen in the UI thread. 753 */ 754 @Override 755 public void unregisterAccountObserver(DataSetObserver obs) { 756 mAccountObservers.unregisterObserver(obs); 757 } 758 759 @Override 760 public void registerAllAccountObserver(DataSetObserver observer) { 761 mAllAccountObservers.registerObserver(observer); 762 } 763 764 @Override 765 public void unregisterAllAccountObserver(DataSetObserver observer) { 766 mAllAccountObservers.unregisterObserver(observer); 767 } 768 769 @Override 770 public Account[] getAllAccounts() { 771 return mAllAccounts; 772 } 773 774 @Override 775 public Account getAccount() { 776 return mAccount; 777 } 778 779 @Override 780 public void registerDrawerClosedObserver(final DataSetObserver observer) { 781 mDrawerObservers.registerObserver(observer); 782 } 783 784 @Override 785 public void unregisterDrawerClosedObserver(final DataSetObserver observer) { 786 mDrawerObservers.unregisterObserver(observer); 787 } 788 789 /** 790 * If the drawer is open, the function locks the drawer to the closed, thereby sliding in 791 * the drawer to the left edge, disabling events, and refreshing it once it's either closed 792 * or put in an idle state. 793 */ 794 @Override 795 public void closeDrawer(final boolean hasNewFolderOrAccount, Account nextAccount, 796 Folder nextFolder) { 797 if (!isDrawerEnabled()) { 798 mDrawerObservers.notifyChanged(); 799 return; 800 } 801 // If there are no new folders or accounts to switch to, just close the drawer 802 if (!hasNewFolderOrAccount) { 803 mDrawerContainer.closeDrawers(); 804 return; 805 } 806 // Otherwise, start preloading the conversation list for the new folder. 807 if (nextFolder != null) { 808 preloadConvList(nextAccount, nextFolder); 809 } 810 // Remember if the conversation list view is animating 811 final ConversationListFragment conversationList = getConversationListFragment(); 812 if (conversationList != null) { 813 mListViewForAnimating = conversationList.getListView(); 814 } else { 815 // There is no conversation list to animate, so just set it to null 816 mListViewForAnimating = null; 817 } 818 819 if (mDrawerContainer.isDrawerOpen(mDrawerPullout)) { 820 // Lets the drawer listener update the drawer contents and notify the FolderListFragment 821 mHasNewAccountOrFolder = true; 822 mDrawerContainer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED); 823 } else { 824 // Drawer is already closed, notify observers that is the case. 825 mDrawerObservers.notifyChanged(); 826 } 827 } 828 829 /** 830 * Load the conversation list early for the given folder. This happens when some UI element 831 * (usually the drawer) instructs the controller that an account change or folder change is 832 * imminent. While the UI element is animating, the controller can preload the conversation 833 * list for the default inbox of the account provided here or to the folder provided here. 834 * 835 * @param nextAccount The account which the app will switch to shortly, possibly null. 836 * @param nextFolder The folder which the app will switch to shortly, possibly null. 837 */ 838 protected void preloadConvList(Account nextAccount, Folder nextFolder) { 839 // Fire off the conversation list loader for this account already with a fake 840 // listener. 841 final Bundle args = new Bundle(2); 842 if (nextAccount != null) { 843 args.putParcelable(BUNDLE_ACCOUNT_KEY, nextAccount); 844 } else { 845 args.putParcelable(BUNDLE_ACCOUNT_KEY, mAccount); 846 } 847 if (nextFolder != null) { 848 args.putParcelable(BUNDLE_FOLDER_KEY, nextFolder); 849 } else { 850 LogUtils.e(LOG_TAG, new Error(), "AAC.preloadConvList(): Got an empty folder"); 851 } 852 mFolder = null; 853 final LoaderManager lm = mActivity.getLoaderManager(); 854 lm.destroyLoader(LOADER_CONVERSATION_LIST); 855 lm.initLoader(LOADER_CONVERSATION_LIST, args, mListCursorCallbacks); 856 } 857 858 /** 859 * Initiates the async request to create a fake search folder, which returns conversations that 860 * match the query term provided by the user. Returns immediately. 861 * @param intent Intent that the app was started with. This intent contains the search query. 862 */ 863 private void fetchSearchFolder(Intent intent) { 864 final Bundle args = new Bundle(1); 865 args.putString(ConversationListContext.EXTRA_SEARCH_QUERY, intent 866 .getStringExtra(ConversationListContext.EXTRA_SEARCH_QUERY)); 867 mActivity.getLoaderManager().restartLoader(LOADER_SEARCH, args, mFolderCallbacks); 868 } 869 870 @Override 871 public void onFolderChanged(Folder folder, final boolean force) { 872 /** If the folder doesn't exist, or its parent URI is empty, 873 * this is not a child folder */ 874 final boolean isTopLevel = (folder == null) || (folder.parent == Uri.EMPTY); 875 final int mode = mViewMode.getMode(); 876 mDrawerToggle.setDrawerIndicatorEnabled( 877 getShouldShowDrawerIndicator(mode, isTopLevel)); 878 mDrawerContainer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED); 879 880 mDrawerContainer.closeDrawers(); 881 882 if (mFolder == null || !mFolder.equals(folder)) { 883 // We are actually changing the folder, so exit cab mode 884 exitCabMode(); 885 } 886 887 final String query; 888 if (folder != null && folder.isType(FolderType.SEARCH)) { 889 query = mConvListContext.searchQuery; 890 } else { 891 query = null; 892 } 893 894 changeFolder(folder, query, force); 895 } 896 897 /** 898 * Sets the folder state without changing view mode and without creating a list fragment, if 899 * possible. 900 * @param folder the folder whose list of conversations are to be shown 901 * @param query the query string for a list of conversations matching a search 902 */ 903 private void setListContext(Folder folder, String query) { 904 updateFolder(folder); 905 if (query != null) { 906 mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder, query); 907 } else { 908 mConvListContext = ConversationListContext.forFolder(mAccount, mFolder); 909 } 910 cancelRefreshTask(); 911 } 912 913 /** 914 * Changes the folder to the value provided here. This causes the view mode to change. 915 * @param folder the folder to change to 916 * @param query if non-null, this represents the search string that the folder represents. 917 * @param force <code>true</code> to force a folder change, <code>false</code> to disallow 918 * changing to the current folder 919 */ 920 private void changeFolder(Folder folder, String query, final boolean force) { 921 if (!Objects.equal(mFolder, folder)) { 922 commitDestructiveActions(false); 923 } 924 if (folder != null && (!folder.equals(mFolder) || force) 925 || (mViewMode.getMode() != ViewMode.CONVERSATION_LIST)) { 926 setListContext(folder, query); 927 showConversationList(mConvListContext); 928 // Touch the current folder: it is different, and it has been accessed. 929 mRecentFolderList.touchFolder(mFolder, mAccount); 930 } 931 resetActionBarIcon(); 932 } 933 934 @Override 935 public void onFolderSelected(Folder folder) { 936 onFolderChanged(folder, false /* force */); 937 } 938 939 /** 940 * Adds a listener interested in change in the recent folders. If a class is storing a 941 * reference to the recent folders, it should listen on changes, so it can receive updates. 942 * Must happen in the UI thread. 943 */ 944 @Override 945 public void registerRecentFolderObserver(DataSetObserver obs) { 946 mRecentFolderObservers.registerObserver(obs); 947 } 948 949 /** 950 * Removes a listener from receiving recent folder changes. 951 * Must happen in the UI thread. 952 */ 953 @Override 954 public void unregisterRecentFolderObserver(DataSetObserver obs) { 955 mRecentFolderObservers.unregisterObserver(obs); 956 } 957 958 @Override 959 public RecentFolderList getRecentFolders() { 960 return mRecentFolderList; 961 } 962 963 @Override 964 public void loadAccountInbox() { 965 boolean handled = false; 966 if (mFolderWatcher != null) { 967 final Folder inbox = mFolderWatcher.getDefaultInbox(mAccount); 968 if (inbox != null) { 969 onFolderChanged(inbox, false /* force */); 970 handled = true; 971 } 972 } 973 if (!handled) { 974 LogUtils.d(LOG_TAG, "Starting a LOADER_ACCOUNT_INBOX for %s", mAccount); 975 restartOptionalLoader(LOADER_ACCOUNT_INBOX, mFolderCallbacks, Bundle.EMPTY); 976 } 977 final int mode = mViewMode.getMode(); 978 if (mode == ViewMode.UNKNOWN || mode == ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION) { 979 mViewMode.enterConversationListMode(); 980 } 981 } 982 983 @Override 984 public void setFolderWatcher(FolderWatcher watcher) { 985 mFolderWatcher = watcher; 986 } 987 988 /** 989 * Marks the {@link #mFolderChanged} value if the newFolder is different from the existing 990 * {@link #mFolder}. This should be called immediately <b>before</b> assigning newFolder to 991 * mFolder. 992 * @param newFolder the new folder we are switching to. 993 */ 994 private void setHasFolderChanged(final Folder newFolder) { 995 // We should never try to assign a null folder. But in the rare event that we do, we should 996 // only set the bit when we have a valid folder, and null is not valid. 997 if (newFolder == null) { 998 return; 999 } 1000 // If the previous folder was null, or if the two folders represent different data, then we 1001 // consider that the folder has changed. 1002 if (mFolder == null || !newFolder.equals(mFolder)) { 1003 mFolderChanged = true; 1004 } 1005 } 1006 1007 /** 1008 * Sets the current folder if it is different from the object provided here. This method does 1009 * NOT notify the folder observers that a change has happened. Observers are notified when we 1010 * get an updated folder from the loaders, which will happen as a consequence of this method 1011 * (since this method starts/restarts the loaders). 1012 * @param folder The folder to assign 1013 */ 1014 private void updateFolder(Folder folder) { 1015 if (folder == null || !folder.isInitialized()) { 1016 LogUtils.e(LOG_TAG, new Error(), "AAC.setFolder(%s): Bad input", folder); 1017 return; 1018 } 1019 if (folder.equals(mFolder)) { 1020 LogUtils.d(LOG_TAG, "AAC.setFolder(%s): Input matches mFolder", folder); 1021 return; 1022 } 1023 final boolean wasNull = mFolder == null; 1024 LogUtils.d(LOG_TAG, "AbstractActivityController.setFolder(%s)", folder.name); 1025 final LoaderManager lm = mActivity.getLoaderManager(); 1026 // updateFolder is called from AAC.onLoadFinished() on folder changes. We need to 1027 // ensure that the folder is different from the previous folder before marking the 1028 // folder changed. 1029 setHasFolderChanged(folder); 1030 mFolder = folder; 1031 1032 // We do not need to notify folder observers yet. Instead we start the loaders and 1033 // when the load finishes, we will get an updated folder. Then, we notify the 1034 // folderObservers in onLoadFinished. 1035 mActionBarView.setFolder(mFolder); 1036 1037 // Only when we switch from one folder to another do we want to restart the 1038 // folder and conversation list loaders (to trigger onCreateLoader). 1039 // The first time this runs when the activity is [re-]initialized, we want to re-use the 1040 // previous loader's instance and data upon configuration change (e.g. rotation). 1041 // If there was not already an instance of the loader, init it. 1042 if (lm.getLoader(LOADER_FOLDER_CURSOR) == null) { 1043 lm.initLoader(LOADER_FOLDER_CURSOR, Bundle.EMPTY, mFolderCallbacks); 1044 } else { 1045 lm.restartLoader(LOADER_FOLDER_CURSOR, Bundle.EMPTY, mFolderCallbacks); 1046 } 1047 if (!wasNull && lm.getLoader(LOADER_CONVERSATION_LIST) != null) { 1048 // If there was an existing folder AND we have changed 1049 // folders, we want to restart the loader to get the information 1050 // for the newly selected folder 1051 lm.destroyLoader(LOADER_CONVERSATION_LIST); 1052 } 1053 final Bundle args = new Bundle(2); 1054 args.putParcelable(BUNDLE_ACCOUNT_KEY, mAccount); 1055 args.putParcelable(BUNDLE_FOLDER_KEY, mFolder); 1056 args.putBoolean(BUNDLE_IGNORE_INITIAL_CONVERSATION_LIMIT_KEY, 1057 mIgnoreInitialConversationLimit); 1058 mIgnoreInitialConversationLimit = false; 1059 lm.initLoader(LOADER_CONVERSATION_LIST, args, mListCursorCallbacks); 1060 } 1061 1062 @Override 1063 public Folder getFolder() { 1064 return mFolder; 1065 } 1066 1067 @Override 1068 public Folder getHierarchyFolder() { 1069 return mFolderListFolder; 1070 } 1071 1072 @Override 1073 public void setHierarchyFolder(Folder folder) { 1074 mFolderListFolder = folder; 1075 } 1076 1077 /** 1078 * The mail activity calls other activities for two specific reasons: 1079 * <ul> 1080 * <li>To add an account. And receives the result {@link #ADD_ACCOUNT_REQUEST_CODE}</li> 1081 * <li>To update the password on a current account. The result {@link 1082 * #REAUTHENTICATE_REQUEST_CODE} is received.</li> 1083 * </ul> 1084 * @param requestCode 1085 * @param resultCode 1086 * @param data 1087 */ 1088 @Override 1089 public void onActivityResult(int requestCode, int resultCode, Intent data) { 1090 switch (requestCode) { 1091 case ADD_ACCOUNT_REQUEST_CODE: 1092 // We were waiting for the user to create an account 1093 if (resultCode == Activity.RESULT_OK) { 1094 // restart the loader to get the updated list of accounts 1095 mActivity.getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, Bundle.EMPTY, 1096 mAccountCallbacks); 1097 } else { 1098 // The user failed to create an account, just exit the app 1099 mActivity.finish(); 1100 } 1101 break; 1102 case REAUTHENTICATE_REQUEST_CODE: 1103 if (resultCode == Activity.RESULT_OK) { 1104 // The user successfully authenticated, attempt to refresh the list 1105 final Uri refreshUri = mFolder != null ? mFolder.refreshUri : null; 1106 if (refreshUri != null) { 1107 startAsyncRefreshTask(refreshUri); 1108 } 1109 } 1110 break; 1111 case CHANGE_NAVIGATION_REQUEST_CODE: 1112 if (resultCode == Activity.RESULT_OK && data != null) { 1113 // We have have received a result that indicates we need to navigate to a 1114 // different folder or account. This happens if someone navigates using the 1115 // drawer on the search results activity. 1116 final Folder folder = data.getParcelableExtra(EXTRA_FOLDER); 1117 final Account account = data.getParcelableExtra(EXTRA_ACCOUNT); 1118 if (folder != null) { 1119 onFolderSelected(folder); 1120 mViewMode.enterConversationListMode(); 1121 } else if (account != null) { 1122 switchToDefaultInboxOrChangeAccount(account); 1123 mViewMode.enterConversationListMode(); 1124 } 1125 } 1126 break; 1127 } 1128 } 1129 1130 /** 1131 * Inform the conversation cursor that there has been a visibility change. 1132 * @param visible true if the conversation list is visible, false otherwise. 1133 */ 1134 protected synchronized void informCursorVisiblity(boolean visible) { 1135 if (mConversationListCursor != null) { 1136 Utils.setConversationCursorVisibility(mConversationListCursor, visible, mFolderChanged); 1137 // We have informed the cursor. Subsequent visibility changes should not tell it that 1138 // the folder has changed. 1139 mFolderChanged = false; 1140 } 1141 } 1142 1143 @Override 1144 public void onConversationListVisibilityChanged(boolean visible) { 1145 informCursorVisiblity(visible); 1146 commitAutoAdvanceOperation(); 1147 1148 // Notify special views 1149 final ConversationListFragment convListFragment = getConversationListFragment(); 1150 if (convListFragment != null && convListFragment.getAnimatedAdapter() != null) { 1151 convListFragment.getAnimatedAdapter().onConversationListVisibilityChanged(visible); 1152 } 1153 } 1154 1155 /** 1156 * Called when a conversation is visible. Child classes must call the super class implementation 1157 * before performing local computation. 1158 */ 1159 @Override 1160 public void onConversationVisibilityChanged(boolean visible) { 1161 commitAutoAdvanceOperation(); 1162 } 1163 1164 /** 1165 * Commits any pending destructive action that was earlier deferred by an auto-advance 1166 * mode-change transition. 1167 */ 1168 private void commitAutoAdvanceOperation() { 1169 if (mAutoAdvanceOp != null) { 1170 mAutoAdvanceOp.run(); 1171 mAutoAdvanceOp = null; 1172 } 1173 } 1174 1175 /** 1176 * Initialize development time logging. This can potentially log a lot of PII, and we don't want 1177 * to turn it on for shipped versions. 1178 */ 1179 private void initializeDevLoggingService() { 1180 if (!MailLogService.DEBUG_ENABLED) { 1181 return; 1182 } 1183 // Check every 5 minutes. 1184 final int WAIT_TIME = 5 * 60 * 1000; 1185 // Start a runnable that periodically checks the log level and starts/stops the service. 1186 mLogServiceChecker = new Runnable() { 1187 /** True if currently logging. */ 1188 private boolean mCurrentlyLogging = false; 1189 1190 /** 1191 * If the logging level has been changed since the previous run, start or stop the 1192 * service. 1193 */ 1194 private void startOrStopService() { 1195 // If the log level is already high, start the service. 1196 final Intent i = new Intent(mContext, MailLogService.class); 1197 final boolean loggingEnabled = MailLogService.isLoggingLevelHighEnough(); 1198 if (mCurrentlyLogging == loggingEnabled) { 1199 // No change since previous run, just return; 1200 return; 1201 } 1202 if (loggingEnabled) { 1203 LogUtils.e(LOG_TAG, "Starting MailLogService"); 1204 mContext.startService(i); 1205 } else { 1206 LogUtils.e(LOG_TAG, "Stopping MailLogService"); 1207 mContext.stopService(i); 1208 } 1209 mCurrentlyLogging = loggingEnabled; 1210 } 1211 1212 @Override 1213 public void run() { 1214 startOrStopService(); 1215 mHandler.postDelayed(this, WAIT_TIME); 1216 } 1217 }; 1218 // Start the runnable right away. 1219 mHandler.post(mLogServiceChecker); 1220 } 1221 1222 /** 1223 * The application can be started from the following entry points: 1224 * <ul> 1225 * <li>Launcher: you tap on the Gmail icon in the launcher. This is what most users think of 1226 * as “Starting the app”.</li> 1227 * <li>Shortcut: Users can make a shortcut to take them directly to a label.</li> 1228 * <li>Widget: Shows the contents of a synced label, and allows: 1229 * <ul> 1230 * <li>Viewing the list (tapping on the title)</li> 1231 * <li>Composing a new message (tapping on the new message icon in the title. This 1232 * launches the {@link ComposeActivity}. 1233 * </li> 1234 * <li>Viewing a single message (tapping on a list element)</li> 1235 * </ul> 1236 * 1237 * </li> 1238 * <li>Tapping on a notification: 1239 * <ul> 1240 * <li>Shows message list if more than one message</li> 1241 * <li>Shows the conversation if the notification is for a single message</li> 1242 * </ul> 1243 * </li> 1244 * <li>...and most importantly, the activity life cycle can tear down the application and 1245 * restart it: 1246 * <ul> 1247 * <li>Rotate the application: it is destroyed and recreated.</li> 1248 * <li>Navigate away, and return from recent applications.</li> 1249 * </ul> 1250 * </li> 1251 * <li>Add a new account: fires off an intent to add an account, 1252 * and returns in {@link #onActivityResult(int, int, android.content.Intent)} .</li> 1253 * <li>Re-authenticate your account: again returns in onActivityResult().</li> 1254 * <li>Composing can happen from many entry points: third party applications fire off an 1255 * intent to compose email, and launch directly into the {@link ComposeActivity} 1256 * .</li> 1257 * </ul> 1258 * {@inheritDoc} 1259 */ 1260 @Override 1261 public boolean onCreate(Bundle savedState) { 1262 initializeActionBar(); 1263 initializeDevLoggingService(); 1264 // Allow shortcut keys to function for the ActionBar and menus. 1265 mActivity.setDefaultKeyMode(Activity.DEFAULT_KEYS_SHORTCUT); 1266 mResolver = mActivity.getContentResolver(); 1267 mNewEmailReceiver = new SuppressNotificationReceiver(); 1268 mRecentFolderList.initialize(mActivity); 1269 mVeiledMatcher.initialize(this); 1270 1271 mDrawerToggle = new ActionBarDrawerToggle((Activity) mActivity, mDrawerContainer, 1272 R.drawable.ic_drawer, R.string.drawer_open, R.string.drawer_close); 1273 mDrawerContainer.setDrawerListener(mDrawerListener); 1274 mDrawerContainer.setDrawerShadow( 1275 mContext.getResources().getDrawable(R.drawable.drawer_shadow), Gravity.START); 1276 1277 mDrawerToggle.setDrawerIndicatorEnabled(isDrawerEnabled()); 1278 1279 // All the individual UI components listen for ViewMode changes. This 1280 // simplifies the amount of logic in the AbstractActivityController, but increases the 1281 // possibility of timing-related bugs. 1282 mViewMode.addListener(this); 1283 mPagerController = new ConversationPagerController(mActivity, this); 1284 mToastBar = (ActionableToastBar) mActivity.findViewById(R.id.toast_bar); 1285 attachActionBar(); 1286 1287 mDrawIdler.setRootView(mActivity.getWindow().getDecorView()); 1288 1289 final Intent intent = mActivity.getIntent(); 1290 1291 // Immediately handle a clean launch with intent, and any state restoration 1292 // that does not rely on restored fragments or loader data 1293 // any state restoration that relies on those can be done later in 1294 // onRestoreInstanceState, once fragments are up and loader data is re-delivered 1295 if (savedState != null) { 1296 if (savedState.containsKey(SAVED_ACCOUNT)) { 1297 setAccount((Account) savedState.getParcelable(SAVED_ACCOUNT)); 1298 } 1299 if (savedState.containsKey(SAVED_FOLDER)) { 1300 final Folder folder = savedState.getParcelable(SAVED_FOLDER); 1301 final String query = savedState.getString(SAVED_QUERY, null); 1302 setListContext(folder, query); 1303 } 1304 if (savedState.containsKey(SAVED_ACTION)) { 1305 mDialogAction = savedState.getInt(SAVED_ACTION); 1306 } 1307 mDialogFromSelectedSet = savedState.getBoolean(SAVED_ACTION_FROM_SELECTED, false); 1308 mViewMode.handleRestore(savedState); 1309 } else if (intent != null) { 1310 handleIntent(intent); 1311 } 1312 // Create the accounts loader; this loads the account switch spinner. 1313 mActivity.getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, Bundle.EMPTY, 1314 mAccountCallbacks); 1315 return true; 1316 } 1317 1318 @Override 1319 public void onPostCreate(Bundle savedState) { 1320 // Sync the toggle state after onRestoreInstanceState has occurred. 1321 mDrawerToggle.syncState(); 1322 1323 mHideMenuItems = isDrawerEnabled() && mDrawerContainer.isDrawerOpen(mDrawerPullout); 1324 } 1325 1326 @Override 1327 public void onConfigurationChanged(Configuration newConfig) { 1328 mDrawerToggle.onConfigurationChanged(newConfig); 1329 } 1330 1331 /** 1332 * If drawer is open/visible (even partially), close it. 1333 */ 1334 protected void closeDrawerIfOpen() { 1335 if (!isDrawerEnabled()) { 1336 return; 1337 } 1338 if(mDrawerContainer.isDrawerOpen(mDrawerPullout)) { 1339 mDrawerContainer.closeDrawers(); 1340 } 1341 } 1342 1343 @Override 1344 public void onStart() { 1345 mSafeToModifyFragments = true; 1346 1347 NotificationActionUtils.registerUndoNotificationObserver(mUndoNotificationObserver); 1348 1349 if (mViewMode.getMode() != ViewMode.UNKNOWN) { 1350 Analytics.getInstance().sendView("MainActivity" + mViewMode.toString()); 1351 } 1352 } 1353 1354 @Override 1355 public void onRestart() { 1356 final DialogFragment fragment = (DialogFragment) 1357 mFragmentManager.findFragmentByTag(SYNC_ERROR_DIALOG_FRAGMENT_TAG); 1358 if (fragment != null) { 1359 fragment.dismiss(); 1360 } 1361 // When the user places the app in the background by pressing "home", 1362 // dismiss the toast bar. However, since there is no way to determine if 1363 // home was pressed, just dismiss any existing toast bar when restarting 1364 // the app. 1365 if (mToastBar != null) { 1366 mToastBar.hide(false, false /* actionClicked */); 1367 } 1368 } 1369 1370 @Override 1371 public Dialog onCreateDialog(int id, Bundle bundle) { 1372 return null; 1373 } 1374 1375 @Override 1376 public final boolean onCreateOptionsMenu(Menu menu) { 1377 if (mViewMode.isAdMode()) { 1378 return false; 1379 } 1380 final MenuInflater inflater = mActivity.getMenuInflater(); 1381 inflater.inflate(mActionBarView.getOptionsMenuId(), menu); 1382 mActionBarView.onCreateOptionsMenu(menu); 1383 return true; 1384 } 1385 1386 @Override 1387 public final boolean onKeyDown(int keyCode, KeyEvent event) { 1388 return false; 1389 } 1390 1391 public abstract boolean doesActionChangeConversationListVisibility(int action); 1392 1393 /** 1394 * Helper function that determines if we should associate an undo callback with 1395 * the current menu action item 1396 * @param actionId the id of the action 1397 * @return the appropriate callback handler, or null if not applicable 1398 */ 1399 private UndoCallback getUndoCallbackForDestructiveActionsWithAutoAdvance( 1400 int actionId, final Conversation conv) { 1401 // We associated the undoCallback if the user is going to perform an action on the current 1402 // conversation, causing the current conversation to be removed from view and replacing it 1403 // with another (via Auto Advance). The undoCallback will bring the removed conversation 1404 // back into the view if the action is undone. 1405 final Collection<Conversation> convCol = Conversation.listOf(conv); 1406 final boolean isApplicableForReshow = mAccount != null && 1407 mAccount.settings != null && 1408 mTracker != null && 1409 // ensure that we will show another conversation due to Auto Advance 1410 mTracker.getNextConversation( 1411 mAccount.settings.getAutoAdvanceSetting(), convCol) != null && 1412 // ensure that we are performing the action from conversation view 1413 isCurrentConversationInView(convCol) && 1414 // check for the appropriate destructive actions 1415 doesActionRemoveCurrentConversationFromView(actionId); 1416 return (isApplicableForReshow) ? 1417 new UndoCallback() { 1418 @Override 1419 public void performUndoCallback() { 1420 showConversation(conv); 1421 } 1422 } : null; 1423 } 1424 1425 /** 1426 * Check if the provided action will remove the active conversation from view 1427 * @param actionId the applied action 1428 * @return true if it will remove the conversation from view, false otherwise 1429 */ 1430 private boolean doesActionRemoveCurrentConversationFromView(int actionId) { 1431 return actionId == R.id.archive || 1432 actionId == R.id.delete || 1433 actionId == R.id.remove_folder || 1434 actionId == R.id.report_spam || 1435 actionId == R.id.report_phishing || 1436 actionId == R.id.move_to; 1437 } 1438 1439 @Override 1440 public boolean onOptionsItemSelected(MenuItem item) { 1441 1442 /* 1443 * The action bar home/up action should open or close the drawer. 1444 * mDrawerToggle will take care of this. 1445 */ 1446 if (mDrawerToggle.onOptionsItemSelected(item)) { 1447 Analytics.getInstance().sendEvent(Analytics.EVENT_CATEGORY_MENU_ITEM, "drawer_toggle", 1448 null, 0); 1449 return true; 1450 } 1451 1452 Analytics.getInstance().sendMenuItemEvent(Analytics.EVENT_CATEGORY_MENU_ITEM, 1453 item.getItemId(), "action_bar/" + mViewMode.getModeString(), 0); 1454 1455 final int id = item.getItemId(); 1456 LogUtils.d(LOG_TAG, "AbstractController.onOptionsItemSelected(%d) called.", id); 1457 boolean handled = true; 1458 /** This is NOT a batch action. */ 1459 final boolean isBatch = false; 1460 final Collection<Conversation> target = Conversation.listOf(mCurrentConversation); 1461 final Settings settings = (mAccount == null) ? null : mAccount.settings; 1462 // The user is choosing a new action; commit whatever they had been 1463 // doing before. Don't animate if we are launching a new screen. 1464 commitDestructiveActions(!doesActionChangeConversationListVisibility(id)); 1465 final UndoCallback undoCallback = getUndoCallbackForDestructiveActionsWithAutoAdvance( 1466 id, mCurrentConversation); 1467 1468 if (id == R.id.archive) { 1469 final boolean showDialog = (settings != null && settings.confirmArchive); 1470 confirmAndDelete(id, target, showDialog, R.plurals.confirm_archive_conversation, undoCallback); 1471 } else if (id == R.id.remove_folder) { 1472 delete(R.id.remove_folder, target, 1473 getDeferredRemoveFolder(target, mFolder, true, isBatch, true, undoCallback), 1474 isBatch); 1475 } else if (id == R.id.delete) { 1476 final boolean showDialog = (settings != null && settings.confirmDelete); 1477 confirmAndDelete(id, target, showDialog, R.plurals.confirm_delete_conversation, undoCallback); 1478 } else if (id == R.id.discard_drafts) { 1479 // drafts are lost forever, so always confirm 1480 confirmAndDelete(id, target, true /* showDialog */, 1481 R.plurals.confirm_discard_drafts_conversation, undoCallback); 1482 } else if (id == R.id.mark_important) { 1483 updateConversation(Conversation.listOf(mCurrentConversation), 1484 ConversationColumns.PRIORITY, UIProvider.ConversationPriority.HIGH); 1485 } else if (id == R.id.mark_not_important) { 1486 if (mFolder != null && mFolder.isImportantOnly()) { 1487 delete(R.id.mark_not_important, target, 1488 getDeferredAction(R.id.mark_not_important, target, isBatch, undoCallback), 1489 isBatch); 1490 } else { 1491 updateConversation(Conversation.listOf(mCurrentConversation), 1492 ConversationColumns.PRIORITY, UIProvider.ConversationPriority.LOW); 1493 } 1494 } else if (id == R.id.mute) { 1495 delete(R.id.mute, target, getDeferredAction(R.id.mute, target, isBatch, undoCallback), 1496 isBatch); 1497 } else if (id == R.id.report_spam) { 1498 delete(R.id.report_spam, target, 1499 getDeferredAction(R.id.report_spam, target, isBatch, undoCallback), isBatch); 1500 } else if (id == R.id.mark_not_spam) { 1501 // Currently, since spam messages are only shown in list with 1502 // other spam messages, 1503 // marking a message not as spam is a destructive action 1504 delete(R.id.mark_not_spam, target, 1505 getDeferredAction(R.id.mark_not_spam, target, isBatch, undoCallback), isBatch); 1506 } else if (id == R.id.report_phishing) { 1507 delete(R.id.report_phishing, target, 1508 getDeferredAction(R.id.report_phishing, target, isBatch, undoCallback), isBatch); 1509 } else if (id == android.R.id.home) { 1510 onUpPressed(); 1511 } else if (id == R.id.compose) { 1512 ComposeActivity.compose(mActivity.getActivityContext(), mAccount); 1513 } else if (id == R.id.refresh) { 1514 requestFolderRefresh(); 1515 } else if (id == R.id.settings) { 1516 Utils.showSettings(mActivity.getActivityContext(), mAccount); 1517 } else if (id == R.id.folder_options) { 1518 Utils.showFolderSettings(mActivity.getActivityContext(), mAccount, mFolder); 1519 } else if (id == R.id.help_info_menu_item) { 1520 Utils.showHelp(mActivity.getActivityContext(), mAccount, getHelpContext()); 1521 } else if (id == R.id.feedback_menu_item) { 1522 Utils.sendFeedback(mActivity, mAccount, false); 1523 } else if (id == R.id.manage_folders_item) { 1524 Utils.showManageFolder(mActivity.getActivityContext(), mAccount); 1525 } else if (id == R.id.move_to || id == R.id.change_folders) { 1526 final FolderSelectionDialog dialog = FolderSelectionDialog.getInstance(mAccount, 1527 Conversation.listOf(mCurrentConversation), isBatch, mFolder, 1528 id == R.id.move_to); 1529 if (dialog != null) { 1530 dialog.show(mActivity.getFragmentManager(), null); 1531 } 1532 } else if (id == R.id.move_to_inbox) { 1533 new AsyncTask<Void, Void, Folder>() { 1534 @Override 1535 protected Folder doInBackground(final Void... params) { 1536 // Get the "move to" inbox 1537 return Utils.getFolder(mContext, mAccount.settings.moveToInbox, 1538 true /* allowHidden */); 1539 } 1540 1541 @Override 1542 protected void onPostExecute(final Folder moveToInbox) { 1543 final List<FolderOperation> ops = Lists.newArrayListWithCapacity(1); 1544 // Add inbox 1545 ops.add(new FolderOperation(moveToInbox, true)); 1546 assignFolder(ops, Conversation.listOf(mCurrentConversation), true, 1547 true /* showUndo */, false /* isMoveTo */); 1548 } 1549 }.execute((Void[]) null); 1550 } else if (id == R.id.empty_trash) { 1551 showEmptyDialog(); 1552 } else if (id == R.id.empty_spam) { 1553 showEmptyDialog(); 1554 } else { 1555 handled = false; 1556 } 1557 return handled; 1558 } 1559 1560 /** 1561 * Opens an {@link EmptyFolderDialogFragment} for the current folder. 1562 */ 1563 private void showEmptyDialog() { 1564 if (mFolder != null) { 1565 final EmptyFolderDialogFragment fragment = 1566 EmptyFolderDialogFragment.newInstance(mFolder.totalCount, mFolder.type); 1567 fragment.setListener(this); 1568 fragment.show(mActivity.getFragmentManager(), EmptyFolderDialogFragment.FRAGMENT_TAG); 1569 } 1570 } 1571 1572 @Override 1573 public void onFolderEmptied() { 1574 emptyFolder(); 1575 } 1576 1577 /** 1578 * Performs the work of emptying the currently visible folder. 1579 */ 1580 private void emptyFolder() { 1581 if (mConversationListCursor != null) { 1582 mConversationListCursor.emptyFolder(); 1583 } 1584 } 1585 1586 private void attachEmptyFolderDialogFragmentListener() { 1587 final EmptyFolderDialogFragment fragment = 1588 (EmptyFolderDialogFragment) mActivity.getFragmentManager() 1589 .findFragmentByTag(EmptyFolderDialogFragment.FRAGMENT_TAG); 1590 1591 if (fragment != null) { 1592 fragment.setListener(this); 1593 } 1594 } 1595 1596 /** 1597 * Toggles the drawer pullout. If it was open (Fully extended), the 1598 * drawer will be closed. Otherwise, the drawer will be opened. This should 1599 * only be called when used with a toggle item. Other cases should be handled 1600 * explicitly with just closeDrawers() or openDrawer(View drawerView); 1601 */ 1602 protected void toggleDrawerState() { 1603 if (!isDrawerEnabled()) { 1604 return; 1605 } 1606 if(mDrawerContainer.isDrawerOpen(mDrawerPullout)) { 1607 mDrawerContainer.closeDrawers(); 1608 } else { 1609 mDrawerContainer.openDrawer(mDrawerPullout); 1610 } 1611 } 1612 1613 @Override 1614 public final boolean onUpPressed() { 1615 for (UpOrBackHandler h : mUpOrBackHandlers) { 1616 if (h.onUpPressed()) { 1617 return true; 1618 } 1619 } 1620 return handleUpPress(); 1621 } 1622 1623 @Override 1624 public final boolean onBackPressed() { 1625 for (UpOrBackHandler h : mUpOrBackHandlers) { 1626 if (h.onBackPressed()) { 1627 return true; 1628 } 1629 } 1630 1631 if (isDrawerEnabled() && mDrawerContainer.isDrawerVisible(mDrawerPullout)) { 1632 mDrawerContainer.closeDrawers(); 1633 return true; 1634 } 1635 1636 return handleBackPress(); 1637 } 1638 1639 protected abstract boolean handleBackPress(); 1640 protected abstract boolean handleUpPress(); 1641 1642 @Override 1643 public void addUpOrBackHandler(UpOrBackHandler handler) { 1644 if (mUpOrBackHandlers.contains(handler)) { 1645 return; 1646 } 1647 mUpOrBackHandlers.addFirst(handler); 1648 } 1649 1650 @Override 1651 public void removeUpOrBackHandler(UpOrBackHandler handler) { 1652 mUpOrBackHandlers.remove(handler); 1653 } 1654 1655 @Override 1656 public void updateConversation(Collection<Conversation> target, ContentValues values) { 1657 mConversationListCursor.updateValues(target, values); 1658 refreshConversationList(); 1659 } 1660 1661 @Override 1662 public void updateConversation(Collection <Conversation> target, String columnName, 1663 boolean value) { 1664 mConversationListCursor.updateBoolean(target, columnName, value); 1665 refreshConversationList(); 1666 } 1667 1668 @Override 1669 public void updateConversation(Collection <Conversation> target, String columnName, 1670 int value) { 1671 mConversationListCursor.updateInt(target, columnName, value); 1672 refreshConversationList(); 1673 } 1674 1675 @Override 1676 public void updateConversation(Collection <Conversation> target, String columnName, 1677 String value) { 1678 mConversationListCursor.updateString(target, columnName, value); 1679 refreshConversationList(); 1680 } 1681 1682 @Override 1683 public void markConversationMessagesUnread(final Conversation conv, 1684 final Set<Uri> unreadMessageUris, final byte[] originalConversationInfo) { 1685 // The only caller of this method is the conversation view, from where marking unread should 1686 // *always* take you back to list mode. 1687 showConversation(null); 1688 1689 // locally mark conversation unread (the provider is supposed to propagate message unread 1690 // to conversation unread) 1691 conv.read = false; 1692 if (mConversationListCursor == null) { 1693 LogUtils.d(LOG_TAG, "markConversationMessagesUnread(id=%d), deferring", conv.id); 1694 1695 mConversationListLoadFinishedCallbacks.add(new LoadFinishedCallback() { 1696 @Override 1697 public void onLoadFinished() { 1698 doMarkConversationMessagesUnread(conv, unreadMessageUris, 1699 originalConversationInfo); 1700 } 1701 }); 1702 } else { 1703 LogUtils.d(LOG_TAG, "markConversationMessagesUnread(id=%d), performing", conv.id); 1704 doMarkConversationMessagesUnread(conv, unreadMessageUris, originalConversationInfo); 1705 } 1706 } 1707 1708 private void doMarkConversationMessagesUnread(Conversation conv, Set<Uri> unreadMessageUris, 1709 byte[] originalConversationInfo) { 1710 // Only do a granular 'mark unread' if a subset of messages are unread 1711 final int unreadCount = (unreadMessageUris == null) ? 0 : unreadMessageUris.size(); 1712 final int numMessages = conv.getNumMessages(); 1713 final boolean subsetIsUnread = (numMessages > 1 && unreadCount > 0 1714 && unreadCount < numMessages); 1715 1716 LogUtils.d(LOG_TAG, "markConversationMessagesUnread(conv=%s)" 1717 + ", numMessages=%d, unreadCount=%d, subsetIsUnread=%b", 1718 conv, numMessages, unreadCount, subsetIsUnread); 1719 if (!subsetIsUnread) { 1720 // Conversations are neither marked read, nor viewed, and we don't want to show 1721 // the next conversation. 1722 LogUtils.d(LOG_TAG, ". . doing full mark unread"); 1723 markConversationsRead(Collections.singletonList(conv), false, false, false); 1724 } else { 1725 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) { 1726 final ConversationInfo info = ConversationInfo.fromBlob(originalConversationInfo); 1727 LogUtils.d(LOG_TAG, ". . doing subset mark unread, originalConversationInfo = %s", 1728 info); 1729 } 1730 mConversationListCursor.setConversationColumn(conv.uri, ConversationColumns.READ, 0); 1731 1732 // Locally update conversation's conversationInfo to revert to original version 1733 if (originalConversationInfo != null) { 1734 mConversationListCursor.setConversationColumn(conv.uri, 1735 ConversationColumns.CONVERSATION_INFO, originalConversationInfo); 1736 } 1737 1738 // applyBatch with each CPO as an UPDATE op on each affected message uri 1739 final ArrayList<ContentProviderOperation> ops = Lists.newArrayList(); 1740 String authority = null; 1741 for (Uri messageUri : unreadMessageUris) { 1742 if (authority == null) { 1743 authority = messageUri.getAuthority(); 1744 } 1745 ops.add(ContentProviderOperation.newUpdate(messageUri) 1746 .withValue(UIProvider.MessageColumns.READ, 0) 1747 .build()); 1748 LogUtils.d(LOG_TAG, ". . Adding op: read=0, uri=%s", messageUri); 1749 } 1750 LogUtils.d(LOG_TAG, ". . operations = %s", ops); 1751 new ContentProviderTask() { 1752 @Override 1753 protected void onPostExecute(Result result) { 1754 if (result.exception != null) { 1755 LogUtils.e(LOG_TAG, result.exception, "ContentProviderTask() ERROR."); 1756 } else { 1757 LogUtils.d(LOG_TAG, "ContentProviderTask(): success %s", 1758 Arrays.toString(result.results)); 1759 } 1760 } 1761 }.run(mResolver, authority, ops); 1762 } 1763 } 1764 1765 @Override 1766 public void markConversationsRead(final Collection<Conversation> targets, final boolean read, 1767 final boolean viewed) { 1768 LogUtils.d(LOG_TAG, "markConversationsRead(targets=%s)", targets.toArray()); 1769 1770 if (mConversationListCursor == null) { 1771 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) { 1772 LogUtils.d(LOG_TAG, "markConversationsRead(targets=%s), deferring", 1773 targets.toArray()); 1774 } 1775 mConversationListLoadFinishedCallbacks.add(new LoadFinishedCallback() { 1776 @Override 1777 public void onLoadFinished() { 1778 markConversationsRead(targets, read, viewed, true); 1779 } 1780 }); 1781 } else { 1782 // We want to show the next conversation if we are marking unread. 1783 markConversationsRead(targets, read, viewed, true); 1784 } 1785 } 1786 1787 private void markConversationsRead(final Collection<Conversation> targets, final boolean read, 1788 final boolean markViewed, final boolean showNext) { 1789 LogUtils.d(LOG_TAG, "performing markConversationsRead"); 1790 // Auto-advance if requested and the current conversation is being marked unread 1791 if (showNext && !read) { 1792 final Runnable operation = new Runnable() { 1793 @Override 1794 public void run() { 1795 markConversationsRead(targets, read, markViewed, showNext); 1796 } 1797 }; 1798 1799 if (!showNextConversation(targets, operation)) { 1800 // This method will be called again if the user selects an autoadvance option 1801 return; 1802 } 1803 } 1804 1805 final int size = targets.size(); 1806 final List<ConversationOperation> opList = new ArrayList<ConversationOperation>(size); 1807 for (final Conversation target : targets) { 1808 final ContentValues value = new ContentValues(4); 1809 value.put(ConversationColumns.READ, read); 1810 1811 // We never want to mark unseen here, but we do want to mark it seen 1812 if (read || markViewed) { 1813 value.put(ConversationColumns.SEEN, Boolean.TRUE); 1814 } 1815 1816 // The mark read/unread/viewed operations do not show an undo bar 1817 value.put(ConversationOperations.Parameters.SUPPRESS_UNDO, true); 1818 if (markViewed) { 1819 value.put(ConversationColumns.VIEWED, true); 1820 } 1821 final ConversationInfo info = target.conversationInfo; 1822 final boolean changed = info.markRead(read); 1823 if (changed) { 1824 value.put(ConversationColumns.CONVERSATION_INFO, info.toBlob()); 1825 } 1826 opList.add(mConversationListCursor.getOperationForConversation( 1827 target, ConversationOperation.UPDATE, value)); 1828 // Update the local conversation objects so they immediately change state. 1829 target.read = read; 1830 if (markViewed) { 1831 target.markViewed(); 1832 } 1833 } 1834 mConversationListCursor.updateBulkValues(opList); 1835 } 1836 1837 /** 1838 * Auto-advance to a different conversation if the currently visible conversation in 1839 * conversation mode is affected (deleted, marked unread, etc.). 1840 * 1841 * <p>Does nothing if outside of conversation mode.</p> 1842 * 1843 * @param target the set of conversations being deleted/marked unread 1844 */ 1845 @Override 1846 public void showNextConversation(final Collection<Conversation> target) { 1847 showNextConversation(target, null); 1848 } 1849 1850 /** 1851 * Helper function to determine if the provided set of conversations is in view 1852 * @param target set of conversations that we are interested in 1853 * @return true if they are in view, false otherwise 1854 */ 1855 private boolean isCurrentConversationInView(final Collection<Conversation> target) { 1856 final int viewMode = mViewMode.getMode(); 1857 return (viewMode == ViewMode.CONVERSATION 1858 || viewMode == ViewMode.SEARCH_RESULTS_CONVERSATION) 1859 && Conversation.contains(target, mCurrentConversation); 1860 } 1861 1862 /** 1863 * Auto-advance to a different conversation if the currently visible conversation in 1864 * conversation mode is affected (deleted, marked unread, etc.). 1865 * 1866 * <p>Does nothing if outside of conversation mode.</p> 1867 * <p> 1868 * Clients may pass an operation to execute on the target that this method will run after 1869 * auto-advance is complete. The operation, if provided, may run immediately, or it may run 1870 * later, or not at all. Reasons it may run later include: 1871 * <ul> 1872 * <li>the auto-advance setting is uninitialized and we need to wait for the user to set it</li> 1873 * <li>auto-advance in this configuration requires a mode change, and we need to wait for the 1874 * mode change transition to finish</li> 1875 * </ul> 1876 * <p>If the current conversation is not in the target collection, this method will do nothing, 1877 * and will not execute the operation. 1878 * 1879 * @param target the set of conversations being deleted/marked unread 1880 * @param operation (optional) the operation to execute after advancing 1881 * @return <code>false</code> if this method handled or will execute the operation, 1882 * <code>true</code> otherwise. 1883 */ 1884 private boolean showNextConversation(final Collection<Conversation> target, 1885 final Runnable operation) { 1886 if (isCurrentConversationInView(target)) { 1887 final int autoAdvanceSetting = mAccount.settings.getAutoAdvanceSetting(); 1888 1889 if (autoAdvanceSetting == AutoAdvance.UNSET && mIsTablet) { 1890 displayAutoAdvanceDialogAndPerformAction(operation); 1891 return false; 1892 } else { 1893 // If we don't have one set, but we're here, just take the default 1894 final int autoAdvance = (autoAdvanceSetting == AutoAdvance.UNSET) ? 1895 AutoAdvance.DEFAULT : autoAdvanceSetting; 1896 1897 final Conversation next = mTracker.getNextConversation(autoAdvance, target); 1898 LogUtils.d(LOG_TAG, "showNextConversation: showing %s next.", next); 1899 // Set mAutoAdvanceOp *before* showConversation() to ensure that it runs when the 1900 // transition doesn't run (i.e. it "completes" immediately). 1901 mAutoAdvanceOp = operation; 1902 showConversation(next); 1903 return (mAutoAdvanceOp == null); 1904 } 1905 } 1906 1907 return true; 1908 } 1909 1910 /** 1911 * Displays a the auto-advance dialog, and when the user makes a selection, the preference is 1912 * stored, and the specified operation is run. 1913 */ 1914 private void displayAutoAdvanceDialogAndPerformAction(final Runnable operation) { 1915 final String[] autoAdvanceDisplayOptions = 1916 mContext.getResources().getStringArray(R.array.prefEntries_autoAdvance); 1917 final String[] autoAdvanceOptionValues = 1918 mContext.getResources().getStringArray(R.array.prefValues_autoAdvance); 1919 1920 final String defaultValue = mContext.getString(R.string.prefDefault_autoAdvance); 1921 int initialIndex = 0; 1922 for (int i = 0; i < autoAdvanceOptionValues.length; i++) { 1923 if (defaultValue.equals(autoAdvanceOptionValues[i])) { 1924 initialIndex = i; 1925 break; 1926 } 1927 } 1928 1929 final DialogInterface.OnClickListener listClickListener = 1930 new DialogInterface.OnClickListener() { 1931 @Override 1932 public void onClick(DialogInterface dialog, int whichItem) { 1933 final String autoAdvanceValue = autoAdvanceOptionValues[whichItem]; 1934 final int autoAdvanceValueInt = 1935 UIProvider.AutoAdvance.getAutoAdvanceInt(autoAdvanceValue); 1936 mAccount.settings.setAutoAdvanceSetting(autoAdvanceValueInt); 1937 1938 // Save the user's setting 1939 final ContentValues values = new ContentValues(1); 1940 values.put(AccountColumns.SettingsColumns.AUTO_ADVANCE, autoAdvanceValue); 1941 1942 final ContentResolver resolver = mContext.getContentResolver(); 1943 resolver.update(mAccount.updateSettingsUri, values, null, null); 1944 1945 // Dismiss the dialog, as clicking the items in the list doesn't close the 1946 // dialog. 1947 dialog.dismiss(); 1948 if (operation != null) { 1949 operation.run(); 1950 } 1951 } 1952 }; 1953 1954 new AlertDialog.Builder(mActivity.getActivityContext()).setTitle( 1955 R.string.auto_advance_help_title) 1956 .setSingleChoiceItems(autoAdvanceDisplayOptions, initialIndex, listClickListener) 1957 .setPositiveButton(null, null) 1958 .create() 1959 .show(); 1960 } 1961 1962 @Override 1963 public void starMessage(ConversationMessage msg, boolean starred) { 1964 if (msg.starred == starred) { 1965 return; 1966 } 1967 1968 msg.starred = starred; 1969 1970 // locally propagate the change to the owning conversation 1971 // (figure the provider will properly propagate the change when it commits it) 1972 // 1973 // when unstarring, only propagate the change if this was the only message starred 1974 final boolean conversationStarred = starred || msg.isConversationStarred(); 1975 final Conversation conv = msg.getConversation(); 1976 if (conversationStarred != conv.starred) { 1977 conv.starred = conversationStarred; 1978 mConversationListCursor.setConversationColumn(conv.uri, 1979 ConversationColumns.STARRED, conversationStarred); 1980 } 1981 1982 final ContentValues values = new ContentValues(1); 1983 values.put(UIProvider.MessageColumns.STARRED, starred ? 1 : 0); 1984 1985 new ContentProviderTask.UpdateTask() { 1986 @Override 1987 protected void onPostExecute(Result result) { 1988 // TODO: handle errors? 1989 } 1990 }.run(mResolver, msg.uri, values, null /* selection*/, null /* selectionArgs */); 1991 } 1992 1993 @Override 1994 public void requestFolderRefresh() { 1995 if (mFolder == null) { 1996 return; 1997 } 1998 final ConversationListFragment convList = getConversationListFragment(); 1999 if (convList == null) { 2000 // This could happen if this account is in initial sync (user 2001 // is seeing the "your mail will appear shortly" message) 2002 return; 2003 } 2004 convList.showSyncStatusBar(); 2005 2006 if (mAsyncRefreshTask != null) { 2007 mAsyncRefreshTask.cancel(true); 2008 } 2009 mAsyncRefreshTask = new AsyncRefreshTask(mContext, mFolder.refreshUri); 2010 mAsyncRefreshTask.execute(); 2011 } 2012 2013 /** 2014 * Confirm (based on user's settings) and delete a conversation from the conversation list and 2015 * from the database. 2016 * @param actionId the ID of the menu item that caused the delete: R.id.delete, R.id.archive... 2017 * @param target the conversations to act upon 2018 * @param showDialog true if a confirmation dialog is to be shown, false otherwise. 2019 * @param confirmResource the resource ID of the string that is shown in the confirmation dialog 2020 */ 2021 private void confirmAndDelete(int actionId, final Collection<Conversation> target, 2022 boolean showDialog, int confirmResource, UndoCallback undoCallback) { 2023 final boolean isBatch = false; 2024 if (showDialog) { 2025 makeDialogListener(actionId, isBatch, undoCallback); 2026 final CharSequence message = Utils.formatPlural(mContext, confirmResource, 2027 target.size()); 2028 final ConfirmDialogFragment c = ConfirmDialogFragment.newInstance(message); 2029 c.displayDialog(mActivity.getFragmentManager()); 2030 } else { 2031 delete(0, target, getDeferredAction(actionId, target, isBatch, undoCallback), isBatch); 2032 } 2033 } 2034 2035 @Override 2036 public void delete(final int actionId, final Collection<Conversation> target, 2037 final DestructiveAction action, final boolean isBatch) { 2038 // Order of events is critical! The Conversation View Fragment must be 2039 // notified of the next conversation with showConversation(next) *before* the 2040 // conversation list 2041 // fragment has a chance to delete the conversation, animating it away. 2042 2043 // Update the conversation fragment if the current conversation is 2044 // deleted. 2045 final Runnable operation = new Runnable() { 2046 @Override 2047 public void run() { 2048 delete(actionId, target, action, isBatch); 2049 } 2050 }; 2051 2052 if (!showNextConversation(target, operation)) { 2053 // This method will be called again if the user selects an autoadvance option 2054 return; 2055 } 2056 // If the conversation is in the selected set, remove it from the set. 2057 // Batch selections are cleared in the end of the action, so not done for batch actions. 2058 if (!isBatch) { 2059 for (final Conversation conv : target) { 2060 if (mSelectedSet.contains(conv)) { 2061 mSelectedSet.toggle(conv); 2062 } 2063 } 2064 } 2065 // The conversation list deletes and performs the action if it exists. 2066 final ConversationListFragment convListFragment = getConversationListFragment(); 2067 if (convListFragment != null) { 2068 LogUtils.i(LOG_TAG, "AAC.requestDelete: ListFragment is handling delete."); 2069 convListFragment.requestDelete(actionId, target, action); 2070 return; 2071 } 2072 // No visible UI element handled it on our behalf. Perform the action 2073 // ourself. 2074 LogUtils.i(LOG_TAG, "ACC.requestDelete: performing remove action ourselves"); 2075 action.performAction(); 2076 } 2077 2078 /** 2079 * Requests that the action be performed and the UI state is updated to reflect the new change. 2080 * @param action the action to be performed, specified as a menu id: R.id.archive, ... 2081 */ 2082 private void requestUpdate(final DestructiveAction action) { 2083 action.performAction(); 2084 refreshConversationList(); 2085 } 2086 2087 @Override 2088 public void onPrepareDialog(int id, Dialog dialog, Bundle bundle) { 2089 // TODO(viki): Auto-generated method stub 2090 } 2091 2092 @Override 2093 public boolean onPrepareOptionsMenu(Menu menu) { 2094 return mActionBarView.onPrepareOptionsMenu(menu); 2095 } 2096 2097 @Override 2098 public void onPause() { 2099 mHaveAccountList = false; 2100 enableNotifications(); 2101 } 2102 2103 @Override 2104 public void onResume() { 2105 // Register the receiver that will prevent the status receiver from 2106 // displaying its notification icon as long as we're running. 2107 // The SupressNotificationReceiver will block the broadcast if we're looking at the folder 2108 // that the notification was received for. 2109 disableNotifications(); 2110 2111 mSafeToModifyFragments = true; 2112 2113 attachEmptyFolderDialogFragmentListener(); 2114 2115 // Invalidating the options menu so that when we make changes in settings, 2116 // the changes will always be updated in the action bar/options menu/ 2117 mActivity.invalidateOptionsMenu(); 2118 } 2119 2120 @Override 2121 public void onSaveInstanceState(Bundle outState) { 2122 mViewMode.handleSaveInstanceState(outState); 2123 if (mAccount != null) { 2124 outState.putParcelable(SAVED_ACCOUNT, mAccount); 2125 } 2126 if (mFolder != null) { 2127 outState.putParcelable(SAVED_FOLDER, mFolder); 2128 } 2129 // If this is a search activity, let's store the search query term as well. 2130 if (ConversationListContext.isSearchResult(mConvListContext)) { 2131 outState.putString(SAVED_QUERY, mConvListContext.searchQuery); 2132 } 2133 if (mCurrentConversation != null && mViewMode.isConversationMode()) { 2134 outState.putParcelable(SAVED_CONVERSATION, mCurrentConversation); 2135 } 2136 if (!mSelectedSet.isEmpty()) { 2137 outState.putParcelable(SAVED_SELECTED_SET, mSelectedSet); 2138 } 2139 if (mToastBar.getVisibility() == View.VISIBLE) { 2140 outState.putParcelable(SAVED_TOAST_BAR_OP, mToastBar.getOperation()); 2141 } 2142 final ConversationListFragment convListFragment = getConversationListFragment(); 2143 if (convListFragment != null) { 2144 convListFragment.getAnimatedAdapter().onSaveInstanceState(outState); 2145 } 2146 // If there is a dialog being shown, save the state so we can create a listener for it. 2147 if (mDialogAction != -1) { 2148 outState.putInt(SAVED_ACTION, mDialogAction); 2149 outState.putBoolean(SAVED_ACTION_FROM_SELECTED, mDialogFromSelectedSet); 2150 } 2151 if (mDetachedConvUri != null) { 2152 outState.putParcelable(SAVED_DETACHED_CONV_URI, mDetachedConvUri); 2153 } 2154 2155 outState.putParcelable(SAVED_HIERARCHICAL_FOLDER, mFolderListFolder); 2156 mSafeToModifyFragments = false; 2157 2158 outState.putParcelable(SAVED_INBOX_KEY, mInbox); 2159 2160 outState.putBundle(SAVED_CONVERSATION_LIST_SCROLL_POSITIONS, 2161 mConversationListScrollPositions); 2162 } 2163 2164 /** 2165 * @see #mSafeToModifyFragments 2166 */ 2167 protected boolean safeToModifyFragments() { 2168 return mSafeToModifyFragments; 2169 } 2170 2171 @Override 2172 public void executeSearch(String query) { 2173 AnalyticsTimer.getInstance().trackStart(AnalyticsTimer.SEARCH_TO_LIST); 2174 Intent intent = new Intent(); 2175 intent.setAction(Intent.ACTION_SEARCH); 2176 intent.putExtra(ConversationListContext.EXTRA_SEARCH_QUERY, query); 2177 intent.putExtra(Utils.EXTRA_ACCOUNT, mAccount); 2178 intent.setComponent(mActivity.getComponentName()); 2179 mActionBarView.collapseSearch(); 2180 // Call startActivityForResult here so we can tell if we have navigated to a different folder 2181 // or account from search results. 2182 mActivity.startActivityForResult(intent, CHANGE_NAVIGATION_REQUEST_CODE); 2183 } 2184 2185 @Override 2186 public void onStop() { 2187 NotificationActionUtils.unregisterUndoNotificationObserver(mUndoNotificationObserver); 2188 } 2189 2190 @Override 2191 public void onDestroy() { 2192 // stop listening to the cursor on e.g. configuration changes 2193 if (mConversationListCursor != null) { 2194 mConversationListCursor.removeListener(this); 2195 } 2196 mDrawIdler.setListener(null); 2197 mDrawIdler.setRootView(null); 2198 // unregister the ViewPager's observer on the conversation cursor 2199 mPagerController.onDestroy(); 2200 mActionBarView.onDestroy(); 2201 mRecentFolderList.destroy(); 2202 mDestroyed = true; 2203 mHandler.removeCallbacks(mLogServiceChecker); 2204 mLogServiceChecker = null; 2205 } 2206 2207 /** 2208 * Set the Action Bar icon according to the mode. The Action Bar icon can contain a back button 2209 * or not. The individual controller is responsible for changing the icon based on the mode. 2210 */ 2211 protected abstract void resetActionBarIcon(); 2212 2213 /** 2214 * {@inheritDoc} Subclasses must override this to listen to mode changes 2215 * from the ViewMode. Subclasses <b>must</b> call the parent's 2216 * onViewModeChanged since the parent will handle common state changes. 2217 */ 2218 @Override 2219 public void onViewModeChanged(int newMode) { 2220 // When we step away from the conversation mode, we don't have a current conversation 2221 // anymore. Let's blank it out so clients calling getCurrentConversation are not misled. 2222 if (!ViewMode.isConversationMode(newMode)) { 2223 setCurrentConversation(null); 2224 } 2225 2226 // If the viewmode is not set, preserve existing icon. 2227 if (newMode != ViewMode.UNKNOWN) { 2228 resetActionBarIcon(); 2229 } 2230 2231 if (isDrawerEnabled()) { 2232 /** If the folder doesn't exist, or its parent URI is empty, 2233 * this is not a child folder */ 2234 final boolean isTopLevel = (mFolder == null) || (mFolder.parent == Uri.EMPTY); 2235 mDrawerToggle.setDrawerIndicatorEnabled( 2236 getShouldShowDrawerIndicator(newMode, isTopLevel)); 2237 mDrawerContainer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED); 2238 closeDrawerIfOpen(); 2239 } 2240 } 2241 2242 /** 2243 * Returns true if the drawer icon is shown 2244 * @param viewMode the current view mode 2245 * @param isTopLevel true if the current folder is not a child 2246 * @return whether the drawer indicator is shown 2247 */ 2248 private boolean getShouldShowDrawerIndicator(final int viewMode, 2249 final boolean isTopLevel) { 2250 // If search list/conv mode: disable indicator 2251 // Indicator is enabled either in conversation list or folder list mode. 2252 return isDrawerEnabled() && !ViewMode.isSearchMode(viewMode) 2253 && (viewMode == ViewMode.CONVERSATION_LIST && isTopLevel); 2254 } 2255 2256 public void disablePagerUpdates() { 2257 mPagerController.stopListening(); 2258 } 2259 2260 public boolean isDestroyed() { 2261 return mDestroyed; 2262 } 2263 2264 @Override 2265 public void commitDestructiveActions(boolean animate) { 2266 ConversationListFragment fragment = getConversationListFragment(); 2267 if (fragment != null) { 2268 fragment.commitDestructiveActions(animate); 2269 } 2270 } 2271 2272 @Override 2273 public void onWindowFocusChanged(boolean hasFocus) { 2274 final ConversationListFragment convList = getConversationListFragment(); 2275 // hasFocus already ensures that the window is in focus, so we don't need to call 2276 // AAC.isFragmentVisible(convList) here. 2277 if (hasFocus && convList != null && convList.isVisible()) { 2278 // The conversation list is visible. 2279 informCursorVisiblity(true); 2280 } 2281 } 2282 2283 /** 2284 * Set the account, and carry out all the account-related changes that rely on this. 2285 * @param account new account to set to. 2286 */ 2287 private void setAccount(Account account) { 2288 if (account == null) { 2289 LogUtils.w(LOG_TAG, new Error(), 2290 "AAC ignoring null (presumably invalid) account restoration"); 2291 return; 2292 } 2293 LogUtils.d(LOG_TAG, "AbstractActivityController.setAccount(): account = %s", account.uri); 2294 mAccount = account; 2295 // Only change AAC state here. Do *not* modify any other object's state. The object 2296 // should listen on account changes. 2297 restartOptionalLoader(LOADER_RECENT_FOLDERS, mFolderCallbacks, Bundle.EMPTY); 2298 mActivity.invalidateOptionsMenu(); 2299 disableNotificationsOnAccountChange(mAccount); 2300 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR, mAccountCallbacks, Bundle.EMPTY); 2301 // The Mail instance can be null during test runs. 2302 final MailAppProvider instance = MailAppProvider.getInstance(); 2303 if (instance != null) { 2304 instance.setLastViewedAccount(mAccount.uri.toString()); 2305 } 2306 if (account.settings == null) { 2307 LogUtils.w(LOG_TAG, new Error(), "AAC ignoring account with null settings."); 2308 return; 2309 } 2310 mAccountObservers.notifyChanged(); 2311 perhapsEnterWaitMode(); 2312 } 2313 2314 /** 2315 * Restore the state from the previous bundle. Subclasses should call this 2316 * method from the parent class, since it performs important UI 2317 * initialization. 2318 * 2319 * @param savedState previous state 2320 */ 2321 @Override 2322 public void onRestoreInstanceState(Bundle savedState) { 2323 mDetachedConvUri = savedState.getParcelable(SAVED_DETACHED_CONV_URI); 2324 if (savedState.containsKey(SAVED_CONVERSATION)) { 2325 // Open the conversation. 2326 final Conversation conversation = savedState.getParcelable(SAVED_CONVERSATION); 2327 if (conversation != null && conversation.position < 0) { 2328 // Set the position to 0 on this conversation, as we don't know where it is 2329 // in the list 2330 conversation.position = 0; 2331 } 2332 showConversation(conversation); 2333 } 2334 2335 if (savedState.containsKey(SAVED_TOAST_BAR_OP)) { 2336 ToastBarOperation op = savedState.getParcelable(SAVED_TOAST_BAR_OP); 2337 if (op != null) { 2338 if (op.getType() == ToastBarOperation.UNDO) { 2339 onUndoAvailable(op); 2340 } else if (op.getType() == ToastBarOperation.ERROR) { 2341 onError(mFolder, true); 2342 } 2343 } 2344 } 2345 mFolderListFolder = savedState.getParcelable(SAVED_HIERARCHICAL_FOLDER); 2346 final ConversationListFragment convListFragment = getConversationListFragment(); 2347 if (convListFragment != null) { 2348 convListFragment.getAnimatedAdapter().onRestoreInstanceState(savedState); 2349 } 2350 /* 2351 * Restore the state of selected conversations. This needs to be done after the correct mode 2352 * is set and the action bar is fully initialized. If not, several key pieces of state 2353 * information will be missing, and the split views may not be initialized correctly. 2354 */ 2355 restoreSelectedConversations(savedState); 2356 // Order is important!!! 2357 // The dialog listener needs to happen *after* the selected set is restored. 2358 2359 // If there has been an orientation change, and we need to recreate the listener for the 2360 // confirm dialog fragment (delete/archive/...), then do it here. 2361 if (mDialogAction != -1) { 2362 makeDialogListener(mDialogAction, mDialogFromSelectedSet, 2363 getUndoCallbackForDestructiveActionsWithAutoAdvance( 2364 mDialogAction, mCurrentConversation)); 2365 } 2366 2367 mInbox = savedState.getParcelable(SAVED_INBOX_KEY); 2368 2369 mConversationListScrollPositions.clear(); 2370 mConversationListScrollPositions.putAll( 2371 savedState.getBundle(SAVED_CONVERSATION_LIST_SCROLL_POSITIONS)); 2372 } 2373 2374 /** 2375 * Handle an intent to open the app. This method is called only when there is no saved state, 2376 * so we need to set state that wasn't set before. It is correct to change the viewmode here 2377 * since it has not been previously set. 2378 * 2379 * This method is called for a subset of the reasons mentioned in 2380 * {@link #onCreate(android.os.Bundle)}. Notably, this is called when launching the app from 2381 * notifications, widgets, and shortcuts. 2382 * @param intent intent passed to the activity. 2383 */ 2384 private void handleIntent(Intent intent) { 2385 LogUtils.d(LOG_TAG, "IN AAC.handleIntent. action=%s", intent.getAction()); 2386 if (Intent.ACTION_VIEW.equals(intent.getAction())) { 2387 if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) { 2388 setAccount(Account.newInstance(intent.getStringExtra(Utils.EXTRA_ACCOUNT))); 2389 } 2390 if (mAccount == null) { 2391 return; 2392 } 2393 final boolean isConversationMode = intent.hasExtra(Utils.EXTRA_CONVERSATION); 2394 2395 if (intent.getBooleanExtra(Utils.EXTRA_FROM_NOTIFICATION, false)) { 2396 Analytics.getInstance().setCustomDimension(Analytics.CD_INDEX_ACCOUNT_TYPE, 2397 AnalyticsUtils.getAccountTypeForAccount(mAccount.getEmailAddress())); 2398 Analytics.getInstance().sendEvent("notification_click", 2399 isConversationMode ? "conversation" : "conversation_list", null, 0); 2400 } 2401 2402 if (isConversationMode && mViewMode.getMode() == ViewMode.UNKNOWN) { 2403 mViewMode.enterConversationMode(); 2404 } else { 2405 mViewMode.enterConversationListMode(); 2406 } 2407 // Put the folder and conversation, and ask the loader to create this folder. 2408 final Bundle args = new Bundle(); 2409 2410 final Uri folderUri; 2411 if (intent.hasExtra(Utils.EXTRA_FOLDER_URI)) { 2412 folderUri = intent.getParcelableExtra(Utils.EXTRA_FOLDER_URI); 2413 } else if (intent.hasExtra(Utils.EXTRA_FOLDER)) { 2414 final Folder folder = 2415 Folder.fromString(intent.getStringExtra(Utils.EXTRA_FOLDER)); 2416 folderUri = folder.folderUri.fullUri; 2417 } else { 2418 final Bundle extras = intent.getExtras(); 2419 LogUtils.d(LOG_TAG, "Couldn't find a folder URI in the extras: %s", 2420 extras == null ? "null" : extras.toString()); 2421 folderUri = mAccount.settings.defaultInbox; 2422 } 2423 2424 // Check if we should load all conversations instead of using 2425 // the default behavior which loads an initial subset. 2426 mIgnoreInitialConversationLimit = 2427 intent.getBooleanExtra(Utils.EXTRA_IGNORE_INITIAL_CONVERSATION_LIMIT, false); 2428 2429 args.putParcelable(Utils.EXTRA_FOLDER_URI, folderUri); 2430 args.putParcelable(Utils.EXTRA_CONVERSATION, 2431 intent.getParcelableExtra(Utils.EXTRA_CONVERSATION)); 2432 restartOptionalLoader(LOADER_FIRST_FOLDER, mFolderCallbacks, args); 2433 } else if (Intent.ACTION_SEARCH.equals(intent.getAction())) { 2434 if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) { 2435 mHaveSearchResults = false; 2436 // Save this search query for future suggestions. 2437 final String query = intent.getStringExtra(SearchManager.QUERY); 2438 final String authority = mContext.getString(R.string.suggestions_authority); 2439 final SearchRecentSuggestions suggestions = new SearchRecentSuggestions( 2440 mContext, authority, SuggestionsProvider.MODE); 2441 suggestions.saveRecentQuery(query, null); 2442 setAccount((Account) intent.getParcelableExtra(Utils.EXTRA_ACCOUNT)); 2443 fetchSearchFolder(intent); 2444 if (shouldEnterSearchConvMode()) { 2445 mViewMode.enterSearchResultsConversationMode(); 2446 } else { 2447 mViewMode.enterSearchResultsListMode(); 2448 } 2449 } else { 2450 LogUtils.e(LOG_TAG, "Missing account extra from search intent. Finishing"); 2451 mActivity.finish(); 2452 } 2453 } 2454 if (mAccount != null) { 2455 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR, mAccountCallbacks, Bundle.EMPTY); 2456 } 2457 } 2458 2459 /** 2460 * Returns true if we should enter conversation mode with search. 2461 */ 2462 protected final boolean shouldEnterSearchConvMode() { 2463 return mHaveSearchResults && Utils.showTwoPaneSearchResults(mActivity.getActivityContext()); 2464 } 2465 2466 /** 2467 * Copy any selected conversations stored in the saved bundle into our selection set, 2468 * triggering {@link ConversationSetObserver} callbacks as our selection set changes. 2469 * 2470 */ 2471 private void restoreSelectedConversations(Bundle savedState) { 2472 if (savedState == null) { 2473 mSelectedSet.clear(); 2474 return; 2475 } 2476 final ConversationSelectionSet selectedSet = savedState.getParcelable(SAVED_SELECTED_SET); 2477 if (selectedSet == null || selectedSet.isEmpty()) { 2478 mSelectedSet.clear(); 2479 return; 2480 } 2481 2482 // putAll will take care of calling our registered onSetPopulated method 2483 mSelectedSet.putAll(selectedSet); 2484 } 2485 2486 /** 2487 * Show the conversation provided in the arguments. It is safe to pass a null conversation 2488 * object, which is a signal to back out of conversation view mode. 2489 * Child classes must call super.showConversation() <b>before</b> their own implementations. 2490 * @param conversation the conversation to be shown, or null if we want to back out to list 2491 * mode. 2492 * onLoadFinished(Loader, Cursor) on any callback. 2493 */ 2494 protected void showConversation(Conversation conversation) { 2495 if (conversation != null) { 2496 Utils.sConvLoadTimer.start(); 2497 } 2498 2499 MailLogService.log("AbstractActivityController", "showConversation(%s)", conversation); 2500 // Set the current conversation just in case it wasn't already set. 2501 setCurrentConversation(conversation); 2502 } 2503 2504 /** 2505 * Children can override this method, but they must call super.showWaitForInitialization(). 2506 * {@inheritDoc} 2507 */ 2508 @Override 2509 public void showWaitForInitialization() { 2510 mViewMode.enterWaitingForInitializationMode(); 2511 mWaitFragment = WaitFragment.newInstance(mAccount, true /* expectingMessages */); 2512 } 2513 2514 private void updateWaitMode() { 2515 final FragmentManager manager = mActivity.getFragmentManager(); 2516 final WaitFragment waitFragment = 2517 (WaitFragment)manager.findFragmentByTag(TAG_WAIT); 2518 if (waitFragment != null) { 2519 waitFragment.updateAccount(mAccount); 2520 } 2521 } 2522 2523 /** 2524 * Remove the "Waiting for Initialization" fragment. Child classes are free to override this 2525 * method, though they must call the parent implementation <b>after</b> they do anything. 2526 */ 2527 protected void hideWaitForInitialization() { 2528 mWaitFragment = null; 2529 } 2530 2531 /** 2532 * Use the instance variable and the wait fragment's tag to get the wait fragment. This is 2533 * far superior to using the value of mWaitFragment, which might be invalid or might refer 2534 * to a fragment after it has been destroyed. 2535 * @return a wait fragment that is already attached to the activity, if one exists 2536 */ 2537 protected final WaitFragment getWaitFragment() { 2538 final FragmentManager manager = mActivity.getFragmentManager(); 2539 final WaitFragment waitFrag = (WaitFragment) manager.findFragmentByTag(TAG_WAIT); 2540 if (waitFrag != null) { 2541 // The Fragment Manager knows better, so use its instance. 2542 mWaitFragment = waitFrag; 2543 } 2544 return mWaitFragment; 2545 } 2546 2547 /** 2548 * Returns true if we are waiting for the account to sync, and cannot show any folders or 2549 * conversation for the current account yet. 2550 */ 2551 private boolean inWaitMode() { 2552 final WaitFragment waitFragment = getWaitFragment(); 2553 if (waitFragment != null) { 2554 final Account fragmentAccount = waitFragment.getAccount(); 2555 return fragmentAccount != null && fragmentAccount.uri.equals(mAccount.uri) && 2556 mViewMode.getMode() == ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION; 2557 } 2558 return false; 2559 } 2560 2561 /** 2562 * Children can override this method, but they must call super.showConversationList(). 2563 * {@inheritDoc} 2564 */ 2565 @Override 2566 public void showConversationList(ConversationListContext listContext) { 2567 } 2568 2569 @Override 2570 public final void onConversationSelected(Conversation conversation, boolean inLoaderCallbacks) { 2571 final ConversationListFragment convListFragment = getConversationListFragment(); 2572 if (convListFragment != null && convListFragment.getAnimatedAdapter() != null) { 2573 convListFragment.getAnimatedAdapter().onConversationSelected(); 2574 } 2575 // Only animate destructive actions if we are going to be showing the 2576 // conversation list when we show the next conversation. 2577 commitDestructiveActions(mIsTablet); 2578 showConversation(conversation); 2579 } 2580 2581 @Override 2582 public final void onCabModeEntered() { 2583 final ConversationListFragment convListFragment = getConversationListFragment(); 2584 if (convListFragment != null && convListFragment.getAnimatedAdapter() != null) { 2585 convListFragment.getAnimatedAdapter().onCabModeEntered(); 2586 } 2587 } 2588 2589 @Override 2590 public final void onCabModeExited() { 2591 final ConversationListFragment convListFragment = getConversationListFragment(); 2592 if (convListFragment != null && convListFragment.getAnimatedAdapter() != null) { 2593 convListFragment.getAnimatedAdapter().onCabModeExited(); 2594 } 2595 } 2596 2597 @Override 2598 public Conversation getCurrentConversation() { 2599 return mCurrentConversation; 2600 } 2601 2602 /** 2603 * Set the current conversation. This is the conversation on which all actions are performed. 2604 * Do not modify mCurrentConversation except through this method, which makes it easy to 2605 * perform common actions associated with changing the current conversation. 2606 * @param conversation new conversation to view. Passing null indicates that we are backing 2607 * out to conversation list mode. 2608 */ 2609 @Override 2610 public void setCurrentConversation(Conversation conversation) { 2611 // The controller should come out of detached mode if a new conversation is viewed, or if 2612 // we are going back to conversation list mode. 2613 if (mDetachedConvUri != null && (conversation == null 2614 || !mDetachedConvUri.equals(conversation.uri))) { 2615 clearDetachedMode(); 2616 } 2617 2618 // Must happen *before* setting mCurrentConversation because this sets 2619 // conversation.position if a cursor is available. 2620 mTracker.initialize(conversation); 2621 mCurrentConversation = conversation; 2622 2623 if (mCurrentConversation != null) { 2624 mActionBarView.setCurrentConversation(mCurrentConversation); 2625 mActivity.invalidateOptionsMenu(); 2626 } 2627 } 2628 2629 /** 2630 * {@link LoaderManager} currently has a bug in 2631 * {@link LoaderManager#restartLoader(int, Bundle, android.app.LoaderManager.LoaderCallbacks)} 2632 * where, if a previous onCreateLoader returned a null loader, this method will NPE. Work around 2633 * this bug by destroying any loaders that may have been created as null (essentially because 2634 * they are optional loads, and may not apply to a particular account). 2635 * <p> 2636 * A simple null check before restarting a loader will not work, because that would not 2637 * give the controller a chance to invalidate UI corresponding the prior loader result. 2638 * 2639 * @param id loader ID to safely restart 2640 * @param handler the LoaderCallback which will handle this loader ID. 2641 * @param args arguments, if any, to be passed to the loader. Use {@link Bundle#EMPTY} if no 2642 * arguments need to be specified. 2643 */ 2644 private void restartOptionalLoader(int id, LoaderManager.LoaderCallbacks handler, Bundle args) { 2645 final LoaderManager lm = mActivity.getLoaderManager(); 2646 lm.destroyLoader(id); 2647 lm.restartLoader(id, args, handler); 2648 } 2649 2650 @Override 2651 public void registerConversationListObserver(DataSetObserver observer) { 2652 mConversationListObservable.registerObserver(observer); 2653 } 2654 2655 @Override 2656 public void unregisterConversationListObserver(DataSetObserver observer) { 2657 try { 2658 mConversationListObservable.unregisterObserver(observer); 2659 } catch (IllegalStateException e) { 2660 // Log instead of crash 2661 LogUtils.e(LOG_TAG, e, "unregisterConversationListObserver called for an observer that " 2662 + "hasn't been registered"); 2663 } 2664 } 2665 2666 @Override 2667 public void registerFolderObserver(DataSetObserver observer) { 2668 mFolderObservable.registerObserver(observer); 2669 } 2670 2671 @Override 2672 public void unregisterFolderObserver(DataSetObserver observer) { 2673 try { 2674 mFolderObservable.unregisterObserver(observer); 2675 } catch (IllegalStateException e) { 2676 // Log instead of crash 2677 LogUtils.e(LOG_TAG, e, "unregisterFolderObserver called for an observer that " 2678 + "hasn't been registered"); 2679 } 2680 } 2681 2682 @Override 2683 public void registerConversationLoadedObserver(DataSetObserver observer) { 2684 mPagerController.registerConversationLoadedObserver(observer); 2685 } 2686 2687 @Override 2688 public void unregisterConversationLoadedObserver(DataSetObserver observer) { 2689 try { 2690 mPagerController.unregisterConversationLoadedObserver(observer); 2691 } catch (IllegalStateException e) { 2692 // Log instead of crash 2693 LogUtils.e(LOG_TAG, e, "unregisterConversationLoadedObserver called for an observer " 2694 + "that hasn't been registered"); 2695 } 2696 } 2697 2698 /** 2699 * Returns true if the number of accounts is different, or if the current account has 2700 * changed. This method is meant to filter frequent changes to the list of 2701 * accounts, and only return true if the new list is substantially different from the existing 2702 * list. Returning true is safe here, it leads to more work in creating the 2703 * same account list again. 2704 * @param accountCursor the cursor which points to all the accounts. 2705 * @return true if the number of accounts is changed or current account missing from the list. 2706 */ 2707 private boolean accountsUpdated(ObjectCursor<Account> accountCursor) { 2708 // Check to see if the current account hasn't been set, or the account cursor is empty 2709 if (mAccount == null || !accountCursor.moveToFirst()) { 2710 return true; 2711 } 2712 2713 // Check to see if the number of accounts are different, from the number we saw on the last 2714 // updated 2715 if (mCurrentAccountUris.size() != accountCursor.getCount()) { 2716 return true; 2717 } 2718 2719 // Check to see if the account list is different or if the current account is not found in 2720 // the cursor. 2721 boolean foundCurrentAccount = false; 2722 do { 2723 final Account account = accountCursor.getModel(); 2724 if (!foundCurrentAccount && mAccount.uri.equals(account.uri)) { 2725 if (mAccount.settingsDiffer(account)) { 2726 // Settings changed, and we don't need to look any further. 2727 return true; 2728 } 2729 foundCurrentAccount = true; 2730 } 2731 // Is there a new account that we do not know about? 2732 if (!mCurrentAccountUris.contains(account.uri)) { 2733 return true; 2734 } 2735 } while (accountCursor.moveToNext()); 2736 2737 // As long as we found the current account, the list hasn't been updated 2738 return !foundCurrentAccount; 2739 } 2740 2741 /** 2742 * Updates accounts for the app. If the current account is missing, the first 2743 * account in the list is set to the current account (we <em>have</em> to choose something). 2744 * 2745 * @param accounts cursor into the AccountCache 2746 * @return true if the update was successful, false otherwise 2747 */ 2748 private boolean updateAccounts(ObjectCursor<Account> accounts) { 2749 if (accounts == null || !accounts.moveToFirst()) { 2750 return false; 2751 } 2752 2753 final Account[] allAccounts = Account.getAllAccounts(accounts); 2754 // A match for the current account's URI in the list of accounts. 2755 Account currentFromList = null; 2756 2757 // Save the uris for the accounts and find the current account in the updated cursor. 2758 mCurrentAccountUris.clear(); 2759 for (final Account account : allAccounts) { 2760 LogUtils.d(LOG_TAG, "updateAccounts(%s)", account); 2761 mCurrentAccountUris.add(account.uri); 2762 if (mAccount != null && account.uri.equals(mAccount.uri)) { 2763 currentFromList = account; 2764 } 2765 } 2766 2767 // 1. current account is already set and is in allAccounts: 2768 // 1a. It has changed -> load the updated account. 2769 // 2b. It is unchanged -> no-op 2770 // 2. current account is set and is not in allAccounts -> pick first (acct was deleted?) 2771 // 3. saved preference has an account -> pick that one 2772 // 4. otherwise just pick first 2773 2774 boolean accountChanged = false; 2775 /// Assume case 4, initialize to first account, and see if we can find anything better. 2776 Account newAccount = allAccounts[0]; 2777 if (currentFromList != null) { 2778 // Case 1: Current account exists but has changed 2779 if (!currentFromList.equals(mAccount)) { 2780 newAccount = currentFromList; 2781 accountChanged = true; 2782 } 2783 // Case 1b: else, current account is unchanged: nothing to do. 2784 } else { 2785 // Case 2: Current account is not in allAccounts, the account needs to change. 2786 accountChanged = true; 2787 if (mAccount == null) { 2788 // Case 3: Check for last viewed account, and check if it exists in the list. 2789 final String lastAccountUri = MailAppProvider.getInstance().getLastViewedAccount(); 2790 if (lastAccountUri != null) { 2791 for (final Account account : allAccounts) { 2792 if (lastAccountUri.equals(account.uri.toString())) { 2793 newAccount = account; 2794 break; 2795 } 2796 } 2797 } 2798 } 2799 } 2800 if (accountChanged) { 2801 changeAccount(newAccount); 2802 } 2803 2804 // Whether we have updated the current account or not, we need to update the list of 2805 // accounts in the ActionBar. 2806 mAllAccounts = allAccounts; 2807 mAllAccountObservers.notifyChanged(); 2808 return (allAccounts.length > 0); 2809 } 2810 2811 private void disableNotifications() { 2812 mNewEmailReceiver.activate(mContext, this); 2813 } 2814 2815 private void enableNotifications() { 2816 mNewEmailReceiver.deactivate(); 2817 } 2818 2819 private void disableNotificationsOnAccountChange(Account account) { 2820 // If the new mail suppression receiver is activated for a different account, we want to 2821 // activate it for the new account. 2822 if (mNewEmailReceiver.activated() && 2823 !mNewEmailReceiver.notificationsDisabledForAccount(account)) { 2824 // Deactivate the current receiver, otherwise multiple receivers may be registered. 2825 mNewEmailReceiver.deactivate(); 2826 mNewEmailReceiver.activate(mContext, this); 2827 } 2828 } 2829 2830 /** 2831 * Destructive actions on Conversations. This class should only be created by controllers, and 2832 * clients should only require {@link DestructiveAction}s, not specific implementations of the. 2833 * Only the controllers should know what kind of destructive actions are being created. 2834 */ 2835 public class ConversationAction implements DestructiveAction { 2836 /** 2837 * The action to be performed. This is specified as the resource ID of the menu item 2838 * corresponding to this action: R.id.delete, R.id.report_spam, etc. 2839 */ 2840 private final int mAction; 2841 /** The action will act upon these conversations */ 2842 private final Collection<Conversation> mTarget; 2843 /** Whether this destructive action has already been performed */ 2844 private boolean mCompleted; 2845 /** Whether this is an action on the currently selected set. */ 2846 private final boolean mIsSelectedSet; 2847 2848 private UndoCallback mCallback; 2849 2850 /** 2851 * Create a listener object. 2852 * @param action action is one of four constants: R.id.y_button (archive), 2853 * R.id.delete , R.id.mute, and R.id.report_spam. 2854 * @param target Conversation that we want to apply the action to. 2855 * @param isBatch whether the conversations are in the currently selected batch set. 2856 */ 2857 public ConversationAction(int action, Collection<Conversation> target, boolean isBatch) { 2858 mAction = action; 2859 mTarget = ImmutableList.copyOf(target); 2860 mIsSelectedSet = isBatch; 2861 } 2862 2863 @Override 2864 public void setUndoCallback(UndoCallback undoCallback) { 2865 mCallback = undoCallback; 2866 } 2867 2868 /** 2869 * The action common to child classes. This performs the action specified in the constructor 2870 * on the conversations given here. 2871 */ 2872 @Override 2873 public void performAction() { 2874 if (isPerformed()) { 2875 return; 2876 } 2877 boolean undoEnabled = mAccount.supportsCapability(AccountCapabilities.UNDO); 2878 2879 // Are we destroying the currently shown conversation? Show the next one. 2880 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)){ 2881 LogUtils.d(LOG_TAG, "ConversationAction.performAction():" 2882 + "\nmTarget=%s\nCurrent=%s", 2883 Conversation.toString(mTarget), mCurrentConversation); 2884 } 2885 2886 if (mConversationListCursor == null) { 2887 LogUtils.e(LOG_TAG, "null ConversationCursor in ConversationAction.performAction():" 2888 + "\nmTarget=%s\nCurrent=%s", 2889 Conversation.toString(mTarget), mCurrentConversation); 2890 return; 2891 } 2892 2893 if (mAction == R.id.archive) { 2894 LogUtils.d(LOG_TAG, "Archiving"); 2895 mConversationListCursor.archive(mTarget, mCallback); 2896 } else if (mAction == R.id.delete) { 2897 LogUtils.d(LOG_TAG, "Deleting"); 2898 mConversationListCursor.delete(mTarget, mCallback); 2899 if (mFolder.supportsCapability(FolderCapabilities.DELETE_ACTION_FINAL)) { 2900 undoEnabled = false; 2901 } 2902 } else if (mAction == R.id.mute) { 2903 LogUtils.d(LOG_TAG, "Muting"); 2904 if (mFolder.supportsCapability(FolderCapabilities.DESTRUCTIVE_MUTE)) { 2905 for (Conversation c : mTarget) { 2906 c.localDeleteOnUpdate = true; 2907 } 2908 } 2909 mConversationListCursor.mute(mTarget, mCallback); 2910 } else if (mAction == R.id.report_spam) { 2911 LogUtils.d(LOG_TAG, "Reporting spam"); 2912 mConversationListCursor.reportSpam(mTarget, mCallback); 2913 } else if (mAction == R.id.mark_not_spam) { 2914 LogUtils.d(LOG_TAG, "Marking not spam"); 2915 mConversationListCursor.reportNotSpam(mTarget, mCallback); 2916 } else if (mAction == R.id.report_phishing) { 2917 LogUtils.d(LOG_TAG, "Reporting phishing"); 2918 mConversationListCursor.reportPhishing(mTarget, mCallback); 2919 } else if (mAction == R.id.remove_star) { 2920 LogUtils.d(LOG_TAG, "Removing star"); 2921 // Star removal is destructive in the Starred folder. 2922 mConversationListCursor.updateBoolean(mTarget, ConversationColumns.STARRED, 2923 false); 2924 } else if (mAction == R.id.mark_not_important) { 2925 LogUtils.d(LOG_TAG, "Marking not-important"); 2926 // Marking not important is destructive in a mailbox 2927 // containing only important messages 2928 if (mFolder != null && mFolder.isImportantOnly()) { 2929 for (Conversation conv : mTarget) { 2930 conv.localDeleteOnUpdate = true; 2931 } 2932 } 2933 mConversationListCursor.updateInt(mTarget, ConversationColumns.PRIORITY, 2934 UIProvider.ConversationPriority.LOW); 2935 } else if (mAction == R.id.discard_drafts) { 2936 LogUtils.d(LOG_TAG, "Discarding draft messages"); 2937 // Discarding draft messages is destructive in a "draft" mailbox 2938 if (mFolder != null && mFolder.isDraft()) { 2939 for (Conversation conv : mTarget) { 2940 conv.localDeleteOnUpdate = true; 2941 } 2942 } 2943 mConversationListCursor.discardDrafts(mTarget); 2944 // We don't support undoing discarding drafts 2945 undoEnabled = false; 2946 } 2947 if (undoEnabled) { 2948 mHandler.postDelayed(new Runnable() { 2949 @Override 2950 public void run() { 2951 onUndoAvailable(new ToastBarOperation(mTarget.size(), mAction, 2952 ToastBarOperation.UNDO, mIsSelectedSet, mFolder)); 2953 } 2954 }, mShowUndoBarDelay); 2955 } 2956 refreshConversationList(); 2957 if (mIsSelectedSet) { 2958 mSelectedSet.clear(); 2959 } 2960 } 2961 2962 /** 2963 * Returns true if this action has been performed, false otherwise. 2964 * 2965 */ 2966 private synchronized boolean isPerformed() { 2967 if (mCompleted) { 2968 return true; 2969 } 2970 mCompleted = true; 2971 return false; 2972 } 2973 } 2974 2975 // Called from the FolderSelectionDialog after a user is done selecting folders to assign the 2976 // conversations to. 2977 @Override 2978 public final void assignFolder(Collection<FolderOperation> folderOps, 2979 Collection<Conversation> target, boolean batch, boolean showUndo, 2980 final boolean isMoveTo) { 2981 // Actions are destructive only when the current folder can be assigned 2982 // to (which is the same as being able to un-assign a conversation from the folder) and 2983 // when the list of folders contains the current folder. 2984 final boolean isDestructive = mFolder 2985 .supportsCapability(FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES) 2986 && FolderOperation.isDestructive(folderOps, mFolder); 2987 LogUtils.d(LOG_TAG, "onFolderChangesCommit: isDestructive = %b", isDestructive); 2988 if (isDestructive) { 2989 for (final Conversation c : target) { 2990 c.localDeleteOnUpdate = true; 2991 } 2992 } 2993 final DestructiveAction folderChange; 2994 final UndoCallback undoCallback = isMoveTo ? 2995 getUndoCallbackForDestructiveActionsWithAutoAdvance(R.id.move_to, 2996 mCurrentConversation) 2997 : null; 2998 // Update the UI elements depending no their visibility and availability 2999 // TODO(viki): Consolidate this into a single method requestDelete. 3000 if (isDestructive) { 3001 /* 3002 * If this is a MOVE operation, we want the action folder to be the destination folder. 3003 * Otherwise, we want it to be the current folder. 3004 * 3005 * A set of folder operations is a move if there are exactly two operations: an add and 3006 * a remove. 3007 */ 3008 final Folder actionFolder; 3009 if (folderOps.size() != 2) { 3010 actionFolder = mFolder; 3011 } else { 3012 Folder addedFolder = null; 3013 boolean hasRemove = false; 3014 for (final FolderOperation folderOperation : folderOps) { 3015 if (folderOperation.mAdd) { 3016 addedFolder = folderOperation.mFolder; 3017 } else { 3018 hasRemove = true; 3019 } 3020 } 3021 3022 if (hasRemove && addedFolder != null) { 3023 actionFolder = addedFolder; 3024 } else { 3025 actionFolder = mFolder; 3026 } 3027 } 3028 3029 folderChange = getDeferredFolderChange(target, folderOps, isDestructive, 3030 batch, showUndo, isMoveTo, actionFolder, undoCallback); 3031 delete(0, target, folderChange, batch); 3032 } else { 3033 folderChange = getFolderChange(target, folderOps, isDestructive, 3034 batch, showUndo, false /* isMoveTo */, mFolder, undoCallback); 3035 requestUpdate(folderChange); 3036 } 3037 } 3038 3039 @Override 3040 public final void onRefreshRequired() { 3041 if (isAnimating() || isDragging()) { 3042 LogUtils.i(ConversationCursor.LOG_TAG, "onRefreshRequired: delay until animating done"); 3043 return; 3044 } 3045 // Refresh the query in the background 3046 if (mConversationListCursor.isRefreshRequired()) { 3047 mConversationListCursor.refresh(); 3048 } 3049 } 3050 3051 @Override 3052 public void startDragMode() { 3053 mIsDragHappening = true; 3054 } 3055 3056 @Override 3057 public void stopDragMode() { 3058 mIsDragHappening = false; 3059 if (mConversationListCursor.isRefreshReady()) { 3060 LogUtils.i(ConversationCursor.LOG_TAG, "Stopped dragging: try sync"); 3061 onRefreshReady(); 3062 } 3063 3064 if (mConversationListCursor.isRefreshRequired()) { 3065 LogUtils.i(ConversationCursor.LOG_TAG, "Stopped dragging: refresh"); 3066 mConversationListCursor.refresh(); 3067 } 3068 } 3069 3070 private boolean isDragging() { 3071 return mIsDragHappening; 3072 } 3073 3074 @Override 3075 public boolean isAnimating() { 3076 boolean isAnimating = false; 3077 ConversationListFragment convListFragment = getConversationListFragment(); 3078 if (convListFragment != null) { 3079 isAnimating = convListFragment.isAnimating(); 3080 } 3081 return isAnimating; 3082 } 3083 3084 /** 3085 * Called when the {@link ConversationCursor} is changed or has new data in it. 3086 * <p> 3087 * {@inheritDoc} 3088 */ 3089 @Override 3090 public final void onRefreshReady() { 3091 LogUtils.d(LOG_TAG, "Received refresh ready callback for folder %s", 3092 mFolder != null ? mFolder.id : "-1"); 3093 3094 if (mDestroyed) { 3095 LogUtils.i(LOG_TAG, "ignoring onRefreshReady on destroyed AAC"); 3096 return; 3097 } 3098 3099 if (!isAnimating()) { 3100 // Swap cursors 3101 mConversationListCursor.sync(); 3102 } 3103 mTracker.onCursorUpdated(); 3104 perhapsShowFirstSearchResult(); 3105 } 3106 3107 @Override 3108 public final void onDataSetChanged() { 3109 updateConversationListFragment(); 3110 mConversationListObservable.notifyChanged(); 3111 mSelectedSet.validateAgainstCursor(mConversationListCursor); 3112 } 3113 3114 /** 3115 * If the Conversation List Fragment is visible, updates the fragment. 3116 */ 3117 private void updateConversationListFragment() { 3118 final ConversationListFragment convList = getConversationListFragment(); 3119 if (convList != null) { 3120 refreshConversationList(); 3121 if (isFragmentVisible(convList)) { 3122 informCursorVisiblity(true); 3123 } 3124 } 3125 } 3126 3127 /** 3128 * This class handles throttled refresh of the conversation list 3129 */ 3130 static class RefreshTimerTask extends TimerTask { 3131 final Handler mHandler; 3132 final AbstractActivityController mController; 3133 3134 RefreshTimerTask(AbstractActivityController controller, Handler handler) { 3135 mHandler = handler; 3136 mController = controller; 3137 } 3138 3139 @Override 3140 public void run() { 3141 mHandler.post(new Runnable() { 3142 @Override 3143 public void run() { 3144 LogUtils.d(LOG_TAG, "Delay done... calling onRefreshRequired"); 3145 mController.onRefreshRequired(); 3146 }}); 3147 } 3148 } 3149 3150 /** 3151 * Cancel the refresh task, if it's running 3152 */ 3153 private void cancelRefreshTask () { 3154 if (mConversationListRefreshTask != null) { 3155 mConversationListRefreshTask.cancel(); 3156 mConversationListRefreshTask = null; 3157 } 3158 } 3159 3160 @Override 3161 public void onAnimationEnd(AnimatedAdapter animatedAdapter) { 3162 if (mConversationListCursor == null) { 3163 LogUtils.e(LOG_TAG, "null ConversationCursor in onAnimationEnd"); 3164 return; 3165 } 3166 if (mConversationListCursor.isRefreshReady()) { 3167 LogUtils.i(ConversationCursor.LOG_TAG, "Stopped animating: try sync"); 3168 onRefreshReady(); 3169 } 3170 3171 if (mConversationListCursor.isRefreshRequired()) { 3172 LogUtils.i(ConversationCursor.LOG_TAG, "Stopped animating: refresh"); 3173 mConversationListCursor.refresh(); 3174 } 3175 if (mRecentsDataUpdated) { 3176 mRecentsDataUpdated = false; 3177 mRecentFolderObservers.notifyChanged(); 3178 } 3179 } 3180 3181 @Override 3182 public void onSetEmpty() { 3183 // There are no selected conversations. Ensure that the listener and its associated actions 3184 // are blanked out. 3185 setListener(null, -1); 3186 } 3187 3188 @Override 3189 public void onSetPopulated(ConversationSelectionSet set) { 3190 mCabActionMenu = new SelectedConversationsActionMenu(mActivity, set, mFolder); 3191 if (mViewMode.isListMode() || (mIsTablet && mViewMode.isConversationMode())) { 3192 enableCabMode(); 3193 } 3194 } 3195 3196 @Override 3197 public void onSetChanged(ConversationSelectionSet set) { 3198 // Do nothing. We don't care about changes to the set. 3199 } 3200 3201 @Override 3202 public ConversationSelectionSet getSelectedSet() { 3203 return mSelectedSet; 3204 } 3205 3206 /** 3207 * Disable the Contextual Action Bar (CAB). The selected set is not changed. 3208 */ 3209 protected void disableCabMode() { 3210 // Commit any previous destructive actions when entering/ exiting CAB mode. 3211 commitDestructiveActions(true); 3212 if (mCabActionMenu != null) { 3213 mCabActionMenu.deactivate(); 3214 } 3215 } 3216 3217 /** 3218 * Re-enable the CAB menu if required. The selection set is not changed. 3219 */ 3220 protected void enableCabMode() { 3221 if (mCabActionMenu != null && 3222 !(isDrawerEnabled() && mDrawerContainer.isDrawerOpen(mDrawerPullout))) { 3223 mCabActionMenu.activate(); 3224 } 3225 } 3226 3227 /** 3228 * Re-enable CAB mode only if we have an active selection 3229 */ 3230 protected void maybeEnableCabMode() { 3231 if (!mSelectedSet.isEmpty()) { 3232 if (mCabActionMenu != null) { 3233 mCabActionMenu.activate(); 3234 } 3235 } 3236 } 3237 3238 /** 3239 * Unselect conversations and exit CAB mode. 3240 */ 3241 protected final void exitCabMode() { 3242 mSelectedSet.clear(); 3243 } 3244 3245 @Override 3246 public void startSearch() { 3247 if (mAccount == null) { 3248 // We cannot search if there is no account. Drop the request to the floor. 3249 LogUtils.d(LOG_TAG, "AbstractActivityController.startSearch(): null account"); 3250 return; 3251 } 3252 if (mAccount.supportsSearch()) { 3253 mActionBarView.expandSearch(); 3254 } else { 3255 Toast.makeText(mActivity.getActivityContext(), mActivity.getActivityContext() 3256 .getString(R.string.search_unsupported), Toast.LENGTH_SHORT).show(); 3257 } 3258 } 3259 3260 @Override 3261 public void exitSearchMode() { 3262 if (mViewMode.getMode() == ViewMode.SEARCH_RESULTS_LIST) { 3263 mActivity.finish(); 3264 } 3265 } 3266 3267 /** 3268 * Supports dragging conversations to a folder. 3269 */ 3270 @Override 3271 public boolean supportsDrag(DragEvent event, Folder folder) { 3272 return (folder != null 3273 && event != null 3274 && event.getClipDescription() != null 3275 && folder.supportsCapability 3276 (UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES) 3277 && !mFolder.equals(folder)); 3278 } 3279 3280 /** 3281 * Handles dropping conversations to a folder. 3282 */ 3283 @Override 3284 public void handleDrop(DragEvent event, final Folder folder) { 3285 if (!supportsDrag(event, folder)) { 3286 return; 3287 } 3288 if (folder.isType(UIProvider.FolderType.STARRED)) { 3289 // Moving a conversation to the starred folder adds the star and 3290 // removes the current label 3291 handleDropInStarred(folder); 3292 return; 3293 } 3294 if (mFolder.isType(UIProvider.FolderType.STARRED)) { 3295 handleDragFromStarred(folder); 3296 return; 3297 } 3298 final ArrayList<FolderOperation> dragDropOperations = new ArrayList<FolderOperation>(); 3299 final Collection<Conversation> conversations = mSelectedSet.values(); 3300 // Add the drop target folder. 3301 dragDropOperations.add(new FolderOperation(folder, true)); 3302 // Remove the current folder unless the user is viewing "all". 3303 // That operation should just add the new folder. 3304 boolean isDestructive = !mFolder.isViewAll() 3305 && mFolder.supportsCapability 3306 (UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES); 3307 if (isDestructive) { 3308 dragDropOperations.add(new FolderOperation(mFolder, false)); 3309 } 3310 // Drag and drop is destructive: we remove conversations from the 3311 // current folder. 3312 final DestructiveAction action = 3313 getFolderChange(conversations, dragDropOperations, isDestructive, 3314 true /* isBatch */, true /* showUndo */, true /* isMoveTo */, folder, 3315 null /* undoCallback */); 3316 if (isDestructive) { 3317 delete(0, conversations, action, true); 3318 } else { 3319 action.performAction(); 3320 } 3321 } 3322 3323 private void handleDragFromStarred(Folder folder) { 3324 final Collection<Conversation> conversations = mSelectedSet.values(); 3325 // The conversation list deletes and performs the action if it exists. 3326 final ConversationListFragment convListFragment = getConversationListFragment(); 3327 // There should always be a convlistfragment, or the user could not have 3328 // dragged/ dropped conversations. 3329 if (convListFragment != null) { 3330 LogUtils.d(LOG_TAG, "AAC.requestDelete: ListFragment is handling delete."); 3331 ArrayList<ConversationOperation> ops = new ArrayList<ConversationOperation>(); 3332 ArrayList<Uri> folderUris; 3333 ArrayList<Boolean> adds; 3334 for (Conversation target : conversations) { 3335 folderUris = new ArrayList<Uri>(); 3336 adds = new ArrayList<Boolean>(); 3337 folderUris.add(folder.folderUri.fullUri); 3338 adds.add(Boolean.TRUE); 3339 final HashMap<Uri, Folder> targetFolders = 3340 Folder.hashMapForFolders(target.getRawFolders()); 3341 targetFolders.put(folder.folderUri.fullUri, folder); 3342 ops.add(mConversationListCursor.getConversationFolderOperation(target, 3343 folderUris, adds, targetFolders.values())); 3344 } 3345 if (mConversationListCursor != null) { 3346 mConversationListCursor.updateBulkValues(ops); 3347 } 3348 refreshConversationList(); 3349 mSelectedSet.clear(); 3350 } 3351 } 3352 3353 private void handleDropInStarred(Folder folder) { 3354 final Collection<Conversation> conversations = mSelectedSet.values(); 3355 // The conversation list deletes and performs the action if it exists. 3356 final ConversationListFragment convListFragment = getConversationListFragment(); 3357 // There should always be a convlistfragment, or the user could not have 3358 // dragged/ dropped conversations. 3359 if (convListFragment != null) { 3360 LogUtils.d(LOG_TAG, "AAC.requestDelete: ListFragment is handling delete."); 3361 convListFragment.requestDelete(R.id.change_folders, conversations, 3362 new DroppedInStarredAction(conversations, mFolder, folder)); 3363 } 3364 } 3365 3366 // When dragging conversations to the starred folder, remove from the 3367 // original folder and add a star 3368 private class DroppedInStarredAction implements DestructiveAction { 3369 private final Collection<Conversation> mConversations; 3370 private final Folder mInitialFolder; 3371 private final Folder mStarred; 3372 3373 public DroppedInStarredAction(Collection<Conversation> conversations, Folder initialFolder, 3374 Folder starredFolder) { 3375 mConversations = conversations; 3376 mInitialFolder = initialFolder; 3377 mStarred = starredFolder; 3378 } 3379 3380 @Override 3381 public void setUndoCallback(UndoCallback undoCallback) { 3382 return; // currently not applicable 3383 } 3384 3385 @Override 3386 public void performAction() { 3387 ToastBarOperation undoOp = new ToastBarOperation(mConversations.size(), 3388 R.id.change_folders, ToastBarOperation.UNDO, true /* batch */, mInitialFolder); 3389 onUndoAvailable(undoOp); 3390 ArrayList<ConversationOperation> ops = new ArrayList<ConversationOperation>(); 3391 ContentValues values = new ContentValues(); 3392 ArrayList<Uri> folderUris; 3393 ArrayList<Boolean> adds; 3394 ConversationOperation operation; 3395 for (Conversation target : mConversations) { 3396 folderUris = new ArrayList<Uri>(); 3397 adds = new ArrayList<Boolean>(); 3398 folderUris.add(mStarred.folderUri.fullUri); 3399 adds.add(Boolean.TRUE); 3400 folderUris.add(mInitialFolder.folderUri.fullUri); 3401 adds.add(Boolean.FALSE); 3402 final HashMap<Uri, Folder> targetFolders = 3403 Folder.hashMapForFolders(target.getRawFolders()); 3404 targetFolders.put(mStarred.folderUri.fullUri, mStarred); 3405 targetFolders.remove(mInitialFolder.folderUri.fullUri); 3406 values.put(ConversationColumns.STARRED, true); 3407 operation = mConversationListCursor.getConversationFolderOperation(target, 3408 folderUris, adds, targetFolders.values(), values); 3409 ops.add(operation); 3410 } 3411 if (mConversationListCursor != null) { 3412 mConversationListCursor.updateBulkValues(ops); 3413 } 3414 refreshConversationList(); 3415 mSelectedSet.clear(); 3416 } 3417 } 3418 3419 @Override 3420 public void onTouchEvent(MotionEvent event) { 3421 if (event.getAction() == MotionEvent.ACTION_DOWN) { 3422 if (mToastBar != null && !mToastBar.isEventInToastBar(event)) { 3423 hideOrRepositionToastBar(true); 3424 } 3425 } 3426 } 3427 3428 protected abstract void hideOrRepositionToastBar(boolean animated); 3429 3430 @Override 3431 public void onConversationSeen() { 3432 mPagerController.onConversationSeen(); 3433 } 3434 3435 @Override 3436 public boolean isInitialConversationLoading() { 3437 return mPagerController.isInitialConversationLoading(); 3438 } 3439 3440 /** 3441 * Check if the fragment given here is visible. Checking {@link Fragment#isVisible()} is 3442 * insufficient because that doesn't check if the window is currently in focus or not. 3443 */ 3444 private boolean isFragmentVisible(Fragment in) { 3445 return in != null && in.isVisible() && mActivity.hasWindowFocus(); 3446 } 3447 3448 /** 3449 * This class handles callbacks that create a {@link ConversationCursor}. 3450 */ 3451 private class ConversationListLoaderCallbacks implements 3452 LoaderManager.LoaderCallbacks<ConversationCursor> { 3453 3454 @Override 3455 public Loader<ConversationCursor> onCreateLoader(int id, Bundle args) { 3456 final Account account = args.getParcelable(BUNDLE_ACCOUNT_KEY); 3457 final Folder folder = args.getParcelable(BUNDLE_FOLDER_KEY); 3458 final boolean ignoreInitialConversationLimit = 3459 args.getBoolean(BUNDLE_IGNORE_INITIAL_CONVERSATION_LIMIT_KEY, false); 3460 if (account == null || folder == null) { 3461 return null; 3462 } 3463 return new ConversationCursorLoader(mActivity, account, 3464 folder.conversationListUri, folder.name, ignoreInitialConversationLimit); 3465 } 3466 3467 @Override 3468 public void onLoadFinished(Loader<ConversationCursor> loader, ConversationCursor data) { 3469 LogUtils.d(LOG_TAG, 3470 "IN AAC.ConversationCursor.onLoadFinished, data=%s loader=%s this=%s", 3471 data, loader, this); 3472 if (isDrawerEnabled() && mDrawerListener.getDrawerState() != DrawerLayout.STATE_IDLE) { 3473 LogUtils.d(LOG_TAG, "ConversationListLoaderCallbacks.onLoadFinished: ignoring."); 3474 mConversationListLoadFinishedIgnored = true; 3475 return; 3476 } 3477 // Clear our all pending destructive actions before swapping the conversation cursor 3478 destroyPending(null); 3479 mConversationListCursor = data; 3480 mConversationListCursor.addListener(AbstractActivityController.this); 3481 mDrawIdler.setListener(mConversationListCursor); 3482 mTracker.onCursorUpdated(); 3483 mConversationListObservable.notifyChanged(); 3484 // Handle actions that were deferred until after the conversation list was loaded. 3485 for (LoadFinishedCallback callback : mConversationListLoadFinishedCallbacks) { 3486 callback.onLoadFinished(); 3487 } 3488 mConversationListLoadFinishedCallbacks.clear(); 3489 3490 final ConversationListFragment convList = getConversationListFragment(); 3491 if (isFragmentVisible(convList)) { 3492 // The conversation list is already listening to list changes and gets notified 3493 // in the mConversationListObservable.notifyChanged() line above. We only need to 3494 // check and inform the cursor of the change in visibility here. 3495 informCursorVisiblity(true); 3496 } 3497 perhapsShowFirstSearchResult(); 3498 } 3499 3500 @Override 3501 public void onLoaderReset(Loader<ConversationCursor> loader) { 3502 LogUtils.d(LOG_TAG, 3503 "IN AAC.ConversationCursor.onLoaderReset, data=%s loader=%s this=%s", 3504 mConversationListCursor, loader, this); 3505 3506 if (mConversationListCursor != null) { 3507 // Unregister the listener 3508 mConversationListCursor.removeListener(AbstractActivityController.this); 3509 mDrawIdler.setListener(null); 3510 mConversationListCursor = null; 3511 3512 // Inform anyone who is interested about the change 3513 mTracker.onCursorUpdated(); 3514 mConversationListObservable.notifyChanged(); 3515 } 3516 } 3517 } 3518 3519 /** 3520 * Class to perform {@link LoaderManager.LoaderCallbacks} for creating {@link Folder} objects. 3521 */ 3522 private class FolderLoads implements LoaderManager.LoaderCallbacks<ObjectCursor<Folder>> { 3523 @Override 3524 public Loader<ObjectCursor<Folder>> onCreateLoader(int id, Bundle args) { 3525 final String[] everything = UIProvider.FOLDERS_PROJECTION; 3526 switch (id) { 3527 case LOADER_FOLDER_CURSOR: 3528 LogUtils.d(LOG_TAG, "LOADER_FOLDER_CURSOR created"); 3529 final ObjectCursorLoader<Folder> loader = new 3530 ObjectCursorLoader<Folder>( 3531 mContext, mFolder.folderUri.fullUri, everything, Folder.FACTORY); 3532 loader.setUpdateThrottle(mFolderItemUpdateDelayMs); 3533 return loader; 3534 case LOADER_RECENT_FOLDERS: 3535 LogUtils.d(LOG_TAG, "LOADER_RECENT_FOLDERS created"); 3536 if (mAccount != null && mAccount.recentFolderListUri != null 3537 && !mAccount.recentFolderListUri.equals(Uri.EMPTY)) { 3538 return new ObjectCursorLoader<Folder>(mContext, 3539 mAccount.recentFolderListUri, everything, Folder.FACTORY); 3540 } 3541 break; 3542 case LOADER_ACCOUNT_INBOX: 3543 LogUtils.d(LOG_TAG, "LOADER_ACCOUNT_INBOX created"); 3544 final Uri defaultInbox = Settings.getDefaultInboxUri(mAccount.settings); 3545 final Uri inboxUri = defaultInbox.equals(Uri.EMPTY) ? 3546 mAccount.folderListUri : defaultInbox; 3547 LogUtils.d(LOG_TAG, "Loading the default inbox: %s", inboxUri); 3548 if (inboxUri != null) { 3549 return new ObjectCursorLoader<Folder>(mContext, inboxUri, 3550 everything, Folder.FACTORY); 3551 } 3552 break; 3553 case LOADER_SEARCH: 3554 LogUtils.d(LOG_TAG, "LOADER_SEARCH created"); 3555 return Folder.forSearchResults(mAccount, 3556 args.getString(ConversationListContext.EXTRA_SEARCH_QUERY), 3557 mActivity.getActivityContext()); 3558 case LOADER_FIRST_FOLDER: 3559 LogUtils.d(LOG_TAG, "LOADER_FIRST_FOLDER created"); 3560 final Uri folderUri = args.getParcelable(Utils.EXTRA_FOLDER_URI); 3561 mConversationToShow = args.getParcelable(Utils.EXTRA_CONVERSATION); 3562 if (mConversationToShow != null && mConversationToShow.position < 0){ 3563 mConversationToShow.position = 0; 3564 } 3565 return new ObjectCursorLoader<Folder>(mContext, folderUri, 3566 everything, Folder.FACTORY); 3567 default: 3568 LogUtils.wtf(LOG_TAG, "FolderLoads.onCreateLoader(%d) for invalid id", id); 3569 return null; 3570 } 3571 return null; 3572 } 3573 3574 @Override 3575 public void onLoadFinished(Loader<ObjectCursor<Folder>> loader, ObjectCursor<Folder> data) { 3576 if (data == null) { 3577 LogUtils.e(LOG_TAG, "Received null cursor from loader id: %d", loader.getId()); 3578 } 3579 switch (loader.getId()) { 3580 case LOADER_FOLDER_CURSOR: 3581 if (data != null && data.moveToFirst()) { 3582 final Folder folder = data.getModel(); 3583 setHasFolderChanged(folder); 3584 mFolder = folder; 3585 mFolderObservable.notifyChanged(); 3586 } else { 3587 LogUtils.d(LOG_TAG, "Unable to get the folder %s", 3588 mFolder != null ? mFolder.name : ""); 3589 } 3590 break; 3591 case LOADER_RECENT_FOLDERS: 3592 // Few recent folders and we are running on a phone? Populate the default 3593 // recents. The number of default recent folders is at least 2: every provider 3594 // has at least two folders, and the recent folder count never decreases. 3595 // Having a single recent folder is an erroneous case, and we can gracefully 3596 // recover by populating default recents. The default recents will not stomp on 3597 // the existing value: it will be shown in addition to the default folders: 3598 // the max number of recent folders is more than 1+num(defaultRecents). 3599 if (data != null && data.getCount() <= 1 && !mIsTablet) { 3600 final class PopulateDefault extends AsyncTask<Uri, Void, Void> { 3601 @Override 3602 protected Void doInBackground(Uri... uri) { 3603 // Asking for an update on the URI and ignore the result. 3604 final ContentResolver resolver = mContext.getContentResolver(); 3605 resolver.update(uri[0], null, null, null); 3606 return null; 3607 } 3608 } 3609 final Uri uri = mAccount.defaultRecentFolderListUri; 3610 LogUtils.v(LOG_TAG, "Default recents at %s", uri); 3611 new PopulateDefault().execute(uri); 3612 break; 3613 } 3614 LogUtils.v(LOG_TAG, "Reading recent folders from the cursor."); 3615 mRecentFolderList.loadFromUiProvider(data); 3616 if (isAnimating()) { 3617 mRecentsDataUpdated = true; 3618 } else { 3619 mRecentFolderObservers.notifyChanged(); 3620 } 3621 break; 3622 case LOADER_ACCOUNT_INBOX: 3623 if (data != null && !data.isClosed() && data.moveToFirst()) { 3624 final Folder inbox = data.getModel(); 3625 onFolderChanged(inbox, false /* force */); 3626 // Just want to get the inbox, don't care about updates to it 3627 // as this will be tracked by the folder change listener. 3628 mActivity.getLoaderManager().destroyLoader(LOADER_ACCOUNT_INBOX); 3629 } else { 3630 LogUtils.d(LOG_TAG, "Unable to get the account inbox for account %s", 3631 mAccount != null ? mAccount.getEmailAddress() : ""); 3632 } 3633 break; 3634 case LOADER_SEARCH: 3635 if (data != null && data.getCount() > 0) { 3636 data.moveToFirst(); 3637 final Folder search = data.getModel(); 3638 updateFolder(search); 3639 mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder, 3640 mActivity.getIntent() 3641 .getStringExtra(UIProvider.SearchQueryParameters.QUERY)); 3642 showConversationList(mConvListContext); 3643 mActivity.invalidateOptionsMenu(); 3644 mHaveSearchResults = search.totalCount > 0; 3645 mActivity.getLoaderManager().destroyLoader(LOADER_SEARCH); 3646 } else { 3647 LogUtils.e(LOG_TAG, "Null/empty cursor returned by LOADER_SEARCH loader"); 3648 } 3649 break; 3650 case LOADER_FIRST_FOLDER: 3651 if (data == null || data.getCount() <=0 || !data.moveToFirst()) { 3652 return; 3653 } 3654 final Folder folder = data.getModel(); 3655 boolean handled = false; 3656 if (folder != null) { 3657 onFolderChanged(folder, false /* force */); 3658 handled = true; 3659 } 3660 if (mConversationToShow != null) { 3661 // Open the conversation. 3662 showConversation(mConversationToShow); 3663 handled = true; 3664 } 3665 if (!handled) { 3666 // We have an account, but nothing else: load the default inbox. 3667 loadAccountInbox(); 3668 } 3669 mConversationToShow = null; 3670 // And don't run this anymore. 3671 mActivity.getLoaderManager().destroyLoader(LOADER_FIRST_FOLDER); 3672 break; 3673 } 3674 } 3675 3676 @Override 3677 public void onLoaderReset(Loader<ObjectCursor<Folder>> loader) { 3678 } 3679 } 3680 3681 /** 3682 * Class to perform {@link LoaderManager.LoaderCallbacks} for creating {@link Account} objects. 3683 */ 3684 private class AccountLoads implements LoaderManager.LoaderCallbacks<ObjectCursor<Account>> { 3685 final String[] mProjection = UIProvider.ACCOUNTS_PROJECTION; 3686 final CursorCreator<Account> mFactory = Account.FACTORY; 3687 3688 @Override 3689 public Loader<ObjectCursor<Account>> onCreateLoader(int id, Bundle args) { 3690 switch (id) { 3691 case LOADER_ACCOUNT_CURSOR: 3692 LogUtils.d(LOG_TAG, "LOADER_ACCOUNT_CURSOR created"); 3693 return new ObjectCursorLoader<Account>(mContext, 3694 MailAppProvider.getAccountsUri(), mProjection, mFactory); 3695 case LOADER_ACCOUNT_UPDATE_CURSOR: 3696 LogUtils.d(LOG_TAG, "LOADER_ACCOUNT_UPDATE_CURSOR created"); 3697 return new ObjectCursorLoader<Account>(mContext, mAccount.uri, mProjection, 3698 mFactory); 3699 default: 3700 LogUtils.wtf(LOG_TAG, "Got an id (%d) that I cannot create!", id); 3701 break; 3702 } 3703 return null; 3704 } 3705 3706 @Override 3707 public void onLoadFinished(Loader<ObjectCursor<Account>> loader, 3708 ObjectCursor<Account> data) { 3709 if (data == null) { 3710 LogUtils.e(LOG_TAG, "Received null cursor from loader id: %d", loader.getId()); 3711 } 3712 switch (loader.getId()) { 3713 case LOADER_ACCOUNT_CURSOR: 3714 // We have received an update on the list of accounts. 3715 if (data == null) { 3716 // Nothing useful to do if we have no valid data. 3717 break; 3718 } 3719 final long count = data.getCount(); 3720 if (count == 0) { 3721 // If an empty cursor is returned, the MailAppProvider is indicating that 3722 // no accounts have been specified. We want to navigate to the 3723 // "add account" activity that will handle the intent returned by the 3724 // MailAppProvider 3725 3726 // If the MailAppProvider believes that all accounts have been loaded, 3727 // and the account list is still empty, we want to prompt the user to add 3728 // an account. 3729 final Bundle extras = data.getExtras(); 3730 final boolean accountsLoaded = 3731 extras.getInt(AccountCursorExtraKeys.ACCOUNTS_LOADED) != 0; 3732 3733 if (accountsLoaded) { 3734 final Intent noAccountIntent = MailAppProvider.getNoAccountIntent 3735 (mContext); 3736 if (noAccountIntent != null) { 3737 mActivity.startActivityForResult(noAccountIntent, 3738 ADD_ACCOUNT_REQUEST_CODE); 3739 } 3740 } 3741 } else { 3742 final boolean accountListUpdated = accountsUpdated(data); 3743 if (!mHaveAccountList || accountListUpdated) { 3744 mHaveAccountList = updateAccounts(data); 3745 } 3746 Analytics.getInstance().setCustomDimension(Analytics.CD_INDEX_ACCOUNT_COUNT, 3747 Long.toString(count)); 3748 } 3749 break; 3750 case LOADER_ACCOUNT_UPDATE_CURSOR: 3751 // We have received an update for current account. 3752 if (data != null && data.moveToFirst()) { 3753 final Account updatedAccount = data.getModel(); 3754 // Make sure that this is an update for the current account 3755 if (updatedAccount.uri.equals(mAccount.uri)) { 3756 final Settings previousSettings = mAccount.settings; 3757 3758 // Update the controller's reference to the current account 3759 mAccount = updatedAccount; 3760 LogUtils.d(LOG_TAG, "AbstractActivityController.onLoadFinished(): " 3761 + "mAccount = %s", mAccount.uri); 3762 3763 // Only notify about a settings change if something differs 3764 if (!Objects.equal(mAccount.settings, previousSettings)) { 3765 mAccountObservers.notifyChanged(); 3766 } 3767 perhapsEnterWaitMode(); 3768 } else { 3769 LogUtils.e(LOG_TAG, "Got update for account: %s with current account:" 3770 + " %s", updatedAccount.uri, mAccount.uri); 3771 // We need to restart the loader, so the correct account information 3772 // will be returned. 3773 restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR, this, Bundle.EMPTY); 3774 } 3775 } 3776 break; 3777 } 3778 } 3779 3780 @Override 3781 public void onLoaderReset(Loader<ObjectCursor<Account>> loader) { 3782 // Do nothing. In onLoadFinished() we copy the relevant data from the cursor. 3783 } 3784 } 3785 3786 /** 3787 * Updates controller state based on search results and shows first conversation if required. 3788 */ 3789 private void perhapsShowFirstSearchResult() { 3790 if (mCurrentConversation == null) { 3791 // Shown for search results in two-pane mode only. 3792 mHaveSearchResults = Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction()) 3793 && mConversationListCursor.getCount() > 0; 3794 if (!shouldShowFirstConversation()) { 3795 return; 3796 } 3797 mConversationListCursor.moveToPosition(0); 3798 final Conversation conv = new Conversation(mConversationListCursor); 3799 conv.position = 0; 3800 onConversationSelected(conv, true /* checkSafeToModifyFragments */); 3801 } 3802 } 3803 3804 /** 3805 * Destroy the pending {@link DestructiveAction} till now and assign the given action as the 3806 * next destructive action.. 3807 * @param nextAction the next destructive action to be performed. This can be null. 3808 */ 3809 private void destroyPending(DestructiveAction nextAction) { 3810 // If there is a pending action, perform that first. 3811 if (mPendingDestruction != null) { 3812 mPendingDestruction.performAction(); 3813 } 3814 mPendingDestruction = nextAction; 3815 } 3816 3817 /** 3818 * Register a destructive action with the controller. This performs the previous destructive 3819 * action as a side effect. This method is final because we don't want the child classes to 3820 * embellish this method any more. 3821 * @param action the action to register. 3822 */ 3823 private void registerDestructiveAction(DestructiveAction action) { 3824 // TODO(viki): This is not a good idea. The best solution is for clients to request a 3825 // destructive action from the controller and for the controller to own the action. This is 3826 // a half-way solution while refactoring DestructiveAction. 3827 destroyPending(action); 3828 } 3829 3830 @Override 3831 public final DestructiveAction getBatchAction(int action, UndoCallback undoCallback) { 3832 final DestructiveAction da = new ConversationAction(action, mSelectedSet.values(), true); 3833 da.setUndoCallback(undoCallback); 3834 registerDestructiveAction(da); 3835 return da; 3836 } 3837 3838 @Override 3839 public final DestructiveAction getDeferredBatchAction(int action, UndoCallback undoCallback) { 3840 return getDeferredAction(action, mSelectedSet.values(), true, undoCallback); 3841 } 3842 3843 /** 3844 * Get a destructive action for a menu action. This is a temporary method, 3845 * to control the profusion of {@link DestructiveAction} classes that are 3846 * created. Please do not copy this paradigm. 3847 * @param action the resource ID of the menu action: R.id.delete, for 3848 * example 3849 * @param target the conversations to act upon. 3850 * @return a {@link DestructiveAction} that performs the specified action. 3851 */ 3852 private DestructiveAction getDeferredAction(int action, Collection<Conversation> target, 3853 boolean batch, UndoCallback callback) { 3854 ConversationAction cAction = new ConversationAction(action, target, batch); 3855 cAction.setUndoCallback(callback); 3856 return cAction; 3857 } 3858 3859 /** 3860 * Class to change the folders that are assigned to a set of conversations. This is destructive 3861 * because the user can remove the current folder from the conversation, in which case it has 3862 * to be animated away from the current folder. 3863 */ 3864 private class FolderDestruction implements DestructiveAction { 3865 private final Collection<Conversation> mTarget; 3866 private final ArrayList<FolderOperation> mFolderOps = new ArrayList<FolderOperation>(); 3867 private final boolean mIsDestructive; 3868 /** Whether this destructive action has already been performed */ 3869 private boolean mCompleted; 3870 private final boolean mIsSelectedSet; 3871 private final boolean mShowUndo; 3872 private final int mAction; 3873 private final Folder mActionFolder; 3874 3875 private UndoCallback mUndoCallback; 3876 3877 /** 3878 * Create a new folder destruction object to act on the given conversations. 3879 * @param target conversations to act upon. 3880 * @param actionFolder the {@link Folder} being acted upon, used for displaying the undo bar 3881 */ 3882 private FolderDestruction(final Collection<Conversation> target, 3883 final Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch, 3884 boolean showUndo, int action, final Folder actionFolder) { 3885 mTarget = ImmutableList.copyOf(target); 3886 mFolderOps.addAll(folders); 3887 mIsDestructive = isDestructive; 3888 mIsSelectedSet = isBatch; 3889 mShowUndo = showUndo; 3890 mAction = action; 3891 mActionFolder = actionFolder; 3892 } 3893 3894 @Override 3895 public void setUndoCallback(UndoCallback undoCallback) { 3896 mUndoCallback = undoCallback; 3897 } 3898 3899 @Override 3900 public void performAction() { 3901 if (isPerformed()) { 3902 return; 3903 } 3904 if (mIsDestructive && mShowUndo) { 3905 ToastBarOperation undoOp = new ToastBarOperation(mTarget.size(), mAction, 3906 ToastBarOperation.UNDO, mIsSelectedSet, mActionFolder); 3907 onUndoAvailable(undoOp); 3908 } 3909 // For each conversation, for each operation, add/ remove the 3910 // appropriate folders. 3911 ArrayList<ConversationOperation> ops = new ArrayList<ConversationOperation>(); 3912 ArrayList<Uri> folderUris; 3913 ArrayList<Boolean> adds; 3914 for (Conversation target : mTarget) { 3915 HashMap<Uri, Folder> targetFolders = Folder.hashMapForFolders(target 3916 .getRawFolders()); 3917 folderUris = new ArrayList<Uri>(); 3918 adds = new ArrayList<Boolean>(); 3919 if (mIsDestructive) { 3920 target.localDeleteOnUpdate = true; 3921 } 3922 for (FolderOperation op : mFolderOps) { 3923 folderUris.add(op.mFolder.folderUri.fullUri); 3924 adds.add(op.mAdd ? Boolean.TRUE : Boolean.FALSE); 3925 if (op.mAdd) { 3926 targetFolders.put(op.mFolder.folderUri.fullUri, op.mFolder); 3927 } else { 3928 targetFolders.remove(op.mFolder.folderUri.fullUri); 3929 } 3930 } 3931 ops.add(mConversationListCursor.getConversationFolderOperation(target, 3932 folderUris, adds, targetFolders.values(), mUndoCallback)); 3933 } 3934 if (mConversationListCursor != null) { 3935 mConversationListCursor.updateBulkValues(ops); 3936 } 3937 refreshConversationList(); 3938 if (mIsSelectedSet) { 3939 mSelectedSet.clear(); 3940 } 3941 } 3942 3943 /** 3944 * Returns true if this action has been performed, false otherwise. 3945 * 3946 */ 3947 private synchronized boolean isPerformed() { 3948 if (mCompleted) { 3949 return true; 3950 } 3951 mCompleted = true; 3952 return false; 3953 } 3954 } 3955 3956 public final DestructiveAction getFolderChange(Collection<Conversation> target, 3957 Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch, 3958 boolean showUndo, final boolean isMoveTo, final Folder actionFolder, 3959 UndoCallback undoCallback) { 3960 final DestructiveAction da = getDeferredFolderChange(target, folders, isDestructive, 3961 isBatch, showUndo, isMoveTo, actionFolder, undoCallback); 3962 registerDestructiveAction(da); 3963 return da; 3964 } 3965 3966 public final DestructiveAction getDeferredFolderChange(Collection<Conversation> target, 3967 Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch, 3968 boolean showUndo, final boolean isMoveTo, final Folder actionFolder, 3969 UndoCallback undoCallback) { 3970 final DestructiveAction fd = new FolderDestruction(target, folders, isDestructive, isBatch, 3971 showUndo, isMoveTo ? R.id.move_folder : R.id.change_folders, actionFolder); 3972 fd.setUndoCallback(undoCallback); 3973 return fd; 3974 } 3975 3976 @Override 3977 public final DestructiveAction getDeferredRemoveFolder(Collection<Conversation> target, 3978 Folder toRemove, boolean isDestructive, boolean isBatch, 3979 boolean showUndo, UndoCallback undoCallback) { 3980 Collection<FolderOperation> folderOps = new ArrayList<FolderOperation>(); 3981 folderOps.add(new FolderOperation(toRemove, false)); 3982 final DestructiveAction da = new FolderDestruction(target, folderOps, isDestructive, isBatch, 3983 showUndo, R.id.remove_folder, mFolder); 3984 da.setUndoCallback(undoCallback); 3985 return da; 3986 } 3987 3988 @Override 3989 public final void refreshConversationList() { 3990 final ConversationListFragment convList = getConversationListFragment(); 3991 if (convList == null) { 3992 return; 3993 } 3994 convList.requestListRefresh(); 3995 } 3996 3997 protected final ActionClickedListener getUndoClickedListener( 3998 final AnimatedAdapter listAdapter) { 3999 return new ActionClickedListener() { 4000 @Override 4001 public void onActionClicked(Context context) { 4002 if (mAccount.undoUri != null) { 4003 // NOTE: We might want undo to return the messages affected, in which case 4004 // the resulting cursor might be interesting... 4005 // TODO: Use UIProvider.SEQUENCE_QUERY_PARAMETER to indicate the set of 4006 // commands to undo 4007 if (mConversationListCursor != null) { 4008 mConversationListCursor.undo( 4009 mActivity.getActivityContext(), mAccount.undoUri); 4010 } 4011 if (listAdapter != null) { 4012 listAdapter.setUndo(true); 4013 } 4014 } 4015 } 4016 }; 4017 } 4018 4019 /** 4020 * Shows an error toast in the bottom when a folder was not fetched successfully. 4021 * @param folder the folder which could not be fetched. 4022 * @param replaceVisibleToast if true, this should replace any currently visible toast. 4023 */ 4024 protected final void showErrorToast(final Folder folder, boolean replaceVisibleToast) { 4025 4026 final ActionClickedListener listener; 4027 final int actionTextResourceId; 4028 final int lastSyncResult = folder.lastSyncResult; 4029 switch (lastSyncResult & 0x0f) { 4030 case UIProvider.LastSyncResult.CONNECTION_ERROR: 4031 // The sync request that caused this failure. 4032 final int syncRequest = lastSyncResult >> 4; 4033 // Show: User explicitly pressed the refresh button and there is no connection 4034 // Show: The first time the user enters the app and there is no connection 4035 // TODO(viki): Implement this. 4036 // Reference: http://b/7202801 4037 final boolean showToast = (syncRequest & UIProvider.SyncStatus.USER_REFRESH) != 0; 4038 // Don't show: Already in the app; user switches to a synced label 4039 // Don't show: In a live label and a background sync fails 4040 final boolean avoidToast = !showToast && (folder.syncWindow > 0 4041 || (syncRequest & UIProvider.SyncStatus.BACKGROUND_SYNC) != 0); 4042 if (avoidToast) { 4043 return; 4044 } 4045 listener = getRetryClickedListener(folder); 4046 actionTextResourceId = R.string.retry; 4047 break; 4048 case UIProvider.LastSyncResult.AUTH_ERROR: 4049 listener = getSignInClickedListener(); 4050 actionTextResourceId = R.string.signin; 4051 break; 4052 case UIProvider.LastSyncResult.SECURITY_ERROR: 4053 return; // Currently we do nothing for security errors. 4054 case UIProvider.LastSyncResult.STORAGE_ERROR: 4055 listener = getStorageErrorClickedListener(); 4056 actionTextResourceId = R.string.info; 4057 break; 4058 case UIProvider.LastSyncResult.INTERNAL_ERROR: 4059 listener = getInternalErrorClickedListener(); 4060 actionTextResourceId = R.string.report; 4061 break; 4062 default: 4063 return; 4064 } 4065 mToastBar.show(listener, 4066 R.drawable.ic_alert_white, 4067 Utils.getSyncStatusText(mActivity.getActivityContext(), lastSyncResult), 4068 false, /* showActionIcon */ 4069 actionTextResourceId, 4070 replaceVisibleToast, 4071 new ToastBarOperation(1, 0, ToastBarOperation.ERROR, false, folder)); 4072 } 4073 4074 private ActionClickedListener getRetryClickedListener(final Folder folder) { 4075 return new ActionClickedListener() { 4076 @Override 4077 public void onActionClicked(Context context) { 4078 final Uri uri = folder.refreshUri; 4079 4080 if (uri != null) { 4081 startAsyncRefreshTask(uri); 4082 } 4083 } 4084 }; 4085 } 4086 4087 private ActionClickedListener getSignInClickedListener() { 4088 return new ActionClickedListener() { 4089 @Override 4090 public void onActionClicked(Context context) { 4091 promptUserForAuthentication(mAccount); 4092 } 4093 }; 4094 } 4095 4096 private ActionClickedListener getStorageErrorClickedListener() { 4097 return new ActionClickedListener() { 4098 @Override 4099 public void onActionClicked(Context context) { 4100 showStorageErrorDialog(); 4101 } 4102 }; 4103 } 4104 4105 private void showStorageErrorDialog() { 4106 DialogFragment fragment = (DialogFragment) 4107 mFragmentManager.findFragmentByTag(SYNC_ERROR_DIALOG_FRAGMENT_TAG); 4108 if (fragment == null) { 4109 fragment = SyncErrorDialogFragment.newInstance(); 4110 } 4111 fragment.show(mFragmentManager, SYNC_ERROR_DIALOG_FRAGMENT_TAG); 4112 } 4113 4114 private ActionClickedListener getInternalErrorClickedListener() { 4115 return new ActionClickedListener() { 4116 @Override 4117 public void onActionClicked(Context context) { 4118 Utils.sendFeedback(mActivity, mAccount, true /* reportingProblem */); 4119 } 4120 }; 4121 } 4122 4123 @Override 4124 public void onFooterViewErrorActionClick(Folder folder, int errorStatus) { 4125 Uri uri = null; 4126 switch (errorStatus) { 4127 case UIProvider.LastSyncResult.CONNECTION_ERROR: 4128 if (folder != null && folder.refreshUri != null) { 4129 uri = folder.refreshUri; 4130 } 4131 break; 4132 case UIProvider.LastSyncResult.AUTH_ERROR: 4133 promptUserForAuthentication(mAccount); 4134 return; 4135 case UIProvider.LastSyncResult.SECURITY_ERROR: 4136 return; // Currently we do nothing for security errors. 4137 case UIProvider.LastSyncResult.STORAGE_ERROR: 4138 showStorageErrorDialog(); 4139 return; 4140 case UIProvider.LastSyncResult.INTERNAL_ERROR: 4141 Utils.sendFeedback(mActivity, mAccount, true /* reportingProblem */); 4142 return; 4143 default: 4144 return; 4145 } 4146 4147 if (uri != null) { 4148 startAsyncRefreshTask(uri); 4149 } 4150 } 4151 4152 @Override 4153 public void onFooterViewLoadMoreClick(Folder folder) { 4154 if (folder != null && folder.loadMoreUri != null) { 4155 startAsyncRefreshTask(folder.loadMoreUri); 4156 } 4157 } 4158 4159 private void startAsyncRefreshTask(Uri uri) { 4160 if (mFolderSyncTask != null) { 4161 mFolderSyncTask.cancel(true); 4162 } 4163 mFolderSyncTask = new AsyncRefreshTask(mActivity.getActivityContext(), uri); 4164 mFolderSyncTask.execute(); 4165 } 4166 4167 private void promptUserForAuthentication(Account account) { 4168 if (account != null && !Utils.isEmpty(account.reauthenticationIntentUri)) { 4169 final Intent authenticationIntent = 4170 new Intent(Intent.ACTION_VIEW, account.reauthenticationIntentUri); 4171 mActivity.startActivityForResult(authenticationIntent, REAUTHENTICATE_REQUEST_CODE); 4172 } 4173 } 4174 4175 @Override 4176 public void onAccessibilityStateChanged() { 4177 // Clear the cache of objects. 4178 ConversationItemViewModel.onAccessibilityUpdated(); 4179 // Re-render the list if it exists. 4180 final ConversationListFragment frag = getConversationListFragment(); 4181 if (frag != null) { 4182 AnimatedAdapter adapter = frag.getAnimatedAdapter(); 4183 if (adapter != null) { 4184 adapter.notifyDataSetInvalidated(); 4185 } 4186 } 4187 } 4188 4189 @Override 4190 public void makeDialogListener (final int action, final boolean isBatch, 4191 UndoCallback undoCallback) { 4192 final Collection<Conversation> target; 4193 if (isBatch) { 4194 target = mSelectedSet.values(); 4195 } else { 4196 LogUtils.d(LOG_TAG, "Will act upon %s", mCurrentConversation); 4197 target = Conversation.listOf(mCurrentConversation); 4198 } 4199 final DestructiveAction destructiveAction = getDeferredAction(action, target, isBatch, 4200 undoCallback); 4201 mDialogAction = action; 4202 mDialogFromSelectedSet = isBatch; 4203 mDialogListener = new AlertDialog.OnClickListener() { 4204 @Override 4205 public void onClick(DialogInterface dialog, int which) { 4206 delete(action, target, destructiveAction, isBatch); 4207 // Afterwards, let's remove references to the listener and the action. 4208 setListener(null, -1); 4209 } 4210 }; 4211 } 4212 4213 @Override 4214 public AlertDialog.OnClickListener getListener() { 4215 return mDialogListener; 4216 } 4217 4218 /** 4219 * Sets the listener for the positive action on a confirmation dialog. Since only a single 4220 * confirmation dialog can be shown, this overwrites the previous listener. It is safe to 4221 * unset the listener; in which case action should be set to -1. 4222 * @param listener the listener that will perform the task for this dialog's positive action. 4223 * @param action the action that created this dialog. 4224 */ 4225 private void setListener(AlertDialog.OnClickListener listener, final int action){ 4226 mDialogListener = listener; 4227 mDialogAction = action; 4228 } 4229 4230 @Override 4231 public VeiledAddressMatcher getVeiledAddressMatcher() { 4232 return mVeiledMatcher; 4233 } 4234 4235 @Override 4236 public void setDetachedMode() { 4237 // Tell the conversation list not to select anything. 4238 final ConversationListFragment frag = getConversationListFragment(); 4239 if (frag != null) { 4240 frag.setChoiceNone(); 4241 } else if (mIsTablet) { 4242 // How did we ever land here? Detached mode, and no CLF on tablet??? 4243 LogUtils.e(LOG_TAG, "AAC.setDetachedMode(): CLF = null!"); 4244 } 4245 mDetachedConvUri = mCurrentConversation.uri; 4246 } 4247 4248 private void clearDetachedMode() { 4249 // Tell the conversation list to go back to its usual selection behavior. 4250 final ConversationListFragment frag = getConversationListFragment(); 4251 if (frag != null) { 4252 frag.revertChoiceMode(); 4253 } else if (mIsTablet) { 4254 // How did we ever land here? Detached mode, and no CLF on tablet??? 4255 LogUtils.e(LOG_TAG, "AAC.clearDetachedMode(): CLF = null on tablet!"); 4256 } 4257 mDetachedConvUri = null; 4258 } 4259 4260 @Override 4261 public DrawerController getDrawerController() { 4262 return mDrawerListener; 4263 } 4264 4265 private class MailDrawerListener extends Observable<DrawerLayout.DrawerListener> 4266 implements DrawerLayout.DrawerListener, DrawerController { 4267 private int mDrawerState; 4268 private float mOldSlideOffset; 4269 4270 public MailDrawerListener() { 4271 mDrawerState = DrawerLayout.STATE_IDLE; 4272 mOldSlideOffset = 0.f; 4273 } 4274 4275 @Override 4276 public void registerDrawerListener(DrawerLayout.DrawerListener l) { 4277 registerObserver(l); 4278 } 4279 4280 @Override 4281 public void unregisterDrawerListener(DrawerLayout.DrawerListener l) { 4282 unregisterObserver(l); 4283 } 4284 4285 @Override 4286 public boolean isDrawerOpen() { 4287 return isDrawerEnabled() && mDrawerContainer.isDrawerOpen(mDrawerPullout); 4288 } 4289 4290 @Override 4291 public boolean isDrawerVisible() { 4292 return isDrawerEnabled() && mDrawerContainer.isDrawerVisible(mDrawerPullout); 4293 } 4294 4295 @Override 4296 public void onDrawerOpened(View drawerView) { 4297 mDrawerToggle.onDrawerOpened(drawerView); 4298 4299 for (DrawerLayout.DrawerListener l : mObservers) { 4300 l.onDrawerOpened(drawerView); 4301 } 4302 } 4303 4304 @Override 4305 public void onDrawerClosed(View drawerView) { 4306 mDrawerToggle.onDrawerClosed(drawerView); 4307 if (mHasNewAccountOrFolder) { 4308 refreshDrawer(); 4309 } 4310 4311 // When closed, we want to use either the burger, or up, based on where we are 4312 final int mode = mViewMode.getMode(); 4313 final boolean isTopLevel = (mFolder == null) || (mFolder.parent == Uri.EMPTY); 4314 mDrawerToggle.setDrawerIndicatorEnabled(getShouldShowDrawerIndicator(mode, isTopLevel)); 4315 4316 for (DrawerLayout.DrawerListener l : mObservers) { 4317 l.onDrawerClosed(drawerView); 4318 } 4319 } 4320 4321 /** 4322 * As part of the overriden function, it will animate the alpha of the conversation list 4323 * view along with the drawer sliding when we're in the process of switching accounts or 4324 * folders. Note, this is the same amount of work done as {@link ValueAnimator#ofFloat}. 4325 */ 4326 @Override 4327 public void onDrawerSlide(View drawerView, float slideOffset) { 4328 mDrawerToggle.onDrawerSlide(drawerView, slideOffset); 4329 if (mHasNewAccountOrFolder && mListViewForAnimating != null) { 4330 mListViewForAnimating.setAlpha(slideOffset); 4331 } 4332 4333 // This code handles when to change the visibility of action items 4334 // based on drawer state. The basic logic is that right when we 4335 // open the drawer, we hide the action items. We show the action items 4336 // when the drawer closes. However, due to the animation of the drawer closing, 4337 // to make the reshowing of the action items feel right, we make the items visible 4338 // slightly sooner. 4339 // 4340 // However, to make the animating behavior work properly, we have to know whether 4341 // we're animating open or closed. Only if we're animating closed do we want to 4342 // show the action items early. We save the last slide offset so that we can compare 4343 // the current slide offset to it to determine if we're opening or closing. 4344 if (mDrawerState == DrawerLayout.STATE_SETTLING) { 4345 if (mHideMenuItems && slideOffset < 0.15f && mOldSlideOffset > slideOffset) { 4346 mHideMenuItems = false; 4347 mActivity.invalidateOptionsMenu(); 4348 maybeEnableCabMode(); 4349 } else if (!mHideMenuItems && slideOffset > 0.f && mOldSlideOffset < slideOffset) { 4350 mHideMenuItems = true; 4351 mActivity.invalidateOptionsMenu(); 4352 disableCabMode(); 4353 } 4354 } else { 4355 if (mHideMenuItems && Float.compare(slideOffset, 0.f) == 0) { 4356 mHideMenuItems = false; 4357 mActivity.invalidateOptionsMenu(); 4358 maybeEnableCabMode(); 4359 } else if (!mHideMenuItems && slideOffset > 0.f) { 4360 mHideMenuItems = true; 4361 mActivity.invalidateOptionsMenu(); 4362 disableCabMode(); 4363 } 4364 } 4365 4366 mOldSlideOffset = slideOffset; 4367 4368 // If we're sliding, we always want to show the burger 4369 mDrawerToggle.setDrawerIndicatorEnabled(true /* enable */); 4370 4371 for (DrawerLayout.DrawerListener l : mObservers) { 4372 l.onDrawerSlide(drawerView, slideOffset); 4373 } 4374 } 4375 4376 /** 4377 * This condition here should only be called when the drawer is stuck in a weird state 4378 * and doesn't register the onDrawerClosed, but shows up as idle. Make sure to refresh 4379 * and, more importantly, unlock the drawer when this is the case. 4380 */ 4381 @Override 4382 public void onDrawerStateChanged(int newState) { 4383 LogUtils.d(LOG_TAG, "AAC onDrawerStateChanged %d", newState); 4384 mDrawerState = newState; 4385 mDrawerToggle.onDrawerStateChanged(mDrawerState); 4386 4387 for (DrawerLayout.DrawerListener l : mObservers) { 4388 l.onDrawerStateChanged(newState); 4389 } 4390 4391 if (mViewMode.isSearchMode()) { 4392 return; 4393 } 4394 if (mDrawerState == DrawerLayout.STATE_IDLE) { 4395 if (mHasNewAccountOrFolder) { 4396 refreshDrawer(); 4397 } 4398 if (mConversationListLoadFinishedIgnored) { 4399 mConversationListLoadFinishedIgnored = false; 4400 final Bundle args = new Bundle(); 4401 args.putParcelable(BUNDLE_ACCOUNT_KEY, mAccount); 4402 args.putParcelable(BUNDLE_FOLDER_KEY, mFolder); 4403 mActivity.getLoaderManager().initLoader( 4404 LOADER_CONVERSATION_LIST, args, mListCursorCallbacks); 4405 } 4406 } 4407 } 4408 4409 /** 4410 * If we've reached a stable drawer state, unlock the drawer for usage, clear the 4411 * conversation list, and finish end actions. Also, make 4412 * {@link #mHasNewAccountOrFolder} false to reflect we're done changing. 4413 */ 4414 public void refreshDrawer() { 4415 mHasNewAccountOrFolder = false; 4416 mDrawerContainer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED); 4417 ConversationListFragment conversationList = getConversationListFragment(); 4418 if (conversationList != null) { 4419 conversationList.clear(); 4420 } 4421 mDrawerObservers.notifyChanged(); 4422 } 4423 4424 /** 4425 * Returns the most recent update of the {@link DrawerLayout}'s state provided 4426 * by {@link #onDrawerStateChanged(int)}. 4427 * @return The {@link DrawerLayout}'s current state. One of 4428 * {@link DrawerLayout#STATE_DRAGGING}, {@link DrawerLayout#STATE_IDLE}, 4429 * or {@link DrawerLayout#STATE_SETTLING}. 4430 */ 4431 public int getDrawerState() { 4432 return mDrawerState; 4433 } 4434 } 4435 4436 @Override 4437 public boolean isDrawerPullEnabled() { 4438 return true; 4439 } 4440 4441 @Override 4442 public boolean shouldHideMenuItems() { 4443 return mHideMenuItems; 4444 } 4445 4446 protected void navigateUpFolderHierarchy() { 4447 new AsyncTask<Void, Void, Folder>() { 4448 @Override 4449 protected Folder doInBackground(final Void... params) { 4450 if (mInbox == null) { 4451 // We don't have an inbox, but we need it 4452 final Cursor cursor = mContext.getContentResolver().query( 4453 mAccount.settings.defaultInbox, UIProvider.FOLDERS_PROJECTION, null, 4454 null, null); 4455 4456 if (cursor != null) { 4457 try { 4458 if (cursor.moveToFirst()) { 4459 mInbox = new Folder(cursor); 4460 } 4461 } finally { 4462 cursor.close(); 4463 } 4464 } 4465 } 4466 4467 // Now try to load our parent 4468 final Folder folder; 4469 4470 if (mFolder != null) { 4471 Cursor cursor = null; 4472 try { 4473 cursor = mContext.getContentResolver().query(mFolder.parent, 4474 UIProvider.FOLDERS_PROJECTION, null, null, null); 4475 4476 if (cursor == null || !cursor.moveToFirst()) { 4477 // We couldn't load the parent, so use the inbox 4478 folder = mInbox; 4479 } else { 4480 folder = new Folder(cursor); 4481 } 4482 } finally { 4483 if (cursor != null) { 4484 cursor.close(); 4485 } 4486 } 4487 } else { 4488 folder = mInbox; 4489 } 4490 4491 return folder; 4492 } 4493 4494 @Override 4495 protected void onPostExecute(final Folder result) { 4496 onFolderSelected(result); 4497 } 4498 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null); 4499 } 4500 4501 @Override 4502 public Parcelable getConversationListScrollPosition(final String folderUri) { 4503 return mConversationListScrollPositions.getParcelable(folderUri); 4504 } 4505 4506 @Override 4507 public void setConversationListScrollPosition(final String folderUri, 4508 final Parcelable savedPosition) { 4509 mConversationListScrollPositions.putParcelable(folderUri, savedPosition); 4510 } 4511} 4512