AbstractActivityController.java revision bc2e1b13b409086ea6e3edb5e795566b6c4ebb22
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
18package com.android.mail.ui;
19
20import android.app.ActionBar;
21import android.app.ActionBar.LayoutParams;
22import android.app.Activity;
23import android.app.AlertDialog;
24import android.app.Dialog;
25import android.app.DialogFragment;
26import android.app.Fragment;
27import android.app.FragmentManager;
28import android.app.LoaderManager;
29import android.app.SearchManager;
30import android.content.ContentProviderOperation;
31import android.content.ContentResolver;
32import android.content.ContentValues;
33import android.content.Context;
34import android.content.CursorLoader;
35import android.content.DialogInterface;
36import android.content.DialogInterface.OnClickListener;
37import android.content.Intent;
38import android.content.Loader;
39import android.content.res.Resources;
40import android.database.Cursor;
41import android.database.DataSetObservable;
42import android.database.DataSetObserver;
43import android.net.Uri;
44import android.os.AsyncTask;
45import android.os.Bundle;
46import android.os.Handler;
47import android.provider.SearchRecentSuggestions;
48import android.text.TextUtils;
49import android.view.DragEvent;
50import android.view.KeyEvent;
51import android.view.LayoutInflater;
52import android.view.Menu;
53import android.view.MenuInflater;
54import android.view.MenuItem;
55import android.view.MotionEvent;
56import android.view.View;
57import android.widget.Toast;
58
59import com.android.mail.ConversationListContext;
60import com.android.mail.R;
61import com.android.mail.browse.ConfirmDialogFragment;
62import com.android.mail.browse.ConversationCursor;
63import com.android.mail.browse.ConversationCursor.ConversationOperation;
64import com.android.mail.browse.ConversationItemViewModel;
65import com.android.mail.browse.ConversationPagerController;
66import com.android.mail.browse.MessageCursor.ConversationMessage;
67import com.android.mail.browse.SelectedConversationsActionMenu;
68import com.android.mail.browse.SyncErrorDialogFragment;
69import com.android.mail.compose.ComposeActivity;
70import com.android.mail.providers.Account;
71import com.android.mail.providers.Conversation;
72import com.android.mail.providers.ConversationInfo;
73import com.android.mail.providers.Folder;
74import com.android.mail.providers.FolderWatcher;
75import com.android.mail.providers.MailAppProvider;
76import com.android.mail.providers.Settings;
77import com.android.mail.providers.SuggestionsProvider;
78import com.android.mail.providers.UIProvider;
79import com.android.mail.providers.UIProvider.AccountCapabilities;
80import com.android.mail.providers.UIProvider.AccountColumns;
81import com.android.mail.providers.UIProvider.AccountCursorExtraKeys;
82import com.android.mail.providers.UIProvider.AutoAdvance;
83import com.android.mail.providers.UIProvider.ConversationColumns;
84import com.android.mail.providers.UIProvider.ConversationOperations;
85import com.android.mail.providers.UIProvider.FolderCapabilities;
86import com.android.mail.ui.ActionableToastBar.ActionClickedListener;
87import com.android.mail.utils.ContentProviderTask;
88import com.android.mail.utils.LogTag;
89import com.android.mail.utils.LogUtils;
90import com.android.mail.utils.NotificationActionUtils;
91import com.android.mail.utils.Utils;
92import com.android.mail.utils.VeiledAddressMatcher;
93
94import com.google.common.base.Objects;
95import com.google.common.collect.ImmutableList;
96import com.google.common.collect.Lists;
97import com.google.common.collect.Sets;
98
99import java.util.ArrayList;
100import java.util.Collection;
101import java.util.Collections;
102import java.util.Deque;
103import java.util.HashMap;
104import java.util.List;
105import java.util.Set;
106import java.util.TimerTask;
107
108
109/**
110 * This is an abstract implementation of the Activity Controller. This class
111 * knows how to respond to menu items, state changes, layout changes, etc. It
112 * weaves together the views and listeners, dispatching actions to the
113 * respective underlying classes.
114 * <p>
115 * Even though this class is abstract, it should provide default implementations
116 * for most, if not all the methods in the ActivityController interface. This
117 * makes the task of the subclasses easier: OnePaneActivityController and
118 * TwoPaneActivityController can be concise when the common functionality is in
119 * AbstractActivityController.
120 * </p>
121 * <p>
122 * In the Gmail codebase, this was called BaseActivityController
123 * </p>
124 */
125public abstract class AbstractActivityController implements ActivityController {
126    // Keys for serialization of various information in Bundles.
127    /** Tag for {@link #mAccount} */
128    private static final String SAVED_ACCOUNT = "saved-account";
129    /** Tag for {@link #mFolder} */
130    private static final String SAVED_FOLDER = "saved-folder";
131    /** Tag for {@link #mCurrentConversation} */
132    private static final String SAVED_CONVERSATION = "saved-conversation";
133    /** Tag for {@link #mSelectedSet} */
134    private static final String SAVED_SELECTED_SET = "saved-selected-set";
135    /** Tag for {@link ActionableToastBar#getOperation()} */
136    private static final String SAVED_TOAST_BAR_OP = "saved-toast-bar-op";
137    /** Tag for {@link #mFolderListFolder} */
138    private static final String SAVED_HIERARCHICAL_FOLDER = "saved-hierarchical-folder";
139    /** Tag for {@link ConversationListContext#searchQuery} */
140    private static final String SAVED_QUERY = "saved-query";
141    /** Tag for {@link #mDialogAction} */
142    private static final String SAVED_ACTION = "saved-action";
143    /** Tag for {@link #mDialogFromSelectedSet} */
144    private static final String SAVED_ACTION_FROM_SELECTED = "saved-action-from-selected";
145    /** Tag for {@link #mDetachedConvUri} */
146    private static final String SAVED_DETACHED_CONV_URI = "saved-detached-conv-uri";
147
148    /** Tag  used when loading a wait fragment */
149    protected static final String TAG_WAIT = "wait-fragment";
150    /** Tag used when loading a conversation list fragment. */
151    public static final String TAG_CONVERSATION_LIST = "tag-conversation-list";
152    /** Tag used when loading a folder list fragment. */
153    protected static final String TAG_FOLDER_LIST = "tag-folder-list";
154
155    protected Account mAccount;
156    protected Folder mFolder;
157    /** True when {@link #mFolder} is first shown to the user. */
158    private boolean mFolderChanged = false;
159    protected MailActionBarView mActionBarView;
160    protected final ControllableActivity mActivity;
161    protected final Context mContext;
162    private final FragmentManager mFragmentManager;
163    protected final RecentFolderList mRecentFolderList;
164    protected ConversationListContext mConvListContext;
165    protected Conversation mCurrentConversation;
166    /**
167     * The hash of {@link #mCurrentConversation} in detached mode. 0 if we are not in detached mode.
168     */
169    private Uri mDetachedConvUri;
170
171    /** A {@link android.content.BroadcastReceiver} that suppresses new e-mail notifications. */
172    private SuppressNotificationReceiver mNewEmailReceiver = null;
173
174    protected Handler mHandler = new Handler();
175
176    /**
177     * The current mode of the application. All changes in mode are initiated by
178     * the activity controller. View mode changes are propagated to classes that
179     * attach themselves as listeners of view mode changes.
180     */
181    protected final ViewMode mViewMode;
182    protected ContentResolver mResolver;
183    protected boolean isLoaderInitialized = false;
184    private AsyncRefreshTask mAsyncRefreshTask;
185
186    private boolean mDestroyed;
187
188    /** True if running on tablet */
189    private final boolean mIsTablet;
190
191    /**
192     * Are we in a point in the Activity/Fragment lifecycle where it's safe to execute fragment
193     * transactions? (including back stack manipulation)
194     * <p>
195     * Per docs in {@link FragmentManager#beginTransaction()}, this flag starts out true, switches
196     * to false after {@link Activity#onSaveInstanceState}, and becomes true again in both onStart
197     * and onResume.
198     */
199    private boolean mSafeToModifyFragments = true;
200
201    private final Set<Uri> mCurrentAccountUris = Sets.newHashSet();
202    protected ConversationCursor mConversationListCursor;
203    private final DataSetObservable mConversationListObservable = new DataSetObservable() {
204        @Override
205        public void registerObserver(DataSetObserver observer) {
206            final int count = mObservers.size();
207            super.registerObserver(observer);
208            LogUtils.d(LOG_TAG, "IN AAC.register(List)Observer: %s before=%d after=%d", observer,
209                    count, mObservers.size());
210        }
211        @Override
212        public void unregisterObserver(DataSetObserver observer) {
213            final int count = mObservers.size();
214            super.unregisterObserver(observer);
215            LogUtils.d(LOG_TAG, "IN AAC.unregister(List)Observer: %s before=%d after=%d", observer,
216                    count, mObservers.size());
217        }
218    };
219
220    /**
221     * Interface for actions that are deferred until after a load completes. This is for handling
222     * user actions which affect cursors (e.g. marking messages read or unread) that happen before
223     * that cursor is loaded.
224     */
225    private interface LoadFinishedCallback {
226        void onLoadFinished();
227    }
228
229    /** The deferred actions to execute when mConversationListCursor load completes. */
230    private final ArrayList<LoadFinishedCallback> mConversationListLoadFinishedCallbacks =
231            new ArrayList<LoadFinishedCallback>();
232
233    private RefreshTimerTask mConversationListRefreshTask;
234
235    /** Listeners that are interested in changes to the current account. */
236    private final DataSetObservable mAccountObservers = new DataSetObservable() {
237        @Override
238        public void registerObserver(DataSetObserver observer) {
239            final int count = mObservers.size();
240            super.registerObserver(observer);
241            LogUtils.d(LOG_TAG, "IN AAC.register(Account)Observer: %s before=%d after=%d",
242                    observer, count, mObservers.size());
243        }
244        @Override
245        public void unregisterObserver(DataSetObserver observer) {
246            final int count = mObservers.size();
247            super.unregisterObserver(observer);
248            LogUtils.d(LOG_TAG, "IN AAC.unregister(Account)Observer: %s before=%d after=%d",
249                    observer, count, mObservers.size());
250        }
251    };
252
253    /** Listeners that are interested in changes to the recent folders. */
254    private final DataSetObservable mRecentFolderObservers = new DataSetObservable() {
255        @Override
256        public void registerObserver(DataSetObserver observer) {
257            final int count = mObservers.size();
258            super.registerObserver(observer);
259            LogUtils.d(LOG_TAG, "IN AAC.register(RecentFolder)Observer: %s before=%d after=%d",
260                    observer, count, mObservers.size());
261        }
262        @Override
263        public void unregisterObserver(DataSetObserver observer) {
264            final int count = mObservers.size();
265            super.unregisterObserver(observer);
266            LogUtils.d(LOG_TAG, "IN AAC.unregister(RecentFolder)Observer: %s before=%d after=%d",
267                    observer, count, mObservers.size());
268        }
269    };
270
271    /**
272     * Selected conversations, if any.
273     */
274    private final ConversationSelectionSet mSelectedSet = new ConversationSelectionSet();
275
276    private final int mFolderItemUpdateDelayMs;
277
278    /** Keeps track of selected and unselected conversations */
279    final protected ConversationPositionTracker mTracker;
280
281    /**
282     * Action menu associated with the selected set.
283     */
284    SelectedConversationsActionMenu mCabActionMenu;
285    protected ActionableToastBar mToastBar;
286    protected ConversationPagerController mPagerController;
287
288    // this is split out from the general loader dispatcher because its loader doesn't return a
289    // basic Cursor
290    private final ConversationListLoaderCallbacks mListCursorCallbacks =
291            new ConversationListLoaderCallbacks();
292
293    private final DataSetObservable mFolderObservable = new DataSetObservable();
294
295    /**
296     * Matched addresses that must be shielded from users because they are temporary. Even though
297     * this is instantiated from settings, this matcher is valid for all accounts, and is expected
298     * to live past the life of an account.
299     */
300    private final VeiledAddressMatcher mVeiledMatcher;
301
302    protected static final String LOG_TAG = LogTag.getLogTag();
303    /** Constants used to differentiate between the types of loaders. */
304    private static final int LOADER_ACCOUNT_CURSOR = 0;
305    private static final int LOADER_FOLDER_CURSOR = 2;
306    private static final int LOADER_RECENT_FOLDERS = 3;
307    private static final int LOADER_CONVERSATION_LIST = 4;
308    private static final int LOADER_ACCOUNT_INBOX = 5;
309    private static final int LOADER_SEARCH = 6;
310    private static final int LOADER_ACCOUNT_UPDATE_CURSOR = 7;
311    /**
312     * Guaranteed to be the last loader ID used by the activity. Loaders are owned by Activity or
313     * fragments, and within an activity, loader IDs need to be unique. A hack to ensure that the
314     * {@link FolderWatcher} can create its folder loaders without clashing with the IDs of those
315     * of the {@link AbstractActivityController}. Currently, the {@link FolderWatcher} is the only
316     * other class that uses this activity's LoaderManager. If another class needs activity-level
317     * loaders, consider consolidating the loaders in a central location: a UI-less fragment
318     * perhaps.
319     */
320    public static final int LAST_LOADER_ID = 100;
321
322    private static final int ADD_ACCOUNT_REQUEST_CODE = 1;
323    private static final int REAUTHENTICATE_REQUEST_CODE = 2;
324
325    /** The pending destructive action to be carried out before swapping the conversation cursor.*/
326    private DestructiveAction mPendingDestruction;
327    protected AsyncRefreshTask mFolderSyncTask;
328    // Task for setting any share intents for the account to enabled.
329    // This gets cancelled if the user kills the app before it finishes, and
330    // will just run the next time the user opens the app.
331    private AsyncTask<String, Void, Void> mEnableShareIntents;
332    private Folder mFolderListFolder;
333    private boolean mIsDragHappening;
334    private int mShowUndoBarDelay;
335    private boolean mRecentsDataUpdated;
336    /** A wait fragment we added, if any. */
337    private WaitFragment mWaitFragment;
338    /** True if we have results from a search query */
339    private boolean mHaveSearchResults = false;
340    /** If a confirmation dialog is being show, the listener for the positive action. */
341    private OnClickListener mDialogListener;
342    /**
343     * If a confirmation dialog is being show, the resource of the action: R.id.delete, etc.  This
344     * is used to create a new {@link #mDialogListener} on orientation changes.
345     */
346    private int mDialogAction = -1;
347    /**
348     * If a confirmation dialog is being shown, this is true if the dialog acts on the selected set
349     * and false if it acts on the currently selected conversation
350     */
351    private boolean mDialogFromSelectedSet;
352
353    private final Deque<UpOrBackHandler> mUpOrBackHandlers = Lists.newLinkedList();
354
355    public static final String SYNC_ERROR_DIALOG_FRAGMENT_TAG = "SyncErrorDialogFragment";
356
357    private final DataSetObserver mUndoNotificationObserver = new DataSetObserver() {
358        @Override
359        public void onChanged() {
360            super.onChanged();
361
362            if (mConversationListCursor != null) {
363                mConversationListCursor.handleNotificationActions();
364            }
365        }
366    };
367
368    public AbstractActivityController(MailActivity activity, ViewMode viewMode) {
369        mActivity = activity;
370        mFragmentManager = mActivity.getFragmentManager();
371        mViewMode = viewMode;
372        mContext = activity.getApplicationContext();
373        mRecentFolderList = new RecentFolderList(mContext);
374        mTracker = new ConversationPositionTracker(this);
375        // Allow the fragment to observe changes to its own selection set. No other object is
376        // aware of the selected set.
377        mSelectedSet.addObserver(this);
378
379        final Resources r = mContext.getResources();
380        mFolderItemUpdateDelayMs = r.getInteger(R.integer.folder_item_refresh_delay_ms);
381        mShowUndoBarDelay = r.getInteger(R.integer.show_undo_bar_delay_ms);
382        mVeiledMatcher = VeiledAddressMatcher.newInstance(activity.getResources());
383        mIsTablet = Utils.useTabletUI(r);
384    }
385
386    @Override
387    public Account getCurrentAccount() {
388        return mAccount;
389    }
390
391    @Override
392    public ConversationListContext getCurrentListContext() {
393        return mConvListContext;
394    }
395
396    @Override
397    public String getHelpContext() {
398        final int mode = mViewMode.getMode();
399        final int helpContextResId;
400        switch (mode) {
401            case ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION:
402                helpContextResId = R.string.wait_help_context;
403                break;
404            default:
405                helpContextResId = R.string.main_help_context;
406        }
407        return mContext.getString(helpContextResId);
408    }
409
410    @Override
411    public final ConversationCursor getConversationListCursor() {
412        return mConversationListCursor;
413    }
414
415    /**
416     * Check if the fragment is attached to an activity and has a root view.
417     * @param in
418     * @return true if the fragment is valid, false otherwise
419     */
420    private static final boolean isValidFragment(Fragment in) {
421        if (in == null || in.getActivity() == null || in.getView() == null) {
422            return false;
423        }
424        return true;
425    }
426
427    /**
428     * Get the conversation list fragment for this activity. If the conversation list fragment is
429     * not attached, this method returns null.
430     *
431     * Caution! This method returns the {@link ConversationListFragment} after the fragment has been
432     * added, <b>and</b> after the {@link FragmentManager} has run through its queue to add the
433     * fragment. There is a non-trivial amount of time after the fragment is instantiated and before
434     * this call returns a non-null value, depending on the {@link FragmentManager}. If you
435     * need the fragment immediately after adding it, consider making the fragment an observer of
436     * the controller and perform the task immediately on {@link Fragment#onActivityCreated(Bundle)}
437     */
438    protected ConversationListFragment getConversationListFragment() {
439        final Fragment fragment = mFragmentManager.findFragmentByTag(TAG_CONVERSATION_LIST);
440        if (isValidFragment(fragment)) {
441            return (ConversationListFragment) fragment;
442        }
443        return null;
444    }
445
446    /**
447     * Returns the folder list fragment attached with this activity. If no such fragment is attached
448     * this method returns null.
449     *
450     * Caution! This method returns the {@link FolderListFragment} after the fragment has been
451     * added, <b>and</b> after the {@link FragmentManager} has run through its queue to add the
452     * fragment. There is a non-trivial amount of time after the fragment is instantiated and before
453     * this call returns a non-null value, depending on the {@link FragmentManager}. If you
454     * need the fragment immediately after adding it, consider making the fragment an observer of
455     * the controller and perform the task immediately on {@link Fragment#onActivityCreated(Bundle)}
456     */
457    protected FolderListFragment getFolderListFragment() {
458        final Fragment fragment = mFragmentManager.findFragmentByTag(TAG_FOLDER_LIST);
459        if (isValidFragment(fragment)) {
460            return (FolderListFragment) fragment;
461        }
462        return null;
463    }
464
465    /**
466     * Initialize the action bar. This is not visible to OnePaneController and
467     * TwoPaneController so they cannot override this behavior.
468     */
469    private void initializeActionBar() {
470        final ActionBar actionBar = mActivity.getActionBar();
471        if (actionBar == null) {
472            return;
473        }
474
475        // be sure to inherit from the ActionBar theme when inflating
476        final LayoutInflater inflater = LayoutInflater.from(actionBar.getThemedContext());
477        final boolean isSearch = mActivity.getIntent() != null
478                && Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction());
479        mActionBarView = (MailActionBarView) inflater.inflate(
480                isSearch ? R.layout.search_actionbar_view : R.layout.actionbar_view, null);
481        mActionBarView.initialize(mActivity, this, mViewMode, actionBar, mRecentFolderList);
482    }
483
484    /**
485     * Attach the action bar to the activity.
486     */
487    private void attachActionBar() {
488        final ActionBar actionBar = mActivity.getActionBar();
489        if (actionBar != null && mActionBarView != null) {
490            actionBar.setCustomView(mActionBarView, new ActionBar.LayoutParams(
491                    LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
492            // Show a custom view and home icon, but remove the title
493            final int mask = ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_SHOW_TITLE
494                    | ActionBar.DISPLAY_SHOW_HOME;
495            final int enabled = ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_SHOW_HOME;
496            actionBar.setDisplayOptions(enabled, mask);
497            mActionBarView.attach();
498        }
499        mViewMode.addListener(mActionBarView);
500    }
501
502    /**
503     * Returns whether the conversation list fragment is visible or not.
504     * Different layouts will have their own notion on the visibility of
505     * fragments, so this method needs to be overriden.
506     *
507     */
508    protected abstract boolean isConversationListVisible();
509
510    /**
511     * If required, starts wait mode for the current account.
512     */
513    final void perhapsEnterWaitMode() {
514        // If the account is not initialized, then show the wait fragment, since nothing can be
515        // shown.
516        if (mAccount.isAccountInitializationRequired()) {
517            showWaitForInitialization();
518            return;
519        }
520
521        final boolean inWaitingMode = inWaitMode();
522        final boolean isSyncRequired = mAccount.isAccountSyncRequired();
523        if (isSyncRequired) {
524            if (inWaitingMode) {
525                // Update the WaitFragment's account object
526                updateWaitMode();
527            } else {
528                // Transition to waiting mode
529                showWaitForInitialization();
530            }
531        } else if (inWaitingMode) {
532            // Dismiss waiting mode
533            hideWaitForInitialization();
534        }
535    }
536
537    @Override
538    public void onAccountChanged(Account account) {
539        // Is the account or account settings different from the existing account?
540        final boolean firstLoad = mAccount == null;
541        final boolean accountChanged = firstLoad || !account.uri.equals(mAccount.uri);
542        // If nothing has changed, return early without wasting any more time.
543        if (!accountChanged && !account.settingsDiffer(mAccount)) {
544            return;
545        }
546        // We also don't want to do anything if the new account is null
547        if (account == null) {
548            LogUtils.e(LOG_TAG, "AAC.onAccountChanged(null) called.");
549            return;
550        }
551        final String accountName = account.name;
552        mHandler.post(new Runnable() {
553            @Override
554            public void run() {
555                MailActivity.setNfcMessage(accountName);
556            }
557        });
558        if (accountChanged) {
559            commitDestructiveActions(false);
560        }
561        // Change the account here
562        setAccount(account);
563        // And carry out associated actions.
564        cancelRefreshTask();
565        if (accountChanged) {
566            loadAccountInbox();
567        }
568        // Check if we need to force setting up an account before proceeding.
569        if (mAccount != null && !Uri.EMPTY.equals(mAccount.settings.setupIntentUri)) {
570            // Launch the intent!
571            final Intent intent = new Intent(Intent.ACTION_EDIT);
572            intent.setData(mAccount.settings.setupIntentUri);
573            mActivity.startActivity(intent);
574        }
575    }
576
577    /**
578     * Adds a listener interested in change in the current account. If a class is storing a
579     * reference to the current account, it should listen on changes, so it can receive updates to
580     * settings. Must happen in the UI thread.
581     */
582    @Override
583    public void registerAccountObserver(DataSetObserver obs) {
584        mAccountObservers.registerObserver(obs);
585    }
586
587    /**
588     * Removes a listener from receiving current account changes.
589     * Must happen in the UI thread.
590     */
591    @Override
592    public void unregisterAccountObserver(DataSetObserver obs) {
593        mAccountObservers.unregisterObserver(obs);
594    }
595
596    @Override
597    public Account getAccount() {
598        return mAccount;
599    }
600
601    private void fetchSearchFolder(Intent intent) {
602        final Bundle args = new Bundle();
603        args.putString(ConversationListContext.EXTRA_SEARCH_QUERY, intent
604                .getStringExtra(ConversationListContext.EXTRA_SEARCH_QUERY));
605        mActivity.getLoaderManager().restartLoader(LOADER_SEARCH, args, this);
606    }
607
608    @Override
609    public void onFolderChanged(Folder folder) {
610        changeFolder(folder, null);
611    }
612
613    /**
614     * Sets the folder state without changing view mode and without creating a list fragment, if
615     * possible.
616     * @param folder
617     */
618    private void setListContext(Folder folder, String query) {
619        updateFolder(folder);
620        if (query != null) {
621            mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder, query);
622        } else {
623            mConvListContext = ConversationListContext.forFolder(mAccount, mFolder);
624        }
625        cancelRefreshTask();
626    }
627
628    /**
629     * Changes the folder to the value provided here. This causes the view mode to change.
630     * @param folder the folder to change to
631     * @param query if non-null, this represents the search string that the folder represents.
632     */
633    private final void changeFolder(Folder folder, String query) {
634        if (!Objects.equal(mFolder, folder)) {
635            commitDestructiveActions(false);
636        }
637        if (folder != null && !folder.equals(mFolder)
638                || (mViewMode.getMode() != ViewMode.CONVERSATION_LIST)) {
639            setListContext(folder, query);
640            showConversationList(mConvListContext);
641        }
642        resetActionBarIcon();
643    }
644
645    @Override
646    public void onFolderSelected(Folder folder) {
647        onFolderChanged(folder);
648    }
649
650    /**
651     * Update the recent folders. This only needs to be done once when accessing a new folder.
652     */
653    private void updateRecentFolderList() {
654        if (mFolder != null) {
655            mRecentFolderList.touchFolder(mFolder, mAccount);
656        }
657    }
658
659    /**
660     * Adds a listener interested in change in the recent folders. If a class is storing a
661     * reference to the recent folders, it should listen on changes, so it can receive updates.
662     * Must happen in the UI thread.
663     */
664    @Override
665    public void registerRecentFolderObserver(DataSetObserver obs) {
666        mRecentFolderObservers.registerObserver(obs);
667    }
668
669    /**
670     * Removes a listener from receiving recent folder changes.
671     * Must happen in the UI thread.
672     */
673    @Override
674    public void unregisterRecentFolderObserver(DataSetObserver obs) {
675        mRecentFolderObservers.unregisterObserver(obs);
676    }
677
678    @Override
679    public RecentFolderList getRecentFolders() {
680        return mRecentFolderList;
681    }
682
683    // TODO(mindyp): set this up to store a copy of the folder as a transient
684    // field in the account.
685    @Override
686    public void loadAccountInbox() {
687        restartOptionalLoader(LOADER_ACCOUNT_INBOX);
688    }
689
690    /**
691     * Marks the {@link #mFolderChanged} value if the newFolder is different from the existing
692     * {@link #mFolder}. This should be called immediately <b>before</b> assigning newFolder to
693     * mFolder.
694     * @param newFolder
695     */
696    private final void setHasFolderChanged(final Folder newFolder) {
697        // We should never try to assign a null folder. But in the rare event that we do, we should
698        // only set the bit when we have a valid folder, and null is not valid.
699        if (newFolder == null) {
700            return;
701        }
702        // If the previous folder was null, or if the two folders represent different data, then we
703        // consider that the folder has changed.
704        if (mFolder == null || !newFolder.uri.equals(mFolder.uri)) {
705            mFolderChanged = true;
706        }
707    }
708
709    /**
710     * Sets the current folder if it is different from the object provided here. This method does
711     * NOT notify the folder observers that a change has happened. Observers are notified when we
712     * get an updated folder from the loaders, which will happen as a consequence of this method
713     * (since this method starts/restarts the loaders).
714     * @param folder The folder to assign
715     */
716    private void updateFolder(Folder folder) {
717        if (folder == null || !folder.isInitialized()) {
718            LogUtils.e(LOG_TAG, new Error(), "AAC.setFolder(%s): Bad input", folder);
719            return;
720        }
721        if (folder.equals(mFolder)) {
722            LogUtils.d(LOG_TAG, "AAC.setFolder(%s): Input matches mFolder", folder);
723            return;
724        }
725        final boolean wasNull = mFolder == null;
726        LogUtils.d(LOG_TAG, "AbstractActivityController.setFolder(%s)", folder.name);
727        final LoaderManager lm = mActivity.getLoaderManager();
728        // updateFolder is called from AAC.onLoadFinished() on folder changes.  We need to
729        // ensure that the folder is different from the previous folder before marking the
730        // folder changed.
731        setHasFolderChanged(folder);
732        mFolder = folder;
733
734        // We do not need to notify folder observers yet. Instead we start the loaders and
735        // when the load finishes, we will get an updated folder. Then, we notify the
736        // folderObservers in onLoadFinished.
737        mActionBarView.setFolder(mFolder);
738
739        // Only when we switch from one folder to another do we want to restart the
740        // folder and conversation list loaders (to trigger onCreateLoader).
741        // The first time this runs when the activity is [re-]initialized, we want to re-use the
742        // previous loader's instance and data upon configuration change (e.g. rotation).
743        // If there was not already an instance of the loader, init it.
744        if (lm.getLoader(LOADER_FOLDER_CURSOR) == null) {
745            lm.initLoader(LOADER_FOLDER_CURSOR, null, this);
746        } else {
747            lm.restartLoader(LOADER_FOLDER_CURSOR, null, this);
748        }
749        // In this case, we are starting from no folder, which would occur
750        // the first time the app was launched or on orientation changes.
751        // We want to attach to an existing loader, if available.
752        if (wasNull || lm.getLoader(LOADER_CONVERSATION_LIST) == null) {
753            lm.initLoader(LOADER_CONVERSATION_LIST, null, mListCursorCallbacks);
754        } else {
755            // However, if there was an existing folder AND we have changed
756            // folders, we want to restart the loader to get the information
757            // for the newly selected folder
758            lm.destroyLoader(LOADER_CONVERSATION_LIST);
759            lm.initLoader(LOADER_CONVERSATION_LIST, null, mListCursorCallbacks);
760        }
761    }
762
763    @Override
764    public Folder getFolder() {
765        return mFolder;
766    }
767
768    @Override
769    public Folder getHierarchyFolder() {
770        return mFolderListFolder;
771    }
772
773    @Override
774    public void setHierarchyFolder(Folder folder) {
775        mFolderListFolder = folder;
776    }
777
778    @Override
779    public void onActivityResult(int requestCode, int resultCode, Intent data) {
780        switch (requestCode) {
781            case ADD_ACCOUNT_REQUEST_CODE:
782                // We were waiting for the user to create an account
783                if (resultCode == Activity.RESULT_OK) {
784                    // restart the loader to get the updated list of accounts
785                    mActivity.getLoaderManager().initLoader(
786                            LOADER_ACCOUNT_CURSOR, null, this);
787                } else {
788                    // The user failed to create an account, just exit the app
789                    mActivity.finish();
790                }
791                break;
792            case REAUTHENTICATE_REQUEST_CODE:
793                if (resultCode == Activity.RESULT_OK) {
794                    // The user successfully authenticated, attempt to refresh the list
795                    final Uri refreshUri = mFolder != null ? mFolder.refreshUri : null;
796                    if (refreshUri != null) {
797                        startAsyncRefreshTask(refreshUri);
798                    }
799                }
800                break;
801        }
802    }
803
804    /**
805     * Inform the conversation cursor that there has been a visibility change.
806     * @param visible
807     */
808    protected synchronized void informCursorVisiblity(boolean visible) {
809        if (mConversationListCursor != null) {
810            Utils.setConversationCursorVisibility(mConversationListCursor, visible, mFolderChanged);
811            // We have informed the cursor. Subsequent visibility changes should not tell it that
812            // the folder has changed.
813            mFolderChanged = false;
814        }
815    }
816
817    @Override
818    public void onConversationListVisibilityChanged(boolean visible) {
819        informCursorVisiblity(visible);
820    }
821
822    /**
823     * Called when a conversation is visible. Child classes must call the super class implementation
824     * before performing local computation.
825     */
826    @Override
827    public void onConversationVisibilityChanged(boolean visible) {
828    }
829
830    @Override
831    public boolean onCreate(Bundle savedState) {
832        initializeActionBar();
833        // Allow shortcut keys to function for the ActionBar and menus.
834        mActivity.setDefaultKeyMode(Activity.DEFAULT_KEYS_SHORTCUT);
835        mResolver = mActivity.getContentResolver();
836        mNewEmailReceiver = new SuppressNotificationReceiver();
837        mRecentFolderList.initialize(mActivity);
838        mVeiledMatcher.initialize(this);
839
840        // All the individual UI components listen for ViewMode changes. This
841        // simplifies the amount of logic in the AbstractActivityController, but increases the
842        // possibility of timing-related bugs.
843        mViewMode.addListener(this);
844        mPagerController = new ConversationPagerController(mActivity, this);
845        mToastBar = (ActionableToastBar) mActivity.findViewById(R.id.toast_bar);
846        attachActionBar();
847        FolderSelectionDialog.setDialogDismissed();
848
849        final Intent intent = mActivity.getIntent();
850        // Immediately handle a clean launch with intent, and any state restoration
851        // that does not rely on restored fragments or loader data
852        // any state restoration that relies on those can be done later in
853        // onRestoreInstanceState, once fragments are up and loader data is re-delivered
854        if (savedState != null) {
855            if (savedState.containsKey(SAVED_ACCOUNT)) {
856                setAccount((Account) savedState.getParcelable(SAVED_ACCOUNT));
857            }
858            if (savedState.containsKey(SAVED_FOLDER)) {
859                final Folder folder = savedState.getParcelable(SAVED_FOLDER);
860                final String query = savedState.getString(SAVED_QUERY, null);
861                setListContext(folder, query);
862            }
863            if (savedState.containsKey(SAVED_ACTION)) {
864                mDialogAction = savedState.getInt(SAVED_ACTION);
865            }
866            mDialogFromSelectedSet = savedState.getBoolean(SAVED_ACTION_FROM_SELECTED, false);
867            mViewMode.handleRestore(savedState);
868        } else if (intent != null) {
869            handleIntent(intent);
870        }
871        // Create the accounts loader; this loads the account switch spinner.
872        mActivity.getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this);
873        return true;
874    }
875
876    @Override
877    public void onStart() {
878        mSafeToModifyFragments = true;
879
880        NotificationActionUtils.registerUndoNotificationObserver(mUndoNotificationObserver);
881    }
882
883    @Override
884    public void onRestart() {
885        DialogFragment fragment = (DialogFragment)
886                mFragmentManager.findFragmentByTag(SYNC_ERROR_DIALOG_FRAGMENT_TAG);
887        if (fragment != null) {
888            fragment.dismiss();
889        }
890        // When the user places the app in the background by pressing "home",
891        // dismiss the toast bar. However, since there is no way to determine if
892        // home was pressed, just dismiss any existing toast bar when restarting
893        // the app.
894        if (mToastBar != null) {
895            mToastBar.hide(false);
896        }
897    }
898
899    @Override
900    public Dialog onCreateDialog(int id, Bundle bundle) {
901        return null;
902    }
903
904    @Override
905    public final boolean onCreateOptionsMenu(Menu menu) {
906        final MenuInflater inflater = mActivity.getMenuInflater();
907        inflater.inflate(mActionBarView.getOptionsMenuId(), menu);
908        mActionBarView.onCreateOptionsMenu(menu);
909        return true;
910    }
911
912    @Override
913    public final boolean onKeyDown(int keyCode, KeyEvent event) {
914        return false;
915    }
916
917    public abstract boolean doesActionChangeConversationListVisibility(int action);
918
919    @Override
920    public final boolean onOptionsItemSelected(MenuItem item) {
921        final int id = item.getItemId();
922        LogUtils.d(LOG_TAG, "AbstractController.onOptionsItemSelected(%d) called.", id);
923        boolean handled = true;
924        final Collection<Conversation> target = Conversation.listOf(mCurrentConversation);
925        final Settings settings = (mAccount == null) ? null : mAccount.settings;
926        // The user is choosing a new action; commit whatever they had been
927        // doing before. Don't animate if we are launching a new screen.
928        commitDestructiveActions(!doesActionChangeConversationListVisibility(id));
929        switch (id) {
930            case R.id.archive: {
931                final boolean showDialog = (settings != null && settings.confirmArchive);
932                confirmAndDelete(id, target, showDialog, R.plurals.confirm_archive_conversation);
933                break;
934            }
935            case R.id.remove_folder:
936                delete(R.id.remove_folder, target,
937                        getDeferredRemoveFolder(target, mFolder, true, false, true));
938                break;
939            case R.id.delete: {
940                final boolean showDialog = (settings != null && settings.confirmDelete);
941                confirmAndDelete(id, target, showDialog, R.plurals.confirm_delete_conversation);
942                break;
943            }
944            case R.id.discard_drafts: {
945                final boolean showDialog = (settings != null && settings.confirmDelete);
946                confirmAndDelete(id, target, showDialog,
947                        R.plurals.confirm_discard_drafts_conversation);
948                break;
949            }
950            case R.id.mark_important:
951                updateConversation(Conversation.listOf(mCurrentConversation),
952                        ConversationColumns.PRIORITY, UIProvider.ConversationPriority.HIGH);
953                break;
954            case R.id.mark_not_important:
955                if (mFolder != null && mFolder.isImportantOnly()) {
956                    delete(R.id.mark_not_important, target,
957                            getDeferredAction(R.id.mark_not_important, target, false));
958                } else {
959                    updateConversation(Conversation.listOf(mCurrentConversation),
960                            ConversationColumns.PRIORITY, UIProvider.ConversationPriority.LOW);
961                }
962                break;
963            case R.id.mute:
964                delete(R.id.mute, target, getDeferredAction(R.id.mute, target, false));
965                break;
966            case R.id.report_spam:
967                delete(R.id.report_spam, target,
968                        getDeferredAction(R.id.report_spam, target, false));
969                break;
970            case R.id.mark_not_spam:
971                // Currently, since spam messages are only shown in list with
972                // other spam messages,
973                // marking a message not as spam is a destructive action
974                delete(R.id.mark_not_spam, target,
975                        getDeferredAction(R.id.mark_not_spam, target, false));
976                break;
977            case R.id.report_phishing:
978                delete(R.id.report_phishing, target,
979                        getDeferredAction(R.id.report_phishing, target, false));
980                break;
981            case android.R.id.home:
982                onUpPressed();
983                break;
984            case R.id.compose:
985                ComposeActivity.compose(mActivity.getActivityContext(), mAccount);
986                break;
987            case R.id.show_all_folders:
988                showFolderList();
989                break;
990            case R.id.refresh:
991                requestFolderRefresh();
992                break;
993            case R.id.settings:
994                Utils.showSettings(mActivity.getActivityContext(), mAccount);
995                break;
996            case R.id.folder_options:
997                Utils.showFolderSettings(mActivity.getActivityContext(), mAccount, mFolder);
998                break;
999            case R.id.help_info_menu_item:
1000                Utils.showHelp(mActivity.getActivityContext(), mAccount, getHelpContext());
1001                break;
1002            case R.id.feedback_menu_item:
1003                Utils.sendFeedback(mActivity, mAccount, false);
1004                break;
1005            case R.id.manage_folders_item:
1006                Utils.showManageFolder(mActivity.getActivityContext(), mAccount);
1007                break;
1008            case R.id.move_to:
1009                /* fall through */
1010            case R.id.change_folder:
1011                final FolderSelectionDialog dialog = FolderSelectionDialog.getInstance(
1012                        mActivity.getActivityContext(), mAccount, this,
1013                        Conversation.listOf(mCurrentConversation), false, mFolder,
1014                        id == R.id.move_to);
1015                if (dialog != null) {
1016                    dialog.show();
1017                }
1018                break;
1019            default:
1020                handled = false;
1021                break;
1022        }
1023        return handled;
1024    }
1025
1026    @Override
1027    public final boolean onUpPressed() {
1028        for (UpOrBackHandler h : mUpOrBackHandlers) {
1029            if (h.onUpPressed()) {
1030                return true;
1031            }
1032        }
1033        return handleUpPress();
1034    }
1035
1036    @Override
1037    public final boolean onBackPressed() {
1038        for (UpOrBackHandler h : mUpOrBackHandlers) {
1039            if (h.onBackPressed()) {
1040                return true;
1041            }
1042        }
1043        return handleBackPress();
1044    }
1045
1046    protected abstract boolean handleBackPress();
1047    protected abstract boolean handleUpPress();
1048
1049    @Override
1050    public void addUpOrBackHandler(UpOrBackHandler handler) {
1051        if (mUpOrBackHandlers.contains(handler)) {
1052            return;
1053        }
1054        mUpOrBackHandlers.addFirst(handler);
1055    }
1056
1057    @Override
1058    public void removeUpOrBackHandler(UpOrBackHandler handler) {
1059        mUpOrBackHandlers.remove(handler);
1060    }
1061
1062    @Override
1063    public void updateConversation(Collection<Conversation> target, ContentValues values) {
1064        mConversationListCursor.updateValues(mContext, target, values);
1065        refreshConversationList();
1066    }
1067
1068    @Override
1069    public void updateConversation(Collection <Conversation> target, String columnName,
1070            boolean value) {
1071        mConversationListCursor.updateBoolean(mContext, target, columnName, value);
1072        refreshConversationList();
1073    }
1074
1075    @Override
1076    public void updateConversation(Collection <Conversation> target, String columnName,
1077            int value) {
1078        mConversationListCursor.updateInt(mContext, target, columnName, value);
1079        refreshConversationList();
1080    }
1081
1082    @Override
1083    public void updateConversation(Collection <Conversation> target, String columnName,
1084            String value) {
1085        mConversationListCursor.updateString(mContext, target, columnName, value);
1086        refreshConversationList();
1087    }
1088
1089    @Override
1090    public void markConversationMessagesUnread(final Conversation conv,
1091            final Set<Uri> unreadMessageUris, final byte[] originalConversationInfo) {
1092        // The only caller of this method is the conversation view, from where marking unread should
1093        // *always* take you back to list mode.
1094        showConversation(null);
1095
1096        // locally mark conversation unread (the provider is supposed to propagate message unread
1097        // to conversation unread)
1098        conv.read = false;
1099        if (mConversationListCursor == null) {
1100            LogUtils.d(LOG_TAG, "markConversationMessagesUnread(id=%d), deferring", conv.id);
1101
1102            mConversationListLoadFinishedCallbacks.add(new LoadFinishedCallback() {
1103                @Override
1104                public void onLoadFinished() {
1105                    doMarkConversationMessagesUnread(conv, unreadMessageUris,
1106                            originalConversationInfo);
1107                }
1108            });
1109        } else {
1110            LogUtils.d(LOG_TAG, "markConversationMessagesUnread(id=%d), performing", conv.id);
1111            doMarkConversationMessagesUnread(conv, unreadMessageUris, originalConversationInfo);
1112        }
1113    }
1114
1115    private void doMarkConversationMessagesUnread(Conversation conv, Set<Uri> unreadMessageUris,
1116            byte[] originalConversationInfo) {
1117        // Only do a granular 'mark unread' if a subset of messages are unread
1118        final int unreadCount = (unreadMessageUris == null) ? 0 : unreadMessageUris.size();
1119        final int numMessages = conv.getNumMessages();
1120        final boolean subsetIsUnread = (numMessages > 1 && unreadCount > 0
1121                && unreadCount < numMessages);
1122
1123        LogUtils.d(LOG_TAG, "markConversationMessagesUnread(id=%d (subject=%))"
1124                + ", numMessages=%d, unreadCount=%d, subsetIsUnread=%b",
1125                conv.id, conv.subject, numMessages, unreadCount, subsetIsUnread);
1126        if (!subsetIsUnread) {
1127            // Conversations are neither marked read, nor viewed, and we don't want to show
1128            // the next conversation.
1129            LogUtils.d(LOG_TAG, ". . doing full mark unread");
1130            markConversationsRead(Collections.singletonList(conv), false, false, false);
1131        } else {
1132            if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
1133                final ConversationInfo info = ConversationInfo.fromBlob(originalConversationInfo);
1134                LogUtils.d(LOG_TAG, ". . doing subset mark unread, originalConversationInfo = %s",
1135                        info);
1136            }
1137            mConversationListCursor.setConversationColumn(conv.uri, ConversationColumns.READ, 0);
1138
1139            // Locally update conversation's conversationInfo to revert to original version
1140            if (originalConversationInfo != null) {
1141                mConversationListCursor.setConversationColumn(conv.uri,
1142                        ConversationColumns.CONVERSATION_INFO, originalConversationInfo);
1143            }
1144
1145            // applyBatch with each CPO as an UPDATE op on each affected message uri
1146            final ArrayList<ContentProviderOperation> ops = Lists.newArrayList();
1147            String authority = null;
1148            for (Uri messageUri : unreadMessageUris) {
1149                if (authority == null) {
1150                    authority = messageUri.getAuthority();
1151                }
1152                ops.add(ContentProviderOperation.newUpdate(messageUri)
1153                        .withValue(UIProvider.MessageColumns.READ, 0)
1154                        .build());
1155                LogUtils.d(LOG_TAG, ". . Adding op: read=0, uri=%s", messageUri);
1156            }
1157            LogUtils.d(LOG_TAG, ". . operations = %s", ops);
1158            new ContentProviderTask() {
1159                @Override
1160                protected void onPostExecute(Result result) {
1161                    // TODO: handle errors?
1162                }
1163            }.run(mResolver, authority, ops);
1164        }
1165    }
1166
1167    @Override
1168    public void markConversationsRead(final Collection<Conversation> targets, final boolean read,
1169            final boolean viewed) {
1170        if (mConversationListCursor == null) {
1171            if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
1172                LogUtils.d(LOG_TAG, "markConversationsRead(targets=%s), deferring",
1173                        targets.toArray());
1174            }
1175            mConversationListLoadFinishedCallbacks.add(new LoadFinishedCallback() {
1176                @Override
1177                public void onLoadFinished() {
1178                    markConversationsRead(targets, read, viewed, true);
1179                }
1180            });
1181        } else {
1182            // We want to show the next conversation if we are marking unread.
1183            markConversationsRead(targets, read, viewed, true);
1184        }
1185    }
1186
1187    private void markConversationsRead(final Collection<Conversation> targets, final boolean read,
1188            final boolean markViewed, final boolean showNext) {
1189        LogUtils.d(LOG_TAG, "performing markConversationsRead");
1190        // Auto-advance if requested and the current conversation is being marked unread
1191        if (showNext && !read) {
1192            final Runnable operation = new Runnable() {
1193                @Override
1194                public void run() {
1195                    markConversationsRead(targets, read, markViewed, showNext);
1196                }
1197            };
1198
1199            if (!showNextConversation(targets, operation)) {
1200                // This method will be called again if the user selects an autoadvance option
1201                return;
1202            }
1203        }
1204
1205        final int size = targets.size();
1206        final List<ConversationOperation> opList = new ArrayList<ConversationOperation>(size);
1207        for (final Conversation target : targets) {
1208            final ContentValues value = new ContentValues();
1209            value.put(ConversationColumns.READ, read);
1210
1211            // We never want to mark unseen here, but we do want to mark it seen
1212            if (read || markViewed) {
1213                value.put(ConversationColumns.SEEN, Boolean.TRUE);
1214            }
1215
1216            // The mark read/unread/viewed operations do not show an undo bar
1217            value.put(ConversationOperations.Parameters.SUPPRESS_UNDO, true);
1218            if (markViewed) {
1219                value.put(ConversationColumns.VIEWED, true);
1220            }
1221            final ConversationInfo info = target.conversationInfo;
1222            if (info != null) {
1223                boolean changed = info.markRead(read);
1224                if (changed) {
1225                    value.put(ConversationColumns.CONVERSATION_INFO, info.toBlob());
1226                }
1227            }
1228            opList.add(mConversationListCursor.getOperationForConversation(
1229                    target, ConversationOperation.UPDATE, value));
1230            // Update the local conversation objects so they immediately change state.
1231            target.read = read;
1232            if (markViewed) {
1233                target.markViewed();
1234            }
1235        }
1236        mConversationListCursor.updateBulkValues(mContext, opList);
1237    }
1238
1239    /**
1240     * Auto-advance to a different conversation if the currently visible conversation in
1241     * conversation mode is affected (deleted, marked unread, etc.).
1242     *
1243     * <p>Does nothing if outside of conversation mode.</p>
1244     *
1245     * @param target the set of conversations being deleted/marked unread
1246     */
1247    @Override
1248    public void showNextConversation(final Collection<Conversation> target) {
1249        showNextConversation(target, null);
1250    }
1251
1252    /**
1253     * Auto-advance to a different conversation if the currently visible conversation in
1254     * conversation mode is affected (deleted, marked unread, etc.).
1255     *
1256     * <p>Does nothing if outside of conversation mode.</p>
1257     *
1258     * @param target the set of conversations being deleted/marked unread
1259     * @param operation if auto-advance setting is unset, this operation is run after the user
1260     *        is prompted to select a setting.
1261     * @return <code>false</code> if we aborted because the user has not yet specified a default
1262     *         action, <code>true</code> otherwise
1263     */
1264    private boolean showNextConversation(final Collection<Conversation> target,
1265            final Runnable operation) {
1266        final int viewMode = mViewMode.getMode();
1267        final boolean currentConversationInView = (viewMode == ViewMode.CONVERSATION
1268                || viewMode == ViewMode.SEARCH_RESULTS_CONVERSATION)
1269                && Conversation.contains(target, mCurrentConversation);
1270
1271        if (currentConversationInView) {
1272            final int autoAdvanceSetting = mAccount.settings.getAutoAdvanceSetting();
1273
1274            if (autoAdvanceSetting == AutoAdvance.UNSET && mIsTablet) {
1275                displayAutoAdvanceDialogAndPerformAction(operation);
1276                return false;
1277            } else {
1278                // If we don't have one set, but we're here, just take the default
1279                final int autoAdvance = (autoAdvanceSetting == AutoAdvance.UNSET) ?
1280                        AutoAdvance.DEFAULT : autoAdvanceSetting;
1281
1282                final Conversation next = mTracker.getNextConversation(autoAdvance, target);
1283                LogUtils.d(LOG_TAG, "showNextConversation: showing %s next.", next);
1284                showConversation(next);
1285                return true;
1286            }
1287        }
1288
1289        return true;
1290    }
1291
1292    /**
1293     * Displays a the auto-advance dialog, and when the user makes a selection, the preference is
1294     * stored, and the specified operation is run.
1295     */
1296    private void displayAutoAdvanceDialogAndPerformAction(final Runnable operation) {
1297        final String[] autoAdvanceDisplayOptions =
1298                mContext.getResources().getStringArray(R.array.prefEntries_autoAdvance);
1299        final String[] autoAdvanceOptionValues =
1300                mContext.getResources().getStringArray(R.array.prefValues_autoAdvance);
1301
1302        final String defaultValue = mContext.getString(R.string.prefDefault_autoAdvance);
1303        int initialIndex = 0;
1304        for (int i = 0; i < autoAdvanceOptionValues.length; i++) {
1305            if (defaultValue.equals(autoAdvanceOptionValues[i])) {
1306                initialIndex = i;
1307                break;
1308            }
1309        }
1310
1311        final DialogInterface.OnClickListener listClickListener =
1312                new DialogInterface.OnClickListener() {
1313                    @Override
1314                    public void onClick(DialogInterface dialog, int whichItem) {
1315                        final String autoAdvanceValue = autoAdvanceOptionValues[whichItem];
1316                        final int autoAdvanceValueInt =
1317                                UIProvider.AutoAdvance.getAutoAdvanceInt(autoAdvanceValue);
1318                        mAccount.settings.setAutoAdvanceSetting(autoAdvanceValueInt);
1319
1320                        // Save the user's setting
1321                        final ContentValues values = new ContentValues(1);
1322                        values.put(AccountColumns.SettingsColumns.AUTO_ADVANCE, autoAdvanceValue);
1323
1324                        final ContentResolver resolver = mContext.getContentResolver();
1325                        resolver.update(mAccount.updateSettingsUri, values, null, null);
1326
1327                        // Dismiss the dialog, as clicking the items in the list doesn't close the
1328                        // dialog.
1329                        dialog.dismiss();
1330                        if (operation != null) {
1331                            operation.run();
1332                        }
1333                    }
1334                };
1335
1336        new AlertDialog.Builder(mActivity.getActivityContext()).setTitle(
1337                R.string.auto_advance_help_title)
1338                .setSingleChoiceItems(autoAdvanceDisplayOptions, initialIndex, listClickListener)
1339                .setPositiveButton(null, null)
1340                .create()
1341                .show();
1342    }
1343
1344    @Override
1345    public void starMessage(ConversationMessage msg, boolean starred) {
1346        if (msg.starred == starred) {
1347            return;
1348        }
1349
1350        msg.starred = starred;
1351
1352        // locally propagate the change to the owning conversation
1353        // (figure the provider will properly propagate the change when it commits it)
1354        //
1355        // when unstarring, only propagate the change if this was the only message starred
1356        final boolean conversationStarred = starred || msg.isConversationStarred();
1357        final Conversation conv = msg.getConversation();
1358        if (conversationStarred != conv.starred) {
1359            conv.starred = conversationStarred;
1360            mConversationListCursor.setConversationColumn(conv.uri,
1361                    ConversationColumns.STARRED, conversationStarred);
1362        }
1363
1364        final ContentValues values = new ContentValues(1);
1365        values.put(UIProvider.MessageColumns.STARRED, starred ? 1 : 0);
1366
1367        new ContentProviderTask.UpdateTask() {
1368            @Override
1369            protected void onPostExecute(Result result) {
1370                // TODO: handle errors?
1371            }
1372        }.run(mResolver, msg.uri, values, null /* selection*/, null /* selectionArgs */);
1373    }
1374
1375    private void requestFolderRefresh() {
1376        if (mFolder != null) {
1377            if (mAsyncRefreshTask != null) {
1378                mAsyncRefreshTask.cancel(true);
1379            }
1380            mAsyncRefreshTask = new AsyncRefreshTask(mContext, mFolder.refreshUri);
1381            mAsyncRefreshTask.execute();
1382        }
1383    }
1384
1385    /**
1386     * Confirm (based on user's settings) and delete a conversation from the conversation list and
1387     * from the database.
1388     * @param actionId the ID of the menu item that caused the delete: R.id.delete, R.id.archive...
1389     * @param target the conversations to act upon
1390     * @param showDialog true if a confirmation dialog is to be shown, false otherwise.
1391     * @param confirmResource the resource ID of the string that is shown in the confirmation dialog
1392     */
1393    private void confirmAndDelete(int actionId, final Collection<Conversation> target,
1394            boolean showDialog, int confirmResource) {
1395        if (showDialog) {
1396            makeDialogListener(actionId, false);
1397            final CharSequence message = Utils.formatPlural(mContext, confirmResource,
1398                    target.size());
1399            final ConfirmDialogFragment c = ConfirmDialogFragment.newInstance(message);
1400            c.displayDialog(mActivity.getFragmentManager());
1401        } else {
1402            delete(0, target, getDeferredAction(actionId, target, false));
1403        }
1404    }
1405
1406    @Override
1407    public void delete(final int actionId, final Collection<Conversation> target,
1408            final DestructiveAction action) {
1409        // Order of events is critical! The Conversation View Fragment must be
1410        // notified of the next conversation with showConversation(next) *before* the
1411        // conversation list
1412        // fragment has a chance to delete the conversation, animating it away.
1413
1414        // Update the conversation fragment if the current conversation is
1415        // deleted.
1416        final Runnable operation = new Runnable() {
1417            @Override
1418            public void run() {
1419                delete(actionId, target, action);
1420            }
1421        };
1422
1423        if (!showNextConversation(target, operation)) {
1424            // This method will be called again if the user selects an autoadvance option
1425            return;
1426        }
1427
1428        // The conversation list deletes and performs the action if it exists.
1429        final ConversationListFragment convListFragment = getConversationListFragment();
1430        if (convListFragment != null) {
1431            LogUtils.d(LOG_TAG, "AAC.requestDelete: ListFragment is handling delete.");
1432            convListFragment.requestDelete(actionId, target, action);
1433            return;
1434        }
1435        // No visible UI element handled it on our behalf. Perform the action
1436        // ourself.
1437        action.performAction();
1438    }
1439
1440    /**
1441     * Requests that the action be performed and the UI state is updated to reflect the new change.
1442     * @param target
1443     * @param action
1444     */
1445    private void requestUpdate(final Collection<Conversation> target,
1446            final DestructiveAction action) {
1447        action.performAction();
1448        refreshConversationList();
1449    }
1450
1451    @Override
1452    public void onPrepareDialog(int id, Dialog dialog, Bundle bundle) {
1453        // TODO(viki): Auto-generated method stub
1454    }
1455
1456    @Override
1457    public boolean onPrepareOptionsMenu(Menu menu) {
1458        return mActionBarView.onPrepareOptionsMenu(menu);
1459    }
1460
1461    @Override
1462    public void onPause() {
1463        isLoaderInitialized = false;
1464        enableNotifications();
1465    }
1466
1467    @Override
1468    public void onResume() {
1469        // Register the receiver that will prevent the status receiver from
1470        // displaying its notification icon as long as we're running.
1471        // The SupressNotificationReceiver will block the broadcast if we're looking at the folder
1472        // that the notification was received for.
1473        disableNotifications();
1474
1475        mSafeToModifyFragments = true;
1476    }
1477
1478    @Override
1479    public void onSaveInstanceState(Bundle outState) {
1480        mViewMode.handleSaveInstanceState(outState);
1481        if (mAccount != null) {
1482            outState.putParcelable(SAVED_ACCOUNT, mAccount);
1483        }
1484        if (mFolder != null) {
1485            outState.putParcelable(SAVED_FOLDER, mFolder);
1486        }
1487        // If this is a search activity, let's store the search query term as well.
1488        if (ConversationListContext.isSearchResult(mConvListContext)) {
1489            outState.putString(SAVED_QUERY, mConvListContext.searchQuery);
1490        }
1491        if (mCurrentConversation != null && mViewMode.isConversationMode()) {
1492            outState.putParcelable(SAVED_CONVERSATION, mCurrentConversation);
1493        }
1494        if (!mSelectedSet.isEmpty()) {
1495            outState.putParcelable(SAVED_SELECTED_SET, mSelectedSet);
1496        }
1497        if (mToastBar.getVisibility() == View.VISIBLE) {
1498            outState.putParcelable(SAVED_TOAST_BAR_OP, mToastBar.getOperation());
1499        }
1500        final ConversationListFragment convListFragment = getConversationListFragment();
1501        if (convListFragment != null) {
1502            convListFragment.getAnimatedAdapter().onSaveInstanceState(outState);
1503        }
1504        // If there is a dialog being shown, save the state so we can create a listener for it.
1505        if (mDialogAction != -1) {
1506            outState.putInt(SAVED_ACTION, mDialogAction);
1507            outState.putBoolean(SAVED_ACTION_FROM_SELECTED, mDialogFromSelectedSet);
1508        }
1509        if (mDetachedConvUri != null) {
1510            outState.putParcelable(SAVED_DETACHED_CONV_URI, mDetachedConvUri);
1511        }
1512        mSafeToModifyFragments = false;
1513        outState.putParcelable(SAVED_HIERARCHICAL_FOLDER, mFolderListFolder);
1514    }
1515
1516    /**
1517     * @see #mSafeToModifyFragments
1518     */
1519    protected boolean safeToModifyFragments() {
1520        return mSafeToModifyFragments;
1521    }
1522
1523    @Override
1524    public void onSearchRequested(String query) {
1525        Intent intent = new Intent();
1526        intent.setAction(Intent.ACTION_SEARCH);
1527        intent.putExtra(ConversationListContext.EXTRA_SEARCH_QUERY, query);
1528        intent.putExtra(Utils.EXTRA_ACCOUNT, mAccount);
1529        intent.setComponent(mActivity.getComponentName());
1530        mActionBarView.collapseSearch();
1531        mActivity.startActivity(intent);
1532    }
1533
1534    @Override
1535    public void onStop() {
1536        if (mEnableShareIntents != null) {
1537            mEnableShareIntents.cancel(true);
1538        }
1539
1540        NotificationActionUtils.unregisterUndoNotificationObserver(mUndoNotificationObserver);
1541    }
1542
1543    @Override
1544    public void onDestroy() {
1545        // stop listening to the cursor on e.g. configuration changes
1546        if (mConversationListCursor != null) {
1547            mConversationListCursor.removeListener(this);
1548        }
1549        // unregister the ViewPager's observer on the conversation cursor
1550        mPagerController.onDestroy();
1551        mActionBarView.onDestroy();
1552        mRecentFolderList.destroy();
1553        mDestroyed = true;
1554    }
1555
1556    /**
1557     * Set the Action Bar icon according to the mode. The Action Bar icon can contain a back button
1558     * or not. The individual controller is responsible for changing the icon based on the mode.
1559     */
1560    protected abstract void resetActionBarIcon();
1561
1562    /**
1563     * {@inheritDoc} Subclasses must override this to listen to mode changes
1564     * from the ViewMode. Subclasses <b>must</b> call the parent's
1565     * onViewModeChanged since the parent will handle common state changes.
1566     */
1567    @Override
1568    public void onViewModeChanged(int newMode) {
1569        // When we step away from the conversation mode, we don't have a current conversation
1570        // anymore. Let's blank it out so clients calling getCurrentConversation are not misled.
1571        if (!ViewMode.isConversationMode(newMode)) {
1572            setCurrentConversation(null);
1573        }
1574        // If the viewmode is not set, preserve existing icon.
1575        if (newMode != ViewMode.UNKNOWN) {
1576            resetActionBarIcon();
1577        }
1578    }
1579
1580    public void disablePagerUpdates() {
1581        mPagerController.stopListening();
1582    }
1583
1584    public boolean isDestroyed() {
1585        return mDestroyed;
1586    }
1587
1588    @Override
1589    public void commitDestructiveActions(boolean animate) {
1590        ConversationListFragment fragment = getConversationListFragment();
1591        if (fragment != null) {
1592            fragment.commitDestructiveActions(animate);
1593        }
1594    }
1595
1596    @Override
1597    public void onWindowFocusChanged(boolean hasFocus) {
1598        final ConversationListFragment convList = getConversationListFragment();
1599        // hasFocus already ensures that the window is in focus, so we don't need to call
1600        // AAC.isFragmentVisible(convList) here.
1601        if (hasFocus && convList != null && convList.isVisible()) {
1602            // The conversation list is visible.
1603            informCursorVisiblity(true);
1604        }
1605    }
1606
1607    /**
1608     * Set the account, and carry out all the account-related changes that rely on this.
1609     * @param account
1610     */
1611    private void setAccount(Account account) {
1612        if (account == null) {
1613            LogUtils.w(LOG_TAG, new Error(),
1614                    "AAC ignoring null (presumably invalid) account restoration");
1615            return;
1616        }
1617        LogUtils.d(LOG_TAG, "AbstractActivityController.setAccount(): account = %s", account.uri);
1618        mAccount = account;
1619        // Only change AAC state here. Do *not* modify any other object's state. The object
1620        // should listen on account changes.
1621        restartOptionalLoader(LOADER_RECENT_FOLDERS);
1622        mActivity.invalidateOptionsMenu();
1623        disableNotificationsOnAccountChange(mAccount);
1624        restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR);
1625        // The Mail instance can be null during test runs.
1626        final MailAppProvider instance = MailAppProvider.getInstance();
1627        if (instance != null) {
1628            instance.setLastViewedAccount(mAccount.uri.toString());
1629        }
1630        if (account.settings == null) {
1631            LogUtils.w(LOG_TAG, new Error(), "AAC ignoring account with null settings.");
1632            return;
1633        }
1634        mAccountObservers.notifyChanged();
1635        perhapsEnterWaitMode();
1636    }
1637
1638    /**
1639     * Restore the state from the previous bundle. Subclasses should call this
1640     * method from the parent class, since it performs important UI
1641     * initialization.
1642     *
1643     * @param savedState
1644     */
1645    @Override
1646    public void onRestoreInstanceState(Bundle savedState) {
1647        mDetachedConvUri = savedState.getParcelable(SAVED_DETACHED_CONV_URI);
1648        if (savedState.containsKey(SAVED_CONVERSATION)) {
1649            // Open the conversation.
1650            final Conversation conversation = savedState.getParcelable(SAVED_CONVERSATION);
1651            if (conversation != null && conversation.position < 0) {
1652                // Set the position to 0 on this conversation, as we don't know where it is
1653                // in the list
1654                conversation.position = 0;
1655            }
1656            showConversation(conversation);
1657        }
1658
1659        if (savedState.containsKey(SAVED_TOAST_BAR_OP)) {
1660            ToastBarOperation op = savedState.getParcelable(SAVED_TOAST_BAR_OP);
1661            if (op != null) {
1662                if (op.getType() == ToastBarOperation.UNDO) {
1663                    onUndoAvailable(op);
1664                } else if (op.getType() == ToastBarOperation.ERROR) {
1665                    onError(mFolder, true);
1666                }
1667            }
1668        }
1669        mFolderListFolder = savedState.getParcelable(SAVED_HIERARCHICAL_FOLDER);
1670        final ConversationListFragment convListFragment = getConversationListFragment();
1671        if (convListFragment != null) {
1672            convListFragment.getAnimatedAdapter().onRestoreInstanceState(savedState);
1673        }
1674        /*
1675         * Restore the state of selected conversations. This needs to be done after the correct mode
1676         * is set and the action bar is fully initialized. If not, several key pieces of state
1677         * information will be missing, and the split views may not be initialized correctly.
1678         */
1679        restoreSelectedConversations(savedState);
1680        // Order is important!!!
1681        // The dialog listener needs to happen *after* the selected set is restored.
1682
1683        // If there has been an orientation change, and we need to recreate the listener for the
1684        // confirm dialog fragment (delete/archive/...), then do it here.
1685        if (mDialogAction != -1) {
1686            makeDialogListener(mDialogAction, mDialogFromSelectedSet);
1687        }
1688    }
1689
1690    /**
1691     * Handle an intent to open the app. This method is called only when there is no saved state,
1692     * so we need to set state that wasn't set before. It is correct to change the viewmode here
1693     * since it has not been previously set.
1694     * @param intent
1695     */
1696    private void handleIntent(Intent intent) {
1697        boolean handled = false;
1698        if (Intent.ACTION_VIEW.equals(intent.getAction())) {
1699            if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) {
1700                setAccount(Account.newinstance(intent.getStringExtra(Utils.EXTRA_ACCOUNT)));
1701            }
1702            if (mAccount == null) {
1703                return;
1704            }
1705            final boolean isConversationMode = intent.hasExtra(Utils.EXTRA_CONVERSATION);
1706            if (isConversationMode && mViewMode.getMode() == ViewMode.UNKNOWN) {
1707                mViewMode.enterConversationMode();
1708            } else {
1709                mViewMode.enterConversationListMode();
1710            }
1711            final Folder folder = intent.getParcelableExtra(Utils.EXTRA_FOLDER);
1712            if (folder != null) {
1713                onFolderChanged(folder);
1714                handled = true;
1715            }
1716
1717            if (isConversationMode) {
1718                // Open the conversation.
1719                LogUtils.d(LOG_TAG, "SHOW THE CONVERSATION at %s",
1720                        intent.getParcelableExtra(Utils.EXTRA_CONVERSATION));
1721                final Conversation conversation =
1722                        intent.getParcelableExtra(Utils.EXTRA_CONVERSATION);
1723                if (conversation != null && conversation.position < 0) {
1724                    // Set the position to 0 on this conversation, as we don't know where it is
1725                    // in the list
1726                    conversation.position = 0;
1727                }
1728                showConversation(conversation);
1729                handled = true;
1730            }
1731
1732            if (!handled) {
1733                // We have an account, but nothing else: load the default inbox.
1734                loadAccountInbox();
1735            }
1736        } else if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
1737            if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) {
1738                mHaveSearchResults = false;
1739                // Save this search query for future suggestions.
1740                final String query = intent.getStringExtra(SearchManager.QUERY);
1741                final String authority = mContext.getString(R.string.suggestions_authority);
1742                final SearchRecentSuggestions suggestions = new SearchRecentSuggestions(
1743                        mContext, authority, SuggestionsProvider.MODE);
1744                suggestions.saveRecentQuery(query, null);
1745                setAccount((Account) intent.getParcelableExtra(Utils.EXTRA_ACCOUNT));
1746                fetchSearchFolder(intent);
1747                if (shouldEnterSearchConvMode()) {
1748                    mViewMode.enterSearchResultsConversationMode();
1749                } else {
1750                    mViewMode.enterSearchResultsListMode();
1751                }
1752            } else {
1753                LogUtils.e(LOG_TAG, "Missing account extra from search intent.  Finishing");
1754                mActivity.finish();
1755            }
1756        }
1757        if (mAccount != null) {
1758            restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR);
1759        }
1760    }
1761
1762    /**
1763     * Returns true if we should enter conversation mode with search.
1764     */
1765    protected final boolean shouldEnterSearchConvMode() {
1766        return mHaveSearchResults && Utils.showTwoPaneSearchResults(mActivity.getActivityContext());
1767    }
1768
1769    /**
1770     * Copy any selected conversations stored in the saved bundle into our selection set,
1771     * triggering {@link ConversationSetObserver} callbacks as our selection set changes.
1772     *
1773     */
1774    private final void restoreSelectedConversations(Bundle savedState) {
1775        if (savedState == null) {
1776            mSelectedSet.clear();
1777            return;
1778        }
1779        final ConversationSelectionSet selectedSet = savedState.getParcelable(SAVED_SELECTED_SET);
1780        if (selectedSet == null || selectedSet.isEmpty()) {
1781            mSelectedSet.clear();
1782            return;
1783        }
1784
1785        // putAll will take care of calling our registered onSetPopulated method
1786        mSelectedSet.putAll(selectedSet);
1787    }
1788
1789    @Override
1790    public SubjectDisplayChanger getSubjectDisplayChanger() {
1791        return mActionBarView;
1792    }
1793
1794    private final void showConversation(Conversation conversation) {
1795        showConversation(conversation, false /* inLoaderCallbacks */);
1796    }
1797
1798    /**
1799     * Show the conversation provided in the arguments. It is safe to pass a null conversation
1800     * object, which is a signal to back out of conversation view mode.
1801     * Child classes must call super.showConversation() <b>before</b> their own implementations.
1802     * @param conversation
1803     * @param inLoaderCallbacks true if the method is called as a result of
1804     * {@link #onLoadFinished(Loader, Cursor)}
1805     */
1806    protected void showConversation(Conversation conversation, boolean inLoaderCallbacks) {
1807        // Set the current conversation just in case it wasn't already set.
1808        setCurrentConversation(conversation);
1809        // Add the folder that we were viewing to the recent folders list.
1810        // TODO: this may need to be fine tuned.  If this is the signal that is indicating that
1811        // the list is shown to the user, this could fire in one pane if the user goes directly
1812        // to a conversation
1813        updateRecentFolderList();
1814    }
1815
1816    /**
1817     * Children can override this method, but they must call super.showWaitForInitialization().
1818     * {@inheritDoc}
1819     */
1820    @Override
1821    public void showWaitForInitialization() {
1822        mViewMode.enterWaitingForInitializationMode();
1823        mWaitFragment = WaitFragment.newInstance(mAccount);
1824    }
1825
1826    private void updateWaitMode() {
1827        final FragmentManager manager = mActivity.getFragmentManager();
1828        final WaitFragment waitFragment =
1829                (WaitFragment)manager.findFragmentByTag(TAG_WAIT);
1830        if (waitFragment != null) {
1831            waitFragment.updateAccount(mAccount);
1832        }
1833    }
1834
1835    /**
1836     * Remove the "Waiting for Initialization" fragment. Child classes are free to override this
1837     * method, though they must call the parent implementation <b>after</b> they do anything.
1838     */
1839    protected void hideWaitForInitialization() {
1840        mWaitFragment = null;
1841    }
1842
1843    /**
1844     * Use the instance variable and the wait fragment's tag to get the wait fragment.  This is
1845     * far superior to using the value of mWaitFragment, which might be invalid or might refer
1846     * to a fragment after it has been destroyed.
1847     * @return
1848     */
1849    protected final WaitFragment getWaitFragment() {
1850        final FragmentManager manager = mActivity.getFragmentManager();
1851        final WaitFragment waitFrag = (WaitFragment) manager.findFragmentByTag(TAG_WAIT);
1852        if (waitFrag != null) {
1853            // The Fragment Manager knows better, so use its instance.
1854            mWaitFragment = waitFrag;
1855        }
1856        return mWaitFragment;
1857    }
1858
1859    /**
1860     * Returns true if we are waiting for the account to sync, and cannot show any folders or
1861     * conversation for the current account yet.
1862     */
1863    private boolean inWaitMode() {
1864        final WaitFragment waitFragment = getWaitFragment();
1865        if (waitFragment != null) {
1866            final Account fragmentAccount = waitFragment.getAccount();
1867            return fragmentAccount != null && fragmentAccount.uri.equals(mAccount.uri) &&
1868                    mViewMode.getMode() == ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION;
1869        }
1870        return false;
1871    }
1872
1873    /**
1874     * Children can override this method, but they must call super.showConversationList().
1875     * {@inheritDoc}
1876     */
1877    @Override
1878    public void showConversationList(ConversationListContext listContext) {
1879    }
1880
1881    @Override
1882    public final void onConversationSelected(Conversation conversation, boolean inLoaderCallbacks) {
1883        // Only animate destructive actions if we are going to be showing the
1884        // conversation list when we show the next conversation.
1885        commitDestructiveActions(mIsTablet);
1886        showConversation(conversation, inLoaderCallbacks);
1887    }
1888
1889    @Override
1890    public Conversation getCurrentConversation() {
1891        return mCurrentConversation;
1892    }
1893
1894    /**
1895     * Set the current conversation. This is the conversation on which all actions are performed.
1896     * Do not modify mCurrentConversation except through this method, which makes it easy to
1897     * perform common actions associated with changing the current conversation.
1898     * @param conversation
1899     */
1900    @Override
1901    public void setCurrentConversation(Conversation conversation) {
1902        // The controller should come out of detached mode if a new conversation is viewed, or if
1903        // we are going back to conversation list mode.
1904        if (mDetachedConvUri != null && (conversation == null
1905                || !mDetachedConvUri.equals(conversation.uri))) {
1906            clearDetachedMode();
1907        }
1908
1909        // Must happen *before* setting mCurrentConversation because this sets
1910        // conversation.position if a cursor is available.
1911        mTracker.initialize(conversation);
1912        mCurrentConversation = conversation;
1913
1914        if (mCurrentConversation != null) {
1915            mActionBarView.setCurrentConversation(mCurrentConversation);
1916            mActionBarView.setSubject(mCurrentConversation.subject);
1917            mActivity.invalidateOptionsMenu();
1918        }
1919    }
1920
1921    /**
1922     * {@inheritDoc}
1923     */
1924    @Override
1925    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
1926        switch (id) {
1927            case LOADER_ACCOUNT_CURSOR:
1928                return new CursorLoader(mContext, MailAppProvider.getAccountsUri(),
1929                        UIProvider.ACCOUNTS_PROJECTION, null, null, null);
1930            case LOADER_FOLDER_CURSOR:
1931                final CursorLoader loader = new CursorLoader(mContext, mFolder.uri,
1932                        UIProvider.FOLDERS_PROJECTION, null, null, null);
1933                loader.setUpdateThrottle(mFolderItemUpdateDelayMs);
1934                return loader;
1935            case LOADER_RECENT_FOLDERS:
1936                if (mAccount != null && mAccount.recentFolderListUri != null) {
1937                    return new CursorLoader(mContext, mAccount.recentFolderListUri,
1938                            UIProvider.FOLDERS_PROJECTION, null, null, null);
1939                }
1940                break;
1941            case LOADER_ACCOUNT_INBOX:
1942                final Uri defaultInbox = Settings.getDefaultInboxUri(mAccount.settings);
1943                final Uri inboxUri = defaultInbox.equals(Uri.EMPTY) ?
1944                    mAccount.folderListUri : defaultInbox;
1945                LogUtils.d(LOG_TAG, "Loading the default inbox: %s", inboxUri);
1946                if (inboxUri != null) {
1947                    return new CursorLoader(mContext, inboxUri, UIProvider.FOLDERS_PROJECTION, null,
1948                            null, null);
1949                }
1950                break;
1951            case LOADER_SEARCH:
1952                return Folder.forSearchResults(mAccount,
1953                        args.getString(ConversationListContext.EXTRA_SEARCH_QUERY),
1954                        mActivity.getActivityContext());
1955            case LOADER_ACCOUNT_UPDATE_CURSOR:
1956                return new CursorLoader(mContext, mAccount.uri, UIProvider.ACCOUNTS_PROJECTION,
1957                        null, null, null);
1958            default:
1959                LogUtils.wtf(LOG_TAG, "Loader returned unexpected id: %d", id);
1960        }
1961        return null;
1962    }
1963
1964    @Override
1965    public void onLoaderReset(Loader<Cursor> loader) {
1966
1967    }
1968
1969    /**
1970     * {@link LoaderManager} currently has a bug in
1971     * {@link LoaderManager#restartLoader(int, Bundle, android.app.LoaderManager.LoaderCallbacks)}
1972     * where, if a previous onCreateLoader returned a null loader, this method will NPE. Work around
1973     * this bug by destroying any loaders that may have been created as null (essentially because
1974     * they are optional loads, and may not apply to a particular account).
1975     * <p>
1976     * A simple null check before restarting a loader will not work, because that would not
1977     * give the controller a chance to invalidate UI corresponding the prior loader result.
1978     *
1979     * @param id loader ID to safely restart
1980     */
1981    private void restartOptionalLoader(int id) {
1982        final LoaderManager lm = mActivity.getLoaderManager();
1983        lm.destroyLoader(id);
1984        lm.restartLoader(id, Bundle.EMPTY, this);
1985    }
1986
1987    @Override
1988    public void registerConversationListObserver(DataSetObserver observer) {
1989        mConversationListObservable.registerObserver(observer);
1990    }
1991
1992    @Override
1993    public void unregisterConversationListObserver(DataSetObserver observer) {
1994        mConversationListObservable.unregisterObserver(observer);
1995    }
1996
1997    @Override
1998    public void registerFolderObserver(DataSetObserver observer) {
1999        mFolderObservable.registerObserver(observer);
2000    }
2001
2002    @Override
2003    public void unregisterFolderObserver(DataSetObserver observer) {
2004        mFolderObservable.unregisterObserver(observer);
2005    }
2006
2007    @Override
2008    public void registerConversationLoadedObserver(DataSetObserver observer) {
2009        mPagerController.registerConversationLoadedObserver(observer);
2010    }
2011
2012    @Override
2013    public void unregisterConversationLoadedObserver(DataSetObserver observer) {
2014        mPagerController.unregisterConversationLoadedObserver(observer);
2015    }
2016
2017    /**
2018     * Returns true if the number of accounts is different, or if the current account has been
2019     * removed from the device
2020     * @param accountCursor
2021     * @return
2022     */
2023    private boolean accountsUpdated(Cursor accountCursor) {
2024        // Check to see if the current account hasn't been set, or the account cursor is empty
2025        if (mAccount == null || !accountCursor.moveToFirst()) {
2026            return true;
2027        }
2028
2029        // Check to see if the number of accounts are different, from the number we saw on the last
2030        // updated
2031        if (mCurrentAccountUris.size() != accountCursor.getCount()) {
2032            return true;
2033        }
2034
2035        // Check to see if the account list is different or if the current account is not found in
2036        // the cursor.
2037        boolean foundCurrentAccount = false;
2038        do {
2039            final Uri accountUri = Uri.parse(accountCursor.getString(
2040                    accountCursor.getColumnIndex(UIProvider.AccountColumns.URI)));
2041            if (!foundCurrentAccount && mAccount.uri.equals(accountUri)) {
2042                foundCurrentAccount = true;
2043            }
2044            // Is there a new account that we do not know about?
2045            if (!mCurrentAccountUris.contains(accountUri)) {
2046                return true;
2047            }
2048        } while (accountCursor.moveToNext());
2049
2050        // As long as we found the current account, the list hasn't been updated
2051        return !foundCurrentAccount;
2052    }
2053
2054    /**
2055     * Updates accounts for the app. If the current account is missing, the first
2056     * account in the list is set to the current account (we <em>have</em> to choose something).
2057     *
2058     * @param accounts cursor into the AccountCache
2059     * @return true if the update was successful, false otherwise
2060     */
2061    private boolean updateAccounts(Cursor accounts) {
2062        if (accounts == null || !accounts.moveToFirst()) {
2063            return false;
2064        }
2065
2066        final Account[] allAccounts = Account.getAllAccounts(accounts);
2067        // A match for the current account's URI in the list of accounts.
2068        Account currentFromList = null;
2069
2070        // Save the uris for the accounts and find the current account in the updated cursor.
2071        mCurrentAccountUris.clear();
2072        for (final Account account : allAccounts) {
2073            LogUtils.d(LOG_TAG, "updateAccounts(%s)", account);
2074            mCurrentAccountUris.add(account.uri);
2075            if (mAccount != null && account.uri.equals(mAccount.uri)) {
2076                currentFromList = account;
2077            }
2078        }
2079
2080        // 1. current account is already set and is in allAccounts:
2081        //    1a. It has changed -> load the updated account.
2082        //    2b. It is unchanged -> no-op
2083        // 2. current account is set and is not in allAccounts -> pick first (acct was deleted?)
2084        // 3. saved preference has an account -> pick that one
2085        // 4. otherwise just pick first
2086
2087        boolean accountChanged = false;
2088        /// Assume case 4, initialize to first account, and see if we can find anything better.
2089        Account newAccount = allAccounts[0];
2090        if (currentFromList != null) {
2091            // Case 1: Current account exists but has changed
2092            if (!currentFromList.equals(mAccount)) {
2093                newAccount = currentFromList;
2094                accountChanged = true;
2095            }
2096            // Case 1b: else, current account is unchanged: nothing to do.
2097        } else {
2098            // Case 2: Current account is not in allAccounts, the account needs to change.
2099            accountChanged = true;
2100            if (mAccount == null) {
2101                // Case 3: Check for last viewed account, and check if it exists in the list.
2102                final String lastAccountUri = MailAppProvider.getInstance().getLastViewedAccount();
2103                if (lastAccountUri != null) {
2104                    for (final Account account : allAccounts) {
2105                        if (lastAccountUri.equals(account.uri.toString())) {
2106                            newAccount = account;
2107                            break;
2108                        }
2109                    }
2110                }
2111            }
2112        }
2113        if (accountChanged) {
2114            onAccountChanged(newAccount);
2115        }
2116        // Whether we have updated the current account or not, we need to update the list of
2117        // accounts in the ActionBar.
2118        mActionBarView.setAccounts(allAccounts);
2119        return (allAccounts.length > 0);
2120    }
2121
2122    private void disableNotifications() {
2123        mNewEmailReceiver.activate(mContext, this);
2124    }
2125
2126    private void enableNotifications() {
2127        mNewEmailReceiver.deactivate();
2128    }
2129
2130    private void disableNotificationsOnAccountChange(Account account) {
2131        // If the new mail suppression receiver is activated for a different account, we want to
2132        // activate it for the new account.
2133        if (mNewEmailReceiver.activated() &&
2134                !mNewEmailReceiver.notificationsDisabledForAccount(account)) {
2135            // Deactivate the current receiver, otherwise multiple receivers may be registered.
2136            mNewEmailReceiver.deactivate();
2137            mNewEmailReceiver.activate(mContext, this);
2138        }
2139    }
2140
2141    /**
2142     * {@inheritDoc}
2143     */
2144    @Override
2145    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
2146        // We want to reinitialize only if we haven't ever been initialized, or
2147        // if the current account has vanished.
2148        if (data == null) {
2149            LogUtils.e(LOG_TAG, "Received null cursor from loader id: %d", loader.getId());
2150        }
2151        switch (loader.getId()) {
2152            case LOADER_ACCOUNT_CURSOR:
2153                if (data == null) {
2154                    // Nothing useful to do if we have no valid data.
2155                    break;
2156                }
2157                if (data.getCount() == 0) {
2158                    // If an empty cursor is returned, the MailAppProvider is indicating that
2159                    // no accounts have been specified.  We want to navigate to the "add account"
2160                    // activity that will handle the intent returned by the MailAppProvider
2161
2162                    // If the MailAppProvider believes that all accounts have been loaded, and the
2163                    // account list is still empty, we want to prompt the user to add an account
2164                    final Bundle extras = data.getExtras();
2165                    final boolean accountsLoaded =
2166                            extras.getInt(AccountCursorExtraKeys.ACCOUNTS_LOADED) != 0;
2167
2168                    if (accountsLoaded) {
2169                        final Intent noAccountIntent = MailAppProvider.getNoAccountIntent(mContext);
2170                        if (noAccountIntent != null) {
2171                            mActivity.startActivityForResult(noAccountIntent,
2172                                    ADD_ACCOUNT_REQUEST_CODE);
2173                        }
2174                    }
2175                } else {
2176                    final boolean accountListUpdated = accountsUpdated(data);
2177                    if (!isLoaderInitialized || accountListUpdated) {
2178                        isLoaderInitialized = updateAccounts(data);
2179                    }
2180                }
2181                break;
2182            case LOADER_ACCOUNT_UPDATE_CURSOR:
2183                // We have gotten an update for current account.
2184
2185                // Make sure that this is an update for the current account
2186                if (data != null && data.moveToFirst()) {
2187                    final Account updatedAccount = new Account(data);
2188
2189                    if (updatedAccount.uri.equals(mAccount.uri)) {
2190                        // Keep a reference to the previous settings object
2191                        final Settings previousSettings = mAccount.settings;
2192
2193                        // Update the controller's reference to the current account
2194                        mAccount = updatedAccount;
2195                        LogUtils.d(LOG_TAG, "AbstractActivityController.onLoadFinished(): "
2196                                + "mAccount = %s", mAccount.uri);
2197
2198                        // Only notify about a settings change if something differs
2199                        if (!Objects.equal(mAccount.settings, previousSettings)) {
2200                            mAccountObservers.notifyChanged();
2201                        }
2202                        perhapsEnterWaitMode();
2203                    } else {
2204                        LogUtils.e(LOG_TAG, "Got update for account: %s with current account: %s",
2205                                updatedAccount.uri, mAccount.uri);
2206                        // We need to restart the loader, so the correct account information will
2207                        // be returned
2208                        restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR);
2209                    }
2210                }
2211                break;
2212            case LOADER_FOLDER_CURSOR:
2213                // Check status of the cursor.
2214                if (data != null && data.moveToFirst()) {
2215                    final Folder folder = new Folder(data);
2216                    LogUtils.d(LOG_TAG, "FOLDER STATUS = %d", folder.syncStatus);
2217                    setHasFolderChanged(folder);
2218                    mFolder = folder;
2219                    mFolderObservable.notifyChanged();
2220                } else {
2221                    LogUtils.d(LOG_TAG, "Unable to get the folder %s",
2222                            mFolder != null ? mAccount.name : "");
2223                }
2224                break;
2225            case LOADER_RECENT_FOLDERS:
2226                // Few recent folders and we are running on a phone? Populate the default recents.
2227                // The number of default recent folders is at least 2: every provider has at
2228                // least two folders, and the recent folder count never decreases. Having a single
2229                // recent folder is an erroneous case, and we can gracefully recover by populating
2230                // default recents. The default recents will not stomp on the existing value: it
2231                // will be shown in addition to the default folders: the max number of recent
2232                // folders is more than 1+num(defaultRecents).
2233                if (data != null && data.getCount() <= 1 && !mIsTablet) {
2234                    final class PopulateDefault extends AsyncTask<Uri, Void, Void> {
2235                        @Override
2236                        protected Void doInBackground(Uri... uri) {
2237                            // Asking for an update on the URI and ignore the result.
2238                            final ContentResolver resolver = mContext.getContentResolver();
2239                            resolver.update(uri[0], null, null, null);
2240                            return null;
2241                        }
2242                    }
2243                    final Uri uri = mAccount.defaultRecentFolderListUri;
2244                    LogUtils.v(LOG_TAG, "Default recents at %s", uri);
2245                    new PopulateDefault().execute(uri);
2246                    break;
2247                }
2248                LogUtils.v(LOG_TAG, "Reading recent folders from the cursor.");
2249                loadRecentFolders(data);
2250                break;
2251            case LOADER_ACCOUNT_INBOX:
2252                if (data != null && !data.isClosed() && data.moveToFirst()) {
2253                    Folder inbox = new Folder(data);
2254                    onFolderChanged(inbox);
2255                    // Just want to get the inbox, don't care about updates to it
2256                    // as this will be tracked by the folder change listener.
2257                    mActivity.getLoaderManager().destroyLoader(LOADER_ACCOUNT_INBOX);
2258                } else {
2259                    LogUtils.d(LOG_TAG, "Unable to get the account inbox for account %s",
2260                            mAccount != null ? mAccount.name : "");
2261                }
2262                break;
2263            case LOADER_SEARCH:
2264                if (data != null && data.getCount() > 0) {
2265                    data.moveToFirst();
2266                    final Folder search = new Folder(data);
2267                    updateFolder(search);
2268                    mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder,
2269                            mActivity.getIntent()
2270                                    .getStringExtra(UIProvider.SearchQueryParameters.QUERY));
2271                    showConversationList(mConvListContext);
2272                    mActivity.invalidateOptionsMenu();
2273                    mHaveSearchResults = search.totalCount > 0;
2274                    mActivity.getLoaderManager().destroyLoader(LOADER_SEARCH);
2275                } else {
2276                    LogUtils.e(LOG_TAG, "Null or empty cursor returned by LOADER_SEARCH loader");
2277                }
2278                break;
2279        }
2280    }
2281
2282
2283    /**
2284     * Destructive actions on Conversations. This class should only be created by controllers, and
2285     * clients should only require {@link DestructiveAction}s, not specific implementations of the.
2286     * Only the controllers should know what kind of destructive actions are being created.
2287     */
2288    public class ConversationAction implements DestructiveAction {
2289        /**
2290         * The action to be performed. This is specified as the resource ID of the menu item
2291         * corresponding to this action: R.id.delete, R.id.report_spam, etc.
2292         */
2293        private final int mAction;
2294        /** The action will act upon these conversations */
2295        private final Collection<Conversation> mTarget;
2296        /** Whether this destructive action has already been performed */
2297        private boolean mCompleted;
2298        /** Whether this is an action on the currently selected set. */
2299        private final boolean mIsSelectedSet;
2300
2301        /**
2302         * Create a listener object. action is one of four constants: R.id.y_button (archive),
2303         * R.id.delete , R.id.mute, and R.id.report_spam.
2304         * @param action
2305         * @param target Conversation that we want to apply the action to.
2306         * @param isBatch whether the conversations are in the currently selected batch set.
2307         */
2308        public ConversationAction(int action, Collection<Conversation> target, boolean isBatch) {
2309            mAction = action;
2310            mTarget = ImmutableList.copyOf(target);
2311            mIsSelectedSet = isBatch;
2312        }
2313
2314        /**
2315         * The action common to child classes. This performs the action specified in the constructor
2316         * on the conversations given here.
2317         */
2318        @Override
2319        public void performAction() {
2320            if (isPerformed()) {
2321                return;
2322            }
2323            boolean undoEnabled = mAccount.supportsCapability(AccountCapabilities.UNDO);
2324
2325            // Are we destroying the currently shown conversation? Show the next one.
2326            if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)){
2327                LogUtils.d(LOG_TAG, "ConversationAction.performAction():"
2328                        + "\nmTarget=%s\nCurrent=%s",
2329                        Conversation.toString(mTarget), mCurrentConversation);
2330            }
2331
2332            if (mConversationListCursor == null) {
2333                LogUtils.e(LOG_TAG, "null ConversationCursor in ConversationAction.performAction():"
2334                        + "\nmTarget=%s\nCurrent=%s",
2335                        Conversation.toString(mTarget), mCurrentConversation);
2336                return;
2337            }
2338
2339            switch (mAction) {
2340                case R.id.archive:
2341                    LogUtils.d(LOG_TAG, "Archiving");
2342                    mConversationListCursor.archive(mContext, mTarget);
2343                    break;
2344                case R.id.delete:
2345                    LogUtils.d(LOG_TAG, "Deleting");
2346                    mConversationListCursor.delete(mContext, mTarget);
2347                    if (mFolder.supportsCapability(FolderCapabilities.DELETE_ACTION_FINAL)) {
2348                        undoEnabled = false;
2349                    }
2350                    break;
2351                case R.id.mute:
2352                    LogUtils.d(LOG_TAG, "Muting");
2353                    if (mFolder.supportsCapability(FolderCapabilities.DESTRUCTIVE_MUTE)) {
2354                        for (Conversation c : mTarget) {
2355                            c.localDeleteOnUpdate = true;
2356                        }
2357                    }
2358                    mConversationListCursor.mute(mContext, mTarget);
2359                    break;
2360                case R.id.report_spam:
2361                    LogUtils.d(LOG_TAG, "Reporting spam");
2362                    mConversationListCursor.reportSpam(mContext, mTarget);
2363                    break;
2364                case R.id.mark_not_spam:
2365                    LogUtils.d(LOG_TAG, "Marking not spam");
2366                    mConversationListCursor.reportNotSpam(mContext, mTarget);
2367                    break;
2368                case R.id.report_phishing:
2369                    LogUtils.d(LOG_TAG, "Reporting phishing");
2370                    mConversationListCursor.reportPhishing(mContext, mTarget);
2371                    break;
2372                case R.id.remove_star:
2373                    LogUtils.d(LOG_TAG, "Removing star");
2374                    // Star removal is destructive in the Starred folder.
2375                    mConversationListCursor.updateBoolean(mContext, mTarget,
2376                            ConversationColumns.STARRED, false);
2377                    break;
2378                case R.id.mark_not_important:
2379                    LogUtils.d(LOG_TAG, "Marking not-important");
2380                    // Marking not important is destructive in a mailbox
2381                    // containing only important messages
2382                    if (mFolder != null && mFolder.isImportantOnly()) {
2383                        for (Conversation conv : mTarget) {
2384                            conv.localDeleteOnUpdate = true;
2385                        }
2386                    }
2387                    mConversationListCursor.updateInt(mContext, mTarget,
2388                            ConversationColumns.PRIORITY, UIProvider.ConversationPriority.LOW);
2389                    break;
2390                case R.id.discard_drafts:
2391                    LogUtils.d(LOG_TAG, "Discarding draft messages");
2392                    // Discarding draft messages is destructive in a "draft" mailbox
2393                    if (mFolder != null && mFolder.isDraft()) {
2394                        for (Conversation conv : mTarget) {
2395                            conv.localDeleteOnUpdate = true;
2396                        }
2397                    }
2398                    mConversationListCursor.discardDrafts(mContext, mTarget);
2399                    // We don't support undoing discarding drafts
2400                    undoEnabled = false;
2401                    break;
2402            }
2403            if (undoEnabled) {
2404                mHandler.postDelayed(new Runnable() {
2405                    @Override
2406                    public void run() {
2407                        onUndoAvailable(new ToastBarOperation(mTarget.size(), mAction,
2408                                ToastBarOperation.UNDO, mIsSelectedSet));
2409                    }
2410                }, mShowUndoBarDelay);
2411            }
2412            refreshConversationList();
2413            if (mIsSelectedSet) {
2414                mSelectedSet.clear();
2415            }
2416        }
2417
2418        /**
2419         * Returns true if this action has been performed, false otherwise.
2420         *
2421         */
2422        private synchronized boolean isPerformed() {
2423            if (mCompleted) {
2424                return true;
2425            }
2426            mCompleted = true;
2427            return false;
2428        }
2429    }
2430
2431    // Called from the FolderSelectionDialog after a user is done selecting folders to assign the
2432    // conversations to.
2433    @Override
2434    public final void assignFolder(Collection<FolderOperation> folderOps,
2435            Collection<Conversation> target, boolean batch, boolean showUndo) {
2436        // Actions are destructive only when the current folder can be assigned
2437        // to (which is the same as being able to un-assign a conversation from the folder) and
2438        // when the list of folders contains the current folder.
2439        final boolean isDestructive = mFolder
2440                .supportsCapability(FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES)
2441                && FolderOperation.isDestructive(folderOps, mFolder);
2442        LogUtils.d(LOG_TAG, "onFolderChangesCommit: isDestructive = %b", isDestructive);
2443        if (isDestructive) {
2444            for (final Conversation c : target) {
2445                c.localDeleteOnUpdate = true;
2446            }
2447        }
2448        final DestructiveAction folderChange;
2449        // Update the UI elements depending no their visibility and availability
2450        // TODO(viki): Consolidate this into a single method requestDelete.
2451        if (isDestructive) {
2452            folderChange = getDeferredFolderChange(target, folderOps, isDestructive,
2453                    batch, showUndo);
2454            delete(0, target, folderChange);
2455        } else {
2456            folderChange = getFolderChange(target, folderOps, isDestructive,
2457                    batch, showUndo);
2458            requestUpdate(target, folderChange);
2459        }
2460    }
2461
2462    @Override
2463    public final void onRefreshRequired() {
2464        if (isAnimating() || isDragging()) {
2465            LogUtils.d(LOG_TAG, "onRefreshRequired: delay until animating done");
2466            return;
2467        }
2468        // Refresh the query in the background
2469        if (mConversationListCursor.isRefreshRequired()) {
2470            mConversationListCursor.refresh();
2471        }
2472    }
2473
2474    @Override
2475    public void startDragMode() {
2476        mIsDragHappening = true;
2477    }
2478
2479    @Override
2480    public void stopDragMode() {
2481        mIsDragHappening = false;
2482        if (mConversationListCursor.isRefreshReady()) {
2483            LogUtils.d(LOG_TAG, "Stopped animating: try sync");
2484            onRefreshReady();
2485        }
2486
2487        if (mConversationListCursor.isRefreshRequired()) {
2488            LogUtils.d(LOG_TAG, "Stopped animating: refresh");
2489            mConversationListCursor.refresh();
2490        }
2491    }
2492
2493    private boolean isDragging() {
2494        return mIsDragHappening;
2495    }
2496
2497    @Override
2498    public boolean isAnimating() {
2499        boolean isAnimating = false;
2500        ConversationListFragment convListFragment = getConversationListFragment();
2501        if (convListFragment != null) {
2502            AnimatedAdapter adapter = convListFragment.getAnimatedAdapter();
2503            if (adapter != null) {
2504                isAnimating = adapter.isAnimating();
2505            }
2506        }
2507        return isAnimating;
2508    }
2509
2510    /**
2511     * Called when the {@link ConversationCursor} is changed or has new data in it.
2512     * <p>
2513     * {@inheritDoc}
2514     */
2515    @Override
2516    public final void onRefreshReady() {
2517        LogUtils.d(LOG_TAG, "Received refresh ready callback for folder %s",
2518                mFolder != null ? mFolder.id : "-1");
2519
2520        if (mDestroyed) {
2521            LogUtils.i(LOG_TAG, "ignoring onRefreshReady on destroyed AAC");
2522            return;
2523        }
2524
2525        if (!isAnimating()) {
2526            // Swap cursors
2527            mConversationListCursor.sync();
2528        }
2529        mTracker.onCursorUpdated();
2530        perhapsShowFirstSearchResult();
2531    }
2532
2533    @Override
2534    public final void onDataSetChanged() {
2535        updateConversationListFragment();
2536        mConversationListObservable.notifyChanged();
2537        mSelectedSet.validateAgainstCursor(mConversationListCursor);
2538    }
2539
2540    /**
2541     * If the Conversation List Fragment is visible, updates the fragment.
2542     */
2543    private final void updateConversationListFragment() {
2544        final ConversationListFragment convList = getConversationListFragment();
2545        if (convList != null) {
2546            refreshConversationList();
2547            if (isFragmentVisible(convList)) {
2548                informCursorVisiblity(true);
2549            }
2550        }
2551    }
2552
2553    /**
2554     * This class handles throttled refresh of the conversation list
2555     */
2556    static class RefreshTimerTask extends TimerTask {
2557        final Handler mHandler;
2558        final AbstractActivityController mController;
2559
2560        RefreshTimerTask(AbstractActivityController controller, Handler handler) {
2561            mHandler = handler;
2562            mController = controller;
2563        }
2564
2565        @Override
2566        public void run() {
2567            mHandler.post(new Runnable() {
2568                @Override
2569                public void run() {
2570                    LogUtils.d(LOG_TAG, "Delay done... calling onRefreshRequired");
2571                    mController.onRefreshRequired();
2572                }});
2573        }
2574    }
2575
2576    /**
2577     * Cancel the refresh task, if it's running
2578     */
2579    private void cancelRefreshTask () {
2580        if (mConversationListRefreshTask != null) {
2581            mConversationListRefreshTask.cancel();
2582            mConversationListRefreshTask = null;
2583        }
2584    }
2585
2586    private void loadRecentFolders(Cursor data) {
2587        mRecentFolderList.loadFromUiProvider(data);
2588        if (isAnimating()) {
2589            mRecentsDataUpdated = true;
2590        } else {
2591            mRecentFolderObservers.notifyChanged();
2592        }
2593    }
2594
2595    @Override
2596    public void onAnimationEnd(AnimatedAdapter animatedAdapter) {
2597        if (mConversationListCursor == null) {
2598            LogUtils.e(LOG_TAG, "null ConversationCursor in onAnimationEnd");
2599            return;
2600        }
2601        if (mConversationListCursor.isRefreshReady()) {
2602            LogUtils.d(LOG_TAG, "Stopped animating: try sync");
2603            onRefreshReady();
2604        }
2605
2606        if (mConversationListCursor.isRefreshRequired()) {
2607            LogUtils.d(LOG_TAG, "Stopped animating: refresh");
2608            mConversationListCursor.refresh();
2609        }
2610        if (mRecentsDataUpdated) {
2611            mRecentsDataUpdated = false;
2612            mRecentFolderObservers.notifyChanged();
2613        }
2614        FolderListFragment frag = this.getFolderListFragment();
2615        if (frag != null) {
2616            frag.onAnimationEnd();
2617        }
2618    }
2619
2620    @Override
2621    public void onSetEmpty() {
2622        // There are no selected conversations. Ensure that the listener and its associated actions
2623        // are blanked out.
2624        setListener(null, -1);
2625    }
2626
2627    @Override
2628    public void onSetPopulated(ConversationSelectionSet set) {
2629        mCabActionMenu = new SelectedConversationsActionMenu(mActivity, set, mFolder);
2630        if (mViewMode.isListMode() || (mIsTablet && mViewMode.isConversationMode())) {
2631            enableCabMode();
2632        }
2633    }
2634
2635    @Override
2636    public void onSetChanged(ConversationSelectionSet set) {
2637        // Do nothing. We don't care about changes to the set.
2638    }
2639
2640    @Override
2641    public ConversationSelectionSet getSelectedSet() {
2642        return mSelectedSet;
2643    }
2644
2645    /**
2646     * Disable the Contextual Action Bar (CAB). The selected set is not changed.
2647     */
2648    protected void disableCabMode() {
2649        // Commit any previous destructive actions when entering/ exiting CAB mode.
2650        commitDestructiveActions(true);
2651        if (mCabActionMenu != null) {
2652            mCabActionMenu.deactivate();
2653        }
2654    }
2655
2656    /**
2657     * Re-enable the CAB menu if required. The selection set is not changed.
2658     */
2659    protected void enableCabMode() {
2660        if (mCabActionMenu != null) {
2661            mCabActionMenu.activate();
2662        }
2663    }
2664
2665    /**
2666     * Unselect conversations and exit CAB mode.
2667     */
2668    protected final void exitCabMode() {
2669        mSelectedSet.clear();
2670    }
2671
2672    @Override
2673    public void startSearch() {
2674        if (mAccount == null) {
2675            // We cannot search if there is no account. Drop the request to the floor.
2676            LogUtils.d(LOG_TAG, "AbstractActivityController.startSearch(): null account");
2677            return;
2678        }
2679        if (mAccount.supportsCapability(UIProvider.AccountCapabilities.LOCAL_SEARCH)
2680                | mAccount.supportsCapability(UIProvider.AccountCapabilities.SERVER_SEARCH)) {
2681            onSearchRequested(mActionBarView.getQuery());
2682        } else {
2683            Toast.makeText(mActivity.getActivityContext(), mActivity.getActivityContext()
2684                    .getString(R.string.search_unsupported), Toast.LENGTH_SHORT).show();
2685        }
2686    }
2687
2688    @Override
2689    public void exitSearchMode() {
2690        if (mViewMode.getMode() == ViewMode.SEARCH_RESULTS_LIST) {
2691            mActivity.finish();
2692        }
2693    }
2694
2695    /**
2696     * Supports dragging conversations to a folder.
2697     */
2698    @Override
2699    public boolean supportsDrag(DragEvent event, Folder folder) {
2700        return (folder != null
2701                && event != null
2702                && event.getClipDescription() != null
2703                && folder.supportsCapability
2704                    (UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES)
2705                && folder.supportsCapability
2706                    (UIProvider.FolderCapabilities.CAN_HOLD_MAIL)
2707                && !mFolder.uri.equals(folder.uri));
2708    }
2709
2710    /**
2711     * Handles dropping conversations to a folder.
2712     */
2713    @Override
2714    public void handleDrop(DragEvent event, final Folder folder) {
2715        if (!supportsDrag(event, folder)) {
2716            return;
2717        }
2718        if (folder.type == UIProvider.FolderType.STARRED) {
2719            // Moving a conversation to the starred folder adds the star and
2720            // removes the current label
2721            handleDropInStarred(folder);
2722            return;
2723        }
2724        if (mFolder.type == UIProvider.FolderType.STARRED) {
2725            handleDragFromStarred(folder);
2726            return;
2727        }
2728        final ArrayList<FolderOperation> dragDropOperations = new ArrayList<FolderOperation>();
2729        final Collection<Conversation> conversations = mSelectedSet.values();
2730        // Add the drop target folder.
2731        dragDropOperations.add(new FolderOperation(folder, true));
2732        // Remove the current folder unless the user is viewing "all".
2733        // That operation should just add the new folder.
2734        boolean isDestructive = !mFolder.isViewAll()
2735                && mFolder.supportsCapability
2736                    (UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES);
2737        if (isDestructive) {
2738            dragDropOperations.add(new FolderOperation(mFolder, false));
2739        }
2740        // Drag and drop is destructive: we remove conversations from the
2741        // current folder.
2742        final DestructiveAction action = getFolderChange(conversations, dragDropOperations,
2743                isDestructive, true, true);
2744        if (isDestructive) {
2745            delete(0, conversations, action);
2746        } else {
2747            action.performAction();
2748        }
2749    }
2750
2751    private void handleDragFromStarred(Folder folder) {
2752        final Collection<Conversation> conversations = mSelectedSet.values();
2753        // The conversation list deletes and performs the action if it exists.
2754        final ConversationListFragment convListFragment = getConversationListFragment();
2755        // There should always be a convlistfragment, or the user could not have
2756        // dragged/ dropped conversations.
2757        if (convListFragment != null) {
2758            LogUtils.d(LOG_TAG, "AAC.requestDelete: ListFragment is handling delete.");
2759            ArrayList<ConversationOperation> ops = new ArrayList<ConversationOperation>();
2760            ArrayList<Uri> folderUris;
2761            ArrayList<Boolean> adds;
2762            for (Conversation target : conversations) {
2763                folderUris = new ArrayList<Uri>();
2764                adds = new ArrayList<Boolean>();
2765                folderUris.add(folder.uri);
2766                adds.add(Boolean.TRUE);
2767                final HashMap<Uri, Folder> targetFolders =
2768                        Folder.hashMapForFolders(target.getRawFolders());
2769                targetFolders.put(folder.uri, folder);
2770                ops.add(mConversationListCursor.getConversationFolderOperation(target,
2771                        folderUris, adds, targetFolders.values()));
2772            }
2773            if (mConversationListCursor != null) {
2774                mConversationListCursor.updateBulkValues(mContext, ops);
2775            }
2776            refreshConversationList();
2777            mSelectedSet.clear();
2778            return;
2779        }
2780    }
2781
2782    private void handleDropInStarred(Folder folder) {
2783        final Collection<Conversation> conversations = mSelectedSet.values();
2784        // The conversation list deletes and performs the action if it exists.
2785        final ConversationListFragment convListFragment = getConversationListFragment();
2786        // There should always be a convlistfragment, or the user could not have
2787        // dragged/ dropped conversations.
2788        if (convListFragment != null) {
2789            LogUtils.d(LOG_TAG, "AAC.requestDelete: ListFragment is handling delete.");
2790            convListFragment.requestDelete(R.id.change_folder, conversations,
2791                    new DroppedInStarredAction(conversations, mFolder, folder));
2792            return;
2793        }
2794    }
2795
2796    // When dragging conversations to the starred folder, remove from the
2797    // original folder and add a star
2798    private class DroppedInStarredAction implements DestructiveAction {
2799        private Collection<Conversation> mConversations;
2800        private Folder mInitialFolder;
2801        private Folder mStarred;
2802
2803        public DroppedInStarredAction(Collection<Conversation> conversations, Folder initialFolder,
2804                Folder starredFolder) {
2805            mConversations = conversations;
2806            mInitialFolder = initialFolder;
2807            mStarred = starredFolder;
2808        }
2809
2810        @Override
2811        public void performAction() {
2812            ToastBarOperation undoOp = new ToastBarOperation(mConversations.size(),
2813                    R.id.change_folder, ToastBarOperation.UNDO, true);
2814            onUndoAvailable(undoOp);
2815            ArrayList<ConversationOperation> ops = new ArrayList<ConversationOperation>();
2816            ContentValues values = new ContentValues();
2817            ArrayList<Uri> folderUris;
2818            ArrayList<Boolean> adds;
2819            ConversationOperation operation;
2820            for (Conversation target : mConversations) {
2821                folderUris = new ArrayList<Uri>();
2822                adds = new ArrayList<Boolean>();
2823                folderUris.add(mStarred.uri);
2824                adds.add(Boolean.TRUE);
2825                folderUris.add(mInitialFolder.uri);
2826                adds.add(Boolean.FALSE);
2827                final HashMap<Uri, Folder> targetFolders =
2828                        Folder.hashMapForFolders(target.getRawFolders());
2829                targetFolders.put(mStarred.uri, mStarred);
2830                targetFolders.remove(mInitialFolder.uri);
2831                values.put(ConversationColumns.STARRED, true);
2832                operation = mConversationListCursor.getConversationFolderOperation(target,
2833                        folderUris, adds, targetFolders.values(), values);
2834                ops.add(operation);
2835            }
2836            if (mConversationListCursor != null) {
2837                mConversationListCursor.updateBulkValues(mContext, ops);
2838            }
2839            refreshConversationList();
2840            mSelectedSet.clear();
2841        }
2842    }
2843
2844    @Override
2845    public void onTouchEvent(MotionEvent event) {
2846        if (event.getAction() == MotionEvent.ACTION_DOWN) {
2847            if (mToastBar != null && !mToastBar.isEventInToastBar(event)) {
2848                hideOrRepositionToastBar(true);
2849            }
2850        }
2851    }
2852
2853    protected abstract void hideOrRepositionToastBar(boolean animated);
2854
2855    @Override
2856    public void onConversationSeen(Conversation conv) {
2857        mPagerController.onConversationSeen(conv);
2858    }
2859
2860    @Override
2861    public boolean isInitialConversationLoading() {
2862        return mPagerController.isInitialConversationLoading();
2863    }
2864
2865    /**
2866     * Check if the fragment given here is visible. Checking {@link Fragment#isVisible()} is
2867     * insufficient because that doesn't check if the window is currently in focus or not.
2868     */
2869    private final boolean isFragmentVisible(Fragment in) {
2870        return in != null && in.isVisible() && mActivity.hasWindowFocus();
2871    }
2872
2873    private class ConversationListLoaderCallbacks implements
2874        LoaderManager.LoaderCallbacks<ConversationCursor> {
2875
2876        @Override
2877        public Loader<ConversationCursor> onCreateLoader(int id, Bundle args) {
2878            Loader<ConversationCursor> result = new ConversationCursorLoader((Activity) mActivity,
2879                    mAccount, mFolder.conversationListUri, mFolder.name);
2880            return result;
2881        }
2882
2883        @Override
2884        public void onLoadFinished(Loader<ConversationCursor> loader, ConversationCursor data) {
2885            LogUtils.d(LOG_TAG, "IN AAC.ConversationCursor.onLoadFinished, data=%s loader=%s",
2886                    data, loader);
2887            // Clear our all pending destructive actions before swapping the conversation cursor
2888            destroyPending(null);
2889            mConversationListCursor = data;
2890            mConversationListCursor.addListener(AbstractActivityController.this);
2891            mTracker.onCursorUpdated();
2892            mConversationListObservable.notifyChanged();
2893            // Handle actions that were deferred until after the conversation list was loaded.
2894            for (LoadFinishedCallback callback : mConversationListLoadFinishedCallbacks) {
2895                callback.onLoadFinished();
2896            }
2897            mConversationListLoadFinishedCallbacks.clear();
2898
2899            final ConversationListFragment convList = getConversationListFragment();
2900            if (isFragmentVisible(convList)) {
2901                // The conversation list is already listening to list changes and gets notified
2902                // in the mConversationListObservable.notifyChanged() line above. We only need to
2903                // check and inform the cursor of the change in visibility here.
2904                informCursorVisiblity(true);
2905            }
2906            perhapsShowFirstSearchResult();
2907        }
2908
2909        @Override
2910        public void onLoaderReset(Loader<ConversationCursor> loader) {
2911            LogUtils.d(LOG_TAG, "IN AAC.ConversationCursor.onLoaderReset, data=%s loader=%s",
2912                    mConversationListCursor, loader);
2913
2914            if (mConversationListCursor != null) {
2915                // Unregister the listener
2916                mConversationListCursor.removeListener(AbstractActivityController.this);
2917                mConversationListCursor = null;
2918
2919                // Inform anyone who is interested about the change
2920                mTracker.onCursorUpdated();
2921                mConversationListObservable.notifyChanged();
2922            }
2923        }
2924    }
2925
2926    /**
2927     * Updates controller state based on search results and shows first conversation if required.
2928     */
2929    private final void perhapsShowFirstSearchResult() {
2930        if (mCurrentConversation == null) {
2931            // Shown for search results in two-pane mode only.
2932            mHaveSearchResults = Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())
2933                    && mConversationListCursor.getCount() > 0;
2934            if (!shouldShowFirstConversation()) {
2935                return;
2936            }
2937            mConversationListCursor.moveToPosition(0);
2938            final Conversation conv = new Conversation(mConversationListCursor);
2939            conv.position = 0;
2940            onConversationSelected(conv, true /* checkSafeToModifyFragments */);
2941        }
2942    }
2943
2944    /**
2945     * Destroy the pending {@link DestructiveAction} till now and assign the given action as the
2946     * next destructive action..
2947     * @param nextAction the next destructive action to be performed. This can be null.
2948     */
2949    private final void destroyPending(DestructiveAction nextAction) {
2950        // If there is a pending action, perform that first.
2951        if (mPendingDestruction != null) {
2952            mPendingDestruction.performAction();
2953        }
2954        mPendingDestruction = nextAction;
2955    }
2956
2957    /**
2958     * Register a destructive action with the controller. This performs the previous destructive
2959     * action as a side effect. This method is final because we don't want the child classes to
2960     * embellish this method any more.
2961     * @param action
2962     */
2963    private final void registerDestructiveAction(DestructiveAction action) {
2964        // TODO(viki): This is not a good idea. The best solution is for clients to request a
2965        // destructive action from the controller and for the controller to own the action. This is
2966        // a half-way solution while refactoring DestructiveAction.
2967        destroyPending(action);
2968        return;
2969    }
2970
2971    @Override
2972    public final DestructiveAction getBatchAction(int action) {
2973        final DestructiveAction da = new ConversationAction(action, mSelectedSet.values(), true);
2974        registerDestructiveAction(da);
2975        return da;
2976    }
2977
2978    @Override
2979    public final DestructiveAction getDeferredBatchAction(int action) {
2980        return getDeferredAction(action, mSelectedSet.values(), true);
2981    }
2982
2983    /**
2984     * Get a destructive action for a menu action. This is a temporary method,
2985     * to control the profusion of {@link DestructiveAction} classes that are
2986     * created. Please do not copy this paradigm.
2987     * @param action the resource ID of the menu action: R.id.delete, for
2988     *            example
2989     * @param target the conversations to act upon.
2990     * @return a {@link DestructiveAction} that performs the specified action.
2991     */
2992    private final DestructiveAction getDeferredAction(int action, Collection<Conversation> target,
2993            boolean batch) {
2994        return new ConversationAction(action, target, batch);
2995    }
2996
2997    /**
2998     * Class to change the folders that are assigned to a set of conversations. This is destructive
2999     * because the user can remove the current folder from the conversation, in which case it has
3000     * to be animated away from the current folder.
3001     */
3002    private class FolderDestruction implements DestructiveAction {
3003        private final Collection<Conversation> mTarget;
3004        private final ArrayList<FolderOperation> mFolderOps = new ArrayList<FolderOperation>();
3005        private final boolean mIsDestructive;
3006        /** Whether this destructive action has already been performed */
3007        private boolean mCompleted;
3008        private boolean mIsSelectedSet;
3009        private boolean mShowUndo;
3010        private int mAction;
3011
3012        /**
3013         * Create a new folder destruction object to act on the given conversations.
3014         * @param target
3015         */
3016        private FolderDestruction(final Collection<Conversation> target,
3017                final Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
3018                boolean showUndo, int action) {
3019            mTarget = ImmutableList.copyOf(target);
3020            mFolderOps.addAll(folders);
3021            mIsDestructive = isDestructive;
3022            mIsSelectedSet = isBatch;
3023            mShowUndo = showUndo;
3024            mAction = action;
3025        }
3026
3027        @Override
3028        public void performAction() {
3029            if (isPerformed()) {
3030                return;
3031            }
3032            if (mIsDestructive && mShowUndo) {
3033                ToastBarOperation undoOp = new ToastBarOperation(mTarget.size(), mAction,
3034                        ToastBarOperation.UNDO, mIsSelectedSet);
3035                onUndoAvailable(undoOp);
3036            }
3037            // For each conversation, for each operation, add/ remove the
3038            // appropriate folders.
3039            ArrayList<ConversationOperation> ops = new ArrayList<ConversationOperation>();
3040            ArrayList<Uri> folderUris;
3041            ArrayList<Boolean> adds;
3042            for (Conversation target : mTarget) {
3043                HashMap<Uri, Folder> targetFolders = Folder.hashMapForFolders(target
3044                        .getRawFolders());
3045                folderUris = new ArrayList<Uri>();
3046                adds = new ArrayList<Boolean>();
3047                if (mIsDestructive) {
3048                    target.localDeleteOnUpdate = true;
3049                }
3050                for (FolderOperation op : mFolderOps) {
3051                    folderUris.add(op.mFolder.uri);
3052                    adds.add(op.mAdd ? Boolean.TRUE : Boolean.FALSE);
3053                    if (op.mAdd) {
3054                        targetFolders.put(op.mFolder.uri, op.mFolder);
3055                    } else {
3056                        targetFolders.remove(op.mFolder.uri);
3057                    }
3058                }
3059                ops.add(mConversationListCursor.getConversationFolderOperation(target,
3060                        folderUris, adds, targetFolders.values()));
3061            }
3062            if (mConversationListCursor != null) {
3063                mConversationListCursor.updateBulkValues(mContext, ops);
3064            }
3065            refreshConversationList();
3066            if (mIsSelectedSet) {
3067                mSelectedSet.clear();
3068            }
3069        }
3070
3071        /**
3072         * Returns true if this action has been performed, false otherwise.
3073         *
3074         */
3075        private synchronized boolean isPerformed() {
3076            if (mCompleted) {
3077                return true;
3078            }
3079            mCompleted = true;
3080            return false;
3081        }
3082    }
3083
3084    public final DestructiveAction getFolderChange(Collection<Conversation> target,
3085            Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
3086            boolean showUndo) {
3087        final DestructiveAction da = getDeferredFolderChange(target, folders, isDestructive,
3088                isBatch, showUndo);
3089        registerDestructiveAction(da);
3090        return da;
3091    }
3092
3093    public final DestructiveAction getDeferredFolderChange(Collection<Conversation> target,
3094            Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
3095            boolean showUndo) {
3096        final DestructiveAction da = new FolderDestruction(target, folders, isDestructive, isBatch,
3097                showUndo, R.id.change_folder);
3098        return da;
3099    }
3100
3101    @Override
3102    public final DestructiveAction getDeferredRemoveFolder(Collection<Conversation> target,
3103            Folder toRemove, boolean isDestructive, boolean isBatch,
3104            boolean showUndo) {
3105        Collection<FolderOperation> folderOps = new ArrayList<FolderOperation>();
3106        folderOps.add(new FolderOperation(toRemove, false));
3107        return new FolderDestruction(target, folderOps, isDestructive, isBatch,
3108                showUndo, R.id.remove_folder);
3109    }
3110
3111    @Override
3112    public final void refreshConversationList() {
3113        final ConversationListFragment convList = getConversationListFragment();
3114        if (convList == null) {
3115            return;
3116        }
3117        convList.requestListRefresh();
3118    }
3119
3120    protected final ActionClickedListener getUndoClickedListener(
3121            final AnimatedAdapter listAdapter) {
3122        return new ActionClickedListener() {
3123            @Override
3124            public void onActionClicked() {
3125                if (mAccount.undoUri != null) {
3126                    // NOTE: We might want undo to return the messages affected, in which case
3127                    // the resulting cursor might be interesting...
3128                    // TODO: Use UIProvider.SEQUENCE_QUERY_PARAMETER to indicate the set of
3129                    // commands to undo
3130                    if (mConversationListCursor != null) {
3131                        mConversationListCursor.undo(
3132                                mActivity.getActivityContext(), mAccount.undoUri);
3133                    }
3134                    if (listAdapter != null) {
3135                        listAdapter.setUndo(true);
3136                    }
3137                }
3138            }
3139        };
3140    }
3141
3142    /**
3143     * Shows an error toast in the bottom when a folder was not fetched successfully.
3144     * @param folder the folder which could not be fetched.
3145     * @param replaceVisibleToast if true, this should replace any currently visible toast.
3146     */
3147    protected final void showErrorToast(final Folder folder, boolean replaceVisibleToast) {
3148        mToastBar.setConversationMode(false);
3149
3150        final ActionClickedListener listener;
3151        final int actionTextResourceId;
3152        final int lastSyncResult = folder.lastSyncResult;
3153        switch (lastSyncResult & 0x0f) {
3154            case UIProvider.LastSyncResult.CONNECTION_ERROR:
3155                // The sync request that caused this failure.
3156                final int syncRequest = lastSyncResult >> 4;
3157                // Show: User explicitly pressed the refresh button and there is no connection
3158                // Show: The first time the user enters the app and there is no connection
3159                //       TODO(viki): Implement this.
3160                // Reference: http://b/7202801
3161                final boolean showToast = (syncRequest & UIProvider.SyncStatus.USER_REFRESH) != 0;
3162                // Don't show: Already in the app; user switches to a synced label
3163                // Don't show: In a live label and a background sync fails
3164                final boolean avoidToast = !showToast && (folder.syncWindow > 0
3165                        || (syncRequest & UIProvider.SyncStatus.BACKGROUND_SYNC) != 0);
3166                if (avoidToast) {
3167                    return;
3168                }
3169                listener = getRetryClickedListener(folder);
3170                actionTextResourceId = R.string.retry;
3171                break;
3172            case UIProvider.LastSyncResult.AUTH_ERROR:
3173                listener = getSignInClickedListener();
3174                actionTextResourceId = R.string.signin;
3175                break;
3176            case UIProvider.LastSyncResult.SECURITY_ERROR:
3177                return; // Currently we do nothing for security errors.
3178            case UIProvider.LastSyncResult.STORAGE_ERROR:
3179                listener = getStorageErrorClickedListener();
3180                actionTextResourceId = R.string.info;
3181                break;
3182            case UIProvider.LastSyncResult.INTERNAL_ERROR:
3183                listener = getInternalErrorClickedListener();
3184                actionTextResourceId = R.string.report;
3185                break;
3186            default:
3187                return;
3188        }
3189        mToastBar.show(listener,
3190                R.drawable.ic_alert_white,
3191                Utils.getSyncStatusText(mActivity.getActivityContext(), lastSyncResult),
3192                false, /* showActionIcon */
3193                actionTextResourceId,
3194                replaceVisibleToast,
3195                new ToastBarOperation(1, 0, ToastBarOperation.ERROR, false));
3196    }
3197
3198    private ActionClickedListener getRetryClickedListener(final Folder folder) {
3199        return new ActionClickedListener() {
3200            @Override
3201            public void onActionClicked() {
3202                final Uri uri = folder.refreshUri;
3203
3204                if (uri != null) {
3205                    startAsyncRefreshTask(uri);
3206                }
3207            }
3208        };
3209    }
3210
3211    private ActionClickedListener getSignInClickedListener() {
3212        return new ActionClickedListener() {
3213            @Override
3214            public void onActionClicked() {
3215                promptUserForAuthentication(mAccount);
3216            }
3217        };
3218    }
3219
3220    private ActionClickedListener getStorageErrorClickedListener() {
3221        return new ActionClickedListener() {
3222            @Override
3223            public void onActionClicked() {
3224                showStorageErrorDialog();
3225            }
3226        };
3227    }
3228
3229    private void showStorageErrorDialog() {
3230        DialogFragment fragment = (DialogFragment)
3231                mFragmentManager.findFragmentByTag(SYNC_ERROR_DIALOG_FRAGMENT_TAG);
3232        if (fragment == null) {
3233            fragment = SyncErrorDialogFragment.newInstance();
3234        }
3235        fragment.show(mFragmentManager, SYNC_ERROR_DIALOG_FRAGMENT_TAG);
3236    }
3237
3238    private ActionClickedListener getInternalErrorClickedListener() {
3239        return new ActionClickedListener() {
3240            @Override
3241            public void onActionClicked() {
3242                Utils.sendFeedback(mActivity, mAccount, true /* reportingProblem */);
3243            }
3244        };
3245    }
3246
3247    @Override
3248    public void onFooterViewErrorActionClick(Folder folder, int errorStatus) {
3249        Uri uri = null;
3250        switch (errorStatus) {
3251            case UIProvider.LastSyncResult.CONNECTION_ERROR:
3252                if (folder != null && folder.refreshUri != null) {
3253                    uri = folder.refreshUri;
3254                }
3255                break;
3256            case UIProvider.LastSyncResult.AUTH_ERROR:
3257                promptUserForAuthentication(mAccount);
3258                return;
3259            case UIProvider.LastSyncResult.SECURITY_ERROR:
3260                return; // Currently we do nothing for security errors.
3261            case UIProvider.LastSyncResult.STORAGE_ERROR:
3262                showStorageErrorDialog();
3263                return;
3264            case UIProvider.LastSyncResult.INTERNAL_ERROR:
3265                Utils.sendFeedback(mActivity, mAccount, true /* reportingProblem */);
3266                return;
3267            default:
3268                return;
3269        }
3270
3271        if (uri != null) {
3272            startAsyncRefreshTask(uri);
3273        }
3274    }
3275
3276    @Override
3277    public void onFooterViewLoadMoreClick(Folder folder) {
3278        if (folder != null && folder.loadMoreUri != null) {
3279            startAsyncRefreshTask(folder.loadMoreUri);
3280        }
3281    }
3282
3283    private void startAsyncRefreshTask(Uri uri) {
3284        if (mFolderSyncTask != null) {
3285            mFolderSyncTask.cancel(true);
3286        }
3287        mFolderSyncTask = new AsyncRefreshTask(mActivity.getActivityContext(), uri);
3288        mFolderSyncTask.execute();
3289    }
3290
3291    private void promptUserForAuthentication(Account account) {
3292        if (account != null && !Utils.isEmpty(account.reauthenticationIntentUri)) {
3293            final Intent authenticationIntent =
3294                    new Intent(Intent.ACTION_VIEW, account.reauthenticationIntentUri);
3295            mActivity.startActivityForResult(authenticationIntent, REAUTHENTICATE_REQUEST_CODE);
3296        }
3297    }
3298
3299    @Override
3300    public void onAccessibilityStateChanged() {
3301        // Clear the cache of objects.
3302        ConversationItemViewModel.onAccessibilityUpdated();
3303        // Re-render the list if it exists.
3304        final ConversationListFragment frag = getConversationListFragment();
3305        if (frag != null) {
3306            AnimatedAdapter adapter = frag.getAnimatedAdapter();
3307            if (adapter != null) {
3308                adapter.notifyDataSetInvalidated();
3309            }
3310        }
3311    }
3312
3313    @Override
3314    public void makeDialogListener (final int action, boolean isBatch) {
3315        final Collection<Conversation> target;
3316        if (isBatch) {
3317            target = mSelectedSet.values();
3318        } else {
3319            LogUtils.d(LOG_TAG, "Will act upon %s", mCurrentConversation);
3320            target = Conversation.listOf(mCurrentConversation);
3321        }
3322        final DestructiveAction destructiveAction = getDeferredAction(action, target, isBatch);
3323        mDialogAction = action;
3324        mDialogFromSelectedSet = isBatch;
3325        mDialogListener = new AlertDialog.OnClickListener() {
3326            @Override
3327            public void onClick(DialogInterface dialog, int which) {
3328                delete(action, target, destructiveAction);
3329                // Afterwards, let's remove references to the listener and the action.
3330                setListener(null, -1);
3331            }
3332        };
3333    }
3334
3335    @Override
3336    public AlertDialog.OnClickListener getListener() {
3337        return mDialogListener;
3338    }
3339
3340    /**
3341     * Sets the listener for the positive action on a confirmation dialog.  Since only a single
3342     * confirmation dialog can be shown, this overwrites the previous listener.  It is safe to
3343     * unset the listener; in which case action should be set to -1.
3344     * @param listener
3345     * @param action
3346     */
3347    private void setListener(AlertDialog.OnClickListener listener, final int action){
3348        mDialogListener = listener;
3349        mDialogAction = action;
3350    }
3351
3352    @Override
3353    public VeiledAddressMatcher getVeiledAddressMatcher() {
3354        return mVeiledMatcher;
3355    }
3356
3357    @Override
3358    public void setDetachedMode() {
3359        // Tell the conversation list not to select anything.
3360        final ConversationListFragment frag = getConversationListFragment();
3361        if (frag != null) {
3362            frag.setChoiceNone();
3363        } else if (mIsTablet) {
3364            // How did we ever land here? Detached mode, and no CLF on tablet???
3365            LogUtils.e(LOG_TAG, "AAC.setDetachedMode(): CLF = null!");
3366        }
3367        mDetachedConvUri = mCurrentConversation.uri;
3368    }
3369
3370    private void clearDetachedMode() {
3371        // Tell the conversation list to go back to its usual selection behavior.
3372        final ConversationListFragment frag = getConversationListFragment();
3373        if (frag != null) {
3374            frag.revertChoiceMode();
3375        } else if (mIsTablet) {
3376            // How did we ever land here? Detached mode, and no CLF on tablet???
3377            LogUtils.e(LOG_TAG, "AAC.clearDetachedMode(): CLF = null on tablet!");
3378        }
3379        mDetachedConvUri = null;
3380    }
3381
3382}
3383