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