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