1168 lines
53 KiB
Java
1168 lines
53 KiB
Java
/*
|
|
* Copyright (C) 2015 The Android Open Source Project
|
|
* Copyright (C) 2024 The LineageOS Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
package com.android.messaging.ui.conversation;
|
|
|
|
import android.content.Context;
|
|
import android.content.res.Resources;
|
|
import android.database.Cursor;
|
|
import android.graphics.Rect;
|
|
import android.graphics.drawable.Drawable;
|
|
import android.net.Uri;
|
|
import androidx.annotation.Nullable;
|
|
import android.text.Spanned;
|
|
import android.text.TextUtils;
|
|
import android.text.format.DateUtils;
|
|
import android.text.format.Formatter;
|
|
import android.text.style.URLSpan;
|
|
import android.text.util.Linkify;
|
|
import android.util.AttributeSet;
|
|
import android.util.DisplayMetrics;
|
|
import android.view.Gravity;
|
|
import android.view.LayoutInflater;
|
|
import android.view.MotionEvent;
|
|
import android.view.View;
|
|
import android.view.ViewGroup;
|
|
import android.view.WindowManager;
|
|
import android.widget.FrameLayout;
|
|
import android.widget.ImageView.ScaleType;
|
|
import android.widget.LinearLayout;
|
|
import android.widget.TextView;
|
|
|
|
import com.android.messaging.R;
|
|
import com.android.messaging.datamodel.DataModel;
|
|
import com.android.messaging.datamodel.data.ConversationMessageData;
|
|
import com.android.messaging.datamodel.data.MessageData;
|
|
import com.android.messaging.datamodel.data.MessagePartData;
|
|
import com.android.messaging.datamodel.data.SubscriptionListData.SubscriptionListEntry;
|
|
import com.android.messaging.datamodel.media.ImageRequestDescriptor;
|
|
import com.android.messaging.datamodel.media.MessagePartImageRequestDescriptor;
|
|
import com.android.messaging.datamodel.media.UriImageRequestDescriptor;
|
|
import com.android.messaging.sms.MmsUtils;
|
|
import com.android.messaging.ui.AsyncImageView;
|
|
import com.android.messaging.ui.AsyncImageView.AsyncImageViewDelayLoader;
|
|
import com.android.messaging.ui.AudioAttachmentView;
|
|
import com.android.messaging.ui.ContactIconView;
|
|
import com.android.messaging.ui.ConversationDrawables;
|
|
import com.android.messaging.ui.MultiAttachmentLayout;
|
|
import com.android.messaging.ui.MultiAttachmentLayout.OnAttachmentClickListener;
|
|
import com.android.messaging.ui.PersonItemView;
|
|
import com.android.messaging.ui.UIIntents;
|
|
import com.android.messaging.ui.VideoThumbnailView;
|
|
import com.android.messaging.util.AccessibilityUtil;
|
|
import com.android.messaging.util.Assert;
|
|
import com.android.messaging.util.AvatarUriUtil;
|
|
import com.android.messaging.util.ContentType;
|
|
import com.android.messaging.util.ImageUtils;
|
|
import com.android.messaging.util.OsUtil;
|
|
import com.android.messaging.util.PhoneUtils;
|
|
import com.android.messaging.util.UiUtils;
|
|
import com.android.messaging.util.YouTubeUtil;
|
|
import com.google.common.base.Predicate;
|
|
|
|
import java.util.Collections;
|
|
import java.util.Comparator;
|
|
import java.util.List;
|
|
|
|
/**
|
|
* The view for a single entry in a conversation.
|
|
*/
|
|
public class ConversationMessageView extends FrameLayout implements View.OnClickListener,
|
|
View.OnLongClickListener, OnAttachmentClickListener {
|
|
public interface ConversationMessageViewHost {
|
|
boolean onAttachmentClick(ConversationMessageView view, MessagePartData attachment,
|
|
Rect imageBounds, boolean longPress);
|
|
SubscriptionListEntry getSubscriptionEntryForSelfParticipant(String selfParticipantId,
|
|
boolean excludeDefault);
|
|
}
|
|
|
|
private final ConversationMessageData mData;
|
|
|
|
private LinearLayout mMessageAttachmentsView;
|
|
private MultiAttachmentLayout mMultiAttachmentView;
|
|
private AsyncImageView mMessageImageView;
|
|
private TextView mMessageTextView;
|
|
private boolean mMessageTextHasLinks;
|
|
private boolean mMessageHasYouTubeLink;
|
|
private TextView mStatusTextView;
|
|
private TextView mTitleTextView;
|
|
private TextView mMmsInfoTextView;
|
|
private LinearLayout mMessageTitleLayout;
|
|
private TextView mSenderNameTextView;
|
|
private ContactIconView mContactIconView;
|
|
private ConversationMessageBubbleView mMessageBubble;
|
|
private View mSubjectView;
|
|
private TextView mSubjectLabel;
|
|
private TextView mSubjectText;
|
|
private View mDeliveredBadge;
|
|
private ViewGroup mMessageMetadataView;
|
|
private ViewGroup mMessageTextAndInfoView;
|
|
private TextView mSimNameView;
|
|
|
|
private boolean mOneOnOne;
|
|
private ConversationMessageViewHost mHost;
|
|
|
|
public ConversationMessageView(final Context context, final AttributeSet attrs) {
|
|
super(context, attrs);
|
|
// TODO: we should switch to using Binding and DataModel factory methods.
|
|
mData = new ConversationMessageData();
|
|
}
|
|
|
|
@Override
|
|
protected void onFinishInflate() {
|
|
mContactIconView = (ContactIconView) findViewById(R.id.conversation_icon);
|
|
mContactIconView.setOnLongClickListener(view -> {
|
|
ConversationMessageView.this.performLongClick();
|
|
return true;
|
|
});
|
|
|
|
mMessageAttachmentsView = (LinearLayout) findViewById(R.id.message_attachments);
|
|
mMultiAttachmentView = (MultiAttachmentLayout) findViewById(R.id.multiple_attachments);
|
|
mMultiAttachmentView.setOnAttachmentClickListener(this);
|
|
|
|
mMessageImageView = (AsyncImageView) findViewById(R.id.message_image);
|
|
mMessageImageView.setOnClickListener(this);
|
|
mMessageImageView.setOnLongClickListener(this);
|
|
|
|
mMessageTextView = (TextView) findViewById(R.id.message_text);
|
|
mMessageTextView.setOnClickListener(this);
|
|
IgnoreLinkLongClickHelper.ignoreLinkLongClick(mMessageTextView, this);
|
|
|
|
mStatusTextView = (TextView) findViewById(R.id.message_status);
|
|
mTitleTextView = (TextView) findViewById(R.id.message_title);
|
|
mMmsInfoTextView = (TextView) findViewById(R.id.mms_info);
|
|
mMessageTitleLayout = (LinearLayout) findViewById(R.id.message_title_layout);
|
|
mSenderNameTextView = (TextView) findViewById(R.id.message_sender_name);
|
|
mMessageBubble = (ConversationMessageBubbleView) findViewById(R.id.message_content);
|
|
mSubjectView = findViewById(R.id.subject_container);
|
|
mSubjectLabel = (TextView) mSubjectView.findViewById(R.id.subject_label);
|
|
mSubjectText = (TextView) mSubjectView.findViewById(R.id.subject_text);
|
|
mDeliveredBadge = findViewById(R.id.smsDeliveredBadge);
|
|
mMessageMetadataView = (ViewGroup) findViewById(R.id.message_metadata);
|
|
mMessageTextAndInfoView = (ViewGroup) findViewById(R.id.message_text_and_info);
|
|
mSimNameView = (TextView) findViewById(R.id.sim_name);
|
|
}
|
|
|
|
@Override
|
|
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
|
|
final int horizontalSpace = MeasureSpec.getSize(widthMeasureSpec);
|
|
final int iconSize = getResources()
|
|
.getDimensionPixelSize(R.dimen.conversation_message_contact_icon_size);
|
|
|
|
final int unspecifiedMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
|
|
final int iconMeasureSpec = MeasureSpec.makeMeasureSpec(iconSize, MeasureSpec.EXACTLY);
|
|
|
|
mContactIconView.measure(iconMeasureSpec, iconMeasureSpec);
|
|
|
|
final int arrowWidth =
|
|
getResources().getDimensionPixelSize(R.dimen.message_bubble_arrow_width);
|
|
|
|
// We need to subtract contact icon width twice from the horizontal space to get
|
|
// the max leftover space because we want the message bubble to extend no further than the
|
|
// starting position of the message bubble in the opposite direction.
|
|
final int maxLeftoverSpace = horizontalSpace - mContactIconView.getMeasuredWidth() * 2
|
|
- arrowWidth - getPaddingLeft() - getPaddingRight();
|
|
final int messageContentWidthMeasureSpec = MeasureSpec.makeMeasureSpec(maxLeftoverSpace,
|
|
MeasureSpec.AT_MOST);
|
|
|
|
mMessageBubble.measure(messageContentWidthMeasureSpec, unspecifiedMeasureSpec);
|
|
|
|
final int maxHeight = Math.max(mContactIconView.getMeasuredHeight(),
|
|
mMessageBubble.getMeasuredHeight());
|
|
setMeasuredDimension(horizontalSpace, maxHeight + getPaddingBottom() + getPaddingTop());
|
|
}
|
|
|
|
@Override
|
|
protected void onLayout(final boolean changed, final int left, final int top, final int right,
|
|
final int bottom) {
|
|
final boolean isRtl = AccessibilityUtil.isLayoutRtl(this);
|
|
|
|
final int iconWidth = mContactIconView.getMeasuredWidth();
|
|
final int iconHeight = mContactIconView.getMeasuredHeight();
|
|
final int iconTop = getPaddingTop();
|
|
final int contentWidth = (right -left) - iconWidth - getPaddingLeft() - getPaddingRight();
|
|
final int contentHeight = mMessageBubble.getMeasuredHeight();
|
|
final int contentTop = iconTop;
|
|
|
|
final int iconLeft;
|
|
final int contentLeft;
|
|
if (mData.getIsIncoming()) {
|
|
if (isRtl) {
|
|
iconLeft = (right - left) - getPaddingRight() - iconWidth;
|
|
contentLeft = iconLeft - contentWidth;
|
|
} else {
|
|
iconLeft = getPaddingLeft();
|
|
contentLeft = iconLeft + iconWidth;
|
|
}
|
|
} else {
|
|
if (isRtl) {
|
|
iconLeft = getPaddingLeft();
|
|
contentLeft = iconLeft + iconWidth;
|
|
} else {
|
|
iconLeft = (right - left) - getPaddingRight() - iconWidth;
|
|
contentLeft = iconLeft - contentWidth;
|
|
}
|
|
}
|
|
|
|
mContactIconView.layout(iconLeft, iconTop, iconLeft + iconWidth, iconTop + iconHeight);
|
|
|
|
mMessageBubble.layout(contentLeft, contentTop, contentLeft + contentWidth,
|
|
contentTop + contentHeight);
|
|
}
|
|
|
|
/**
|
|
* Fills in the data associated with this view.
|
|
*
|
|
* @param cursor The cursor from a MessageList that this view is in, pointing to its entry.
|
|
*/
|
|
public void bind(final Cursor cursor) {
|
|
bind(cursor, true, null);
|
|
}
|
|
|
|
/**
|
|
* Fills in the data associated with this view.
|
|
*
|
|
* @param cursor The cursor from a MessageList that this view is in, pointing to its entry.
|
|
* @param oneOnOne Whether this is a 1:1 conversation
|
|
*/
|
|
public void bind(final Cursor cursor,
|
|
final boolean oneOnOne, final String selectedMessageId) {
|
|
mOneOnOne = oneOnOne;
|
|
|
|
// Update our UI model
|
|
mData.bind(cursor);
|
|
setSelected(TextUtils.equals(mData.getMessageId(), selectedMessageId));
|
|
|
|
// Update text and image content for the view.
|
|
updateViewContent();
|
|
|
|
// Update colors and layout parameters for the view.
|
|
updateViewAppearance();
|
|
|
|
updateContentDescription();
|
|
}
|
|
|
|
public void setHost(final ConversationMessageViewHost host) {
|
|
mHost = host;
|
|
}
|
|
|
|
/**
|
|
* Sets a delay loader instance to manage loading / resuming of image attachments.
|
|
*/
|
|
public void setImageViewDelayLoader(final AsyncImageViewDelayLoader delayLoader) {
|
|
Assert.notNull(mMessageImageView);
|
|
mMessageImageView.setDelayLoader(delayLoader);
|
|
mMultiAttachmentView.setImageViewDelayLoader(delayLoader);
|
|
}
|
|
|
|
public ConversationMessageData getData() {
|
|
return mData;
|
|
}
|
|
|
|
/**
|
|
* Returns whether we should show simplified visual style for the message view (i.e. hide the
|
|
* avatar and bubble arrow, reduce padding).
|
|
*/
|
|
private boolean shouldShowSimplifiedVisualStyle() {
|
|
return mData.getCanClusterWithPreviousMessage();
|
|
}
|
|
|
|
/**
|
|
* Returns whether we need to show a message bubble for text content.
|
|
*/
|
|
private boolean shouldShowMessageTextBubble() {
|
|
if (mData.hasText()) {
|
|
return true;
|
|
}
|
|
final String subjectText = MmsUtils.cleanseMmsSubject(getResources(),
|
|
mData.getMmsSubject());
|
|
if (!TextUtils.isEmpty(subjectText)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private void updateViewContent() {
|
|
updateMessageContent();
|
|
int titleResId = -1;
|
|
int statusResId = -1;
|
|
String statusText = null;
|
|
switch(mData.getStatus()) {
|
|
case MessageData.BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING:
|
|
case MessageData.BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING:
|
|
case MessageData.BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD:
|
|
case MessageData.BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD:
|
|
titleResId = R.string.message_title_downloading;
|
|
statusResId = R.string.message_status_downloading;
|
|
break;
|
|
|
|
case MessageData.BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD:
|
|
if (!OsUtil.isSecondaryUser()) {
|
|
titleResId = R.string.message_title_manual_download;
|
|
if (isSelected()) {
|
|
statusResId = R.string.message_status_download_action;
|
|
} else {
|
|
statusResId = R.string.message_status_download;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case MessageData.BUGLE_STATUS_INCOMING_EXPIRED_OR_NOT_AVAILABLE:
|
|
if (!OsUtil.isSecondaryUser()) {
|
|
titleResId = R.string.message_title_download_failed;
|
|
statusResId = R.string.message_status_download_error;
|
|
}
|
|
break;
|
|
|
|
case MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED:
|
|
if (!OsUtil.isSecondaryUser()) {
|
|
titleResId = R.string.message_title_download_failed;
|
|
if (isSelected()) {
|
|
statusResId = R.string.message_status_download_action;
|
|
} else {
|
|
statusResId = R.string.message_status_download;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND:
|
|
case MessageData.BUGLE_STATUS_OUTGOING_SENDING:
|
|
statusResId = R.string.message_status_sending;
|
|
break;
|
|
|
|
case MessageData.BUGLE_STATUS_OUTGOING_RESENDING:
|
|
case MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY:
|
|
statusResId = R.string.message_status_send_retrying;
|
|
break;
|
|
|
|
case MessageData.BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER:
|
|
statusResId = R.string.message_status_send_failed_emergency_number;
|
|
break;
|
|
|
|
case MessageData.BUGLE_STATUS_OUTGOING_FAILED:
|
|
// don't show the error state unless we're the default sms app
|
|
if (PhoneUtils.getDefault().isDefaultSmsApp()) {
|
|
if (isSelected()) {
|
|
statusResId = R.string.message_status_resend;
|
|
} else {
|
|
statusResId = MmsUtils.mapRawStatusToErrorResourceId(
|
|
mData.getStatus(), mData.getRawTelephonyStatus());
|
|
}
|
|
break;
|
|
}
|
|
// FALL THROUGH HERE
|
|
|
|
case MessageData.BUGLE_STATUS_OUTGOING_COMPLETE:
|
|
case MessageData.BUGLE_STATUS_OUTGOING_DELIVERED:
|
|
case MessageData.BUGLE_STATUS_INCOMING_COMPLETE:
|
|
default:
|
|
if (!mData.getCanClusterWithNextMessage()) {
|
|
statusText = mData.getFormattedReceivedTimeStamp();
|
|
}
|
|
break;
|
|
}
|
|
|
|
final boolean titleVisible = (titleResId >= 0);
|
|
if (titleVisible) {
|
|
final String titleText = getResources().getString(titleResId);
|
|
mTitleTextView.setText(titleText);
|
|
|
|
final String mmsInfoText = getResources().getString(
|
|
R.string.mms_info,
|
|
Formatter.formatFileSize(getContext(), mData.getSmsMessageSize()),
|
|
DateUtils.formatDateTime(
|
|
getContext(),
|
|
mData.getMmsExpiry(),
|
|
DateUtils.FORMAT_SHOW_DATE |
|
|
DateUtils.FORMAT_SHOW_TIME |
|
|
DateUtils.FORMAT_NUMERIC_DATE |
|
|
DateUtils.FORMAT_NO_YEAR));
|
|
mMmsInfoTextView.setText(mmsInfoText);
|
|
mMessageTitleLayout.setVisibility(View.VISIBLE);
|
|
} else {
|
|
mMessageTitleLayout.setVisibility(View.GONE);
|
|
}
|
|
|
|
final String subjectText = MmsUtils.cleanseMmsSubject(getResources(),
|
|
mData.getMmsSubject());
|
|
final boolean subjectVisible = !TextUtils.isEmpty(subjectText);
|
|
|
|
final boolean senderNameVisible = !mOneOnOne && !mData.getCanClusterWithNextMessage()
|
|
&& mData.getIsIncoming();
|
|
if (senderNameVisible) {
|
|
mSenderNameTextView.setText(mData.getSenderDisplayName());
|
|
mSenderNameTextView.setVisibility(View.VISIBLE);
|
|
} else {
|
|
mSenderNameTextView.setVisibility(View.GONE);
|
|
}
|
|
|
|
if (statusResId >= 0) {
|
|
statusText = getResources().getString(statusResId);
|
|
}
|
|
|
|
// We set the text even if the view will be GONE for accessibility
|
|
mStatusTextView.setText(statusText);
|
|
final boolean statusVisible = !TextUtils.isEmpty(statusText);
|
|
if (statusVisible) {
|
|
mStatusTextView.setVisibility(View.VISIBLE);
|
|
} else {
|
|
mStatusTextView.setVisibility(View.GONE);
|
|
}
|
|
|
|
final boolean deliveredBadgeVisible =
|
|
mData.getStatus() == MessageData.BUGLE_STATUS_OUTGOING_DELIVERED;
|
|
mDeliveredBadge.setVisibility(deliveredBadgeVisible ? View.VISIBLE : View.GONE);
|
|
|
|
// Update the sim indicator.
|
|
final boolean showSimIconAsIncoming = mData.getIsIncoming() &&
|
|
(!mData.hasAttachments() || shouldShowMessageTextBubble());
|
|
final SubscriptionListEntry subscriptionEntry =
|
|
mHost.getSubscriptionEntryForSelfParticipant(mData.getSelfParticipantId(),
|
|
true /* excludeDefault */);
|
|
final boolean simNameVisible = subscriptionEntry != null &&
|
|
!TextUtils.isEmpty(subscriptionEntry.displayName) &&
|
|
!mData.getCanClusterWithNextMessage();
|
|
if (simNameVisible) {
|
|
final String simNameText = mData.getIsIncoming() ? getResources().getString(
|
|
R.string.incoming_sim_name_text, subscriptionEntry.displayName) :
|
|
subscriptionEntry.displayName;
|
|
mSimNameView.setText(simNameText);
|
|
mSimNameView.setTextColor(showSimIconAsIncoming ? getResources().getColor(
|
|
R.color.timestamp_text_incoming) : subscriptionEntry.displayColor);
|
|
mSimNameView.setVisibility(VISIBLE);
|
|
} else {
|
|
mSimNameView.setText(null);
|
|
mSimNameView.setVisibility(GONE);
|
|
}
|
|
|
|
final boolean metadataVisible = senderNameVisible || statusVisible
|
|
|| deliveredBadgeVisible || simNameVisible;
|
|
mMessageMetadataView.setVisibility(metadataVisible ? View.VISIBLE : View.GONE);
|
|
|
|
final boolean messageTextAndOrInfoVisible = titleVisible || subjectVisible
|
|
|| mData.hasText() || metadataVisible;
|
|
mMessageTextAndInfoView.setVisibility(
|
|
messageTextAndOrInfoVisible ? View.VISIBLE : View.GONE);
|
|
|
|
if (shouldShowSimplifiedVisualStyle()) {
|
|
mContactIconView.setVisibility(View.GONE);
|
|
mContactIconView.setImageResourceUri(null);
|
|
} else {
|
|
mContactIconView.setVisibility(View.VISIBLE);
|
|
final Uri avatarUri = AvatarUriUtil.createAvatarUri(
|
|
mData.getSenderProfilePhotoUri(),
|
|
mData.getSenderFullName(),
|
|
mData.getSenderNormalizedDestination(),
|
|
mData.getSenderContactLookupKey());
|
|
mContactIconView.setImageResourceUri(avatarUri, mData.getSenderContactId(),
|
|
mData.getSenderContactLookupKey(), mData.getSenderNormalizedDestination());
|
|
}
|
|
}
|
|
|
|
private void updateMessageContent() {
|
|
// We must update the text before the attachments since we search the text to see if we
|
|
// should make a preview youtube image in the attachments
|
|
updateMessageText();
|
|
updateMessageAttachments();
|
|
updateMessageSubject();
|
|
mMessageBubble.bind(mData);
|
|
}
|
|
|
|
private void updateMessageAttachments() {
|
|
// Bind video, audio, and VCard attachments. If there are multiple, they stack vertically.
|
|
bindAttachmentsOfSameType(sVideoFilter,
|
|
R.layout.message_video_attachment, mVideoViewBinder, VideoThumbnailView.class);
|
|
bindAttachmentsOfSameType(sAudioFilter,
|
|
R.layout.message_audio_attachment, mAudioViewBinder, AudioAttachmentView.class);
|
|
bindAttachmentsOfSameType(sVCardFilter,
|
|
R.layout.message_vcard_attachment, mVCardViewBinder, PersonItemView.class);
|
|
|
|
// Bind image attachments. If there are multiple, they are shown in a collage view.
|
|
final List<MessagePartData> imageParts = mData.getAttachments(sImageFilter);
|
|
if (imageParts.size() > 1) {
|
|
Collections.sort(imageParts, sImageComparator);
|
|
mMultiAttachmentView.bindAttachments(imageParts, null, imageParts.size());
|
|
mMultiAttachmentView.setVisibility(View.VISIBLE);
|
|
} else {
|
|
mMultiAttachmentView.setVisibility(View.GONE);
|
|
}
|
|
|
|
// In the case that we have no image attachments and exactly one youtube link in a message
|
|
// then we will show a preview.
|
|
String youtubeThumbnailUrl = null;
|
|
String originalYoutubeLink = null;
|
|
if (mMessageTextHasLinks && imageParts.size() == 0) {
|
|
CharSequence messageTextWithSpans = mMessageTextView.getText();
|
|
final URLSpan[] spans = ((Spanned) messageTextWithSpans).getSpans(0,
|
|
messageTextWithSpans.length(), URLSpan.class);
|
|
for (URLSpan span : spans) {
|
|
String url = span.getURL();
|
|
String youtubeLinkForUrl = YouTubeUtil.getYoutubePreviewImageLink(url);
|
|
if (!TextUtils.isEmpty(youtubeLinkForUrl)) {
|
|
if (TextUtils.isEmpty(youtubeThumbnailUrl)) {
|
|
// Save the youtube link if we don't already have one
|
|
youtubeThumbnailUrl = youtubeLinkForUrl;
|
|
originalYoutubeLink = url;
|
|
} else {
|
|
// We already have a youtube link. This means we have two youtube links so
|
|
// we shall show none.
|
|
youtubeThumbnailUrl = null;
|
|
originalYoutubeLink = null;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// We need to keep track if we have a youtube link in the message so that we will not show
|
|
// the arrow
|
|
mMessageHasYouTubeLink = !TextUtils.isEmpty(youtubeThumbnailUrl);
|
|
|
|
// We will show the message image view if there is one attachment or one youtube link
|
|
if (imageParts.size() == 1 || mMessageHasYouTubeLink) {
|
|
// Get the display metrics for a hint for how large to pull the image data into
|
|
final WindowManager windowManager = (WindowManager) getContext().
|
|
getSystemService(Context.WINDOW_SERVICE);
|
|
final DisplayMetrics displayMetrics = new DisplayMetrics();
|
|
windowManager.getDefaultDisplay().getMetrics(displayMetrics);
|
|
|
|
final int iconSize = getResources()
|
|
.getDimensionPixelSize(R.dimen.conversation_message_contact_icon_size);
|
|
final int desiredWidth = displayMetrics.widthPixels - iconSize - iconSize;
|
|
|
|
if (imageParts.size() == 1) {
|
|
final MessagePartData imagePart = imageParts.get(0);
|
|
// If the image is big, we want to scale it down to save memory since we're going to
|
|
// scale it down to fit into the bubble width. We don't constrain the height.
|
|
final ImageRequestDescriptor imageRequest =
|
|
new MessagePartImageRequestDescriptor(imagePart,
|
|
desiredWidth,
|
|
MessagePartData.UNSPECIFIED_SIZE,
|
|
false);
|
|
adjustImageViewBounds(imagePart);
|
|
mMessageImageView.setImageResourceId(imageRequest);
|
|
mMessageImageView.setTag(imagePart);
|
|
} else {
|
|
// Youtube Thumbnail image
|
|
final ImageRequestDescriptor imageRequest =
|
|
new UriImageRequestDescriptor(Uri.parse(youtubeThumbnailUrl), desiredWidth,
|
|
MessagePartData.UNSPECIFIED_SIZE, true /* allowCompression */,
|
|
true /* isStatic */, false /* cropToCircle */,
|
|
ImageUtils.DEFAULT_CIRCLE_BACKGROUND_COLOR /* circleBackgroundColor */,
|
|
ImageUtils.DEFAULT_CIRCLE_STROKE_COLOR /* circleStrokeColor */);
|
|
mMessageImageView.setImageResourceId(imageRequest);
|
|
mMessageImageView.setTag(originalYoutubeLink);
|
|
}
|
|
mMessageImageView.setVisibility(View.VISIBLE);
|
|
} else {
|
|
mMessageImageView.setImageResourceId(null);
|
|
mMessageImageView.setVisibility(View.GONE);
|
|
}
|
|
|
|
// Show the message attachments container if any of its children are visible
|
|
boolean attachmentsVisible = false;
|
|
for (int i = 0, size = mMessageAttachmentsView.getChildCount(); i < size; i++) {
|
|
final View attachmentView = mMessageAttachmentsView.getChildAt(i);
|
|
if (attachmentView.getVisibility() == View.VISIBLE) {
|
|
attachmentsVisible = true;
|
|
break;
|
|
}
|
|
}
|
|
mMessageAttachmentsView.setVisibility(attachmentsVisible ? View.VISIBLE : View.GONE);
|
|
}
|
|
|
|
private void bindAttachmentsOfSameType(final Predicate<MessagePartData> attachmentTypeFilter,
|
|
final int attachmentViewLayoutRes, final AttachmentViewBinder viewBinder,
|
|
final Class<?> attachmentViewClass) {
|
|
final LayoutInflater layoutInflater = LayoutInflater.from(getContext());
|
|
|
|
// Iterate through all attachments of a particular type (video, audio, etc).
|
|
// Find the first attachment index that matches the given type if possible.
|
|
int attachmentViewIndex = -1;
|
|
View existingAttachmentView;
|
|
do {
|
|
existingAttachmentView = mMessageAttachmentsView.getChildAt(++attachmentViewIndex);
|
|
} while (existingAttachmentView != null &&
|
|
!(attachmentViewClass.isInstance(existingAttachmentView)));
|
|
|
|
for (final MessagePartData attachment : mData.getAttachments(attachmentTypeFilter)) {
|
|
View attachmentView = mMessageAttachmentsView.getChildAt(attachmentViewIndex);
|
|
if (!attachmentViewClass.isInstance(attachmentView)) {
|
|
attachmentView = layoutInflater.inflate(attachmentViewLayoutRes,
|
|
mMessageAttachmentsView, false /* attachToRoot */);
|
|
attachmentView.setOnClickListener(this);
|
|
attachmentView.setOnLongClickListener(this);
|
|
mMessageAttachmentsView.addView(attachmentView, attachmentViewIndex);
|
|
}
|
|
viewBinder.bindView(attachmentView, attachment);
|
|
attachmentView.setTag(attachment);
|
|
attachmentView.setVisibility(View.VISIBLE);
|
|
attachmentViewIndex++;
|
|
}
|
|
// If there are unused views left over, unbind or remove them.
|
|
while (attachmentViewIndex < mMessageAttachmentsView.getChildCount()) {
|
|
final View attachmentView = mMessageAttachmentsView.getChildAt(attachmentViewIndex);
|
|
if (attachmentViewClass.isInstance(attachmentView)) {
|
|
mMessageAttachmentsView.removeViewAt(attachmentViewIndex);
|
|
} else {
|
|
// No more views of this type; we're done.
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void updateMessageSubject() {
|
|
final String subjectText = MmsUtils.cleanseMmsSubject(getResources(),
|
|
mData.getMmsSubject());
|
|
final boolean subjectVisible = !TextUtils.isEmpty(subjectText);
|
|
|
|
if (subjectVisible) {
|
|
mSubjectText.setText(subjectText);
|
|
mSubjectView.setVisibility(View.VISIBLE);
|
|
} else {
|
|
mSubjectView.setVisibility(View.GONE);
|
|
}
|
|
}
|
|
|
|
private void updateMessageText() {
|
|
final String text = mData.getText();
|
|
if (!TextUtils.isEmpty(text)) {
|
|
mMessageTextView.setText(text);
|
|
// Linkify phone numbers, web urls, emails, and map addresses to allow users to
|
|
// click on them and take the default intent.
|
|
mMessageTextHasLinks = Linkify.addLinks(mMessageTextView, Linkify.ALL);
|
|
mMessageTextView.setVisibility(View.VISIBLE);
|
|
} else {
|
|
mMessageTextView.setVisibility(View.GONE);
|
|
mMessageTextHasLinks = false;
|
|
}
|
|
}
|
|
|
|
private void updateViewAppearance() {
|
|
final Resources res = getResources();
|
|
final ConversationDrawables drawableProvider = ConversationDrawables.get();
|
|
final boolean incoming = mData.getIsIncoming();
|
|
final boolean outgoing = !incoming;
|
|
|
|
final int messageTopPaddingClustered =
|
|
res.getDimensionPixelSize(R.dimen.message_padding_same_author);
|
|
final int messageTopPaddingDefault =
|
|
res.getDimensionPixelSize(R.dimen.message_padding_default);
|
|
final int arrowWidth = res.getDimensionPixelOffset(R.dimen.message_bubble_arrow_width);
|
|
final int messageTextMinHeightDefault = res.getDimensionPixelSize(
|
|
R.dimen.conversation_message_contact_icon_size);
|
|
final int messageTextLeftRightPadding = res.getDimensionPixelOffset(
|
|
R.dimen.message_text_left_right_padding);
|
|
final int textTopPaddingDefault = res.getDimensionPixelOffset(
|
|
R.dimen.message_text_top_padding);
|
|
final int textBottomPaddingDefault = res.getDimensionPixelOffset(
|
|
R.dimen.message_text_bottom_padding);
|
|
|
|
// These values depend on whether the message has text, attachments, or both.
|
|
// We intentionally don't set defaults, so the compiler will tell us if we forget
|
|
// to set one of them, or if we set one more than once.
|
|
final int contentLeftPadding, contentRightPadding;
|
|
final Drawable textBackground;
|
|
final int textMinHeight;
|
|
final int textTopMargin;
|
|
final int textTopPadding, textBottomPadding;
|
|
final int textLeftPadding, textRightPadding;
|
|
|
|
if (mData.hasAttachments()) {
|
|
if (shouldShowMessageTextBubble()) {
|
|
// Text and attachment(s)
|
|
contentLeftPadding = incoming ? arrowWidth : 0;
|
|
contentRightPadding = outgoing ? arrowWidth : 0;
|
|
textBackground = drawableProvider.getBubbleDrawable(
|
|
isSelected(),
|
|
incoming,
|
|
mData.hasIncomingErrorStatus(),
|
|
mData.getSenderContactLookupKey());
|
|
textMinHeight = messageTextMinHeightDefault;
|
|
textTopMargin = messageTopPaddingClustered;
|
|
textTopPadding = textTopPaddingDefault;
|
|
textBottomPadding = textBottomPaddingDefault;
|
|
textLeftPadding = messageTextLeftRightPadding;
|
|
textRightPadding = messageTextLeftRightPadding;
|
|
mMessageTextView.setTextIsSelectable(isSelected());
|
|
} else {
|
|
// Attachment(s) only
|
|
contentLeftPadding = incoming ? arrowWidth : 0;
|
|
contentRightPadding = outgoing ? arrowWidth : 0;
|
|
textBackground = null;
|
|
textMinHeight = 0;
|
|
textTopMargin = 0;
|
|
textTopPadding = 0;
|
|
textBottomPadding = 0;
|
|
textLeftPadding = 0;
|
|
textRightPadding = 0;
|
|
}
|
|
} else {
|
|
// Text only
|
|
contentLeftPadding = incoming ? arrowWidth : 0;
|
|
contentRightPadding = outgoing ? arrowWidth : 0;
|
|
textBackground = drawableProvider.getBubbleDrawable(
|
|
isSelected(),
|
|
incoming,
|
|
mData.hasIncomingErrorStatus(),
|
|
mData.getSenderContactLookupKey());
|
|
textMinHeight = messageTextMinHeightDefault;
|
|
textTopMargin = 0;
|
|
textTopPadding = textTopPaddingDefault;
|
|
textBottomPadding = textBottomPaddingDefault;
|
|
mMessageTextView.setTextIsSelectable(isSelected());
|
|
textLeftPadding = messageTextLeftRightPadding;
|
|
textRightPadding = messageTextLeftRightPadding;
|
|
}
|
|
|
|
// These values do not depend on whether the message includes attachments
|
|
final int gravity = incoming ? (Gravity.START | Gravity.CENTER_VERTICAL) :
|
|
(Gravity.END | Gravity.CENTER_VERTICAL);
|
|
final int messageTopPadding = shouldShowSimplifiedVisualStyle() ?
|
|
messageTopPaddingClustered : messageTopPaddingDefault;
|
|
final int metadataTopPadding = res.getDimensionPixelOffset(
|
|
R.dimen.message_metadata_top_padding);
|
|
|
|
// Update the message text/info views
|
|
ImageUtils.setBackgroundDrawableOnView(mMessageTextAndInfoView, textBackground);
|
|
mMessageTextAndInfoView.setMinimumHeight(textMinHeight);
|
|
final LinearLayout.LayoutParams textAndInfoLayoutParams =
|
|
(LinearLayout.LayoutParams) mMessageTextAndInfoView.getLayoutParams();
|
|
textAndInfoLayoutParams.topMargin = textTopMargin;
|
|
|
|
if (UiUtils.isRtlMode()) {
|
|
// Need to switch right and left padding in RtL mode
|
|
mMessageTextAndInfoView.setPadding(textRightPadding, textTopPadding, textLeftPadding,
|
|
textBottomPadding);
|
|
mMessageBubble.setPadding(contentRightPadding, 0, contentLeftPadding, 0);
|
|
} else {
|
|
mMessageTextAndInfoView.setPadding(textLeftPadding, textTopPadding, textRightPadding,
|
|
textBottomPadding);
|
|
mMessageBubble.setPadding(contentLeftPadding, 0, contentRightPadding, 0);
|
|
}
|
|
|
|
// Update the message row and message bubble views
|
|
setPadding(getPaddingLeft(), messageTopPadding, getPaddingRight(), 0);
|
|
mMessageBubble.setGravity(gravity);
|
|
updateMessageAttachmentsAppearance(gravity);
|
|
|
|
mMessageMetadataView.setPadding(0, metadataTopPadding, 0, 0);
|
|
|
|
updateTextAppearance();
|
|
|
|
requestLayout();
|
|
}
|
|
|
|
private void updateContentDescription() {
|
|
StringBuilder description = new StringBuilder();
|
|
|
|
Resources res = getResources();
|
|
String separator = res.getString(R.string.enumeration_comma);
|
|
|
|
// Sender information
|
|
boolean hasPlainTextMessage = !(TextUtils.isEmpty(mData.getText()) ||
|
|
mMessageTextHasLinks);
|
|
if (mData.getIsIncoming()) {
|
|
int senderResId = hasPlainTextMessage
|
|
? R.string.incoming_text_sender_content_description
|
|
: R.string.incoming_sender_content_description;
|
|
description.append(res.getString(senderResId, mData.getSenderDisplayName()));
|
|
} else {
|
|
int senderResId = hasPlainTextMessage
|
|
? R.string.outgoing_text_sender_content_description
|
|
: R.string.outgoing_sender_content_description;
|
|
description.append(res.getString(senderResId));
|
|
}
|
|
|
|
if (mSubjectView.getVisibility() == View.VISIBLE) {
|
|
description.append(separator);
|
|
description.append(mSubjectText.getText());
|
|
}
|
|
|
|
if (mMessageTextView.getVisibility() == View.VISIBLE) {
|
|
// If the message has hyperlinks, we will let the user navigate to the text message so
|
|
// that the hyperlink can be clicked. Otherwise, the text message does not need to
|
|
// be reachable.
|
|
if (mMessageTextHasLinks) {
|
|
mMessageTextView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
|
|
} else {
|
|
mMessageTextView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
|
description.append(separator);
|
|
description.append(mMessageTextView.getText());
|
|
}
|
|
}
|
|
|
|
if (mMessageTitleLayout.getVisibility() == View.VISIBLE) {
|
|
description.append(separator);
|
|
description.append(mTitleTextView.getText());
|
|
|
|
description.append(separator);
|
|
description.append(mMmsInfoTextView.getText());
|
|
}
|
|
|
|
if (mStatusTextView.getVisibility() == View.VISIBLE) {
|
|
description.append(separator);
|
|
description.append(mStatusTextView.getText());
|
|
}
|
|
|
|
if (mSimNameView.getVisibility() == View.VISIBLE) {
|
|
description.append(separator);
|
|
description.append(mSimNameView.getText());
|
|
}
|
|
|
|
if (mDeliveredBadge.getVisibility() == View.VISIBLE) {
|
|
description.append(separator);
|
|
description.append(res.getString(R.string.delivered_status_content_description));
|
|
}
|
|
|
|
setContentDescription(description);
|
|
}
|
|
|
|
private void updateMessageAttachmentsAppearance(final int gravity) {
|
|
mMessageAttachmentsView.setGravity(gravity);
|
|
|
|
// Tint image/video attachments when selected
|
|
final int selectedImageTint = getResources().getColor(R.color.message_image_selected_tint);
|
|
if (mMessageImageView.getVisibility() == View.VISIBLE) {
|
|
if (isSelected()) {
|
|
mMessageImageView.setColorFilter(selectedImageTint);
|
|
} else {
|
|
mMessageImageView.clearColorFilter();
|
|
}
|
|
}
|
|
if (mMultiAttachmentView.getVisibility() == View.VISIBLE) {
|
|
if (isSelected()) {
|
|
mMultiAttachmentView.setColorFilter(selectedImageTint);
|
|
} else {
|
|
mMultiAttachmentView.clearColorFilter();
|
|
}
|
|
}
|
|
for (int i = 0, size = mMessageAttachmentsView.getChildCount(); i < size; i++) {
|
|
final View attachmentView = mMessageAttachmentsView.getChildAt(i);
|
|
if (attachmentView instanceof VideoThumbnailView
|
|
&& attachmentView.getVisibility() == View.VISIBLE) {
|
|
final VideoThumbnailView videoView = (VideoThumbnailView) attachmentView;
|
|
if (isSelected()) {
|
|
videoView.setColorFilter(selectedImageTint);
|
|
} else {
|
|
videoView.clearColorFilter();
|
|
}
|
|
}
|
|
}
|
|
|
|
// If there are multiple attachment bubbles in a single message, add some separation.
|
|
final int multipleAttachmentPadding =
|
|
getResources().getDimensionPixelSize(R.dimen.message_padding_same_author);
|
|
|
|
boolean previousVisibleView = false;
|
|
for (int i = 0, size = mMessageAttachmentsView.getChildCount(); i < size; i++) {
|
|
final View attachmentView = mMessageAttachmentsView.getChildAt(i);
|
|
if (attachmentView.getVisibility() == View.VISIBLE) {
|
|
final int margin = previousVisibleView ? multipleAttachmentPadding : 0;
|
|
((LinearLayout.LayoutParams) attachmentView.getLayoutParams()).topMargin = margin;
|
|
// updateViewAppearance calls requestLayout() at the end, so we don't need to here
|
|
previousVisibleView = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void updateTextAppearance() {
|
|
int messageColorResId;
|
|
int statusColorResId = -1;
|
|
int infoColorResId = -1;
|
|
int timestampColorResId;
|
|
int subjectLabelColorResId;
|
|
if (isSelected()) {
|
|
messageColorResId = R.color.message_text_color_incoming;
|
|
statusColorResId = R.color.message_action_status_text;
|
|
infoColorResId = R.color.message_action_info_text;
|
|
if (shouldShowMessageTextBubble()) {
|
|
timestampColorResId = R.color.message_action_timestamp_text;
|
|
subjectLabelColorResId = R.color.message_action_timestamp_text;
|
|
} else {
|
|
// If there's no text, the timestamp will be shown below the attachments,
|
|
// against the conversation view background.
|
|
timestampColorResId = R.color.timestamp_text_outgoing;
|
|
subjectLabelColorResId = R.color.timestamp_text_outgoing;
|
|
}
|
|
} else {
|
|
messageColorResId = (mData.getIsIncoming() ?
|
|
R.color.message_text_color_incoming : R.color.message_text_color_outgoing);
|
|
statusColorResId = messageColorResId;
|
|
infoColorResId = R.color.timestamp_text_incoming;
|
|
switch(mData.getStatus()) {
|
|
|
|
case MessageData.BUGLE_STATUS_OUTGOING_FAILED:
|
|
case MessageData.BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER:
|
|
timestampColorResId = R.color.message_failed_timestamp_text;
|
|
subjectLabelColorResId = R.color.timestamp_text_outgoing;
|
|
break;
|
|
|
|
case MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND:
|
|
case MessageData.BUGLE_STATUS_OUTGOING_SENDING:
|
|
case MessageData.BUGLE_STATUS_OUTGOING_RESENDING:
|
|
case MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY:
|
|
case MessageData.BUGLE_STATUS_OUTGOING_COMPLETE:
|
|
case MessageData.BUGLE_STATUS_OUTGOING_DELIVERED:
|
|
timestampColorResId = R.color.timestamp_text_outgoing;
|
|
subjectLabelColorResId = R.color.timestamp_text_outgoing;
|
|
break;
|
|
|
|
case MessageData.BUGLE_STATUS_INCOMING_EXPIRED_OR_NOT_AVAILABLE:
|
|
case MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED:
|
|
messageColorResId = R.color.message_text_color_incoming_download_failed;
|
|
timestampColorResId = R.color.message_download_failed_timestamp_text;
|
|
subjectLabelColorResId = R.color.message_text_color_incoming_download_failed;
|
|
statusColorResId = R.color.message_download_failed_status_text;
|
|
infoColorResId = R.color.message_info_text_incoming_download_failed;
|
|
break;
|
|
|
|
case MessageData.BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING:
|
|
case MessageData.BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING:
|
|
case MessageData.BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD:
|
|
case MessageData.BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD:
|
|
case MessageData.BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD:
|
|
timestampColorResId = R.color.message_text_color_incoming;
|
|
subjectLabelColorResId = R.color.message_text_color_incoming;
|
|
infoColorResId = R.color.timestamp_text_incoming;
|
|
break;
|
|
|
|
case MessageData.BUGLE_STATUS_INCOMING_COMPLETE:
|
|
default:
|
|
timestampColorResId = R.color.timestamp_text_incoming;
|
|
subjectLabelColorResId = R.color.timestamp_text_incoming;
|
|
infoColorResId = -1; // Not used
|
|
break;
|
|
}
|
|
}
|
|
final int messageColor = getResources().getColor(messageColorResId);
|
|
mMessageTextView.setTextColor(messageColor);
|
|
mMessageTextView.setLinkTextColor(messageColor);
|
|
mSubjectText.setTextColor(messageColor);
|
|
if (statusColorResId >= 0) {
|
|
mTitleTextView.setTextColor(getResources().getColor(statusColorResId));
|
|
}
|
|
if (infoColorResId >= 0) {
|
|
mMmsInfoTextView.setTextColor(getResources().getColor(infoColorResId));
|
|
}
|
|
if (timestampColorResId == R.color.timestamp_text_incoming &&
|
|
mData.hasAttachments() && !shouldShowMessageTextBubble()) {
|
|
timestampColorResId = R.color.timestamp_text_outgoing;
|
|
}
|
|
mStatusTextView.setTextColor(getResources().getColor(timestampColorResId));
|
|
|
|
mSubjectLabel.setTextColor(getResources().getColor(subjectLabelColorResId));
|
|
mSenderNameTextView.setTextColor(getResources().getColor(timestampColorResId));
|
|
}
|
|
|
|
/**
|
|
* If we don't know the size of the image, we want to show it in a fixed-sized frame to
|
|
* avoid janks when the image is loaded and resized. Otherwise, we can set the imageview to
|
|
* take on normal layout params.
|
|
*/
|
|
private void adjustImageViewBounds(final MessagePartData imageAttachment) {
|
|
Assert.isTrue(ContentType.isImageType(imageAttachment.getContentType()));
|
|
final ViewGroup.LayoutParams layoutParams = mMessageImageView.getLayoutParams();
|
|
if (imageAttachment.getWidth() == MessagePartData.UNSPECIFIED_SIZE ||
|
|
imageAttachment.getHeight() == MessagePartData.UNSPECIFIED_SIZE) {
|
|
// We don't know the size of the image attachment, enable letterboxing on the image
|
|
// and show a fixed sized attachment. This should happen at most once per image since
|
|
// after the image is loaded we then save the image dimensions to the db so that the
|
|
// next time we can display the full size.
|
|
layoutParams.width = getResources()
|
|
.getDimensionPixelSize(R.dimen.image_attachment_fallback_width);
|
|
layoutParams.height = getResources()
|
|
.getDimensionPixelSize(R.dimen.image_attachment_fallback_height);
|
|
mMessageImageView.setScaleType(ScaleType.CENTER_CROP);
|
|
} else {
|
|
layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
|
|
layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
|
|
// ScaleType.CENTER_INSIDE and FIT_CENTER behave similarly for most images. However,
|
|
// FIT_CENTER works better for small images as it enlarges the image such that the
|
|
// minimum size ("android:minWidth" etc) is honored.
|
|
mMessageImageView.setScaleType(ScaleType.FIT_CENTER);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onClick(final View view) {
|
|
final Object tag = view.getTag();
|
|
if (tag instanceof MessagePartData) {
|
|
final Rect bounds = UiUtils.getMeasuredBoundsOnScreen(view);
|
|
onAttachmentClick((MessagePartData) tag, bounds, false /* longPress */);
|
|
} else if (tag instanceof String) {
|
|
// Currently the only object that would make a tag of a string is a youtube preview
|
|
// image
|
|
UIIntents.get().launchBrowserForUrl(getContext(), (String) tag);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean onLongClick(final View view) {
|
|
if (view == mMessageTextView) {
|
|
// Avoid trying to reselect the message
|
|
if (isSelected()) {
|
|
return false;
|
|
}
|
|
|
|
// Preemptively handle the long click event on message text so it's not handled by
|
|
// the link spans.
|
|
return performLongClick();
|
|
}
|
|
|
|
final Object tag = view.getTag();
|
|
if (tag instanceof MessagePartData) {
|
|
final Rect bounds = UiUtils.getMeasuredBoundsOnScreen(view);
|
|
return onAttachmentClick((MessagePartData) tag, bounds, true /* longPress */);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public boolean onAttachmentClick(final MessagePartData attachment,
|
|
final Rect viewBoundsOnScreen, final boolean longPress) {
|
|
return mHost.onAttachmentClick(this, attachment, viewBoundsOnScreen, longPress);
|
|
}
|
|
|
|
public ContactIconView getContactIconView() {
|
|
return mContactIconView;
|
|
}
|
|
|
|
// Sort photos in MultiAttachLayout in the same order as the ConversationImagePartsView
|
|
static final Comparator<MessagePartData> sImageComparator =
|
|
Comparator.comparing(MessagePartData::getPartId);
|
|
|
|
static final Predicate<MessagePartData> sVideoFilter = MessagePartData::isVideo;
|
|
static final Predicate<MessagePartData> sAudioFilter = MessagePartData::isAudio;
|
|
static final Predicate<MessagePartData> sVCardFilter = MessagePartData::isVCard;
|
|
static final Predicate<MessagePartData> sImageFilter = MessagePartData::isImage;
|
|
|
|
interface AttachmentViewBinder {
|
|
void bindView(View view, MessagePartData attachment);
|
|
void unbind(View view);
|
|
}
|
|
|
|
final AttachmentViewBinder mVideoViewBinder = new AttachmentViewBinder() {
|
|
@Override
|
|
public void bindView(final View view, final MessagePartData attachment) {
|
|
((VideoThumbnailView) view).setSource(attachment, mData.getIsIncoming());
|
|
}
|
|
|
|
@Override
|
|
public void unbind(final View view) {
|
|
((VideoThumbnailView) view).setSource((Uri) null, mData.getIsIncoming());
|
|
}
|
|
};
|
|
|
|
final AttachmentViewBinder mAudioViewBinder = new AttachmentViewBinder() {
|
|
@Override
|
|
public void bindView(final View view, final MessagePartData attachment) {
|
|
final AudioAttachmentView audioView = (AudioAttachmentView) view;
|
|
audioView.bindMessagePartData(attachment, mData.getIsIncoming(), isSelected());
|
|
audioView.setBackground(ConversationDrawables.get().getBubbleDrawable(
|
|
isSelected(), mData.getIsIncoming(), mData.hasIncomingErrorStatus(),
|
|
mData.getSenderContactLookupKey()));
|
|
}
|
|
|
|
@Override
|
|
public void unbind(final View view) {
|
|
((AudioAttachmentView) view).bindMessagePartData(null, mData.getIsIncoming(), false);
|
|
}
|
|
};
|
|
|
|
final AttachmentViewBinder mVCardViewBinder = new AttachmentViewBinder() {
|
|
@Override
|
|
public void bindView(final View view, final MessagePartData attachment) {
|
|
final PersonItemView personView = (PersonItemView) view;
|
|
personView.bind(DataModel.get().createVCardContactItemData(getContext(),
|
|
attachment));
|
|
personView.setBackground(ConversationDrawables.get().getBubbleDrawable(
|
|
isSelected(), mData.getIsIncoming(), mData.hasIncomingErrorStatus(),
|
|
mData.getSenderContactLookupKey()));
|
|
final int nameTextColorRes;
|
|
final int detailsTextColorRes;
|
|
if (isSelected()) {
|
|
nameTextColorRes = R.color.message_text_color_incoming;
|
|
detailsTextColorRes = R.color.message_text_color_incoming;
|
|
} else {
|
|
nameTextColorRes = mData.getIsIncoming() ? R.color.message_text_color_incoming
|
|
: R.color.message_text_color_outgoing;
|
|
detailsTextColorRes = mData.getIsIncoming() ? R.color.timestamp_text_incoming
|
|
: R.color.timestamp_text_outgoing;
|
|
}
|
|
personView.setNameTextColor(getResources().getColor(nameTextColorRes));
|
|
personView.setDetailsTextColor(getResources().getColor(detailsTextColorRes));
|
|
}
|
|
|
|
@Override
|
|
public void unbind(final View view) {
|
|
((PersonItemView) view).bind(null);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* A helper class that allows us to handle long clicks on linkified message text view (i.e. to
|
|
* select the message) so it's not handled by the link spans to launch apps for the links.
|
|
*/
|
|
private static class IgnoreLinkLongClickHelper implements OnLongClickListener, OnTouchListener {
|
|
private boolean mIsLongClick;
|
|
private final OnLongClickListener mDelegateLongClickListener;
|
|
|
|
/**
|
|
* Ignore long clicks on linkified texts for a given text view.
|
|
* @param textView the TextView to ignore long clicks on
|
|
* @param longClickListener a delegate OnLongClickListener to be called when the view is
|
|
* long clicked.
|
|
*/
|
|
public static void ignoreLinkLongClick(final TextView textView,
|
|
@Nullable final OnLongClickListener longClickListener) {
|
|
final IgnoreLinkLongClickHelper helper =
|
|
new IgnoreLinkLongClickHelper(longClickListener);
|
|
textView.setOnLongClickListener(helper);
|
|
textView.setOnTouchListener(helper);
|
|
}
|
|
|
|
private IgnoreLinkLongClickHelper(@Nullable final OnLongClickListener longClickListener) {
|
|
mDelegateLongClickListener = longClickListener;
|
|
}
|
|
|
|
@Override
|
|
public boolean onLongClick(final View v) {
|
|
// Record that this click is a long click.
|
|
mIsLongClick = true;
|
|
if (mDelegateLongClickListener != null) {
|
|
return mDelegateLongClickListener.onLongClick(v);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public boolean onTouch(final View v, final MotionEvent event) {
|
|
if (event.getActionMasked() == MotionEvent.ACTION_UP && mIsLongClick) {
|
|
// This touch event is a long click, preemptively handle this touch event so that
|
|
// the link span won't get a onClicked() callback.
|
|
mIsLongClick = false;
|
|
return false;
|
|
}
|
|
|
|
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
|
|
mIsLongClick = false;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
}
|