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