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