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