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