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