1/** 2 * Copyright (c) 2011, Google Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.mail.browse; 18 19import android.annotation.SuppressLint; 20import android.app.FragmentManager; 21import android.content.AsyncQueryHandler; 22import android.content.Context; 23import android.content.res.Resources; 24import android.database.DataSetObserver; 25import android.graphics.Bitmap; 26import android.graphics.Canvas; 27import android.graphics.Color; 28import android.graphics.Paint; 29import android.graphics.PorterDuff; 30import android.graphics.PorterDuffXfermode; 31import android.support.v4.text.BidiFormatter; 32import android.text.Html; 33import android.text.Spannable; 34import android.text.Spanned; 35import android.text.TextUtils; 36import android.text.method.LinkMovementMethod; 37import android.text.style.URLSpan; 38import android.util.AttributeSet; 39import android.view.LayoutInflater; 40import android.view.Menu; 41import android.view.MenuItem; 42import android.view.View; 43import android.view.View.OnClickListener; 44import android.view.ViewGroup; 45import android.widget.PopupMenu; 46import android.widget.PopupMenu.OnMenuItemClickListener; 47import android.widget.QuickContactBadge; 48import android.widget.TextView; 49import android.widget.Toast; 50 51import com.android.emailcommon.mail.Address; 52import com.android.mail.ContactInfo; 53import com.android.mail.ContactInfoSource; 54import com.android.mail.R; 55import com.android.mail.analytics.Analytics; 56import com.android.mail.browse.ConversationViewAdapter.MessageHeaderItem; 57import com.android.mail.compose.ComposeActivity; 58import com.android.mail.perf.Timer; 59import com.android.mail.photomanager.LetterTileProvider; 60import com.android.mail.print.PrintUtils; 61import com.android.mail.providers.Account; 62import com.android.mail.providers.Conversation; 63import com.android.mail.providers.Message; 64import com.android.mail.providers.Settings; 65import com.android.mail.providers.UIProvider; 66import com.android.mail.text.EmailAddressSpan; 67import com.android.mail.ui.AbstractConversationViewFragment; 68import com.android.mail.ui.ImageCanvas; 69import com.android.mail.utils.LogTag; 70import com.android.mail.utils.LogUtils; 71import com.android.mail.utils.StyleUtils; 72import com.android.mail.utils.Utils; 73import com.android.mail.utils.VeiledAddressMatcher; 74import com.google.common.annotations.VisibleForTesting; 75 76import java.io.IOException; 77import java.io.StringReader; 78import java.util.Map; 79 80public class MessageHeaderView extends SnapHeader implements OnClickListener, 81 OnMenuItemClickListener, ConversationContainer.DetachListener { 82 83 /** 84 * Cap very long recipient lists during summary construction for efficiency. 85 */ 86 private static final int SUMMARY_MAX_RECIPIENTS = 50; 87 88 private static final int MAX_SNIPPET_LENGTH = 100; 89 90 private static final int SHOW_IMAGE_PROMPT_ONCE = 1; 91 private static final int SHOW_IMAGE_PROMPT_ALWAYS = 2; 92 93 private static final String HEADER_RENDER_TAG = "message header render"; 94 private static final String LAYOUT_TAG = "message header layout"; 95 private static final String MEASURE_TAG = "message header measure"; 96 97 private static final String LOG_TAG = LogTag.getLogTag(); 98 99 // This is a debug only feature 100 public static final boolean ENABLE_REPORT_RENDERING_PROBLEM = false; 101 102 private MessageHeaderViewCallbacks mCallbacks; 103 104 private View mBorderView; 105 private ViewGroup mUpperHeaderView; 106 private View mTitleContainer; 107 private View mSnapHeaderBottomBorder; 108 private TextView mSenderNameView; 109 private TextView mRecipientSummary; 110 private TextView mDateView; 111 private View mHideDetailsView; 112 private TextView mSnippetView; 113 private MessageHeaderContactBadge mPhotoView; 114 private ViewGroup mExtraContentView; 115 private ViewGroup mExpandedDetailsView; 116 private SpamWarningView mSpamWarningView; 117 private TextView mImagePromptView; 118 private MessageInviteView mInviteView; 119 private View mForwardButton; 120 private View mOverflowButton; 121 private View mDraftIcon; 122 private View mEditDraftButton; 123 private TextView mUpperDateView; 124 private View mReplyButton; 125 private View mReplyAllButton; 126 private View mAttachmentIcon; 127 private final EmailCopyContextMenu mEmailCopyMenu; 128 129 // temporary fields to reference raw data between initial render and details 130 // expansion 131 private String[] mFrom; 132 private String[] mTo; 133 private String[] mCc; 134 private String[] mBcc; 135 private String[] mReplyTo; 136 137 private boolean mIsDraft = false; 138 139 private int mSendingState; 140 141 private String mSnippet; 142 143 private Address mSender; 144 145 private ContactInfoSource mContactInfoSource; 146 147 private boolean mPreMeasuring; 148 149 private ConversationAccountController mAccountController; 150 151 private Map<String, Address> mAddressCache; 152 153 private boolean mShowImagePrompt; 154 155 private PopupMenu mPopup; 156 157 private MessageHeaderItem mMessageHeaderItem; 158 private ConversationMessage mMessage; 159 160 private boolean mRecipientSummaryValid; 161 private boolean mExpandedDetailsValid; 162 163 private final LayoutInflater mInflater; 164 165 private AsyncQueryHandler mQueryHandler; 166 167 private boolean mObservingContactInfo; 168 169 /** 170 * What I call myself? "me" in English, and internationalized correctly. 171 */ 172 private final String mMyName; 173 174 private final DataSetObserver mContactInfoObserver = new DataSetObserver() { 175 @Override 176 public void onChanged() { 177 updateContactInfo(); 178 } 179 }; 180 181 private boolean mExpandable = true; 182 183 private VeiledAddressMatcher mVeiledMatcher; 184 185 private boolean mIsViewOnlyMode = false; 186 187 private LetterTileProvider mLetterTileProvider; 188 private final int mContactPhotoWidth; 189 private final int mContactPhotoHeight; 190 private final int mTitleContainerMarginEnd; 191 192 /** 193 * The snappy header has special visibility rules (i.e. no details header, 194 * even though it has an expanded appearance) 195 */ 196 private boolean mIsSnappy; 197 198 private BidiFormatter mBidiFormatter; 199 200 201 public interface MessageHeaderViewCallbacks { 202 void setMessageSpacerHeight(MessageHeaderItem item, int newSpacerHeight); 203 204 void setMessageExpanded(MessageHeaderItem item, int newSpacerHeight); 205 206 void setMessageDetailsExpanded(MessageHeaderItem messageHeaderItem, boolean expanded, 207 int previousMessageHeaderItemHeight); 208 209 void showExternalResources(Message msg); 210 211 void showExternalResources(String senderRawAddress); 212 213 boolean supportsMessageTransforms(); 214 215 String getMessageTransforms(Message msg); 216 217 FragmentManager getFragmentManager(); 218 219 /** 220 * @return <tt>true</tt> if this header is contained within a SecureConversationViewFragment 221 * and cannot assume the content is <strong>not</strong> malicious 222 */ 223 boolean isSecure(); 224 } 225 226 public MessageHeaderView(Context context) { 227 this(context, null); 228 } 229 230 public MessageHeaderView(Context context, AttributeSet attrs) { 231 this(context, attrs, -1); 232 } 233 234 public MessageHeaderView(Context context, AttributeSet attrs, int defStyle) { 235 super(context, attrs, defStyle); 236 237 mIsSnappy = false; 238 mEmailCopyMenu = new EmailCopyContextMenu(getContext()); 239 mInflater = LayoutInflater.from(context); 240 mMyName = context.getString(R.string.me_object_pronoun); 241 242 final Resources res = getResources(); 243 mContactPhotoWidth = res.getDimensionPixelSize(R.dimen.contact_image_width); 244 mContactPhotoHeight = res.getDimensionPixelSize(R.dimen.contact_image_height); 245 mTitleContainerMarginEnd = res.getDimensionPixelSize(R.dimen.conversation_view_margin_side); 246 } 247 248 @Override 249 protected void onFinishInflate() { 250 super.onFinishInflate(); 251 mBorderView = findViewById(R.id.message_header_border); 252 mUpperHeaderView = (ViewGroup) findViewById(R.id.upper_header); 253 mTitleContainer = findViewById(R.id.title_container); 254 mSnapHeaderBottomBorder = findViewById(R.id.snap_header_bottom_border); 255 mSenderNameView = (TextView) findViewById(R.id.sender_name); 256 mRecipientSummary = (TextView) findViewById(R.id.recipient_summary); 257 mDateView = (TextView) findViewById(R.id.send_date); 258 mHideDetailsView = findViewById(R.id.hide_details); 259 mSnippetView = (TextView) findViewById(R.id.email_snippet); 260 mPhotoView = (MessageHeaderContactBadge) findViewById(R.id.photo); 261 mPhotoView.setQuickContactBadge( 262 (QuickContactBadge) findViewById(R.id.invisible_quick_contact)); 263 mReplyButton = findViewById(R.id.reply); 264 mReplyAllButton = findViewById(R.id.reply_all); 265 mForwardButton = findViewById(R.id.forward); 266 mOverflowButton = findViewById(R.id.overflow); 267 mDraftIcon = findViewById(R.id.draft); 268 mEditDraftButton = findViewById(R.id.edit_draft); 269 mUpperDateView = (TextView) findViewById(R.id.upper_date); 270 mAttachmentIcon = findViewById(R.id.attachment); 271 mExtraContentView = (ViewGroup) findViewById(R.id.header_extra_content); 272 273 setExpanded(true); 274 275 registerMessageClickTargets(mReplyButton, mReplyAllButton, mForwardButton, 276 mEditDraftButton, mOverflowButton, mUpperHeaderView, mDateView, mHideDetailsView); 277 278 mUpperHeaderView.setOnCreateContextMenuListener(mEmailCopyMenu); 279 } 280 281 private void registerMessageClickTargets(View... views) { 282 for (View v : views) { 283 if (v != null) { 284 v.setOnClickListener(this); 285 } 286 } 287 } 288 289 @Override 290 public void initialize(ConversationAccountController accountController, 291 Map<String, Address> addressCache, MessageHeaderViewCallbacks callbacks, 292 ContactInfoSource contactInfoSource, VeiledAddressMatcher veiledAddressMatcher) { 293 initialize(accountController, addressCache); 294 setCallbacks(callbacks); 295 setContactInfoSource(contactInfoSource); 296 setVeiledMatcher(veiledAddressMatcher); 297 } 298 299 /** 300 * Associate the header with a contact info source for later contact 301 * presence/photo lookup. 302 */ 303 public void setContactInfoSource(ContactInfoSource contactInfoSource) { 304 mContactInfoSource = contactInfoSource; 305 } 306 307 public void setCallbacks(MessageHeaderViewCallbacks callbacks) { 308 mCallbacks = callbacks; 309 } 310 311 public void setVeiledMatcher(VeiledAddressMatcher matcher) { 312 mVeiledMatcher = matcher; 313 } 314 315 public boolean isExpanded() { 316 // (let's just arbitrarily say that unbound views are expanded by default) 317 return mMessageHeaderItem == null || mMessageHeaderItem.isExpanded(); 318 } 319 320 @Override 321 public void onDetachedFromParent() { 322 unbind(); 323 } 324 325 /** 326 * Headers that are unbound will not match any rendered header (matches() 327 * will return false). Unbinding is not guaranteed to *hide* the view's old 328 * data, though. To re-bind this header to message data, call render() or 329 * renderUpperHeaderFrom(). 330 */ 331 @Override 332 public void unbind() { 333 mMessageHeaderItem = null; 334 mMessage = null; 335 336 if (mObservingContactInfo) { 337 mContactInfoSource.unregisterObserver(mContactInfoObserver); 338 mObservingContactInfo = false; 339 } 340 } 341 342 public void initialize(ConversationAccountController accountController, 343 Map<String, Address> addressCache) { 344 mAccountController = accountController; 345 mAddressCache = addressCache; 346 } 347 348 private Account getAccount() { 349 return mAccountController != null ? mAccountController.getAccount() : null; 350 } 351 352 public void bind(MessageHeaderItem headerItem, boolean measureOnly) { 353 if (mMessageHeaderItem != null && mMessageHeaderItem == headerItem) { 354 return; 355 } 356 357 mMessageHeaderItem = headerItem; 358 render(measureOnly); 359 } 360 361 /** 362 * Rebinds the view to its data. This will only update the view 363 * if the {@link MessageHeaderItem} sent as a parameter is the 364 * same as the view's current {@link MessageHeaderItem} and the 365 * view's expanded state differs from the item's expanded state. 366 */ 367 public void rebind(MessageHeaderItem headerItem) { 368 if (mMessageHeaderItem == null || mMessageHeaderItem != headerItem || 369 isActivated() == isExpanded()) { 370 return; 371 } 372 373 render(false /* measureOnly */); 374 } 375 376 @Override 377 public void refresh() { 378 render(false); 379 } 380 381 private BidiFormatter getBidiFormatter() { 382 if (mBidiFormatter == null) { 383 final ConversationViewAdapter adapter = mMessageHeaderItem != null 384 ? mMessageHeaderItem.getAdapter() : null; 385 if (adapter == null) { 386 mBidiFormatter = BidiFormatter.getInstance(); 387 } else { 388 mBidiFormatter = adapter.getBidiFormatter(); 389 } 390 } 391 return mBidiFormatter; 392 } 393 394 private void render(boolean measureOnly) { 395 if (mMessageHeaderItem == null) { 396 return; 397 } 398 399 Timer t = new Timer(); 400 t.start(HEADER_RENDER_TAG); 401 402 mRecipientSummaryValid = false; 403 mExpandedDetailsValid = false; 404 405 mMessage = mMessageHeaderItem.getMessage(); 406 407 final Account account = getAccount(); 408 final boolean alwaysShowImagesForAccount = (account != null) && 409 (account.settings.showImages == Settings.ShowImages.ALWAYS); 410 411 final boolean alwaysShowImagesForMessage = mMessage.shouldShowImagePrompt(); 412 413 if (!alwaysShowImagesForMessage) { 414 // we don't need the "Show picture" prompt if the user allows images for this message 415 mShowImagePrompt = false; 416 } else if (mCallbacks.isSecure()) { 417 // in a secure view we always display the "Show picture" prompt 418 mShowImagePrompt = true; 419 } else { 420 // otherwise honor the account setting for automatically showing pictures 421 mShowImagePrompt = !alwaysShowImagesForAccount; 422 } 423 424 setExpanded(mMessageHeaderItem.isExpanded()); 425 426 mFrom = mMessage.getFromAddresses(); 427 mTo = mMessage.getToAddresses(); 428 mCc = mMessage.getCcAddresses(); 429 mBcc = mMessage.getBccAddresses(); 430 mReplyTo = mMessage.getReplyToAddresses(); 431 432 /** 433 * Turns draft mode on or off. Draft mode hides message operations other 434 * than "edit", hides contact photo, hides presence, and changes the 435 * sender name to "Draft". 436 */ 437 mIsDraft = mMessage.draftType != UIProvider.DraftType.NOT_A_DRAFT; 438 mSendingState = mMessage.sendingState; 439 440 // If this was a sent message AND: 441 // 1. the account has a custom from, the cursor will populate the 442 // selected custom from as the fromAddress when a message is sent but 443 // not yet synced. 444 // 2. the account has no custom froms, fromAddress will be empty, and we 445 // can safely fall back and show the account name as sender since it's 446 // the only possible fromAddress. 447 String from = mMessage.getFrom(); 448 if (TextUtils.isEmpty(from)) { 449 from = (account != null) ? account.getEmailAddress() : ""; 450 } 451 mSender = getAddress(from); 452 453 updateChildVisibility(); 454 455 final String snippet; 456 if (mIsDraft || mSendingState != UIProvider.ConversationSendingState.OTHER) { 457 snippet = makeSnippet(mMessage.snippet); 458 } else { 459 snippet = mMessage.snippet; 460 } 461 mSnippet = snippet == null ? null : getBidiFormatter().unicodeWrap(snippet); 462 463 mSenderNameView.setText(getHeaderTitle()); 464 setRecipientSummary(); 465 setDateText(); 466 mSnippetView.setText(mSnippet); 467 setAddressOnContextMenu(); 468 469 if (mUpperDateView != null) { 470 mUpperDateView.setText(mMessageHeaderItem.getTimestampShort()); 471 } 472 473 if (measureOnly) { 474 // avoid leaving any state around that would interfere with future regular bind() calls 475 unbind(); 476 } else { 477 updateContactInfo(); 478 if (!mObservingContactInfo) { 479 mContactInfoSource.registerObserver(mContactInfoObserver); 480 mObservingContactInfo = true; 481 } 482 } 483 484 t.pause(HEADER_RENDER_TAG); 485 } 486 487 /** 488 * Update context menu's address field for when the user long presses 489 * on the message header and attempts to copy/send email. 490 */ 491 private void setAddressOnContextMenu() { 492 if (mSender != null) { 493 mEmailCopyMenu.setAddress(mSender.getAddress()); 494 } 495 } 496 497 @Override 498 public boolean isBoundTo(ConversationOverlayItem item) { 499 return item == mMessageHeaderItem; 500 } 501 502 public Address getAddress(String emailStr) { 503 return Utils.getAddress(mAddressCache, emailStr); 504 } 505 506 private void updateSpacerHeight() { 507 final int h = measureHeight(); 508 509 mMessageHeaderItem.setHeight(h); 510 if (mCallbacks != null) { 511 mCallbacks.setMessageSpacerHeight(mMessageHeaderItem, h); 512 } 513 } 514 515 private int measureHeight() { 516 ViewGroup parent = (ViewGroup) getParent(); 517 if (parent == null) { 518 LogUtils.e(LOG_TAG, new Error(), "Unable to measure height of detached header"); 519 return getHeight(); 520 } 521 mPreMeasuring = true; 522 final int h = Utils.measureViewHeight(this, parent); 523 mPreMeasuring = false; 524 return h; 525 } 526 527 private CharSequence getHeaderTitle() { 528 CharSequence title; 529 switch (mSendingState) { 530 case UIProvider.ConversationSendingState.QUEUED: 531 case UIProvider.ConversationSendingState.SENDING: 532 title = getResources().getString(R.string.sending); 533 break; 534 case UIProvider.ConversationSendingState.RETRYING: 535 title = getResources().getString(R.string.message_retrying); 536 break; 537 case UIProvider.ConversationSendingState.SEND_ERROR: 538 title = getResources().getString(R.string.message_failed); 539 break; 540 default: 541 if (mIsDraft) { 542 title = SendersView.getSingularDraftString(getContext()); 543 } else { 544 title = getBidiFormatter().unicodeWrap( 545 getSenderName(mSender)); 546 } 547 } 548 549 return title; 550 } 551 552 private void setRecipientSummary() { 553 if (!mRecipientSummaryValid) { 554 if (mMessageHeaderItem.recipientSummaryText == null) { 555 final Account account = getAccount(); 556 final String meEmailAddress = (account != null) ? account.getEmailAddress() : ""; 557 mMessageHeaderItem.recipientSummaryText = getRecipientSummaryText(getContext(), 558 meEmailAddress, mMyName, mTo, mCc, mBcc, mAddressCache, mVeiledMatcher, 559 getBidiFormatter()); 560 } 561 mRecipientSummary.setText(mMessageHeaderItem.recipientSummaryText); 562 mRecipientSummaryValid = true; 563 } 564 } 565 566 private void setDateText() { 567 if (mIsSnappy) { 568 mDateView.setText(mMessageHeaderItem.getTimestampLong()); 569 mDateView.setOnClickListener(null); 570 } else { 571 mDateView.setMovementMethod(LinkMovementMethod.getInstance()); 572 mDateView.setText(Html.fromHtml(getResources().getString( 573 R.string.date_and_view_details, mMessageHeaderItem.getTimestampLong()))); 574 StyleUtils.stripUnderlinesAndUrl(mDateView); 575 } 576 } 577 578 /** 579 * Return the name, if known, or just the address. 580 */ 581 private static String getSenderName(Address sender) { 582 if (sender == null) { 583 return ""; 584 } 585 final String displayName = sender.getPersonal(); 586 return TextUtils.isEmpty(displayName) ? sender.getAddress() : displayName; 587 } 588 589 private static void setChildVisibility(int visibility, View... children) { 590 for (View v : children) { 591 if (v != null) { 592 v.setVisibility(visibility); 593 } 594 } 595 } 596 597 private void setExpanded(final boolean expanded) { 598 // use View's 'activated' flag to store expanded state 599 // child view state lists can use this to toggle drawables 600 setActivated(expanded); 601 if (mMessageHeaderItem != null) { 602 mMessageHeaderItem.setExpanded(expanded); 603 } 604 } 605 606 /** 607 * Update the visibility of the many child views based on expanded/collapsed 608 * and draft/normal state. 609 */ 610 private void updateChildVisibility() { 611 // Too bad this can't be done with an XML state list... 612 613 if (mIsViewOnlyMode) { 614 setMessageDetailsVisibility(VISIBLE); 615 setChildVisibility(GONE, mSnapHeaderBottomBorder); 616 617 setChildVisibility(GONE, mReplyButton, mReplyAllButton, mForwardButton, 618 mOverflowButton, mDraftIcon, mEditDraftButton, 619 mAttachmentIcon, mUpperDateView, mSnippetView); 620 setChildVisibility(VISIBLE, mPhotoView, mRecipientSummary); 621 622 setChildMarginEnd(mTitleContainer, 0); 623 } else if (isExpanded()) { 624 int normalVis, draftVis; 625 626 final boolean isSnappy = isSnappy(); 627 setMessageDetailsVisibility((isSnappy) ? GONE : VISIBLE); 628 setChildVisibility(isSnappy ? VISIBLE : GONE, mSnapHeaderBottomBorder); 629 630 if (mIsDraft) { 631 normalVis = GONE; 632 draftVis = VISIBLE; 633 } else { 634 normalVis = VISIBLE; 635 draftVis = GONE; 636 } 637 638 setReplyOrReplyAllVisible(); 639 setChildVisibility(normalVis, mPhotoView, mForwardButton, mOverflowButton); 640 setChildVisibility(draftVis, mDraftIcon, mEditDraftButton); 641 setChildVisibility(VISIBLE, mRecipientSummary); 642 setChildVisibility(GONE, mAttachmentIcon, mUpperDateView, mSnippetView); 643 644 setChildMarginEnd(mTitleContainer, 0); 645 } else { 646 setMessageDetailsVisibility(GONE); 647 setChildVisibility(GONE, mSnapHeaderBottomBorder); 648 setChildVisibility(VISIBLE, mSnippetView, mUpperDateView); 649 650 setChildVisibility(GONE, mEditDraftButton, mReplyButton, mReplyAllButton, 651 mForwardButton, mOverflowButton, mRecipientSummary, 652 mDateView, mHideDetailsView); 653 654 setChildVisibility(mMessage.hasAttachments ? VISIBLE : GONE, 655 mAttachmentIcon); 656 657 if (mIsDraft) { 658 setChildVisibility(VISIBLE, mDraftIcon); 659 setChildVisibility(GONE, mPhotoView); 660 } else { 661 setChildVisibility(GONE, mDraftIcon); 662 setChildVisibility(VISIBLE, mPhotoView); 663 } 664 665 setChildMarginEnd(mTitleContainer, mTitleContainerMarginEnd); 666 } 667 668 final ConversationViewAdapter adapter = mMessageHeaderItem.getAdapter(); 669 if (adapter != null) { 670 mBorderView.setVisibility( 671 adapter.isPreviousItemSuperCollapsed(mMessageHeaderItem) ? GONE : VISIBLE); 672 } else { 673 mBorderView.setVisibility(VISIBLE); 674 } 675 } 676 677 /** 678 * If an overflow menu is present in this header's layout, set the 679 * visibility of "Reply" and "Reply All" actions based on a user preference. 680 * Only one of those actions will be visible when an overflow is present. If 681 * no overflow is present (e.g. big phone or tablet), it's assumed we have 682 * plenty of screen real estate and can show both. 683 */ 684 private void setReplyOrReplyAllVisible() { 685 if (mIsDraft) { 686 setChildVisibility(GONE, mReplyButton, mReplyAllButton); 687 return; 688 } else if (mOverflowButton == null) { 689 setChildVisibility(VISIBLE, mReplyButton, mReplyAllButton); 690 return; 691 } 692 693 final Account account = getAccount(); 694 final boolean defaultReplyAll = (account != null) ? account.settings.replyBehavior 695 == UIProvider.DefaultReplyBehavior.REPLY_ALL : false; 696 setChildVisibility(defaultReplyAll ? GONE : VISIBLE, mReplyButton); 697 setChildVisibility(defaultReplyAll ? VISIBLE : GONE, mReplyAllButton); 698 } 699 700 @SuppressLint("NewApi") 701 private static void setChildMarginEnd(View childView, int marginEnd) { 702 MarginLayoutParams mlp = (MarginLayoutParams) childView.getLayoutParams(); 703 if (Utils.isRunningJBMR1OrLater()) { 704 mlp.setMarginEnd(marginEnd); 705 } else { 706 mlp.rightMargin = marginEnd; 707 } 708 childView.setLayoutParams(mlp); 709 } 710 711 712 713 @VisibleForTesting 714 static CharSequence getRecipientSummaryText(Context context, String meEmailAddress, 715 String myName, String[] to, String[] cc, String[] bcc, 716 Map<String, Address> addressCache, VeiledAddressMatcher matcher, 717 BidiFormatter bidiFormatter) { 718 719 final RecipientListsBuilder builder = new RecipientListsBuilder( 720 context, meEmailAddress, myName, addressCache, matcher, bidiFormatter); 721 722 builder.append(to); 723 builder.append(cc); 724 builder.append(bcc); 725 726 return builder.build(); 727 } 728 729 /** 730 * Utility class to build a list of recipient lists. 731 */ 732 private static class RecipientListsBuilder { 733 private final Context mContext; 734 private final String mMeEmailAddress; 735 private final String mMyName; 736 private final StringBuilder mBuilder = new StringBuilder(); 737 private final CharSequence mComma; 738 private final Map<String, Address> mAddressCache; 739 private final VeiledAddressMatcher mMatcher; 740 private final BidiFormatter mBidiFormatter; 741 742 int mRecipientCount = 0; 743 boolean mFirst = true; 744 745 public RecipientListsBuilder(Context context, String meEmailAddress, String myName, 746 Map<String, Address> addressCache, VeiledAddressMatcher matcher, 747 BidiFormatter bidiFormatter) { 748 mContext = context; 749 mMeEmailAddress = meEmailAddress; 750 mMyName = myName; 751 mComma = mContext.getText(R.string.enumeration_comma); 752 mAddressCache = addressCache; 753 mMatcher = matcher; 754 mBidiFormatter = bidiFormatter; 755 } 756 757 public void append(String[] recipients) { 758 final int addLimit = SUMMARY_MAX_RECIPIENTS - mRecipientCount; 759 final boolean hasRecipients = appendRecipients(recipients, addLimit); 760 if (hasRecipients) { 761 mRecipientCount += Math.min(addLimit, recipients.length); 762 } 763 } 764 765 /** 766 * Appends formatted recipients of the message to the recipient list, 767 * as long as there are recipients left to append and the maximum number 768 * of addresses limit has not been reached. 769 * @param rawAddrs The addresses to append. 770 * @param maxToCopy The maximum number of addresses to append. 771 * @return {@code true} if a recipient has been appended. {@code false}, otherwise. 772 */ 773 private boolean appendRecipients(String[] rawAddrs, 774 int maxToCopy) { 775 if (rawAddrs == null || rawAddrs.length == 0 || maxToCopy == 0) { 776 return false; 777 } 778 779 final int len = Math.min(maxToCopy, rawAddrs.length); 780 for (int i = 0; i < len; i++) { 781 final Address email = Utils.getAddress(mAddressCache, rawAddrs[i]); 782 final String emailAddress = email.getAddress(); 783 final String name; 784 if (mMatcher != null && mMatcher.isVeiledAddress(emailAddress)) { 785 if (TextUtils.isEmpty(email.getPersonal())) { 786 // Let's write something more readable. 787 name = mContext.getString(VeiledAddressMatcher.VEILED_SUMMARY_UNKNOWN); 788 } else { 789 name = email.getSimplifiedName(); 790 } 791 } else { 792 // Not a veiled address, show first part of email, or "me". 793 name = mMeEmailAddress.equals(emailAddress) ? 794 mMyName : email.getSimplifiedName(); 795 } 796 797 // duplicate TextUtils.join() logic to minimize temporary allocations 798 if (mFirst) { 799 mFirst = false; 800 } else { 801 mBuilder.append(mComma); 802 } 803 mBuilder.append(mBidiFormatter.unicodeWrap(name)); 804 } 805 806 return true; 807 } 808 809 public CharSequence build() { 810 return mContext.getString(R.string.to_message_header, mBuilder); 811 } 812 } 813 814 private void updateContactInfo() { 815 if (mContactInfoSource == null || mSender == null) { 816 mPhotoView.setImageToDefault(); 817 mPhotoView.setContentDescription(getResources().getString( 818 R.string.contact_info_string_default)); 819 return; 820 } 821 822 // Set the photo to either a found Bitmap or the default 823 // and ensure either the contact URI or email is set so the click 824 // handling works 825 String contentDesc = getResources().getString(R.string.contact_info_string, 826 !TextUtils.isEmpty(mSender.getPersonal()) 827 ? mSender.getPersonal() 828 : mSender.getAddress()); 829 mPhotoView.setContentDescription(contentDesc); 830 boolean photoSet = false; 831 final String email = mSender.getAddress(); 832 final ContactInfo info = mContactInfoSource.getContactInfo(email); 833 final Resources res = getResources(); 834 if (info != null) { 835 if (info.contactUri != null) { 836 mPhotoView.assignContactUri(info.contactUri); 837 } else { 838 mPhotoView.assignContactFromEmail(email, true /* lazyLookup */); 839 } 840 841 if (info.photo != null) { 842 mPhotoView.setImageBitmap(frameBitmapInCircle(info.photo)); 843 photoSet = true; 844 } 845 } else { 846 mPhotoView.assignContactFromEmail(email, true /* lazyLookup */); 847 } 848 849 if (!photoSet) { 850 mPhotoView.setImageBitmap( 851 frameBitmapInCircle(makeLetterTile(mSender.getPersonal(), email))); 852 } 853 } 854 855 private Bitmap makeLetterTile( 856 String displayName, String senderAddress) { 857 if (mLetterTileProvider == null) { 858 mLetterTileProvider = new LetterTileProvider(getContext()); 859 } 860 861 final ImageCanvas.Dimensions dimensions = new ImageCanvas.Dimensions( 862 mContactPhotoWidth, mContactPhotoHeight, ImageCanvas.Dimensions.SCALE_ONE); 863 return mLetterTileProvider.getLetterTile(dimensions, displayName, senderAddress); 864 } 865 866 /** 867 * Frames the input bitmap in a circle. 868 */ 869 private static Bitmap frameBitmapInCircle(Bitmap input) { 870 if (input == null) { 871 return null; 872 } 873 874 // Crop the image if not squared. 875 int inputWidth = input.getWidth(); 876 int inputHeight = input.getHeight(); 877 int targetX, targetY, targetSize; 878 if (inputWidth >= inputHeight) { 879 targetX = inputWidth / 2 - inputHeight / 2; 880 targetY = 0; 881 targetSize = inputHeight; 882 } else { 883 targetX = 0; 884 targetY = inputHeight / 2 - inputWidth / 2; 885 targetSize = inputWidth; 886 } 887 888 // Create an output bitmap and a canvas to draw on it. 889 Bitmap output = Bitmap.createBitmap(targetSize, targetSize, Bitmap.Config.ARGB_8888); 890 Canvas canvas = new Canvas(output); 891 892 // Create a black paint to draw the mask. 893 Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); 894 paint.setColor(Color.BLACK); 895 896 // Draw a circle. 897 canvas.drawCircle(targetSize / 2, targetSize / 2, targetSize / 2, paint); 898 899 // Replace the black parts of the mask with the input image. 900 paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); 901 canvas.drawBitmap(input, targetX /* left */, targetY /* top */, paint); 902 903 return output; 904 } 905 906 @Override 907 public boolean onMenuItemClick(MenuItem item) { 908 mPopup.dismiss(); 909 return onClick(null, item.getItemId()); 910 } 911 912 @Override 913 public void onClick(View v) { 914 onClick(v, v.getId()); 915 } 916 917 /** 918 * Handles clicks on either views or menu items. View parameter can be null 919 * for menu item clicks. 920 */ 921 public boolean onClick(final View v, final int id) { 922 if (mMessage == null) { 923 LogUtils.i(LOG_TAG, "ignoring message header tap on unbound view"); 924 return false; 925 } 926 927 boolean handled = true; 928 929 if (id == R.id.reply) { 930 ComposeActivity.reply(getContext(), getAccount(), mMessage); 931 } else if (id == R.id.reply_all) { 932 ComposeActivity.replyAll(getContext(), getAccount(), mMessage); 933 } else if (id == R.id.forward) { 934 ComposeActivity.forward(getContext(), getAccount(), mMessage); 935 } else if (id == R.id.print_message) { 936 printMessage(); 937 } else if (id == R.id.report_rendering_problem) { 938 final String text = getContext().getString(R.string.report_rendering_problem_desc); 939 ComposeActivity.reportRenderingFeedback(getContext(), getAccount(), mMessage, 940 text + "\n\n" + mCallbacks.getMessageTransforms(mMessage)); 941 } else if (id == R.id.report_rendering_improvement) { 942 final String text = getContext().getString(R.string.report_rendering_improvement_desc); 943 ComposeActivity.reportRenderingFeedback(getContext(), getAccount(), mMessage, 944 text + "\n\n" + mCallbacks.getMessageTransforms(mMessage)); 945 } else if (id == R.id.edit_draft) { 946 ComposeActivity.editDraft(getContext(), getAccount(), mMessage); 947 } else if (id == R.id.overflow) { 948 if (mPopup == null) { 949 mPopup = new PopupMenu(getContext(), v); 950 mPopup.getMenuInflater().inflate(R.menu.message_header_overflow_menu, 951 mPopup.getMenu()); 952 mPopup.setOnMenuItemClickListener(this); 953 } 954 final boolean defaultReplyAll = getAccount().settings.replyBehavior 955 == UIProvider.DefaultReplyBehavior.REPLY_ALL; 956 final Menu m = mPopup.getMenu(); 957 m.findItem(R.id.reply).setVisible(defaultReplyAll); 958 m.findItem(R.id.reply_all).setVisible(!defaultReplyAll); 959 m.findItem(R.id.print_message).setVisible(Utils.isRunningKitkatOrLater()); 960 961 final boolean reportRendering = ENABLE_REPORT_RENDERING_PROBLEM 962 && mCallbacks.supportsMessageTransforms(); 963 m.findItem(R.id.report_rendering_improvement).setVisible(reportRendering); 964 m.findItem(R.id.report_rendering_problem).setVisible(reportRendering); 965 966 mPopup.show(); 967 } else if (id == R.id.send_date || id == R.id.hide_details || 968 id == R.id.details_expanded_content) { 969 toggleMessageDetails(); 970 } else if (id == R.id.upper_header) { 971 toggleExpanded(); 972 } else if (id == R.id.show_pictures_text) { 973 handleShowImagePromptClick(v); 974 } else { 975 LogUtils.i(LOG_TAG, "unrecognized header tap: %d", id); 976 handled = false; 977 } 978 979 if (handled && id != R.id.overflow) { 980 Analytics.getInstance().sendMenuItemEvent(Analytics.EVENT_CATEGORY_MENU_ITEM, id, 981 "message_header", 0); 982 } 983 984 return handled; 985 } 986 987 private void printMessage() { 988 // Secure conversation view does not use a conversation view adapter 989 // so it's safe to test for existence as a signal to use javascript or not. 990 final boolean useJavascript = mMessageHeaderItem.getAdapter() != null; 991 final Account account = getAccount(); 992 final Conversation conversation = mMessage.getConversation(); 993 final String baseUri = 994 AbstractConversationViewFragment.buildBaseUri(getContext(), account, conversation); 995 PrintUtils.printMessage(getContext(), mMessage, conversation.subject, 996 mAddressCache, conversation.getBaseUri(baseUri), useJavascript); 997 } 998 999 /** 1000 * Set to true if the user should not be able to perform message actions 1001 * on the message such as reply/reply all/forward/star/etc. 1002 * 1003 * Default is false. 1004 */ 1005 public void setViewOnlyMode(boolean isViewOnlyMode) { 1006 mIsViewOnlyMode = isViewOnlyMode; 1007 } 1008 1009 public void setExpandable(boolean expandable) { 1010 mExpandable = expandable; 1011 } 1012 1013 public void toggleExpanded() { 1014 if (!mExpandable) { 1015 return; 1016 } 1017 setExpanded(!isExpanded()); 1018 1019 // The snappy header will disappear; no reason to update text. 1020 if (!isSnappy()) { 1021 mSenderNameView.setText(getHeaderTitle()); 1022 setRecipientSummary(); 1023 setDateText(); 1024 mSnippetView.setText(mSnippet); 1025 } 1026 1027 updateChildVisibility(); 1028 1029 // Force-measure the new header height so we can set the spacer size and 1030 // reveal the message div in one pass. Force-measuring makes it unnecessary to set 1031 // mSizeChanged. 1032 int h = measureHeight(); 1033 mMessageHeaderItem.setHeight(h); 1034 if (mCallbacks != null) { 1035 mCallbacks.setMessageExpanded(mMessageHeaderItem, h); 1036 } 1037 } 1038 1039 private static boolean isValidPosition(int position, int size) { 1040 return position >= 0 && position < size; 1041 } 1042 1043 @Override 1044 public void setSnappy() { 1045 mIsSnappy = true; 1046 hideMessageDetails(); 1047 } 1048 1049 private boolean isSnappy() { 1050 return mIsSnappy; 1051 } 1052 1053 private void toggleMessageDetails() { 1054 int heightBefore = measureHeight(); 1055 final boolean expand = 1056 (mExpandedDetailsView == null || mExpandedDetailsView.getVisibility() == GONE); 1057 setMessageDetailsExpanded(expand); 1058 updateSpacerHeight(); 1059 if (mCallbacks != null) { 1060 mCallbacks.setMessageDetailsExpanded(mMessageHeaderItem, expand, heightBefore); 1061 } 1062 } 1063 1064 private void setMessageDetailsExpanded(boolean expand) { 1065 if (expand) { 1066 showExpandedDetails(); 1067 } else { 1068 hideExpandedDetails(); 1069 } 1070 1071 if (mMessageHeaderItem != null) { 1072 mMessageHeaderItem.detailsExpanded = expand; 1073 } 1074 } 1075 1076 public void setMessageDetailsVisibility(int vis) { 1077 if (vis == GONE) { 1078 hideExpandedDetails(); 1079 hideSpamWarning(); 1080 hideShowImagePrompt(); 1081 hideInvite(); 1082 mUpperHeaderView.setOnCreateContextMenuListener(null); 1083 } else { 1084 setMessageDetailsExpanded(mMessageHeaderItem.detailsExpanded); 1085 if (mMessage.spamWarningString == null) { 1086 hideSpamWarning(); 1087 } else { 1088 showSpamWarning(); 1089 } 1090 if (mShowImagePrompt) { 1091 if (mMessageHeaderItem.getShowImages()) { 1092 showImagePromptAlways(true); 1093 } else { 1094 showImagePromptOnce(); 1095 } 1096 } else { 1097 hideShowImagePrompt(); 1098 } 1099 if (mMessage.isFlaggedCalendarInvite()) { 1100 showInvite(); 1101 } else { 1102 hideInvite(); 1103 } 1104 mUpperHeaderView.setOnCreateContextMenuListener(mEmailCopyMenu); 1105 } 1106 } 1107 1108 private void hideMessageDetails() { 1109 setMessageDetailsVisibility(GONE); 1110 } 1111 1112 private void hideExpandedDetails() { 1113 if (mExpandedDetailsView != null) { 1114 mExpandedDetailsView.setVisibility(GONE); 1115 } 1116 mDateView.setVisibility(VISIBLE); 1117 mHideDetailsView.setVisibility(GONE); 1118 } 1119 1120 private void hideInvite() { 1121 if (mInviteView != null) { 1122 mInviteView.setVisibility(GONE); 1123 } 1124 } 1125 1126 private void showInvite() { 1127 if (mInviteView == null) { 1128 mInviteView = (MessageInviteView) mInflater.inflate( 1129 R.layout.conversation_message_invite, this, false); 1130 mExtraContentView.addView(mInviteView); 1131 } 1132 mInviteView.bind(mMessage); 1133 mInviteView.setVisibility(VISIBLE); 1134 } 1135 1136 private void hideShowImagePrompt() { 1137 if (mImagePromptView != null) { 1138 mImagePromptView.setVisibility(GONE); 1139 } 1140 } 1141 1142 private void showImagePromptOnce() { 1143 if (mImagePromptView == null) { 1144 mImagePromptView = (TextView) mInflater.inflate( 1145 R.layout.conversation_message_show_pics, this, false); 1146 mExtraContentView.addView(mImagePromptView); 1147 mImagePromptView.setOnClickListener(this); 1148 } 1149 mImagePromptView.setVisibility(VISIBLE); 1150 mImagePromptView.setText(R.string.show_images); 1151 mImagePromptView.setTag(SHOW_IMAGE_PROMPT_ONCE); 1152 } 1153 1154 /** 1155 * Shows the "Always show pictures" message 1156 * 1157 * @param initialShowing <code>true</code> if this is the first time we are showing the prompt 1158 * for "show images", <code>false</code> if we are transitioning from "Show pictures" 1159 */ 1160 private void showImagePromptAlways(final boolean initialShowing) { 1161 if (initialShowing) { 1162 // Initialize the view 1163 showImagePromptOnce(); 1164 } 1165 1166 mImagePromptView.setText(R.string.always_show_images); 1167 mImagePromptView.setTag(SHOW_IMAGE_PROMPT_ALWAYS); 1168 1169 if (!initialShowing) { 1170 // the new text's line count may differ, so update the spacer height 1171 updateSpacerHeight(); 1172 } 1173 } 1174 1175 private void hideSpamWarning() { 1176 if (mSpamWarningView != null) { 1177 mSpamWarningView.setVisibility(GONE); 1178 } 1179 } 1180 1181 private void showSpamWarning() { 1182 if (mSpamWarningView == null) { 1183 mSpamWarningView = (SpamWarningView) 1184 mInflater.inflate(R.layout.conversation_message_spam_warning, this, false); 1185 mExtraContentView.addView(mSpamWarningView); 1186 } 1187 1188 mSpamWarningView.showSpamWarning(mMessage, mSender); 1189 } 1190 1191 private void handleShowImagePromptClick(View v) { 1192 Integer state = (Integer) v.getTag(); 1193 if (state == null) { 1194 return; 1195 } 1196 switch (state) { 1197 case SHOW_IMAGE_PROMPT_ONCE: 1198 if (mCallbacks != null) { 1199 mCallbacks.showExternalResources(mMessage); 1200 } 1201 if (mMessageHeaderItem != null) { 1202 mMessageHeaderItem.setShowImages(true); 1203 } 1204 if (mIsViewOnlyMode) { 1205 hideShowImagePrompt(); 1206 } else { 1207 showImagePromptAlways(false); 1208 } 1209 break; 1210 case SHOW_IMAGE_PROMPT_ALWAYS: 1211 mMessage.markAlwaysShowImages(getQueryHandler(), 0 /* token */, null /* cookie */); 1212 1213 if (mCallbacks != null) { 1214 mCallbacks.showExternalResources(mMessage.getFrom()); 1215 } 1216 1217 mShowImagePrompt = false; 1218 v.setTag(null); 1219 v.setVisibility(GONE); 1220 updateSpacerHeight(); 1221 Toast.makeText(getContext(), R.string.always_show_images_toast, Toast.LENGTH_SHORT) 1222 .show(); 1223 break; 1224 } 1225 } 1226 1227 private AsyncQueryHandler getQueryHandler() { 1228 if (mQueryHandler == null) { 1229 mQueryHandler = new AsyncQueryHandler(getContext().getContentResolver()) {}; 1230 } 1231 return mQueryHandler; 1232 } 1233 1234 /** 1235 * Makes expanded details visible. If necessary, will inflate expanded 1236 * details layout and render using saved-off state (senders, timestamp, 1237 * etc). 1238 */ 1239 private void showExpandedDetails() { 1240 // lazily create expanded details view 1241 final boolean expandedViewCreated = ensureExpandedDetailsView(); 1242 if (expandedViewCreated) { 1243 mExtraContentView.addView(mExpandedDetailsView, 0); 1244 } 1245 mExpandedDetailsView.setVisibility(VISIBLE); 1246 mDateView.setVisibility(GONE); 1247 mHideDetailsView.setVisibility(VISIBLE); 1248 } 1249 1250 private boolean ensureExpandedDetailsView() { 1251 boolean viewCreated = false; 1252 if (mExpandedDetailsView == null) { 1253 View v = inflateExpandedDetails(mInflater); 1254 v.setOnClickListener(this); 1255 1256 mExpandedDetailsView = (ViewGroup) v; 1257 viewCreated = true; 1258 } 1259 if (!mExpandedDetailsValid) { 1260 renderExpandedDetails(getResources(), mExpandedDetailsView, mMessage.viaDomain, 1261 mAddressCache, getAccount(), mVeiledMatcher, mFrom, mReplyTo, mTo, mCc, mBcc, 1262 mMessageHeaderItem.getTimestampFull(), 1263 getBidiFormatter()); 1264 1265 mExpandedDetailsValid = true; 1266 } 1267 return viewCreated; 1268 } 1269 1270 public static View inflateExpandedDetails(LayoutInflater inflater) { 1271 return inflater.inflate(R.layout.conversation_message_header_details, null, false); 1272 } 1273 1274 public static void renderExpandedDetails(Resources res, View detailsView, 1275 String viaDomain, Map<String, Address> addressCache, Account account, 1276 VeiledAddressMatcher veiledMatcher, String[] from, String[] replyTo, 1277 String[] to, String[] cc, String[] bcc, CharSequence receivedTimestamp, 1278 BidiFormatter bidiFormatter) { 1279 renderEmailList(res, R.id.from_heading, R.id.from_details, from, viaDomain, 1280 detailsView, addressCache, account, veiledMatcher, bidiFormatter); 1281 renderEmailList(res, R.id.replyto_heading, R.id.replyto_details, replyTo, viaDomain, 1282 detailsView, addressCache, account, veiledMatcher, bidiFormatter); 1283 renderEmailList(res, R.id.to_heading, R.id.to_details, to, viaDomain, 1284 detailsView, addressCache, account, veiledMatcher, bidiFormatter); 1285 renderEmailList(res, R.id.cc_heading, R.id.cc_details, cc, viaDomain, 1286 detailsView, addressCache, account, veiledMatcher, bidiFormatter); 1287 renderEmailList(res, R.id.bcc_heading, R.id.bcc_details, bcc, viaDomain, 1288 detailsView, addressCache, account, veiledMatcher, bidiFormatter); 1289 1290 // Render date 1291 detailsView.findViewById(R.id.date_heading).setVisibility(VISIBLE); 1292 final TextView date = (TextView) detailsView.findViewById(R.id.date_details); 1293 date.setText(receivedTimestamp); 1294 date.setVisibility(VISIBLE); 1295 } 1296 1297 /** 1298 * Render an email list for the expanded message details view. 1299 */ 1300 private static void renderEmailList(Resources res, int headerId, int detailsId, 1301 String[] emails, String viaDomain, View rootView, 1302 Map<String, Address> addressCache, Account account, 1303 VeiledAddressMatcher veiledMatcher, BidiFormatter bidiFormatter) { 1304 if (emails == null || emails.length == 0) { 1305 return; 1306 } 1307 final String[] formattedEmails = new String[emails.length]; 1308 for (int i = 0; i < emails.length; i++) { 1309 final Address email = Utils.getAddress(addressCache, emails[i]); 1310 String name = email.getPersonal(); 1311 final String address = email.getAddress(); 1312 // Check if the address here is a veiled address. If it is, we need to display an 1313 // alternate layout 1314 final boolean isVeiledAddress = veiledMatcher != null && 1315 veiledMatcher.isVeiledAddress(address); 1316 final String addressShown; 1317 if (isVeiledAddress) { 1318 // Add the warning at the end of the name, and remove the address. The alternate 1319 // text cannot be put in the address part, because the address is made into a link, 1320 // and the alternate human-readable text is not a link. 1321 addressShown = ""; 1322 if (TextUtils.isEmpty(name)) { 1323 // Empty name and we will block out the address. Let's write something more 1324 // readable. 1325 name = res.getString(VeiledAddressMatcher.VEILED_ALTERNATE_TEXT_UNKNOWN_PERSON); 1326 } else { 1327 name = name + res.getString(VeiledAddressMatcher.VEILED_ALTERNATE_TEXT); 1328 } 1329 } else { 1330 addressShown = address; 1331 } 1332 if (name == null || name.length() == 0 || name.equalsIgnoreCase(addressShown)) { 1333 formattedEmails[i] = bidiFormatter.unicodeWrap(addressShown); 1334 } else { 1335 // The one downside to having the showViaDomain here is that 1336 // if the sender does not have a name, it will not show the via info 1337 if (viaDomain != null) { 1338 formattedEmails[i] = res.getString( 1339 R.string.address_display_format_with_via_domain, 1340 bidiFormatter.unicodeWrap(name), 1341 bidiFormatter.unicodeWrap(addressShown), 1342 bidiFormatter.unicodeWrap(viaDomain)); 1343 } else { 1344 formattedEmails[i] = res.getString(R.string.address_display_format, 1345 bidiFormatter.unicodeWrap(name), 1346 bidiFormatter.unicodeWrap(addressShown)); 1347 } 1348 } 1349 } 1350 1351 rootView.findViewById(headerId).setVisibility(VISIBLE); 1352 final TextView detailsText = (TextView) rootView.findViewById(detailsId); 1353 detailsText.setText(TextUtils.join("\n", formattedEmails)); 1354 stripUnderlines(detailsText, account); 1355 detailsText.setVisibility(VISIBLE); 1356 } 1357 1358 private static void stripUnderlines(TextView textView, Account account) { 1359 final Spannable spannable = (Spannable) textView.getText(); 1360 final URLSpan[] urls = textView.getUrls(); 1361 1362 for (URLSpan span : urls) { 1363 final int start = spannable.getSpanStart(span); 1364 final int end = spannable.getSpanEnd(span); 1365 spannable.removeSpan(span); 1366 span = new EmailAddressSpan(account, span.getURL().substring(7)); 1367 spannable.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 1368 } 1369 } 1370 1371 /** 1372 * Returns a short plaintext snippet generated from the given HTML message 1373 * body. Collapses whitespace, ignores '<' and '>' characters and 1374 * everything in between, and truncates the snippet to no more than 100 1375 * characters. 1376 * 1377 * @return Short plaintext snippet 1378 */ 1379 @VisibleForTesting 1380 static String makeSnippet(final String messageBody) { 1381 if (TextUtils.isEmpty(messageBody)) { 1382 return null; 1383 } 1384 1385 final StringBuilder snippet = new StringBuilder(MAX_SNIPPET_LENGTH); 1386 1387 final StringReader reader = new StringReader(messageBody); 1388 try { 1389 int c; 1390 while ((c = reader.read()) != -1 && snippet.length() < MAX_SNIPPET_LENGTH) { 1391 // Collapse whitespace. 1392 if (Character.isWhitespace(c)) { 1393 snippet.append(' '); 1394 do { 1395 c = reader.read(); 1396 } while (Character.isWhitespace(c)); 1397 if (c == -1) { 1398 break; 1399 } 1400 } 1401 1402 if (c == '<') { 1403 // Ignore everything up to and including the next '>' 1404 // character. 1405 while ((c = reader.read()) != -1) { 1406 if (c == '>') { 1407 break; 1408 } 1409 } 1410 1411 // If we reached the end of the message body, exit. 1412 if (c == -1) { 1413 break; 1414 } 1415 } else if (c == '&') { 1416 // Read HTML entity. 1417 StringBuilder sb = new StringBuilder(); 1418 1419 while ((c = reader.read()) != -1) { 1420 if (c == ';') { 1421 break; 1422 } 1423 sb.append((char) c); 1424 } 1425 1426 String entity = sb.toString(); 1427 if ("nbsp".equals(entity)) { 1428 snippet.append(' '); 1429 } else if ("lt".equals(entity)) { 1430 snippet.append('<'); 1431 } else if ("gt".equals(entity)) { 1432 snippet.append('>'); 1433 } else if ("amp".equals(entity)) { 1434 snippet.append('&'); 1435 } else if ("quot".equals(entity)) { 1436 snippet.append('"'); 1437 } else if ("apos".equals(entity) || "#39".equals(entity)) { 1438 snippet.append('\''); 1439 } else { 1440 // Unknown entity; just append the literal string. 1441 snippet.append('&').append(entity); 1442 if (c == ';') { 1443 snippet.append(';'); 1444 } 1445 } 1446 1447 // If we reached the end of the message body, exit. 1448 if (c == -1) { 1449 break; 1450 } 1451 } else { 1452 // The current character is a non-whitespace character that 1453 // isn't inside some 1454 // HTML tag and is not part of an HTML entity. 1455 snippet.append((char) c); 1456 } 1457 } 1458 } catch (IOException e) { 1459 LogUtils.wtf(LOG_TAG, e, "Really? IOException while reading a freaking string?!? "); 1460 } 1461 1462 return snippet.toString(); 1463 } 1464 1465 @Override 1466 protected void onLayout(boolean changed, int l, int t, int r, int b) { 1467 Timer perf = new Timer(); 1468 perf.start(LAYOUT_TAG); 1469 super.onLayout(changed, l, t, r, b); 1470 perf.pause(LAYOUT_TAG); 1471 } 1472 1473 @Override 1474 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 1475 Timer t = new Timer(); 1476 if (Timer.ENABLE_TIMER && !mPreMeasuring) { 1477 t.count("header measure id=" + mMessage.id); 1478 t.start(MEASURE_TAG); 1479 } 1480 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1481 if (!mPreMeasuring) { 1482 t.pause(MEASURE_TAG); 1483 } 1484 } 1485} 1486