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