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