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 androidx.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 { onAttachmentClick(ConversationMessageView view, MessagePartData attachment, Rect imageBounds, boolean longPress)85 boolean onAttachmentClick(ConversationMessageView view, MessagePartData attachment, 86 Rect imageBounds, boolean longPress); getSubscriptionEntryForSelfParticipant(String selfParticipantId, boolean excludeDefault)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 ConversationMessageView(final Context context, final AttributeSet attrs)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 onFinishInflate()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 onMeasure(final int widthMeasureSpec, final int heightMeasureSpec)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 onLayout(final boolean changed, final int left, final int top, final int right, final int bottom)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 */ bind(final Cursor cursor)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 */ bind(final Cursor cursor, final boolean oneOnOne, final String selectedMessageId)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 setHost(final ConversationMessageViewHost host)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 */ setImageViewDelayLoader(final AsyncImageViewDelayLoader delayLoader)267 public void setImageViewDelayLoader(final AsyncImageViewDelayLoader delayLoader) { 268 Assert.notNull(mMessageImageView); 269 mMessageImageView.setDelayLoader(delayLoader); 270 mMultiAttachmentView.setImageViewDelayLoader(delayLoader); 271 } 272 getData()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 */ shouldShowSimplifiedVisualStyle()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 */ shouldShowMessageBubbleArrow()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 */ shouldShowMessageTextBubble()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 updateViewContent()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_OUTGOING_DELIVERED: 381 case MessageData.BUGLE_STATUS_INCOMING_COMPLETE: 382 default: 383 if (!mData.getCanClusterWithNextMessage()) { 384 statusText = mData.getFormattedReceivedTimeStamp(); 385 } 386 break; 387 } 388 389 final boolean titleVisible = (titleResId >= 0); 390 if (titleVisible) { 391 final String titleText = getResources().getString(titleResId); 392 mTitleTextView.setText(titleText); 393 394 final String mmsInfoText = getResources().getString( 395 R.string.mms_info, 396 Formatter.formatFileSize(getContext(), mData.getSmsMessageSize()), 397 DateUtils.formatDateTime( 398 getContext(), 399 mData.getMmsExpiry(), 400 DateUtils.FORMAT_SHOW_DATE | 401 DateUtils.FORMAT_SHOW_TIME | 402 DateUtils.FORMAT_NUMERIC_DATE | 403 DateUtils.FORMAT_NO_YEAR)); 404 mMmsInfoTextView.setText(mmsInfoText); 405 mMessageTitleLayout.setVisibility(View.VISIBLE); 406 } else { 407 mMessageTitleLayout.setVisibility(View.GONE); 408 } 409 410 final String subjectText = MmsUtils.cleanseMmsSubject(getResources(), 411 mData.getMmsSubject()); 412 final boolean subjectVisible = !TextUtils.isEmpty(subjectText); 413 414 final boolean senderNameVisible = !mOneOnOne && !mData.getCanClusterWithNextMessage() 415 && mData.getIsIncoming(); 416 if (senderNameVisible) { 417 mSenderNameTextView.setText(mData.getSenderDisplayName()); 418 mSenderNameTextView.setVisibility(View.VISIBLE); 419 } else { 420 mSenderNameTextView.setVisibility(View.GONE); 421 } 422 423 if (statusResId >= 0) { 424 statusText = getResources().getString(statusResId); 425 } 426 427 // We set the text even if the view will be GONE for accessibility 428 mStatusTextView.setText(statusText); 429 final boolean statusVisible = !TextUtils.isEmpty(statusText); 430 if (statusVisible) { 431 mStatusTextView.setVisibility(View.VISIBLE); 432 } else { 433 mStatusTextView.setVisibility(View.GONE); 434 } 435 436 final boolean deliveredBadgeVisible = 437 mData.getStatus() == MessageData.BUGLE_STATUS_OUTGOING_DELIVERED; 438 mDeliveredBadge.setVisibility(deliveredBadgeVisible ? View.VISIBLE : View.GONE); 439 440 // Update the sim indicator. 441 final boolean showSimIconAsIncoming = mData.getIsIncoming() && 442 (!mData.hasAttachments() || shouldShowMessageTextBubble()); 443 final SubscriptionListEntry subscriptionEntry = 444 mHost.getSubscriptionEntryForSelfParticipant(mData.getSelfParticipantId(), 445 true /* excludeDefault */); 446 final boolean simNameVisible = subscriptionEntry != null && 447 !TextUtils.isEmpty(subscriptionEntry.displayName) && 448 !mData.getCanClusterWithNextMessage(); 449 if (simNameVisible) { 450 final String simNameText = mData.getIsIncoming() ? getResources().getString( 451 R.string.incoming_sim_name_text, subscriptionEntry.displayName) : 452 subscriptionEntry.displayName; 453 mSimNameView.setText(simNameText); 454 mSimNameView.setTextColor(showSimIconAsIncoming ? getResources().getColor( 455 R.color.timestamp_text_incoming) : subscriptionEntry.displayColor); 456 mSimNameView.setVisibility(VISIBLE); 457 } else { 458 mSimNameView.setText(null); 459 mSimNameView.setVisibility(GONE); 460 } 461 462 final boolean metadataVisible = senderNameVisible || statusVisible 463 || deliveredBadgeVisible || simNameVisible; 464 mMessageMetadataView.setVisibility(metadataVisible ? View.VISIBLE : View.GONE); 465 466 final boolean messageTextAndOrInfoVisible = titleVisible || subjectVisible 467 || mData.hasText() || metadataVisible; 468 mMessageTextAndInfoView.setVisibility( 469 messageTextAndOrInfoVisible ? View.VISIBLE : View.GONE); 470 471 if (shouldShowSimplifiedVisualStyle()) { 472 mContactIconView.setVisibility(View.GONE); 473 mContactIconView.setImageResourceUri(null); 474 } else { 475 mContactIconView.setVisibility(View.VISIBLE); 476 final Uri avatarUri = AvatarUriUtil.createAvatarUri( 477 mData.getSenderProfilePhotoUri(), 478 mData.getSenderFullName(), 479 mData.getSenderNormalizedDestination(), 480 mData.getSenderContactLookupKey()); 481 mContactIconView.setImageResourceUri(avatarUri, mData.getSenderContactId(), 482 mData.getSenderContactLookupKey(), mData.getSenderNormalizedDestination()); 483 } 484 } 485 updateMessageContent()486 private void updateMessageContent() { 487 // We must update the text before the attachments since we search the text to see if we 488 // should make a preview youtube image in the attachments 489 updateMessageText(); 490 updateMessageAttachments(); 491 updateMessageSubject(); 492 mMessageBubble.bind(mData); 493 } 494 updateMessageAttachments()495 private void updateMessageAttachments() { 496 // Bind video, audio, and VCard attachments. If there are multiple, they stack vertically. 497 bindAttachmentsOfSameType(sVideoFilter, 498 R.layout.message_video_attachment, mVideoViewBinder, VideoThumbnailView.class); 499 bindAttachmentsOfSameType(sAudioFilter, 500 R.layout.message_audio_attachment, mAudioViewBinder, AudioAttachmentView.class); 501 bindAttachmentsOfSameType(sVCardFilter, 502 R.layout.message_vcard_attachment, mVCardViewBinder, PersonItemView.class); 503 504 // Bind image attachments. If there are multiple, they are shown in a collage view. 505 final List<MessagePartData> imageParts = mData.getAttachments(sImageFilter); 506 if (imageParts.size() > 1) { 507 Collections.sort(imageParts, sImageComparator); 508 mMultiAttachmentView.bindAttachments(imageParts, null, imageParts.size()); 509 mMultiAttachmentView.setVisibility(View.VISIBLE); 510 } else { 511 mMultiAttachmentView.setVisibility(View.GONE); 512 } 513 514 // In the case that we have no image attachments and exactly one youtube link in a message 515 // then we will show a preview. 516 String youtubeThumbnailUrl = null; 517 String originalYoutubeLink = null; 518 if (mMessageTextHasLinks && imageParts.size() == 0) { 519 CharSequence messageTextWithSpans = mMessageTextView.getText(); 520 final URLSpan[] spans = ((Spanned) messageTextWithSpans).getSpans(0, 521 messageTextWithSpans.length(), URLSpan.class); 522 for (URLSpan span : spans) { 523 String url = span.getURL(); 524 String youtubeLinkForUrl = YouTubeUtil.getYoutubePreviewImageLink(url); 525 if (!TextUtils.isEmpty(youtubeLinkForUrl)) { 526 if (TextUtils.isEmpty(youtubeThumbnailUrl)) { 527 // Save the youtube link if we don't already have one 528 youtubeThumbnailUrl = youtubeLinkForUrl; 529 originalYoutubeLink = url; 530 } else { 531 // We already have a youtube link. This means we have two youtube links so 532 // we shall show none. 533 youtubeThumbnailUrl = null; 534 originalYoutubeLink = null; 535 break; 536 } 537 } 538 } 539 } 540 // We need to keep track if we have a youtube link in the message so that we will not show 541 // the arrow 542 mMessageHasYouTubeLink = !TextUtils.isEmpty(youtubeThumbnailUrl); 543 544 // We will show the message image view if there is one attachment or one youtube link 545 if (imageParts.size() == 1 || mMessageHasYouTubeLink) { 546 // Get the display metrics for a hint for how large to pull the image data into 547 final WindowManager windowManager = (WindowManager) getContext(). 548 getSystemService(Context.WINDOW_SERVICE); 549 final DisplayMetrics displayMetrics = new DisplayMetrics(); 550 windowManager.getDefaultDisplay().getMetrics(displayMetrics); 551 552 final int iconSize = getResources() 553 .getDimensionPixelSize(R.dimen.conversation_message_contact_icon_size); 554 final int desiredWidth = displayMetrics.widthPixels - iconSize - iconSize; 555 556 if (imageParts.size() == 1) { 557 final MessagePartData imagePart = imageParts.get(0); 558 // If the image is big, we want to scale it down to save memory since we're going to 559 // scale it down to fit into the bubble width. We don't constrain the height. 560 final ImageRequestDescriptor imageRequest = 561 new MessagePartImageRequestDescriptor(imagePart, 562 desiredWidth, 563 MessagePartData.UNSPECIFIED_SIZE, 564 false); 565 adjustImageViewBounds(imagePart); 566 mMessageImageView.setImageResourceId(imageRequest); 567 mMessageImageView.setTag(imagePart); 568 } else { 569 // Youtube Thumbnail image 570 final ImageRequestDescriptor imageRequest = 571 new UriImageRequestDescriptor(Uri.parse(youtubeThumbnailUrl), desiredWidth, 572 MessagePartData.UNSPECIFIED_SIZE, true /* allowCompression */, 573 true /* isStatic */, false /* cropToCircle */, 574 ImageUtils.DEFAULT_CIRCLE_BACKGROUND_COLOR /* circleBackgroundColor */, 575 ImageUtils.DEFAULT_CIRCLE_STROKE_COLOR /* circleStrokeColor */); 576 mMessageImageView.setImageResourceId(imageRequest); 577 mMessageImageView.setTag(originalYoutubeLink); 578 } 579 mMessageImageView.setVisibility(View.VISIBLE); 580 } else { 581 mMessageImageView.setImageResourceId(null); 582 mMessageImageView.setVisibility(View.GONE); 583 } 584 585 // Show the message attachments container if any of its children are visible 586 boolean attachmentsVisible = false; 587 for (int i = 0, size = mMessageAttachmentsView.getChildCount(); i < size; i++) { 588 final View attachmentView = mMessageAttachmentsView.getChildAt(i); 589 if (attachmentView.getVisibility() == View.VISIBLE) { 590 attachmentsVisible = true; 591 break; 592 } 593 } 594 mMessageAttachmentsView.setVisibility(attachmentsVisible ? View.VISIBLE : View.GONE); 595 } 596 bindAttachmentsOfSameType(final Predicate<MessagePartData> attachmentTypeFilter, final int attachmentViewLayoutRes, final AttachmentViewBinder viewBinder, final Class<?> attachmentViewClass)597 private void bindAttachmentsOfSameType(final Predicate<MessagePartData> attachmentTypeFilter, 598 final int attachmentViewLayoutRes, final AttachmentViewBinder viewBinder, 599 final Class<?> attachmentViewClass) { 600 final LayoutInflater layoutInflater = LayoutInflater.from(getContext()); 601 602 // Iterate through all attachments of a particular type (video, audio, etc). 603 // Find the first attachment index that matches the given type if possible. 604 int attachmentViewIndex = -1; 605 View existingAttachmentView; 606 do { 607 existingAttachmentView = mMessageAttachmentsView.getChildAt(++attachmentViewIndex); 608 } while (existingAttachmentView != null && 609 !(attachmentViewClass.isInstance(existingAttachmentView))); 610 611 for (final MessagePartData attachment : mData.getAttachments(attachmentTypeFilter)) { 612 View attachmentView = mMessageAttachmentsView.getChildAt(attachmentViewIndex); 613 if (!attachmentViewClass.isInstance(attachmentView)) { 614 attachmentView = layoutInflater.inflate(attachmentViewLayoutRes, 615 mMessageAttachmentsView, false /* attachToRoot */); 616 attachmentView.setOnClickListener(this); 617 attachmentView.setOnLongClickListener(this); 618 mMessageAttachmentsView.addView(attachmentView, attachmentViewIndex); 619 } 620 viewBinder.bindView(attachmentView, attachment); 621 attachmentView.setTag(attachment); 622 attachmentView.setVisibility(View.VISIBLE); 623 attachmentViewIndex++; 624 } 625 // If there are unused views left over, unbind or remove them. 626 while (attachmentViewIndex < mMessageAttachmentsView.getChildCount()) { 627 final View attachmentView = mMessageAttachmentsView.getChildAt(attachmentViewIndex); 628 if (attachmentViewClass.isInstance(attachmentView)) { 629 mMessageAttachmentsView.removeViewAt(attachmentViewIndex); 630 } else { 631 // No more views of this type; we're done. 632 break; 633 } 634 } 635 } 636 updateMessageSubject()637 private void updateMessageSubject() { 638 final String subjectText = MmsUtils.cleanseMmsSubject(getResources(), 639 mData.getMmsSubject()); 640 final boolean subjectVisible = !TextUtils.isEmpty(subjectText); 641 642 if (subjectVisible) { 643 mSubjectText.setText(subjectText); 644 mSubjectView.setVisibility(View.VISIBLE); 645 } else { 646 mSubjectView.setVisibility(View.GONE); 647 } 648 } 649 updateMessageText()650 private void updateMessageText() { 651 final String text = mData.getText(); 652 if (!TextUtils.isEmpty(text)) { 653 mMessageTextView.setText(text); 654 // Linkify phone numbers, web urls, emails, and map addresses to allow users to 655 // click on them and take the default intent. 656 mMessageTextHasLinks = Linkify.addLinks(mMessageTextView, Linkify.ALL); 657 mMessageTextView.setVisibility(View.VISIBLE); 658 } else { 659 mMessageTextView.setVisibility(View.GONE); 660 mMessageTextHasLinks = false; 661 } 662 } 663 updateViewAppearance()664 private void updateViewAppearance() { 665 final Resources res = getResources(); 666 final ConversationDrawables drawableProvider = ConversationDrawables.get(); 667 final boolean incoming = mData.getIsIncoming(); 668 final boolean outgoing = !incoming; 669 final boolean showArrow = shouldShowMessageBubbleArrow(); 670 671 final int messageTopPaddingClustered = 672 res.getDimensionPixelSize(R.dimen.message_padding_same_author); 673 final int messageTopPaddingDefault = 674 res.getDimensionPixelSize(R.dimen.message_padding_default); 675 final int arrowWidth = res.getDimensionPixelOffset(R.dimen.message_bubble_arrow_width); 676 final int messageTextMinHeightDefault = res.getDimensionPixelSize( 677 R.dimen.conversation_message_contact_icon_size); 678 final int messageTextLeftRightPadding = res.getDimensionPixelOffset( 679 R.dimen.message_text_left_right_padding); 680 final int textTopPaddingDefault = res.getDimensionPixelOffset( 681 R.dimen.message_text_top_padding); 682 final int textBottomPaddingDefault = res.getDimensionPixelOffset( 683 R.dimen.message_text_bottom_padding); 684 685 // These values depend on whether the message has text, attachments, or both. 686 // We intentionally don't set defaults, so the compiler will tell us if we forget 687 // to set one of them, or if we set one more than once. 688 final int contentLeftPadding, contentRightPadding; 689 final Drawable textBackground; 690 final int textMinHeight; 691 final int textTopMargin; 692 final int textTopPadding, textBottomPadding; 693 final int textLeftPadding, textRightPadding; 694 695 if (mData.hasAttachments()) { 696 if (shouldShowMessageTextBubble()) { 697 // Text and attachment(s) 698 contentLeftPadding = incoming ? arrowWidth : 0; 699 contentRightPadding = outgoing ? arrowWidth : 0; 700 textBackground = drawableProvider.getBubbleDrawable( 701 isSelected(), 702 incoming, 703 false /* needArrow */, 704 mData.hasIncomingErrorStatus()); 705 textMinHeight = messageTextMinHeightDefault; 706 textTopMargin = messageTopPaddingClustered; 707 textTopPadding = textTopPaddingDefault; 708 textBottomPadding = textBottomPaddingDefault; 709 textLeftPadding = messageTextLeftRightPadding; 710 textRightPadding = messageTextLeftRightPadding; 711 } else { 712 // Attachment(s) only 713 contentLeftPadding = incoming ? arrowWidth : 0; 714 contentRightPadding = outgoing ? arrowWidth : 0; 715 textBackground = null; 716 textMinHeight = 0; 717 textTopMargin = 0; 718 textTopPadding = 0; 719 textBottomPadding = 0; 720 textLeftPadding = 0; 721 textRightPadding = 0; 722 } 723 } else { 724 // Text only 725 contentLeftPadding = (!showArrow && incoming) ? arrowWidth : 0; 726 contentRightPadding = (!showArrow && outgoing) ? arrowWidth : 0; 727 textBackground = drawableProvider.getBubbleDrawable( 728 isSelected(), 729 incoming, 730 shouldShowMessageBubbleArrow(), 731 mData.hasIncomingErrorStatus()); 732 textMinHeight = messageTextMinHeightDefault; 733 textTopMargin = 0; 734 textTopPadding = textTopPaddingDefault; 735 textBottomPadding = textBottomPaddingDefault; 736 if (showArrow && incoming) { 737 textLeftPadding = messageTextLeftRightPadding + arrowWidth; 738 } else { 739 textLeftPadding = messageTextLeftRightPadding; 740 } 741 if (showArrow && outgoing) { 742 textRightPadding = messageTextLeftRightPadding + arrowWidth; 743 } else { 744 textRightPadding = messageTextLeftRightPadding; 745 } 746 } 747 748 // These values do not depend on whether the message includes attachments 749 final int gravity = incoming ? (Gravity.START | Gravity.CENTER_VERTICAL) : 750 (Gravity.END | Gravity.CENTER_VERTICAL); 751 final int messageTopPadding = shouldShowSimplifiedVisualStyle() ? 752 messageTopPaddingClustered : messageTopPaddingDefault; 753 final int metadataTopPadding = res.getDimensionPixelOffset( 754 R.dimen.message_metadata_top_padding); 755 756 // Update the message text/info views 757 ImageUtils.setBackgroundDrawableOnView(mMessageTextAndInfoView, textBackground); 758 mMessageTextAndInfoView.setMinimumHeight(textMinHeight); 759 final LinearLayout.LayoutParams textAndInfoLayoutParams = 760 (LinearLayout.LayoutParams) mMessageTextAndInfoView.getLayoutParams(); 761 textAndInfoLayoutParams.topMargin = textTopMargin; 762 763 if (UiUtils.isRtlMode()) { 764 // Need to switch right and left padding in RtL mode 765 mMessageTextAndInfoView.setPadding(textRightPadding, textTopPadding, textLeftPadding, 766 textBottomPadding); 767 mMessageBubble.setPadding(contentRightPadding, 0, contentLeftPadding, 0); 768 } else { 769 mMessageTextAndInfoView.setPadding(textLeftPadding, textTopPadding, textRightPadding, 770 textBottomPadding); 771 mMessageBubble.setPadding(contentLeftPadding, 0, contentRightPadding, 0); 772 } 773 774 // Update the message row and message bubble views 775 setPadding(getPaddingLeft(), messageTopPadding, getPaddingRight(), 0); 776 mMessageBubble.setGravity(gravity); 777 updateMessageAttachmentsAppearance(gravity); 778 779 mMessageMetadataView.setPadding(0, metadataTopPadding, 0, 0); 780 781 updateTextAppearance(); 782 783 requestLayout(); 784 } 785 updateContentDescription()786 private void updateContentDescription() { 787 StringBuilder description = new StringBuilder(); 788 789 Resources res = getResources(); 790 String separator = res.getString(R.string.enumeration_comma); 791 792 // Sender information 793 boolean hasPlainTextMessage = !(TextUtils.isEmpty(mData.getText()) || 794 mMessageTextHasLinks); 795 if (mData.getIsIncoming()) { 796 int senderResId = hasPlainTextMessage 797 ? R.string.incoming_text_sender_content_description 798 : R.string.incoming_sender_content_description; 799 description.append(res.getString(senderResId, mData.getSenderDisplayName())); 800 } else { 801 int senderResId = hasPlainTextMessage 802 ? R.string.outgoing_text_sender_content_description 803 : R.string.outgoing_sender_content_description; 804 description.append(res.getString(senderResId)); 805 } 806 807 if (mSubjectView.getVisibility() == View.VISIBLE) { 808 description.append(separator); 809 description.append(mSubjectText.getText()); 810 } 811 812 if (mMessageTextView.getVisibility() == View.VISIBLE) { 813 // If the message has hyperlinks, we will let the user navigate to the text message so 814 // that the hyperlink can be clicked. Otherwise, the text message does not need to 815 // be reachable. 816 if (mMessageTextHasLinks) { 817 mMessageTextView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); 818 } else { 819 mMessageTextView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); 820 description.append(separator); 821 description.append(mMessageTextView.getText()); 822 } 823 } 824 825 if (mMessageTitleLayout.getVisibility() == View.VISIBLE) { 826 description.append(separator); 827 description.append(mTitleTextView.getText()); 828 829 description.append(separator); 830 description.append(mMmsInfoTextView.getText()); 831 } 832 833 if (mStatusTextView.getVisibility() == View.VISIBLE) { 834 description.append(separator); 835 description.append(mStatusTextView.getText()); 836 } 837 838 if (mSimNameView.getVisibility() == View.VISIBLE) { 839 description.append(separator); 840 description.append(mSimNameView.getText()); 841 } 842 843 if (mDeliveredBadge.getVisibility() == View.VISIBLE) { 844 description.append(separator); 845 description.append(res.getString(R.string.delivered_status_content_description)); 846 } 847 848 setContentDescription(description); 849 } 850 updateMessageAttachmentsAppearance(final int gravity)851 private void updateMessageAttachmentsAppearance(final int gravity) { 852 mMessageAttachmentsView.setGravity(gravity); 853 854 // Tint image/video attachments when selected 855 final int selectedImageTint = getResources().getColor(R.color.message_image_selected_tint); 856 if (mMessageImageView.getVisibility() == View.VISIBLE) { 857 if (isSelected()) { 858 mMessageImageView.setColorFilter(selectedImageTint); 859 } else { 860 mMessageImageView.clearColorFilter(); 861 } 862 } 863 if (mMultiAttachmentView.getVisibility() == View.VISIBLE) { 864 if (isSelected()) { 865 mMultiAttachmentView.setColorFilter(selectedImageTint); 866 } else { 867 mMultiAttachmentView.clearColorFilter(); 868 } 869 } 870 for (int i = 0, size = mMessageAttachmentsView.getChildCount(); i < size; i++) { 871 final View attachmentView = mMessageAttachmentsView.getChildAt(i); 872 if (attachmentView instanceof VideoThumbnailView 873 && attachmentView.getVisibility() == View.VISIBLE) { 874 final VideoThumbnailView videoView = (VideoThumbnailView) attachmentView; 875 if (isSelected()) { 876 videoView.setColorFilter(selectedImageTint); 877 } else { 878 videoView.clearColorFilter(); 879 } 880 } 881 } 882 883 // If there are multiple attachment bubbles in a single message, add some separation. 884 final int multipleAttachmentPadding = 885 getResources().getDimensionPixelSize(R.dimen.message_padding_same_author); 886 887 boolean previousVisibleView = false; 888 for (int i = 0, size = mMessageAttachmentsView.getChildCount(); i < size; i++) { 889 final View attachmentView = mMessageAttachmentsView.getChildAt(i); 890 if (attachmentView.getVisibility() == View.VISIBLE) { 891 final int margin = previousVisibleView ? multipleAttachmentPadding : 0; 892 ((LinearLayout.LayoutParams) attachmentView.getLayoutParams()).topMargin = margin; 893 // updateViewAppearance calls requestLayout() at the end, so we don't need to here 894 previousVisibleView = true; 895 } 896 } 897 } 898 updateTextAppearance()899 private void updateTextAppearance() { 900 int messageColorResId; 901 int statusColorResId = -1; 902 int infoColorResId = -1; 903 int timestampColorResId; 904 int subjectLabelColorResId; 905 if (isSelected()) { 906 messageColorResId = R.color.message_text_color_incoming; 907 statusColorResId = R.color.message_action_status_text; 908 infoColorResId = R.color.message_action_info_text; 909 if (shouldShowMessageTextBubble()) { 910 timestampColorResId = R.color.message_action_timestamp_text; 911 subjectLabelColorResId = R.color.message_action_timestamp_text; 912 } else { 913 // If there's no text, the timestamp will be shown below the attachments, 914 // against the conversation view background. 915 timestampColorResId = R.color.timestamp_text_outgoing; 916 subjectLabelColorResId = R.color.timestamp_text_outgoing; 917 } 918 } else { 919 messageColorResId = (mData.getIsIncoming() ? 920 R.color.message_text_color_incoming : R.color.message_text_color_outgoing); 921 statusColorResId = messageColorResId; 922 infoColorResId = R.color.timestamp_text_incoming; 923 switch(mData.getStatus()) { 924 925 case MessageData.BUGLE_STATUS_OUTGOING_FAILED: 926 case MessageData.BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER: 927 timestampColorResId = R.color.message_failed_timestamp_text; 928 subjectLabelColorResId = R.color.timestamp_text_outgoing; 929 break; 930 931 case MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND: 932 case MessageData.BUGLE_STATUS_OUTGOING_SENDING: 933 case MessageData.BUGLE_STATUS_OUTGOING_RESENDING: 934 case MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY: 935 case MessageData.BUGLE_STATUS_OUTGOING_COMPLETE: 936 case MessageData.BUGLE_STATUS_OUTGOING_DELIVERED: 937 timestampColorResId = R.color.timestamp_text_outgoing; 938 subjectLabelColorResId = R.color.timestamp_text_outgoing; 939 break; 940 941 case MessageData.BUGLE_STATUS_INCOMING_EXPIRED_OR_NOT_AVAILABLE: 942 case MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED: 943 messageColorResId = R.color.message_text_color_incoming_download_failed; 944 timestampColorResId = R.color.message_download_failed_timestamp_text; 945 subjectLabelColorResId = R.color.message_text_color_incoming_download_failed; 946 statusColorResId = R.color.message_download_failed_status_text; 947 infoColorResId = R.color.message_info_text_incoming_download_failed; 948 break; 949 950 case MessageData.BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING: 951 case MessageData.BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING: 952 case MessageData.BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD: 953 case MessageData.BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD: 954 case MessageData.BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD: 955 timestampColorResId = R.color.message_text_color_incoming; 956 subjectLabelColorResId = R.color.message_text_color_incoming; 957 infoColorResId = R.color.timestamp_text_incoming; 958 break; 959 960 case MessageData.BUGLE_STATUS_INCOMING_COMPLETE: 961 default: 962 timestampColorResId = R.color.timestamp_text_incoming; 963 subjectLabelColorResId = R.color.timestamp_text_incoming; 964 infoColorResId = -1; // Not used 965 break; 966 } 967 } 968 final int messageColor = getResources().getColor(messageColorResId); 969 mMessageTextView.setTextColor(messageColor); 970 mMessageTextView.setLinkTextColor(messageColor); 971 mSubjectText.setTextColor(messageColor); 972 if (statusColorResId >= 0) { 973 mTitleTextView.setTextColor(getResources().getColor(statusColorResId)); 974 } 975 if (infoColorResId >= 0) { 976 mMmsInfoTextView.setTextColor(getResources().getColor(infoColorResId)); 977 } 978 if (timestampColorResId == R.color.timestamp_text_incoming && 979 mData.hasAttachments() && !shouldShowMessageTextBubble()) { 980 timestampColorResId = R.color.timestamp_text_outgoing; 981 } 982 mStatusTextView.setTextColor(getResources().getColor(timestampColorResId)); 983 984 mSubjectLabel.setTextColor(getResources().getColor(subjectLabelColorResId)); 985 mSenderNameTextView.setTextColor(getResources().getColor(timestampColorResId)); 986 } 987 988 /** 989 * If we don't know the size of the image, we want to show it in a fixed-sized frame to 990 * avoid janks when the image is loaded and resized. Otherwise, we can set the imageview to 991 * take on normal layout params. 992 */ adjustImageViewBounds(final MessagePartData imageAttachment)993 private void adjustImageViewBounds(final MessagePartData imageAttachment) { 994 Assert.isTrue(ContentType.isImageType(imageAttachment.getContentType())); 995 final ViewGroup.LayoutParams layoutParams = mMessageImageView.getLayoutParams(); 996 if (imageAttachment.getWidth() == MessagePartData.UNSPECIFIED_SIZE || 997 imageAttachment.getHeight() == MessagePartData.UNSPECIFIED_SIZE) { 998 // We don't know the size of the image attachment, enable letterboxing on the image 999 // and show a fixed sized attachment. This should happen at most once per image since 1000 // after the image is loaded we then save the image dimensions to the db so that the 1001 // next time we can display the full size. 1002 layoutParams.width = getResources() 1003 .getDimensionPixelSize(R.dimen.image_attachment_fallback_width); 1004 layoutParams.height = getResources() 1005 .getDimensionPixelSize(R.dimen.image_attachment_fallback_height); 1006 mMessageImageView.setScaleType(ScaleType.CENTER_CROP); 1007 } else { 1008 layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; 1009 layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; 1010 // ScaleType.CENTER_INSIDE and FIT_CENTER behave similarly for most images. However, 1011 // FIT_CENTER works better for small images as it enlarges the image such that the 1012 // minimum size ("android:minWidth" etc) is honored. 1013 mMessageImageView.setScaleType(ScaleType.FIT_CENTER); 1014 } 1015 } 1016 1017 @Override onClick(final View view)1018 public void onClick(final View view) { 1019 final Object tag = view.getTag(); 1020 if (tag instanceof MessagePartData) { 1021 final Rect bounds = UiUtils.getMeasuredBoundsOnScreen(view); 1022 onAttachmentClick((MessagePartData) tag, bounds, false /* longPress */); 1023 } else if (tag instanceof String) { 1024 // Currently the only object that would make a tag of a string is a youtube preview 1025 // image 1026 UIIntents.get().launchBrowserForUrl(getContext(), (String) tag); 1027 } 1028 } 1029 1030 @Override onLongClick(final View view)1031 public boolean onLongClick(final View view) { 1032 if (view == mMessageTextView) { 1033 // Preemptively handle the long click event on message text so it's not handled by 1034 // the link spans. 1035 return performLongClick(); 1036 } 1037 1038 final Object tag = view.getTag(); 1039 if (tag instanceof MessagePartData) { 1040 final Rect bounds = UiUtils.getMeasuredBoundsOnScreen(view); 1041 return onAttachmentClick((MessagePartData) tag, bounds, true /* longPress */); 1042 } 1043 1044 return false; 1045 } 1046 1047 @Override onAttachmentClick(final MessagePartData attachment, final Rect viewBoundsOnScreen, final boolean longPress)1048 public boolean onAttachmentClick(final MessagePartData attachment, 1049 final Rect viewBoundsOnScreen, final boolean longPress) { 1050 return mHost.onAttachmentClick(this, attachment, viewBoundsOnScreen, longPress); 1051 } 1052 getContactIconView()1053 public ContactIconView getContactIconView() { 1054 return mContactIconView; 1055 } 1056 1057 // Sort photos in MultiAttachLayout in the same order as the ConversationImagePartsView 1058 static final Comparator<MessagePartData> sImageComparator = new Comparator<MessagePartData>(){ 1059 @Override 1060 public int compare(final MessagePartData x, final MessagePartData y) { 1061 return x.getPartId().compareTo(y.getPartId()); 1062 } 1063 }; 1064 1065 static final Predicate<MessagePartData> sVideoFilter = new Predicate<MessagePartData>() { 1066 @Override 1067 public boolean apply(final MessagePartData part) { 1068 return part.isVideo(); 1069 } 1070 }; 1071 1072 static final Predicate<MessagePartData> sAudioFilter = new Predicate<MessagePartData>() { 1073 @Override 1074 public boolean apply(final MessagePartData part) { 1075 return part.isAudio(); 1076 } 1077 }; 1078 1079 static final Predicate<MessagePartData> sVCardFilter = new Predicate<MessagePartData>() { 1080 @Override 1081 public boolean apply(final MessagePartData part) { 1082 return part.isVCard(); 1083 } 1084 }; 1085 1086 static final Predicate<MessagePartData> sImageFilter = new Predicate<MessagePartData>() { 1087 @Override 1088 public boolean apply(final MessagePartData part) { 1089 return part.isImage(); 1090 } 1091 }; 1092 1093 interface AttachmentViewBinder { bindView(View view, MessagePartData attachment)1094 void bindView(View view, MessagePartData attachment); unbind(View view)1095 void unbind(View view); 1096 } 1097 1098 final AttachmentViewBinder mVideoViewBinder = new AttachmentViewBinder() { 1099 @Override 1100 public void bindView(final View view, final MessagePartData attachment) { 1101 ((VideoThumbnailView) view).setSource(attachment, mData.getIsIncoming()); 1102 } 1103 1104 @Override 1105 public void unbind(final View view) { 1106 ((VideoThumbnailView) view).setSource((Uri) null, mData.getIsIncoming()); 1107 } 1108 }; 1109 1110 final AttachmentViewBinder mAudioViewBinder = new AttachmentViewBinder() { 1111 @Override 1112 public void bindView(final View view, final MessagePartData attachment) { 1113 final AudioAttachmentView audioView = (AudioAttachmentView) view; 1114 audioView.bindMessagePartData(attachment, mData.getIsIncoming(), isSelected()); 1115 audioView.setBackground(ConversationDrawables.get().getBubbleDrawable( 1116 isSelected(), mData.getIsIncoming(), false /* needArrow */, 1117 mData.hasIncomingErrorStatus())); 1118 } 1119 1120 @Override 1121 public void unbind(final View view) { 1122 ((AudioAttachmentView) view).bindMessagePartData(null, mData.getIsIncoming(), false); 1123 } 1124 }; 1125 1126 final AttachmentViewBinder mVCardViewBinder = new AttachmentViewBinder() { 1127 @Override 1128 public void bindView(final View view, final MessagePartData attachment) { 1129 final PersonItemView personView = (PersonItemView) view; 1130 personView.bind(DataModel.get().createVCardContactItemData(getContext(), 1131 attachment)); 1132 personView.setBackground(ConversationDrawables.get().getBubbleDrawable( 1133 isSelected(), mData.getIsIncoming(), false /* needArrow */, 1134 mData.hasIncomingErrorStatus())); 1135 final int nameTextColorRes; 1136 final int detailsTextColorRes; 1137 if (isSelected()) { 1138 nameTextColorRes = R.color.message_text_color_incoming; 1139 detailsTextColorRes = R.color.message_text_color_incoming; 1140 } else { 1141 nameTextColorRes = mData.getIsIncoming() ? R.color.message_text_color_incoming 1142 : R.color.message_text_color_outgoing; 1143 detailsTextColorRes = mData.getIsIncoming() ? R.color.timestamp_text_incoming 1144 : R.color.timestamp_text_outgoing; 1145 } 1146 personView.setNameTextColor(getResources().getColor(nameTextColorRes)); 1147 personView.setDetailsTextColor(getResources().getColor(detailsTextColorRes)); 1148 } 1149 1150 @Override 1151 public void unbind(final View view) { 1152 ((PersonItemView) view).bind(null); 1153 } 1154 }; 1155 1156 /** 1157 * A helper class that allows us to handle long clicks on linkified message text view (i.e. to 1158 * select the message) so it's not handled by the link spans to launch apps for the links. 1159 */ 1160 private static class IgnoreLinkLongClickHelper implements OnLongClickListener, OnTouchListener { 1161 private boolean mIsLongClick; 1162 private final OnLongClickListener mDelegateLongClickListener; 1163 1164 /** 1165 * Ignore long clicks on linkified texts for a given text view. 1166 * @param textView the TextView to ignore long clicks on 1167 * @param longClickListener a delegate OnLongClickListener to be called when the view is 1168 * long clicked. 1169 */ ignoreLinkLongClick(final TextView textView, @Nullable final OnLongClickListener longClickListener)1170 public static void ignoreLinkLongClick(final TextView textView, 1171 @Nullable final OnLongClickListener longClickListener) { 1172 final IgnoreLinkLongClickHelper helper = 1173 new IgnoreLinkLongClickHelper(longClickListener); 1174 textView.setOnLongClickListener(helper); 1175 textView.setOnTouchListener(helper); 1176 } 1177 IgnoreLinkLongClickHelper(@ullable final OnLongClickListener longClickListener)1178 private IgnoreLinkLongClickHelper(@Nullable final OnLongClickListener longClickListener) { 1179 mDelegateLongClickListener = longClickListener; 1180 } 1181 1182 @Override onLongClick(final View v)1183 public boolean onLongClick(final View v) { 1184 // Record that this click is a long click. 1185 mIsLongClick = true; 1186 if (mDelegateLongClickListener != null) { 1187 return mDelegateLongClickListener.onLongClick(v); 1188 } 1189 return false; 1190 } 1191 1192 @Override onTouch(final View v, final MotionEvent event)1193 public boolean onTouch(final View v, final MotionEvent event) { 1194 if (event.getActionMasked() == MotionEvent.ACTION_UP && mIsLongClick) { 1195 // This touch event is a long click, preemptively handle this touch event so that 1196 // the link span won't get a onClicked() callback. 1197 mIsLongClick = false; 1198 return true; 1199 } 1200 1201 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { 1202 mIsLongClick = false; 1203 } 1204 return false; 1205 } 1206 } 1207 } 1208