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