blob: 4ba93bc50dff06c2a3a980b48772abf13a287a0f [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
19import android.annotation.AttrRes;
20import android.annotation.NonNull;
21import android.annotation.Nullable;
22import android.annotation.StyleRes;
23import android.app.Notification;
Selim Cinek9acd6732018-03-23 16:39:02 -070024import android.app.Person;
Selim Cinek88188f22017-09-19 16:46:56 -070025import android.content.Context;
26import android.graphics.Bitmap;
27import android.graphics.Canvas;
28import android.graphics.Color;
29import android.graphics.Paint;
Selim Cinekd3809992017-10-27 16:09:20 -070030import android.graphics.Rect;
Selim Cinek88188f22017-09-19 16:46:56 -070031import android.graphics.drawable.Icon;
32import android.os.Bundle;
33import android.os.Parcelable;
34import android.text.TextUtils;
35import android.util.ArrayMap;
36import android.util.AttributeSet;
Selim Cinekd3809992017-10-27 16:09:20 -070037import android.util.DisplayMetrics;
Selim Cinek88188f22017-09-19 16:46:56 -070038import android.view.RemotableViewMethod;
Selim Cinekd3809992017-10-27 16:09:20 -070039import android.view.ViewTreeObserver;
40import android.view.animation.Interpolator;
41import android.view.animation.PathInterpolator;
Selim Cinek88188f22017-09-19 16:46:56 -070042import android.widget.FrameLayout;
43import android.widget.RemoteViews;
44import android.widget.TextView;
45
46import com.android.internal.R;
47import com.android.internal.graphics.ColorUtils;
48import com.android.internal.util.NotificationColorUtil;
49
50import java.util.ArrayList;
51import java.util.List;
52import java.util.function.Consumer;
Tony Huang45523682018-05-02 11:42:27 +080053import java.util.regex.Pattern;
Selim Cinek88188f22017-09-19 16:46:56 -070054
55/**
56 * A custom-built layout for the Notification.MessagingStyle allows dynamic addition and removal
57 * messages and adapts the layout accordingly.
58 */
59@RemoteViews.RemoteView
60public class MessagingLayout extends FrameLayout {
61
62 private static final float COLOR_SHIFT_AMOUNT = 60;
Tony Huang1250cd12018-06-06 15:40:47 +080063 /**
64 * Pattren for filter some ingonable characters.
65 * p{Z} for any kind of whitespace or invisible separator.
66 * p{C} for any kind of punctuation character.
67 */
68 private static final Pattern IGNORABLE_CHAR_PATTERN
69 = Pattern.compile("[\\p{C}\\p{Z}]");
Tony Huang45523682018-05-02 11:42:27 +080070 private static final Pattern SPECIAL_CHAR_PATTERN
71 = Pattern.compile ("[!@#$%&*()_+=|<>?{}\\[\\]~-]");
Selim Cinek88188f22017-09-19 16:46:56 -070072 private static final Consumer<MessagingMessage> REMOVE_MESSAGE
73 = MessagingMessage::removeMessage;
Selim Cinekd3809992017-10-27 16:09:20 -070074 public static final Interpolator LINEAR_OUT_SLOW_IN = new PathInterpolator(0f, 0f, 0.2f, 1f);
75 public static final Interpolator FAST_OUT_LINEAR_IN = new PathInterpolator(0.4f, 0f, 1f, 1f);
76 public static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f);
77 public static final OnLayoutChangeListener MESSAGING_PROPERTY_ANIMATOR
78 = new MessagingPropertyAnimator();
Selim Cinek88188f22017-09-19 16:46:56 -070079 private List<MessagingMessage> mMessages = new ArrayList<>();
80 private List<MessagingMessage> mHistoricMessages = new ArrayList<>();
81 private MessagingLinearLayout mMessagingLinearLayout;
Selim Cinek88188f22017-09-19 16:46:56 -070082 private boolean mShowHistoricMessages;
83 private ArrayList<MessagingGroup> mGroups = new ArrayList<>();
84 private TextView mTitleView;
85 private int mLayoutColor;
Kenny Guy14d035c2018-05-02 19:10:36 +010086 private int mSenderTextColor;
87 private int mMessageTextColor;
Selim Cinek88188f22017-09-19 16:46:56 -070088 private int mAvatarSize;
89 private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
90 private Paint mTextPaint = new Paint();
91 private CharSequence mConversationTitle;
Selim Cinekce8794f2018-05-23 16:46:05 -070092 private Icon mAvatarReplacement;
Selim Cinek88188f22017-09-19 16:46:56 -070093 private boolean mIsOneToOne;
Selim Cinekd3809992017-10-27 16:09:20 -070094 private ArrayList<MessagingGroup> mAddedGroups = new ArrayList<>();
Selim Cinek9acd6732018-03-23 16:39:02 -070095 private Person mUser;
Selim Cinek2dd3e722018-01-19 11:06:06 -080096 private CharSequence mNameReplacement;
Selim Cinek85d0e6e2018-03-23 18:08:32 -070097 private boolean mDisplayImagesAtEnd;
Selim Cinek88188f22017-09-19 16:46:56 -070098
99 public MessagingLayout(@NonNull Context context) {
100 super(context);
101 }
102
103 public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
104 super(context, attrs);
105 }
106
107 public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs,
108 @AttrRes int defStyleAttr) {
109 super(context, attrs, defStyleAttr);
110 }
111
112 public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs,
113 @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
114 super(context, attrs, defStyleAttr, defStyleRes);
115 }
116
117 @Override
118 protected void onFinishInflate() {
119 super.onFinishInflate();
120 mMessagingLinearLayout = findViewById(R.id.notification_messaging);
Selim Cinek1d6b50e2017-10-27 16:10:57 -0700121 mMessagingLinearLayout.setMessagingLayout(this);
Selim Cinekd3809992017-10-27 16:09:20 -0700122 // We still want to clip, but only on the top, since views can temporarily out of bounds
123 // during transitions.
124 DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
Selim Cinek1110fd72018-05-01 10:53:26 -0700125 int size = Math.max(displayMetrics.widthPixels, displayMetrics.heightPixels);
126 Rect rect = new Rect(0, 0, size, size);
Selim Cinekd3809992017-10-27 16:09:20 -0700127 mMessagingLinearLayout.setClipBounds(rect);
Selim Cinek88188f22017-09-19 16:46:56 -0700128 mTitleView = findViewById(R.id.title);
129 mAvatarSize = getResources().getDimensionPixelSize(R.dimen.messaging_avatar_size);
130 mTextPaint.setTextAlign(Paint.Align.CENTER);
131 mTextPaint.setAntiAlias(true);
132 }
133
134 @RemotableViewMethod
Selim Cinekce8794f2018-05-23 16:46:05 -0700135 public void setAvatarReplacement(Icon icon) {
136 mAvatarReplacement = icon;
Selim Cinek88188f22017-09-19 16:46:56 -0700137 }
138
139 @RemotableViewMethod
Selim Cinek2dd3e722018-01-19 11:06:06 -0800140 public void setNameReplacement(CharSequence nameReplacement) {
141 mNameReplacement = nameReplacement;
142 }
143
144 @RemotableViewMethod
Selim Cinek85d0e6e2018-03-23 18:08:32 -0700145 public void setDisplayImagesAtEnd(boolean atEnd) {
146 mDisplayImagesAtEnd = atEnd;
Selim Cinek7199ed92018-01-26 13:36:43 -0800147 }
148
149 @RemotableViewMethod
Selim Cinek88188f22017-09-19 16:46:56 -0700150 public void setData(Bundle extras) {
151 Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES);
152 List<Notification.MessagingStyle.Message> newMessages
153 = Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages);
154 Parcelable[] histMessages = extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES);
155 List<Notification.MessagingStyle.Message> newHistoricMessages
156 = Notification.MessagingStyle.Message.getMessagesFromBundleArray(histMessages);
Selim Cinekafeed292017-12-12 17:32:44 -0800157 setUser(extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON));
Selim Cinek88188f22017-09-19 16:46:56 -0700158 mConversationTitle = null;
159 TextView headerText = findViewById(R.id.header_text);
160 if (headerText != null) {
161 mConversationTitle = headerText.getText();
162 }
Selim Cinek1397ea32018-01-16 17:34:52 -0800163 addRemoteInputHistoryToMessages(newMessages,
164 extras.getCharSequenceArray(Notification.EXTRA_REMOTE_INPUT_HISTORY));
Kenny Guya0f6de82018-04-06 16:20:16 +0100165 boolean showSpinner =
166 extras.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false);
167 bind(newMessages, newHistoricMessages, showSpinner);
Selim Cinek88188f22017-09-19 16:46:56 -0700168 }
169
Selim Cinek1397ea32018-01-16 17:34:52 -0800170 private void addRemoteInputHistoryToMessages(
171 List<Notification.MessagingStyle.Message> newMessages,
172 CharSequence[] remoteInputHistory) {
173 if (remoteInputHistory == null || remoteInputHistory.length == 0) {
174 return;
175 }
176 for (int i = remoteInputHistory.length - 1; i >= 0; i--) {
177 CharSequence message = remoteInputHistory[i];
178 newMessages.add(new Notification.MessagingStyle.Message(
Kenny Guya0f6de82018-04-06 16:20:16 +0100179 message, 0, (Person) null, true /* remoteHistory */));
Selim Cinek1397ea32018-01-16 17:34:52 -0800180 }
181 }
182
Selim Cinek88188f22017-09-19 16:46:56 -0700183 private void bind(List<Notification.MessagingStyle.Message> newMessages,
Kenny Guya0f6de82018-04-06 16:20:16 +0100184 List<Notification.MessagingStyle.Message> newHistoricMessages,
185 boolean showSpinner) {
Selim Cinek88188f22017-09-19 16:46:56 -0700186
187 List<MessagingMessage> historicMessages = createMessages(newHistoricMessages,
188 true /* isHistoric */);
189 List<MessagingMessage> messages = createMessages(newMessages, false /* isHistoric */);
Selim Cinekf68af9b2018-05-24 16:37:22 -0700190
191 ArrayList<MessagingGroup> oldGroups = new ArrayList<>(mGroups);
Kenny Guya0f6de82018-04-06 16:20:16 +0100192 addMessagesToGroups(historicMessages, messages, showSpinner);
Selim Cinek88188f22017-09-19 16:46:56 -0700193
Selim Cinekf68af9b2018-05-24 16:37:22 -0700194 // Let's first check which groups were removed altogether and remove them in one animation
195 removeGroups(oldGroups);
196
Selim Cinek88188f22017-09-19 16:46:56 -0700197 // Let's remove the remaining messages
198 mMessages.forEach(REMOVE_MESSAGE);
199 mHistoricMessages.forEach(REMOVE_MESSAGE);
200
201 mMessages = messages;
202 mHistoricMessages = historicMessages;
203
Selim Cinek88188f22017-09-19 16:46:56 -0700204 updateHistoricMessageVisibility();
205 updateTitleAndNamesDisplay();
206 }
207
Selim Cinekf68af9b2018-05-24 16:37:22 -0700208 private void removeGroups(ArrayList<MessagingGroup> oldGroups) {
209 int size = oldGroups.size();
210 for (int i = 0; i < size; i++) {
211 MessagingGroup group = oldGroups.get(i);
212 if (!mGroups.contains(group)) {
213 List<MessagingMessage> messages = group.getMessages();
214 Runnable endRunnable = () -> {
215 mMessagingLinearLayout.removeTransientView(group);
216 group.recycle();
217 };
218
219 boolean wasShown = group.isShown();
220 mMessagingLinearLayout.removeView(group);
221 if (wasShown && !MessagingLinearLayout.isGone(group)) {
222 mMessagingLinearLayout.addTransientView(group, 0);
223 group.removeGroupAnimated(endRunnable);
224 } else {
225 endRunnable.run();
226 }
227 mMessages.removeAll(messages);
228 mHistoricMessages.removeAll(messages);
229 }
230 }
231 }
232
Selim Cinek88188f22017-09-19 16:46:56 -0700233 private void updateTitleAndNamesDisplay() {
234 ArrayMap<CharSequence, String> uniqueNames = new ArrayMap<>();
235 ArrayMap<Character, CharSequence> uniqueCharacters = new ArrayMap<>();
236 for (int i = 0; i < mGroups.size(); i++) {
237 MessagingGroup group = mGroups.get(i);
238 CharSequence senderName = group.getSenderName();
Selim Cinekafeed292017-12-12 17:32:44 -0800239 if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)) {
Selim Cinek88188f22017-09-19 16:46:56 -0700240 continue;
241 }
Selim Cinekafeed292017-12-12 17:32:44 -0800242 if (!uniqueNames.containsKey(senderName)) {
Tony Huang1250cd12018-06-06 15:40:47 +0800243 // Only use visible characters to get uniqueNames
244 String pureSenderName = IGNORABLE_CHAR_PATTERN
245 .matcher(senderName).replaceAll("" /* replacement */);
246 char c = pureSenderName.charAt(0);
Selim Cinek88188f22017-09-19 16:46:56 -0700247 if (uniqueCharacters.containsKey(c)) {
248 // this character was already used, lets make it more unique. We first need to
249 // resolve the existing character if it exists
250 CharSequence existingName = uniqueCharacters.get(c);
251 if (existingName != null) {
252 uniqueNames.put(existingName, findNameSplit((String) existingName));
253 uniqueCharacters.put(c, null);
254 }
255 uniqueNames.put(senderName, findNameSplit((String) senderName));
256 } else {
257 uniqueNames.put(senderName, Character.toString(c));
Tony Huang1250cd12018-06-06 15:40:47 +0800258 uniqueCharacters.put(c, pureSenderName);
Selim Cinek88188f22017-09-19 16:46:56 -0700259 }
260 }
261 }
262
263 // Now that we have the correct symbols, let's look what we have cached
264 ArrayMap<CharSequence, Icon> cachedAvatars = new ArrayMap<>();
265 for (int i = 0; i < mGroups.size(); i++) {
266 // Let's now set the avatars
267 MessagingGroup group = mGroups.get(i);
Selim Cinek1397ea32018-01-16 17:34:52 -0800268 boolean isOwnMessage = group.getSender() == mUser;
Selim Cinek88188f22017-09-19 16:46:56 -0700269 CharSequence senderName = group.getSenderName();
Selim Cinekafeed292017-12-12 17:32:44 -0800270 if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)
Selim Cinekce8794f2018-05-23 16:46:05 -0700271 || (mIsOneToOne && mAvatarReplacement != null && !isOwnMessage)) {
Selim Cinek88188f22017-09-19 16:46:56 -0700272 continue;
273 }
274 String symbol = uniqueNames.get(senderName);
275 Icon cachedIcon = group.getAvatarSymbolIfMatching(senderName,
276 symbol, mLayoutColor);
277 if (cachedIcon != null) {
278 cachedAvatars.put(senderName, cachedIcon);
279 }
280 }
281
282 for (int i = 0; i < mGroups.size(); i++) {
283 // Let's now set the avatars
284 MessagingGroup group = mGroups.get(i);
285 CharSequence senderName = group.getSenderName();
Selim Cinekafeed292017-12-12 17:32:44 -0800286 if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)) {
Selim Cinek88188f22017-09-19 16:46:56 -0700287 continue;
288 }
Selim Cinekce8794f2018-05-23 16:46:05 -0700289 if (mIsOneToOne && mAvatarReplacement != null && group.getSender() != mUser) {
290 group.setAvatar(mAvatarReplacement);
Selim Cinek88188f22017-09-19 16:46:56 -0700291 } else {
292 Icon cachedIcon = cachedAvatars.get(senderName);
293 if (cachedIcon == null) {
294 cachedIcon = createAvatarSymbol(senderName, uniqueNames.get(senderName),
295 mLayoutColor);
296 cachedAvatars.put(senderName, cachedIcon);
297 }
298 group.setCreatedAvatar(cachedIcon, senderName, uniqueNames.get(senderName),
299 mLayoutColor);
300 }
301 }
302 }
303
304 public Icon createAvatarSymbol(CharSequence senderName, String symbol, int layoutColor) {
Tony Huang45523682018-05-02 11:42:27 +0800305 if (symbol.isEmpty() || TextUtils.isDigitsOnly(symbol) ||
306 SPECIAL_CHAR_PATTERN.matcher(symbol).find()) {
307 Icon avatarIcon = Icon.createWithResource(getContext(),
308 com.android.internal.R.drawable.messaging_user);
309 avatarIcon.setTint(findColor(senderName, layoutColor));
310 return avatarIcon;
311 } else {
312 Bitmap bitmap = Bitmap.createBitmap(mAvatarSize, mAvatarSize, Bitmap.Config.ARGB_8888);
313 Canvas canvas = new Canvas(bitmap);
314 float radius = mAvatarSize / 2.0f;
315 int color = findColor(senderName, layoutColor);
316 mPaint.setColor(color);
317 canvas.drawCircle(radius, radius, radius, mPaint);
318 boolean needDarkText = ColorUtils.calculateLuminance(color) > 0.5f;
319 mTextPaint.setColor(needDarkText ? Color.BLACK : Color.WHITE);
320 mTextPaint.setTextSize(symbol.length() == 1 ? mAvatarSize * 0.5f : mAvatarSize * 0.3f);
321 int yPos = (int) (radius - ((mTextPaint.descent() + mTextPaint.ascent()) / 2));
322 canvas.drawText(symbol, radius, yPos, mTextPaint);
323 return Icon.createWithBitmap(bitmap);
324 }
Selim Cinek88188f22017-09-19 16:46:56 -0700325 }
326
327 private int findColor(CharSequence senderName, int layoutColor) {
328 double luminance = NotificationColorUtil.calculateLuminance(layoutColor);
329 float shift = Math.abs(senderName.hashCode()) % 5 / 4.0f - 0.5f;
330
331 // we need to offset the range if the luminance is too close to the borders
332 shift += Math.max(COLOR_SHIFT_AMOUNT / 2.0f / 100 - luminance, 0);
333 shift -= Math.max(COLOR_SHIFT_AMOUNT / 2.0f / 100 - (1.0f - luminance), 0);
334 return NotificationColorUtil.getShiftedColor(layoutColor,
335 (int) (shift * COLOR_SHIFT_AMOUNT));
336 }
337
338 private String findNameSplit(String existingName) {
339 String[] split = existingName.split(" ");
340 if (split.length > 1) {
341 return Character.toString(split[0].charAt(0))
342 + Character.toString(split[1].charAt(0));
343 }
344 return existingName.substring(0, 1);
345 }
346
347 @RemotableViewMethod
348 public void setLayoutColor(int color) {
349 mLayoutColor = color;
350 }
351
352 @RemotableViewMethod
353 public void setIsOneToOne(boolean oneToOne) {
354 mIsOneToOne = oneToOne;
355 }
356
Kenny Guy14d035c2018-05-02 19:10:36 +0100357 @RemotableViewMethod
358 public void setSenderTextColor(int color) {
359 mSenderTextColor = color;
360 }
361
362 @RemotableViewMethod
363 public void setMessageTextColor(int color) {
364 mMessageTextColor = color;
365 }
366
Selim Cinek9acd6732018-03-23 16:39:02 -0700367 public void setUser(Person user) {
Selim Cinekafeed292017-12-12 17:32:44 -0800368 mUser = user;
Selim Cinek1397ea32018-01-16 17:34:52 -0800369 if (mUser.getIcon() == null) {
370 Icon userIcon = Icon.createWithResource(getContext(),
371 com.android.internal.R.drawable.messaging_user);
372 userIcon.setTint(mLayoutColor);
Selim Cinek9acd6732018-03-23 16:39:02 -0700373 mUser = mUser.toBuilder().setIcon(userIcon).build();
Selim Cinek1397ea32018-01-16 17:34:52 -0800374 }
Selim Cinekafeed292017-12-12 17:32:44 -0800375 }
376
Selim Cinek88188f22017-09-19 16:46:56 -0700377 private void addMessagesToGroups(List<MessagingMessage> historicMessages,
Kenny Guya0f6de82018-04-06 16:20:16 +0100378 List<MessagingMessage> messages, boolean showSpinner) {
Selim Cinek88188f22017-09-19 16:46:56 -0700379 // Let's first find our groups!
380 List<List<MessagingMessage>> groups = new ArrayList<>();
Selim Cinek9acd6732018-03-23 16:39:02 -0700381 List<Person> senders = new ArrayList<>();
Selim Cinek88188f22017-09-19 16:46:56 -0700382
383 // Lets first find the groups
384 findGroups(historicMessages, messages, groups, senders);
385
386 // Let's now create the views and reorder them accordingly
Kenny Guya0f6de82018-04-06 16:20:16 +0100387 createGroupViews(groups, senders, showSpinner);
Selim Cinek88188f22017-09-19 16:46:56 -0700388 }
389
Selim Cinekafeed292017-12-12 17:32:44 -0800390 private void createGroupViews(List<List<MessagingMessage>> groups,
Kenny Guya0f6de82018-04-06 16:20:16 +0100391 List<Person> senders, boolean showSpinner) {
Selim Cinek88188f22017-09-19 16:46:56 -0700392 mGroups.clear();
393 for (int groupIndex = 0; groupIndex < groups.size(); groupIndex++) {
394 List<MessagingMessage> group = groups.get(groupIndex);
395 MessagingGroup newGroup = null;
396 // we'll just take the first group that exists or create one there is none
397 for (int messageIndex = group.size() - 1; messageIndex >= 0; messageIndex--) {
398 MessagingMessage message = group.get(messageIndex);
399 newGroup = message.getGroup();
400 if (newGroup != null) {
401 break;
402 }
403 }
404 if (newGroup == null) {
405 newGroup = MessagingGroup.createGroup(mMessagingLinearLayout);
Selim Cinekd3809992017-10-27 16:09:20 -0700406 mAddedGroups.add(newGroup);
Selim Cinek88188f22017-09-19 16:46:56 -0700407 }
Selim Cinek85d0e6e2018-03-23 18:08:32 -0700408 newGroup.setDisplayImagesAtEnd(mDisplayImagesAtEnd);
Selim Cinek88188f22017-09-19 16:46:56 -0700409 newGroup.setLayoutColor(mLayoutColor);
Kenny Guy14d035c2018-05-02 19:10:36 +0100410 newGroup.setTextColors(mSenderTextColor, mMessageTextColor);
Selim Cinek9acd6732018-03-23 16:39:02 -0700411 Person sender = senders.get(groupIndex);
Selim Cinek2dd3e722018-01-19 11:06:06 -0800412 CharSequence nameOverride = null;
413 if (sender != mUser && mNameReplacement != null) {
414 nameOverride = mNameReplacement;
415 }
416 newGroup.setSender(sender, nameOverride);
Kenny Guya0f6de82018-04-06 16:20:16 +0100417 newGroup.setSending(groupIndex == (groups.size() - 1) && showSpinner);
Selim Cinek88188f22017-09-19 16:46:56 -0700418 mGroups.add(newGroup);
419
420 if (mMessagingLinearLayout.indexOfChild(newGroup) != groupIndex) {
421 mMessagingLinearLayout.removeView(newGroup);
422 mMessagingLinearLayout.addView(newGroup, groupIndex);
423 }
424 newGroup.setMessages(group);
425 }
426 }
427
428 private void findGroups(List<MessagingMessage> historicMessages,
429 List<MessagingMessage> messages, List<List<MessagingMessage>> groups,
Selim Cinek9acd6732018-03-23 16:39:02 -0700430 List<Person> senders) {
Selim Cinekcb8b9852017-12-15 18:01:52 -0800431 CharSequence currentSenderKey = null;
Selim Cinek88188f22017-09-19 16:46:56 -0700432 List<MessagingMessage> currentGroup = null;
433 int histSize = historicMessages.size();
434 for (int i = 0; i < histSize + messages.size(); i++) {
435 MessagingMessage message;
436 if (i < histSize) {
437 message = historicMessages.get(i);
438 } else {
439 message = messages.get(i - histSize);
440 }
441 boolean isNewGroup = currentGroup == null;
Selim Cinek9acd6732018-03-23 16:39:02 -0700442 Person sender = message.getMessage().getSenderPerson();
Selim Cinekafeed292017-12-12 17:32:44 -0800443 CharSequence key = sender == null ? null
444 : sender.getKey() == null ? sender.getName() : sender.getKey();
Selim Cinekcb8b9852017-12-15 18:01:52 -0800445 isNewGroup |= !TextUtils.equals(key, currentSenderKey);
Selim Cinek88188f22017-09-19 16:46:56 -0700446 if (isNewGroup) {
447 currentGroup = new ArrayList<>();
448 groups.add(currentGroup);
Selim Cinekafeed292017-12-12 17:32:44 -0800449 if (sender == null) {
450 sender = mUser;
451 }
452 senders.add(sender);
Selim Cinekcb8b9852017-12-15 18:01:52 -0800453 currentSenderKey = key;
Selim Cinek88188f22017-09-19 16:46:56 -0700454 }
455 currentGroup.add(message);
456 }
457 }
458
Selim Cinek88188f22017-09-19 16:46:56 -0700459 /**
460 * Creates new messages, reusing existing ones if they are available.
461 *
462 * @param newMessages the messages to parse.
463 */
464 private List<MessagingMessage> createMessages(
465 List<Notification.MessagingStyle.Message> newMessages, boolean historic) {
466 List<MessagingMessage> result = new ArrayList<>();;
467 for (int i = 0; i < newMessages.size(); i++) {
468 Notification.MessagingStyle.Message m = newMessages.get(i);
469 MessagingMessage message = findAndRemoveMatchingMessage(m);
470 if (message == null) {
471 message = MessagingMessage.createMessage(this, m);
472 }
473 message.setIsHistoric(historic);
474 result.add(message);
475 }
476 return result;
477 }
478
479 private MessagingMessage findAndRemoveMatchingMessage(Notification.MessagingStyle.Message m) {
480 for (int i = 0; i < mMessages.size(); i++) {
481 MessagingMessage existing = mMessages.get(i);
482 if (existing.sameAs(m)) {
483 mMessages.remove(i);
484 return existing;
485 }
486 }
487 for (int i = 0; i < mHistoricMessages.size(); i++) {
488 MessagingMessage existing = mHistoricMessages.get(i);
489 if (existing.sameAs(m)) {
490 mHistoricMessages.remove(i);
491 return existing;
492 }
493 }
494 return null;
495 }
496
497 public void showHistoricMessages(boolean show) {
498 mShowHistoricMessages = show;
499 updateHistoricMessageVisibility();
500 }
501
502 private void updateHistoricMessageVisibility() {
Selim Cinekf45ca9b2018-05-04 11:37:24 -0700503 int numHistoric = mHistoricMessages.size();
504 for (int i = 0; i < numHistoric; i++) {
Selim Cinek88188f22017-09-19 16:46:56 -0700505 MessagingMessage existing = mHistoricMessages.get(i);
506 existing.setVisibility(mShowHistoricMessages ? VISIBLE : GONE);
507 }
Selim Cinekf45ca9b2018-05-04 11:37:24 -0700508 int numGroups = mGroups.size();
509 for (int i = 0; i < numGroups; i++) {
510 MessagingGroup group = mGroups.get(i);
511 int visibleChildren = 0;
512 List<MessagingMessage> messages = group.getMessages();
513 int numGroupMessages = messages.size();
514 for (int j = 0; j < numGroupMessages; j++) {
515 MessagingMessage message = messages.get(j);
516 if (message.getVisibility() != GONE) {
517 visibleChildren++;
518 }
519 }
520 if (visibleChildren > 0 && group.getVisibility() == GONE) {
521 group.setVisibility(VISIBLE);
522 } else if (visibleChildren == 0 && group.getVisibility() != GONE) {
523 group.setVisibility(GONE);
524 }
525 }
Selim Cinek88188f22017-09-19 16:46:56 -0700526 }
527
Selim Cinekd3809992017-10-27 16:09:20 -0700528 @Override
529 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
530 super.onLayout(changed, left, top, right, bottom);
531 if (!mAddedGroups.isEmpty()) {
532 getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
533 @Override
534 public boolean onPreDraw() {
535 for (MessagingGroup group : mAddedGroups) {
536 if (!group.isShown()) {
537 continue;
538 }
539 MessagingPropertyAnimator.fadeIn(group.getAvatar());
Selim Cinek1397ea32018-01-16 17:34:52 -0800540 MessagingPropertyAnimator.fadeIn(group.getSenderView());
Selim Cinekd3809992017-10-27 16:09:20 -0700541 MessagingPropertyAnimator.startLocalTranslationFrom(group,
542 group.getHeight(), LINEAR_OUT_SLOW_IN);
543 }
544 mAddedGroups.clear();
545 getViewTreeObserver().removeOnPreDrawListener(this);
546 return true;
547 }
548 });
549 }
550 }
551
Selim Cinek88188f22017-09-19 16:46:56 -0700552 public MessagingLinearLayout getMessagingLinearLayout() {
553 return mMessagingLinearLayout;
554 }
Selim Cinek1d6b50e2017-10-27 16:10:57 -0700555
556 public ArrayList<MessagingGroup> getMessagingGroups() {
557 return mGroups;
558 }
Selim Cinek88188f22017-09-19 16:46:56 -0700559}