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 mMessageTextView.setTextIsSelectable(isSelected()); 712 } else { 713 // Attachment(s) only 714 contentLeftPadding = incoming ? arrowWidth : 0; 715 contentRightPadding = outgoing ? arrowWidth : 0; 716 textBackground = null; 717 textMinHeight = 0; 718 textTopMargin = 0; 719 textTopPadding = 0; 720 textBottomPadding = 0; 721 textLeftPadding = 0; 722 textRightPadding = 0; 723 } 724 } else { 725 // Text only 726 contentLeftPadding = (!showArrow && incoming) ? arrowWidth : 0; 727 contentRightPadding = (!showArrow && outgoing) ? arrowWidth : 0; 728 textBackground = drawableProvider.getBubbleDrawable( 729 isSelected(), 730 incoming, 731 shouldShowMessageBubbleArrow(), 732 mData.hasIncomingErrorStatus()); 733 textMinHeight = messageTextMinHeightDefault; 734 textTopMargin = 0; 735 textTopPadding = textTopPaddingDefault; 736 textBottomPadding = textBottomPaddingDefault; 737 mMessageTextView.setTextIsSelectable(isSelected()); 738 if (showArrow && incoming) { 739 textLeftPadding = messageTextLeftRightPadding + arrowWidth; 740 } else { 741 textLeftPadding = messageTextLeftRightPadding; 742 } 743 if (showArrow && outgoing) { 744 textRightPadding = messageTextLeftRightPadding + arrowWidth; 745 } else { 746 textRightPadding = messageTextLeftRightPadding; 747 } 748 } 749 750 // These values do not depend on whether the message includes attachments 751 final int gravity = incoming ? (Gravity.START | Gravity.CENTER_VERTICAL) : 752 (Gravity.END | Gravity.CENTER_VERTICAL); 753 final int messageTopPadding = shouldShowSimplifiedVisualStyle() ? 754 messageTopPaddingClustered : messageTopPaddingDefault; 755 final int metadataTopPadding = res.getDimensionPixelOffset( 756 R.dimen.message_metadata_top_padding); 757 758 // Update the message text/info views 759 ImageUtils.setBackgroundDrawableOnView(mMessageTextAndInfoView, textBackground); 760 mMessageTextAndInfoView.setMinimumHeight(textMinHeight); 761 final LinearLayout.LayoutParams textAndInfoLayoutParams = 762 (LinearLayout.LayoutParams) mMessageTextAndInfoView.getLayoutParams(); 763 textAndInfoLayoutParams.topMargin = textTopMargin; 764 765 if (UiUtils.isRtlMode()) { 766 // Need to switch right and left padding in RtL mode 767 mMessageTextAndInfoView.setPadding(textRightPadding, textTopPadding, textLeftPadding, 768 textBottomPadding); 769 mMessageBubble.setPadding(contentRightPadding, 0, contentLeftPadding, 0); 770 } else { 771 mMessageTextAndInfoView.setPadding(textLeftPadding, textTopPadding, textRightPadding, 772 textBottomPadding); 773 mMessageBubble.setPadding(contentLeftPadding, 0, contentRightPadding, 0); 774 } 775 776 // Update the message row and message bubble views 777 setPadding(getPaddingLeft(), messageTopPadding, getPaddingRight(), 0); 778 mMessageBubble.setGravity(gravity); 779 updateMessageAttachmentsAppearance(gravity); 780 781 mMessageMetadataView.setPadding(0, metadataTopPadding, 0, 0); 782 783 updateTextAppearance(); 784 785 requestLayout(); 786 } 787 updateContentDescription()788 private void updateContentDescription() { 789 StringBuilder description = new StringBuilder(); 790 791 Resources res = getResources(); 792 String separator = res.getString(R.string.enumeration_comma); 793 794 // Sender information 795 boolean hasPlainTextMessage = !(TextUtils.isEmpty(mData.getText()) || 796 mMessageTextHasLinks); 797 if (mData.getIsIncoming()) { 798 int senderResId = hasPlainTextMessage 799 ? R.string.incoming_text_sender_content_description 800 : R.string.incoming_sender_content_description; 801 description.append(res.getString(senderResId, mData.getSenderDisplayName())); 802 } else { 803 int senderResId = hasPlainTextMessage 804 ? R.string.outgoing_text_sender_content_description 805 : R.string.outgoing_sender_content_description; 806 description.append(res.getString(senderResId)); 807 } 808 809 if (mSubjectView.getVisibility() == View.VISIBLE) { 810 description.append(separator); 811 description.append(mSubjectText.getText()); 812 } 813 814 if (mMessageTextView.getVisibility() == View.VISIBLE) { 815 // If the message has hyperlinks, we will let the user navigate to the text message so 816 // that the hyperlink can be clicked. Otherwise, the text message does not need to 817 // be reachable. 818 if (mMessageTextHasLinks) { 819 mMessageTextView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); 820 } else { 821 mMessageTextView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); 822 description.append(separator); 823 description.append(mMessageTextView.getText()); 824 } 825 } 826 827 if (mMessageTitleLayout.getVisibility() == View.VISIBLE) { 828 description.append(separator); 829 description.append(mTitleTextView.getText()); 830 831 description.append(separator); 832 description.append(mMmsInfoTextView.getText()); 833 } 834 835 if (mStatusTextView.getVisibility() == View.VISIBLE) { 836 description.append(separator); 837 description.append(mStatusTextView.getText()); 838 } 839 840 if (mSimNameView.getVisibility() == View.VISIBLE) { 841 description.append(separator); 842 description.append(mSimNameView.getText()); 843 } 844 845 if (mDeliveredBadge.getVisibility() == View.VISIBLE) { 846 description.append(separator); 847 description.append(res.getString(R.string.delivered_status_content_description)); 848 } 849 850 setContentDescription(description); 851 } 852 updateMessageAttachmentsAppearance(final int gravity)853 private void updateMessageAttachmentsAppearance(final int gravity) { 854 mMessageAttachmentsView.setGravity(gravity); 855 856 // Tint image/video attachments when selected 857 final int selectedImageTint = getResources().getColor(R.color.message_image_selected_tint); 858 if (mMessageImageView.getVisibility() == View.VISIBLE) { 859 if (isSelected()) { 860 mMessageImageView.setColorFilter(selectedImageTint); 861 } else { 862 mMessageImageView.clearColorFilter(); 863 } 864 } 865 if (mMultiAttachmentView.getVisibility() == View.VISIBLE) { 866 if (isSelected()) { 867 mMultiAttachmentView.setColorFilter(selectedImageTint); 868 } else { 869 mMultiAttachmentView.clearColorFilter(); 870 } 871 } 872 for (int i = 0, size = mMessageAttachmentsView.getChildCount(); i < size; i++) { 873 final View attachmentView = mMessageAttachmentsView.getChildAt(i); 874 if (attachmentView instanceof VideoThumbnailView 875 && attachmentView.getVisibility() == View.VISIBLE) { 876 final VideoThumbnailView videoView = (VideoThumbnailView) attachmentView; 877 if (isSelected()) { 878 videoView.setColorFilter(selectedImageTint); 879 } else { 880 videoView.clearColorFilter(); 881 } 882 } 883 } 884 885 // If there are multiple attachment bubbles in a single message, add some separation. 886 final int multipleAttachmentPadding = 887 getResources().getDimensionPixelSize(R.dimen.message_padding_same_author); 888 889 boolean previousVisibleView = false; 890 for (int i = 0, size = mMessageAttachmentsView.getChildCount(); i < size; i++) { 891 final View attachmentView = mMessageAttachmentsView.getChildAt(i); 892 if (attachmentView.getVisibility() == View.VISIBLE) { 893 final int margin = previousVisibleView ? multipleAttachmentPadding : 0; 894 ((LinearLayout.LayoutParams) attachmentView.getLayoutParams()).topMargin = margin; 895 // updateViewAppearance calls requestLayout() at the end, so we don't need to here 896 previousVisibleView = true; 897 } 898 } 899 } 900 updateTextAppearance()901 private void updateTextAppearance() { 902 int messageColorResId; 903 int statusColorResId = -1; 904 int infoColorResId = -1; 905 int timestampColorResId; 906 int subjectLabelColorResId; 907 if (isSelected()) { 908 messageColorResId = R.color.message_text_color_incoming; 909 statusColorResId = R.color.message_action_status_text; 910 infoColorResId = R.color.message_action_info_text; 911 if (shouldShowMessageTextBubble()) { 912 timestampColorResId = R.color.message_action_timestamp_text; 913 subjectLabelColorResId = R.color.message_action_timestamp_text; 914 } else { 915 // If there's no text, the timestamp will be shown below the attachments, 916 // against the conversation view background. 917 timestampColorResId = R.color.timestamp_text_outgoing; 918 subjectLabelColorResId = R.color.timestamp_text_outgoing; 919 } 920 } else { 921 messageColorResId = (mData.getIsIncoming() ? 922 R.color.message_text_color_incoming : R.color.message_text_color_outgoing); 923 statusColorResId = messageColorResId; 924 infoColorResId = R.color.timestamp_text_incoming; 925 switch(mData.getStatus()) { 926 927 case MessageData.BUGLE_STATUS_OUTGOING_FAILED: 928 case MessageData.BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER: 929 timestampColorResId = R.color.message_failed_timestamp_text; 930 subjectLabelColorResId = R.color.timestamp_text_outgoing; 931 break; 932 933 case MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND: 934 case MessageData.BUGLE_STATUS_OUTGOING_SENDING: 935 case MessageData.BUGLE_STATUS_OUTGOING_RESENDING: 936 case MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY: 937 case MessageData.BUGLE_STATUS_OUTGOING_COMPLETE: 938 case MessageData.BUGLE_STATUS_OUTGOING_DELIVERED: 939 timestampColorResId = R.color.timestamp_text_outgoing; 940 subjectLabelColorResId = R.color.timestamp_text_outgoing; 941 break; 942 943 case MessageData.BUGLE_STATUS_INCOMING_EXPIRED_OR_NOT_AVAILABLE: 944 case MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED: 945 messageColorResId = R.color.message_text_color_incoming_download_failed; 946 timestampColorResId = R.color.message_download_failed_timestamp_text; 947 subjectLabelColorResId = R.color.message_text_color_incoming_download_failed; 948 statusColorResId = R.color.message_download_failed_status_text; 949 infoColorResId = R.color.message_info_text_incoming_download_failed; 950 break; 951 952 case MessageData.BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING: 953 case MessageData.BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING: 954 case MessageData.BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD: 955 case MessageData.BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD: 956 case MessageData.BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD: 957 timestampColorResId = R.color.message_text_color_incoming; 958 subjectLabelColorResId = R.color.message_text_color_incoming; 959 infoColorResId = R.color.timestamp_text_incoming; 960 break; 961 962 case MessageData.BUGLE_STATUS_INCOMING_COMPLETE: 963 default: 964 timestampColorResId = R.color.timestamp_text_incoming; 965 subjectLabelColorResId = R.color.timestamp_text_incoming; 966 infoColorResId = -1; // Not used 967 break; 968 } 969 } 970 final int messageColor = getResources().getColor(messageColorResId); 971 mMessageTextView.setTextColor(messageColor); 972 mMessageTextView.setLinkTextColor(messageColor); 973 mSubjectText.setTextColor(messageColor); 974 if (statusColorResId >= 0) { 975 mTitleTextView.setTextColor(getResources().getColor(statusColorResId)); 976 } 977 if (infoColorResId >= 0) { 978 mMmsInfoTextView.setTextColor(getResources().getColor(infoColorResId)); 979 } 980 if (timestampColorResId == R.color.timestamp_text_incoming && 981 mData.hasAttachments() && !shouldShowMessageTextBubble()) { 982 timestampColorResId = R.color.timestamp_text_outgoing; 983 } 984 mStatusTextView.setTextColor(getResources().getColor(timestampColorResId)); 985 986 mSubjectLabel.setTextColor(getResources().getColor(subjectLabelColorResId)); 987 mSenderNameTextView.setTextColor(getResources().getColor(timestampColorResId)); 988 } 989 990 /** 991 * If we don't know the size of the image, we want to show it in a fixed-sized frame to 992 * avoid janks when the image is loaded and resized. Otherwise, we can set the imageview to 993 * take on normal layout params. 994 */ adjustImageViewBounds(final MessagePartData imageAttachment)995 private void adjustImageViewBounds(final MessagePartData imageAttachment) { 996 Assert.isTrue(ContentType.isImageType(imageAttachment.getContentType())); 997 final ViewGroup.LayoutParams layoutParams = mMessageImageView.getLayoutParams(); 998 if (imageAttachment.getWidth() == MessagePartData.UNSPECIFIED_SIZE || 999 imageAttachment.getHeight() == MessagePartData.UNSPECIFIED_SIZE) { 1000 // We don't know the size of the image attachment, enable letterboxing on the image 1001 // and show a fixed sized attachment. This should happen at most once per image since 1002 // after the image is loaded we then save the image dimensions to the db so that the 1003 // next time we can display the full size. 1004 layoutParams.width = getResources() 1005 .getDimensionPixelSize(R.dimen.image_attachment_fallback_width); 1006 layoutParams.height = getResources() 1007 .getDimensionPixelSize(R.dimen.image_attachment_fallback_height); 1008 mMessageImageView.setScaleType(ScaleType.CENTER_CROP); 1009 } else { 1010 layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; 1011 layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; 1012 // ScaleType.CENTER_INSIDE and FIT_CENTER behave similarly for most images. However, 1013 // FIT_CENTER works better for small images as it enlarges the image such that the 1014 // minimum size ("android:minWidth" etc) is honored. 1015 mMessageImageView.setScaleType(ScaleType.FIT_CENTER); 1016 } 1017 } 1018 1019 @Override onClick(final View view)1020 public void onClick(final View view) { 1021 final Object tag = view.getTag(); 1022 if (tag instanceof MessagePartData) { 1023 final Rect bounds = UiUtils.getMeasuredBoundsOnScreen(view); 1024 onAttachmentClick((MessagePartData) tag, bounds, false /* longPress */); 1025 } else if (tag instanceof String) { 1026 // Currently the only object that would make a tag of a string is a youtube preview 1027 // image 1028 UIIntents.get().launchBrowserForUrl(getContext(), (String) tag); 1029 } 1030 } 1031 1032 @Override onLongClick(final View view)1033 public boolean onLongClick(final View view) { 1034 if (view == mMessageTextView) { 1035 // Avoid trying to reselect the message 1036 if (isSelected()) { 1037 return false; 1038 } 1039 1040 // Preemptively handle the long click event on message text so it's not handled by 1041 // the link spans. 1042 return performLongClick(); 1043 } 1044 1045 final Object tag = view.getTag(); 1046 if (tag instanceof MessagePartData) { 1047 final Rect bounds = UiUtils.getMeasuredBoundsOnScreen(view); 1048 return onAttachmentClick((MessagePartData) tag, bounds, true /* longPress */); 1049 } 1050 1051 return false; 1052 } 1053 1054 @Override onAttachmentClick(final MessagePartData attachment, final Rect viewBoundsOnScreen, final boolean longPress)1055 public boolean onAttachmentClick(final MessagePartData attachment, 1056 final Rect viewBoundsOnScreen, final boolean longPress) { 1057 return mHost.onAttachmentClick(this, attachment, viewBoundsOnScreen, longPress); 1058 } 1059 getContactIconView()1060 public ContactIconView getContactIconView() { 1061 return mContactIconView; 1062 } 1063 1064 // Sort photos in MultiAttachLayout in the same order as the ConversationImagePartsView 1065 static final Comparator<MessagePartData> sImageComparator = new Comparator<MessagePartData>(){ 1066 @Override 1067 public int compare(final MessagePartData x, final MessagePartData y) { 1068 return x.getPartId().compareTo(y.getPartId()); 1069 } 1070 }; 1071 1072 static final Predicate<MessagePartData> sVideoFilter = new Predicate<MessagePartData>() { 1073 @Override 1074 public boolean apply(final MessagePartData part) { 1075 return part.isVideo(); 1076 } 1077 }; 1078 1079 static final Predicate<MessagePartData> sAudioFilter = new Predicate<MessagePartData>() { 1080 @Override 1081 public boolean apply(final MessagePartData part) { 1082 return part.isAudio(); 1083 } 1084 }; 1085 1086 static final Predicate<MessagePartData> sVCardFilter = new Predicate<MessagePartData>() { 1087 @Override 1088 public boolean apply(final MessagePartData part) { 1089 return part.isVCard(); 1090 } 1091 }; 1092 1093 static final Predicate<MessagePartData> sImageFilter = new Predicate<MessagePartData>() { 1094 @Override 1095 public boolean apply(final MessagePartData part) { 1096 return part.isImage(); 1097 } 1098 }; 1099 1100 interface AttachmentViewBinder { bindView(View view, MessagePartData attachment)1101 void bindView(View view, MessagePartData attachment); unbind(View view)1102 void unbind(View view); 1103 } 1104 1105 final AttachmentViewBinder mVideoViewBinder = new AttachmentViewBinder() { 1106 @Override 1107 public void bindView(final View view, final MessagePartData attachment) { 1108 ((VideoThumbnailView) view).setSource(attachment, mData.getIsIncoming()); 1109 } 1110 1111 @Override 1112 public void unbind(final View view) { 1113 ((VideoThumbnailView) view).setSource((Uri) null, mData.getIsIncoming()); 1114 } 1115 }; 1116 1117 final AttachmentViewBinder mAudioViewBinder = new AttachmentViewBinder() { 1118 @Override 1119 public void bindView(final View view, final MessagePartData attachment) { 1120 final AudioAttachmentView audioView = (AudioAttachmentView) view; 1121 audioView.bindMessagePartData(attachment, mData.getIsIncoming(), isSelected()); 1122 audioView.setBackground(ConversationDrawables.get().getBubbleDrawable( 1123 isSelected(), mData.getIsIncoming(), false /* needArrow */, 1124 mData.hasIncomingErrorStatus())); 1125 } 1126 1127 @Override 1128 public void unbind(final View view) { 1129 ((AudioAttachmentView) view).bindMessagePartData(null, mData.getIsIncoming(), false); 1130 } 1131 }; 1132 1133 final AttachmentViewBinder mVCardViewBinder = new AttachmentViewBinder() { 1134 @Override 1135 public void bindView(final View view, final MessagePartData attachment) { 1136 final PersonItemView personView = (PersonItemView) view; 1137 personView.bind(DataModel.get().createVCardContactItemData(getContext(), 1138 attachment)); 1139 personView.setBackground(ConversationDrawables.get().getBubbleDrawable( 1140 isSelected(), mData.getIsIncoming(), false /* needArrow */, 1141 mData.hasIncomingErrorStatus())); 1142 final int nameTextColorRes; 1143 final int detailsTextColorRes; 1144 if (isSelected()) { 1145 nameTextColorRes = R.color.message_text_color_incoming; 1146 detailsTextColorRes = R.color.message_text_color_incoming; 1147 } else { 1148 nameTextColorRes = mData.getIsIncoming() ? R.color.message_text_color_incoming 1149 : R.color.message_text_color_outgoing; 1150 detailsTextColorRes = mData.getIsIncoming() ? R.color.timestamp_text_incoming 1151 : R.color.timestamp_text_outgoing; 1152 } 1153 personView.setNameTextColor(getResources().getColor(nameTextColorRes)); 1154 personView.setDetailsTextColor(getResources().getColor(detailsTextColorRes)); 1155 } 1156 1157 @Override 1158 public void unbind(final View view) { 1159 ((PersonItemView) view).bind(null); 1160 } 1161 }; 1162 1163 /** 1164 * A helper class that allows us to handle long clicks on linkified message text view (i.e. to 1165 * select the message) so it's not handled by the link spans to launch apps for the links. 1166 */ 1167 private static class IgnoreLinkLongClickHelper implements OnLongClickListener, OnTouchListener { 1168 private boolean mIsLongClick; 1169 private final OnLongClickListener mDelegateLongClickListener; 1170 1171 /** 1172 * Ignore long clicks on linkified texts for a given text view. 1173 * @param textView the TextView to ignore long clicks on 1174 * @param longClickListener a delegate OnLongClickListener to be called when the view is 1175 * long clicked. 1176 */ ignoreLinkLongClick(final TextView textView, @Nullable final OnLongClickListener longClickListener)1177 public static void ignoreLinkLongClick(final TextView textView, 1178 @Nullable final OnLongClickListener longClickListener) { 1179 final IgnoreLinkLongClickHelper helper = 1180 new IgnoreLinkLongClickHelper(longClickListener); 1181 textView.setOnLongClickListener(helper); 1182 textView.setOnTouchListener(helper); 1183 } 1184 IgnoreLinkLongClickHelper(@ullable final OnLongClickListener longClickListener)1185 private IgnoreLinkLongClickHelper(@Nullable final OnLongClickListener longClickListener) { 1186 mDelegateLongClickListener = longClickListener; 1187 } 1188 1189 @Override onLongClick(final View v)1190 public boolean onLongClick(final View v) { 1191 // Record that this click is a long click. 1192 mIsLongClick = true; 1193 if (mDelegateLongClickListener != null) { 1194 return mDelegateLongClickListener.onLongClick(v); 1195 } 1196 return false; 1197 } 1198 1199 @Override onTouch(final View v, final MotionEvent event)1200 public boolean onTouch(final View v, final MotionEvent event) { 1201 if (event.getActionMasked() == MotionEvent.ACTION_UP && mIsLongClick) { 1202 // This touch event is a long click, preemptively handle this touch event so that 1203 // the link span won't get a onClicked() callback. 1204 mIsLongClick = false; 1205 return false; 1206 } 1207 1208 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { 1209 mIsLongClick = false; 1210 } 1211 return false; 1212 } 1213 } 1214 } 1215