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 
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
61         implements ImageMessageConsumer, IMessagingLayout {
62 
63     private static final float COLOR_SHIFT_AMOUNT = 60;
64     public static final Interpolator LINEAR_OUT_SLOW_IN = new PathInterpolator(0f, 0f, 0.2f, 1f);
65     public static final Interpolator FAST_OUT_LINEAR_IN = new PathInterpolator(0.4f, 0f, 1f, 1f);
66     public static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f);
67     public static final OnLayoutChangeListener MESSAGING_PROPERTY_ANIMATOR
68             = new MessagingPropertyAnimator();
69     private final PeopleHelper mPeopleHelper = new PeopleHelper();
70     private List<MessagingMessage> mMessages = new ArrayList<>();
71     private List<MessagingMessage> mHistoricMessages = new ArrayList<>();
72     private MessagingLinearLayout mMessagingLinearLayout;
73     private boolean mShowHistoricMessages;
74     private ArrayList<MessagingGroup> mGroups = new ArrayList<>();
75     private MessagingLinearLayout mImageMessageContainer;
76     private ImageView mRightIconView;
77     private Rect mMessagingClipRect;
78     private int mLayoutColor;
79     private int mSenderTextColor;
80     private int mMessageTextColor;
81     private Icon mAvatarReplacement;
82     private boolean mIsOneToOne;
83     private ArrayList<MessagingGroup> mAddedGroups = new ArrayList<>();
84     private Person mUser;
85     private CharSequence mNameReplacement;
86     private boolean mIsCollapsed;
87     private ImageResolver mImageResolver;
88     private CharSequence mConversationTitle;
89     private ArrayList<MessagingLinearLayout.MessagingChild> mToRecycle = new ArrayList<>();
90     private boolean mPrecomputedTextEnabled = false;
MessagingLayout(@onNull Context context)91     public MessagingLayout(@NonNull Context context) {
92         super(context);
93     }
94 
MessagingLayout(@onNull Context context, @Nullable AttributeSet attrs)95     public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
96         super(context, attrs);
97     }
98 
MessagingLayout(@onNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr)99     public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs,
100             @AttrRes int defStyleAttr) {
101         super(context, attrs, defStyleAttr);
102     }
103 
MessagingLayout(@onNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes)104     public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs,
105             @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
106         super(context, attrs, defStyleAttr, defStyleRes);
107     }
108 
109     @Override
onFinishInflate()110     protected void onFinishInflate() {
111         super.onFinishInflate();
112         mPeopleHelper.init(getContext());
113         mMessagingLinearLayout = findViewById(R.id.notification_messaging);
114         mImageMessageContainer = findViewById(R.id.conversation_image_message_container);
115         mRightIconView = findViewById(R.id.right_icon);
116         // We still want to clip, but only on the top, since views can temporarily out of bounds
117         // during transitions.
118         DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
119         int size = Math.max(displayMetrics.widthPixels, displayMetrics.heightPixels);
120         mMessagingClipRect = new Rect(0, 0, size, size);
121         setMessagingClippingDisabled(false);
122     }
123 
124     @RemotableViewMethod
setAvatarReplacement(Icon icon)125     public void setAvatarReplacement(Icon icon) {
126         mAvatarReplacement = icon;
127     }
128 
129     @RemotableViewMethod
setNameReplacement(CharSequence nameReplacement)130     public void setNameReplacement(CharSequence nameReplacement) {
131         mNameReplacement = nameReplacement;
132     }
133 
134     /**
135      * Set this layout to show the collapsed representation.
136      *
137      * @param isCollapsed is it collapsed
138      */
139     @RemotableViewMethod
setIsCollapsed(boolean isCollapsed)140     public void setIsCollapsed(boolean isCollapsed) {
141         mIsCollapsed = isCollapsed;
142     }
143 
144     @RemotableViewMethod
setLargeIcon(Icon largeIcon)145     public void setLargeIcon(Icon largeIcon) {
146         // Unused
147     }
148 
149     /**
150      * Sets the conversation title of this conversation.
151      *
152      * @param conversationTitle the conversation title
153      */
154     @RemotableViewMethod
setConversationTitle(CharSequence conversationTitle)155     public void setConversationTitle(CharSequence conversationTitle) {
156         mConversationTitle = conversationTitle;
157     }
158 
159     /**
160      * Set Messaging data
161      * @param extras Bundle contains messaging data
162      */
163     @RemotableViewMethod(asyncImpl = "setDataAsync")
setData(Bundle extras)164     public void setData(Bundle extras) {
165         bind(parseMessagingData(extras, /* usePrecomputedText= */false));
166     }
167 
168     @NonNull
parseMessagingData(Bundle extras, boolean usePrecomputedText)169     private MessagingData parseMessagingData(Bundle extras, boolean usePrecomputedText) {
170         Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES);
171         List<Notification.MessagingStyle.Message> newMessages =
172                 Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages);
173         Parcelable[] histMessages = extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES);
174         List<Notification.MessagingStyle.Message> newHistoricMessages =
175                 Notification.MessagingStyle.Message.getMessagesFromBundleArray(histMessages);
176         setUser(extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON,
177                 Person.class));
178         RemoteInputHistoryItem[] history =
179                 (RemoteInputHistoryItem[]) extras.getParcelableArray(
180                         Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS,
181                         RemoteInputHistoryItem.class);
182         addRemoteInputHistoryToMessages(newMessages, history);
183 
184         final Person user = extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON, Person.class);
185         boolean showSpinner =
186                 extras.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false);
187 
188         final List<MessagingMessage> historicMessagingMessages = createMessages(newHistoricMessages,
189                 /* isHistoric= */true, usePrecomputedText);
190         final List<MessagingMessage> newMessagingMessages =
191                 createMessages(newMessages, /* isHistoric */false, usePrecomputedText);
192         // Let's first find our groups!
193         List<List<MessagingMessage>> groups = new ArrayList<>();
194         List<Person> senders = new ArrayList<>();
195 
196         // Lets first find the groups
197         findGroups(historicMessagingMessages, newMessagingMessages, groups, senders);
198 
199         return new MessagingData(user, showSpinner,
200                 historicMessagingMessages, newMessagingMessages, groups, senders);
201     }
202 
203     /**
204      * RemotableViewMethod's asyncImpl of {@link #setData(Bundle)}.
205      * This should be called on a background thread, and returns a Runnable which is then must be
206      * called on the main thread to complete the operation and set text.
207      * @param extras Bundle contains messaging data
208      * @hide
209      */
210     @NonNull
setDataAsync(Bundle extras)211     public Runnable setDataAsync(Bundle extras) {
212         if (!mPrecomputedTextEnabled) {
213             return () -> setData(extras);
214         }
215 
216         final MessagingData messagingData =
217                 parseMessagingData(extras, /* usePrecomputedText= */true);
218 
219         return () -> {
220             finalizeInflate(messagingData.getHistoricMessagingMessages());
221             finalizeInflate(messagingData.getNewMessagingMessages());
222             bind(messagingData);
223         };
224     }
225 
226     /**
227      * enable/disable precomputed text usage
228      * @hide
229      */
setPrecomputedTextEnabled(boolean precomputedTextEnabled)230     public void setPrecomputedTextEnabled(boolean precomputedTextEnabled) {
231         mPrecomputedTextEnabled = precomputedTextEnabled;
232     }
233 
finalizeInflate(List<MessagingMessage> historicMessagingMessages)234     private void finalizeInflate(List<MessagingMessage> historicMessagingMessages) {
235         for (MessagingMessage messagingMessage: historicMessagingMessages) {
236             messagingMessage.finalizeInflate();
237         }
238     }
239 
240     @Override
setImageResolver(ImageResolver resolver)241     public void setImageResolver(ImageResolver resolver) {
242         mImageResolver = resolver;
243     }
244 
addRemoteInputHistoryToMessages( List<Notification.MessagingStyle.Message> newMessages, RemoteInputHistoryItem[] remoteInputHistory)245     private void addRemoteInputHistoryToMessages(
246             List<Notification.MessagingStyle.Message> newMessages,
247             RemoteInputHistoryItem[] remoteInputHistory) {
248         if (remoteInputHistory == null || remoteInputHistory.length == 0) {
249             return;
250         }
251         for (int i = remoteInputHistory.length - 1; i >= 0; i--) {
252             RemoteInputHistoryItem historyMessage = remoteInputHistory[i];
253             Notification.MessagingStyle.Message message = new Notification.MessagingStyle.Message(
254                     historyMessage.getText(), 0, (Person) null, true /* remoteHistory */);
255             if (historyMessage.getUri() != null) {
256                 message.setData(historyMessage.getMimeType(), historyMessage.getUri());
257             }
258             newMessages.add(message);
259         }
260     }
261 
bind(MessagingData messagingData)262     private void bind(MessagingData messagingData) {
263         setUser(messagingData.getUser());
264 
265         // Let's now create the views and reorder them accordingly
266         ArrayList<MessagingGroup> oldGroups = new ArrayList<>(mGroups);
267         createGroupViews(messagingData.getGroups(), messagingData.getSenders(),
268                 messagingData.getShowSpinner());
269 
270         // Let's first check which groups were removed altogether and remove them in one animation
271         removeGroups(oldGroups);
272 
273         // Let's remove the remaining messages
274         for (MessagingMessage message : mMessages) {
275             message.removeMessage(mToRecycle);
276         }
277         for (MessagingMessage historicMessage : mHistoricMessages) {
278             historicMessage.removeMessage(mToRecycle);
279         }
280 
281         mMessages = messagingData.getNewMessagingMessages();
282         mHistoricMessages = messagingData.getHistoricMessagingMessages();
283 
284         updateHistoricMessageVisibility();
285         updateTitleAndNamesDisplay();
286         // after groups are finalized, hide the first sender name if it's showing as the title
287         mPeopleHelper.maybeHideFirstSenderName(mGroups, mIsOneToOne, mConversationTitle);
288         updateImageMessages();
289 
290         // Recycle everything at the end of the update, now that we know it's no longer needed.
291         for (MessagingLinearLayout.MessagingChild child : mToRecycle) {
292             child.recycle();
293         }
294         mToRecycle.clear();
295     }
296 
updateImageMessages()297     private void updateImageMessages() {
298         View newMessage = null;
299         if (mImageMessageContainer == null) {
300             return;
301         }
302         if (mIsCollapsed && !mGroups.isEmpty()) {
303             // When collapsed, we're displaying the image message in a dedicated container
304             // on the right of the layout instead of inline. Let's add the isolated image there
305             MessagingGroup messagingGroup = mGroups.get(mGroups.size() - 1);
306             MessagingImageMessage isolatedMessage = messagingGroup.getIsolatedMessage();
307             if (isolatedMessage != null) {
308                 newMessage = isolatedMessage.getView();
309             }
310         }
311         // Remove all messages that don't belong into the image layout
312         View previousMessage = mImageMessageContainer.getChildAt(0);
313         if (previousMessage != newMessage) {
314             mImageMessageContainer.removeView(previousMessage);
315             if (newMessage != null) {
316                 mImageMessageContainer.addView(newMessage);
317             }
318         }
319         mImageMessageContainer.setVisibility(newMessage != null ? VISIBLE : GONE);
320 
321         // When showing an image message, do not show the large icon.  Removing the drawable
322         // prevents it from being shown in the left_icon view (by the grouping util).
323         if (newMessage != null && mRightIconView != null && mRightIconView.getDrawable() != null) {
324             mRightIconView.setImageDrawable(null);
325             mRightIconView.setVisibility(GONE);
326         }
327     }
328 
removeGroups(ArrayList<MessagingGroup> oldGroups)329     private void removeGroups(ArrayList<MessagingGroup> oldGroups) {
330         int size = oldGroups.size();
331         for (int i = 0; i < size; i++) {
332             MessagingGroup group = oldGroups.get(i);
333             if (!mGroups.contains(group)) {
334                 List<MessagingMessage> messages = group.getMessages();
335 
336                 boolean wasShown = group.isShown();
337                 mMessagingLinearLayout.removeView(group);
338                 if (wasShown && !MessagingLinearLayout.isGone(group)) {
339                     mMessagingLinearLayout.addTransientView(group, 0);
340                     group.removeGroupAnimated(() -> {
341                         mMessagingLinearLayout.removeTransientView(group);
342                         group.recycle();
343                     });
344                 } else {
345                     mToRecycle.add(group);
346                 }
347                 mMessages.removeAll(messages);
348                 mHistoricMessages.removeAll(messages);
349             }
350         }
351     }
352 
updateTitleAndNamesDisplay()353     private void updateTitleAndNamesDisplay() {
354         Map<CharSequence, String> uniqueNames = mPeopleHelper.mapUniqueNamesToPrefix(mGroups);
355 
356         // Now that we have the correct symbols, let's look what we have cached
357         ArrayMap<CharSequence, Icon> cachedAvatars = new ArrayMap<>();
358         for (int i = 0; i < mGroups.size(); i++) {
359             // Let's now set the avatars
360             MessagingGroup group = mGroups.get(i);
361             boolean isOwnMessage = group.getSender() == mUser;
362             CharSequence senderName = group.getSenderName();
363             if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)
364                     || (mIsOneToOne && mAvatarReplacement != null && !isOwnMessage)) {
365                 continue;
366             }
367             String symbol = uniqueNames.get(senderName);
368             Icon cachedIcon = group.getAvatarSymbolIfMatching(senderName,
369                     symbol, mLayoutColor);
370             if (cachedIcon != null) {
371                 cachedAvatars.put(senderName, cachedIcon);
372             }
373         }
374 
375         for (int i = 0; i < mGroups.size(); i++) {
376             // Let's now set the avatars
377             MessagingGroup group = mGroups.get(i);
378             CharSequence senderName = group.getSenderName();
379             if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)) {
380                 continue;
381             }
382             if (mIsOneToOne && mAvatarReplacement != null && group.getSender() != mUser) {
383                 group.setAvatar(mAvatarReplacement);
384             } else {
385                 Icon cachedIcon = cachedAvatars.get(senderName);
386                 if (cachedIcon == null) {
387                     cachedIcon = createAvatarSymbol(senderName, uniqueNames.get(senderName),
388                             mLayoutColor);
389                     cachedAvatars.put(senderName, cachedIcon);
390                 }
391                 group.setCreatedAvatar(cachedIcon, senderName, uniqueNames.get(senderName),
392                         mLayoutColor);
393             }
394         }
395     }
396 
createAvatarSymbol(CharSequence senderName, String symbol, int layoutColor)397     public Icon createAvatarSymbol(CharSequence senderName, String symbol, int layoutColor) {
398         return mPeopleHelper.createAvatarSymbol(senderName, symbol, layoutColor);
399     }
400 
findColor(CharSequence senderName, int layoutColor)401     private int findColor(CharSequence senderName, int layoutColor) {
402         double luminance = ContrastColorUtil.calculateLuminance(layoutColor);
403         float shift = Math.abs(senderName.hashCode()) % 5 / 4.0f - 0.5f;
404 
405         // we need to offset the range if the luminance is too close to the borders
406         shift += Math.max(COLOR_SHIFT_AMOUNT / 2.0f / 100 - luminance, 0);
407         shift -= Math.max(COLOR_SHIFT_AMOUNT / 2.0f / 100 - (1.0f - luminance), 0);
408         return ContrastColorUtil.getShiftedColor(layoutColor,
409                 (int) (shift * COLOR_SHIFT_AMOUNT));
410     }
411 
findNameSplit(String existingName)412     private String findNameSplit(String existingName) {
413         String[] split = existingName.split(" ");
414         if (split.length > 1) {
415             return Character.toString(split[0].charAt(0))
416                     + Character.toString(split[1].charAt(0));
417         }
418         return existingName.substring(0, 1);
419     }
420 
421     @RemotableViewMethod
setLayoutColor(int color)422     public void setLayoutColor(int color) {
423         mLayoutColor = color;
424     }
425 
426     @RemotableViewMethod
setIsOneToOne(boolean oneToOne)427     public void setIsOneToOne(boolean oneToOne) {
428         mIsOneToOne = oneToOne;
429     }
430 
431     @RemotableViewMethod
setSenderTextColor(int color)432     public void setSenderTextColor(int color) {
433         mSenderTextColor = color;
434     }
435 
436 
437     /**
438      * @param color the color of the notification background
439      */
440     @RemotableViewMethod
setNotificationBackgroundColor(int color)441     public void setNotificationBackgroundColor(int color) {
442         // Nothing to do with this
443     }
444 
445     @RemotableViewMethod
setMessageTextColor(int color)446     public void setMessageTextColor(int color) {
447         mMessageTextColor = color;
448     }
449 
setUser(Person user)450     public void setUser(Person user) {
451         mUser = user;
452         if (mUser.getIcon() == null) {
453             Icon userIcon = Icon.createWithResource(getContext(),
454                     com.android.internal.R.drawable.messaging_user);
455             userIcon.setTint(mLayoutColor);
456             mUser = mUser.toBuilder().setIcon(userIcon).build();
457         }
458     }
459 
createGroupViews(List<List<MessagingMessage>> groups, List<Person> senders, boolean showSpinner)460     private void createGroupViews(List<List<MessagingMessage>> groups,
461             List<Person> senders, boolean showSpinner) {
462         mGroups.clear();
463         for (int groupIndex = 0; groupIndex < groups.size(); groupIndex++) {
464             List<MessagingMessage> group = groups.get(groupIndex);
465             MessagingGroup newGroup = null;
466             // we'll just take the first group that exists or create one there is none
467             for (int messageIndex = group.size() - 1; messageIndex >= 0; messageIndex--) {
468                 MessagingMessage message = group.get(messageIndex);
469                 newGroup = message.getGroup();
470                 if (newGroup != null) {
471                     break;
472                 }
473             }
474             if (newGroup == null) {
475                 newGroup = MessagingGroup.createGroup(mMessagingLinearLayout);
476                 mAddedGroups.add(newGroup);
477             } else if (newGroup.getParent() != mMessagingLinearLayout) {
478                 throw new IllegalStateException(
479                         "group parent was " + newGroup.getParent() + " but expected "
480                                 + mMessagingLinearLayout);
481             }
482             newGroup.setImageDisplayLocation(mIsCollapsed
483                     ? IMAGE_DISPLAY_LOCATION_EXTERNAL
484                     : IMAGE_DISPLAY_LOCATION_INLINE);
485             newGroup.setIsInConversation(false);
486             newGroup.setLayoutColor(mLayoutColor);
487             newGroup.setTextColors(mSenderTextColor, mMessageTextColor);
488             Person sender = senders.get(groupIndex);
489             CharSequence nameOverride = null;
490             if (sender != mUser && mNameReplacement != null) {
491                 nameOverride = mNameReplacement;
492             }
493             newGroup.setSingleLine(mIsCollapsed);
494             newGroup.setShowingAvatar(!mIsCollapsed);
495             newGroup.setSender(sender, nameOverride);
496             newGroup.setSending(groupIndex == (groups.size() - 1) && showSpinner);
497             mGroups.add(newGroup);
498 
499             if (mMessagingLinearLayout.indexOfChild(newGroup) != groupIndex) {
500                 mMessagingLinearLayout.removeView(newGroup);
501                 mMessagingLinearLayout.addView(newGroup, groupIndex);
502             }
503             newGroup.setMessages(group);
504         }
505     }
506 
findGroups(List<MessagingMessage> historicMessages, List<MessagingMessage> messages, List<List<MessagingMessage>> groups, List<Person> senders)507     private void findGroups(List<MessagingMessage> historicMessages,
508             List<MessagingMessage> messages, List<List<MessagingMessage>> groups,
509             List<Person> senders) {
510         CharSequence currentSenderKey = null;
511         List<MessagingMessage> currentGroup = null;
512         int histSize = historicMessages.size();
513         for (int i = 0; i < histSize + messages.size(); i++) {
514             MessagingMessage message;
515             if (i < histSize) {
516                 message = historicMessages.get(i);
517             } else {
518                 message = messages.get(i - histSize);
519             }
520             boolean isNewGroup = currentGroup == null;
521             Person sender =
522                     message.getMessage() == null ? null : message.getMessage().getSenderPerson();
523             CharSequence key = sender == null ? null
524                     : sender.getKey() == null ? sender.getName() : sender.getKey();
525             isNewGroup |= !TextUtils.equals(key, currentSenderKey);
526             if (isNewGroup) {
527                 currentGroup = new ArrayList<>();
528                 groups.add(currentGroup);
529                 if (sender == null) {
530                     sender = mUser;
531                 }
532                 senders.add(sender);
533                 currentSenderKey = key;
534             }
535             currentGroup.add(message);
536         }
537     }
538 
539     /**
540      * Creates new messages, reusing existing ones if they are available.
541      *
542      * @param newMessages the messages to parse.
543      */
createMessages( List<Notification.MessagingStyle.Message> newMessages, boolean isHistoric, boolean usePrecomputedText)544     private List<MessagingMessage> createMessages(
545             List<Notification.MessagingStyle.Message> newMessages, boolean isHistoric,
546             boolean usePrecomputedText) {
547         List<MessagingMessage> result = new ArrayList<>();
548         for (int i = 0; i < newMessages.size(); i++) {
549             Notification.MessagingStyle.Message m = newMessages.get(i);
550             MessagingMessage message = findAndRemoveMatchingMessage(m);
551             if (message == null) {
552                 message = MessagingMessage.createMessage(this, m,
553                         mImageResolver, usePrecomputedText);
554             }
555             message.setIsHistoric(isHistoric);
556             result.add(message);
557         }
558         return result;
559     }
560 
findAndRemoveMatchingMessage(Notification.MessagingStyle.Message m)561     private MessagingMessage findAndRemoveMatchingMessage(Notification.MessagingStyle.Message m) {
562         for (int i = 0; i < mMessages.size(); i++) {
563             MessagingMessage existing = mMessages.get(i);
564             if (existing.sameAs(m)) {
565                 mMessages.remove(i);
566                 return existing;
567             }
568         }
569         for (int i = 0; i < mHistoricMessages.size(); i++) {
570             MessagingMessage existing = mHistoricMessages.get(i);
571             if (existing.sameAs(m)) {
572                 mHistoricMessages.remove(i);
573                 return existing;
574             }
575         }
576         return null;
577     }
578 
showHistoricMessages(boolean show)579     public void showHistoricMessages(boolean show) {
580         mShowHistoricMessages = show;
581         updateHistoricMessageVisibility();
582     }
583 
updateHistoricMessageVisibility()584     private void updateHistoricMessageVisibility() {
585         int numHistoric = mHistoricMessages.size();
586         for (int i = 0; i < numHistoric; i++) {
587             MessagingMessage existing = mHistoricMessages.get(i);
588             existing.setVisibility(mShowHistoricMessages ? VISIBLE : GONE);
589         }
590         int numGroups = mGroups.size();
591         for (int i = 0; i < numGroups; i++) {
592             MessagingGroup group = mGroups.get(i);
593             int visibleChildren = 0;
594             List<MessagingMessage> messages = group.getMessages();
595             int numGroupMessages = messages.size();
596             for (int j = 0; j < numGroupMessages; j++) {
597                 MessagingMessage message = messages.get(j);
598                 if (message.getVisibility() != GONE) {
599                     visibleChildren++;
600                 }
601             }
602             if (visibleChildren > 0 && group.getVisibility() == GONE) {
603                 group.setVisibility(VISIBLE);
604             } else if (visibleChildren == 0 && group.getVisibility() != GONE)   {
605                 group.setVisibility(GONE);
606             }
607         }
608     }
609 
610     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)611     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
612         super.onLayout(changed, left, top, right, bottom);
613         if (!mAddedGroups.isEmpty()) {
614             getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
615                 @Override
616                 public boolean onPreDraw() {
617                     for (MessagingGroup group : mAddedGroups) {
618                         if (!group.isShown()) {
619                             continue;
620                         }
621                         MessagingPropertyAnimator.fadeIn(group.getAvatar());
622                         MessagingPropertyAnimator.fadeIn(group.getSenderView());
623                         MessagingPropertyAnimator.startLocalTranslationFrom(group,
624                                 group.getHeight(), LINEAR_OUT_SLOW_IN);
625                     }
626                     mAddedGroups.clear();
627                     getViewTreeObserver().removeOnPreDrawListener(this);
628                     return true;
629                 }
630             });
631         }
632     }
633 
getMessagingLinearLayout()634     public MessagingLinearLayout getMessagingLinearLayout() {
635         return mMessagingLinearLayout;
636     }
637 
638     @Nullable
getImageMessageContainer()639     public ViewGroup getImageMessageContainer() {
640         return mImageMessageContainer;
641     }
642 
getMessagingGroups()643     public ArrayList<MessagingGroup> getMessagingGroups() {
644         return mGroups;
645     }
646 
647     @Override
setMessagingClippingDisabled(boolean clippingDisabled)648     public void setMessagingClippingDisabled(boolean clippingDisabled) {
649         mMessagingLinearLayout.setClipBounds(clippingDisabled ? null : mMessagingClipRect);
650     }
651 }
652