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