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