ConversationViewFragment.java revision ae92e15b48a00613e06ea4c6b274bd73a4e6ec40
1/*
2 * Copyright (C) 2012 Google Inc.
3 * Licensed to The Android Open Source Project.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package com.android.mail.ui;
19
20
21import android.content.ContentResolver;
22import android.content.Context;
23import android.content.Loader;
24import android.content.res.Resources;
25import android.database.Cursor;
26import android.database.DataSetObserver;
27import android.net.Uri;
28import android.os.AsyncTask;
29import android.os.Bundle;
30import android.os.SystemClock;
31import android.text.TextUtils;
32import android.view.LayoutInflater;
33import android.view.ScaleGestureDetector;
34import android.view.ScaleGestureDetector.OnScaleGestureListener;
35import android.view.View;
36import android.view.View.OnLayoutChangeListener;
37import android.view.ViewGroup;
38import android.webkit.ConsoleMessage;
39import android.webkit.CookieManager;
40import android.webkit.CookieSyncManager;
41import android.webkit.JavascriptInterface;
42import android.webkit.WebChromeClient;
43import android.webkit.WebSettings;
44import android.webkit.WebView;
45import android.webkit.WebViewClient;
46import android.widget.TextView;
47
48import com.android.mail.FormattedDateBuilder;
49import com.android.mail.R;
50import com.android.mail.browse.ConversationContainer;
51import com.android.mail.browse.ConversationContainer.OverlayPosition;
52import com.android.mail.browse.ConversationOverlayItem;
53import com.android.mail.browse.ConversationViewAdapter;
54import com.android.mail.browse.ConversationViewAdapter.MessageFooterItem;
55import com.android.mail.browse.ConversationViewAdapter.MessageHeaderItem;
56import com.android.mail.browse.ConversationViewAdapter.SuperCollapsedBlockItem;
57import com.android.mail.browse.ConversationViewHeader;
58import com.android.mail.browse.ConversationWebView;
59import com.android.mail.browse.MailWebView.ContentSizeChangeListener;
60import com.android.mail.browse.MessageCursor;
61import com.android.mail.browse.MessageCursor.ConversationMessage;
62import com.android.mail.browse.MessageHeaderView;
63import com.android.mail.browse.ScrollIndicatorsView;
64import com.android.mail.browse.SuperCollapsedBlock;
65import com.android.mail.browse.WebViewContextMenu;
66import com.android.mail.preferences.MailPrefs;
67import com.android.mail.providers.Account;
68import com.android.mail.providers.Address;
69import com.android.mail.providers.Conversation;
70import com.android.mail.providers.Message;
71import com.android.mail.providers.UIProvider;
72import com.android.mail.ui.ConversationViewState.ExpansionState;
73import com.android.mail.utils.LogTag;
74import com.android.mail.utils.LogUtils;
75import com.android.mail.utils.Utils;
76import com.google.common.collect.ImmutableList;
77import com.google.common.collect.Lists;
78import com.google.common.collect.Maps;
79import com.google.common.collect.Sets;
80
81import java.util.ArrayList;
82import java.util.List;
83import java.util.Map;
84import java.util.Set;
85
86
87/**
88 * The conversation view UI component.
89 */
90public final class ConversationViewFragment extends AbstractConversationViewFragment implements
91        SuperCollapsedBlock.OnClickListener,
92        OnLayoutChangeListener {
93
94    private static final String LOG_TAG = LogTag.getLogTag();
95    public static final String LAYOUT_TAG = "ConvLayout";
96
97    private static final boolean ENABLE_CSS_ZOOM = false;
98
99    /**
100     * Difference in the height of the message header whose details have been expanded/collapsed
101     */
102    private int mDiff = 0;
103
104    /**
105     * Default value for {@link #mLoadWaitReason}. Conversation load will happen immediately.
106     */
107    private final int LOAD_NOW = 0;
108    /**
109     * Value for {@link #mLoadWaitReason} that means we are offscreen and waiting for the visible
110     * conversation to finish loading before beginning our load.
111     * <p>
112     * When this value is set, the fragment should register with {@link ConversationListCallbacks}
113     * to know when the visible conversation is loaded. When it is unset, it should unregister.
114     */
115    private final int LOAD_WAIT_FOR_INITIAL_CONVERSATION = 1;
116    /**
117     * Value for {@link #mLoadWaitReason} used when a conversation is too heavyweight to load at
118     * all when not visible (e.g. requires network fetch, or too complex). Conversation load will
119     * wait until this fragment is visible.
120     */
121    private final int LOAD_WAIT_UNTIL_VISIBLE = 2;
122
123    private ConversationContainer mConversationContainer;
124
125    private ConversationWebView mWebView;
126
127    private ScrollIndicatorsView mScrollIndicators;
128
129    private View mNewMessageBar;
130
131    private HtmlConversationTemplates mTemplates;
132
133    private final MailJsBridge mJsBridge = new MailJsBridge();
134
135    private final WebViewClient mWebViewClient = new ConversationWebViewClient();
136
137    private ConversationViewAdapter mAdapter;
138
139    private boolean mViewsCreated;
140    // True if we attempted to render before the views were laid out
141    // We will render immediately once layout is done
142    private boolean mNeedRender;
143
144    /**
145     * Temporary string containing the message bodies of the messages within a super-collapsed
146     * block, for one-time use during block expansion. We cannot easily pass the body HTML
147     * into JS without problematic escaping, so hold onto it momentarily and signal JS to fetch it
148     * using {@link MailJsBridge}.
149     */
150    private String mTempBodiesHtml;
151
152    private int  mMaxAutoLoadMessages;
153
154    private int mSideMarginPx;
155
156    /**
157     * If this conversation fragment is not visible, and it's inappropriate to load up front,
158     * this is the reason we are waiting. This flag should be cleared once it's okay to load
159     * the conversation.
160     */
161    private int mLoadWaitReason = LOAD_NOW;
162
163    private boolean mEnableContentReadySignal;
164
165    private ContentSizeChangeListener mWebViewSizeChangeListener;
166
167    private float mWebViewYPercent;
168
169    /**
170     * Has loadData been called on the WebView yet?
171     */
172    private boolean mWebViewLoadedData;
173
174    private long mWebViewLoadStartMs;
175
176    private final Map<String, String> mMessageTransforms = Maps.newHashMap();
177
178    private final DataSetObserver mLoadedObserver = new DataSetObserver() {
179        @Override
180        public void onChanged() {
181            getHandler().post(new FragmentRunnable("delayedConversationLoad") {
182                @Override
183                public void go() {
184                    LogUtils.d(LOG_TAG, "CVF load observer fired, this=%s",
185                            ConversationViewFragment.this);
186                    handleDelayedConversationLoad();
187                }
188            });
189        }
190    };
191
192    private final Runnable mOnProgressDismiss = new FragmentRunnable("onProgressDismiss") {
193        @Override
194        public void go() {
195            if (isUserVisible()) {
196                onConversationSeen();
197            }
198            mWebView.onRenderComplete();
199        }
200    };
201
202    private static final boolean DEBUG_DUMP_CONVERSATION_HTML = false;
203    private static final boolean DISABLE_OFFSCREEN_LOADING = false;
204    private static final boolean DEBUG_DUMP_CURSOR_CONTENTS = false;
205
206    private static final String BUNDLE_KEY_WEBVIEW_Y_PERCENT =
207            ConversationViewFragment.class.getName() + "webview-y-percent";
208
209    /**
210     * Constructor needs to be public to handle orientation changes and activity lifecycle events.
211     */
212    public ConversationViewFragment() {
213        super();
214    }
215
216    /**
217     * Creates a new instance of {@link ConversationViewFragment}, initialized
218     * to display a conversation with other parameters inherited/copied from an existing bundle,
219     * typically one created using {@link #makeBasicArgs}.
220     */
221    public static ConversationViewFragment newInstance(Bundle existingArgs,
222            Conversation conversation) {
223        ConversationViewFragment f = new ConversationViewFragment();
224        Bundle args = new Bundle(existingArgs);
225        args.putParcelable(ARG_CONVERSATION, conversation);
226        f.setArguments(args);
227        return f;
228    }
229
230    @Override
231    public void onAccountChanged(Account newAccount, Account oldAccount) {
232        // if overview mode has changed, re-render completely (no need to also update headers)
233        if (isOverviewMode(newAccount) != isOverviewMode(oldAccount)) {
234            setupOverviewMode();
235            final MessageCursor c = getMessageCursor();
236            if (c != null) {
237                renderConversation(c);
238            } else {
239                // Null cursor means this fragment is either waiting to load or in the middle of
240                // loading. Either way, a future render will happen anyway, and the new setting
241                // will take effect when that happens.
242            }
243            return;
244        }
245
246        // settings may have been updated; refresh views that are known to
247        // depend on settings
248        mAdapter.notifyDataSetChanged();
249    }
250
251    @Override
252    public void onActivityCreated(Bundle savedInstanceState) {
253        LogUtils.d(LOG_TAG, "IN CVF.onActivityCreated, this=%s visible=%s", this, isUserVisible());
254        super.onActivityCreated(savedInstanceState);
255
256        if (mActivity == null || mActivity.isFinishing()) {
257            // Activity is finishing, just bail.
258            return;
259        }
260
261        Context context = getContext();
262        mTemplates = new HtmlConversationTemplates(context);
263
264        final FormattedDateBuilder dateBuilder = new FormattedDateBuilder(context);
265
266        mAdapter = new ConversationViewAdapter(mActivity, this,
267                getLoaderManager(), this, getContactInfoSource(), this,
268                this, mAddressCache, dateBuilder);
269        mConversationContainer.setOverlayAdapter(mAdapter);
270
271        // set up snap header (the adapter usually does this with the other ones)
272        final MessageHeaderView snapHeader = mConversationContainer.getSnapHeader();
273        initHeaderView(snapHeader, dateBuilder);
274
275        mMaxAutoLoadMessages = getResources().getInteger(R.integer.max_auto_load_messages);
276
277        mSideMarginPx = getResources().getDimensionPixelOffset(
278                R.dimen.conversation_view_margin_side) + getResources().getDimensionPixelOffset(
279                R.dimen.conversation_message_content_margin_side);
280
281        mWebView.setOnCreateContextMenuListener(new WebViewContextMenu(getActivity()));
282
283        // set this up here instead of onCreateView to ensure the latest Account is loaded
284        setupOverviewMode();
285
286        // Defer the call to initLoader with a Handler.
287        // We want to wait until we know which fragments are present and their final visibility
288        // states before going off and doing work. This prevents extraneous loading from occurring
289        // as the ViewPager shifts about before the initial position is set.
290        //
291        // e.g. click on item #10
292        // ViewPager.setAdapter() actually first loads #0 and #1 under the assumption that #0 is
293        // the initial primary item
294        // Then CPC immediately sets the primary item to #10, which tears down #0/#1 and sets up
295        // #9/#10/#11.
296        getHandler().post(new FragmentRunnable("showConversation") {
297            @Override
298            public void go() {
299                showConversation();
300            }
301        });
302
303        if (mConversation.conversationBaseUri != null &&
304                !Utils.isEmpty(mAccount.accoutCookieQueryUri)) {
305            // Set the cookie for this base url
306            new SetCookieTask(getContext(), mConversation.conversationBaseUri,
307                    mAccount.accoutCookieQueryUri).execute();
308        }
309    }
310
311    private void initHeaderView(MessageHeaderView headerView, FormattedDateBuilder dateBuilder) {
312        headerView.initialize(dateBuilder, this, mAddressCache);
313        headerView.setCallbacks(this);
314        headerView.setContactInfoSource(getContactInfoSource());
315        headerView.setVeiledMatcher(mActivity.getAccountController().getVeiledAddressMatcher());
316    }
317
318    @Override
319    public void onCreate(Bundle savedState) {
320        super.onCreate(savedState);
321
322        if (savedState != null) {
323            mWebViewYPercent = savedState.getFloat(BUNDLE_KEY_WEBVIEW_Y_PERCENT);
324        }
325    }
326
327    @Override
328    public View onCreateView(LayoutInflater inflater,
329            ViewGroup container, Bundle savedInstanceState) {
330
331        View rootView = inflater.inflate(R.layout.conversation_view, container, false);
332        mConversationContainer = (ConversationContainer) rootView
333                .findViewById(R.id.conversation_container);
334        mConversationContainer.setAccountController(this);
335
336        mNewMessageBar = mConversationContainer.findViewById(R.id.new_message_notification_bar);
337        mNewMessageBar.setOnClickListener(new View.OnClickListener() {
338            @Override
339            public void onClick(View v) {
340                onNewMessageBarClick();
341            }
342        });
343
344        instantiateProgressIndicators(rootView);
345
346        mWebView = (ConversationWebView) mConversationContainer.findViewById(R.id.webview);
347
348        mWebView.addJavascriptInterface(mJsBridge, "mail");
349        // On JB or newer, we use the 'webkitAnimationStart' DOM event to signal load complete
350        // Below JB, try to speed up initial render by having the webview do supplemental draws to
351        // custom a software canvas.
352        // TODO(mindyp):
353        //PAGE READINESS SIGNAL FOR JELLYBEAN AND NEWER
354        // Notify the app on 'webkitAnimationStart' of a simple dummy element with a simple no-op
355        // animation that immediately runs on page load. The app uses this as a signal that the
356        // content is loaded and ready to draw, since WebView delays firing this event until the
357        // layers are composited and everything is ready to draw.
358        // This signal does not seem to be reliable, so just use the old method for now.
359        mEnableContentReadySignal = Utils.isRunningJellybeanOrLater();
360        mWebView.setUseSoftwareLayer(!mEnableContentReadySignal);
361        mWebView.onUserVisibilityChanged(isUserVisible());
362        mWebView.setWebViewClient(mWebViewClient);
363        final WebChromeClient wcc = new WebChromeClient() {
364            @Override
365            public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
366                LogUtils.i(LOG_TAG, "JS: %s (%s:%d) f=%s", consoleMessage.message(),
367                        consoleMessage.sourceId(), consoleMessage.lineNumber(),
368                        ConversationViewFragment.this);
369                return true;
370            }
371        };
372        mWebView.setWebChromeClient(wcc);
373
374        final WebSettings settings = mWebView.getSettings();
375
376        mScrollIndicators = (ScrollIndicatorsView) rootView.findViewById(R.id.scroll_indicators);
377        mScrollIndicators.setSourceView(mWebView);
378
379        settings.setJavaScriptEnabled(true);
380
381        final float fontScale = getResources().getConfiguration().fontScale;
382        final int desiredFontSizePx = getResources()
383                .getInteger(R.integer.conversation_desired_font_size_px);
384        final int unstyledFontSizePx = getResources()
385                .getInteger(R.integer.conversation_unstyled_font_size_px);
386
387        int textZoom = settings.getTextZoom();
388        // apply a correction to the default body text style to get regular text to the size we want
389        textZoom = textZoom * desiredFontSizePx / unstyledFontSizePx;
390        // then apply any system font scaling
391        textZoom = (int) (textZoom * fontScale);
392        settings.setTextZoom(textZoom);
393
394        mViewsCreated = true;
395        mWebViewLoadedData = false;
396
397        return rootView;
398    }
399
400    @Override
401    public void onDestroyView() {
402        super.onDestroyView();
403        mConversationContainer.setOverlayAdapter(null);
404        mAdapter = null;
405        resetLoadWaiting(); // be sure to unregister any active load observer
406        mViewsCreated = false;
407    }
408
409    @Override
410    protected WebView getWebView() {
411        return mWebView;
412    }
413
414    @Override
415    public void onSaveInstanceState(Bundle outState) {
416        super.onSaveInstanceState(outState);
417
418        outState.putFloat(BUNDLE_KEY_WEBVIEW_Y_PERCENT, calculateScrollYPercent());
419    }
420
421    private float calculateScrollYPercent() {
422        final float p;
423        if (mWebView == null) {
424            // onCreateView hasn't been called, return 0 as the user hasn't scrolled the view.
425            return 0;
426        }
427
428        final int scrollY = mWebView.getScrollY();
429        final int viewH = mWebView.getHeight();
430        final int webH = (int) (mWebView.getContentHeight() * mWebView.getScale());
431
432        if (webH == 0 || webH <= viewH) {
433            p = 0;
434        } else if (scrollY + viewH >= webH) {
435            // The very bottom is a special case, it acts as a stronger anchor than the scroll top
436            // at that point.
437            p = 1.0f;
438        } else {
439            p = (float) scrollY / webH;
440        }
441        return p;
442    }
443
444    private void resetLoadWaiting() {
445        if (mLoadWaitReason == LOAD_WAIT_FOR_INITIAL_CONVERSATION) {
446            getListController().unregisterConversationLoadedObserver(mLoadedObserver);
447        }
448        mLoadWaitReason = LOAD_NOW;
449    }
450
451    @Override
452    protected void markUnread() {
453        super.markUnread();
454        // Ignore unsafe calls made after a fragment is detached from an activity
455        final ControllableActivity activity = (ControllableActivity) getActivity();
456        if (activity == null) {
457            LogUtils.w(LOG_TAG, "ignoring markUnread for conv=%s", mConversation.id);
458            return;
459        }
460
461        if (mViewState == null) {
462            LogUtils.i(LOG_TAG, "ignoring markUnread for conv with no view state (%d)",
463                    mConversation.id);
464            return;
465        }
466        activity.getConversationUpdater().markConversationMessagesUnread(mConversation,
467                mViewState.getUnreadMessageUris(), mViewState.getConversationInfo());
468    }
469
470    @Override
471    public void onUserVisibleHintChanged() {
472        final boolean userVisible = isUserVisible();
473
474        if (!userVisible) {
475            dismissLoadingStatus();
476        } else if (mViewsCreated) {
477            if (getMessageCursor() != null) {
478                LogUtils.d(LOG_TAG, "Fragment is now user-visible, onConversationSeen: %s", this);
479                onConversationSeen();
480            } else if (isLoadWaiting()) {
481                LogUtils.d(LOG_TAG, "Fragment is now user-visible, showing conversation: %s", this);
482                handleDelayedConversationLoad();
483            }
484        }
485
486        if (mWebView != null) {
487            mWebView.onUserVisibilityChanged(userVisible);
488        }
489    }
490
491    /**
492     * Will either call initLoader now to begin loading, or set {@link #mLoadWaitReason} and do
493     * nothing (in which case you should later call {@link #handleDelayedConversationLoad()}).
494     */
495    private void showConversation() {
496        final int reason;
497
498        if (isUserVisible()) {
499            LogUtils.i(LOG_TAG,
500                    "SHOWCONV: CVF is user-visible, immediately loading conversation (%s)", this);
501            reason = LOAD_NOW;
502            timerMark("CVF.showConversation");
503        } else {
504            final boolean disableOffscreenLoading = DISABLE_OFFSCREEN_LOADING
505                    || (mConversation.isRemote
506                            || mConversation.getNumMessages() > mMaxAutoLoadMessages);
507
508            // When not visible, we should not immediately load if either this conversation is
509            // too heavyweight, or if the main/initial conversation is busy loading.
510            if (disableOffscreenLoading) {
511                reason = LOAD_WAIT_UNTIL_VISIBLE;
512                LogUtils.i(LOG_TAG, "SHOWCONV: CVF waiting until visible to load (%s)", this);
513            } else if (getListController().isInitialConversationLoading()) {
514                reason = LOAD_WAIT_FOR_INITIAL_CONVERSATION;
515                LogUtils.i(LOG_TAG, "SHOWCONV: CVF waiting for initial to finish (%s)", this);
516                getListController().registerConversationLoadedObserver(mLoadedObserver);
517            } else {
518                LogUtils.i(LOG_TAG,
519                        "SHOWCONV: CVF is not visible, but no reason to wait. loading now. (%s)",
520                        this);
521                reason = LOAD_NOW;
522            }
523        }
524
525        mLoadWaitReason = reason;
526        if (mLoadWaitReason == LOAD_NOW) {
527            startConversationLoad();
528        }
529    }
530
531    private void handleDelayedConversationLoad() {
532        resetLoadWaiting();
533        startConversationLoad();
534    }
535
536    private void startConversationLoad() {
537        mWebView.setVisibility(View.VISIBLE);
538        getLoaderManager().initLoader(MESSAGE_LOADER, Bundle.EMPTY, getMessageLoaderCallbacks());
539        // TODO(mindyp): don't show loading status for a previously rendered
540        // conversation. Ielieve this is better done by making sure don't show loading status
541        // until XX ms have passed without loading completed.
542        showLoadingStatus();
543    }
544
545    private void revealConversation() {
546        timerMark("revealing conversation");
547        dismissLoadingStatus(mOnProgressDismiss);
548    }
549
550    private boolean isLoadWaiting() {
551        return mLoadWaitReason != LOAD_NOW;
552    }
553
554    private void renderConversation(MessageCursor messageCursor) {
555        final String convHtml = renderMessageBodies(messageCursor, mEnableContentReadySignal);
556        timerMark("rendered conversation");
557
558        if (DEBUG_DUMP_CONVERSATION_HTML) {
559            java.io.FileWriter fw = null;
560            try {
561                fw = new java.io.FileWriter("/sdcard/conv" + mConversation.id
562                        + ".html");
563                fw.write(convHtml);
564            } catch (java.io.IOException e) {
565                e.printStackTrace();
566            } finally {
567                if (fw != null) {
568                    try {
569                        fw.close();
570                    } catch (java.io.IOException e) {
571                        e.printStackTrace();
572                    }
573                }
574            }
575        }
576
577        // save off existing scroll position before re-rendering
578        if (mWebViewLoadedData) {
579            mWebViewYPercent = calculateScrollYPercent();
580        }
581
582        mWebView.loadDataWithBaseURL(mBaseUri, convHtml, "text/html", "utf-8", null);
583        mWebViewLoadedData = true;
584        mWebViewLoadStartMs = SystemClock.uptimeMillis();
585    }
586
587    /**
588     * Populate the adapter with overlay views (message headers, super-collapsed blocks, a
589     * conversation header), and return an HTML document with spacer divs inserted for all overlays.
590     *
591     */
592    private String renderMessageBodies(MessageCursor messageCursor,
593            boolean enableContentReadySignal) {
594        int pos = -1;
595
596        LogUtils.d(LOG_TAG, "IN renderMessageBodies, fragment=%s", this);
597        boolean allowNetworkImages = false;
598
599        // TODO: re-use any existing adapter item state (expanded, details expanded, show pics)
600
601        // Walk through the cursor and build up an overlay adapter as you go.
602        // Each overlay has an entry in the adapter for easy scroll handling in the container.
603        // Items are not necessarily 1:1 in cursor and adapter because of super-collapsed blocks.
604        // When adding adapter items, also add their heights to help the container later determine
605        // overlay dimensions.
606
607        // When re-rendering, prevent ConversationContainer from laying out overlays until after
608        // the new spacers are positioned by WebView.
609        mConversationContainer.invalidateSpacerGeometry();
610
611        mAdapter.clear();
612
613        // re-evaluate the message parts of the view state, since the messages may have changed
614        // since the previous render
615        final ConversationViewState prevState = mViewState;
616        mViewState = new ConversationViewState(prevState);
617
618        // N.B. the units of height for spacers are actually dp and not px because WebView assumes
619        // a pixel is an mdpi pixel, unless you set device-dpi.
620
621        // add a single conversation header item
622        final int convHeaderPos = mAdapter.addConversationHeader(mConversation);
623        final int convHeaderPx = measureOverlayHeight(convHeaderPos);
624
625        mTemplates.startConversation(mWebView.screenPxToWebPx(mSideMarginPx),
626                mWebView.screenPxToWebPx(convHeaderPx));
627
628        int collapsedStart = -1;
629        ConversationMessage prevCollapsedMsg = null;
630        boolean prevSafeForImages = false;
631
632        while (messageCursor.moveToPosition(++pos)) {
633            final ConversationMessage msg = messageCursor.getMessage();
634
635            final boolean safeForImages =
636                    msg.alwaysShowImages || prevState.getShouldShowImages(msg);
637            allowNetworkImages |= safeForImages;
638
639            final Integer savedExpanded = prevState.getExpansionState(msg);
640            final int expandedState;
641            if (savedExpanded != null) {
642                if (ExpansionState.isSuperCollapsed(savedExpanded) && messageCursor.isLast()) {
643                    // override saved state when this is now the new last message
644                    // this happens to the second-to-last message when you discard a draft
645                    expandedState = ExpansionState.EXPANDED;
646                } else {
647                    expandedState = savedExpanded;
648                }
649            } else {
650                // new messages that are not expanded default to being eligible for super-collapse
651                expandedState = (!msg.read || msg.starred || messageCursor.isLast()) ?
652                        ExpansionState.EXPANDED : ExpansionState.SUPER_COLLAPSED;
653            }
654            mViewState.setShouldShowImages(msg, prevState.getShouldShowImages(msg));
655            mViewState.setExpansionState(msg, expandedState);
656
657            // save off "read" state from the cursor
658            // later, the view may not match the cursor (e.g. conversation marked read on open)
659            // however, if a previous state indicated this message was unread, trust that instead
660            // so "mark unread" marks all originally unread messages
661            mViewState.setReadState(msg, msg.read && !prevState.isUnread(msg));
662
663            // We only want to consider this for inclusion in the super collapsed block if
664            // 1) The we don't have previous state about this message  (The first time that the
665            //    user opens a conversation)
666            // 2) The previously saved state for this message indicates that this message is
667            //    in the super collapsed block.
668            if (ExpansionState.isSuperCollapsed(expandedState)) {
669                // contribute to a super-collapsed block that will be emitted just before the
670                // next expanded header
671                if (collapsedStart < 0) {
672                    collapsedStart = pos;
673                }
674                prevCollapsedMsg = msg;
675                prevSafeForImages = safeForImages;
676                continue;
677            }
678
679            // resolve any deferred decisions on previous collapsed items
680            if (collapsedStart >= 0) {
681                if (pos - collapsedStart == 1) {
682                    // special-case for a single collapsed message: no need to super-collapse it
683                    renderMessage(prevCollapsedMsg, false /* expanded */,
684                            prevSafeForImages);
685                } else {
686                    renderSuperCollapsedBlock(collapsedStart, pos - 1);
687                }
688                prevCollapsedMsg = null;
689                collapsedStart = -1;
690            }
691
692            renderMessage(msg, ExpansionState.isExpanded(expandedState), safeForImages);
693        }
694
695        mWebView.getSettings().setBlockNetworkImage(!allowNetworkImages);
696
697        final boolean applyTransforms = shouldApplyTransforms();
698
699        final MailPrefs prefs = MailPrefs.get(getContext());
700
701        // If the conversation has specified a base uri, use it here, otherwise use mBaseUri
702        return mTemplates.endConversation(mBaseUri, mConversation.getBaseUri(mBaseUri), 320,
703                mWebView.getViewportWidth(), enableContentReadySignal, isOverviewMode(mAccount),
704                applyTransforms, applyTransforms);
705    }
706
707    private void renderSuperCollapsedBlock(int start, int end) {
708        final int blockPos = mAdapter.addSuperCollapsedBlock(start, end);
709        final int blockPx = measureOverlayHeight(blockPos);
710        mTemplates.appendSuperCollapsedHtml(start, mWebView.screenPxToWebPx(blockPx));
711    }
712
713    private void renderMessage(ConversationMessage msg, boolean expanded,
714            boolean safeForImages) {
715        final int headerPos = mAdapter.addMessageHeader(msg, expanded,
716                mViewState.getShouldShowImages(msg));
717        final MessageHeaderItem headerItem = (MessageHeaderItem) mAdapter.getItem(headerPos);
718
719        final int footerPos = mAdapter.addMessageFooter(headerItem);
720
721        // Measure item header and footer heights to allocate spacers in HTML
722        // But since the views themselves don't exist yet, render each item temporarily into
723        // a host view for measurement.
724        final int headerPx = measureOverlayHeight(headerPos);
725        final int footerPx = measureOverlayHeight(footerPos);
726
727        mTemplates.appendMessageHtml(msg, expanded, safeForImages,
728                mWebView.screenPxToWebPx(headerPx), mWebView.screenPxToWebPx(footerPx));
729        timerMark("rendered message");
730    }
731
732    private String renderCollapsedHeaders(MessageCursor cursor,
733            SuperCollapsedBlockItem blockToReplace) {
734        final List<ConversationOverlayItem> replacements = Lists.newArrayList();
735
736        mTemplates.reset();
737
738        // In devices with non-integral density multiplier, screen pixels translate to non-integral
739        // web pixels. Keep track of the error that occurs when we cast all heights to int
740        float error = 0f;
741        for (int i = blockToReplace.getStart(), end = blockToReplace.getEnd(); i <= end; i++) {
742            cursor.moveToPosition(i);
743            final ConversationMessage msg = cursor.getMessage();
744            final MessageHeaderItem header = mAdapter.newMessageHeaderItem(msg,
745                    false /* expanded */, mViewState.getShouldShowImages(msg));
746            final MessageFooterItem footer = mAdapter.newMessageFooterItem(header);
747
748            final int headerPx = measureOverlayHeight(header);
749            final int footerPx = measureOverlayHeight(footer);
750            error += mWebView.screenPxToWebPxError(headerPx)
751                    + mWebView.screenPxToWebPxError(footerPx);
752
753            // When the error becomes greater than 1 pixel, make the next header 1 pixel taller
754            int correction = 0;
755            if (error >= 1) {
756                correction = 1;
757                error -= 1;
758            }
759
760            mTemplates.appendMessageHtml(msg, false /* expanded */, msg.alwaysShowImages,
761                    mWebView.screenPxToWebPx(headerPx) + correction,
762                    mWebView.screenPxToWebPx(footerPx));
763            replacements.add(header);
764            replacements.add(footer);
765
766            mViewState.setExpansionState(msg, ExpansionState.COLLAPSED);
767        }
768
769        mAdapter.replaceSuperCollapsedBlock(blockToReplace, replacements);
770        mAdapter.notifyDataSetChanged();
771
772        return mTemplates.emit();
773    }
774
775    private int measureOverlayHeight(int position) {
776        return measureOverlayHeight(mAdapter.getItem(position));
777    }
778
779    /**
780     * Measure the height of an adapter view by rendering an adapter item into a temporary
781     * host view, and asking the view to immediately measure itself. This method will reuse
782     * a previous adapter view from {@link ConversationContainer}'s scrap views if one was generated
783     * earlier.
784     * <p>
785     * After measuring the height, this method also saves the height in the
786     * {@link ConversationOverlayItem} for later use in overlay positioning.
787     *
788     * @param convItem adapter item with data to render and measure
789     * @return height of the rendered view in screen px
790     */
791    private int measureOverlayHeight(ConversationOverlayItem convItem) {
792        final int type = convItem.getType();
793
794        final View convertView = mConversationContainer.getScrapView(type);
795        final View hostView = mAdapter.getView(convItem, convertView, mConversationContainer,
796                true /* measureOnly */);
797        if (convertView == null) {
798            mConversationContainer.addScrapView(type, hostView);
799        }
800
801        final int heightPx = mConversationContainer.measureOverlay(hostView);
802        convItem.setHeight(heightPx);
803        convItem.markMeasurementValid();
804
805        return heightPx;
806    }
807
808    @Override
809    public void onConversationViewHeaderHeightChange(int newHeight) {
810        final int h = mWebView.screenPxToWebPx(newHeight);
811
812        mWebView.loadUrl(String.format("javascript:setConversationHeaderSpacerHeight(%s);", h));
813    }
814
815    // END conversation header callbacks
816
817    // START message header callbacks
818    @Override
819    public void setMessageSpacerHeight(MessageHeaderItem item, int newSpacerHeightPx) {
820        mConversationContainer.invalidateSpacerGeometry();
821
822        // update message HTML spacer height
823        final int h = mWebView.screenPxToWebPx(newSpacerHeightPx);
824        LogUtils.i(LAYOUT_TAG, "setting HTML spacer h=%dwebPx (%dscreenPx)", h,
825                newSpacerHeightPx);
826        mWebView.loadUrl(String.format("javascript:setMessageHeaderSpacerHeight('%s', %s);",
827                mTemplates.getMessageDomId(item.getMessage()), h));
828    }
829
830    @Override
831    public void setMessageExpanded(MessageHeaderItem item, int newSpacerHeightPx) {
832        mConversationContainer.invalidateSpacerGeometry();
833
834        // show/hide the HTML message body and update the spacer height
835        final int h = mWebView.screenPxToWebPx(newSpacerHeightPx);
836        LogUtils.i(LAYOUT_TAG, "setting HTML spacer expanded=%s h=%dwebPx (%dscreenPx)",
837                item.isExpanded(), h, newSpacerHeightPx);
838        mWebView.loadUrl(String.format("javascript:setMessageBodyVisible('%s', %s, %s);",
839                mTemplates.getMessageDomId(item.getMessage()), item.isExpanded(), h));
840
841        mViewState.setExpansionState(item.getMessage(),
842                item.isExpanded() ? ExpansionState.EXPANDED : ExpansionState.COLLAPSED);
843    }
844
845    @Override
846    public void showExternalResources(final Message msg) {
847        mViewState.setShouldShowImages(msg, true);
848        mWebView.getSettings().setBlockNetworkImage(false);
849        mWebView.loadUrl("javascript:unblockImages(['" + mTemplates.getMessageDomId(msg) + "']);");
850    }
851
852    @Override
853    public void showExternalResources(final String senderRawAddress) {
854        mWebView.getSettings().setBlockNetworkImage(false);
855
856        final Address sender = getAddress(senderRawAddress);
857        final MessageCursor cursor = getMessageCursor();
858
859        final List<String> messageDomIds = new ArrayList<String>();
860
861        int pos = -1;
862        while (cursor.moveToPosition(++pos)) {
863            final ConversationMessage message = cursor.getMessage();
864            if (sender.equals(getAddress(message.getFrom()))) {
865                message.alwaysShowImages = true;
866
867                mViewState.setShouldShowImages(message, true);
868                messageDomIds.add(mTemplates.getMessageDomId(message));
869            }
870        }
871
872        final String url = String.format(
873                "javascript:unblockImages(['%s']);", TextUtils.join("','", messageDomIds));
874        mWebView.loadUrl(url);
875    }
876
877    @Override
878    public boolean supportsMessageTransforms() {
879        return true;
880    }
881
882    @Override
883    public String getMessageTransforms(final Message msg) {
884        final String domId = mTemplates.getMessageDomId(msg);
885        return (domId == null) ? null : mMessageTransforms.get(domId);
886    }
887
888    // END message header callbacks
889
890    @Override
891    public void showUntransformedConversation() {
892        super.showUntransformedConversation();
893        renderConversation(getMessageCursor());
894    }
895
896    @Override
897    public void onSuperCollapsedClick(SuperCollapsedBlockItem item) {
898        MessageCursor cursor = getMessageCursor();
899        if (cursor == null || !mViewsCreated) {
900            return;
901        }
902
903        mTempBodiesHtml = renderCollapsedHeaders(cursor, item);
904        mWebView.loadUrl("javascript:replaceSuperCollapsedBlock(" + item.getStart() + ")");
905    }
906
907    private void showNewMessageNotification(NewMessagesInfo info) {
908        final TextView descriptionView = (TextView) mNewMessageBar.findViewById(
909                R.id.new_message_description);
910        descriptionView.setText(info.getNotificationText());
911        mNewMessageBar.setVisibility(View.VISIBLE);
912    }
913
914    private void onNewMessageBarClick() {
915        mNewMessageBar.setVisibility(View.GONE);
916
917        renderConversation(getMessageCursor()); // mCursor is already up-to-date
918                                                // per onLoadFinished()
919    }
920
921    private static OverlayPosition[] parsePositions(final String[] topArray,
922            final String[] bottomArray) {
923        final int len = topArray.length;
924        final OverlayPosition[] positions = new OverlayPosition[len];
925        for (int i = 0; i < len; i++) {
926            positions[i] = new OverlayPosition(
927                    Integer.parseInt(topArray[i]), Integer.parseInt(bottomArray[i]));
928        }
929        return positions;
930    }
931
932    private Address getAddress(String rawFrom) {
933        Address addr;
934        synchronized (mAddressCache) {
935            addr = mAddressCache.get(rawFrom);
936            if (addr == null) {
937                addr = Address.getEmailAddress(rawFrom);
938                mAddressCache.put(rawFrom, addr);
939            }
940        }
941        return addr;
942    }
943
944    private void ensureContentSizeChangeListener() {
945        if (mWebViewSizeChangeListener == null) {
946            mWebViewSizeChangeListener = new ContentSizeChangeListener() {
947                @Override
948                public void onHeightChange(int h) {
949                    // When WebKit says the DOM height has changed, re-measure
950                    // bodies and re-position their headers.
951                    // This is separate from the typical JavaScript DOM change
952                    // listeners because cases like NARROW_COLUMNS text reflow do not trigger DOM
953                    // events.
954                    mWebView.loadUrl("javascript:measurePositions();");
955                }
956            };
957        }
958        mWebView.setContentSizeChangeListener(mWebViewSizeChangeListener);
959    }
960
961    private static boolean isOverviewMode(Account acct) {
962        return acct.settings.isOverviewMode();
963    }
964
965    private void setupOverviewMode() {
966        // for now, overview mode means use the built-in WebView zoom and disable custom scale
967        // gesture handling
968        final boolean overviewMode = isOverviewMode(mAccount);
969        final WebSettings settings = mWebView.getSettings();
970        settings.setUseWideViewPort(overviewMode);
971
972        final OnScaleGestureListener listener;
973
974        settings.setSupportZoom(overviewMode);
975        settings.setBuiltInZoomControls(overviewMode);
976        if (overviewMode) {
977            settings.setDisplayZoomControls(false);
978        }
979        listener = ENABLE_CSS_ZOOM && !overviewMode ? new CssScaleInterceptor() : null;
980
981        mWebView.setOnScaleGestureListener(listener);
982    }
983
984    private class ConversationWebViewClient extends AbstractConversationWebViewClient {
985        @Override
986        public void onPageFinished(WebView view, String url) {
987            // Ignore unsafe calls made after a fragment is detached from an activity.
988            // This method needs to, for example, get at the loader manager, which needs
989            // the fragment to be added.
990            if (!isAdded() || !mViewsCreated) {
991                LogUtils.i(LOG_TAG, "ignoring CVF.onPageFinished, url=%s fragment=%s", url,
992                        ConversationViewFragment.this);
993                return;
994            }
995
996            LogUtils.i(LOG_TAG, "IN CVF.onPageFinished, url=%s fragment=%s wv=%s t=%sms", url,
997                    ConversationViewFragment.this, view,
998                    (SystemClock.uptimeMillis() - mWebViewLoadStartMs));
999
1000            ensureContentSizeChangeListener();
1001
1002            if (!mEnableContentReadySignal) {
1003                revealConversation();
1004            }
1005
1006            final Set<String> emailAddresses = Sets.newHashSet();
1007            final List<Address> cacheCopy;
1008            synchronized (mAddressCache) {
1009                cacheCopy = ImmutableList.copyOf(mAddressCache.values());
1010            }
1011            for (Address addr : cacheCopy) {
1012                emailAddresses.add(addr.getAddress());
1013            }
1014            ContactLoaderCallbacks callbacks = getContactInfoSource();
1015            getContactInfoSource().setSenders(emailAddresses);
1016            getLoaderManager().restartLoader(CONTACT_LOADER, Bundle.EMPTY, callbacks);
1017        }
1018
1019        @Override
1020        public boolean shouldOverrideUrlLoading(WebView view, String url) {
1021            return mViewsCreated && super.shouldOverrideUrlLoading(view, url);
1022        }
1023    }
1024
1025    /**
1026     * NOTE: all public methods must be listed in the proguard flags so that they can be accessed
1027     * via reflection and not stripped.
1028     *
1029     */
1030    private class MailJsBridge {
1031
1032        @SuppressWarnings("unused")
1033        @JavascriptInterface
1034        public void onWebContentGeometryChange(final String[] overlayTopStrs,
1035                final String[] overlayBottomStrs) {
1036            getHandler().post(new FragmentRunnable("onWebContentGeometryChange") {
1037
1038                @Override
1039                public void go() {
1040                    try {
1041                        if (!mViewsCreated) {
1042                            LogUtils.d(LOG_TAG, "ignoring webContentGeometryChange because views"
1043                                    + " are gone, %s", ConversationViewFragment.this);
1044                            return;
1045                        }
1046                        mConversationContainer.onGeometryChange(
1047                                parsePositions(overlayTopStrs, overlayBottomStrs));
1048                        if (mDiff != 0) {
1049                            // SCROLL!
1050                            int scale = (int) (mWebView.getScale() / mWebView.getInitialScale());
1051                            if (scale > 1) {
1052                                mWebView.scrollBy(0, (mDiff * (scale - 1)));
1053                            }
1054                            mDiff = 0;
1055                        }
1056                    } catch (Throwable t) {
1057                        LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onWebContentGeometryChange");
1058                    }
1059                }
1060            });
1061        }
1062
1063        @SuppressWarnings("unused")
1064        @JavascriptInterface
1065        public String getTempMessageBodies() {
1066            try {
1067                if (!mViewsCreated) {
1068                    return "";
1069                }
1070
1071                final String s = mTempBodiesHtml;
1072                mTempBodiesHtml = null;
1073                return s;
1074            } catch (Throwable t) {
1075                LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.getTempMessageBodies");
1076                return "";
1077            }
1078        }
1079
1080        @SuppressWarnings("unused")
1081        @JavascriptInterface
1082        public String getMessageBody(String domId) {
1083            try {
1084                final MessageCursor cursor = getMessageCursor();
1085                if (!mViewsCreated || cursor == null) {
1086                    return "";
1087                }
1088
1089                int pos = -1;
1090                while (cursor.moveToPosition(++pos)) {
1091                    final ConversationMessage msg = cursor.getMessage();
1092                    if (TextUtils.equals(domId, mTemplates.getMessageDomId(msg))) {
1093                        return msg.getBodyAsHtml();
1094                    }
1095                }
1096
1097                return "";
1098
1099            } catch (Throwable t) {
1100                LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.getMessageBody");
1101                return "";
1102            }
1103        }
1104
1105        @SuppressWarnings("unused")
1106        @JavascriptInterface
1107        public String getMessageSender(String domId) {
1108            try {
1109                final MessageCursor cursor = getMessageCursor();
1110                if (!mViewsCreated || cursor == null) {
1111                    return "";
1112                }
1113
1114                int pos = -1;
1115                while (cursor.moveToPosition(++pos)) {
1116                    final ConversationMessage msg = cursor.getMessage();
1117                    if (TextUtils.equals(domId, mTemplates.getMessageDomId(msg))) {
1118                        return getAddress(msg.getFrom()).getAddress();
1119                    }
1120                }
1121
1122                return "";
1123
1124            } catch (Throwable t) {
1125                LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.getMessageSender");
1126                return "";
1127            }
1128        }
1129
1130        @SuppressWarnings("unused")
1131        @JavascriptInterface
1132        public void onContentReady() {
1133            getHandler().post(new FragmentRunnable("onContentReady") {
1134                @Override
1135                public void go() {
1136                    try {
1137                        if (mWebViewLoadStartMs != 0) {
1138                            LogUtils.i(LOG_TAG, "IN CVF.onContentReady, f=%s vis=%s t=%sms",
1139                                    ConversationViewFragment.this,
1140                                    isUserVisible(),
1141                                    (SystemClock.uptimeMillis() - mWebViewLoadStartMs));
1142                        }
1143                        revealConversation();
1144                    } catch (Throwable t) {
1145                        LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onContentReady");
1146                        // Still try to show the conversation.
1147                        revealConversation();
1148                    }
1149                }
1150            });
1151        }
1152
1153        @SuppressWarnings("unused")
1154        @JavascriptInterface
1155        public float getScrollYPercent() {
1156            try {
1157                return mWebViewYPercent;
1158            } catch (Throwable t) {
1159                LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.getScrollYPercent");
1160                return 0f;
1161            }
1162        }
1163
1164        @SuppressWarnings("unused")
1165        @JavascriptInterface
1166        public void onMessageTransform(String messageDomId, String transformText) {
1167            try {
1168                LogUtils.i(LOG_TAG, "TRANSFORM: (%s) %s", messageDomId, transformText);
1169                mMessageTransforms.put(messageDomId, transformText);
1170                onConversationTransformed();
1171            } catch (Throwable t) {
1172                LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onMessageTransform");
1173                return;
1174            }
1175        }
1176    }
1177
1178    private class NewMessagesInfo {
1179        int count;
1180        int countFromSelf;
1181        String senderAddress;
1182
1183        /**
1184         * Return the display text for the new message notification overlay. It will be formatted
1185         * appropriately for a single new message vs. multiple new messages.
1186         *
1187         * @return display text
1188         */
1189        public String getNotificationText() {
1190            Resources res = getResources();
1191            if (count > 1) {
1192                return res.getString(R.string.new_incoming_messages_many, count);
1193            } else {
1194                final Address addr = getAddress(senderAddress);
1195                return res.getString(R.string.new_incoming_messages_one,
1196                        TextUtils.isEmpty(addr.getName()) ? addr.getAddress() : addr.getName());
1197            }
1198        }
1199    }
1200
1201    @Override
1202    public void onMessageCursorLoadFinished(Loader<Cursor> loader, MessageCursor newCursor,
1203            MessageCursor oldCursor) {
1204        /*
1205         * what kind of changes affect the MessageCursor? 1. new message(s) 2.
1206         * read/unread state change 3. deleted message, either regular or draft
1207         * 4. updated message, either from self or from others, updated in
1208         * content or state or sender 5. star/unstar of message (technically
1209         * similar to #1) 6. other label change Use MessageCursor.hashCode() to
1210         * sort out interesting vs. no-op cursor updates.
1211         */
1212
1213        if (oldCursor != null && !oldCursor.isClosed()) {
1214            final NewMessagesInfo info = getNewIncomingMessagesInfo(newCursor);
1215
1216            if (info.count > 0) {
1217                // don't immediately render new incoming messages from other
1218                // senders
1219                // (to avoid a new message from losing the user's focus)
1220                LogUtils.i(LOG_TAG, "CONV RENDER: conversation updated"
1221                        + ", holding cursor for new incoming message (%s)", this);
1222                showNewMessageNotification(info);
1223                return;
1224            }
1225
1226            final int oldState = oldCursor.getStateHashCode();
1227            final boolean changed = newCursor.getStateHashCode() != oldState;
1228
1229            if (!changed) {
1230                final boolean processedInPlace = processInPlaceUpdates(newCursor, oldCursor);
1231                if (processedInPlace) {
1232                    LogUtils.i(LOG_TAG, "CONV RENDER: processed update(s) in place (%s)", this);
1233                } else {
1234                    LogUtils.i(LOG_TAG, "CONV RENDER: uninteresting update"
1235                            + ", ignoring this conversation update (%s)", this);
1236                }
1237                return;
1238            } else if (info.countFromSelf == 1) {
1239                // Special-case the very common case of a new cursor that is the same as the old
1240                // one, except that there is a new message from yourself. This happens upon send.
1241                final boolean sameExceptNewLast = newCursor.getStateHashCode(1) == oldState;
1242                if (sameExceptNewLast) {
1243                    LogUtils.i(LOG_TAG, "CONV RENDER: update is a single new message from self"
1244                            + " (%s)", this);
1245                    newCursor.moveToLast();
1246                    processNewOutgoingMessage(newCursor.getMessage());
1247                    return;
1248                }
1249            }
1250            // cursors are different, and not due to an incoming message. fall
1251            // through and render.
1252            LogUtils.i(LOG_TAG, "CONV RENDER: conversation updated"
1253                    + ", but not due to incoming message. rendering. (%s)", this);
1254
1255            if (DEBUG_DUMP_CURSOR_CONTENTS) {
1256                LogUtils.i(LOG_TAG, "old cursor: %s", oldCursor.getDebugDump());
1257                LogUtils.i(LOG_TAG, "new cursor: %s", newCursor.getDebugDump());
1258            }
1259        } else {
1260            LogUtils.i(LOG_TAG, "CONV RENDER: initial render. (%s)", this);
1261            timerMark("message cursor load finished");
1262        }
1263
1264        // if layout hasn't happened, delay render
1265        // This is needed in addition to the showConversation() delay to speed
1266        // up rotation and restoration.
1267        if (mConversationContainer.getWidth() == 0) {
1268            mNeedRender = true;
1269            mConversationContainer.addOnLayoutChangeListener(this);
1270        } else {
1271            renderConversation(newCursor);
1272        }
1273    }
1274
1275    private NewMessagesInfo getNewIncomingMessagesInfo(MessageCursor newCursor) {
1276        final NewMessagesInfo info = new NewMessagesInfo();
1277
1278        int pos = -1;
1279        while (newCursor.moveToPosition(++pos)) {
1280            final Message m = newCursor.getMessage();
1281            if (!mViewState.contains(m)) {
1282                LogUtils.i(LOG_TAG, "conversation diff: found new msg: %s", m.uri);
1283
1284                final Address from = getAddress(m.getFrom());
1285                // distinguish ours from theirs
1286                // new messages from the account owner should not trigger a
1287                // notification
1288                if (mAccount.ownsFromAddress(from.getAddress())) {
1289                    LogUtils.i(LOG_TAG, "found message from self: %s", m.uri);
1290                    info.countFromSelf++;
1291                    continue;
1292                }
1293
1294                info.count++;
1295                info.senderAddress = m.getFrom();
1296            }
1297        }
1298        return info;
1299    }
1300
1301    private boolean processInPlaceUpdates(MessageCursor newCursor, MessageCursor oldCursor) {
1302        final Set<String> idsOfChangedBodies = Sets.newHashSet();
1303        final List<Integer> changedOverlayPositions = Lists.newArrayList();
1304
1305        boolean changed = false;
1306
1307        int pos = 0;
1308        while (true) {
1309            if (!newCursor.moveToPosition(pos) || !oldCursor.moveToPosition(pos)) {
1310                break;
1311            }
1312
1313            final ConversationMessage newMsg = newCursor.getMessage();
1314            final ConversationMessage oldMsg = oldCursor.getMessage();
1315
1316            if (!TextUtils.equals(newMsg.getFrom(), oldMsg.getFrom()) ||
1317                    newMsg.isSending != oldMsg.isSending) {
1318                mAdapter.updateItemsForMessage(newMsg, changedOverlayPositions);
1319                LogUtils.i(LOG_TAG, "msg #%d (%d): detected from/sending change. isSending=%s",
1320                        pos, newMsg.id, newMsg.isSending);
1321            }
1322
1323            // update changed message bodies in-place
1324            if (!TextUtils.equals(newMsg.bodyHtml, oldMsg.bodyHtml) ||
1325                    !TextUtils.equals(newMsg.bodyText, oldMsg.bodyText)) {
1326                // maybe just set a flag to notify JS to re-request changed bodies
1327                idsOfChangedBodies.add('"' + mTemplates.getMessageDomId(newMsg) + '"');
1328                LogUtils.i(LOG_TAG, "msg #%d (%d): detected body change", pos, newMsg.id);
1329            }
1330
1331            pos++;
1332        }
1333
1334
1335        if (!changedOverlayPositions.isEmpty()) {
1336            // notify once after the entire adapter is updated
1337            mConversationContainer.onOverlayModelUpdate(changedOverlayPositions);
1338            changed = true;
1339        }
1340
1341        if (!idsOfChangedBodies.isEmpty()) {
1342            mWebView.loadUrl(String.format("javascript:replaceMessageBodies([%s]);",
1343                    TextUtils.join(",", idsOfChangedBodies)));
1344            changed = true;
1345        }
1346
1347        return changed;
1348    }
1349
1350    private void processNewOutgoingMessage(ConversationMessage msg) {
1351        mTemplates.reset();
1352        // this method will add some items to mAdapter, but we deliberately want to avoid notifying
1353        // adapter listeners (i.e. ConversationContainer) until onWebContentGeometryChange is next
1354        // called, to prevent N+1 headers rendering with N message bodies.
1355        renderMessage(msg, true /* expanded */, msg.alwaysShowImages);
1356        mTempBodiesHtml = mTemplates.emit();
1357
1358        mViewState.setExpansionState(msg, ExpansionState.EXPANDED);
1359        // FIXME: should the provider set this as initial state?
1360        mViewState.setReadState(msg, false /* read */);
1361
1362        // From now until the updated spacer geometry is returned, the adapter items are mismatched
1363        // with the existing spacers. Do not let them layout.
1364        mConversationContainer.invalidateSpacerGeometry();
1365
1366        mWebView.loadUrl("javascript:appendMessageHtml();");
1367    }
1368
1369    private class SetCookieTask extends AsyncTask<Void, Void, Void> {
1370        final String mUri;
1371        final Uri mAccountCookieQueryUri;
1372        final ContentResolver mResolver;
1373
1374        SetCookieTask(Context context, Uri baseUri, Uri accountCookieQueryUri) {
1375            mUri = baseUri.toString();
1376            mAccountCookieQueryUri = accountCookieQueryUri;
1377            mResolver = context.getContentResolver();
1378        }
1379
1380        @Override
1381        public Void doInBackground(Void... args) {
1382            // First query for the coookie string from the UI provider
1383            final Cursor cookieCursor = mResolver.query(mAccountCookieQueryUri,
1384                    UIProvider.ACCOUNT_COOKIE_PROJECTION, null, null, null);
1385            if (cookieCursor == null) {
1386                return null;
1387            }
1388
1389            try {
1390                if (cookieCursor.moveToFirst()) {
1391                    final String cookie = cookieCursor.getString(
1392                            cookieCursor.getColumnIndex(UIProvider.AccountCookieColumns.COOKIE));
1393
1394                    if (cookie != null) {
1395                        final CookieSyncManager csm =
1396                            CookieSyncManager.createInstance(getContext());
1397                        CookieManager.getInstance().setCookie(mUri, cookie);
1398                        csm.sync();
1399                    }
1400                }
1401
1402            } finally {
1403                cookieCursor.close();
1404            }
1405
1406
1407            return null;
1408        }
1409    }
1410
1411    @Override
1412    public void onConversationUpdated(Conversation conv) {
1413        final ConversationViewHeader headerView = (ConversationViewHeader) mConversationContainer
1414                .findViewById(R.id.conversation_header);
1415        mConversation = conv;
1416        if (headerView != null) {
1417            headerView.onConversationUpdated(conv);
1418            headerView.setSubject(conv.subject);
1419        }
1420    }
1421
1422    @Override
1423    public void onLayoutChange(View v, int left, int top, int right,
1424            int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
1425        boolean sizeChanged = mNeedRender
1426                && mConversationContainer.getWidth() != 0;
1427        if (sizeChanged) {
1428            mNeedRender = false;
1429            mConversationContainer.removeOnLayoutChangeListener(this);
1430            renderConversation(getMessageCursor());
1431        }
1432    }
1433
1434    @Override
1435    public void setMessageDetailsExpanded(MessageHeaderItem i, boolean expanded,
1436            int heightBefore) {
1437        mDiff = (expanded ? 1 : -1) * Math.abs(i.getHeight() - heightBefore);
1438    }
1439
1440    private class CssScaleInterceptor implements OnScaleGestureListener {
1441
1442        private float getFocusXWebPx(ScaleGestureDetector detector) {
1443            return (detector.getFocusX() - mSideMarginPx) / mWebView.getInitialScale();
1444        }
1445
1446        private float getFocusYWebPx(ScaleGestureDetector detector) {
1447            return detector.getFocusY() / mWebView.getInitialScale();
1448        }
1449
1450        @Override
1451        public boolean onScale(ScaleGestureDetector detector) {
1452            mWebView.loadUrl(String.format("javascript:onScale(%s, %s, %s);",
1453                    detector.getScaleFactor(), getFocusXWebPx(detector),
1454                    getFocusYWebPx(detector)));
1455            return false;
1456        }
1457
1458        @Override
1459        public boolean onScaleBegin(ScaleGestureDetector detector) {
1460            mWebView.loadUrl(String.format("javascript:onScaleBegin(%s, %s);",
1461                    getFocusXWebPx(detector), getFocusYWebPx(detector)));
1462            return true;
1463        }
1464
1465        @Override
1466        public void onScaleEnd(ScaleGestureDetector detector) {
1467            mWebView.loadUrl(String.format("javascript:onScaleEnd(%s, %s);",
1468                    getFocusXWebPx(detector), getFocusYWebPx(detector)));
1469        }
1470
1471    }
1472
1473}
1474