blob: c28e3ad5052efb05fa5f84d75406766fe045191c [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;
21
Selim Cinek20d1ee22020-02-03 16:04:26 -050022import android.annotation.AttrRes;
23import android.annotation.NonNull;
24import android.annotation.Nullable;
25import android.annotation.StyleRes;
26import android.app.Notification;
27import android.app.Person;
28import android.app.RemoteInputHistoryItem;
29import android.content.Context;
Selim Cineke9714eb2020-03-09 11:21:49 -070030import android.content.res.ColorStateList;
Selim Cinek20d1ee22020-02-03 16:04:26 -050031import android.graphics.Bitmap;
32import android.graphics.Canvas;
33import android.graphics.Color;
34import android.graphics.Paint;
35import android.graphics.Rect;
36import android.graphics.drawable.Icon;
37import android.os.Bundle;
38import android.os.Parcelable;
39import android.text.TextUtils;
40import android.util.ArrayMap;
Selim Cinek514b52c2020-03-17 18:42:12 -070041import android.util.ArraySet;
Selim Cinek20d1ee22020-02-03 16:04:26 -050042import android.util.AttributeSet;
43import android.util.DisplayMetrics;
44import android.view.Gravity;
45import android.view.RemotableViewMethod;
46import android.view.View;
47import android.view.ViewGroup;
48import android.view.ViewTreeObserver;
49import android.view.animation.Interpolator;
50import android.view.animation.PathInterpolator;
51import android.widget.FrameLayout;
52import android.widget.ImageView;
Selim Cinek514b52c2020-03-17 18:42:12 -070053import android.widget.LinearLayout;
Selim Cinek20d1ee22020-02-03 16:04:26 -050054import android.widget.RemoteViews;
55import android.widget.TextView;
56
57import com.android.internal.R;
58import com.android.internal.graphics.ColorUtils;
59import com.android.internal.util.ContrastColorUtil;
60
61import java.util.ArrayList;
62import java.util.List;
63import java.util.function.Consumer;
64import java.util.regex.Pattern;
65
66/**
67 * A custom-built layout for the Notification.MessagingStyle allows dynamic addition and removal
68 * messages and adapts the layout accordingly.
69 */
70@RemoteViews.RemoteView
71public class ConversationLayout extends FrameLayout
72 implements ImageMessageConsumer, IMessagingLayout {
73
Selim Cinek20d1ee22020-02-03 16:04:26 -050074 private static final float COLOR_SHIFT_AMOUNT = 60;
75 /**
76 * Pattren for filter some ingonable characters.
77 * p{Z} for any kind of whitespace or invisible separator.
78 * p{C} for any kind of punctuation character.
79 */
80 private static final Pattern IGNORABLE_CHAR_PATTERN
81 = Pattern.compile("[\\p{C}\\p{Z}]");
82 private static final Pattern SPECIAL_CHAR_PATTERN
83 = Pattern.compile ("[!@#$%&*()_+=|<>?{}\\[\\]~-]");
84 private static final Consumer<MessagingMessage> REMOVE_MESSAGE
85 = MessagingMessage::removeMessage;
86 public static final Interpolator LINEAR_OUT_SLOW_IN = new PathInterpolator(0f, 0f, 0.2f, 1f);
87 public static final Interpolator FAST_OUT_LINEAR_IN = new PathInterpolator(0.4f, 0f, 1f, 1f);
88 public static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f);
89 public static final OnLayoutChangeListener MESSAGING_PROPERTY_ANIMATOR
90 = new MessagingPropertyAnimator();
91 private List<MessagingMessage> mMessages = new ArrayList<>();
92 private List<MessagingMessage> mHistoricMessages = new ArrayList<>();
93 private MessagingLinearLayout mMessagingLinearLayout;
94 private boolean mShowHistoricMessages;
95 private ArrayList<MessagingGroup> mGroups = new ArrayList<>();
Selim Cinek20d1ee22020-02-03 16:04:26 -050096 private int mLayoutColor;
97 private int mSenderTextColor;
98 private int mMessageTextColor;
99 private int mAvatarSize;
100 private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
101 private Paint mTextPaint = new Paint();
102 private Icon mAvatarReplacement;
103 private boolean mIsOneToOne;
104 private ArrayList<MessagingGroup> mAddedGroups = new ArrayList<>();
105 private Person mUser;
106 private CharSequence mNameReplacement;
107 private boolean mIsCollapsed;
108 private ImageResolver mImageResolver;
109 private ImageView mConversationIcon;
Selim Cinekbf322ee2020-03-20 20:20:31 -0700110 private View mConversationIconContainer;
111 private int mConversationIconTopPadding;
112 private int mConversationIconTopPaddingExpandedGroup;
113 private int mConversationIconTopPaddingNoAppName;
114 private int mExpandedGroupMessagePaddingNoAppName;
Selim Cinek2f7f7b82020-03-10 15:41:13 -0700115 private TextView mConversationText;
Selim Cinek20d1ee22020-02-03 16:04:26 -0500116 private View mConversationIconBadge;
117 private Icon mLargeIcon;
118 private View mExpandButtonContainer;
Selim Cinek54fe9ad2020-03-04 13:56:42 -0800119 private ViewGroup mExpandButtonAndContentContainer;
Selim Cinek20d1ee22020-02-03 16:04:26 -0500120 private NotificationExpandButton mExpandButton;
Selim Cinek514b52c2020-03-17 18:42:12 -0700121 private MessagingLinearLayout mImageMessageContainer;
Selim Cinek20d1ee22020-02-03 16:04:26 -0500122 private int mExpandButtonExpandedTopMargin;
123 private int mBadgedSideMargins;
Selim Cinekbf322ee2020-03-20 20:20:31 -0700124 private int mConversationAvatarSize;
125 private int mConversationAvatarSizeExpanded;
Selim Cinekf2b8aa72020-03-05 15:29:02 -0800126 private CachingIconView mIcon;
Steve Elliott928bb162020-03-17 17:52:53 -0400127 private View mImportanceRingView;
Selim Cinekbf322ee2020-03-20 20:20:31 -0700128 private int mExpandedGroupSideMargin;
129 private int mExpandedGroupSideMarginFacePile;
Selim Cinek8baa70f2020-03-09 20:09:35 -0700130 private View mConversationFacePile;
131 private int mNotificationBackgroundColor;
Selim Cinek2f7f7b82020-03-10 15:41:13 -0700132 private CharSequence mFallbackChatName;
133 private CharSequence mFallbackGroupChatName;
134 private CharSequence mConversationTitle;
Selim Cinekafc20582020-03-13 20:15:38 -0700135 private int mNotificationHeaderExpandedPadding;
136 private View mConversationHeader;
137 private View mContentContainer;
138 private boolean mExpandable = true;
139 private int mContentMarginEnd;
Selim Cinek514b52c2020-03-17 18:42:12 -0700140 private Rect mMessagingClipRect;
Selim Cinekbf322ee2020-03-20 20:20:31 -0700141 private ObservableTextView mAppName;
142 private boolean mAppNameGone;
143 private int mFacePileAvatarSize;
144 private int mFacePileAvatarSizeExpandedGroup;
145 private int mFacePileProtectionWidth;
146 private int mFacePileProtectionWidthExpanded;
Selim Cinek20d1ee22020-02-03 16:04:26 -0500147
148 public ConversationLayout(@NonNull Context context) {
149 super(context);
150 }
151
152 public ConversationLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
153 super(context, attrs);
154 }
155
156 public ConversationLayout(@NonNull Context context, @Nullable AttributeSet attrs,
157 @AttrRes int defStyleAttr) {
158 super(context, attrs, defStyleAttr);
159 }
160
161 public ConversationLayout(@NonNull Context context, @Nullable AttributeSet attrs,
162 @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
163 super(context, attrs, defStyleAttr, defStyleRes);
164 }
165
166 @Override
167 protected void onFinishInflate() {
168 super.onFinishInflate();
169 mMessagingLinearLayout = findViewById(R.id.notification_messaging);
170 mMessagingLinearLayout.setMessagingLayout(this);
Selim Cinek514b52c2020-03-17 18:42:12 -0700171 mImageMessageContainer = findViewById(R.id.conversation_image_message_container);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500172 // We still want to clip, but only on the top, since views can temporarily out of bounds
173 // during transitions.
174 DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
175 int size = Math.max(displayMetrics.widthPixels, displayMetrics.heightPixels);
Selim Cinek514b52c2020-03-17 18:42:12 -0700176 mMessagingClipRect = new Rect(0, 0, size, size);
177 setMessagingClippingDisabled(false);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500178 mAvatarSize = getResources().getDimensionPixelSize(R.dimen.messaging_avatar_size);
179 mTextPaint.setTextAlign(Paint.Align.CENTER);
180 mTextPaint.setAntiAlias(true);
181 mConversationIcon = findViewById(R.id.conversation_icon);
Selim Cinekbf322ee2020-03-20 20:20:31 -0700182 mConversationIconContainer = findViewById(R.id.conversation_icon_container);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500183 mIcon = findViewById(R.id.icon);
Steve Elliott928bb162020-03-17 17:52:53 -0400184 mImportanceRingView = findViewById(R.id.conversation_icon_badge_ring);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500185 mConversationIconBadge = findViewById(R.id.conversation_icon_badge);
Selim Cinekf2b8aa72020-03-05 15:29:02 -0800186 mIcon.setOnVisibilityChangedListener((visibility) -> {
187 // Always keep the badge visibility in sync with the icon. This is necessary in cases
188 // Where the icon is being hidden externally like in group children.
189 mConversationIconBadge.setVisibility(visibility);
190 });
Selim Cinek2f7f7b82020-03-10 15:41:13 -0700191 mConversationText = findViewById(R.id.conversation_text);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500192 mExpandButtonContainer = findViewById(R.id.expand_button_container);
Selim Cinekafc20582020-03-13 20:15:38 -0700193 mConversationHeader = findViewById(R.id.conversation_header);
194 mContentContainer = findViewById(R.id.notification_action_list_margin_target);
Selim Cinek54fe9ad2020-03-04 13:56:42 -0800195 mExpandButtonAndContentContainer = findViewById(R.id.expand_button_and_content_container);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500196 mExpandButton = findViewById(R.id.expand_button);
197 mExpandButtonExpandedTopMargin = getResources().getDimensionPixelSize(
198 R.dimen.conversation_expand_button_top_margin_expanded);
Selim Cinekafc20582020-03-13 20:15:38 -0700199 mNotificationHeaderExpandedPadding = getResources().getDimensionPixelSize(
200 R.dimen.conversation_header_expanded_padding_end);
201 mContentMarginEnd = getResources().getDimensionPixelSize(
202 R.dimen.notification_content_margin_end);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500203 mBadgedSideMargins = getResources().getDimensionPixelSize(
204 R.dimen.conversation_badge_side_margin);
Selim Cinekbf322ee2020-03-20 20:20:31 -0700205 mConversationAvatarSize = getResources().getDimensionPixelSize(
206 R.dimen.conversation_avatar_size);
207 mConversationAvatarSizeExpanded = getResources().getDimensionPixelSize(
208 R.dimen.conversation_avatar_size_group_expanded);
209 mConversationIconTopPadding = getResources().getDimensionPixelSize(
210 R.dimen.conversation_icon_container_top_padding);
211 mConversationIconTopPaddingExpandedGroup = getResources().getDimensionPixelSize(
212 R.dimen.conversation_icon_container_top_padding_small_avatar);
213 mConversationIconTopPaddingNoAppName = getResources().getDimensionPixelSize(
214 R.dimen.conversation_icon_container_top_padding_no_app_name);
215 mExpandedGroupMessagePaddingNoAppName = getResources().getDimensionPixelSize(
216 R.dimen.expanded_group_conversation_message_padding_without_app_name);
217 mExpandedGroupSideMargin = getResources().getDimensionPixelSize(
218 R.dimen.conversation_badge_side_margin_group_expanded);
219 mExpandedGroupSideMarginFacePile = getResources().getDimensionPixelSize(
220 R.dimen.conversation_badge_side_margin_group_expanded_face_pile);
Selim Cinek8baa70f2020-03-09 20:09:35 -0700221 mConversationFacePile = findViewById(R.id.conversation_face_pile);
Selim Cinekbf322ee2020-03-20 20:20:31 -0700222 mFacePileAvatarSize = getResources().getDimensionPixelSize(
223 R.dimen.conversation_face_pile_avatar_size);
224 mFacePileAvatarSizeExpandedGroup = getResources().getDimensionPixelSize(
225 R.dimen.conversation_face_pile_avatar_size_group_expanded);
226 mFacePileProtectionWidth = getResources().getDimensionPixelSize(
227 R.dimen.conversation_face_pile_protection_width);
228 mFacePileProtectionWidthExpanded = getResources().getDimensionPixelSize(
229 R.dimen.conversation_face_pile_protection_width_expanded);
Selim Cinek2f7f7b82020-03-10 15:41:13 -0700230 mFallbackChatName = getResources().getString(
231 R.string.conversation_title_fallback_one_to_one);
232 mFallbackGroupChatName = getResources().getString(
233 R.string.conversation_title_fallback_group_chat);
Steve Elliott52440e92020-03-19 15:18:58 -0400234 mAppName = findViewById(R.id.app_name_text);
Selim Cinekbf322ee2020-03-20 20:20:31 -0700235 mAppNameGone = mAppName.getVisibility() == GONE;
236 mAppName.setOnVisibilityChangedListener((visibility) -> {
237 onAppNameVisibilityChanged();
238 });
Selim Cinek20d1ee22020-02-03 16:04:26 -0500239 }
240
241 @RemotableViewMethod
242 public void setAvatarReplacement(Icon icon) {
243 mAvatarReplacement = icon;
244 }
245
246 @RemotableViewMethod
247 public void setNameReplacement(CharSequence nameReplacement) {
248 mNameReplacement = nameReplacement;
249 }
250
251 /**
Steve Elliott928bb162020-03-17 17:52:53 -0400252 * Sets this conversation as "important", adding some additional UI treatment.
253 */
254 @RemotableViewMethod
255 public void setIsImportantConversation(boolean isImportantConversation) {
256 mImportanceRingView.setVisibility(isImportantConversation ? VISIBLE : GONE);
257 }
258
259 /**
Selim Cinek20d1ee22020-02-03 16:04:26 -0500260 * Set this layout to show the collapsed representation.
261 *
262 * @param isCollapsed is it collapsed
263 */
264 @RemotableViewMethod
265 public void setIsCollapsed(boolean isCollapsed) {
266 mIsCollapsed = isCollapsed;
267 mMessagingLinearLayout.setMaxDisplayedLines(isCollapsed ? 1 : Integer.MAX_VALUE);
268 updateExpandButton();
Selim Cinekbf322ee2020-03-20 20:20:31 -0700269 updateContentEndPaddings();
Selim Cinek20d1ee22020-02-03 16:04:26 -0500270 }
271
272 @RemotableViewMethod
273 public void setData(Bundle extras) {
274 Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES);
275 List<Notification.MessagingStyle.Message> newMessages
276 = Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages);
277 Parcelable[] histMessages = extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES);
278 List<Notification.MessagingStyle.Message> newHistoricMessages
279 = Notification.MessagingStyle.Message.getMessagesFromBundleArray(histMessages);
280
281 // mUser now set (would be nice to avoid the side effect but WHATEVER)
282 setUser(extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON));
283
284
285 // Append remote input history to newMessages (again, side effect is lame but WHATEVS)
286 RemoteInputHistoryItem[] history = (RemoteInputHistoryItem[])
287 extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
288 addRemoteInputHistoryToMessages(newMessages, history);
289
290 boolean showSpinner =
291 extras.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false);
292
293 // bind it, baby
294 bind(newMessages, newHistoricMessages, showSpinner);
295 }
296
297 @Override
298 public void setImageResolver(ImageResolver resolver) {
299 mImageResolver = resolver;
300 }
301
302 private void addRemoteInputHistoryToMessages(
303 List<Notification.MessagingStyle.Message> newMessages,
304 RemoteInputHistoryItem[] remoteInputHistory) {
305 if (remoteInputHistory == null || remoteInputHistory.length == 0) {
306 return;
307 }
308 for (int i = remoteInputHistory.length - 1; i >= 0; i--) {
309 RemoteInputHistoryItem historyMessage = remoteInputHistory[i];
310 Notification.MessagingStyle.Message message = new Notification.MessagingStyle.Message(
311 historyMessage.getText(), 0, (Person) null, true /* remoteHistory */);
312 if (historyMessage.getUri() != null) {
313 message.setData(historyMessage.getMimeType(), historyMessage.getUri());
314 }
315 newMessages.add(message);
316 }
317 }
318
319 private void bind(List<Notification.MessagingStyle.Message> newMessages,
320 List<Notification.MessagingStyle.Message> newHistoricMessages,
321 boolean showSpinner) {
322 // convert MessagingStyle.Message to MessagingMessage, re-using ones from a previous binding
323 // if they exist
324 List<MessagingMessage> historicMessages = createMessages(newHistoricMessages,
325 true /* isHistoric */);
326 List<MessagingMessage> messages = createMessages(newMessages, false /* isHistoric */);
327
328 // Copy our groups, before they get clobbered
329 ArrayList<MessagingGroup> oldGroups = new ArrayList<>(mGroups);
330
331 // Add our new MessagingMessages to groups
332 List<List<MessagingMessage>> groups = new ArrayList<>();
333 List<Person> senders = new ArrayList<>();
334
335 // Lets first find the groups (populate `groups` and `senders`)
336 findGroups(historicMessages, messages, groups, senders);
337
338 // Let's now create the views and reorder them accordingly
339 // side-effect: updates mGroups, mAddedGroups
340 createGroupViews(groups, senders, showSpinner);
341
342 // Let's first check which groups were removed altogether and remove them in one animation
343 removeGroups(oldGroups);
344
345 // Let's remove the remaining messages
346 mMessages.forEach(REMOVE_MESSAGE);
347 mHistoricMessages.forEach(REMOVE_MESSAGE);
348
349 mMessages = messages;
350 mHistoricMessages = historicMessages;
351
352 updateHistoricMessageVisibility();
353 updateTitleAndNamesDisplay();
354
Selim Cinek857f2792020-03-03 19:06:21 -0800355 updateConversationLayout();
Selim Cinek20d1ee22020-02-03 16:04:26 -0500356 }
357
Selim Cinek857f2792020-03-03 19:06:21 -0800358 /**
359 * Update the layout according to the data provided (i.e mIsOneToOne, expanded etc);
360 */
361 private void updateConversationLayout() {
Selim Cinek20d1ee22020-02-03 16:04:26 -0500362 // Set avatar and name
Selim Cinek2f7f7b82020-03-10 15:41:13 -0700363 CharSequence conversationText = mConversationTitle;
364 // TODO: display the secondary text somewhere
Selim Cinek20d1ee22020-02-03 16:04:26 -0500365 if (mIsOneToOne) {
366 // Let's resolve the icon / text from the last sender
367 mConversationIcon.setVisibility(VISIBLE);
Selim Cinek8baa70f2020-03-09 20:09:35 -0700368 mConversationFacePile.setVisibility(GONE);
Selim Cinek857f2792020-03-03 19:06:21 -0800369 CharSequence userKey = getKey(mUser);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500370 for (int i = mGroups.size() - 1; i >= 0; i--) {
371 MessagingGroup messagingGroup = mGroups.get(i);
372 Person messageSender = messagingGroup.getSender();
Selim Cinek857f2792020-03-03 19:06:21 -0800373 if ((messageSender != null && !TextUtils.equals(userKey, getKey(messageSender)))
374 || i == 0) {
Selim Cinek2f7f7b82020-03-10 15:41:13 -0700375 if (TextUtils.isEmpty(conversationText)) {
376 // We use the sendername as header text if no conversation title is provided
377 // (This usually happens for most 1:1 conversations)
378 conversationText = messagingGroup.getSenderName();
379 }
380 Icon avatarIcon = messagingGroup.getAvatarIcon();
381 if (avatarIcon == null) {
382 avatarIcon = createAvatarSymbol(conversationText, "", mLayoutColor);
383 }
384 mConversationIcon.setImageIcon(avatarIcon);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500385 break;
386 }
387 }
Selim Cinek20d1ee22020-02-03 16:04:26 -0500388 } else {
Selim Cinekbf322ee2020-03-20 20:20:31 -0700389 if (mLargeIcon != null) {
390 mConversationIcon.setVisibility(VISIBLE);
Selim Cinek8baa70f2020-03-09 20:09:35 -0700391 mConversationFacePile.setVisibility(GONE);
Selim Cinekbf322ee2020-03-20 20:20:31 -0700392 mConversationIcon.setImageIcon(mLargeIcon);
393 } else {
Selim Cinek20d1ee22020-02-03 16:04:26 -0500394 mConversationIcon.setVisibility(GONE);
Selim Cinekbf322ee2020-03-20 20:20:31 -0700395 // This will also inflate it!
396 mConversationFacePile.setVisibility(VISIBLE);
397 // rebind the value to the inflated view instead of the stub
398 mConversationFacePile = findViewById(R.id.conversation_face_pile);
399 bindFacePile();
Selim Cinek20d1ee22020-02-03 16:04:26 -0500400 }
401 }
Selim Cinek2f7f7b82020-03-10 15:41:13 -0700402 if (TextUtils.isEmpty(conversationText)) {
403 conversationText = mIsOneToOne ? mFallbackChatName : mFallbackGroupChatName;
404 }
405 mConversationText.setText(conversationText);
Selim Cinek857f2792020-03-03 19:06:21 -0800406 // Update if the groups can hide the sender if they are first (applies to 1:1 conversations)
407 // This needs to happen after all of the above o update all of the groups
408 for (int i = mGroups.size() - 1; i >= 0; i--) {
409 MessagingGroup messagingGroup = mGroups.get(i);
410 CharSequence messageSender = messagingGroup.getSenderName();
411 boolean canHide = mIsOneToOne
Selim Cinek2f7f7b82020-03-10 15:41:13 -0700412 && TextUtils.equals(conversationText, messageSender);
Selim Cinek857f2792020-03-03 19:06:21 -0800413 messagingGroup.setCanHideSenderIfFirst(canHide);
414 }
Selim Cinekbf322ee2020-03-20 20:20:31 -0700415 updateAppName();
Selim Cinek857f2792020-03-03 19:06:21 -0800416 updateIconPositionAndSize();
Selim Cinek514b52c2020-03-17 18:42:12 -0700417 updateImageMessages();
Selim Cinekbf322ee2020-03-20 20:20:31 -0700418 updatePaddingsBasedOnContentAvailability();
Selim Cinek514b52c2020-03-17 18:42:12 -0700419 }
420
421 private void updateImageMessages() {
422 boolean displayExternalImage = false;
423 ArraySet<View> newMessages = new ArraySet<>();
424 if (mIsCollapsed) {
425
426 // When collapsed, we're displaying all image messages in a dedicated container
427 // on the right of the layout instead of inline. Let's add all isolated images there
428 int imageIndex = 0;
429 for (int i = 0; i < mGroups.size(); i++) {
430 MessagingGroup messagingGroup = mGroups.get(i);
431 MessagingImageMessage isolatedMessage = messagingGroup.getIsolatedMessage();
432 if (isolatedMessage != null) {
433 newMessages.add(isolatedMessage.getView());
434 displayExternalImage = true;
435 if (imageIndex
436 != mImageMessageContainer.indexOfChild(isolatedMessage.getView())) {
437 mImageMessageContainer.removeView(isolatedMessage.getView());
438 mImageMessageContainer.addView(isolatedMessage.getView(), imageIndex);
439 }
440 imageIndex++;
441 }
442 }
443 }
444 // Remove all messages that don't belong into the image layout
445 for (int i = 0; i < mImageMessageContainer.getChildCount(); i++) {
446 View child = mImageMessageContainer.getChildAt(i);
447 if (!newMessages.contains(child)) {
448 mImageMessageContainer.removeView(child);
449 i--;
450 }
451 }
452 mImageMessageContainer.setVisibility(displayExternalImage ? VISIBLE : GONE);
Selim Cinek857f2792020-03-03 19:06:21 -0800453 }
454
Selim Cinek8baa70f2020-03-09 20:09:35 -0700455 private void bindFacePile() {
456 // Let's bind the face pile
457 View bottomBackground = mConversationFacePile.findViewById(
458 R.id.conversation_face_pile_bottom_background);
459 applyNotificationBackgroundColor(bottomBackground);
460 ImageView bottomView = mConversationFacePile.findViewById(
461 R.id.conversation_face_pile_bottom);
462 ImageView topView = mConversationFacePile.findViewById(
463 R.id.conversation_face_pile_top);
464 // Let's find the two last conversations:
465 Icon secondLastIcon = null;
466 CharSequence lastKey = null;
467 Icon lastIcon = null;
468 CharSequence userKey = getKey(mUser);
469 for (int i = mGroups.size() - 1; i >= 0; i--) {
470 MessagingGroup messagingGroup = mGroups.get(i);
471 Person messageSender = messagingGroup.getSender();
472 boolean notUser = messageSender != null
473 && !TextUtils.equals(userKey, getKey(messageSender));
474 boolean notIncluded = messageSender != null
475 && !TextUtils.equals(lastKey, getKey(messageSender));
476 if ((notUser && notIncluded)
477 || (i == 0 && lastKey == null)) {
478 if (lastIcon == null) {
479 lastIcon = messagingGroup.getAvatarIcon();
480 lastKey = getKey(messageSender);
481 } else {
482 secondLastIcon = messagingGroup.getAvatarIcon();
483 break;
484 }
485 }
486 }
487 if (lastIcon == null) {
488 lastIcon = createAvatarSymbol(" ", "", mLayoutColor);
489 }
490 bottomView.setImageIcon(lastIcon);
491 if (secondLastIcon == null) {
492 secondLastIcon = createAvatarSymbol("", "", mLayoutColor);
493 }
494 topView.setImageIcon(secondLastIcon);
Selim Cinekbf322ee2020-03-20 20:20:31 -0700495
496 int conversationAvatarSize;
497 int facepileAvatarSize;
498 int facePileBackgroundSize;
499 if (mIsCollapsed) {
500 conversationAvatarSize = mConversationAvatarSize;
501 facepileAvatarSize = mFacePileAvatarSize;
502 facePileBackgroundSize = facepileAvatarSize + 2 * mFacePileProtectionWidth;
503 } else {
504 conversationAvatarSize = mConversationAvatarSizeExpanded;
505 facepileAvatarSize = mFacePileAvatarSizeExpandedGroup;
506 facePileBackgroundSize = facepileAvatarSize + 2 * mFacePileProtectionWidthExpanded;
507 }
508 LayoutParams layoutParams = (LayoutParams) mConversationIcon.getLayoutParams();
509 layoutParams.width = conversationAvatarSize;
510 layoutParams.height = conversationAvatarSize;
511 mConversationFacePile.setLayoutParams(layoutParams);
512
513 layoutParams = (LayoutParams) bottomView.getLayoutParams();
514 layoutParams.width = facepileAvatarSize;
515 layoutParams.height = facepileAvatarSize;
516 bottomView.setLayoutParams(layoutParams);
517
518 layoutParams = (LayoutParams) topView.getLayoutParams();
519 layoutParams.width = facepileAvatarSize;
520 layoutParams.height = facepileAvatarSize;
521 topView.setLayoutParams(layoutParams);
522
523 layoutParams = (LayoutParams) bottomBackground.getLayoutParams();
524 layoutParams.width = facePileBackgroundSize;
525 layoutParams.height = facePileBackgroundSize;
526 bottomBackground.setLayoutParams(layoutParams);
Selim Cinek8baa70f2020-03-09 20:09:35 -0700527 }
528
Steve Elliott52440e92020-03-19 15:18:58 -0400529 private void updateAppName() {
530 mAppName.setVisibility(mIsCollapsed ? GONE : VISIBLE);
531 }
532
Selim Cinekcef53f32020-03-19 19:31:25 -0700533 public boolean shouldHideAppName() {
534 return mIsCollapsed;
535 }
536
Selim Cinek857f2792020-03-03 19:06:21 -0800537 /**
538 * update the icon position and sizing
539 */
540 private void updateIconPositionAndSize() {
Selim Cinekbf322ee2020-03-20 20:20:31 -0700541 int sidemargin;
542 int conversationAvatarSize;
Selim Cinek20d1ee22020-02-03 16:04:26 -0500543 if (mIsOneToOne || mIsCollapsed) {
Selim Cinekbf322ee2020-03-20 20:20:31 -0700544 sidemargin = mBadgedSideMargins;
545 conversationAvatarSize = mConversationAvatarSize;
Selim Cinek20d1ee22020-02-03 16:04:26 -0500546 } else {
Selim Cinekbf322ee2020-03-20 20:20:31 -0700547 sidemargin = mConversationFacePile.getVisibility() == VISIBLE
548 ? mExpandedGroupSideMarginFacePile
549 : mExpandedGroupSideMargin;
550 conversationAvatarSize = mConversationAvatarSizeExpanded;
Selim Cinek20d1ee22020-02-03 16:04:26 -0500551 }
Selim Cinek857f2792020-03-03 19:06:21 -0800552 LayoutParams layoutParams =
Selim Cinek20d1ee22020-02-03 16:04:26 -0500553 (LayoutParams) mConversationIconBadge.getLayoutParams();
Selim Cinekbf322ee2020-03-20 20:20:31 -0700554 layoutParams.topMargin = sidemargin;
555 layoutParams.setMarginStart(sidemargin);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500556 mConversationIconBadge.setLayoutParams(layoutParams);
Selim Cinekbf322ee2020-03-20 20:20:31 -0700557
558 if (mConversationIcon.getVisibility() == VISIBLE) {
559 layoutParams = (LayoutParams) mConversationIcon.getLayoutParams();
560 layoutParams.width = conversationAvatarSize;
561 layoutParams.height = conversationAvatarSize;
562 mConversationIcon.setLayoutParams(layoutParams);
563 }
564 }
565
566 private void updatePaddingsBasedOnContentAvailability() {
567 int containerTopPadding;
568 int messagingPadding = 0;
569 if (mIsOneToOne || mIsCollapsed) {
570 containerTopPadding = mConversationIconTopPadding;
571 } else {
572 if (mAppName.getVisibility() != GONE) {
573 // The app name is visible, let's center outselves in the two lines
574 containerTopPadding = mConversationIconTopPaddingExpandedGroup;
575 } else {
576 // App name is gone, let's center ourselves int he one remaining line
577 containerTopPadding = mConversationIconTopPaddingNoAppName;
578
579 // The app name is gone and we're a group, we'll need to add some extra padding
580 // to the messages, since otherwise it will overlap with the group
581 messagingPadding = mExpandedGroupMessagePaddingNoAppName;
582 }
583 }
584
585 mConversationIconContainer.setPaddingRelative(
586 mConversationIconContainer.getPaddingStart(),
587 containerTopPadding,
588 mConversationIconContainer.getPaddingEnd(),
589 mConversationIconContainer.getPaddingBottom());
590
591 mMessagingLinearLayout.setPaddingRelative(
592 mMessagingLinearLayout.getPaddingStart(),
593 messagingPadding,
594 mMessagingLinearLayout.getPaddingEnd(),
595 mMessagingLinearLayout.getPaddingBottom());
Selim Cinek20d1ee22020-02-03 16:04:26 -0500596 }
597
598 @RemotableViewMethod
599 public void setLargeIcon(Icon largeIcon) {
600 mLargeIcon = largeIcon;
601 }
602
Selim Cinek2f7f7b82020-03-10 15:41:13 -0700603 /**
604 * Sets the conversation title of this conversation.
605 *
606 * @param conversationTitle the conversation title
607 */
608 @RemotableViewMethod
609 public void setConversationTitle(CharSequence conversationTitle) {
610 mConversationTitle = conversationTitle;
611 }
612
Selim Cinek20d1ee22020-02-03 16:04:26 -0500613 private void removeGroups(ArrayList<MessagingGroup> oldGroups) {
614 int size = oldGroups.size();
615 for (int i = 0; i < size; i++) {
616 MessagingGroup group = oldGroups.get(i);
617 if (!mGroups.contains(group)) {
618 List<MessagingMessage> messages = group.getMessages();
619 Runnable endRunnable = () -> {
620 mMessagingLinearLayout.removeTransientView(group);
621 group.recycle();
622 };
623
624 boolean wasShown = group.isShown();
625 mMessagingLinearLayout.removeView(group);
626 if (wasShown && !MessagingLinearLayout.isGone(group)) {
627 mMessagingLinearLayout.addTransientView(group, 0);
628 group.removeGroupAnimated(endRunnable);
629 } else {
630 endRunnable.run();
631 }
632 mMessages.removeAll(messages);
633 mHistoricMessages.removeAll(messages);
634 }
635 }
636 }
637
638 private void updateTitleAndNamesDisplay() {
639 ArrayMap<CharSequence, String> uniqueNames = new ArrayMap<>();
640 ArrayMap<Character, CharSequence> uniqueCharacters = new ArrayMap<>();
641 for (int i = 0; i < mGroups.size(); i++) {
642 MessagingGroup group = mGroups.get(i);
643 CharSequence senderName = group.getSenderName();
644 if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)) {
645 continue;
646 }
647 if (!uniqueNames.containsKey(senderName)) {
648 // Only use visible characters to get uniqueNames
649 String pureSenderName = IGNORABLE_CHAR_PATTERN
650 .matcher(senderName).replaceAll("" /* replacement */);
651 char c = pureSenderName.charAt(0);
652 if (uniqueCharacters.containsKey(c)) {
653 // this character was already used, lets make it more unique. We first need to
654 // resolve the existing character if it exists
655 CharSequence existingName = uniqueCharacters.get(c);
656 if (existingName != null) {
657 uniqueNames.put(existingName, findNameSplit((String) existingName));
658 uniqueCharacters.put(c, null);
659 }
660 uniqueNames.put(senderName, findNameSplit((String) senderName));
661 } else {
662 uniqueNames.put(senderName, Character.toString(c));
663 uniqueCharacters.put(c, pureSenderName);
664 }
665 }
666 }
667
668 // Now that we have the correct symbols, let's look what we have cached
669 ArrayMap<CharSequence, Icon> cachedAvatars = new ArrayMap<>();
670 for (int i = 0; i < mGroups.size(); i++) {
671 // Let's now set the avatars
672 MessagingGroup group = mGroups.get(i);
673 boolean isOwnMessage = group.getSender() == mUser;
674 CharSequence senderName = group.getSenderName();
675 if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)
676 || (mIsOneToOne && mAvatarReplacement != null && !isOwnMessage)) {
677 continue;
678 }
679 String symbol = uniqueNames.get(senderName);
680 Icon cachedIcon = group.getAvatarSymbolIfMatching(senderName,
681 symbol, mLayoutColor);
682 if (cachedIcon != null) {
683 cachedAvatars.put(senderName, cachedIcon);
684 }
685 }
686
687 for (int i = 0; i < mGroups.size(); i++) {
688 // Let's now set the avatars
689 MessagingGroup group = mGroups.get(i);
690 CharSequence senderName = group.getSenderName();
691 if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)) {
692 continue;
693 }
694 if (mIsOneToOne && mAvatarReplacement != null && group.getSender() != mUser) {
695 group.setAvatar(mAvatarReplacement);
696 } else {
697 Icon cachedIcon = cachedAvatars.get(senderName);
698 if (cachedIcon == null) {
699 cachedIcon = createAvatarSymbol(senderName, uniqueNames.get(senderName),
700 mLayoutColor);
701 cachedAvatars.put(senderName, cachedIcon);
702 }
703 group.setCreatedAvatar(cachedIcon, senderName, uniqueNames.get(senderName),
704 mLayoutColor);
705 }
706 }
707 }
708
709 private Icon createAvatarSymbol(CharSequence senderName, String symbol, int layoutColor) {
710 if (symbol.isEmpty() || TextUtils.isDigitsOnly(symbol) ||
711 SPECIAL_CHAR_PATTERN.matcher(symbol).find()) {
712 Icon avatarIcon = Icon.createWithResource(getContext(),
713 R.drawable.messaging_user);
714 avatarIcon.setTint(findColor(senderName, layoutColor));
715 return avatarIcon;
716 } else {
717 Bitmap bitmap = Bitmap.createBitmap(mAvatarSize, mAvatarSize, Bitmap.Config.ARGB_8888);
718 Canvas canvas = new Canvas(bitmap);
719 float radius = mAvatarSize / 2.0f;
720 int color = findColor(senderName, layoutColor);
721 mPaint.setColor(color);
722 canvas.drawCircle(radius, radius, radius, mPaint);
723 boolean needDarkText = ColorUtils.calculateLuminance(color) > 0.5f;
724 mTextPaint.setColor(needDarkText ? Color.BLACK : Color.WHITE);
725 mTextPaint.setTextSize(symbol.length() == 1 ? mAvatarSize * 0.5f : mAvatarSize * 0.3f);
726 int yPos = (int) (radius - ((mTextPaint.descent() + mTextPaint.ascent()) / 2));
727 canvas.drawText(symbol, radius, yPos, mTextPaint);
728 return Icon.createWithBitmap(bitmap);
729 }
730 }
731
732 private int findColor(CharSequence senderName, int layoutColor) {
733 double luminance = ContrastColorUtil.calculateLuminance(layoutColor);
734 float shift = Math.abs(senderName.hashCode()) % 5 / 4.0f - 0.5f;
735
736 // we need to offset the range if the luminance is too close to the borders
737 shift += Math.max(COLOR_SHIFT_AMOUNT / 2.0f / 100 - luminance, 0);
738 shift -= Math.max(COLOR_SHIFT_AMOUNT / 2.0f / 100 - (1.0f - luminance), 0);
739 return ContrastColorUtil.getShiftedColor(layoutColor,
740 (int) (shift * COLOR_SHIFT_AMOUNT));
741 }
742
743 private String findNameSplit(String existingName) {
744 String[] split = existingName.split(" ");
745 if (split.length > 1) {
746 return Character.toString(split[0].charAt(0))
747 + Character.toString(split[1].charAt(0));
748 }
749 return existingName.substring(0, 1);
750 }
751
752 @RemotableViewMethod
753 public void setLayoutColor(int color) {
754 mLayoutColor = color;
755 }
756
757 @RemotableViewMethod
758 public void setIsOneToOne(boolean oneToOne) {
759 mIsOneToOne = oneToOne;
760 }
761
762 @RemotableViewMethod
763 public void setSenderTextColor(int color) {
764 mSenderTextColor = color;
765 }
766
Selim Cineke9714eb2020-03-09 11:21:49 -0700767 /**
768 * @param color the color of the notification background
769 */
770 @RemotableViewMethod
771 public void setNotificationBackgroundColor(int color) {
Selim Cinek8baa70f2020-03-09 20:09:35 -0700772 mNotificationBackgroundColor = color;
773 applyNotificationBackgroundColor(mConversationIconBadge);
774 }
775
776 private void applyNotificationBackgroundColor(View view) {
777 view.setBackgroundTintList(ColorStateList.valueOf(mNotificationBackgroundColor));
Selim Cineke9714eb2020-03-09 11:21:49 -0700778 }
779
Selim Cinek20d1ee22020-02-03 16:04:26 -0500780 @RemotableViewMethod
781 public void setMessageTextColor(int color) {
782 mMessageTextColor = color;
783 }
784
785 private void setUser(Person user) {
786 mUser = user;
787 if (mUser.getIcon() == null) {
788 Icon userIcon = Icon.createWithResource(getContext(),
789 R.drawable.messaging_user);
790 userIcon.setTint(mLayoutColor);
791 mUser = mUser.toBuilder().setIcon(userIcon).build();
792 }
793 }
794
795 private void createGroupViews(List<List<MessagingMessage>> groups,
796 List<Person> senders, boolean showSpinner) {
797 mGroups.clear();
798 for (int groupIndex = 0; groupIndex < groups.size(); groupIndex++) {
799 List<MessagingMessage> group = groups.get(groupIndex);
800 MessagingGroup newGroup = null;
801 // we'll just take the first group that exists or create one there is none
802 for (int messageIndex = group.size() - 1; messageIndex >= 0; messageIndex--) {
803 MessagingMessage message = group.get(messageIndex);
804 newGroup = message.getGroup();
805 if (newGroup != null) {
806 break;
807 }
808 }
809 // Create a new group, adding it to the linear layout as well
810 if (newGroup == null) {
811 newGroup = MessagingGroup.createGroup(mMessagingLinearLayout);
812 mAddedGroups.add(newGroup);
813 }
Selim Cinek514b52c2020-03-17 18:42:12 -0700814 newGroup.setImageDisplayLocation(mIsCollapsed
815 ? IMAGE_DISPLAY_LOCATION_EXTERNAL
816 : IMAGE_DISPLAY_LOCATION_INLINE);
Selim Cineka91778a32020-03-13 17:30:34 -0700817 newGroup.setIsInConversation(true);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500818 newGroup.setLayoutColor(mLayoutColor);
819 newGroup.setTextColors(mSenderTextColor, mMessageTextColor);
820 Person sender = senders.get(groupIndex);
821 CharSequence nameOverride = null;
822 if (sender != mUser && mNameReplacement != null) {
823 nameOverride = mNameReplacement;
824 }
825 newGroup.setShowingAvatar(!mIsOneToOne && !mIsCollapsed);
826 newGroup.setSingleLine(mIsCollapsed);
827 newGroup.setSender(sender, nameOverride);
828 newGroup.setSending(groupIndex == (groups.size() - 1) && showSpinner);
829 mGroups.add(newGroup);
830
831 // Reposition to the correct place (if we're re-using a group)
832 if (mMessagingLinearLayout.indexOfChild(newGroup) != groupIndex) {
833 mMessagingLinearLayout.removeView(newGroup);
834 mMessagingLinearLayout.addView(newGroup, groupIndex);
835 }
836 newGroup.setMessages(group);
837 }
838 }
839
840 private void findGroups(List<MessagingMessage> historicMessages,
841 List<MessagingMessage> messages, List<List<MessagingMessage>> groups,
842 List<Person> senders) {
843 CharSequence currentSenderKey = null;
844 List<MessagingMessage> currentGroup = null;
845 int histSize = historicMessages.size();
846 for (int i = 0; i < histSize + messages.size(); i++) {
847 MessagingMessage message;
848 if (i < histSize) {
849 message = historicMessages.get(i);
850 } else {
851 message = messages.get(i - histSize);
852 }
853 boolean isNewGroup = currentGroup == null;
854 Person sender = message.getMessage().getSenderPerson();
Selim Cinek857f2792020-03-03 19:06:21 -0800855 CharSequence key = getKey(sender);
Selim Cinek20d1ee22020-02-03 16:04:26 -0500856 isNewGroup |= !TextUtils.equals(key, currentSenderKey);
857 if (isNewGroup) {
858 currentGroup = new ArrayList<>();
859 groups.add(currentGroup);
860 if (sender == null) {
861 sender = mUser;
862 }
863 senders.add(sender);
864 currentSenderKey = key;
865 }
866 currentGroup.add(message);
867 }
868 }
869
Selim Cinek857f2792020-03-03 19:06:21 -0800870 private CharSequence getKey(Person person) {
871 return person == null ? null : person.getKey() == null ? person.getName() : person.getKey();
872 }
873
Selim Cinek20d1ee22020-02-03 16:04:26 -0500874 /**
875 * Creates new messages, reusing existing ones if they are available.
876 *
877 * @param newMessages the messages to parse.
878 */
879 private List<MessagingMessage> createMessages(
880 List<Notification.MessagingStyle.Message> newMessages, boolean historic) {
881 List<MessagingMessage> result = new ArrayList<>();
882 for (int i = 0; i < newMessages.size(); i++) {
883 Notification.MessagingStyle.Message m = newMessages.get(i);
884 MessagingMessage message = findAndRemoveMatchingMessage(m);
885 if (message == null) {
886 message = MessagingMessage.createMessage(this, m, mImageResolver);
887 }
888 message.setIsHistoric(historic);
889 result.add(message);
890 }
891 return result;
892 }
893
894 private MessagingMessage findAndRemoveMatchingMessage(Notification.MessagingStyle.Message m) {
895 for (int i = 0; i < mMessages.size(); i++) {
896 MessagingMessage existing = mMessages.get(i);
897 if (existing.sameAs(m)) {
898 mMessages.remove(i);
899 return existing;
900 }
901 }
902 for (int i = 0; i < mHistoricMessages.size(); i++) {
903 MessagingMessage existing = mHistoricMessages.get(i);
904 if (existing.sameAs(m)) {
905 mHistoricMessages.remove(i);
906 return existing;
907 }
908 }
909 return null;
910 }
911
912 public void showHistoricMessages(boolean show) {
913 mShowHistoricMessages = show;
914 updateHistoricMessageVisibility();
915 }
916
917 private void updateHistoricMessageVisibility() {
918 int numHistoric = mHistoricMessages.size();
919 for (int i = 0; i < numHistoric; i++) {
920 MessagingMessage existing = mHistoricMessages.get(i);
921 existing.setVisibility(mShowHistoricMessages ? VISIBLE : GONE);
922 }
923 int numGroups = mGroups.size();
924 for (int i = 0; i < numGroups; i++) {
925 MessagingGroup group = mGroups.get(i);
926 int visibleChildren = 0;
927 List<MessagingMessage> messages = group.getMessages();
928 int numGroupMessages = messages.size();
929 for (int j = 0; j < numGroupMessages; j++) {
930 MessagingMessage message = messages.get(j);
931 if (message.getVisibility() != GONE) {
932 visibleChildren++;
933 }
934 }
935 if (visibleChildren > 0 && group.getVisibility() == GONE) {
936 group.setVisibility(VISIBLE);
937 } else if (visibleChildren == 0 && group.getVisibility() != GONE) {
938 group.setVisibility(GONE);
939 }
940 }
941 }
942
943 @Override
944 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
945 super.onLayout(changed, left, top, right, bottom);
946 if (!mAddedGroups.isEmpty()) {
947 getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
948 @Override
949 public boolean onPreDraw() {
950 for (MessagingGroup group : mAddedGroups) {
951 if (!group.isShown()) {
952 continue;
953 }
954 MessagingPropertyAnimator.fadeIn(group.getAvatar());
955 MessagingPropertyAnimator.fadeIn(group.getSenderView());
956 MessagingPropertyAnimator.startLocalTranslationFrom(group,
957 group.getHeight(), LINEAR_OUT_SLOW_IN);
958 }
959 mAddedGroups.clear();
960 getViewTreeObserver().removeOnPreDrawListener(this);
961 return true;
962 }
963 });
964 }
965 }
966
967 public MessagingLinearLayout getMessagingLinearLayout() {
968 return mMessagingLinearLayout;
969 }
970
Selim Cinek514b52c2020-03-17 18:42:12 -0700971 public @NonNull ViewGroup getImageMessageContainer() {
972 return mImageMessageContainer;
973 }
974
Selim Cinek20d1ee22020-02-03 16:04:26 -0500975 public ArrayList<MessagingGroup> getMessagingGroups() {
976 return mGroups;
977 }
978
979 private void updateExpandButton() {
980 int drawableId;
981 int contentDescriptionId;
982 int gravity;
983 int topMargin = 0;
Selim Cinek54fe9ad2020-03-04 13:56:42 -0800984 ViewGroup newContainer;
Selim Cinek20d1ee22020-02-03 16:04:26 -0500985 if (mIsCollapsed) {
986 drawableId = R.drawable.ic_expand_notification;
987 contentDescriptionId = R.string.expand_button_content_description_collapsed;
988 gravity = Gravity.CENTER;
Selim Cinek54fe9ad2020-03-04 13:56:42 -0800989 newContainer = mExpandButtonAndContentContainer;
Selim Cinek20d1ee22020-02-03 16:04:26 -0500990 } else {
991 drawableId = R.drawable.ic_collapse_notification;
992 contentDescriptionId = R.string.expand_button_content_description_expanded;
993 gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
994 topMargin = mExpandButtonExpandedTopMargin;
Selim Cinek54fe9ad2020-03-04 13:56:42 -0800995 newContainer = this;
Selim Cinek20d1ee22020-02-03 16:04:26 -0500996 }
997 mExpandButton.setImageDrawable(getContext().getDrawable(drawableId));
998 mExpandButton.setColorFilter(mExpandButton.getOriginalNotificationColor());
999
Selim Cinek54fe9ad2020-03-04 13:56:42 -08001000 // We need to make sure that the expand button is in the linearlayout pushing over the
1001 // content when collapsed, but allows the content to flow under it when expanded.
1002 if (newContainer != mExpandButtonContainer.getParent()) {
1003 ((ViewGroup) mExpandButtonContainer.getParent()).removeView(mExpandButtonContainer);
1004 newContainer.addView(mExpandButtonContainer);
Selim Cinek54fe9ad2020-03-04 13:56:42 -08001005 }
1006
Selim Cinek20d1ee22020-02-03 16:04:26 -05001007 // update if the expand button is centered
Selim Cinek514b52c2020-03-17 18:42:12 -07001008 LinearLayout.LayoutParams layoutParams =
1009 (LinearLayout.LayoutParams) mExpandButton.getLayoutParams();
Selim Cinek20d1ee22020-02-03 16:04:26 -05001010 layoutParams.gravity = gravity;
1011 layoutParams.topMargin = topMargin;
1012 mExpandButton.setLayoutParams(layoutParams);
1013
1014 mExpandButtonContainer.setContentDescription(mContext.getText(contentDescriptionId));
Selim Cinekafc20582020-03-13 20:15:38 -07001015 }
Selim Cinek54fe9ad2020-03-04 13:56:42 -08001016
Selim Cinekbf322ee2020-03-20 20:20:31 -07001017 private void updateContentEndPaddings() {
Selim Cinekafc20582020-03-13 20:15:38 -07001018
1019 // Let's make sure the conversation header can't run into the expand button when we're
1020 // collapsed and update the paddings of the content
1021 int headerPaddingEnd;
1022 int contentPaddingEnd;
1023 if (!mExpandable) {
1024 headerPaddingEnd = 0;
1025 contentPaddingEnd = mContentMarginEnd;
1026 } else if (mIsCollapsed) {
1027 headerPaddingEnd = 0;
1028 contentPaddingEnd = 0;
1029 } else {
1030 headerPaddingEnd = mNotificationHeaderExpandedPadding;
1031 contentPaddingEnd = mContentMarginEnd;
1032 }
1033 mConversationHeader.setPaddingRelative(
1034 mConversationHeader.getPaddingStart(),
1035 mConversationHeader.getPaddingTop(),
1036 headerPaddingEnd,
1037 mConversationHeader.getPaddingBottom());
1038
1039 mContentContainer.setPaddingRelative(
1040 mContentContainer.getPaddingStart(),
1041 mContentContainer.getPaddingTop(),
1042 contentPaddingEnd,
1043 mContentContainer.getPaddingBottom());
Selim Cinek20d1ee22020-02-03 16:04:26 -05001044 }
1045
Selim Cinekbf322ee2020-03-20 20:20:31 -07001046 private void onAppNameVisibilityChanged() {
1047 boolean appNameGone = mAppName.getVisibility() == GONE;
1048 if (appNameGone != mAppNameGone) {
1049 mAppNameGone = appNameGone;
1050 updatePaddingsBasedOnContentAvailability();
1051 }
1052 }
1053
Selim Cinek20d1ee22020-02-03 16:04:26 -05001054 public void updateExpandability(boolean expandable, @Nullable OnClickListener onClickListener) {
Selim Cinekafc20582020-03-13 20:15:38 -07001055 mExpandable = expandable;
Selim Cinek20d1ee22020-02-03 16:04:26 -05001056 if (expandable) {
1057 mExpandButtonContainer.setVisibility(VISIBLE);
1058 mExpandButtonContainer.setOnClickListener(onClickListener);
1059 } else {
1060 // TODO: handle content paddings to end of layout
1061 mExpandButtonContainer.setVisibility(GONE);
1062 }
Selim Cinekbf322ee2020-03-20 20:20:31 -07001063 updateContentEndPaddings();
Selim Cinek20d1ee22020-02-03 16:04:26 -05001064 }
Selim Cinek514b52c2020-03-17 18:42:12 -07001065
1066 @Override
1067 public void setMessagingClippingDisabled(boolean clippingDisabled) {
1068 mMessagingLinearLayout.setClipBounds(clippingDisabled ? null : mMessagingClipRect);
1069 }
Selim Cinek20d1ee22020-02-03 16:04:26 -05001070}