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