AbstractConversationViewFragment.java revision ce9372d8ee7daf9b05569e98f684f5b7e14f6a04
1/*
2 * Copyright (C) 2012 Google Inc.
3 * Licensed to The Android Open Source Project.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package com.android.mail.ui;
19
20import android.app.Activity;
21import android.app.Fragment;
22import android.app.LoaderManager;
23import android.content.Context;
24import android.content.Loader;
25import android.database.Cursor;
26import android.net.Uri;
27import android.os.Bundle;
28import android.os.Handler;
29import android.view.Menu;
30import android.view.MenuInflater;
31import android.view.MenuItem;
32
33import com.android.emailcommon.mail.Address;
34import com.android.mail.R;
35import com.android.mail.analytics.Analytics;
36import com.android.mail.browse.ConversationAccountController;
37import com.android.mail.browse.ConversationMessage;
38import com.android.mail.browse.ConversationViewHeader.ConversationViewHeaderCallbacks;
39import com.android.mail.browse.MessageCursor;
40import com.android.mail.browse.MessageCursor.ConversationController;
41import com.android.mail.content.ObjectCursor;
42import com.android.mail.content.ObjectCursorLoader;
43import com.android.mail.providers.Account;
44import com.android.mail.providers.AccountObserver;
45import com.android.mail.providers.Conversation;
46import com.android.mail.providers.Folder;
47import com.android.mail.providers.ListParams;
48import com.android.mail.providers.Settings;
49import com.android.mail.providers.UIProvider;
50import com.android.mail.providers.UIProvider.CursorStatus;
51import com.android.mail.utils.LogTag;
52import com.android.mail.utils.LogUtils;
53import com.android.mail.utils.Utils;
54
55import java.util.Arrays;
56import java.util.Collections;
57import java.util.HashMap;
58import java.util.Map;
59
60public abstract class AbstractConversationViewFragment extends Fragment implements
61        ConversationController, ConversationAccountController,
62        ConversationViewHeaderCallbacks {
63
64    protected static final String ARG_ACCOUNT = "account";
65    public static final String ARG_CONVERSATION = "conversation";
66    private static final String LOG_TAG = LogTag.getLogTag();
67    protected static final int MESSAGE_LOADER = 0;
68    protected static final int CONTACT_LOADER = 1;
69    public static final int ATTACHMENT_OPTION1_LOADER = 2;
70    protected ControllableActivity mActivity;
71    private final MessageLoaderCallbacks mMessageLoaderCallbacks = new MessageLoaderCallbacks();
72    private ContactLoaderCallbacks mContactLoaderCallbacks;
73    private MenuItem mChangeFoldersMenuItem;
74    protected Conversation mConversation;
75    protected String mBaseUri;
76    protected Account mAccount;
77
78    /**
79     * Must be instantiated in a derived class's onCreate.
80     */
81    protected AbstractConversationWebViewClient mWebViewClient;
82
83    /**
84     * Cache of email address strings to parsed Address objects.
85     * <p>
86     * Remember to synchronize on the map when reading or writing to this cache, because some
87     * instances use it off the UI thread (e.g. from WebView).
88     */
89    protected final Map<String, Address> mAddressCache = Collections.synchronizedMap(
90            new HashMap<String, Address>());
91    private MessageCursor mCursor;
92    private Context mContext;
93    /**
94     * A backwards-compatible version of {{@link #getUserVisibleHint()}. Like the framework flag,
95     * this flag is saved and restored.
96     */
97    private boolean mUserVisible;
98
99    private final Handler mHandler = new Handler();
100    /** True if we want to avoid marking the conversation as viewed and read. */
101    private boolean mSuppressMarkingViewed;
102    /**
103     * Parcelable state of the conversation view. Can safely be used without null checking any time
104     * after {@link #onCreate(Bundle)}.
105     */
106    protected ConversationViewState mViewState;
107
108    private boolean mIsDetached;
109
110    private boolean mHasConversationBeenTransformed;
111    private boolean mHasConversationTransformBeenReverted;
112
113    protected boolean mConversationSeen = false;
114
115    private final AccountObserver mAccountObserver = new AccountObserver() {
116        @Override
117        public void onChanged(Account newAccount) {
118            final Account oldAccount = mAccount;
119            mAccount = newAccount;
120            mWebViewClient.setAccount(mAccount);
121            onAccountChanged(newAccount, oldAccount);
122        }
123    };
124
125    private static final String BUNDLE_VIEW_STATE =
126            AbstractConversationViewFragment.class.getName() + "viewstate";
127    /**
128     * We save the user visible flag so the various transitions that occur during rotation do not
129     * cause unnecessary visibility change.
130     */
131    private static final String BUNDLE_USER_VISIBLE =
132            AbstractConversationViewFragment.class.getName() + "uservisible";
133
134    private static final String BUNDLE_DETACHED =
135            AbstractConversationViewFragment.class.getName() + "detached";
136
137    private static final String BUNDLE_KEY_HAS_CONVERSATION_BEEN_TRANSFORMED =
138            AbstractConversationViewFragment.class.getName() + "conversationtransformed";
139    private static final String BUNDLE_KEY_HAS_CONVERSATION_BEEN_REVERTED =
140            AbstractConversationViewFragment.class.getName() + "conversationreverted";
141
142    public static Bundle makeBasicArgs(Account account) {
143        Bundle args = new Bundle();
144        args.putParcelable(ARG_ACCOUNT, account);
145        return args;
146    }
147
148    /**
149     * Constructor needs to be public to handle orientation changes and activity
150     * lifecycle events.
151     */
152    public AbstractConversationViewFragment() {
153        super();
154    }
155
156    /**
157     * Subclasses must override, since this depends on how many messages are
158     * shown in the conversation view.
159     */
160    protected void markUnread() {
161        // Do not automatically mark this conversation viewed and read.
162        mSuppressMarkingViewed = true;
163    }
164
165    /**
166     * Subclasses must override this, since they may want to display a single or
167     * many messages related to this conversation.
168     */
169    protected abstract void onMessageCursorLoadFinished(
170            Loader<ObjectCursor<ConversationMessage>> loader,
171            MessageCursor newCursor, MessageCursor oldCursor);
172
173    /**
174     * Subclasses must override this, since they may want to display a single or
175     * many messages related to this conversation.
176     */
177    @Override
178    public abstract void onConversationViewHeaderHeightChange(int newHeight);
179
180    public abstract void onUserVisibleHintChanged();
181
182    /**
183     * Subclasses must override this.
184     */
185    protected abstract void onAccountChanged(Account newAccount, Account oldAccount);
186
187    @Override
188    public void onCreate(Bundle savedState) {
189        super.onCreate(savedState);
190
191        parseArguments();
192        setBaseUri();
193
194        LogUtils.d(LOG_TAG, "onCreate in ConversationViewFragment (this=%s)", this);
195        // Not really, we just want to get a crack to store a reference to the change_folder item
196        setHasOptionsMenu(true);
197
198        if (savedState != null) {
199            mViewState = savedState.getParcelable(BUNDLE_VIEW_STATE);
200            mUserVisible = savedState.getBoolean(BUNDLE_USER_VISIBLE);
201            mIsDetached = savedState.getBoolean(BUNDLE_DETACHED, false);
202            mHasConversationBeenTransformed =
203                    savedState.getBoolean(BUNDLE_KEY_HAS_CONVERSATION_BEEN_TRANSFORMED, false);
204            mHasConversationTransformBeenReverted =
205                    savedState.getBoolean(BUNDLE_KEY_HAS_CONVERSATION_BEEN_REVERTED, false);
206        } else {
207            mViewState = getNewViewState();
208            mHasConversationBeenTransformed = false;
209            mHasConversationTransformBeenReverted = false;
210        }
211    }
212
213    /**
214     * Can be overridden in case a subclass needs to get additional arguments.
215     */
216    protected void parseArguments() {
217        final Bundle args = getArguments();
218        mAccount = args.getParcelable(ARG_ACCOUNT);
219        mConversation = args.getParcelable(ARG_CONVERSATION);
220    }
221
222    /**
223     * Can be overridden in case a subclass needs a different uri format
224     * (such as one that does not rely on account and/or conversation.
225     */
226    protected void setBaseUri() {
227        mBaseUri = buildBaseUri(getContext(), mAccount, mConversation);
228    }
229
230    public static String buildBaseUri(Context context, Account account, Conversation conversation) {
231        // Since the uri specified in the conversation base uri may not be unique, we specify a
232        // base uri that us guaranteed to be unique for this conversation.
233        return "x-thread://" + account.getAccountId(context).hashCode() + "/" + conversation.id;
234    }
235
236    @Override
237    public String toString() {
238        // log extra info at DEBUG level or finer
239        final String s = super.toString();
240        if (!LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG) || mConversation == null) {
241            return s;
242        }
243        return "(" + s + " conv=" + mConversation + ")";
244    }
245
246    @Override
247    public void onActivityCreated(Bundle savedInstanceState) {
248        super.onActivityCreated(savedInstanceState);
249        final Activity activity = getActivity();
250        if (!(activity instanceof ControllableActivity)) {
251            LogUtils.wtf(LOG_TAG, "ConversationViewFragment expects only a ControllableActivity to"
252                    + "create it. Cannot proceed.");
253        }
254        if (activity == null || activity.isFinishing()) {
255            // Activity is finishing, just bail.
256            return;
257        }
258        mActivity = (ControllableActivity) activity;
259        mContext = activity.getApplicationContext();
260        mWebViewClient.setActivity(activity);
261        mAccount = mAccountObserver.initialize(mActivity.getAccountController());
262        mWebViewClient.setAccount(mAccount);
263    }
264
265    @Override
266    public ConversationUpdater getListController() {
267        final ControllableActivity activity = (ControllableActivity) getActivity();
268        return activity != null ? activity.getConversationUpdater() : null;
269    }
270
271    public Context getContext() {
272        return mContext;
273    }
274
275    @Override
276    public Conversation getConversation() {
277        return mConversation;
278    }
279
280    @Override
281    public MessageCursor getMessageCursor() {
282        return mCursor;
283    }
284
285    public Handler getHandler() {
286        return mHandler;
287    }
288
289    public MessageLoaderCallbacks getMessageLoaderCallbacks() {
290        return mMessageLoaderCallbacks;
291    }
292
293    public ContactLoaderCallbacks getContactInfoSource() {
294        if (mContactLoaderCallbacks == null) {
295            mContactLoaderCallbacks = mActivity.getContactLoaderCallbacks();
296        }
297        return mContactLoaderCallbacks;
298    }
299
300    @Override
301    public Account getAccount() {
302        return mAccount;
303    }
304
305    @Override
306    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
307        super.onCreateOptionsMenu(menu, inflater);
308        mChangeFoldersMenuItem = menu.findItem(R.id.change_folders);
309    }
310
311    @Override
312    public boolean onOptionsItemSelected(MenuItem item) {
313        if (!isUserVisible()) {
314            // Unclear how this is happening. Current theory is that this fragment was scheduled
315            // to be removed, but the remove transaction failed. When the Activity is later
316            // restored, the FragmentManager restores this fragment, but Fragment.mMenuVisible is
317            // stuck at its initial value (true), which makes this zombie fragment eligible for
318            // menu item clicks.
319            //
320            // Work around this by relying on the (properly restored) extra user visible hint.
321            LogUtils.e(LOG_TAG,
322                    "ACVF ignoring onOptionsItemSelected b/c userVisibleHint is false. f=%s", this);
323            if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
324                LogUtils.e(LOG_TAG, "%s", Utils.dumpFragment(this));
325            }
326            return false;
327        }
328
329        boolean handled = false;
330        final int itemId = item.getItemId();
331        if (itemId == R.id.inside_conversation_unread) {
332            markUnread();
333            handled = true;
334        } else if (itemId == R.id.show_original) {
335            showUntransformedConversation();
336            handled = true;
337        } else if (itemId == R.id.print_all) {
338            printConversation();
339            handled = true;
340        }
341        return handled;
342    }
343
344    @Override
345    public void onPrepareOptionsMenu(Menu menu) {
346        // Only show option if we support message transforms and message has been transformed.
347        Utils.setMenuItemVisibility(menu, R.id.show_original, supportsMessageTransforms() &&
348                mHasConversationBeenTransformed && !mHasConversationTransformBeenReverted);
349
350        final MenuItem printMenuItem = menu.findItem(R.id.print_all);
351        if (printMenuItem != null) {
352            // compute the visibility of the print menu item
353            printMenuItem.setVisible(Utils.isRunningKitkatOrLater() && shouldShowPrintInOverflow());
354
355            // compute the text displayed on the print menu item
356            if (mConversation.getNumMessages() == 1) {
357                printMenuItem.setTitle(R.string.print);
358            } else {
359                printMenuItem.setTitle(R.string.print_all);
360            }
361        }
362    }
363
364    abstract boolean supportsMessageTransforms();
365
366    // BEGIN conversation header callbacks
367    @Override
368    public void onFoldersClicked() {
369        if (mChangeFoldersMenuItem == null) {
370            LogUtils.e(LOG_TAG, "unable to open 'change folders' dialog for a conversation");
371            return;
372        }
373        mActivity.onOptionsItemSelected(mChangeFoldersMenuItem);
374    }
375    // END conversation header callbacks
376
377    @Override
378    public void onStart() {
379        super.onStart();
380
381        Analytics.getInstance().sendView(getClass().getName());
382    }
383
384    @Override
385    public void onSaveInstanceState(Bundle outState) {
386        if (mViewState != null) {
387            outState.putParcelable(BUNDLE_VIEW_STATE, mViewState);
388        }
389        outState.putBoolean(BUNDLE_USER_VISIBLE, mUserVisible);
390        outState.putBoolean(BUNDLE_DETACHED, mIsDetached);
391        outState.putBoolean(BUNDLE_KEY_HAS_CONVERSATION_BEEN_TRANSFORMED,
392                mHasConversationBeenTransformed);
393        outState.putBoolean(BUNDLE_KEY_HAS_CONVERSATION_BEEN_REVERTED,
394                mHasConversationTransformBeenReverted);
395    }
396
397    @Override
398    public void onDestroyView() {
399        super.onDestroyView();
400        mAccountObserver.unregisterAndDestroy();
401    }
402
403    /**
404     * {@link #setUserVisibleHint(boolean)} only works on API >= 15, so implement our own for
405     * reliability on older platforms.
406     */
407    public void setExtraUserVisibleHint(boolean isVisibleToUser) {
408        LogUtils.v(LOG_TAG, "in CVF.setHint, val=%s (%s)", isVisibleToUser, this);
409        if (mUserVisible != isVisibleToUser) {
410            mUserVisible = isVisibleToUser;
411            MessageCursor cursor = getMessageCursor();
412            if (mUserVisible && (cursor != null && cursor.isLoaded() && cursor.getCount() == 0)) {
413                // Pop back to conversation list and show error.
414                onError();
415                return;
416            }
417            onUserVisibleHintChanged();
418        }
419    }
420
421    public boolean isUserVisible() {
422        return mUserVisible;
423    }
424
425    protected void timerMark(String msg) {
426        if (isUserVisible()) {
427            Utils.sConvLoadTimer.mark(msg);
428        }
429    }
430
431    private class MessageLoaderCallbacks
432            implements LoaderManager.LoaderCallbacks<ObjectCursor<ConversationMessage>> {
433
434        @Override
435        public Loader<ObjectCursor<ConversationMessage>> onCreateLoader(int id, Bundle args) {
436            return new MessageLoader(mActivity.getActivityContext(), mConversation.messageListUri);
437        }
438
439        @Override
440        public void onLoadFinished(Loader<ObjectCursor<ConversationMessage>> loader,
441                    ObjectCursor<ConversationMessage> data) {
442            // ignore truly duplicate results
443            // this can happen when restoring after rotation
444            if (mCursor == data) {
445                return;
446            } else {
447                final MessageCursor messageCursor = (MessageCursor) data;
448
449                // bind the cursor to this fragment so it can access to the current list controller
450                messageCursor.setController(AbstractConversationViewFragment.this);
451
452                if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
453                    LogUtils.d(LOG_TAG, "LOADED CONVERSATION= %s", messageCursor.getDebugDump());
454                }
455
456                // We have no messages: exit conversation view.
457                if (messageCursor.getCount() == 0
458                        && (!CursorStatus.isWaitingForResults(messageCursor.getStatus())
459                                || mIsDetached)) {
460                    if (mUserVisible) {
461                        onError();
462                    } else {
463                        // we expect that the pager adapter will remove this
464                        // conversation fragment on its own due to a separate
465                        // conversation cursor update (we might get here if the
466                        // message list update fires first. nothing to do
467                        // because we expect to be torn down soon.)
468                        LogUtils.i(LOG_TAG, "CVF: offscreen conv has no messages, ignoring update"
469                                + " in anticipation of conv cursor update. c=%s",
470                                mConversation.uri);
471                    }
472                    // existing mCursor will imminently be closed, must stop referencing it
473                    // since we expect to be kicked out soon, it doesn't matter what mCursor
474                    // becomes
475                    mCursor = null;
476                    return;
477                }
478
479                // ignore cursors that are still loading results
480                if (!messageCursor.isLoaded()) {
481                    // existing mCursor will imminently be closed, must stop referencing it
482                    // in this case, the new cursor is also no good, and since don't expect to get
483                    // here except in initial load situations, it's safest to just ensure the
484                    // reference is null
485                    mCursor = null;
486                    return;
487                }
488                final MessageCursor oldCursor = mCursor;
489                mCursor = messageCursor;
490                onMessageCursorLoadFinished(loader, mCursor, oldCursor);
491            }
492        }
493
494        @Override
495        public void onLoaderReset(Loader<ObjectCursor<ConversationMessage>>  loader) {
496            mCursor = null;
497        }
498
499    }
500
501    private void onError() {
502        // need to exit this view- conversation may have been
503        // deleted, or for whatever reason is now invalid (e.g.
504        // discard single draft)
505        //
506        // N.B. this may involve a fragment transaction, which
507        // FragmentManager will refuse to execute directly
508        // within onLoadFinished. Make sure the controller knows.
509        LogUtils.i(LOG_TAG, "CVF: visible conv has no messages, exiting conv mode");
510        // TODO(mindyp): handle ERROR status by showing an error
511        // message to the user that there are no messages in
512        // this conversation
513        popOut();
514    }
515
516    private void popOut() {
517        mHandler.post(new FragmentRunnable("popOut", this) {
518            @Override
519            public void go() {
520                if (mActivity != null) {
521                    mActivity.getListHandler()
522                            .onConversationSelected(null, true /* inLoaderCallbacks */);
523                }
524            }
525        });
526    }
527
528    /**
529     * @see Folder#getTypeDescription()
530     */
531    protected String getCurrentFolderTypeDesc() {
532        final Folder currFolder;
533        if (mActivity != null) {
534            currFolder = mActivity.getFolderController().getFolder();
535        } else {
536            currFolder = null;
537        }
538        final String folderStr;
539        if (currFolder != null) {
540            folderStr = currFolder.getTypeDescription();
541        } else {
542            folderStr = "unknown_folder";
543        }
544        return folderStr;
545    }
546
547    private void logConversationView() {
548      final String folderStr = getCurrentFolderTypeDesc();
549      Analytics.getInstance().sendEvent("view_conversation", folderStr,
550              mConversation.isRemote ? "unsynced" : "synced", mConversation.getNumMessages());
551    }
552
553    protected void onConversationSeen() {
554        LogUtils.d(LOG_TAG, "AbstractConversationViewFragment#onConversationSeen()");
555
556        // Ignore unsafe calls made after a fragment is detached from an activity
557        final ControllableActivity activity = (ControllableActivity) getActivity();
558        if (activity == null) {
559            LogUtils.w(LOG_TAG, "ignoring onConversationSeen for conv=%s", mConversation.id);
560            return;
561        }
562
563        // this method is called 2x on rotation; debounce this a bit so as not to
564        // dramatically skew analytics data too much. Ideally, it should be called zero times
565        // on rotation...
566        if (!mConversationSeen) {
567            logConversationView();
568        }
569
570        mViewState.setInfoForConversation(mConversation);
571
572        LogUtils.d(LOG_TAG, "onConversationSeen() - mSuppressMarkingViewed = %b",
573                mSuppressMarkingViewed);
574        // In most circumstances we want to mark the conversation as viewed and read, since the
575        // user has read it.  However, if the user has already marked the conversation unread, we
576        // do not want a  later mark-read operation to undo this.  So we check this variable which
577        // is set in #markUnread() which suppresses automatic mark-read.
578        if (!mSuppressMarkingViewed) {
579            // mark viewed/read if not previously marked viewed by this conversation view,
580            // or if unread messages still exist in the message list cursor
581            // we don't want to keep marking viewed on rotation or restore
582            // but we do want future re-renders to mark read (e.g. "New message from X" case)
583            final MessageCursor cursor = getMessageCursor();
584            LogUtils.d(LOG_TAG, "onConversationSeen() - mConversation.isViewed() = %b, "
585                    + "cursor null = %b, cursor.isConversationRead() = %b",
586                    mConversation.isViewed(), cursor == null,
587                    cursor != null && cursor.isConversationRead());
588            if (!mConversation.isViewed() || (cursor != null && !cursor.isConversationRead())) {
589                // Mark the conversation viewed and read.
590                activity.getConversationUpdater()
591                        .markConversationsRead(Arrays.asList(mConversation), true, true);
592
593                // and update the Message objects in the cursor so the next time a cursor update
594                // happens with these messages marked read, we know to ignore it
595                if (cursor != null && !cursor.isClosed()) {
596                    cursor.markMessagesRead();
597                }
598            }
599        }
600        activity.getListHandler().onConversationSeen();
601
602        mConversationSeen = true;
603    }
604
605    protected ConversationViewState getNewViewState() {
606        return new ConversationViewState();
607    }
608
609    private static class MessageLoader extends ObjectCursorLoader<ConversationMessage> {
610        private boolean mDeliveredFirstResults = false;
611
612        public MessageLoader(Context c, Uri messageListUri) {
613            super(c, messageListUri, UIProvider.MESSAGE_PROJECTION, ConversationMessage.FACTORY);
614        }
615
616        @Override
617        public void deliverResult(ObjectCursor<ConversationMessage> result) {
618            // We want to deliver these results, and then we want to make sure
619            // that any subsequent
620            // queries do not hit the network
621            super.deliverResult(result);
622
623            if (!mDeliveredFirstResults) {
624                mDeliveredFirstResults = true;
625                Uri uri = getUri();
626
627                // Create a ListParams that tells the provider to not hit the
628                // network
629                final ListParams listParams = new ListParams(ListParams.NO_LIMIT,
630                        false /* useNetwork */);
631
632                // Build the new uri with this additional parameter
633                uri = uri
634                        .buildUpon()
635                        .appendQueryParameter(UIProvider.LIST_PARAMS_QUERY_PARAMETER,
636                                listParams.serialize()).build();
637                setUri(uri);
638            }
639        }
640
641        @Override
642        protected ObjectCursor<ConversationMessage> getObjectCursor(Cursor inner) {
643            return new MessageCursor(inner);
644        }
645    }
646
647    public abstract void onConversationUpdated(Conversation conversation);
648
649    public void onDetachedModeEntered() {
650        // If we have no messages, then we have nothing to display, so leave this view.
651        // Otherwise, just set the detached flag.
652        final Cursor messageCursor = getMessageCursor();
653
654        if (messageCursor == null || messageCursor.getCount() == 0) {
655            popOut();
656        } else {
657            mIsDetached = true;
658        }
659    }
660
661    /**
662     * Called when the JavaScript reports that it transformed a message.
663     * Sets a flag to true and invalidates the options menu so it will
664     * include the "Revert auto-sizing" menu option.
665     */
666    public void onConversationTransformed() {
667        mHasConversationBeenTransformed = true;
668        mHandler.post(new FragmentRunnable("invalidateOptionsMenu", this) {
669            @Override
670            public void go() {
671                mActivity.invalidateOptionsMenu();
672            }
673        });
674    }
675
676    /**
677     * Called when the "Revert auto-sizing" option is selected. Default
678     * implementation simply sets a value on whether transforms should be
679     * applied. Derived classes should override this class and force a
680     * re-render so that the conversation renders without
681     */
682    public void showUntransformedConversation() {
683        // must set the value to true so we don't show the options menu item again
684        mHasConversationTransformBeenReverted = true;
685    }
686
687    /**
688     * Returns {@code true} if the conversation should be transformed. {@code false}, otherwise.
689     * @return {@code true} if the conversation should be transformed. {@code false}, otherwise.
690     */
691    public boolean shouldApplyTransforms() {
692        return (mAccount.enableMessageTransforms > 0) &&
693                !mHasConversationTransformBeenReverted;
694    }
695
696    /**
697     * The Print item in the overflow menu of the Conversation view is shown based on the return
698     * from this method.
699     *
700     * @return {@code true} if the conversation can be printed; {@code false} otherwise.
701     */
702    protected abstract boolean shouldShowPrintInOverflow();
703
704    /**
705     * Prints all messages in the conversation.
706     */
707    protected abstract void printConversation();
708
709    public boolean shouldAlwaysShowImages() {
710        return (mAccount != null) && (mAccount.settings.showImages == Settings.ShowImages.ALWAYS);
711    }
712}
713