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