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