blob: 0791ed3c42eca5395e9dedd882011c40987f4a8c [file] [log] [blame]
Selim Cinek20d1ee22020-02-03 16:04:26 -05001/*
2 * Copyright (C) 2020 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.internal.widget;
18
Selim Cinek514b52c2020-03-17 18:42:12 -070019import static com.android.internal.widget.MessagingGroup.IMAGE_DISPLAY_LOCATION_EXTERNAL;
20import static com.android.internal.widget.MessagingGroup.IMAGE_DISPLAY_LOCATION_INLINE;
Selim Cinek9f2d21f2020-03-27 12:36:38 -070021import static com.android.internal.widget.MessagingPropertyAnimator.ALPHA_IN;
22import static com.android.internal.widget.MessagingPropertyAnimator.ALPHA_OUT;
Selim Cinek514b52c2020-03-17 18:42:12 -070023
Steve Elliott8b4929d2020-06-02 13:38:04 -040024import android.animation.Animator;
25import android.animation.AnimatorListenerAdapter;
26import android.animation.AnimatorSet;
27import android.animation.ValueAnimator;
Selim Cinek20d1ee22020-02-03 16:04:26 -050028import android.annotation.AttrRes;
29import android.annotation.NonNull;
30import android.annotation.Nullable;
31import android.annotation.StyleRes;
32import android.app.Notification;
33import android.app.Person;
34import android.app.RemoteInputHistoryItem;
35import android.content.Context;
Selim Cineke9714eb2020-03-09 11:21:49 -070036import android.content.res.ColorStateList;
Selim Cinek20d1ee22020-02-03 16:04:26 -050037import android.graphics.Bitmap;
38import android.graphics.Canvas;
39import android.graphics.Color;
40import android.graphics.Paint;
41import android.graphics.Rect;
Steve Elliott936df152020-04-14 13:59:53 -040042import android.graphics.Typeface;
Steve Elliott8b4929d2020-06-02 13:38:04 -040043import android.graphics.drawable.GradientDrawable;
Selim Cinek20d1ee22020-02-03 16:04:26 -050044import android.graphics.drawable.Icon;
45import android.os.Bundle;
46import android.os.Parcelable;
Steve Elliott936df152020-04-14 13:59:53 -040047import android.text.Spannable;
48import android.text.SpannableString;
Selim Cinek20d1ee22020-02-03 16:04:26 -050049import android.text.TextUtils;
Steve Elliott936df152020-04-14 13:59:53 -040050import android.text.style.StyleSpan;
Selim Cinek20d1ee22020-02-03 16:04:26 -050051import android.util.ArrayMap;
52import android.util.AttributeSet;
53import android.util.DisplayMetrics;
54import android.view.Gravity;
55import android.view.RemotableViewMethod;
Selim Cinek4237e822020-03-31 17:22:28 -070056import android.view.TouchDelegate;
Selim Cinek20d1ee22020-02-03 16:04:26 -050057import android.view.View;
58import android.view.ViewGroup;
59import android.view.ViewTreeObserver;
60import android.view.animation.Interpolator;
61import android.view.animation.PathInterpolator;
62import android.widget.FrameLayout;
63import android.widget.ImageView;
Selim Cinek514b52c2020-03-17 18:42:12 -070064import android.widget.LinearLayout;
Selim Cinek20d1ee22020-02-03 16:04:26 -050065import android.widget.RemoteViews;
66import android.widget.TextView;
67
68import com.android.internal.R;
69import com.android.internal.graphics.ColorUtils;
70import com.android.internal.util.ContrastColorUtil;
71
72import java.util.ArrayList;
73import java.util.List;
Steve Elliott239e6cb2020-03-25 14:26:42 -040074import java.util.Locale;
Steve Elliott242057d2020-06-10 15:15:19 -040075import java.util.Objects;
Selim Cinek20d1ee22020-02-03 16:04:26 -050076import java.util.function.Consumer;
77import java.util.regex.Pattern;
78
79/**
80 * A custom-built layout for the Notification.MessagingStyle allows dynamic addition and removal
81 * messages and adapts the layout accordingly.
82 */
83@RemoteViews.RemoteView
84public class ConversationLayout extends FrameLayout
85 implements ImageMessageConsumer, IMessagingLayout {
86
Selim Cinek20d1ee22020-02-03 16:04:26 -050087 private static final float COLOR_SHIFT_AMOUNT = 60;
88 /**
Steve Elliott666124b2020-06-09 15:43:19 -040089 * Pattern for filter some ignorable characters.
Selim Cinek20d1ee22020-02-03 16:04:26 -050090 * p{Z} for any kind of whitespace or invisible separator.
91 * p{C} for any kind of punctuation character.
92 */
93 private static final Pattern IGNORABLE_CHAR_PATTERN
94 = Pattern.compile("[\\p{C}\\p{Z}]");
95 private static final Pattern SPECIAL_CHAR_PATTERN
96 = Pattern.compile ("[!@#$%&*()_+=|<>?{}\\[\\]~-]");
97 private static final Consumer<MessagingMessage> REMOVE_MESSAGE
98 = MessagingMessage::removeMessage;
99 public static final Interpolator LINEAR_OUT_SLOW_IN = new PathInterpolator(0f, 0f, 0.2f, 1f);
100 public static final Interpolator FAST_OUT_LINEAR_IN = new PathInterpolator(0.4f, 0f, 1f, 1f);
101 public static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f);
Steve Elliott8b4929d2020-06-02 13:38:04 -0400102 public static final Interpolator OVERSHOOT = new PathInterpolator(0.4f, 0f, 0.2f, 1.4f);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500103 public static final OnLayoutChangeListener MESSAGING_PROPERTY_ANIMATOR
104 = new MessagingPropertyAnimator();
Steve Elliott8b4929d2020-06-02 13:38:04 -0400105 public static final int IMPORTANCE_ANIM_GROW_DURATION = 250;
106 public static final int IMPORTANCE_ANIM_SHRINK_DURATION = 200;
107 public static final int IMPORTANCE_ANIM_SHRINK_DELAY = 25;
Selim Cinek20d1ee22020-02-03 16:04:26 -0500108 private List<MessagingMessage> mMessages = new ArrayList<>();
109 private List<MessagingMessage> mHistoricMessages = new ArrayList<>();
110 private MessagingLinearLayout mMessagingLinearLayout;
111 private boolean mShowHistoricMessages;
112 private ArrayList<MessagingGroup> mGroups = new ArrayList<>();
Selim Cinek20d1ee22020-02-03 16:04:26 -0500113 private int mLayoutColor;
114 private int mSenderTextColor;
115 private int mMessageTextColor;
116 private int mAvatarSize;
117 private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
118 private Paint mTextPaint = new Paint();
119 private Icon mAvatarReplacement;
120 private boolean mIsOneToOne;
121 private ArrayList<MessagingGroup> mAddedGroups = new ArrayList<>();
122 private Person mUser;
123 private CharSequence mNameReplacement;
124 private boolean mIsCollapsed;
125 private ImageResolver mImageResolver;
Steve Elliott936df152020-04-14 13:59:53 -0400126 private CachingIconView mConversationIconView;
Selim Cinekbf322ee2020-03-20 20:20:31 -0700127 private View mConversationIconContainer;
Selim Cinekbf322ee2020-03-20 20:20:31 -0700128 private int mConversationIconTopPaddingExpandedGroup;
Steve Elliottf7ef4ef2020-04-27 15:40:18 -0400129 private int mConversationIconTopPadding;
130 private int mExpandedGroupMessagePadding;
Selim Cinek2f7f7b82020-03-10 15:41:13 -0700131 private TextView mConversationText;
Selim Cinek20d1ee22020-02-03 16:04:26 -0500132 private View mConversationIconBadge;
Selim Cinek9f2d21f2020-03-27 12:36:38 -0700133 private CachingIconView mConversationIconBadgeBg;
Selim Cinek20d1ee22020-02-03 16:04:26 -0500134 private Icon mLargeIcon;
135 private View mExpandButtonContainer;
Selim Cinek28db07e2020-04-03 17:33:24 -0700136 private View mExpandButtonInnerContainer;
Selim Cinek54fe9ad2020-03-04 13:56:42 -0800137 private ViewGroup mExpandButtonAndContentContainer;
Selim Cinek20d1ee22020-02-03 16:04:26 -0500138 private NotificationExpandButton mExpandButton;
Selim Cinek514b52c2020-03-17 18:42:12 -0700139 private MessagingLinearLayout mImageMessageContainer;
Selim Cinek20d1ee22020-02-03 16:04:26 -0500140 private int mExpandButtonExpandedTopMargin;
141 private int mBadgedSideMargins;
Selim Cinekbf322ee2020-03-20 20:20:31 -0700142 private int mConversationAvatarSize;
143 private int mConversationAvatarSizeExpanded;
Selim Cinekf2b8aa72020-03-05 15:29:02 -0800144 private CachingIconView mIcon;
Selim Cinek9f2d21f2020-03-27 12:36:38 -0700145 private CachingIconView mImportanceRingView;
Selim Cinekbf322ee2020-03-20 20:20:31 -0700146 private int mExpandedGroupSideMargin;
147 private int mExpandedGroupSideMarginFacePile;
Selim Cinek8baa70f2020-03-09 20:09:35 -0700148 private View mConversationFacePile;
149 private int mNotificationBackgroundColor;
Selim Cinek2f7f7b82020-03-10 15:41:13 -0700150 private CharSequence mFallbackChatName;
151 private CharSequence mFallbackGroupChatName;
152 private CharSequence mConversationTitle;
Selim Cinekafc20582020-03-13 20:15:38 -0700153 private int mNotificationHeaderExpandedPadding;
154 private View mConversationHeader;
155 private View mContentContainer;
156 private boolean mExpandable = true;
157 private int mContentMarginEnd;
Selim Cinek514b52c2020-03-17 18:42:12 -0700158 private Rect mMessagingClipRect;
Selim Cinekbf322ee2020-03-20 20:20:31 -0700159 private ObservableTextView mAppName;
Selim Cineke027da22020-03-30 19:03:13 -0700160 private ViewGroup mActions;
161 private int mConversationContentStart;
162 private int mInternalButtonPadding;
Selim Cinekbf322ee2020-03-20 20:20:31 -0700163 private boolean mAppNameGone;
164 private int mFacePileAvatarSize;
165 private int mFacePileAvatarSizeExpandedGroup;
166 private int mFacePileProtectionWidth;
167 private int mFacePileProtectionWidthExpanded;
Selim Cinek9ed6e042020-03-26 15:45:51 -0700168 private boolean mImportantConversation;
Steve Elliott239e6cb2020-03-25 14:26:42 -0400169 private TextView mUnreadBadge;
Selim Cinek4237e822020-03-31 17:22:28 -0700170 private ViewGroup mAppOps;
171 private Rect mAppOpsTouchRect = new Rect();
172 private float mMinTouchSize;
Steve Elliott936df152020-04-14 13:59:53 -0400173 private Icon mConversationIcon;
Steve Elliott3d360da2020-06-03 20:50:23 -0400174 private Icon mShortcutIcon;
Steve Elliottf7ef4ef2020-04-27 15:40:18 -0400175 private View mAppNameDivider;
Selim Cinek20d1ee22020-02-03 16:04:26 -0500176
177 public ConversationLayout(@NonNull Context context) {
178 super(context);
179 }
180
181 public ConversationLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
182 super(context, attrs);
183 }
184
185 public ConversationLayout(@NonNull Context context, @Nullable AttributeSet attrs,
186 @AttrRes int defStyleAttr) {
187 super(context, attrs, defStyleAttr);
188 }
189
190 public ConversationLayout(@NonNull Context context, @Nullable AttributeSet attrs,
191 @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
192 super(context, attrs, defStyleAttr, defStyleRes);
193 }
194
195 @Override
196 protected void onFinishInflate() {
197 super.onFinishInflate();
198 mMessagingLinearLayout = findViewById(R.id.notification_messaging);
Selim Cineke027da22020-03-30 19:03:13 -0700199 mActions = findViewById(R.id.actions);
Selim Cinek514b52c2020-03-17 18:42:12 -0700200 mImageMessageContainer = findViewById(R.id.conversation_image_message_container);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500201 // We still want to clip, but only on the top, since views can temporarily out of bounds
202 // during transitions.
203 DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
204 int size = Math.max(displayMetrics.widthPixels, displayMetrics.heightPixels);
Selim Cinek514b52c2020-03-17 18:42:12 -0700205 mMessagingClipRect = new Rect(0, 0, size, size);
206 setMessagingClippingDisabled(false);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500207 mAvatarSize = getResources().getDimensionPixelSize(R.dimen.messaging_avatar_size);
208 mTextPaint.setTextAlign(Paint.Align.CENTER);
209 mTextPaint.setAntiAlias(true);
Steve Elliott936df152020-04-14 13:59:53 -0400210 mConversationIconView = findViewById(R.id.conversation_icon);
Selim Cinekbf322ee2020-03-20 20:20:31 -0700211 mConversationIconContainer = findViewById(R.id.conversation_icon_container);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500212 mIcon = findViewById(R.id.icon);
Selim Cinek4237e822020-03-31 17:22:28 -0700213 mAppOps = findViewById(com.android.internal.R.id.app_ops);
214 mMinTouchSize = 48 * getResources().getDisplayMetrics().density;
Steve Elliott928bb162020-03-17 17:52:53 -0400215 mImportanceRingView = findViewById(R.id.conversation_icon_badge_ring);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500216 mConversationIconBadge = findViewById(R.id.conversation_icon_badge);
Selim Cinekd00d01a2020-03-23 19:00:26 -0700217 mConversationIconBadgeBg = findViewById(R.id.conversation_icon_badge_bg);
Selim Cinekf2b8aa72020-03-05 15:29:02 -0800218 mIcon.setOnVisibilityChangedListener((visibility) -> {
Selim Cinek9f2d21f2020-03-27 12:36:38 -0700219
220 // Let's hide the background directly or in an animated way
221 boolean isGone = visibility == GONE;
222 int oldVisibility = mConversationIconBadgeBg.getVisibility();
223 boolean wasGone = oldVisibility == GONE;
224 if (wasGone != isGone) {
225 // Keep the badge gone state in sync with the icon. This is necessary in cases
226 // Where the icon is being hidden externally like in group children.
227 mConversationIconBadgeBg.animate().cancel();
228 mConversationIconBadgeBg.setVisibility(visibility);
229 }
230
231 // Let's handle the importance ring which can also be be gone normally
232 oldVisibility = mImportanceRingView.getVisibility();
233 wasGone = oldVisibility == GONE;
234 visibility = !mImportantConversation ? GONE : visibility;
Steve Elliott45946032020-06-15 15:40:31 -0400235 boolean isRingGone = visibility == GONE;
236 if (wasGone != isRingGone) {
Selim Cinek9f2d21f2020-03-27 12:36:38 -0700237 // Keep the badge visibility in sync with the icon. This is necessary in cases
238 // Where the icon is being hidden externally like in group children.
239 mImportanceRingView.animate().cancel();
240 mImportanceRingView.setVisibility(visibility);
241 }
Steve Elliott45946032020-06-15 15:40:31 -0400242
243 oldVisibility = mConversationIconBadge.getVisibility();
244 wasGone = oldVisibility == GONE;
245 if (wasGone != isGone) {
246 mConversationIconBadge.animate().cancel();
247 mConversationIconBadge.setVisibility(visibility);
248 }
Selim Cinek9f2d21f2020-03-27 12:36:38 -0700249 });
250 // When the small icon is gone, hide the rest of the badge
251 mIcon.setOnForceHiddenChangedListener((forceHidden) -> {
252 animateViewForceHidden(mConversationIconBadgeBg, forceHidden);
253 animateViewForceHidden(mImportanceRingView, forceHidden);
254 });
255
256 // When the conversation icon is gone, hide the whole badge
Steve Elliott936df152020-04-14 13:59:53 -0400257 mConversationIconView.setOnForceHiddenChangedListener((forceHidden) -> {
Selim Cinek9f2d21f2020-03-27 12:36:38 -0700258 animateViewForceHidden(mConversationIconBadgeBg, forceHidden);
259 animateViewForceHidden(mImportanceRingView, forceHidden);
260 animateViewForceHidden(mIcon, forceHidden);
Selim Cinekf2b8aa72020-03-05 15:29:02 -0800261 });
Selim Cinek2f7f7b82020-03-10 15:41:13 -0700262 mConversationText = findViewById(R.id.conversation_text);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500263 mExpandButtonContainer = findViewById(R.id.expand_button_container);
Selim Cinekafc20582020-03-13 20:15:38 -0700264 mConversationHeader = findViewById(R.id.conversation_header);
265 mContentContainer = findViewById(R.id.notification_action_list_margin_target);
Selim Cinek54fe9ad2020-03-04 13:56:42 -0800266 mExpandButtonAndContentContainer = findViewById(R.id.expand_button_and_content_container);
Selim Cinek28db07e2020-04-03 17:33:24 -0700267 mExpandButtonInnerContainer = findViewById(R.id.expand_button_inner_container);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500268 mExpandButton = findViewById(R.id.expand_button);
269 mExpandButtonExpandedTopMargin = getResources().getDimensionPixelSize(
270 R.dimen.conversation_expand_button_top_margin_expanded);
Selim Cinekafc20582020-03-13 20:15:38 -0700271 mNotificationHeaderExpandedPadding = getResources().getDimensionPixelSize(
272 R.dimen.conversation_header_expanded_padding_end);
273 mContentMarginEnd = getResources().getDimensionPixelSize(
274 R.dimen.notification_content_margin_end);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500275 mBadgedSideMargins = getResources().getDimensionPixelSize(
276 R.dimen.conversation_badge_side_margin);
Selim Cinekbf322ee2020-03-20 20:20:31 -0700277 mConversationAvatarSize = getResources().getDimensionPixelSize(
278 R.dimen.conversation_avatar_size);
279 mConversationAvatarSizeExpanded = getResources().getDimensionPixelSize(
280 R.dimen.conversation_avatar_size_group_expanded);
Selim Cinekbf322ee2020-03-20 20:20:31 -0700281 mConversationIconTopPaddingExpandedGroup = getResources().getDimensionPixelSize(
282 R.dimen.conversation_icon_container_top_padding_small_avatar);
Steve Elliottf7ef4ef2020-04-27 15:40:18 -0400283 mConversationIconTopPadding = getResources().getDimensionPixelSize(
284 R.dimen.conversation_icon_container_top_padding);
285 mExpandedGroupMessagePadding = getResources().getDimensionPixelSize(
286 R.dimen.expanded_group_conversation_message_padding);
Selim Cinekbf322ee2020-03-20 20:20:31 -0700287 mExpandedGroupSideMargin = getResources().getDimensionPixelSize(
288 R.dimen.conversation_badge_side_margin_group_expanded);
289 mExpandedGroupSideMarginFacePile = getResources().getDimensionPixelSize(
290 R.dimen.conversation_badge_side_margin_group_expanded_face_pile);
Selim Cinek8baa70f2020-03-09 20:09:35 -0700291 mConversationFacePile = findViewById(R.id.conversation_face_pile);
Selim Cinekbf322ee2020-03-20 20:20:31 -0700292 mFacePileAvatarSize = getResources().getDimensionPixelSize(
293 R.dimen.conversation_face_pile_avatar_size);
294 mFacePileAvatarSizeExpandedGroup = getResources().getDimensionPixelSize(
295 R.dimen.conversation_face_pile_avatar_size_group_expanded);
296 mFacePileProtectionWidth = getResources().getDimensionPixelSize(
297 R.dimen.conversation_face_pile_protection_width);
298 mFacePileProtectionWidthExpanded = getResources().getDimensionPixelSize(
299 R.dimen.conversation_face_pile_protection_width_expanded);
Selim Cinek2f7f7b82020-03-10 15:41:13 -0700300 mFallbackChatName = getResources().getString(
301 R.string.conversation_title_fallback_one_to_one);
302 mFallbackGroupChatName = getResources().getString(
303 R.string.conversation_title_fallback_group_chat);
Steve Elliott52440e92020-03-19 15:18:58 -0400304 mAppName = findViewById(R.id.app_name_text);
Steve Elliottf7ef4ef2020-04-27 15:40:18 -0400305 mAppNameDivider = findViewById(R.id.app_name_divider);
Selim Cinekbf322ee2020-03-20 20:20:31 -0700306 mAppNameGone = mAppName.getVisibility() == GONE;
307 mAppName.setOnVisibilityChangedListener((visibility) -> {
308 onAppNameVisibilityChanged();
309 });
Steve Elliott239e6cb2020-03-25 14:26:42 -0400310 mUnreadBadge = findViewById(R.id.conversation_unread_count);
Selim Cineke027da22020-03-30 19:03:13 -0700311 mConversationContentStart = getResources().getDimensionPixelSize(
312 R.dimen.conversation_content_start);
313 mInternalButtonPadding
314 = getResources().getDimensionPixelSize(R.dimen.button_padding_horizontal_material)
315 + getResources().getDimensionPixelSize(R.dimen.button_inset_horizontal_material);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500316 }
317
Selim Cinek9f2d21f2020-03-27 12:36:38 -0700318 private void animateViewForceHidden(CachingIconView view, boolean forceHidden) {
319 boolean nowForceHidden = view.willBeForceHidden() || view.isForceHidden();
320 if (forceHidden == nowForceHidden) {
321 // We are either already forceHidden or will be
322 return;
323 }
324 view.animate().cancel();
325 view.setWillBeForceHidden(forceHidden);
326 view.animate()
327 .scaleX(forceHidden ? 0.5f : 1.0f)
328 .scaleY(forceHidden ? 0.5f : 1.0f)
329 .alpha(forceHidden ? 0.0f : 1.0f)
330 .setInterpolator(forceHidden ? ALPHA_OUT : ALPHA_IN)
331 .setDuration(160);
332 if (view.getVisibility() != VISIBLE) {
333 view.setForceHidden(forceHidden);
334 } else {
335 view.animate().withEndAction(() -> view.setForceHidden(forceHidden));
336 }
337 view.animate().start();
338 }
339
Selim Cinek20d1ee22020-02-03 16:04:26 -0500340 @RemotableViewMethod
341 public void setAvatarReplacement(Icon icon) {
342 mAvatarReplacement = icon;
343 }
344
345 @RemotableViewMethod
346 public void setNameReplacement(CharSequence nameReplacement) {
347 mNameReplacement = nameReplacement;
348 }
349
Steve Elliott8b4929d2020-06-02 13:38:04 -0400350 /** Sets this conversation as "important", adding some additional UI treatment. */
Steve Elliott928bb162020-03-17 17:52:53 -0400351 @RemotableViewMethod
352 public void setIsImportantConversation(boolean isImportantConversation) {
Steve Elliott8b4929d2020-06-02 13:38:04 -0400353 setIsImportantConversation(isImportantConversation, false);
354 }
355
356 /** @hide **/
357 public void setIsImportantConversation(boolean isImportantConversation, boolean animate) {
Selim Cinek9ed6e042020-03-26 15:45:51 -0700358 mImportantConversation = isImportantConversation;
Steve Elliott8b4929d2020-06-02 13:38:04 -0400359 mImportanceRingView.setVisibility(isImportantConversation && mIcon.getVisibility() != GONE
360 ? VISIBLE : GONE);
361
362 if (animate && isImportantConversation) {
363 GradientDrawable ring = (GradientDrawable) mImportanceRingView.getDrawable();
364 ring.mutate();
365 GradientDrawable bg = (GradientDrawable) mConversationIconBadgeBg.getDrawable();
366 bg.mutate();
367 int ringColor = getResources()
368 .getColor(R.color.conversation_important_highlight);
369 int standardThickness = getResources()
370 .getDimensionPixelSize(R.dimen.importance_ring_stroke_width);
371 int largeThickness = getResources()
372 .getDimensionPixelSize(R.dimen.importance_ring_anim_max_stroke_width);
373 int standardSize = getResources().getDimensionPixelSize(
374 R.dimen.importance_ring_size);
375 int baseSize = standardSize - standardThickness * 2;
376 int bgSize = getResources()
377 .getDimensionPixelSize(R.dimen.conversation_icon_size_badged);
378
379 ValueAnimator.AnimatorUpdateListener animatorUpdateListener = animation -> {
380 int strokeWidth = Math.round((float) animation.getAnimatedValue());
381 ring.setStroke(strokeWidth, ringColor);
382 int newSize = baseSize + strokeWidth * 2;
383 ring.setSize(newSize, newSize);
384 mImportanceRingView.invalidate();
385 };
386
387 ValueAnimator growAnimation = ValueAnimator.ofFloat(0, largeThickness);
388 growAnimation.setInterpolator(LINEAR_OUT_SLOW_IN);
389 growAnimation.setDuration(IMPORTANCE_ANIM_GROW_DURATION);
390 growAnimation.addUpdateListener(animatorUpdateListener);
391
392 ValueAnimator shrinkAnimation =
393 ValueAnimator.ofFloat(largeThickness, standardThickness);
394 shrinkAnimation.setDuration(IMPORTANCE_ANIM_SHRINK_DURATION);
395 shrinkAnimation.setStartDelay(IMPORTANCE_ANIM_SHRINK_DELAY);
396 shrinkAnimation.setInterpolator(OVERSHOOT);
397 shrinkAnimation.addUpdateListener(animatorUpdateListener);
398 shrinkAnimation.addListener(new AnimatorListenerAdapter() {
399 @Override
400 public void onAnimationStart(Animator animation) {
401 // Shrink the badge bg so that it doesn't peek behind the animation
402 bg.setSize(baseSize, baseSize);
403 mConversationIconBadgeBg.invalidate();
404 }
405
406 @Override
407 public void onAnimationEnd(Animator animation) {
408 // Reset bg back to normal size
409 bg.setSize(bgSize, bgSize);
410 mConversationIconBadgeBg.invalidate();
411 }
412 });
413
414 AnimatorSet anims = new AnimatorSet();
415 anims.playSequentially(growAnimation, shrinkAnimation);
416 anims.start();
417 }
Steve Elliott928bb162020-03-17 17:52:53 -0400418 }
419
Selim Cinek9ed6e042020-03-26 15:45:51 -0700420 public boolean isImportantConversation() {
421 return mImportantConversation;
422 }
423
Steve Elliott928bb162020-03-17 17:52:53 -0400424 /**
Selim Cinek20d1ee22020-02-03 16:04:26 -0500425 * Set this layout to show the collapsed representation.
426 *
427 * @param isCollapsed is it collapsed
428 */
429 @RemotableViewMethod
430 public void setIsCollapsed(boolean isCollapsed) {
431 mIsCollapsed = isCollapsed;
432 mMessagingLinearLayout.setMaxDisplayedLines(isCollapsed ? 1 : Integer.MAX_VALUE);
433 updateExpandButton();
Selim Cinekbf322ee2020-03-20 20:20:31 -0700434 updateContentEndPaddings();
Selim Cinek20d1ee22020-02-03 16:04:26 -0500435 }
436
437 @RemotableViewMethod
438 public void setData(Bundle extras) {
439 Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES);
440 List<Notification.MessagingStyle.Message> newMessages
441 = Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages);
442 Parcelable[] histMessages = extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES);
443 List<Notification.MessagingStyle.Message> newHistoricMessages
444 = Notification.MessagingStyle.Message.getMessagesFromBundleArray(histMessages);
445
446 // mUser now set (would be nice to avoid the side effect but WHATEVER)
447 setUser(extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON));
448
Selim Cinek20d1ee22020-02-03 16:04:26 -0500449 // Append remote input history to newMessages (again, side effect is lame but WHATEVS)
450 RemoteInputHistoryItem[] history = (RemoteInputHistoryItem[])
451 extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
452 addRemoteInputHistoryToMessages(newMessages, history);
453
454 boolean showSpinner =
455 extras.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500456 // bind it, baby
457 bind(newMessages, newHistoricMessages, showSpinner);
Steve Elliott239e6cb2020-03-25 14:26:42 -0400458
459 int unreadCount = extras.getInt(Notification.EXTRA_CONVERSATION_UNREAD_MESSAGE_COUNT);
460 setUnreadCount(unreadCount);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500461 }
462
463 @Override
464 public void setImageResolver(ImageResolver resolver) {
465 mImageResolver = resolver;
466 }
467
Steve Elliott239e6cb2020-03-25 14:26:42 -0400468 /** @hide */
469 public void setUnreadCount(int unreadCount) {
Selim Cinek94d0be82020-04-06 19:20:47 -0700470 boolean visible = mIsCollapsed && unreadCount > 1;
471 mUnreadBadge.setVisibility(visible ? VISIBLE : GONE);
472 if (visible) {
473 CharSequence text = unreadCount >= 100
474 ? getResources().getString(R.string.unread_convo_overflow, 99)
475 : String.format(Locale.getDefault(), "%d", unreadCount);
476 mUnreadBadge.setText(text);
477 mUnreadBadge.setBackgroundTintList(ColorStateList.valueOf(mLayoutColor));
478 boolean needDarkText = ColorUtils.calculateLuminance(mLayoutColor) > 0.5f;
479 mUnreadBadge.setTextColor(needDarkText ? Color.BLACK : Color.WHITE);
480 }
Steve Elliott239e6cb2020-03-25 14:26:42 -0400481 }
482
Selim Cinek20d1ee22020-02-03 16:04:26 -0500483 private void addRemoteInputHistoryToMessages(
484 List<Notification.MessagingStyle.Message> newMessages,
485 RemoteInputHistoryItem[] remoteInputHistory) {
486 if (remoteInputHistory == null || remoteInputHistory.length == 0) {
487 return;
488 }
489 for (int i = remoteInputHistory.length - 1; i >= 0; i--) {
490 RemoteInputHistoryItem historyMessage = remoteInputHistory[i];
491 Notification.MessagingStyle.Message message = new Notification.MessagingStyle.Message(
492 historyMessage.getText(), 0, (Person) null, true /* remoteHistory */);
493 if (historyMessage.getUri() != null) {
494 message.setData(historyMessage.getMimeType(), historyMessage.getUri());
495 }
496 newMessages.add(message);
497 }
498 }
499
500 private void bind(List<Notification.MessagingStyle.Message> newMessages,
501 List<Notification.MessagingStyle.Message> newHistoricMessages,
502 boolean showSpinner) {
503 // convert MessagingStyle.Message to MessagingMessage, re-using ones from a previous binding
504 // if they exist
505 List<MessagingMessage> historicMessages = createMessages(newHistoricMessages,
506 true /* isHistoric */);
507 List<MessagingMessage> messages = createMessages(newMessages, false /* isHistoric */);
508
509 // Copy our groups, before they get clobbered
510 ArrayList<MessagingGroup> oldGroups = new ArrayList<>(mGroups);
511
512 // Add our new MessagingMessages to groups
513 List<List<MessagingMessage>> groups = new ArrayList<>();
514 List<Person> senders = new ArrayList<>();
515
516 // Lets first find the groups (populate `groups` and `senders`)
517 findGroups(historicMessages, messages, groups, senders);
518
519 // Let's now create the views and reorder them accordingly
520 // side-effect: updates mGroups, mAddedGroups
521 createGroupViews(groups, senders, showSpinner);
522
523 // Let's first check which groups were removed altogether and remove them in one animation
524 removeGroups(oldGroups);
525
526 // Let's remove the remaining messages
527 mMessages.forEach(REMOVE_MESSAGE);
528 mHistoricMessages.forEach(REMOVE_MESSAGE);
529
530 mMessages = messages;
531 mHistoricMessages = historicMessages;
532
533 updateHistoricMessageVisibility();
534 updateTitleAndNamesDisplay();
535
Selim Cinek857f2792020-03-03 19:06:21 -0800536 updateConversationLayout();
Selim Cinek20d1ee22020-02-03 16:04:26 -0500537 }
538
Selim Cinek857f2792020-03-03 19:06:21 -0800539 /**
540 * Update the layout according to the data provided (i.e mIsOneToOne, expanded etc);
541 */
542 private void updateConversationLayout() {
Selim Cinek20d1ee22020-02-03 16:04:26 -0500543 // Set avatar and name
Selim Cinek2f7f7b82020-03-10 15:41:13 -0700544 CharSequence conversationText = mConversationTitle;
Steve Elliott3d360da2020-06-03 20:50:23 -0400545 mConversationIcon = mShortcutIcon;
Selim Cinek20d1ee22020-02-03 16:04:26 -0500546 if (mIsOneToOne) {
547 // Let's resolve the icon / text from the last sender
Selim Cinek857f2792020-03-03 19:06:21 -0800548 CharSequence userKey = getKey(mUser);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500549 for (int i = mGroups.size() - 1; i >= 0; i--) {
550 MessagingGroup messagingGroup = mGroups.get(i);
551 Person messageSender = messagingGroup.getSender();
Selim Cinek857f2792020-03-03 19:06:21 -0800552 if ((messageSender != null && !TextUtils.equals(userKey, getKey(messageSender)))
553 || i == 0) {
Selim Cinek2f7f7b82020-03-10 15:41:13 -0700554 if (TextUtils.isEmpty(conversationText)) {
555 // We use the sendername as header text if no conversation title is provided
556 // (This usually happens for most 1:1 conversations)
557 conversationText = messagingGroup.getSenderName();
558 }
Steve Elliott63738292020-05-29 15:56:00 -0400559 if (mConversationIcon == null) {
560 Icon avatarIcon = messagingGroup.getAvatarIcon();
561 if (avatarIcon == null) {
562 avatarIcon = createAvatarSymbol(conversationText, "", mLayoutColor);
563 }
564 mConversationIcon = avatarIcon;
Selim Cinek2f7f7b82020-03-10 15:41:13 -0700565 }
Selim Cinek20d1ee22020-02-03 16:04:26 -0500566 break;
567 }
568 }
Steve Elliott3d360da2020-06-03 20:50:23 -0400569 }
570 if (mConversationIcon == null) {
571 mConversationIcon = mLargeIcon;
572 }
573 if (mIsOneToOne || mConversationIcon != null) {
574 mConversationIconView.setVisibility(VISIBLE);
575 mConversationFacePile.setVisibility(GONE);
576 mConversationIconView.setImageIcon(mConversationIcon);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500577 } else {
Steve Elliott3d360da2020-06-03 20:50:23 -0400578 mConversationIconView.setVisibility(GONE);
579 // This will also inflate it!
580 mConversationFacePile.setVisibility(VISIBLE);
581 // rebind the value to the inflated view instead of the stub
582 mConversationFacePile = findViewById(R.id.conversation_face_pile);
583 bindFacePile();
Selim Cinek20d1ee22020-02-03 16:04:26 -0500584 }
Selim Cinek2f7f7b82020-03-10 15:41:13 -0700585 if (TextUtils.isEmpty(conversationText)) {
586 conversationText = mIsOneToOne ? mFallbackChatName : mFallbackGroupChatName;
587 }
588 mConversationText.setText(conversationText);
Selim Cinek857f2792020-03-03 19:06:21 -0800589 // Update if the groups can hide the sender if they are first (applies to 1:1 conversations)
590 // This needs to happen after all of the above o update all of the groups
591 for (int i = mGroups.size() - 1; i >= 0; i--) {
592 MessagingGroup messagingGroup = mGroups.get(i);
593 CharSequence messageSender = messagingGroup.getSenderName();
594 boolean canHide = mIsOneToOne
Selim Cinek2f7f7b82020-03-10 15:41:13 -0700595 && TextUtils.equals(conversationText, messageSender);
Selim Cinek857f2792020-03-03 19:06:21 -0800596 messagingGroup.setCanHideSenderIfFirst(canHide);
597 }
Selim Cinekbf322ee2020-03-20 20:20:31 -0700598 updateAppName();
Selim Cinek857f2792020-03-03 19:06:21 -0800599 updateIconPositionAndSize();
Selim Cinek514b52c2020-03-17 18:42:12 -0700600 updateImageMessages();
Selim Cinekbf322ee2020-03-20 20:20:31 -0700601 updatePaddingsBasedOnContentAvailability();
Selim Cineke027da22020-03-30 19:03:13 -0700602 updateActionListPadding();
Steve Elliottf7ef4ef2020-04-27 15:40:18 -0400603 updateAppNameDividerVisibility();
Selim Cineke027da22020-03-30 19:03:13 -0700604 }
605
606 private void updateActionListPadding() {
607 if (mActions == null) {
608 return;
609 }
610 View firstAction = mActions.getChildAt(0);
611 if (firstAction != null) {
612 // Let's visually position the first action where the content starts
613 int paddingStart = mConversationContentStart;
614
615 MarginLayoutParams layoutParams = (MarginLayoutParams) firstAction.getLayoutParams();
616 paddingStart -= layoutParams.getMarginStart();
617 paddingStart -= mInternalButtonPadding;
618
619 mActions.setPaddingRelative(paddingStart,
620 mActions.getPaddingTop(),
621 mActions.getPaddingEnd(),
622 mActions.getPaddingBottom());
623 }
Selim Cinek514b52c2020-03-17 18:42:12 -0700624 }
625
626 private void updateImageMessages() {
Selim Cineka959fe42020-04-06 18:16:40 -0700627 View newMessage = null;
628 if (mIsCollapsed && mGroups.size() > 0) {
Selim Cinek514b52c2020-03-17 18:42:12 -0700629
Selim Cineka959fe42020-04-06 18:16:40 -0700630 // When collapsed, we're displaying the image message in a dedicated container
631 // on the right of the layout instead of inline. Let's add the isolated image there
632 MessagingGroup messagingGroup = mGroups.get(mGroups.size() -1);
633 MessagingImageMessage isolatedMessage = messagingGroup.getIsolatedMessage();
634 if (isolatedMessage != null) {
635 newMessage = isolatedMessage.getView();
Selim Cinek514b52c2020-03-17 18:42:12 -0700636 }
637 }
638 // Remove all messages that don't belong into the image layout
Selim Cineka959fe42020-04-06 18:16:40 -0700639 View previousMessage = mImageMessageContainer.getChildAt(0);
640 if (previousMessage != newMessage) {
641 mImageMessageContainer.removeView(previousMessage);
642 if (newMessage != null) {
643 mImageMessageContainer.addView(newMessage);
Selim Cinek514b52c2020-03-17 18:42:12 -0700644 }
645 }
Selim Cineka959fe42020-04-06 18:16:40 -0700646 mImageMessageContainer.setVisibility(newMessage != null ? VISIBLE : GONE);
Selim Cinek857f2792020-03-03 19:06:21 -0800647 }
648
Steve Elliott936df152020-04-14 13:59:53 -0400649 public void bindFacePile(ImageView bottomBackground, ImageView bottomView, ImageView topView) {
Selim Cinek8baa70f2020-03-09 20:09:35 -0700650 applyNotificationBackgroundColor(bottomBackground);
Selim Cinek8baa70f2020-03-09 20:09:35 -0700651 // Let's find the two last conversations:
652 Icon secondLastIcon = null;
653 CharSequence lastKey = null;
654 Icon lastIcon = null;
655 CharSequence userKey = getKey(mUser);
656 for (int i = mGroups.size() - 1; i >= 0; i--) {
657 MessagingGroup messagingGroup = mGroups.get(i);
658 Person messageSender = messagingGroup.getSender();
659 boolean notUser = messageSender != null
660 && !TextUtils.equals(userKey, getKey(messageSender));
661 boolean notIncluded = messageSender != null
662 && !TextUtils.equals(lastKey, getKey(messageSender));
663 if ((notUser && notIncluded)
664 || (i == 0 && lastKey == null)) {
665 if (lastIcon == null) {
666 lastIcon = messagingGroup.getAvatarIcon();
667 lastKey = getKey(messageSender);
668 } else {
669 secondLastIcon = messagingGroup.getAvatarIcon();
670 break;
671 }
672 }
673 }
674 if (lastIcon == null) {
675 lastIcon = createAvatarSymbol(" ", "", mLayoutColor);
676 }
677 bottomView.setImageIcon(lastIcon);
678 if (secondLastIcon == null) {
679 secondLastIcon = createAvatarSymbol("", "", mLayoutColor);
680 }
681 topView.setImageIcon(secondLastIcon);
Steve Elliott936df152020-04-14 13:59:53 -0400682 }
683
684 private void bindFacePile() {
685 ImageView bottomBackground = mConversationFacePile.findViewById(
686 R.id.conversation_face_pile_bottom_background);
687 ImageView bottomView = mConversationFacePile.findViewById(
688 R.id.conversation_face_pile_bottom);
689 ImageView topView = mConversationFacePile.findViewById(
690 R.id.conversation_face_pile_top);
691
692 bindFacePile(bottomBackground, bottomView, topView);
Selim Cinekbf322ee2020-03-20 20:20:31 -0700693
694 int conversationAvatarSize;
695 int facepileAvatarSize;
696 int facePileBackgroundSize;
697 if (mIsCollapsed) {
698 conversationAvatarSize = mConversationAvatarSize;
699 facepileAvatarSize = mFacePileAvatarSize;
700 facePileBackgroundSize = facepileAvatarSize + 2 * mFacePileProtectionWidth;
701 } else {
702 conversationAvatarSize = mConversationAvatarSizeExpanded;
703 facepileAvatarSize = mFacePileAvatarSizeExpandedGroup;
704 facePileBackgroundSize = facepileAvatarSize + 2 * mFacePileProtectionWidthExpanded;
705 }
Steve Elliott936df152020-04-14 13:59:53 -0400706 LayoutParams layoutParams = (LayoutParams) mConversationIconView.getLayoutParams();
Selim Cinekbf322ee2020-03-20 20:20:31 -0700707 layoutParams.width = conversationAvatarSize;
708 layoutParams.height = conversationAvatarSize;
709 mConversationFacePile.setLayoutParams(layoutParams);
710
711 layoutParams = (LayoutParams) bottomView.getLayoutParams();
712 layoutParams.width = facepileAvatarSize;
713 layoutParams.height = facepileAvatarSize;
714 bottomView.setLayoutParams(layoutParams);
715
716 layoutParams = (LayoutParams) topView.getLayoutParams();
717 layoutParams.width = facepileAvatarSize;
718 layoutParams.height = facepileAvatarSize;
719 topView.setLayoutParams(layoutParams);
720
721 layoutParams = (LayoutParams) bottomBackground.getLayoutParams();
722 layoutParams.width = facePileBackgroundSize;
723 layoutParams.height = facePileBackgroundSize;
724 bottomBackground.setLayoutParams(layoutParams);
Selim Cinek8baa70f2020-03-09 20:09:35 -0700725 }
726
Steve Elliott52440e92020-03-19 15:18:58 -0400727 private void updateAppName() {
728 mAppName.setVisibility(mIsCollapsed ? GONE : VISIBLE);
729 }
730
Selim Cinekcef53f32020-03-19 19:31:25 -0700731 public boolean shouldHideAppName() {
732 return mIsCollapsed;
733 }
734
Selim Cinek857f2792020-03-03 19:06:21 -0800735 /**
736 * update the icon position and sizing
737 */
738 private void updateIconPositionAndSize() {
Selim Cinekbf322ee2020-03-20 20:20:31 -0700739 int sidemargin;
740 int conversationAvatarSize;
Selim Cinek20d1ee22020-02-03 16:04:26 -0500741 if (mIsOneToOne || mIsCollapsed) {
Selim Cinekbf322ee2020-03-20 20:20:31 -0700742 sidemargin = mBadgedSideMargins;
743 conversationAvatarSize = mConversationAvatarSize;
Selim Cinek20d1ee22020-02-03 16:04:26 -0500744 } else {
Selim Cinekbf322ee2020-03-20 20:20:31 -0700745 sidemargin = mConversationFacePile.getVisibility() == VISIBLE
746 ? mExpandedGroupSideMarginFacePile
747 : mExpandedGroupSideMargin;
748 conversationAvatarSize = mConversationAvatarSizeExpanded;
Selim Cinek20d1ee22020-02-03 16:04:26 -0500749 }
Selim Cinek857f2792020-03-03 19:06:21 -0800750 LayoutParams layoutParams =
Selim Cinek20d1ee22020-02-03 16:04:26 -0500751 (LayoutParams) mConversationIconBadge.getLayoutParams();
Selim Cinekbf322ee2020-03-20 20:20:31 -0700752 layoutParams.topMargin = sidemargin;
753 layoutParams.setMarginStart(sidemargin);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500754 mConversationIconBadge.setLayoutParams(layoutParams);
Selim Cinekbf322ee2020-03-20 20:20:31 -0700755
Steve Elliott936df152020-04-14 13:59:53 -0400756 if (mConversationIconView.getVisibility() == VISIBLE) {
757 layoutParams = (LayoutParams) mConversationIconView.getLayoutParams();
Selim Cinekbf322ee2020-03-20 20:20:31 -0700758 layoutParams.width = conversationAvatarSize;
759 layoutParams.height = conversationAvatarSize;
Steve Elliott936df152020-04-14 13:59:53 -0400760 mConversationIconView.setLayoutParams(layoutParams);
Selim Cinekbf322ee2020-03-20 20:20:31 -0700761 }
762 }
763
764 private void updatePaddingsBasedOnContentAvailability() {
Steve Elliottf7ef4ef2020-04-27 15:40:18 -0400765 int messagingPadding = mIsOneToOne || mIsCollapsed
766 ? 0
767 // Add some extra padding to the messages, since otherwise it will overlap with the
768 // group
769 : mExpandedGroupMessagePadding;
Selim Cinekbf322ee2020-03-20 20:20:31 -0700770
Steve Elliott282498a2020-06-10 14:02:09 -0400771 int iconPadding = mIsOneToOne || mIsCollapsed
772 ? mConversationIconTopPadding
773 : mConversationIconTopPaddingExpandedGroup;
774
Selim Cinekbf322ee2020-03-20 20:20:31 -0700775 mConversationIconContainer.setPaddingRelative(
776 mConversationIconContainer.getPaddingStart(),
Steve Elliott282498a2020-06-10 14:02:09 -0400777 iconPadding,
Selim Cinekbf322ee2020-03-20 20:20:31 -0700778 mConversationIconContainer.getPaddingEnd(),
779 mConversationIconContainer.getPaddingBottom());
780
781 mMessagingLinearLayout.setPaddingRelative(
782 mMessagingLinearLayout.getPaddingStart(),
783 messagingPadding,
784 mMessagingLinearLayout.getPaddingEnd(),
785 mMessagingLinearLayout.getPaddingBottom());
Selim Cinek20d1ee22020-02-03 16:04:26 -0500786 }
787
788 @RemotableViewMethod
789 public void setLargeIcon(Icon largeIcon) {
790 mLargeIcon = largeIcon;
791 }
792
Steve Elliott63738292020-05-29 15:56:00 -0400793 @RemotableViewMethod
Steve Elliott86bc69a2020-06-10 11:35:01 -0400794 public void setShortcutIcon(Icon shortcutIcon) {
795 mShortcutIcon = shortcutIcon;
Steve Elliott63738292020-05-29 15:56:00 -0400796 }
797
Selim Cinek2f7f7b82020-03-10 15:41:13 -0700798 /**
799 * Sets the conversation title of this conversation.
800 *
801 * @param conversationTitle the conversation title
802 */
803 @RemotableViewMethod
804 public void setConversationTitle(CharSequence conversationTitle) {
Steve Elliott242057d2020-06-10 15:15:19 -0400805 // Remove formatting from the title.
806 mConversationTitle = conversationTitle != null ? conversationTitle.toString() : null;
Selim Cinek2f7f7b82020-03-10 15:41:13 -0700807 }
808
Steve Elliott936df152020-04-14 13:59:53 -0400809 public CharSequence getConversationTitle() {
810 return mConversationText.getText();
811 }
812
Selim Cinek20d1ee22020-02-03 16:04:26 -0500813 private void removeGroups(ArrayList<MessagingGroup> oldGroups) {
814 int size = oldGroups.size();
815 for (int i = 0; i < size; i++) {
816 MessagingGroup group = oldGroups.get(i);
817 if (!mGroups.contains(group)) {
818 List<MessagingMessage> messages = group.getMessages();
819 Runnable endRunnable = () -> {
820 mMessagingLinearLayout.removeTransientView(group);
821 group.recycle();
822 };
823
824 boolean wasShown = group.isShown();
825 mMessagingLinearLayout.removeView(group);
826 if (wasShown && !MessagingLinearLayout.isGone(group)) {
827 mMessagingLinearLayout.addTransientView(group, 0);
828 group.removeGroupAnimated(endRunnable);
829 } else {
830 endRunnable.run();
831 }
832 mMessages.removeAll(messages);
833 mHistoricMessages.removeAll(messages);
834 }
835 }
836 }
837
838 private void updateTitleAndNamesDisplay() {
839 ArrayMap<CharSequence, String> uniqueNames = new ArrayMap<>();
840 ArrayMap<Character, CharSequence> uniqueCharacters = new ArrayMap<>();
841 for (int i = 0; i < mGroups.size(); i++) {
842 MessagingGroup group = mGroups.get(i);
843 CharSequence senderName = group.getSenderName();
844 if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)) {
845 continue;
846 }
847 if (!uniqueNames.containsKey(senderName)) {
848 // Only use visible characters to get uniqueNames
849 String pureSenderName = IGNORABLE_CHAR_PATTERN
850 .matcher(senderName).replaceAll("" /* replacement */);
851 char c = pureSenderName.charAt(0);
852 if (uniqueCharacters.containsKey(c)) {
853 // this character was already used, lets make it more unique. We first need to
854 // resolve the existing character if it exists
855 CharSequence existingName = uniqueCharacters.get(c);
856 if (existingName != null) {
857 uniqueNames.put(existingName, findNameSplit((String) existingName));
858 uniqueCharacters.put(c, null);
859 }
860 uniqueNames.put(senderName, findNameSplit((String) senderName));
861 } else {
862 uniqueNames.put(senderName, Character.toString(c));
863 uniqueCharacters.put(c, pureSenderName);
864 }
865 }
866 }
867
868 // Now that we have the correct symbols, let's look what we have cached
869 ArrayMap<CharSequence, Icon> cachedAvatars = new ArrayMap<>();
870 for (int i = 0; i < mGroups.size(); i++) {
871 // Let's now set the avatars
872 MessagingGroup group = mGroups.get(i);
873 boolean isOwnMessage = group.getSender() == mUser;
874 CharSequence senderName = group.getSenderName();
875 if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)
876 || (mIsOneToOne && mAvatarReplacement != null && !isOwnMessage)) {
877 continue;
878 }
879 String symbol = uniqueNames.get(senderName);
880 Icon cachedIcon = group.getAvatarSymbolIfMatching(senderName,
881 symbol, mLayoutColor);
882 if (cachedIcon != null) {
883 cachedAvatars.put(senderName, cachedIcon);
884 }
885 }
886
887 for (int i = 0; i < mGroups.size(); i++) {
888 // Let's now set the avatars
889 MessagingGroup group = mGroups.get(i);
890 CharSequence senderName = group.getSenderName();
891 if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)) {
892 continue;
893 }
894 if (mIsOneToOne && mAvatarReplacement != null && group.getSender() != mUser) {
895 group.setAvatar(mAvatarReplacement);
896 } else {
897 Icon cachedIcon = cachedAvatars.get(senderName);
898 if (cachedIcon == null) {
899 cachedIcon = createAvatarSymbol(senderName, uniqueNames.get(senderName),
900 mLayoutColor);
901 cachedAvatars.put(senderName, cachedIcon);
902 }
903 group.setCreatedAvatar(cachedIcon, senderName, uniqueNames.get(senderName),
904 mLayoutColor);
905 }
906 }
907 }
908
909 private Icon createAvatarSymbol(CharSequence senderName, String symbol, int layoutColor) {
910 if (symbol.isEmpty() || TextUtils.isDigitsOnly(symbol) ||
911 SPECIAL_CHAR_PATTERN.matcher(symbol).find()) {
912 Icon avatarIcon = Icon.createWithResource(getContext(),
913 R.drawable.messaging_user);
914 avatarIcon.setTint(findColor(senderName, layoutColor));
915 return avatarIcon;
916 } else {
917 Bitmap bitmap = Bitmap.createBitmap(mAvatarSize, mAvatarSize, Bitmap.Config.ARGB_8888);
918 Canvas canvas = new Canvas(bitmap);
919 float radius = mAvatarSize / 2.0f;
920 int color = findColor(senderName, layoutColor);
921 mPaint.setColor(color);
922 canvas.drawCircle(radius, radius, radius, mPaint);
923 boolean needDarkText = ColorUtils.calculateLuminance(color) > 0.5f;
924 mTextPaint.setColor(needDarkText ? Color.BLACK : Color.WHITE);
925 mTextPaint.setTextSize(symbol.length() == 1 ? mAvatarSize * 0.5f : mAvatarSize * 0.3f);
926 int yPos = (int) (radius - ((mTextPaint.descent() + mTextPaint.ascent()) / 2));
927 canvas.drawText(symbol, radius, yPos, mTextPaint);
928 return Icon.createWithBitmap(bitmap);
929 }
930 }
931
932 private int findColor(CharSequence senderName, int layoutColor) {
933 double luminance = ContrastColorUtil.calculateLuminance(layoutColor);
934 float shift = Math.abs(senderName.hashCode()) % 5 / 4.0f - 0.5f;
935
936 // we need to offset the range if the luminance is too close to the borders
937 shift += Math.max(COLOR_SHIFT_AMOUNT / 2.0f / 100 - luminance, 0);
938 shift -= Math.max(COLOR_SHIFT_AMOUNT / 2.0f / 100 - (1.0f - luminance), 0);
939 return ContrastColorUtil.getShiftedColor(layoutColor,
940 (int) (shift * COLOR_SHIFT_AMOUNT));
941 }
942
943 private String findNameSplit(String existingName) {
944 String[] split = existingName.split(" ");
945 if (split.length > 1) {
946 return Character.toString(split[0].charAt(0))
947 + Character.toString(split[1].charAt(0));
948 }
949 return existingName.substring(0, 1);
950 }
951
952 @RemotableViewMethod
953 public void setLayoutColor(int color) {
954 mLayoutColor = color;
955 }
956
957 @RemotableViewMethod
958 public void setIsOneToOne(boolean oneToOne) {
959 mIsOneToOne = oneToOne;
960 }
961
962 @RemotableViewMethod
963 public void setSenderTextColor(int color) {
964 mSenderTextColor = color;
Selim Cinek4237e822020-03-31 17:22:28 -0700965 mConversationText.setTextColor(color);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500966 }
967
Selim Cineke9714eb2020-03-09 11:21:49 -0700968 /**
969 * @param color the color of the notification background
970 */
971 @RemotableViewMethod
972 public void setNotificationBackgroundColor(int color) {
Selim Cinek8baa70f2020-03-09 20:09:35 -0700973 mNotificationBackgroundColor = color;
Selim Cinekd00d01a2020-03-23 19:00:26 -0700974 applyNotificationBackgroundColor(mConversationIconBadgeBg);
Selim Cinek8baa70f2020-03-09 20:09:35 -0700975 }
976
Selim Cinekd00d01a2020-03-23 19:00:26 -0700977 private void applyNotificationBackgroundColor(ImageView view) {
978 view.setImageTintList(ColorStateList.valueOf(mNotificationBackgroundColor));
Selim Cineke9714eb2020-03-09 11:21:49 -0700979 }
980
Selim Cinek20d1ee22020-02-03 16:04:26 -0500981 @RemotableViewMethod
982 public void setMessageTextColor(int color) {
983 mMessageTextColor = color;
984 }
985
986 private void setUser(Person user) {
987 mUser = user;
988 if (mUser.getIcon() == null) {
989 Icon userIcon = Icon.createWithResource(getContext(),
990 R.drawable.messaging_user);
991 userIcon.setTint(mLayoutColor);
992 mUser = mUser.toBuilder().setIcon(userIcon).build();
993 }
994 }
995
996 private void createGroupViews(List<List<MessagingMessage>> groups,
997 List<Person> senders, boolean showSpinner) {
998 mGroups.clear();
999 for (int groupIndex = 0; groupIndex < groups.size(); groupIndex++) {
1000 List<MessagingMessage> group = groups.get(groupIndex);
1001 MessagingGroup newGroup = null;
1002 // we'll just take the first group that exists or create one there is none
1003 for (int messageIndex = group.size() - 1; messageIndex >= 0; messageIndex--) {
1004 MessagingMessage message = group.get(messageIndex);
1005 newGroup = message.getGroup();
1006 if (newGroup != null) {
1007 break;
1008 }
1009 }
1010 // Create a new group, adding it to the linear layout as well
1011 if (newGroup == null) {
1012 newGroup = MessagingGroup.createGroup(mMessagingLinearLayout);
1013 mAddedGroups.add(newGroup);
1014 }
Selim Cinek514b52c2020-03-17 18:42:12 -07001015 newGroup.setImageDisplayLocation(mIsCollapsed
1016 ? IMAGE_DISPLAY_LOCATION_EXTERNAL
1017 : IMAGE_DISPLAY_LOCATION_INLINE);
Selim Cineka91778a32020-03-13 17:30:34 -07001018 newGroup.setIsInConversation(true);
Selim Cinek20d1ee22020-02-03 16:04:26 -05001019 newGroup.setLayoutColor(mLayoutColor);
1020 newGroup.setTextColors(mSenderTextColor, mMessageTextColor);
1021 Person sender = senders.get(groupIndex);
1022 CharSequence nameOverride = null;
1023 if (sender != mUser && mNameReplacement != null) {
1024 nameOverride = mNameReplacement;
1025 }
1026 newGroup.setShowingAvatar(!mIsOneToOne && !mIsCollapsed);
1027 newGroup.setSingleLine(mIsCollapsed);
1028 newGroup.setSender(sender, nameOverride);
1029 newGroup.setSending(groupIndex == (groups.size() - 1) && showSpinner);
1030 mGroups.add(newGroup);
1031
1032 // Reposition to the correct place (if we're re-using a group)
1033 if (mMessagingLinearLayout.indexOfChild(newGroup) != groupIndex) {
1034 mMessagingLinearLayout.removeView(newGroup);
1035 mMessagingLinearLayout.addView(newGroup, groupIndex);
1036 }
1037 newGroup.setMessages(group);
1038 }
1039 }
1040
1041 private void findGroups(List<MessagingMessage> historicMessages,
1042 List<MessagingMessage> messages, List<List<MessagingMessage>> groups,
1043 List<Person> senders) {
1044 CharSequence currentSenderKey = null;
1045 List<MessagingMessage> currentGroup = null;
1046 int histSize = historicMessages.size();
1047 for (int i = 0; i < histSize + messages.size(); i++) {
1048 MessagingMessage message;
1049 if (i < histSize) {
1050 message = historicMessages.get(i);
1051 } else {
1052 message = messages.get(i - histSize);
1053 }
1054 boolean isNewGroup = currentGroup == null;
1055 Person sender = message.getMessage().getSenderPerson();
Selim Cinek857f2792020-03-03 19:06:21 -08001056 CharSequence key = getKey(sender);
Selim Cinek20d1ee22020-02-03 16:04:26 -05001057 isNewGroup |= !TextUtils.equals(key, currentSenderKey);
1058 if (isNewGroup) {
1059 currentGroup = new ArrayList<>();
1060 groups.add(currentGroup);
1061 if (sender == null) {
1062 sender = mUser;
Steve Elliott242057d2020-06-10 15:15:19 -04001063 } else {
1064 // Remove all formatting from the sender name
1065 sender = sender.toBuilder().setName(Objects.toString(sender.getName())).build();
Selim Cinek20d1ee22020-02-03 16:04:26 -05001066 }
1067 senders.add(sender);
1068 currentSenderKey = key;
1069 }
1070 currentGroup.add(message);
1071 }
1072 }
1073
Selim Cinek857f2792020-03-03 19:06:21 -08001074 private CharSequence getKey(Person person) {
1075 return person == null ? null : person.getKey() == null ? person.getName() : person.getKey();
1076 }
1077
Selim Cinek20d1ee22020-02-03 16:04:26 -05001078 /**
1079 * Creates new messages, reusing existing ones if they are available.
1080 *
1081 * @param newMessages the messages to parse.
1082 */
1083 private List<MessagingMessage> createMessages(
1084 List<Notification.MessagingStyle.Message> newMessages, boolean historic) {
1085 List<MessagingMessage> result = new ArrayList<>();
1086 for (int i = 0; i < newMessages.size(); i++) {
1087 Notification.MessagingStyle.Message m = newMessages.get(i);
1088 MessagingMessage message = findAndRemoveMatchingMessage(m);
1089 if (message == null) {
1090 message = MessagingMessage.createMessage(this, m, mImageResolver);
1091 }
1092 message.setIsHistoric(historic);
1093 result.add(message);
1094 }
1095 return result;
1096 }
1097
1098 private MessagingMessage findAndRemoveMatchingMessage(Notification.MessagingStyle.Message m) {
1099 for (int i = 0; i < mMessages.size(); i++) {
1100 MessagingMessage existing = mMessages.get(i);
1101 if (existing.sameAs(m)) {
1102 mMessages.remove(i);
1103 return existing;
1104 }
1105 }
1106 for (int i = 0; i < mHistoricMessages.size(); i++) {
1107 MessagingMessage existing = mHistoricMessages.get(i);
1108 if (existing.sameAs(m)) {
1109 mHistoricMessages.remove(i);
1110 return existing;
1111 }
1112 }
1113 return null;
1114 }
1115
1116 public void showHistoricMessages(boolean show) {
1117 mShowHistoricMessages = show;
1118 updateHistoricMessageVisibility();
1119 }
1120
1121 private void updateHistoricMessageVisibility() {
1122 int numHistoric = mHistoricMessages.size();
1123 for (int i = 0; i < numHistoric; i++) {
1124 MessagingMessage existing = mHistoricMessages.get(i);
1125 existing.setVisibility(mShowHistoricMessages ? VISIBLE : GONE);
1126 }
1127 int numGroups = mGroups.size();
1128 for (int i = 0; i < numGroups; i++) {
1129 MessagingGroup group = mGroups.get(i);
1130 int visibleChildren = 0;
1131 List<MessagingMessage> messages = group.getMessages();
1132 int numGroupMessages = messages.size();
1133 for (int j = 0; j < numGroupMessages; j++) {
1134 MessagingMessage message = messages.get(j);
1135 if (message.getVisibility() != GONE) {
1136 visibleChildren++;
1137 }
1138 }
1139 if (visibleChildren > 0 && group.getVisibility() == GONE) {
1140 group.setVisibility(VISIBLE);
1141 } else if (visibleChildren == 0 && group.getVisibility() != GONE) {
1142 group.setVisibility(GONE);
1143 }
1144 }
1145 }
1146
1147 @Override
1148 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
1149 super.onLayout(changed, left, top, right, bottom);
1150 if (!mAddedGroups.isEmpty()) {
1151 getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
1152 @Override
1153 public boolean onPreDraw() {
1154 for (MessagingGroup group : mAddedGroups) {
1155 if (!group.isShown()) {
1156 continue;
1157 }
1158 MessagingPropertyAnimator.fadeIn(group.getAvatar());
1159 MessagingPropertyAnimator.fadeIn(group.getSenderView());
1160 MessagingPropertyAnimator.startLocalTranslationFrom(group,
1161 group.getHeight(), LINEAR_OUT_SLOW_IN);
1162 }
1163 mAddedGroups.clear();
1164 getViewTreeObserver().removeOnPreDrawListener(this);
1165 return true;
1166 }
1167 });
1168 }
Selim Cinek4237e822020-03-31 17:22:28 -07001169 if (mAppOps.getWidth() > 0) {
1170
1171 // Let's increase the touch size of the app ops view if it's here
1172 mAppOpsTouchRect.set(
1173 mAppOps.getLeft(),
1174 mAppOps.getTop(),
1175 mAppOps.getRight(),
1176 mAppOps.getBottom());
1177 for (int i = 0; i < mAppOps.getChildCount(); i++) {
1178 View child = mAppOps.getChildAt(i);
1179 if (child.getVisibility() == GONE) {
1180 continue;
1181 }
1182 // Make sure each child has at least a minTouchSize touch target around it
1183 float childTouchLeft = child.getLeft() + child.getWidth() / 2.0f
1184 - mMinTouchSize / 2.0f;
1185 float childTouchRight = childTouchLeft + mMinTouchSize;
1186 mAppOpsTouchRect.left = (int) Math.min(mAppOpsTouchRect.left,
1187 mAppOps.getLeft() + childTouchLeft);
1188 mAppOpsTouchRect.right = (int) Math.max(mAppOpsTouchRect.right,
1189 mAppOps.getLeft() + childTouchRight);
1190 }
1191
1192 // Increase the height
1193 int heightIncrease = 0;
1194 if (mAppOpsTouchRect.height() < mMinTouchSize) {
1195 heightIncrease = (int) Math.ceil((mMinTouchSize - mAppOpsTouchRect.height())
1196 / 2.0f);
1197 }
1198 mAppOpsTouchRect.inset(0, -heightIncrease);
1199
1200 // Let's adjust the hitrect since app ops isn't a direct child
1201 ViewGroup viewGroup = (ViewGroup) mAppOps.getParent();
1202 while (viewGroup != this) {
1203 mAppOpsTouchRect.offset(viewGroup.getLeft(), viewGroup.getTop());
1204 viewGroup = (ViewGroup) viewGroup.getParent();
1205 }
1206 //
1207 // Extend the size of the app opps to be at least 48dp
1208 setTouchDelegate(new TouchDelegate(mAppOpsTouchRect, mAppOps));
1209 }
Selim Cinek20d1ee22020-02-03 16:04:26 -05001210 }
1211
1212 public MessagingLinearLayout getMessagingLinearLayout() {
1213 return mMessagingLinearLayout;
1214 }
1215
Selim Cinek514b52c2020-03-17 18:42:12 -07001216 public @NonNull ViewGroup getImageMessageContainer() {
1217 return mImageMessageContainer;
1218 }
1219
Selim Cinek20d1ee22020-02-03 16:04:26 -05001220 public ArrayList<MessagingGroup> getMessagingGroups() {
1221 return mGroups;
1222 }
1223
1224 private void updateExpandButton() {
1225 int drawableId;
1226 int contentDescriptionId;
1227 int gravity;
1228 int topMargin = 0;
Selim Cinek54fe9ad2020-03-04 13:56:42 -08001229 ViewGroup newContainer;
Selim Cinek20d1ee22020-02-03 16:04:26 -05001230 if (mIsCollapsed) {
1231 drawableId = R.drawable.ic_expand_notification;
1232 contentDescriptionId = R.string.expand_button_content_description_collapsed;
1233 gravity = Gravity.CENTER;
Selim Cinek54fe9ad2020-03-04 13:56:42 -08001234 newContainer = mExpandButtonAndContentContainer;
Selim Cinek20d1ee22020-02-03 16:04:26 -05001235 } else {
1236 drawableId = R.drawable.ic_collapse_notification;
1237 contentDescriptionId = R.string.expand_button_content_description_expanded;
1238 gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
1239 topMargin = mExpandButtonExpandedTopMargin;
Selim Cinek54fe9ad2020-03-04 13:56:42 -08001240 newContainer = this;
Selim Cinek20d1ee22020-02-03 16:04:26 -05001241 }
1242 mExpandButton.setImageDrawable(getContext().getDrawable(drawableId));
1243 mExpandButton.setColorFilter(mExpandButton.getOriginalNotificationColor());
1244
Selim Cinek54fe9ad2020-03-04 13:56:42 -08001245 // We need to make sure that the expand button is in the linearlayout pushing over the
1246 // content when collapsed, but allows the content to flow under it when expanded.
1247 if (newContainer != mExpandButtonContainer.getParent()) {
1248 ((ViewGroup) mExpandButtonContainer.getParent()).removeView(mExpandButtonContainer);
1249 newContainer.addView(mExpandButtonContainer);
Selim Cinek54fe9ad2020-03-04 13:56:42 -08001250 }
1251
Selim Cinek20d1ee22020-02-03 16:04:26 -05001252 // update if the expand button is centered
Selim Cinek514b52c2020-03-17 18:42:12 -07001253 LinearLayout.LayoutParams layoutParams =
1254 (LinearLayout.LayoutParams) mExpandButton.getLayoutParams();
Selim Cinek20d1ee22020-02-03 16:04:26 -05001255 layoutParams.gravity = gravity;
1256 layoutParams.topMargin = topMargin;
1257 mExpandButton.setLayoutParams(layoutParams);
1258
Selim Cinek28db07e2020-04-03 17:33:24 -07001259 mExpandButtonInnerContainer.setContentDescription(mContext.getText(contentDescriptionId));
Selim Cinekafc20582020-03-13 20:15:38 -07001260 }
Selim Cinek54fe9ad2020-03-04 13:56:42 -08001261
Selim Cinekbf322ee2020-03-20 20:20:31 -07001262 private void updateContentEndPaddings() {
Selim Cinekafc20582020-03-13 20:15:38 -07001263 // Let's make sure the conversation header can't run into the expand button when we're
1264 // collapsed and update the paddings of the content
1265 int headerPaddingEnd;
1266 int contentPaddingEnd;
1267 if (!mExpandable) {
1268 headerPaddingEnd = 0;
1269 contentPaddingEnd = mContentMarginEnd;
1270 } else if (mIsCollapsed) {
1271 headerPaddingEnd = 0;
1272 contentPaddingEnd = 0;
1273 } else {
1274 headerPaddingEnd = mNotificationHeaderExpandedPadding;
1275 contentPaddingEnd = mContentMarginEnd;
1276 }
1277 mConversationHeader.setPaddingRelative(
1278 mConversationHeader.getPaddingStart(),
1279 mConversationHeader.getPaddingTop(),
1280 headerPaddingEnd,
1281 mConversationHeader.getPaddingBottom());
1282
1283 mContentContainer.setPaddingRelative(
1284 mContentContainer.getPaddingStart(),
1285 mContentContainer.getPaddingTop(),
1286 contentPaddingEnd,
1287 mContentContainer.getPaddingBottom());
Selim Cinek20d1ee22020-02-03 16:04:26 -05001288 }
1289
Selim Cinekbf322ee2020-03-20 20:20:31 -07001290 private void onAppNameVisibilityChanged() {
1291 boolean appNameGone = mAppName.getVisibility() == GONE;
1292 if (appNameGone != mAppNameGone) {
1293 mAppNameGone = appNameGone;
Steve Elliottf7ef4ef2020-04-27 15:40:18 -04001294 updateAppNameDividerVisibility();
Selim Cinekbf322ee2020-03-20 20:20:31 -07001295 }
1296 }
1297
Steve Elliottf7ef4ef2020-04-27 15:40:18 -04001298 private void updateAppNameDividerVisibility() {
1299 mAppNameDivider.setVisibility(mAppNameGone ? GONE : VISIBLE);
1300 }
1301
Selim Cinek20d1ee22020-02-03 16:04:26 -05001302 public void updateExpandability(boolean expandable, @Nullable OnClickListener onClickListener) {
Selim Cinekafc20582020-03-13 20:15:38 -07001303 mExpandable = expandable;
Selim Cinek20d1ee22020-02-03 16:04:26 -05001304 if (expandable) {
1305 mExpandButtonContainer.setVisibility(VISIBLE);
Selim Cinek28db07e2020-04-03 17:33:24 -07001306 mExpandButtonInnerContainer.setOnClickListener(onClickListener);
Steve Elliott0afc3012020-06-10 16:54:24 -04001307 mConversationIconContainer.setOnClickListener(onClickListener);
Selim Cinek20d1ee22020-02-03 16:04:26 -05001308 } else {
Selim Cinek20d1ee22020-02-03 16:04:26 -05001309 mExpandButtonContainer.setVisibility(GONE);
Steve Elliott0afc3012020-06-10 16:54:24 -04001310 mConversationIconContainer.setOnClickListener(null);
Selim Cinek20d1ee22020-02-03 16:04:26 -05001311 }
Selim Cinekbf322ee2020-03-20 20:20:31 -07001312 updateContentEndPaddings();
Selim Cinek20d1ee22020-02-03 16:04:26 -05001313 }
Selim Cinek514b52c2020-03-17 18:42:12 -07001314
1315 @Override
1316 public void setMessagingClippingDisabled(boolean clippingDisabled) {
1317 mMessagingLinearLayout.setClipBounds(clippingDisabled ? null : mMessagingClipRect);
1318 }
Steve Elliott936df152020-04-14 13:59:53 -04001319
1320 @Nullable
1321 public CharSequence getConversationSenderName() {
1322 if (mGroups.isEmpty()) {
1323 return null;
1324 }
1325 final CharSequence name = mGroups.get(mGroups.size() - 1).getSenderName();
1326 return getResources().getString(R.string.conversation_single_line_name_display, name);
1327 }
1328
1329 public boolean isOneToOne() {
1330 return mIsOneToOne;
1331 }
1332
1333 @Nullable
1334 public CharSequence getConversationText() {
1335 if (mMessages.isEmpty()) {
1336 return null;
1337 }
1338 final MessagingMessage messagingMessage = mMessages.get(mMessages.size() - 1);
1339 final CharSequence text = messagingMessage.getMessage().getText();
1340 if (text == null && messagingMessage instanceof MessagingImageMessage) {
1341 final String unformatted =
1342 getResources().getString(R.string.conversation_single_line_image_placeholder);
1343 SpannableString spannableString = new SpannableString(unformatted);
1344 spannableString.setSpan(
1345 new StyleSpan(Typeface.ITALIC),
1346 0,
1347 spannableString.length(),
1348 Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
1349 return spannableString;
1350 }
1351 return text;
1352 }
1353
1354 @Nullable
1355 public Icon getConversationIcon() {
1356 return mConversationIcon;
1357 }
Selim Cinek20d1ee22020-02-03 16:04:26 -05001358}