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