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