1 /* 2 * Copyright (C) 2015 The Android Open Source Project 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 package com.android.messaging.ui.conversation; 17 18 import android.content.Context; 19 import android.content.res.Resources; 20 import android.database.Cursor; 21 import android.graphics.Rect; 22 import android.graphics.drawable.Drawable; 23 import android.net.Uri; 24 import android.support.annotation.Nullable; 25 import android.text.Spanned; 26 import android.text.TextUtils; 27 import android.text.format.DateUtils; 28 import android.text.format.Formatter; 29 import android.text.style.URLSpan; 30 import android.text.util.Linkify; 31 import android.util.AttributeSet; 32 import android.util.DisplayMetrics; 33 import android.view.Gravity; 34 import android.view.LayoutInflater; 35 import android.view.MotionEvent; 36 import android.view.View; 37 import android.view.ViewGroup; 38 import android.view.WindowManager; 39 import android.widget.FrameLayout; 40 import android.widget.ImageView.ScaleType; 41 import android.widget.LinearLayout; 42 import android.widget.TextView; 43 44 import com.android.messaging.R; 45 import com.android.messaging.datamodel.DataModel; 46 import com.android.messaging.datamodel.data.ConversationMessageData; 47 import com.android.messaging.datamodel.data.MessageData; 48 import com.android.messaging.datamodel.data.MessagePartData; 49 import com.android.messaging.datamodel.data.SubscriptionListData.SubscriptionListEntry; 50 import com.android.messaging.datamodel.media.ImageRequestDescriptor; 51 import com.android.messaging.datamodel.media.MessagePartImageRequestDescriptor; 52 import com.android.messaging.datamodel.media.UriImageRequestDescriptor; 53 import com.android.messaging.sms.MmsUtils; 54 import com.android.messaging.ui.AsyncImageView; 55 import com.android.messaging.ui.AsyncImageView.AsyncImageViewDelayLoader; 56 import com.android.messaging.ui.AudioAttachmentView; 57 import com.android.messaging.ui.ContactIconView; 58 import com.android.messaging.ui.ConversationDrawables; 59 import com.android.messaging.ui.MultiAttachmentLayout; 60 import com.android.messaging.ui.MultiAttachmentLayout.OnAttachmentClickListener; 61 import com.android.messaging.ui.PersonItemView; 62 import com.android.messaging.ui.UIIntents; 63 import com.android.messaging.ui.VideoThumbnailView; 64 import com.android.messaging.util.AccessibilityUtil; 65 import com.android.messaging.util.Assert; 66 import com.android.messaging.util.AvatarUriUtil; 67 import com.android.messaging.util.ContentType; 68 import com.android.messaging.util.ImageUtils; 69 import com.android.messaging.util.OsUtil; 70 import com.android.messaging.util.PhoneUtils; 71 import com.android.messaging.util.UiUtils; 72 import com.android.messaging.util.YouTubeUtil; 73 import com.google.common.base.Predicate; 74 75 import java.util.Collections; 76 import java.util.Comparator; 77 import java.util.List; 78 79 /** 80 * The view for a single entry in a conversation. 81 */ 82 public class ConversationMessageView extends FrameLayout implements View.OnClickListener, 83 View.OnLongClickListener, OnAttachmentClickListener { 84 public interface ConversationMessageViewHost { 85 boolean onAttachmentClick(ConversationMessageView view, MessagePartData attachment, 86 Rect imageBounds, boolean longPress); 87 SubscriptionListEntry getSubscriptionEntryForSelfParticipant(String selfParticipantId, 88 boolean excludeDefault); 89 } 90 91 private final ConversationMessageData mData; 92 93 private LinearLayout mMessageAttachmentsView; 94 private MultiAttachmentLayout mMultiAttachmentView; 95 private AsyncImageView mMessageImageView; 96 private TextView mMessageTextView; 97 private boolean mMessageTextHasLinks; 98 private boolean mMessageHasYouTubeLink; 99 private TextView mStatusTextView; 100 private TextView mTitleTextView; 101 private TextView mMmsInfoTextView; 102 private LinearLayout mMessageTitleLayout; 103 private TextView mSenderNameTextView; 104 private ContactIconView mContactIconView; 105 private ConversationMessageBubbleView mMessageBubble; 106 private View mSubjectView; 107 private TextView mSubjectLabel; 108 private TextView mSubjectText; 109 private View mDeliveredBadge; 110 private ViewGroup mMessageMetadataView; 111 private ViewGroup mMessageTextAndInfoView; 112 private TextView mSimNameView; 113 114 private boolean mOneOnOne; 115 private ConversationMessageViewHost mHost; 116 117 public ConversationMessageView(final Context context, final AttributeSet attrs) { 118 super(context, attrs); 119 // TODO: we should switch to using Binding and DataModel factory methods. 120 mData = new ConversationMessageData(); 121 } 122 123 @Override 124 protected void onFinishInflate() { 125 mContactIconView = (ContactIconView) findViewById(R.id.conversation_icon); 126 mContactIconView.setOnLongClickListener(new OnLongClickListener() { 127 @Override 128 public boolean onLongClick(final View view) { 129 ConversationMessageView.this.performLongClick(); 130 return true; 131 } 132 }); 133 134 mMessageAttachmentsView = (LinearLayout) findViewById(R.id.message_attachments); 135 mMultiAttachmentView = (MultiAttachmentLayout) findViewById(R.id.multiple_attachments); 136 mMultiAttachmentView.setOnAttachmentClickListener(this); 137 138 mMessageImageView = (AsyncImageView) findViewById(R.id.message_image); 139 mMessageImageView.setOnClickListener(this); 140 mMessageImageView.setOnLongClickListener(this); 141 142 mMessageTextView = (TextView) findViewById(R.id.message_text); 143 mMessageTextView.setOnClickListener(this); 144 IgnoreLinkLongClickHelper.ignoreLinkLongClick(mMessageTextView, this); 145 146 mStatusTextView = (TextView) findViewById(R.id.message_status); 147 mTitleTextView = (TextView) findViewById(R.id.message_title); 148 mMmsInfoTextView = (TextView) findViewById(R.id.mms_info); 149 mMessageTitleLayout = (LinearLayout) findViewById(R.id.message_title_layout); 150 mSenderNameTextView = (TextView) findViewById(R.id.message_sender_name); 151 mMessageBubble = (ConversationMessageBubbleView) findViewById(R.id.message_content); 152 mSubjectView = findViewById(R.id.subject_container); 153 mSubjectLabel = (TextView) mSubjectView.findViewById(R.id.subject_label); 154 mSubjectText = (TextView) mSubjectView.findViewById(R.id.subject_text); 155 mDeliveredBadge = findViewById(R.id.smsDeliveredBadge); 156 mMessageMetadataView = (ViewGroup) findViewById(R.id.message_metadata); 157 mMessageTextAndInfoView = (ViewGroup) findViewById(R.id.message_text_and_info); 158 mSimNameView = (TextView) findViewById(R.id.sim_name); 159 } 160 161 @Override 162 protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { 163 final int horizontalSpace = MeasureSpec.getSize(widthMeasureSpec); 164 final int iconSize = getResources() 165 .getDimensionPixelSize(R.dimen.conversation_message_contact_icon_size); 166 167 final int unspecifiedMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 168 final int iconMeasureSpec = MeasureSpec.makeMeasureSpec(iconSize, MeasureSpec.EXACTLY); 169 170 mContactIconView.measure(iconMeasureSpec, iconMeasureSpec); 171 172 final int arrowWidth = 173 getResources().getDimensionPixelSize(R.dimen.message_bubble_arrow_width); 174 175 // We need to subtract contact icon width twice from the horizontal space to get 176 // the max leftover space because we want the message bubble to extend no further than the 177 // starting position of the message bubble in the opposite direction. 178 final int maxLeftoverSpace = horizontalSpace - mContactIconView.getMeasuredWidth() * 2 179 - arrowWidth - getPaddingLeft() - getPaddingRight(); 180 final int messageContentWidthMeasureSpec = MeasureSpec.makeMeasureSpec(maxLeftoverSpace, 181 MeasureSpec.AT_MOST); 182 183 mMessageBubble.measure(messageContentWidthMeasureSpec, unspecifiedMeasureSpec); 184 185 final int maxHeight = Math.max(mContactIconView.getMeasuredHeight(), 186 mMessageBubble.getMeasuredHeight()); 187 setMeasuredDimension(horizontalSpace, maxHeight + getPaddingBottom() + getPaddingTop()); 188 } 189 190 @Override 191 protected void onLayout(final boolean changed, final int left, final int top, final int right, 192 final int bottom) { 193 final boolean isRtl = AccessibilityUtil.isLayoutRtl(this); 194 195 final int iconWidth = mContactIconView.getMeasuredWidth(); 196 final int iconHeight = mContactIconView.getMeasuredHeight(); 197 final int iconTop = getPaddingTop(); 198 final int contentWidth = (right -left) - iconWidth - getPaddingLeft() - getPaddingRight(); 199 final int contentHeight = mMessageBubble.getMeasuredHeight(); 200 final int contentTop = iconTop; 201 202 final int iconLeft; 203 final int contentLeft; 204 if (mData.getIsIncoming()) { 205 if (isRtl) { 206 iconLeft = (right - left) - getPaddingRight() - iconWidth; 207 contentLeft = iconLeft - contentWidth; 208 } else { 209 iconLeft = getPaddingLeft(); 210 contentLeft = iconLeft + iconWidth; 211 } 212 } else { 213 if (isRtl) { 214 iconLeft = getPaddingLeft(); 215 contentLeft = iconLeft + iconWidth; 216 } else { 217 iconLeft = (right - left) - getPaddingRight() - iconWidth; 218 contentLeft = iconLeft - contentWidth; 219 } 220 } 221 222 mContactIconView.layout(iconLeft, iconTop, iconLeft + iconWidth, iconTop + iconHeight); 223 224 mMessageBubble.layout(contentLeft, contentTop, contentLeft + contentWidth, 225 contentTop + contentHeight); 226 } 227 228 /** 229 * Fills in the data associated with this view. 230 * 231 * @param cursor The cursor from a MessageList that this view is in, pointing to its entry. 232 */ 233 public void bind(final Cursor cursor) { 234 bind(cursor, true, null); 235 } 236 237 /** 238 * Fills in the data associated with this view. 239 * 240 * @param cursor The cursor from a MessageList that this view is in, pointing to its entry. 241 * @param oneOnOne Whether this is a 1:1 conversation 242 */ 243 public void bind(final Cursor cursor, 244 final boolean oneOnOne, final String selectedMessageId) { 245 mOneOnOne = oneOnOne; 246 247 // Update our UI model 248 mData.bind(cursor); 249 setSelected(TextUtils.equals(mData.getMessageId(), selectedMessageId)); 250 251 // Update text and image content for the view. 252 updateViewContent(); 253 254 // Update colors and layout parameters for the view. 255 updateViewAppearance(); 256 257 updateContentDescription(); 258 } 259 260 public void setHost(final ConversationMessageViewHost host) { 261 mHost = host; 262 } 263 264 /** 265 * Sets a delay loader instance to manage loading / resuming of image attachments. 266 */ 267 public void setImageViewDelayLoader(final AsyncImageViewDelayLoader delayLoader) { 268 Assert.notNull(mMessageImageView); 269 mMessageImageView.setDelayLoader(delayLoader); 270 mMultiAttachmentView.setImageViewDelayLoader(delayLoader); 271 } 272 273 public ConversationMessageData getData() { 274 return mData; 275 } 276 277 /** 278 * Returns whether we should show simplified visual style for the message view (i.e. hide the 279 * avatar and bubble arrow, reduce padding). 280 */ 281 private boolean shouldShowSimplifiedVisualStyle() { 282 return mData.getCanClusterWithPreviousMessage(); 283 } 284 285 /** 286 * Returns whether we need to show message bubble arrow. We don't show arrow if the message 287 * contains media attachments or if shouldShowSimplifiedVisualStyle() is true. 288 */ 289 private boolean shouldShowMessageBubbleArrow() { 290 return !shouldShowSimplifiedVisualStyle() 291 && !(mData.hasAttachments() || mMessageHasYouTubeLink); 292 } 293 294 /** 295 * Returns whether we need to show a message bubble for text content. 296 */ 297 private boolean shouldShowMessageTextBubble() { 298 if (mData.hasText()) { 299 return true; 300 } 301 final String subjectText = MmsUtils.cleanseMmsSubject(getResources(), 302 mData.getMmsSubject()); 303 if (!TextUtils.isEmpty(subjectText)) { 304 return true; 305 } 306 return false; 307 } 308 309 private void updateViewContent() { 310 updateMessageContent(); 311 int titleResId = -1; 312 int statusResId = -1; 313 String statusText = null; 314 switch(mData.getStatus()) { 315 case MessageData.BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING: 316 case MessageData.BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING: 317 case MessageData.BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD: 318 case MessageData.BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD: 319 titleResId = R.string.message_title_downloading; 320 statusResId = R.string.message_status_downloading; 321 break; 322 323 case MessageData.BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD: 324 if (!OsUtil.isSecondaryUser()) { 325 titleResId = R.string.message_title_manual_download; 326 if (isSelected()) { 327 statusResId = R.string.message_status_download_action; 328 } else { 329 statusResId = R.string.message_status_download; 330 } 331 } 332 break; 333 334 case MessageData.BUGLE_STATUS_INCOMING_EXPIRED_OR_NOT_AVAILABLE: 335 if (!OsUtil.isSecondaryUser()) { 336 titleResId = R.string.message_title_download_failed; 337 statusResId = R.string.message_status_download_error; 338 } 339 break; 340 341 case MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED: 342 if (!OsUtil.isSecondaryUser()) { 343 titleResId = R.string.message_title_download_failed; 344 if (isSelected()) { 345 statusResId = R.string.message_status_download_action; 346 } else { 347 statusResId = R.string.message_status_download; 348 } 349 } 350 break; 351 352 case MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND: 353 case MessageData.BUGLE_STATUS_OUTGOING_SENDING: 354 statusResId = R.string.message_status_sending; 355 break; 356 357 case MessageData.BUGLE_STATUS_OUTGOING_RESENDING: 358 case MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY: 359 statusResId = R.string.message_status_send_retrying; 360 break; 361 362 case MessageData.BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER: 363 statusResId = R.string.message_status_send_failed_emergency_number; 364 break; 365 366 case MessageData.BUGLE_STATUS_OUTGOING_FAILED: 367 // don't show the error state unless we're the default sms app 368 if (PhoneUtils.getDefault().isDefaultSmsApp()) { 369 if (isSelected()) { 370 statusResId = R.string.message_status_resend; 371 } else { 372 statusResId = MmsUtils.mapRawStatusToErrorResourceId( 373 mData.getStatus(), mData.getRawTelephonyStatus()); 374 } 375 break; 376 } 377 // FALL THROUGH HERE 378 379 case MessageData.BUGLE_STATUS_OUTGOING_COMPLETE: 380 case MessageData.BUGLE_STATUS_INCOMING_COMPLETE: 381 default: 382 if (!mData.getCanClusterWithNextMessage()) { 383 statusText = mData.getFormattedReceivedTimeStamp(); 384 } 385 break; 386 } 387 388 final boolean titleVisible = (titleResId >= 0); 389 if (titleVisible) { 390 final String titleText = getResources().getString(titleResId); 391 mTitleTextView.setText(titleText); 392 393 final String mmsInfoText = getResources().getString( 394 R.string.mms_info, 395 Formatter.formatFileSize(getContext(), mData.getSmsMessageSize()), 396 DateUtils.formatDateTime( 397 getContext(), 398 mData.getMmsExpiry(), 399 DateUtils.FORMAT_SHOW_DATE | 400 DateUtils.FORMAT_SHOW_TIME | 401 DateUtils.FORMAT_NUMERIC_DATE | 402 DateUtils.FORMAT_NO_YEAR)); 403 mMmsInfoTextView.setText(mmsInfoText); 404 mMessageTitleLayout.setVisibility(View.VISIBLE); 405 } else { 406 mMessageTitleLayout.setVisibility(View.GONE); 407 } 408 409 final String subjectText = MmsUtils.cleanseMmsSubject(getResources(), 410 mData.getMmsSubject()); 411 final boolean subjectVisible = !TextUtils.isEmpty(subjectText); 412 413 final boolean senderNameVisible = !mOneOnOne && !mData.getCanClusterWithNextMessage() 414 && mData.getIsIncoming(); 415 if (senderNameVisible) { 416 mSenderNameTextView.setText(mData.getSenderDisplayName()); 417 mSenderNameTextView.setVisibility(View.VISIBLE); 418 } else { 419 mSenderNameTextView.setVisibility(View.GONE); 420 } 421 422 if (statusResId >= 0) { 423 statusText = getResources().getString(statusResId); 424 } 425 426 // We set the text even if the view will be GONE for accessibility 427 mStatusTextView.setText(statusText); 428 final boolean statusVisible = !TextUtils.isEmpty(statusText); 429 if (statusVisible) { 430 mStatusTextView.setVisibility(View.VISIBLE); 431 } else { 432 mStatusTextView.setVisibility(View.GONE); 433 } 434 435 final boolean deliveredBadgeVisible = 436 mData.getStatus() == MessageData.BUGLE_STATUS_OUTGOING_DELIVERED; 437 mDeliveredBadge.setVisibility(deliveredBadgeVisible ? View.VISIBLE : View.GONE); 438 439 // Update the sim indicator. 440 final boolean showSimIconAsIncoming = mData.getIsIncoming() && 441 (!mData.hasAttachments() || shouldShowMessageTextBubble()); 442 final SubscriptionListEntry subscriptionEntry = 443 mHost.getSubscriptionEntryForSelfParticipant(mData.getSelfParticipantId(), 444 true /* excludeDefault */); 445 final boolean simNameVisible = subscriptionEntry != null && 446 !TextUtils.isEmpty(subscriptionEntry.displayName) && 447 !mData.getCanClusterWithNextMessage(); 448 if (simNameVisible) { 449 final String simNameText = mData.getIsIncoming() ? getResources().getString( 450 R.string.incoming_sim_name_text, subscriptionEntry.displayName) : 451 subscriptionEntry.displayName; 452 mSimNameView.setText(simNameText); 453 mSimNameView.setTextColor(showSimIconAsIncoming ? getResources().getColor( 454 R.color.timestamp_text_incoming) : subscriptionEntry.displayColor); 455 mSimNameView.setVisibility(VISIBLE); 456 } else { 457 mSimNameView.setText(null); 458 mSimNameView.setVisibility(GONE); 459 } 460 461 final boolean metadataVisible = senderNameVisible || statusVisible 462 || deliveredBadgeVisible || simNameVisible; 463 mMessageMetadataView.setVisibility(metadataVisible ? View.VISIBLE : View.GONE); 464 465 final boolean messageTextAndOrInfoVisible = titleVisible || subjectVisible 466 || mData.hasText() || metadataVisible; 467 mMessageTextAndInfoView.setVisibility( 468 messageTextAndOrInfoVisible ? View.VISIBLE : View.GONE); 469 470 if (shouldShowSimplifiedVisualStyle()) { 471 mContactIconView.setVisibility(View.GONE); 472 mContactIconView.setImageResourceUri(null); 473 } else { 474 mContactIconView.setVisibility(View.VISIBLE); 475 final Uri avatarUri = AvatarUriUtil.createAvatarUri( 476 mData.getSenderProfilePhotoUri(), 477 mData.getSenderFullName(), 478 mData.getSenderNormalizedDestination(), 479 mData.getSenderContactLookupKey()); 480 mContactIconView.setImageResourceUri(avatarUri, mData.getSenderContactId(), 481 mData.getSenderContactLookupKey(), mData.getSenderNormalizedDestination()); 482 } 483 } 484 485 private void updateMessageContent() { 486 // We must update the text before the attachments since we search the text to see if we 487 // should make a preview youtube image in the attachments 488 updateMessageText(); 489 updateMessageAttachments(); 490 updateMessageSubject(); 491 mMessageBubble.bind(mData); 492 } 493 494 private void updateMessageAttachments() { 495 // Bind video, audio, and VCard attachments. If there are multiple, they stack vertically. 496 bindAttachmentsOfSameType(sVideoFilter, 497 R.layout.message_video_attachment, mVideoViewBinder, VideoThumbnailView.class); 498 bindAttachmentsOfSameType(sAudioFilter, 499 R.layout.message_audio_attachment, mAudioViewBinder, AudioAttachmentView.class); 500 bindAttachmentsOfSameType(sVCardFilter, 501 R.layout.message_vcard_attachment, mVCardViewBinder, PersonItemView.class); 502 503 // Bind image attachments. If there are multiple, they are shown in a collage view. 504 final List<MessagePartData> imageParts = mData.getAttachments(sImageFilter); 505 if (imageParts.size() > 1) { 506 Collections.sort(imageParts, sImageComparator); 507 mMultiAttachmentView.bindAttachments(imageParts, null, imageParts.size()); 508 mMultiAttachmentView.setVisibility(View.VISIBLE); 509 } else { 510 mMultiAttachmentView.setVisibility(View.GONE); 511 } 512 513 // In the case that we have no image attachments and exactly one youtube link in a message 514 // then we will show a preview. 515 String youtubeThumbnailUrl = null; 516 String originalYoutubeLink = null; 517 if (mMessageTextHasLinks && imageParts.size() == 0) { 518 CharSequence messageTextWithSpans = mMessageTextView.getText(); 519 final URLSpan[] spans = ((Spanned) messageTextWithSpans).getSpans(0, 520 messageTextWithSpans.length(), URLSpan.class); 521 for (URLSpan span : spans) { 522 String url = span.getURL(); 523 String youtubeLinkForUrl = YouTubeUtil.getYoutubePreviewImageLink(url); 524 if (!TextUtils.isEmpty(youtubeLinkForUrl)) { 525 if (TextUtils.isEmpty(youtubeThumbnailUrl)) { 526 // Save the youtube link if we don't already have one 527 youtubeThumbnailUrl = youtubeLinkForUrl; 528 originalYoutubeLink = url; 529 } else { 530 // We already have a youtube link. This means we have two youtube links so 531 // we shall show none. 532 youtubeThumbnailUrl = null; 533 originalYoutubeLink = null; 534 break; 535 } 536 } 537 } 538 } 539 // We need to keep track if we have a youtube link in the message so that we will not show 540 // the arrow 541 mMessageHasYouTubeLink = !TextUtils.isEmpty(youtubeThumbnailUrl); 542 543 // We will show the message image view if there is one attachment or one youtube link 544 if (imageParts.size() == 1 || mMessageHasYouTubeLink) { 545 // Get the display metrics for a hint for how large to pull the image data into 546 final WindowManager windowManager = (WindowManager) getContext(). 547 getSystemService(Context.WINDOW_SERVICE); 548 final DisplayMetrics displayMetrics = new DisplayMetrics(); 549 windowManager.getDefaultDisplay().getMetrics(displayMetrics); 550 551 final int iconSize = getResources() 552 .getDimensionPixelSize(R.dimen.conversation_message_contact_icon_size); 553 final int desiredWidth = displayMetrics.widthPixels - iconSize - iconSize; 554 555 if (imageParts.size() == 1) { 556 final MessagePartData imagePart = imageParts.get(0); 557 // If the image is big, we want to scale it down to save memory since we're going to 558 // scale it down to fit into the bubble width. We don't constrain the height. 559 final ImageRequestDescriptor imageRequest = 560 new MessagePartImageRequestDescriptor(imagePart, 561 desiredWidth, 562 MessagePartData.UNSPECIFIED_SIZE, 563 false); 564 adjustImageViewBounds(imagePart); 565 mMessageImageView.setImageResourceId(imageRequest); 566 mMessageImageView.setTag(imagePart); 567 } else { 568 // Youtube Thumbnail image 569 final ImageRequestDescriptor imageRequest = 570 new UriImageRequestDescriptor(Uri.parse(youtubeThumbnailUrl), desiredWidth, 571 MessagePartData.UNSPECIFIED_SIZE, true /* allowCompression */, 572 true /* isStatic */, false /* cropToCircle */, 573 ImageUtils.DEFAULT_CIRCLE_BACKGROUND_COLOR /* circleBackgroundColor */, 574 ImageUtils.DEFAULT_CIRCLE_STROKE_COLOR /* circleStrokeColor */); 575 mMessageImageView.setImageResourceId(imageRequest); 576 mMessageImageView.setTag(originalYoutubeLink); 577 } 578 mMessageImageView.setVisibility(View.VISIBLE); 579 } else { 580 mMessageImageView.setImageResourceId(null); 581 mMessageImageView.setVisibility(View.GONE); 582 } 583 584 // Show the message attachments container if any of its children are visible 585 boolean attachmentsVisible = false; 586 for (int i = 0, size = mMessageAttachmentsView.getChildCount(); i < size; i++) { 587 final View attachmentView = mMessageAttachmentsView.getChildAt(i); 588 if (attachmentView.getVisibility() == View.VISIBLE) { 589 attachmentsVisible = true; 590 break; 591 } 592 } 593 mMessageAttachmentsView.setVisibility(attachmentsVisible ? View.VISIBLE : View.GONE); 594 } 595 596 private void bindAttachmentsOfSameType(final Predicate<MessagePartData> attachmentTypeFilter, 597 final int attachmentViewLayoutRes, final AttachmentViewBinder viewBinder, 598 final Class<?> attachmentViewClass) { 599 final LayoutInflater layoutInflater = LayoutInflater.from(getContext()); 600 601 // Iterate through all attachments of a particular type (video, audio, etc). 602 // Find the first attachment index that matches the given type if possible. 603 int attachmentViewIndex = -1; 604 View existingAttachmentView; 605 do { 606 existingAttachmentView = mMessageAttachmentsView.getChildAt(++attachmentViewIndex); 607 } while (existingAttachmentView != null && 608 !(attachmentViewClass.isInstance(existingAttachmentView))); 609 610 for (final MessagePartData attachment : mData.getAttachments(attachmentTypeFilter)) { 611 View attachmentView = mMessageAttachmentsView.getChildAt(attachmentViewIndex); 612 if (!attachmentViewClass.isInstance(attachmentView)) { 613 attachmentView = layoutInflater.inflate(attachmentViewLayoutRes, 614 mMessageAttachmentsView, false /* attachToRoot */); 615 attachmentView.setOnClickListener(this); 616 attachmentView.setOnLongClickListener(this); 617 mMessageAttachmentsView.addView(attachmentView, attachmentViewIndex); 618 } 619 viewBinder.bindView(attachmentView, attachment); 620 attachmentView.setTag(attachment); 621 attachmentView.setVisibility(View.VISIBLE); 622 attachmentViewIndex++; 623 } 624 // If there are unused views left over, unbind or remove them. 625 while (attachmentViewIndex < mMessageAttachmentsView.getChildCount()) { 626 final View attachmentView = mMessageAttachmentsView.getChildAt(attachmentViewIndex); 627 if (attachmentViewClass.isInstance(attachmentView)) { 628 mMessageAttachmentsView.removeViewAt(attachmentViewIndex); 629 } else { 630 // No more views of this type; we're done. 631 break; 632 } 633 } 634 } 635 636 private void updateMessageSubject() { 637 final String subjectText = MmsUtils.cleanseMmsSubject(getResources(), 638 mData.getMmsSubject()); 639 final boolean subjectVisible = !TextUtils.isEmpty(subjectText); 640 641 if (subjectVisible) { 642 mSubjectText.setText(subjectText); 643 mSubjectView.setVisibility(View.VISIBLE); 644 } else { 645 mSubjectView.setVisibility(View.GONE); 646 } 647 } 648 649 private void updateMessageText() { 650 final String text = mData.getText(); 651 if (!TextUtils.isEmpty(text)) { 652 mMessageTextView.setText(text); 653 // Linkify phone numbers, web urls, emails, and map addresses to allow users to 654 // click on them and take the default intent. 655 mMessageTextHasLinks = Linkify.addLinks(mMessageTextView, Linkify.ALL); 656 mMessageTextView.setVisibility(View.VISIBLE); 657 } else { 658 mMessageTextView.setVisibility(View.GONE); 659 mMessageTextHasLinks = false; 660 } 661 } 662 663 private void updateViewAppearance() { 664 final Resources res = getResources(); 665 final ConversationDrawables drawableProvider = ConversationDrawables.get(); 666 final boolean incoming = mData.getIsIncoming(); 667 final boolean outgoing = !incoming; 668 final boolean showArrow = shouldShowMessageBubbleArrow(); 669 670 final int messageTopPaddingClustered = 671 res.getDimensionPixelSize(R.dimen.message_padding_same_author); 672 final int messageTopPaddingDefault = 673 res.getDimensionPixelSize(R.dimen.message_padding_default); 674 final int arrowWidth = res.getDimensionPixelOffset(R.dimen.message_bubble_arrow_width); 675 final int messageTextMinHeightDefault = res.getDimensionPixelSize( 676 R.dimen.conversation_message_contact_icon_size); 677 final int messageTextLeftRightPadding = res.getDimensionPixelOffset( 678 R.dimen.message_text_left_right_padding); 679 final int textTopPaddingDefault = res.getDimensionPixelOffset( 680 R.dimen.message_text_top_padding); 681 final int textBottomPaddingDefault = res.getDimensionPixelOffset( 682 R.dimen.message_text_bottom_padding); 683 684 // These values depend on whether the message has text, attachments, or both. 685 // We intentionally don't set defaults, so the compiler will tell us if we forget 686 // to set one of them, or if we set one more than once. 687 final int contentLeftPadding, contentRightPadding; 688 final Drawable textBackground; 689 final int textMinHeight; 690 final int textTopMargin; 691 final int textTopPadding, textBottomPadding; 692 final int textLeftPadding, textRightPadding; 693 694 if (mData.hasAttachments()) { 695 if (shouldShowMessageTextBubble()) { 696 // Text and attachment(s) 697 contentLeftPadding = incoming ? arrowWidth : 0; 698 contentRightPadding = outgoing ? arrowWidth : 0; 699 textBackground = drawableProvider.getBubbleDrawable( 700 isSelected(), 701 incoming, 702 false /* needArrow */, 703 mData.hasIncomingErrorStatus()); 704 textMinHeight = messageTextMinHeightDefault; 705 textTopMargin = messageTopPaddingClustered; 706 textTopPadding = textTopPaddingDefault; 707 textBottomPadding = textBottomPaddingDefault; 708 textLeftPadding = messageTextLeftRightPadding; 709 textRightPadding = messageTextLeftRightPadding; 710 } else { 711 // Attachment(s) only 712 contentLeftPadding = incoming ? arrowWidth : 0; 713 contentRightPadding = outgoing ? arrowWidth : 0; 714 textBackground = null; 715 textMinHeight = 0; 716 textTopMargin = 0; 717 textTopPadding = 0; 718 textBottomPadding = 0; 719 textLeftPadding = 0; 720 textRightPadding = 0; 721 } 722 } else { 723 // Text only 724 contentLeftPadding = (!showArrow && incoming) ? arrowWidth : 0; 725 contentRightPadding = (!showArrow && outgoing) ? arrowWidth : 0; 726 textBackground = drawableProvider.getBubbleDrawable( 727 isSelected(), 728 incoming, 729 shouldShowMessageBubbleArrow(), 730 mData.hasIncomingErrorStatus()); 731 textMinHeight = messageTextMinHeightDefault; 732 textTopMargin = 0; 733 textTopPadding = textTopPaddingDefault; 734 textBottomPadding = textBottomPaddingDefault; 735 if (showArrow && incoming) { 736 textLeftPadding = messageTextLeftRightPadding + arrowWidth; 737 } else { 738 textLeftPadding = messageTextLeftRightPadding; 739 } 740 if (showArrow && outgoing) { 741 textRightPadding = messageTextLeftRightPadding + arrowWidth; 742 } else { 743 textRightPadding = messageTextLeftRightPadding; 744 } 745 } 746 747 // These values do not depend on whether the message includes attachments 748 final int gravity = incoming ? (Gravity.START | Gravity.CENTER_VERTICAL) : 749 (Gravity.END | Gravity.CENTER_VERTICAL); 750 final int messageTopPadding = shouldShowSimplifiedVisualStyle() ? 751 messageTopPaddingClustered : messageTopPaddingDefault; 752 final int metadataTopPadding = res.getDimensionPixelOffset( 753 R.dimen.message_metadata_top_padding); 754 755 // Update the message text/info views 756 ImageUtils.setBackgroundDrawableOnView(mMessageTextAndInfoView, textBackground); 757 mMessageTextAndInfoView.setMinimumHeight(textMinHeight); 758 final LinearLayout.LayoutParams textAndInfoLayoutParams = 759 (LinearLayout.LayoutParams) mMessageTextAndInfoView.getLayoutParams(); 760 textAndInfoLayoutParams.topMargin = textTopMargin; 761 762 if (UiUtils.isRtlMode()) { 763 // Need to switch right and left padding in RtL mode 764 mMessageTextAndInfoView.setPadding(textRightPadding, textTopPadding, textLeftPadding, 765 textBottomPadding); 766 mMessageBubble.setPadding(contentRightPadding, 0, contentLeftPadding, 0); 767 } else { 768 mMessageTextAndInfoView.setPadding(textLeftPadding, textTopPadding, textRightPadding, 769 textBottomPadding); 770 mMessageBubble.setPadding(contentLeftPadding, 0, contentRightPadding, 0); 771 } 772 773 // Update the message row and message bubble views 774 setPadding(getPaddingLeft(), messageTopPadding, getPaddingRight(), 0); 775 mMessageBubble.setGravity(gravity); 776 updateMessageAttachmentsAppearance(gravity); 777 778 mMessageMetadataView.setPadding(0, metadataTopPadding, 0, 0); 779 780 updateTextAppearance(); 781 782 requestLayout(); 783 } 784 785 private void updateContentDescription() { 786 StringBuilder description = new StringBuilder(); 787 788 Resources res = getResources(); 789 String separator = res.getString(R.string.enumeration_comma); 790 791 // Sender information 792 boolean hasPlainTextMessage = !(TextUtils.isEmpty(mData.getText()) || 793 mMessageTextHasLinks); 794 if (mData.getIsIncoming()) { 795 int senderResId = hasPlainTextMessage 796 ? R.string.incoming_text_sender_content_description 797 : R.string.incoming_sender_content_description; 798 description.append(res.getString(senderResId, mData.getSenderDisplayName())); 799 } else { 800 int senderResId = hasPlainTextMessage 801 ? R.string.outgoing_text_sender_content_description 802 : R.string.outgoing_sender_content_description; 803 description.append(res.getString(senderResId)); 804 } 805 806 if (mSubjectView.getVisibility() == View.VISIBLE) { 807 description.append(separator); 808 description.append(mSubjectText.getText()); 809 } 810 811 if (mMessageTextView.getVisibility() == View.VISIBLE) { 812 // If the message has hyperlinks, we will let the user navigate to the text message so 813 // that the hyperlink can be clicked. Otherwise, the text message does not need to 814 // be reachable. 815 if (mMessageTextHasLinks) { 816 mMessageTextView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); 817 } else { 818 mMessageTextView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); 819 description.append(separator); 820 description.append(mMessageTextView.getText()); 821 } 822 } 823 824 if (mMessageTitleLayout.getVisibility() == View.VISIBLE) { 825 description.append(separator); 826 description.append(mTitleTextView.getText()); 827 828 description.append(separator); 829 description.append(mMmsInfoTextView.getText()); 830 } 831 832 if (mStatusTextView.getVisibility() == View.VISIBLE) { 833 description.append(separator); 834 description.append(mStatusTextView.getText()); 835 } 836 837 if (mSimNameView.getVisibility() == View.VISIBLE) { 838 description.append(separator); 839 description.append(mSimNameView.getText()); 840 } 841 842 if (mDeliveredBadge.getVisibility() == View.VISIBLE) { 843 description.append(separator); 844 description.append(res.getString(R.string.delivered_status_content_description)); 845 } 846 847 setContentDescription(description); 848 } 849 850 private void updateMessageAttachmentsAppearance(final int gravity) { 851 mMessageAttachmentsView.setGravity(gravity); 852 853 // Tint image/video attachments when selected 854 final int selectedImageTint = getResources().getColor(R.color.message_image_selected_tint); 855 if (mMessageImageView.getVisibility() == View.VISIBLE) { 856 if (isSelected()) { 857 mMessageImageView.setColorFilter(selectedImageTint); 858 } else { 859 mMessageImageView.clearColorFilter(); 860 } 861 } 862 if (mMultiAttachmentView.getVisibility() == View.VISIBLE) { 863 if (isSelected()) { 864 mMultiAttachmentView.setColorFilter(selectedImageTint); 865 } else { 866 mMultiAttachmentView.clearColorFilter(); 867 } 868 } 869 for (int i = 0, size = mMessageAttachmentsView.getChildCount(); i < size; i++) { 870 final View attachmentView = mMessageAttachmentsView.getChildAt(i); 871 if (attachmentView instanceof VideoThumbnailView 872 && attachmentView.getVisibility() == View.VISIBLE) { 873 final VideoThumbnailView videoView = (VideoThumbnailView) attachmentView; 874 if (isSelected()) { 875 videoView.setColorFilter(selectedImageTint); 876 } else { 877 videoView.clearColorFilter(); 878 } 879 } 880 } 881 882 // If there are multiple attachment bubbles in a single message, add some separation. 883 final int multipleAttachmentPadding = 884 getResources().getDimensionPixelSize(R.dimen.message_padding_same_author); 885 886 boolean previousVisibleView = false; 887 for (int i = 0, size = mMessageAttachmentsView.getChildCount(); i < size; i++) { 888 final View attachmentView = mMessageAttachmentsView.getChildAt(i); 889 if (attachmentView.getVisibility() == View.VISIBLE) { 890 final int margin = previousVisibleView ? multipleAttachmentPadding : 0; 891 ((LinearLayout.LayoutParams) attachmentView.getLayoutParams()).topMargin = margin; 892 // updateViewAppearance calls requestLayout() at the end, so we don't need to here 893 previousVisibleView = true; 894 } 895 } 896 } 897 898 private void updateTextAppearance() { 899 int messageColorResId; 900 int statusColorResId = -1; 901 int infoColorResId = -1; 902 int timestampColorResId; 903 int subjectLabelColorResId; 904 if (isSelected()) { 905 messageColorResId = R.color.message_text_color_incoming; 906 statusColorResId = R.color.message_action_status_text; 907 infoColorResId = R.color.message_action_info_text; 908 if (shouldShowMessageTextBubble()) { 909 timestampColorResId = R.color.message_action_timestamp_text; 910 subjectLabelColorResId = R.color.message_action_timestamp_text; 911 } else { 912 // If there's no text, the timestamp will be shown below the attachments, 913 // against the conversation view background. 914 timestampColorResId = R.color.timestamp_text_outgoing; 915 subjectLabelColorResId = R.color.timestamp_text_outgoing; 916 } 917 } else { 918 messageColorResId = (mData.getIsIncoming() ? 919 R.color.message_text_color_incoming : R.color.message_text_color_outgoing); 920 statusColorResId = messageColorResId; 921 infoColorResId = R.color.timestamp_text_incoming; 922 switch(mData.getStatus()) { 923 924 case MessageData.BUGLE_STATUS_OUTGOING_FAILED: 925 case MessageData.BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER: 926 timestampColorResId = R.color.message_failed_timestamp_text; 927 subjectLabelColorResId = R.color.timestamp_text_outgoing; 928 break; 929 930 case MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND: 931 case MessageData.BUGLE_STATUS_OUTGOING_SENDING: 932 case MessageData.BUGLE_STATUS_OUTGOING_RESENDING: 933 case MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY: 934 case MessageData.BUGLE_STATUS_OUTGOING_COMPLETE: 935 case MessageData.BUGLE_STATUS_OUTGOING_DELIVERED: 936 timestampColorResId = R.color.timestamp_text_outgoing; 937 subjectLabelColorResId = R.color.timestamp_text_outgoing; 938 break; 939 940 case MessageData.BUGLE_STATUS_INCOMING_EXPIRED_OR_NOT_AVAILABLE: 941 case MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED: 942 messageColorResId = R.color.message_text_color_incoming_download_failed; 943 timestampColorResId = R.color.message_download_failed_timestamp_text; 944 subjectLabelColorResId = R.color.message_text_color_incoming_download_failed; 945 statusColorResId = R.color.message_download_failed_status_text; 946 infoColorResId = R.color.message_info_text_incoming_download_failed; 947 break; 948 949 case MessageData.BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING: 950 case MessageData.BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING: 951 case MessageData.BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD: 952 case MessageData.BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD: 953 case MessageData.BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD: 954 timestampColorResId = R.color.message_text_color_incoming; 955 subjectLabelColorResId = R.color.message_text_color_incoming; 956 infoColorResId = R.color.timestamp_text_incoming; 957 break; 958 959 case MessageData.BUGLE_STATUS_INCOMING_COMPLETE: 960 default: 961 timestampColorResId = R.color.timestamp_text_incoming; 962 subjectLabelColorResId = R.color.timestamp_text_incoming; 963 infoColorResId = -1; // Not used 964 break; 965 } 966 } 967 final int messageColor = getResources().getColor(messageColorResId); 968 mMessageTextView.setTextColor(messageColor); 969 mMessageTextView.setLinkTextColor(messageColor); 970 mSubjectText.setTextColor(messageColor); 971 if (statusColorResId >= 0) { 972 mTitleTextView.setTextColor(getResources().getColor(statusColorResId)); 973 } 974 if (infoColorResId >= 0) { 975 mMmsInfoTextView.setTextColor(getResources().getColor(infoColorResId)); 976 } 977 if (timestampColorResId == R.color.timestamp_text_incoming && 978 mData.hasAttachments() && !shouldShowMessageTextBubble()) { 979 timestampColorResId = R.color.timestamp_text_outgoing; 980 } 981 mStatusTextView.setTextColor(getResources().getColor(timestampColorResId)); 982 983 mSubjectLabel.setTextColor(getResources().getColor(subjectLabelColorResId)); 984 mSenderNameTextView.setTextColor(getResources().getColor(timestampColorResId)); 985 } 986 987 /** 988 * If we don't know the size of the image, we want to show it in a fixed-sized frame to 989 * avoid janks when the image is loaded and resized. Otherwise, we can set the imageview to 990 * take on normal layout params. 991 */ 992 private void adjustImageViewBounds(final MessagePartData imageAttachment) { 993 Assert.isTrue(ContentType.isImageType(imageAttachment.getContentType())); 994 final ViewGroup.LayoutParams layoutParams = mMessageImageView.getLayoutParams(); 995 if (imageAttachment.getWidth() == MessagePartData.UNSPECIFIED_SIZE || 996 imageAttachment.getHeight() == MessagePartData.UNSPECIFIED_SIZE) { 997 // We don't know the size of the image attachment, enable letterboxing on the image 998 // and show a fixed sized attachment. This should happen at most once per image since 999 // after the image is loaded we then save the image dimensions to the db so that the 1000 // next time we can display the full size. 1001 layoutParams.width = getResources() 1002 .getDimensionPixelSize(R.dimen.image_attachment_fallback_width); 1003 layoutParams.height = getResources() 1004 .getDimensionPixelSize(R.dimen.image_attachment_fallback_height); 1005 mMessageImageView.setScaleType(ScaleType.CENTER_CROP); 1006 } else { 1007 layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; 1008 layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; 1009 // ScaleType.CENTER_INSIDE and FIT_CENTER behave similarly for most images. However, 1010 // FIT_CENTER works better for small images as it enlarges the image such that the 1011 // minimum size ("android:minWidth" etc) is honored. 1012 mMessageImageView.setScaleType(ScaleType.FIT_CENTER); 1013 } 1014 } 1015 1016 @Override 1017 public void onClick(final View view) { 1018 final Object tag = view.getTag(); 1019 if (tag instanceof MessagePartData) { 1020 final Rect bounds = UiUtils.getMeasuredBoundsOnScreen(view); 1021 onAttachmentClick((MessagePartData) tag, bounds, false /* longPress */); 1022 } else if (tag instanceof String) { 1023 // Currently the only object that would make a tag of a string is a youtube preview 1024 // image 1025 UIIntents.get().launchBrowserForUrl(getContext(), (String) tag); 1026 } 1027 } 1028 1029 @Override 1030 public boolean onLongClick(final View view) { 1031 if (view == mMessageTextView) { 1032 // Preemptively handle the long click event on message text so it's not handled by 1033 // the link spans. 1034 return performLongClick(); 1035 } 1036 1037 final Object tag = view.getTag(); 1038 if (tag instanceof MessagePartData) { 1039 final Rect bounds = UiUtils.getMeasuredBoundsOnScreen(view); 1040 return onAttachmentClick((MessagePartData) tag, bounds, true /* longPress */); 1041 } 1042 1043 return false; 1044 } 1045 1046 @Override 1047 public boolean onAttachmentClick(final MessagePartData attachment, 1048 final Rect viewBoundsOnScreen, final boolean longPress) { 1049 return mHost.onAttachmentClick(this, attachment, viewBoundsOnScreen, longPress); 1050 } 1051 1052 public ContactIconView getContactIconView() { 1053 return mContactIconView; 1054 } 1055 1056 // Sort photos in MultiAttachLayout in the same order as the ConversationImagePartsView 1057 static final Comparator<MessagePartData> sImageComparator = new Comparator<MessagePartData>(){ 1058 @Override 1059 public int compare(final MessagePartData x, final MessagePartData y) { 1060 return x.getPartId().compareTo(y.getPartId()); 1061 } 1062 }; 1063 1064 static final Predicate<MessagePartData> sVideoFilter = new Predicate<MessagePartData>() { 1065 @Override 1066 public boolean apply(final MessagePartData part) { 1067 return part.isVideo(); 1068 } 1069 }; 1070 1071 static final Predicate<MessagePartData> sAudioFilter = new Predicate<MessagePartData>() { 1072 @Override 1073 public boolean apply(final MessagePartData part) { 1074 return part.isAudio(); 1075 } 1076 }; 1077 1078 static final Predicate<MessagePartData> sVCardFilter = new Predicate<MessagePartData>() { 1079 @Override 1080 public boolean apply(final MessagePartData part) { 1081 return part.isVCard(); 1082 } 1083 }; 1084 1085 static final Predicate<MessagePartData> sImageFilter = new Predicate<MessagePartData>() { 1086 @Override 1087 public boolean apply(final MessagePartData part) { 1088 return part.isImage(); 1089 } 1090 }; 1091 1092 interface AttachmentViewBinder { 1093 void bindView(View view, MessagePartData attachment); 1094 void unbind(View view); 1095 } 1096 1097 final AttachmentViewBinder mVideoViewBinder = new AttachmentViewBinder() { 1098 @Override 1099 public void bindView(final View view, final MessagePartData attachment) { 1100 ((VideoThumbnailView) view).setSource(attachment, mData.getIsIncoming()); 1101 } 1102 1103 @Override 1104 public void unbind(final View view) { 1105 ((VideoThumbnailView) view).setSource((Uri) null, mData.getIsIncoming()); 1106 } 1107 }; 1108 1109 final AttachmentViewBinder mAudioViewBinder = new AttachmentViewBinder() { 1110 @Override 1111 public void bindView(final View view, final MessagePartData attachment) { 1112 final AudioAttachmentView audioView = (AudioAttachmentView) view; 1113 audioView.bindMessagePartData(attachment, mData.getIsIncoming(), isSelected()); 1114 audioView.setBackground(ConversationDrawables.get().getBubbleDrawable( 1115 isSelected(), mData.getIsIncoming(), false /* needArrow */, 1116 mData.hasIncomingErrorStatus())); 1117 } 1118 1119 @Override 1120 public void unbind(final View view) { 1121 ((AudioAttachmentView) view).bindMessagePartData(null, mData.getIsIncoming(), false); 1122 } 1123 }; 1124 1125 final AttachmentViewBinder mVCardViewBinder = new AttachmentViewBinder() { 1126 @Override 1127 public void bindView(final View view, final MessagePartData attachment) { 1128 final PersonItemView personView = (PersonItemView) view; 1129 personView.bind(DataModel.get().createVCardContactItemData(getContext(), 1130 attachment)); 1131 personView.setBackground(ConversationDrawables.get().getBubbleDrawable( 1132 isSelected(), mData.getIsIncoming(), false /* needArrow */, 1133 mData.hasIncomingErrorStatus())); 1134 final int nameTextColorRes; 1135 final int detailsTextColorRes; 1136 if (isSelected()) { 1137 nameTextColorRes = R.color.message_text_color_incoming; 1138 detailsTextColorRes = R.color.message_text_color_incoming; 1139 } else { 1140 nameTextColorRes = mData.getIsIncoming() ? R.color.message_text_color_incoming 1141 : R.color.message_text_color_outgoing; 1142 detailsTextColorRes = mData.getIsIncoming() ? R.color.timestamp_text_incoming 1143 : R.color.timestamp_text_outgoing; 1144 } 1145 personView.setNameTextColor(getResources().getColor(nameTextColorRes)); 1146 personView.setDetailsTextColor(getResources().getColor(detailsTextColorRes)); 1147 } 1148 1149 @Override 1150 public void unbind(final View view) { 1151 ((PersonItemView) view).bind(null); 1152 } 1153 }; 1154 1155 /** 1156 * A helper class that allows us to handle long clicks on linkified message text view (i.e. to 1157 * select the message) so it's not handled by the link spans to launch apps for the links. 1158 */ 1159 private static class IgnoreLinkLongClickHelper implements OnLongClickListener, OnTouchListener { 1160 private boolean mIsLongClick; 1161 private final OnLongClickListener mDelegateLongClickListener; 1162 1163 /** 1164 * Ignore long clicks on linkified texts for a given text view. 1165 * @param textView the TextView to ignore long clicks on 1166 * @param longClickListener a delegate OnLongClickListener to be called when the view is 1167 * long clicked. 1168 */ 1169 public static void ignoreLinkLongClick(final TextView textView, 1170 @Nullable final OnLongClickListener longClickListener) { 1171 final IgnoreLinkLongClickHelper helper = 1172 new IgnoreLinkLongClickHelper(longClickListener); 1173 textView.setOnLongClickListener(helper); 1174 textView.setOnTouchListener(helper); 1175 } 1176 1177 private IgnoreLinkLongClickHelper(@Nullable final OnLongClickListener longClickListener) { 1178 mDelegateLongClickListener = longClickListener; 1179 } 1180 1181 @Override 1182 public boolean onLongClick(final View v) { 1183 // Record that this click is a long click. 1184 mIsLongClick = true; 1185 if (mDelegateLongClickListener != null) { 1186 return mDelegateLongClickListener.onLongClick(v); 1187 } 1188 return false; 1189 } 1190 1191 @Override 1192 public boolean onTouch(final View v, final MotionEvent event) { 1193 if (event.getActionMasked() == MotionEvent.ACTION_UP && mIsLongClick) { 1194 // This touch event is a long click, preemptively handle this touch event so that 1195 // the link span won't get a onClicked() callback. 1196 mIsLongClick = false; 1197 return true; 1198 } 1199 1200 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { 1201 mIsLongClick = false; 1202 } 1203 return false; 1204 } 1205 } 1206 } 1207