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