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