• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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_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