1 /*
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 
17 package com.android.internal.widget;
18 
19 import android.annotation.AttrRes;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.annotation.StyleRes;
23 import android.app.Notification;
24 import android.app.Person;
25 import android.content.Context;
26 import android.graphics.Bitmap;
27 import android.graphics.Canvas;
28 import android.graphics.Color;
29 import android.graphics.Paint;
30 import android.graphics.Rect;
31 import android.graphics.drawable.Icon;
32 import android.os.Bundle;
33 import android.os.Parcelable;
34 import android.text.TextUtils;
35 import android.util.ArrayMap;
36 import android.util.AttributeSet;
37 import android.util.DisplayMetrics;
38 import android.view.RemotableViewMethod;
39 import android.view.ViewTreeObserver;
40 import android.view.animation.Interpolator;
41 import android.view.animation.PathInterpolator;
42 import android.widget.FrameLayout;
43 import android.widget.RemoteViews;
44 import android.widget.TextView;
45 
46 import com.android.internal.R;
47 import com.android.internal.graphics.ColorUtils;
48 import com.android.internal.util.ContrastColorUtil;
49 
50 import java.util.ArrayList;
51 import java.util.List;
52 import java.util.function.Consumer;
53 import java.util.regex.Pattern;
54 
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
60 public class MessagingLayout extends FrameLayout implements ImageMessageConsumer {
61 
62     private static final float COLOR_SHIFT_AMOUNT = 60;
63     /**
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}]");
70     private static final Pattern SPECIAL_CHAR_PATTERN
71             = Pattern.compile ("[!@#$%&*()_+=|<>?{}\\[\\]~-]");
72     private static final Consumer<MessagingMessage> REMOVE_MESSAGE
73             = MessagingMessage::removeMessage;
74     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();
79     private List<MessagingMessage> mMessages = new ArrayList<>();
80     private List<MessagingMessage> mHistoricMessages = new ArrayList<>();
81     private MessagingLinearLayout mMessagingLinearLayout;
82     private boolean mShowHistoricMessages;
83     private ArrayList<MessagingGroup> mGroups = new ArrayList<>();
84     private TextView mTitleView;
85     private int mLayoutColor;
86     private int mSenderTextColor;
87     private int mMessageTextColor;
88     private int mAvatarSize;
89     private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
90     private Paint mTextPaint = new Paint();
91     private CharSequence mConversationTitle;
92     private Icon mAvatarReplacement;
93     private boolean mIsOneToOne;
94     private ArrayList<MessagingGroup> mAddedGroups = new ArrayList<>();
95     private Person mUser;
96     private CharSequence mNameReplacement;
97     private boolean mDisplayImagesAtEnd;
98     private ImageResolver mImageResolver;
99 
MessagingLayout(@onNull Context context)100     public MessagingLayout(@NonNull Context context) {
101         super(context);
102     }
103 
MessagingLayout(@onNull Context context, @Nullable AttributeSet attrs)104     public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
105         super(context, attrs);
106     }
107 
MessagingLayout(@onNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr)108     public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs,
109             @AttrRes int defStyleAttr) {
110         super(context, attrs, defStyleAttr);
111     }
112 
MessagingLayout(@onNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes)113     public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs,
114             @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
115         super(context, attrs, defStyleAttr, defStyleRes);
116     }
117 
118     @Override
onFinishInflate()119     protected void onFinishInflate() {
120         super.onFinishInflate();
121         mMessagingLinearLayout = findViewById(R.id.notification_messaging);
122         mMessagingLinearLayout.setMessagingLayout(this);
123         // We still want to clip, but only on the top, since views can temporarily out of bounds
124         // during transitions.
125         DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
126         int size = Math.max(displayMetrics.widthPixels, displayMetrics.heightPixels);
127         Rect rect = new Rect(0, 0, size, size);
128         mMessagingLinearLayout.setClipBounds(rect);
129         mTitleView = findViewById(R.id.title);
130         mAvatarSize = getResources().getDimensionPixelSize(R.dimen.messaging_avatar_size);
131         mTextPaint.setTextAlign(Paint.Align.CENTER);
132         mTextPaint.setAntiAlias(true);
133     }
134 
135     @RemotableViewMethod
setAvatarReplacement(Icon icon)136     public void setAvatarReplacement(Icon icon) {
137         mAvatarReplacement = icon;
138     }
139 
140     @RemotableViewMethod
setNameReplacement(CharSequence nameReplacement)141     public void setNameReplacement(CharSequence nameReplacement) {
142         mNameReplacement = nameReplacement;
143     }
144 
145     @RemotableViewMethod
setDisplayImagesAtEnd(boolean atEnd)146     public void setDisplayImagesAtEnd(boolean atEnd) {
147         mDisplayImagesAtEnd = atEnd;
148     }
149 
150     @RemotableViewMethod
setData(Bundle extras)151     public void setData(Bundle extras) {
152         Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES);
153         List<Notification.MessagingStyle.Message> newMessages
154                 = Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages);
155         Parcelable[] histMessages = extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES);
156         List<Notification.MessagingStyle.Message> newHistoricMessages
157                 = Notification.MessagingStyle.Message.getMessagesFromBundleArray(histMessages);
158         setUser(extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON));
159         mConversationTitle = null;
160         TextView headerText = findViewById(R.id.header_text);
161         if (headerText != null) {
162             mConversationTitle = headerText.getText();
163         }
164         addRemoteInputHistoryToMessages(newMessages,
165                 extras.getCharSequenceArray(Notification.EXTRA_REMOTE_INPUT_HISTORY));
166         boolean showSpinner =
167                 extras.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false);
168         bind(newMessages, newHistoricMessages, showSpinner);
169     }
170 
171     @Override
setImageResolver(ImageResolver resolver)172     public void setImageResolver(ImageResolver resolver) {
173         mImageResolver = resolver;
174     }
175 
addRemoteInputHistoryToMessages( List<Notification.MessagingStyle.Message> newMessages, CharSequence[] remoteInputHistory)176     private void addRemoteInputHistoryToMessages(
177             List<Notification.MessagingStyle.Message> newMessages,
178             CharSequence[] remoteInputHistory) {
179         if (remoteInputHistory == null || remoteInputHistory.length == 0) {
180             return;
181         }
182         for (int i = remoteInputHistory.length - 1; i >= 0; i--) {
183             CharSequence message = remoteInputHistory[i];
184             newMessages.add(new Notification.MessagingStyle.Message(
185                     message, 0, (Person) null, true /* remoteHistory */));
186         }
187     }
188 
bind(List<Notification.MessagingStyle.Message> newMessages, List<Notification.MessagingStyle.Message> newHistoricMessages, boolean showSpinner)189     private void bind(List<Notification.MessagingStyle.Message> newMessages,
190             List<Notification.MessagingStyle.Message> newHistoricMessages,
191             boolean showSpinner) {
192 
193         List<MessagingMessage> historicMessages = createMessages(newHistoricMessages,
194                 true /* isHistoric */);
195         List<MessagingMessage> messages = createMessages(newMessages, false /* isHistoric */);
196 
197         ArrayList<MessagingGroup> oldGroups = new ArrayList<>(mGroups);
198         addMessagesToGroups(historicMessages, messages, showSpinner);
199 
200         // Let's first check which groups were removed altogether and remove them in one animation
201         removeGroups(oldGroups);
202 
203         // Let's remove the remaining messages
204         mMessages.forEach(REMOVE_MESSAGE);
205         mHistoricMessages.forEach(REMOVE_MESSAGE);
206 
207         mMessages = messages;
208         mHistoricMessages = historicMessages;
209 
210         updateHistoricMessageVisibility();
211         updateTitleAndNamesDisplay();
212     }
213 
removeGroups(ArrayList<MessagingGroup> oldGroups)214     private void removeGroups(ArrayList<MessagingGroup> oldGroups) {
215         int size = oldGroups.size();
216         for (int i = 0; i < size; i++) {
217             MessagingGroup group = oldGroups.get(i);
218             if (!mGroups.contains(group)) {
219                 List<MessagingMessage> messages = group.getMessages();
220                 Runnable endRunnable = () -> {
221                     mMessagingLinearLayout.removeTransientView(group);
222                     group.recycle();
223                 };
224 
225                 boolean wasShown = group.isShown();
226                 mMessagingLinearLayout.removeView(group);
227                 if (wasShown && !MessagingLinearLayout.isGone(group)) {
228                     mMessagingLinearLayout.addTransientView(group, 0);
229                     group.removeGroupAnimated(endRunnable);
230                 } else {
231                     endRunnable.run();
232                 }
233                 mMessages.removeAll(messages);
234                 mHistoricMessages.removeAll(messages);
235             }
236         }
237     }
238 
updateTitleAndNamesDisplay()239     private void updateTitleAndNamesDisplay() {
240         ArrayMap<CharSequence, String> uniqueNames = new ArrayMap<>();
241         ArrayMap<Character, CharSequence> uniqueCharacters = new ArrayMap<>();
242         for (int i = 0; i < mGroups.size(); i++) {
243             MessagingGroup group = mGroups.get(i);
244             CharSequence senderName = group.getSenderName();
245             if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)) {
246                 continue;
247             }
248             if (!uniqueNames.containsKey(senderName)) {
249                 // Only use visible characters to get uniqueNames
250                 String pureSenderName = IGNORABLE_CHAR_PATTERN
251                         .matcher(senderName).replaceAll("" /* replacement */);
252                 char c = pureSenderName.charAt(0);
253                 if (uniqueCharacters.containsKey(c)) {
254                     // this character was already used, lets make it more unique. We first need to
255                     // resolve the existing character if it exists
256                     CharSequence existingName = uniqueCharacters.get(c);
257                     if (existingName != null) {
258                         uniqueNames.put(existingName, findNameSplit((String) existingName));
259                         uniqueCharacters.put(c, null);
260                     }
261                     uniqueNames.put(senderName, findNameSplit((String) senderName));
262                 } else {
263                     uniqueNames.put(senderName, Character.toString(c));
264                     uniqueCharacters.put(c, pureSenderName);
265                 }
266             }
267         }
268 
269         // Now that we have the correct symbols, let's look what we have cached
270         ArrayMap<CharSequence, Icon> cachedAvatars = new ArrayMap<>();
271         for (int i = 0; i < mGroups.size(); i++) {
272             // Let's now set the avatars
273             MessagingGroup group = mGroups.get(i);
274             boolean isOwnMessage = group.getSender() == mUser;
275             CharSequence senderName = group.getSenderName();
276             if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)
277                     || (mIsOneToOne && mAvatarReplacement != null && !isOwnMessage)) {
278                 continue;
279             }
280             String symbol = uniqueNames.get(senderName);
281             Icon cachedIcon = group.getAvatarSymbolIfMatching(senderName,
282                     symbol, mLayoutColor);
283             if (cachedIcon != null) {
284                 cachedAvatars.put(senderName, cachedIcon);
285             }
286         }
287 
288         for (int i = 0; i < mGroups.size(); i++) {
289             // Let's now set the avatars
290             MessagingGroup group = mGroups.get(i);
291             CharSequence senderName = group.getSenderName();
292             if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)) {
293                 continue;
294             }
295             if (mIsOneToOne && mAvatarReplacement != null && group.getSender() != mUser) {
296                 group.setAvatar(mAvatarReplacement);
297             } else {
298                 Icon cachedIcon = cachedAvatars.get(senderName);
299                 if (cachedIcon == null) {
300                     cachedIcon = createAvatarSymbol(senderName, uniqueNames.get(senderName),
301                             mLayoutColor);
302                     cachedAvatars.put(senderName, cachedIcon);
303                 }
304                 group.setCreatedAvatar(cachedIcon, senderName, uniqueNames.get(senderName),
305                         mLayoutColor);
306             }
307         }
308     }
309 
createAvatarSymbol(CharSequence senderName, String symbol, int layoutColor)310     public Icon createAvatarSymbol(CharSequence senderName, String symbol, int layoutColor) {
311         if (symbol.isEmpty() || TextUtils.isDigitsOnly(symbol) ||
312                 SPECIAL_CHAR_PATTERN.matcher(symbol).find()) {
313             Icon avatarIcon = Icon.createWithResource(getContext(),
314                     com.android.internal.R.drawable.messaging_user);
315             avatarIcon.setTint(findColor(senderName, layoutColor));
316             return avatarIcon;
317         } else {
318             Bitmap bitmap = Bitmap.createBitmap(mAvatarSize, mAvatarSize, Bitmap.Config.ARGB_8888);
319             Canvas canvas = new Canvas(bitmap);
320             float radius = mAvatarSize / 2.0f;
321             int color = findColor(senderName, layoutColor);
322             mPaint.setColor(color);
323             canvas.drawCircle(radius, radius, radius, mPaint);
324             boolean needDarkText = ColorUtils.calculateLuminance(color) > 0.5f;
325             mTextPaint.setColor(needDarkText ? Color.BLACK : Color.WHITE);
326             mTextPaint.setTextSize(symbol.length() == 1 ? mAvatarSize * 0.5f : mAvatarSize * 0.3f);
327             int yPos = (int) (radius - ((mTextPaint.descent() + mTextPaint.ascent()) / 2));
328             canvas.drawText(symbol, radius, yPos, mTextPaint);
329             return Icon.createWithBitmap(bitmap);
330         }
331     }
332 
findColor(CharSequence senderName, int layoutColor)333     private int findColor(CharSequence senderName, int layoutColor) {
334         double luminance = ContrastColorUtil.calculateLuminance(layoutColor);
335         float shift = Math.abs(senderName.hashCode()) % 5 / 4.0f - 0.5f;
336 
337         // we need to offset the range if the luminance is too close to the borders
338         shift += Math.max(COLOR_SHIFT_AMOUNT / 2.0f / 100 - luminance, 0);
339         shift -= Math.max(COLOR_SHIFT_AMOUNT / 2.0f / 100 - (1.0f - luminance), 0);
340         return ContrastColorUtil.getShiftedColor(layoutColor,
341                 (int) (shift * COLOR_SHIFT_AMOUNT));
342     }
343 
findNameSplit(String existingName)344     private String findNameSplit(String existingName) {
345         String[] split = existingName.split(" ");
346         if (split.length > 1) {
347             return Character.toString(split[0].charAt(0))
348                     + Character.toString(split[1].charAt(0));
349         }
350         return existingName.substring(0, 1);
351     }
352 
353     @RemotableViewMethod
setLayoutColor(int color)354     public void setLayoutColor(int color) {
355         mLayoutColor = color;
356     }
357 
358     @RemotableViewMethod
setIsOneToOne(boolean oneToOne)359     public void setIsOneToOne(boolean oneToOne) {
360         mIsOneToOne = oneToOne;
361     }
362 
363     @RemotableViewMethod
setSenderTextColor(int color)364     public void setSenderTextColor(int color) {
365         mSenderTextColor = color;
366     }
367 
368     @RemotableViewMethod
setMessageTextColor(int color)369     public void setMessageTextColor(int color) {
370         mMessageTextColor = color;
371     }
372 
setUser(Person user)373     public void setUser(Person user) {
374         mUser = user;
375         if (mUser.getIcon() == null) {
376             Icon userIcon = Icon.createWithResource(getContext(),
377                     com.android.internal.R.drawable.messaging_user);
378             userIcon.setTint(mLayoutColor);
379             mUser = mUser.toBuilder().setIcon(userIcon).build();
380         }
381     }
382 
addMessagesToGroups(List<MessagingMessage> historicMessages, List<MessagingMessage> messages, boolean showSpinner)383     private void addMessagesToGroups(List<MessagingMessage> historicMessages,
384             List<MessagingMessage> messages, boolean showSpinner) {
385         // Let's first find our groups!
386         List<List<MessagingMessage>> groups = new ArrayList<>();
387         List<Person> senders = new ArrayList<>();
388 
389         // Lets first find the groups
390         findGroups(historicMessages, messages, groups, senders);
391 
392         // Let's now create the views and reorder them accordingly
393         createGroupViews(groups, senders, showSpinner);
394     }
395 
createGroupViews(List<List<MessagingMessage>> groups, List<Person> senders, boolean showSpinner)396     private void createGroupViews(List<List<MessagingMessage>> groups,
397             List<Person> senders, boolean showSpinner) {
398         mGroups.clear();
399         for (int groupIndex = 0; groupIndex < groups.size(); groupIndex++) {
400             List<MessagingMessage> group = groups.get(groupIndex);
401             MessagingGroup newGroup = null;
402             // we'll just take the first group that exists or create one there is none
403             for (int messageIndex = group.size() - 1; messageIndex >= 0; messageIndex--) {
404                 MessagingMessage message = group.get(messageIndex);
405                 newGroup = message.getGroup();
406                 if (newGroup != null) {
407                     break;
408                 }
409             }
410             if (newGroup == null) {
411                 newGroup = MessagingGroup.createGroup(mMessagingLinearLayout);
412                 mAddedGroups.add(newGroup);
413             }
414             newGroup.setDisplayImagesAtEnd(mDisplayImagesAtEnd);
415             newGroup.setLayoutColor(mLayoutColor);
416             newGroup.setTextColors(mSenderTextColor, mMessageTextColor);
417             Person sender = senders.get(groupIndex);
418             CharSequence nameOverride = null;
419             if (sender != mUser && mNameReplacement != null) {
420                 nameOverride = mNameReplacement;
421             }
422             newGroup.setSender(sender, nameOverride);
423             newGroup.setSending(groupIndex == (groups.size() - 1) && showSpinner);
424             mGroups.add(newGroup);
425 
426             if (mMessagingLinearLayout.indexOfChild(newGroup) != groupIndex) {
427                 mMessagingLinearLayout.removeView(newGroup);
428                 mMessagingLinearLayout.addView(newGroup, groupIndex);
429             }
430             newGroup.setMessages(group);
431         }
432     }
433 
findGroups(List<MessagingMessage> historicMessages, List<MessagingMessage> messages, List<List<MessagingMessage>> groups, List<Person> senders)434     private void findGroups(List<MessagingMessage> historicMessages,
435             List<MessagingMessage> messages, List<List<MessagingMessage>> groups,
436             List<Person> senders) {
437         CharSequence currentSenderKey = null;
438         List<MessagingMessage> currentGroup = null;
439         int histSize = historicMessages.size();
440         for (int i = 0; i < histSize + messages.size(); i++) {
441             MessagingMessage message;
442             if (i < histSize) {
443                 message = historicMessages.get(i);
444             } else {
445                 message = messages.get(i - histSize);
446             }
447             boolean isNewGroup = currentGroup == null;
448             Person sender = message.getMessage().getSenderPerson();
449             CharSequence key = sender == null ? null
450                     : sender.getKey() == null ? sender.getName() : sender.getKey();
451             isNewGroup |= !TextUtils.equals(key, currentSenderKey);
452             if (isNewGroup) {
453                 currentGroup = new ArrayList<>();
454                 groups.add(currentGroup);
455                 if (sender == null) {
456                     sender = mUser;
457                 }
458                 senders.add(sender);
459                 currentSenderKey = key;
460             }
461             currentGroup.add(message);
462         }
463     }
464 
465     /**
466      * Creates new messages, reusing existing ones if they are available.
467      *
468      * @param newMessages the messages to parse.
469      */
createMessages( List<Notification.MessagingStyle.Message> newMessages, boolean historic)470     private List<MessagingMessage> createMessages(
471             List<Notification.MessagingStyle.Message> newMessages, boolean historic) {
472         List<MessagingMessage> result = new ArrayList<>();
473         for (int i = 0; i < newMessages.size(); i++) {
474             Notification.MessagingStyle.Message m = newMessages.get(i);
475             MessagingMessage message = findAndRemoveMatchingMessage(m);
476             if (message == null) {
477                 message = MessagingMessage.createMessage(this, m, mImageResolver);
478             }
479             message.setIsHistoric(historic);
480             result.add(message);
481         }
482         return result;
483     }
484 
findAndRemoveMatchingMessage(Notification.MessagingStyle.Message m)485     private MessagingMessage findAndRemoveMatchingMessage(Notification.MessagingStyle.Message m) {
486         for (int i = 0; i < mMessages.size(); i++) {
487             MessagingMessage existing = mMessages.get(i);
488             if (existing.sameAs(m)) {
489                 mMessages.remove(i);
490                 return existing;
491             }
492         }
493         for (int i = 0; i < mHistoricMessages.size(); i++) {
494             MessagingMessage existing = mHistoricMessages.get(i);
495             if (existing.sameAs(m)) {
496                 mHistoricMessages.remove(i);
497                 return existing;
498             }
499         }
500         return null;
501     }
502 
showHistoricMessages(boolean show)503     public void showHistoricMessages(boolean show) {
504         mShowHistoricMessages = show;
505         updateHistoricMessageVisibility();
506     }
507 
updateHistoricMessageVisibility()508     private void updateHistoricMessageVisibility() {
509         int numHistoric = mHistoricMessages.size();
510         for (int i = 0; i < numHistoric; i++) {
511             MessagingMessage existing = mHistoricMessages.get(i);
512             existing.setVisibility(mShowHistoricMessages ? VISIBLE : GONE);
513         }
514         int numGroups = mGroups.size();
515         for (int i = 0; i < numGroups; i++) {
516             MessagingGroup group = mGroups.get(i);
517             int visibleChildren = 0;
518             List<MessagingMessage> messages = group.getMessages();
519             int numGroupMessages = messages.size();
520             for (int j = 0; j < numGroupMessages; j++) {
521                 MessagingMessage message = messages.get(j);
522                 if (message.getVisibility() != GONE) {
523                     visibleChildren++;
524                 }
525             }
526             if (visibleChildren > 0 && group.getVisibility() == GONE) {
527                 group.setVisibility(VISIBLE);
528             } else if (visibleChildren == 0 && group.getVisibility() != GONE)   {
529                 group.setVisibility(GONE);
530             }
531         }
532     }
533 
534     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)535     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
536         super.onLayout(changed, left, top, right, bottom);
537         if (!mAddedGroups.isEmpty()) {
538             getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
539                 @Override
540                 public boolean onPreDraw() {
541                     for (MessagingGroup group : mAddedGroups) {
542                         if (!group.isShown()) {
543                             continue;
544                         }
545                         MessagingPropertyAnimator.fadeIn(group.getAvatar());
546                         MessagingPropertyAnimator.fadeIn(group.getSenderView());
547                         MessagingPropertyAnimator.startLocalTranslationFrom(group,
548                                 group.getHeight(), LINEAR_OUT_SLOW_IN);
549                     }
550                     mAddedGroups.clear();
551                     getViewTreeObserver().removeOnPreDrawListener(this);
552                     return true;
553                 }
554             });
555         }
556     }
557 
getMessagingLinearLayout()558     public MessagingLinearLayout getMessagingLinearLayout() {
559         return mMessagingLinearLayout;
560     }
561 
getMessagingGroups()562     public ArrayList<MessagingGroup> getMessagingGroups() {
563         return mGroups;
564     }
565 }
566