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