Home | History | Annotate | Download | only in activity
      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 
     17 package com.android.email.activity;
     18 
     19 import android.app.Activity;
     20 import android.app.Fragment;
     21 import android.app.FragmentManager;
     22 import android.app.FragmentTransaction;
     23 import android.os.Bundle;
     24 import android.util.Log;
     25 import android.view.Menu;
     26 import android.view.MenuInflater;
     27 import android.view.MenuItem;
     28 
     29 import com.android.email.Email;
     30 import com.android.email.FolderProperties;
     31 import com.android.email.MessageListContext;
     32 import com.android.email.Preferences;
     33 import com.android.email.R;
     34 import com.android.email.RefreshManager;
     35 import com.android.email.RequireManualSyncDialog;
     36 import com.android.email.activity.setup.AccountSettings;
     37 import com.android.email.activity.setup.MailboxSettings;
     38 import com.android.emailcommon.Logging;
     39 import com.android.emailcommon.provider.Account;
     40 import com.android.emailcommon.provider.EmailContent.Message;
     41 import com.android.emailcommon.provider.HostAuth;
     42 import com.android.emailcommon.provider.Mailbox;
     43 import com.android.emailcommon.utility.EmailAsyncTask;
     44 import com.android.emailcommon.utility.Utility;
     45 import com.google.common.base.Objects;
     46 import com.google.common.base.Preconditions;
     47 
     48 import java.util.LinkedList;
     49 import java.util.List;
     50 
     51 /**
     52  * Base class for the UI controller.
     53  */
     54 abstract 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