UIControllerBase.java revision 627bc6ed57ee06cc588e64ff959bfd7870b659b6
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 android.app.Activity;
20import android.app.Fragment;
21import android.app.FragmentManager;
22import android.app.FragmentTransaction;
23import android.os.Bundle;
24import android.util.Log;
25import android.view.Menu;
26import android.view.MenuInflater;
27import android.view.MenuItem;
28
29import com.android.email.Email;
30import com.android.email.R;
31import com.android.email.RefreshManager;
32import com.android.email.activity.setup.AccountSettings;
33import com.android.emailcommon.Logging;
34import com.android.emailcommon.provider.Account;
35import com.android.emailcommon.provider.EmailContent.Message;
36import com.android.emailcommon.provider.Mailbox;
37import com.android.emailcommon.utility.EmailAsyncTask;
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        mActionBarController.onActivityDestroy();
214        mRefreshManager.unregisterListener(mRefreshListener);
215        mTaskTracker.cancellAllInterrupt();
216    }
217
218    /**
219     * Handles the {@link android.app.Activity#onSaveInstanceState} callback.
220     */
221    public void onSaveInstanceState(Bundle outState) {
222        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
223            Log.d(Logging.LOG_TAG, this + " onSaveInstanceState");
224        }
225        outState.putBoolean(BUNDLE_KEY_RESUME_INBOX_LOOKUP, mResumeInboxLookup);
226        outState.putLong(BUNDLE_KEY_INBOX_LOOKUP_ACCOUNT_ID, mInboxLookupAccountId);
227        mActionBarController.onSaveInstanceState(outState);
228    }
229
230    /**
231     * Handles the {@link android.app.Activity#onRestoreInstanceState} callback.
232     */
233    public void onRestoreInstanceState(Bundle savedInstanceState) {
234        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
235            Log.d(Logging.LOG_TAG, this + " restoreInstanceState");
236        }
237        mResumeInboxLookup = savedInstanceState.getBoolean(BUNDLE_KEY_RESUME_INBOX_LOOKUP);
238        mInboxLookupAccountId = savedInstanceState.getLong(BUNDLE_KEY_INBOX_LOOKUP_ACCOUNT_ID);
239        mActionBarController.onRestoreInstanceState(savedInstanceState);
240    }
241
242    /**
243     * Install a fragment.  Must be caleld from the host activity's
244     * {@link FragmentInstallable#onInstallFragment}.
245     */
246    public final void onInstallFragment(Fragment fragment) {
247        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
248            Log.d(Logging.LOG_TAG, this + " onInstallFragment  fragment=" + fragment);
249        }
250        if (fragment instanceof MailboxListFragment) {
251            installMailboxListFragment((MailboxListFragment) fragment);
252        } else if (fragment instanceof MessageListFragment) {
253            installMessageListFragment((MessageListFragment) fragment);
254        } else if (fragment instanceof MessageViewFragment) {
255            installMessageViewFragment((MessageViewFragment) fragment);
256        } else {
257            throw new IllegalArgumentException("Tried to install unknown fragment");
258        }
259    }
260
261    /** Install fragment */
262    protected void installMailboxListFragment(MailboxListFragment fragment) {
263        mMailboxListFragment = fragment;
264        mMailboxListFragment.setCallback(this);
265        refreshActionBar();
266    }
267
268    /** Install fragment */
269    protected void installMessageListFragment(MessageListFragment fragment) {
270        mMessageListFragment = fragment;
271        mMessageListFragment.setCallback(this);
272        refreshActionBar();
273    }
274
275    /** Install fragment */
276    protected void installMessageViewFragment(MessageViewFragment fragment) {
277        mMessageViewFragment = fragment;
278        mMessageViewFragment.setCallback(this);
279        refreshActionBar();
280    }
281
282    /**
283     * Uninstall a fragment.  Must be caleld from the host activity's
284     * {@link FragmentInstallable#onUninstallFragment}.
285     */
286    public final void onUninstallFragment(Fragment fragment) {
287        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
288            Log.d(Logging.LOG_TAG, this + " onUninstallFragment  fragment=" + fragment);
289        }
290        mRemovedFragments.remove(fragment);
291        if (fragment == mMailboxListFragment) {
292            uninstallMailboxListFragment();
293        } else if (fragment == mMessageListFragment) {
294            uninstallMessageListFragment();
295        } else if (fragment == mMessageViewFragment) {
296            uninstallMessageViewFragment();
297        } else {
298            throw new IllegalArgumentException("Tried to uninstall unknown fragment");
299        }
300    }
301
302    /** Uninstall {@link MailboxListFragment} */
303    protected void uninstallMailboxListFragment() {
304        mMailboxListFragment.setCallback(null);
305        mMailboxListFragment = null;
306    }
307
308    /** Uninstall {@link MessageListFragment} */
309    protected void uninstallMessageListFragment() {
310        mMessageListFragment.setCallback(null);
311        mMessageListFragment = null;
312    }
313
314    /** Uninstall {@link MessageViewFragment} */
315    protected void uninstallMessageViewFragment() {
316        mMessageViewFragment.setCallback(null);
317        mMessageViewFragment = null;
318    }
319
320    /**
321     * If a {@link Fragment} is not already in {@link #mRemovedFragments},
322     * {@link FragmentTransaction#remove} it and add to the list.
323     *
324     * Do nothing if {@code fragment} is null.
325     */
326    protected final void removeFragment(FragmentTransaction ft, Fragment fragment) {
327        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
328            Log.d(Logging.LOG_TAG, this + " removeFragment fragment=" + fragment);
329        }
330        if (fragment == null) {
331            return;
332        }
333        if (!mRemovedFragments.contains(fragment)) {
334            ft.remove(fragment);
335            addFragmentToRemovalList(fragment);
336        }
337    }
338
339    /**
340     * Remove a {@link Fragment} from {@link #mRemovedFragments}.  No-op if {@code fragment} is
341     * null.
342     *
343     * {@link #removeMailboxListFragment}, {@link #removeMessageListFragment} and
344     * {@link #removeMessageViewFragment} all call this, so subclasses don't have to do this when
345     * using them.
346     *
347     * However, unfortunately, subclasses have to call this manually when popping from the
348     * back stack to avoid double-delete.
349     */
350    protected void addFragmentToRemovalList(Fragment fragment) {
351        if (fragment != null) {
352            mRemovedFragments.add(fragment);
353        }
354    }
355
356    /**
357     * Remove the fragment if it's installed.
358     */
359    protected FragmentTransaction removeMailboxListFragment(FragmentTransaction ft) {
360        removeFragment(ft, mMailboxListFragment);
361        return ft;
362    }
363
364    /**
365     * Remove the fragment if it's installed.
366     */
367    protected FragmentTransaction removeMessageListFragment(FragmentTransaction ft) {
368        removeFragment(ft, mMessageListFragment);
369        return ft;
370    }
371
372    /**
373     * Remove the fragment if it's installed.
374     */
375    protected FragmentTransaction removeMessageViewFragment(FragmentTransaction ft) {
376        removeFragment(ft, mMessageViewFragment);
377        return ft;
378    }
379
380    /** @return true if a {@link MailboxListFragment} is installed. */
381    protected final boolean isMailboxListInstalled() {
382        return mMailboxListFragment != null;
383    }
384
385    /** @return true if a {@link MessageListFragment} is installed. */
386    protected final boolean isMessageListInstalled() {
387        return mMessageListFragment != null;
388    }
389
390    /** @return true if a {@link MessageViewFragment} is installed. */
391    protected final boolean isMessageViewInstalled() {
392        return mMessageViewFragment != null;
393    }
394
395    /** @return the installed {@link MailboxListFragment} or null. */
396    protected final MailboxListFragment getMailboxListFragment() {
397        return mMailboxListFragment;
398    }
399
400    /** @return the installed {@link MessageListFragment} or null. */
401    protected final MessageListFragment getMessageListFragment() {
402        return mMessageListFragment;
403    }
404
405    /** @return the installed {@link MessageViewFragment} or null. */
406    protected final MessageViewFragment getMessageViewFragment() {
407        return mMessageViewFragment;
408    }
409
410    /**
411     * @return the currently selected account ID, *or* {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
412     *
413     * @see #getActualAccountId()
414     */
415    public abstract long getUIAccountId();
416
417    /**
418     * @return true if an account is selected, or the current view is the combined view.
419     */
420    public final boolean isAccountSelected() {
421        return getUIAccountId() != Account.NO_ACCOUNT;
422    }
423
424    /**
425     * @return if an actual account is selected.  (i.e. {@link Account#ACCOUNT_ID_COMBINED_VIEW}
426     * is not considered "actual".s)
427     */
428    public final boolean isActualAccountSelected() {
429        return isAccountSelected() && (getUIAccountId() != Account.ACCOUNT_ID_COMBINED_VIEW);
430    }
431
432    /**
433     * @return the currently selected account ID.  If the current view is the combined view,
434     * it'll return {@link Account#NO_ACCOUNT}.
435     *
436     * @see #getUIAccountId()
437     */
438    public final long getActualAccountId() {
439        return isActualAccountSelected() ? getUIAccountId() : Account.NO_ACCOUNT;
440    }
441
442    /**
443     * Show the default view for the given account.
444     *
445     * No-op if the given account is already selected.
446     *
447     * @param accountId ID of the account to load.  Can be {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
448     *     Must never be {@link Account#NO_ACCOUNT}.
449     */
450    public final void switchAccount(long accountId) {
451        if (accountId == getUIAccountId()) {
452            // Do nothing if the account is already selected.  Not even going back to the inbox.
453            return;
454        }
455        openAccount(accountId);
456    }
457
458    /**
459     * Shortcut for {@link #open} with {@link Mailbox#NO_MAILBOX} and {@link Message#NO_MESSAGE}.
460     */
461    protected final void openAccount(long accountId) {
462        open(accountId, Mailbox.NO_MAILBOX, Message.NO_MESSAGE);
463    }
464
465    /**
466     * Shortcut for {@link #open} with {@link Message#NO_MESSAGE}.
467     */
468    protected final void openMailbox(long accountId, long mailboxId) {
469        open(accountId, mailboxId, Message.NO_MESSAGE);
470    }
471
472    /**
473     * Loads the given account and optionally selects the given mailbox and message.  Used to open
474     * a particular view at a request from outside of the activity, such as the widget.
475     *
476     * @param accountId ID of the account to load.  Can be {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
477     *     Must never be {@link Account#NO_ACCOUNT}.
478     * @param mailboxId ID of the mailbox to load. If {@link Mailbox#NO_MAILBOX},
479     *     load the account's inbox.
480     * @param messageId ID of the message to load. If {@link Message#NO_MESSAGE},
481     *     do not open a message.
482     */
483    public abstract void open(long accountId, long mailboxId, long messageId);
484
485    /**
486     * Performs the back action.
487     *
488     * NOTE The method in the base class has precedence.  Subclasses overriding this method MUST
489     * call super's method first.
490     *
491     * @param isSystemBackKey <code>true</code> if the system back key was pressed.
492     * <code>false</code> if it's caused by the "home" icon click on the action bar.
493     */
494    public boolean onBackPressed(boolean isSystemBackKey) {
495        if (mActionBarController.onBackPressed(isSystemBackKey)) {
496            return true;
497        }
498        return false;
499    }
500
501    /**
502     * Must be called from {@link Activity#onSearchRequested()}.
503     */
504    public void onSearchRequested() {
505        mActionBarController.enterSearchMode(null);
506    }
507
508    /**
509     * Callback called when the inbox lookup (started by {@link #startInboxLookup}) is finished.
510     */
511    protected abstract MailboxFinder.Callback getInboxLookupCallback();
512
513    private final MailboxFinder.Callback mMailboxFinderCallback = new MailboxFinder.Callback() {
514        private void cleanUp() {
515            mInboxFinder = null;
516        }
517
518        @Override
519        public void onAccountNotFound() {
520            getInboxLookupCallback().onAccountNotFound();
521            cleanUp();
522        }
523
524        @Override
525        public void onAccountSecurityHold(long accountId) {
526            getInboxLookupCallback().onAccountSecurityHold(accountId);
527            cleanUp();
528        }
529
530        @Override
531        public void onMailboxFound(long accountId, long mailboxId) {
532            getInboxLookupCallback().onMailboxFound(accountId, mailboxId);
533            cleanUp();
534        }
535
536        @Override
537        public void onMailboxNotFound(long accountId) {
538            getInboxLookupCallback().onMailboxNotFound(accountId);
539            cleanUp();
540        }
541    };
542
543    /**
544     * Start inbox lookup.
545     */
546    protected void startInboxLookup(long accountId) {
547        if (mInboxFinder != null) {
548            return; // already running
549        }
550        mInboxLookupAccountId = accountId;
551        if (!mResumed) {
552            mResumeInboxLookup = true; // Don't start yet.
553            return;
554        }
555        mInboxFinder = new MailboxFinder(mActivity, accountId, Mailbox.TYPE_INBOX,
556                mMailboxFinderCallback);
557        mInboxFinder.startLookup();
558    }
559
560    /**
561     * Stop inbox lookup.
562     */
563    protected void stopInboxLookup() {
564        if (mInboxFinder == null) {
565            return; // not running
566        }
567        mInboxFinder.cancel();
568        mInboxFinder = null;
569    }
570
571    /** @return true if the search menu option should be enabled. */
572    protected boolean canSearch() {
573        return false;
574    }
575
576    /**
577     * Handles the {@link android.app.Activity#onCreateOptionsMenu} callback.
578     */
579    public boolean onCreateOptionsMenu(MenuInflater inflater, Menu menu) {
580        inflater.inflate(R.menu.email_activity_options, menu);
581        return true;
582    }
583
584    /**
585     * Handles the {@link android.app.Activity#onPrepareOptionsMenu} callback.
586     */
587    public boolean onPrepareOptionsMenu(MenuInflater inflater, Menu menu) {
588
589        // Update the refresh button.
590        MenuItem item = menu.findItem(R.id.refresh);
591        if (isRefreshEnabled()) {
592            item.setVisible(true);
593            if (isRefreshInProgress()) {
594                item.setActionView(R.layout.action_bar_indeterminate_progress);
595            } else {
596                item.setActionView(null);
597            }
598        } else {
599            item.setVisible(false);
600        }
601
602        // STOPSHIP Temporary search options code
603        // Only show search/sync options for EAS 12.0 and later
604        boolean canSearch = false;
605        if (canSearch()) {
606            long accountId = getActualAccountId();
607            if (accountId > 0) {
608                // Move database operations out of the UI thread
609                if ("eas".equals(Account.getProtocol(mActivity, accountId))) {
610                    Account account = Account.restoreAccountWithId(mActivity, accountId);
611                    if (account != null) {
612                        // We should set a flag in the account indicating ability to handle search
613                        String protocolVersion = account.mProtocolVersion;
614                        if (Double.parseDouble(protocolVersion) >= 12.0) {
615                            canSearch = true;
616                        }
617                    }
618                }
619            }
620        }
621        // Should use an isSearchable call to prevent search on inappropriate accounts/boxes
622        // STOPSHIP Figure out where the "canSearch" test belongs
623        menu.findItem(R.id.search).setVisible(true); //canSearch);
624
625        return true;
626    }
627
628    /**
629     * Handles the {@link android.app.Activity#onOptionsItemSelected} callback.
630     *
631     * @return true if the option item is handled.
632     */
633    public boolean onOptionsItemSelected(MenuItem item) {
634        switch (item.getItemId()) {
635            case android.R.id.home:
636                // Comes from the action bar when the app icon on the left is pressed.
637                // It works like a back press, but it won't close the activity.
638                return onBackPressed(false);
639            case R.id.compose:
640                return onCompose();
641            case R.id.refresh:
642                onRefresh();
643                return true;
644            case R.id.account_settings:
645                return onAccountSettings();
646            case R.id.search:
647                onSearchRequested();
648                return true;
649        }
650        return false;
651    }
652
653    /**
654     * Opens the message compose activity.
655     */
656    private boolean onCompose() {
657        if (!isAccountSelected()) {
658            return false; // this shouldn't really happen
659        }
660        MessageCompose.actionCompose(mActivity, getActualAccountId());
661        return true;
662    }
663
664    /**
665     * Handles the "Settings" option item.  Opens the settings activity.
666     */
667    private boolean onAccountSettings() {
668        AccountSettings.actionSettings(mActivity, getActualAccountId());
669        return true;
670    }
671
672    /**
673     * STOPSHIP For experimental UI.  Remove this.
674     *
675     * @return mailbox ID for "mailbox settings" option.
676     */
677    public abstract long getMailboxSettingsMailboxId();
678
679    /**
680     * STOPSHIP For experimental UI.  Make it abstract protected.
681     *
682     * Performs "refesh".
683     */
684    public abstract void onRefresh();
685
686    /**
687     * @return true if refresh is in progress for the current mailbox.
688     */
689    protected abstract boolean isRefreshInProgress();
690
691    /**
692     * @return true if the UI should enable the "refresh" command.
693     */
694    protected abstract boolean isRefreshEnabled();
695
696    /**
697     * Refresh the action bar and menu items, including the "refreshing" icon.
698     */
699    protected void refreshActionBar() {
700        if (mActionBarController != null) {
701            mActionBarController.refresh();
702        }
703        mActivity.invalidateOptionsMenu();
704    }
705
706    @Override
707    public String toString() {
708        return getClass().getSimpleName(); // Shown on logcat
709    }
710}
711