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