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