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.content.ContentResolver;
     21 import android.content.Context;
     22 import android.content.Loader;
     23 import android.content.res.Resources;
     24 import android.database.Cursor;
     25 import android.database.DataSetObserver;
     26 import android.graphics.Rect;
     27 import android.net.Uri;
     28 import android.os.AsyncTask;
     29 import android.os.Bundle;
     30 import android.os.SystemClock;
     31 import android.support.annotation.IdRes;
     32 import android.support.annotation.Nullable;
     33 import android.support.v4.text.BidiFormatter;
     34 import android.support.v4.util.ArrayMap;
     35 import android.text.TextUtils;
     36 import android.view.KeyEvent;
     37 import android.view.LayoutInflater;
     38 import android.view.View;
     39 import android.view.View.OnLayoutChangeListener;
     40 import android.view.ViewGroup;
     41 import android.webkit.ConsoleMessage;
     42 import android.webkit.CookieManager;
     43 import android.webkit.CookieSyncManager;
     44 import android.webkit.JavascriptInterface;
     45 import android.webkit.WebChromeClient;
     46 import android.webkit.WebResourceResponse;
     47 import android.webkit.WebSettings;
     48 import android.webkit.WebView;
     49 
     50 import com.android.emailcommon.mail.Address;
     51 import com.android.mail.FormattedDateBuilder;
     52 import com.android.mail.R;
     53 import com.android.mail.analytics.Analytics;
     54 import com.android.mail.analytics.AnalyticsTimer;
     55 import com.android.mail.browse.ConversationContainer;
     56 import com.android.mail.browse.ConversationContainer.OverlayPosition;
     57 import com.android.mail.browse.ConversationFooterView.ConversationFooterCallbacks;
     58 import com.android.mail.browse.ConversationMessage;
     59 import com.android.mail.browse.ConversationOverlayItem;
     60 import com.android.mail.browse.ConversationViewAdapter;
     61 import com.android.mail.browse.ConversationViewAdapter.ConversationFooterItem;
     62 import com.android.mail.browse.ConversationViewAdapter.MessageFooterItem;
     63 import com.android.mail.browse.ConversationViewAdapter.MessageHeaderItem;
     64 import com.android.mail.browse.ConversationViewAdapter.SuperCollapsedBlockItem;
     65 import com.android.mail.browse.ConversationViewHeader;
     66 import com.android.mail.browse.ConversationWebView;
     67 import com.android.mail.browse.InlineAttachmentViewIntentBuilderCreator;
     68 import com.android.mail.browse.InlineAttachmentViewIntentBuilderCreatorHolder;
     69 import com.android.mail.browse.MailWebView.ContentSizeChangeListener;
     70 import com.android.mail.browse.MessageCursor;
     71 import com.android.mail.browse.MessageFooterView;
     72 import com.android.mail.browse.MessageHeaderView;
     73 import com.android.mail.browse.ScrollIndicatorsView;
     74 import com.android.mail.browse.SuperCollapsedBlock;
     75 import com.android.mail.browse.WebViewContextMenu;
     76 import com.android.mail.compose.ComposeActivity;
     77 import com.android.mail.content.ObjectCursor;
     78 import com.android.mail.print.PrintUtils;
     79 import com.android.mail.providers.Account;
     80 import com.android.mail.providers.Conversation;
     81 import com.android.mail.providers.Message;
     82 import com.android.mail.providers.Settings;
     83 import com.android.mail.providers.UIProvider;
     84 import com.android.mail.ui.ConversationViewState.ExpansionState;
     85 import com.android.mail.utils.ConversationViewUtils;
     86 import com.android.mail.utils.KeyboardUtils;
     87 import com.android.mail.utils.LogTag;
     88 import com.android.mail.utils.LogUtils;
     89 import com.android.mail.utils.Utils;
     90 import com.android.mail.utils.ViewUtils;
     91 import com.google.common.collect.ImmutableList;
     92 import com.google.common.collect.Lists;
     93 import com.google.common.collect.Maps;
     94 import com.google.common.collect.Sets;
     95 
     96 import java.util.ArrayList;
     97 import java.util.List;
     98 import java.util.Map;
     99 import java.util.Set;
    100 
    101 /**
    102  * The conversation view UI component.
    103  */
    104 public class ConversationViewFragment extends AbstractConversationViewFragment implements
    105         SuperCollapsedBlock.OnClickListener, OnLayoutChangeListener,
    106         MessageHeaderView.MessageHeaderViewCallbacks, MessageFooterView.MessageFooterCallbacks,
    107         WebViewContextMenu.Callbacks, ConversationFooterCallbacks, View.OnKeyListener {
    108 
    109     private static final String LOG_TAG = LogTag.getLogTag();
    110     public static final String LAYOUT_TAG = "ConvLayout";
    111 
    112     /**
    113      * Difference in the height of the message header whose details have been expanded/collapsed
    114      */
    115     private int mDiff = 0;
    116 
    117     /**
    118      * Default value for {@link #mLoadWaitReason}. Conversation load will happen immediately.
    119      */
    120     private final int LOAD_NOW = 0;
    121     /**
    122      * Value for {@link #mLoadWaitReason} that means we are offscreen and waiting for the visible
    123      * conversation to finish loading before beginning our load.
    124      * <p>
    125      * When this value is set, the fragment should register with {@link ConversationListCallbacks}
    126      * to know when the visible conversation is loaded. When it is unset, it should unregister.
    127      */
    128     private final int LOAD_WAIT_FOR_INITIAL_CONVERSATION = 1;
    129     /**
    130      * Value for {@link #mLoadWaitReason} used when a conversation is too heavyweight to load at
    131      * all when not visible (e.g. requires network fetch, or too complex). Conversation load will
    132      * wait until this fragment is visible.
    133      */
    134     private final int LOAD_WAIT_UNTIL_VISIBLE = 2;
    135 
    136     // Default scroll distance when the user tries to scroll with up/down
    137     private final int DEFAULT_VERTICAL_SCROLL_DISTANCE_PX = 50;
    138 
    139     // Keyboard navigation
    140     private KeyboardNavigationController mNavigationController;
    141     // Since we manually control navigation for most of the conversation view due to problems
    142     // with two-pane layout but still rely on the system for SOME navigation, we need to keep track
    143     // of the view that had focus when KeyEvent.ACTION_DOWN was fired. This is because we only
    144     // manually change focus on KeyEvent.ACTION_UP (to prevent holding down the DOWN button and
    145     // lagging the app), however, the view in focus might have changed between ACTION_UP and
    146     // ACTION_DOWN since the system might have handled the ACTION_DOWN and moved focus.
    147     private View mOriginalKeyedView;
    148     private int mMaxScreenHeight;
    149     private int mTopOfVisibleScreen;
    150 
    151     protected ConversationContainer mConversationContainer;
    152 
    153     protected ConversationWebView mWebView;
    154 
    155     private ViewGroup mTopmostOverlay;
    156 
    157     private ConversationViewProgressController mProgressController;
    158 
    159     private ActionableToastBar mNewMessageBar;
    160     private ActionableToastBar.ActionClickedListener mNewMessageBarActionListener;
    161 
    162     protected HtmlConversationTemplates mTemplates;
    163 
    164     private final MailJsBridge mJsBridge = new MailJsBridge();
    165 
    166     protected ConversationViewAdapter mAdapter;
    167 
    168     protected boolean mViewsCreated;
    169     // True if we attempted to render before the views were laid out
    170     // We will render immediately once layout is done
    171     private boolean mNeedRender;
    172 
    173     /**
    174      * Temporary string containing the message bodies of the messages within a super-collapsed
    175      * block, for one-time use during block expansion. We cannot easily pass the body HTML
    176      * into JS without problematic escaping, so hold onto it momentarily and signal JS to fetch it
    177      * using {@link MailJsBridge}.
    178      */
    179     private String mTempBodiesHtml;
    180 
    181     private int  mMaxAutoLoadMessages;
    182 
    183     protected int mSideMarginPx;
    184 
    185     /**
    186      * If this conversation fragment is not visible, and it's inappropriate to load up front,
    187      * this is the reason we are waiting. This flag should be cleared once it's okay to load
    188      * the conversation.
    189      */
    190     private int mLoadWaitReason = LOAD_NOW;
    191 
    192     private boolean mEnableContentReadySignal;
    193 
    194     private ContentSizeChangeListener mWebViewSizeChangeListener;
    195 
    196     private float mWebViewYPercent;
    197 
    198     /**
    199      * Has loadData been called on the WebView yet?
    200      */
    201     private boolean mWebViewLoadedData;
    202 
    203     private long mWebViewLoadStartMs;
    204 
    205     private final Map<String, String> mMessageTransforms = Maps.newHashMap();
    206 
    207     private final DataSetObserver mLoadedObserver = new DataSetObserver() {
    208         @Override
    209         public void onChanged() {
    210             getHandler().post(new FragmentRunnable("delayedConversationLoad",
    211                     ConversationViewFragment.this) {
    212                 @Override
    213                 public void go() {
    214                     LogUtils.d(LOG_TAG, "CVF load observer fired, this=%s",
    215                             ConversationViewFragment.this);
    216                     handleDelayedConversationLoad();
    217                 }
    218             });
    219         }
    220     };
    221 
    222     private final Runnable mOnProgressDismiss = new FragmentRunnable("onProgressDismiss", this) {
    223         @Override
    224         public void go() {
    225             LogUtils.d(LOG_TAG, "onProgressDismiss go() - isUserVisible() = %b", isUserVisible());
    226             if (isUserVisible()) {
    227                 onConversationSeen();
    228             }
    229             mWebView.onRenderComplete();
    230         }
    231     };
    232 
    233     private static final boolean DEBUG_DUMP_CONVERSATION_HTML = false;
    234     private static final boolean DISABLE_OFFSCREEN_LOADING = false;
    235     private static final boolean DEBUG_DUMP_CURSOR_CONTENTS = false;
    236 
    237     private static final String BUNDLE_KEY_WEBVIEW_Y_PERCENT =
    238             ConversationViewFragment.class.getName() + "webview-y-percent";
    239 
    240     private BidiFormatter mBidiFormatter;
    241 
    242     /**
    243      * Contains a mapping between inline image attachments and their local message id.
    244      */
    245     private Map<String, String> mUrlToMessageIdMap;
    246 
    247     /**
    248      * Constructor needs to be public to handle orientation changes and activity lifecycle events.
    249      */
    250     public ConversationViewFragment() {}
    251 
    252     /**
    253      * Creates a new instance of {@link ConversationViewFragment}, initialized
    254      * to display a conversation with other parameters inherited/copied from an existing bundle,
    255      * typically one created using {@link #makeBasicArgs}.
    256      */
    257     public static ConversationViewFragment newInstance(Bundle existingArgs,
    258             Conversation conversation) {
    259         ConversationViewFragment f = new ConversationViewFragment();
    260         Bundle args = new Bundle(existingArgs);
    261         args.putParcelable(ARG_CONVERSATION, conversation);
    262         f.setArguments(args);
    263         return f;
    264     }
    265 
    266     @Override
    267     public void onAccountChanged(Account newAccount, Account oldAccount) {
    268         // if overview mode has changed, re-render completely (no need to also update headers)
    269         if (isOverviewMode(newAccount) != isOverviewMode(oldAccount)) {
    270             setupOverviewMode();
    271             final MessageCursor c = getMessageCursor();
    272             if (c != null) {
    273                 renderConversation(c);
    274             } else {
    275                 // Null cursor means this fragment is either waiting to load or in the middle of
    276                 // loading. Either way, a future render will happen anyway, and the new setting
    277                 // will take effect when that happens.
    278             }
    279             return;
    280         }
    281 
    282         // settings may have been updated; refresh views that are known to
    283         // depend on settings
    284         mAdapter.notifyDataSetChanged();
    285     }
    286 
    287     @Override
    288     public void onActivityCreated(Bundle savedInstanceState) {
    289         LogUtils.d(LOG_TAG, "IN CVF.onActivityCreated, this=%s visible=%s", this, isUserVisible());
    290         super.onActivityCreated(savedInstanceState);
    291 
    292         if (mActivity == null || mActivity.isFinishing()) {
    293             // Activity is finishing, just bail.
    294             return;
    295         }
    296 
    297         Context context = getContext();
    298         mTemplates = new HtmlConversationTemplates(context);
    299 
    300         final FormattedDateBuilder dateBuilder = new FormattedDateBuilder(context);
    301 
    302         mNavigationController = mActivity.getKeyboardNavigationController();
    303 
    304         mAdapter = new ConversationViewAdapter(mActivity, this,
    305                 getLoaderManager(), this, this, getContactInfoSource(), this, this,
    306                 getListController(), this, mAddressCache, dateBuilder, mBidiFormatter, this);
    307         mConversationContainer.setOverlayAdapter(mAdapter);
    308 
    309         // set up snap header (the adapter usually does this with the other ones)
    310         mConversationContainer.getSnapHeader().initialize(
    311                 this, mAddressCache, this, getContactInfoSource(),
    312                 mActivity.getAccountController().getVeiledAddressMatcher());
    313 
    314         final Resources resources = getResources();
    315         mMaxAutoLoadMessages = resources.getInteger(R.integer.max_auto_load_messages);
    316 
    317         mSideMarginPx = resources.getDimensionPixelOffset(
    318                 R.dimen.conversation_message_content_margin_side);
    319 
    320         mUrlToMessageIdMap = new ArrayMap<String, String>();
    321         final InlineAttachmentViewIntentBuilderCreator creator =
    322                 InlineAttachmentViewIntentBuilderCreatorHolder.
    323                 getInlineAttachmentViewIntentCreator();
    324         final WebViewContextMenu contextMenu = new WebViewContextMenu(getActivity(),
    325                 creator.createInlineAttachmentViewIntentBuilder(mAccount,
    326                 mConversation != null ? mConversation.id : -1));
    327         contextMenu.setCallbacks(this);
    328         mWebView.setOnCreateContextMenuListener(contextMenu);
    329 
    330         // set this up here instead of onCreateView to ensure the latest Account is loaded
    331         setupOverviewMode();
    332 
    333         // Defer the call to initLoader with a Handler.
    334         // We want to wait until we know which fragments are present and their final visibility
    335         // states before going off and doing work. This prevents extraneous loading from occurring
    336         // as the ViewPager shifts about before the initial position is set.
    337         //
    338         // e.g. click on item #10
    339         // ViewPager.setAdapter() actually first loads #0 and #1 under the assumption that #0 is
    340         // the initial primary item
    341         // Then CPC immediately sets the primary item to #10, which tears down #0/#1 and sets up
    342         // #9/#10/#11.
    343         getHandler().post(new FragmentRunnable("showConversation", this) {
    344             @Override
    345             public void go() {
    346                 showConversation();
    347             }
    348         });
    349 
    350         if (mConversation != null && mConversation.conversationBaseUri != null &&
    351                 !Utils.isEmpty(mAccount.accountCookieQueryUri)) {
    352             // Set the cookie for this base url
    353             new SetCookieTask(getContext(), mConversation.conversationBaseUri.toString(),
    354                     mAccount.accountCookieQueryUri).execute();
    355         }
    356 
    357         // Find the height of the screen for manually scrolling the webview via keyboard.
    358         final Rect screen = new Rect();
    359         mActivity.getWindow().getDecorView().getWindowVisibleDisplayFrame(screen);
    360         mMaxScreenHeight = screen.bottom;
    361         mTopOfVisibleScreen = screen.top + mActivity.getSupportActionBar().getHeight();
    362     }
    363 
    364     @Override
    365     public void onCreate(Bundle savedState) {
    366         super.onCreate(savedState);
    367 
    368         mWebViewClient = createConversationWebViewClient();
    369 
    370         if (savedState != null) {
    371             mWebViewYPercent = savedState.getFloat(BUNDLE_KEY_WEBVIEW_Y_PERCENT);
    372         }
    373 
    374         mBidiFormatter = BidiFormatter.getInstance();
    375     }
    376 
    377     protected ConversationWebViewClient createConversationWebViewClient() {
    378         return new ConversationWebViewClient(mAccount);
    379     }
    380 
    381     @Override
    382     public View onCreateView(LayoutInflater inflater,
    383             ViewGroup container, Bundle savedInstanceState) {
    384         View rootView = inflater.inflate(R.layout.conversation_view, container, false);
    385         mConversationContainer = (ConversationContainer) rootView
    386                 .findViewById(R.id.conversation_container);
    387         mConversationContainer.setAccountController(this);
    388 
    389         mTopmostOverlay =
    390                 (ViewGroup) mConversationContainer.findViewById(R.id.conversation_topmost_overlay);
    391         mTopmostOverlay.setOnKeyListener(this);
    392         inflateSnapHeader(mTopmostOverlay, inflater);
    393         mConversationContainer.setupSnapHeader();
    394 
    395         setupNewMessageBar();
    396 
    397         mProgressController = new ConversationViewProgressController(this, getHandler());
    398         mProgressController.instantiateProgressIndicators(rootView);
    399 
    400         mWebView = (ConversationWebView)
    401                 mConversationContainer.findViewById(R.id.conversation_webview);
    402 
    403         mWebView.addJavascriptInterface(mJsBridge, "mail");
    404         // On JB or newer, we use the 'webkitAnimationStart' DOM event to signal load complete
    405         // Below JB, try to speed up initial render by having the webview do supplemental draws to
    406         // custom a software canvas.
    407         // TODO(mindyp):
    408         //PAGE READINESS SIGNAL FOR JELLYBEAN AND NEWER
    409         // Notify the app on 'webkitAnimationStart' of a simple dummy element with a simple no-op
    410         // animation that immediately runs on page load. The app uses this as a signal that the
    411         // content is loaded and ready to draw, since WebView delays firing this event until the
    412         // layers are composited and everything is ready to draw.
    413         // This signal does not seem to be reliable, so just use the old method for now.
    414         final boolean isJBOrLater = Utils.isRunningJellybeanOrLater();
    415         final boolean isUserVisible = isUserVisible();
    416         mWebView.setUseSoftwareLayer(!isJBOrLater);
    417         mEnableContentReadySignal = isJBOrLater && isUserVisible;
    418         mWebView.onUserVisibilityChanged(isUserVisible);
    419         mWebView.setWebViewClient(mWebViewClient);
    420         final WebChromeClient wcc = new WebChromeClient() {
    421             @Override
    422             public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
    423                 if (consoleMessage.messageLevel() == ConsoleMessage.MessageLevel.ERROR) {
    424                     LogUtils.e(LOG_TAG, "JS: %s (%s:%d) f=%s", consoleMessage.message(),
    425                             consoleMessage.sourceId(), consoleMessage.lineNumber(),
    426                             ConversationViewFragment.this);
    427                 } else {
    428                     LogUtils.i(LOG_TAG, "JS: %s (%s:%d) f=%s", consoleMessage.message(),
    429                             consoleMessage.sourceId(), consoleMessage.lineNumber(),
    430                             ConversationViewFragment.this);
    431                 }
    432                 return true;
    433             }
    434         };
    435         mWebView.setWebChromeClient(wcc);
    436 
    437         final WebSettings settings = mWebView.getSettings();
    438 
    439         final ScrollIndicatorsView scrollIndicators =
    440                 (ScrollIndicatorsView) rootView.findViewById(R.id.scroll_indicators);
    441         scrollIndicators.setSourceView(mWebView);
    442 
    443         settings.setJavaScriptEnabled(true);
    444 
    445         ConversationViewUtils.setTextZoom(getResources(), settings);
    446 
    447         if (Utils.isRunningLOrLater()) {
    448             CookieManager.getInstance().setAcceptThirdPartyCookies(mWebView, true /* accept */);
    449         }
    450 
    451         mViewsCreated = true;
    452         mWebViewLoadedData = false;
    453 
    454         return rootView;
    455     }
    456 
    457     protected void inflateSnapHeader(ViewGroup topmostOverlay, LayoutInflater inflater) {
    458         inflater.inflate(R.layout.conversation_topmost_overlay_items, topmostOverlay, true);
    459     }
    460 
    461     protected void setupNewMessageBar() {
    462         mNewMessageBar = (ActionableToastBar) mConversationContainer.findViewById(
    463                 R.id.new_message_notification_bar);
    464         mNewMessageBarActionListener = new ActionableToastBar.ActionClickedListener() {
    465             @Override
    466             public void onActionClicked(Context context) {
    467                 onNewMessageBarClick();
    468             }
    469         };
    470     }
    471 
    472     @Override
    473     public void onResume() {
    474         super.onResume();
    475         if (mWebView != null) {
    476             mWebView.onResume();
    477         }
    478     }
    479 
    480     @Override
    481     public void onPause() {
    482         super.onPause();
    483         if (mWebView != null) {
    484             mWebView.onPause();
    485         }
    486     }
    487 
    488     @Override
    489     public void onDestroyView() {
    490         super.onDestroyView();
    491         mConversationContainer.setOverlayAdapter(null);
    492         mAdapter = null;
    493         resetLoadWaiting(); // be sure to unregister any active load observer
    494         mViewsCreated = false;
    495     }
    496 
    497     @Override
    498     public void onSaveInstanceState(Bundle outState) {
    499         super.onSaveInstanceState(outState);
    500 
    501         outState.putFloat(BUNDLE_KEY_WEBVIEW_Y_PERCENT, calculateScrollYPercent());
    502     }
    503 
    504     private float calculateScrollYPercent() {
    505         final float p;
    506         if (mWebView == null) {
    507             // onCreateView hasn't been called, return 0 as the user hasn't scrolled the view.
    508             return 0;
    509         }
    510 
    511         final int scrollY = mWebView.getScrollY();
    512         final int viewH = mWebView.getHeight();
    513         final int webH = (int) (mWebView.getContentHeight() * mWebView.getScale());
    514 
    515         if (webH == 0 || webH <= viewH) {
    516             p = 0;
    517         } else if (scrollY + viewH >= webH) {
    518             // The very bottom is a special case, it acts as a stronger anchor than the scroll top
    519             // at that point.
    520             p = 1.0f;
    521         } else {
    522             p = (float) scrollY / webH;
    523         }
    524         return p;
    525     }
    526 
    527     private void resetLoadWaiting() {
    528         if (mLoadWaitReason == LOAD_WAIT_FOR_INITIAL_CONVERSATION) {
    529             getListController().unregisterConversationLoadedObserver(mLoadedObserver);
    530         }
    531         mLoadWaitReason = LOAD_NOW;
    532     }
    533 
    534     @Override
    535     protected void markUnread() {
    536         super.markUnread();
    537         // Ignore unsafe calls made after a fragment is detached from an activity
    538         final ControllableActivity activity = (ControllableActivity) getActivity();
    539         if (activity == null) {
    540             LogUtils.w(LOG_TAG, "ignoring markUnread for conv=%s", mConversation.id);
    541             return;
    542         }
    543 
    544         if (mViewState == null) {
    545             LogUtils.i(LOG_TAG, "ignoring markUnread for conv with no view state (%d)",
    546                     mConversation.id);
    547             return;
    548         }
    549         activity.getConversationUpdater().markConversationMessagesUnread(mConversation,
    550                 mViewState.getUnreadMessageUris(), mViewState.getConversationInfo());
    551     }
    552 
    553     @Override
    554     public void onUserVisibleHintChanged() {
    555         final boolean userVisible = isUserVisible();
    556         LogUtils.d(LOG_TAG, "ConversationViewFragment#onUserVisibleHintChanged(), userVisible = %b",
    557                 userVisible);
    558 
    559         if (!userVisible) {
    560             mProgressController.dismissLoadingStatus();
    561         } else if (mViewsCreated) {
    562             String loadTag = null;
    563             final boolean isInitialLoading;
    564             if (mActivity != null) {
    565                 isInitialLoading = mActivity.getConversationUpdater()
    566                     .isInitialConversationLoading();
    567             } else {
    568                 isInitialLoading = true;
    569             }
    570 
    571             if (getMessageCursor() != null) {
    572                 LogUtils.d(LOG_TAG, "Fragment is now user-visible, onConversationSeen: %s", this);
    573                 if (!isInitialLoading) {
    574                     loadTag = "preloaded";
    575                 }
    576                 onConversationSeen();
    577             } else if (isLoadWaiting()) {
    578                 LogUtils.d(LOG_TAG, "Fragment is now user-visible, showing conversation: %s", this);
    579                 if (!isInitialLoading) {
    580                     loadTag = "load_deferred";
    581                 }
    582                 handleDelayedConversationLoad();
    583             }
    584 
    585             if (loadTag != null) {
    586                 // pager swipes are visibility transitions to 'visible' except during initial
    587                 // pager load on A) enter conversation mode B) rotate C) 2-pane conv-mode list-tap
    588               Analytics.getInstance().sendEvent("pager_swipe", loadTag,
    589                       getCurrentFolderTypeDesc(), 0);
    590             }
    591         }
    592 
    593         if (mWebView != null) {
    594             mWebView.onUserVisibilityChanged(userVisible);
    595         }
    596     }
    597 
    598     /**
    599      * Will either call initLoader now to begin loading, or set {@link #mLoadWaitReason} and do
    600      * nothing (in which case you should later call {@link #handleDelayedConversationLoad()}).
    601      */
    602     private void showConversation() {
    603         final int reason;
    604 
    605         if (isUserVisible()) {
    606             LogUtils.i(LOG_TAG,
    607                     "SHOWCONV: CVF is user-visible, immediately loading conversation (%s)", this);
    608             reason = LOAD_NOW;
    609             timerMark("CVF.showConversation");
    610         } else {
    611             final boolean disableOffscreenLoading = DISABLE_OFFSCREEN_LOADING
    612                     || Utils.isLowRamDevice(getContext())
    613                     || (mConversation != null && (mConversation.isRemote
    614                             || mConversation.getNumMessages() > mMaxAutoLoadMessages));
    615 
    616             // When not visible, we should not immediately load if either this conversation is
    617             // too heavyweight, or if the main/initial conversation is busy loading.
    618             if (disableOffscreenLoading) {
    619                 reason = LOAD_WAIT_UNTIL_VISIBLE;
    620                 LogUtils.i(LOG_TAG, "SHOWCONV: CVF waiting until visible to load (%s)", this);
    621             } else if (getListController().isInitialConversationLoading()) {
    622                 reason = LOAD_WAIT_FOR_INITIAL_CONVERSATION;
    623                 LogUtils.i(LOG_TAG, "SHOWCONV: CVF waiting for initial to finish (%s)", this);
    624                 getListController().registerConversationLoadedObserver(mLoadedObserver);
    625             } else {
    626                 LogUtils.i(LOG_TAG,
    627                         "SHOWCONV: CVF is not visible, but no reason to wait. loading now. (%s)",
    628                         this);
    629                 reason = LOAD_NOW;
    630             }
    631         }
    632 
    633         mLoadWaitReason = reason;
    634         if (mLoadWaitReason == LOAD_NOW) {
    635             startConversationLoad();
    636         }
    637     }
    638 
    639     private void handleDelayedConversationLoad() {
    640         resetLoadWaiting();
    641         startConversationLoad();
    642     }
    643 
    644     private void startConversationLoad() {
    645         mWebView.setVisibility(View.VISIBLE);
    646         loadContent();
    647         // TODO(mindyp): don't show loading status for a previously rendered
    648         // conversation. Ielieve this is better done by making sure don't show loading status
    649         // until XX ms have passed without loading completed.
    650         mProgressController.showLoadingStatus(isUserVisible());
    651     }
    652 
    653     /**
    654      * Can be overridden in case a subclass needs to load something other than
    655      * the messages of a conversation.
    656      */
    657     protected void loadContent() {
    658         getLoaderManager().initLoader(MESSAGE_LOADER, Bundle.EMPTY, getMessageLoaderCallbacks());
    659     }
    660 
    661     private void revealConversation() {
    662         timerMark("revealing conversation");
    663         mProgressController.dismissLoadingStatus(mOnProgressDismiss);
    664         if (isUserVisible()) {
    665             AnalyticsTimer.getInstance().logDuration(AnalyticsTimer.OPEN_CONV_VIEW_FROM_LIST,
    666                     true /* isDestructive */, "open_conversation", "from_list", null);
    667         }
    668     }
    669 
    670     private boolean isLoadWaiting() {
    671         return mLoadWaitReason != LOAD_NOW;
    672     }
    673 
    674     private void renderConversation(MessageCursor messageCursor) {
    675         final String convHtml = renderMessageBodies(messageCursor, mEnableContentReadySignal);
    676         timerMark("rendered conversation");
    677 
    678         if (DEBUG_DUMP_CONVERSATION_HTML) {
    679             java.io.FileWriter fw = null;
    680             try {
    681                 fw = new java.io.FileWriter(getSdCardFilePath());
    682                 fw.write(convHtml);
    683             } catch (java.io.IOException e) {
    684                 e.printStackTrace();
    685             } finally {
    686                 if (fw != null) {
    687                     try {
    688                         fw.close();
    689                     } catch (java.io.IOException e) {
    690                         e.printStackTrace();
    691                     }
    692                 }
    693             }
    694         }
    695 
    696         // save off existing scroll position before re-rendering
    697         if (mWebViewLoadedData) {
    698             mWebViewYPercent = calculateScrollYPercent();
    699         }
    700 
    701         mWebView.loadDataWithBaseURL(mBaseUri, convHtml, "text/html", "utf-8", null);
    702         mWebViewLoadedData = true;
    703         mWebViewLoadStartMs = SystemClock.uptimeMillis();
    704     }
    705 
    706     protected String getSdCardFilePath() {
    707         return "/sdcard/conv" + mConversation.id + ".html";
    708     }
    709 
    710     /**
    711      * Populate the adapter with overlay views (message headers, super-collapsed blocks, a
    712      * conversation header), and return an HTML document with spacer divs inserted for all overlays.
    713      *
    714      */
    715     protected String renderMessageBodies(MessageCursor messageCursor,
    716             boolean enableContentReadySignal) {
    717         int pos = -1;
    718 
    719         LogUtils.d(LOG_TAG, "IN renderMessageBodies, fragment=%s", this);
    720         boolean allowNetworkImages = false;
    721 
    722         // TODO: re-use any existing adapter item state (expanded, details expanded, show pics)
    723 
    724         // Walk through the cursor and build up an overlay adapter as you go.
    725         // Each overlay has an entry in the adapter for easy scroll handling in the container.
    726         // Items are not necessarily 1:1 in cursor and adapter because of super-collapsed blocks.
    727         // When adding adapter items, also add their heights to help the container later determine
    728         // overlay dimensions.
    729 
    730         // When re-rendering, prevent ConversationContainer from laying out overlays until after
    731         // the new spacers are positioned by WebView.
    732         mConversationContainer.invalidateSpacerGeometry();
    733 
    734         mAdapter.clear();
    735 
    736         // re-evaluate the message parts of the view state, since the messages may have changed
    737         // since the previous render
    738         final ConversationViewState prevState = mViewState;
    739         mViewState = new ConversationViewState(prevState);
    740 
    741         // N.B. the units of height for spacers are actually dp and not px because WebView assumes
    742         // a pixel is an mdpi pixel, unless you set device-dpi.
    743 
    744         // add a single conversation header item
    745         final int convHeaderPos = mAdapter.addConversationHeader(mConversation);
    746         final int convHeaderPx = measureOverlayHeight(convHeaderPos);
    747 
    748         mTemplates.startConversation(mWebView.getViewportWidth(),
    749                 mWebView.screenPxToWebPx(mSideMarginPx), mWebView.screenPxToWebPx(convHeaderPx));
    750 
    751         int collapsedStart = -1;
    752         ConversationMessage prevCollapsedMsg = null;
    753 
    754         final boolean alwaysShowImages = shouldAlwaysShowImages();
    755 
    756         boolean prevSafeForImages = alwaysShowImages;
    757 
    758         boolean hasDraft = false;
    759         while (messageCursor.moveToPosition(++pos)) {
    760             final ConversationMessage msg = messageCursor.getMessage();
    761 
    762             final boolean safeForImages = alwaysShowImages ||
    763                     msg.alwaysShowImages || prevState.getShouldShowImages(msg);
    764             allowNetworkImages |= safeForImages;
    765 
    766             final Integer savedExpanded = prevState.getExpansionState(msg);
    767             final int expandedState;
    768             if (savedExpanded != null) {
    769                 if (ExpansionState.isSuperCollapsed(savedExpanded) && messageCursor.isLast()) {
    770                     // override saved state when this is now the new last message
    771                     // this happens to the second-to-last message when you discard a draft
    772                     expandedState = ExpansionState.EXPANDED;
    773                 } else {
    774                     expandedState = savedExpanded;
    775                 }
    776             } else {
    777                 // new messages that are not expanded default to being eligible for super-collapse
    778                 if (msg.starred || !msg.read || messageCursor.isLast()) {
    779                     expandedState = ExpansionState.EXPANDED;
    780                 } else if (messageCursor.isFirst()) {
    781                     expandedState = ExpansionState.COLLAPSED;
    782                 } else {
    783                     expandedState = ExpansionState.SUPER_COLLAPSED;
    784                     hasDraft |= msg.isDraft();
    785                 }
    786             }
    787             mViewState.setShouldShowImages(msg, prevState.getShouldShowImages(msg));
    788             mViewState.setExpansionState(msg, expandedState);
    789 
    790             // save off "read" state from the cursor
    791             // later, the view may not match the cursor (e.g. conversation marked read on open)
    792             // however, if a previous state indicated this message was unread, trust that instead
    793             // so "mark unread" marks all originally unread messages
    794             mViewState.setReadState(msg, msg.read && !prevState.isUnread(msg));
    795 
    796             // We only want to consider this for inclusion in the super collapsed block if
    797             // 1) The we don't have previous state about this message  (The first time that the
    798             //    user opens a conversation)
    799             // 2) The previously saved state for this message indicates that this message is
    800             //    in the super collapsed block.
    801             if (ExpansionState.isSuperCollapsed(expandedState)) {
    802                 // contribute to a super-collapsed block that will be emitted just before the
    803                 // next expanded header
    804                 if (collapsedStart < 0) {
    805                     collapsedStart = pos;
    806                 }
    807                 prevCollapsedMsg = msg;
    808                 prevSafeForImages = safeForImages;
    809 
    810                 // This line puts the from address in the address cache so that
    811                 // we get the sender image for it if it's in a super-collapsed block.
    812                 getAddress(msg.getFrom());
    813                 continue;
    814             }
    815 
    816             // resolve any deferred decisions on previous collapsed items
    817             if (collapsedStart >= 0) {
    818                 if (pos - collapsedStart == 1) {
    819                     // Special-case for a single collapsed message: no need to super-collapse it.
    820                     renderMessage(prevCollapsedMsg, false /* expanded */, prevSafeForImages);
    821                 } else {
    822                     renderSuperCollapsedBlock(collapsedStart, pos - 1, hasDraft);
    823                 }
    824                 hasDraft = false; // reset hasDraft
    825                 prevCollapsedMsg = null;
    826                 collapsedStart = -1;
    827             }
    828 
    829             renderMessage(msg, ExpansionState.isExpanded(expandedState), safeForImages);
    830         }
    831 
    832         final MessageHeaderItem lastHeaderItem = getLastMessageHeaderItem();
    833         final int convFooterPos = mAdapter.addConversationFooter(lastHeaderItem);
    834         final int convFooterPx = measureOverlayHeight(convFooterPos);
    835 
    836         mWebView.getSettings().setBlockNetworkImage(!allowNetworkImages);
    837 
    838         final boolean applyTransforms = shouldApplyTransforms();
    839 
    840         // If the conversation has specified a base uri, use it here, otherwise use mBaseUri
    841         return mTemplates.endConversation(mWebView.screenPxToWebPx(convFooterPx), mBaseUri,
    842                 mConversation.getBaseUri(mBaseUri),
    843                 mWebView.getViewportWidth(), mWebView.getWidthInDp(mSideMarginPx),
    844                 enableContentReadySignal, isOverviewMode(mAccount), applyTransforms,
    845                 applyTransforms);
    846     }
    847 
    848     private MessageHeaderItem getLastMessageHeaderItem() {
    849         int pos = mAdapter.getCount();
    850         while (--pos >= 0) {
    851             final ConversationOverlayItem item = mAdapter.getItem(pos);
    852             if (item instanceof MessageHeaderItem) {
    853                 return (MessageHeaderItem) item;
    854             }
    855         }
    856         LogUtils.wtf(LOG_TAG, "No message header found");
    857         return null;
    858     }
    859 
    860     private void renderSuperCollapsedBlock(int start, int end, boolean hasDraft) {
    861         final int blockPos = mAdapter.addSuperCollapsedBlock(start, end, hasDraft);
    862         final int blockPx = measureOverlayHeight(blockPos);
    863         mTemplates.appendSuperCollapsedHtml(start, mWebView.screenPxToWebPx(blockPx));
    864     }
    865 
    866     private void renderMessage(ConversationMessage msg, boolean expanded, boolean safeForImages) {
    867 
    868         final int headerPos = mAdapter.addMessageHeader(msg, expanded,
    869                 mViewState.getShouldShowImages(msg));
    870         final MessageHeaderItem headerItem = (MessageHeaderItem) mAdapter.getItem(headerPos);
    871 
    872         final int footerPos = mAdapter.addMessageFooter(headerItem);
    873 
    874         // Measure item header and footer heights to allocate spacers in HTML
    875         // But since the views themselves don't exist yet, render each item temporarily into
    876         // a host view for measurement.
    877         final int headerPx = measureOverlayHeight(headerPos);
    878         final int footerPx = measureOverlayHeight(footerPos);
    879 
    880         mTemplates.appendMessageHtml(msg, expanded, safeForImages,
    881                 mWebView.screenPxToWebPx(headerPx), mWebView.screenPxToWebPx(footerPx));
    882         timerMark("rendered message");
    883     }
    884 
    885     private String renderCollapsedHeaders(MessageCursor cursor,
    886             SuperCollapsedBlockItem blockToReplace) {
    887         final List<ConversationOverlayItem> replacements = Lists.newArrayList();
    888 
    889         mTemplates.reset();
    890 
    891         final boolean alwaysShowImages = (mAccount != null) &&
    892                 (mAccount.settings.showImages == Settings.ShowImages.ALWAYS);
    893 
    894         // In devices with non-integral density multiplier, screen pixels translate to non-integral
    895         // web pixels. Keep track of the error that occurs when we cast all heights to int
    896         float error = 0f;
    897         boolean first = true;
    898         for (int i = blockToReplace.getStart(), end = blockToReplace.getEnd(); i <= end; i++) {
    899             cursor.moveToPosition(i);
    900             final ConversationMessage msg = cursor.getMessage();
    901 
    902             final MessageHeaderItem header = ConversationViewAdapter.newMessageHeaderItem(
    903                     mAdapter, mAdapter.getDateBuilder(), msg, false /* expanded */,
    904                     alwaysShowImages || mViewState.getShouldShowImages(msg));
    905             final MessageFooterItem footer = mAdapter.newMessageFooterItem(mAdapter, header);
    906 
    907             final int headerPx = measureOverlayHeight(header);
    908             final int footerPx = measureOverlayHeight(footer);
    909             error += mWebView.screenPxToWebPxError(headerPx)
    910                     + mWebView.screenPxToWebPxError(footerPx);
    911 
    912             // When the error becomes greater than 1 pixel, make the next header 1 pixel taller
    913             int correction = 0;
    914             if (error >= 1) {
    915                 correction = 1;
    916                 error -= 1;
    917             }
    918 
    919             mTemplates.appendMessageHtml(msg, false /* expanded */,
    920                     alwaysShowImages || msg.alwaysShowImages,
    921                     mWebView.screenPxToWebPx(headerPx) + correction,
    922                     mWebView.screenPxToWebPx(footerPx));
    923             replacements.add(header);
    924             replacements.add(footer);
    925 
    926             mViewState.setExpansionState(msg, ExpansionState.COLLAPSED);
    927         }
    928 
    929         mAdapter.replaceSuperCollapsedBlock(blockToReplace, replacements);
    930         mAdapter.notifyDataSetChanged();
    931 
    932         return mTemplates.emit();
    933     }
    934 
    935     protected int measureOverlayHeight(int position) {
    936         return measureOverlayHeight(mAdapter.getItem(position));
    937     }
    938 
    939     /**
    940      * Measure the height of an adapter view by rendering an adapter item into a temporary
    941      * host view, and asking the view to immediately measure itself. This method will reuse
    942      * a previous adapter view from {@link ConversationContainer}'s scrap views if one was generated
    943      * earlier.
    944      * <p>
    945      * After measuring the height, this method also saves the height in the
    946      * {@link ConversationOverlayItem} for later use in overlay positioning.
    947      *
    948      * @param convItem adapter item with data to render and measure
    949      * @return height of the rendered view in screen px
    950      */
    951     private int measureOverlayHeight(ConversationOverlayItem convItem) {
    952         final int type = convItem.getType();
    953 
    954         final View convertView = mConversationContainer.getScrapView(type);
    955         final View hostView = mAdapter.getView(convItem, convertView, mConversationContainer,
    956                 true /* measureOnly */);
    957         if (convertView == null) {
    958             mConversationContainer.addScrapView(type, hostView);
    959         }
    960 
    961         final int heightPx = mConversationContainer.measureOverlay(hostView);
    962         convItem.setHeight(heightPx);
    963         convItem.markMeasurementValid();
    964 
    965         return heightPx;
    966     }
    967 
    968     @Override
    969     public void onConversationViewHeaderHeightChange(int newHeight) {
    970         final int h = mWebView.screenPxToWebPx(newHeight);
    971 
    972         mWebView.loadUrl(String.format("javascript:setConversationHeaderSpacerHeight(%s);", h));
    973     }
    974 
    975     // END conversation header callbacks
    976 
    977     // START conversation footer callbacks
    978 
    979     @Override
    980     public void onConversationFooterHeightChange(int newHeight) {
    981         final int h = mWebView.screenPxToWebPx(newHeight);
    982 
    983         mWebView.loadUrl(String.format("javascript:setConversationFooterSpacerHeight(%s);", h));
    984     }
    985 
    986     // END conversation footer callbacks
    987 
    988     // START message header callbacks
    989     @Override
    990     public void setMessageSpacerHeight(MessageHeaderItem item, int newSpacerHeightPx) {
    991         mConversationContainer.invalidateSpacerGeometry();
    992 
    993         // update message HTML spacer height
    994         final int h = mWebView.screenPxToWebPx(newSpacerHeightPx);
    995         LogUtils.i(LAYOUT_TAG, "setting HTML spacer h=%dwebPx (%dscreenPx)", h,
    996                 newSpacerHeightPx);
    997         mWebView.loadUrl(String.format("javascript:setMessageHeaderSpacerHeight('%s', %s);",
    998                 mTemplates.getMessageDomId(item.getMessage()), h));
    999     }
   1000 
   1001     @Override
   1002     public void setMessageExpanded(MessageHeaderItem item, int newSpacerHeightPx) {
   1003         mConversationContainer.invalidateSpacerGeometry();
   1004 
   1005         // show/hide the HTML message body and update the spacer height
   1006         final int h = mWebView.screenPxToWebPx(newSpacerHeightPx);
   1007         LogUtils.i(LAYOUT_TAG, "setting HTML spacer expanded=%s h=%dwebPx (%dscreenPx)",
   1008                 item.isExpanded(), h, newSpacerHeightPx);
   1009         mWebView.loadUrl(String.format("javascript:setMessageBodyVisible('%s', %s, %s);",
   1010                 mTemplates.getMessageDomId(item.getMessage()), item.isExpanded(), h));
   1011 
   1012         mViewState.setExpansionState(item.getMessage(),
   1013                 item.isExpanded() ? ExpansionState.EXPANDED : ExpansionState.COLLAPSED);
   1014     }
   1015 
   1016     @Override
   1017     public void showExternalResources(final Message msg) {
   1018         mViewState.setShouldShowImages(msg, true);
   1019         mWebView.getSettings().setBlockNetworkImage(false);
   1020         mWebView.loadUrl("javascript:unblockImages(['" + mTemplates.getMessageDomId(msg) + "']);");
   1021     }
   1022 
   1023     @Override
   1024     public void showExternalResources(final String senderRawAddress) {
   1025         mWebView.getSettings().setBlockNetworkImage(false);
   1026 
   1027         final Address sender = getAddress(senderRawAddress);
   1028         if (sender == null) {
   1029             // Don't need to unblock any images
   1030             return;
   1031         }
   1032         final MessageCursor cursor = getMessageCursor();
   1033 
   1034         final List<String> messageDomIds = new ArrayList<>();
   1035 
   1036         int pos = -1;
   1037         while (cursor.moveToPosition(++pos)) {
   1038             final ConversationMessage message = cursor.getMessage();
   1039             if (sender.equals(getAddress(message.getFrom()))) {
   1040                 message.alwaysShowImages = true;
   1041 
   1042                 mViewState.setShouldShowImages(message, true);
   1043                 messageDomIds.add(mTemplates.getMessageDomId(message));
   1044             }
   1045         }
   1046 
   1047         final String url = String.format(
   1048                 "javascript:unblockImages(['%s']);", TextUtils.join("','", messageDomIds));
   1049         mWebView.loadUrl(url);
   1050     }
   1051 
   1052     @Override
   1053     public boolean supportsMessageTransforms() {
   1054         return true;
   1055     }
   1056 
   1057     @Override
   1058     public String getMessageTransforms(final Message msg) {
   1059         final String domId = mTemplates.getMessageDomId(msg);
   1060         return (domId == null) ? null : mMessageTransforms.get(domId);
   1061     }
   1062 
   1063     @Override
   1064     public boolean isSecure() {
   1065         return false;
   1066     }
   1067 
   1068     // END message header callbacks
   1069 
   1070     @Override
   1071     public void showUntransformedConversation() {
   1072         super.showUntransformedConversation();
   1073         final MessageCursor cursor = getMessageCursor();
   1074         if  (cursor != null) {
   1075             renderConversation(cursor);
   1076         }
   1077     }
   1078 
   1079     @Override
   1080     public void onSuperCollapsedClick(SuperCollapsedBlockItem item) {
   1081         MessageCursor cursor = getMessageCursor();
   1082         if (cursor == null || !mViewsCreated) {
   1083             return;
   1084         }
   1085 
   1086         mTempBodiesHtml = renderCollapsedHeaders(cursor, item);
   1087         mWebView.loadUrl("javascript:replaceSuperCollapsedBlock(" + item.getStart() + ")");
   1088         mConversationContainer.focusFirstMessageHeader();
   1089     }
   1090 
   1091     private void showNewMessageNotification(NewMessagesInfo info) {
   1092         mNewMessageBar.show(mNewMessageBarActionListener, info.getNotificationText(), R.string.show,
   1093                 true /* replaceVisibleToast */, false /* autohide */, null /* ToastBarOperation */);
   1094     }
   1095 
   1096     private void onNewMessageBarClick() {
   1097         mNewMessageBar.hide(true, true);
   1098 
   1099         renderConversation(getMessageCursor()); // mCursor is already up-to-date
   1100                                                 // per onLoadFinished()
   1101     }
   1102 
   1103     private static OverlayPosition[] parsePositions(final int[] topArray, final int[] bottomArray) {
   1104         final int len = topArray.length;
   1105         final OverlayPosition[] positions = new OverlayPosition[len];
   1106         for (int i = 0; i < len; i++) {
   1107             positions[i] = new OverlayPosition(topArray[i], bottomArray[i]);
   1108         }
   1109         return positions;
   1110     }
   1111 
   1112     protected @Nullable Address getAddress(String rawFrom) {
   1113         return Utils.getAddress(mAddressCache, rawFrom);
   1114     }
   1115 
   1116     private void ensureContentSizeChangeListener() {
   1117         if (mWebViewSizeChangeListener == null) {
   1118             mWebViewSizeChangeListener = new ContentSizeChangeListener() {
   1119                 @Override
   1120                 public void onHeightChange(int h) {
   1121                     // When WebKit says the DOM height has changed, re-measure
   1122                     // bodies and re-position their headers.
   1123                     // This is separate from the typical JavaScript DOM change
   1124                     // listeners because cases like NARROW_COLUMNS text reflow do not trigger DOM
   1125                     // events.
   1126                     mWebView.loadUrl("javascript:measurePositions();");
   1127                 }
   1128             };
   1129         }
   1130         mWebView.setContentSizeChangeListener(mWebViewSizeChangeListener);
   1131     }
   1132 
   1133     public static boolean isOverviewMode(Account acct) {
   1134         return acct.settings.isOverviewMode();
   1135     }
   1136 
   1137     private void setupOverviewMode() {
   1138         // for now, overview mode means use the built-in WebView zoom and disable custom scale
   1139         // gesture handling
   1140         final boolean overviewMode = isOverviewMode(mAccount);
   1141         final WebSettings settings = mWebView.getSettings();
   1142         final WebSettings.LayoutAlgorithm layout;
   1143         settings.setUseWideViewPort(overviewMode);
   1144         settings.setSupportZoom(overviewMode);
   1145         settings.setBuiltInZoomControls(overviewMode);
   1146         settings.setLoadWithOverviewMode(overviewMode);
   1147         if (overviewMode) {
   1148             settings.setDisplayZoomControls(false);
   1149             layout = WebSettings.LayoutAlgorithm.NORMAL;
   1150         } else {
   1151             layout = WebSettings.LayoutAlgorithm.NARROW_COLUMNS;
   1152         }
   1153         settings.setLayoutAlgorithm(layout);
   1154     }
   1155 
   1156     @Override
   1157     public ConversationMessage getMessageForClickedUrl(String url) {
   1158         final String domMessageId = mUrlToMessageIdMap.get(url);
   1159         if (domMessageId == null) {
   1160             return null;
   1161         }
   1162         final MessageCursor messageCursor = getMessageCursor();
   1163         if (messageCursor == null) {
   1164             return null;
   1165         }
   1166         final String messageId = mTemplates.getMessageIdForDomId(domMessageId);
   1167         return messageCursor.getMessageForId(Long.parseLong(messageId));
   1168     }
   1169 
   1170     /**
   1171      * Determines if we should intercept the left/right key event generated by the hardware
   1172      * keyboard so the framework won't handle directional navigation for us.
   1173      */
   1174     private boolean shouldInterceptLeftRightEvents(@IdRes int id, boolean isLeft, boolean isRight,
   1175             boolean twoPaneLand) {
   1176         return twoPaneLand && (id == R.id.conversation_topmost_overlay ||
   1177                 id == R.id.upper_header ||
   1178                 id == R.id.super_collapsed_block ||
   1179                 id == R.id.message_footer ||
   1180                 (id == R.id.overflow && isRight) ||
   1181                 (id == R.id.reply_button && isLeft) ||
   1182                 (id == R.id.forward_button && isRight));
   1183     }
   1184 
   1185     /**
   1186      * Indicates if the direction with the provided id should navigate away from the conversation
   1187      * view. Note that this is only applicable in two-pane landscape mode.
   1188      */
   1189     private boolean shouldNavigateAway(@IdRes int id, boolean isLeft, boolean twoPaneLand) {
   1190         return twoPaneLand && isLeft && (id == R.id.conversation_topmost_overlay ||
   1191                 id == R.id.upper_header ||
   1192                 id == R.id.super_collapsed_block ||
   1193                 id == R.id.message_footer ||
   1194                 id == R.id.reply_button);
   1195     }
   1196 
   1197     @Override
   1198     public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
   1199         if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
   1200             mOriginalKeyedView = view;
   1201         }
   1202 
   1203         if (mOriginalKeyedView != null) {
   1204             final int id = mOriginalKeyedView.getId();
   1205             final boolean isRtl = ViewUtils.isViewRtl(mOriginalKeyedView);
   1206             final boolean isActionUp = keyEvent.getAction() == KeyEvent.ACTION_UP;
   1207             final boolean isStart = KeyboardUtils.isKeycodeDirectionStart(keyCode, isRtl);
   1208             final boolean isEnd = KeyboardUtils.isKeycodeDirectionEnd(keyCode, isRtl);
   1209             final boolean isUp = keyCode == KeyEvent.KEYCODE_DPAD_UP;
   1210             final boolean isDown = keyCode == KeyEvent.KEYCODE_DPAD_DOWN;
   1211 
   1212             // First we run the event by the controller
   1213             // We manually check if the view+direction combination should shift focus away from the
   1214             // conversation view to the thread list in two-pane landscape mode.
   1215             final boolean isTwoPaneLand = mNavigationController.isTwoPaneLandscape();
   1216             final boolean navigateAway = shouldNavigateAway(id, isStart, isTwoPaneLand);
   1217             if (mNavigationController.onInterceptKeyFromCV(keyCode, keyEvent, navigateAway)) {
   1218                 return true;
   1219             }
   1220 
   1221             // If controller didn't handle the event, check directional interception.
   1222             if ((isStart || isEnd) && shouldInterceptLeftRightEvents(
   1223                     id, isStart, isEnd, isTwoPaneLand)) {
   1224                 return true;
   1225             } else if (isUp || isDown) {
   1226                 // We don't do anything on up/down for overlay
   1227                 if (id == R.id.conversation_topmost_overlay) {
   1228                     return true;
   1229                 }
   1230 
   1231                 // We manually handle up/down navigation through the overlay items because the
   1232                 // system's default isn't optimal for two-pane landscape since it's not a real list.
   1233                 final View next = mConversationContainer.getNextOverlayView(mOriginalKeyedView,
   1234                         isDown);
   1235                 if (next != null) {
   1236                     focusAndScrollToView(next);
   1237                 } else if (!isActionUp) {
   1238                     // Scroll in the direction of the arrow if next view isn't found.
   1239                     final int currentY = mWebView.getScrollY();
   1240                     if (isUp && currentY > 0) {
   1241                         mWebView.scrollBy(0,
   1242                                 -Math.min(currentY, DEFAULT_VERTICAL_SCROLL_DISTANCE_PX));
   1243                     } else if (isDown) {
   1244                         final int webviewEnd = (int) (mWebView.getContentHeight() *
   1245                                 mWebView.getScale());
   1246                         final int currentEnd = currentY + mWebView.getHeight();
   1247                         if (currentEnd < webviewEnd) {
   1248                             mWebView.scrollBy(0, Math.min(webviewEnd - currentEnd,
   1249                                     DEFAULT_VERTICAL_SCROLL_DISTANCE_PX));
   1250                         }
   1251                     }
   1252                 }
   1253                 return true;
   1254             }
   1255 
   1256             // Finally we handle the special keys
   1257             if (keyCode == KeyEvent.KEYCODE_BACK && id != R.id.conversation_topmost_overlay) {
   1258                 if (isActionUp) {
   1259                     mTopmostOverlay.requestFocus();
   1260                 }
   1261                 return true;
   1262             } else if (keyCode == KeyEvent.KEYCODE_ENTER &&
   1263                     id == R.id.conversation_topmost_overlay) {
   1264                 if (isActionUp) {
   1265                     mWebView.scrollTo(0, 0);
   1266                     mConversationContainer.focusFirstMessageHeader();
   1267                 }
   1268                 return true;
   1269             }
   1270         }
   1271         return false;
   1272     }
   1273 
   1274     private void focusAndScrollToView(View v) {
   1275         // Make sure that v is in view
   1276         final int[] coords = new int[2];
   1277         v.getLocationOnScreen(coords);
   1278         final int bottom = coords[1] + v.getHeight();
   1279         if (bottom > mMaxScreenHeight) {
   1280             mWebView.scrollBy(0, bottom - mMaxScreenHeight);
   1281         } else if (coords[1] < mTopOfVisibleScreen) {
   1282             mWebView.scrollBy(0, coords[1] - mTopOfVisibleScreen);
   1283         }
   1284         v.requestFocus();
   1285     }
   1286 
   1287     public class ConversationWebViewClient extends AbstractConversationWebViewClient {
   1288         public ConversationWebViewClient(Account account) {
   1289             super(account);
   1290         }
   1291 
   1292         @Override
   1293         public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
   1294             // try to locate the message associated with the url
   1295             final ConversationMessage cm = getMessageForClickedUrl(url);
   1296             if (cm != null) {
   1297                 // try to load the url assuming it is a cid url
   1298                 final Uri uri = Uri.parse(url);
   1299                 final WebResourceResponse response = loadCIDUri(uri, cm);
   1300                 if (response != null) {
   1301                     return response;
   1302                 }
   1303             }
   1304 
   1305             // otherwise, attempt the default handling
   1306             return super.shouldInterceptRequest(view, url);
   1307         }
   1308 
   1309         @Override
   1310         public void onPageFinished(WebView view, String url) {
   1311             // Ignore unsafe calls made after a fragment is detached from an activity.
   1312             // This method needs to, for example, get at the loader manager, which needs
   1313             // the fragment to be added.
   1314             if (!isAdded() || !mViewsCreated) {
   1315                 LogUtils.d(LOG_TAG, "ignoring CVF.onPageFinished, url=%s fragment=%s", url,
   1316                         ConversationViewFragment.this);
   1317                 return;
   1318             }
   1319 
   1320             LogUtils.d(LOG_TAG, "IN CVF.onPageFinished, url=%s fragment=%s wv=%s t=%sms", url,
   1321                     ConversationViewFragment.this, view,
   1322                     (SystemClock.uptimeMillis() - mWebViewLoadStartMs));
   1323 
   1324             ensureContentSizeChangeListener();
   1325 
   1326             if (!mEnableContentReadySignal) {
   1327                 revealConversation();
   1328             }
   1329 
   1330             final Set<String> emailAddresses = Sets.newHashSet();
   1331             final List<Address> cacheCopy;
   1332             synchronized (mAddressCache) {
   1333                 cacheCopy = ImmutableList.copyOf(mAddressCache.values());
   1334             }
   1335             for (Address addr : cacheCopy) {
   1336                 emailAddresses.add(addr.getAddress());
   1337             }
   1338             final ContactLoaderCallbacks callbacks = getContactInfoSource();
   1339             callbacks.setSenders(emailAddresses);
   1340             getLoaderManager().restartLoader(CONTACT_LOADER, Bundle.EMPTY, callbacks);
   1341         }
   1342 
   1343         @Override
   1344         public boolean shouldOverrideUrlLoading(WebView view, String url) {
   1345             return mViewsCreated && super.shouldOverrideUrlLoading(view, url);
   1346         }
   1347     }
   1348 
   1349     /**
   1350      * NOTE: all public methods must be listed in the proguard flags so that they can be accessed
   1351      * via reflection and not stripped.
   1352      *
   1353      */
   1354     private class MailJsBridge {
   1355         @JavascriptInterface
   1356         public void onWebContentGeometryChange(final int[] overlayTopStrs,
   1357                 final int[] overlayBottomStrs) {
   1358             try {
   1359                 getHandler().post(new FragmentRunnable("onWebContentGeometryChange",
   1360                         ConversationViewFragment.this) {
   1361                     @Override
   1362                     public void go() {
   1363                         if (!mViewsCreated) {
   1364                             LogUtils.d(LOG_TAG, "ignoring webContentGeometryChange because views"
   1365                                     + " are gone, %s", ConversationViewFragment.this);
   1366                             return;
   1367                         }
   1368                         mConversationContainer.onGeometryChange(
   1369                                 parsePositions(overlayTopStrs, overlayBottomStrs));
   1370                         if (mDiff != 0) {
   1371                             // SCROLL!
   1372                             int scale = (int) (mWebView.getScale() / mWebView.getInitialScale());
   1373                             if (scale > 1) {
   1374                                 mWebView.scrollBy(0, (mDiff * (scale - 1)));
   1375                             }
   1376                             mDiff = 0;
   1377                         }
   1378                     }
   1379                 });
   1380             } catch (Throwable t) {
   1381                 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onWebContentGeometryChange");
   1382             }
   1383         }
   1384 
   1385         @JavascriptInterface
   1386         public String getTempMessageBodies() {
   1387             try {
   1388                 if (!mViewsCreated) {
   1389                     return "";
   1390                 }
   1391 
   1392                 final String s = mTempBodiesHtml;
   1393                 mTempBodiesHtml = null;
   1394                 return s;
   1395             } catch (Throwable t) {
   1396                 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.getTempMessageBodies");
   1397                 return "";
   1398             }
   1399         }
   1400 
   1401         @JavascriptInterface
   1402         public String getMessageBody(String domId) {
   1403             try {
   1404                 final MessageCursor cursor = getMessageCursor();
   1405                 if (!mViewsCreated || cursor == null) {
   1406                     return "";
   1407                 }
   1408 
   1409                 int pos = -1;
   1410                 while (cursor.moveToPosition(++pos)) {
   1411                     final ConversationMessage msg = cursor.getMessage();
   1412                     if (TextUtils.equals(domId, mTemplates.getMessageDomId(msg))) {
   1413                         return HtmlConversationTemplates.wrapMessageBody(msg.getBodyAsHtml());
   1414                     }
   1415                 }
   1416 
   1417                 return "";
   1418 
   1419             } catch (Throwable t) {
   1420                 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.getMessageBody");
   1421                 return "";
   1422             }
   1423         }
   1424 
   1425         @JavascriptInterface
   1426         public String getMessageSender(String domId) {
   1427             try {
   1428                 final MessageCursor cursor = getMessageCursor();
   1429                 if (!mViewsCreated || cursor == null) {
   1430                     return "";
   1431                 }
   1432 
   1433                 int pos = -1;
   1434                 while (cursor.moveToPosition(++pos)) {
   1435                     final ConversationMessage msg = cursor.getMessage();
   1436                     if (TextUtils.equals(domId, mTemplates.getMessageDomId(msg))) {
   1437                         final Address address = getAddress(msg.getFrom());
   1438                         if (address != null) {
   1439                             return address.getAddress();
   1440                         } else {
   1441                             // Fall through to return an empty string
   1442                             break;
   1443                         }
   1444                     }
   1445                 }
   1446 
   1447                 return "";
   1448 
   1449             } catch (Throwable t) {
   1450                 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.getMessageSender");
   1451                 return "";
   1452             }
   1453         }
   1454 
   1455         @JavascriptInterface
   1456         public void onContentReady() {
   1457             try {
   1458                 getHandler().post(new FragmentRunnable("onContentReady",
   1459                         ConversationViewFragment.this) {
   1460                     @Override
   1461                     public void go() {
   1462                         try {
   1463                             if (mWebViewLoadStartMs != 0) {
   1464                                 LogUtils.i(LOG_TAG, "IN CVF.onContentReady, f=%s vis=%s t=%sms",
   1465                                         ConversationViewFragment.this,
   1466                                         isUserVisible(),
   1467                                         (SystemClock.uptimeMillis() - mWebViewLoadStartMs));
   1468                             }
   1469                             revealConversation();
   1470                         } catch (Throwable t) {
   1471                             LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onContentReady");
   1472                             // Still try to show the conversation.
   1473                             revealConversation();
   1474                         }
   1475                     }
   1476                 });
   1477             } catch (Throwable t) {
   1478                 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onContentReady");
   1479             }
   1480         }
   1481 
   1482         @JavascriptInterface
   1483         public float getScrollYPercent() {
   1484             try {
   1485                 return mWebViewYPercent;
   1486             } catch (Throwable t) {
   1487                 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.getScrollYPercent");
   1488                 return 0f;
   1489             }
   1490         }
   1491 
   1492         @JavascriptInterface
   1493         public void onMessageTransform(String messageDomId, String transformText) {
   1494             try {
   1495                 LogUtils.i(LOG_TAG, "TRANSFORM: (%s) %s", messageDomId, transformText);
   1496                 mMessageTransforms.put(messageDomId, transformText);
   1497                 onConversationTransformed();
   1498             } catch (Throwable t) {
   1499                 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onMessageTransform");
   1500             }
   1501         }
   1502 
   1503         @JavascriptInterface
   1504         public void onInlineAttachmentsParsed(final String[] urls, final String[] messageIds) {
   1505             try {
   1506                 getHandler().post(new FragmentRunnable("onInlineAttachmentsParsed",
   1507                         ConversationViewFragment.this) {
   1508                     @Override
   1509                     public void go() {
   1510                         try {
   1511                             for (int i = 0, size = urls.length; i < size; i++) {
   1512                                 mUrlToMessageIdMap.put(urls[i], messageIds[i]);
   1513                             }
   1514                         } catch (ArrayIndexOutOfBoundsException e) {
   1515                             LogUtils.e(LOG_TAG, e,
   1516                                     "Number of urls does not match number of message ids - %s:%s",
   1517                                     urls.length, messageIds.length);
   1518                         }
   1519                     }
   1520                 });
   1521             } catch (Throwable t) {
   1522                 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onInlineAttachmentsParsed");
   1523             }
   1524         }
   1525     }
   1526 
   1527     private class NewMessagesInfo {
   1528         int count;
   1529         int countFromSelf;
   1530 
   1531         /**
   1532          * Return the display text for the new message notification overlay. It will be formatted
   1533          * appropriately for a single new message vs. multiple new messages.
   1534          *
   1535          * @return display text
   1536          */
   1537         public String getNotificationText() {
   1538             return getResources().getQuantityString(R.plurals.new_incoming_messages, count, count);
   1539         }
   1540     }
   1541 
   1542     @Override
   1543     public void onMessageCursorLoadFinished(Loader<ObjectCursor<ConversationMessage>> loader,
   1544             MessageCursor newCursor, MessageCursor oldCursor) {
   1545         /*
   1546          * what kind of changes affect the MessageCursor? 1. new message(s) 2.
   1547          * read/unread state change 3. deleted message, either regular or draft
   1548          * 4. updated message, either from self or from others, updated in
   1549          * content or state or sender 5. star/unstar of message (technically
   1550          * similar to #1) 6. other label change Use MessageCursor.hashCode() to
   1551          * sort out interesting vs. no-op cursor updates.
   1552          */
   1553 
   1554         if (oldCursor != null && !oldCursor.isClosed()) {
   1555             final NewMessagesInfo info = getNewIncomingMessagesInfo(newCursor);
   1556 
   1557             if (info.count > 0) {
   1558                 // don't immediately render new incoming messages from other
   1559                 // senders
   1560                 // (to avoid a new message from losing the user's focus)
   1561                 LogUtils.i(LOG_TAG, "CONV RENDER: conversation updated"
   1562                         + ", holding cursor for new incoming message (%s)", this);
   1563                 showNewMessageNotification(info);
   1564                 return;
   1565             }
   1566 
   1567             final int oldState = oldCursor.getStateHashCode();
   1568             final boolean changed = newCursor.getStateHashCode() != oldState;
   1569 
   1570             if (!changed) {
   1571                 final boolean processedInPlace = processInPlaceUpdates(newCursor, oldCursor);
   1572                 if (processedInPlace) {
   1573                     LogUtils.i(LOG_TAG, "CONV RENDER: processed update(s) in place (%s)", this);
   1574                 } else {
   1575                     LogUtils.i(LOG_TAG, "CONV RENDER: uninteresting update"
   1576                             + ", ignoring this conversation update (%s)", this);
   1577                 }
   1578                 return;
   1579             } else if (info.countFromSelf == 1) {
   1580                 // Special-case the very common case of a new cursor that is the same as the old
   1581                 // one, except that there is a new message from yourself. This happens upon send.
   1582                 final boolean sameExceptNewLast = newCursor.getStateHashCode(1) == oldState;
   1583                 if (sameExceptNewLast) {
   1584                     LogUtils.i(LOG_TAG, "CONV RENDER: update is a single new message from self"
   1585                             + " (%s)", this);
   1586                     newCursor.moveToLast();
   1587                     processNewOutgoingMessage(newCursor.getMessage());
   1588                     return;
   1589                 }
   1590             }
   1591             // cursors are different, and not due to an incoming message. fall
   1592             // through and render.
   1593             LogUtils.i(LOG_TAG, "CONV RENDER: conversation updated"
   1594                     + ", but not due to incoming message. rendering. (%s)", this);
   1595 
   1596             if (DEBUG_DUMP_CURSOR_CONTENTS) {
   1597                 LogUtils.i(LOG_TAG, "old cursor: %s", oldCursor.getDebugDump());
   1598                 LogUtils.i(LOG_TAG, "new cursor: %s", newCursor.getDebugDump());
   1599             }
   1600         } else {
   1601             LogUtils.i(LOG_TAG, "CONV RENDER: initial render. (%s)", this);
   1602             timerMark("message cursor load finished");
   1603         }
   1604 
   1605         renderContent(newCursor);
   1606     }
   1607 
   1608     protected void renderContent(MessageCursor messageCursor) {
   1609         // if layout hasn't happened, delay render
   1610         // This is needed in addition to the showConversation() delay to speed
   1611         // up rotation and restoration.
   1612         if (mConversationContainer.getWidth() == 0) {
   1613             mNeedRender = true;
   1614             mConversationContainer.addOnLayoutChangeListener(this);
   1615         } else {
   1616             renderConversation(messageCursor);
   1617         }
   1618     }
   1619 
   1620     private NewMessagesInfo getNewIncomingMessagesInfo(MessageCursor newCursor) {
   1621         final NewMessagesInfo info = new NewMessagesInfo();
   1622 
   1623         int pos = -1;
   1624         while (newCursor.moveToPosition(++pos)) {
   1625             final Message m = newCursor.getMessage();
   1626             if (!mViewState.contains(m)) {
   1627                 LogUtils.i(LOG_TAG, "conversation diff: found new msg: %s", m.uri);
   1628 
   1629                 final Address from = getAddress(m.getFrom());
   1630                 // distinguish ours from theirs
   1631                 // new messages from the account owner should not trigger a
   1632                 // notification
   1633                 if (from == null || mAccount.ownsFromAddress(from.getAddress())) {
   1634                     LogUtils.i(LOG_TAG, "found message from self: %s", m.uri);
   1635                     info.countFromSelf++;
   1636                     continue;
   1637                 }
   1638 
   1639                 info.count++;
   1640             }
   1641         }
   1642         return info;
   1643     }
   1644 
   1645     private boolean processInPlaceUpdates(MessageCursor newCursor, MessageCursor oldCursor) {
   1646         final Set<String> idsOfChangedBodies = Sets.newHashSet();
   1647         final List<Integer> changedOverlayPositions = Lists.newArrayList();
   1648 
   1649         boolean changed = false;
   1650 
   1651         int pos = 0;
   1652         while (true) {
   1653             if (!newCursor.moveToPosition(pos) || !oldCursor.moveToPosition(pos)) {
   1654                 break;
   1655             }
   1656 
   1657             final ConversationMessage newMsg = newCursor.getMessage();
   1658             final ConversationMessage oldMsg = oldCursor.getMessage();
   1659 
   1660             // We are going to update the data in the adapter whenever any input fields change.
   1661             // This ensures that the Message object that ComposeActivity uses will be correctly
   1662             // aligned with the most up-to-date data.
   1663             if (!newMsg.isEqual(oldMsg)) {
   1664                 mAdapter.updateItemsForMessage(newMsg, changedOverlayPositions);
   1665                 LogUtils.i(LOG_TAG, "msg #%d (%d): detected field(s) change. sendingState=%s",
   1666                         pos, newMsg.id, newMsg.sendingState);
   1667             }
   1668 
   1669             // update changed message bodies in-place
   1670             if (!TextUtils.equals(newMsg.bodyHtml, oldMsg.bodyHtml) ||
   1671                     !TextUtils.equals(newMsg.bodyText, oldMsg.bodyText)) {
   1672                 // maybe just set a flag to notify JS to re-request changed bodies
   1673                 idsOfChangedBodies.add('"' + mTemplates.getMessageDomId(newMsg) + '"');
   1674                 LogUtils.i(LOG_TAG, "msg #%d (%d): detected body change", pos, newMsg.id);
   1675             }
   1676 
   1677             pos++;
   1678         }
   1679 
   1680 
   1681         if (!changedOverlayPositions.isEmpty()) {
   1682             // notify once after the entire adapter is updated
   1683             mConversationContainer.onOverlayModelUpdate(changedOverlayPositions);
   1684             changed = true;
   1685         }
   1686 
   1687         final ConversationFooterItem footerItem = mAdapter.getFooterItem();
   1688         if (footerItem != null) {
   1689             footerItem.invalidateMeasurement();
   1690         }
   1691         if (!idsOfChangedBodies.isEmpty()) {
   1692             mWebView.loadUrl(String.format("javascript:replaceMessageBodies([%s]);",
   1693                     TextUtils.join(",", idsOfChangedBodies)));
   1694             changed = true;
   1695         }
   1696 
   1697         return changed;
   1698     }
   1699 
   1700     private void processNewOutgoingMessage(ConversationMessage msg) {
   1701         // Temporarily remove the ConversationFooterItem and its view.
   1702         // It will get re-added right after the new message is added.
   1703         final ConversationFooterItem footerItem = mAdapter.removeFooterItem();
   1704         // if no footer, just skip the work for it. The rest should be fine to do.
   1705         if (footerItem != null) {
   1706             mConversationContainer.removeViewAtAdapterIndex(footerItem.getPosition());
   1707         } else {
   1708             LogUtils.i(LOG_TAG, "footer item not found");
   1709         }
   1710 
   1711         mTemplates.reset();
   1712         // this method will add some items to mAdapter, but we deliberately want to avoid notifying
   1713         // adapter listeners (i.e. ConversationContainer) until onWebContentGeometryChange is next
   1714         // called, to prevent N+1 headers rendering with N message bodies.
   1715         renderMessage(msg, true /* expanded */, msg.alwaysShowImages);
   1716         mTempBodiesHtml = mTemplates.emit();
   1717 
   1718         if (footerItem != null) {
   1719             footerItem.setLastMessageHeaderItem(getLastMessageHeaderItem());
   1720             footerItem.invalidateMeasurement();
   1721             mAdapter.addItem(footerItem);
   1722         }
   1723 
   1724         mViewState.setExpansionState(msg, ExpansionState.EXPANDED);
   1725         // FIXME: should the provider set this as initial state?
   1726         mViewState.setReadState(msg, false /* read */);
   1727 
   1728         // From now until the updated spacer geometry is returned, the adapter items are mismatched
   1729         // with the existing spacers. Do not let them layout.
   1730         mConversationContainer.invalidateSpacerGeometry();
   1731 
   1732         mWebView.loadUrl("javascript:appendMessageHtml();");
   1733     }
   1734 
   1735     private static class SetCookieTask extends AsyncTask<Void, Void, Void> {
   1736         private final Context mContext;
   1737         private final String mUri;
   1738         private final Uri mAccountCookieQueryUri;
   1739         private final ContentResolver mResolver;
   1740 
   1741         /* package */ SetCookieTask(Context context, String baseUri, Uri accountCookieQueryUri) {
   1742             mContext = context;
   1743             mUri = baseUri;
   1744             mAccountCookieQueryUri = accountCookieQueryUri;
   1745             mResolver = context.getContentResolver();
   1746         }
   1747 
   1748         @Override
   1749         public Void doInBackground(Void... args) {
   1750             // First query for the cookie string from the UI provider
   1751             final Cursor cookieCursor = mResolver.query(mAccountCookieQueryUri,
   1752                     UIProvider.ACCOUNT_COOKIE_PROJECTION, null, null, null);
   1753             if (cookieCursor == null) {
   1754                 return null;
   1755             }
   1756 
   1757             try {
   1758                 if (cookieCursor.moveToFirst()) {
   1759                     final String cookie = cookieCursor.getString(
   1760                             cookieCursor.getColumnIndex(UIProvider.AccountCookieColumns.COOKIE));
   1761 
   1762                     if (cookie != null) {
   1763                         final CookieSyncManager csm =
   1764                                 CookieSyncManager.createInstance(mContext);
   1765                         CookieManager.getInstance().setCookie(mUri, cookie);
   1766                         csm.sync();
   1767                     }
   1768                 }
   1769 
   1770             } finally {
   1771                 cookieCursor.close();
   1772             }
   1773 
   1774 
   1775             return null;
   1776         }
   1777     }
   1778 
   1779     @Override
   1780     public void onConversationUpdated(Conversation conv) {
   1781         final ConversationViewHeader headerView = (ConversationViewHeader) mConversationContainer
   1782                 .findViewById(R.id.conversation_header);
   1783         mConversation = conv;
   1784         if (headerView != null) {
   1785             headerView.onConversationUpdated(conv);
   1786         }
   1787     }
   1788 
   1789     @Override
   1790     public void onLayoutChange(View v, int left, int top, int right,
   1791             int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
   1792         boolean sizeChanged = mNeedRender
   1793                 && mConversationContainer.getWidth() != 0;
   1794         if (sizeChanged) {
   1795             mNeedRender = false;
   1796             mConversationContainer.removeOnLayoutChangeListener(this);
   1797             renderConversation(getMessageCursor());
   1798         }
   1799     }
   1800 
   1801     @Override
   1802     public void setMessageDetailsExpanded(MessageHeaderItem i, boolean expanded, int heightBefore) {
   1803         mDiff = (expanded ? 1 : -1) * Math.abs(i.getHeight() - heightBefore);
   1804     }
   1805 
   1806     /**
   1807      * @return {@code true} because either the Print or Print All menu item is shown in GMail
   1808      */
   1809     @Override
   1810     protected boolean shouldShowPrintInOverflow() {
   1811         return true;
   1812     }
   1813 
   1814     @Override
   1815     protected void printConversation() {
   1816         PrintUtils.printConversation(mActivity.getActivityContext(), getMessageCursor(),
   1817                 mAddressCache, mConversation.getBaseUri(mBaseUri), true /* useJavascript */);
   1818     }
   1819 
   1820     @Override
   1821     protected void handleReply() {
   1822         final MessageHeaderItem item = getLastMessageHeaderItem();
   1823         if (item != null) {
   1824             final ConversationMessage msg = item.getMessage();
   1825             if (msg != null) {
   1826                 ComposeActivity.reply(getActivity(), mAccount, msg);
   1827             }
   1828         }
   1829     }
   1830 
   1831     @Override
   1832     protected void handleReplyAll() {
   1833         final MessageHeaderItem item = getLastMessageHeaderItem();
   1834         if (item != null) {
   1835             final ConversationMessage msg = item.getMessage();
   1836             if (msg != null) {
   1837                 ComposeActivity.replyAll(getActivity(), mAccount, msg);
   1838             }
   1839         }
   1840     }
   1841 }
   1842