blob: 0fd610908fd55ede8593ea11eec7bebec248f1df [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 Huang45523682018-05-02 11:42:27 +080063 private static final Pattern SPECIAL_CHAR_PATTERN
64 = Pattern.compile ("[!@#$%&*()_+=|<>?{}\\[\\]~-]");
Selim Cinek88188f22017-09-19 16:46:56 -070065 private static final Consumer<MessagingMessage> REMOVE_MESSAGE
66 = MessagingMessage::removeMessage;
Selim Cinekd3809992017-10-27 16:09:20 -070067 public static final Interpolator LINEAR_OUT_SLOW_IN = new PathInterpolator(0f, 0f, 0.2f, 1f);
68 public static final Interpolator FAST_OUT_LINEAR_IN = new PathInterpolator(0.4f, 0f, 1f, 1f);
69 public static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f);
70 public static final OnLayoutChangeListener MESSAGING_PROPERTY_ANIMATOR
71 = new MessagingPropertyAnimator();
Selim Cinek88188f22017-09-19 16:46:56 -070072 private List<MessagingMessage> mMessages = new ArrayList<>();
73 private List<MessagingMessage> mHistoricMessages = new ArrayList<>();
74 private MessagingLinearLayout mMessagingLinearLayout;
Selim Cinek88188f22017-09-19 16:46:56 -070075 private boolean mShowHistoricMessages;
76 private ArrayList<MessagingGroup> mGroups = new ArrayList<>();
77 private TextView mTitleView;
78 private int mLayoutColor;
Kenny Guy14d035c2018-05-02 19:10:36 +010079 private int mSenderTextColor;
80 private int mMessageTextColor;
Selim Cinek88188f22017-09-19 16:46:56 -070081 private int mAvatarSize;
82 private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
83 private Paint mTextPaint = new Paint();
84 private CharSequence mConversationTitle;
Selim Cinekce8794f2018-05-23 16:46:05 -070085 private Icon mAvatarReplacement;
Selim Cinek88188f22017-09-19 16:46:56 -070086 private boolean mIsOneToOne;
Selim Cinekd3809992017-10-27 16:09:20 -070087 private ArrayList<MessagingGroup> mAddedGroups = new ArrayList<>();
Selim Cinek9acd6732018-03-23 16:39:02 -070088 private Person mUser;
Selim Cinek2dd3e722018-01-19 11:06:06 -080089 private CharSequence mNameReplacement;
Selim Cinek85d0e6e2018-03-23 18:08:32 -070090 private boolean mDisplayImagesAtEnd;
Selim Cinek88188f22017-09-19 16:46:56 -070091
92 public MessagingLayout(@NonNull Context context) {
93 super(context);
94 }
95
96 public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
97 super(context, attrs);
98 }
99
100 public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs,
101 @AttrRes int defStyleAttr) {
102 super(context, attrs, defStyleAttr);
103 }
104
105 public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs,
106 @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
107 super(context, attrs, defStyleAttr, defStyleRes);
108 }
109
110 @Override
111 protected void onFinishInflate() {
112 super.onFinishInflate();
113 mMessagingLinearLayout = findViewById(R.id.notification_messaging);
Selim Cinek1d6b50e2017-10-27 16:10:57 -0700114 mMessagingLinearLayout.setMessagingLayout(this);
Selim Cinekd3809992017-10-27 16:09:20 -0700115 // We still want to clip, but only on the top, since views can temporarily out of bounds
116 // during transitions.
117 DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
Selim Cinek1110fd72018-05-01 10:53:26 -0700118 int size = Math.max(displayMetrics.widthPixels, displayMetrics.heightPixels);
119 Rect rect = new Rect(0, 0, size, size);
Selim Cinekd3809992017-10-27 16:09:20 -0700120 mMessagingLinearLayout.setClipBounds(rect);
Selim Cinek88188f22017-09-19 16:46:56 -0700121 mTitleView = findViewById(R.id.title);
122 mAvatarSize = getResources().getDimensionPixelSize(R.dimen.messaging_avatar_size);
123 mTextPaint.setTextAlign(Paint.Align.CENTER);
124 mTextPaint.setAntiAlias(true);
125 }
126
127 @RemotableViewMethod
Selim Cinekce8794f2018-05-23 16:46:05 -0700128 public void setAvatarReplacement(Icon icon) {
129 mAvatarReplacement = icon;
Selim Cinek88188f22017-09-19 16:46:56 -0700130 }
131
132 @RemotableViewMethod
Selim Cinek2dd3e722018-01-19 11:06:06 -0800133 public void setNameReplacement(CharSequence nameReplacement) {
134 mNameReplacement = nameReplacement;
135 }
136
137 @RemotableViewMethod
Selim Cinek85d0e6e2018-03-23 18:08:32 -0700138 public void setDisplayImagesAtEnd(boolean atEnd) {
139 mDisplayImagesAtEnd = atEnd;
Selim Cinek7199ed92018-01-26 13:36:43 -0800140 }
141
142 @RemotableViewMethod
Selim Cinek88188f22017-09-19 16:46:56 -0700143 public void setData(Bundle extras) {
144 Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES);
145 List<Notification.MessagingStyle.Message> newMessages
146 = Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages);
147 Parcelable[] histMessages = extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES);
148 List<Notification.MessagingStyle.Message> newHistoricMessages
149 = Notification.MessagingStyle.Message.getMessagesFromBundleArray(histMessages);
Selim Cinekafeed292017-12-12 17:32:44 -0800150 setUser(extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON));
Selim Cinek88188f22017-09-19 16:46:56 -0700151 mConversationTitle = null;
152 TextView headerText = findViewById(R.id.header_text);
153 if (headerText != null) {
154 mConversationTitle = headerText.getText();
155 }
Selim Cinek1397ea32018-01-16 17:34:52 -0800156 addRemoteInputHistoryToMessages(newMessages,
157 extras.getCharSequenceArray(Notification.EXTRA_REMOTE_INPUT_HISTORY));
Kenny Guya0f6de82018-04-06 16:20:16 +0100158 boolean showSpinner =
159 extras.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false);
160 bind(newMessages, newHistoricMessages, showSpinner);
Selim Cinek88188f22017-09-19 16:46:56 -0700161 }
162
Selim Cinek1397ea32018-01-16 17:34:52 -0800163 private void addRemoteInputHistoryToMessages(
164 List<Notification.MessagingStyle.Message> newMessages,
165 CharSequence[] remoteInputHistory) {
166 if (remoteInputHistory == null || remoteInputHistory.length == 0) {
167 return;
168 }
169 for (int i = remoteInputHistory.length - 1; i >= 0; i--) {
170 CharSequence message = remoteInputHistory[i];
171 newMessages.add(new Notification.MessagingStyle.Message(
Kenny Guya0f6de82018-04-06 16:20:16 +0100172 message, 0, (Person) null, true /* remoteHistory */));
Selim Cinek1397ea32018-01-16 17:34:52 -0800173 }
174 }
175
Selim Cinek88188f22017-09-19 16:46:56 -0700176 private void bind(List<Notification.MessagingStyle.Message> newMessages,
Kenny Guya0f6de82018-04-06 16:20:16 +0100177 List<Notification.MessagingStyle.Message> newHistoricMessages,
178 boolean showSpinner) {
Selim Cinek88188f22017-09-19 16:46:56 -0700179
180 List<MessagingMessage> historicMessages = createMessages(newHistoricMessages,
181 true /* isHistoric */);
182 List<MessagingMessage> messages = createMessages(newMessages, false /* isHistoric */);
Selim Cinekf68af9b2018-05-24 16:37:22 -0700183
184 ArrayList<MessagingGroup> oldGroups = new ArrayList<>(mGroups);
Kenny Guya0f6de82018-04-06 16:20:16 +0100185 addMessagesToGroups(historicMessages, messages, showSpinner);
Selim Cinek88188f22017-09-19 16:46:56 -0700186
Selim Cinekf68af9b2018-05-24 16:37:22 -0700187 // Let's first check which groups were removed altogether and remove them in one animation
188 removeGroups(oldGroups);
189
Selim Cinek88188f22017-09-19 16:46:56 -0700190 // Let's remove the remaining messages
191 mMessages.forEach(REMOVE_MESSAGE);
192 mHistoricMessages.forEach(REMOVE_MESSAGE);
193
194 mMessages = messages;
195 mHistoricMessages = historicMessages;
196
Selim Cinek88188f22017-09-19 16:46:56 -0700197 updateHistoricMessageVisibility();
198 updateTitleAndNamesDisplay();
199 }
200
Selim Cinekf68af9b2018-05-24 16:37:22 -0700201 private void removeGroups(ArrayList<MessagingGroup> oldGroups) {
202 int size = oldGroups.size();
203 for (int i = 0; i < size; i++) {
204 MessagingGroup group = oldGroups.get(i);
205 if (!mGroups.contains(group)) {
206 List<MessagingMessage> messages = group.getMessages();
207 Runnable endRunnable = () -> {
208 mMessagingLinearLayout.removeTransientView(group);
209 group.recycle();
210 };
211
212 boolean wasShown = group.isShown();
213 mMessagingLinearLayout.removeView(group);
214 if (wasShown && !MessagingLinearLayout.isGone(group)) {
215 mMessagingLinearLayout.addTransientView(group, 0);
216 group.removeGroupAnimated(endRunnable);
217 } else {
218 endRunnable.run();
219 }
220 mMessages.removeAll(messages);
221 mHistoricMessages.removeAll(messages);
222 }
223 }
224 }
225
Selim Cinek88188f22017-09-19 16:46:56 -0700226 private void updateTitleAndNamesDisplay() {
227 ArrayMap<CharSequence, String> uniqueNames = new ArrayMap<>();
228 ArrayMap<Character, CharSequence> uniqueCharacters = new ArrayMap<>();
229 for (int i = 0; i < mGroups.size(); i++) {
230 MessagingGroup group = mGroups.get(i);
231 CharSequence senderName = group.getSenderName();
Selim Cinekafeed292017-12-12 17:32:44 -0800232 if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)) {
Selim Cinek88188f22017-09-19 16:46:56 -0700233 continue;
234 }
Selim Cinekafeed292017-12-12 17:32:44 -0800235 if (!uniqueNames.containsKey(senderName)) {
Selim Cinek88188f22017-09-19 16:46:56 -0700236 char c = senderName.charAt(0);
237 if (uniqueCharacters.containsKey(c)) {
238 // this character was already used, lets make it more unique. We first need to
239 // resolve the existing character if it exists
240 CharSequence existingName = uniqueCharacters.get(c);
241 if (existingName != null) {
242 uniqueNames.put(existingName, findNameSplit((String) existingName));
243 uniqueCharacters.put(c, null);
244 }
245 uniqueNames.put(senderName, findNameSplit((String) senderName));
246 } else {
247 uniqueNames.put(senderName, Character.toString(c));
248 uniqueCharacters.put(c, senderName);
249 }
250 }
251 }
252
253 // Now that we have the correct symbols, let's look what we have cached
254 ArrayMap<CharSequence, Icon> cachedAvatars = new ArrayMap<>();
255 for (int i = 0; i < mGroups.size(); i++) {
256 // Let's now set the avatars
257 MessagingGroup group = mGroups.get(i);
Selim Cinek1397ea32018-01-16 17:34:52 -0800258 boolean isOwnMessage = group.getSender() == mUser;
Selim Cinek88188f22017-09-19 16:46:56 -0700259 CharSequence senderName = group.getSenderName();
Selim Cinekafeed292017-12-12 17:32:44 -0800260 if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)
Selim Cinekce8794f2018-05-23 16:46:05 -0700261 || (mIsOneToOne && mAvatarReplacement != null && !isOwnMessage)) {
Selim Cinek88188f22017-09-19 16:46:56 -0700262 continue;
263 }
264 String symbol = uniqueNames.get(senderName);
265 Icon cachedIcon = group.getAvatarSymbolIfMatching(senderName,
266 symbol, mLayoutColor);
267 if (cachedIcon != null) {
268 cachedAvatars.put(senderName, cachedIcon);
269 }
270 }
271
272 for (int i = 0; i < mGroups.size(); i++) {
273 // Let's now set the avatars
274 MessagingGroup group = mGroups.get(i);
275 CharSequence senderName = group.getSenderName();
Selim Cinekafeed292017-12-12 17:32:44 -0800276 if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)) {
Selim Cinek88188f22017-09-19 16:46:56 -0700277 continue;
278 }
Selim Cinekce8794f2018-05-23 16:46:05 -0700279 if (mIsOneToOne && mAvatarReplacement != null && group.getSender() != mUser) {
280 group.setAvatar(mAvatarReplacement);
Selim Cinek88188f22017-09-19 16:46:56 -0700281 } else {
282 Icon cachedIcon = cachedAvatars.get(senderName);
283 if (cachedIcon == null) {
284 cachedIcon = createAvatarSymbol(senderName, uniqueNames.get(senderName),
285 mLayoutColor);
286 cachedAvatars.put(senderName, cachedIcon);
287 }
288 group.setCreatedAvatar(cachedIcon, senderName, uniqueNames.get(senderName),
289 mLayoutColor);
290 }
291 }
292 }
293
294 public Icon createAvatarSymbol(CharSequence senderName, String symbol, int layoutColor) {
Tony Huang45523682018-05-02 11:42:27 +0800295 if (symbol.isEmpty() || TextUtils.isDigitsOnly(symbol) ||
296 SPECIAL_CHAR_PATTERN.matcher(symbol).find()) {
297 Icon avatarIcon = Icon.createWithResource(getContext(),
298 com.android.internal.R.drawable.messaging_user);
299 avatarIcon.setTint(findColor(senderName, layoutColor));
300 return avatarIcon;
301 } else {
302 Bitmap bitmap = Bitmap.createBitmap(mAvatarSize, mAvatarSize, Bitmap.Config.ARGB_8888);
303 Canvas canvas = new Canvas(bitmap);
304 float radius = mAvatarSize / 2.0f;
305 int color = findColor(senderName, layoutColor);
306 mPaint.setColor(color);
307 canvas.drawCircle(radius, radius, radius, mPaint);
308 boolean needDarkText = ColorUtils.calculateLuminance(color) > 0.5f;
309 mTextPaint.setColor(needDarkText ? Color.BLACK : Color.WHITE);
310 mTextPaint.setTextSize(symbol.length() == 1 ? mAvatarSize * 0.5f : mAvatarSize * 0.3f);
311 int yPos = (int) (radius - ((mTextPaint.descent() + mTextPaint.ascent()) / 2));
312 canvas.drawText(symbol, radius, yPos, mTextPaint);
313 return Icon.createWithBitmap(bitmap);
314 }
Selim Cinek88188f22017-09-19 16:46:56 -0700315 }
316
317 private int findColor(CharSequence senderName, int layoutColor) {
318 double luminance = NotificationColorUtil.calculateLuminance(layoutColor);
319 float shift = Math.abs(senderName.hashCode()) % 5 / 4.0f - 0.5f;
320
321 // we need to offset the range if the luminance is too close to the borders
322 shift += Math.max(COLOR_SHIFT_AMOUNT / 2.0f / 100 - luminance, 0);
323 shift -= Math.max(COLOR_SHIFT_AMOUNT / 2.0f / 100 - (1.0f - luminance), 0);
324 return NotificationColorUtil.getShiftedColor(layoutColor,
325 (int) (shift * COLOR_SHIFT_AMOUNT));
326 }
327
328 private String findNameSplit(String existingName) {
329 String[] split = existingName.split(" ");
330 if (split.length > 1) {
331 return Character.toString(split[0].charAt(0))
332 + Character.toString(split[1].charAt(0));
333 }
334 return existingName.substring(0, 1);
335 }
336
337 @RemotableViewMethod
338 public void setLayoutColor(int color) {
339 mLayoutColor = color;
340 }
341
342 @RemotableViewMethod
343 public void setIsOneToOne(boolean oneToOne) {
344 mIsOneToOne = oneToOne;
345 }
346
Kenny Guy14d035c2018-05-02 19:10:36 +0100347 @RemotableViewMethod
348 public void setSenderTextColor(int color) {
349 mSenderTextColor = color;
350 }
351
352 @RemotableViewMethod
353 public void setMessageTextColor(int color) {
354 mMessageTextColor = color;
355 }
356
Selim Cinek9acd6732018-03-23 16:39:02 -0700357 public void setUser(Person user) {
Selim Cinekafeed292017-12-12 17:32:44 -0800358 mUser = user;
Selim Cinek1397ea32018-01-16 17:34:52 -0800359 if (mUser.getIcon() == null) {
360 Icon userIcon = Icon.createWithResource(getContext(),
361 com.android.internal.R.drawable.messaging_user);
362 userIcon.setTint(mLayoutColor);
Selim Cinek9acd6732018-03-23 16:39:02 -0700363 mUser = mUser.toBuilder().setIcon(userIcon).build();
Selim Cinek1397ea32018-01-16 17:34:52 -0800364 }
Selim Cinekafeed292017-12-12 17:32:44 -0800365 }
366
Selim Cinek88188f22017-09-19 16:46:56 -0700367 private void addMessagesToGroups(List<MessagingMessage> historicMessages,
Kenny Guya0f6de82018-04-06 16:20:16 +0100368 List<MessagingMessage> messages, boolean showSpinner) {
Selim Cinek88188f22017-09-19 16:46:56 -0700369 // Let's first find our groups!
370 List<List<MessagingMessage>> groups = new ArrayList<>();
Selim Cinek9acd6732018-03-23 16:39:02 -0700371 List<Person> senders = new ArrayList<>();
Selim Cinek88188f22017-09-19 16:46:56 -0700372
373 // Lets first find the groups
374 findGroups(historicMessages, messages, groups, senders);
375
376 // Let's now create the views and reorder them accordingly
Kenny Guya0f6de82018-04-06 16:20:16 +0100377 createGroupViews(groups, senders, showSpinner);
Selim Cinek88188f22017-09-19 16:46:56 -0700378 }
379
Selim Cinekafeed292017-12-12 17:32:44 -0800380 private void createGroupViews(List<List<MessagingMessage>> groups,
Kenny Guya0f6de82018-04-06 16:20:16 +0100381 List<Person> senders, boolean showSpinner) {
Selim Cinek88188f22017-09-19 16:46:56 -0700382 mGroups.clear();
383 for (int groupIndex = 0; groupIndex < groups.size(); groupIndex++) {
384 List<MessagingMessage> group = groups.get(groupIndex);
385 MessagingGroup newGroup = null;
386 // we'll just take the first group that exists or create one there is none
387 for (int messageIndex = group.size() - 1; messageIndex >= 0; messageIndex--) {
388 MessagingMessage message = group.get(messageIndex);
389 newGroup = message.getGroup();
390 if (newGroup != null) {
391 break;
392 }
393 }
394 if (newGroup == null) {
395 newGroup = MessagingGroup.createGroup(mMessagingLinearLayout);
Selim Cinekd3809992017-10-27 16:09:20 -0700396 mAddedGroups.add(newGroup);
Selim Cinek88188f22017-09-19 16:46:56 -0700397 }
Selim Cinek85d0e6e2018-03-23 18:08:32 -0700398 newGroup.setDisplayImagesAtEnd(mDisplayImagesAtEnd);
Selim Cinek88188f22017-09-19 16:46:56 -0700399 newGroup.setLayoutColor(mLayoutColor);
Kenny Guy14d035c2018-05-02 19:10:36 +0100400 newGroup.setTextColors(mSenderTextColor, mMessageTextColor);
Selim Cinek9acd6732018-03-23 16:39:02 -0700401 Person sender = senders.get(groupIndex);
Selim Cinek2dd3e722018-01-19 11:06:06 -0800402 CharSequence nameOverride = null;
403 if (sender != mUser && mNameReplacement != null) {
404 nameOverride = mNameReplacement;
405 }
406 newGroup.setSender(sender, nameOverride);
Kenny Guya0f6de82018-04-06 16:20:16 +0100407 newGroup.setSending(groupIndex == (groups.size() - 1) && showSpinner);
Selim Cinek88188f22017-09-19 16:46:56 -0700408 mGroups.add(newGroup);
409
410 if (mMessagingLinearLayout.indexOfChild(newGroup) != groupIndex) {
411 mMessagingLinearLayout.removeView(newGroup);
412 mMessagingLinearLayout.addView(newGroup, groupIndex);
413 }
414 newGroup.setMessages(group);
415 }
416 }
417
418 private void findGroups(List<MessagingMessage> historicMessages,
419 List<MessagingMessage> messages, List<List<MessagingMessage>> groups,
Selim Cinek9acd6732018-03-23 16:39:02 -0700420 List<Person> senders) {
Selim Cinekcb8b9852017-12-15 18:01:52 -0800421 CharSequence currentSenderKey = null;
Selim Cinek88188f22017-09-19 16:46:56 -0700422 List<MessagingMessage> currentGroup = null;
423 int histSize = historicMessages.size();
424 for (int i = 0; i < histSize + messages.size(); i++) {
425 MessagingMessage message;
426 if (i < histSize) {
427 message = historicMessages.get(i);
428 } else {
429 message = messages.get(i - histSize);
430 }
431 boolean isNewGroup = currentGroup == null;
Selim Cinek9acd6732018-03-23 16:39:02 -0700432 Person sender = message.getMessage().getSenderPerson();
Selim Cinekafeed292017-12-12 17:32:44 -0800433 CharSequence key = sender == null ? null
434 : sender.getKey() == null ? sender.getName() : sender.getKey();
Selim Cinekcb8b9852017-12-15 18:01:52 -0800435 isNewGroup |= !TextUtils.equals(key, currentSenderKey);
Selim Cinek88188f22017-09-19 16:46:56 -0700436 if (isNewGroup) {
437 currentGroup = new ArrayList<>();
438 groups.add(currentGroup);
Selim Cinekafeed292017-12-12 17:32:44 -0800439 if (sender == null) {
440 sender = mUser;
441 }
442 senders.add(sender);
Selim Cinekcb8b9852017-12-15 18:01:52 -0800443 currentSenderKey = key;
Selim Cinek88188f22017-09-19 16:46:56 -0700444 }
445 currentGroup.add(message);
446 }
447 }
448
Selim Cinek88188f22017-09-19 16:46:56 -0700449 /**
450 * Creates new messages, reusing existing ones if they are available.
451 *
452 * @param newMessages the messages to parse.
453 */
454 private List<MessagingMessage> createMessages(
455 List<Notification.MessagingStyle.Message> newMessages, boolean historic) {
456 List<MessagingMessage> result = new ArrayList<>();;
457 for (int i = 0; i < newMessages.size(); i++) {
458 Notification.MessagingStyle.Message m = newMessages.get(i);
459 MessagingMessage message = findAndRemoveMatchingMessage(m);
460 if (message == null) {
461 message = MessagingMessage.createMessage(this, m);
462 }
463 message.setIsHistoric(historic);
464 result.add(message);
465 }
466 return result;
467 }
468
469 private MessagingMessage findAndRemoveMatchingMessage(Notification.MessagingStyle.Message m) {
470 for (int i = 0; i < mMessages.size(); i++) {
471 MessagingMessage existing = mMessages.get(i);
472 if (existing.sameAs(m)) {
473 mMessages.remove(i);
474 return existing;
475 }
476 }
477 for (int i = 0; i < mHistoricMessages.size(); i++) {
478 MessagingMessage existing = mHistoricMessages.get(i);
479 if (existing.sameAs(m)) {
480 mHistoricMessages.remove(i);
481 return existing;
482 }
483 }
484 return null;
485 }
486
487 public void showHistoricMessages(boolean show) {
488 mShowHistoricMessages = show;
489 updateHistoricMessageVisibility();
490 }
491
492 private void updateHistoricMessageVisibility() {
Selim Cinekf45ca9b2018-05-04 11:37:24 -0700493 int numHistoric = mHistoricMessages.size();
494 for (int i = 0; i < numHistoric; i++) {
Selim Cinek88188f22017-09-19 16:46:56 -0700495 MessagingMessage existing = mHistoricMessages.get(i);
496 existing.setVisibility(mShowHistoricMessages ? VISIBLE : GONE);
497 }
Selim Cinekf45ca9b2018-05-04 11:37:24 -0700498 int numGroups = mGroups.size();
499 for (int i = 0; i < numGroups; i++) {
500 MessagingGroup group = mGroups.get(i);
501 int visibleChildren = 0;
502 List<MessagingMessage> messages = group.getMessages();
503 int numGroupMessages = messages.size();
504 for (int j = 0; j < numGroupMessages; j++) {
505 MessagingMessage message = messages.get(j);
506 if (message.getVisibility() != GONE) {
507 visibleChildren++;
508 }
509 }
510 if (visibleChildren > 0 && group.getVisibility() == GONE) {
511 group.setVisibility(VISIBLE);
512 } else if (visibleChildren == 0 && group.getVisibility() != GONE) {
513 group.setVisibility(GONE);
514 }
515 }
Selim Cinek88188f22017-09-19 16:46:56 -0700516 }
517
Selim Cinekd3809992017-10-27 16:09:20 -0700518 @Override
519 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
520 super.onLayout(changed, left, top, right, bottom);
521 if (!mAddedGroups.isEmpty()) {
522 getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
523 @Override
524 public boolean onPreDraw() {
525 for (MessagingGroup group : mAddedGroups) {
526 if (!group.isShown()) {
527 continue;
528 }
529 MessagingPropertyAnimator.fadeIn(group.getAvatar());
Selim Cinek1397ea32018-01-16 17:34:52 -0800530 MessagingPropertyAnimator.fadeIn(group.getSenderView());
Selim Cinekd3809992017-10-27 16:09:20 -0700531 MessagingPropertyAnimator.startLocalTranslationFrom(group,
532 group.getHeight(), LINEAR_OUT_SLOW_IN);
533 }
534 mAddedGroups.clear();
535 getViewTreeObserver().removeOnPreDrawListener(this);
536 return true;
537 }
538 });
539 }
540 }
541
Selim Cinek88188f22017-09-19 16:46:56 -0700542 public MessagingLinearLayout getMessagingLinearLayout() {
543 return mMessagingLinearLayout;
544 }
Selim Cinek1d6b50e2017-10-27 16:10:57 -0700545
546 public ArrayList<MessagingGroup> getMessagingGroups() {
547 return mGroups;
548 }
Selim Cinek88188f22017-09-19 16:46:56 -0700549}