Home | History | Annotate | Download | only in ui
      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 {
    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 
    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
    505     public Account getCurrentAccount() {
    506         return mAccount;
    507     }
    508 
    509     @Override
    510     public ConversationListContext getCurrentListContext() {
    511         return mConvListContext;
    512     }
    513 
    514     @Override
    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
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    625     protected abstract boolean isConversationListVisible();
    626 
    627     /**
    628      * If required, starts wait mode for the current account.
    629      */
    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
    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
    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
    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
    726     public void unregisterAccountObserver(DataSetObserver obs) {
    727         mAccountObservers.unregisterObserver(obs);
    728     }
    729 
    730     @Override
    731     public void registerAllAccountObserver(DataSetObserver observer) {
    732         mAllAccountObservers.registerObserver(observer);
    733     }
    734 
    735     @Override
    736     public void unregisterAllAccountObserver(DataSetObserver observer) {
    737         mAllAccountObservers.unregisterObserver(observer);
    738     }
    739 
    740     @Override
    741     public Account[] getAllAccounts() {
    742         return mAllAccounts;
    743     }
    744 
    745     @Override
    746     public Account getAccount() {
    747         return mAccount;
    748     }
    749 
    750     @Override
    751     public void registerDrawerClosedObserver(final DataSetObserver observer) {
    752         mDrawerObservers.registerObserver(observer);
    753     }
    754 
    755     @Override
    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
    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      */
    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      */
    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
    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      */
    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      */
    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
    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
    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
    926     public void unregisterRecentFolderObserver(DataSetObserver obs) {
    927         mRecentFolderObservers.unregisterObserver(obs);
    928     }
    929 
    930     @Override
    931     public RecentFolderList getRecentFolders() {
    932         return mRecentFolderList;
    933     }
    934 
    935     @Override
    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
    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      */
    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      */
    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
   1032     public Folder getFolder() {
   1033         return mFolder;
   1034     }
   1035 
   1036     @Override
   1037     public Folder getHierarchyFolder() {
   1038         return mFolderListFolder;
   1039     }
   1040 
   1041     @Override
   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
   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      */
   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
   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
   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      */
   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      */
   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
   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
   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
   1285     public void onConfigurationChanged(Configuration newConfig) {
   1286         mDrawerToggle.onConfigurationChanged(newConfig);
   1287     }
   1288 
   1289     /**
   1290      * If drawer is open/visible (even partially), close it.
   1291      */
   1292     protected void closeDrawerIfOpen() {
   1293         if (!isDrawerEnabled()) {
   1294             return;
   1295         }
   1296         if(mDrawerContainer.isDrawerOpen(mDrawerPullout)) {
   1297             mDrawerContainer.closeDrawers();
   1298         }
   1299     }
   1300 
   1301     @Override
   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
   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
   1329     public Dialog onCreateDialog(int id, Bundle bundle) {
   1330         return null;
   1331     }
   1332 
   1333     @Override
   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
   1345     public final boolean onKeyDown(int keyCode, KeyEvent event) {
   1346         return false;
   1347     }
   1348 
   1349     public abstract boolean doesActionChangeConversationListVisibility(int action);
   1350 
   1351     @Override
   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      */
   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
   1480     public void onFolderEmptied() {
   1481         emptyFolder();
   1482     }
   1483 
   1484     /**
   1485      * Performs the work of emptying the currently visible folder.
   1486      */
   1487     private void emptyFolder() {
   1488         if (mConversationListCursor != null) {
   1489             mConversationListCursor.emptyFolder();
   1490         }
   1491     }
   1492 
   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      */
   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
   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
   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 
   1546     protected abstract boolean handleBackPress();
   1547     protected abstract boolean handleUpPress();
   1548 
   1549     @Override
   1550     public void addUpOrBackHandler(UpOrBackHandler handler) {
   1551         if (mUpOrBackHandlers.contains(handler)) {
   1552             return;
   1553         }
   1554         mUpOrBackHandlers.addFirst(handler);
   1555     }
   1556 
   1557     @Override
   1558     public void removeUpOrBackHandler(UpOrBackHandler handler) {
   1559         mUpOrBackHandlers.remove(handler);
   1560     }
   1561 
   1562     @Override
   1563     public void updateConversation(Collection<Conversation> target, ContentValues values) {
   1564         mConversationListCursor.updateValues(target, values);
   1565         refreshConversationList();
   1566     }
   1567 
   1568     @Override
   1569     public void updateConversation(Collection <Conversation> target, String columnName,
   1570             boolean value) {
   1571         mConversationListCursor.updateBoolean(target, columnName, value);
   1572         refreshConversationList();
   1573     }
   1574 
   1575     @Override
   1576     public void updateConversation(Collection <Conversation> target, String columnName,
   1577             int value) {
   1578         mConversationListCursor.updateInt(target, columnName, value);
   1579         refreshConversationList();
   1580     }
   1581 
   1582     @Override
   1583     public void updateConversation(Collection <Conversation> target, String columnName,
   1584             String value) {
   1585         mConversationListCursor.updateString(target, columnName, value);
   1586         refreshConversationList();
   1587     }
   1588 
   1589     @Override
   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 
   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
   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 
   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
   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      */
   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      */
   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
   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
   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      */
   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
   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      */
   1984     private void requestUpdate(final DestructiveAction action) {
   1985         action.performAction();
   1986         refreshConversationList();
   1987     }
   1988 
   1989     @Override
   1990     public void onPrepareDialog(int id, Dialog dialog, Bundle bundle) {
   1991         // TODO(viki): Auto-generated method stub
   1992     }
   1993 
   1994     @Override
   1995     public boolean onPrepareOptionsMenu(Menu menu) {
   1996         return mActionBarView.onPrepareOptionsMenu(menu);
   1997     }
   1998 
   1999     @Override
   2000     public void onPause() {
   2001         mHaveAccountList = false;
   2002         enableNotifications();
   2003     }
   2004 
   2005     @Override
   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
   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      */
   2069     protected boolean safeToModifyFragments() {
   2070         return mSafeToModifyFragments;
   2071     }
   2072 
   2073     @Override
   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
   2085     public void onStop() {
   2086         NotificationActionUtils.unregisterUndoNotificationObserver(mUndoNotificationObserver);
   2087     }
   2088 
   2089     @Override
   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      */
   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
   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      */
   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      */
   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 
   2171     public void disablePagerUpdates() {
   2172         mPagerController.stopListening();
   2173     }
   2174 
   2175     public boolean isDestroyed() {
   2176         return mDestroyed;
   2177     }
   2178 
   2179     @Override
   2180     public void commitDestructiveActions(boolean animate) {
   2181         ConversationListFragment fragment = getConversationListFragment();
   2182         if (fragment != null) {
   2183             fragment.commitDestructiveActions(animate);
   2184         }
   2185     }
   2186 
   2187     @Override
   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      */
   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
   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      */
   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      */
   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      */
   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 
   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      */
   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
   2422     public void showWaitForInitialization() {
   2423         mViewMode.enterWaitingForInitializationMode();
   2424         mWaitFragment = WaitFragment.newInstance(mAccount);
   2425     }
   2426 
   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      */
   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      */
   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      */
   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
   2479     public void showConversationList(ConversationListContext listContext) {
   2480     }
   2481 
   2482     @Override
   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
   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
   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
   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
   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      */
   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
   2564     public void registerConversationListObserver(DataSetObserver observer) {
   2565         mConversationListObservable.registerObserver(observer);
   2566     }
   2567 
   2568     @Override
   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
   2580     public void registerFolderObserver(DataSetObserver observer) {
   2581         mFolderObservable.registerObserver(observer);
   2582     }
   2583 
   2584     @Override
   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
   2596     public void registerConversationLoadedObserver(DataSetObserver observer) {
   2597         mPagerController.registerConversationLoadedObserver(observer);
   2598     }
   2599 
   2600     @Override
   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      */
   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      */
   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 
   2724     private void disableNotifications() {
   2725         mNewEmailReceiver.activate(mContext, this);
   2726     }
   2727 
   2728     private void enableNotifications() {
   2729         mNewEmailReceiver.deactivate();
   2730     }
   2731 
   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          */
   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
   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          */
   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
   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
   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
   2954     public void startDragMode() {
   2955         mIsDragHappening = true;
   2956     }
   2957 
   2958     @Override
   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 
   2972     private boolean isDragging() {
   2973         return mIsDragHappening;
   2974     }
   2975 
   2976     @Override
   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
   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
   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      */
   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 
   3036         RefreshTimerTask(AbstractActivityController controller, Handler handler) {
   3037             mHandler = handler;
   3038             mController = controller;
   3039         }
   3040 
   3041         @Override
   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      */
   3055     private void cancelRefreshTask () {
   3056         if (mConversationListRefreshTask != null) {
   3057             mConversationListRefreshTask.cancel();
   3058             mConversationListRefreshTask = null;
   3059         }
   3060     }
   3061 
   3062     @Override
   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
   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
   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
   3099     public void onSetChanged(ConversationSelectionSet set) {
   3100         // Do nothing. We don't care about changes to the set.
   3101     }
   3102 
   3103     @Override
   3104     public ConversationSelectionSet getSelectedSet() {
   3105         return mSelectedSet;
   3106     }
   3107 
   3108     /**
   3109      * Disable the Contextual Action Bar (CAB). The selected set is not changed.
   3110      */
   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      */
   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      */
   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      */
   3143     protected final void exitCabMode() {
   3144         mSelectedSet.clear();
   3145     }
   3146 
   3147     @Override
   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
   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
   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
   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 
   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 
   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 
   3275         public DroppedInStarredAction(Collection<Conversation> conversations, Folder initialFolder,
   3276                 Folder starredFolder) {
   3277             mConversations = conversations;
   3278             mInitialFolder = initialFolder;
   3279             mStarred = starredFolder;
   3280         }
   3281 
   3282         @Override
   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
   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 
   3325     protected abstract void hideOrRepositionToastBar(boolean animated);
   3326 
   3327     @Override
   3328     public void onConversationSeen() {
   3329         mPagerController.onConversationSeen();
   3330     }
   3331 
   3332     @Override
   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      */
   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
   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
   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
   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
   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
   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
   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
   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
   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
   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      */
   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      */
   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      */
   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
   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
   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      */
   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          */
   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
   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          */
   3832         private synchronized boolean isPerformed() {
   3833             if (mCompleted) {
   3834                 return true;
   3835             }
   3836             mCompleted = true;
   3837             return false;
   3838         }
   3839     }
   3840 
   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 
   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
   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
   3868     public final void refreshConversationList() {
   3869         final ConversationListFragment convList = getConversationListFragment();
   3870         if (convList == null) {
   3871             return;
   3872         }
   3873         convList.requestListRefresh();
   3874     }
   3875 
   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