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