AbstractActivityController.java revision f9323cdb22acca9ed7873da90407863517ebd90b
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.Dialog;
24import android.content.ContentResolver;
25import android.content.Context;
26import android.content.CursorLoader;
27import android.content.Intent;
28import android.content.Loader;
29import android.database.Cursor;
30import android.os.AsyncTask;
31import android.os.Bundle;
32import android.os.Handler;
33import android.view.ActionMode;
34import android.view.KeyEvent;
35import android.view.LayoutInflater;
36import android.view.Menu;
37import android.view.MenuInflater;
38import android.view.MenuItem;
39import android.view.MotionEvent;
40import android.view.View;
41import android.widget.LinearLayout;
42import android.widget.Toast;
43
44import com.android.mail.R;
45import com.android.mail.ConversationListContext;
46import com.android.mail.compose.ComposeActivity;
47import com.android.mail.providers.Account;
48import com.android.mail.providers.AccountCacheProvider;
49import com.android.mail.providers.Conversation;
50import com.android.mail.providers.Folder;
51import com.android.mail.providers.UIProvider;
52import com.android.mail.providers.UIProvider.AccountCapabilities;
53import com.android.mail.providers.UIProvider.LastSyncResult;
54import com.android.mail.ui.AsyncRefreshTask;
55import com.android.mail.utils.LogUtils;
56import com.android.mail.utils.Utils;
57
58/**
59 * This is an abstract implementation of the Activity Controller. This class
60 * knows how to respond to menu items, state changes, layout changes, etc. It
61 * weaves together the views and listeners, dispatching actions to the
62 * respective underlying classes.
63 * <p>
64 * Even though this class is abstract, it should provide default implementations
65 * for most, if not all the methods in the ActivityController interface. This
66 * makes the task of the subclasses easier: OnePaneActivityController and
67 * TwoPaneActivityController can be concise when the common functionality is in
68 * AbstractActivityController.
69 * </p>
70 * <p>
71 * In the Gmail codebase, this was called BaseActivityController
72 * </p>
73 */
74public abstract class AbstractActivityController implements ActivityController {
75    private static final String SAVED_CONVERSATION = "saved-conversation";
76    private static final String SAVED_CONVERSATION_POSITION = "saved-conv-pos";
77    // Keys for serialization of various information in Bundles.
78    private static final String SAVED_LIST_CONTEXT = "saved-list-context";
79
80    /**
81     * Are we on a tablet device or not.
82     */
83    public final boolean IS_TABLET_DEVICE;
84
85    protected Account mAccount;
86    protected Folder mFolder;
87    protected ActionBarView mActionBarView;
88    protected final RestrictedActivity mActivity;
89    protected final Context mContext;
90    protected ConversationListContext mConvListContext;
91    private FetchAccountFolderTask mFetchAccountFolderTask;
92    protected Conversation mCurrentConversation;
93
94    protected ConversationListFragment mConversationListFragment;
95    /**
96     * The current mode of the application. All changes in mode are initiated by
97     * the activity controller. View mode changes are propagated to classes that
98     * attach themselves as listeners of view mode changes.
99     */
100    protected final ViewMode mViewMode;
101    protected ContentResolver mResolver;
102    protected FolderListFragment mFolderListFragment;
103    protected ConversationViewFragment mConversationViewFragment;
104    protected boolean isLoaderInitialized = false;
105    private AsyncRefreshTask mAsyncRefreshTask;
106
107    private MenuItem mRefreshItem;
108    private MenuItem mHelpItem;
109    private View mRefreshActionView;
110    private boolean mRefreshInProgress;
111    private final Handler mHandler = new Handler();
112    private final Runnable mInvalidateMenu = new Runnable() {
113        @Override
114        public void run() {
115            mActivity.invalidateOptionsMenu();
116        }
117    };
118    protected static final String LOG_TAG = new LogUtils().getLogTag();
119    private static final int ACCOUNT_CURSOR_LOADER = 0;
120    private static final int FOLDER_CURSOR_LOADER = 1;
121
122    public AbstractActivityController(MailActivity activity, ViewMode viewMode) {
123        mActivity = activity;
124        mViewMode = viewMode;
125        mContext = activity.getApplicationContext();
126        IS_TABLET_DEVICE = Utils.useTabletUI(mContext);
127    }
128
129    @Override
130    public synchronized void attachConversationList(ConversationListFragment fragment) {
131        // If there is an existing fragment, unregister it
132        if (mConversationListFragment != null) {
133            mViewMode.removeListener(mConversationListFragment);
134        }
135        mConversationListFragment = fragment;
136        // If the current fragment is non-null, add it as a listener.
137        if (fragment != null) {
138            mViewMode.addListener(mConversationListFragment);
139        }
140    }
141
142    @Override
143    public synchronized void attachFolderList(FolderListFragment fragment) {
144        // If there is an existing fragment, unregister it
145        if (mFolderListFragment != null) {
146            mViewMode.removeListener(mFolderListFragment);
147        }
148        mFolderListFragment = fragment;
149        if (fragment != null) {
150            mViewMode.addListener(mFolderListFragment);
151        }
152    }
153
154    @Override
155    public void attachConversationView(ConversationViewFragment conversationViewFragment) {
156        mConversationViewFragment = conversationViewFragment;
157    }
158
159    @Override
160    public void clearSubject() {
161        // TODO(viki): Auto-generated method stub
162    }
163
164    @Override
165    public void enterSearchMode() {
166        // TODO(viki): Auto-generated method stub
167    }
168
169    @Override
170    public void exitSearchMode() {
171        // TODO(viki): Auto-generated method stub
172    }
173
174    @Override
175    public Account getCurrentAccount() {
176        return mAccount;
177    }
178
179    @Override
180    public ConversationListContext getCurrentListContext() {
181        return mConvListContext;
182    }
183
184    @Override
185    public String getHelpContext() {
186        return "Mail";
187    }
188
189    @Override
190    public int getMode() {
191        return mViewMode.getMode();
192    }
193
194    @Override
195    public String getUnshownSubject(String subject) {
196        // Calculate how much of the subject is shown, and return the remaining.
197        return null;
198    }
199
200    @Override
201    public void handleConversationLoadError() {
202        // TODO(viki): Auto-generated method stub
203    }
204
205    @Override
206    public void handleSearchRequested() {
207        // TODO(viki): Auto-generated method stub
208    }
209
210    /**
211     * Initialize the action bar. This is not visible to OnePaneController and
212     * TwoPaneController so they cannot override this behavior.
213     */
214    private void initCustomActionBarView() {
215        ActionBar actionBar = mActivity.getActionBar();
216        mActionBarView = (MailActionBar) LayoutInflater.from(mContext).inflate(
217                R.layout.actionbar_view, null);
218
219        if (actionBar != null && mActionBarView != null) {
220            // Why have a different variable for the same thing? We should apply
221            // the same actions
222            // on mActionBarView instead.
223            mActionBarView.initialize(mActivity, this, mViewMode, actionBar);
224            actionBar.setCustomView((LinearLayout) mActionBarView, new ActionBar.LayoutParams(
225                    LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
226            actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM,
227                    ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_SHOW_TITLE);
228        }
229    }
230
231    /**
232     * Returns whether the conversation list fragment is visible or not.
233     * Different layouts will have their own notion on the visibility of
234     * fragments, so this method needs to be overriden.
235     *
236     * @return
237     */
238    protected abstract boolean isConversationListVisible();
239
240    @Override
241    public void onAccountChanged(Account account) {
242        if (!account.equals(mAccount)) {
243            mAccount = account;
244            // Account changed; existing folder is invalid.
245            mFolder = null;
246            fetchAccountFolderInfo();
247
248            updateHelpMenuItem();
249        }
250    }
251
252    private void fetchAccountFolderInfo() {
253        if (mFetchAccountFolderTask != null) {
254            mFetchAccountFolderTask.cancel(true);
255        }
256        mFetchAccountFolderTask = new FetchAccountFolderTask();
257        mFetchAccountFolderTask.execute();
258    }
259
260    @Override
261    public void onFolderChanged(Folder folder) {
262        if (!folder.equals(mFolder)) {
263            setFolder(folder);
264            mConvListContext = ConversationListContext.forFolder(mContext, mAccount, mFolder);
265            showConversationList(mConvListContext);
266        }
267    }
268
269    private void setFolder(Folder folder) {
270        // Start watching folder for sync status.
271        if (!folder.equals(mFolder)) {
272            mRefreshInProgress = false;
273            mFolder = folder;
274            mActionBarView.setFolder(mFolder);
275            mActivity.getLoaderManager().restartLoader(FOLDER_CURSOR_LOADER, null, this);
276        }
277    }
278
279    @Override
280    public void onActionModeFinished(ActionMode mode) {
281        // TODO(viki): Auto-generated method stub
282    }
283
284    @Override
285    public void onActionModeStarted(ActionMode mode) {
286        // TODO(viki): Auto-generated method stub
287    }
288
289    @Override
290    public void onActivityResult(int requestCode, int resultCode, Intent data) {
291        // TODO(viki): Auto-generated method stub
292    }
293
294    @Override
295    public void onConversationListVisibilityChanged(boolean visible) {
296        // TODO(viki): Auto-generated method stub
297    }
298
299    /**
300     * By default, doing nothing is right. A two-pane controller will need to
301     * override this.
302     */
303    @Override
304    public void onConversationVisibilityChanged(boolean visible) {
305        // Do nothing.
306        return;
307    }
308
309    @Override
310    public boolean onCreate(Bundle savedState) {
311        // Initialize the action bar view.
312        initCustomActionBarView();
313        // Allow shortcut keys to function for the ActionBar and menus.
314        mActivity.setDefaultKeyMode(Activity.DEFAULT_KEYS_SHORTCUT);
315        mResolver = mActivity.getContentResolver();
316
317        // All the individual UI components listen for ViewMode changes. This
318        // simplifies the
319        // amount of logic in the AbstractActivityController, but increases the
320        // possibility of
321        // timing-related bugs.
322        mViewMode.addListener(this);
323        assert (mActionBarView != null);
324        mViewMode.addListener(mActionBarView);
325
326        restoreState(savedState);
327        return true;
328    }
329
330    @Override
331    public Dialog onCreateDialog(int id, Bundle bundle) {
332        // TODO(viki): Auto-generated method stub
333        return null;
334    }
335
336    @Override
337    public boolean onCreateOptionsMenu(Menu menu) {
338        MenuInflater inflater = mActivity.getMenuInflater();
339        inflater.inflate(mActionBarView.getOptionsMenuId(), menu);
340        mRefreshItem = menu.findItem(R.id.refresh);
341        mHelpItem = menu.findItem(R.id.help_info_menu_item);
342        return true;
343    }
344
345    @Override
346    public void onEndBulkOperation() {
347        // TODO(viki): Auto-generated method stub
348
349    }
350
351    @Override
352    public boolean onKeyDown(int keyCode, KeyEvent event) {
353        // TODO(viki): Auto-generated method stub
354        return false;
355    }
356
357    @Override
358    public boolean onOptionsItemSelected(MenuItem item) {
359        int id = item.getItemId();
360        boolean handled = true;
361        switch (id) {
362            case android.R.id.home:
363                onUpPressed();
364                break;
365            case R.id.compose:
366                ComposeActivity.compose(mActivity.getActivityContext(), mAccount);
367                break;
368            case R.id.show_all_folders:
369                showFolderList();
370                break;
371            case R.id.refresh:
372                requestFolderRefresh();
373                break;
374            case R.id.preferences:
375                showPreferences();
376                break;
377            case R.id.help_info_menu_item:
378                // TODO: enable context sensitive help
379                Utils.showHelp(mActivity.getActivityContext(), mAccount.helpIntentUri, null);
380                break;
381            default:
382                handled = false;
383                break;
384        }
385        return handled;
386    }
387
388    private void requestFolderRefresh() {
389        if (mFolder != null) {
390            if (mAsyncRefreshTask != null) {
391                mAsyncRefreshTask.cancel(true);
392            }
393            mAsyncRefreshTask = new AsyncRefreshTask(mContext, mFolder);
394            mAsyncRefreshTask.execute();
395        }
396    }
397
398    public void onRefreshStarted() {
399        if (!mRefreshInProgress) {
400            mRefreshInProgress = true;
401            mHandler.post(mInvalidateMenu);
402        }
403    }
404
405    public void onRefreshStopped(int status) {
406        if (mRefreshInProgress) {
407            mRefreshInProgress = false;
408            switch (status) {
409                case LastSyncResult.SUCCESS:
410                    break;
411                default:
412                    Context context = mActivity.getActivityContext();
413                    Toast.makeText(context, Utils.getSyncStatusText(context, status),
414                            Toast.LENGTH_LONG).show();
415                    break;
416            }
417            mHandler.post(mInvalidateMenu);
418        }
419    }
420
421    @Override
422    public void onPause() {
423        isLoaderInitialized = false;
424    }
425
426    @Override
427    public void onPrepareDialog(int id, Dialog dialog, Bundle bundle) {
428        // TODO(viki): Auto-generated method stub
429
430    }
431
432    @Override
433    public boolean onPrepareOptionsMenu(Menu menu) {
434        if (mRefreshInProgress) {
435            if (mRefreshItem != null) {
436                if (mRefreshActionView == null) {
437                    mRefreshItem.setActionView(R.layout.action_bar_indeterminate_progress);
438                    mRefreshActionView = mRefreshItem.getActionView();
439                } else {
440                    mRefreshItem.setActionView(mRefreshActionView);
441                }
442            }
443        } else {
444            if (mRefreshItem != null) {
445                mRefreshItem.setActionView(null);
446            }
447        }
448
449        // Show/hide the help menu item
450        updateHelpMenuItem();
451        return true;
452    }
453
454    private void updateHelpMenuItem() {
455        if (mHelpItem != null) {
456            mHelpItem.setVisible(mAccount != null
457                    && mAccount.supportsCapability(AccountCapabilities.HELP_CONTENT));
458        }
459    }
460
461    @Override
462    public void onResume() {
463        if (mActionBarView != null) {
464            mActionBarView.onResume();
465        }
466    }
467
468    @Override
469    public void onSaveInstanceState(Bundle outState) {
470        if (mConvListContext != null) {
471            outState.putBundle(SAVED_LIST_CONTEXT, mConvListContext.toBundle());
472        }
473    }
474
475    @Override
476    public void showPreferences() {
477        final Intent preferenceIntent = new Intent(Intent.ACTION_EDIT, mAccount.settingIntentUri);
478        mActivity.startActivity(preferenceIntent);
479    }
480
481    @Override
482    public void onSearchRequested() {
483        // TODO(viki): Auto-generated method stub
484    }
485
486    @Override
487    public void onStartBulkOperation() {
488        // TODO(viki): Auto-generated method stub
489    }
490
491    @Override
492    public void onStartDragMode() {
493        // TODO(viki): Auto-generated method stub
494    }
495
496    @Override
497    public void onStop() {
498        // TODO(viki): Auto-generated method stub
499    }
500
501    @Override
502    public void onStopDragMode() {
503        // TODO(viki): Auto-generated method stub
504    }
505
506    /**
507     * {@inheritDoc} Subclasses must override this to listen to mode changes
508     * from the ViewMode. Subclasses <b>must</b> call the parent's
509     * onViewModeChanged since the parent will handle common state changes.
510     */
511    @Override
512    public void onViewModeChanged(int newMode) {
513        // Perform any mode specific work here.
514        // reset the action bar icon based on the mode. Why don't the individual
515        // controllers do
516        // this themselves?
517
518        // In conversation list mode, clean up the conversation.
519        if (newMode == ViewMode.CONVERSATION_LIST) {
520            // Clean up the conversation here.
521        }
522
523        // We don't want to invalidate the options menu when switching to
524        // conversation
525        // mode, as it will happen when the conversation finishes loading.
526        if (newMode != ViewMode.CONVERSATION) {
527            mActivity.invalidateOptionsMenu();
528        }
529    }
530
531    @Override
532    public void onWindowFocusChanged(boolean hasFocus) {
533        // TODO(viki): Auto-generated method stub
534    }
535
536    @Override
537    public void reloadSearch(String string) {
538        // TODO(viki): Auto-generated method stub
539    }
540
541    /**
542     * @param savedState
543     */
544    protected void restoreListContext(Bundle savedState) {
545        // TODO(viki): Auto-generated method stub
546        Bundle listContextBundle = savedState.getBundle(SAVED_LIST_CONTEXT);
547        if (listContextBundle != null) {
548            mConvListContext = ConversationListContext.forBundle(listContextBundle);
549        }
550    }
551
552    /**
553     * Restore the state from the previous bundle. Subclasses should call this
554     * method from the parent class, since it performs important UI
555     * initialization.
556     *
557     * @param savedState
558     */
559    protected void restoreState(Bundle savedState) {
560        if (savedState != null) {
561            restoreListContext(savedState);
562            // Attach the menu handler here.
563
564            // Restore the view mode
565            mViewMode.handleRestore(savedState);
566        } else {
567            final Intent intent = mActivity.getIntent();
568            if (intent != null && Intent.ACTION_VIEW.equals(intent.getAction())) {
569                if (intent.hasExtra(Utils.EXTRA_CONVERSATION)) {
570                    // Open the conversation.
571                    LogUtils.d(LOG_TAG, "SHOW THE CONVERSATION at %s",
572                            intent.getParcelableExtra(Utils.EXTRA_CONVERSATION));
573                    mAccount = ((Account) intent.getParcelableExtra(Utils.EXTRA_ACCOUNT));
574                    updateHelpMenuItem();
575                    mFolder = ((Folder) intent.getParcelableExtra(Utils.EXTRA_FOLDER));
576                    mConvListContext = ConversationListContext.forIntent(mContext, mAccount,
577                            mActivity.getIntent());
578                    showConversationList(mConvListContext);
579                    showConversation((Conversation) intent
580                            .getParcelableExtra(Utils.EXTRA_CONVERSATION));
581                } else if (intent.hasExtra(Utils.EXTRA_FOLDER)) {
582                    // Open the folder.
583                    LogUtils.d(LOG_TAG, "SHOW THE FOLDER at %s",
584                            intent.getParcelableExtra(Utils.EXTRA_FOLDER));
585                    mAccount = ((Account) intent.getParcelableExtra(Utils.EXTRA_ACCOUNT));
586                    updateHelpMenuItem();
587                    onFolderChanged((Folder) intent.getParcelableExtra(Utils.EXTRA_FOLDER));
588                }
589            }
590            // Update the active folder in the action bar.
591            mActionBarView.setFolder(mFolder);
592        }
593        // Create the accounts loader; this loads the acount switch spinner.
594        mActivity.getLoaderManager().initLoader(ACCOUNT_CURSOR_LOADER, null, this);
595    }
596
597    @Override
598    public void setSubject(String subject) {
599        // Do something useful with the subject. This requires changing the
600        // conversation view's subject text.
601    }
602
603    @Override
604    public void startActionBarStatusCursorLoader(String account) {
605        // TODO(viki): Auto-generated method stub
606    }
607
608    @Override
609    public void stopActionBarStatusCursorLoader(String account) {
610        // TODO(viki): Auto-generated method stub
611    }
612
613    @Override
614    public void toggleStar(boolean toggleOn, long conversationId, long maxMessageId) {
615        // TODO(viki): Auto-generated method stub
616    }
617
618    @Override
619    public void onConversationSelected(Conversation conversation) {
620        mCurrentConversation = conversation;
621        showConversation(mCurrentConversation);
622        mViewMode.enterConversationMode();
623    }
624
625    /**
626     * {@inheritDoc}
627     */
628    @Override
629    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
630        // Create a loader to listen in on account changes.
631        if (id == ACCOUNT_CURSOR_LOADER) {
632            return new CursorLoader(mContext, AccountCacheProvider.getAccountsUri(),
633                    UIProvider.ACCOUNTS_PROJECTION, null, null, null);
634        } else if (id == FOLDER_CURSOR_LOADER) {
635            return new CursorLoader(mActivity.getActivityContext(), mFolder.uri,
636                    UIProvider.FOLDERS_PROJECTION, null, null, null);
637        }
638        return null;
639    }
640
641    /**
642     * Return whether the given account exists in the cursor.
643     *
644     * @param accountCursor
645     * @param account
646     * @return true if the account exists in the account cursor, false
647     *         otherwise.
648     */
649    private boolean existsInCursor(Cursor accountCursor, Account account) {
650        accountCursor.moveToFirst();
651        do {
652            if (account.equals(new Account(accountCursor)))
653                return true;
654        } while (accountCursor.moveToNext());
655        return false;
656    }
657
658    /**
659     * Update the accounts on the device. This currently loads the first account
660     * in the list.
661     *
662     * @param loader
663     * @param data
664     * @return true if the update was successful, false otherwise
665     */
666    private boolean updateAccounts(Loader<Cursor> loader, Cursor accounts) {
667        // Load the first account in the absence of any other information.
668        if (accounts == null || !accounts.moveToFirst()) {
669            return false;
670        }
671        Account newAccount = mAccount == null ? new Account(accounts) : mAccount;
672        onAccountChanged(newAccount);
673        final Account[] allAccounts = Account.getAllAccounts(accounts);
674        mActionBarView.setAccounts(allAccounts);
675        return (allAccounts.length > 0);
676    }
677
678    /**
679     * {@inheritDoc}
680     */
681    @Override
682    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
683        // We want to reinitialize only if we haven't ever been initialized, or
684        // if the current account has vanished.
685        final int id = loader.getId();
686        if (id == ACCOUNT_CURSOR_LOADER) {
687            if (!isLoaderInitialized || !existsInCursor(data, mAccount)) {
688                isLoaderInitialized = updateAccounts(loader, data);
689            }
690        } else if (id == FOLDER_CURSOR_LOADER) {
691            // Check status of the cursor.
692            if (data != null) {
693                data.moveToFirst();
694                Folder folder = new Folder(data);
695                if (folder.isSyncInProgress()) {
696                    onRefreshStarted();
697                } else {
698                    // Stop the spinner here.
699                    onRefreshStopped(folder.lastSyncResult);
700                }
701                LogUtils.v(LOG_TAG, "FOLDER STATUS = " + folder.syncStatus);
702            }
703        }
704    }
705
706    /**
707     * {@inheritDoc}
708     */
709    @Override
710    public void onLoaderReset(Loader<Cursor> loader) {
711        // Do nothing for now, since we don't have any state. When a load is
712        // finished, the
713        // onLoadFinished will be called and we will be fine.
714    }
715
716    @Override
717    public void onTouchEvent(MotionEvent event) {
718        if (event.getAction() == MotionEvent.ACTION_DOWN) {
719            int mode = mViewMode.getMode();
720            if (mode == ViewMode.CONVERSATION_LIST) {
721                mConversationListFragment.onTouchEvent(event);
722            } else if (mode == ViewMode.CONVERSATION) {
723                mConversationViewFragment.onTouchEvent(event);
724            }
725        }
726    }
727
728    private class FetchAccountFolderTask extends AsyncTask<Void, Void, ConversationListContext> {
729        @Override
730        public ConversationListContext doInBackground(Void... params) {
731            return ConversationListContext.forFolder(mContext, mAccount, mFolder);
732        }
733
734        @Override
735        public void onPostExecute(ConversationListContext result) {
736            mConvListContext = result;
737            setFolder(mConvListContext.mFolder);
738            showConversationList(mConvListContext);
739            mFetchAccountFolderTask = null;
740        }
741    }
742}
743