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