UIControllerBase.java revision e06e1224414c181e729f7952d80bb70d59fedc20
1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.email.activity;
18
19import com.android.email.Email;
20import com.android.email.R;
21import com.android.email.RefreshManager;
22import com.android.email.activity.setup.AccountSettings;
23import com.android.emailcommon.Logging;
24import com.android.emailcommon.provider.EmailContent.Account;
25import com.android.emailcommon.provider.EmailContent.Message;
26import com.android.emailcommon.provider.Mailbox;
27import com.android.emailcommon.utility.EmailAsyncTask;
28
29import android.app.Activity;
30import android.app.Fragment;
31import android.app.FragmentManager;
32import android.app.FragmentTransaction;
33import android.os.Bundle;
34import android.util.Log;
35import android.view.Menu;
36import android.view.MenuInflater;
37import android.view.MenuItem;
38
39import java.util.LinkedList;
40import java.util.List;
41
42/**
43 * Base class for the UI controller.
44 */
45abstract class UIControllerBase implements MailboxListFragment.Callback,
46        MessageListFragment.Callback, MessageViewFragment.Callback  {
47    static final boolean DEBUG_FRAGMENTS = false; // DO NOT SUBMIT WITH TRUE
48
49    protected static final String BUNDLE_KEY_RESUME_INBOX_LOOKUP
50            = "UIController.state.resumeInboxLookup";
51    protected static final String BUNDLE_KEY_INBOX_LOOKUP_ACCOUNT_ID
52            = "UIController.state.inboxLookupAccountId";
53
54    /** The owner activity */
55    final EmailActivity mActivity;
56
57    private final ActionBarController mActionBarController;
58
59    final EmailAsyncTask.Tracker mTaskTracker = new EmailAsyncTask.Tracker();
60
61    final RefreshManager mRefreshManager;
62
63    /** {@code true} if the activity is resumed. */
64    private boolean mResumed;
65
66    /**
67     * Use to find Inbox.  This should only run while the activity is resumed, because otherwise
68     * we may not be able to perform fragment transactions when we get a callback.
69     * See also {@link #mResumeInboxLookup}.
70     */
71    private MailboxFinder mInboxFinder;
72
73    /**
74     * Account ID passed to {@link #startInboxLookup(long)}.  We save it for resuming it in
75     * {@link #onActivityResume()}.
76     */
77    private long mInboxLookupAccountId;
78
79    /**
80     * We (re)start inbox lookup in {@link #onActivityResume} if it's set.
81     * Set in {@link #onActivityPause()} if it's still running, or {@link #startInboxLookup} is
82     * called before the activity is resumed.
83     */
84    private boolean mResumeInboxLookup;
85
86    /**
87     * Fragments that are installed.
88     *
89     * A fragment is installed in {@link Fragment#onActivityCreated} and uninstalled in
90     * {@link Fragment#onDestroyView}, using {@link FragmentInstallable} callbacks.
91     *
92     * This means fragments in the back stack are *not* installed.
93     *
94     * We set callbacks to fragments only when they are installed.
95     *
96     * @see FragmentInstallable
97     */
98    private MailboxListFragment mMailboxListFragment;
99    private MessageListFragment mMessageListFragment;
100    private MessageViewFragment mMessageViewFragment;
101
102    /**
103     * To avoid double-deleting a fragment (which will cause a runtime exception),
104     * we put a fragment in this list when we {@link FragmentTransaction#remove(Fragment)} it,
105     * and remove from the list when we actually uninstall it.
106     */
107    private final List<Fragment> mRemovedFragments = new LinkedList<Fragment>();
108
109    private final RefreshManager.Listener mRefreshListener
110            = new RefreshManager.Listener() {
111        @Override
112        public void onMessagingError(final long accountId, long mailboxId, final String message) {
113            refreshActionBar();
114        }
115
116        @Override
117        public void onRefreshStatusChanged(long accountId, long mailboxId) {
118            refreshActionBar();
119        }
120    };
121
122    public UIControllerBase(EmailActivity activity) {
123        mActivity = activity;
124        mRefreshManager = RefreshManager.getInstance(mActivity);
125        mActionBarController = createActionBarController(activity);
126        if (DEBUG_FRAGMENTS) {
127            FragmentManager.enableDebugLogging(true);
128        }
129    }
130
131    /**
132     * Called by the base class to let a subclass create an {@link ActionBarController}.
133     */
134    protected abstract ActionBarController createActionBarController(Activity activity);
135
136    /** @return the layout ID for the activity. */
137    public abstract int getLayoutId();
138
139    /**
140     * Must be called just after the activity sets up the content view.  Used to initialize views.
141     *
142     * (Due to the complexity regarding class/activity initialization order, we can't do this in
143     * the constructor.)
144     */
145    public void onActivityViewReady() {
146        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
147            Log.d(Logging.LOG_TAG, this + " onActivityViewReady");
148        }
149    }
150
151    /**
152     * Called at the end of {@link EmailActivity#onCreate}.
153     */
154    public void onActivityCreated() {
155        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
156            Log.d(Logging.LOG_TAG, this + " onActivityCreated");
157        }
158        mRefreshManager.registerListener(mRefreshListener);
159        mActionBarController.onActivityCreated();
160    }
161
162    /**
163     * Handles the {@link android.app.Activity#onStart} callback.
164     */
165    public void onActivityStart() {
166        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
167            Log.d(Logging.LOG_TAG, this + " onActivityStart");
168        }
169    }
170
171    /**
172     * Handles the {@link android.app.Activity#onResume} callback.
173     */
174    public void onActivityResume() {
175        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
176            Log.d(Logging.LOG_TAG, this + " onActivityResume");
177        }
178        mResumed = true;
179        if (mResumeInboxLookup) {
180            startInboxLookup(mInboxLookupAccountId);
181            mResumeInboxLookup = false;
182        }
183    }
184
185    /**
186     * Handles the {@link android.app.Activity#onPause} callback.
187     */
188    public void onActivityPause() {
189        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
190            Log.d(Logging.LOG_TAG, this + " onActivityPause");
191        }
192        mResumeInboxLookup = (mInboxFinder != null);
193        stopInboxLookup();
194    }
195
196    /**
197     * Handles the {@link android.app.Activity#onStop} callback.
198     */
199    public void onActivityStop() {
200        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
201            Log.d(Logging.LOG_TAG, this + " onActivityStop");
202        }
203        mResumed = false;
204    }
205
206    /**
207     * Handles the {@link android.app.Activity#onDestroy} callback.
208     */
209    public void onActivityDestroy() {
210        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
211            Log.d(Logging.LOG_TAG, this + " onActivityDestroy");
212        }
213        mRefreshManager.unregisterListener(mRefreshListener);
214        mTaskTracker.cancellAllInterrupt();
215    }
216
217    /**
218     * Handles the {@link android.app.Activity#onSaveInstanceState} callback.
219     */
220    public void onSaveInstanceState(Bundle outState) {
221        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
222            Log.d(Logging.LOG_TAG, this + " onSaveInstanceState");
223        }
224        outState.putBoolean(BUNDLE_KEY_RESUME_INBOX_LOOKUP, mResumeInboxLookup);
225        outState.putLong(BUNDLE_KEY_INBOX_LOOKUP_ACCOUNT_ID, mInboxLookupAccountId);
226    }
227
228    /**
229     * Handles the {@link android.app.Activity#onRestoreInstanceState} callback.
230     */
231    public void restoreInstanceState(Bundle savedInstanceState) {
232        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
233            Log.d(Logging.LOG_TAG, this + " restoreInstanceState");
234        }
235        mResumeInboxLookup = savedInstanceState.getBoolean(BUNDLE_KEY_RESUME_INBOX_LOOKUP);
236        mInboxLookupAccountId = savedInstanceState.getLong(BUNDLE_KEY_INBOX_LOOKUP_ACCOUNT_ID);
237    }
238
239    /**
240     * Install a fragment.  Must be caleld from the host activity's
241     * {@link FragmentInstallable#onInstallFragment}.
242     */
243    public final void onInstallFragment(Fragment fragment) {
244        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
245            Log.d(Logging.LOG_TAG, this + " onInstallFragment  fragment=" + fragment);
246        }
247        if (fragment instanceof MailboxListFragment) {
248            installMailboxListFragment((MailboxListFragment) fragment);
249        } else if (fragment instanceof MessageListFragment) {
250            installMessageListFragment((MessageListFragment) fragment);
251        } else if (fragment instanceof MessageViewFragment) {
252            installMessageViewFragment((MessageViewFragment) fragment);
253        } else {
254            throw new IllegalArgumentException("Tried to install unknown fragment");
255        }
256    }
257
258    /** Install fragment */
259    protected void installMailboxListFragment(MailboxListFragment fragment) {
260        mMailboxListFragment = fragment;
261        mMailboxListFragment.setCallback(this);
262        refreshActionBar();
263    }
264
265    /** Install fragment */
266    protected void installMessageListFragment(MessageListFragment fragment) {
267        mMessageListFragment = fragment;
268        mMessageListFragment.setCallback(this);
269        refreshActionBar();
270    }
271
272    /** Install fragment */
273    protected void installMessageViewFragment(MessageViewFragment fragment) {
274        mMessageViewFragment = fragment;
275        mMessageViewFragment.setCallback(this);
276        refreshActionBar();
277    }
278
279    /**
280     * Uninstall a fragment.  Must be caleld from the host activity's
281     * {@link FragmentInstallable#onUninstallFragment}.
282     */
283    public final void onUninstallFragment(Fragment fragment) {
284        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
285            Log.d(Logging.LOG_TAG, this + " onUninstallFragment  fragment=" + fragment);
286        }
287        mRemovedFragments.remove(fragment);
288        if (fragment == mMailboxListFragment) {
289            uninstallMailboxListFragment();
290        } else if (fragment == mMessageListFragment) {
291            uninstallMessageListFragment();
292        } else if (fragment == mMessageViewFragment) {
293            uninstallMessageViewFragment();
294        } else {
295            throw new IllegalArgumentException("Tried to uninstall unknown fragment");
296        }
297    }
298
299    /** Uninstall {@link MailboxListFragment} */
300    protected void uninstallMailboxListFragment() {
301        mMailboxListFragment.setCallback(null);
302        mMailboxListFragment = null;
303    }
304
305    /** Uninstall {@link MessageListFragment} */
306    protected void uninstallMessageListFragment() {
307        mMessageListFragment.setCallback(null);
308        mMessageListFragment = null;
309    }
310
311    /** Uninstall {@link MessageViewFragment} */
312    protected void uninstallMessageViewFragment() {
313        mMessageViewFragment.setCallback(null);
314        mMessageViewFragment = null;
315    }
316
317    /**
318     * If a {@link Fragment} is not already in {@link #mRemovedFragments},
319     * {@link FragmentTransaction#remove} it and add to the list.
320     *
321     * Do nothing if {@code fragment} is null.
322     */
323    protected final void removeFragment(FragmentTransaction ft, Fragment fragment) {
324        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
325            Log.d(Logging.LOG_TAG, this + " removeFragment fragment=" + fragment);
326        }
327        if (fragment == null) {
328            return;
329        }
330        if (!mRemovedFragments.contains(fragment)) {
331            ft.remove(fragment);
332            addFragmentToRemovalList(fragment);
333        }
334    }
335
336    /**
337     * Remove a {@link Fragment} from {@link #mRemovedFragments}.  No-op if {@code fragment} is
338     * null.
339     *
340     * {@link #removeMailboxListFragment}, {@link #removeMessageListFragment} and
341     * {@link #removeMessageViewFragment} all call this, so subclasses don't have to do this when
342     * using them.
343     *
344     * However, unfortunately, subclasses have to call this manually when popping from the
345     * back stack to avoid double-delete.
346     */
347    protected void addFragmentToRemovalList(Fragment fragment) {
348        if (fragment != null) {
349            mRemovedFragments.add(fragment);
350        }
351    }
352
353    /**
354     * Remove the fragment if it's installed.
355     */
356    protected FragmentTransaction removeMailboxListFragment(FragmentTransaction ft) {
357        removeFragment(ft, mMailboxListFragment);
358        return ft;
359    }
360
361    /**
362     * Remove the fragment if it's installed.
363     */
364    protected FragmentTransaction removeMessageListFragment(FragmentTransaction ft) {
365        removeFragment(ft, mMessageListFragment);
366        return ft;
367    }
368
369    /**
370     * Remove the fragment if it's installed.
371     */
372    protected FragmentTransaction removeMessageViewFragment(FragmentTransaction ft) {
373        removeFragment(ft, mMessageViewFragment);
374        return ft;
375    }
376
377    /** @return true if a {@link MailboxListFragment} is installed. */
378    protected final boolean isMailboxListInstalled() {
379        return mMailboxListFragment != null;
380    }
381
382    /** @return true if a {@link MessageListFragment} is installed. */
383    protected final boolean isMessageListInstalled() {
384        return mMessageListFragment != null;
385    }
386
387    /** @return true if a {@link MessageViewFragment} is installed. */
388    protected final boolean isMessageViewInstalled() {
389        return mMessageViewFragment != null;
390    }
391
392    /** @return the installed {@link MailboxListFragment} or null. */
393    protected final MailboxListFragment getMailboxListFragment() {
394        return mMailboxListFragment;
395    }
396
397    /** @return the installed {@link MessageListFragment} or null. */
398    protected final MessageListFragment getMessageListFragment() {
399        return mMessageListFragment;
400    }
401
402    /** @return the installed {@link MessageViewFragment} or null. */
403    protected final MessageViewFragment getMessageViewFragment() {
404        return mMessageViewFragment;
405    }
406
407    /**
408     * @return the currently selected account ID, *or* {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
409     *
410     * @see #getActualAccountId()
411     */
412    public abstract long getUIAccountId();
413
414    /**
415     * @return true if an account is selected, or the current view is the combined view.
416     */
417    public final boolean isAccountSelected() {
418        return getUIAccountId() != Account.NO_ACCOUNT;
419    }
420
421    /**
422     * @return if an actual account is selected.  (i.e. {@link Account#ACCOUNT_ID_COMBINED_VIEW}
423     * is not considered "actual".s)
424     */
425    public final boolean isActualAccountSelected() {
426        return isAccountSelected() && (getUIAccountId() != Account.ACCOUNT_ID_COMBINED_VIEW);
427    }
428
429    /**
430     * @return the currently selected account ID.  If the current view is the combined view,
431     * it'll return {@link Account#NO_ACCOUNT}.
432     *
433     * @see #getUIAccountId()
434     */
435    public final long getActualAccountId() {
436        return isActualAccountSelected() ? getUIAccountId() : Account.NO_ACCOUNT;
437    }
438
439    /**
440     * Show the default view for the given account.
441     *
442     * No-op if the given account is already selected.
443     *
444     * @param accountId ID of the account to load.  Can be {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
445     *     Must never be {@link Account#NO_ACCOUNT}.
446     */
447    public final void switchAccount(long accountId) {
448        if (accountId == getUIAccountId()) {
449            // Do nothing if the account is already selected.  Not even going back to the inbox.
450            return;
451        }
452        openAccount(accountId);
453    }
454
455    /**
456     * Shortcut for {@link #open} with {@link Mailbox#NO_MAILBOX} and {@link Message#NO_MESSAGE}.
457     */
458    protected final void openAccount(long accountId) {
459        open(accountId, Mailbox.NO_MAILBOX, Message.NO_MESSAGE);
460    }
461
462    /**
463     * Shortcut for {@link #open} with {@link Message#NO_MESSAGE}.
464     */
465    protected final void openMailbox(long accountId, long mailboxId) {
466        open(accountId, mailboxId, Message.NO_MESSAGE);
467    }
468
469    /**
470     * Loads the given account and optionally selects the given mailbox and message.  Used to open
471     * a particular view at a request from outside of the activity, such as the widget.
472     *
473     * @param accountId ID of the account to load.  Can be {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
474     *     Must never be {@link Account#NO_ACCOUNT}.
475     * @param mailboxId ID of the mailbox to load. If {@link Mailbox#NO_MAILBOX},
476     *     load the account's inbox.
477     * @param messageId ID of the message to load. If {@link Message#NO_MESSAGE},
478     *     do not open a message.
479     */
480    public abstract void open(long accountId, long mailboxId, long messageId);
481
482    /**
483     * Performs the back action.
484     *
485     * @param isSystemBackKey <code>true</code> if the system back key was pressed.
486     * <code>false</code> if it's caused by the "home" icon click on the action bar.
487     */
488    public abstract boolean onBackPressed(boolean isSystemBackKey);
489
490    /**
491     * Callback called when the inbox lookup (started by {@link #startInboxLookup}) is finished.
492     */
493    protected abstract MailboxFinder.Callback getInboxLookupCallback();
494
495    private final MailboxFinder.Callback mMailboxFinderCallback = new MailboxFinder.Callback() {
496        private void cleanUp() {
497            mInboxFinder = null;
498        }
499
500        @Override
501        public void onAccountNotFound() {
502            getInboxLookupCallback().onAccountNotFound();
503            cleanUp();
504        }
505
506        @Override
507        public void onAccountSecurityHold(long accountId) {
508            getInboxLookupCallback().onAccountSecurityHold(accountId);
509            cleanUp();
510        }
511
512        @Override
513        public void onMailboxFound(long accountId, long mailboxId) {
514            getInboxLookupCallback().onMailboxFound(accountId, mailboxId);
515            cleanUp();
516        }
517
518        @Override
519        public void onMailboxNotFound(long accountId) {
520            getInboxLookupCallback().onMailboxNotFound(accountId);
521            cleanUp();
522        }
523    };
524
525    /**
526     * Start inbox lookup.
527     */
528    protected void startInboxLookup(long accountId) {
529        if (mInboxFinder != null) {
530            return; // already running
531        }
532        mInboxLookupAccountId = accountId;
533        if (!mResumed) {
534            mResumeInboxLookup = true; // Don't start yet.
535            return;
536        }
537        mInboxFinder = new MailboxFinder(mActivity, accountId, Mailbox.TYPE_INBOX,
538                mMailboxFinderCallback);
539        mInboxFinder.startLookup();
540    }
541
542    /**
543     * Stop inbox lookup.
544     */
545    protected void stopInboxLookup() {
546        if (mInboxFinder == null) {
547            return; // not running
548        }
549        mInboxFinder.cancel();
550        mInboxFinder = null;
551    }
552
553    /**
554     * Handles the {@link android.app.Activity#onCreateOptionsMenu} callback.
555     */
556    public boolean onCreateOptionsMenu(MenuInflater inflater, Menu menu) {
557        inflater.inflate(R.menu.email_activity_options, menu);
558        return true;
559    }
560
561    /**
562     * Handles the {@link android.app.Activity#onPrepareOptionsMenu} callback.
563     */
564    public boolean onPrepareOptionsMenu(MenuInflater inflater, Menu menu) {
565
566        // Update the refresh button.
567        MenuItem item = menu.findItem(R.id.refresh);
568        if (isRefreshEnabled()) {
569            item.setVisible(true);
570            if (isRefreshInProgress()) {
571                item.setActionView(R.layout.action_bar_indeterminate_progress);
572            } else {
573                item.setActionView(null);
574            }
575        } else {
576            item.setVisible(false);
577        }
578        return true;
579    }
580
581    /**
582     * Handles the {@link android.app.Activity#onOptionsItemSelected} callback.
583     *
584     * @return true if the option item is handled.
585     */
586    public boolean onOptionsItemSelected(MenuItem item) {
587        switch (item.getItemId()) {
588            case android.R.id.home:
589                // Comes from the action bar when the app icon on the left is pressed.
590                // It works like a back press, but it won't close the activity.
591                return onBackPressed(false);
592            case R.id.compose:
593                return onCompose();
594            case R.id.refresh:
595                onRefresh();
596                return true;
597            case R.id.account_settings:
598                return onAccountSettings();
599        }
600        return false;
601    }
602
603    /**
604     * Opens the message compose activity.
605     */
606    private boolean onCompose() {
607        if (!isAccountSelected()) {
608            return false; // this shouldn't really happen
609        }
610        MessageCompose.actionCompose(mActivity, getActualAccountId());
611        return true;
612    }
613
614    /**
615     * Handles the "Settings" option item.  Opens the settings activity.
616     */
617    private boolean onAccountSettings() {
618        AccountSettings.actionSettings(mActivity, getActualAccountId());
619        return true;
620    }
621
622    /**
623     * STOPSHIP For experimental UI.  Remove this.
624     *
625     * @return mailbox ID which we search for messages.
626     */
627    public abstract long getSearchMailboxId();
628
629    /**
630     * STOPSHIP For experimental UI.  Remove this.
631     *
632     * @return mailbox ID for "mailbox settings" option.
633     */
634    public abstract long getMailboxSettingsMailboxId();
635
636    /**
637     * STOPSHIP For experimental UI.  Make it abstract protected.
638     *
639     * Performs "refesh".
640     */
641    public abstract void onRefresh();
642
643    /**
644     * @return true if refresh is in progress for the current mailbox.
645     */
646    protected abstract boolean isRefreshInProgress();
647
648    /**
649     * @return true if the UI should enable the "refresh" command.
650     */
651    protected abstract boolean isRefreshEnabled();
652
653    /**
654     * Refresh the action bar and menu items, including the "refreshing" icon.
655     */
656    protected void refreshActionBar() {
657        if (mActionBarController != null) {
658            mActionBarController.refresh();
659        }
660        mActivity.invalidateOptionsMenu();
661    }
662
663    @Override
664    public String toString() {
665        return getClass().getSimpleName(); // Shown on logcat
666    }
667}
668