blob: 5d4407bf8370ca3f58954f739629272e585fccdc [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 Cinek20d1ee22020-02-03 16:04:26 -0500200 mMessagingLinearLayout.setMessagingLayout(this);
Selim Cinek514b52c2020-03-17 18:42:12 -0700201 mImageMessageContainer = findViewById(R.id.conversation_image_message_container);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500202 // We still want to clip, but only on the top, since views can temporarily out of bounds
203 // during transitions.
204 DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
205 int size = Math.max(displayMetrics.widthPixels, displayMetrics.heightPixels);
Selim Cinek514b52c2020-03-17 18:42:12 -0700206 mMessagingClipRect = new Rect(0, 0, size, size);
207 setMessagingClippingDisabled(false);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500208 mAvatarSize = getResources().getDimensionPixelSize(R.dimen.messaging_avatar_size);
209 mTextPaint.setTextAlign(Paint.Align.CENTER);
210 mTextPaint.setAntiAlias(true);
Steve Elliott936df152020-04-14 13:59:53 -0400211 mConversationIconView = findViewById(R.id.conversation_icon);
Selim Cinekbf322ee2020-03-20 20:20:31 -0700212 mConversationIconContainer = findViewById(R.id.conversation_icon_container);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500213 mIcon = findViewById(R.id.icon);
Selim Cinek4237e822020-03-31 17:22:28 -0700214 mAppOps = findViewById(com.android.internal.R.id.app_ops);
215 mMinTouchSize = 48 * getResources().getDisplayMetrics().density;
Steve Elliott928bb162020-03-17 17:52:53 -0400216 mImportanceRingView = findViewById(R.id.conversation_icon_badge_ring);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500217 mConversationIconBadge = findViewById(R.id.conversation_icon_badge);
Selim Cinekd00d01a2020-03-23 19:00:26 -0700218 mConversationIconBadgeBg = findViewById(R.id.conversation_icon_badge_bg);
Selim Cinekf2b8aa72020-03-05 15:29:02 -0800219 mIcon.setOnVisibilityChangedListener((visibility) -> {
Selim Cinek9f2d21f2020-03-27 12:36:38 -0700220
221 // Let's hide the background directly or in an animated way
222 boolean isGone = visibility == GONE;
223 int oldVisibility = mConversationIconBadgeBg.getVisibility();
224 boolean wasGone = oldVisibility == GONE;
225 if (wasGone != isGone) {
226 // Keep the badge gone state in sync with the icon. This is necessary in cases
227 // Where the icon is being hidden externally like in group children.
228 mConversationIconBadgeBg.animate().cancel();
229 mConversationIconBadgeBg.setVisibility(visibility);
230 }
231
232 // Let's handle the importance ring which can also be be gone normally
233 oldVisibility = mImportanceRingView.getVisibility();
234 wasGone = oldVisibility == GONE;
235 visibility = !mImportantConversation ? GONE : visibility;
Steve Elliott45946032020-06-15 15:40:31 -0400236 boolean isRingGone = visibility == GONE;
237 if (wasGone != isRingGone) {
Selim Cinek9f2d21f2020-03-27 12:36:38 -0700238 // Keep the badge visibility in sync with the icon. This is necessary in cases
239 // Where the icon is being hidden externally like in group children.
240 mImportanceRingView.animate().cancel();
241 mImportanceRingView.setVisibility(visibility);
242 }
Steve Elliott45946032020-06-15 15:40:31 -0400243
244 oldVisibility = mConversationIconBadge.getVisibility();
245 wasGone = oldVisibility == GONE;
246 if (wasGone != isGone) {
247 mConversationIconBadge.animate().cancel();
248 mConversationIconBadge.setVisibility(visibility);
249 }
Selim Cinek9f2d21f2020-03-27 12:36:38 -0700250 });
251 // When the small icon is gone, hide the rest of the badge
252 mIcon.setOnForceHiddenChangedListener((forceHidden) -> {
253 animateViewForceHidden(mConversationIconBadgeBg, forceHidden);
254 animateViewForceHidden(mImportanceRingView, forceHidden);
255 });
256
257 // When the conversation icon is gone, hide the whole badge
Steve Elliott936df152020-04-14 13:59:53 -0400258 mConversationIconView.setOnForceHiddenChangedListener((forceHidden) -> {
Selim Cinek9f2d21f2020-03-27 12:36:38 -0700259 animateViewForceHidden(mConversationIconBadgeBg, forceHidden);
260 animateViewForceHidden(mImportanceRingView, forceHidden);
261 animateViewForceHidden(mIcon, forceHidden);
Selim Cinekf2b8aa72020-03-05 15:29:02 -0800262 });
Selim Cinek2f7f7b82020-03-10 15:41:13 -0700263 mConversationText = findViewById(R.id.conversation_text);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500264 mExpandButtonContainer = findViewById(R.id.expand_button_container);
Selim Cinekafc20582020-03-13 20:15:38 -0700265 mConversationHeader = findViewById(R.id.conversation_header);
266 mContentContainer = findViewById(R.id.notification_action_list_margin_target);
Selim Cinek54fe9ad2020-03-04 13:56:42 -0800267 mExpandButtonAndContentContainer = findViewById(R.id.expand_button_and_content_container);
Selim Cinek28db07e2020-04-03 17:33:24 -0700268 mExpandButtonInnerContainer = findViewById(R.id.expand_button_inner_container);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500269 mExpandButton = findViewById(R.id.expand_button);
270 mExpandButtonExpandedTopMargin = getResources().getDimensionPixelSize(
271 R.dimen.conversation_expand_button_top_margin_expanded);
Selim Cinekafc20582020-03-13 20:15:38 -0700272 mNotificationHeaderExpandedPadding = getResources().getDimensionPixelSize(
273 R.dimen.conversation_header_expanded_padding_end);
274 mContentMarginEnd = getResources().getDimensionPixelSize(
275 R.dimen.notification_content_margin_end);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500276 mBadgedSideMargins = getResources().getDimensionPixelSize(
277 R.dimen.conversation_badge_side_margin);
Selim Cinekbf322ee2020-03-20 20:20:31 -0700278 mConversationAvatarSize = getResources().getDimensionPixelSize(
279 R.dimen.conversation_avatar_size);
280 mConversationAvatarSizeExpanded = getResources().getDimensionPixelSize(
281 R.dimen.conversation_avatar_size_group_expanded);
Selim Cinekbf322ee2020-03-20 20:20:31 -0700282 mConversationIconTopPaddingExpandedGroup = getResources().getDimensionPixelSize(
283 R.dimen.conversation_icon_container_top_padding_small_avatar);
Steve Elliottf7ef4ef2020-04-27 15:40:18 -0400284 mConversationIconTopPadding = getResources().getDimensionPixelSize(
285 R.dimen.conversation_icon_container_top_padding);
286 mExpandedGroupMessagePadding = getResources().getDimensionPixelSize(
287 R.dimen.expanded_group_conversation_message_padding);
Selim Cinekbf322ee2020-03-20 20:20:31 -0700288 mExpandedGroupSideMargin = getResources().getDimensionPixelSize(
289 R.dimen.conversation_badge_side_margin_group_expanded);
290 mExpandedGroupSideMarginFacePile = getResources().getDimensionPixelSize(
291 R.dimen.conversation_badge_side_margin_group_expanded_face_pile);
Selim Cinek8baa70f2020-03-09 20:09:35 -0700292 mConversationFacePile = findViewById(R.id.conversation_face_pile);
Selim Cinekbf322ee2020-03-20 20:20:31 -0700293 mFacePileAvatarSize = getResources().getDimensionPixelSize(
294 R.dimen.conversation_face_pile_avatar_size);
295 mFacePileAvatarSizeExpandedGroup = getResources().getDimensionPixelSize(
296 R.dimen.conversation_face_pile_avatar_size_group_expanded);
297 mFacePileProtectionWidth = getResources().getDimensionPixelSize(
298 R.dimen.conversation_face_pile_protection_width);
299 mFacePileProtectionWidthExpanded = getResources().getDimensionPixelSize(
300 R.dimen.conversation_face_pile_protection_width_expanded);
Selim Cinek2f7f7b82020-03-10 15:41:13 -0700301 mFallbackChatName = getResources().getString(
302 R.string.conversation_title_fallback_one_to_one);
303 mFallbackGroupChatName = getResources().getString(
304 R.string.conversation_title_fallback_group_chat);
Steve Elliott52440e92020-03-19 15:18:58 -0400305 mAppName = findViewById(R.id.app_name_text);
Steve Elliottf7ef4ef2020-04-27 15:40:18 -0400306 mAppNameDivider = findViewById(R.id.app_name_divider);
Selim Cinekbf322ee2020-03-20 20:20:31 -0700307 mAppNameGone = mAppName.getVisibility() == GONE;
308 mAppName.setOnVisibilityChangedListener((visibility) -> {
309 onAppNameVisibilityChanged();
310 });
Steve Elliott239e6cb2020-03-25 14:26:42 -0400311 mUnreadBadge = findViewById(R.id.conversation_unread_count);
Selim Cineke027da22020-03-30 19:03:13 -0700312 mConversationContentStart = getResources().getDimensionPixelSize(
313 R.dimen.conversation_content_start);
314 mInternalButtonPadding
315 = getResources().getDimensionPixelSize(R.dimen.button_padding_horizontal_material)
316 + getResources().getDimensionPixelSize(R.dimen.button_inset_horizontal_material);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500317 }
318
Selim Cinek9f2d21f2020-03-27 12:36:38 -0700319 private void animateViewForceHidden(CachingIconView view, boolean forceHidden) {
320 boolean nowForceHidden = view.willBeForceHidden() || view.isForceHidden();
321 if (forceHidden == nowForceHidden) {
322 // We are either already forceHidden or will be
323 return;
324 }
325 view.animate().cancel();
326 view.setWillBeForceHidden(forceHidden);
327 view.animate()
328 .scaleX(forceHidden ? 0.5f : 1.0f)
329 .scaleY(forceHidden ? 0.5f : 1.0f)
330 .alpha(forceHidden ? 0.0f : 1.0f)
331 .setInterpolator(forceHidden ? ALPHA_OUT : ALPHA_IN)
332 .setDuration(160);
333 if (view.getVisibility() != VISIBLE) {
334 view.setForceHidden(forceHidden);
335 } else {
336 view.animate().withEndAction(() -> view.setForceHidden(forceHidden));
337 }
338 view.animate().start();
339 }
340
Selim Cinek20d1ee22020-02-03 16:04:26 -0500341 @RemotableViewMethod
342 public void setAvatarReplacement(Icon icon) {
343 mAvatarReplacement = icon;
344 }
345
346 @RemotableViewMethod
347 public void setNameReplacement(CharSequence nameReplacement) {
348 mNameReplacement = nameReplacement;
349 }
350
Steve Elliott8b4929d2020-06-02 13:38:04 -0400351 /** Sets this conversation as "important", adding some additional UI treatment. */
Steve Elliott928bb162020-03-17 17:52:53 -0400352 @RemotableViewMethod
353 public void setIsImportantConversation(boolean isImportantConversation) {
Steve Elliott8b4929d2020-06-02 13:38:04 -0400354 setIsImportantConversation(isImportantConversation, false);
355 }
356
357 /** @hide **/
358 public void setIsImportantConversation(boolean isImportantConversation, boolean animate) {
Selim Cinek9ed6e042020-03-26 15:45:51 -0700359 mImportantConversation = isImportantConversation;
Steve Elliott8b4929d2020-06-02 13:38:04 -0400360 mImportanceRingView.setVisibility(isImportantConversation && mIcon.getVisibility() != GONE
361 ? VISIBLE : GONE);
362
363 if (animate && isImportantConversation) {
364 GradientDrawable ring = (GradientDrawable) mImportanceRingView.getDrawable();
365 ring.mutate();
366 GradientDrawable bg = (GradientDrawable) mConversationIconBadgeBg.getDrawable();
367 bg.mutate();
368 int ringColor = getResources()
369 .getColor(R.color.conversation_important_highlight);
370 int standardThickness = getResources()
371 .getDimensionPixelSize(R.dimen.importance_ring_stroke_width);
372 int largeThickness = getResources()
373 .getDimensionPixelSize(R.dimen.importance_ring_anim_max_stroke_width);
374 int standardSize = getResources().getDimensionPixelSize(
375 R.dimen.importance_ring_size);
376 int baseSize = standardSize - standardThickness * 2;
377 int bgSize = getResources()
378 .getDimensionPixelSize(R.dimen.conversation_icon_size_badged);
379
380 ValueAnimator.AnimatorUpdateListener animatorUpdateListener = animation -> {
381 int strokeWidth = Math.round((float) animation.getAnimatedValue());
382 ring.setStroke(strokeWidth, ringColor);
383 int newSize = baseSize + strokeWidth * 2;
384 ring.setSize(newSize, newSize);
385 mImportanceRingView.invalidate();
386 };
387
388 ValueAnimator growAnimation = ValueAnimator.ofFloat(0, largeThickness);
389 growAnimation.setInterpolator(LINEAR_OUT_SLOW_IN);
390 growAnimation.setDuration(IMPORTANCE_ANIM_GROW_DURATION);
391 growAnimation.addUpdateListener(animatorUpdateListener);
392
393 ValueAnimator shrinkAnimation =
394 ValueAnimator.ofFloat(largeThickness, standardThickness);
395 shrinkAnimation.setDuration(IMPORTANCE_ANIM_SHRINK_DURATION);
396 shrinkAnimation.setStartDelay(IMPORTANCE_ANIM_SHRINK_DELAY);
397 shrinkAnimation.setInterpolator(OVERSHOOT);
398 shrinkAnimation.addUpdateListener(animatorUpdateListener);
399 shrinkAnimation.addListener(new AnimatorListenerAdapter() {
400 @Override
401 public void onAnimationStart(Animator animation) {
402 // Shrink the badge bg so that it doesn't peek behind the animation
403 bg.setSize(baseSize, baseSize);
404 mConversationIconBadgeBg.invalidate();
405 }
406
407 @Override
408 public void onAnimationEnd(Animator animation) {
409 // Reset bg back to normal size
410 bg.setSize(bgSize, bgSize);
411 mConversationIconBadgeBg.invalidate();
412 }
413 });
414
415 AnimatorSet anims = new AnimatorSet();
416 anims.playSequentially(growAnimation, shrinkAnimation);
417 anims.start();
418 }
Steve Elliott928bb162020-03-17 17:52:53 -0400419 }
420
Selim Cinek9ed6e042020-03-26 15:45:51 -0700421 public boolean isImportantConversation() {
422 return mImportantConversation;
423 }
424
Steve Elliott928bb162020-03-17 17:52:53 -0400425 /**
Selim Cinek20d1ee22020-02-03 16:04:26 -0500426 * Set this layout to show the collapsed representation.
427 *
428 * @param isCollapsed is it collapsed
429 */
430 @RemotableViewMethod
431 public void setIsCollapsed(boolean isCollapsed) {
432 mIsCollapsed = isCollapsed;
433 mMessagingLinearLayout.setMaxDisplayedLines(isCollapsed ? 1 : Integer.MAX_VALUE);
434 updateExpandButton();
Selim Cinekbf322ee2020-03-20 20:20:31 -0700435 updateContentEndPaddings();
Selim Cinek20d1ee22020-02-03 16:04:26 -0500436 }
437
438 @RemotableViewMethod
439 public void setData(Bundle extras) {
440 Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES);
441 List<Notification.MessagingStyle.Message> newMessages
442 = Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages);
443 Parcelable[] histMessages = extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES);
444 List<Notification.MessagingStyle.Message> newHistoricMessages
445 = Notification.MessagingStyle.Message.getMessagesFromBundleArray(histMessages);
446
447 // mUser now set (would be nice to avoid the side effect but WHATEVER)
448 setUser(extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON));
449
Selim Cinek20d1ee22020-02-03 16:04:26 -0500450 // Append remote input history to newMessages (again, side effect is lame but WHATEVS)
451 RemoteInputHistoryItem[] history = (RemoteInputHistoryItem[])
452 extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
453 addRemoteInputHistoryToMessages(newMessages, history);
454
455 boolean showSpinner =
456 extras.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500457 // bind it, baby
458 bind(newMessages, newHistoricMessages, showSpinner);
Steve Elliott239e6cb2020-03-25 14:26:42 -0400459
460 int unreadCount = extras.getInt(Notification.EXTRA_CONVERSATION_UNREAD_MESSAGE_COUNT);
461 setUnreadCount(unreadCount);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500462 }
463
464 @Override
465 public void setImageResolver(ImageResolver resolver) {
466 mImageResolver = resolver;
467 }
468
Steve Elliott239e6cb2020-03-25 14:26:42 -0400469 /** @hide */
470 public void setUnreadCount(int unreadCount) {
Selim Cinek94d0be82020-04-06 19:20:47 -0700471 boolean visible = mIsCollapsed && unreadCount > 1;
472 mUnreadBadge.setVisibility(visible ? VISIBLE : GONE);
473 if (visible) {
474 CharSequence text = unreadCount >= 100
475 ? getResources().getString(R.string.unread_convo_overflow, 99)
476 : String.format(Locale.getDefault(), "%d", unreadCount);
477 mUnreadBadge.setText(text);
478 mUnreadBadge.setBackgroundTintList(ColorStateList.valueOf(mLayoutColor));
479 boolean needDarkText = ColorUtils.calculateLuminance(mLayoutColor) > 0.5f;
480 mUnreadBadge.setTextColor(needDarkText ? Color.BLACK : Color.WHITE);
481 }
Steve Elliott239e6cb2020-03-25 14:26:42 -0400482 }
483
Selim Cinek20d1ee22020-02-03 16:04:26 -0500484 private void addRemoteInputHistoryToMessages(
485 List<Notification.MessagingStyle.Message> newMessages,
486 RemoteInputHistoryItem[] remoteInputHistory) {
487 if (remoteInputHistory == null || remoteInputHistory.length == 0) {
488 return;
489 }
490 for (int i = remoteInputHistory.length - 1; i >= 0; i--) {
491 RemoteInputHistoryItem historyMessage = remoteInputHistory[i];
492 Notification.MessagingStyle.Message message = new Notification.MessagingStyle.Message(
493 historyMessage.getText(), 0, (Person) null, true /* remoteHistory */);
494 if (historyMessage.getUri() != null) {
495 message.setData(historyMessage.getMimeType(), historyMessage.getUri());
496 }
497 newMessages.add(message);
498 }
499 }
500
501 private void bind(List<Notification.MessagingStyle.Message> newMessages,
502 List<Notification.MessagingStyle.Message> newHistoricMessages,
503 boolean showSpinner) {
504 // convert MessagingStyle.Message to MessagingMessage, re-using ones from a previous binding
505 // if they exist
506 List<MessagingMessage> historicMessages = createMessages(newHistoricMessages,
507 true /* isHistoric */);
508 List<MessagingMessage> messages = createMessages(newMessages, false /* isHistoric */);
509
510 // Copy our groups, before they get clobbered
511 ArrayList<MessagingGroup> oldGroups = new ArrayList<>(mGroups);
512
513 // Add our new MessagingMessages to groups
514 List<List<MessagingMessage>> groups = new ArrayList<>();
515 List<Person> senders = new ArrayList<>();
516
517 // Lets first find the groups (populate `groups` and `senders`)
518 findGroups(historicMessages, messages, groups, senders);
519
520 // Let's now create the views and reorder them accordingly
521 // side-effect: updates mGroups, mAddedGroups
522 createGroupViews(groups, senders, showSpinner);
523
524 // Let's first check which groups were removed altogether and remove them in one animation
525 removeGroups(oldGroups);
526
527 // Let's remove the remaining messages
528 mMessages.forEach(REMOVE_MESSAGE);
529 mHistoricMessages.forEach(REMOVE_MESSAGE);
530
531 mMessages = messages;
532 mHistoricMessages = historicMessages;
533
534 updateHistoricMessageVisibility();
535 updateTitleAndNamesDisplay();
536
Selim Cinek857f2792020-03-03 19:06:21 -0800537 updateConversationLayout();
Selim Cinek20d1ee22020-02-03 16:04:26 -0500538 }
539
Selim Cinek857f2792020-03-03 19:06:21 -0800540 /**
541 * Update the layout according to the data provided (i.e mIsOneToOne, expanded etc);
542 */
543 private void updateConversationLayout() {
Selim Cinek20d1ee22020-02-03 16:04:26 -0500544 // Set avatar and name
Selim Cinek2f7f7b82020-03-10 15:41:13 -0700545 CharSequence conversationText = mConversationTitle;
Steve Elliott3d360da2020-06-03 20:50:23 -0400546 mConversationIcon = mShortcutIcon;
Selim Cinek20d1ee22020-02-03 16:04:26 -0500547 if (mIsOneToOne) {
548 // Let's resolve the icon / text from the last sender
Selim Cinek857f2792020-03-03 19:06:21 -0800549 CharSequence userKey = getKey(mUser);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500550 for (int i = mGroups.size() - 1; i >= 0; i--) {
551 MessagingGroup messagingGroup = mGroups.get(i);
552 Person messageSender = messagingGroup.getSender();
Selim Cinek857f2792020-03-03 19:06:21 -0800553 if ((messageSender != null && !TextUtils.equals(userKey, getKey(messageSender)))
554 || i == 0) {
Selim Cinek2f7f7b82020-03-10 15:41:13 -0700555 if (TextUtils.isEmpty(conversationText)) {
556 // We use the sendername as header text if no conversation title is provided
557 // (This usually happens for most 1:1 conversations)
558 conversationText = messagingGroup.getSenderName();
559 }
Steve Elliott63738292020-05-29 15:56:00 -0400560 if (mConversationIcon == null) {
561 Icon avatarIcon = messagingGroup.getAvatarIcon();
562 if (avatarIcon == null) {
563 avatarIcon = createAvatarSymbol(conversationText, "", mLayoutColor);
564 }
565 mConversationIcon = avatarIcon;
Selim Cinek2f7f7b82020-03-10 15:41:13 -0700566 }
Selim Cinek20d1ee22020-02-03 16:04:26 -0500567 break;
568 }
569 }
Steve Elliott3d360da2020-06-03 20:50:23 -0400570 }
571 if (mConversationIcon == null) {
572 mConversationIcon = mLargeIcon;
573 }
574 if (mIsOneToOne || mConversationIcon != null) {
575 mConversationIconView.setVisibility(VISIBLE);
576 mConversationFacePile.setVisibility(GONE);
577 mConversationIconView.setImageIcon(mConversationIcon);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500578 } else {
Steve Elliott3d360da2020-06-03 20:50:23 -0400579 mConversationIconView.setVisibility(GONE);
580 // This will also inflate it!
581 mConversationFacePile.setVisibility(VISIBLE);
582 // rebind the value to the inflated view instead of the stub
583 mConversationFacePile = findViewById(R.id.conversation_face_pile);
584 bindFacePile();
Selim Cinek20d1ee22020-02-03 16:04:26 -0500585 }
Selim Cinek2f7f7b82020-03-10 15:41:13 -0700586 if (TextUtils.isEmpty(conversationText)) {
587 conversationText = mIsOneToOne ? mFallbackChatName : mFallbackGroupChatName;
588 }
589 mConversationText.setText(conversationText);
Selim Cinek857f2792020-03-03 19:06:21 -0800590 // Update if the groups can hide the sender if they are first (applies to 1:1 conversations)
591 // This needs to happen after all of the above o update all of the groups
592 for (int i = mGroups.size() - 1; i >= 0; i--) {
593 MessagingGroup messagingGroup = mGroups.get(i);
594 CharSequence messageSender = messagingGroup.getSenderName();
595 boolean canHide = mIsOneToOne
Selim Cinek2f7f7b82020-03-10 15:41:13 -0700596 && TextUtils.equals(conversationText, messageSender);
Selim Cinek857f2792020-03-03 19:06:21 -0800597 messagingGroup.setCanHideSenderIfFirst(canHide);
598 }
Selim Cinekbf322ee2020-03-20 20:20:31 -0700599 updateAppName();
Selim Cinek857f2792020-03-03 19:06:21 -0800600 updateIconPositionAndSize();
Selim Cinek514b52c2020-03-17 18:42:12 -0700601 updateImageMessages();
Selim Cinekbf322ee2020-03-20 20:20:31 -0700602 updatePaddingsBasedOnContentAvailability();
Selim Cineke027da22020-03-30 19:03:13 -0700603 updateActionListPadding();
Steve Elliottf7ef4ef2020-04-27 15:40:18 -0400604 updateAppNameDividerVisibility();
Selim Cineke027da22020-03-30 19:03:13 -0700605 }
606
607 private void updateActionListPadding() {
608 if (mActions == null) {
609 return;
610 }
611 View firstAction = mActions.getChildAt(0);
612 if (firstAction != null) {
613 // Let's visually position the first action where the content starts
614 int paddingStart = mConversationContentStart;
615
616 MarginLayoutParams layoutParams = (MarginLayoutParams) firstAction.getLayoutParams();
617 paddingStart -= layoutParams.getMarginStart();
618 paddingStart -= mInternalButtonPadding;
619
620 mActions.setPaddingRelative(paddingStart,
621 mActions.getPaddingTop(),
622 mActions.getPaddingEnd(),
623 mActions.getPaddingBottom());
624 }
Selim Cinek514b52c2020-03-17 18:42:12 -0700625 }
626
627 private void updateImageMessages() {
Selim Cineka959fe42020-04-06 18:16:40 -0700628 View newMessage = null;
629 if (mIsCollapsed && mGroups.size() > 0) {
Selim Cinek514b52c2020-03-17 18:42:12 -0700630
Selim Cineka959fe42020-04-06 18:16:40 -0700631 // When collapsed, we're displaying the image message in a dedicated container
632 // on the right of the layout instead of inline. Let's add the isolated image there
633 MessagingGroup messagingGroup = mGroups.get(mGroups.size() -1);
634 MessagingImageMessage isolatedMessage = messagingGroup.getIsolatedMessage();
635 if (isolatedMessage != null) {
636 newMessage = isolatedMessage.getView();
Selim Cinek514b52c2020-03-17 18:42:12 -0700637 }
638 }
639 // Remove all messages that don't belong into the image layout
Selim Cineka959fe42020-04-06 18:16:40 -0700640 View previousMessage = mImageMessageContainer.getChildAt(0);
641 if (previousMessage != newMessage) {
642 mImageMessageContainer.removeView(previousMessage);
643 if (newMessage != null) {
644 mImageMessageContainer.addView(newMessage);
Selim Cinek514b52c2020-03-17 18:42:12 -0700645 }
646 }
Selim Cineka959fe42020-04-06 18:16:40 -0700647 mImageMessageContainer.setVisibility(newMessage != null ? VISIBLE : GONE);
Selim Cinek857f2792020-03-03 19:06:21 -0800648 }
649
Steve Elliott936df152020-04-14 13:59:53 -0400650 public void bindFacePile(ImageView bottomBackground, ImageView bottomView, ImageView topView) {
Selim Cinek8baa70f2020-03-09 20:09:35 -0700651 applyNotificationBackgroundColor(bottomBackground);
Selim Cinek8baa70f2020-03-09 20:09:35 -0700652 // Let's find the two last conversations:
653 Icon secondLastIcon = null;
654 CharSequence lastKey = null;
655 Icon lastIcon = null;
656 CharSequence userKey = getKey(mUser);
657 for (int i = mGroups.size() - 1; i >= 0; i--) {
658 MessagingGroup messagingGroup = mGroups.get(i);
659 Person messageSender = messagingGroup.getSender();
660 boolean notUser = messageSender != null
661 && !TextUtils.equals(userKey, getKey(messageSender));
662 boolean notIncluded = messageSender != null
663 && !TextUtils.equals(lastKey, getKey(messageSender));
664 if ((notUser && notIncluded)
665 || (i == 0 && lastKey == null)) {
666 if (lastIcon == null) {
667 lastIcon = messagingGroup.getAvatarIcon();
668 lastKey = getKey(messageSender);
669 } else {
670 secondLastIcon = messagingGroup.getAvatarIcon();
671 break;
672 }
673 }
674 }
675 if (lastIcon == null) {
676 lastIcon = createAvatarSymbol(" ", "", mLayoutColor);
677 }
678 bottomView.setImageIcon(lastIcon);
679 if (secondLastIcon == null) {
680 secondLastIcon = createAvatarSymbol("", "", mLayoutColor);
681 }
682 topView.setImageIcon(secondLastIcon);
Steve Elliott936df152020-04-14 13:59:53 -0400683 }
684
685 private void bindFacePile() {
686 ImageView bottomBackground = mConversationFacePile.findViewById(
687 R.id.conversation_face_pile_bottom_background);
688 ImageView bottomView = mConversationFacePile.findViewById(
689 R.id.conversation_face_pile_bottom);
690 ImageView topView = mConversationFacePile.findViewById(
691 R.id.conversation_face_pile_top);
692
693 bindFacePile(bottomBackground, bottomView, topView);
Selim Cinekbf322ee2020-03-20 20:20:31 -0700694
695 int conversationAvatarSize;
696 int facepileAvatarSize;
697 int facePileBackgroundSize;
698 if (mIsCollapsed) {
699 conversationAvatarSize = mConversationAvatarSize;
700 facepileAvatarSize = mFacePileAvatarSize;
701 facePileBackgroundSize = facepileAvatarSize + 2 * mFacePileProtectionWidth;
702 } else {
703 conversationAvatarSize = mConversationAvatarSizeExpanded;
704 facepileAvatarSize = mFacePileAvatarSizeExpandedGroup;
705 facePileBackgroundSize = facepileAvatarSize + 2 * mFacePileProtectionWidthExpanded;
706 }
Steve Elliott936df152020-04-14 13:59:53 -0400707 LayoutParams layoutParams = (LayoutParams) mConversationIconView.getLayoutParams();
Selim Cinekbf322ee2020-03-20 20:20:31 -0700708 layoutParams.width = conversationAvatarSize;
709 layoutParams.height = conversationAvatarSize;
710 mConversationFacePile.setLayoutParams(layoutParams);
711
712 layoutParams = (LayoutParams) bottomView.getLayoutParams();
713 layoutParams.width = facepileAvatarSize;
714 layoutParams.height = facepileAvatarSize;
715 bottomView.setLayoutParams(layoutParams);
716
717 layoutParams = (LayoutParams) topView.getLayoutParams();
718 layoutParams.width = facepileAvatarSize;
719 layoutParams.height = facepileAvatarSize;
720 topView.setLayoutParams(layoutParams);
721
722 layoutParams = (LayoutParams) bottomBackground.getLayoutParams();
723 layoutParams.width = facePileBackgroundSize;
724 layoutParams.height = facePileBackgroundSize;
725 bottomBackground.setLayoutParams(layoutParams);
Selim Cinek8baa70f2020-03-09 20:09:35 -0700726 }
727
Steve Elliott52440e92020-03-19 15:18:58 -0400728 private void updateAppName() {
729 mAppName.setVisibility(mIsCollapsed ? GONE : VISIBLE);
730 }
731
Selim Cinekcef53f32020-03-19 19:31:25 -0700732 public boolean shouldHideAppName() {
733 return mIsCollapsed;
734 }
735
Selim Cinek857f2792020-03-03 19:06:21 -0800736 /**
737 * update the icon position and sizing
738 */
739 private void updateIconPositionAndSize() {
Selim Cinekbf322ee2020-03-20 20:20:31 -0700740 int sidemargin;
741 int conversationAvatarSize;
Selim Cinek20d1ee22020-02-03 16:04:26 -0500742 if (mIsOneToOne || mIsCollapsed) {
Selim Cinekbf322ee2020-03-20 20:20:31 -0700743 sidemargin = mBadgedSideMargins;
744 conversationAvatarSize = mConversationAvatarSize;
Selim Cinek20d1ee22020-02-03 16:04:26 -0500745 } else {
Selim Cinekbf322ee2020-03-20 20:20:31 -0700746 sidemargin = mConversationFacePile.getVisibility() == VISIBLE
747 ? mExpandedGroupSideMarginFacePile
748 : mExpandedGroupSideMargin;
749 conversationAvatarSize = mConversationAvatarSizeExpanded;
Selim Cinek20d1ee22020-02-03 16:04:26 -0500750 }
Selim Cinek857f2792020-03-03 19:06:21 -0800751 LayoutParams layoutParams =
Selim Cinek20d1ee22020-02-03 16:04:26 -0500752 (LayoutParams) mConversationIconBadge.getLayoutParams();
Selim Cinekbf322ee2020-03-20 20:20:31 -0700753 layoutParams.topMargin = sidemargin;
754 layoutParams.setMarginStart(sidemargin);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500755 mConversationIconBadge.setLayoutParams(layoutParams);
Selim Cinekbf322ee2020-03-20 20:20:31 -0700756
Steve Elliott936df152020-04-14 13:59:53 -0400757 if (mConversationIconView.getVisibility() == VISIBLE) {
758 layoutParams = (LayoutParams) mConversationIconView.getLayoutParams();
Selim Cinekbf322ee2020-03-20 20:20:31 -0700759 layoutParams.width = conversationAvatarSize;
760 layoutParams.height = conversationAvatarSize;
Steve Elliott936df152020-04-14 13:59:53 -0400761 mConversationIconView.setLayoutParams(layoutParams);
Selim Cinekbf322ee2020-03-20 20:20:31 -0700762 }
763 }
764
765 private void updatePaddingsBasedOnContentAvailability() {
Steve Elliottf7ef4ef2020-04-27 15:40:18 -0400766 int messagingPadding = mIsOneToOne || mIsCollapsed
767 ? 0
768 // Add some extra padding to the messages, since otherwise it will overlap with the
769 // group
770 : mExpandedGroupMessagePadding;
Selim Cinekbf322ee2020-03-20 20:20:31 -0700771
Steve Elliott282498a2020-06-10 14:02:09 -0400772 int iconPadding = mIsOneToOne || mIsCollapsed
773 ? mConversationIconTopPadding
774 : mConversationIconTopPaddingExpandedGroup;
775
Selim Cinekbf322ee2020-03-20 20:20:31 -0700776 mConversationIconContainer.setPaddingRelative(
777 mConversationIconContainer.getPaddingStart(),
Steve Elliott282498a2020-06-10 14:02:09 -0400778 iconPadding,
Selim Cinekbf322ee2020-03-20 20:20:31 -0700779 mConversationIconContainer.getPaddingEnd(),
780 mConversationIconContainer.getPaddingBottom());
781
782 mMessagingLinearLayout.setPaddingRelative(
783 mMessagingLinearLayout.getPaddingStart(),
784 messagingPadding,
785 mMessagingLinearLayout.getPaddingEnd(),
786 mMessagingLinearLayout.getPaddingBottom());
Selim Cinek20d1ee22020-02-03 16:04:26 -0500787 }
788
789 @RemotableViewMethod
790 public void setLargeIcon(Icon largeIcon) {
791 mLargeIcon = largeIcon;
792 }
793
Steve Elliott63738292020-05-29 15:56:00 -0400794 @RemotableViewMethod
Steve Elliott86bc69a2020-06-10 11:35:01 -0400795 public void setShortcutIcon(Icon shortcutIcon) {
796 mShortcutIcon = shortcutIcon;
Steve Elliott63738292020-05-29 15:56:00 -0400797 }
798
Selim Cinek2f7f7b82020-03-10 15:41:13 -0700799 /**
800 * Sets the conversation title of this conversation.
801 *
802 * @param conversationTitle the conversation title
803 */
804 @RemotableViewMethod
805 public void setConversationTitle(CharSequence conversationTitle) {
Steve Elliott242057d2020-06-10 15:15:19 -0400806 // Remove formatting from the title.
807 mConversationTitle = conversationTitle != null ? conversationTitle.toString() : null;
Selim Cinek2f7f7b82020-03-10 15:41:13 -0700808 }
809
Steve Elliott936df152020-04-14 13:59:53 -0400810 public CharSequence getConversationTitle() {
811 return mConversationText.getText();
812 }
813
Selim Cinek20d1ee22020-02-03 16:04:26 -0500814 private void removeGroups(ArrayList<MessagingGroup> oldGroups) {
815 int size = oldGroups.size();
816 for (int i = 0; i < size; i++) {
817 MessagingGroup group = oldGroups.get(i);
818 if (!mGroups.contains(group)) {
819 List<MessagingMessage> messages = group.getMessages();
820 Runnable endRunnable = () -> {
821 mMessagingLinearLayout.removeTransientView(group);
822 group.recycle();
823 };
824
825 boolean wasShown = group.isShown();
826 mMessagingLinearLayout.removeView(group);
827 if (wasShown && !MessagingLinearLayout.isGone(group)) {
828 mMessagingLinearLayout.addTransientView(group, 0);
829 group.removeGroupAnimated(endRunnable);
830 } else {
831 endRunnable.run();
832 }
833 mMessages.removeAll(messages);
834 mHistoricMessages.removeAll(messages);
835 }
836 }
837 }
838
839 private void updateTitleAndNamesDisplay() {
840 ArrayMap<CharSequence, String> uniqueNames = new ArrayMap<>();
841 ArrayMap<Character, CharSequence> uniqueCharacters = new ArrayMap<>();
842 for (int i = 0; i < mGroups.size(); i++) {
843 MessagingGroup group = mGroups.get(i);
844 CharSequence senderName = group.getSenderName();
845 if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)) {
846 continue;
847 }
848 if (!uniqueNames.containsKey(senderName)) {
849 // Only use visible characters to get uniqueNames
850 String pureSenderName = IGNORABLE_CHAR_PATTERN
851 .matcher(senderName).replaceAll("" /* replacement */);
852 char c = pureSenderName.charAt(0);
853 if (uniqueCharacters.containsKey(c)) {
854 // this character was already used, lets make it more unique. We first need to
855 // resolve the existing character if it exists
856 CharSequence existingName = uniqueCharacters.get(c);
857 if (existingName != null) {
858 uniqueNames.put(existingName, findNameSplit((String) existingName));
859 uniqueCharacters.put(c, null);
860 }
861 uniqueNames.put(senderName, findNameSplit((String) senderName));
862 } else {
863 uniqueNames.put(senderName, Character.toString(c));
864 uniqueCharacters.put(c, pureSenderName);
865 }
866 }
867 }
868
869 // Now that we have the correct symbols, let's look what we have cached
870 ArrayMap<CharSequence, Icon> cachedAvatars = new ArrayMap<>();
871 for (int i = 0; i < mGroups.size(); i++) {
872 // Let's now set the avatars
873 MessagingGroup group = mGroups.get(i);
874 boolean isOwnMessage = group.getSender() == mUser;
875 CharSequence senderName = group.getSenderName();
876 if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)
877 || (mIsOneToOne && mAvatarReplacement != null && !isOwnMessage)) {
878 continue;
879 }
880 String symbol = uniqueNames.get(senderName);
881 Icon cachedIcon = group.getAvatarSymbolIfMatching(senderName,
882 symbol, mLayoutColor);
883 if (cachedIcon != null) {
884 cachedAvatars.put(senderName, cachedIcon);
885 }
886 }
887
888 for (int i = 0; i < mGroups.size(); i++) {
889 // Let's now set the avatars
890 MessagingGroup group = mGroups.get(i);
891 CharSequence senderName = group.getSenderName();
892 if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)) {
893 continue;
894 }
895 if (mIsOneToOne && mAvatarReplacement != null && group.getSender() != mUser) {
896 group.setAvatar(mAvatarReplacement);
897 } else {
898 Icon cachedIcon = cachedAvatars.get(senderName);
899 if (cachedIcon == null) {
900 cachedIcon = createAvatarSymbol(senderName, uniqueNames.get(senderName),
901 mLayoutColor);
902 cachedAvatars.put(senderName, cachedIcon);
903 }
904 group.setCreatedAvatar(cachedIcon, senderName, uniqueNames.get(senderName),
905 mLayoutColor);
906 }
907 }
908 }
909
910 private Icon createAvatarSymbol(CharSequence senderName, String symbol, int layoutColor) {
911 if (symbol.isEmpty() || TextUtils.isDigitsOnly(symbol) ||
912 SPECIAL_CHAR_PATTERN.matcher(symbol).find()) {
913 Icon avatarIcon = Icon.createWithResource(getContext(),
914 R.drawable.messaging_user);
915 avatarIcon.setTint(findColor(senderName, layoutColor));
916 return avatarIcon;
917 } else {
918 Bitmap bitmap = Bitmap.createBitmap(mAvatarSize, mAvatarSize, Bitmap.Config.ARGB_8888);
919 Canvas canvas = new Canvas(bitmap);
920 float radius = mAvatarSize / 2.0f;
921 int color = findColor(senderName, layoutColor);
922 mPaint.setColor(color);
923 canvas.drawCircle(radius, radius, radius, mPaint);
924 boolean needDarkText = ColorUtils.calculateLuminance(color) > 0.5f;
925 mTextPaint.setColor(needDarkText ? Color.BLACK : Color.WHITE);
926 mTextPaint.setTextSize(symbol.length() == 1 ? mAvatarSize * 0.5f : mAvatarSize * 0.3f);
927 int yPos = (int) (radius - ((mTextPaint.descent() + mTextPaint.ascent()) / 2));
928 canvas.drawText(symbol, radius, yPos, mTextPaint);
929 return Icon.createWithBitmap(bitmap);
930 }
931 }
932
933 private int findColor(CharSequence senderName, int layoutColor) {
934 double luminance = ContrastColorUtil.calculateLuminance(layoutColor);
935 float shift = Math.abs(senderName.hashCode()) % 5 / 4.0f - 0.5f;
936
937 // we need to offset the range if the luminance is too close to the borders
938 shift += Math.max(COLOR_SHIFT_AMOUNT / 2.0f / 100 - luminance, 0);
939 shift -= Math.max(COLOR_SHIFT_AMOUNT / 2.0f / 100 - (1.0f - luminance), 0);
940 return ContrastColorUtil.getShiftedColor(layoutColor,
941 (int) (shift * COLOR_SHIFT_AMOUNT));
942 }
943
944 private String findNameSplit(String existingName) {
945 String[] split = existingName.split(" ");
946 if (split.length > 1) {
947 return Character.toString(split[0].charAt(0))
948 + Character.toString(split[1].charAt(0));
949 }
950 return existingName.substring(0, 1);
951 }
952
953 @RemotableViewMethod
954 public void setLayoutColor(int color) {
955 mLayoutColor = color;
956 }
957
958 @RemotableViewMethod
959 public void setIsOneToOne(boolean oneToOne) {
960 mIsOneToOne = oneToOne;
961 }
962
963 @RemotableViewMethod
964 public void setSenderTextColor(int color) {
965 mSenderTextColor = color;
Selim Cinek4237e822020-03-31 17:22:28 -0700966 mConversationText.setTextColor(color);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500967 }
968
Selim Cineke9714eb2020-03-09 11:21:49 -0700969 /**
970 * @param color the color of the notification background
971 */
972 @RemotableViewMethod
973 public void setNotificationBackgroundColor(int color) {
Selim Cinek8baa70f2020-03-09 20:09:35 -0700974 mNotificationBackgroundColor = color;
Selim Cinekd00d01a2020-03-23 19:00:26 -0700975 applyNotificationBackgroundColor(mConversationIconBadgeBg);
Selim Cinek8baa70f2020-03-09 20:09:35 -0700976 }
977
Selim Cinekd00d01a2020-03-23 19:00:26 -0700978 private void applyNotificationBackgroundColor(ImageView view) {
979 view.setImageTintList(ColorStateList.valueOf(mNotificationBackgroundColor));
Selim Cineke9714eb2020-03-09 11:21:49 -0700980 }
981
Selim Cinek20d1ee22020-02-03 16:04:26 -0500982 @RemotableViewMethod
983 public void setMessageTextColor(int color) {
984 mMessageTextColor = color;
985 }
986
987 private void setUser(Person user) {
988 mUser = user;
989 if (mUser.getIcon() == null) {
990 Icon userIcon = Icon.createWithResource(getContext(),
991 R.drawable.messaging_user);
992 userIcon.setTint(mLayoutColor);
993 mUser = mUser.toBuilder().setIcon(userIcon).build();
994 }
995 }
996
997 private void createGroupViews(List<List<MessagingMessage>> groups,
998 List<Person> senders, boolean showSpinner) {
999 mGroups.clear();
1000 for (int groupIndex = 0; groupIndex < groups.size(); groupIndex++) {
1001 List<MessagingMessage> group = groups.get(groupIndex);
1002 MessagingGroup newGroup = null;
1003 // we'll just take the first group that exists or create one there is none
1004 for (int messageIndex = group.size() - 1; messageIndex >= 0; messageIndex--) {
1005 MessagingMessage message = group.get(messageIndex);
1006 newGroup = message.getGroup();
1007 if (newGroup != null) {
1008 break;
1009 }
1010 }
1011 // Create a new group, adding it to the linear layout as well
1012 if (newGroup == null) {
1013 newGroup = MessagingGroup.createGroup(mMessagingLinearLayout);
1014 mAddedGroups.add(newGroup);
1015 }
Selim Cinek514b52c2020-03-17 18:42:12 -07001016 newGroup.setImageDisplayLocation(mIsCollapsed
1017 ? IMAGE_DISPLAY_LOCATION_EXTERNAL
1018 : IMAGE_DISPLAY_LOCATION_INLINE);
Selim Cineka91778a32020-03-13 17:30:34 -07001019 newGroup.setIsInConversation(true);
Selim Cinek20d1ee22020-02-03 16:04:26 -05001020 newGroup.setLayoutColor(mLayoutColor);
1021 newGroup.setTextColors(mSenderTextColor, mMessageTextColor);
1022 Person sender = senders.get(groupIndex);
1023 CharSequence nameOverride = null;
1024 if (sender != mUser && mNameReplacement != null) {
1025 nameOverride = mNameReplacement;
1026 }
1027 newGroup.setShowingAvatar(!mIsOneToOne && !mIsCollapsed);
1028 newGroup.setSingleLine(mIsCollapsed);
1029 newGroup.setSender(sender, nameOverride);
1030 newGroup.setSending(groupIndex == (groups.size() - 1) && showSpinner);
1031 mGroups.add(newGroup);
1032
1033 // Reposition to the correct place (if we're re-using a group)
1034 if (mMessagingLinearLayout.indexOfChild(newGroup) != groupIndex) {
1035 mMessagingLinearLayout.removeView(newGroup);
1036 mMessagingLinearLayout.addView(newGroup, groupIndex);
1037 }
1038 newGroup.setMessages(group);
1039 }
1040 }
1041
1042 private void findGroups(List<MessagingMessage> historicMessages,
1043 List<MessagingMessage> messages, List<List<MessagingMessage>> groups,
1044 List<Person> senders) {
1045 CharSequence currentSenderKey = null;
1046 List<MessagingMessage> currentGroup = null;
1047 int histSize = historicMessages.size();
1048 for (int i = 0; i < histSize + messages.size(); i++) {
1049 MessagingMessage message;
1050 if (i < histSize) {
1051 message = historicMessages.get(i);
1052 } else {
1053 message = messages.get(i - histSize);
1054 }
1055 boolean isNewGroup = currentGroup == null;
1056 Person sender = message.getMessage().getSenderPerson();
Selim Cinek857f2792020-03-03 19:06:21 -08001057 CharSequence key = getKey(sender);
Selim Cinek20d1ee22020-02-03 16:04:26 -05001058 isNewGroup |= !TextUtils.equals(key, currentSenderKey);
1059 if (isNewGroup) {
1060 currentGroup = new ArrayList<>();
1061 groups.add(currentGroup);
1062 if (sender == null) {
1063 sender = mUser;
Steve Elliott242057d2020-06-10 15:15:19 -04001064 } else {
1065 // Remove all formatting from the sender name
1066 sender = sender.toBuilder().setName(Objects.toString(sender.getName())).build();
Selim Cinek20d1ee22020-02-03 16:04:26 -05001067 }
1068 senders.add(sender);
1069 currentSenderKey = key;
1070 }
1071 currentGroup.add(message);
1072 }
1073 }
1074
Selim Cinek857f2792020-03-03 19:06:21 -08001075 private CharSequence getKey(Person person) {
1076 return person == null ? null : person.getKey() == null ? person.getName() : person.getKey();
1077 }
1078
Selim Cinek20d1ee22020-02-03 16:04:26 -05001079 /**
1080 * Creates new messages, reusing existing ones if they are available.
1081 *
1082 * @param newMessages the messages to parse.
1083 */
1084 private List<MessagingMessage> createMessages(
1085 List<Notification.MessagingStyle.Message> newMessages, boolean historic) {
1086 List<MessagingMessage> result = new ArrayList<>();
1087 for (int i = 0; i < newMessages.size(); i++) {
1088 Notification.MessagingStyle.Message m = newMessages.get(i);
1089 MessagingMessage message = findAndRemoveMatchingMessage(m);
1090 if (message == null) {
1091 message = MessagingMessage.createMessage(this, m, mImageResolver);
1092 }
1093 message.setIsHistoric(historic);
1094 result.add(message);
1095 }
1096 return result;
1097 }
1098
1099 private MessagingMessage findAndRemoveMatchingMessage(Notification.MessagingStyle.Message m) {
1100 for (int i = 0; i < mMessages.size(); i++) {
1101 MessagingMessage existing = mMessages.get(i);
1102 if (existing.sameAs(m)) {
1103 mMessages.remove(i);
1104 return existing;
1105 }
1106 }
1107 for (int i = 0; i < mHistoricMessages.size(); i++) {
1108 MessagingMessage existing = mHistoricMessages.get(i);
1109 if (existing.sameAs(m)) {
1110 mHistoricMessages.remove(i);
1111 return existing;
1112 }
1113 }
1114 return null;
1115 }
1116
1117 public void showHistoricMessages(boolean show) {
1118 mShowHistoricMessages = show;
1119 updateHistoricMessageVisibility();
1120 }
1121
1122 private void updateHistoricMessageVisibility() {
1123 int numHistoric = mHistoricMessages.size();
1124 for (int i = 0; i < numHistoric; i++) {
1125 MessagingMessage existing = mHistoricMessages.get(i);
1126 existing.setVisibility(mShowHistoricMessages ? VISIBLE : GONE);
1127 }
1128 int numGroups = mGroups.size();
1129 for (int i = 0; i < numGroups; i++) {
1130 MessagingGroup group = mGroups.get(i);
1131 int visibleChildren = 0;
1132 List<MessagingMessage> messages = group.getMessages();
1133 int numGroupMessages = messages.size();
1134 for (int j = 0; j < numGroupMessages; j++) {
1135 MessagingMessage message = messages.get(j);
1136 if (message.getVisibility() != GONE) {
1137 visibleChildren++;
1138 }
1139 }
1140 if (visibleChildren > 0 && group.getVisibility() == GONE) {
1141 group.setVisibility(VISIBLE);
1142 } else if (visibleChildren == 0 && group.getVisibility() != GONE) {
1143 group.setVisibility(GONE);
1144 }
1145 }
1146 }
1147
1148 @Override
1149 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
1150 super.onLayout(changed, left, top, right, bottom);
1151 if (!mAddedGroups.isEmpty()) {
1152 getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
1153 @Override
1154 public boolean onPreDraw() {
1155 for (MessagingGroup group : mAddedGroups) {
1156 if (!group.isShown()) {
1157 continue;
1158 }
1159 MessagingPropertyAnimator.fadeIn(group.getAvatar());
1160 MessagingPropertyAnimator.fadeIn(group.getSenderView());
1161 MessagingPropertyAnimator.startLocalTranslationFrom(group,
1162 group.getHeight(), LINEAR_OUT_SLOW_IN);
1163 }
1164 mAddedGroups.clear();
1165 getViewTreeObserver().removeOnPreDrawListener(this);
1166 return true;
1167 }
1168 });
1169 }
Selim Cinek4237e822020-03-31 17:22:28 -07001170 if (mAppOps.getWidth() > 0) {
1171
1172 // Let's increase the touch size of the app ops view if it's here
1173 mAppOpsTouchRect.set(
1174 mAppOps.getLeft(),
1175 mAppOps.getTop(),
1176 mAppOps.getRight(),
1177 mAppOps.getBottom());
1178 for (int i = 0; i < mAppOps.getChildCount(); i++) {
1179 View child = mAppOps.getChildAt(i);
1180 if (child.getVisibility() == GONE) {
1181 continue;
1182 }
1183 // Make sure each child has at least a minTouchSize touch target around it
1184 float childTouchLeft = child.getLeft() + child.getWidth() / 2.0f
1185 - mMinTouchSize / 2.0f;
1186 float childTouchRight = childTouchLeft + mMinTouchSize;
1187 mAppOpsTouchRect.left = (int) Math.min(mAppOpsTouchRect.left,
1188 mAppOps.getLeft() + childTouchLeft);
1189 mAppOpsTouchRect.right = (int) Math.max(mAppOpsTouchRect.right,
1190 mAppOps.getLeft() + childTouchRight);
1191 }
1192
1193 // Increase the height
1194 int heightIncrease = 0;
1195 if (mAppOpsTouchRect.height() < mMinTouchSize) {
1196 heightIncrease = (int) Math.ceil((mMinTouchSize - mAppOpsTouchRect.height())
1197 / 2.0f);
1198 }
1199 mAppOpsTouchRect.inset(0, -heightIncrease);
1200
1201 // Let's adjust the hitrect since app ops isn't a direct child
1202 ViewGroup viewGroup = (ViewGroup) mAppOps.getParent();
1203 while (viewGroup != this) {
1204 mAppOpsTouchRect.offset(viewGroup.getLeft(), viewGroup.getTop());
1205 viewGroup = (ViewGroup) viewGroup.getParent();
1206 }
1207 //
1208 // Extend the size of the app opps to be at least 48dp
1209 setTouchDelegate(new TouchDelegate(mAppOpsTouchRect, mAppOps));
1210 }
Selim Cinek20d1ee22020-02-03 16:04:26 -05001211 }
1212
1213 public MessagingLinearLayout getMessagingLinearLayout() {
1214 return mMessagingLinearLayout;
1215 }
1216
Selim Cinek514b52c2020-03-17 18:42:12 -07001217 public @NonNull ViewGroup getImageMessageContainer() {
1218 return mImageMessageContainer;
1219 }
1220
Selim Cinek20d1ee22020-02-03 16:04:26 -05001221 public ArrayList<MessagingGroup> getMessagingGroups() {
1222 return mGroups;
1223 }
1224
1225 private void updateExpandButton() {
1226 int drawableId;
1227 int contentDescriptionId;
1228 int gravity;
1229 int topMargin = 0;
Selim Cinek54fe9ad2020-03-04 13:56:42 -08001230 ViewGroup newContainer;
Selim Cinek20d1ee22020-02-03 16:04:26 -05001231 if (mIsCollapsed) {
1232 drawableId = R.drawable.ic_expand_notification;
1233 contentDescriptionId = R.string.expand_button_content_description_collapsed;
1234 gravity = Gravity.CENTER;
Selim Cinek54fe9ad2020-03-04 13:56:42 -08001235 newContainer = mExpandButtonAndContentContainer;
Selim Cinek20d1ee22020-02-03 16:04:26 -05001236 } else {
1237 drawableId = R.drawable.ic_collapse_notification;
1238 contentDescriptionId = R.string.expand_button_content_description_expanded;
1239 gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
1240 topMargin = mExpandButtonExpandedTopMargin;
Selim Cinek54fe9ad2020-03-04 13:56:42 -08001241 newContainer = this;
Selim Cinek20d1ee22020-02-03 16:04:26 -05001242 }
1243 mExpandButton.setImageDrawable(getContext().getDrawable(drawableId));
1244 mExpandButton.setColorFilter(mExpandButton.getOriginalNotificationColor());
1245
Selim Cinek54fe9ad2020-03-04 13:56:42 -08001246 // We need to make sure that the expand button is in the linearlayout pushing over the
1247 // content when collapsed, but allows the content to flow under it when expanded.
1248 if (newContainer != mExpandButtonContainer.getParent()) {
1249 ((ViewGroup) mExpandButtonContainer.getParent()).removeView(mExpandButtonContainer);
1250 newContainer.addView(mExpandButtonContainer);
Selim Cinek54fe9ad2020-03-04 13:56:42 -08001251 }
1252
Selim Cinek20d1ee22020-02-03 16:04:26 -05001253 // update if the expand button is centered
Selim Cinek514b52c2020-03-17 18:42:12 -07001254 LinearLayout.LayoutParams layoutParams =
1255 (LinearLayout.LayoutParams) mExpandButton.getLayoutParams();
Selim Cinek20d1ee22020-02-03 16:04:26 -05001256 layoutParams.gravity = gravity;
1257 layoutParams.topMargin = topMargin;
1258 mExpandButton.setLayoutParams(layoutParams);
1259
Selim Cinek28db07e2020-04-03 17:33:24 -07001260 mExpandButtonInnerContainer.setContentDescription(mContext.getText(contentDescriptionId));
Selim Cinekafc20582020-03-13 20:15:38 -07001261 }
Selim Cinek54fe9ad2020-03-04 13:56:42 -08001262
Selim Cinekbf322ee2020-03-20 20:20:31 -07001263 private void updateContentEndPaddings() {
Selim Cinekafc20582020-03-13 20:15:38 -07001264 // Let's make sure the conversation header can't run into the expand button when we're
1265 // collapsed and update the paddings of the content
1266 int headerPaddingEnd;
1267 int contentPaddingEnd;
1268 if (!mExpandable) {
1269 headerPaddingEnd = 0;
1270 contentPaddingEnd = mContentMarginEnd;
1271 } else if (mIsCollapsed) {
1272 headerPaddingEnd = 0;
1273 contentPaddingEnd = 0;
1274 } else {
1275 headerPaddingEnd = mNotificationHeaderExpandedPadding;
1276 contentPaddingEnd = mContentMarginEnd;
1277 }
1278 mConversationHeader.setPaddingRelative(
1279 mConversationHeader.getPaddingStart(),
1280 mConversationHeader.getPaddingTop(),
1281 headerPaddingEnd,
1282 mConversationHeader.getPaddingBottom());
1283
1284 mContentContainer.setPaddingRelative(
1285 mContentContainer.getPaddingStart(),
1286 mContentContainer.getPaddingTop(),
1287 contentPaddingEnd,
1288 mContentContainer.getPaddingBottom());
Selim Cinek20d1ee22020-02-03 16:04:26 -05001289 }
1290
Selim Cinekbf322ee2020-03-20 20:20:31 -07001291 private void onAppNameVisibilityChanged() {
1292 boolean appNameGone = mAppName.getVisibility() == GONE;
1293 if (appNameGone != mAppNameGone) {
1294 mAppNameGone = appNameGone;
Steve Elliottf7ef4ef2020-04-27 15:40:18 -04001295 updateAppNameDividerVisibility();
Selim Cinekbf322ee2020-03-20 20:20:31 -07001296 }
1297 }
1298
Steve Elliottf7ef4ef2020-04-27 15:40:18 -04001299 private void updateAppNameDividerVisibility() {
1300 mAppNameDivider.setVisibility(mAppNameGone ? GONE : VISIBLE);
1301 }
1302
Selim Cinek20d1ee22020-02-03 16:04:26 -05001303 public void updateExpandability(boolean expandable, @Nullable OnClickListener onClickListener) {
Selim Cinekafc20582020-03-13 20:15:38 -07001304 mExpandable = expandable;
Selim Cinek20d1ee22020-02-03 16:04:26 -05001305 if (expandable) {
1306 mExpandButtonContainer.setVisibility(VISIBLE);
Selim Cinek28db07e2020-04-03 17:33:24 -07001307 mExpandButtonInnerContainer.setOnClickListener(onClickListener);
Selim Cinek20d1ee22020-02-03 16:04:26 -05001308 } else {
Selim Cinek20d1ee22020-02-03 16:04:26 -05001309 mExpandButtonContainer.setVisibility(GONE);
1310 }
Selim Cinekbf322ee2020-03-20 20:20:31 -07001311 updateContentEndPaddings();
Selim Cinek20d1ee22020-02-03 16:04:26 -05001312 }
Selim Cinek514b52c2020-03-17 18:42:12 -07001313
1314 @Override
1315 public void setMessagingClippingDisabled(boolean clippingDisabled) {
1316 mMessagingLinearLayout.setClipBounds(clippingDisabled ? null : mMessagingClipRect);
1317 }
Steve Elliott936df152020-04-14 13:59:53 -04001318
1319 @Nullable
1320 public CharSequence getConversationSenderName() {
1321 if (mGroups.isEmpty()) {
1322 return null;
1323 }
1324 final CharSequence name = mGroups.get(mGroups.size() - 1).getSenderName();
1325 return getResources().getString(R.string.conversation_single_line_name_display, name);
1326 }
1327
1328 public boolean isOneToOne() {
1329 return mIsOneToOne;
1330 }
1331
1332 @Nullable
1333 public CharSequence getConversationText() {
1334 if (mMessages.isEmpty()) {
1335 return null;
1336 }
1337 final MessagingMessage messagingMessage = mMessages.get(mMessages.size() - 1);
1338 final CharSequence text = messagingMessage.getMessage().getText();
1339 if (text == null && messagingMessage instanceof MessagingImageMessage) {
1340 final String unformatted =
1341 getResources().getString(R.string.conversation_single_line_image_placeholder);
1342 SpannableString spannableString = new SpannableString(unformatted);
1343 spannableString.setSpan(
1344 new StyleSpan(Typeface.ITALIC),
1345 0,
1346 spannableString.length(),
1347 Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
1348 return spannableString;
1349 }
1350 return text;
1351 }
1352
1353 @Nullable
1354 public Icon getConversationIcon() {
1355 return mConversationIcon;
1356 }
Selim Cinek20d1ee22020-02-03 16:04:26 -05001357}