• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 import android.widget.flags.Flags;
48 
49 import com.android.internal.R;
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     public static final Interpolator LINEAR_OUT_SLOW_IN = new PathInterpolator(0f, 0f, 0.2f, 1f);
64     public static final Interpolator FAST_OUT_LINEAR_IN = new PathInterpolator(0.4f, 0f, 1f, 1f);
65     public static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f);
66     private static final int MAX_SUMMARIZATION_LINES = 3;
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 final 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 final 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 final ArrayList<MessagingLinearLayout.MessagingChild> mToRecycle = new ArrayList<>();
90     private boolean mPrecomputedTextEnabled = false;
91     private CharSequence mSummarizedContent;
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(asyncImpl = "setAvatarReplacementAsync")
setAvatarReplacement(Icon icon)127     public void setAvatarReplacement(Icon icon) {
128         mAvatarReplacement = icon;
129     }
130 
131     /**
132      * @hide
133      */
setAvatarReplacementAsync(Icon icon)134     public Runnable setAvatarReplacementAsync(Icon icon) {
135         mAvatarReplacement = icon;
136         return () -> {};
137     }
138 
139     @RemotableViewMethod(asyncImpl = "setNameReplacementAsync")
setNameReplacement(CharSequence nameReplacement)140     public void setNameReplacement(CharSequence nameReplacement) {
141         mNameReplacement = nameReplacement;
142     }
143 
144     /**
145      * @hide
146      */
setNameReplacementAsync(CharSequence nameReplacement)147     public Runnable setNameReplacementAsync(CharSequence nameReplacement) {
148         mNameReplacement = nameReplacement;
149         return () -> {};
150     }
151 
152     /**
153      * Set this layout to show the collapsed representation.
154      *
155      * @param isCollapsed is it collapsed
156      */
157     @RemotableViewMethod(asyncImpl = "setIsCollapsedAsync")
setIsCollapsed(boolean isCollapsed)158     public void setIsCollapsed(boolean isCollapsed) {
159         mIsCollapsed = isCollapsed;
160         if (mIsCollapsed) {
161             mMessagingLinearLayout.setMaxDisplayedLines(
162                     android.app.Flags.nmCollapsedLines() ? 2 : 1);
163         }
164     }
165 
166     /**
167      * setDataAsync needs to do different stuff for the collapsed vs expanded view, so store the
168      * collapsed state early.
169      */
setIsCollapsedAsync(boolean isCollapsed)170     public Runnable setIsCollapsedAsync(boolean isCollapsed) {
171         mIsCollapsed = isCollapsed;
172         return () -> {};
173     }
174 
175     @RemotableViewMethod
setLargeIcon(Icon largeIcon)176     public void setLargeIcon(Icon largeIcon) {
177         // Unused
178     }
179 
180     /**
181      * Sets the conversation title of this conversation.
182      *
183      * @param conversationTitle the conversation title
184      */
185     @RemotableViewMethod(asyncImpl = "setConversationTitleAsync")
setConversationTitle(CharSequence conversationTitle)186     public void setConversationTitle(CharSequence conversationTitle) {
187         mConversationTitle = conversationTitle;
188     }
189 
190     /**
191      * @hide
192      */
setConversationTitleAsync(CharSequence conversationTitle)193     public Runnable setConversationTitleAsync(CharSequence conversationTitle) {
194         mConversationTitle = conversationTitle;
195         return ()->{};
196     }
197 
198     /**
199      * Set Messaging data
200      * @param extras Bundle contains messaging data
201      */
202     @RemotableViewMethod(asyncImpl = "setDataAsync")
setData(Bundle extras)203     public void setData(Bundle extras) {
204         bind(parseMessagingData(extras, /* usePrecomputedText= */false));
205     }
206 
207     @NonNull
parseMessagingData(Bundle extras, boolean usePrecomputedText)208     private MessagingData parseMessagingData(Bundle extras, boolean usePrecomputedText) {
209         Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES);
210         List<Notification.MessagingStyle.Message> newMessages =
211                 Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages);
212         Parcelable[] histMessages = extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES);
213         List<Notification.MessagingStyle.Message> newHistoricMessages =
214                 Notification.MessagingStyle.Message.getMessagesFromBundleArray(histMessages);
215         setUser(extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON,
216                 Person.class));
217         RemoteInputHistoryItem[] history = extras.getParcelableArray(
218                 Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS, RemoteInputHistoryItem.class);
219         addRemoteInputHistoryToMessages(newMessages, history);
220 
221         final Person user = extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON, Person.class);
222         boolean showSpinner =
223                 extras.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false);
224 
225 
226         final List<MessagingMessage> historicMessagingMessages = createMessages(newHistoricMessages,
227                 /* isHistoric= */true, usePrecomputedText);
228         List<MessagingMessage> newMessagingMessages;
229         mSummarizedContent = extras.getCharSequence(Notification.EXTRA_SUMMARIZED_CONTENT);
230         if (!TextUtils.isEmpty(mSummarizedContent) && mIsCollapsed) {
231             mMessagingLinearLayout.setMaxDisplayedLines(MAX_SUMMARIZATION_LINES);
232             Notification.MessagingStyle.Message summary =
233                     new Notification.MessagingStyle.Message(mSummarizedContent,  0, "");
234             newMessagingMessages = createMessages(List.of(summary), false, usePrecomputedText);
235         } else {
236             newMessagingMessages =
237                     createMessages(newMessages, /* isHistoric= */false, usePrecomputedText);
238         }
239 
240         // Let's first find our groups!
241         List<List<MessagingMessage>> groups = new ArrayList<>();
242         List<Person> senders = new ArrayList<>();
243 
244         // Lets first find the groups
245         findGroups(historicMessagingMessages, newMessagingMessages, groups, senders);
246 
247         return new MessagingData(user, showSpinner, historicMessagingMessages, newMessagingMessages,
248                 groups, senders, mSummarizedContent);
249     }
250 
251     /**
252      * RemotableViewMethod's asyncImpl of {@link #setData(Bundle)}.
253      * This should be called on a background thread, and returns a Runnable which is then must be
254      * called on the main thread to complete the operation and set text.
255      * @param extras Bundle contains messaging data
256      * @hide
257      */
258     @NonNull
setDataAsync(Bundle extras)259     public Runnable setDataAsync(Bundle extras) {
260         if (!mPrecomputedTextEnabled) {
261             return () -> setData(extras);
262         }
263 
264         final MessagingData messagingData =
265                 parseMessagingData(extras, /* usePrecomputedText= */true);
266 
267         return () -> {
268             finalizeInflate(messagingData.getHistoricMessagingMessages());
269             finalizeInflate(messagingData.getNewMessagingMessages());
270             bind(messagingData);
271         };
272     }
273 
274     /**
275      * enable/disable precomputed text usage
276      * @hide
277      */
setPrecomputedTextEnabled(boolean precomputedTextEnabled)278     public void setPrecomputedTextEnabled(boolean precomputedTextEnabled) {
279         mPrecomputedTextEnabled = precomputedTextEnabled;
280     }
281 
finalizeInflate(List<MessagingMessage> historicMessagingMessages)282     private void finalizeInflate(List<MessagingMessage> historicMessagingMessages) {
283         for (MessagingMessage messagingMessage: historicMessagingMessages) {
284             messagingMessage.finalizeInflate();
285         }
286     }
287 
288     @Override
setImageResolver(ImageResolver resolver)289     public void setImageResolver(ImageResolver resolver) {
290         mImageResolver = resolver;
291     }
292 
addRemoteInputHistoryToMessages( List<Notification.MessagingStyle.Message> newMessages, RemoteInputHistoryItem[] remoteInputHistory)293     private void addRemoteInputHistoryToMessages(
294             List<Notification.MessagingStyle.Message> newMessages,
295             RemoteInputHistoryItem[] remoteInputHistory) {
296         if (remoteInputHistory == null || remoteInputHistory.length == 0) {
297             return;
298         }
299         for (int i = remoteInputHistory.length - 1; i >= 0; i--) {
300             RemoteInputHistoryItem historyMessage = remoteInputHistory[i];
301             Notification.MessagingStyle.Message message = new Notification.MessagingStyle.Message(
302                     historyMessage.getText(), 0, null, true /* remoteHistory */);
303             if (historyMessage.getUri() != null) {
304                 message.setData(historyMessage.getMimeType(), historyMessage.getUri());
305             }
306             newMessages.add(message);
307         }
308     }
309 
bind(MessagingData messagingData)310     private void bind(MessagingData messagingData) {
311         setUser(messagingData.getUser());
312 
313         // Let's now create the views and reorder them accordingly
314         ArrayList<MessagingGroup> oldGroups = new ArrayList<>(mGroups);
315         createGroupViews(messagingData.getGroups(), messagingData.getSenders(),
316                 messagingData.getShowSpinner());
317 
318         // Let's first check which groups were removed altogether and remove them in one animation
319         removeGroups(oldGroups);
320 
321         // Let's remove the remaining messages
322         for (MessagingMessage message : mMessages) {
323             message.removeMessage(mToRecycle);
324         }
325         for (MessagingMessage historicMessage : mHistoricMessages) {
326             historicMessage.removeMessage(mToRecycle);
327         }
328 
329         mMessages = messagingData.getNewMessagingMessages();
330         mHistoricMessages = messagingData.getHistoricMessagingMessages();
331 
332         updateHistoricMessageVisibility();
333         updateTitleAndNamesDisplay();
334         // after groups are finalized, hide the first sender name if it's showing as the title
335         mPeopleHelper.maybeHideFirstSenderName(mGroups, mIsOneToOne, mConversationTitle);
336         updateImageMessages();
337 
338         // Recycle everything at the end of the update, now that we know it's no longer needed.
339         for (MessagingLinearLayout.MessagingChild child : mToRecycle) {
340             child.recycle();
341         }
342         mToRecycle.clear();
343     }
344 
updateImageMessages()345     private void updateImageMessages() {
346         if (mImageMessageContainer == null) {
347             return;
348         }
349         View newMessage = getNewImageMessage();
350         // Remove all messages that don't belong into the image layout
351         View previousMessage = mImageMessageContainer.getChildAt(0);
352         if (previousMessage != newMessage) {
353             mImageMessageContainer.removeView(previousMessage);
354             if (newMessage != null) {
355                 mImageMessageContainer.addView(newMessage);
356             }
357         }
358         mImageMessageContainer.setVisibility(newMessage != null ? VISIBLE : GONE);
359 
360         // When showing an image message, do not show the large icon.  Removing the drawable
361         // prevents it from being shown in the left_icon view (by the grouping util).
362         if (newMessage != null && mRightIconView != null && mRightIconView.getDrawable() != null) {
363             mRightIconView.setImageDrawable(null);
364             mRightIconView.setVisibility(GONE);
365         }
366     }
367 
368     @Nullable
getNewImageMessage()369     private View getNewImageMessage() {
370         if (mIsCollapsed && !mGroups.isEmpty()) {
371             // When collapsed, we're displaying the image message in a dedicated container
372             // on the right of the layout instead of inline. Let's add the isolated image there
373             MessagingGroup messagingGroup = mGroups.getLast();
374             MessagingImageMessage isolatedMessage = messagingGroup.getIsolatedMessage();
375             if (isolatedMessage != null) {
376                 return isolatedMessage.getView();
377             }
378         }
379         return null;
380     }
381 
removeGroups(ArrayList<MessagingGroup> oldGroups)382     private void removeGroups(ArrayList<MessagingGroup> oldGroups) {
383         int size = oldGroups.size();
384         for (int i = 0; i < size; i++) {
385             MessagingGroup group = oldGroups.get(i);
386             if (!mGroups.contains(group)) {
387                 List<MessagingMessage> messages = group.getMessages();
388 
389                 boolean wasShown = group.isShown();
390                 mMessagingLinearLayout.removeView(group);
391                 if (wasShown && !MessagingLinearLayout.isGone(group)) {
392                     mMessagingLinearLayout.addTransientView(group, 0);
393                     group.removeGroupAnimated(() -> {
394                         mMessagingLinearLayout.removeTransientView(group);
395                         group.recycle();
396                     });
397                 } else {
398                     mToRecycle.add(group);
399                 }
400                 mMessages.removeAll(messages);
401                 mHistoricMessages.removeAll(messages);
402             }
403         }
404     }
405 
updateTitleAndNamesDisplay()406     private void updateTitleAndNamesDisplay() {
407         Map<CharSequence, String> uniqueNames = mPeopleHelper.mapUniqueNamesToPrefix(mGroups);
408 
409         // Now that we have the correct symbols, let's look what we have cached
410         ArrayMap<CharSequence, Icon> cachedAvatars = new ArrayMap<>();
411         for (int i = 0; i < mGroups.size(); i++) {
412             // Let's now set the avatars
413             MessagingGroup group = mGroups.get(i);
414             boolean isOwnMessage = group.getSender() == mUser;
415             CharSequence senderName = group.getSenderName();
416             if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)
417                     || (mIsOneToOne && mAvatarReplacement != null && !isOwnMessage)) {
418                 continue;
419             }
420             String symbol = uniqueNames.get(senderName);
421             Icon cachedIcon = group.getAvatarSymbolIfMatching(senderName,
422                     symbol, mLayoutColor);
423             if (cachedIcon != null) {
424                 cachedAvatars.put(senderName, cachedIcon);
425             }
426         }
427 
428         for (int i = 0; i < mGroups.size(); i++) {
429             // Let's now set the avatars
430             MessagingGroup group = mGroups.get(i);
431             CharSequence senderName = group.getSenderName();
432             if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)) {
433                 continue;
434             }
435             if (mIsOneToOne && mAvatarReplacement != null && group.getSender() != mUser) {
436                 group.setAvatar(mAvatarReplacement);
437             } else {
438                 Icon cachedIcon = cachedAvatars.get(senderName);
439                 if (cachedIcon == null) {
440                     cachedIcon = createAvatarSymbol(senderName, uniqueNames.get(senderName),
441                             mLayoutColor);
442                     cachedAvatars.put(senderName, cachedIcon);
443                 }
444                 group.setCreatedAvatar(cachedIcon, senderName, uniqueNames.get(senderName),
445                         mLayoutColor);
446             }
447         }
448     }
449 
createAvatarSymbol(CharSequence senderName, String symbol, int layoutColor)450     public Icon createAvatarSymbol(CharSequence senderName, String symbol, int layoutColor) {
451         return mPeopleHelper.createAvatarSymbol(senderName, symbol, layoutColor);
452     }
453 
454     @RemotableViewMethod(asyncImpl = "setLayoutColorAsync")
setLayoutColor(int color)455     public void setLayoutColor(int color) {
456         mLayoutColor = color;
457     }
458 
459     /**
460      * @hide
461      */
setLayoutColorAsync(int color)462     public Runnable setLayoutColorAsync(int color) {
463         mLayoutColor = color;
464         return () -> {};
465     }
466 
467     @RemotableViewMethod(asyncImpl = "setIsOneToOneAsync")
setIsOneToOne(boolean oneToOne)468     public void setIsOneToOne(boolean oneToOne) {
469         mIsOneToOne = oneToOne;
470     }
471 
472     /**
473      * @hide
474      */
setIsOneToOneAsync(boolean oneToOne)475     public Runnable setIsOneToOneAsync(boolean oneToOne) {
476         mIsOneToOne = oneToOne;
477         return () -> {};
478     }
479 
480     @RemotableViewMethod(asyncImpl = "setSenderTextColorAsync")
setSenderTextColor(int color)481     public void setSenderTextColor(int color) {
482         mSenderTextColor = color;
483     }
484 
485     /**
486      * @hide
487      */
setSenderTextColorAsync(int color)488     public Runnable setSenderTextColorAsync(int color) {
489         mSenderTextColor = color;
490         return () -> {};
491     }
492     /**
493      * @param color the color of the notification background
494      */
495     @RemotableViewMethod
setNotificationBackgroundColor(int color)496     public void setNotificationBackgroundColor(int color) {
497         // Nothing to do with this
498     }
499 
500     @RemotableViewMethod(asyncImpl = "setMessageTextColorAsync")
setMessageTextColor(int color)501     public void setMessageTextColor(int color) {
502         mMessageTextColor = color;
503     }
504 
505     /**
506      * @hide
507      */
setMessageTextColorAsync(int color)508     public Runnable setMessageTextColorAsync(int color) {
509         mMessageTextColor = color;
510         return () -> {};
511     }
512 
setUser(Person user)513     public void setUser(Person user) {
514         mUser = user;
515         if (mUser.getIcon() == null) {
516             Icon userIcon = Icon.createWithResource(getContext(),
517                     com.android.internal.R.drawable.messaging_user);
518             userIcon.setTint(mLayoutColor);
519             mUser = mUser.toBuilder().setIcon(userIcon).build();
520         }
521     }
522 
createGroupViews(List<List<MessagingMessage>> groups, List<Person> senders, boolean showSpinner)523     private void createGroupViews(List<List<MessagingMessage>> groups,
524             List<Person> senders, boolean showSpinner) {
525         mGroups.clear();
526         for (int groupIndex = 0; groupIndex < groups.size(); groupIndex++) {
527             List<MessagingMessage> group = groups.get(groupIndex);
528             MessagingGroup newGroup = null;
529             // we'll just take the first group that exists or create one there is none
530             for (int messageIndex = group.size() - 1; messageIndex >= 0; messageIndex--) {
531                 MessagingMessage message = group.get(messageIndex);
532                 newGroup = message.getGroup();
533                 if (newGroup != null) {
534                     break;
535                 }
536             }
537             if (newGroup == null) {
538                 newGroup = MessagingGroup.createGroup(mMessagingLinearLayout);
539                 mAddedGroups.add(newGroup);
540             } else if (newGroup.getParent() != mMessagingLinearLayout) {
541                 throw new IllegalStateException(
542                         "group parent was " + newGroup.getParent() + " but expected "
543                                 + mMessagingLinearLayout);
544             }
545             newGroup.setImageDisplayLocation(mIsCollapsed
546                     ? IMAGE_DISPLAY_LOCATION_EXTERNAL
547                     : IMAGE_DISPLAY_LOCATION_INLINE);
548             newGroup.setIsInConversation(false);
549             newGroup.setLayoutColor(mLayoutColor);
550             newGroup.setTextColors(mSenderTextColor, mMessageTextColor);
551             Person sender = senders.get(groupIndex);
552             CharSequence nameOverride = null;
553             if (sender != mUser && mNameReplacement != null) {
554                 nameOverride = mNameReplacement;
555             }
556             newGroup.setSingleLine(mIsCollapsed
557                     ? !android.app.Flags.nmCollapsedLines() && TextUtils.isEmpty(mSummarizedContent)
558                     : false);
559             newGroup.setShowingAvatar(!mIsCollapsed);
560             newGroup.setIsCollapsed(mIsCollapsed);
561             newGroup.setSender(sender, nameOverride);
562             newGroup.setSending(groupIndex == (groups.size() - 1) && showSpinner);
563             mGroups.add(newGroup);
564 
565             if (mMessagingLinearLayout.indexOfChild(newGroup) != groupIndex) {
566                 mMessagingLinearLayout.removeView(newGroup);
567                 mMessagingLinearLayout.addView(newGroup, groupIndex);
568             }
569             newGroup.setMessages(group);
570         }
571 
572         if (Flags.dropNonExistingMessages()) {
573             // remove groups from mAddedGroups when they are no longer in mGroups.
574             mAddedGroups.removeIf(
575                     messagingGroup -> !mGroups.contains(messagingGroup));
576         }
577     }
578 
findGroups(List<MessagingMessage> historicMessages, List<MessagingMessage> messages, List<List<MessagingMessage>> groups, List<Person> senders)579     private void findGroups(List<MessagingMessage> historicMessages,
580             List<MessagingMessage> messages, List<List<MessagingMessage>> groups,
581             List<Person> senders) {
582         CharSequence currentSenderKey = null;
583         List<MessagingMessage> currentGroup = null;
584         int histSize = historicMessages.size();
585         for (int i = 0; i < histSize + messages.size(); i++) {
586             MessagingMessage message;
587             if (i < histSize) {
588                 message = historicMessages.get(i);
589             } else {
590                 message = messages.get(i - histSize);
591             }
592             boolean isNewGroup = currentGroup == null;
593             Person sender =
594                     message.getMessage() == null ? null : message.getMessage().getSenderPerson();
595             CharSequence key = sender == null ? null
596                     : sender.getKey() == null ? sender.getName() : sender.getKey();
597             isNewGroup |= !TextUtils.equals(key, currentSenderKey);
598             if (isNewGroup) {
599                 currentGroup = new ArrayList<>();
600                 groups.add(currentGroup);
601                 if (sender == null) {
602                     sender = mUser;
603                 }
604                 senders.add(sender);
605                 currentSenderKey = key;
606             }
607             currentGroup.add(message);
608         }
609     }
610 
611     /**
612      * Creates new messages, reusing existing ones if they are available.
613      *
614      * @param newMessages the messages to parse.
615      */
createMessages( List<Notification.MessagingStyle.Message> newMessages, boolean isHistoric, boolean usePrecomputedText)616     private List<MessagingMessage> createMessages(
617             List<Notification.MessagingStyle.Message> newMessages, boolean isHistoric,
618             boolean usePrecomputedText) {
619         List<MessagingMessage> result = new ArrayList<>();
620         for (int i = 0; i < newMessages.size(); i++) {
621             Notification.MessagingStyle.Message m = newMessages.get(i);
622             MessagingMessage message = findAndRemoveMatchingMessage(m);
623             if (message == null) {
624                 message = MessagingMessage.createMessage(this, m,
625                         mImageResolver, usePrecomputedText);
626             }
627             message.setIsHistoric(isHistoric);
628             result.add(message);
629         }
630         return result;
631     }
632 
findAndRemoveMatchingMessage(Notification.MessagingStyle.Message m)633     private MessagingMessage findAndRemoveMatchingMessage(Notification.MessagingStyle.Message m) {
634         for (int i = 0; i < mMessages.size(); i++) {
635             MessagingMessage existing = mMessages.get(i);
636             if (existing.sameAs(m)) {
637                 mMessages.remove(i);
638                 return existing;
639             }
640         }
641         for (int i = 0; i < mHistoricMessages.size(); i++) {
642             MessagingMessage existing = mHistoricMessages.get(i);
643             if (existing.sameAs(m)) {
644                 mHistoricMessages.remove(i);
645                 return existing;
646             }
647         }
648         return null;
649     }
650 
showHistoricMessages(boolean show)651     public void showHistoricMessages(boolean show) {
652         mShowHistoricMessages = show;
653         updateHistoricMessageVisibility();
654     }
655 
updateHistoricMessageVisibility()656     private void updateHistoricMessageVisibility() {
657         int numHistoric = mHistoricMessages.size();
658         for (int i = 0; i < numHistoric; i++) {
659             MessagingMessage existing = mHistoricMessages.get(i);
660             existing.setVisibility(mShowHistoricMessages ? VISIBLE : GONE);
661         }
662         int numGroups = mGroups.size();
663         for (int i = 0; i < numGroups; i++) {
664             MessagingGroup group = mGroups.get(i);
665             int visibleChildren = 0;
666             List<MessagingMessage> messages = group.getMessages();
667             int numGroupMessages = messages.size();
668             for (int j = 0; j < numGroupMessages; j++) {
669                 MessagingMessage message = messages.get(j);
670                 if (message.getVisibility() != GONE) {
671                     visibleChildren++;
672                 }
673             }
674             if (visibleChildren > 0 && group.getVisibility() == GONE) {
675                 group.setVisibility(VISIBLE);
676             } else if (visibleChildren == 0 && group.getVisibility() != GONE)   {
677                 group.setVisibility(GONE);
678             }
679         }
680     }
681 
682     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)683     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
684         super.onLayout(changed, left, top, right, bottom);
685         if (!mAddedGroups.isEmpty()) {
686             getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
687                 @Override
688                 public boolean onPreDraw() {
689                     for (MessagingGroup group : mAddedGroups) {
690                         if (!group.isShown()) {
691                             continue;
692                         }
693                         MessagingPropertyAnimator.fadeIn(group.getAvatar());
694                         MessagingPropertyAnimator.fadeIn(group.getSenderView());
695                         MessagingPropertyAnimator.startLocalTranslationFrom(group,
696                                 group.getHeight(), LINEAR_OUT_SLOW_IN);
697                     }
698                     mAddedGroups.clear();
699                     getViewTreeObserver().removeOnPreDrawListener(this);
700                     return true;
701                 }
702             });
703         }
704     }
705 
getMessagingLinearLayout()706     public MessagingLinearLayout getMessagingLinearLayout() {
707         return mMessagingLinearLayout;
708     }
709 
710     @Nullable
getImageMessageContainer()711     public ViewGroup getImageMessageContainer() {
712         return mImageMessageContainer;
713     }
714 
getMessagingGroups()715     public ArrayList<MessagingGroup> getMessagingGroups() {
716         return mGroups;
717     }
718 
719     @Override
setMessagingClippingDisabled(boolean clippingDisabled)720     public void setMessagingClippingDisabled(boolean clippingDisabled) {
721         mMessagingLinearLayout.setClipBounds(clippingDisabled ? null : mMessagingClipRect);
722     }
723 }
724