UIControllerBase.java revision 5aa3d71209130bc3189440523d51dc4615446bbc
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     * Commit a {@link FragmentTransaction}.
463     */
464    protected void commitFragmentTransaction(FragmentTransaction ft) {
465        if (DEBUG_FRAGMENTS) {
466            Log.d(Logging.LOG_TAG, this + " commitFragmentTransaction: " + ft);
467        }
468        if (!ft.isEmpty()) {
469            // NB: there should be no cases in which a transaction is committed after
470            // onSaveInstanceState. Unfortunately, the "state loss" check also happens when in
471            // LoaderCallbacks.onLoadFinished, and we wish to perform transactions there. The check
472            // by the framework is conservative and prevents cases where there are transactions
473            // affecting Loader lifecycles - but we have no such cases.
474            // TODO: use asynchronous callbacks from loaders to avoid this implicit dependency
475            ft.commitAllowingStateLoss();
476            mFragmentManager.executePendingTransactions();
477        }
478    }
479
480    /**
481     * @return the currently selected account ID, *or* {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
482     *
483     * @see #getActualAccountId()
484     */
485    public abstract long getUIAccountId();
486
487    /**
488     * @return true if an account is selected, or the current view is the combined view.
489     */
490    public final boolean isAccountSelected() {
491        return getUIAccountId() != Account.NO_ACCOUNT;
492    }
493
494    /**
495     * @return if an actual account is selected.  (i.e. {@link Account#ACCOUNT_ID_COMBINED_VIEW}
496     * is not considered "actual".s)
497     */
498    public final boolean isActualAccountSelected() {
499        return isAccountSelected() && (getUIAccountId() != Account.ACCOUNT_ID_COMBINED_VIEW);
500    }
501
502    /**
503     * @return the currently selected account ID.  If the current view is the combined view,
504     * it'll return {@link Account#NO_ACCOUNT}.
505     *
506     * @see #getUIAccountId()
507     */
508    public final long getActualAccountId() {
509        return isActualAccountSelected() ? getUIAccountId() : Account.NO_ACCOUNT;
510    }
511
512    /**
513     * Show the default view for the given account.
514     *
515     * @param accountId ID of the account to load.  Can be {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
516     *     Must never be {@link Account#NO_ACCOUNT}.
517     * @param forceShowInbox If {@code false} and the given account is already selected, do nothing.
518     *        If {@code false}, we always change the view even if the account is selected.
519     */
520    public final void switchAccount(long accountId, boolean forceShowInbox) {
521
522        if (Account.isSecurityHold(mActivity, accountId)) {
523            ActivityHelper.showSecurityHoldDialog(mActivity, accountId);
524            mActivity.finish();
525            return;
526        }
527
528        if (accountId == getUIAccountId() && !forceShowInbox) {
529            // Do nothing if the account is already selected.  Not even going back to the inbox.
530            return;
531        }
532
533        if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
534            openMailbox(accountId, Mailbox.QUERY_ALL_INBOXES);
535        } else {
536            long inboxId = Mailbox.findMailboxOfType(mActivity, accountId, Mailbox.TYPE_INBOX);
537            if (inboxId == Mailbox.NO_MAILBOX) {
538                // The account doesn't have Inbox yet... Redirect to Welcome and let it wait for
539                // the initial sync...
540                Log.w(Logging.LOG_TAG, "Account " + accountId +" doesn't have Inbox.  Redirecting"
541                        + " to Welcome...");
542                Welcome.actionOpenAccountInbox(mActivity, accountId);
543                mActivity.finish();
544                return;
545            } else {
546                openMailbox(accountId, inboxId);
547            }
548        }
549    }
550
551    /**
552     * Returns the id of the parent mailbox used for the mailbox list fragment.
553     *
554     * IMPORTANT: Do not confuse {@link #getMailboxListMailboxId()} with
555     *     {@link #getMessageListMailboxId()}
556     */
557    protected long getMailboxListMailboxId() {
558        return isMailboxListInstalled() ? getMailboxListFragment().getSelectedMailboxId()
559                : Mailbox.NO_MAILBOX;
560    }
561
562    /**
563     * Returns the id of the mailbox used for the message list fragment.
564     *
565     * IMPORTANT: Do not confuse {@link #getMailboxListMailboxId()} with
566     *     {@link #getMessageListMailboxId()}
567     */
568    protected long getMessageListMailboxId() {
569        return isMessageListInstalled() ? getMessageListFragment().getMailboxId()
570                : Mailbox.NO_MAILBOX;
571    }
572
573    /**
574     * Shortcut for {@link #open} with {@link Message#NO_MESSAGE}.
575     */
576    protected final void openMailbox(long accountId, long mailboxId) {
577        open(MessageListContext.forMailbox(accountId, mailboxId), Message.NO_MESSAGE);
578    }
579
580    /**
581     * Opens a given list
582     * @param listContext the list context for the message list to open
583     * @param messageId if specified and not {@link Message#NO_MESSAGE}, will open the message
584     *     in the message list.
585     */
586    public final void open(final MessageListContext listContext, final long messageId) {
587        setListContext(listContext);
588        openInternal(listContext, messageId);
589
590        if (listContext.isSearch()) {
591            mActionBarController.enterSearchMode(listContext.getSearchParams().mFilter);
592        }
593    }
594
595    /**
596     * Sets the internal value of the list context for the message list.
597     */
598    protected void setListContext(MessageListContext listContext) {
599        if (Objects.equal(listContext, mListContext)) {
600            return;
601        }
602
603        if (Email.DEBUG && Logging.DEBUG_LIFECYCLE) {
604            Log.i(Logging.LOG_TAG, this + " setListContext: " + listContext);
605        }
606        mListContext = listContext;
607    }
608
609    protected abstract void openInternal(
610            final MessageListContext listContext, final long messageId);
611
612    /**
613     * Performs the back action.
614     *
615     * @param isSystemBackKey <code>true</code> if the system back key was pressed.
616     * <code>false</code> if it's caused by the "home" icon click on the action bar.
617     */
618    public abstract boolean onBackPressed(boolean isSystemBackKey);
619
620    /**
621     * Must be called from {@link Activity#onSearchRequested()}.
622     * This initiates the search entry mode - see {@link #onSearchSubmit} for when the search
623     * is actually submitted.
624     */
625    public void onSearchRequested() {
626        long accountId = getActualAccountId();
627        boolean accountSearchable = false;
628        if (accountId > 0) {
629            Account account = Account.restoreAccountWithId(mActivity, accountId);
630            if (account != null) {
631                String protocol = account.getProtocol(mActivity);
632                accountSearchable = (account.mFlags & Account.FLAGS_SUPPORTS_SEARCH) != 0;
633            }
634        }
635
636        if (!accountSearchable) {
637            return;
638        }
639
640        if (isMessageListReady()) {
641            mActionBarController.enterSearchMode(null);
642        }
643    }
644
645    /**
646     * @return Whether or not a message list is ready and has its initial meta data loaded.
647     */
648    protected boolean isMessageListReady() {
649        return isMessageListInstalled() && getMessageListFragment().hasDataLoaded();
650    }
651
652    /**
653     * Determines the mailbox to search, if a search was to be initiated now.
654     * This will return {@code null} if the UI is not focused on any particular mailbox to search
655     * on.
656     */
657    private Mailbox getSearchableMailbox() {
658        if (!isMessageListReady()) {
659            return null;
660        }
661        MessageListFragment messageList = getMessageListFragment();
662
663        // If already in a search, future searches will search the original mailbox.
664        return mListContext.isSearch()
665                ? messageList.getSearchedMailbox()
666                : messageList.getMailbox();
667    }
668
669    // TODO: this logic probably needs to be tested in the backends as well, so it may be nice
670    // to consolidate this to a centralized place, so that they don't get out of sync.
671    /**
672     * @return whether or not this account should do a global search instead when a user
673     *     initiates a search on the given mailbox.
674     */
675    private static boolean shouldDoGlobalSearch(Account account, Mailbox mailbox) {
676        return ((account.mFlags & Account.FLAGS_SUPPORTS_GLOBAL_SEARCH) != 0)
677                && (mailbox.mType == Mailbox.TYPE_INBOX);
678    }
679
680    /**
681     * Retrieves the hint text to be shown for when a search entry is being made.
682     */
683    protected String getSearchHint() {
684        if (!isMessageListReady()) {
685            return "";
686        }
687        Account account = getMessageListFragment().getAccount();
688        Mailbox mailbox = getSearchableMailbox();
689
690        if (mailbox == null) {
691            return "";
692        }
693
694        if (shouldDoGlobalSearch(account, mailbox)) {
695            return mActivity.getString(R.string.search_hint);
696        }
697
698        // Regular mailbox, or IMAP - search within that mailbox.
699        String mailboxName = FolderProperties.getInstance(mActivity).getDisplayName(mailbox);
700        return String.format(
701                mActivity.getString(R.string.search_mailbox_hint),
702                mailboxName);
703    }
704
705    /**
706     * Kicks off a search query, if the UI is in a state where a search is possible.
707     */
708    protected void onSearchSubmit(final String queryTerm) {
709        final long accountId = getUIAccountId();
710        if (!Account.isNormalAccount(accountId)) {
711            return; // Invalid account to search from.
712        }
713
714        Mailbox searchableMailbox = getSearchableMailbox();
715        if (searchableMailbox == null) {
716            return;
717        }
718        final long mailboxId = searchableMailbox.mId;
719
720        if (Email.DEBUG) {
721            Log.d(Logging.LOG_TAG,
722                    "Submitting search: [" + queryTerm + "] in mailboxId=" + mailboxId);
723        }
724
725        mActivity.startActivity(EmailActivity.createSearchIntent(
726                mActivity, accountId, mailboxId, queryTerm));
727
728
729        // TODO: this causes a slight flicker.
730        // A new instance of the activity will sit on top. When the user exits search and
731        // returns to this activity, the search box should not be open then.
732        mActionBarController.exitSearchMode();
733    }
734
735    /**
736     * Handles exiting of search entry mode.
737     */
738    protected void onSearchExit() {
739        if ((mListContext != null) && mListContext.isSearch()) {
740            mActivity.finish();
741        }
742    }
743
744    /**
745     * Handles the {@link android.app.Activity#onCreateOptionsMenu} callback.
746     */
747    public boolean onCreateOptionsMenu(MenuInflater inflater, Menu menu) {
748        inflater.inflate(R.menu.email_activity_options, menu);
749        return true;
750    }
751
752    /**
753     * Handles the {@link android.app.Activity#onPrepareOptionsMenu} callback.
754     */
755    public boolean onPrepareOptionsMenu(MenuInflater inflater, Menu menu) {
756        // Update the refresh button.
757        MenuItem item = menu.findItem(R.id.refresh);
758        if (isRefreshEnabled()) {
759            item.setVisible(true);
760            mRefreshListener.setRefreshIcon(item);
761        } else {
762            item.setVisible(false);
763            mRefreshListener.setRefreshIcon(null);
764        }
765
766        // Deal with protocol-specific menu options.
767        boolean isEas = false;
768        boolean accountSearchable = false;
769        long accountId = getActualAccountId();
770        if (accountId > 0) {
771            Account account = Account.restoreAccountWithId(mActivity, accountId);
772            if (account != null) {
773                String protocol = account.getProtocol(mActivity);
774                if (HostAuth.SCHEME_EAS.equals(protocol)) {
775                    isEas = true;
776                }
777                accountSearchable = (account.mFlags & Account.FLAGS_SUPPORTS_SEARCH) != 0;
778            }
779        }
780
781        // TODO: Should use an isSyncable call to prevent drafts/outbox from allowing this
782        menu.findItem(R.id.search).setVisible(accountSearchable);
783        // TODO Show only for syncable mailbox as well.
784        menu.findItem(R.id.mailbox_settings).setVisible(isEas
785                && (getMailboxSettingsMailboxId() != Mailbox.NO_MAILBOX));
786
787        return true;
788    }
789
790    /**
791     * Handles the {@link android.app.Activity#onOptionsItemSelected} callback.
792     *
793     * @return true if the option item is handled.
794     */
795    public boolean onOptionsItemSelected(MenuItem item) {
796        switch (item.getItemId()) {
797            case android.R.id.home:
798                // Comes from the action bar when the app icon on the left is pressed.
799                // It works like a back press, but it won't close the activity.
800                return onBackPressed(false);
801            case R.id.compose:
802                return onCompose();
803            case R.id.refresh:
804                onRefresh();
805                return true;
806            case R.id.account_settings:
807                return onAccountSettings();
808            case R.id.search:
809                onSearchRequested();
810                return true;
811            case R.id.mailbox_settings:
812                final long mailboxId = getMailboxSettingsMailboxId();
813                if (mailboxId != Mailbox.NO_MAILBOX) {
814                    MailboxSettings.start(mActivity, mailboxId);
815                }
816                return true;
817        }
818        return false;
819    }
820
821    /**
822     * Opens the message compose activity.
823     */
824    private boolean onCompose() {
825        if (!isAccountSelected()) {
826            return false; // this shouldn't really happen
827        }
828        MessageCompose.actionCompose(mActivity, getActualAccountId());
829        return true;
830    }
831
832    /**
833     * Handles the "Settings" option item.  Opens the settings activity.
834     */
835    private boolean onAccountSettings() {
836        AccountSettings.actionSettings(mActivity, getActualAccountId());
837        return true;
838    }
839
840    /**
841     * @return the ID of the message in focus and visible, if any. Returns
842     *     {@link Message#NO_MESSAGE} if no message is opened.
843     */
844    protected long getMessageId() {
845        return isMessageViewInstalled()
846                ? getMessageViewFragment().getMessageId()
847                : Message.NO_MESSAGE;
848    }
849
850
851    /**
852     * @return mailbox ID for "mailbox settings" option.
853     */
854    protected abstract long getMailboxSettingsMailboxId();
855
856    /**
857     * Performs "refesh".
858     */
859    protected abstract void onRefresh();
860
861    /**
862     * @return true if refresh is in progress for the current mailbox.
863     */
864    protected abstract boolean isRefreshInProgress();
865
866    /**
867     * @return true if the UI should enable the "refresh" command.
868     */
869    protected abstract boolean isRefreshEnabled();
870
871    /**
872     * Refresh the action bar and menu items, including the "refreshing" icon.
873     */
874    protected void refreshActionBar() {
875        if (mActionBarController != null) {
876            mActionBarController.refresh();
877        }
878        mActivity.invalidateOptionsMenu();
879    }
880
881    // MessageListFragment.Callback
882    @Override
883    public void onMailboxNotFound() {
884        // Something bad happened - the account or mailbox we were looking for was deleted.
885        // Just restart and let the entry flow find a good default view.
886        Utility.showToast(mActivity, R.string.toast_mailbox_not_found);
887        long accountId = getUIAccountId();
888        if (accountId != Account.NO_ACCOUNT) {
889            mActivity.startActivity(Welcome.createOpenAccountInboxIntent(mActivity, accountId));
890        } else {
891            Welcome.actionStart(mActivity);
892
893        }
894        mActivity.finish();
895    }
896
897    protected final MessageOrderManager getMessageOrderManager() {
898        return mOrderManager;
899    }
900
901    /** Perform "auto-advance. */
902    protected final void doAutoAdvance() {
903        switch (Preferences.getPreferences(mActivity).getAutoAdvanceDirection()) {
904            case Preferences.AUTO_ADVANCE_NEWER:
905                if (moveToNewer()) return;
906                break;
907            case Preferences.AUTO_ADVANCE_OLDER:
908                if (moveToOlder()) return;
909                break;
910        }
911        if (isMessageViewInstalled()) { // We really should have the message view but just in case
912            // Go back to mailbox list.
913            // Use onBackPressed(), so we'll restore the message view state, such as scroll
914            // position.
915            // Also make sure to pass false to isSystemBackKey, so on two-pane we don't go back
916            // to the collapsed mode.
917            onBackPressed(true);
918        }
919    }
920
921    /**
922     * Subclass must implement it to enable/disable the newer/older buttons.
923     */
924    protected abstract void updateNavigationArrows();
925
926    protected final boolean moveToOlder() {
927        if ((mOrderManager != null) && mOrderManager.moveToOlder()) {
928            navigateToMessage(mOrderManager.getCurrentMessageId());
929            return true;
930        }
931        return false;
932    }
933
934    protected final boolean moveToNewer() {
935        if ((mOrderManager != null) && mOrderManager.moveToNewer()) {
936            navigateToMessage(mOrderManager.getCurrentMessageId());
937            return true;
938        }
939        return false;
940    }
941
942    /**
943     * Called when the user taps newer/older.  Subclass must implement it to open the specified
944     * message.
945     *
946     * It's a bit different from just showing the message view fragment; on one-pane we show the
947     * message view fragment but don't want to change back state.
948     */
949    protected abstract void navigateToMessage(long messageId);
950
951    /**
952     * Potentially create a new {@link MessageOrderManager}; if it's not already started or if
953     * the account has changed, and sync it to the current message.
954     */
955    private void updateMessageOrderManager() {
956        if (!isMessageViewInstalled()) {
957            return;
958        }
959        Preconditions.checkNotNull(mListContext);
960
961        final long mailboxId = mListContext.getMailboxId();
962        if (mOrderManager == null || mOrderManager.getMailboxId() != mailboxId) {
963            stopMessageOrderManager();
964            mOrderManager =
965                new MessageOrderManager(mActivity, mailboxId, mMessageOrderManagerCallback);
966        }
967        mOrderManager.moveTo(getMessageId());
968        updateNavigationArrows();
969    }
970
971    /**
972     * Stop {@link MessageOrderManager}.
973     */
974    protected final void stopMessageOrderManager() {
975        if (mOrderManager != null) {
976            mOrderManager.close();
977            mOrderManager = null;
978        }
979    }
980
981    private class MessageOrderManagerCallback implements MessageOrderManager.Callback {
982        @Override
983        public void onMessagesChanged() {
984            updateNavigationArrows();
985        }
986
987        @Override
988        public void onMessageNotFound() {
989            doAutoAdvance();
990        }
991    }
992
993    @Override
994    public String toString() {
995        return getClass().getSimpleName(); // Shown on logcat
996    }
997}
998