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