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