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