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