AbstractActivityController.java revision a8ead90ce1e6c66e4ecacdf7cfa25c2cafc9bb3b
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.Fragment;
26import android.app.FragmentManager;
27import android.app.LoaderManager;
28import android.app.SearchManager;
29import android.content.ComponentName;
30import android.content.ContentResolver;
31import android.content.Context;
32import android.content.CursorLoader;
33import android.content.DialogInterface;
34import android.content.Intent;
35import android.content.Loader;
36import android.content.pm.PackageManager;
37import android.content.res.Resources;
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.widget.AbsListView;
55import android.widget.AbsListView.OnScrollListener;
56import android.widget.Toast;
57
58import com.android.mail.ConversationListContext;
59import com.android.mail.R;
60import com.android.mail.browse.ConversationCursor;
61import com.android.mail.browse.ConversationPagerController;
62import com.android.mail.browse.SelectedConversationsActionMenu;
63import com.android.mail.compose.ComposeActivity;
64import com.android.mail.providers.Account;
65import com.android.mail.providers.Conversation;
66import com.android.mail.providers.Folder;
67import com.android.mail.providers.MailAppProvider;
68import com.android.mail.providers.Settings;
69import com.android.mail.providers.SuggestionsProvider;
70import com.android.mail.providers.UIProvider;
71import com.android.mail.providers.UIProvider.AccountCapabilities;
72import com.android.mail.providers.UIProvider.AccountCursorExtraKeys;
73import com.android.mail.providers.UIProvider.AutoAdvance;
74import com.android.mail.providers.UIProvider.ConversationColumns;
75import com.android.mail.providers.UIProvider.FolderCapabilities;
76import com.android.mail.ui.ActionableToastBar.ActionClickedListener;
77import com.android.mail.utils.LogTag;
78import com.android.mail.utils.LogUtils;
79import com.android.mail.utils.Utils;
80import com.google.common.collect.ImmutableList;
81import com.google.common.collect.Lists;
82import com.google.common.collect.Sets;
83
84import org.json.JSONException;
85
86import java.util.ArrayList;
87import java.util.Collection;
88import java.util.Set;
89import java.util.TimerTask;
90
91
92/**
93 * This is an abstract implementation of the Activity Controller. This class
94 * knows how to respond to menu items, state changes, layout changes, etc. It
95 * weaves together the views and listeners, dispatching actions to the
96 * respective underlying classes.
97 * <p>
98 * Even though this class is abstract, it should provide default implementations
99 * for most, if not all the methods in the ActivityController interface. This
100 * makes the task of the subclasses easier: OnePaneActivityController and
101 * TwoPaneActivityController can be concise when the common functionality is in
102 * AbstractActivityController.
103 * </p>
104 * <p>
105 * In the Gmail codebase, this was called BaseActivityController
106 * </p>
107 */
108public abstract class AbstractActivityController implements ActivityController {
109    // Keys for serialization of various information in Bundles.
110    /** Tag for {@link #mAccount} */
111    private static final String SAVED_ACCOUNT = "saved-account";
112    /** Tag for {@link #mFolder} */
113    private static final String SAVED_FOLDER = "saved-folder";
114    /** Tag for {@link #mCurrentConversation} */
115    private static final String SAVED_CONVERSATION = "saved-conversation";
116    /** Tag for {@link #mSelectedSet} */
117    private static final String SAVED_SELECTED_SET = "saved-selected-set";
118
119    /** Tag  used when loading a wait fragment */
120    protected static final String TAG_WAIT = "wait-fragment";
121    /** Tag used when loading a conversation list fragment. */
122    public static final String TAG_CONVERSATION_LIST = "tag-conversation-list";
123    /** Tag used when loading a conversation fragment. */
124    public static final String TAG_CONVERSATION = "tag-conversation";
125    /** Tag used when loading a folder list fragment. */
126    protected static final String TAG_FOLDER_LIST = "tag-folder-list";
127
128    protected Account mAccount;
129    protected Folder mFolder;
130    protected MailActionBarView mActionBarView;
131    protected final RestrictedActivity mActivity;
132    protected final Context mContext;
133    private final FragmentManager mFragmentManager;
134    protected final RecentFolderList mRecentFolderList;
135    protected ConversationListContext mConvListContext;
136    protected Conversation mCurrentConversation;
137
138    /** A {@link android.content.BroadcastReceiver} that suppresses new e-mail notifications. */
139    private SuppressNotificationReceiver mNewEmailReceiver = null;
140
141    protected Handler mHandler = new Handler();
142    /**
143     * The current mode of the application. All changes in mode are initiated by
144     * the activity controller. View mode changes are propagated to classes that
145     * attach themselves as listeners of view mode changes.
146     */
147    protected final ViewMode mViewMode;
148    protected ContentResolver mResolver;
149    protected boolean isLoaderInitialized = false;
150    private AsyncRefreshTask mAsyncRefreshTask;
151
152    private final Set<Uri> mCurrentAccountUris = Sets.newHashSet();
153    protected ConversationCursor mConversationListCursor;
154    private final DataSetObservable mConversationListObservable = new DataSetObservable() {
155        @Override
156        public void registerObserver(DataSetObserver observer) {
157            final int count = mObservers.size();
158            super.registerObserver(observer);
159            LogUtils.d(LOG_TAG, "IN AAC.registerListObserver: %s before=%d after=%d", observer,
160                    count, mObservers.size());
161        }
162        @Override
163        public void unregisterObserver(DataSetObserver observer) {
164            final int count = mObservers.size();
165            super.unregisterObserver(observer);
166            LogUtils.d(LOG_TAG, "IN AAC.unregisterListObserver: %s before=%d after=%d", observer,
167                    count, mObservers.size());
168        }
169    };
170    protected boolean mConversationListenerAdded = false;
171
172    private boolean mIsConversationListScrolling = false;
173    private long mConversationListRefreshTime = 0;
174    private RefreshTimerTask mConversationListRefreshTask;
175
176    /** Listeners that are interested in changes to current account settings. */
177    private final ArrayList<Settings.ChangeListener> mSettingsListeners = Lists.newArrayList();
178
179    /**
180     * Selected conversations, if any.
181     */
182    private final ConversationSelectionSet mSelectedSet = new ConversationSelectionSet();
183
184    private final int mFolderItemUpdateDelayMs;
185
186    /** Keeps track of selected and unselected conversations */
187    final protected ConversationPositionTracker mTracker = new ConversationPositionTracker();
188
189    /**
190     * Action menu associated with the selected set.
191     */
192    SelectedConversationsActionMenu mCabActionMenu;
193    protected ActionableToastBar mToastBar;
194    protected ConversationPagerController mPagerController;
195
196    // this is split out from the general loader dispatcher because its loader doesn't return a
197    // basic Cursor
198    private final ConversationListLoaderCallbacks mListCursorCallbacks =
199            new ConversationListLoaderCallbacks();
200
201    protected static final String LOG_TAG = LogTag.getLogTag();
202    /** Constants used to differentiate between the types of loaders. */
203    private static final int LOADER_ACCOUNT_CURSOR = 0;
204    private static final int LOADER_FOLDER_CURSOR = 2;
205    private static final int LOADER_RECENT_FOLDERS = 3;
206    private static final int LOADER_CONVERSATION_LIST = 4;
207    private static final int LOADER_ACCOUNT_INBOX = 5;
208    private static final int LOADER_SEARCH = 6;
209    private static final int LOADER_ACCOUNT_UPDATE_CURSOR = 7;
210
211    private static final int ADD_ACCOUNT_REQUEST_CODE = 1;
212
213    /** The pending destructive action to be carried out before swapping the conversation cursor.*/
214    private DestructiveAction mPendingDestruction;
215    /** Indicates if a conversation view is visible. */
216    private boolean mIsConversationVisible;
217    protected AsyncRefreshTask mFolderSyncTask;
218    // Task for setting any share intents for the account to enabled.
219    // This gets cancelled if the user kills the app before it finishes, and
220    // will just run the next time the user opens the app.
221    private AsyncTask<String, Void, Void> mEnableShareIntents;
222
223    public AbstractActivityController(MailActivity activity, ViewMode viewMode) {
224        mActivity = activity;
225        mFragmentManager = mActivity.getFragmentManager();
226        mViewMode = viewMode;
227        mContext = activity.getApplicationContext();
228        mRecentFolderList = new RecentFolderList(mContext);
229        // Allow the fragment to observe changes to its own selection set. No other object is
230        // aware of the selected set.
231        mSelectedSet.addObserver(this);
232
233        mFolderItemUpdateDelayMs =
234                mContext.getResources().getInteger(R.integer.folder_item_refresh_delay_ms);
235    }
236
237    @Override
238    public Account getCurrentAccount() {
239        return mAccount;
240    }
241
242    @Override
243    public ConversationListContext getCurrentListContext() {
244        return mConvListContext;
245    }
246
247    @Override
248    public String getHelpContext() {
249        return "Mail";
250    }
251
252    @Override
253    public final ConversationCursor getConversationListCursor() {
254        return mConversationListCursor;
255    }
256
257    /**
258     * Check if the fragment is attached to an activity and has a root view.
259     * @param in
260     * @return true if the fragment is valid, false otherwise
261     */
262    private static final boolean isValidFragment(Fragment in) {
263        if (in == null || in.getActivity() == null || in.getView() == null) {
264            return false;
265        }
266        return true;
267    }
268
269    /**
270     * Get the conversation list fragment for this activity. If the conversation list fragment
271     * is not attached, this method returns null
272     * @return
273     */
274    protected ConversationListFragment getConversationListFragment() {
275        final Fragment fragment = mFragmentManager.findFragmentByTag(TAG_CONVERSATION_LIST);
276        if (isValidFragment(fragment)) {
277            return (ConversationListFragment) fragment;
278        }
279        return null;
280    }
281
282    /**
283     * Get the conversation view fragment for this activity. If the conversation view fragment
284     * is not attached, this method returns null
285     * @return
286     */
287    protected ConversationViewFragment getConversationViewFragment() {
288        final Fragment fragment = mFragmentManager.findFragmentByTag(TAG_CONVERSATION);
289        if (isValidFragment(fragment)) {
290            return (ConversationViewFragment) fragment;
291        }
292        return null;
293    }
294
295    /**
296     * Returns the folder list fragment attached with this activity. If no such fragment is attached
297     * this method returns null.
298     * @return
299     */
300    protected FolderListFragment getFolderListFragment() {
301        final Fragment fragment = mFragmentManager.findFragmentByTag(TAG_FOLDER_LIST);
302        if (isValidFragment(fragment)) {
303            return (FolderListFragment) fragment;
304        }
305        return null;
306    }
307
308    /**
309     * Initialize the action bar. This is not visible to OnePaneController and
310     * TwoPaneController so they cannot override this behavior.
311     */
312    private void initializeActionBar() {
313        final ActionBar actionBar = mActivity.getActionBar();
314        if (actionBar == null) {
315            return;
316        }
317
318        // be sure to inherit from the ActionBar theme when inflating
319        final LayoutInflater inflater = LayoutInflater.from(actionBar.getThemedContext());
320        final boolean isSearch = mActivity.getIntent() != null
321                && Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction());
322        mActionBarView = (MailActionBarView) inflater.inflate(
323                isSearch ? R.layout.search_actionbar_view : R.layout.actionbar_view, null);
324        // Why have a different variable for the same thing? We should apply
325        // the same actions
326        // on mActionBarView instead.
327        mActionBarView.initialize(mActivity, this, mViewMode, actionBar, mRecentFolderList);
328    }
329
330    /**
331     * Attach the action bar to the activity.
332     */
333    private void attachActionBar() {
334        final ActionBar actionBar = mActivity.getActionBar();
335        if (actionBar != null && mActionBarView != null) {
336            actionBar.setCustomView(mActionBarView, new ActionBar.LayoutParams(
337                    LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
338            actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM,
339                    ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_SHOW_TITLE);
340            mActionBarView.attach();
341        }
342        mViewMode.addListener(mActionBarView);
343    }
344
345    /**
346     * Returns whether the conversation list fragment is visible or not.
347     * Different layouts will have their own notion on the visibility of
348     * fragments, so this method needs to be overriden.
349     *
350     * @return
351     */
352    protected abstract boolean isConversationListVisible();
353
354    /**
355     * Switch the current account to the one provided as an argument to the method.
356     * @param account
357     */
358    private void switchAccount(Account account){
359        // Current account is different from the new account, restart loaders and show
360        // the account Inbox.
361        mAccount = account;
362        LogUtils.d(LOG_TAG, "AbstractActivityController.switchAccount(): mAccount = %s",
363                mAccount.uri);
364        cancelRefreshTask();
365        onSettingsChanged(mAccount.settings);
366        mActionBarView.setAccount(mAccount);
367        loadAccountInbox();
368
369        mRecentFolderList.setCurrentAccount(account);
370        restartOptionalLoader(LOADER_RECENT_FOLDERS);
371        mActivity.invalidateOptionsMenu();
372        disableNotificationsOnAccountChange(mAccount);
373        restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR);
374        MailAppProvider.getInstance().setLastViewedAccount(mAccount.uri.toString());
375    }
376
377    @Override
378    public void onAccountChanged(Account account) {
379        LogUtils.d(LOG_TAG, "onAccountChanged (%s) called.", account.uri);
380        final boolean accountChanged = (mAccount == null) || !account.uri.equals(mAccount.uri);
381        if (accountChanged) {
382            switchAccount(account);
383            return;
384        }
385        // Current account is the same as the new account, but the settings might be different.
386        if (!account.settings.equals(mAccount.settings)){
387            onSettingsChanged(account.settings);
388            return;
389        }
390    }
391
392    /**
393     * Changes the settings for the current account. The new settings are provided as a parameter.
394     * @param settings
395     */
396    public void onSettingsChanged(Settings settings) {
397        dispatchSettingsChange(settings);
398        resetActionBarIcon();
399        mActivity.invalidateOptionsMenu();
400        // If the user was viewing the default Inbox here, and the new setting contains a different
401        // default Inbox, we don't want to load a different folder here.
402    }
403
404    @Override
405    public Settings getSettings() {
406        return mAccount.settings;
407    }
408
409    /**
410     * Adds a listener interested in change in settings. If a class is storing a reference to
411     * Settings, it should listen on changes, so it can receive updates to settings.
412     * Must happen in the UI thread.
413     */
414    public void addSettingsListener(Settings.ChangeListener listener) {
415        mSettingsListeners.add(listener);
416    }
417
418    /**
419     * Removes a listener from receiving settings changes.
420     * Must happen in the UI thread.
421     */
422    public void removeSettingsListener(Settings.ChangeListener listener) {
423        mSettingsListeners.remove(listener);
424    }
425
426    /**
427     * Method that lets the settings listeners know when the settings got changed.
428     */
429    private void dispatchSettingsChange(Settings updatedSettings) {
430        // Copy the list of current listeners so that
431        final ArrayList<Settings.ChangeListener> allListeners =
432                new ArrayList<Settings.ChangeListener>(mSettingsListeners);
433        for (Settings.ChangeListener listener : allListeners) {
434            if (listener != null) {
435                listener.onSettingsChanged(updatedSettings);
436            }
437        }
438        // And we know that the ConversationListFragment is interested in changes to settings,
439        // though it hasn't registered itself with us.
440        final ConversationListFragment convList = getConversationListFragment();
441        if (convList != null) {
442            convList.onSettingsChanged(updatedSettings);
443        }
444    }
445
446    private void fetchSearchFolder(Intent intent) {
447        Bundle args = new Bundle();
448        args.putString(ConversationListContext.EXTRA_SEARCH_QUERY, intent
449                .getStringExtra(ConversationListContext.EXTRA_SEARCH_QUERY));
450        mActivity.getLoaderManager().restartLoader(LOADER_SEARCH, args, this);
451    }
452
453    @Override
454    public void onFolderChanged(Folder folder) {
455        if (folder != null && !folder.equals(mFolder)) {
456            updateFolder(folder);
457            mConvListContext = ConversationListContext.forFolder(mContext, mAccount, mFolder);
458            showConversationList(mConvListContext);
459
460            // Add the folder that we were viewing to the recent folders list.
461            // TODO: this may need to be fine tuned.  If this is the signal that is indicating that
462            // the list is shown to the user, this could fire in one pane if the user goes directly
463            // to a conversation
464            updateRecentFolderList();
465            cancelRefreshTask();
466        }
467    }
468
469    @Override
470    public void onFolderSelected(Folder folder) {
471        onFolderChanged(folder);
472    }
473
474    /**
475     * Update the recent folders. This only needs to be done once when accessing a new folder.
476     */
477    private void updateRecentFolderList() {
478        if (mFolder != null) {
479            mRecentFolderList.touchFolder(mFolder, mAccount);
480        }
481    }
482
483    // TODO(mindyp): set this up to store a copy of the folder as a transient
484    // field in the account.
485    @Override
486    public void loadAccountInbox() {
487        restartOptionalLoader(LOADER_ACCOUNT_INBOX);
488    }
489
490    /** Set the current folder */
491    private void updateFolder(Folder folder) {
492        // Start watching folder for sync status.
493        if (folder != null && !folder.equals(mFolder)) {
494            LogUtils.d(LOG_TAG, "AbstractActivityController.setFolder(%s)", folder.name);
495            final LoaderManager lm = mActivity.getLoaderManager();
496            mActionBarView.setRefreshInProgress(false);
497            setFolder(folder);
498            mActionBarView.setFolder(mFolder);
499
500            // Only when we switch from one folder to another do we want to restart the
501            // folder and conversation list loaders (to trigger onCreateLoader).
502            // The first time this runs when the activity is [re-]initialized, we want to re-use the
503            // previous loader's instance and data upon configuration change (e.g. rotation).
504            // If there was not already an instance of the loader, init it.
505            if (lm.getLoader(LOADER_FOLDER_CURSOR) == null) {
506                lm.initLoader(LOADER_FOLDER_CURSOR, null, this);
507                lm.initLoader(LOADER_CONVERSATION_LIST, null, mListCursorCallbacks);
508            } else {
509                lm.restartLoader(LOADER_FOLDER_CURSOR, null, this);
510                lm.restartLoader(LOADER_CONVERSATION_LIST, null, mListCursorCallbacks);
511            }
512        } else if (folder == null) {
513            LogUtils.wtf(LOG_TAG, "Folder in setFolder is null");
514        }
515    }
516
517    /**
518     * Set the folder that is used for all current operations, including what
519     * conversation list to show (if applicable), what item to select in the
520     * FolderListFragment.
521     *
522     * @param folder
523     */
524    public void setFolder(Folder folder) {
525        mFolder = folder;
526    }
527
528    @Override
529    public Folder getFolder() {
530        return mFolder;
531    }
532
533    @Override
534    public void onActivityResult(int requestCode, int resultCode, Intent data) {
535        if (requestCode == ADD_ACCOUNT_REQUEST_CODE) {
536            // We were waiting for the user to create an account
537            if (resultCode == Activity.RESULT_OK) {
538                // restart the loader to get the updated list of accounts
539                mActivity.getLoaderManager().initLoader(
540                        LOADER_ACCOUNT_CURSOR, null, this);
541            } else {
542                // The user failed to create an account, just exit the app
543                mActivity.finish();
544            }
545        }
546    }
547
548    @Override
549    public void onConversationListVisibilityChanged(boolean visible) {
550        if (mConversationListCursor != null) {
551            // The conversation list is visible.
552            Utils.setConversationCursorVisibility(mConversationListCursor, visible);
553        }
554    }
555
556    /**
557     * Called when a conversation is visible. Child classes must call the super class implementation
558     * before performing local computation.
559     */
560    @Override
561    public void onConversationVisibilityChanged(boolean visible) {
562        mIsConversationVisible = visible;
563        return;
564    }
565
566    @Override
567    public boolean onCreate(Bundle savedState) {
568        initializeActionBar();
569        // Allow shortcut keys to function for the ActionBar and menus.
570        mActivity.setDefaultKeyMode(Activity.DEFAULT_KEYS_SHORTCUT);
571        mResolver = mActivity.getContentResolver();
572        mNewEmailReceiver = new SuppressNotificationReceiver();
573
574        // All the individual UI components listen for ViewMode changes. This
575        // simplifies the amount of logic in the AbstractActivityController, but increases the
576        // possibility of timing-related bugs.
577        mViewMode.addListener(this);
578        mPagerController = new ConversationPagerController(mActivity, this);
579        mToastBar = (ActionableToastBar) mActivity.findViewById(R.id.toast_bar);
580        attachActionBar();
581
582        final Intent intent = mActivity.getIntent();
583        // Immediately handle a clean launch with intent, and any state restoration
584        // that does not rely on restored fragments or loader data
585        // any state restoration that relies on those can be done later in
586        // onRestoreInstanceState, once fragments are up and loader data is re-delivered
587        if (savedState != null) {
588            if (savedState.containsKey(SAVED_ACCOUNT)) {
589                setAccount((Account) savedState.getParcelable(SAVED_ACCOUNT));
590                mActivity.invalidateOptionsMenu();
591            }
592            if (savedState.containsKey(SAVED_FOLDER)) {
593                // Open the folder.
594                onFolderChanged((Folder) savedState.getParcelable(SAVED_FOLDER));
595            }
596        } else if (intent != null) {
597            handleIntent(intent);
598        }
599        // Create the accounts loader; this loads the account switch spinner.
600        mActivity.getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this);
601        return true;
602    }
603
604    @Override
605    public Dialog onCreateDialog(int id, Bundle bundle) {
606        return null;
607    }
608
609    @Override
610    public final boolean onCreateOptionsMenu(Menu menu) {
611        MenuInflater inflater = mActivity.getMenuInflater();
612        inflater.inflate(mActionBarView.getOptionsMenuId(), menu);
613        mActionBarView.onCreateOptionsMenu(menu);
614        return true;
615    }
616
617    @Override
618    public final boolean onKeyDown(int keyCode, KeyEvent event) {
619        // TODO(viki): Auto-generated method stub
620        return false;
621    }
622
623    @Override
624    public final boolean onOptionsItemSelected(MenuItem item) {
625        final int id = item.getItemId();
626        LogUtils.d(LOG_TAG, "AbstractController.onOptionsItemSelected(%d) called.", id);
627        boolean handled = true;
628        final Collection<Conversation> target = Conversation.listOf(mCurrentConversation);
629        final Settings settings = (mAccount == null) ? null : mAccount.settings;
630        switch (id) {
631            case R.id.archive: {
632                final boolean showDialog = (settings != null && settings.confirmArchive);
633                confirmAndDelete(target, showDialog, R.plurals.confirm_archive_conversation,
634                        getAction(R.id.archive, target));
635                break;
636            }
637            case R.id.delete: {
638                final boolean showDialog = (settings != null && settings.confirmDelete);
639                confirmAndDelete(target, showDialog, R.plurals.confirm_delete_conversation,
640                        getAction(R.id.delete, target));
641                break;
642            }
643            case R.id.mark_important:
644                updateConversation(Conversation.listOf(mCurrentConversation),
645                        ConversationColumns.PRIORITY, UIProvider.ConversationPriority.HIGH);
646                break;
647            case R.id.mark_not_important:
648                updateConversation(Conversation.listOf(mCurrentConversation),
649                        ConversationColumns.PRIORITY, UIProvider.ConversationPriority.LOW);
650                break;
651            case R.id.mute:
652                delete(target, getAction(R.id.mute, target));
653                break;
654            case R.id.report_spam:
655                delete(target, getAction(R.id.report_spam, target));
656                break;
657            case R.id.mark_not_spam:
658                // Currently, since spam messages are only shown in list with other spam messages,
659                // marking a message not as spam is a destructive action
660                delete(target, getAction(R.id.mark_not_spam, target));
661                break;
662            case R.id.inside_conversation_unread:
663                // TODO(viki): This is strange, and potentially incorrect. READ is an int column
664                // in the provider.
665                updateConversation(Conversation.listOf(mCurrentConversation),
666                        ConversationColumns.READ, false);
667                mViewMode.enterConversationListMode();
668                break;
669            case android.R.id.home:
670                onUpPressed();
671                break;
672            case R.id.compose:
673                ComposeActivity.compose(mActivity.getActivityContext(), mAccount);
674                break;
675            case R.id.show_all_folders:
676                showFolderList();
677                break;
678            case R.id.refresh:
679                requestFolderRefresh();
680                break;
681            case R.id.settings:
682                Utils.showSettings(mActivity.getActivityContext(), mAccount);
683                break;
684            case R.id.folder_options:
685                Utils.showFolderSettings(mActivity.getActivityContext(), mAccount, mFolder);
686                break;
687            case R.id.help_info_menu_item:
688                // TODO: enable context sensitive help
689                Utils.showHelp(mActivity.getActivityContext(), mAccount, null);
690                break;
691            case R.id.feedback_menu_item:
692                Utils.sendFeedback(mActivity.getActivityContext(), mAccount);
693                break;
694            case R.id.manage_folders_item:
695                Utils.showManageFolder(mActivity.getActivityContext(), mAccount);
696                break;
697            case R.id.change_folder:
698                new FoldersSelectionDialog(mActivity.getActivityContext(), mAccount, this,
699                        Conversation.listOf(mCurrentConversation), false).show();
700                break;
701            default:
702                handled = false;
703                break;
704        }
705        return handled;
706    }
707
708    @Override
709    public void updateConversation(Collection <Conversation> target, String columnName,
710            boolean value) {
711        mConversationListCursor.updateBoolean(mContext, target, columnName, value);
712        refreshConversationList();
713    }
714
715    @Override
716    public void updateConversation(Collection <Conversation> target, String columnName, int value) {
717        mConversationListCursor.updateInt(mContext, target, columnName, value);
718        refreshConversationList();
719    }
720
721    @Override
722    public void updateConversation(Collection <Conversation> target, String columnName,
723            String value) {
724        mConversationListCursor.updateString(mContext, target, columnName, value);
725        refreshConversationList();
726    }
727
728    private void requestFolderRefresh() {
729        if (mFolder != null) {
730            if (mAsyncRefreshTask != null) {
731                mAsyncRefreshTask.cancel(true);
732            }
733            mAsyncRefreshTask = new AsyncRefreshTask(mContext, mFolder.refreshUri);
734            mAsyncRefreshTask.execute();
735        }
736    }
737
738    /**
739     * Confirm (based on user's settings) and delete a conversation from the conversation list and
740     * from the database.
741     * @param target the conversations to act upon
742     * @param showDialog true if a confirmation dialog is to be shown, false otherwise.
743     * @param confirmResource the resource ID of the string that is shown in the confirmation dialog
744     * @param action the action to perform after animating the deletion of the conversations.
745     */
746    protected void confirmAndDelete(final Collection<Conversation> target, boolean showDialog,
747            int confirmResource, final DestructiveAction action) {
748        if (showDialog) {
749            final AlertDialog.OnClickListener onClick = new AlertDialog.OnClickListener() {
750                @Override
751                public void onClick(DialogInterface dialog, int which) {
752                    delete(target, action);
753                }
754            };
755            final CharSequence message = Utils.formatPlural(mContext, confirmResource,
756                    target.size());
757            new AlertDialog.Builder(mActivity.getActivityContext()).setMessage(message)
758                    .setPositiveButton(R.string.ok, onClick)
759                    .setNegativeButton(R.string.cancel, null)
760                    .create().show();
761        } else {
762            delete(target, action);
763        }
764    }
765
766    @Override
767    public void delete(final Collection<Conversation> target, final DestructiveAction action) {
768        // The conversation list handles deletion if it exists.
769        final ConversationListFragment convList = getConversationListFragment();
770        if (convList != null) {
771            LogUtils.d(LOG_TAG, "AAC.requestDelete: ListFragment is handling delete.");
772            convList.requestDelete(target, action);
773            return;
774        }
775        // Update the conversation fragment if the current conversation is deleted.
776        if (getConversationViewFragment() != null &&
777                !Conversation.contains(target, mCurrentConversation)) {
778            final Conversation next = mTracker.getNextConversation(
779                    Settings.getAutoAdvanceSetting(mAccount.settings), target,
780                    mCurrentConversation);
781            LogUtils.d(LOG_TAG, "requestDelete: showing %s next.", next);
782            showConversation(next);
783        }
784        // No visible UI element handled it on our behalf. Perform the action ourself.
785        action.performAction();
786    }
787
788    /**
789     * Requests that the action be performed and the UI state is updated to reflect the new change.
790     * @param target
791     * @param action
792     */
793    private void requestUpdate(final Collection<Conversation> target,
794            final DestructiveAction action) {
795        action.performAction();
796        refreshConversationList();
797    }
798
799    @Override
800    public void onPrepareDialog(int id, Dialog dialog, Bundle bundle) {
801        // TODO(viki): Auto-generated method stub
802    }
803
804    @Override
805    public boolean onPrepareOptionsMenu(Menu menu) {
806        mActionBarView.onPrepareOptionsMenu(menu);
807        return true;
808    }
809
810    @Override
811    public void onPause() {
812        isLoaderInitialized = false;
813        enableNotifications();
814        commitLeaveBehindItems();
815    }
816
817    @Override
818    public void onResume() {
819        // Register the receiver that will prevent the status receiver from
820        // displaying its notification icon as long as we're running.
821        // The SupressNotificationReceiver will block the broadcast if we're looking at the folder
822        // that the notification was received for.
823        disableNotifications();
824
825        if (mActionBarView != null) {
826            mActionBarView.onResume();
827        }
828
829    }
830
831    @Override
832    public void onSaveInstanceState(Bundle outState) {
833        if (mAccount != null) {
834            LogUtils.d(LOG_TAG, "Saving the account now");
835            outState.putParcelable(SAVED_ACCOUNT, mAccount);
836        }
837        if (mFolder != null) {
838            outState.putParcelable(SAVED_FOLDER, mFolder);
839        }
840        if (mCurrentConversation != null
841                && (mViewMode.getMode() == ViewMode.CONVERSATION ||
842                mViewMode.getMode() == ViewMode.SEARCH_RESULTS_CONVERSATION)) {
843            outState.putParcelable(SAVED_CONVERSATION, mCurrentConversation);
844        }
845        if (!mSelectedSet.isEmpty()) {
846            outState.putParcelable(SAVED_SELECTED_SET, mSelectedSet);
847        }
848    }
849
850    @Override
851    public void onSearchRequested(String query) {
852        Intent intent = new Intent();
853        intent.setAction(Intent.ACTION_SEARCH);
854        intent.putExtra(ConversationListContext.EXTRA_SEARCH_QUERY, query);
855        intent.putExtra(Utils.EXTRA_ACCOUNT, mAccount);
856        intent.setComponent(mActivity.getComponentName());
857        mActionBarView.collapseSearch();
858        mActivity.startActivity(intent);
859    }
860
861    @Override
862    public void onStop() {
863        if (mEnableShareIntents != null) {
864            mEnableShareIntents.cancel(true);
865        }
866    }
867
868    @Override
869    public void onDestroy() {
870        // unregister the ViewPager's observer on the conversation cursor
871        mPagerController.onDestroy();
872    }
873
874    /**
875     * {@inheritDoc} Subclasses must override this to listen to mode changes
876     * from the ViewMode. Subclasses <b>must</b> call the parent's
877     * onViewModeChanged since the parent will handle common state changes.
878     */
879    @Override
880    public void onViewModeChanged(int newMode) {
881        // Perform any mode specific work here.
882        // reset the action bar icon based on the mode. Why don't the individual
883        // controllers do
884        // this themselves?
885
886        // We don't want to invalidate the options menu when switching to
887        // conversation
888        // mode, as it will happen when the conversation finishes loading.
889        if (newMode != ViewMode.CONVERSATION) {
890            mActivity.invalidateOptionsMenu();
891        }
892    }
893
894    protected void commitLeaveBehindItems() {
895        ConversationListFragment fragment = getConversationListFragment();
896        if (fragment != null) {
897            fragment.commitLeaveBehindItems();
898        }
899    }
900
901    @Override
902    public void onWindowFocusChanged(boolean hasFocus) {
903        ConversationListFragment convList = getConversationListFragment();
904        if (hasFocus && convList != null && convList.isVisible()) {
905            // The conversation list is visible.
906            Utils.setConversationCursorVisibility(mConversationListCursor, true);
907        }
908    }
909
910    private void setAccount(Account account) {
911        if (account == null) {
912            LogUtils.w(LOG_TAG, new Error(),
913                    "AAC ignoring null (presumably invalid) account restoration");
914            return;
915        }
916        LogUtils.d(LOG_TAG, "AbstractActivityController.setAccount(): account = %s", account.uri);
917        mAccount = account;
918        mActionBarView.setAccount(mAccount);
919        if (account.settings == null) {
920            LogUtils.w(LOG_TAG, new Error(), "AAC ignoring account with null settings.");
921            return;
922        }
923        dispatchSettingsChange(mAccount.settings);
924    }
925
926    /**
927     * Restore the state from the previous bundle. Subclasses should call this
928     * method from the parent class, since it performs important UI
929     * initialization.
930     *
931     * @param savedState
932     */
933    @Override
934    public void onRestoreInstanceState(Bundle savedState) {
935        LogUtils.d(LOG_TAG, "IN AAC.onRestoreInstanceState");
936        if (savedState.containsKey(SAVED_CONVERSATION)) {
937            // Open the conversation.
938            final Conversation conversation =
939                    (Conversation)savedState.getParcelable(SAVED_CONVERSATION);
940            if (conversation != null && conversation.position < 0) {
941                // Set the position to 0 on this conversation, as we don't know where it is
942                // in the list
943                conversation.position = 0;
944            }
945            setCurrentConversation(conversation);
946            showConversation(mCurrentConversation);
947        }
948
949        /**
950         * Restore the state of selected conversations. This needs to be done after the correct mode
951         * is set and the action bar is fully initialized. If not, several key pieces of state
952         * information will be missing, and the split views may not be initialized correctly.
953         * @param savedState
954         */
955        restoreSelectedConversations(savedState);
956    }
957
958    private void handleIntent(Intent intent) {
959        boolean handled = false;
960        if (Intent.ACTION_VIEW.equals(intent.getAction())) {
961            if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) {
962                setAccount(Account.newinstance(intent
963                        .getStringExtra(Utils.EXTRA_ACCOUNT)));
964            }
965            if (mAccount == null) {
966                return;
967            }
968            mActivity.invalidateOptionsMenu();
969
970            Folder folder = null;
971            if (intent.hasExtra(Utils.EXTRA_FOLDER)) {
972                // Open the folder.
973                try {
974                    folder = Folder
975                            .fromJSONString(intent.getStringExtra(Utils.EXTRA_FOLDER));
976                } catch (JSONException e) {
977                    LogUtils.wtf(LOG_TAG, e, "Unable to parse folder extra");
978                }
979            }
980            if (folder != null) {
981                onFolderChanged(folder);
982                handled = true;
983            }
984
985            if (intent.hasExtra(Utils.EXTRA_CONVERSATION)) {
986                // Open the conversation.
987                LogUtils.d(LOG_TAG, "SHOW THE CONVERSATION at %s",
988                        intent.getParcelableExtra(Utils.EXTRA_CONVERSATION));
989                final Conversation conversation =
990                        (Conversation)intent.getParcelableExtra(Utils.EXTRA_CONVERSATION);
991                if (conversation != null && conversation.position < 0) {
992                    // Set the position to 0 on this conversation, as we don't know where it is
993                    // in the list
994                    conversation.position = 0;
995                }
996                setCurrentConversation(conversation);
997                showConversation(mCurrentConversation);
998                handled = true;
999            }
1000
1001            if (!handled) {
1002                // Nothing was saved; just load the account inbox.
1003                loadAccountInbox();
1004            }
1005        } else if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
1006            if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) {
1007                // Save this search query for future suggestions.
1008                final String query = intent.getStringExtra(SearchManager.QUERY);
1009                final String authority = mContext.getString(R.string.suggestions_authority);
1010                SearchRecentSuggestions suggestions = new SearchRecentSuggestions(
1011                        mContext, authority, SuggestionsProvider.MODE);
1012                suggestions.saveRecentQuery(query, null);
1013                if (Utils.showTwoPaneSearchResults(mActivity.getActivityContext())) {
1014                    mViewMode.enterSearchResultsConversationMode();
1015                } else {
1016                    mViewMode.enterSearchResultsListMode();
1017                }
1018                setAccount((Account) intent.getParcelableExtra(Utils.EXTRA_ACCOUNT));
1019                mActivity.invalidateOptionsMenu();
1020                restartOptionalLoader(LOADER_RECENT_FOLDERS);
1021                mRecentFolderList.setCurrentAccount(mAccount);
1022                fetchSearchFolder(intent);
1023            } else {
1024                LogUtils.e(LOG_TAG, "Missing account extra from search intent.  Finishing");
1025                mActivity.finish();
1026            }
1027        }
1028        if (mAccount != null) {
1029            restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR);
1030        }
1031    }
1032
1033    /**
1034     * Copy any selected conversations stored in the saved bundle into our selection set,
1035     * triggering {@link ConversationSetObserver} callbacks as our selection set changes.
1036     *
1037     */
1038    private final void restoreSelectedConversations(Bundle savedState) {
1039        if (savedState == null) {
1040            mSelectedSet.clear();
1041            return;
1042        }
1043        final ConversationSelectionSet selectedSet = savedState.getParcelable(SAVED_SELECTED_SET);
1044        if (selectedSet == null || selectedSet.isEmpty()) {
1045            mSelectedSet.clear();
1046            return;
1047        }
1048
1049        // putAll will take care of calling our registered onSetPopulated method
1050        mSelectedSet.putAll(selectedSet);
1051    }
1052
1053    @Override
1054    public SubjectDisplayChanger getSubjectDisplayChanger() {
1055        return mActionBarView;
1056    }
1057
1058    /**
1059     * Children can override this method, but they must call super.showConversation().
1060     * {@inheritDoc}
1061     */
1062    @Override
1063    public void showConversation(Conversation conversation) {
1064        // Set the current conversation just in case it wasn't already set.
1065        setCurrentConversation(conversation);
1066    }
1067
1068    /**
1069     * Children can override this method, but they must call super.showWaitForInitialization().
1070     * {@inheritDoc}
1071     */
1072    @Override
1073    public void showWaitForInitialization() {
1074        mViewMode.enterWaitingForInitializationMode();
1075    }
1076
1077    @Override
1078    public void hideWaitForInitialization() {
1079    }
1080
1081    @Override
1082    public void updateWaitMode() {
1083        final FragmentManager manager = mActivity.getFragmentManager();
1084        final WaitFragment waitFragment =
1085                (WaitFragment)manager.findFragmentByTag(TAG_WAIT);
1086        if (waitFragment != null) {
1087            waitFragment.updateAccount(mAccount);
1088        }
1089    }
1090
1091    /**
1092     * Returns true if we are waiting for the account to sync, and cannot show any folders or
1093     * conversation for the current account yet.
1094     * @return
1095     */
1096    public boolean inWaitMode() {
1097        final FragmentManager manager = mActivity.getFragmentManager();
1098        final WaitFragment waitFragment =
1099                (WaitFragment)manager.findFragmentByTag(TAG_WAIT);
1100        if (waitFragment != null) {
1101            final Account fragmentAccount = waitFragment.getAccount();
1102            return fragmentAccount.uri.equals(mAccount.uri) &&
1103                    mViewMode.getMode() == ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION;
1104        }
1105        return false;
1106    }
1107
1108    /**
1109     * Children can override this method, but they must call super.showConversationList().
1110     * {@inheritDoc}
1111     */
1112    @Override
1113    public void showConversationList(ConversationListContext listContext) {
1114    }
1115
1116    @Override
1117    public void onConversationSelected(Conversation conversation) {
1118        showConversation(conversation);
1119        if (Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())) {
1120            mViewMode.enterSearchResultsConversationMode();
1121        } else {
1122            mViewMode.enterConversationMode();
1123        }
1124    }
1125
1126    /**
1127     * Set the current conversation. This is the conversation on which all actions are performed.
1128     * Do not modify mCurrentConversation except through this method, which makes it easy to
1129     * perform common actions associated with changing the current conversation.
1130     * @param conversation
1131     */
1132    @Override
1133    public void setCurrentConversation(Conversation conversation) {
1134        mCurrentConversation = conversation;
1135        mTracker.initialize(mCurrentConversation);
1136    }
1137
1138    /**
1139     * {@inheritDoc}
1140     */
1141    @Override
1142    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
1143        // Create a loader to listen in on account changes.
1144        switch (id) {
1145            case LOADER_ACCOUNT_CURSOR:
1146                return new CursorLoader(mContext, MailAppProvider.getAccountsUri(),
1147                        UIProvider.ACCOUNTS_PROJECTION, null, null, null);
1148            case LOADER_FOLDER_CURSOR:
1149                final CursorLoader loader = new CursorLoader(mContext, mFolder.uri,
1150                        UIProvider.FOLDERS_PROJECTION, null, null, null);
1151                loader.setUpdateThrottle(mFolderItemUpdateDelayMs);
1152                return loader;
1153            case LOADER_RECENT_FOLDERS:
1154                if (mAccount != null && mAccount.recentFolderListUri != null) {
1155                    return new CursorLoader(mContext, mAccount.recentFolderListUri,
1156                            UIProvider.FOLDERS_PROJECTION, null, null, null);
1157                }
1158                break;
1159            case LOADER_ACCOUNT_INBOX:
1160                final Uri defaultInbox = Settings.getDefaultInboxUri(mAccount.settings);
1161                final Uri inboxUri = defaultInbox.equals(Uri.EMPTY) ?
1162                    mAccount.folderListUri : defaultInbox;
1163                LogUtils.d(LOG_TAG, "Loading the default inbox: %s", inboxUri);
1164                if (inboxUri != null) {
1165                    return new CursorLoader(mContext, inboxUri, UIProvider.FOLDERS_PROJECTION, null,
1166                            null, null);
1167                }
1168                break;
1169            case LOADER_SEARCH:
1170                return Folder.forSearchResults(mAccount,
1171                        args.getString(ConversationListContext.EXTRA_SEARCH_QUERY),
1172                        mActivity.getActivityContext());
1173            case LOADER_ACCOUNT_UPDATE_CURSOR:
1174                return new CursorLoader(mContext, mAccount.uri, UIProvider.ACCOUNTS_PROJECTION,
1175                        null, null, null);
1176            default:
1177                LogUtils.wtf(LOG_TAG, "Loader returned unexpected id: %d", id);
1178        }
1179        return null;
1180    }
1181
1182    @Override
1183    public void onLoaderReset(Loader<Cursor> loader) {
1184
1185    }
1186
1187    /**
1188     * {@link LoaderManager} currently has a bug in
1189     * {@link LoaderManager#restartLoader(int, Bundle, android.app.LoaderManager.LoaderCallbacks)}
1190     * where, if a previous onCreateLoader returned a null loader, this method will NPE. Work around
1191     * this bug by destroying any loaders that may have been created as null (essentially because
1192     * they are optional loads, and may not apply to a particular account).
1193     * <p>
1194     * A simple null check before restarting a loader will not work, because that would not
1195     * give the controller a chance to invalidate UI corresponding the prior loader result.
1196     *
1197     * @param id loader ID to safely restart
1198     */
1199    private void restartOptionalLoader(int id) {
1200        final LoaderManager lm = mActivity.getLoaderManager();
1201        lm.destroyLoader(id);
1202        lm.restartLoader(id, Bundle.EMPTY, this);
1203    }
1204
1205    @Override
1206    public void registerConversationListObserver(DataSetObserver observer) {
1207        mConversationListObservable.registerObserver(observer);
1208    }
1209
1210    @Override
1211    public void unregisterConversationListObserver(DataSetObserver observer) {
1212        mConversationListObservable.unregisterObserver(observer);
1213    }
1214
1215    private boolean accountsUpdated(Cursor accountCursor) {
1216        // Check to see if the current account hasn't been set, or the account cursor is empty
1217        if (mAccount == null || !accountCursor.moveToFirst()) {
1218            return true;
1219        }
1220
1221        // Check to see if the number of accounts are different, from the number we saw on the last
1222        // updated
1223        if (mCurrentAccountUris.size() != accountCursor.getCount()) {
1224            return true;
1225        }
1226
1227        // Check to see if the account list is different or if the current account is not found in
1228        // the cursor.
1229        boolean foundCurrentAccount = false;
1230        do {
1231            final Uri accountUri =
1232                    Uri.parse(accountCursor.getString(UIProvider.ACCOUNT_URI_COLUMN));
1233            if (!foundCurrentAccount && mAccount.uri.equals(accountUri)) {
1234                foundCurrentAccount = true;
1235            }
1236
1237            if (!mCurrentAccountUris.contains(accountUri)) {
1238                return true;
1239            }
1240        } while (accountCursor.moveToNext());
1241
1242        // As long as we found the current account, the list hasn't been updated
1243        return !foundCurrentAccount;
1244    }
1245
1246    /**
1247     * Update the accounts on the device. This currently loads the first account
1248     * in the list.
1249     *
1250     * @param loader
1251     * @param accounts cursor into the AccountCache
1252     * @return true if the update was successful, false otherwise
1253     */
1254    private boolean updateAccounts(Loader<Cursor> loader, Cursor accounts) {
1255        if (accounts == null || !accounts.moveToFirst()) {
1256            return false;
1257        }
1258
1259        final Account[] allAccounts = Account.getAllAccounts(accounts);
1260
1261        // Save the uris for the accounts
1262        mCurrentAccountUris.clear();
1263        for (Account account : allAccounts) {
1264            mCurrentAccountUris.add(account.uri);
1265        }
1266
1267        // 1. current account is already set and is in allAccounts -> no-op
1268        // 2. current account is set and is not in allAccounts -> pick first (acct was deleted?)
1269        // 3. saved pref has an account -> pick that one
1270        // 4. otherwise just pick first
1271
1272        Account newAccount = null;
1273
1274        if (mAccount != null) {
1275            if (!mCurrentAccountUris.contains(mAccount.uri)) {
1276                newAccount = allAccounts[0];
1277            } else {
1278                newAccount = mAccount;
1279            }
1280        } else {
1281            final String lastAccountUri = MailAppProvider.getInstance().getLastViewedAccount();
1282            if (lastAccountUri != null) {
1283                for (int i = 0; i < allAccounts.length; i++) {
1284                    final Account acct = allAccounts[i];
1285                    if (lastAccountUri.equals(acct.uri.toString())) {
1286                        newAccount = acct;
1287                        break;
1288                    }
1289                }
1290            }
1291            if (newAccount == null) {
1292                newAccount = allAccounts[0];
1293            }
1294        }
1295
1296        onAccountChanged(newAccount);
1297        mActionBarView.setAccounts(allAccounts);
1298        return (allAccounts.length > 0);
1299    }
1300
1301    private void disableNotifications() {
1302        mNewEmailReceiver.activate(mContext, this);
1303    }
1304
1305    private void enableNotifications() {
1306        mNewEmailReceiver.deactivate();
1307    }
1308
1309    private void disableNotificationsOnAccountChange(Account account) {
1310        // If the new mail suppression receiver is activated for a different account, we want to
1311        // activate it for the new account.
1312        if (mNewEmailReceiver.activated() &&
1313                !mNewEmailReceiver.notificationsDisabledForAccount(account)) {
1314            // Deactivate the current receiver, otherwise multiple receivers may be registered.
1315            mNewEmailReceiver.deactivate();
1316            mNewEmailReceiver.activate(mContext, this);
1317        }
1318    }
1319
1320    /**
1321     * {@inheritDoc}
1322     */
1323    @Override
1324    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
1325        // We want to reinitialize only if we haven't ever been initialized, or
1326        // if the current account has vanished.
1327        if (data == null) {
1328            LogUtils.e(LOG_TAG, "Received null cursor from loader id: %d", loader.getId());
1329        }
1330        switch (loader.getId()) {
1331            case LOADER_ACCOUNT_CURSOR:
1332                // If the account list is not null, and the account list cursor is empty,
1333                // we need to start the specified activity.
1334                if (data != null && data.getCount() == 0) {
1335                    // If an empty cursor is returned, the MailAppProvider is indicating that
1336                    // no accounts have been specified.  We want to navigate to the "add account"
1337                    // activity that will handle the intent returned by the MailAppProvider
1338
1339                    // If the MailAppProvider believes that all accounts have been loaded, and the
1340                    // account list is still empty, we want to prompt the user to add an account
1341                    final Bundle extras = data.getExtras();
1342                    final boolean accountsLoaded =
1343                            extras.getInt(AccountCursorExtraKeys.ACCOUNTS_LOADED) != 0;
1344
1345                    if (accountsLoaded) {
1346                        final Intent noAccountIntent = MailAppProvider.getNoAccountIntent(mContext);
1347                        if (noAccountIntent != null) {
1348                            mActivity.startActivityForResult(noAccountIntent,
1349                                    ADD_ACCOUNT_REQUEST_CODE);
1350                        }
1351                    }
1352                } else {
1353                    final boolean accountListUpdated = accountsUpdated(data);
1354                    if (!isLoaderInitialized || accountListUpdated) {
1355                        isLoaderInitialized = updateAccounts(loader, data);
1356                    }
1357                }
1358                break;
1359            case LOADER_ACCOUNT_UPDATE_CURSOR:
1360                // We have gotten an update for current account.
1361
1362                // Make sure that this is an update for what is the current account
1363                if (data != null && data.moveToFirst()) {
1364                    final Account updatedAccount = new Account(data);
1365
1366                    if (updatedAccount.uri.equals(mAccount.uri)) {
1367                        // Update the controller's reference to the current account
1368                        mAccount = updatedAccount;
1369                        LogUtils.d(LOG_TAG, "AbstractActivityController.onLoadFinished(): "
1370                                + "mAccount = %s", mAccount.uri);
1371                        dispatchSettingsChange(mAccount.settings);
1372
1373                        // Got an update for the current account
1374                        final boolean inWaitingMode = inWaitMode();
1375                        if (!updatedAccount.isAccountIntialized() && !inWaitingMode) {
1376                            // Transition to waiting mode
1377                            showWaitForInitialization();
1378                        } else if (updatedAccount.isAccountIntialized()) {
1379                            if (inWaitingMode) {
1380                                // Dismiss waiting mode
1381                                hideWaitForInitialization();
1382                            }
1383                            initializeShareIntents();
1384                        } else if (!updatedAccount.isAccountIntialized() && inWaitingMode) {
1385                            // Update the WaitFragment's account object
1386                            updateWaitMode();
1387                        }
1388                    } else {
1389                        LogUtils.e(LOG_TAG, "Got update for account: %s with current account: %s",
1390                                updatedAccount.uri, mAccount.uri);
1391                        // We need to restart the loader, so the correct account information will
1392                        // be returned
1393                        restartOptionalLoader(LOADER_ACCOUNT_UPDATE_CURSOR);
1394                    }
1395                }
1396                break;
1397            case LOADER_FOLDER_CURSOR:
1398                // Check status of the cursor.
1399                if (data != null && data.moveToFirst()) {
1400                    Folder folder = new Folder(data);
1401                    if (folder.isSyncInProgress()) {
1402                        mActionBarView.onRefreshStarted();
1403                    } else {
1404                        // Stop the spinner here.
1405                        mActionBarView.onRefreshStopped(folder.lastSyncResult);
1406                    }
1407                    mActionBarView.onFolderUpdated(folder);
1408                    final ConversationListFragment convList = getConversationListFragment();
1409                    if (convList != null) {
1410                        convList.onFolderUpdated(folder);
1411                    }
1412                    LogUtils.d(LOG_TAG, "FOLDER STATUS = %d", folder.syncStatus);
1413                } else {
1414                    LogUtils.d(LOG_TAG, "Unable to get the folder %s",
1415                            mFolder != null ? mAccount.name : "");
1416                }
1417                break;
1418            case LOADER_RECENT_FOLDERS:
1419                // No recent folders and we are running on a phone? Populate the default recents.
1420                if (data != null && data.getCount() == 0 && !Utils.useTabletUI(mContext)) {
1421                    final class PopulateDefault extends AsyncTask<Uri, Void, Void> {
1422                        @Override
1423                        protected Void doInBackground(Uri... uri) {
1424                            // Asking for an update on the URI and ignore the result.
1425                            final ContentResolver resolver = mContext.getContentResolver();
1426                            resolver.update(uri[0], null, null, null);
1427                            return null;
1428                        }
1429                    }
1430                    final Uri uri = mAccount.defaultRecentFolderListUri;
1431                    LogUtils.v(LOG_TAG, "Default recents at %s", uri);
1432                    new PopulateDefault().execute(uri);
1433                    break;
1434                }
1435                LogUtils.v(LOG_TAG, "Reading recent folders from the cursor.");
1436                mRecentFolderList.loadFromUiProvider(data);
1437                mActionBarView.requestRecentFoldersAndRedraw();
1438                break;
1439            case LOADER_ACCOUNT_INBOX:
1440                if (data != null && !data.isClosed() && data.moveToFirst()) {
1441                    Folder inbox = new Folder(data);
1442                    onFolderChanged(inbox);
1443                    // Just want to get the inbox, don't care about updates to it
1444                    // as this will be tracked by the folder change listener.
1445                    mActivity.getLoaderManager().destroyLoader(LOADER_ACCOUNT_INBOX);
1446                } else {
1447                    LogUtils.d(LOG_TAG, "Unable to get the account inbox for account %s",
1448                            mAccount != null ? mAccount.name : "");
1449                }
1450                break;
1451            case LOADER_SEARCH:
1452                data.moveToFirst();
1453                Folder search = new Folder(data);
1454                updateFolder(search);
1455                mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder,
1456                        mActivity.getIntent()
1457                                .getStringExtra(UIProvider.SearchQueryParameters.QUERY));
1458                showConversationList(mConvListContext);
1459                mActivity.invalidateOptionsMenu();
1460                mActivity.getLoaderManager().destroyLoader(LOADER_SEARCH);
1461                break;
1462        }
1463    }
1464
1465    private void initializeShareIntents() {
1466        Resources res = mContext.getResources();
1467        String composeName = res.getString(R.string.compose_component_name);
1468        initializeComponent(composeName);
1469        String autoSendName = res.getString(R.string.autosend_component_name);
1470        initializeComponent(autoSendName);
1471    }
1472
1473    private void initializeComponent(String name) {
1474        if (!TextUtils.isEmpty(name)) {
1475            final PackageManager pm = mContext.getPackageManager();
1476            final ComponentName component = new ComponentName(mContext, name);
1477            if (pm.getComponentEnabledSetting(component)
1478                    != PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
1479                mEnableShareIntents = new AsyncTask<String, Void, Void>() {
1480                    @Override
1481                    protected Void doInBackground(String... args) {
1482                        pm.setComponentEnabledSetting(component,
1483                                PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
1484                                PackageManager.DONT_KILL_APP);
1485                        return null;
1486                    }
1487                }.execute(name);
1488            }
1489        }
1490    }
1491
1492    /**
1493     * Destructive actions on Conversations. This class should only be created by controllers, and
1494     * clients should only require {@link DestructiveAction}s, not specific implementations of the.
1495     * Only the controllers should know what kind of destructive actions are being created.
1496     */
1497    private class ConversationAction implements DestructiveAction {
1498        /**
1499         * The action to be performed. This is specified as the resource ID of the menu item
1500         * corresponding to this action: R.id.delete, R.id.report_spam, etc.
1501         */
1502        private final int mAction;
1503        /** The action will act upon these conversations */
1504        private final Collection<Conversation> mTarget;
1505        /** Whether this destructive action has already been performed */
1506        private boolean mCompleted;
1507        /** Whether this is an action on the currently selected set. */
1508        private final boolean mIsSelectedSet;
1509
1510        /**
1511         * Create a listener object. action is one of four constants: R.id.y_button (archive),
1512         * R.id.delete , R.id.mute, and R.id.report_spam.
1513         * @param action
1514         * @param target Conversation that we want to apply the action to.
1515         * @param isBatch whether the conversations are in the currently selected batch set.
1516         */
1517        public ConversationAction(int action, Collection<Conversation> target, boolean isBatch) {
1518            mAction = action;
1519            mTarget = ImmutableList.copyOf(target);
1520            mIsSelectedSet = isBatch;
1521        }
1522
1523        /**
1524         * The action common to child classes. This performs the action specified in the constructor
1525         * on the conversations given here.
1526         */
1527        @Override
1528        public void performAction() {
1529            if (isPerformed()) {
1530                return;
1531            }
1532            // Certain actions force a return to list.
1533            boolean undoEnabled = mAccount.supportsCapability(AccountCapabilities.UNDO);
1534
1535            // Are we destroying the currently shown conversation? Show the next one.
1536            if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)){
1537                LogUtils.d(LOG_TAG, "ConversationAction.performAction(): mIsConversationVisible=%b"
1538                        + "\nmTarget=%s\nCurrent=%s", mIsConversationVisible,
1539                        Conversation.toString(mTarget), mCurrentConversation);
1540            }
1541            if (mIsConversationVisible && Conversation.contains(mTarget, mCurrentConversation)) {
1542                int advance = Settings.getAutoAdvanceSetting(mAccount.settings);
1543                final Conversation next = advance == AutoAdvance.LIST ? null : mTracker
1544                        .getNextConversation(advance, mTarget, mCurrentConversation);
1545                LogUtils.d(LOG_TAG, "Next conversation is: %s", next);
1546                showConversation(next);
1547            }
1548
1549            switch (mAction) {
1550                case R.id.archive:
1551                    LogUtils.d(LOG_TAG, "Archiving");
1552                    mConversationListCursor.archive(mContext, mTarget);
1553                    break;
1554                case R.id.delete:
1555                    LogUtils.d(LOG_TAG, "Deleting");
1556                    mConversationListCursor.delete(mContext, mTarget);
1557                    if (mFolder.supportsCapability(FolderCapabilities.DELETE_ACTION_FINAL)) {
1558                        undoEnabled = false;
1559                    }
1560                    break;
1561                case R.id.mute:
1562                    LogUtils.d(LOG_TAG, "Muting");
1563                    if (mFolder.supportsCapability(FolderCapabilities.DESTRUCTIVE_MUTE)) {
1564                        for (Conversation c : mTarget) {
1565                            c.localDeleteOnUpdate = true;
1566                        }
1567                    }
1568                    mConversationListCursor.mute(mContext, mTarget);
1569                    break;
1570                case R.id.report_spam:
1571                    LogUtils.d(LOG_TAG, "Reporting spam");
1572                    mConversationListCursor.reportSpam(mContext, mTarget);
1573                    break;
1574                case R.id.mark_not_spam:
1575                    LogUtils.d(LOG_TAG, "Marking not spam");
1576                    mConversationListCursor.reportNotSpam(mContext, mTarget);
1577                    break;
1578                case R.id.remove_star:
1579                    LogUtils.d(LOG_TAG, "Removing star");
1580                    // Star removal is destructive in the Starred folder.
1581                    mConversationListCursor.updateBoolean(mContext, mTarget,
1582                            ConversationColumns.STARRED, false);
1583                    break;
1584                case R.id.mark_not_important:
1585                    LogUtils.d(LOG_TAG, "Marking not-important");
1586                    // Marking not important is destructive in a mailbox containing only important
1587                    // messages
1588                    mConversationListCursor.updateInt(mContext, mTarget,
1589                            ConversationColumns.PRIORITY, UIProvider.ConversationPriority.LOW);
1590                    break;
1591            }
1592            if (undoEnabled) {
1593                onUndoAvailable(new UndoOperation(mTarget.size(), mAction));
1594            }
1595            refreshConversationList();
1596            if (mIsSelectedSet) {
1597                mSelectedSet.clear();
1598            }
1599        }
1600
1601        /**
1602         * Returns true if this action has been performed, false otherwise.
1603         * @return
1604         */
1605        private synchronized boolean isPerformed() {
1606            if (mCompleted) {
1607                return true;
1608            }
1609            mCompleted = true;
1610            return false;
1611        }
1612    }
1613
1614    /**
1615     * Get a destructive action for a menu action.
1616     * This is a temporary method, to control the profusion of {@link DestructiveAction} classes
1617     * that are created. Please do not copy this paradigm.
1618     * @param action the resource ID of the menu action: R.id.delete, for example
1619     * @param target the conversations to act upon.
1620     * @return a {@link DestructiveAction} that performs the specified action.
1621     */
1622    private final DestructiveAction getAction(int action, Collection<Conversation> target) {
1623        final DestructiveAction da = new ConversationAction(action, target, false);
1624        registerDestructiveAction(da);
1625        return da;
1626    }
1627
1628    // Called from the FolderSelectionDialog after a user is done selecting folders to assign the
1629    // conversations to.
1630    @Override
1631    public final void assignFolder(
1632            Collection<Folder> folders, Collection<Conversation> target, boolean batch) {
1633        // Actions are destructive only when the current folder can be assigned to (which is the
1634        // same as being able to un-assign a conversation from the folder) and when the list of
1635        // folders contains the current folder.
1636        final boolean isDestructive =
1637                mFolder.supportsCapability(FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES) &&
1638                !Folder.containerIncludes(folders, mFolder);
1639        LogUtils.d(LOG_TAG, "onFolderChangesCommit: isDestructive = %b", isDestructive);
1640        if (isDestructive) {
1641            for (final Conversation c : target) {
1642                c.localDeleteOnUpdate = true;
1643            }
1644        }
1645        final DestructiveAction folderChange = getFolderChange(target, folders, isDestructive,
1646                batch);
1647        // Update the UI elements depending no their visibility and availability
1648        // TODO(viki): Consolidate this into a single method requestDelete.
1649        if (isDestructive) {
1650            delete(target, folderChange);
1651        } else {
1652            requestUpdate(target, folderChange);
1653        }
1654    }
1655
1656    @Override
1657    public final void onRefreshRequired() {
1658        if (mIsConversationListScrolling) {
1659            LogUtils.d(LOG_TAG, "onRefreshRequired: delay until scrolling done");
1660            return;
1661        }
1662        // Refresh the query in the background
1663        final long now = System.currentTimeMillis();
1664        final long sinceLastRefresh = now - mConversationListRefreshTime;
1665            if (mConversationListCursor.isRefreshRequired()) {
1666                mConversationListCursor.refresh();
1667                mTracker.updateCursor(mConversationListCursor);
1668                mConversationListRefreshTime = now;
1669            }
1670    }
1671
1672    /**
1673     * Called when the {@link ConversationCursor} is changed or has new data in it.
1674     * <p>
1675     * {@inheritDoc}
1676     */
1677    @Override
1678    public final void onRefreshReady() {
1679        if (!mIsConversationListScrolling) {
1680            // Swap cursors
1681            mConversationListCursor.sync();
1682        }
1683        mTracker.updateCursor(mConversationListCursor);
1684    }
1685
1686    @Override
1687    public final void onDataSetChanged() {
1688        updateConversationListFragment();
1689        mConversationListObservable.notifyChanged();
1690    }
1691
1692    /**
1693     * If the Conversation List Fragment is visible, updates the fragment.
1694     */
1695    private final void updateConversationListFragment() {
1696        final ConversationListFragment convList = getConversationListFragment();
1697        if (convList != null) {
1698            refreshConversationList();
1699            if (convList.isVisible()) {
1700                Utils.setConversationCursorVisibility(mConversationListCursor, true);
1701            }
1702        }
1703    }
1704
1705    /**
1706     * This class handles throttled refresh of the conversation list
1707     */
1708    static class RefreshTimerTask extends TimerTask {
1709        final Handler mHandler;
1710        final AbstractActivityController mController;
1711
1712        RefreshTimerTask(AbstractActivityController controller, Handler handler) {
1713            mHandler = handler;
1714            mController = controller;
1715        }
1716
1717        @Override
1718        public void run() {
1719            mHandler.post(new Runnable() {
1720                @Override
1721                public void run() {
1722                    LogUtils.d(LOG_TAG, "Delay done... calling onRefreshRequired");
1723                    mController.onRefreshRequired();
1724                }});
1725        }
1726    }
1727
1728    /**
1729     * Cancel the refresh task, if it's running
1730     */
1731    private void cancelRefreshTask () {
1732        if (mConversationListRefreshTask != null) {
1733            mConversationListRefreshTask.cancel();
1734            mConversationListRefreshTask = null;
1735        }
1736    }
1737
1738    @Override
1739    public void onScrollStateChanged(AbsListView view, int scrollState) {
1740        boolean isScrolling = (scrollState != OnScrollListener.SCROLL_STATE_IDLE);
1741        if (!isScrolling) {
1742            if (mConversationListCursor.isRefreshRequired()) {
1743                LogUtils.d(LOG_TAG, "Stop scrolling: refresh");
1744                mConversationListCursor.refresh();
1745            } else if (mConversationListCursor.isRefreshReady()) {
1746                LogUtils.d(LOG_TAG, "Stop scrolling: try sync");
1747                onRefreshReady();
1748            }
1749        }
1750        mIsConversationListScrolling = isScrolling;
1751    }
1752
1753    @Override
1754    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
1755            int totalItemCount) {
1756    }
1757
1758    @Override
1759    public void onSetEmpty() {
1760    }
1761
1762    @Override
1763    public void onSetPopulated(ConversationSelectionSet set) {
1764        final ConversationListFragment convList = getConversationListFragment();
1765        if (convList == null) {
1766            return;
1767        }
1768        mCabActionMenu = new SelectedConversationsActionMenu(mActivity, set, mAccount, mFolder,
1769                (SwipeableListView) convList.getListView());
1770        enableCabMode();
1771    }
1772
1773    @Override
1774    public void onSetChanged(ConversationSelectionSet set) {
1775        // Do nothing. We don't care about changes to the set.
1776    }
1777
1778    @Override
1779    public ConversationSelectionSet getSelectedSet() {
1780        return mSelectedSet;
1781    }
1782
1783    /**
1784     * Disable the Contextual Action Bar (CAB). The selected set is not changed.
1785     */
1786    protected void disableCabMode() {
1787        if (mCabActionMenu != null) {
1788            mCabActionMenu.deactivate();
1789        }
1790    }
1791
1792    /**
1793     * Re-enable the CAB menu if required. The selection set is not changed.
1794     */
1795    protected void enableCabMode() {
1796        if (mCabActionMenu != null) {
1797            mCabActionMenu.activate();
1798        }
1799    }
1800
1801    /**
1802     * Unselect conversations and exit CAB mode.
1803     */
1804    protected final void exitCabMode() {
1805        mSelectedSet.clear();
1806    }
1807
1808    @Override
1809    public void startSearch() {
1810        if (mAccount == null) {
1811            // We cannot search if there is no account. Drop the request to the floor.
1812            LogUtils.d(LOG_TAG, "AbstractActivityController.startSearch(): null account");
1813            return;
1814        }
1815        if (mAccount.supportsCapability(UIProvider.AccountCapabilities.LOCAL_SEARCH)
1816                | mAccount.supportsCapability(UIProvider.AccountCapabilities.SERVER_SEARCH)) {
1817            onSearchRequested(mActionBarView.getQuery());
1818        } else {
1819            Toast.makeText(mActivity.getActivityContext(), mActivity.getActivityContext()
1820                    .getString(R.string.search_unsupported), Toast.LENGTH_SHORT).show();
1821        }
1822    }
1823
1824    @Override
1825    public void exitSearchMode() {
1826        if (mViewMode.getMode() == ViewMode.SEARCH_RESULTS_LIST) {
1827            mActivity.finish();
1828        }
1829    }
1830
1831    /**
1832     * Supports dragging conversations to a folder.
1833     */
1834    @Override
1835    public boolean supportsDrag(DragEvent event, Folder folder) {
1836        return (folder != null
1837                && event != null
1838                && event.getClipDescription() != null
1839                && folder.supportsCapability
1840                    (UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES)
1841                && folder.supportsCapability
1842                    (UIProvider.FolderCapabilities.CAN_HOLD_MAIL)
1843                && !mFolder.uri.equals(folder.uri));
1844    }
1845
1846    /**
1847     * Handles dropping conversations to a label.
1848     */
1849    @Override
1850    public void handleDrop(DragEvent event, final Folder folder) {
1851        if (!supportsDrag(event, folder)) {
1852            return;
1853        }
1854        final Collection<Conversation> conversations = mSelectedSet.values();
1855        final Collection<Folder> dropTarget = Folder.listOf(folder);
1856        // Drag and drop is destructive: we remove conversations from the current folder.
1857        final DestructiveAction action = getFolderChange(conversations, dropTarget, true, true);
1858        delete(conversations, action);
1859    }
1860
1861    @Override
1862    public void onTouchEvent(MotionEvent event) {
1863        if (event.getAction() == MotionEvent.ACTION_DOWN) {
1864            if (mToastBar != null && !mToastBar.isEventInToastBar(event)) {
1865                mToastBar.hide(true);
1866            }
1867        }
1868    }
1869
1870    @Override
1871    public void onConversationSeen(Conversation conv) {
1872        mPagerController.onConversationSeen(conv);
1873    }
1874
1875    private class ConversationListLoaderCallbacks implements
1876        LoaderManager.LoaderCallbacks<ConversationCursor> {
1877
1878        @Override
1879        public Loader<ConversationCursor> onCreateLoader(int id, Bundle args) {
1880            Loader<ConversationCursor> result = new ConversationCursorLoader((Activity) mActivity,
1881                    mAccount, mFolder.conversationListUri, mFolder.name);
1882            return result;
1883        }
1884
1885        @Override
1886        public void onLoadFinished(Loader<ConversationCursor> loader, ConversationCursor data) {
1887            LogUtils.d(LOG_TAG, "IN AAC.ConversationCursor.onLoadFinished, data=%s loader=%s",
1888                    data, loader);
1889            // Clear our all pending destructive actions before swapping the conversation cursor
1890            destroyPending(null);
1891            mConversationListCursor = data;
1892            mConversationListCursor.addListener(AbstractActivityController.this);
1893
1894            mConversationListObservable.notifyChanged();
1895
1896            // Register the AbstractActivityController as a listener to changes in
1897            // data in the cursor.
1898            final ConversationListFragment convList = getConversationListFragment();
1899            if (convList != null) {
1900                convList.onCursorUpdated();
1901                convList.getListView().setOnScrollListener(AbstractActivityController.this);
1902
1903                if (convList.isVisible()) {
1904                    // The conversation list is visible.
1905                    Utils.setConversationCursorVisibility(mConversationListCursor, true);
1906                }
1907            }
1908            // Shown for search results in two-pane mode only.
1909            if (shouldShowFirstConversation()) {
1910                if (mConversationListCursor.getCount() > 0) {
1911                    mConversationListCursor.moveToPosition(0);
1912                    if (convList != null) {
1913                        convList.getListView().setItemChecked(0, true);
1914                    }
1915                    final Conversation conv = new Conversation(mConversationListCursor);
1916                    conv.position = 0;
1917                    onConversationSelected(conv);
1918                }
1919            }
1920        }
1921
1922        @Override
1923        public void onLoaderReset(Loader<ConversationCursor> loader) {
1924            final ConversationListFragment convList = getConversationListFragment();
1925            if (convList == null) {
1926                return;
1927            }
1928            convList.onCursorUpdated();
1929        }
1930    }
1931
1932    @Override
1933    public void sendConversationRead(String toFragment, Conversation conversation, boolean state,
1934            boolean local) {
1935        if (toFragment.equals(TAG_CONVERSATION_LIST)) {
1936            if (mConversationListCursor != null) {
1937                if (local) {
1938                    mConversationListCursor.setConversationColumn(conversation.uri.toString(), ConversationColumns.READ,
1939                            state);
1940                } else {
1941                    mConversationListCursor.markRead(mContext, state, conversation);
1942                }
1943            }
1944        } else if (toFragment.equals(TAG_CONVERSATION)) {
1945            // TODO Handle setting read in conversation view
1946        }
1947    }
1948
1949    @Override
1950    public void sendConversationUriStarred(String toFragment, String conversationUri,
1951            boolean state, boolean local) {
1952        if (toFragment.equals(TAG_CONVERSATION_LIST)) {
1953            if (mConversationListCursor != null) {
1954                if (local) {
1955                    mConversationListCursor.setConversationColumn(conversationUri, ConversationColumns.STARRED, state);
1956                } else {
1957                    mConversationListCursor.updateBoolean(mContext, conversationUri, ConversationColumns.STARRED, state);
1958                }
1959            }
1960        } else if (toFragment.equals(TAG_CONVERSATION)) {
1961            // TODO Handle setting starred in conversation view
1962        }
1963    }
1964
1965    /**
1966     * Destroy the pending {@link DestructiveAction} till now and assign the given action as the
1967     * next destructive action..
1968     * @param nextAction the next destructive action to be performed. This can be null.
1969     */
1970    private final void destroyPending(DestructiveAction nextAction) {
1971        // If there is a pending action, perform that first.
1972        if (mPendingDestruction != null) {
1973            mPendingDestruction.performAction();
1974        }
1975        mPendingDestruction = nextAction;
1976    }
1977
1978    /**
1979     * Register a destructive action with the controller. This performs the previous destructive
1980     * action as a side effect. This method is final because we don't want the child classes to
1981     * embellish this method any more.
1982     * @param action
1983     */
1984    private final void registerDestructiveAction(DestructiveAction action) {
1985        // TODO(viki): This is not a good idea. The best solution is for clients to request a
1986        // destructive action from the controller and for the controller to own the action. This is
1987        // a half-way solution while refactoring DestructiveAction.
1988        destroyPending(action);
1989        return;
1990    }
1991
1992    @Override
1993    public final DestructiveAction getBatchAction(int action) {
1994        final DestructiveAction da = new ConversationAction(action, mSelectedSet.values(), true);
1995        registerDestructiveAction(da);
1996        return da;
1997    }
1998
1999    /**
2000     * Class to change the folders that are assigned to a set of conversations. This is destructive
2001     * because the user can remove the current folder from the conversation, in which case it has
2002     * to be animated away from the current folder.
2003     */
2004    private class FolderDestruction implements DestructiveAction {
2005        private final Collection<Conversation> mTarget;
2006        private final ArrayList<Folder> mFolderList = new ArrayList<Folder>();
2007        private final boolean mIsDestructive;
2008        /** Whether this destructive action has already been performed */
2009        private boolean mCompleted;
2010        private boolean mIsSelectedSet;
2011
2012        /**
2013         * Create a new folder destruction object to act on the given conversations.
2014         * @param target
2015         */
2016        private FolderDestruction(final Collection<Conversation> target,
2017                final Collection<Folder> folders, boolean isDestructive, boolean isBatch) {
2018            mTarget = ImmutableList.copyOf(target);
2019            mFolderList.addAll(folders);
2020            mIsDestructive = isDestructive;
2021            mIsSelectedSet = isBatch;
2022        }
2023
2024        @Override
2025        public void performAction() {
2026            if (isPerformed()) {
2027                return;
2028            }
2029            if (mIsDestructive) {
2030                UndoOperation undoOp = new UndoOperation(mTarget.size(), R.id.change_folder);
2031                onUndoAvailable(undoOp);
2032            }
2033            mConversationListCursor.updateString(mContext, mTarget,
2034                    ConversationColumns.FOLDER_LIST, Folder.getUriString(mFolderList));
2035            mConversationListCursor.updateString(mContext, mTarget,
2036                    ConversationColumns.RAW_FOLDERS,
2037                    Folder.getSerializedFolderString(mFolder, mFolderList));
2038            refreshConversationList();
2039            if (mIsSelectedSet) {
2040                mSelectedSet.clear();
2041            }
2042        }
2043        /**
2044         * Returns true if this action has been performed, false otherwise.
2045         * @return
2046         */
2047        private synchronized boolean isPerformed() {
2048            if (mCompleted) {
2049                return true;
2050            }
2051            mCompleted = true;
2052            return false;
2053        }
2054    }
2055
2056    private final DestructiveAction getFolderChange(Collection<Conversation> target,
2057            Collection<Folder> folders, boolean isDestructive, boolean isBatch){
2058        final DestructiveAction da = new FolderDestruction(target, folders, isDestructive, isBatch);
2059        registerDestructiveAction(da);
2060        return da;
2061    }
2062
2063    @Override
2064    public final void refreshConversationList() {
2065        final ConversationListFragment convList = getConversationListFragment();
2066        if (convList == null) {
2067            return;
2068        }
2069        convList.requestListRefresh();
2070    }
2071
2072    protected final ActionClickedListener getUndoClickedListener(
2073            final AnimatedAdapter listAdapter) {
2074        return new ActionClickedListener() {
2075            @Override
2076            public void onActionClicked() {
2077                if (mAccount.undoUri != null) {
2078                    // NOTE: We might want undo to return the messages affected, in which case
2079                    // the resulting cursor might be interesting...
2080                    // TODO: Use UIProvider.SEQUENCE_QUERY_PARAMETER to indicate the set of
2081                    // commands to undo
2082                    if (mConversationListCursor != null) {
2083                        mConversationListCursor.undo(
2084                                mActivity.getActivityContext(), mAccount.undoUri);
2085                    }
2086                    if (listAdapter != null) {
2087                        listAdapter.setUndo(true);
2088                    }
2089                }
2090            }
2091        };
2092    }
2093
2094    protected final void showErrorToast(final Folder folder) {
2095        mToastBar.setConversationMode(false);
2096        mToastBar.show(
2097                getRetryClickedListener(folder),
2098                R.drawable.ic_email_network_error,
2099                Utils.getSyncStatusText(mActivity.getActivityContext(),
2100                        folder.lastSyncResult),
2101                false, /* showActionIcon */
2102                R.string.retry,
2103                false); /* replaceVisibleToast */
2104    }
2105
2106    private final ActionClickedListener getRetryClickedListener(final Folder folder) {
2107        return new ActionClickedListener() {
2108            @Override
2109            public void onActionClicked() {
2110                final Uri uri = folder.refreshUri;
2111
2112                if (uri != null) {
2113                    if (mFolderSyncTask != null) {
2114                        mFolderSyncTask.cancel(true);
2115                    }
2116                    mFolderSyncTask = new AsyncRefreshTask(mActivity.getActivityContext(), uri);
2117                    mFolderSyncTask.execute();
2118                }
2119            }
2120        };
2121    }
2122}
2123