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