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