ConversationViewFragment.java revision ba283732e7cbcc55a3dbc8ab78950cc38cd078fb
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 20import android.app.Activity; 21import android.app.Fragment; 22import android.app.LoaderManager; 23import android.content.ActivityNotFoundException; 24import android.content.Context; 25import android.content.CursorLoader; 26import android.content.Intent; 27import android.content.Loader; 28import android.database.Cursor; 29import android.net.Uri; 30import android.os.Bundle; 31import android.os.Handler; 32import android.provider.Browser; 33import android.view.LayoutInflater; 34import android.view.Menu; 35import android.view.MenuInflater; 36import android.view.MenuItem; 37import android.view.View; 38import android.view.ViewGroup; 39import android.webkit.ConsoleMessage; 40import android.webkit.WebChromeClient; 41import android.webkit.WebSettings; 42import android.webkit.WebView; 43import android.webkit.WebViewClient; 44 45import com.android.mail.R; 46import com.android.mail.browse.ConversationContainer; 47import com.android.mail.browse.ConversationOverlayItem; 48import com.android.mail.browse.ConversationViewAdapter; 49import com.android.mail.browse.ConversationViewAdapter.MessageFooterItem; 50import com.android.mail.browse.ConversationViewAdapter.MessageHeaderItem; 51import com.android.mail.browse.ConversationViewAdapter.SuperCollapsedBlockItem; 52import com.android.mail.browse.ConversationViewHeader; 53import com.android.mail.browse.ConversationWebView; 54import com.android.mail.browse.MessageCursor; 55import com.android.mail.browse.MessageFooterView; 56import com.android.mail.browse.MessageHeaderView.MessageHeaderViewCallbacks; 57import com.android.mail.browse.SuperCollapsedBlock; 58import com.android.mail.providers.Account; 59import com.android.mail.providers.Address; 60import com.android.mail.providers.Conversation; 61import com.android.mail.providers.Folder; 62import com.android.mail.providers.ListParams; 63import com.android.mail.providers.Message; 64import com.android.mail.providers.Settings; 65import com.android.mail.providers.UIProvider; 66import com.android.mail.providers.UIProvider.AccountCapabilities; 67import com.android.mail.providers.UIProvider.FolderCapabilities; 68import com.android.mail.utils.LogTag; 69import com.android.mail.utils.LogUtils; 70import com.android.mail.utils.Utils; 71import com.google.common.collect.Lists; 72import com.google.common.collect.Maps; 73 74import java.util.List; 75import java.util.Map; 76 77 78/** 79 * The conversation view UI component. 80 */ 81public final class ConversationViewFragment extends Fragment implements 82 LoaderManager.LoaderCallbacks<Cursor>, 83 ConversationViewHeader.ConversationViewHeaderCallbacks, 84 MessageHeaderViewCallbacks, 85 SuperCollapsedBlock.OnClickListener { 86 87 private static final String LOG_TAG = LogTag.getLogTag(); 88 public static final String LAYOUT_TAG = "ConvLayout"; 89 90 private static final int MESSAGE_LOADER_ID = 0; 91 92 private ControllableActivity mActivity; 93 94 private Context mContext; 95 96 private Conversation mConversation; 97 98 private ConversationContainer mConversationContainer; 99 100 private Account mAccount; 101 102 private ConversationWebView mWebView; 103 104 private HtmlConversationTemplates mTemplates; 105 106 private String mBaseUri; 107 108 private final Handler mHandler = new Handler(); 109 110 private final MailJsBridge mJsBridge = new MailJsBridge(); 111 112 private final WebViewClient mWebViewClient = new ConversationWebViewClient(); 113 114 private ConversationViewAdapter mAdapter; 115 private MessageCursor mCursor; 116 117 private boolean mViewsCreated; 118 119 private MenuItem mChangeFoldersMenuItem; 120 121 private float mDensity; 122 123 /** 124 * Folder is used to help determine valid menu actions for this conversation. 125 */ 126 private Folder mFolder; 127 128 private final Map<String, Address> mAddressCache = Maps.newHashMap(); 129 130 /** 131 * Temporary string containing the message bodies of the messages within a super-collapsed 132 * block, for one-time use during block expansion. We cannot easily pass the body HTML 133 * into JS without problematic escaping, so hold onto it momentarily and signal JS to fetch it 134 * using {@link MailJsBridge}. 135 */ 136 private String mTempBodiesHtml; 137 138 private boolean mUserVisible; 139 140 private int mMaxAutoLoadMessages; 141 142 private boolean mDeferredConversationLoad; 143 144 private static final String ARG_ACCOUNT = "account"; 145 public static final String ARG_CONVERSATION = "conversation"; 146 private static final String ARG_FOLDER = "folder"; 147 148 private static final boolean DEBUG_DUMP_CONVERSATION_HTML = false; 149 150 /** 151 * Constructor needs to be public to handle orientation changes and activity lifecycle events. 152 */ 153 public ConversationViewFragment() { 154 super(); 155 } 156 157 /** 158 * Creates a new instance of {@link ConversationViewFragment}, initialized 159 * to display a conversation with other parameters inherited/copied from an existing bundle, 160 * typically one created using {@link #makeBasicArgs}. 161 */ 162 public static ConversationViewFragment newInstance(Bundle existingArgs, 163 Conversation conversation) { 164 ConversationViewFragment f = new ConversationViewFragment(); 165 Bundle args = new Bundle(existingArgs); 166 args.putParcelable(ARG_CONVERSATION, conversation); 167 f.setArguments(args); 168 return f; 169 } 170 171 public static Bundle makeBasicArgs(Account account, Folder folder) { 172 Bundle args = new Bundle(); 173 args.putParcelable(ARG_ACCOUNT, account); 174 args.putParcelable(ARG_FOLDER, folder); 175 return args; 176 } 177 178 @Override 179 public void onActivityCreated(Bundle savedInstanceState) { 180 LogUtils.d(LOG_TAG, "IN CVF.onActivityCreated, this=%s subj=%s", this, 181 mConversation.subject); 182 super.onActivityCreated(savedInstanceState); 183 // Strictly speaking, we get back an android.app.Activity from getActivity. However, the 184 // only activity creating a ConversationListContext is a MailActivity which is of type 185 // ControllableActivity, so this cast should be safe. If this cast fails, some other 186 // activity is creating ConversationListFragments. This activity must be of type 187 // ControllableActivity. 188 final Activity activity = getActivity(); 189 if (!(activity instanceof ControllableActivity)) { 190 LogUtils.wtf(LOG_TAG, "ConversationViewFragment expects only a ControllableActivity to" 191 + "create it. Cannot proceed."); 192 } 193 mActivity = (ControllableActivity) activity; 194 mContext = mActivity.getApplicationContext(); 195 if (mActivity.isFinishing()) { 196 // Activity is finishing, just bail. 197 return; 198 } 199 mTemplates = new HtmlConversationTemplates(mContext); 200 201 mAdapter = new ConversationViewAdapter(mActivity.getActivityContext(), mAccount, 202 getLoaderManager(), this, this, this, mAddressCache); 203 mConversationContainer.setOverlayAdapter(mAdapter); 204 205 mDensity = getResources().getDisplayMetrics().density; 206 207 mMaxAutoLoadMessages = getResources().getInteger(R.integer.max_auto_load_messages); 208 209 showConversation(); 210 } 211 212 @Override 213 public void onCreate(Bundle savedState) { 214 LogUtils.d(LOG_TAG, "onCreate in ConversationViewFragment (this=%s)", this); 215 super.onCreate(savedState); 216 217 Bundle args = getArguments(); 218 mAccount = args.getParcelable(ARG_ACCOUNT); 219 mConversation = args.getParcelable(ARG_CONVERSATION); 220 mFolder = args.getParcelable(ARG_FOLDER); 221 mBaseUri = "x-thread://" + mAccount.name + "/" + mConversation.id; 222 223 // Not really, we just want to get a crack to store a reference to the change_folder item 224 setHasOptionsMenu(true); 225 } 226 227 @Override 228 public View onCreateView(LayoutInflater inflater, 229 ViewGroup container, Bundle savedInstanceState) { 230 View rootView = inflater.inflate(R.layout.conversation_view, container, false); 231 mConversationContainer = (ConversationContainer) rootView 232 .findViewById(R.id.conversation_container); 233 mWebView = (ConversationWebView) mConversationContainer.findViewById(R.id.webview); 234 235 mWebView.addJavascriptInterface(mJsBridge, "mail"); 236 mWebView.setWebViewClient(mWebViewClient); 237 mWebView.setWebChromeClient(new WebChromeClient() { 238 @Override 239 public boolean onConsoleMessage(ConsoleMessage consoleMessage) { 240 LogUtils.i(LOG_TAG, "JS: %s (%s:%d)", consoleMessage.message(), 241 consoleMessage.sourceId(), consoleMessage.lineNumber()); 242 return true; 243 } 244 }); 245 246 final WebSettings settings = mWebView.getSettings(); 247 248 settings.setJavaScriptEnabled(true); 249 settings.setUseWideViewPort(true); 250 251 settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NORMAL); 252 253 settings.setSupportZoom(true); 254 settings.setBuiltInZoomControls(true); 255 settings.setDisplayZoomControls(false); 256 257 final float fontScale = getResources().getConfiguration().fontScale; 258 final int desiredFontSizePx = getResources() 259 .getInteger(R.integer.conversation_desired_font_size_px); 260 final int unstyledFontSizePx = getResources() 261 .getInteger(R.integer.conversation_unstyled_font_size_px); 262 263 int textZoom = settings.getTextZoom(); 264 // apply a correction to the default body text style to get regular text to the size we want 265 textZoom = textZoom * desiredFontSizePx / unstyledFontSizePx; 266 // then apply any system font scaling 267 textZoom = (int) (textZoom * fontScale); 268 settings.setTextZoom(textZoom); 269 270 mViewsCreated = true; 271 272 return rootView; 273 } 274 275 @Override 276 public void onDestroyView() { 277 super.onDestroyView(); 278 mConversationContainer.setOverlayAdapter(null); 279 mAdapter = null; 280 mViewsCreated = false; 281 } 282 283 @Override 284 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 285 super.onCreateOptionsMenu(menu, inflater); 286 287 mChangeFoldersMenuItem = menu.findItem(R.id.change_folder); 288 } 289 290 @Override 291 public void onPrepareOptionsMenu(Menu menu) { 292 super.onPrepareOptionsMenu(menu); 293 boolean showMarkImportant = !mConversation.isImportant(); 294 Utils.setMenuItemVisibility( 295 menu, 296 R.id.mark_important, 297 showMarkImportant 298 && mAccount 299 .supportsCapability(UIProvider.AccountCapabilities.MARK_IMPORTANT)); 300 Utils.setMenuItemVisibility( 301 menu, 302 R.id.mark_not_important, 303 !showMarkImportant 304 && mAccount 305 .supportsCapability(UIProvider.AccountCapabilities.MARK_IMPORTANT)); 306 // TODO(mindyp) show/ hide spam and mute based on conversation 307 // properties to be added. 308 Utils.setMenuItemVisibility(menu, R.id.archive, 309 mAccount.supportsCapability(AccountCapabilities.ARCHIVE) && mFolder != null 310 && mFolder.supportsCapability(FolderCapabilities.ARCHIVE)); 311 Utils.setMenuItemVisibility(menu, R.id.report_spam, 312 mAccount.supportsCapability(AccountCapabilities.REPORT_SPAM) && mFolder != null 313 && mFolder.supportsCapability(FolderCapabilities.REPORT_SPAM) 314 && !mConversation.spam); 315 Utils.setMenuItemVisibility( 316 menu, 317 R.id.mute, 318 mAccount.supportsCapability(AccountCapabilities.MUTE) && mFolder != null 319 && mFolder.supportsCapability(FolderCapabilities.DESTRUCTIVE_MUTE) 320 && !mConversation.muted); 321 } 322 323 /** 324 * {@link #setUserVisibleHint(boolean)} only works on API >= 15, so implement our own for 325 * reliability on older platforms. 326 */ 327 public void setExtraUserVisibleHint(boolean isVisibleToUser) { 328 LogUtils.v(LOG_TAG, "in CVF.setHint, val=%s (%s)", isVisibleToUser, this); 329 330 if (mUserVisible != isVisibleToUser) { 331 mUserVisible = isVisibleToUser; 332 333 if (isVisibleToUser && mViewsCreated) { 334 335 if (mCursor == null && mDeferredConversationLoad) { 336 // load 337 LogUtils.v(LOG_TAG, "Fragment is now user-visible, showing conversation: %s", 338 mConversation.uri); 339 showConversation(); 340 mDeferredConversationLoad = false; 341 } else { 342 onConversationSeen(); 343 } 344 345 } 346 } 347 } 348 349 /** 350 * Handles a request to show a new conversation list, either from a search query or for viewing 351 * a folder. This will initiate a data load, and hence must be called on the UI thread. 352 */ 353 private void showConversation() { 354 if (!mUserVisible && mConversation.numMessages > mMaxAutoLoadMessages) { 355 LogUtils.v(LOG_TAG, "Fragment not user-visible, not showing conversation: %s", 356 mConversation.uri); 357 mDeferredConversationLoad = true; 358 return; 359 } 360 LogUtils.v(LOG_TAG, 361 "Fragment is short or user-visible, immediately rendering conversation: %s", 362 mConversation.uri); 363 getLoaderManager().initLoader(MESSAGE_LOADER_ID, Bundle.EMPTY, this); 364 } 365 366 public Conversation getConversation() { 367 return mConversation; 368 } 369 370 @Override 371 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 372 return new MessageLoader(mContext, mConversation.messageListUri, 373 mActivity.getListHandler()); 374 } 375 376 @Override 377 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 378 MessageCursor messageCursor = (MessageCursor) data; 379 380 // ignore truly duplicate results 381 // this can happen when restoring after rotation 382 if (mCursor == messageCursor) { 383 return; 384 } 385 386 // TODO: handle Gmail loading states (like LOADING and ERROR) 387 if (messageCursor.getCount() == 0) { 388 if (mCursor != null) { 389 // TODO: need to exit this view- conversation may have been deleted, or for 390 // whatever reason is now invalid 391 } else { 392 // ignore zero-sized cursors during initial load 393 } 394 return; 395 } 396 397 // TODO: if this is not user-visible, delay render until user-visible fragment is done. 398 // This is needed in addition to the showConversation() delay to speed up rotation and 399 // restoration. 400 401 renderConversation(messageCursor); 402 } 403 404 @Override 405 public void onLoaderReset(Loader<Cursor> loader) { 406 mCursor = null; 407 // TODO: null out all Message.mMessageCursor references 408 } 409 410 private void renderConversation(MessageCursor messageCursor) { 411 final String convHtml = renderMessageBodies(messageCursor); 412 413 if (DEBUG_DUMP_CONVERSATION_HTML) { 414 java.io.FileWriter fw = null; 415 try { 416 fw = new java.io.FileWriter("/sdcard/conv" + mConversation.id 417 + ".html"); 418 fw.write(convHtml); 419 } catch (java.io.IOException e) { 420 e.printStackTrace(); 421 } finally { 422 if (fw != null) { 423 try { 424 fw.close(); 425 } catch (java.io.IOException e) { 426 e.printStackTrace(); 427 } 428 } 429 } 430 } 431 432 mWebView.loadDataWithBaseURL(mBaseUri, convHtml, "text/html", "utf-8", null); 433 mCursor = messageCursor; 434 } 435 436 /** 437 * Populate the adapter with overlay views (message headers, super-collapsed blocks, a 438 * conversation header), and return an HTML document with spacer divs inserted for all overlays. 439 * 440 */ 441 private String renderMessageBodies(MessageCursor messageCursor) { 442 int pos = -1; 443 444 LogUtils.d(LOG_TAG, "IN renderMessageBodies, fragment=%s subj=%s", this, 445 mConversation.subject); 446 boolean allowNetworkImages = false; 447 448 // TODO: re-use any existing adapter item state (expanded, details expanded, show pics) 449 final Settings settings = mActivity.getSettings(); 450 if (settings != null) { 451 mAdapter.setDefaultReplyAll(settings.replyBehavior == 452 UIProvider.DefaultReplyBehavior.REPLY_ALL); 453 } 454 // Walk through the cursor and build up an overlay adapter as you go. 455 // Each overlay has an entry in the adapter for easy scroll handling in the container. 456 // Items are not necessarily 1:1 in cursor and adapter because of super-collapsed blocks. 457 // When adding adapter items, also add their heights to help the container later determine 458 // overlay dimensions. 459 460 mAdapter.clear(); 461 462 // We don't need to kick off attachment loaders during this first measurement phase, 463 // so disable them temporarily. 464 MessageFooterView.enableAttachmentLoaders(false); 465 466 // N.B. the units of height for spacers are actually dp and not px because WebView assumes 467 // a pixel is an mdpi pixel, unless you set device-dpi. 468 469 // add a single conversation header item 470 final int convHeaderPos = mAdapter.addConversationHeader(mConversation); 471 final int convHeaderDp = measureOverlayHeight(convHeaderPos); 472 473 mTemplates.startConversation(convHeaderDp); 474 475 int collapsedStart = -1; 476 Message prevCollapsedMsg = null; 477 boolean prevSafeForImages = false; 478 479 while (messageCursor.moveToPosition(++pos)) { 480 final Message msg = messageCursor.getMessage(); 481 482 // TODO: save/restore 'show pics' state 483 final boolean safeForImages = msg.alwaysShowImages /* || savedStateSaysSafe */; 484 allowNetworkImages |= safeForImages; 485 486 final boolean expanded = !msg.read || msg.starred || messageCursor.isLast(); 487 488 if (!expanded) { 489 // contribute to a super-collapsed block that will be emitted just before the next 490 // expanded header 491 if (collapsedStart < 0) { 492 collapsedStart = pos; 493 } 494 prevCollapsedMsg = msg; 495 prevSafeForImages = safeForImages; 496 continue; 497 } 498 499 // resolve any deferred decisions on previous collapsed items 500 if (collapsedStart >= 0) { 501 if (pos - collapsedStart == 1) { 502 // special-case for a single collapsed message: no need to super-collapse it 503 renderMessage(prevCollapsedMsg, false /* expanded */, 504 prevSafeForImages); 505 } else { 506 renderSuperCollapsedBlock(collapsedStart, pos - 1); 507 } 508 prevCollapsedMsg = null; 509 collapsedStart = -1; 510 } 511 512 renderMessage(msg, expanded, safeForImages); 513 } 514 515 // Re-enable attachment loaders 516 MessageFooterView.enableAttachmentLoaders(true); 517 518 mWebView.getSettings().setBlockNetworkImage(!allowNetworkImages); 519 520 return mTemplates.endConversation(mBaseUri, 320); 521 } 522 523 private void renderSuperCollapsedBlock(int start, int end) { 524 final int blockPos = mAdapter.addSuperCollapsedBlock(start, end); 525 final int blockDp = measureOverlayHeight(blockPos); 526 mTemplates.appendSuperCollapsedHtml(start, blockDp); 527 } 528 529 private void renderMessage(Message msg, boolean expanded, boolean safeForImages) { 530 final int headerPos = mAdapter.addMessageHeader(msg, expanded); 531 final MessageHeaderItem headerItem = (MessageHeaderItem) mAdapter.getItem(headerPos); 532 533 final int footerPos = mAdapter.addMessageFooter(headerItem); 534 535 // Measure item header and footer heights to allocate spacers in HTML 536 // But since the views themselves don't exist yet, render each item temporarily into 537 // a host view for measurement. 538 final int headerDp = measureOverlayHeight(headerPos); 539 final int footerDp = measureOverlayHeight(footerPos); 540 541 mTemplates.appendMessageHtml(msg, expanded, safeForImages, 1.0f, headerDp, 542 footerDp); 543 } 544 545 private String renderCollapsedHeaders(MessageCursor cursor, 546 SuperCollapsedBlockItem blockToReplace) { 547 final List<ConversationOverlayItem> replacements = Lists.newArrayList(); 548 549 mTemplates.reset(); 550 551 for (int i = blockToReplace.getStart(), end = blockToReplace.getEnd(); i <= end; i++) { 552 cursor.moveToPosition(i); 553 final Message msg = cursor.getMessage(); 554 final MessageHeaderItem header = mAdapter.newMessageHeaderItem(msg, 555 false /* expanded */); 556 final MessageFooterItem footer = mAdapter.newMessageFooterItem(header); 557 558 final int headerDp = measureOverlayHeight(header); 559 final int footerDp = measureOverlayHeight(footer); 560 561 mTemplates.appendMessageHtml(msg, false /* expanded */, msg.alwaysShowImages, 1.0f, 562 headerDp, footerDp); 563 replacements.add(header); 564 replacements.add(footer); 565 } 566 567 mAdapter.replaceSuperCollapsedBlock(blockToReplace, replacements); 568 569 return mTemplates.emit(); 570 } 571 572 private int measureOverlayHeight(int position) { 573 return measureOverlayHeight(mAdapter.getItem(position)); 574 } 575 576 /** 577 * Measure the height of an adapter view by rendering and adapter item into a temporary 578 * host view, and asking the view to immediately measure itself. This method will reuse 579 * a previous adapter view from {@link ConversationContainer}'s scrap views if one was generated 580 * earlier. 581 * <p> 582 * After measuring the height, this method also saves the height in the 583 * {@link ConversationOverlayItem} for later use in overlay positioning. 584 * 585 * @param convItem adapter item with data to render and measure 586 * @return height in dp of the rendered view 587 */ 588 private int measureOverlayHeight(ConversationOverlayItem convItem) { 589 final int type = convItem.getType(); 590 591 final View convertView = mConversationContainer.getScrapView(type); 592 final View hostView = mAdapter.getView(convItem, convertView, mConversationContainer); 593 if (convertView == null) { 594 mConversationContainer.addScrapView(type, hostView); 595 } 596 597 final int heightPx = mConversationContainer.measureOverlay(hostView); 598 convItem.setHeight(heightPx); 599 convItem.markMeasurementValid(); 600 601 return (int) (heightPx / mDensity); 602 } 603 604 private void onConversationSeen() { 605 // Ignore unsafe calls made after a fragment is detached from an activity 606 final ControllableActivity activity = (ControllableActivity) getActivity(); 607 if (activity == null) { 608 LogUtils.w(LOG_TAG, "ignoring onConversationSeen for conv=%s", mConversation.id); 609 return; 610 } 611 612 // mark as read upon open 613 if (!mConversation.read) { 614 activity.getListHandler().sendConversationRead( 615 AbstractActivityController.TAG_CONVERSATION_LIST, mConversation, true, 616 false /*local*/); 617 mConversation.read = true; 618 } 619 620 activity.onConversationSeen(mConversation); 621 622 final SubjectDisplayChanger sdc = activity.getSubjectDisplayChanger(); 623 if (sdc != null) { 624 sdc.setSubject(mConversation.subject); 625 } 626 } 627 628 // BEGIN conversation header callbacks 629 @Override 630 public void onFoldersClicked() { 631 if (mChangeFoldersMenuItem == null) { 632 LogUtils.e(LOG_TAG, "unable to open 'change folders' dialog for a conversation"); 633 return; 634 } 635 mActivity.onOptionsItemSelected(mChangeFoldersMenuItem); 636 } 637 638 @Override 639 public void onConversationViewHeaderHeightChange(int newHeight) { 640 // TODO: propagate the new height to the header's HTML spacer. This can happen when labels 641 // are added/removed 642 } 643 644 @Override 645 public String getSubjectRemainder(String subject) { 646 final SubjectDisplayChanger sdc = mActivity.getSubjectDisplayChanger(); 647 if (sdc == null) { 648 return subject; 649 } 650 return sdc.getUnshownSubject(subject); 651 } 652 // END conversation header callbacks 653 654 // START message header callbacks 655 @Override 656 public void setMessageSpacerHeight(MessageHeaderItem item, int newSpacerHeightPx) { 657 mConversationContainer.invalidateSpacerGeometry(); 658 659 // update message HTML spacer height 660 LogUtils.i(LAYOUT_TAG, "setting HTML spacer h=%dpx", newSpacerHeightPx); 661 final int heightDp = (int) (newSpacerHeightPx / mDensity); 662 mWebView.loadUrl(String.format("javascript:setMessageHeaderSpacerHeight('%s', %d);", 663 mTemplates.getMessageDomId(item.message), heightDp)); 664 } 665 666 @Override 667 public void setMessageExpanded(MessageHeaderItem item, int newSpacerHeightPx) { 668 mConversationContainer.invalidateSpacerGeometry(); 669 670 // show/hide the HTML message body and update the spacer height 671 LogUtils.i(LAYOUT_TAG, "setting HTML spacer expanded=%s h=%dpx", item.isExpanded(), 672 newSpacerHeightPx); 673 final int heightDp = (int) (newSpacerHeightPx / mDensity); 674 mWebView.loadUrl(String.format("javascript:setMessageBodyVisible('%s', %s, %d);", 675 mTemplates.getMessageDomId(item.message), item.isExpanded(), heightDp)); 676 } 677 678 @Override 679 public void showExternalResources(Message msg) { 680 mWebView.getSettings().setBlockNetworkImage(false); 681 mWebView.loadUrl("javascript:unblockImages('" + mTemplates.getMessageDomId(msg) + "');"); 682 } 683 // END message header callbacks 684 685 @Override 686 public void onSuperCollapsedClick(SuperCollapsedBlockItem item) { 687 if (mCursor == null || !mViewsCreated) { 688 return; 689 } 690 691 mTempBodiesHtml = renderCollapsedHeaders(mCursor, item); 692 mWebView.loadUrl("javascript:replaceSuperCollapsedBlock(" + item.getStart() + ")"); 693 } 694 695 private static class MessageLoader extends CursorLoader { 696 private boolean mDeliveredFirstResults = false; 697 private final ConversationListCallbacks mListController; 698 699 public MessageLoader(Context c, Uri uri, ConversationListCallbacks listController) { 700 super(c, uri, UIProvider.MESSAGE_PROJECTION, null, null, null); 701 mListController = listController; 702 } 703 704 @Override 705 public Cursor loadInBackground() { 706 return new MessageCursor(super.loadInBackground(), mListController); 707 } 708 709 @Override 710 public void deliverResult(Cursor result) { 711 // We want to deliver these results, and then we want to make sure that any subsequent 712 // queries do not hit the network 713 super.deliverResult(result); 714 715 if (!mDeliveredFirstResults) { 716 mDeliveredFirstResults = true; 717 Uri uri = getUri(); 718 719 // Create a ListParams that tells the provider to not hit the network 720 final ListParams listParams = 721 new ListParams(ListParams.NO_LIMIT, false /* useNetwork */); 722 723 // Build the new uri with this additional parameter 724 uri = uri.buildUpon().appendQueryParameter( 725 UIProvider.LIST_PARAMS_QUERY_PARAMETER, listParams.serialize()).build(); 726 setUri(uri); 727 } 728 } 729 } 730 731 private static int[] parseInts(final String[] stringArray) { 732 final int len = stringArray.length; 733 final int[] ints = new int[len]; 734 for (int i = 0; i < len; i++) { 735 ints[i] = Integer.parseInt(stringArray[i]); 736 } 737 return ints; 738 } 739 740 private class ConversationWebViewClient extends WebViewClient { 741 742 @Override 743 public void onPageFinished(WebView view, String url) { 744 LogUtils.i(LOG_TAG, "IN CVF.onPageFinished, url=%s fragment=%s", url, 745 ConversationViewFragment.this); 746 747 super.onPageFinished(view, url); 748 749 // TODO: save off individual message unread state (here, or in onLoadFinished?) so 750 // 'mark unread' restores the original unread state for each individual message 751 752 if (mUserVisible) { 753 onConversationSeen(); 754 } 755 } 756 757 @Override 758 public boolean shouldOverrideUrlLoading(WebView view, String url) { 759 final Activity activity = getActivity(); 760 if (!mViewsCreated || activity == null) { 761 return false; 762 } 763 764 boolean result = false; 765 final Uri uri = Uri.parse(url); 766 Intent intent = new Intent(Intent.ACTION_VIEW, uri); 767 intent.putExtra(Browser.EXTRA_APPLICATION_ID, activity.getPackageName()); 768 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 769 770 // FIXME: give provider a chance to customize url intents? 771 // Utils.addGoogleUriAccountIntentExtras(mContext, uri, mAccount, intent); 772 773 try { 774 activity.startActivity(intent); 775 result = true; 776 } catch (ActivityNotFoundException ex) { 777 // If no application can handle the URL, assume that the 778 // caller can handle it. 779 } 780 781 return result; 782 } 783 784 } 785 786 /** 787 * NOTE: all public methods must be listed in the proguard flags so that they can be accessed 788 * via reflection and not stripped. 789 * 790 */ 791 private class MailJsBridge { 792 793 @SuppressWarnings("unused") 794 public void onWebContentGeometryChange(final String[] overlayBottomStrs) { 795 try { 796 mHandler.post(new Runnable() { 797 @Override 798 public void run() { 799 if (!mViewsCreated) { 800 LogUtils.d(LOG_TAG, "ignoring webContentGeometryChange because views" + 801 " are gone, %s", ConversationViewFragment.this); 802 return; 803 } 804 805 mConversationContainer.onGeometryChange(parseInts(overlayBottomStrs)); 806 } 807 }); 808 } catch (Throwable t) { 809 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onWebContentGeometryChange"); 810 } 811 } 812 813 @SuppressWarnings("unused") 814 public String getTempMessageBodies() { 815 try { 816 if (!mViewsCreated) { 817 return ""; 818 } 819 820 final String s = mTempBodiesHtml; 821 mTempBodiesHtml = null; 822 return s; 823 } catch (Throwable t) { 824 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.getTempMessageBodies"); 825 return ""; 826 } 827 } 828 829 } 830 831} 832