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