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