blob: a162e4e10c71f3157caa3d310a5b4746b7c6fe4f [file] [log] [blame]
Selim Cinek88188f22017-09-19 16:46:56 -07001/*
2 * Copyright (C) 2017 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_AT_END;
20import static com.android.internal.widget.MessagingGroup.IMAGE_DISPLAY_LOCATION_INLINE;
21
Selim Cinek88188f22017-09-19 16:46:56 -070022import android.annotation.AttrRes;
23import android.annotation.NonNull;
24import android.annotation.Nullable;
25import android.annotation.StyleRes;
26import android.app.Notification;
Selim Cinek9acd6732018-03-23 16:39:02 -070027import android.app.Person;
Aran Ink2e3cc412019-12-13 16:30:17 -050028import android.app.RemoteInputHistoryItem;
Selim Cinek88188f22017-09-19 16:46:56 -070029import android.content.Context;
30import android.graphics.Bitmap;
31import android.graphics.Canvas;
32import android.graphics.Color;
33import android.graphics.Paint;
Selim Cinekd3809992017-10-27 16:09:20 -070034import android.graphics.Rect;
Selim Cinek88188f22017-09-19 16:46:56 -070035import android.graphics.drawable.Icon;
36import android.os.Bundle;
37import android.os.Parcelable;
38import android.text.TextUtils;
39import android.util.ArrayMap;
40import android.util.AttributeSet;
Selim Cinekd3809992017-10-27 16:09:20 -070041import android.util.DisplayMetrics;
Selim Cinek88188f22017-09-19 16:46:56 -070042import android.view.RemotableViewMethod;
Selim Cinekd3809992017-10-27 16:09:20 -070043import android.view.ViewTreeObserver;
44import android.view.animation.Interpolator;
45import android.view.animation.PathInterpolator;
Selim Cinek88188f22017-09-19 16:46:56 -070046import android.widget.FrameLayout;
47import android.widget.RemoteViews;
48import android.widget.TextView;
49
50import com.android.internal.R;
51import com.android.internal.graphics.ColorUtils;
Lucas Dupina291d192018-06-07 13:59:42 -070052import com.android.internal.util.ContrastColorUtil;
Selim Cinek88188f22017-09-19 16:46:56 -070053
54import java.util.ArrayList;
55import java.util.List;
56import java.util.function.Consumer;
Tony Huang45523682018-05-02 11:42:27 +080057import java.util.regex.Pattern;
Selim Cinek88188f22017-09-19 16:46:56 -070058
59/**
60 * A custom-built layout for the Notification.MessagingStyle allows dynamic addition and removal
61 * messages and adapts the layout accordingly.
62 */
63@RemoteViews.RemoteView
Selim Cinek20d1ee22020-02-03 16:04:26 -050064public class MessagingLayout extends FrameLayout
65 implements ImageMessageConsumer, IMessagingLayout {
Selim Cinek88188f22017-09-19 16:46:56 -070066
67 private static final float COLOR_SHIFT_AMOUNT = 60;
Tony Huang1250cd12018-06-06 15:40:47 +080068 /**
69 * Pattren for filter some ingonable characters.
70 * p{Z} for any kind of whitespace or invisible separator.
71 * p{C} for any kind of punctuation character.
72 */
73 private static final Pattern IGNORABLE_CHAR_PATTERN
74 = Pattern.compile("[\\p{C}\\p{Z}]");
Tony Huang45523682018-05-02 11:42:27 +080075 private static final Pattern SPECIAL_CHAR_PATTERN
76 = Pattern.compile ("[!@#$%&*()_+=|<>?{}\\[\\]~-]");
Selim Cinek88188f22017-09-19 16:46:56 -070077 private static final Consumer<MessagingMessage> REMOVE_MESSAGE
78 = MessagingMessage::removeMessage;
Selim Cinekd3809992017-10-27 16:09:20 -070079 public static final Interpolator LINEAR_OUT_SLOW_IN = new PathInterpolator(0f, 0f, 0.2f, 1f);
80 public static final Interpolator FAST_OUT_LINEAR_IN = new PathInterpolator(0.4f, 0f, 1f, 1f);
81 public static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f);
82 public static final OnLayoutChangeListener MESSAGING_PROPERTY_ANIMATOR
83 = new MessagingPropertyAnimator();
Selim Cinek88188f22017-09-19 16:46:56 -070084 private List<MessagingMessage> mMessages = new ArrayList<>();
85 private List<MessagingMessage> mHistoricMessages = new ArrayList<>();
86 private MessagingLinearLayout mMessagingLinearLayout;
Selim Cinek88188f22017-09-19 16:46:56 -070087 private boolean mShowHistoricMessages;
88 private ArrayList<MessagingGroup> mGroups = new ArrayList<>();
89 private TextView mTitleView;
90 private int mLayoutColor;
Kenny Guy14d035c2018-05-02 19:10:36 +010091 private int mSenderTextColor;
92 private int mMessageTextColor;
Selim Cinek88188f22017-09-19 16:46:56 -070093 private int mAvatarSize;
94 private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
95 private Paint mTextPaint = new Paint();
96 private CharSequence mConversationTitle;
Selim Cinekce8794f2018-05-23 16:46:05 -070097 private Icon mAvatarReplacement;
Selim Cinek88188f22017-09-19 16:46:56 -070098 private boolean mIsOneToOne;
Selim Cinekd3809992017-10-27 16:09:20 -070099 private ArrayList<MessagingGroup> mAddedGroups = new ArrayList<>();
Selim Cinek9acd6732018-03-23 16:39:02 -0700100 private Person mUser;
Selim Cinek2dd3e722018-01-19 11:06:06 -0800101 private CharSequence mNameReplacement;
Selim Cinek85d0e6e2018-03-23 18:08:32 -0700102 private boolean mDisplayImagesAtEnd;
Ahan Wude396fa2018-05-08 20:42:24 +0800103 private ImageResolver mImageResolver;
Selim Cinek88188f22017-09-19 16:46:56 -0700104
105 public MessagingLayout(@NonNull Context context) {
106 super(context);
107 }
108
109 public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
110 super(context, attrs);
111 }
112
113 public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs,
114 @AttrRes int defStyleAttr) {
115 super(context, attrs, defStyleAttr);
116 }
117
118 public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs,
119 @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
120 super(context, attrs, defStyleAttr, defStyleRes);
121 }
122
123 @Override
124 protected void onFinishInflate() {
125 super.onFinishInflate();
126 mMessagingLinearLayout = findViewById(R.id.notification_messaging);
Selim Cinek1d6b50e2017-10-27 16:10:57 -0700127 mMessagingLinearLayout.setMessagingLayout(this);
Selim Cinekd3809992017-10-27 16:09:20 -0700128 // We still want to clip, but only on the top, since views can temporarily out of bounds
129 // during transitions.
130 DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
Selim Cinek1110fd72018-05-01 10:53:26 -0700131 int size = Math.max(displayMetrics.widthPixels, displayMetrics.heightPixels);
132 Rect rect = new Rect(0, 0, size, size);
Selim Cinekd3809992017-10-27 16:09:20 -0700133 mMessagingLinearLayout.setClipBounds(rect);
Selim Cinek88188f22017-09-19 16:46:56 -0700134 mTitleView = findViewById(R.id.title);
135 mAvatarSize = getResources().getDimensionPixelSize(R.dimen.messaging_avatar_size);
136 mTextPaint.setTextAlign(Paint.Align.CENTER);
137 mTextPaint.setAntiAlias(true);
138 }
139
140 @RemotableViewMethod
Selim Cinekce8794f2018-05-23 16:46:05 -0700141 public void setAvatarReplacement(Icon icon) {
142 mAvatarReplacement = icon;
Selim Cinek88188f22017-09-19 16:46:56 -0700143 }
144
145 @RemotableViewMethod
Selim Cinek2dd3e722018-01-19 11:06:06 -0800146 public void setNameReplacement(CharSequence nameReplacement) {
147 mNameReplacement = nameReplacement;
148 }
149
Selim Cinek20d1ee22020-02-03 16:04:26 -0500150 /**
151 * Set this layout to show the collapsed representation.
152 *
153 * @param isCollapsed is it collapsed
154 */
Selim Cinek2dd3e722018-01-19 11:06:06 -0800155 @RemotableViewMethod
Selim Cinek20d1ee22020-02-03 16:04:26 -0500156 public void setIsCollapsed(boolean isCollapsed) {
157 mDisplayImagesAtEnd = isCollapsed;
158 }
159
160 @RemotableViewMethod
161 public void setLargeIcon(Icon largeIcon) {
162 // Unused
Selim Cinek7199ed92018-01-26 13:36:43 -0800163 }
164
Selim Cinek2f7f7b82020-03-10 15:41:13 -0700165 /**
166 * Sets the conversation title of this conversation.
167 *
168 * @param conversationTitle the conversation title
169 */
170 @RemotableViewMethod
171 public void setConversationTitle(CharSequence conversationTitle) {
172 // Unused
173 }
174
Selim Cinek7199ed92018-01-26 13:36:43 -0800175 @RemotableViewMethod
Selim Cinek88188f22017-09-19 16:46:56 -0700176 public void setData(Bundle extras) {
177 Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES);
178 List<Notification.MessagingStyle.Message> newMessages
179 = Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages);
180 Parcelable[] histMessages = extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES);
181 List<Notification.MessagingStyle.Message> newHistoricMessages
182 = Notification.MessagingStyle.Message.getMessagesFromBundleArray(histMessages);
Selim Cinekafeed292017-12-12 17:32:44 -0800183 setUser(extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON));
Selim Cinek88188f22017-09-19 16:46:56 -0700184 mConversationTitle = null;
185 TextView headerText = findViewById(R.id.header_text);
186 if (headerText != null) {
187 mConversationTitle = headerText.getText();
188 }
Aran Ink2e3cc412019-12-13 16:30:17 -0500189 RemoteInputHistoryItem[] history = (RemoteInputHistoryItem[])
190 extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
191 addRemoteInputHistoryToMessages(newMessages, history);
Kenny Guya0f6de82018-04-06 16:20:16 +0100192 boolean showSpinner =
193 extras.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false);
194 bind(newMessages, newHistoricMessages, showSpinner);
Selim Cinek88188f22017-09-19 16:46:56 -0700195 }
196
Ahan Wude396fa2018-05-08 20:42:24 +0800197 @Override
198 public void setImageResolver(ImageResolver resolver) {
199 mImageResolver = resolver;
200 }
201
Selim Cinek1397ea32018-01-16 17:34:52 -0800202 private void addRemoteInputHistoryToMessages(
203 List<Notification.MessagingStyle.Message> newMessages,
Aran Ink2e3cc412019-12-13 16:30:17 -0500204 RemoteInputHistoryItem[] remoteInputHistory) {
Selim Cinek1397ea32018-01-16 17:34:52 -0800205 if (remoteInputHistory == null || remoteInputHistory.length == 0) {
206 return;
207 }
208 for (int i = remoteInputHistory.length - 1; i >= 0; i--) {
Aran Ink2e3cc412019-12-13 16:30:17 -0500209 RemoteInputHistoryItem historyMessage = remoteInputHistory[i];
210 Notification.MessagingStyle.Message message = new Notification.MessagingStyle.Message(
211 historyMessage.getText(), 0, (Person) null, true /* remoteHistory */);
212 if (historyMessage.getUri() != null) {
213 message.setData(historyMessage.getMimeType(), historyMessage.getUri());
214 }
215 newMessages.add(message);
Selim Cinek1397ea32018-01-16 17:34:52 -0800216 }
217 }
218
Selim Cinek88188f22017-09-19 16:46:56 -0700219 private void bind(List<Notification.MessagingStyle.Message> newMessages,
Kenny Guya0f6de82018-04-06 16:20:16 +0100220 List<Notification.MessagingStyle.Message> newHistoricMessages,
221 boolean showSpinner) {
Selim Cinek88188f22017-09-19 16:46:56 -0700222
223 List<MessagingMessage> historicMessages = createMessages(newHistoricMessages,
224 true /* isHistoric */);
225 List<MessagingMessage> messages = createMessages(newMessages, false /* isHistoric */);
Selim Cinekf68af9b2018-05-24 16:37:22 -0700226
227 ArrayList<MessagingGroup> oldGroups = new ArrayList<>(mGroups);
Kenny Guya0f6de82018-04-06 16:20:16 +0100228 addMessagesToGroups(historicMessages, messages, showSpinner);
Selim Cinek88188f22017-09-19 16:46:56 -0700229
Selim Cinekf68af9b2018-05-24 16:37:22 -0700230 // Let's first check which groups were removed altogether and remove them in one animation
231 removeGroups(oldGroups);
232
Selim Cinek88188f22017-09-19 16:46:56 -0700233 // Let's remove the remaining messages
234 mMessages.forEach(REMOVE_MESSAGE);
235 mHistoricMessages.forEach(REMOVE_MESSAGE);
236
237 mMessages = messages;
238 mHistoricMessages = historicMessages;
239
Selim Cinek88188f22017-09-19 16:46:56 -0700240 updateHistoricMessageVisibility();
241 updateTitleAndNamesDisplay();
242 }
243
Selim Cinekf68af9b2018-05-24 16:37:22 -0700244 private void removeGroups(ArrayList<MessagingGroup> oldGroups) {
245 int size = oldGroups.size();
246 for (int i = 0; i < size; i++) {
247 MessagingGroup group = oldGroups.get(i);
248 if (!mGroups.contains(group)) {
249 List<MessagingMessage> messages = group.getMessages();
250 Runnable endRunnable = () -> {
251 mMessagingLinearLayout.removeTransientView(group);
252 group.recycle();
253 };
254
255 boolean wasShown = group.isShown();
256 mMessagingLinearLayout.removeView(group);
257 if (wasShown && !MessagingLinearLayout.isGone(group)) {
258 mMessagingLinearLayout.addTransientView(group, 0);
259 group.removeGroupAnimated(endRunnable);
260 } else {
261 endRunnable.run();
262 }
263 mMessages.removeAll(messages);
264 mHistoricMessages.removeAll(messages);
265 }
266 }
267 }
268
Selim Cinek88188f22017-09-19 16:46:56 -0700269 private void updateTitleAndNamesDisplay() {
270 ArrayMap<CharSequence, String> uniqueNames = new ArrayMap<>();
271 ArrayMap<Character, CharSequence> uniqueCharacters = new ArrayMap<>();
272 for (int i = 0; i < mGroups.size(); i++) {
273 MessagingGroup group = mGroups.get(i);
274 CharSequence senderName = group.getSenderName();
Selim Cinekafeed292017-12-12 17:32:44 -0800275 if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)) {
Selim Cinek88188f22017-09-19 16:46:56 -0700276 continue;
277 }
Selim Cinekafeed292017-12-12 17:32:44 -0800278 if (!uniqueNames.containsKey(senderName)) {
Tony Huang1250cd12018-06-06 15:40:47 +0800279 // Only use visible characters to get uniqueNames
280 String pureSenderName = IGNORABLE_CHAR_PATTERN
281 .matcher(senderName).replaceAll("" /* replacement */);
282 char c = pureSenderName.charAt(0);
Selim Cinek88188f22017-09-19 16:46:56 -0700283 if (uniqueCharacters.containsKey(c)) {
284 // this character was already used, lets make it more unique. We first need to
285 // resolve the existing character if it exists
286 CharSequence existingName = uniqueCharacters.get(c);
287 if (existingName != null) {
288 uniqueNames.put(existingName, findNameSplit((String) existingName));
289 uniqueCharacters.put(c, null);
290 }
291 uniqueNames.put(senderName, findNameSplit((String) senderName));
292 } else {
293 uniqueNames.put(senderName, Character.toString(c));
Tony Huang1250cd12018-06-06 15:40:47 +0800294 uniqueCharacters.put(c, pureSenderName);
Selim Cinek88188f22017-09-19 16:46:56 -0700295 }
296 }
297 }
298
299 // Now that we have the correct symbols, let's look what we have cached
300 ArrayMap<CharSequence, Icon> cachedAvatars = new ArrayMap<>();
301 for (int i = 0; i < mGroups.size(); i++) {
302 // Let's now set the avatars
303 MessagingGroup group = mGroups.get(i);
Selim Cinek1397ea32018-01-16 17:34:52 -0800304 boolean isOwnMessage = group.getSender() == mUser;
Selim Cinek88188f22017-09-19 16:46:56 -0700305 CharSequence senderName = group.getSenderName();
Selim Cinekafeed292017-12-12 17:32:44 -0800306 if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)
Selim Cinekce8794f2018-05-23 16:46:05 -0700307 || (mIsOneToOne && mAvatarReplacement != null && !isOwnMessage)) {
Selim Cinek88188f22017-09-19 16:46:56 -0700308 continue;
309 }
310 String symbol = uniqueNames.get(senderName);
311 Icon cachedIcon = group.getAvatarSymbolIfMatching(senderName,
312 symbol, mLayoutColor);
313 if (cachedIcon != null) {
314 cachedAvatars.put(senderName, cachedIcon);
315 }
316 }
317
318 for (int i = 0; i < mGroups.size(); i++) {
319 // Let's now set the avatars
320 MessagingGroup group = mGroups.get(i);
321 CharSequence senderName = group.getSenderName();
Selim Cinekafeed292017-12-12 17:32:44 -0800322 if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)) {
Selim Cinek88188f22017-09-19 16:46:56 -0700323 continue;
324 }
Selim Cinekce8794f2018-05-23 16:46:05 -0700325 if (mIsOneToOne && mAvatarReplacement != null && group.getSender() != mUser) {
326 group.setAvatar(mAvatarReplacement);
Selim Cinek88188f22017-09-19 16:46:56 -0700327 } else {
328 Icon cachedIcon = cachedAvatars.get(senderName);
329 if (cachedIcon == null) {
330 cachedIcon = createAvatarSymbol(senderName, uniqueNames.get(senderName),
331 mLayoutColor);
332 cachedAvatars.put(senderName, cachedIcon);
333 }
334 group.setCreatedAvatar(cachedIcon, senderName, uniqueNames.get(senderName),
335 mLayoutColor);
336 }
337 }
338 }
339
340 public Icon createAvatarSymbol(CharSequence senderName, String symbol, int layoutColor) {
Tony Huang45523682018-05-02 11:42:27 +0800341 if (symbol.isEmpty() || TextUtils.isDigitsOnly(symbol) ||
342 SPECIAL_CHAR_PATTERN.matcher(symbol).find()) {
343 Icon avatarIcon = Icon.createWithResource(getContext(),
344 com.android.internal.R.drawable.messaging_user);
345 avatarIcon.setTint(findColor(senderName, layoutColor));
346 return avatarIcon;
347 } else {
348 Bitmap bitmap = Bitmap.createBitmap(mAvatarSize, mAvatarSize, Bitmap.Config.ARGB_8888);
349 Canvas canvas = new Canvas(bitmap);
350 float radius = mAvatarSize / 2.0f;
351 int color = findColor(senderName, layoutColor);
352 mPaint.setColor(color);
353 canvas.drawCircle(radius, radius, radius, mPaint);
354 boolean needDarkText = ColorUtils.calculateLuminance(color) > 0.5f;
355 mTextPaint.setColor(needDarkText ? Color.BLACK : Color.WHITE);
356 mTextPaint.setTextSize(symbol.length() == 1 ? mAvatarSize * 0.5f : mAvatarSize * 0.3f);
357 int yPos = (int) (radius - ((mTextPaint.descent() + mTextPaint.ascent()) / 2));
358 canvas.drawText(symbol, radius, yPos, mTextPaint);
359 return Icon.createWithBitmap(bitmap);
360 }
Selim Cinek88188f22017-09-19 16:46:56 -0700361 }
362
363 private int findColor(CharSequence senderName, int layoutColor) {
Lucas Dupina291d192018-06-07 13:59:42 -0700364 double luminance = ContrastColorUtil.calculateLuminance(layoutColor);
Selim Cinek88188f22017-09-19 16:46:56 -0700365 float shift = Math.abs(senderName.hashCode()) % 5 / 4.0f - 0.5f;
366
367 // we need to offset the range if the luminance is too close to the borders
368 shift += Math.max(COLOR_SHIFT_AMOUNT / 2.0f / 100 - luminance, 0);
369 shift -= Math.max(COLOR_SHIFT_AMOUNT / 2.0f / 100 - (1.0f - luminance), 0);
Lucas Dupina291d192018-06-07 13:59:42 -0700370 return ContrastColorUtil.getShiftedColor(layoutColor,
Selim Cinek88188f22017-09-19 16:46:56 -0700371 (int) (shift * COLOR_SHIFT_AMOUNT));
372 }
373
374 private String findNameSplit(String existingName) {
375 String[] split = existingName.split(" ");
376 if (split.length > 1) {
377 return Character.toString(split[0].charAt(0))
378 + Character.toString(split[1].charAt(0));
379 }
380 return existingName.substring(0, 1);
381 }
382
383 @RemotableViewMethod
384 public void setLayoutColor(int color) {
385 mLayoutColor = color;
386 }
387
388 @RemotableViewMethod
389 public void setIsOneToOne(boolean oneToOne) {
390 mIsOneToOne = oneToOne;
391 }
392
Kenny Guy14d035c2018-05-02 19:10:36 +0100393 @RemotableViewMethod
394 public void setSenderTextColor(int color) {
395 mSenderTextColor = color;
396 }
397
Selim Cineke9714eb2020-03-09 11:21:49 -0700398
399 /**
400 * @param color the color of the notification background
401 */
402 @RemotableViewMethod
403 public void setNotificationBackgroundColor(int color) {
404 // Nothing to do with this
405 }
406
Kenny Guy14d035c2018-05-02 19:10:36 +0100407 @RemotableViewMethod
408 public void setMessageTextColor(int color) {
409 mMessageTextColor = color;
410 }
411
Selim Cinek9acd6732018-03-23 16:39:02 -0700412 public void setUser(Person user) {
Selim Cinekafeed292017-12-12 17:32:44 -0800413 mUser = user;
Selim Cinek1397ea32018-01-16 17:34:52 -0800414 if (mUser.getIcon() == null) {
415 Icon userIcon = Icon.createWithResource(getContext(),
416 com.android.internal.R.drawable.messaging_user);
417 userIcon.setTint(mLayoutColor);
Selim Cinek9acd6732018-03-23 16:39:02 -0700418 mUser = mUser.toBuilder().setIcon(userIcon).build();
Selim Cinek1397ea32018-01-16 17:34:52 -0800419 }
Selim Cinekafeed292017-12-12 17:32:44 -0800420 }
421
Selim Cinek88188f22017-09-19 16:46:56 -0700422 private void addMessagesToGroups(List<MessagingMessage> historicMessages,
Kenny Guya0f6de82018-04-06 16:20:16 +0100423 List<MessagingMessage> messages, boolean showSpinner) {
Selim Cinek88188f22017-09-19 16:46:56 -0700424 // Let's first find our groups!
425 List<List<MessagingMessage>> groups = new ArrayList<>();
Selim Cinek9acd6732018-03-23 16:39:02 -0700426 List<Person> senders = new ArrayList<>();
Selim Cinek88188f22017-09-19 16:46:56 -0700427
428 // Lets first find the groups
429 findGroups(historicMessages, messages, groups, senders);
430
431 // Let's now create the views and reorder them accordingly
Kenny Guya0f6de82018-04-06 16:20:16 +0100432 createGroupViews(groups, senders, showSpinner);
Selim Cinek88188f22017-09-19 16:46:56 -0700433 }
434
Selim Cinekafeed292017-12-12 17:32:44 -0800435 private void createGroupViews(List<List<MessagingMessage>> groups,
Kenny Guya0f6de82018-04-06 16:20:16 +0100436 List<Person> senders, boolean showSpinner) {
Selim Cinek88188f22017-09-19 16:46:56 -0700437 mGroups.clear();
438 for (int groupIndex = 0; groupIndex < groups.size(); groupIndex++) {
439 List<MessagingMessage> group = groups.get(groupIndex);
440 MessagingGroup newGroup = null;
441 // we'll just take the first group that exists or create one there is none
442 for (int messageIndex = group.size() - 1; messageIndex >= 0; messageIndex--) {
443 MessagingMessage message = group.get(messageIndex);
444 newGroup = message.getGroup();
445 if (newGroup != null) {
446 break;
447 }
448 }
449 if (newGroup == null) {
450 newGroup = MessagingGroup.createGroup(mMessagingLinearLayout);
Selim Cinekd3809992017-10-27 16:09:20 -0700451 mAddedGroups.add(newGroup);
Selim Cinek88188f22017-09-19 16:46:56 -0700452 }
Selim Cinek514b52c2020-03-17 18:42:12 -0700453 newGroup.setImageDisplayLocation(mDisplayImagesAtEnd
454 ? IMAGE_DISPLAY_LOCATION_AT_END
455 : IMAGE_DISPLAY_LOCATION_INLINE);
Selim Cineka91778a32020-03-13 17:30:34 -0700456 newGroup.setIsInConversation(false);
Selim Cinek88188f22017-09-19 16:46:56 -0700457 newGroup.setLayoutColor(mLayoutColor);
Kenny Guy14d035c2018-05-02 19:10:36 +0100458 newGroup.setTextColors(mSenderTextColor, mMessageTextColor);
Selim Cinek9acd6732018-03-23 16:39:02 -0700459 Person sender = senders.get(groupIndex);
Selim Cinek2dd3e722018-01-19 11:06:06 -0800460 CharSequence nameOverride = null;
461 if (sender != mUser && mNameReplacement != null) {
462 nameOverride = mNameReplacement;
463 }
464 newGroup.setSender(sender, nameOverride);
Kenny Guya0f6de82018-04-06 16:20:16 +0100465 newGroup.setSending(groupIndex == (groups.size() - 1) && showSpinner);
Selim Cinek88188f22017-09-19 16:46:56 -0700466 mGroups.add(newGroup);
467
468 if (mMessagingLinearLayout.indexOfChild(newGroup) != groupIndex) {
469 mMessagingLinearLayout.removeView(newGroup);
470 mMessagingLinearLayout.addView(newGroup, groupIndex);
471 }
472 newGroup.setMessages(group);
473 }
474 }
475
476 private void findGroups(List<MessagingMessage> historicMessages,
477 List<MessagingMessage> messages, List<List<MessagingMessage>> groups,
Selim Cinek9acd6732018-03-23 16:39:02 -0700478 List<Person> senders) {
Selim Cinekcb8b9852017-12-15 18:01:52 -0800479 CharSequence currentSenderKey = null;
Selim Cinek88188f22017-09-19 16:46:56 -0700480 List<MessagingMessage> currentGroup = null;
481 int histSize = historicMessages.size();
482 for (int i = 0; i < histSize + messages.size(); i++) {
483 MessagingMessage message;
484 if (i < histSize) {
485 message = historicMessages.get(i);
486 } else {
487 message = messages.get(i - histSize);
488 }
489 boolean isNewGroup = currentGroup == null;
Selim Cinek9acd6732018-03-23 16:39:02 -0700490 Person sender = message.getMessage().getSenderPerson();
Selim Cinekafeed292017-12-12 17:32:44 -0800491 CharSequence key = sender == null ? null
492 : sender.getKey() == null ? sender.getName() : sender.getKey();
Selim Cinekcb8b9852017-12-15 18:01:52 -0800493 isNewGroup |= !TextUtils.equals(key, currentSenderKey);
Selim Cinek88188f22017-09-19 16:46:56 -0700494 if (isNewGroup) {
495 currentGroup = new ArrayList<>();
496 groups.add(currentGroup);
Selim Cinekafeed292017-12-12 17:32:44 -0800497 if (sender == null) {
498 sender = mUser;
499 }
500 senders.add(sender);
Selim Cinekcb8b9852017-12-15 18:01:52 -0800501 currentSenderKey = key;
Selim Cinek88188f22017-09-19 16:46:56 -0700502 }
503 currentGroup.add(message);
504 }
505 }
506
Selim Cinek88188f22017-09-19 16:46:56 -0700507 /**
508 * Creates new messages, reusing existing ones if they are available.
509 *
510 * @param newMessages the messages to parse.
511 */
512 private List<MessagingMessage> createMessages(
513 List<Notification.MessagingStyle.Message> newMessages, boolean historic) {
Ahan Wude396fa2018-05-08 20:42:24 +0800514 List<MessagingMessage> result = new ArrayList<>();
Selim Cinek88188f22017-09-19 16:46:56 -0700515 for (int i = 0; i < newMessages.size(); i++) {
516 Notification.MessagingStyle.Message m = newMessages.get(i);
517 MessagingMessage message = findAndRemoveMatchingMessage(m);
518 if (message == null) {
Ahan Wude396fa2018-05-08 20:42:24 +0800519 message = MessagingMessage.createMessage(this, m, mImageResolver);
Selim Cinek88188f22017-09-19 16:46:56 -0700520 }
521 message.setIsHistoric(historic);
522 result.add(message);
523 }
524 return result;
525 }
526
527 private MessagingMessage findAndRemoveMatchingMessage(Notification.MessagingStyle.Message m) {
528 for (int i = 0; i < mMessages.size(); i++) {
529 MessagingMessage existing = mMessages.get(i);
530 if (existing.sameAs(m)) {
531 mMessages.remove(i);
532 return existing;
533 }
534 }
535 for (int i = 0; i < mHistoricMessages.size(); i++) {
536 MessagingMessage existing = mHistoricMessages.get(i);
537 if (existing.sameAs(m)) {
538 mHistoricMessages.remove(i);
539 return existing;
540 }
541 }
542 return null;
543 }
544
545 public void showHistoricMessages(boolean show) {
546 mShowHistoricMessages = show;
547 updateHistoricMessageVisibility();
548 }
549
550 private void updateHistoricMessageVisibility() {
Selim Cinekf45ca9b2018-05-04 11:37:24 -0700551 int numHistoric = mHistoricMessages.size();
552 for (int i = 0; i < numHistoric; i++) {
Selim Cinek88188f22017-09-19 16:46:56 -0700553 MessagingMessage existing = mHistoricMessages.get(i);
554 existing.setVisibility(mShowHistoricMessages ? VISIBLE : GONE);
555 }
Selim Cinekf45ca9b2018-05-04 11:37:24 -0700556 int numGroups = mGroups.size();
557 for (int i = 0; i < numGroups; i++) {
558 MessagingGroup group = mGroups.get(i);
559 int visibleChildren = 0;
560 List<MessagingMessage> messages = group.getMessages();
561 int numGroupMessages = messages.size();
562 for (int j = 0; j < numGroupMessages; j++) {
563 MessagingMessage message = messages.get(j);
564 if (message.getVisibility() != GONE) {
565 visibleChildren++;
566 }
567 }
568 if (visibleChildren > 0 && group.getVisibility() == GONE) {
569 group.setVisibility(VISIBLE);
570 } else if (visibleChildren == 0 && group.getVisibility() != GONE) {
571 group.setVisibility(GONE);
572 }
573 }
Selim Cinek88188f22017-09-19 16:46:56 -0700574 }
575
Selim Cinekd3809992017-10-27 16:09:20 -0700576 @Override
577 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
578 super.onLayout(changed, left, top, right, bottom);
579 if (!mAddedGroups.isEmpty()) {
580 getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
581 @Override
582 public boolean onPreDraw() {
583 for (MessagingGroup group : mAddedGroups) {
584 if (!group.isShown()) {
585 continue;
586 }
587 MessagingPropertyAnimator.fadeIn(group.getAvatar());
Selim Cinek1397ea32018-01-16 17:34:52 -0800588 MessagingPropertyAnimator.fadeIn(group.getSenderView());
Selim Cinekd3809992017-10-27 16:09:20 -0700589 MessagingPropertyAnimator.startLocalTranslationFrom(group,
590 group.getHeight(), LINEAR_OUT_SLOW_IN);
591 }
592 mAddedGroups.clear();
593 getViewTreeObserver().removeOnPreDrawListener(this);
594 return true;
595 }
596 });
597 }
598 }
599
Selim Cinek88188f22017-09-19 16:46:56 -0700600 public MessagingLinearLayout getMessagingLinearLayout() {
601 return mMessagingLinearLayout;
602 }
Selim Cinek1d6b50e2017-10-27 16:10:57 -0700603
604 public ArrayList<MessagingGroup> getMessagingGroups() {
605 return mGroups;
606 }
Selim Cinek514b52c2020-03-17 18:42:12 -0700607
608 @Override
609 public void setMessagingClippingDisabled(boolean clippingDisabled) {
610 // Don't do anything, this is only used for the ConversationLayout
611 }
Selim Cinek88188f22017-09-19 16:46:56 -0700612}