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