AbstractActivityController.java revision 7ebdfd0f7b082c1e9aad07b2820352fb58beaa3b
1/*******************************************************************************
2 *      Copyright (C) 2012 Google Inc.
3 *      Licensed to The Android Open Source Project.
4 *
5 *      Licensed under the Apache License, Version 2.0 (the "License");
6 *      you may not use this file except in compliance with the License.
7 *      You may obtain a copy of the License at
8 *
9 *           http://www.apache.org/licenses/LICENSE-2.0
10 *
11 *      Unless required by applicable law or agreed to in writing, software
12 *      distributed under the License is distributed on an "AS IS" BASIS,
13 *      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *      See the License for the specific language governing permissions and
15 *      limitations under the License.
16 *******************************************************************************/
17
18package com.android.mail.ui;
19
20import android.app.ActionBar;
21import android.app.ActionBar.LayoutParams;
22import android.app.Activity;
23import android.app.AlertDialog;
24import android.app.Dialog;
25import android.app.LoaderManager;
26import android.content.ContentResolver;
27import android.content.Context;
28import android.content.CursorLoader;
29import android.content.DialogInterface;
30import android.content.Intent;
31import android.content.Loader;
32import android.database.Cursor;
33import android.net.Uri;
34import android.os.AsyncTask;
35import android.os.Bundle;
36import android.os.Handler;
37import android.text.TextUtils;
38import android.view.KeyEvent;
39import android.view.LayoutInflater;
40import android.view.Menu;
41import android.view.MenuInflater;
42import android.view.MenuItem;
43import android.view.MotionEvent;
44
45import com.android.mail.ConversationListContext;
46import com.android.mail.R;
47import com.android.mail.compose.ComposeActivity;
48import com.android.mail.providers.Account;
49import com.android.mail.providers.AccountCacheProvider;
50import com.android.mail.providers.Conversation;
51import com.android.mail.providers.Folder;
52import com.android.mail.providers.Settings;
53import com.android.mail.providers.UIProvider;
54import com.android.mail.providers.UIProvider.AutoAdvance;
55import com.android.mail.providers.UIProvider.ConversationColumns;
56import com.android.mail.providers.UIProvider.FolderCapabilities;
57import com.android.mail.utils.LogUtils;
58import com.android.mail.utils.Utils;
59import com.google.common.collect.ImmutableList;
60import com.google.common.collect.Sets;
61
62import java.util.ArrayList;
63import java.util.Arrays;
64import java.util.Collection;
65import java.util.Collections;
66import java.util.HashSet;
67import java.util.Set;
68
69
70/**
71 * This is an abstract implementation of the Activity Controller. This class
72 * knows how to respond to menu items, state changes, layout changes, etc. It
73 * weaves together the views and listeners, dispatching actions to the
74 * respective underlying classes.
75 * <p>
76 * Even though this class is abstract, it should provide default implementations
77 * for most, if not all the methods in the ActivityController interface. This
78 * makes the task of the subclasses easier: OnePaneActivityController and
79 * TwoPaneActivityController can be concise when the common functionality is in
80 * AbstractActivityController.
81 * </p>
82 * <p>
83 * In the Gmail codebase, this was called BaseActivityController
84 * </p>
85 */
86public abstract class AbstractActivityController implements ActivityController {
87    // Keys for serialization of various information in Bundles.
88    private static final String SAVED_LIST_CONTEXT = "saved-list-context";
89    private static final String SAVED_ACCOUNT = "saved-account";
90
91    /** Are we on a tablet device or not. */
92    public final boolean IS_TABLET_DEVICE;
93
94    protected Account mAccount;
95    protected Folder mFolder;
96    protected ActionBarView mActionBarView;
97    protected final RestrictedActivity mActivity;
98    protected final Context mContext;
99    protected final RecentFolderList mRecentFolderList;
100    protected ConversationListContext mConvListContext;
101    private FetchAccountFolderTask mFetchAccountFolderTask;
102    protected Conversation mCurrentConversation;
103
104    /** A {@link android.content.BroadcastReceiver} that suppresses new e-mail notifications. */
105    private SuppressNotificationReceiver mNewEmailReceiver = null;
106
107    protected Handler mHandler = new Handler();
108    protected ConversationListFragment mConversationListFragment;
109    /**
110     * The current mode of the application. All changes in mode are initiated by
111     * the activity controller. View mode changes are propagated to classes that
112     * attach themselves as listeners of view mode changes.
113     */
114    protected final ViewMode mViewMode;
115    protected ContentResolver mResolver;
116    protected FolderListFragment mFolderListFragment;
117    protected ConversationViewFragment mConversationViewFragment;
118    protected boolean isLoaderInitialized = false;
119    private AsyncRefreshTask mAsyncRefreshTask;
120
121    private final Set<Uri> mCurrentAccountUris = Sets.newHashSet();
122    protected Settings mCachedSettings;
123    private FetchSearchFolderTask mFetchSearchFolderTask;
124    private FetchInboxTask mFetchInboxTask;
125
126    protected static final String LOG_TAG = new LogUtils().getLogTag();
127    /** Constants used to differentiate between the types of loaders. */
128    private static final int LOADER_ACCOUNT_CURSOR = 0;
129    private static final int LOADER_ACCOUNT_SETTINGS = 1;
130    private static final int LOADER_FOLDER_CURSOR = 2;
131    private static final int LOADER_RECENT_FOLDERS = 3;
132
133    public AbstractActivityController(MailActivity activity, ViewMode viewMode) {
134        mActivity = activity;
135        mViewMode = viewMode;
136        mContext = activity.getApplicationContext();
137        IS_TABLET_DEVICE = Utils.useTabletUI(mContext);
138        mRecentFolderList = new RecentFolderList(mContext, this);
139    }
140
141    @Override
142    public synchronized void attachConversationList(ConversationListFragment fragment) {
143        // If there is an existing fragment, unregister it
144        if (mConversationListFragment != null) {
145            mViewMode.removeListener(mConversationListFragment);
146        }
147        mConversationListFragment = fragment;
148        // If the current fragment is non-null, add it as a listener.
149        if (fragment != null) {
150            mViewMode.addListener(mConversationListFragment);
151        }
152    }
153
154    @Override
155    public synchronized void attachFolderList(FolderListFragment fragment) {
156        // If there is an existing fragment, unregister it
157        if (mFolderListFragment != null) {
158            mViewMode.removeListener(mFolderListFragment);
159        }
160        mFolderListFragment = fragment;
161        if (fragment != null) {
162            mViewMode.addListener(mFolderListFragment);
163        }
164    }
165
166    @Override
167    public void attachConversationView(ConversationViewFragment conversationViewFragment) {
168        mConversationViewFragment = conversationViewFragment;
169    }
170
171    @Override
172    public void clearSubject() {
173        // TODO(viki): Auto-generated method stub
174    }
175
176    @Override
177    public Account getCurrentAccount() {
178        return mAccount;
179    }
180
181    @Override
182    public ConversationListContext getCurrentListContext() {
183        return mConvListContext;
184    }
185
186    @Override
187    public String getHelpContext() {
188        return "Mail";
189    }
190
191    @Override
192    public int getMode() {
193        return mViewMode.getMode();
194    }
195
196    @Override
197    public String getUnshownSubject(String subject) {
198        // Calculate how much of the subject is shown, and return the remaining.
199        return null;
200    }
201
202    @Override
203    public void handleConversationLoadError() {
204        // TODO(viki): Auto-generated method stub
205    }
206
207    /**
208     * Initialize the action bar. This is not visible to OnePaneController and
209     * TwoPaneController so they cannot override this behavior.
210     */
211    private void initCustomActionBarView() {
212        ActionBar actionBar = mActivity.getActionBar();
213        mActionBarView = (ActionBarView) LayoutInflater.from(mContext).inflate(
214                R.layout.actionbar_view, null);
215        if (actionBar != null && mActionBarView != null) {
216            // Why have a different variable for the same thing? We should apply
217            // the same actions
218            // on mActionBarView instead.
219            mActionBarView.initialize(mActivity, this, mViewMode, actionBar, mRecentFolderList);
220            actionBar.setCustomView(mActionBarView, new ActionBar.LayoutParams(
221                    LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
222            actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM,
223                    ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_SHOW_TITLE);
224        }
225    }
226
227    /**
228     * Returns whether the conversation list fragment is visible or not.
229     * Different layouts will have their own notion on the visibility of
230     * fragments, so this method needs to be overriden.
231     *
232     * @return
233     */
234    protected abstract boolean isConversationListVisible();
235
236    @Override
237    public void onAccountChanged(Account account) {
238        if (!account.equals(mAccount)) {
239            mAccount = account;
240            mRecentFolderList.setCurrentAccount(account);
241            restartOptionalLoader(LOADER_RECENT_FOLDERS, null /* args */);
242            restartOptionalLoader(LOADER_ACCOUNT_SETTINGS, null /* args */);
243            mActionBarView.setAccount(mAccount);
244            mActivity.invalidateOptionsMenu();
245
246            disableNotificationsOnAccountChange(mAccount);
247
248            // Account changed; existing folder is invalid.
249            mFolder = null;
250            fetchAccountFolderInfo();
251        }
252    }
253
254    public void onSettingsChanged(Settings settings) {
255        mCachedSettings = settings;
256        resetActionBarIcon();
257    }
258
259    @Override
260    public Settings getSettings() {
261        return mCachedSettings;
262    }
263
264    private void fetchAccountFolderInfo() {
265        if (mFetchAccountFolderTask != null) {
266            mFetchAccountFolderTask.cancel(true);
267        }
268        mFetchAccountFolderTask = new FetchAccountFolderTask();
269        mFetchAccountFolderTask.execute();
270    }
271
272    private void fetchSearchFolder(Intent intent) {
273        if (mFetchSearchFolderTask != null) {
274            mFetchSearchFolderTask.cancel(true);
275        }
276        mFetchSearchFolderTask = new FetchSearchFolderTask(intent
277                .getStringExtra(ConversationListContext.EXTRA_SEARCH_QUERY));
278        mFetchSearchFolderTask.execute();
279    }
280
281    @Override
282    public void onFolderChanged(Folder folder) {
283        if (folder != null && !folder.equals(mFolder)) {
284            setFolder(folder);
285            mConvListContext = ConversationListContext.forFolder(mContext, mAccount, mFolder);
286            showConversationList(mConvListContext);
287
288            // Add the folder that we were viewing to the recent folders list.
289            // TODO: this may need to be fine tuned.  If this is the signal that is indicating that
290            // the list is shown to the user, this could fire in one pane if the user goes directly
291            // to a conversation
292            updateRecentFolderList();
293        }
294    }
295
296    private void updateRecentFolderList() {
297        mRecentFolderList.setCurrentAccount(mAccount);
298        mRecentFolderList.touchFolder(mFolder);
299    }
300
301    // TODO(mindyp): set this up to store a copy of the folder locally
302    // as soon as we realize we haven't gotten the inbox folder yet.
303    public void loadInbox() {
304        if (mFetchInboxTask != null) {
305            mFetchInboxTask.cancel(true);
306        }
307        mFetchInboxTask = new FetchInboxTask();
308        mFetchInboxTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
309    }
310
311    /** Set the current folder */
312    private void setFolder(Folder folder) {
313        // Start watching folder for sync status.
314        if (folder != null && !folder.equals(mFolder)) {
315            mActionBarView.setRefreshInProgress(false);
316            mFolder = folder;
317            mActionBarView.setFolder(mFolder);
318            mActivity.getLoaderManager().restartLoader(LOADER_FOLDER_CURSOR, null, this);
319        } else if (folder == null) {
320            LogUtils.wtf(LOG_TAG, "Folder in setFolder is null");
321        }
322    }
323
324    @Override
325    public void onActivityResult(int requestCode, int resultCode, Intent data) {
326        // TODO(viki): Auto-generated method stub
327    }
328
329    @Override
330    public void onConversationListVisibilityChanged(boolean visible) {
331        // TODO(viki): Auto-generated method stub
332    }
333
334    /**
335     * By default, doing nothing is right. A two-pane controller will need to
336     * override this.
337     */
338    @Override
339    public void onConversationVisibilityChanged(boolean visible) {
340        // Do nothing.
341        return;
342    }
343
344    @Override
345    public boolean onCreate(Bundle savedState) {
346        // Initialize the action bar view.
347        initCustomActionBarView();
348        // Allow shortcut keys to function for the ActionBar and menus.
349        mActivity.setDefaultKeyMode(Activity.DEFAULT_KEYS_SHORTCUT);
350        mResolver = mActivity.getContentResolver();
351
352        mNewEmailReceiver = new SuppressNotificationReceiver();
353
354        // All the individual UI components listen for ViewMode changes. This
355        // simplifies the amount of logic in the AbstractActivityController, but increases the
356        // possibility of timing-related bugs.
357        mViewMode.addListener(this);
358        assert (mActionBarView != null);
359        mViewMode.addListener(mActionBarView);
360
361        restoreState(savedState);
362        return true;
363    }
364
365    @Override
366    public Dialog onCreateDialog(int id, Bundle bundle) {
367        // TODO(viki): Auto-generated method stub
368        return null;
369    }
370
371    @Override
372    public boolean onCreateOptionsMenu(Menu menu) {
373        MenuInflater inflater = mActivity.getMenuInflater();
374        inflater.inflate(mActionBarView.getOptionsMenuId(), menu);
375        mActionBarView.onCreateOptionsMenu(menu);
376        return true;
377    }
378
379    @Override
380    public boolean onKeyDown(int keyCode, KeyEvent event) {
381        // TODO(viki): Auto-generated method stub
382        return false;
383    }
384
385    @Override
386    public boolean onOptionsItemSelected(MenuItem item) {
387        final int id = item.getItemId();
388        boolean handled = true;
389        switch (id) {
390            case android.R.id.home:
391                onUpPressed();
392                break;
393            case R.id.compose:
394                ComposeActivity.compose(mActivity.getActivityContext(), mAccount);
395                break;
396            case R.id.show_all_folders:
397                showFolderList();
398                break;
399            case R.id.refresh:
400                requestFolderRefresh();
401                break;
402            case R.id.settings:
403                Utils.showSettings(mActivity.getActivityContext(), mAccount);
404                break;
405            case R.id.help_info_menu_item:
406                // TODO: enable context sensitive help
407                Utils.showHelp(mActivity.getActivityContext(), mAccount.helpIntentUri, null);
408                break;
409            case R.id.feedback_menu_item:
410                Utils.sendFeedback(mActivity.getActivityContext(), mAccount);
411                break;
412            default:
413                handled = false;
414                break;
415        }
416        return handled;
417    }
418
419    /**
420     * Return the auto advance setting for the current account.
421     * @param activity
422     * @return the autoadvance setting, a constant from {@link AutoAdvance}
423     */
424    static int getAutoAdvanceSetting(RestrictedActivity activity) {
425        final Settings settings = activity.getSettings();
426        // TODO(mindyp): if this isn't set, then show the dialog telling the user to set it.
427        // Remove defaulting to AutoAdvance.LIST.
428        final int autoAdvance = (settings != null) ?
429                (settings.autoAdvance == AutoAdvance.UNSET ?
430                        AutoAdvance.LIST : settings.autoAdvance)
431                : AutoAdvance.LIST;
432        return autoAdvance;
433    }
434
435    /**
436     * Implements folder changes. This class is a listener because folder changes need to be
437     * performed <b>after</b> the ConversationListFragment has finished animating away the
438     * removal of the conversation.
439     *
440     */
441    private class FolderChangeListener implements ActionCompleteListener {
442        private final String mFolderChangeList;
443        private final boolean mDestructiveChange;
444
445        public FolderChangeListener(String changeList, boolean destructive) {
446            mFolderChangeList = changeList;
447            mDestructiveChange = destructive;
448        }
449
450        @Override
451        public void onActionComplete() {
452            // Only show undo if this was a destructive folder change.
453            if (mDestructiveChange) {
454                mConversationListFragment.onUndoAvailable(new UndoOperation(1, R.id.change_folder));
455            }
456            // Update the folders for this conversation
457            Conversation.updateString(mContext, Collections.singletonList(mCurrentConversation),
458                    ConversationColumns.FOLDER_LIST, mFolderChangeList);
459            mConversationListFragment.requestListRefresh();
460        }
461    }
462
463    /**
464     * Update the specified column name in conversation for a boolean value.
465     * @param columnName
466     * @param value
467     */
468    protected void updateCurrentConversation(String columnName, boolean value) {
469        Conversation.updateBoolean(mContext, ImmutableList.of(mCurrentConversation), columnName,
470                value);
471        mConversationListFragment.requestListRefresh();
472    }
473
474    /**
475     * Update the specified column name in conversation for an integer value.
476     * @param columnName
477     * @param value
478     */
479    protected void updateCurrentConversation(String columnName, int value) {
480        Conversation.updateInt(mContext, ImmutableList.of(mCurrentConversation), columnName, value);
481        mConversationListFragment.requestListRefresh();
482    }
483
484    private void requestFolderRefresh() {
485        if (mFolder != null) {
486            if (mAsyncRefreshTask != null) {
487                mAsyncRefreshTask.cancel(true);
488            }
489            mAsyncRefreshTask = new AsyncRefreshTask(mContext, mFolder);
490            mAsyncRefreshTask.execute();
491        }
492    }
493
494    /**
495     * Confirm (based on user's settings) and delete a conversation from the conversation list and
496     * from the database.
497     * @param showDialog
498     * @param confirmResource
499     * @param listener
500     */
501    protected void confirmAndDelete(boolean showDialog, int confirmResource,
502            final ActionCompleteListener listener) {
503        final ArrayList<Conversation> single = new ArrayList<Conversation>();
504        single.add(mCurrentConversation);
505        if (showDialog) {
506            final AlertDialog.OnClickListener onClick = new AlertDialog.OnClickListener() {
507                @Override
508                public void onClick(DialogInterface dialog, int which) {
509                    requestDelete(listener);
510                }
511            };
512            final CharSequence message = Utils.formatPlural(mContext, confirmResource, 1);
513            new AlertDialog.Builder(mActivity.getActivityContext()).setMessage(message)
514                    .setPositiveButton(R.string.ok, onClick)
515                    .setNegativeButton(R.string.cancel, null)
516                    .create().show();
517        } else {
518            requestDelete(listener);
519        }
520    }
521
522
523    protected abstract void requestDelete(ActionCompleteListener listener);
524
525    @Override
526    public void onCommit(String uris) {
527        // Get currently active folder info and compare it to the list
528        // these conversations have been given; if they no longer contain
529        // the selected folder, delete them from the list.
530        HashSet<String> folderUris = new HashSet<String>();
531        if (!TextUtils.isEmpty(uris)) {
532            folderUris.addAll(Arrays.asList(uris.split(",")));
533        }
534        final boolean destructiveChange = !folderUris.contains(mFolder.uri);
535        FolderChangeListener listener = new FolderChangeListener(uris, destructiveChange);
536        if (destructiveChange) {
537            mCurrentConversation.localDeleteOnUpdate = true;
538            mConversationListFragment.requestDelete(listener);
539        } else {
540            listener.onActionComplete();
541        }
542    }
543
544    @Override
545    public void onPrepareDialog(int id, Dialog dialog, Bundle bundle) {
546        // TODO(viki): Auto-generated method stub
547
548    }
549
550    @Override
551    public boolean onPrepareOptionsMenu(Menu menu) {
552        mActionBarView.onPrepareOptionsMenu(menu);
553        return true;
554    }
555
556    @Override
557    public void onPause() {
558        isLoaderInitialized = false;
559
560        enableNotifications();
561    }
562
563    @Override
564    public void onResume() {
565        // Register the receiver that will prevent the status receiver from
566        // displaying its notification icon as long as we're running.
567        // The SupressNotificationReceiver will block the broadcast if we're looking at the folder
568        // that the notification was received for.
569        disableNotifications();
570
571        if (mActionBarView != null) {
572            mActionBarView.onResume();
573        }
574
575    }
576
577    @Override
578    public void onSaveInstanceState(Bundle outState) {
579        if (mAccount != null) {
580            LogUtils.d(LOG_TAG, "Saving the account now");
581            outState.putParcelable(SAVED_ACCOUNT, mAccount);
582        }
583        if (mConvListContext != null) {
584            outState.putBundle(SAVED_LIST_CONTEXT, mConvListContext.toBundle());
585        }
586    }
587
588    @Override
589    public void onSearchRequested(String query) {
590        Intent intent = new Intent();
591        intent.setAction(Intent.ACTION_SEARCH);
592        intent.putExtra(ConversationListContext.EXTRA_SEARCH_QUERY, query);
593        intent.putExtra(Utils.EXTRA_ACCOUNT, mAccount);
594        intent.setComponent(mActivity.getComponentName());
595        mActivity.startActivity(intent);
596    }
597
598    @Override
599    public void onStartDragMode() {
600        // TODO(viki): Auto-generated method stub
601    }
602
603    @Override
604    public void onStop() {
605        // TODO(viki): Auto-generated method stub
606    }
607
608    @Override
609    public void onStopDragMode() {
610        // TODO(viki): Auto-generated method stub
611    }
612
613    /**
614     * {@inheritDoc} Subclasses must override this to listen to mode changes
615     * from the ViewMode. Subclasses <b>must</b> call the parent's
616     * onViewModeChanged since the parent will handle common state changes.
617     */
618    @Override
619    public void onViewModeChanged(int newMode) {
620        // Perform any mode specific work here.
621        // reset the action bar icon based on the mode. Why don't the individual
622        // controllers do
623        // this themselves?
624
625        // In conversation list mode, clean up the conversation.
626        if (newMode == ViewMode.CONVERSATION_LIST) {
627            // Clean up the conversation here.
628        }
629
630        // We don't want to invalidate the options menu when switching to
631        // conversation
632        // mode, as it will happen when the conversation finishes loading.
633        if (newMode != ViewMode.CONVERSATION) {
634            mActivity.invalidateOptionsMenu();
635        }
636    }
637
638    @Override
639    public void onWindowFocusChanged(boolean hasFocus) {
640        // TODO(viki): Auto-generated method stub
641    }
642
643    /**
644     * @param savedState
645     */
646    protected void restoreListContext(Bundle savedState) {
647        Bundle listContextBundle = savedState.getBundle(SAVED_LIST_CONTEXT);
648        if (listContextBundle != null) {
649            mConvListContext = ConversationListContext.forBundle(listContextBundle);
650            mFolder = mConvListContext.folder;
651        }
652    }
653
654    /**
655     * Restore the state from the previous bundle. Subclasses should call this
656     * method from the parent class, since it performs important UI
657     * initialization.
658     *
659     * @param savedState
660     */
661    protected void restoreState(Bundle savedState) {
662        final Intent intent = mActivity.getIntent();
663        if (savedState != null) {
664            restoreListContext(savedState);
665            mAccount = savedState.getParcelable(SAVED_ACCOUNT);
666            mActionBarView.setAccount(mAccount);
667            restartOptionalLoader(LOADER_ACCOUNT_SETTINGS, null /* args */);
668        } else if (intent != null) {
669            if (Intent.ACTION_VIEW.equals(intent.getAction())) {
670                if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) {
671                    mAccount = ((Account) intent.getParcelableExtra(Utils.EXTRA_ACCOUNT));
672                    mActionBarView.setAccount(mAccount);
673                    restartOptionalLoader(LOADER_ACCOUNT_SETTINGS, null /* args */);
674                    mActivity.invalidateOptionsMenu();
675                }
676                if (intent.hasExtra(Utils.EXTRA_FOLDER)) {
677                    // Open the folder.
678                    LogUtils.d(LOG_TAG, "SHOW THE FOLDER at %s",
679                            intent.getParcelableExtra(Utils.EXTRA_FOLDER));
680                    onFolderChanged((Folder) intent.getParcelableExtra(Utils.EXTRA_FOLDER));
681                }
682                if (intent.hasExtra(Utils.EXTRA_CONVERSATION)) {
683                    // Open the conversation.
684                    LogUtils.d(LOG_TAG, "SHOW THE CONVERSATION at %s",
685                            intent.getParcelableExtra(Utils.EXTRA_CONVERSATION));
686                    setCurrentConversation((Conversation) intent
687                            .getParcelableExtra(Utils.EXTRA_CONVERSATION));
688                    showConversation(this.mCurrentConversation);
689                }
690            } else if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
691                mViewMode.enterSearchResultsListMode();
692                mAccount = ((Account) intent.getParcelableExtra(Utils.EXTRA_ACCOUNT));
693                mActionBarView.setAccount(mAccount);
694                fetchSearchFolder(intent);
695            }
696        }
697        // Create the accounts loader; this loads the account switch spinner.
698        mActivity.getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this);
699    }
700
701    @Override
702    public void setSubject(String subject) {
703        // Do something useful with the subject. This requires changing the
704        // conversation view's subject text.
705    }
706
707    /**
708     * Children can override this method, but they must call super.showConversation().
709     * {@inheritDoc}
710     */
711    @Override
712    public void showConversation(Conversation conversation) {
713    }
714
715    @Override
716    public void onConversationSelected(Conversation conversation) {
717        setCurrentConversation(conversation);
718        showConversation(mCurrentConversation);
719        if (mConvListContext != null && mConvListContext.isSearchResult()) {
720            mViewMode.enterSearchResultsConversationMode();
721        } else {
722            mViewMode.enterConversationMode();
723        }
724    }
725
726    public void setCurrentConversation(Conversation conversation) {
727        mCurrentConversation = conversation;
728    }
729
730    /**
731     * {@inheritDoc}
732     */
733    @Override
734    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
735        // Create a loader to listen in on account changes.
736        switch (id) {
737            case LOADER_ACCOUNT_CURSOR:
738                return new CursorLoader(mContext, AccountCacheProvider.getAccountsUri(),
739                        UIProvider.ACCOUNTS_PROJECTION, null, null, null);
740            case LOADER_FOLDER_CURSOR:
741                return new CursorLoader(mContext, mFolder.uri,
742                        UIProvider.FOLDERS_PROJECTION, null, null, null);
743            case LOADER_ACCOUNT_SETTINGS:
744                if (mAccount.settingsQueryUri != null) {
745                    return new CursorLoader(mContext, mAccount.settingsQueryUri,
746                            UIProvider.SETTINGS_PROJECTION, null, null, null);
747                }
748                break;
749            case LOADER_RECENT_FOLDERS:
750                if (mAccount.recentFolderListUri != null) {
751                    return new CursorLoader(mContext, mAccount.recentFolderListUri,
752                            UIProvider.FOLDERS_PROJECTION, null, null, null);
753                }
754                break;
755            default:
756                LogUtils.wtf(LOG_TAG, "Loader returned unexpected id: " + id);
757        }
758        return null;
759    }
760
761    /**
762     * {@link LoaderManager} currently has a bug in
763     * {@link LoaderManager#restartLoader(int, Bundle, android.app.LoaderManager.LoaderCallbacks)}
764     * where, if a previous onCreateLoader returned a null loader, this method will NPE. Work around
765     * this bug by destroying any loaders that may have been created as null (essentially because
766     * they are optional loads, and may not apply to a particular account).
767     * <p>
768     * A simple null check before restarting a loader will not work, because that would not
769     * give the controller a chance to invalidate UI corresponding the prior loader result.
770     *
771     * @param id loader ID to safely restart
772     * @param args arguments to pass to the restarted loader
773     */
774    private void restartOptionalLoader(int id, Bundle args) {
775        final LoaderManager lm = mActivity.getLoaderManager();
776        lm.destroyLoader(id);
777        lm.restartLoader(id, args, this);
778    }
779
780    private boolean accountsUpdated(Cursor accountCursor) {
781        // Check to see if the current account hasn't been set, or the account cursor is empty
782        if (mAccount == null || !accountCursor.moveToFirst()) {
783            return true;
784        }
785
786        // Check to see if the number of accounts are different, from the number we saw on the last
787        // updated
788        if (mCurrentAccountUris.size() != accountCursor.getCount()) {
789            return true;
790        }
791
792        // Check to see if the account list is different or if the current account is not found in
793        // the cursor.
794        boolean foundCurrentAccount = false;
795        do {
796            final Uri accountUri =
797                    Uri.parse(accountCursor.getString(UIProvider.ACCOUNT_URI_COLUMN));
798            if (!foundCurrentAccount && mAccount.uri.equals(accountUri)) {
799                foundCurrentAccount = true;
800            }
801
802            if (!mCurrentAccountUris.contains(accountUri)) {
803                return true;
804            }
805        } while (accountCursor.moveToNext());
806
807        // As long as we found the current account, the list hasn't been updated
808        return !foundCurrentAccount;
809    }
810
811    /**
812     * Update the accounts on the device. This currently loads the first account
813     * in the list.
814     *
815     * @param loader
816     * @param accounts cursor into the AccountCache
817     * @return true if the update was successful, false otherwise
818     */
819    private boolean updateAccounts(Loader<Cursor> loader, Cursor accounts) {
820        if (accounts == null || !accounts.moveToFirst()) {
821            return false;
822        }
823
824        final Account[] allAccounts = Account.getAllAccounts(accounts);
825
826        // Save the uris for the accounts
827        mCurrentAccountUris.clear();
828        for (Account account : allAccounts) {
829            mCurrentAccountUris.add(account.uri);
830        }
831
832        final Account newAccount;
833        if (mAccount == null || !mCurrentAccountUris.contains(mAccount.uri)) {
834            accounts.moveToFirst();
835            newAccount = new Account(accounts);
836        } else {
837            newAccount = mAccount;
838        }
839        // Only bother updating the account/folder if the new account is different than the
840        // existing one
841        final boolean refetchFolderInfo = !newAccount.equals(mAccount);
842        onAccountChanged(newAccount);
843
844        if(refetchFolderInfo) {
845            fetchAccountFolderInfo();
846        }
847
848        mActionBarView.setAccounts(allAccounts);
849        return (allAccounts.length > 0);
850    }
851
852    private void disableNotifications() {
853        mNewEmailReceiver.activate(mContext, this);
854    }
855
856    private void enableNotifications() {
857        mNewEmailReceiver.deactivate();
858    }
859
860    private void disableNotificationsOnAccountChange(Account account) {
861        // If the new mail suppression receiver is activated for a different account, we want to
862        // activate it for the new account.
863        if (mNewEmailReceiver.activated() &&
864                !mNewEmailReceiver.notificationsDisabledForAccount(account)) {
865            // Deactivate the current receiver, otherwise multiple receivers may be registered.
866            mNewEmailReceiver.deactivate();
867            mNewEmailReceiver.activate(mContext, this);
868        }
869    }
870
871    /**
872     * {@inheritDoc}
873     */
874    @Override
875    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
876        // We want to reinitialize only if we haven't ever been initialized, or
877        // if the current account has vanished.
878        if (data == null) {
879            LogUtils.e(LOG_TAG, "Received null cursor from loader id: %d", loader.getId());
880        }
881        switch (loader.getId()) {
882            case LOADER_ACCOUNT_CURSOR:
883                final boolean accountListUpdated = accountsUpdated(data);
884                if (!isLoaderInitialized || accountListUpdated) {
885                    isLoaderInitialized = updateAccounts(loader, data);
886                }
887                break;
888            case LOADER_FOLDER_CURSOR:
889                // Check status of the cursor.
890                data.moveToFirst();
891                Folder folder = new Folder(data);
892                if (folder.isSyncInProgress()) {
893                    mActionBarView.onRefreshStarted();
894                } else {
895                    // Stop the spinner here.
896                    mActionBarView.onRefreshStopped(folder.lastSyncResult);
897                }
898                if (mConversationListFragment != null) {
899                    mConversationListFragment.onFolderUpdated(folder);
900                }
901                LogUtils.v(LOG_TAG, "FOLDER STATUS = " + folder.syncStatus);
902                break;
903            case LOADER_ACCOUNT_SETTINGS:
904                data.moveToFirst();
905                onSettingsChanged(new Settings(data));
906                break;
907            case LOADER_RECENT_FOLDERS:
908                mRecentFolderList.loadFromUiProvider(data);
909                break;
910        }
911    }
912
913    /**
914     * {@inheritDoc}
915     */
916    @Override
917    public void onLoaderReset(Loader<Cursor> loader) {
918        switch (loader.getId()) {
919            case LOADER_ACCOUNT_SETTINGS:
920                onSettingsChanged(null);
921                break;
922        }
923    }
924
925    @Override
926    public void onTouchEvent(MotionEvent event) {
927        if (event.getAction() == MotionEvent.ACTION_DOWN) {
928            int mode = mViewMode.getMode();
929            if (mode == ViewMode.CONVERSATION_LIST) {
930                mConversationListFragment.onTouchEvent(event);
931            } else if (mode == ViewMode.CONVERSATION) {
932                mConversationViewFragment.onTouchEvent(event);
933            }
934        }
935    }
936
937    private class FetchInboxTask extends AsyncTask<Void, Void, ConversationListContext> {
938        @Override
939        public ConversationListContext doInBackground(Void... params) {
940            // Gets the default inbox since there is no context.
941            return ConversationListContext.forFolder(mActivity.getActivityContext(), mAccount,
942                    (Folder) null);
943        }
944
945        @Override
946        public void onPostExecute(ConversationListContext result) {
947            mConvListContext = result;
948            setFolder(mConvListContext.folder);
949            if (mFolderListFragment != null) {
950                mFolderListFragment.selectFolder(mConvListContext.folder);
951            }
952            showConversationList(mConvListContext);
953
954            // Add the folder that we were viewing to the recent folders list.
955            updateRecentFolderList();
956        }
957    }
958
959    private class FetchAccountFolderTask extends AsyncTask<Void, Void, ConversationListContext> {
960        @Override
961        public ConversationListContext doInBackground(Void... params) {
962            return ConversationListContext.forFolder(mContext, mAccount, mFolder);
963        }
964
965        @Override
966        public void onPostExecute(ConversationListContext result) {
967            mConvListContext = result;
968            setFolder(mConvListContext.folder);
969            if (mFolderListFragment != null) {
970                mFolderListFragment.selectFolder(mConvListContext.folder);
971            }
972            showConversationList(mConvListContext);
973            mFetchAccountFolderTask = null;
974
975            // Add the folder that we were viewing to the recent folders list.
976            updateRecentFolderList();
977        }
978    }
979
980    private class FetchSearchFolderTask extends AsyncTask<Void, Void, Folder> {
981        String mQuery;
982        public FetchSearchFolderTask(String query) {
983            mQuery = query;
984        }
985
986        @Override
987        public Folder doInBackground(Void... params) {
988            Folder searchFolder = Folder.forSearchResults(mAccount, mQuery,
989                    mActivity.getActivityContext());
990            return searchFolder;
991        }
992
993        @Override
994        public void onPostExecute(Folder folder) {
995            setFolder(folder);
996            mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder, mQuery);
997            showConversationList(mConvListContext);
998            mActivity.invalidateOptionsMenu();
999        }
1000    }
1001
1002    protected abstract class DestructiveActionListener implements ActionCompleteListener {
1003        protected final int mAction;
1004
1005        /**
1006         * Create a listener object. action is one of four constants: R.id.y_button (archive),
1007         * R.id.delete , R.id.mute, and R.id.report_spam.
1008         * @param action
1009         */
1010        public DestructiveActionListener(int action) {
1011            mAction = action;
1012        }
1013
1014        public void performConversationAction(Collection<Conversation> single) {
1015            switch (mAction) {
1016                case R.id.y_button:
1017                    LogUtils.d(LOG_TAG, "Archiving conversation " + mCurrentConversation);
1018                    Conversation.archive(mContext, single);
1019                    break;
1020                case R.id.delete:
1021                    LogUtils.d(LOG_TAG, "Deleting conversation " + mCurrentConversation);
1022                    Conversation.delete(mContext, single);
1023                    break;
1024                case R.id.mute:
1025                    LogUtils.d(LOG_TAG, "Muting conversation " + mCurrentConversation);
1026                    if (mFolder.supportsCapability(FolderCapabilities.DESTRUCTIVE_MUTE))
1027                        mCurrentConversation.localDeleteOnUpdate = true;
1028                    Conversation.mute(mContext, single);
1029                    break;
1030                case R.id.report_spam:
1031                    LogUtils.d(LOG_TAG, "reporting spam conversation " + mCurrentConversation);
1032                    Conversation.reportSpam(mContext, single);
1033                    break;
1034            }
1035        }
1036
1037        public Conversation getNextConversation() {
1038            Conversation next = null;
1039            int pref = getAutoAdvanceSetting(mActivity);
1040            Cursor c = mConversationListFragment.getConversationListCursor();
1041            if (c != null) {
1042                c.moveToPosition(mCurrentConversation.position);
1043            }
1044            switch (pref) {
1045                case AutoAdvance.NEWER:
1046                    if (c.moveToPrevious()) {
1047                        next = new Conversation(c);
1048                    }
1049                    break;
1050                case AutoAdvance.OLDER:
1051                    if (c.moveToNext()) {
1052                        next = new Conversation(c);
1053                    }
1054                    break;
1055            }
1056            return next;
1057        }
1058
1059        @Override
1060        public abstract void onActionComplete();
1061    }
1062}
1063