UIControllerBase.java revision 22d1a794cd9636634bb31689f53603c0ae64c522
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.FolderProperties;
31import com.android.email.MessageListContext;
32import com.android.email.Preferences;
33import com.android.email.R;
34import com.android.email.RefreshManager;
35import com.android.email.activity.setup.AccountSettings;
36import com.android.emailcommon.Logging;
37import com.android.emailcommon.provider.Account;
38import com.android.emailcommon.provider.EmailContent.Message;
39import com.android.emailcommon.provider.HostAuth;
40import com.android.emailcommon.provider.Mailbox;
41import com.android.emailcommon.utility.EmailAsyncTask;
42import com.android.emailcommon.utility.Utility;
43import com.google.common.base.Objects;
44import com.google.common.base.Preconditions;
45
46import java.util.LinkedList;
47import java.util.List;
48
49/**
50 * Base class for the UI controller.
51 */
52abstract class UIControllerBase implements MailboxListFragment.Callback,
53        MessageListFragment.Callback, MessageViewFragment.Callback  {
54    static final boolean DEBUG_FRAGMENTS = false; // DO NOT SUBMIT WITH TRUE
55
56    static final String KEY_LIST_CONTEXT = "UIControllerBase.listContext";
57
58    /** The owner activity */
59    final EmailActivity mActivity;
60    final FragmentManager mFragmentManager;
61
62    protected final ActionBarController mActionBarController;
63
64    private MessageOrderManager mOrderManager;
65    private final MessageOrderManagerCallback mMessageOrderManagerCallback =
66            new MessageOrderManagerCallback();
67
68    final EmailAsyncTask.Tracker mTaskTracker = new EmailAsyncTask.Tracker();
69
70    final RefreshManager mRefreshManager;
71
72    /**
73     * Fragments that are installed.
74     *
75     * A fragment is installed in {@link Fragment#onActivityCreated} and uninstalled in
76     * {@link Fragment#onDestroyView}, using {@link FragmentInstallable} callbacks.
77     *
78     * This means fragments in the back stack are *not* installed.
79     *
80     * We set callbacks to fragments only when they are installed.
81     *
82     * @see FragmentInstallable
83     */
84    private MailboxListFragment mMailboxListFragment;
85    private MessageListFragment mMessageListFragment;
86    private MessageViewFragment mMessageViewFragment;
87
88    /**
89     * To avoid double-deleting a fragment (which will cause a runtime exception),
90     * we put a fragment in this list when we {@link FragmentTransaction#remove(Fragment)} it,
91     * and remove from the list when we actually uninstall it.
92     */
93    private final List<Fragment> mRemovedFragments = new LinkedList<Fragment>();
94
95    /**
96     * The active context for the current MessageList.
97     * In some UI layouts such as the one-pane view, the message list may not be visible, but is
98     * on the backstack. This list context will still be accessible in those cases.
99     *
100     * Should be set using {@link #setListContext(MessageListContext)}.
101     */
102    protected MessageListContext mListContext;
103
104    private final RefreshManager.Listener mRefreshListener
105            = new RefreshManager.Listener() {
106        @Override
107        public void onMessagingError(final long accountId, long mailboxId, final String message) {
108            refreshActionBar();
109        }
110
111        @Override
112        public void onRefreshStatusChanged(long accountId, long mailboxId) {
113            refreshActionBar();
114        }
115    };
116
117    public UIControllerBase(EmailActivity activity) {
118        mActivity = activity;
119        mFragmentManager = activity.getFragmentManager();
120        mRefreshManager = RefreshManager.getInstance(mActivity);
121        mActionBarController = createActionBarController(activity);
122        if (DEBUG_FRAGMENTS) {
123            FragmentManager.enableDebugLogging(true);
124        }
125    }
126
127    /**
128     * Called by the base class to let a subclass create an {@link ActionBarController}.
129     */
130    protected abstract ActionBarController createActionBarController(Activity activity);
131
132    /** @return the layout ID for the activity. */
133    public abstract int getLayoutId();
134
135    /**
136     * Must be called just after the activity sets up the content view.  Used to initialize views.
137     *
138     * (Due to the complexity regarding class/activity initialization order, we can't do this in
139     * the constructor.)
140     */
141    public void onActivityViewReady() {
142        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
143            Log.d(Logging.LOG_TAG, this + " onActivityViewReady");
144        }
145    }
146
147    /**
148     * Called at the end of {@link EmailActivity#onCreate}.
149     */
150    public void onActivityCreated() {
151        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
152            Log.d(Logging.LOG_TAG, this + " onActivityCreated");
153        }
154        mRefreshManager.registerListener(mRefreshListener);
155        mActionBarController.onActivityCreated();
156    }
157
158    /**
159     * Handles the {@link android.app.Activity#onStart} callback.
160     */
161    public void onActivityStart() {
162        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
163            Log.d(Logging.LOG_TAG, this + " onActivityStart");
164        }
165        if (isMessageViewInstalled()) {
166            updateMessageOrderManager();
167        }
168    }
169
170    /**
171     * Handles the {@link android.app.Activity#onResume} callback.
172     */
173    public void onActivityResume() {
174        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
175            Log.d(Logging.LOG_TAG, this + " onActivityResume");
176        }
177        refreshActionBar();
178    }
179
180    /**
181     * Handles the {@link android.app.Activity#onPause} callback.
182     */
183    public void onActivityPause() {
184        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
185            Log.d(Logging.LOG_TAG, this + " onActivityPause");
186        }
187    }
188
189    /**
190     * Handles the {@link android.app.Activity#onStop} callback.
191     */
192    public void onActivityStop() {
193        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
194            Log.d(Logging.LOG_TAG, this + " onActivityStop");
195        }
196        stopMessageOrderManager();
197    }
198
199    /**
200     * Handles the {@link android.app.Activity#onDestroy} callback.
201     */
202    public void onActivityDestroy() {
203        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
204            Log.d(Logging.LOG_TAG, this + " onActivityDestroy");
205        }
206        mActionBarController.onActivityDestroy();
207        mRefreshManager.unregisterListener(mRefreshListener);
208        mTaskTracker.cancellAllInterrupt();
209    }
210
211    /**
212     * Handles the {@link android.app.Activity#onSaveInstanceState} callback.
213     */
214    public void onSaveInstanceState(Bundle outState) {
215        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
216            Log.d(Logging.LOG_TAG, this + " onSaveInstanceState");
217        }
218        mActionBarController.onSaveInstanceState(outState);
219        outState.putParcelable(KEY_LIST_CONTEXT, mListContext);
220    }
221
222    /**
223     * Handles the {@link android.app.Activity#onRestoreInstanceState} callback.
224     */
225    public void onRestoreInstanceState(Bundle savedInstanceState) {
226        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
227            Log.d(Logging.LOG_TAG, this + " restoreInstanceState");
228        }
229        mActionBarController.onRestoreInstanceState(savedInstanceState);
230        mListContext = savedInstanceState.getParcelable(KEY_LIST_CONTEXT);
231    }
232
233    // MessageViewFragment$Callback
234    @Override
235    public void onMessageSetUnread() {
236        doAutoAdvance();
237    }
238
239    // MessageViewFragment$Callback
240    @Override
241    public void onMessageNotExists() {
242        doAutoAdvance();
243    }
244
245    // MessageViewFragment$Callback
246    @Override
247    public void onRespondedToInvite(int response) {
248        doAutoAdvance();
249    }
250
251    // MessageViewFragment$Callback
252    @Override
253    public void onBeforeMessageGone() {
254        doAutoAdvance();
255    }
256
257    /**
258     * Install a fragment.  Must be caleld from the host activity's
259     * {@link FragmentInstallable#onInstallFragment}.
260     */
261    public final void onInstallFragment(Fragment fragment) {
262        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
263            Log.d(Logging.LOG_TAG, this + " onInstallFragment  fragment=" + fragment);
264        }
265        if (fragment instanceof MailboxListFragment) {
266            installMailboxListFragment((MailboxListFragment) fragment);
267        } else if (fragment instanceof MessageListFragment) {
268            installMessageListFragment((MessageListFragment) fragment);
269        } else if (fragment instanceof MessageViewFragment) {
270            installMessageViewFragment((MessageViewFragment) fragment);
271        } else {
272            throw new IllegalArgumentException("Tried to install unknown fragment");
273        }
274    }
275
276    /** Install fragment */
277    protected void installMailboxListFragment(MailboxListFragment fragment) {
278        mMailboxListFragment = fragment;
279        mMailboxListFragment.setCallback(this);
280        refreshActionBar();
281    }
282
283    /** Install fragment */
284    protected void installMessageListFragment(MessageListFragment fragment) {
285        mMessageListFragment = fragment;
286        mMessageListFragment.setCallback(this);
287        refreshActionBar();
288    }
289
290    /** Install fragment */
291    protected void installMessageViewFragment(MessageViewFragment fragment) {
292        mMessageViewFragment = fragment;
293        mMessageViewFragment.setCallback(this);
294
295        updateMessageOrderManager();
296        refreshActionBar();
297    }
298
299    /**
300     * Uninstall a fragment.  Must be caleld from the host activity's
301     * {@link FragmentInstallable#onUninstallFragment}.
302     */
303    public final void onUninstallFragment(Fragment fragment) {
304        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
305            Log.d(Logging.LOG_TAG, this + " onUninstallFragment  fragment=" + fragment);
306        }
307        mRemovedFragments.remove(fragment);
308        if (fragment == mMailboxListFragment) {
309            uninstallMailboxListFragment();
310        } else if (fragment == mMessageListFragment) {
311            uninstallMessageListFragment();
312        } else if (fragment == mMessageViewFragment) {
313            uninstallMessageViewFragment();
314        } else {
315            throw new IllegalArgumentException("Tried to uninstall unknown fragment");
316        }
317    }
318
319    /** Uninstall {@link MailboxListFragment} */
320    protected void uninstallMailboxListFragment() {
321        mMailboxListFragment.setCallback(null);
322        mMailboxListFragment = null;
323    }
324
325    /** Uninstall {@link MessageListFragment} */
326    protected void uninstallMessageListFragment() {
327        mMessageListFragment.setCallback(null);
328        mMessageListFragment = null;
329    }
330
331    /** Uninstall {@link MessageViewFragment} */
332    protected void uninstallMessageViewFragment() {
333        mMessageViewFragment.setCallback(null);
334        mMessageViewFragment = null;
335    }
336
337    /**
338     * If a {@link Fragment} is not already in {@link #mRemovedFragments},
339     * {@link FragmentTransaction#remove} it and add to the list.
340     *
341     * Do nothing if {@code fragment} is null.
342     */
343    protected final void removeFragment(FragmentTransaction ft, Fragment fragment) {
344        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
345            Log.d(Logging.LOG_TAG, this + " removeFragment fragment=" + fragment);
346        }
347        if (fragment == null) {
348            return;
349        }
350        if (!mRemovedFragments.contains(fragment)) {
351            // STOPSHIP Remove log/catch.  b/4905749.
352            Log.d(Logging.LOG_TAG, "Removing " + fragment);
353            try {
354                ft.remove(fragment);
355            } catch (RuntimeException ex) {
356                Log.e(Logging.LOG_TAG, "Got RuntimeException trying to remove fragment: "
357                        + fragment, ex);
358                Log.e(Logging.LOG_TAG, Utility.dumpFragment(fragment));
359                throw ex;
360            }
361            addFragmentToRemovalList(fragment);
362        }
363    }
364
365    /**
366     * Remove a {@link Fragment} from {@link #mRemovedFragments}.  No-op if {@code fragment} is
367     * null.
368     *
369     * {@link #removeMailboxListFragment}, {@link #removeMessageListFragment} and
370     * {@link #removeMessageViewFragment} all call this, so subclasses don't have to do this when
371     * using them.
372     *
373     * However, unfortunately, subclasses have to call this manually when popping from the
374     * back stack to avoid double-delete.
375     */
376    protected void addFragmentToRemovalList(Fragment fragment) {
377        if (fragment != null) {
378            mRemovedFragments.add(fragment);
379        }
380    }
381
382    /**
383     * Remove the fragment if it's installed.
384     */
385    protected FragmentTransaction removeMailboxListFragment(FragmentTransaction ft) {
386        removeFragment(ft, mMailboxListFragment);
387        return ft;
388    }
389
390    /**
391     * Remove the fragment if it's installed.
392     */
393    protected FragmentTransaction removeMessageListFragment(FragmentTransaction ft) {
394        removeFragment(ft, mMessageListFragment);
395        return ft;
396    }
397
398    /**
399     * Remove the fragment if it's installed.
400     */
401    protected FragmentTransaction removeMessageViewFragment(FragmentTransaction ft) {
402        removeFragment(ft, mMessageViewFragment);
403        return ft;
404    }
405
406    /** @return true if a {@link MailboxListFragment} is installed. */
407    protected final boolean isMailboxListInstalled() {
408        return mMailboxListFragment != null;
409    }
410
411    /** @return true if a {@link MessageListFragment} is installed. */
412    protected final boolean isMessageListInstalled() {
413        return mMessageListFragment != null;
414    }
415
416    /** @return true if a {@link MessageViewFragment} is installed. */
417    protected final boolean isMessageViewInstalled() {
418        return mMessageViewFragment != null;
419    }
420
421    /** @return the installed {@link MailboxListFragment} or null. */
422    protected final MailboxListFragment getMailboxListFragment() {
423        return mMailboxListFragment;
424    }
425
426    /** @return the installed {@link MessageListFragment} or null. */
427    protected final MessageListFragment getMessageListFragment() {
428        return mMessageListFragment;
429    }
430
431    /** @return the installed {@link MessageViewFragment} or null. */
432    protected final MessageViewFragment getMessageViewFragment() {
433        return mMessageViewFragment;
434    }
435
436    /**
437     * @return the currently selected account ID, *or* {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
438     *
439     * @see #getActualAccountId()
440     */
441    public abstract long getUIAccountId();
442
443    /**
444     * @return true if an account is selected, or the current view is the combined view.
445     */
446    public final boolean isAccountSelected() {
447        return getUIAccountId() != Account.NO_ACCOUNT;
448    }
449
450    /**
451     * @return if an actual account is selected.  (i.e. {@link Account#ACCOUNT_ID_COMBINED_VIEW}
452     * is not considered "actual".s)
453     */
454    public final boolean isActualAccountSelected() {
455        return isAccountSelected() && (getUIAccountId() != Account.ACCOUNT_ID_COMBINED_VIEW);
456    }
457
458    /**
459     * @return the currently selected account ID.  If the current view is the combined view,
460     * it'll return {@link Account#NO_ACCOUNT}.
461     *
462     * @see #getUIAccountId()
463     */
464    public final long getActualAccountId() {
465        return isActualAccountSelected() ? getUIAccountId() : Account.NO_ACCOUNT;
466    }
467
468    /**
469     * Show the default view for the given account.
470     *
471     * @param accountId ID of the account to load.  Can be {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
472     *     Must never be {@link Account#NO_ACCOUNT}.
473     * @param forceShowInbox If {@code false} and the given account is already selected, do nothing.
474     *        If {@code false}, we always change the view even if the account is selected.
475     */
476    public final void switchAccount(long accountId, boolean forceShowInbox) {
477
478        if (Account.isSecurityHold(mActivity, accountId)) {
479            ActivityHelper.showSecurityHoldDialog(mActivity, accountId);
480            mActivity.finish();
481            return;
482        }
483
484        if (accountId == getUIAccountId() && !forceShowInbox) {
485            // Do nothing if the account is already selected.  Not even going back to the inbox.
486            return;
487        }
488
489        if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
490            openMailbox(accountId, Mailbox.QUERY_ALL_INBOXES);
491        } else {
492            long inboxId = Mailbox.findMailboxOfType(mActivity, accountId, Mailbox.TYPE_INBOX);
493            if (inboxId == Mailbox.NO_MAILBOX) {
494                // The account doesn't have Inbox yet... Redirect to Welcome and let it wait for
495                // the initial sync...
496                Log.w(Logging.LOG_TAG, "Account " + accountId +" doesn't have Inbox.  Redirecting"
497                        + " to Welcome...");
498                Welcome.actionOpenAccountInbox(mActivity, accountId);
499                mActivity.finish();
500                return;
501            } else {
502                openMailbox(accountId, inboxId);
503            }
504        }
505    }
506
507    /**
508     * Returns the id of the parent mailbox used for the mailbox list fragment.
509     *
510     * IMPORTANT: Do not confuse {@link #getMailboxListMailboxId()} with
511     *     {@link #getMessageListMailboxId()}
512     */
513    protected long getMailboxListMailboxId() {
514        return isMailboxListInstalled() ? getMailboxListFragment().getSelectedMailboxId()
515                : Mailbox.NO_MAILBOX;
516    }
517
518    /**
519     * Returns the id of the mailbox used for the message list fragment.
520     *
521     * IMPORTANT: Do not confuse {@link #getMailboxListMailboxId()} with
522     *     {@link #getMessageListMailboxId()}
523     */
524    protected long getMessageListMailboxId() {
525        return isMessageListInstalled() ? getMessageListFragment().getMailboxId()
526                : Mailbox.NO_MAILBOX;
527    }
528
529    /**
530     * Shortcut for {@link #open} with {@link Message#NO_MESSAGE}.
531     */
532    protected final void openMailbox(long accountId, long mailboxId) {
533        open(MessageListContext.forMailbox(accountId, mailboxId), Message.NO_MESSAGE);
534    }
535
536    /**
537     * Opens a given list
538     * @param listContext the list context for the message list to open
539     * @param messageId if specified and not {@link Message#NO_MESSAGE}, will open the message
540     *     in the message list.
541     */
542    public final void open(final MessageListContext listContext, final long messageId) {
543        setListContext(listContext);
544        openInternal(listContext, messageId);
545
546        if (listContext.isSearch()) {
547            mActionBarController.enterSearchMode(listContext.getSearchParams().mFilter);
548        }
549    }
550
551    /**
552     * Sets the internal value of the list context for the message list.
553     */
554    protected void setListContext(MessageListContext listContext) {
555        if (Objects.equal(listContext, mListContext)) {
556            return;
557        }
558
559        // TODO: remove this when the search mailbox no longer shows up on the list
560        // Special case search. Since the search mailbox shows up in the mailbox list, the mailbox
561        // list can give us a callback to open that mailbox, and it will look like a normal
562        // mailbox open instead of a search, blowing away a perfectly good list context.
563        if (mListContext != null
564                && mListContext.isSearch()
565                && mListContext.getMailboxId() == listContext.getMailboxId()) {
566            return;
567        }
568
569        if (Email.DEBUG && Logging.DEBUG_LIFECYCLE) {
570            Log.i(Logging.LOG_TAG, this + " setListContext: " + listContext);
571        }
572        mListContext = listContext;
573    }
574
575    protected abstract void openInternal(
576            final MessageListContext listContext, final long messageId);
577
578    /**
579     * Performs the back action.
580     *
581     * NOTE The method in the base class has precedence.  Subclasses overriding this method MUST
582     * call super's method first.
583     *
584     * @param isSystemBackKey <code>true</code> if the system back key was pressed.
585     * <code>false</code> if it's caused by the "home" icon click on the action bar.
586     */
587    public boolean onBackPressed(boolean isSystemBackKey) {
588        if (mActionBarController.onBackPressed(isSystemBackKey)) {
589            return true;
590        }
591        return false;
592    }
593
594    /**
595     * Must be called from {@link Activity#onSearchRequested()}.
596     * This initiates the search entry mode - see {@link #onSearchSubmit} for when the search
597     * is actually submitted.
598     */
599    public void onSearchRequested() {
600        if (isMessageListReady()) {
601            mActionBarController.enterSearchMode(null);
602        }
603    }
604
605    /**
606     * @return Whether or not a message list is ready and has its initial meta data loaded.
607     */
608    protected boolean isMessageListReady() {
609        return isMessageListInstalled() && getMessageListFragment().hasDataLoaded();
610    }
611
612    /**
613     * Determines the mailbox to search, if a search was to be initiated now.
614     * This will return {@code null} if the UI is not focused on any particular mailbox to search
615     * on.
616     */
617    private Mailbox getSearchableMailbox() {
618        if (!isMessageListReady()) {
619            return null;
620        }
621        MessageListFragment messageList = getMessageListFragment();
622
623        // If already in a search, future searches will search the original mailbox.
624        return mListContext.isSearch()
625                ? messageList.getSearchedMailbox()
626                : messageList.getMailbox();
627    }
628
629    // TODO: this logic probably needs to be tested in the backends as well, so it may be nice
630    // to consolidate this to a centralized place, so that they don't get out of sync.
631    /**
632     * @return whether or not this account should do a global search instead when a user
633     *     initiates a search on the given mailbox.
634     */
635    private static boolean shouldDoGlobalSearch(Account account, Mailbox mailbox) {
636        return ((account.mFlags & Account.FLAGS_SUPPORTS_GLOBAL_SEARCH) != 0)
637                && (mailbox.mType == Mailbox.TYPE_INBOX);
638    }
639
640    /**
641     * Retrieves the hint text to be shown for when a search entry is being made.
642     */
643    protected String getSearchHint() {
644        if (!isMessageListReady()) {
645            return "";
646        }
647        Account account = getMessageListFragment().getAccount();
648        Mailbox mailbox = getSearchableMailbox();
649
650        if (mailbox == null) {
651            return "";
652        }
653
654        if (shouldDoGlobalSearch(account, mailbox)) {
655            return mActivity.getString(R.string.search_hint);
656        }
657
658        // Regular mailbox, or IMAP - search within that mailbox.
659        String mailboxName = FolderProperties.getInstance(mActivity).getDisplayName(mailbox);
660        return String.format(
661                mActivity.getString(R.string.search_mailbox_hint),
662                mailboxName);
663    }
664
665    /**
666     * Kicks off a search query, if the UI is in a state where a search is possible.
667     */
668    protected void onSearchSubmit(final String queryTerm) {
669        final long accountId = getUIAccountId();
670        if (!Account.isNormalAccount(accountId)) {
671            return; // Invalid account to search from.
672        }
673
674        Mailbox searchableMailbox = getSearchableMailbox();
675        if (searchableMailbox == null) {
676            return;
677        }
678        final long mailboxId = searchableMailbox.mId;
679
680        if (Email.DEBUG) {
681            Log.d(Logging.LOG_TAG,
682                    "Submitting search: [" + queryTerm + "] in mailboxId=" + mailboxId);
683        }
684
685        mActivity.startActivity(EmailActivity.createSearchIntent(
686                mActivity, accountId, mailboxId, queryTerm));
687
688
689        // TODO: this causes a slight flicker.
690        // A new instance of the activity will sit on top. When the user exits search and
691        // returns to this activity, the search box should not be open then.
692        mActionBarController.exitSearchMode();
693    }
694
695    /**
696     * Handles exiting of search entry mode.
697     */
698    protected void onSearchExit() {
699        if ((mListContext != null) && mListContext.isSearch()) {
700            mActivity.finish();
701        }
702    }
703
704    /**
705     * Handles the {@link android.app.Activity#onCreateOptionsMenu} callback.
706     */
707    public boolean onCreateOptionsMenu(MenuInflater inflater, Menu menu) {
708        inflater.inflate(R.menu.email_activity_options, menu);
709        return true;
710    }
711
712    /**
713     * Handles the {@link android.app.Activity#onPrepareOptionsMenu} callback.
714     */
715    public boolean onPrepareOptionsMenu(MenuInflater inflater, Menu menu) {
716
717        // Update the refresh button.
718        MenuItem item = menu.findItem(R.id.refresh);
719        if (isRefreshEnabled()) {
720            item.setVisible(true);
721            if (isRefreshInProgress()) {
722                item.setActionView(R.layout.action_bar_indeterminate_progress);
723            } else {
724                item.setActionView(null);
725            }
726        } else {
727            item.setVisible(false);
728        }
729
730        // Deal with protocol-specific menu options.
731        boolean isEas = false;
732        boolean accountSearchable = false;
733        long accountId = getActualAccountId();
734        if (accountId > 0) {
735            Account account = Account.restoreAccountWithId(mActivity, accountId);
736            if (account != null) {
737                String protocol = account.getProtocol(mActivity);
738                if (HostAuth.SCHEME_EAS.equals(protocol)) {
739                    isEas = true;
740                }
741                accountSearchable = (account.mFlags & Account.FLAGS_SUPPORTS_SEARCH) != 0;
742            }
743        }
744
745        // TODO: Should use an isSyncable call to prevent drafts/outbox from allowing this
746        menu.findItem(R.id.search).setVisible(accountSearchable && isMessageListReady());
747        menu.findItem(R.id.sync_lookback).setVisible(isEas);
748        menu.findItem(R.id.sync_frequency).setVisible(isEas);
749
750        return true;
751    }
752
753    /**
754     * Handles the {@link android.app.Activity#onOptionsItemSelected} callback.
755     *
756     * @return true if the option item is handled.
757     */
758    public boolean onOptionsItemSelected(MenuItem item) {
759        switch (item.getItemId()) {
760            case android.R.id.home:
761                // Comes from the action bar when the app icon on the left is pressed.
762                // It works like a back press, but it won't close the activity.
763                return onBackPressed(false);
764            case R.id.compose:
765                return onCompose();
766            case R.id.refresh:
767                onRefresh();
768                return true;
769            case R.id.account_settings:
770                return onAccountSettings();
771            case R.id.search:
772                onSearchRequested();
773                return true;
774        }
775        return false;
776    }
777
778    /**
779     * Opens the message compose activity.
780     */
781    private boolean onCompose() {
782        if (!isAccountSelected()) {
783            return false; // this shouldn't really happen
784        }
785        MessageCompose.actionCompose(mActivity, getActualAccountId());
786        return true;
787    }
788
789    /**
790     * Handles the "Settings" option item.  Opens the settings activity.
791     */
792    private boolean onAccountSettings() {
793        AccountSettings.actionSettings(mActivity, getActualAccountId());
794        return true;
795    }
796
797    /**
798     * @return the ID of the message in focus and visible, if any. Returns
799     *     {@link Message#NO_MESSAGE} if no message is opened.
800     */
801    protected long getMessageId() {
802        return isMessageViewInstalled()
803                ? getMessageViewFragment().getMessageId()
804                : Message.NO_MESSAGE;
805    }
806
807
808    /**
809     * STOPSHIP For experimental UI.  Remove this.
810     *
811     * @return mailbox ID for "mailbox settings" option.
812     */
813    public abstract long getMailboxSettingsMailboxId();
814
815    /**
816     * STOPSHIP For experimental UI.  Make it abstract protected.
817     *
818     * Performs "refesh".
819     */
820    public abstract void onRefresh();
821
822    /**
823     * @return true if refresh is in progress for the current mailbox.
824     */
825    protected abstract boolean isRefreshInProgress();
826
827    /**
828     * @return true if the UI should enable the "refresh" command.
829     */
830    protected abstract boolean isRefreshEnabled();
831
832    /**
833     * Refresh the action bar and menu items, including the "refreshing" icon.
834     */
835    protected void refreshActionBar() {
836        if (mActionBarController != null) {
837            mActionBarController.refresh();
838        }
839        mActivity.invalidateOptionsMenu();
840    }
841
842    protected final MessageOrderManager getMessageOrderManager() {
843        return mOrderManager;
844    }
845
846    /** Perform "auto-advance. */
847    protected final void doAutoAdvance() {
848        switch (Preferences.getPreferences(mActivity).getAutoAdvanceDirection()) {
849            case Preferences.AUTO_ADVANCE_NEWER:
850                if (moveToNewer()) return;
851                break;
852            case Preferences.AUTO_ADVANCE_OLDER:
853                if (moveToOlder()) return;
854                break;
855        }
856        if (isMessageViewInstalled()) { // We really should have the message view but just in case
857            // Go back to mailbox list.
858            // Use onBackPressed(), so we'll restore the message view state, such as scroll
859            // position.
860            // Also make sure to pass false to isSystemBackKey, so on two-pane we don't go back
861            // to the collapsed mode.
862            onBackPressed(true);
863        }
864    }
865
866    /**
867     * Subclass must implement it to enable/disable the newer/older buttons.
868     */
869    protected abstract void updateNavigationArrows();
870
871    protected final boolean moveToOlder() {
872        if ((mOrderManager != null) && mOrderManager.moveToOlder()) {
873            navigateToMessage(mOrderManager.getCurrentMessageId());
874            return true;
875        }
876        return false;
877    }
878
879    protected final boolean moveToNewer() {
880        if ((mOrderManager != null) && mOrderManager.moveToNewer()) {
881            navigateToMessage(mOrderManager.getCurrentMessageId());
882            return true;
883        }
884        return false;
885    }
886
887    /**
888     * Called when the user taps newer/older.  Subclass must implement it to open the specified
889     * message.
890     *
891     * It's a bit different from just showing the message view fragment; on one-pane we show the
892     * message view fragment but don't want to change back state.
893     */
894    protected abstract void navigateToMessage(long messageId);
895
896    /**
897     * Potentially create a new {@link MessageOrderManager}; if it's not already started or if
898     * the account has changed, and sync it to the current message.
899     */
900    private void updateMessageOrderManager() {
901        if (!isMessageViewInstalled()) {
902            return;
903        }
904        Preconditions.checkNotNull(mListContext);
905
906        final long mailboxId = mListContext.getMailboxId();
907        if (mOrderManager == null || mOrderManager.getMailboxId() != mailboxId) {
908            stopMessageOrderManager();
909            mOrderManager =
910                new MessageOrderManager(mActivity, mailboxId, mMessageOrderManagerCallback);
911        }
912        mOrderManager.moveTo(getMessageId());
913        updateNavigationArrows();
914    }
915
916    /**
917     * Stop {@link MessageOrderManager}.
918     */
919    protected final void stopMessageOrderManager() {
920        if (mOrderManager != null) {
921            mOrderManager.close();
922            mOrderManager = null;
923        }
924    }
925
926    private class MessageOrderManagerCallback implements MessageOrderManager.Callback {
927        @Override
928        public void onMessagesChanged() {
929            updateNavigationArrows();
930        }
931
932        @Override
933        public void onMessageNotFound() {
934            doAutoAdvance();
935        }
936    }
937
938    @Override
939    public String toString() {
940        return getClass().getSimpleName(); // Shown on logcat
941    }
942}
943