/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.email.activity;

import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;

import com.android.email.Email;
import com.android.email.FolderProperties;
import com.android.email.MessageListContext;
import com.android.email.Preferences;
import com.android.email.R;
import com.android.email.RefreshManager;
import com.android.email.RequireManualSyncDialog;
import com.android.email.activity.setup.AccountSettings;
import com.android.email.activity.setup.MailboxSettings;
import com.android.emailcommon.Logging;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent.Message;
import com.android.emailcommon.provider.HostAuth;
import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.utility.EmailAsyncTask;
import com.android.emailcommon.utility.Utility;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;

import java.util.LinkedList;
import java.util.List;

/**
 * Base class for the UI controller.
 */
abstract class UIControllerBase implements MailboxListFragment.Callback,
        MessageListFragment.Callback, MessageViewFragment.Callback  {
    static final boolean DEBUG_FRAGMENTS = false; // DO NOT SUBMIT WITH TRUE

    static final String KEY_LIST_CONTEXT = "UIControllerBase.listContext";

    /** The owner activity */
    final EmailActivity mActivity;
    final FragmentManager mFragmentManager;

    protected final ActionBarController mActionBarController;

    private MessageOrderManager mOrderManager;
    private final MessageOrderManagerCallback mMessageOrderManagerCallback =
            new MessageOrderManagerCallback();

    final EmailAsyncTask.Tracker mTaskTracker = new EmailAsyncTask.Tracker();

    final RefreshManager mRefreshManager;

    /**
     * Fragments that are installed.
     *
     * A fragment is installed in {@link Fragment#onActivityCreated} and uninstalled in
     * {@link Fragment#onDestroyView}, using {@link FragmentInstallable} callbacks.
     *
     * This means fragments in the back stack are *not* installed.
     *
     * We set callbacks to fragments only when they are installed.
     *
     * @see FragmentInstallable
     */
    private MailboxListFragment mMailboxListFragment;
    private MessageListFragment mMessageListFragment;
    private MessageViewFragment mMessageViewFragment;

    /**
     * To avoid double-deleting a fragment (which will cause a runtime exception),
     * we put a fragment in this list when we {@link FragmentTransaction#remove(Fragment)} it,
     * and remove from the list when we actually uninstall it.
     */
    private final List<Fragment> mRemovedFragments = new LinkedList<Fragment>();

    /**
     * The NfcHandler implements Near Field Communication sharing features
     * whenever the activity is in the foreground.
     */
    private NfcHandler mNfcHandler;

    /**
     * The active context for the current MessageList.
     * In some UI layouts such as the one-pane view, the message list may not be visible, but is
     * on the backstack. This list context will still be accessible in those cases.
     *
     * Should be set using {@link #setListContext(MessageListContext)}.
     */
    protected MessageListContext mListContext;

    private class RefreshListener implements RefreshManager.Listener {
        private MenuItem mRefreshIcon;

        @Override
        public void onMessagingError(final long accountId, long mailboxId, final String message) {
            updateRefreshIcon();
        }

        @Override
        public void onRefreshStatusChanged(long accountId, long mailboxId) {
            updateRefreshIcon();
        }

        void setRefreshIcon(MenuItem icon) {
            mRefreshIcon = icon;
            updateRefreshIcon();
        }

        private void updateRefreshIcon() {
            if (mRefreshIcon == null) {
                return;
            }

            if (isRefreshInProgress()) {
                mRefreshIcon.setActionView(R.layout.action_bar_indeterminate_progress);
            } else {
                mRefreshIcon.setActionView(null);
            }
        }
    };

    protected final RefreshListener mRefreshListener = new RefreshListener();

    public UIControllerBase(EmailActivity activity) {
        mActivity = activity;
        mFragmentManager = activity.getFragmentManager();
        mRefreshManager = RefreshManager.getInstance(mActivity);
        mActionBarController = createActionBarController(activity);
        if (DEBUG_FRAGMENTS) {
            FragmentManager.enableDebugLogging(true);
        }
    }

    /**
     * Called by the base class to let a subclass create an {@link ActionBarController}.
     */
    protected abstract ActionBarController createActionBarController(Activity activity);

    /** @return the layout ID for the activity. */
    public abstract int getLayoutId();

    /**
     * Must be called just after the activity sets up the content view.  Used to initialize views.
     *
     * (Due to the complexity regarding class/activity initialization order, we can't do this in
     * the constructor.)
     */
    public void onActivityViewReady() {
        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
            Log.d(Logging.LOG_TAG, this + " onActivityViewReady");
        }
    }

    /**
     * Called at the end of {@link EmailActivity#onCreate}.
     */
    public void onActivityCreated() {
        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
            Log.d(Logging.LOG_TAG, this + " onActivityCreated");
        }
        mRefreshManager.registerListener(mRefreshListener);
        mActionBarController.onActivityCreated();
        mNfcHandler = NfcHandler.register(this, mActivity);
    }

    /**
     * Handles the {@link android.app.Activity#onStart} callback.
     */
    public void onActivityStart() {
        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
            Log.d(Logging.LOG_TAG, this + " onActivityStart");
        }
        if (isMessageViewInstalled()) {
            updateMessageOrderManager();
        }
    }

    /**
     * Handles the {@link android.app.Activity#onResume} callback.
     */
    public void onActivityResume() {
        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
            Log.d(Logging.LOG_TAG, this + " onActivityResume");
        }
        refreshActionBar();
        if (mNfcHandler != null) {
            mNfcHandler.onAccountChanged();  // workaround for email not set on initial load
        }
        long accountId = getUIAccountId();
        Preferences.getPreferences(mActivity).setLastUsedAccountId(accountId);
        showAccountSpecificWarning(accountId);
    }

    /**
     * Handles the {@link android.app.Activity#onPause} callback.
     */
    public void onActivityPause() {
        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
            Log.d(Logging.LOG_TAG, this + " onActivityPause");
        }
    }

    /**
     * Handles the {@link android.app.Activity#onStop} callback.
     */
    public void onActivityStop() {
        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
            Log.d(Logging.LOG_TAG, this + " onActivityStop");
        }
        stopMessageOrderManager();
    }

    /**
     * Handles the {@link android.app.Activity#onDestroy} callback.
     */
    public void onActivityDestroy() {
        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
            Log.d(Logging.LOG_TAG, this + " onActivityDestroy");
        }
        mActionBarController.onActivityDestroy();
        mRefreshManager.unregisterListener(mRefreshListener);
        mTaskTracker.cancellAllInterrupt();
    }

    /**
     * Handles the {@link android.app.Activity#onSaveInstanceState} callback.
     */
    public void onSaveInstanceState(Bundle outState) {
        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
            Log.d(Logging.LOG_TAG, this + " onSaveInstanceState");
        }
        mActionBarController.onSaveInstanceState(outState);
        outState.putParcelable(KEY_LIST_CONTEXT, mListContext);
    }

    /**
     * Handles the {@link android.app.Activity#onRestoreInstanceState} callback.
     */
    public void onRestoreInstanceState(Bundle savedInstanceState) {
        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
            Log.d(Logging.LOG_TAG, this + " restoreInstanceState");
        }
        mActionBarController.onRestoreInstanceState(savedInstanceState);
        mListContext = savedInstanceState.getParcelable(KEY_LIST_CONTEXT);
    }

    // MessageViewFragment$Callback
    @Override
    public void onMessageSetUnread() {
        doAutoAdvance();
    }

    // MessageViewFragment$Callback
    @Override
    public void onMessageNotExists() {
        doAutoAdvance();
    }

    // MessageViewFragment$Callback
    @Override
    public void onRespondedToInvite(int response) {
        doAutoAdvance();
    }

    // MessageViewFragment$Callback
    @Override
    public void onBeforeMessageGone() {
        doAutoAdvance();
    }

    /**
     * Install a fragment.  Must be caleld from the host activity's
     * {@link FragmentInstallable#onInstallFragment}.
     */
    public final void onInstallFragment(Fragment fragment) {
        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
            Log.d(Logging.LOG_TAG, this + " onInstallFragment  fragment=" + fragment);
        }
        if (fragment instanceof MailboxListFragment) {
            installMailboxListFragment((MailboxListFragment) fragment);
        } else if (fragment instanceof MessageListFragment) {
            installMessageListFragment((MessageListFragment) fragment);
        } else if (fragment instanceof MessageViewFragment) {
            installMessageViewFragment((MessageViewFragment) fragment);
        } else {
            throw new IllegalArgumentException("Tried to install unknown fragment");
        }
    }

    /** Install fragment */
    protected void installMailboxListFragment(MailboxListFragment fragment) {
        mMailboxListFragment = fragment;
        mMailboxListFragment.setCallback(this);

        // TODO: consolidate this refresh with the one that the Fragment itself does. since
        // the fragment calls setHasOptionsMenu(true) - it invalidates when it gets attached.
        // However the timing is slightly different and leads to a delay in update if this isn't
        // here - investigate why. same for the other installs.
        refreshActionBar();
    }

    /** Install fragment */
    protected void installMessageListFragment(MessageListFragment fragment) {
        mMessageListFragment = fragment;
        mMessageListFragment.setCallback(this);
        refreshActionBar();
    }

    /** Install fragment */
    protected void installMessageViewFragment(MessageViewFragment fragment) {
        mMessageViewFragment = fragment;
        mMessageViewFragment.setCallback(this);

        updateMessageOrderManager();
        refreshActionBar();
    }

    /**
     * Uninstall a fragment.  Must be caleld from the host activity's
     * {@link FragmentInstallable#onUninstallFragment}.
     */
    public final void onUninstallFragment(Fragment fragment) {
        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
            Log.d(Logging.LOG_TAG, this + " onUninstallFragment  fragment=" + fragment);
        }
        mRemovedFragments.remove(fragment);
        if (fragment == mMailboxListFragment) {
            uninstallMailboxListFragment();
        } else if (fragment == mMessageListFragment) {
            uninstallMessageListFragment();
        } else if (fragment == mMessageViewFragment) {
            uninstallMessageViewFragment();
        } else {
            throw new IllegalArgumentException("Tried to uninstall unknown fragment");
        }
    }

    /** Uninstall {@link MailboxListFragment} */
    protected void uninstallMailboxListFragment() {
        mMailboxListFragment.setCallback(null);
        mMailboxListFragment = null;
    }

    /** Uninstall {@link MessageListFragment} */
    protected void uninstallMessageListFragment() {
        mMessageListFragment.setCallback(null);
        mMessageListFragment = null;
    }

    /** Uninstall {@link MessageViewFragment} */
    protected void uninstallMessageViewFragment() {
        mMessageViewFragment.setCallback(null);
        mMessageViewFragment = null;
    }

    /**
     * If a {@link Fragment} is not already in {@link #mRemovedFragments},
     * {@link FragmentTransaction#remove} it and add to the list.
     *
     * Do nothing if {@code fragment} is null.
     */
    protected final void removeFragment(FragmentTransaction ft, Fragment fragment) {
        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
            Log.d(Logging.LOG_TAG, this + " removeFragment fragment=" + fragment);
        }
        if (fragment == null) {
            return;
        }
        if (!mRemovedFragments.contains(fragment)) {
            // Remove try/catch when b/4981556 is fixed (framework bug)
            try {
                ft.remove(fragment);
            } catch (IllegalStateException ex) {
                Log.e(Logging.LOG_TAG, "Swalling IllegalStateException due to known bug for "
                        + " fragment: " + fragment, ex);
                Log.e(Logging.LOG_TAG, Utility.dumpFragment(fragment));
            }
            addFragmentToRemovalList(fragment);
        }
    }

    /**
     * Remove a {@link Fragment} from {@link #mRemovedFragments}.  No-op if {@code fragment} is
     * null.
     *
     * {@link #removeMailboxListFragment}, {@link #removeMessageListFragment} and
     * {@link #removeMessageViewFragment} all call this, so subclasses don't have to do this when
     * using them.
     *
     * However, unfortunately, subclasses have to call this manually when popping from the
     * back stack to avoid double-delete.
     */
    protected void addFragmentToRemovalList(Fragment fragment) {
        if (fragment != null) {
            mRemovedFragments.add(fragment);
        }
    }

    /**
     * Remove the fragment if it's installed.
     */
    protected FragmentTransaction removeMailboxListFragment(FragmentTransaction ft) {
        removeFragment(ft, mMailboxListFragment);
        return ft;
    }

    /**
     * Remove the fragment if it's installed.
     */
    protected FragmentTransaction removeMessageListFragment(FragmentTransaction ft) {
        removeFragment(ft, mMessageListFragment);
        return ft;
    }

    /**
     * Remove the fragment if it's installed.
     */
    protected FragmentTransaction removeMessageViewFragment(FragmentTransaction ft) {
        removeFragment(ft, mMessageViewFragment);
        return ft;
    }

    /** @return true if a {@link MailboxListFragment} is installed. */
    protected final boolean isMailboxListInstalled() {
        return mMailboxListFragment != null;
    }

    /** @return true if a {@link MessageListFragment} is installed. */
    protected final boolean isMessageListInstalled() {
        return mMessageListFragment != null;
    }

    /** @return true if a {@link MessageViewFragment} is installed. */
    protected final boolean isMessageViewInstalled() {
        return mMessageViewFragment != null;
    }

    /** @return the installed {@link MailboxListFragment} or null. */
    protected final MailboxListFragment getMailboxListFragment() {
        return mMailboxListFragment;
    }

    /** @return the installed {@link MessageListFragment} or null. */
    protected final MessageListFragment getMessageListFragment() {
        return mMessageListFragment;
    }

    /** @return the installed {@link MessageViewFragment} or null. */
    protected final MessageViewFragment getMessageViewFragment() {
        return mMessageViewFragment;
    }

    /**
     * Commit a {@link FragmentTransaction}.
     */
    protected void commitFragmentTransaction(FragmentTransaction ft) {
        if (DEBUG_FRAGMENTS) {
            Log.d(Logging.LOG_TAG, this + " commitFragmentTransaction: " + ft);
        }
        if (!ft.isEmpty()) {
            // NB: there should be no cases in which a transaction is committed after
            // onSaveInstanceState. Unfortunately, the "state loss" check also happens when in
            // LoaderCallbacks.onLoadFinished, and we wish to perform transactions there. The check
            // by the framework is conservative and prevents cases where there are transactions
            // affecting Loader lifecycles - but we have no such cases.
            // TODO: use asynchronous callbacks from loaders to avoid this implicit dependency
            ft.commitAllowingStateLoss();
            mFragmentManager.executePendingTransactions();
        }
    }

    /**
     * @return the currently selected account ID, *or* {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
     *
     * @see #getActualAccountId()
     */
    public abstract long getUIAccountId();

    /**
     * @return true if an account is selected, or the current view is the combined view.
     */
    public final boolean isAccountSelected() {
        return getUIAccountId() != Account.NO_ACCOUNT;
    }

    /**
     * @return if an actual account is selected.  (i.e. {@link Account#ACCOUNT_ID_COMBINED_VIEW}
     * is not considered "actual".s)
     */
    public final boolean isActualAccountSelected() {
        return isAccountSelected() && (getUIAccountId() != Account.ACCOUNT_ID_COMBINED_VIEW);
    }

    /**
     * @return the currently selected account ID.  If the current view is the combined view,
     * it'll return {@link Account#NO_ACCOUNT}.
     *
     * @see #getUIAccountId()
     */
    public final long getActualAccountId() {
        return isActualAccountSelected() ? getUIAccountId() : Account.NO_ACCOUNT;
    }

    /**
     * Show the default view for the given account.
     *
     * @param accountId ID of the account to load.  Can be {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
     *     Must never be {@link Account#NO_ACCOUNT}.
     * @param forceShowInbox If {@code false} and the given account is already selected, do nothing.
     *        If {@code false}, we always change the view even if the account is selected.
     */
    public final void switchAccount(long accountId, boolean forceShowInbox) {

        if (Account.isSecurityHold(mActivity, accountId)) {
            ActivityHelper.showSecurityHoldDialog(mActivity, accountId);
            mActivity.finish();
            return;
        }

        if (accountId == getUIAccountId() && !forceShowInbox) {
            // Do nothing if the account is already selected.  Not even going back to the inbox.
            return;
        }
        if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
            openMailbox(accountId, Mailbox.QUERY_ALL_INBOXES);
        } else {
            long inboxId = Mailbox.findMailboxOfType(mActivity, accountId, Mailbox.TYPE_INBOX);
            if (inboxId == Mailbox.NO_MAILBOX) {
                // The account doesn't have Inbox yet... Redirect to Welcome and let it wait for
                // the initial sync...
                Log.w(Logging.LOG_TAG, "Account " + accountId +" doesn't have Inbox.  Redirecting"
                        + " to Welcome...");
                Welcome.actionOpenAccountInbox(mActivity, accountId);
                mActivity.finish();
            } else {
                openMailbox(accountId, inboxId);
            }
        }
        if (mNfcHandler != null) {
            mNfcHandler.onAccountChanged();
        }
        Preferences.getPreferences(mActivity).setLastUsedAccountId(accountId);
        showAccountSpecificWarning(accountId);
    }

    /**
     * Returns the id of the parent mailbox used for the mailbox list fragment.
     *
     * IMPORTANT: Do not confuse {@link #getMailboxListMailboxId()} with
     *     {@link #getMessageListMailboxId()}
     */
    protected long getMailboxListMailboxId() {
        return isMailboxListInstalled() ? getMailboxListFragment().getSelectedMailboxId()
                : Mailbox.NO_MAILBOX;
    }

    /**
     * Returns the id of the mailbox used for the message list fragment.
     *
     * IMPORTANT: Do not confuse {@link #getMailboxListMailboxId()} with
     *     {@link #getMessageListMailboxId()}
     */
    protected long getMessageListMailboxId() {
        return isMessageListInstalled() ? getMessageListFragment().getMailboxId()
                : Mailbox.NO_MAILBOX;
    }

    /**
     * Shortcut for {@link #open} with {@link Message#NO_MESSAGE}.
     */
    protected final void openMailbox(long accountId, long mailboxId) {
        open(MessageListContext.forMailbox(accountId, mailboxId), Message.NO_MESSAGE);
    }

    /**
     * Opens a given list
     * @param listContext the list context for the message list to open
     * @param messageId if specified and not {@link Message#NO_MESSAGE}, will open the message
     *     in the message list.
     */
    public final void open(final MessageListContext listContext, final long messageId) {
        setListContext(listContext);
        openInternal(listContext, messageId);

        if (listContext.isSearch()) {
            mActionBarController.enterSearchMode(listContext.getSearchParams().mFilter);
        }
    }

    /**
     * Sets the internal value of the list context for the message list.
     */
    protected void setListContext(MessageListContext listContext) {
        if (Objects.equal(listContext, mListContext)) {
            return;
        }

        if (Email.DEBUG && Logging.DEBUG_LIFECYCLE) {
            Log.i(Logging.LOG_TAG, this + " setListContext: " + listContext);
        }
        mListContext = listContext;
    }

    protected abstract void openInternal(
            final MessageListContext listContext, final long messageId);

    /**
     * Performs the back action.
     *
     * @param isSystemBackKey <code>true</code> if the system back key was pressed.
     * <code>false</code> if it's caused by the "home" icon click on the action bar.
     */
    public abstract boolean onBackPressed(boolean isSystemBackKey);

    public void onSearchStarted() {
        // Show/hide the original search icon.
        mActivity.invalidateOptionsMenu();
    }

    /**
     * Must be called from {@link Activity#onSearchRequested()}.
     * This initiates the search entry mode - see {@link #onSearchSubmit} for when the search
     * is actually submitted.
     */
    public void onSearchRequested() {
        long accountId = getActualAccountId();
        boolean accountSearchable = false;
        if (accountId > 0) {
            Account account = Account.restoreAccountWithId(mActivity, accountId);
            if (account != null) {
                String protocol = account.getProtocol(mActivity);
                accountSearchable = (account.mFlags & Account.FLAGS_SUPPORTS_SEARCH) != 0;
            }
        }

        if (!accountSearchable) {
            return;
        }

        if (isMessageListReady()) {
            mActionBarController.enterSearchMode(null);
        }
    }

    /**
     * @return Whether or not a message list is ready and has its initial meta data loaded.
     */
    protected boolean isMessageListReady() {
        return isMessageListInstalled() && getMessageListFragment().hasDataLoaded();
    }

    /**
     * Determines the mailbox to search, if a search was to be initiated now.
     * This will return {@code null} if the UI is not focused on any particular mailbox to search
     * on.
     */
    private Mailbox getSearchableMailbox() {
        if (!isMessageListReady()) {
            return null;
        }
        MessageListFragment messageList = getMessageListFragment();

        // If already in a search, future searches will search the original mailbox.
        return mListContext.isSearch()
                ? messageList.getSearchedMailbox()
                : messageList.getMailbox();
    }

    // TODO: this logic probably needs to be tested in the backends as well, so it may be nice
    // to consolidate this to a centralized place, so that they don't get out of sync.
    /**
     * @return whether or not this account should do a global search instead when a user
     *     initiates a search on the given mailbox.
     */
    private static boolean shouldDoGlobalSearch(Account account, Mailbox mailbox) {
        return ((account.mFlags & Account.FLAGS_SUPPORTS_GLOBAL_SEARCH) != 0)
                && (mailbox.mType == Mailbox.TYPE_INBOX);
    }

    /**
     * Retrieves the hint text to be shown for when a search entry is being made.
     */
    protected String getSearchHint() {
        if (!isMessageListReady()) {
            return "";
        }
        Account account = getMessageListFragment().getAccount();
        Mailbox mailbox = getSearchableMailbox();

        if (mailbox == null) {
            return "";
        }

        if (shouldDoGlobalSearch(account, mailbox)) {
            return mActivity.getString(R.string.search_hint);
        }

        // Regular mailbox, or IMAP - search within that mailbox.
        String mailboxName = FolderProperties.getInstance(mActivity).getDisplayName(mailbox);
        return String.format(
                mActivity.getString(R.string.search_mailbox_hint),
                mailboxName);
    }

    /**
     * Kicks off a search query, if the UI is in a state where a search is possible.
     */
    protected void onSearchSubmit(final String queryTerm) {
        final long accountId = getUIAccountId();
        if (!Account.isNormalAccount(accountId)) {
            return; // Invalid account to search from.
        }

        Mailbox searchableMailbox = getSearchableMailbox();
        if (searchableMailbox == null) {
            return;
        }
        final long mailboxId = searchableMailbox.mId;

        if (Email.DEBUG) {
            Log.d(Logging.LOG_TAG,
                    "Submitting search: [" + queryTerm + "] in mailboxId=" + mailboxId);
        }

        mActivity.startActivity(EmailActivity.createSearchIntent(
                mActivity, accountId, mailboxId, queryTerm));


        // TODO: this causes a slight flicker.
        // A new instance of the activity will sit on top. When the user exits search and
        // returns to this activity, the search box should not be open then.
        mActionBarController.exitSearchMode();
    }

    /**
     * Handles exiting of search entry mode.
     */
    protected void onSearchExit() {
        if ((mListContext != null) && mListContext.isSearch()) {
            mActivity.finish();
        } else {
            // Re show the search icon.
            mActivity.invalidateOptionsMenu();
        }
    }

    /**
     * Handles the {@link android.app.Activity#onCreateOptionsMenu} callback.
     */
    public boolean onCreateOptionsMenu(MenuInflater inflater, Menu menu) {
        inflater.inflate(R.menu.email_activity_options, menu);
        return true;
    }

    /**
     * Handles the {@link android.app.Activity#onPrepareOptionsMenu} callback.
     */
    public boolean onPrepareOptionsMenu(MenuInflater inflater, Menu menu) {
        // Update the refresh button.
        MenuItem item = menu.findItem(R.id.refresh);
        if (item != null) {
            if (isRefreshEnabled()) {
                item.setVisible(true);
                mRefreshListener.setRefreshIcon(item);
            } else {
                item.setVisible(false);
                mRefreshListener.setRefreshIcon(null);
            }
        }

        // Deal with protocol-specific menu options.
        boolean mailboxHasServerCounterpart = false;
        boolean accountSearchable = false;
        boolean isEas = false;

        if (isMessageListReady()) {
            long accountId = getActualAccountId();
            if (accountId > 0) {
                Account account = Account.restoreAccountWithId(mActivity, accountId);
                if (account != null) {
                    String protocol = account.getProtocol(mActivity);
                    isEas = HostAuth.SCHEME_EAS.equals(protocol);
                    Mailbox mailbox = getMessageListFragment().getMailbox();
                    mailboxHasServerCounterpart = (mailbox != null)
                            && mailbox.loadsFromServer(protocol);
                    accountSearchable = (account.mFlags & Account.FLAGS_SUPPORTS_SEARCH) != 0;
                }
            }
        }

        boolean showSearchIcon = !mActionBarController.isInSearchMode()
                && accountSearchable && mailboxHasServerCounterpart;

        MenuItem search = menu.findItem(R.id.search);
        if (search != null) {
            search.setVisible(showSearchIcon);
        }
        MenuItem settings = menu.findItem(R.id.mailbox_settings);
        if (settings != null) {
            settings.setVisible(isEas && mailboxHasServerCounterpart);
        }
        return true;
    }

    /**
     * Handles the {@link android.app.Activity#onOptionsItemSelected} callback.
     *
     * @return true if the option item is handled.
     */
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case android.R.id.home:
                // Comes from the action bar when the app icon on the left is pressed.
                // It works like a back press, but it won't close the activity.
                return onBackPressed(false);
            case R.id.compose:
                return onCompose();
            case R.id.refresh:
                onRefresh();
                return true;
            case R.id.account_settings:
                return onAccountSettings();
            case R.id.search:
                onSearchRequested();
                return true;
            case R.id.mailbox_settings:
                final long mailboxId = getMailboxSettingsMailboxId();
                if (mailboxId != Mailbox.NO_MAILBOX) {
                    MailboxSettings.start(mActivity, mailboxId);
                }
                return true;
        }
        return false;
    }

    /**
     * Opens the message compose activity.
     */
    private boolean onCompose() {
        if (!isAccountSelected()) {
            return false; // this shouldn't really happen
        }
        MessageCompose.actionCompose(mActivity, getActualAccountId());
        return true;
    }

    /**
     * Handles the "Settings" option item.  Opens the settings activity.
     */
    private boolean onAccountSettings() {
        AccountSettings.actionSettings(mActivity, getActualAccountId());
        return true;
    }

    /**
     * @return the ID of the message in focus and visible, if any. Returns
     *     {@link Message#NO_MESSAGE} if no message is opened.
     */
    protected long getMessageId() {
        return isMessageViewInstalled()
                ? getMessageViewFragment().getMessageId()
                : Message.NO_MESSAGE;
    }


    /**
     * @return mailbox ID for "mailbox settings" option.
     */
    protected abstract long getMailboxSettingsMailboxId();

    /**
     * Performs "refesh".
     */
    protected abstract void onRefresh();

    /**
     * @return true if refresh is in progress for the current mailbox.
     */
    protected abstract boolean isRefreshInProgress();

    /**
     * @return true if the UI should enable the "refresh" command.
     */
    protected abstract boolean isRefreshEnabled();

    /**
     * Refresh the action bar and menu items, including the "refreshing" icon.
     */
    protected void refreshActionBar() {
        if (mActionBarController != null) {
            mActionBarController.refresh();
        }
        mActivity.invalidateOptionsMenu();
    }

    // MessageListFragment.Callback
    @Override
    public void onMailboxNotFound(boolean isFirstLoad) {
        // Something bad happened - the account or mailbox we were looking for was deleted.
        // Just restart and let the entry flow find a good default view.
        if (isFirstLoad) {
            // Only show this if it's the first load (e.g. a shortcut) rather an a return to
            // a mailbox (which might be in a just-deleted account)
            Utility.showToast(mActivity, R.string.toast_mailbox_not_found);
        }
        long accountId = getUIAccountId();
        if (accountId != Account.NO_ACCOUNT) {
            mActivity.startActivity(Welcome.createOpenAccountInboxIntent(mActivity, accountId));
        } else {
            Welcome.actionStart(mActivity);

        }
        mActivity.finish();
    }

    protected final MessageOrderManager getMessageOrderManager() {
        return mOrderManager;
    }

    /** Perform "auto-advance. */
    protected final void doAutoAdvance() {
        switch (Preferences.getPreferences(mActivity).getAutoAdvanceDirection()) {
            case Preferences.AUTO_ADVANCE_NEWER:
                if (moveToNewer()) return;
                break;
            case Preferences.AUTO_ADVANCE_OLDER:
                if (moveToOlder()) return;
                break;
        }
        if (isMessageViewInstalled()) { // We really should have the message view but just in case
            // Go back to mailbox list.
            // Use onBackPressed(), so we'll restore the message view state, such as scroll
            // position.
            // Also make sure to pass false to isSystemBackKey, so on two-pane we don't go back
            // to the collapsed mode.
            onBackPressed(true);
        }
    }

    /**
     * Subclass must implement it to enable/disable the newer/older buttons.
     */
    protected abstract void updateNavigationArrows();

    protected final boolean moveToOlder() {
        if ((mOrderManager != null) && mOrderManager.moveToOlder()) {
            navigateToMessage(mOrderManager.getCurrentMessageId());
            return true;
        }
        return false;
    }

    protected final boolean moveToNewer() {
        if ((mOrderManager != null) && mOrderManager.moveToNewer()) {
            navigateToMessage(mOrderManager.getCurrentMessageId());
            return true;
        }
        return false;
    }

    /**
     * Called when the user taps newer/older.  Subclass must implement it to open the specified
     * message.
     *
     * It's a bit different from just showing the message view fragment; on one-pane we show the
     * message view fragment but don't want to change back state.
     */
    protected abstract void navigateToMessage(long messageId);

    /**
     * Potentially create a new {@link MessageOrderManager}; if it's not already started or if
     * the account has changed, and sync it to the current message.
     */
    private void updateMessageOrderManager() {
        if (!isMessageViewInstalled()) {
            return;
        }
        Preconditions.checkNotNull(mListContext);

        if (mOrderManager == null || !mOrderManager.getListContext().equals(mListContext)) {
            stopMessageOrderManager();
            mOrderManager = new MessageOrderManager(
                    mActivity, mListContext, mMessageOrderManagerCallback);
        }
        mOrderManager.moveTo(getMessageId());
        updateNavigationArrows();
    }

    /**
     * Stop {@link MessageOrderManager}.
     */
    protected final void stopMessageOrderManager() {
        if (mOrderManager != null) {
            mOrderManager.close();
            mOrderManager = null;
        }
    }

    private class MessageOrderManagerCallback implements MessageOrderManager.Callback {
        @Override
        public void onMessagesChanged() {
            updateNavigationArrows();
        }

        @Override
        public void onMessageNotFound() {
            doAutoAdvance();
        }
    }


    private void showAccountSpecificWarning(long accountId) {
        if (accountId != Account.NO_ACCOUNT && accountId != Account.NO_ACCOUNT) {
            Account account = Account.restoreAccountWithId(mActivity, accountId);
            if (account != null &&
                    Preferences.getPreferences(mActivity)
                    .shouldShowRequireManualSync(mActivity, account)) {
                new RequireManualSyncDialog(mActivity, account).show();
            }
        }
    }

    @Override
    public String toString() {
        return getClass().getSimpleName(); // Shown on logcat
    }
}
