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