UIControllerBase.java revision 1ef8ec61c9e4d717c6d4dff6136f85f84c387856
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        if (isMessageListReady()) {
627            mActionBarController.enterSearchMode(null);
628        }
629    }
630
631    /**
632     * @return Whether or not a message list is ready and has its initial meta data loaded.
633     */
634    protected boolean isMessageListReady() {
635        return isMessageListInstalled() && getMessageListFragment().hasDataLoaded();
636    }
637
638    /**
639     * Determines the mailbox to search, if a search was to be initiated now.
640     * This will return {@code null} if the UI is not focused on any particular mailbox to search
641     * on.
642     */
643    private Mailbox getSearchableMailbox() {
644        if (!isMessageListReady()) {
645            return null;
646        }
647        MessageListFragment messageList = getMessageListFragment();
648
649        // If already in a search, future searches will search the original mailbox.
650        return mListContext.isSearch()
651                ? messageList.getSearchedMailbox()
652                : messageList.getMailbox();
653    }
654
655    // TODO: this logic probably needs to be tested in the backends as well, so it may be nice
656    // to consolidate this to a centralized place, so that they don't get out of sync.
657    /**
658     * @return whether or not this account should do a global search instead when a user
659     *     initiates a search on the given mailbox.
660     */
661    private static boolean shouldDoGlobalSearch(Account account, Mailbox mailbox) {
662        return ((account.mFlags & Account.FLAGS_SUPPORTS_GLOBAL_SEARCH) != 0)
663                && (mailbox.mType == Mailbox.TYPE_INBOX);
664    }
665
666    /**
667     * Retrieves the hint text to be shown for when a search entry is being made.
668     */
669    protected String getSearchHint() {
670        if (!isMessageListReady()) {
671            return "";
672        }
673        Account account = getMessageListFragment().getAccount();
674        Mailbox mailbox = getSearchableMailbox();
675
676        if (mailbox == null) {
677            return "";
678        }
679
680        if (shouldDoGlobalSearch(account, mailbox)) {
681            return mActivity.getString(R.string.search_hint);
682        }
683
684        // Regular mailbox, or IMAP - search within that mailbox.
685        String mailboxName = FolderProperties.getInstance(mActivity).getDisplayName(mailbox);
686        return String.format(
687                mActivity.getString(R.string.search_mailbox_hint),
688                mailboxName);
689    }
690
691    /**
692     * Kicks off a search query, if the UI is in a state where a search is possible.
693     */
694    protected void onSearchSubmit(final String queryTerm) {
695        final long accountId = getUIAccountId();
696        if (!Account.isNormalAccount(accountId)) {
697            return; // Invalid account to search from.
698        }
699
700        Mailbox searchableMailbox = getSearchableMailbox();
701        if (searchableMailbox == null) {
702            return;
703        }
704        final long mailboxId = searchableMailbox.mId;
705
706        if (Email.DEBUG) {
707            Log.d(Logging.LOG_TAG,
708                    "Submitting search: [" + queryTerm + "] in mailboxId=" + mailboxId);
709        }
710
711        mActivity.startActivity(EmailActivity.createSearchIntent(
712                mActivity, accountId, mailboxId, queryTerm));
713
714
715        // TODO: this causes a slight flicker.
716        // A new instance of the activity will sit on top. When the user exits search and
717        // returns to this activity, the search box should not be open then.
718        mActionBarController.exitSearchMode();
719    }
720
721    /**
722     * Handles exiting of search entry mode.
723     */
724    protected void onSearchExit() {
725        if ((mListContext != null) && mListContext.isSearch()) {
726            mActivity.finish();
727        }
728    }
729
730    /**
731     * Handles the {@link android.app.Activity#onCreateOptionsMenu} callback.
732     */
733    public boolean onCreateOptionsMenu(MenuInflater inflater, Menu menu) {
734        inflater.inflate(R.menu.email_activity_options, menu);
735        return true;
736    }
737
738    /**
739     * Handles the {@link android.app.Activity#onPrepareOptionsMenu} callback.
740     */
741    public boolean onPrepareOptionsMenu(MenuInflater inflater, Menu menu) {
742        // Update the refresh button.
743        MenuItem item = menu.findItem(R.id.refresh);
744        if (isRefreshEnabled()) {
745            item.setVisible(true);
746            mRefreshListener.setRefreshIcon(item);
747        } else {
748            item.setVisible(false);
749            mRefreshListener.setRefreshIcon(null);
750        }
751
752        // Deal with protocol-specific menu options.
753        boolean isEas = false;
754        boolean accountSearchable = false;
755        long accountId = getActualAccountId();
756        if (accountId > 0) {
757            Account account = Account.restoreAccountWithId(mActivity, accountId);
758            if (account != null) {
759                String protocol = account.getProtocol(mActivity);
760                if (HostAuth.SCHEME_EAS.equals(protocol)) {
761                    isEas = true;
762                }
763                accountSearchable = (account.mFlags & Account.FLAGS_SUPPORTS_SEARCH) != 0;
764            }
765        }
766
767        // TODO: Should use an isSyncable call to prevent drafts/outbox from allowing this
768        menu.findItem(R.id.search).setVisible(accountSearchable);
769        // TODO Show only for syncable mailbox as well.
770        menu.findItem(R.id.mailbox_settings).setVisible(isEas
771                && (getMailboxSettingsMailboxId() != Mailbox.NO_MAILBOX));
772
773        return true;
774    }
775
776    /**
777     * Handles the {@link android.app.Activity#onOptionsItemSelected} callback.
778     *
779     * @return true if the option item is handled.
780     */
781    public boolean onOptionsItemSelected(MenuItem item) {
782        switch (item.getItemId()) {
783            case android.R.id.home:
784                // Comes from the action bar when the app icon on the left is pressed.
785                // It works like a back press, but it won't close the activity.
786                return onBackPressed(false);
787            case R.id.compose:
788                return onCompose();
789            case R.id.refresh:
790                onRefresh();
791                return true;
792            case R.id.account_settings:
793                return onAccountSettings();
794            case R.id.search:
795                onSearchRequested();
796                return true;
797            case R.id.mailbox_settings:
798                final long mailboxId = getMailboxSettingsMailboxId();
799                if (mailboxId != Mailbox.NO_MAILBOX) {
800                    MailboxSettings.start(mActivity, mailboxId);
801                }
802                return true;
803        }
804        return false;
805    }
806
807    /**
808     * Opens the message compose activity.
809     */
810    private boolean onCompose() {
811        if (!isAccountSelected()) {
812            return false; // this shouldn't really happen
813        }
814        MessageCompose.actionCompose(mActivity, getActualAccountId());
815        return true;
816    }
817
818    /**
819     * Handles the "Settings" option item.  Opens the settings activity.
820     */
821    private boolean onAccountSettings() {
822        AccountSettings.actionSettings(mActivity, getActualAccountId());
823        return true;
824    }
825
826    /**
827     * @return the ID of the message in focus and visible, if any. Returns
828     *     {@link Message#NO_MESSAGE} if no message is opened.
829     */
830    protected long getMessageId() {
831        return isMessageViewInstalled()
832                ? getMessageViewFragment().getMessageId()
833                : Message.NO_MESSAGE;
834    }
835
836
837    /**
838     * @return mailbox ID for "mailbox settings" option.
839     */
840    protected abstract long getMailboxSettingsMailboxId();
841
842    /**
843     * Performs "refesh".
844     */
845    protected abstract void onRefresh();
846
847    /**
848     * @return true if refresh is in progress for the current mailbox.
849     */
850    protected abstract boolean isRefreshInProgress();
851
852    /**
853     * @return true if the UI should enable the "refresh" command.
854     */
855    protected abstract boolean isRefreshEnabled();
856
857    /**
858     * Refresh the action bar and menu items, including the "refreshing" icon.
859     */
860    protected void refreshActionBar() {
861        if (mActionBarController != null) {
862            mActionBarController.refresh();
863        }
864        mActivity.invalidateOptionsMenu();
865    }
866
867    // MessageListFragment.Callback
868    @Override
869    public void onMailboxNotFound() {
870        // Something bad happened - the account or mailbox we were looking for was deleted.
871        // Just restart and let the entry flow find a good default view.
872        Utility.showToast(mActivity, R.string.toast_mailbox_not_found);
873        Welcome.actionStart(mActivity);
874        mActivity.finish();
875    }
876
877    protected final MessageOrderManager getMessageOrderManager() {
878        return mOrderManager;
879    }
880
881    /** Perform "auto-advance. */
882    protected final void doAutoAdvance() {
883        switch (Preferences.getPreferences(mActivity).getAutoAdvanceDirection()) {
884            case Preferences.AUTO_ADVANCE_NEWER:
885                if (moveToNewer()) return;
886                break;
887            case Preferences.AUTO_ADVANCE_OLDER:
888                if (moveToOlder()) return;
889                break;
890        }
891        if (isMessageViewInstalled()) { // We really should have the message view but just in case
892            // Go back to mailbox list.
893            // Use onBackPressed(), so we'll restore the message view state, such as scroll
894            // position.
895            // Also make sure to pass false to isSystemBackKey, so on two-pane we don't go back
896            // to the collapsed mode.
897            onBackPressed(true);
898        }
899    }
900
901    /**
902     * Subclass must implement it to enable/disable the newer/older buttons.
903     */
904    protected abstract void updateNavigationArrows();
905
906    protected final boolean moveToOlder() {
907        if ((mOrderManager != null) && mOrderManager.moveToOlder()) {
908            navigateToMessage(mOrderManager.getCurrentMessageId());
909            return true;
910        }
911        return false;
912    }
913
914    protected final boolean moveToNewer() {
915        if ((mOrderManager != null) && mOrderManager.moveToNewer()) {
916            navigateToMessage(mOrderManager.getCurrentMessageId());
917            return true;
918        }
919        return false;
920    }
921
922    /**
923     * Called when the user taps newer/older.  Subclass must implement it to open the specified
924     * message.
925     *
926     * It's a bit different from just showing the message view fragment; on one-pane we show the
927     * message view fragment but don't want to change back state.
928     */
929    protected abstract void navigateToMessage(long messageId);
930
931    /**
932     * Potentially create a new {@link MessageOrderManager}; if it's not already started or if
933     * the account has changed, and sync it to the current message.
934     */
935    private void updateMessageOrderManager() {
936        if (!isMessageViewInstalled()) {
937            return;
938        }
939        Preconditions.checkNotNull(mListContext);
940
941        final long mailboxId = mListContext.getMailboxId();
942        if (mOrderManager == null || mOrderManager.getMailboxId() != mailboxId) {
943            stopMessageOrderManager();
944            mOrderManager =
945                new MessageOrderManager(mActivity, mailboxId, mMessageOrderManagerCallback);
946        }
947        mOrderManager.moveTo(getMessageId());
948        updateNavigationArrows();
949    }
950
951    /**
952     * Stop {@link MessageOrderManager}.
953     */
954    protected final void stopMessageOrderManager() {
955        if (mOrderManager != null) {
956            mOrderManager.close();
957            mOrderManager = null;
958        }
959    }
960
961    private class MessageOrderManagerCallback implements MessageOrderManager.Callback {
962        @Override
963        public void onMessagesChanged() {
964            updateNavigationArrows();
965        }
966
967        @Override
968        public void onMessageNotFound() {
969            doAutoAdvance();
970        }
971    }
972
973    @Override
974    public String toString() {
975        return getClass().getSimpleName(); // Shown on logcat
976    }
977}
978