AbstractActivityController.java revision e25998f8c3d20b37682cfe00aadb4a70c81eb8e4
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.LoaderManager;
26import android.app.SearchManager;
27import android.content.ClipData;
28import android.content.ContentResolver;
29import android.content.Context;
30import android.content.CursorLoader;
31import android.content.DialogInterface;
32import android.content.Intent;
33import android.content.Loader;
34import android.database.Cursor;
35import android.net.Uri;
36import android.os.Bundle;
37import android.os.Handler;
38import android.provider.SearchRecentSuggestions;
39import android.view.DragEvent;
40import android.view.KeyEvent;
41import android.view.LayoutInflater;
42import android.view.Menu;
43import android.view.MenuInflater;
44import android.view.MenuItem;
45import android.view.MotionEvent;
46import android.widget.Toast;
47
48import com.android.mail.ConversationListContext;
49import com.android.mail.R;
50import com.android.mail.browse.ConversationCursor;
51import com.android.mail.browse.ConversationCursor.ConversationListener;
52import com.android.mail.browse.SelectedConversationsActionMenu;
53import com.android.mail.compose.ComposeActivity;
54import com.android.mail.providers.Account;
55import com.android.mail.providers.Conversation;
56import com.android.mail.providers.Folder;
57import com.android.mail.providers.MailAppProvider;
58import com.android.mail.providers.Settings;
59import com.android.mail.providers.SuggestionsProvider;
60import com.android.mail.providers.UIProvider;
61import com.android.mail.providers.UIProvider.AccountCursorExtraKeys;
62import com.android.mail.providers.UIProvider.AutoAdvance;
63import com.android.mail.providers.UIProvider.ConversationColumns;
64import com.android.mail.providers.UIProvider.FolderCapabilities;
65import com.android.mail.utils.LogUtils;
66import com.android.mail.utils.Utils;
67import com.google.common.collect.ImmutableList;
68import com.google.common.collect.Lists;
69import com.google.common.collect.Sets;
70
71import java.util.ArrayList;
72import java.util.Collection;
73import java.util.HashSet;
74import java.util.Map;
75import java.util.Set;
76
77
78/**
79 * This is an abstract implementation of the Activity Controller. This class
80 * knows how to respond to menu items, state changes, layout changes, etc. It
81 * weaves together the views and listeners, dispatching actions to the
82 * respective underlying classes.
83 * <p>
84 * Even though this class is abstract, it should provide default implementations
85 * for most, if not all the methods in the ActivityController interface. This
86 * makes the task of the subclasses easier: OnePaneActivityController and
87 * TwoPaneActivityController can be concise when the common functionality is in
88 * AbstractActivityController.
89 * </p>
90 * <p>
91 * In the Gmail codebase, this was called BaseActivityController
92 * </p>
93 */
94public abstract class AbstractActivityController implements ActivityController, ConversationListener {
95    // Keys for serialization of various information in Bundles.
96    private static final String SAVED_ACCOUNT = "saved-account";
97    private static final String SAVED_FOLDER = "saved-folder";
98    private static final String SAVED_CONVERSATION = "saved-conversation";
99    // Batch conversations stored in the Bundle using this key.
100    private static final String SAVED_CONVERSATIONS = "saved-conversations";
101
102    /** Are we on a tablet device or not. */
103    public final boolean IS_TABLET_DEVICE;
104
105    protected Account mAccount;
106    protected Folder mFolder;
107    protected ActionBarView mActionBarView;
108    protected final RestrictedActivity mActivity;
109    protected final Context mContext;
110    protected final RecentFolderList mRecentFolderList;
111    protected ConversationListContext mConvListContext;
112    protected Conversation mCurrentConversation;
113
114    /** A {@link android.content.BroadcastReceiver} that suppresses new e-mail notifications. */
115    private SuppressNotificationReceiver mNewEmailReceiver = null;
116
117    protected Handler mHandler = new Handler();
118    protected ConversationListFragment mConversationListFragment;
119    /**
120     * The current mode of the application. All changes in mode are initiated by
121     * the activity controller. View mode changes are propagated to classes that
122     * attach themselves as listeners of view mode changes.
123     */
124    protected final ViewMode mViewMode;
125    protected ContentResolver mResolver;
126    protected FolderListFragment mFolderListFragment;
127    protected ConversationViewFragment mConversationViewFragment;
128    protected boolean isLoaderInitialized = false;
129    private AsyncRefreshTask mAsyncRefreshTask;
130
131    private final Set<Uri> mCurrentAccountUris = Sets.newHashSet();
132    protected Settings mCachedSettings;
133    protected ConversationCursor mConversationListCursor;
134    protected boolean mConversationListenerAdded = false;
135    /**
136     * Selected conversations, if any.
137     */
138    private final ConversationSelectionSet mSelectedSet = new ConversationSelectionSet();
139
140    /**
141     * Action menu associated with the selected set.
142     */
143    SelectedConversationsActionMenu mCabActionMenu;
144
145    protected static final String LOG_TAG = new LogUtils().getLogTag();
146    /** Constants used to differentiate between the types of loaders. */
147    private static final int LOADER_ACCOUNT_CURSOR = 0;
148    private static final int LOADER_FOLDER_CURSOR = 2;
149    private static final int LOADER_RECENT_FOLDERS = 3;
150    private static final int LOADER_CONVERSATION_LIST = 4;
151    private static final int LOADER_ACCOUNT_INBOX = 5;
152    private static final int LOADER_SEARCH = 6;
153
154
155    private static final int ADD_ACCOUNT_REQUEST_CODE = 1;
156
157    public AbstractActivityController(MailActivity activity, ViewMode viewMode) {
158        mActivity = activity;
159        mViewMode = viewMode;
160        mContext = activity.getApplicationContext();
161        IS_TABLET_DEVICE = Utils.useTabletUI(mContext);
162        mRecentFolderList = new RecentFolderList(mContext, this);
163        // Allow the fragment to observe changes to its own selection set. No other object is
164        // aware of the selected set.
165        mSelectedSet.addObserver(this);
166    }
167
168    @Override
169    public synchronized void attachConversationList(ConversationListFragment fragment) {
170        // If there is an existing fragment, unregister it
171        if (mConversationListFragment != null) {
172            mViewMode.removeListener(mConversationListFragment);
173        }
174        mConversationListFragment = fragment;
175        // If the current fragment is non-null, add it as a listener.
176        if (fragment != null) {
177            mViewMode.addListener(mConversationListFragment);
178        }
179    }
180
181    @Override
182    public synchronized void attachFolderList(FolderListFragment fragment) {
183        // If there is an existing fragment, unregister it
184        if (mFolderListFragment != null) {
185            mViewMode.removeListener(mFolderListFragment);
186        }
187        mFolderListFragment = fragment;
188        if (fragment != null) {
189            mViewMode.addListener(mFolderListFragment);
190        }
191    }
192
193    @Override
194    public void attachConversationView(ConversationViewFragment conversationViewFragment) {
195        mConversationViewFragment = conversationViewFragment;
196    }
197
198    @Override
199    public void clearSubject() {
200        // TODO(viki): Auto-generated method stub
201    }
202
203    @Override
204    public Account getCurrentAccount() {
205        return mAccount;
206    }
207
208    @Override
209    public ConversationListContext getCurrentListContext() {
210        return mConvListContext;
211    }
212
213    @Override
214    public String getHelpContext() {
215        return "Mail";
216    }
217
218    @Override
219    public int getMode() {
220        return mViewMode.getMode();
221    }
222
223    @Override
224    public String getUnshownSubject(String subject) {
225        // Calculate how much of the subject is shown, and return the remaining.
226        return null;
227    }
228
229    @Override
230    public void handleConversationLoadError() {
231        // TODO(viki): Auto-generated method stub
232    }
233
234    @Override
235    public ConversationCursor getConversationListCursor() {
236        return mConversationListCursor;
237    }
238
239    @Override
240    public void initConversationListCursor() {
241        mActivity.getLoaderManager().restartLoader(LOADER_CONVERSATION_LIST, Bundle.EMPTY,
242                new LoaderManager.LoaderCallbacks<ConversationCursor>() {
243
244                    @Override
245                    public void onLoadFinished(Loader<ConversationCursor> loader,
246                            ConversationCursor data) {
247                        mConversationListCursor = data;
248                        if (mConversationListCursor.isRefreshReady()) {
249                            mConversationListCursor.sync();
250                            refreshAdapter();
251                        }
252                        if (mConversationListFragment != null) {
253                            mConversationListFragment.onCursorUpdated();
254                            if (!mConversationListenerAdded) {
255                                // TODO(mindyp): when we move to the cursor loader, we need
256                                // to add/remove the listener when we create/ destroy loaders.
257                                mConversationListCursor
258                                        .addListener(AbstractActivityController.this);
259                                mConversationListenerAdded = true;
260                            }
261                        }
262                        if (shouldShowFirstConversation()) {
263                            if (mConversationListCursor.getCount() > 0) {
264                                mConversationListCursor.moveToPosition(0);
265                                mConversationListFragment.getListView().setItemChecked(0, true);
266                                Conversation conv = new Conversation(mConversationListCursor);
267                                conv.position = 0;
268                                onConversationSelected(conv);
269                            }
270                        }
271
272                    }
273
274                    @Override
275                    public void onLoaderReset(Loader<ConversationCursor> loader) {
276                        if (mConversationListFragment == null) {
277                            return;
278                        }
279                        mConversationListFragment.onCursorUpdated();
280                    }
281
282                    @Override
283                    public Loader<ConversationCursor> onCreateLoader(int id, Bundle args) {
284                        return new ConversationCursorLoader((Activity) mActivity, mAccount,
285                                UIProvider.CONVERSATION_PROJECTION, mFolder.conversationListUri);
286                    }
287
288                });
289    }
290
291    /**
292     * Initialize the action bar. This is not visible to OnePaneController and
293     * TwoPaneController so they cannot override this behavior.
294     */
295    private void initCustomActionBarView() {
296        ActionBar actionBar = mActivity.getActionBar();
297        mActionBarView = (ActionBarView) LayoutInflater.from(mContext).inflate(
298                R.layout.actionbar_view, null);
299        if (actionBar != null && mActionBarView != null) {
300            // Why have a different variable for the same thing? We should apply
301            // the same actions
302            // on mActionBarView instead.
303            mActionBarView.initialize(mActivity, this, mViewMode, actionBar, mRecentFolderList);
304            actionBar.setCustomView(mActionBarView, new ActionBar.LayoutParams(
305                    LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
306            actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM,
307                    ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_SHOW_TITLE);
308        }
309    }
310
311    /**
312     * Returns whether the conversation list fragment is visible or not.
313     * Different layouts will have their own notion on the visibility of
314     * fragments, so this method needs to be overriden.
315     *
316     * @return
317     */
318    protected abstract boolean isConversationListVisible();
319
320    @Override
321    public void onAccountChanged(Account account) {
322        if (!account.equals(mAccount)) {
323            // Current account is different from the new account, restart loaders and show
324            // the account Inbox.
325            mAccount = account;
326            mFolder = null;
327            onSettingsChanged(mAccount.settings);
328            mActionBarView.setAccount(mAccount);
329            loadAccountInbox();
330
331            mRecentFolderList.setCurrentAccount(account);
332            restartOptionalLoader(LOADER_RECENT_FOLDERS);
333            mActivity.invalidateOptionsMenu();
334
335            disableNotificationsOnAccountChange(mAccount);
336
337            MailAppProvider.getInstance().setLastViewedAccount(mAccount.uri.toString());
338        } else {
339            // Current account is the same as the new account. Load the default inbox if the
340            // current inbox is not the same as the default inbox.
341            final Uri oldUri = mFolder != null ? mFolder.uri : Uri.EMPTY;
342            final Uri newUri = getDefaultInboxUri(mCachedSettings);
343            if ((mFolder == null || mFolder.type == UIProvider.FolderType.INBOX)
344                    && !oldUri.equals(newUri)) {
345                loadAccountInbox();
346            }
347        }
348    }
349
350    /**
351     * Returns the URI of the current account's default inbox if available, otherwise
352     * returns the empty URI {@link Uri#EMPTY}
353     * @return
354     */
355    private Uri getDefaultInboxUri(Settings settings) {
356        if (settings != null && settings.defaultInbox != null) {
357            return settings.defaultInbox;
358        }
359        return Uri.EMPTY;
360    }
361
362    public void onSettingsChanged(Settings settings) {
363        final Uri oldUri = getDefaultInboxUri(mCachedSettings);
364        final Uri newUri = getDefaultInboxUri(settings);
365        mCachedSettings = settings;
366        resetActionBarIcon();
367        // Only restart the loader if the defaultInboxUri is not the same as
368        // the folder we are already loading.
369        final boolean changed = !oldUri.equals(newUri);
370        if (settings != null
371                && settings.defaultInbox != null
372                && (mFolder == null
373                // we really only want CHANGES to the inbox setting, not just
374                // the first setting of it.
375                || (mFolder.type == UIProvider.FolderType.INBOX && !oldUri.equals(Uri.EMPTY))
376                && changed)) {
377            loadAccountInbox();
378        }
379    }
380
381    @Override
382    public Settings getSettings() {
383        return mCachedSettings;
384    }
385
386    private void fetchSearchFolder(Intent intent) {
387        Bundle args = new Bundle();
388        args.putString(ConversationListContext.EXTRA_SEARCH_QUERY, intent
389                .getStringExtra(ConversationListContext.EXTRA_SEARCH_QUERY));
390        mActivity.getLoaderManager().restartLoader(LOADER_SEARCH, args, this);
391    }
392
393    @Override
394    public void onFolderChanged(Folder folder) {
395        if (folder != null && !folder.equals(mFolder)) {
396            setFolder(folder);
397            mConvListContext = ConversationListContext.forFolder(mContext, mAccount, mFolder);
398            showConversationList(mConvListContext);
399
400            // Add the folder that we were viewing to the recent folders list.
401            // TODO: this may need to be fine tuned.  If this is the signal that is indicating that
402            // the list is shown to the user, this could fire in one pane if the user goes directly
403            // to a conversation
404            updateRecentFolderList();
405        }
406    }
407
408    /**
409     * Update the recent folders. This only needs to be done once when accessing a new folder.
410     */
411    private void updateRecentFolderList() {
412        if (mFolder != null) {
413            mRecentFolderList.touchFolder(mFolder, mAccount);
414        }
415    }
416
417    // TODO(mindyp): set this up to store a copy of the folder as a transient
418    // field in the account.
419    protected void loadAccountInbox() {
420        restartOptionalLoader(LOADER_ACCOUNT_INBOX);
421    }
422
423    /** Set the current folder */
424    private void setFolder(Folder folder) {
425        // Start watching folder for sync status.
426        if (folder != null && !folder.equals(mFolder)) {
427            mActionBarView.setRefreshInProgress(false);
428            mFolder = folder;
429            mActionBarView.setFolder(mFolder);
430            mActivity.getLoaderManager().restartLoader(LOADER_FOLDER_CURSOR, null, this);
431            initConversationListCursor();
432        } else if (folder == null) {
433            LogUtils.wtf(LOG_TAG, "Folder in setFolder is null");
434        }
435    }
436
437    @Override
438    public void onActivityResult(int requestCode, int resultCode, Intent data) {
439        if (requestCode == ADD_ACCOUNT_REQUEST_CODE) {
440            // We were waiting for the user to create an account
441            if (resultCode == Activity.RESULT_OK) {
442                // restart the loader to get the updated list of accounts
443                mActivity.getLoaderManager().initLoader(
444                        LOADER_ACCOUNT_CURSOR, null, this);
445            } else {
446                // The user failed to create an account, just exit the app
447                mActivity.finish();
448            }
449        }
450    }
451
452    @Override
453    public void onConversationListVisibilityChanged(boolean visible) {
454        // TODO(viki): Auto-generated method stub
455    }
456
457    /**
458     * By default, doing nothing is right. A two-pane controller will need to
459     * override this.
460     */
461    @Override
462    public void onConversationVisibilityChanged(boolean visible) {
463        // Do nothing.
464        return;
465    }
466
467    @Override
468    public boolean onCreate(Bundle savedState) {
469        // Initialize the action bar view.
470        initCustomActionBarView();
471        // Allow shortcut keys to function for the ActionBar and menus.
472        mActivity.setDefaultKeyMode(Activity.DEFAULT_KEYS_SHORTCUT);
473        mResolver = mActivity.getContentResolver();
474
475        mNewEmailReceiver = new SuppressNotificationReceiver();
476
477        // All the individual UI components listen for ViewMode changes. This
478        // simplifies the amount of logic in the AbstractActivityController, but increases the
479        // possibility of timing-related bugs.
480        mViewMode.addListener(this);
481        assert (mActionBarView != null);
482        mViewMode.addListener(mActionBarView);
483
484        restoreState(savedState);
485        return true;
486    }
487
488    @Override
489    public Dialog onCreateDialog(int id, Bundle bundle) {
490        return null;
491    }
492
493    @Override
494    public boolean onCreateOptionsMenu(Menu menu) {
495        MenuInflater inflater = mActivity.getMenuInflater();
496        inflater.inflate(mActionBarView.getOptionsMenuId(), menu);
497        mActionBarView.onCreateOptionsMenu(menu);
498        return true;
499    }
500
501    @Override
502    public boolean onKeyDown(int keyCode, KeyEvent event) {
503        // TODO(viki): Auto-generated method stub
504        return false;
505    }
506
507    @Override
508    public boolean onOptionsItemSelected(MenuItem item) {
509        final int id = item.getItemId();
510        boolean handled = true;
511        switch (id) {
512            case android.R.id.home:
513                onUpPressed();
514                break;
515            case R.id.compose:
516                ComposeActivity.compose(mActivity.getActivityContext(), mAccount);
517                break;
518            case R.id.show_all_folders:
519                showFolderList();
520                break;
521            case R.id.refresh:
522                requestFolderRefresh();
523                break;
524            case R.id.settings:
525                Utils.showSettings(mActivity.getActivityContext(), mAccount);
526                break;
527            case R.id.folder_options:
528                Utils.showFolderSettings(mActivity.getActivityContext(), mAccount, mFolder);
529                break;
530            case R.id.help_info_menu_item:
531                // TODO: enable context sensitive help
532                Utils.showHelp(mActivity.getActivityContext(), mAccount.helpIntentUri, null);
533                break;
534            case R.id.feedback_menu_item:
535                Utils.sendFeedback(mActivity.getActivityContext(), mAccount);
536                break;
537            default:
538                handled = false;
539                break;
540        }
541        return handled;
542    }
543
544    /**
545     * Return the auto advance setting for the current account.
546     * @param activity
547     * @return the autoadvance setting, a constant from {@link AutoAdvance}
548     */
549    static int getAutoAdvanceSetting(RestrictedActivity activity) {
550        final Settings settings = activity.getSettings();
551        // TODO(mindyp): if this isn't set, then show the dialog telling the user to set it.
552        // Remove defaulting to AutoAdvance.LIST.
553        final int autoAdvance = (settings != null) ?
554                (settings.autoAdvance == AutoAdvance.UNSET ?
555                        AutoAdvance.LIST : settings.autoAdvance)
556                : AutoAdvance.LIST;
557        return autoAdvance;
558    }
559
560    /**
561     * Implements folder changes. This class is a listener because folder changes need to be
562     * performed <b>after</b> the ConversationListFragment has finished animating away the
563     * removal of the conversation.
564     *
565     */
566    protected abstract class FolderChangeListener implements ActionCompleteListener {
567        protected final String mFolderChangeList;
568        protected final boolean mDestructiveChange;
569
570        public FolderChangeListener(String changeList, boolean destructive) {
571            mFolderChangeList = changeList;
572            mDestructiveChange = destructive;
573        }
574
575        @Override
576        public abstract void onActionComplete();
577    }
578
579    /**
580     * Update the specified column name in conversation for a boolean value.
581     * @param columnName
582     * @param value
583     */
584    protected void updateCurrentConversation(String columnName, boolean value) {
585        Conversation.updateBoolean(mContext, ImmutableList.of(mCurrentConversation), columnName,
586                value);
587        if (mConversationListFragment != null) {
588            mConversationListFragment.requestListRefresh();
589        }
590    }
591
592    /**
593     * Update the specified column name in conversation for an integer value.
594     * @param columnName
595     * @param value
596     */
597    protected void updateCurrentConversation(String columnName, int value) {
598        Conversation.updateInt(mContext, ImmutableList.of(mCurrentConversation), columnName, value);
599        if (mConversationListFragment != null) {
600            mConversationListFragment.requestListRefresh();
601        }
602    }
603
604    protected void updateCurrentConversation(String columnName, String value) {
605        Conversation.updateString(mContext, ImmutableList.of(mCurrentConversation), columnName,
606                value);
607        if (mConversationListFragment != null) {
608            mConversationListFragment.requestListRefresh();
609        }
610    }
611
612    private void requestFolderRefresh() {
613        if (mFolder != null) {
614            if (mAsyncRefreshTask != null) {
615                mAsyncRefreshTask.cancel(true);
616            }
617            mAsyncRefreshTask = new AsyncRefreshTask(mContext, mFolder);
618            mAsyncRefreshTask.execute();
619        }
620    }
621
622    /**
623     * Confirm (based on user's settings) and delete a conversation from the conversation list and
624     * from the database.
625     * @param showDialog
626     * @param confirmResource
627     * @param listener
628     */
629    protected void confirmAndDelete(boolean showDialog, int confirmResource,
630            final ActionCompleteListener listener) {
631        final ArrayList<Conversation> single = new ArrayList<Conversation>();
632        single.add(mCurrentConversation);
633        if (showDialog) {
634            final AlertDialog.OnClickListener onClick = new AlertDialog.OnClickListener() {
635                @Override
636                public void onClick(DialogInterface dialog, int which) {
637                    requestDelete(listener);
638                }
639            };
640            final CharSequence message = Utils.formatPlural(mContext, confirmResource, 1);
641            new AlertDialog.Builder(mActivity.getActivityContext()).setMessage(message)
642                    .setPositiveButton(R.string.ok, onClick)
643                    .setNegativeButton(R.string.cancel, null)
644                    .create().show();
645        } else {
646            requestDelete(listener);
647        }
648    }
649
650
651    protected abstract void requestDelete(ActionCompleteListener listener);
652
653
654    @Override
655    public void onPrepareDialog(int id, Dialog dialog, Bundle bundle) {
656        // TODO(viki): Auto-generated method stub
657
658    }
659
660    @Override
661    public boolean onPrepareOptionsMenu(Menu menu) {
662        mActionBarView.onPrepareOptionsMenu(menu);
663        return true;
664    }
665
666    @Override
667    public void onPause() {
668        isLoaderInitialized = false;
669
670        enableNotifications();
671    }
672
673    @Override
674    public void onResume() {
675        // Register the receiver that will prevent the status receiver from
676        // displaying its notification icon as long as we're running.
677        // The SupressNotificationReceiver will block the broadcast if we're looking at the folder
678        // that the notification was received for.
679        disableNotifications();
680
681        if (mActionBarView != null) {
682            mActionBarView.onResume();
683        }
684
685    }
686
687    @Override
688    public void onSaveInstanceState(Bundle outState) {
689        if (mAccount != null) {
690            LogUtils.d(LOG_TAG, "Saving the account now");
691            outState.putParcelable(SAVED_ACCOUNT, mAccount);
692        }
693        if (mFolder != null) {
694            outState.putParcelable(SAVED_FOLDER, mFolder);
695        }
696        if (mCurrentConversation != null && mViewMode.getMode() == ViewMode.CONVERSATION) {
697            outState.putParcelable(SAVED_CONVERSATION, mCurrentConversation);
698        }
699        if (!mSelectedSet.isEmpty()) {
700            outState.putParcelable(SAVED_CONVERSATIONS, mSelectedSet);
701        }
702    }
703
704    @Override
705    public void onSearchRequested(String query) {
706        Intent intent = new Intent();
707        intent.setAction(Intent.ACTION_SEARCH);
708        intent.putExtra(ConversationListContext.EXTRA_SEARCH_QUERY, query);
709        intent.putExtra(Utils.EXTRA_ACCOUNT, mAccount);
710        intent.setComponent(mActivity.getComponentName());
711        mActionBarView.collapseSearch();
712        mActivity.startActivity(intent);
713    }
714
715    @Override
716    public void onStartDragMode() {
717        // TODO(viki): Auto-generated method stub
718    }
719
720    @Override
721    public void onStop() {
722        // TODO(viki): Auto-generated method stub
723    }
724
725    @Override
726    public void onStopDragMode() {
727        // TODO(viki): Auto-generated method stub
728    }
729
730    /**
731     * {@inheritDoc} Subclasses must override this to listen to mode changes
732     * from the ViewMode. Subclasses <b>must</b> call the parent's
733     * onViewModeChanged since the parent will handle common state changes.
734     */
735    @Override
736    public void onViewModeChanged(int newMode) {
737        // Perform any mode specific work here.
738        // reset the action bar icon based on the mode. Why don't the individual
739        // controllers do
740        // this themselves?
741
742        // In conversation list mode, clean up the conversation.
743        if (newMode == ViewMode.CONVERSATION_LIST) {
744            // Clean up the conversation here.
745        }
746
747        // We don't want to invalidate the options menu when switching to
748        // conversation
749        // mode, as it will happen when the conversation finishes loading.
750        if (newMode != ViewMode.CONVERSATION) {
751            mActivity.invalidateOptionsMenu();
752        }
753    }
754
755    @Override
756    public void onWindowFocusChanged(boolean hasFocus) {
757        // TODO(viki): Auto-generated method stub
758    }
759
760    /**
761     * Restore the state from the previous bundle. Subclasses should call this
762     * method from the parent class, since it performs important UI
763     * initialization.
764     *
765     * @param savedState
766     */
767    protected void restoreState(Bundle savedState) {
768        final Intent intent = mActivity.getIntent();
769        boolean handled = false;
770        if (savedState != null) {
771            if (savedState.containsKey(SAVED_ACCOUNT)) {
772                mAccount = ((Account) savedState.getParcelable(SAVED_ACCOUNT));
773                mCachedSettings = mAccount.settings;
774                mActionBarView.setAccount(mAccount);
775                mActivity.invalidateOptionsMenu();
776            }
777            if (savedState.containsKey(SAVED_FOLDER)) {
778                // Open the folder.
779                onFolderChanged((Folder) savedState.getParcelable(SAVED_FOLDER));
780                handled = true;
781            }
782            if (savedState.containsKey(SAVED_CONVERSATION)) {
783                // Open the conversation.
784                setCurrentConversation((Conversation) savedState.getParcelable(SAVED_CONVERSATION));
785                showConversation(mCurrentConversation);
786                handled = true;
787            }
788        } else if (intent != null) {
789            if (Intent.ACTION_VIEW.equals(intent.getAction())) {
790                if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) {
791                    mAccount = ((Account) intent.getParcelableExtra(Utils.EXTRA_ACCOUNT));
792                } else if (intent.hasExtra(Utils.EXTRA_ACCOUNT_STRING)) {
793                    mAccount = Account.newinstance(intent
794                            .getStringExtra(Utils.EXTRA_ACCOUNT_STRING));
795                }
796                if (mAccount != null) {
797                    mActionBarView.setAccount(mAccount);
798                    mCachedSettings = mAccount.settings;
799                    mActivity.invalidateOptionsMenu();
800                }
801
802                Folder folder = null;
803                if (intent.hasExtra(Utils.EXTRA_FOLDER)) {
804                    // Open the folder.
805                    LogUtils.d(LOG_TAG, "SHOW THE FOLDER at %s",
806                            intent.getParcelableExtra(Utils.EXTRA_FOLDER));
807                    folder = (Folder) intent.getParcelableExtra(Utils.EXTRA_FOLDER);
808
809                } else if (intent.hasExtra(Utils.EXTRA_FOLDER_STRING)) {
810                    // Open the folder.
811                    folder = new Folder(intent.getStringExtra(Utils.EXTRA_FOLDER_STRING));
812                }
813                if (folder != null) {
814                    onFolderChanged(folder);
815                    handled = true;
816                }
817
818                if (intent.hasExtra(Utils.EXTRA_CONVERSATION)) {
819                    // Open the conversation.
820                    LogUtils.d(LOG_TAG, "SHOW THE CONVERSATION at %s",
821                            intent.getParcelableExtra(Utils.EXTRA_CONVERSATION));
822                    setCurrentConversation((Conversation) intent
823                            .getParcelableExtra(Utils.EXTRA_CONVERSATION));
824                    showConversation(mCurrentConversation);
825                    handled = true;
826                }
827
828                if (!handled) {
829                    // Nothing was saved; just load the account inbox.
830                    loadAccountInbox();
831                }
832            } else if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
833                // Save this search query for future suggestions.
834                final String query = intent.getStringExtra(SearchManager.QUERY);
835                final String authority = mContext.getString(R.string.suggestions_authority);
836                SearchRecentSuggestions suggestions = new SearchRecentSuggestions(
837                        mContext, authority, SuggestionsProvider.MODE);
838                suggestions.saveRecentQuery(query, null);
839
840                mViewMode.enterSearchResultsListMode();
841                mAccount = ((Account) intent.getParcelableExtra(Utils.EXTRA_ACCOUNT));
842                mCachedSettings = mAccount.settings;
843                mActionBarView.setAccount(mAccount);
844                fetchSearchFolder(intent);
845            }
846        }
847
848        /**
849         * Restore the state of selected conversations. This needs to be done after the correct mode
850         * is set and the action bar is fully initialized. If not, several key pieces of state
851         * information will be missing, and the split views may not be initialized correctly.
852         * @param savedState
853         */
854        restoreSelectedConversations(savedState);
855        // Create the accounts loader; this loads the account switch spinner.
856        mActivity.getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this);
857    }
858
859    /**
860     * Copy any selected conversations stored in the saved bundle into our selection set,
861     * triggering {@link ConversationSetObserver} callbacks as our selection set changes.
862     *
863     */
864    private void restoreSelectedConversations(Bundle savedState) {
865        if (savedState == null) {
866            mSelectedSet.clear();
867            return;
868        }
869        final ConversationSelectionSet selectedSet = savedState.getParcelable(SAVED_CONVERSATIONS);
870        if (selectedSet == null || selectedSet.isEmpty()) {
871            mSelectedSet.clear();
872            return;
873        }
874
875        // putAll will take care of calling our registered onSetPopulated method
876        // FIXME: disabled until we correctly handle selection set bringup when list fragment
877        // does not yet exist (b/6268401)
878        //mSelectedSet.putAll(selectedSet);
879    }
880
881    @Override
882    public void setSubject(String subject) {
883        // Do something useful with the subject. This requires changing the
884        // conversation view's subject text.
885    }
886
887    /**
888     * Children can override this method, but they must call super.showConversation().
889     * {@inheritDoc}
890     */
891    @Override
892    public void showConversation(Conversation conversation) {
893        // Set the current conversation just in case it wasn't already set.
894        setCurrentConversation(conversation);
895    }
896
897    /**
898     * Children can override this method, but they must call super.showConversationList().
899     * {@inheritDoc}
900     */
901    @Override
902    public void showConversationList(ConversationListContext listContext) {
903    }
904
905    @Override
906    public void onConversationSelected(Conversation conversation) {
907        showConversation(conversation);
908        if (mConvListContext != null && mConvListContext.isSearchResult()) {
909            mViewMode.enterSearchResultsConversationMode();
910        } else {
911            mViewMode.enterConversationMode();
912        }
913    }
914
915    /**
916     * Set the current conversation. This is the conversation on which all actions are performed.
917     * Do not modify mCurrentConversation except through this method, which makes it easy to
918     * perform common actions associated with changing the current conversation.
919     * @param conversation
920     */
921    protected void setCurrentConversation(Conversation conversation) {
922        mCurrentConversation = conversation;
923    }
924
925    /**
926     * {@inheritDoc}
927     */
928    @Override
929    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
930        // Create a loader to listen in on account changes.
931        switch (id) {
932            case LOADER_ACCOUNT_CURSOR:
933                return new CursorLoader(mContext, MailAppProvider.getAccountsUri(),
934                        UIProvider.ACCOUNTS_PROJECTION, null, null, null);
935            case LOADER_FOLDER_CURSOR:
936                return new CursorLoader(mContext, mFolder.uri, UIProvider.FOLDERS_PROJECTION, null,
937                        null, null);
938            case LOADER_RECENT_FOLDERS:
939                if (mAccount.recentFolderListUri != null) {
940                    return new CursorLoader(mContext, mAccount.recentFolderListUri,
941                            UIProvider.FOLDERS_PROJECTION, null, null, null);
942                }
943                break;
944            case LOADER_ACCOUNT_INBOX:
945                Settings settings = getSettings();
946                Uri inboxUri;
947                if (settings != null) {
948                    inboxUri = settings.defaultInbox;
949                } else {
950                    inboxUri = mAccount.folderListUri;
951                }
952                return new CursorLoader(mContext, inboxUri, UIProvider.FOLDERS_PROJECTION, null,
953                        null, null);
954            case LOADER_SEARCH:
955                return Folder.forSearchResults(mAccount,
956                        args.getString(ConversationListContext.EXTRA_SEARCH_QUERY),
957                        mActivity.getActivityContext());
958            default:
959                LogUtils.wtf(LOG_TAG, "Loader returned unexpected id: %d", id);
960        }
961        return null;
962    }
963
964    @Override
965    public void onLoaderReset(Loader<Cursor> loader) {
966
967    }
968
969    /**
970     * {@link LoaderManager} currently has a bug in
971     * {@link LoaderManager#restartLoader(int, Bundle, android.app.LoaderManager.LoaderCallbacks)}
972     * where, if a previous onCreateLoader returned a null loader, this method will NPE. Work around
973     * this bug by destroying any loaders that may have been created as null (essentially because
974     * they are optional loads, and may not apply to a particular account).
975     * <p>
976     * A simple null check before restarting a loader will not work, because that would not
977     * give the controller a chance to invalidate UI corresponding the prior loader result.
978     *
979     * @param id loader ID to safely restart
980     */
981    private void restartOptionalLoader(int id) {
982        final LoaderManager lm = mActivity.getLoaderManager();
983        lm.destroyLoader(id);
984        lm.restartLoader(id, Bundle.EMPTY, this);
985    }
986
987    /**
988     * Start a loader with the given id. This should be called when we know that the previous
989     * state of the application matches this state, and we are happy if we get the previously
990     * created loader with this id. If that is not true, consider calling
991     * {@link #restartOptionalLoader(int)} instead.
992     * @param id
993     */
994    private void startLoader(int id) {
995        final LoaderManager lm = mActivity.getLoaderManager();
996        lm.initLoader(id, Bundle.EMPTY, this);
997    }
998
999    private boolean accountsUpdated(Cursor accountCursor) {
1000        // Check to see if the current account hasn't been set, or the account cursor is empty
1001        if (mAccount == null || !accountCursor.moveToFirst()) {
1002            return true;
1003        }
1004
1005        // Check to see if the number of accounts are different, from the number we saw on the last
1006        // updated
1007        if (mCurrentAccountUris.size() != accountCursor.getCount()) {
1008            return true;
1009        }
1010
1011        // Check to see if the account list is different or if the current account is not found in
1012        // the cursor.
1013        boolean foundCurrentAccount = false;
1014        do {
1015            final Uri accountUri =
1016                    Uri.parse(accountCursor.getString(UIProvider.ACCOUNT_URI_COLUMN));
1017            if (!foundCurrentAccount && mAccount.uri.equals(accountUri)) {
1018                foundCurrentAccount = true;
1019            }
1020
1021            if (!mCurrentAccountUris.contains(accountUri)) {
1022                return true;
1023            }
1024        } while (accountCursor.moveToNext());
1025
1026        // As long as we found the current account, the list hasn't been updated
1027        return !foundCurrentAccount;
1028    }
1029
1030    /**
1031     * Update the accounts on the device. This currently loads the first account
1032     * in the list.
1033     *
1034     * @param loader
1035     * @param accounts cursor into the AccountCache
1036     * @return true if the update was successful, false otherwise
1037     */
1038    private boolean updateAccounts(Loader<Cursor> loader, Cursor accounts) {
1039        if (accounts == null || !accounts.moveToFirst()) {
1040            return false;
1041        }
1042
1043        final Account[] allAccounts = Account.getAllAccounts(accounts);
1044
1045        // Save the uris for the accounts
1046        mCurrentAccountUris.clear();
1047        for (Account account : allAccounts) {
1048            mCurrentAccountUris.add(account.uri);
1049        }
1050
1051        // 1. current account is already set and is in allAccounts -> no-op
1052        // 2. current account is set and is not in allAccounts -> pick first (acct was deleted?)
1053        // 3. saved pref has an account -> pick that one
1054        // 4. otherwise just pick first
1055
1056        Account newAccount = null;
1057
1058        if (mAccount != null) {
1059            if (!mCurrentAccountUris.contains(mAccount.uri)) {
1060                newAccount = allAccounts[0];
1061            } else {
1062                newAccount = mAccount;
1063            }
1064        } else {
1065            final String lastAccountUri = MailAppProvider.getInstance()
1066                    .getLastViewedAccount();
1067            if (lastAccountUri != null) {
1068                for (int i = 0; i < allAccounts.length; i++) {
1069                    final Account acct = allAccounts[i];
1070                    if (lastAccountUri.equals(acct.uri.toString())) {
1071                        newAccount = acct;
1072                        break;
1073                    }
1074                }
1075            }
1076            if (newAccount == null) {
1077                newAccount = allAccounts[0];
1078            }
1079        }
1080
1081        onAccountChanged(newAccount);
1082
1083        mActionBarView.setAccounts(allAccounts);
1084        return (allAccounts.length > 0);
1085    }
1086
1087    private void disableNotifications() {
1088        mNewEmailReceiver.activate(mContext, this);
1089    }
1090
1091    private void enableNotifications() {
1092        mNewEmailReceiver.deactivate();
1093    }
1094
1095    private void disableNotificationsOnAccountChange(Account account) {
1096        // If the new mail suppression receiver is activated for a different account, we want to
1097        // activate it for the new account.
1098        if (mNewEmailReceiver.activated() &&
1099                !mNewEmailReceiver.notificationsDisabledForAccount(account)) {
1100            // Deactivate the current receiver, otherwise multiple receivers may be registered.
1101            mNewEmailReceiver.deactivate();
1102            mNewEmailReceiver.activate(mContext, this);
1103        }
1104    }
1105
1106    /**
1107     * {@inheritDoc}
1108     */
1109    @Override
1110    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
1111        // We want to reinitialize only if we haven't ever been initialized, or
1112        // if the current account has vanished.
1113        if (data == null) {
1114            LogUtils.e(LOG_TAG, "Received null cursor from loader id: %d", loader.getId());
1115        }
1116        switch (loader.getId()) {
1117            case LOADER_ACCOUNT_CURSOR:
1118                // If the account list is not not null, and the account list cursor is empty,
1119                // we need to start the specified activity.
1120                if (data != null && data.getCount() == 0) {
1121                    // If an empty cursor is returned, the MailAppProvider is indicating that
1122                    // no accounts have been specified.  We want to navigate to the "add account"
1123                    // activity that will handle the intent returned by the MailAppProvider
1124
1125                    // If the MailAppProvider believes that all accounts have been loaded, and the
1126                    // account list is still empty, we want to prompt the user to add an account
1127                    final Bundle extras = data.getExtras();
1128                    final boolean accountsLoaded =
1129                            extras.getInt(AccountCursorExtraKeys.ACCOUNTS_LOADED) != 0;
1130
1131                    if (accountsLoaded) {
1132                        final Intent noAccountIntent = MailAppProvider.getNoAccountIntent(mContext);
1133                        if (noAccountIntent != null) {
1134                            mActivity.startActivityForResult(noAccountIntent,
1135                                    ADD_ACCOUNT_REQUEST_CODE);
1136                        }
1137                    }
1138                } else {
1139                    final boolean accountListUpdated = accountsUpdated(data);
1140                    if (!isLoaderInitialized || accountListUpdated) {
1141                        isLoaderInitialized = updateAccounts(loader, data);
1142                    }
1143                }
1144                break;
1145            case LOADER_FOLDER_CURSOR:
1146                // Check status of the cursor.
1147                data.moveToFirst();
1148                Folder folder = new Folder(data);
1149                if (folder.isSyncInProgress()) {
1150                    mActionBarView.onRefreshStarted();
1151                } else {
1152                    // Stop the spinner here.
1153                    mActionBarView.onRefreshStopped(folder.lastSyncResult);
1154                }
1155                mActionBarView.onFolderUpdated(folder);
1156                if (mConversationListFragment != null) {
1157                    mConversationListFragment.onFolderUpdated(folder);
1158                }
1159                LogUtils.v(LOG_TAG, "FOLDER STATUS = %d", folder.syncStatus);
1160                break;
1161            case LOADER_RECENT_FOLDERS:
1162                mRecentFolderList.loadFromUiProvider(data);
1163                break;
1164            case LOADER_ACCOUNT_INBOX:
1165                if (data.moveToFirst() && !data.isClosed()) {
1166                    Folder inbox = new Folder(data);
1167                    onFolderChanged(inbox);
1168                    // Just want to get the inbox, don't care about updates to it
1169                    // as this will be tracked by the folder change listener.
1170                    mActivity.getLoaderManager().destroyLoader(LOADER_ACCOUNT_INBOX);
1171                } else {
1172                    LogUtils.d(LOG_TAG, "Unable to get the account inbox for account %s",
1173                            mAccount != null ? mAccount.name : "");
1174                }
1175                break;
1176            case LOADER_SEARCH:
1177                data.moveToFirst();
1178                Folder search = new Folder(data);
1179                setFolder(search);
1180                mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder,
1181                        mActivity.getIntent()
1182                                .getStringExtra(UIProvider.SearchQueryParameters.QUERY));
1183                showConversationList(mConvListContext);
1184                mActivity.invalidateOptionsMenu();
1185                mActivity.getLoaderManager().destroyLoader(LOADER_SEARCH);
1186                break;
1187        }
1188    }
1189
1190    @Override
1191    public void onTouchEvent(MotionEvent event) {
1192        if (event.getAction() == MotionEvent.ACTION_DOWN) {
1193            int mode = mViewMode.getMode();
1194            if (mode == ViewMode.CONVERSATION_LIST) {
1195                if (mConversationListFragment != null) {
1196                    mConversationListFragment.onTouchEvent(event);
1197                }
1198            } else if (mode == ViewMode.CONVERSATION) {
1199                if (mConversationViewFragment != null) {
1200                    mConversationViewFragment.onTouchEvent(event);
1201                }
1202            }
1203        }
1204    }
1205
1206    protected abstract class DestructiveActionListener implements ActionCompleteListener {
1207        protected final int mAction;
1208
1209        /**
1210         * Create a listener object. action is one of four constants: R.id.y_button (archive),
1211         * R.id.delete , R.id.mute, and R.id.report_spam.
1212         * @param action
1213         */
1214        public DestructiveActionListener(int action) {
1215            mAction = action;
1216        }
1217
1218        public void performConversationAction(Collection<Conversation> single) {
1219            switch (mAction) {
1220                case R.id.archive:
1221                    LogUtils.d(LOG_TAG, "Archiving conversation %s", mCurrentConversation);
1222                    Conversation.archive(mContext, single);
1223                    break;
1224                case R.id.delete:
1225                    LogUtils.d(LOG_TAG, "Deleting conversation %s", mCurrentConversation);
1226                    Conversation.delete(mContext, single);
1227                    break;
1228                case R.id.mute:
1229                    LogUtils.d(LOG_TAG, "Muting conversation %s", mCurrentConversation);
1230                    if (mFolder.supportsCapability(FolderCapabilities.DESTRUCTIVE_MUTE))
1231                        mCurrentConversation.localDeleteOnUpdate = true;
1232                    Conversation.mute(mContext, single);
1233                    break;
1234                case R.id.report_spam:
1235                    LogUtils.d(LOG_TAG, "reporting spam conversation %s", mCurrentConversation);
1236                    Conversation.reportSpam(mContext, single);
1237                    break;
1238            }
1239        }
1240
1241        public Conversation getNextConversation() {
1242            Conversation next = null;
1243            int pref = getAutoAdvanceSetting(mActivity);
1244            Cursor c = mConversationListCursor;
1245            if (c != null) {
1246                c.moveToPosition(mCurrentConversation.position);
1247            }
1248            switch (pref) {
1249                case AutoAdvance.NEWER:
1250                    if (c.moveToPrevious()) {
1251                        next = new Conversation(c);
1252                    }
1253                    break;
1254                case AutoAdvance.OLDER:
1255                    if (c.moveToNext()) {
1256                        next = new Conversation(c);
1257                    }
1258                    break;
1259            }
1260            return next;
1261        }
1262
1263        @Override
1264        public abstract void onActionComplete();
1265    }
1266
1267    // Called from the FolderSelectionDialog after a user is done changing
1268    // folders.
1269    @Override
1270    public void onFolderChangesCommit(ArrayList<Folder> folderChangeList) {
1271        // Get currently active folder info and compare it to the list
1272        // these conversations have been given; if they no longer contain
1273        // the selected folder, delete them from the list.
1274        HashSet<String> folderUris = new HashSet<String>();
1275        if (folderChangeList != null && !folderChangeList.isEmpty()) {
1276            for (Folder f : folderChangeList) {
1277                folderUris.add(f.uri.toString());
1278            }
1279        }
1280        final boolean destructiveChange = !folderUris.contains(mFolder.uri.toString());
1281        DestructiveActionListener listener = getFolderDestructiveActionListener();
1282        StringBuilder foldersUrisString = new StringBuilder();
1283        boolean first = true;
1284        for (Folder f : folderChangeList) {
1285            if (first) {
1286                first = false;
1287            } else {
1288                foldersUrisString.append(',');
1289            }
1290            foldersUrisString.append(f.uri.toString());
1291        }
1292        updateCurrentConversation(ConversationColumns.FOLDER_LIST, foldersUrisString.toString());
1293        updateCurrentConversation(ConversationColumns.RAW_FOLDERS,
1294                Folder.getSerializedFolderString(mFolder, folderChangeList));
1295        // TODO: (mindyp): set ConversationColumns.RAW_FOLDERS like in
1296        // SelectedConversationsActionMenu
1297        if (destructiveChange) {
1298            mCurrentConversation.localDeleteOnUpdate = true;
1299            requestDelete(listener);
1300        } else if (mConversationListFragment != null) {
1301            mConversationListFragment.requestListRefresh();
1302        }
1303    }
1304
1305    protected abstract DestructiveActionListener getFolderDestructiveActionListener();
1306
1307    @Override
1308    public void onRefreshRequired() {
1309        // Refresh the query in the background
1310        getConversationListCursor().refresh();
1311    }
1312
1313    @Override
1314    public void onRefreshReady() {
1315        ArrayList<Integer> deletedRows = mConversationListCursor.getRefreshDeletions();
1316        // If we have any deletions from the server, animate them away
1317        if (!deletedRows.isEmpty() && mConversationListFragment != null) {
1318            AnimatedAdapter adapter = mConversationListFragment.getAnimatedAdapter();
1319            if (adapter != null) {
1320                mConversationListFragment.getAnimatedAdapter().delete(deletedRows,
1321                       this);
1322            }
1323        } else {
1324            // Swap cursors
1325            getConversationListCursor().sync();
1326            refreshAdapter();
1327        }
1328    }
1329
1330    @Override
1331    public void onDataSetChanged() {
1332        refreshAdapter();
1333    }
1334
1335    private void refreshAdapter() {
1336        if (mConversationListFragment != null) {
1337            AnimatedAdapter adapter = mConversationListFragment.getAnimatedAdapter();
1338            if (adapter != null) {
1339                adapter.notifyDataSetChanged();
1340            }
1341        }
1342    }
1343
1344    @Override
1345    public void onSetEmpty() {
1346    }
1347
1348    @Override
1349    public void onSetPopulated(ConversationSelectionSet set) {
1350        mCabActionMenu = new SelectedConversationsActionMenu(mActivity, set,
1351                mConversationListFragment.getAnimatedAdapter(), this, mConversationListFragment,
1352                mAccount, mFolder, (SwipeableListView) mConversationListFragment.getListView());
1353        enableCabMode();
1354    }
1355
1356
1357    @Override
1358    public void onSetChanged(ConversationSelectionSet set) {
1359        // Do nothing. We don't care about changes to the set.
1360    }
1361
1362    @Override
1363    public ConversationSelectionSet getSelectedSet() {
1364        return mSelectedSet;
1365    }
1366
1367    /**
1368     * Disable the Contextual Action Bar (CAB). The selected set is not changed.
1369     */
1370    protected void disableCabMode() {
1371        if (mCabActionMenu != null) {
1372            mCabActionMenu.deactivate();
1373        }
1374    }
1375
1376    /**
1377     * Re-enable the CAB menu if required. The selection set is not changed.
1378     */
1379    protected void enableCabMode() {
1380        if (mCabActionMenu != null) {
1381            mCabActionMenu.activate();
1382        }
1383    }
1384
1385    @Override
1386    public void onActionComplete() {
1387        if (getConversationListCursor().isRefreshReady()) {
1388            refreshAdapter();
1389        }
1390    }
1391
1392    @Override
1393    public void startSearch() {
1394        if (mAccount.supportsCapability(UIProvider.AccountCapabilities.LOCAL_SEARCH)
1395                | mAccount.supportsCapability(UIProvider.AccountCapabilities.SERVER_SEARCH)) {
1396            onSearchRequested(mActionBarView.getQuery());
1397        } else {
1398            Toast.makeText(mActivity.getActivityContext(), mActivity.getActivityContext()
1399                    .getString(R.string.search_unsupported), Toast.LENGTH_SHORT).show();
1400        }
1401    }
1402
1403    /**
1404     * Supports dragging conversations to a folder.
1405     */
1406    @Override
1407    public boolean supportsDrag(DragEvent event, Folder folder) {
1408        return (folder != null
1409                && event != null
1410                && event.getClipDescription() != null
1411                && folder.supportsCapability
1412                    (UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES)
1413                && folder.supportsCapability
1414                    (UIProvider.FolderCapabilities.CAN_HOLD_MAIL)
1415                && !mFolder.uri.equals(folder.uri));
1416    }
1417
1418    /**
1419     * Handles dropping conversations to a label.
1420     */
1421    @Override
1422    public void handleDrop(DragEvent event, final Folder folder) {
1423        /*
1424         * Expect clip data has form: [conversations_uri, conversationId1,
1425         * maxMessageId1, label1, conversationId2, maxMessageId2, label2, ...]
1426         */
1427        if (!supportsDrag(event, folder)) {
1428            return;
1429        }
1430        ClipData data = event.getClipData();
1431        ArrayList<Integer> conversationPositions = Lists.newArrayList();
1432        for (int i = 1; i < data.getItemCount(); i += 3) {
1433            int position = Integer.parseInt(data.getItemAt(i).getText().toString());
1434            conversationPositions.add(position);
1435        }
1436        final Collection<Conversation> conversations = mSelectedSet.values();
1437        mConversationListFragment.requestDelete(conversations,
1438                new ActionCompleteListener() {
1439                    @Override
1440                    public void onActionComplete() {
1441                        AbstractActivityController.this.onActionComplete();
1442                        ArrayList<Folder> changes = new ArrayList<Folder>();
1443                        changes.add(folder);
1444                        Conversation.updateString(mContext, conversations,
1445                                ConversationColumns.FOLDER_LIST, folder.uri.toString());
1446                        Conversation.updateString(mContext, conversations,
1447                                ConversationColumns.RAW_FOLDERS,
1448                                Folder.getSerializedFolderString(mFolder, changes));
1449                        mConversationListFragment.onUndoAvailable(new UndoOperation(conversations
1450                                .size(), R.id.change_folder));
1451                        mSelectedSet.clear();
1452                    }
1453                });
1454    }
1455}
1456