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 static com.android.internal.widget.MessagingGroup.IMAGE_DISPLAY_LOCATION_EXTERNAL;
20 import static com.android.internal.widget.MessagingGroup.IMAGE_DISPLAY_LOCATION_INLINE;
21 
22 import android.annotation.AttrRes;
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.annotation.StyleRes;
26 import android.app.Notification;
27 import android.app.Person;
28 import android.app.RemoteInputHistoryItem;
29 import android.content.Context;
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.View;
40 import android.view.ViewGroup;
41 import android.view.ViewTreeObserver;
42 import android.view.animation.Interpolator;
43 import android.view.animation.PathInterpolator;
44 import android.widget.FrameLayout;
45 import android.widget.ImageView;
46 import android.widget.RemoteViews;
47 
48 import com.android.internal.R;
49 import com.android.internal.util.ContrastColorUtil;
50 
51 import java.util.ArrayList;
52 import java.util.List;
53 import java.util.Map;
54 import java.util.function.Consumer;
55 
56 /**
57  * A custom-built layout for the Notification.MessagingStyle allows dynamic addition and removal
58  * messages and adapts the layout accordingly.
59  */
60 @RemoteViews.RemoteView
61 public class MessagingLayout extends FrameLayout
62         implements ImageMessageConsumer, IMessagingLayout {
63 
64     private static final float COLOR_SHIFT_AMOUNT = 60;
65     private static final Consumer<MessagingMessage> REMOVE_MESSAGE
66             = MessagingMessage::removeMessage;
67     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();
72     private final PeopleHelper mPeopleHelper = new PeopleHelper();
73     private List<MessagingMessage> mMessages = new ArrayList<>();
74     private List<MessagingMessage> mHistoricMessages = new ArrayList<>();
75     private MessagingLinearLayout mMessagingLinearLayout;
76     private boolean mShowHistoricMessages;
77     private ArrayList<MessagingGroup> mGroups = new ArrayList<>();
78     private MessagingLinearLayout mImageMessageContainer;
79     private ImageView mRightIconView;
80     private Rect mMessagingClipRect;
81     private int mLayoutColor;
82     private int mSenderTextColor;
83     private int mMessageTextColor;
84     private Icon mAvatarReplacement;
85     private boolean mIsOneToOne;
86     private ArrayList<MessagingGroup> mAddedGroups = new ArrayList<>();
87     private Person mUser;
88     private CharSequence mNameReplacement;
89     private boolean mIsCollapsed;
90     private ImageResolver mImageResolver;
91     private CharSequence mConversationTitle;
92 
MessagingLayout(@onNull Context context)93     public MessagingLayout(@NonNull Context context) {
94         super(context);
95     }
96 
MessagingLayout(@onNull Context context, @Nullable AttributeSet attrs)97     public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
98         super(context, attrs);
99     }
100 
MessagingLayout(@onNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr)101     public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs,
102             @AttrRes int defStyleAttr) {
103         super(context, attrs, defStyleAttr);
104     }
105 
MessagingLayout(@onNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes)106     public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs,
107             @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
108         super(context, attrs, defStyleAttr, defStyleRes);
109     }
110 
111     @Override
onFinishInflate()112     protected void onFinishInflate() {
113         super.onFinishInflate();
114         mPeopleHelper.init(getContext());
115         mMessagingLinearLayout = findViewById(R.id.notification_messaging);
116         mImageMessageContainer = findViewById(R.id.conversation_image_message_container);
117         mRightIconView = findViewById(R.id.right_icon);
118         // We still want to clip, but only on the top, since views can temporarily out of bounds
119         // during transitions.
120         DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
121         int size = Math.max(displayMetrics.widthPixels, displayMetrics.heightPixels);
122         mMessagingClipRect = new Rect(0, 0, size, size);
123         setMessagingClippingDisabled(false);
124     }
125 
126     @RemotableViewMethod
setAvatarReplacement(Icon icon)127     public void setAvatarReplacement(Icon icon) {
128         mAvatarReplacement = icon;
129     }
130 
131     @RemotableViewMethod
setNameReplacement(CharSequence nameReplacement)132     public void setNameReplacement(CharSequence nameReplacement) {
133         mNameReplacement = nameReplacement;
134     }
135 
136     /**
137      * Set this layout to show the collapsed representation.
138      *
139      * @param isCollapsed is it collapsed
140      */
141     @RemotableViewMethod
setIsCollapsed(boolean isCollapsed)142     public void setIsCollapsed(boolean isCollapsed) {
143         mIsCollapsed = isCollapsed;
144     }
145 
146     @RemotableViewMethod
setLargeIcon(Icon largeIcon)147     public void setLargeIcon(Icon largeIcon) {
148         // Unused
149     }
150 
151     /**
152      * Sets the conversation title of this conversation.
153      *
154      * @param conversationTitle the conversation title
155      */
156     @RemotableViewMethod
setConversationTitle(CharSequence conversationTitle)157     public void setConversationTitle(CharSequence conversationTitle) {
158         mConversationTitle = conversationTitle;
159     }
160 
161     @RemotableViewMethod
setData(Bundle extras)162     public void setData(Bundle extras) {
163         Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES);
164         List<Notification.MessagingStyle.Message> newMessages
165                 = Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages);
166         Parcelable[] histMessages = extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES);
167         List<Notification.MessagingStyle.Message> newHistoricMessages
168                 = Notification.MessagingStyle.Message.getMessagesFromBundleArray(histMessages);
169         setUser(extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON));
170         RemoteInputHistoryItem[] history = (RemoteInputHistoryItem[])
171                 extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
172         addRemoteInputHistoryToMessages(newMessages, history);
173         boolean showSpinner =
174                 extras.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false);
175         bind(newMessages, newHistoricMessages, showSpinner);
176     }
177 
178     @Override
setImageResolver(ImageResolver resolver)179     public void setImageResolver(ImageResolver resolver) {
180         mImageResolver = resolver;
181     }
182 
addRemoteInputHistoryToMessages( List<Notification.MessagingStyle.Message> newMessages, RemoteInputHistoryItem[] remoteInputHistory)183     private void addRemoteInputHistoryToMessages(
184             List<Notification.MessagingStyle.Message> newMessages,
185             RemoteInputHistoryItem[] remoteInputHistory) {
186         if (remoteInputHistory == null || remoteInputHistory.length == 0) {
187             return;
188         }
189         for (int i = remoteInputHistory.length - 1; i >= 0; i--) {
190             RemoteInputHistoryItem historyMessage = remoteInputHistory[i];
191             Notification.MessagingStyle.Message message = new Notification.MessagingStyle.Message(
192                     historyMessage.getText(), 0, (Person) null, true /* remoteHistory */);
193             if (historyMessage.getUri() != null) {
194                 message.setData(historyMessage.getMimeType(), historyMessage.getUri());
195             }
196             newMessages.add(message);
197         }
198     }
199 
bind(List<Notification.MessagingStyle.Message> newMessages, List<Notification.MessagingStyle.Message> newHistoricMessages, boolean showSpinner)200     private void bind(List<Notification.MessagingStyle.Message> newMessages,
201             List<Notification.MessagingStyle.Message> newHistoricMessages,
202             boolean showSpinner) {
203 
204         List<MessagingMessage> historicMessages = createMessages(newHistoricMessages,
205                 true /* isHistoric */);
206         List<MessagingMessage> messages = createMessages(newMessages, false /* isHistoric */);
207 
208         ArrayList<MessagingGroup> oldGroups = new ArrayList<>(mGroups);
209         addMessagesToGroups(historicMessages, messages, showSpinner);
210 
211         // Let's first check which groups were removed altogether and remove them in one animation
212         removeGroups(oldGroups);
213 
214         // Let's remove the remaining messages
215         mMessages.forEach(REMOVE_MESSAGE);
216         mHistoricMessages.forEach(REMOVE_MESSAGE);
217 
218         mMessages = messages;
219         mHistoricMessages = historicMessages;
220 
221         updateHistoricMessageVisibility();
222         updateTitleAndNamesDisplay();
223         // after groups are finalized, hide the first sender name if it's showing as the title
224         mPeopleHelper.maybeHideFirstSenderName(mGroups, mIsOneToOne, mConversationTitle);
225         updateImageMessages();
226     }
227 
updateImageMessages()228     private void updateImageMessages() {
229         View newMessage = null;
230         if (mImageMessageContainer == null) {
231             return;
232         }
233         if (mIsCollapsed && !mGroups.isEmpty()) {
234             // When collapsed, we're displaying the image message in a dedicated container
235             // on the right of the layout instead of inline. Let's add the isolated image there
236             MessagingGroup messagingGroup = mGroups.get(mGroups.size() - 1);
237             MessagingImageMessage isolatedMessage = messagingGroup.getIsolatedMessage();
238             if (isolatedMessage != null) {
239                 newMessage = isolatedMessage.getView();
240             }
241         }
242         // Remove all messages that don't belong into the image layout
243         View previousMessage = mImageMessageContainer.getChildAt(0);
244         if (previousMessage != newMessage) {
245             mImageMessageContainer.removeView(previousMessage);
246             if (newMessage != null) {
247                 mImageMessageContainer.addView(newMessage);
248             }
249         }
250         mImageMessageContainer.setVisibility(newMessage != null ? VISIBLE : GONE);
251 
252         // When showing an image message, do not show the large icon.  Removing the drawable
253         // prevents it from being shown in the left_icon view (by the grouping util).
254         if (newMessage != null && mRightIconView != null && mRightIconView.getDrawable() != null) {
255             mRightIconView.setImageDrawable(null);
256             mRightIconView.setVisibility(GONE);
257         }
258     }
259 
removeGroups(ArrayList<MessagingGroup> oldGroups)260     private void removeGroups(ArrayList<MessagingGroup> oldGroups) {
261         int size = oldGroups.size();
262         for (int i = 0; i < size; i++) {
263             MessagingGroup group = oldGroups.get(i);
264             if (!mGroups.contains(group)) {
265                 List<MessagingMessage> messages = group.getMessages();
266                 Runnable endRunnable = () -> {
267                     mMessagingLinearLayout.removeTransientView(group);
268                     group.recycle();
269                 };
270 
271                 boolean wasShown = group.isShown();
272                 mMessagingLinearLayout.removeView(group);
273                 if (wasShown && !MessagingLinearLayout.isGone(group)) {
274                     mMessagingLinearLayout.addTransientView(group, 0);
275                     group.removeGroupAnimated(endRunnable);
276                 } else {
277                     endRunnable.run();
278                 }
279                 mMessages.removeAll(messages);
280                 mHistoricMessages.removeAll(messages);
281             }
282         }
283     }
284 
updateTitleAndNamesDisplay()285     private void updateTitleAndNamesDisplay() {
286         Map<CharSequence, String> uniqueNames = mPeopleHelper.mapUniqueNamesToPrefix(mGroups);
287 
288         // Now that we have the correct symbols, let's look what we have cached
289         ArrayMap<CharSequence, Icon> cachedAvatars = new ArrayMap<>();
290         for (int i = 0; i < mGroups.size(); i++) {
291             // Let's now set the avatars
292             MessagingGroup group = mGroups.get(i);
293             boolean isOwnMessage = group.getSender() == mUser;
294             CharSequence senderName = group.getSenderName();
295             if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)
296                     || (mIsOneToOne && mAvatarReplacement != null && !isOwnMessage)) {
297                 continue;
298             }
299             String symbol = uniqueNames.get(senderName);
300             Icon cachedIcon = group.getAvatarSymbolIfMatching(senderName,
301                     symbol, mLayoutColor);
302             if (cachedIcon != null) {
303                 cachedAvatars.put(senderName, cachedIcon);
304             }
305         }
306 
307         for (int i = 0; i < mGroups.size(); i++) {
308             // Let's now set the avatars
309             MessagingGroup group = mGroups.get(i);
310             CharSequence senderName = group.getSenderName();
311             if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)) {
312                 continue;
313             }
314             if (mIsOneToOne && mAvatarReplacement != null && group.getSender() != mUser) {
315                 group.setAvatar(mAvatarReplacement);
316             } else {
317                 Icon cachedIcon = cachedAvatars.get(senderName);
318                 if (cachedIcon == null) {
319                     cachedIcon = createAvatarSymbol(senderName, uniqueNames.get(senderName),
320                             mLayoutColor);
321                     cachedAvatars.put(senderName, cachedIcon);
322                 }
323                 group.setCreatedAvatar(cachedIcon, senderName, uniqueNames.get(senderName),
324                         mLayoutColor);
325             }
326         }
327     }
328 
createAvatarSymbol(CharSequence senderName, String symbol, int layoutColor)329     public Icon createAvatarSymbol(CharSequence senderName, String symbol, int layoutColor) {
330         return mPeopleHelper.createAvatarSymbol(senderName, symbol, layoutColor);
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 
369     /**
370      * @param color the color of the notification background
371      */
372     @RemotableViewMethod
setNotificationBackgroundColor(int color)373     public void setNotificationBackgroundColor(int color) {
374         // Nothing to do with this
375     }
376 
377     @RemotableViewMethod
setMessageTextColor(int color)378     public void setMessageTextColor(int color) {
379         mMessageTextColor = color;
380     }
381 
setUser(Person user)382     public void setUser(Person user) {
383         mUser = user;
384         if (mUser.getIcon() == null) {
385             Icon userIcon = Icon.createWithResource(getContext(),
386                     com.android.internal.R.drawable.messaging_user);
387             userIcon.setTint(mLayoutColor);
388             mUser = mUser.toBuilder().setIcon(userIcon).build();
389         }
390     }
391 
addMessagesToGroups(List<MessagingMessage> historicMessages, List<MessagingMessage> messages, boolean showSpinner)392     private void addMessagesToGroups(List<MessagingMessage> historicMessages,
393             List<MessagingMessage> messages, boolean showSpinner) {
394         // Let's first find our groups!
395         List<List<MessagingMessage>> groups = new ArrayList<>();
396         List<Person> senders = new ArrayList<>();
397 
398         // Lets first find the groups
399         findGroups(historicMessages, messages, groups, senders);
400 
401         // Let's now create the views and reorder them accordingly
402         createGroupViews(groups, senders, showSpinner);
403     }
404 
createGroupViews(List<List<MessagingMessage>> groups, List<Person> senders, boolean showSpinner)405     private void createGroupViews(List<List<MessagingMessage>> groups,
406             List<Person> senders, boolean showSpinner) {
407         mGroups.clear();
408         for (int groupIndex = 0; groupIndex < groups.size(); groupIndex++) {
409             List<MessagingMessage> group = groups.get(groupIndex);
410             MessagingGroup newGroup = null;
411             // we'll just take the first group that exists or create one there is none
412             for (int messageIndex = group.size() - 1; messageIndex >= 0; messageIndex--) {
413                 MessagingMessage message = group.get(messageIndex);
414                 newGroup = message.getGroup();
415                 if (newGroup != null) {
416                     break;
417                 }
418             }
419             if (newGroup == null) {
420                 newGroup = MessagingGroup.createGroup(mMessagingLinearLayout);
421                 mAddedGroups.add(newGroup);
422             }
423             newGroup.setImageDisplayLocation(mIsCollapsed
424                     ? IMAGE_DISPLAY_LOCATION_EXTERNAL
425                     : IMAGE_DISPLAY_LOCATION_INLINE);
426             newGroup.setIsInConversation(false);
427             newGroup.setLayoutColor(mLayoutColor);
428             newGroup.setTextColors(mSenderTextColor, mMessageTextColor);
429             Person sender = senders.get(groupIndex);
430             CharSequence nameOverride = null;
431             if (sender != mUser && mNameReplacement != null) {
432                 nameOverride = mNameReplacement;
433             }
434             newGroup.setSingleLine(mIsCollapsed);
435             newGroup.setShowingAvatar(!mIsCollapsed);
436             newGroup.setSender(sender, nameOverride);
437             newGroup.setSending(groupIndex == (groups.size() - 1) && showSpinner);
438             mGroups.add(newGroup);
439 
440             if (mMessagingLinearLayout.indexOfChild(newGroup) != groupIndex) {
441                 mMessagingLinearLayout.removeView(newGroup);
442                 mMessagingLinearLayout.addView(newGroup, groupIndex);
443             }
444             newGroup.setMessages(group);
445         }
446     }
447 
findGroups(List<MessagingMessage> historicMessages, List<MessagingMessage> messages, List<List<MessagingMessage>> groups, List<Person> senders)448     private void findGroups(List<MessagingMessage> historicMessages,
449             List<MessagingMessage> messages, List<List<MessagingMessage>> groups,
450             List<Person> senders) {
451         CharSequence currentSenderKey = null;
452         List<MessagingMessage> currentGroup = null;
453         int histSize = historicMessages.size();
454         for (int i = 0; i < histSize + messages.size(); i++) {
455             MessagingMessage message;
456             if (i < histSize) {
457                 message = historicMessages.get(i);
458             } else {
459                 message = messages.get(i - histSize);
460             }
461             boolean isNewGroup = currentGroup == null;
462             Person sender = message.getMessage().getSenderPerson();
463             CharSequence key = sender == null ? null
464                     : sender.getKey() == null ? sender.getName() : sender.getKey();
465             isNewGroup |= !TextUtils.equals(key, currentSenderKey);
466             if (isNewGroup) {
467                 currentGroup = new ArrayList<>();
468                 groups.add(currentGroup);
469                 if (sender == null) {
470                     sender = mUser;
471                 }
472                 senders.add(sender);
473                 currentSenderKey = key;
474             }
475             currentGroup.add(message);
476         }
477     }
478 
479     /**
480      * Creates new messages, reusing existing ones if they are available.
481      *
482      * @param newMessages the messages to parse.
483      */
createMessages( List<Notification.MessagingStyle.Message> newMessages, boolean historic)484     private List<MessagingMessage> createMessages(
485             List<Notification.MessagingStyle.Message> newMessages, boolean historic) {
486         List<MessagingMessage> result = new ArrayList<>();
487         for (int i = 0; i < newMessages.size(); i++) {
488             Notification.MessagingStyle.Message m = newMessages.get(i);
489             MessagingMessage message = findAndRemoveMatchingMessage(m);
490             if (message == null) {
491                 message = MessagingMessage.createMessage(this, m, mImageResolver);
492             }
493             message.setIsHistoric(historic);
494             result.add(message);
495         }
496         return result;
497     }
498 
findAndRemoveMatchingMessage(Notification.MessagingStyle.Message m)499     private MessagingMessage findAndRemoveMatchingMessage(Notification.MessagingStyle.Message m) {
500         for (int i = 0; i < mMessages.size(); i++) {
501             MessagingMessage existing = mMessages.get(i);
502             if (existing.sameAs(m)) {
503                 mMessages.remove(i);
504                 return existing;
505             }
506         }
507         for (int i = 0; i < mHistoricMessages.size(); i++) {
508             MessagingMessage existing = mHistoricMessages.get(i);
509             if (existing.sameAs(m)) {
510                 mHistoricMessages.remove(i);
511                 return existing;
512             }
513         }
514         return null;
515     }
516 
showHistoricMessages(boolean show)517     public void showHistoricMessages(boolean show) {
518         mShowHistoricMessages = show;
519         updateHistoricMessageVisibility();
520     }
521 
updateHistoricMessageVisibility()522     private void updateHistoricMessageVisibility() {
523         int numHistoric = mHistoricMessages.size();
524         for (int i = 0; i < numHistoric; i++) {
525             MessagingMessage existing = mHistoricMessages.get(i);
526             existing.setVisibility(mShowHistoricMessages ? VISIBLE : GONE);
527         }
528         int numGroups = mGroups.size();
529         for (int i = 0; i < numGroups; i++) {
530             MessagingGroup group = mGroups.get(i);
531             int visibleChildren = 0;
532             List<MessagingMessage> messages = group.getMessages();
533             int numGroupMessages = messages.size();
534             for (int j = 0; j < numGroupMessages; j++) {
535                 MessagingMessage message = messages.get(j);
536                 if (message.getVisibility() != GONE) {
537                     visibleChildren++;
538                 }
539             }
540             if (visibleChildren > 0 && group.getVisibility() == GONE) {
541                 group.setVisibility(VISIBLE);
542             } else if (visibleChildren == 0 && group.getVisibility() != GONE)   {
543                 group.setVisibility(GONE);
544             }
545         }
546     }
547 
548     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)549     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
550         super.onLayout(changed, left, top, right, bottom);
551         if (!mAddedGroups.isEmpty()) {
552             getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
553                 @Override
554                 public boolean onPreDraw() {
555                     for (MessagingGroup group : mAddedGroups) {
556                         if (!group.isShown()) {
557                             continue;
558                         }
559                         MessagingPropertyAnimator.fadeIn(group.getAvatar());
560                         MessagingPropertyAnimator.fadeIn(group.getSenderView());
561                         MessagingPropertyAnimator.startLocalTranslationFrom(group,
562                                 group.getHeight(), LINEAR_OUT_SLOW_IN);
563                     }
564                     mAddedGroups.clear();
565                     getViewTreeObserver().removeOnPreDrawListener(this);
566                     return true;
567                 }
568             });
569         }
570     }
571 
getMessagingLinearLayout()572     public MessagingLinearLayout getMessagingLinearLayout() {
573         return mMessagingLinearLayout;
574     }
575 
576     @Nullable
getImageMessageContainer()577     public ViewGroup getImageMessageContainer() {
578         return mImageMessageContainer;
579     }
580 
getMessagingGroups()581     public ArrayList<MessagingGroup> getMessagingGroups() {
582         return mGroups;
583     }
584 
585     @Override
setMessagingClippingDisabled(boolean clippingDisabled)586     public void setMessagingClippingDisabled(boolean clippingDisabled) {
587         mMessagingLinearLayout.setClipBounds(clippingDisabled ? null : mMessagingClipRect);
588     }
589 }
590