/*
 * Copyright (C) 2012 Google Inc.
 * Licensed to 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.mail.ui;

import android.app.Activity;
import android.app.Fragment;
import android.app.LoaderManager;
import android.content.Context;
import android.content.Loader;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.Nullable;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;

import com.android.emailcommon.mail.Address;
import com.android.mail.R;
import com.android.mail.analytics.Analytics;
import com.android.mail.browse.ConversationAccountController;
import com.android.mail.browse.ConversationMessage;
import com.android.mail.browse.ConversationViewHeader.ConversationViewHeaderCallbacks;
import com.android.mail.browse.MessageCursor;
import com.android.mail.browse.MessageCursor.ConversationController;
import com.android.mail.content.ObjectCursor;
import com.android.mail.content.ObjectCursorLoader;
import com.android.mail.providers.Account;
import com.android.mail.providers.AccountObserver;
import com.android.mail.providers.Conversation;
import com.android.mail.providers.Folder;
import com.android.mail.providers.ListParams;
import com.android.mail.providers.Settings;
import com.android.mail.providers.UIProvider;
import com.android.mail.providers.UIProvider.CursorStatus;
import com.android.mail.utils.LogTag;
import com.android.mail.utils.LogUtils;
import com.android.mail.utils.Utils;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public abstract class AbstractConversationViewFragment extends Fragment implements
        ConversationController, ConversationAccountController,
        ConversationViewHeaderCallbacks {

    protected static final String ARG_ACCOUNT = "account";
    public static final String ARG_CONVERSATION = "conversation";
    private static final String LOG_TAG = LogTag.getLogTag();
    protected static final int MESSAGE_LOADER = 0;
    protected static final int CONTACT_LOADER = 1;
    public static final int ATTACHMENT_OPTION1_LOADER = 2;
    protected ControllableActivity mActivity;
    private final MessageLoaderCallbacks mMessageLoaderCallbacks = new MessageLoaderCallbacks();
    private ContactLoaderCallbacks mContactLoaderCallbacks;
    private MenuItem mChangeFoldersMenuItem;
    protected Conversation mConversation;
    protected String mBaseUri;
    protected Account mAccount;

    /**
     * Must be instantiated in a derived class's onCreate.
     */
    protected AbstractConversationWebViewClient mWebViewClient;

    /**
     * Cache of email address strings to parsed Address objects.
     * <p>
     * Remember to synchronize on the map when reading or writing to this cache, because some
     * instances use it off the UI thread (e.g. from WebView).
     */
    protected final Map<String, Address> mAddressCache = Collections.synchronizedMap(
            new HashMap<String, Address>());
    private MessageCursor mCursor;
    private Context mContext;
    /**
     * A backwards-compatible version of {{@link #getUserVisibleHint()}. Like the framework flag,
     * this flag is saved and restored.
     */
    private boolean mUserVisible;

    private final Handler mHandler = new Handler();
    /** True if we want to avoid marking the conversation as viewed and read. */
    private boolean mSuppressMarkingViewed;
    /**
     * Parcelable state of the conversation view. Can safely be used without null checking any time
     * after {@link #onCreate(Bundle)}.
     */
    protected ConversationViewState mViewState;

    private boolean mIsDetached;

    private boolean mHasConversationBeenTransformed;
    private boolean mHasConversationTransformBeenReverted;

    protected boolean mConversationSeen = false;

    private final AccountObserver mAccountObserver = new AccountObserver() {
        @Override
        public void onChanged(Account newAccount) {
            final Account oldAccount = mAccount;
            mAccount = newAccount;
            mWebViewClient.setAccount(mAccount);
            onAccountChanged(newAccount, oldAccount);
        }
    };

    private static final String BUNDLE_VIEW_STATE =
            AbstractConversationViewFragment.class.getName() + "viewstate";
    /**
     * We save the user visible flag so the various transitions that occur during rotation do not
     * cause unnecessary visibility change.
     */
    private static final String BUNDLE_USER_VISIBLE =
            AbstractConversationViewFragment.class.getName() + "uservisible";

    private static final String BUNDLE_DETACHED =
            AbstractConversationViewFragment.class.getName() + "detached";

    private static final String BUNDLE_KEY_HAS_CONVERSATION_BEEN_TRANSFORMED =
            AbstractConversationViewFragment.class.getName() + "conversationtransformed";
    private static final String BUNDLE_KEY_HAS_CONVERSATION_BEEN_REVERTED =
            AbstractConversationViewFragment.class.getName() + "conversationreverted";

    public static Bundle makeBasicArgs(Account account) {
        Bundle args = new Bundle();
        args.putParcelable(ARG_ACCOUNT, account);
        return args;
    }

    /**
     * Constructor needs to be public to handle orientation changes and activity
     * lifecycle events.
     */
    public AbstractConversationViewFragment() {
        super();
    }

    /**
     * Subclasses must override, since this depends on how many messages are
     * shown in the conversation view.
     */
    protected void markUnread() {
        // Do not automatically mark this conversation viewed and read.
        mSuppressMarkingViewed = true;
    }

    /**
     * Marks a conversation either 'seen' (force=false), as in when the conversation is made visible
     * and should be marked read, or 'read' (force=true), as in when the action bar menu item to
     * mark this conversation read is selected.
     *
     * @param force true to force marking it read, false to allow peek mode to prevent it
     */
    private final void markRead(boolean force) {
        final ControllableActivity activity = (ControllableActivity) getActivity();
        if (activity == null) {
            return;
        }

        // mark viewed/read if not previously marked viewed by this conversation view,
        // or if unread messages still exist in the message list cursor
        // we don't want to keep marking viewed on rotation or restore
        // but we do want future re-renders to mark read (e.g. "New message from X" case)
        final MessageCursor cursor = getMessageCursor();
        LogUtils.d(LOG_TAG, "onConversationSeen() - mConversation.isViewed() = %b, "
                + "cursor null = %b, cursor.isConversationRead() = %b",
                mConversation.isViewed(), cursor == null,
                cursor != null && cursor.isConversationRead());
        if (!mConversation.isViewed() || (cursor != null && !cursor.isConversationRead())) {
            // Mark the conversation read no matter what if force=true.
            // else only mark it seen if appropriate (2-pane peek=true doesn't mark things seen)
            final boolean convMarkedRead;
            if (force) {
                activity.getConversationUpdater()
                        .markConversationsRead(Arrays.asList(mConversation), true /* read */,
                                true /* viewed */);
                convMarkedRead = true;
            } else {
                convMarkedRead = activity.getConversationUpdater()
                        .markConversationSeen(mConversation);
            }

            // and update the Message objects in the cursor so the next time a cursor update
            // happens with these messages marked read, we know to ignore it
            if (convMarkedRead && cursor != null && !cursor.isClosed()) {
                cursor.markMessagesRead();
            }
        }
    }

    /**
     * Subclasses must override this, since they may want to display a single or
     * many messages related to this conversation.
     */
    protected abstract void onMessageCursorLoadFinished(
            Loader<ObjectCursor<ConversationMessage>> loader,
            MessageCursor newCursor, MessageCursor oldCursor);

    /**
     * Subclasses must override this, since they may want to display a single or
     * many messages related to this conversation.
     */
    @Override
    public abstract void onConversationViewHeaderHeightChange(int newHeight);

    public abstract void onUserVisibleHintChanged();

    /**
     * Subclasses must override this.
     */
    protected abstract void onAccountChanged(Account newAccount, Account oldAccount);

    @Override
    public void onCreate(Bundle savedState) {
        super.onCreate(savedState);

        parseArguments();
        setBaseUri();

        LogUtils.d(LOG_TAG, "onCreate in ConversationViewFragment (this=%s)", this);
        // Not really, we just want to get a crack to store a reference to the change_folder item
        setHasOptionsMenu(true);

        if (savedState != null) {
            mViewState = savedState.getParcelable(BUNDLE_VIEW_STATE);
            mUserVisible = savedState.getBoolean(BUNDLE_USER_VISIBLE);
            mIsDetached = savedState.getBoolean(BUNDLE_DETACHED, false);
            mHasConversationBeenTransformed =
                    savedState.getBoolean(BUNDLE_KEY_HAS_CONVERSATION_BEEN_TRANSFORMED, false);
            mHasConversationTransformBeenReverted =
                    savedState.getBoolean(BUNDLE_KEY_HAS_CONVERSATION_BEEN_REVERTED, false);
        } else {
            mViewState = getNewViewState();
            mHasConversationBeenTransformed = false;
            mHasConversationTransformBeenReverted = false;
        }
    }

    /**
     * Can be overridden in case a subclass needs to get additional arguments.
     */
    protected void parseArguments() {
        final Bundle args = getArguments();
        mAccount = args.getParcelable(ARG_ACCOUNT);
        mConversation = args.getParcelable(ARG_CONVERSATION);
    }

    /**
     * Can be overridden in case a subclass needs a different uri format
     * (such as one that does not rely on account and/or conversation.
     */
    protected void setBaseUri() {
        mBaseUri = buildBaseUri(getContext(), mAccount, mConversation);
    }

    public static String buildBaseUri(Context context, Account account, Conversation conversation) {
        // Since the uri specified in the conversation base uri may not be unique, we specify a
        // base uri that us guaranteed to be unique for this conversation.
        return "x-thread://" + account.getAccountId().hashCode() + "/" + conversation.id;
    }

    @Override
    public String toString() {
        // log extra info at DEBUG level or finer
        final String s = super.toString();
        if (!LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG) || mConversation == null) {
            return s;
        }
        return "(" + s + " conv=" + mConversation + ")";
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        final Activity activity = getActivity();
        if (!(activity instanceof ControllableActivity)) {
            LogUtils.wtf(LOG_TAG, "ConversationViewFragment expects only a ControllableActivity to"
                    + "create it. Cannot proceed.");
        }
        if (activity == null || activity.isFinishing()) {
            // Activity is finishing, just bail.
            return;
        }
        mActivity = (ControllableActivity) activity;
        mContext = activity.getApplicationContext();
        mWebViewClient.setActivity(activity);
        mAccount = mAccountObserver.initialize(mActivity.getAccountController());
        mWebViewClient.setAccount(mAccount);
    }

    @Override
    public ConversationUpdater getListController() {
        final ControllableActivity activity = (ControllableActivity) getActivity();
        return activity != null ? activity.getConversationUpdater() : null;
    }

    public Context getContext() {
        return mContext;
    }

    @Override
    public Conversation getConversation() {
        return mConversation;
    }

    @Override
    public @Nullable MessageCursor getMessageCursor() {
        return mCursor;
    }

    public Handler getHandler() {
        return mHandler;
    }

    public MessageLoaderCallbacks getMessageLoaderCallbacks() {
        return mMessageLoaderCallbacks;
    }

    public ContactLoaderCallbacks getContactInfoSource() {
        if (mContactLoaderCallbacks == null) {
            mContactLoaderCallbacks = mActivity.getContactLoaderCallbacks();
        }
        return mContactLoaderCallbacks;
    }

    @Override
    public Account getAccount() {
        return mAccount;
    }

    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        super.onCreateOptionsMenu(menu, inflater);
        mChangeFoldersMenuItem = menu.findItem(R.id.change_folders);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if (!isUserVisible()) {
            // Unclear how this is happening. Current theory is that this fragment was scheduled
            // to be removed, but the remove transaction failed. When the Activity is later
            // restored, the FragmentManager restores this fragment, but Fragment.mMenuVisible is
            // stuck at its initial value (true), which makes this zombie fragment eligible for
            // menu item clicks.
            //
            // Work around this by relying on the (properly restored) extra user visible hint.
            LogUtils.e(LOG_TAG,
                    "ACVF ignoring onOptionsItemSelected b/c userVisibleHint is false. f=%s", this);
            if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
                LogUtils.e(LOG_TAG, "%s", Utils.dumpFragment(this));
            }
            return false;
        }

        boolean handled = true;
        final int itemId = item.getItemId();
        if (itemId == R.id.inside_conversation_unread || itemId == R.id.toggle_read_unread) {
            markUnread();
        } else if (itemId == R.id.read) {
            markRead(true /* force */);
            mActivity.supportInvalidateOptionsMenu();
        } else if (itemId == R.id.show_original) {
            showUntransformedConversation();
        } else if (itemId == R.id.print_all) {
            printConversation();
        } else if (itemId == R.id.reply) {
            handleReply();
        } else if (itemId == R.id.reply_all) {
            handleReplyAll();
        } else {
            handled = false;
        }
        return handled;
    }

    @Override
    public void onPrepareOptionsMenu(Menu menu) {
        // Only show option if we support message transforms and message has been transformed.
        Utils.setMenuItemPresent(menu, R.id.show_original, supportsMessageTransforms() &&
                mHasConversationBeenTransformed && !mHasConversationTransformBeenReverted);

        final MenuItem printMenuItem = menu.findItem(R.id.print_all);
        if (printMenuItem != null) {
            // compute the visibility of the print menu item
            printMenuItem.setVisible(Utils.isRunningKitkatOrLater() && shouldShowPrintInOverflow());

            // compute the text displayed on the print menu item
            if (mConversation.getNumMessages() == 1) {
                printMenuItem.setTitle(R.string.print);
            } else {
                printMenuItem.setTitle(R.string.print_all);
            }
        }
    }

    abstract boolean supportsMessageTransforms();

    // BEGIN conversation header callbacks
    @Override
    public void onFoldersClicked() {
        if (mChangeFoldersMenuItem == null) {
            LogUtils.e(LOG_TAG, "unable to open 'change folders' dialog for a conversation");
            return;
        }
        mActivity.onOptionsItemSelected(mChangeFoldersMenuItem);
    }
    // END conversation header callbacks

    @Override
    public void onStart() {
        super.onStart();

        Analytics.getInstance().sendView(getClass().getName());
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        if (mViewState != null) {
            outState.putParcelable(BUNDLE_VIEW_STATE, mViewState);
        }
        outState.putBoolean(BUNDLE_USER_VISIBLE, mUserVisible);
        outState.putBoolean(BUNDLE_DETACHED, mIsDetached);
        outState.putBoolean(BUNDLE_KEY_HAS_CONVERSATION_BEEN_TRANSFORMED,
                mHasConversationBeenTransformed);
        outState.putBoolean(BUNDLE_KEY_HAS_CONVERSATION_BEEN_REVERTED,
                mHasConversationTransformBeenReverted);
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        mAccountObserver.unregisterAndDestroy();
    }

    /**
     * {@link #setUserVisibleHint(boolean)} only works on API >= 15, so implement our own for
     * reliability on older platforms.
     */
    public void setExtraUserVisibleHint(boolean isVisibleToUser) {
        LogUtils.v(LOG_TAG, "in CVF.setHint, val=%s (%s)", isVisibleToUser, this);
        if (mUserVisible != isVisibleToUser) {
            mUserVisible = isVisibleToUser;
            MessageCursor cursor = getMessageCursor();
            if (mUserVisible && (cursor != null && cursor.isLoaded() && cursor.getCount() == 0)) {
                // Pop back to conversation list and show error.
                onError();
                return;
            }
            onUserVisibleHintChanged();
        }
    }

    public boolean isUserVisible() {
        return mUserVisible;
    }

    protected void timerMark(String msg) {
        if (isUserVisible()) {
            Utils.sConvLoadTimer.mark(msg);
        }
    }

    private class MessageLoaderCallbacks
            implements LoaderManager.LoaderCallbacks<ObjectCursor<ConversationMessage>> {

        @Override
        public Loader<ObjectCursor<ConversationMessage>> onCreateLoader(int id, Bundle args) {
            return new MessageLoader(mActivity.getActivityContext(), mConversation.messageListUri);
        }

        @Override
        public void onLoadFinished(Loader<ObjectCursor<ConversationMessage>> loader,
                    ObjectCursor<ConversationMessage> data) {
            // ignore truly duplicate results
            // this can happen when restoring after rotation
            if (mCursor == data) {
                return;
            } else {
                final MessageCursor messageCursor = (MessageCursor) data;

                // bind the cursor to this fragment so it can access to the current list controller
                messageCursor.setController(AbstractConversationViewFragment.this);

                if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
                    LogUtils.d(LOG_TAG, "LOADED CONVERSATION= %s", messageCursor.getDebugDump());
                }

                // We have no messages: exit conversation view.
                if (messageCursor.getCount() == 0
                        && (!CursorStatus.isWaitingForResults(messageCursor.getStatus())
                                || mIsDetached)) {
                    if (mUserVisible) {
                        onError();
                    } else {
                        // we expect that the pager adapter will remove this
                        // conversation fragment on its own due to a separate
                        // conversation cursor update (we might get here if the
                        // message list update fires first. nothing to do
                        // because we expect to be torn down soon.)
                        LogUtils.i(LOG_TAG, "CVF: offscreen conv has no messages, ignoring update"
                                + " in anticipation of conv cursor update. c=%s",
                                mConversation.uri);
                    }
                    // existing mCursor will imminently be closed, must stop referencing it
                    // since we expect to be kicked out soon, it doesn't matter what mCursor
                    // becomes
                    mCursor = null;
                    return;
                }

                // ignore cursors that are still loading results
                if (!messageCursor.isLoaded()) {
                    // existing mCursor will imminently be closed, must stop referencing it
                    // in this case, the new cursor is also no good, and since don't expect to get
                    // here except in initial load situations, it's safest to just ensure the
                    // reference is null
                    mCursor = null;
                    return;
                }
                final MessageCursor oldCursor = mCursor;
                mCursor = messageCursor;
                onMessageCursorLoadFinished(loader, mCursor, oldCursor);
            }
        }

        @Override
        public void onLoaderReset(Loader<ObjectCursor<ConversationMessage>>  loader) {
            mCursor = null;
        }

    }

    private void onError() {
        // need to exit this view- conversation may have been
        // deleted, or for whatever reason is now invalid (e.g.
        // discard single draft)
        //
        // N.B. this may involve a fragment transaction, which
        // FragmentManager will refuse to execute directly
        // within onLoadFinished. Make sure the controller knows.
        LogUtils.i(LOG_TAG, "CVF: visible conv has no messages, exiting conv mode");
        // TODO(mindyp): handle ERROR status by showing an error
        // message to the user that there are no messages in
        // this conversation
        popOut();
    }

    private void popOut() {
        mHandler.post(new FragmentRunnable("popOut", this) {
            @Override
            public void go() {
                if (mActivity != null) {
                    mActivity.getListHandler()
                            .onConversationSelected(null, true /* inLoaderCallbacks */);
                }
            }
        });
    }

    /**
     * @see Folder#getTypeDescription()
     */
    protected String getCurrentFolderTypeDesc() {
        final Folder currFolder;
        if (mActivity != null) {
            currFolder = mActivity.getFolderController().getFolder();
        } else {
            currFolder = null;
        }
        final String folderStr;
        if (currFolder != null) {
            folderStr = currFolder.getTypeDescription();
        } else {
            folderStr = "unknown_folder";
        }
        return folderStr;
    }

    private void logConversationView() {
      final String folderStr = getCurrentFolderTypeDesc();
      Analytics.getInstance().sendEvent("view_conversation", folderStr,
              mConversation.isRemote ? "unsynced" : "synced", mConversation.getNumMessages());
    }

    protected final void onConversationSeen() {
        LogUtils.d(LOG_TAG, "AbstractConversationViewFragment#onConversationSeen()");

        // Ignore unsafe calls made after a fragment is detached from an activity
        final ControllableActivity activity = (ControllableActivity) getActivity();
        if (activity == null) {
            LogUtils.w(LOG_TAG, "ignoring onConversationSeen for conv=%s", mConversation.id);
            return;
        }

        // this method is called 2x on rotation; debounce this a bit so as not to
        // dramatically skew analytics data too much. Ideally, it should be called zero times
        // on rotation...
        if (!mConversationSeen) {
            logConversationView();
        }

        mViewState.setInfoForConversation(mConversation);

        LogUtils.d(LOG_TAG, "onConversationSeen() - mSuppressMarkingViewed = %b",
                mSuppressMarkingViewed);
        // In most circumstances we want to mark the conversation as viewed and read, since the
        // user has read it.  However, if the user has already marked the conversation unread, we
        // do not want a  later mark-read operation to undo this.  So we check this variable which
        // is set in #markUnread() which suppresses automatic mark-read.
        if (!mSuppressMarkingViewed) {
            markRead(false /* force */);
        }
        activity.getListHandler().onConversationSeen();

        mConversationSeen = true;
    }

    protected ConversationViewState getNewViewState() {
        return new ConversationViewState();
    }

    private static class MessageLoader extends ObjectCursorLoader<ConversationMessage> {
        private boolean mDeliveredFirstResults = false;

        public MessageLoader(Context c, Uri messageListUri) {
            super(c, messageListUri, UIProvider.MESSAGE_PROJECTION, ConversationMessage.FACTORY);
        }

        @Override
        public void deliverResult(ObjectCursor<ConversationMessage> result) {
            // We want to deliver these results, and then we want to make sure
            // that any subsequent
            // queries do not hit the network
            super.deliverResult(result);

            if (!mDeliveredFirstResults) {
                mDeliveredFirstResults = true;
                Uri uri = getUri();

                // Create a ListParams that tells the provider to not hit the
                // network
                final ListParams listParams = new ListParams(ListParams.NO_LIMIT,
                        false /* useNetwork */);

                // Build the new uri with this additional parameter
                uri = uri
                        .buildUpon()
                        .appendQueryParameter(UIProvider.LIST_PARAMS_QUERY_PARAMETER,
                                listParams.serialize()).build();
                setUri(uri);
            }
        }

        @Override
        protected ObjectCursor<ConversationMessage> getObjectCursor(Cursor inner) {
            return new MessageCursor(inner);
        }
    }

    public abstract void onConversationUpdated(Conversation conversation);

    public void onDetachedModeEntered() {
        // If we have no messages, then we have nothing to display, so leave this view.
        // Otherwise, just set the detached flag.
        final Cursor messageCursor = getMessageCursor();

        if (messageCursor == null || messageCursor.getCount() == 0) {
            popOut();
        } else {
            mIsDetached = true;
        }
    }

    /**
     * Called when the JavaScript reports that it transformed a message.
     * Sets a flag to true and invalidates the options menu so it will
     * include the "Revert auto-sizing" menu option.
     */
    public void onConversationTransformed() {
        mHasConversationBeenTransformed = true;
        mHandler.post(new FragmentRunnable("invalidateOptionsMenu", this) {
            @Override
            public void go() {
                mActivity.supportInvalidateOptionsMenu();
            }
        });
    }

    /**
     * Called when the "Revert auto-sizing" option is selected. Default
     * implementation simply sets a value on whether transforms should be
     * applied. Derived classes should override this class and force a
     * re-render so that the conversation renders without
     */
    public void showUntransformedConversation() {
        // must set the value to true so we don't show the options menu item again
        mHasConversationTransformBeenReverted = true;
    }

    /**
     * Returns {@code true} if the conversation should be transformed. {@code false}, otherwise.
     * @return {@code true} if the conversation should be transformed. {@code false}, otherwise.
     */
    public boolean shouldApplyTransforms() {
        return (mAccount.enableMessageTransforms > 0) &&
                !mHasConversationTransformBeenReverted;
    }

    /**
     * The Print item in the overflow menu of the Conversation view is shown based on the return
     * from this method.
     *
     * @return {@code true} if the conversation can be printed; {@code false} otherwise.
     */
    protected abstract boolean shouldShowPrintInOverflow();

    /**
     * Prints all messages in the conversation.
     */
    protected abstract void printConversation();

    // These methods should perform default reply/replyall action on the last message.
    protected abstract void handleReply();
    protected abstract void handleReplyAll();

    public boolean shouldAlwaysShowImages() {
        return (mAccount != null) && (mAccount.settings.showImages == Settings.ShowImages.ALWAYS);
    }
}
