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