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 */
16package com.android.messaging.ui.conversation;
17
18import android.content.Context;
19import android.content.res.Resources;
20import android.database.Cursor;
21import android.graphics.Rect;
22import android.graphics.drawable.Drawable;
23import android.net.Uri;
24import android.support.annotation.Nullable;
25import android.text.Spanned;
26import android.text.TextUtils;
27import android.text.format.DateUtils;
28import android.text.format.Formatter;
29import android.text.style.URLSpan;
30import android.text.util.Linkify;
31import android.util.AttributeSet;
32import android.util.DisplayMetrics;
33import android.view.Gravity;
34import android.view.LayoutInflater;
35import android.view.MotionEvent;
36import android.view.View;
37import android.view.ViewGroup;
38import android.view.WindowManager;
39import android.widget.FrameLayout;
40import android.widget.ImageView.ScaleType;
41import android.widget.LinearLayout;
42import android.widget.TextView;
43
44import com.android.messaging.R;
45import com.android.messaging.datamodel.DataModel;
46import com.android.messaging.datamodel.data.ConversationMessageData;
47import com.android.messaging.datamodel.data.MessageData;
48import com.android.messaging.datamodel.data.MessagePartData;
49import com.android.messaging.datamodel.data.SubscriptionListData.SubscriptionListEntry;
50import com.android.messaging.datamodel.media.ImageRequestDescriptor;
51import com.android.messaging.datamodel.media.MessagePartImageRequestDescriptor;
52import com.android.messaging.datamodel.media.UriImageRequestDescriptor;
53import com.android.messaging.sms.MmsUtils;
54import com.android.messaging.ui.AsyncImageView;
55import com.android.messaging.ui.AsyncImageView.AsyncImageViewDelayLoader;
56import com.android.messaging.ui.AudioAttachmentView;
57import com.android.messaging.ui.ContactIconView;
58import com.android.messaging.ui.ConversationDrawables;
59import com.android.messaging.ui.MultiAttachmentLayout;
60import com.android.messaging.ui.MultiAttachmentLayout.OnAttachmentClickListener;
61import com.android.messaging.ui.PersonItemView;
62import com.android.messaging.ui.UIIntents;
63import com.android.messaging.ui.VideoThumbnailView;
64import com.android.messaging.util.AccessibilityUtil;
65import com.android.messaging.util.Assert;
66import com.android.messaging.util.AvatarUriUtil;
67import com.android.messaging.util.ContentType;
68import com.android.messaging.util.ImageUtils;
69import com.android.messaging.util.OsUtil;
70import com.android.messaging.util.PhoneUtils;
71import com.android.messaging.util.UiUtils;
72import com.android.messaging.util.YouTubeUtil;
73import com.google.common.base.Predicate;
74
75import java.util.Collections;
76import java.util.Comparator;
77import java.util.List;
78
79/**
80 * The view for a single entry in a conversation.
81 */
82public 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