1 /*
2  * Copyright (C) 2020 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.animation.Animator;
23 import android.animation.AnimatorListenerAdapter;
24 import android.animation.AnimatorSet;
25 import android.animation.ValueAnimator;
26 import android.annotation.AttrRes;
27 import android.annotation.NonNull;
28 import android.annotation.Nullable;
29 import android.annotation.StyleRes;
30 import android.app.Notification;
31 import android.app.Person;
32 import android.app.RemoteInputHistoryItem;
33 import android.content.Context;
34 import android.content.res.ColorStateList;
35 import android.graphics.Rect;
36 import android.graphics.Typeface;
37 import android.graphics.drawable.Drawable;
38 import android.graphics.drawable.GradientDrawable;
39 import android.graphics.drawable.Icon;
40 import android.os.Bundle;
41 import android.os.Parcelable;
42 import android.text.Spannable;
43 import android.text.SpannableString;
44 import android.text.TextUtils;
45 import android.text.style.StyleSpan;
46 import android.util.ArrayMap;
47 import android.util.AttributeSet;
48 import android.util.DisplayMetrics;
49 import android.view.Gravity;
50 import android.view.MotionEvent;
51 import android.view.RemotableViewMethod;
52 import android.view.TouchDelegate;
53 import android.view.View;
54 import android.view.ViewGroup;
55 import android.view.ViewTreeObserver;
56 import android.view.animation.Interpolator;
57 import android.view.animation.PathInterpolator;
58 import android.widget.FrameLayout;
59 import android.widget.ImageView;
60 import android.widget.LinearLayout;
61 import android.widget.RemoteViews;
62 import android.widget.TextView;
63 import android.widget.flags.Flags;
64 
65 import com.android.internal.R;
66 import com.android.internal.widget.ConversationAvatarData.GroupConversationAvatarData;
67 import com.android.internal.widget.ConversationAvatarData.OneToOneConversationAvatarData;
68 
69 import java.util.ArrayList;
70 import java.util.List;
71 import java.util.Map;
72 import java.util.Objects;
73 
74 /**
75  * A custom-built layout for the Notification.MessagingStyle allows dynamic addition and removal
76  * messages and adapts the layout accordingly.
77  */
78 @RemoteViews.RemoteView
79 public class ConversationLayout extends FrameLayout
80         implements ImageMessageConsumer, IMessagingLayout {
81 
82     public static final Interpolator LINEAR_OUT_SLOW_IN = new PathInterpolator(0f, 0f, 0.2f, 1f);
83     public static final Interpolator FAST_OUT_LINEAR_IN = new PathInterpolator(0.4f, 0f, 1f, 1f);
84     public static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f);
85     public static final Interpolator OVERSHOOT = new PathInterpolator(0.4f, 0f, 0.2f, 1.4f);
86     public static final OnLayoutChangeListener MESSAGING_PROPERTY_ANIMATOR
87             = new MessagingPropertyAnimator();
88     public static final int IMPORTANCE_ANIM_GROW_DURATION = 250;
89     public static final int IMPORTANCE_ANIM_SHRINK_DURATION = 200;
90     public static final int IMPORTANCE_ANIM_SHRINK_DELAY = 25;
91     private final PeopleHelper mPeopleHelper = new PeopleHelper();
92     private List<MessagingMessage> mMessages = new ArrayList<>();
93     private List<MessagingMessage> mHistoricMessages = new ArrayList<>();
94     private MessagingLinearLayout mMessagingLinearLayout;
95     private boolean mShowHistoricMessages;
96     private ArrayList<MessagingGroup> mGroups = new ArrayList<>();
97     private int mLayoutColor;
98     private int mSenderTextColor;
99     private int mMessageTextColor;
100     private Icon mAvatarReplacement;
101     private boolean mIsOneToOne;
102     private ArrayList<MessagingGroup> mAddedGroups = new ArrayList<>();
103     private Person mUser;
104     private CharSequence mNameReplacement;
105     private boolean mIsCollapsed;
106     private ImageResolver mImageResolver;
107     private CachingIconView mConversationIconView;
108     private View mConversationIconContainer;
109     private int mConversationIconTopPaddingExpandedGroup;
110     private int mConversationIconTopPadding;
111     private int mExpandedGroupMessagePadding;
112     // TODO (b/217799515) Currently, mConversationText shows the conversation title, the actual
113     //  conversation text is inside of mMessagingLinearLayout, which is misleading, we should rename
114     //  this to mConversationTitleView
115     private TextView mConversationText;
116     private View mConversationIconBadge;
117     private CachingIconView mConversationIconBadgeBg;
118     private Icon mLargeIcon;
119     private View mExpandButtonContainer;
120     private ViewGroup mExpandButtonAndContentContainer;
121     private ViewGroup mExpandButtonContainerA11yContainer;
122     private NotificationExpandButton mExpandButton;
123     private MessagingLinearLayout mImageMessageContainer;
124     private int mBadgeProtrusion;
125     private int mConversationAvatarSize;
126     private int mConversationAvatarSizeExpanded;
127     private CachingIconView mIcon;
128     private CachingIconView mImportanceRingView;
129     private int mExpandedGroupBadgeProtrusion;
130     private int mExpandedGroupBadgeProtrusionFacePile;
131     private View mConversationFacePile;
132     private int mNotificationBackgroundColor;
133     private CharSequence mFallbackChatName;
134     private CharSequence mFallbackGroupChatName;
135     //TODO (b/217799515) Currently, Notification.MessagingStyle, ConversationLayout, and
136     // HybridConversationNotificationView, each has their own definition of "ConversationTitle".
137     // What make things worse is that the term of "ConversationTitle" often confuses with
138     // "ConversationText".
139     // We need to unify them or differentiate the namings.
140     private CharSequence mConversationTitle;
141     private int mMessageSpacingStandard;
142     private int mMessageSpacingGroup;
143     private int mNotificationHeaderExpandedPadding;
144     private View mConversationHeader;
145     private View mContentContainer;
146     private boolean mExpandable = true;
147     private int mContentMarginEnd;
148     private Rect mMessagingClipRect;
149     private ObservableTextView mAppName;
150     private NotificationActionListLayout mActions;
151     private boolean mAppNameGone;
152     private int mFacePileAvatarSize;
153     private int mFacePileAvatarSizeExpandedGroup;
154     private int mFacePileProtectionWidth;
155     private int mFacePileProtectionWidthExpanded;
156     private boolean mImportantConversation;
157     private View mFeedbackIcon;
158     private float mMinTouchSize;
159     private Icon mConversationIcon;
160     private Icon mShortcutIcon;
161     private View mAppNameDivider;
162     private TouchDelegateComposite mTouchDelegate = new TouchDelegateComposite(this);
163     private ArrayList<MessagingLinearLayout.MessagingChild> mToRecycle = new ArrayList<>();
164     private boolean mPrecomputedTextEnabled = false;
165     @Nullable
166     private ConversationHeaderData mConversationHeaderData;
167 
ConversationLayout(@onNull Context context)168     public ConversationLayout(@NonNull Context context) {
169         super(context);
170     }
171 
ConversationLayout(@onNull Context context, @Nullable AttributeSet attrs)172     public ConversationLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
173         super(context, attrs);
174     }
175 
ConversationLayout(@onNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr)176     public ConversationLayout(@NonNull Context context, @Nullable AttributeSet attrs,
177             @AttrRes int defStyleAttr) {
178         super(context, attrs, defStyleAttr);
179     }
180 
ConversationLayout(@onNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes)181     public ConversationLayout(@NonNull Context context, @Nullable AttributeSet attrs,
182             @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
183         super(context, attrs, defStyleAttr, defStyleRes);
184     }
185 
186     @Override
onFinishInflate()187     protected void onFinishInflate() {
188         super.onFinishInflate();
189         mPeopleHelper.init(getContext());
190         mMessagingLinearLayout = findViewById(R.id.notification_messaging);
191         mActions = findViewById(R.id.actions);
192         mImageMessageContainer = findViewById(R.id.conversation_image_message_container);
193         // We still want to clip, but only on the top, since views can temporarily out of bounds
194         // during transitions.
195         DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
196         int size = Math.max(displayMetrics.widthPixels, displayMetrics.heightPixels);
197         mMessagingClipRect = new Rect(0, 0, size, size);
198         setMessagingClippingDisabled(false);
199         mConversationIconView = findViewById(R.id.conversation_icon);
200         mConversationIconContainer = findViewById(R.id.conversation_icon_container);
201         mIcon = findViewById(R.id.icon);
202         mFeedbackIcon = findViewById(com.android.internal.R.id.feedback);
203         mMinTouchSize = 48 * getResources().getDisplayMetrics().density;
204         mImportanceRingView = findViewById(R.id.conversation_icon_badge_ring);
205         mConversationIconBadge = findViewById(R.id.conversation_icon_badge);
206         mConversationIconBadgeBg = findViewById(R.id.conversation_icon_badge_bg);
207         mIcon.setOnVisibilityChangedListener((visibility) -> {
208 
209             // Let's hide the background directly or in an animated way
210             boolean isGone = visibility == GONE;
211             int oldVisibility = mConversationIconBadgeBg.getVisibility();
212             boolean wasGone = oldVisibility == GONE;
213             if (wasGone != isGone) {
214                 // Keep the badge gone state in sync with the icon. This is necessary in cases
215                 // Where the icon is being hidden externally like in group children.
216                 mConversationIconBadgeBg.animate().cancel();
217                 mConversationIconBadgeBg.setVisibility(visibility);
218             }
219 
220             // Let's handle the importance ring which can also be be gone normally
221             oldVisibility = mImportanceRingView.getVisibility();
222             wasGone = oldVisibility == GONE;
223             visibility = !mImportantConversation ? GONE : visibility;
224             boolean isRingGone = visibility == GONE;
225             if (wasGone != isRingGone) {
226                 // Keep the badge visibility in sync with the icon. This is necessary in cases
227                 // Where the icon is being hidden externally like in group children.
228                 mImportanceRingView.animate().cancel();
229                 mImportanceRingView.setVisibility(visibility);
230             }
231 
232             oldVisibility = mConversationIconBadge.getVisibility();
233             wasGone = oldVisibility == GONE;
234             if (wasGone != isGone) {
235                 mConversationIconBadge.animate().cancel();
236                 mConversationIconBadge.setVisibility(visibility);
237             }
238         });
239         // When the small icon is gone, hide the rest of the badge
240         mIcon.setOnForceHiddenChangedListener((forceHidden) -> {
241             mPeopleHelper.animateViewForceHidden(mConversationIconBadgeBg, forceHidden);
242             mPeopleHelper.animateViewForceHidden(mImportanceRingView, forceHidden);
243         });
244 
245         // When the conversation icon is gone, hide the whole badge
246         mConversationIconView.setOnForceHiddenChangedListener((forceHidden) -> {
247             mPeopleHelper.animateViewForceHidden(mConversationIconBadgeBg, forceHidden);
248             mPeopleHelper.animateViewForceHidden(mImportanceRingView, forceHidden);
249             mPeopleHelper.animateViewForceHidden(mIcon, forceHidden);
250         });
251         mConversationText = findViewById(R.id.conversation_text);
252         mExpandButtonContainer = findViewById(R.id.expand_button_container);
253         mExpandButtonContainerA11yContainer =
254                 findViewById(R.id.expand_button_a11y_container);
255         mConversationHeader = findViewById(R.id.conversation_header);
256         mContentContainer = findViewById(R.id.notification_action_list_margin_target);
257         mExpandButtonAndContentContainer = findViewById(R.id.expand_button_and_content_container);
258         mExpandButton = findViewById(R.id.expand_button);
259         mMessageSpacingStandard = getResources().getDimensionPixelSize(
260                 R.dimen.notification_messaging_spacing);
261         mMessageSpacingGroup = getResources().getDimensionPixelSize(
262                 R.dimen.notification_messaging_spacing_conversation_group);
263         mNotificationHeaderExpandedPadding = getResources().getDimensionPixelSize(
264                 R.dimen.conversation_header_expanded_padding_end);
265         mContentMarginEnd = getResources().getDimensionPixelSize(
266                 R.dimen.notification_content_margin_end);
267         mBadgeProtrusion = getResources().getDimensionPixelSize(
268                 R.dimen.conversation_badge_protrusion);
269         mConversationAvatarSize = getResources().getDimensionPixelSize(
270                 R.dimen.conversation_avatar_size);
271         mConversationAvatarSizeExpanded = getResources().getDimensionPixelSize(
272                 R.dimen.conversation_avatar_size_group_expanded);
273         mConversationIconTopPaddingExpandedGroup = getResources().getDimensionPixelSize(
274                 R.dimen.conversation_icon_container_top_padding_small_avatar);
275         mConversationIconTopPadding = getResources().getDimensionPixelSize(
276                 R.dimen.conversation_icon_container_top_padding);
277         mExpandedGroupMessagePadding = getResources().getDimensionPixelSize(
278                 R.dimen.expanded_group_conversation_message_padding);
279         mExpandedGroupBadgeProtrusion = getResources().getDimensionPixelSize(
280                 R.dimen.conversation_badge_protrusion_group_expanded);
281         mExpandedGroupBadgeProtrusionFacePile = getResources().getDimensionPixelSize(
282                 R.dimen.conversation_badge_protrusion_group_expanded_face_pile);
283         mConversationFacePile = findViewById(R.id.conversation_face_pile);
284         mFacePileAvatarSize = getResources().getDimensionPixelSize(
285                 R.dimen.conversation_face_pile_avatar_size);
286         mFacePileAvatarSizeExpandedGroup = getResources().getDimensionPixelSize(
287                 R.dimen.conversation_face_pile_avatar_size_group_expanded);
288         mFacePileProtectionWidth = getResources().getDimensionPixelSize(
289                 R.dimen.conversation_face_pile_protection_width);
290         mFacePileProtectionWidthExpanded = getResources().getDimensionPixelSize(
291                 R.dimen.conversation_face_pile_protection_width_expanded);
292         mFallbackChatName = getResources().getString(
293                 R.string.conversation_title_fallback_one_to_one);
294         mFallbackGroupChatName = getResources().getString(
295                 R.string.conversation_title_fallback_group_chat);
296         mAppName = findViewById(R.id.app_name_text);
297         mAppNameDivider = findViewById(R.id.app_name_divider);
298         mAppNameGone = mAppName.getVisibility() == GONE;
299         mAppName.setOnVisibilityChangedListener((visibility) -> {
300             onAppNameVisibilityChanged();
301         });
302     }
303 
304     @RemotableViewMethod
setAvatarReplacement(Icon icon)305     public void setAvatarReplacement(Icon icon) {
306         mAvatarReplacement = icon;
307     }
308 
309     @RemotableViewMethod
setNameReplacement(CharSequence nameReplacement)310     public void setNameReplacement(CharSequence nameReplacement) {
311         mNameReplacement = nameReplacement;
312     }
313 
314     /**
315      * Sets this conversation as "important", adding some additional UI treatment.
316      */
317     @RemotableViewMethod
setIsImportantConversation(boolean isImportantConversation)318     public void setIsImportantConversation(boolean isImportantConversation) {
319         setIsImportantConversation(isImportantConversation, false);
320     }
321 
322     /**
323      * @hide
324      **/
setIsImportantConversation(boolean isImportantConversation, boolean animate)325     public void setIsImportantConversation(boolean isImportantConversation, boolean animate) {
326         mImportantConversation = isImportantConversation;
327         mImportanceRingView.setVisibility(isImportantConversation && mIcon.getVisibility() != GONE
328                 ? VISIBLE : GONE);
329 
330         if (animate && isImportantConversation) {
331             GradientDrawable ring = (GradientDrawable) mImportanceRingView.getDrawable();
332             ring.mutate();
333             GradientDrawable bg = (GradientDrawable) mConversationIconBadgeBg.getDrawable();
334             bg.mutate();
335             int ringColor = getResources()
336                     .getColor(R.color.conversation_important_highlight);
337             int standardThickness = getResources()
338                     .getDimensionPixelSize(R.dimen.importance_ring_stroke_width);
339             int largeThickness = getResources()
340                     .getDimensionPixelSize(R.dimen.importance_ring_anim_max_stroke_width);
341             int standardSize = getResources().getDimensionPixelSize(
342                     R.dimen.importance_ring_size);
343             int baseSize = standardSize - standardThickness * 2;
344             int bgSize = getResources()
345                     .getDimensionPixelSize(R.dimen.conversation_icon_size_badged);
346 
347             ValueAnimator.AnimatorUpdateListener animatorUpdateListener = animation -> {
348                 int strokeWidth = Math.round((float) animation.getAnimatedValue());
349                 ring.setStroke(strokeWidth, ringColor);
350                 int newSize = baseSize + strokeWidth * 2;
351                 ring.setSize(newSize, newSize);
352                 mImportanceRingView.invalidate();
353             };
354 
355             ValueAnimator growAnimation = ValueAnimator.ofFloat(0, largeThickness);
356             growAnimation.setInterpolator(LINEAR_OUT_SLOW_IN);
357             growAnimation.setDuration(IMPORTANCE_ANIM_GROW_DURATION);
358             growAnimation.addUpdateListener(animatorUpdateListener);
359 
360             ValueAnimator shrinkAnimation =
361                     ValueAnimator.ofFloat(largeThickness, standardThickness);
362             shrinkAnimation.setDuration(IMPORTANCE_ANIM_SHRINK_DURATION);
363             shrinkAnimation.setStartDelay(IMPORTANCE_ANIM_SHRINK_DELAY);
364             shrinkAnimation.setInterpolator(OVERSHOOT);
365             shrinkAnimation.addUpdateListener(animatorUpdateListener);
366             shrinkAnimation.addListener(new AnimatorListenerAdapter() {
367                 @Override
368                 public void onAnimationStart(Animator animation) {
369                     // Shrink the badge bg so that it doesn't peek behind the animation
370                     bg.setSize(baseSize, baseSize);
371                     mConversationIconBadgeBg.invalidate();
372                 }
373 
374                 @Override
375                 public void onAnimationEnd(Animator animation) {
376                     // Reset bg back to normal size
377                     bg.setSize(bgSize, bgSize);
378                     mConversationIconBadgeBg.invalidate();
379                 }
380             });
381 
382             AnimatorSet anims = new AnimatorSet();
383             anims.playSequentially(growAnimation, shrinkAnimation);
384             anims.start();
385         }
386     }
387 
isImportantConversation()388     public boolean isImportantConversation() {
389         return mImportantConversation;
390     }
391 
392     /**
393      * Set this layout to show the collapsed representation.
394      *
395      * @param isCollapsed is it collapsed
396      */
397     @RemotableViewMethod
setIsCollapsed(boolean isCollapsed)398     public void setIsCollapsed(boolean isCollapsed) {
399         mIsCollapsed = isCollapsed;
400         mMessagingLinearLayout.setMaxDisplayedLines(isCollapsed ? 1 : Integer.MAX_VALUE);
401         updateExpandButton();
402         updateContentEndPaddings();
403     }
404 
405     /**
406      * Set conversation data
407      *
408      * @param extras Bundle contains conversation data
409      */
410     @RemotableViewMethod(asyncImpl = "setDataAsync")
setData(Bundle extras)411     public void setData(Bundle extras) {
412         bind(parseMessagingData(extras,
413                 /* usePrecomputedText= */ false,
414                 /*includeConversationIcon= */false));
415     }
416 
417     @NonNull
parseMessagingData(Bundle extras, boolean usePrecomputedText, boolean includeConversationIcon)418     private MessagingData parseMessagingData(Bundle extras, boolean usePrecomputedText,
419             boolean includeConversationIcon) {
420         Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES);
421         List<Notification.MessagingStyle.Message> newMessages =
422                 Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages);
423         Parcelable[] histMessages = extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES);
424         List<Notification.MessagingStyle.Message> newHistoricMessages =
425                 Notification.MessagingStyle.Message.getMessagesFromBundleArray(histMessages);
426 
427         // mUser now set (would be nice to avoid the side effect but WHATEVER)
428         final Person user = extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON, Person.class);
429         // Append remote input history to newMessages (again, side effect is lame but WHATEVS)
430         RemoteInputHistoryItem[] history = (RemoteInputHistoryItem[])
431                 extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS,
432                         RemoteInputHistoryItem.class);
433         addRemoteInputHistoryToMessages(newMessages, history);
434 
435         boolean showSpinner =
436                 extras.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false);
437         int unreadCount = extras.getInt(Notification.EXTRA_CONVERSATION_UNREAD_MESSAGE_COUNT);
438 
439         final List<MessagingMessage> newMessagingMessages =
440                 createMessages(newMessages, /* isHistoric= */false, usePrecomputedText);
441         final List<MessagingMessage> newHistoricMessagingMessages =
442                 createMessages(newHistoricMessages, /* isHistoric= */true, usePrecomputedText);
443 
444         // Add our new MessagingMessages to groups
445         List<List<MessagingMessage>> groups = new ArrayList<>();
446         List<Person> senders = new ArrayList<>();
447         // Lets first find the groups (populate `groups` and `senders`)
448         findGroups(newHistoricMessagingMessages, newMessagingMessages, user, groups, senders);
449 
450         // load conversation header data, avatar and title.
451         final ConversationHeaderData conversationHeaderData;
452         if (includeConversationIcon && Flags.conversationStyleSetAvatarAsync()) {
453             conversationHeaderData = loadConversationHeaderData(mIsOneToOne,
454                     mConversationTitle,
455                     mShortcutIcon,
456                     mLargeIcon, newMessagingMessages, user, groups, mLayoutColor);
457         } else {
458             conversationHeaderData = null;
459         }
460 
461         return new MessagingData(user, showSpinner, unreadCount,
462                 newHistoricMessagingMessages, newMessagingMessages, groups, senders,
463                 conversationHeaderData);
464     }
465 
466     /**
467      * RemotableViewMethod's asyncImpl of {@link #setData(Bundle)}.
468      * This should be called on a background thread, and returns a Runnable which is then must be
469      * called on the main thread to complete the operation and set text.
470      *
471      * @param extras Bundle contains conversation data
472      * @hide
473      */
474     @NonNull
setDataAsync(Bundle extras)475     public Runnable setDataAsync(Bundle extras) {
476         if (!mPrecomputedTextEnabled) {
477             return () -> setData(extras);
478         }
479 
480         final MessagingData messagingData =
481                 parseMessagingData(extras,
482                         /* usePrecomputedText= */ true,
483                         /*includeConversationIcon=*/true);
484 
485         return () -> {
486             finalizeInflate(messagingData.getHistoricMessagingMessages());
487             finalizeInflate(messagingData.getNewMessagingMessages());
488 
489             bind(messagingData);
490         };
491     }
492 
493     /**
494      * enable/disable precomputed text usage
495      *
496      * @hide
497      */
setPrecomputedTextEnabled(boolean precomputedTextEnabled)498     public void setPrecomputedTextEnabled(boolean precomputedTextEnabled) {
499         mPrecomputedTextEnabled = precomputedTextEnabled;
500     }
501 
finalizeInflate(List<MessagingMessage> historicMessagingMessages)502     private void finalizeInflate(List<MessagingMessage> historicMessagingMessages) {
503         for (MessagingMessage messagingMessage : historicMessagingMessages) {
504             messagingMessage.finalizeInflate();
505         }
506     }
507 
508     @Override
setImageResolver(ImageResolver resolver)509     public void setImageResolver(ImageResolver resolver) {
510         mImageResolver = resolver;
511     }
512 
513     /**
514      * @hide
515      */
setUnreadCount(int unreadCount)516     public void setUnreadCount(int unreadCount) {
517         mExpandButton.setNumber(unreadCount);
518     }
519 
addRemoteInputHistoryToMessages( List<Notification.MessagingStyle.Message> newMessages, RemoteInputHistoryItem[] remoteInputHistory)520     private void addRemoteInputHistoryToMessages(
521             List<Notification.MessagingStyle.Message> newMessages,
522             RemoteInputHistoryItem[] remoteInputHistory) {
523         if (remoteInputHistory == null || remoteInputHistory.length == 0) {
524             return;
525         }
526         for (int i = remoteInputHistory.length - 1; i >= 0; i--) {
527             RemoteInputHistoryItem historyMessage = remoteInputHistory[i];
528             Notification.MessagingStyle.Message message = new Notification.MessagingStyle.Message(
529                     historyMessage.getText(), 0, (Person) null, true /* remoteHistory */);
530             if (historyMessage.getUri() != null) {
531                 message.setData(historyMessage.getMimeType(), historyMessage.getUri());
532             }
533             newMessages.add(message);
534         }
535     }
536 
bind(MessagingData messagingData)537     private void bind(MessagingData messagingData) {
538         setUser(messagingData.getUser());
539         setUnreadCount(messagingData.getUnreadCount());
540 
541         // Copy our groups, before they get clobbered
542         ArrayList<MessagingGroup> oldGroups = new ArrayList<>(mGroups);
543 
544         // Let's now create the views and reorder them accordingly
545         //   side-effect: updates mGroups, mAddedGroups
546         createGroupViews(messagingData.getGroups(), messagingData.getSenders(),
547                 messagingData.getShowSpinner());
548 
549         // Let's first check which groups were removed altogether and remove them in one animation
550         removeGroups(oldGroups);
551 
552         // Let's remove the remaining messages
553         for (MessagingMessage message : mMessages) {
554             message.removeMessage(mToRecycle);
555         }
556         for (MessagingMessage historicMessage : mHistoricMessages) {
557             historicMessage.removeMessage(mToRecycle);
558         }
559 
560         mMessages = messagingData.getNewMessagingMessages();
561         mHistoricMessages = messagingData.getHistoricMessagingMessages();
562         updateHistoricMessageVisibility();
563         updateTitleAndNamesDisplay();
564 
565         updateConversationLayout(messagingData);
566 
567         // Recycle everything at the end of the update, now that we know it's no longer needed.
568         for (MessagingLinearLayout.MessagingChild child : mToRecycle) {
569             child.recycle();
570         }
571         mToRecycle.clear();
572     }
573 
574     /**
575      * Update the layout according to the data provided (i.e mIsOneToOne, expanded etc);
576      */
updateConversationLayout(MessagingData messagingData)577     private void updateConversationLayout(MessagingData messagingData) {
578         if (!Flags.conversationStyleSetAvatarAsync()) {
579             computeAndSetConversationAvatarAndName();
580         } else {
581             ConversationHeaderData conversationHeaderData =
582                     messagingData.getConversationHeaderData();
583             if (conversationHeaderData == null) {
584                 conversationHeaderData = loadConversationHeaderData(mIsOneToOne,
585                         mConversationTitle, mShortcutIcon, mLargeIcon, mMessages, mUser,
586                         messagingData.getGroups(),
587                         mLayoutColor);
588             }
589             setConversationAvatarAndNameFromData(conversationHeaderData);
590         }
591 
592         updateAppName();
593         updateIconPositionAndSize();
594         updateImageMessages();
595         updatePaddingsBasedOnContentAvailability();
596         updateActionListPadding();
597         updateAppNameDividerVisibility();
598     }
599 
600     @Deprecated
computeAndSetConversationAvatarAndName()601     private void computeAndSetConversationAvatarAndName() {
602         // Set avatar and name
603         CharSequence conversationText = mConversationTitle;
604         mConversationIcon = mShortcutIcon;
605         if (mIsOneToOne) {
606             // Let's resolve the icon / text from the last sender
607             CharSequence userKey = getKey(mUser);
608             for (int i = mGroups.size() - 1; i >= 0; i--) {
609                 MessagingGroup messagingGroup = mGroups.get(i);
610                 Person messageSender = messagingGroup.getSender();
611                 if ((messageSender != null && !TextUtils.equals(userKey, getKey(messageSender)))
612                         || i == 0) {
613                     if (TextUtils.isEmpty(conversationText)) {
614                         // We use the sendername as header text if no conversation title is provided
615                         // (This usually happens for most 1:1 conversations)
616                         conversationText = messagingGroup.getSenderName();
617                     }
618                     if (mConversationIcon == null) {
619                         Icon avatarIcon = messagingGroup.getAvatarIcon();
620                         if (avatarIcon == null) {
621                             avatarIcon = mPeopleHelper.createAvatarSymbol(conversationText, "",
622                                     mLayoutColor);
623                         }
624                         mConversationIcon = avatarIcon;
625                     }
626                     break;
627                 }
628             }
629         }
630         if (mConversationIcon == null) {
631             mConversationIcon = mLargeIcon;
632         }
633         if (mIsOneToOne || mConversationIcon != null) {
634             mConversationIconView.setVisibility(VISIBLE);
635             mConversationFacePile.setVisibility(GONE);
636             mConversationIconView.setImageIcon(mConversationIcon);
637         } else {
638             mConversationIconView.setVisibility(GONE);
639             // This will also inflate it!
640             mConversationFacePile.setVisibility(VISIBLE);
641             // rebind the value to the inflated view instead of the stub
642             mConversationFacePile = findViewById(R.id.conversation_face_pile);
643             bindFacePile();
644         }
645         if (TextUtils.isEmpty(conversationText)) {
646             conversationText = mIsOneToOne ? mFallbackChatName : mFallbackGroupChatName;
647         }
648         mConversationText.setText(conversationText);
649         // Update if the groups can hide the sender if they are first (applies to 1:1 conversations)
650         // This needs to happen after all of the above o update all of the groups
651         mPeopleHelper.maybeHideFirstSenderName(mGroups, mIsOneToOne, conversationText);
652     }
653 
setConversationAvatarAndNameFromData( ConversationHeaderData conversationHeaderData)654     private void setConversationAvatarAndNameFromData(
655             ConversationHeaderData conversationHeaderData) {
656         mConversationHeaderData = conversationHeaderData;
657         final OneToOneConversationAvatarData oneToOneConversationDrawable;
658         final GroupConversationAvatarData groupConversationAvatarData;
659         final ConversationAvatarData conversationAvatar =
660                 conversationHeaderData.getConversationAvatar();
661         if (conversationAvatar instanceof OneToOneConversationAvatarData) {
662             oneToOneConversationDrawable =
663                     ((OneToOneConversationAvatarData) conversationAvatar);
664             groupConversationAvatarData = null;
665         } else {
666             oneToOneConversationDrawable = null;
667             groupConversationAvatarData = ((GroupConversationAvatarData) conversationAvatar);
668         }
669 
670         if (oneToOneConversationDrawable != null) {
671             mConversationIconView.setVisibility(VISIBLE);
672             mConversationFacePile.setVisibility(GONE);
673             mConversationIconView.setImageDrawable(oneToOneConversationDrawable.mDrawable);
674         } else {
675             mConversationIconView.setVisibility(GONE);
676             // This will also inflate it!
677             mConversationFacePile.setVisibility(VISIBLE);
678             // rebind the value to the inflated view instead of the stub
679             mConversationFacePile = findViewById(R.id.conversation_face_pile);
680             bindFacePile(groupConversationAvatarData);
681         }
682         CharSequence conversationText = conversationHeaderData.getConversationText();
683         if (TextUtils.isEmpty(conversationText)) {
684             conversationText = mIsOneToOne ? mFallbackChatName : mFallbackGroupChatName;
685         }
686         mConversationText.setText(conversationText);
687         // Update if the groups can hide the sender if they are first (applies to 1:1 conversations)
688         // This needs to happen after all of the above o update all of the groups
689         mPeopleHelper.maybeHideFirstSenderName(mGroups, mIsOneToOne, conversationText);
690     }
691 
updateActionListPadding()692     private void updateActionListPadding() {
693         if (mActions != null) {
694             mActions.setCollapsibleIndentDimen(R.dimen.call_notification_collapsible_indent);
695         }
696     }
697 
updateImageMessages()698     private void updateImageMessages() {
699         View newMessage = null;
700         if (mIsCollapsed && mGroups.size() > 0) {
701 
702             // When collapsed, we're displaying the image message in a dedicated container
703             // on the right of the layout instead of inline. Let's add the isolated image there
704             MessagingGroup messagingGroup = mGroups.get(mGroups.size() - 1);
705             MessagingImageMessage isolatedMessage = messagingGroup.getIsolatedMessage();
706             if (isolatedMessage != null) {
707                 newMessage = isolatedMessage.getView();
708             }
709         }
710         // Remove all messages that don't belong into the image layout
711         View previousMessage = mImageMessageContainer.getChildAt(0);
712         if (previousMessage != newMessage) {
713             mImageMessageContainer.removeView(previousMessage);
714             if (newMessage != null) {
715                 mImageMessageContainer.addView(newMessage);
716             }
717         }
718         mImageMessageContainer.setVisibility(newMessage != null ? VISIBLE : GONE);
719     }
720 
bindFacePile(ImageView bottomBackground, ImageView bottomView, ImageView topView)721     public void bindFacePile(ImageView bottomBackground, ImageView bottomView, ImageView topView) {
722         applyNotificationBackgroundColor(bottomBackground);
723         // Let's find the two last conversations:
724         Icon secondLastIcon = null;
725         CharSequence lastKey = null;
726         Icon lastIcon = null;
727         CharSequence userKey = getKey(mUser);
728         for (int i = mGroups.size() - 1; i >= 0; i--) {
729             MessagingGroup messagingGroup = mGroups.get(i);
730             Person messageSender = messagingGroup.getSender();
731             boolean notUser = messageSender != null
732                     && !TextUtils.equals(userKey, getKey(messageSender));
733             boolean notIncluded = messageSender != null
734                     && !TextUtils.equals(lastKey, getKey(messageSender));
735             if ((notUser && notIncluded)
736                     || (i == 0 && lastKey == null)) {
737                 if (lastIcon == null) {
738                     lastIcon = messagingGroup.getAvatarIcon();
739                     lastKey = getKey(messageSender);
740                 } else {
741                     secondLastIcon = messagingGroup.getAvatarIcon();
742                     break;
743                 }
744             }
745         }
746         if (lastIcon == null) {
747             lastIcon = mPeopleHelper.createAvatarSymbol(" ", "", mLayoutColor);
748         }
749         bottomView.setImageIcon(lastIcon);
750         if (secondLastIcon == null) {
751             secondLastIcon = mPeopleHelper.createAvatarSymbol("", "", mLayoutColor);
752         }
753         topView.setImageIcon(secondLastIcon);
754     }
755 
756     @Deprecated
bindFacePile()757     private void bindFacePile() {
758         bindFacePile(null);
759     }
760 
bindFacePile(@ullable GroupConversationAvatarData groupConversationAvatarData)761     private void bindFacePile(@Nullable GroupConversationAvatarData groupConversationAvatarData) {
762         ImageView bottomBackground = mConversationFacePile.findViewById(
763                 R.id.conversation_face_pile_bottom_background);
764         ImageView bottomView = mConversationFacePile.findViewById(
765                 R.id.conversation_face_pile_bottom);
766         ImageView topView = mConversationFacePile.findViewById(
767                 R.id.conversation_face_pile_top);
768 
769         if (groupConversationAvatarData == null) {
770             bindFacePile(bottomBackground, bottomView, topView);
771         } else {
772             bindFacePileWithDrawable(bottomBackground, bottomView, topView,
773                     groupConversationAvatarData);
774 
775         }
776 
777         int conversationAvatarSize;
778         int facepileAvatarSize;
779         int facePileBackgroundSize;
780         if (mIsCollapsed) {
781             conversationAvatarSize = mConversationAvatarSize;
782             facepileAvatarSize = mFacePileAvatarSize;
783             facePileBackgroundSize = facepileAvatarSize + 2 * mFacePileProtectionWidth;
784         } else {
785             conversationAvatarSize = mConversationAvatarSizeExpanded;
786             facepileAvatarSize = mFacePileAvatarSizeExpandedGroup;
787             facePileBackgroundSize = facepileAvatarSize + 2 * mFacePileProtectionWidthExpanded;
788         }
789         LayoutParams layoutParams = (LayoutParams) mConversationFacePile.getLayoutParams();
790         layoutParams.width = conversationAvatarSize;
791         layoutParams.height = conversationAvatarSize;
792         mConversationFacePile.setLayoutParams(layoutParams);
793 
794         layoutParams = (LayoutParams) bottomView.getLayoutParams();
795         layoutParams.width = facepileAvatarSize;
796         layoutParams.height = facepileAvatarSize;
797         bottomView.setLayoutParams(layoutParams);
798 
799         layoutParams = (LayoutParams) topView.getLayoutParams();
800         layoutParams.width = facepileAvatarSize;
801         layoutParams.height = facepileAvatarSize;
802         topView.setLayoutParams(layoutParams);
803 
804         layoutParams = (LayoutParams) bottomBackground.getLayoutParams();
805         layoutParams.width = facePileBackgroundSize;
806         layoutParams.height = facePileBackgroundSize;
807         bottomBackground.setLayoutParams(layoutParams);
808     }
809 
810     /**
811      * Binds group avatar drawables to face pile.
812      */
bindFacePileWithDrawable(ImageView bottomBackground, ImageView bottomView, ImageView topView, GroupConversationAvatarData groupConversationAvatarData)813     public void bindFacePileWithDrawable(ImageView bottomBackground, ImageView bottomView,
814             ImageView topView, GroupConversationAvatarData groupConversationAvatarData) {
815         applyNotificationBackgroundColor(bottomBackground);
816         bottomView.setImageDrawable(groupConversationAvatarData.mLastIcon);
817         topView.setImageDrawable(groupConversationAvatarData.mSecondLastIcon);
818     }
819 
updateAppName()820     private void updateAppName() {
821         mAppName.setVisibility(mIsCollapsed ? GONE : VISIBLE);
822     }
823 
shouldHideAppName()824     public boolean shouldHideAppName() {
825         return mIsCollapsed;
826     }
827 
828     /**
829      * update the icon position and sizing
830      */
updateIconPositionAndSize()831     private void updateIconPositionAndSize() {
832         int badgeProtrusion;
833         int conversationAvatarSize;
834         if (mIsOneToOne || mIsCollapsed) {
835             badgeProtrusion = mBadgeProtrusion;
836             conversationAvatarSize = mConversationAvatarSize;
837         } else {
838             badgeProtrusion = mConversationFacePile.getVisibility() == VISIBLE
839                     ? mExpandedGroupBadgeProtrusionFacePile
840                     : mExpandedGroupBadgeProtrusion;
841             conversationAvatarSize = mConversationAvatarSizeExpanded;
842         }
843 
844         if (mConversationIconView.getVisibility() == VISIBLE) {
845             LayoutParams layoutParams = (LayoutParams) mConversationIconView.getLayoutParams();
846             layoutParams.width = conversationAvatarSize;
847             layoutParams.height = conversationAvatarSize;
848             layoutParams.leftMargin = badgeProtrusion;
849             layoutParams.rightMargin = badgeProtrusion;
850             layoutParams.bottomMargin = badgeProtrusion;
851             mConversationIconView.setLayoutParams(layoutParams);
852         }
853 
854         if (mConversationFacePile.getVisibility() == VISIBLE) {
855             LayoutParams layoutParams = (LayoutParams) mConversationFacePile.getLayoutParams();
856             layoutParams.leftMargin = badgeProtrusion;
857             layoutParams.rightMargin = badgeProtrusion;
858             layoutParams.bottomMargin = badgeProtrusion;
859             mConversationFacePile.setLayoutParams(layoutParams);
860         }
861     }
862 
updatePaddingsBasedOnContentAvailability()863     private void updatePaddingsBasedOnContentAvailability() {
864         // groups have avatars that need more spacing
865         mMessagingLinearLayout.setSpacing(
866                 mIsOneToOne ? mMessageSpacingStandard : mMessageSpacingGroup);
867 
868         int messagingPadding = mIsOneToOne || mIsCollapsed
869                 ? 0
870                 // Add some extra padding to the messages, since otherwise it will overlap with the
871                 // group
872                 : mExpandedGroupMessagePadding;
873 
874         int iconPadding = mIsOneToOne || mIsCollapsed
875                 ? mConversationIconTopPadding
876                 : mConversationIconTopPaddingExpandedGroup;
877 
878         mConversationIconContainer.setPaddingRelative(
879                 mConversationIconContainer.getPaddingStart(),
880                 iconPadding,
881                 mConversationIconContainer.getPaddingEnd(),
882                 mConversationIconContainer.getPaddingBottom());
883 
884         mMessagingLinearLayout.setPaddingRelative(
885                 mMessagingLinearLayout.getPaddingStart(),
886                 messagingPadding,
887                 mMessagingLinearLayout.getPaddingEnd(),
888                 mMessagingLinearLayout.getPaddingBottom());
889     }
890 
891     /**
892      * async version of {@link ConversationLayout#setLargeIcon}
893      */
894     @RemotableViewMethod
setLargeIconAsync(Icon largeIcon)895     public Runnable setLargeIconAsync(Icon largeIcon) {
896         if (!Flags.conversationStyleSetAvatarAsync()) {
897             return () -> setLargeIcon(largeIcon);
898         }
899 
900         mLargeIcon = largeIcon;
901         return NotificationRunnables.NOOP;
902     }
903 
904     @RemotableViewMethod(asyncImpl = "setLargeIconAsync")
setLargeIcon(Icon largeIcon)905     public void setLargeIcon(Icon largeIcon) {
906         mLargeIcon = largeIcon;
907     }
908 
909     /**
910      * async version of {@link ConversationLayout#setShortcutIcon}
911      */
912     @RemotableViewMethod
setShortcutIconAsync(Icon shortcutIcon)913     public Runnable setShortcutIconAsync(Icon shortcutIcon) {
914         if (!Flags.conversationStyleSetAvatarAsync()) {
915             return () -> setShortcutIcon(shortcutIcon);
916         }
917 
918         mShortcutIcon = shortcutIcon;
919         return NotificationRunnables.NOOP;
920     }
921 
922     @RemotableViewMethod(asyncImpl = "setShortcutIconAsync")
setShortcutIcon(Icon shortcutIcon)923     public void setShortcutIcon(Icon shortcutIcon) {
924         mShortcutIcon = shortcutIcon;
925     }
926 
927     /**
928      * async version of {@link ConversationLayout#setConversationTitle}
929      */
930     @RemotableViewMethod
setConversationTitleAsync(CharSequence conversationTitle)931     public Runnable setConversationTitleAsync(CharSequence conversationTitle) {
932         if (!Flags.conversationStyleSetAvatarAsync()) {
933             return () -> setConversationTitle(conversationTitle);
934         }
935 
936         // Remove formatting from the title.
937         mConversationTitle = conversationTitle != null ? conversationTitle.toString() : null;
938         return NotificationRunnables.NOOP;
939     }
940 
941     /**
942      * Sets the conversation title of this conversation.
943      *
944      * @param conversationTitle the conversation title
945      */
946     @RemotableViewMethod(asyncImpl = "setConversationTitleAsync")
setConversationTitle(CharSequence conversationTitle)947     public void setConversationTitle(CharSequence conversationTitle) {
948         // Remove formatting from the title.
949         mConversationTitle = conversationTitle != null ? conversationTitle.toString() : null;
950     }
951 
952     // TODO (b/217799515) getConversationTitle is not consistent with setConversationTitle
953     //  if you call getConversationTitle() immediately after setConversationTitle(), the result
954     //  will not correctly reflect the new change without calling updateConversationLayout, for
955     //  example.
getConversationTitle()956     public CharSequence getConversationTitle() {
957         return mConversationText.getText();
958     }
959 
removeGroups(ArrayList<MessagingGroup> oldGroups)960     private void removeGroups(ArrayList<MessagingGroup> oldGroups) {
961         int size = oldGroups.size();
962         for (int i = 0; i < size; i++) {
963             MessagingGroup group = oldGroups.get(i);
964             if (!mGroups.contains(group)) {
965                 List<MessagingMessage> messages = group.getMessages();
966                 boolean wasShown = group.isShown();
967                 mMessagingLinearLayout.removeView(group);
968                 if (wasShown && !MessagingLinearLayout.isGone(group)) {
969                     mMessagingLinearLayout.addTransientView(group, 0);
970                     group.removeGroupAnimated(() -> {
971                         mMessagingLinearLayout.removeTransientView(group);
972                         group.recycle();
973                     });
974                 } else {
975                     // Defer recycling until after the update is done, since we may still need the
976                     // old group around to perform other updates.
977                     mToRecycle.add(group);
978                 }
979                 mMessages.removeAll(messages);
980                 mHistoricMessages.removeAll(messages);
981             }
982         }
983     }
984 
updateTitleAndNamesDisplay()985     private void updateTitleAndNamesDisplay() {
986         // Map of unique names to their prefix
987         Map<CharSequence, String> uniqueNames = mPeopleHelper.mapUniqueNamesToPrefix(mGroups);
988 
989         // Now that we have the correct symbols, let's look what we have cached
990         ArrayMap<CharSequence, Icon> cachedAvatars = new ArrayMap<>();
991         for (int i = 0; i < mGroups.size(); i++) {
992             // Let's now set the avatars
993             MessagingGroup group = mGroups.get(i);
994             boolean isOwnMessage = group.getSender() == mUser;
995             CharSequence senderName = group.getSenderName();
996             if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)
997                     || (mIsOneToOne && mAvatarReplacement != null && !isOwnMessage)) {
998                 continue;
999             }
1000             String symbol = uniqueNames.get(senderName);
1001             Icon cachedIcon = group.getAvatarSymbolIfMatching(senderName,
1002                     symbol, mLayoutColor);
1003             if (cachedIcon != null) {
1004                 cachedAvatars.put(senderName, cachedIcon);
1005             }
1006         }
1007 
1008         for (int i = 0; i < mGroups.size(); i++) {
1009             // Let's now set the avatars
1010             MessagingGroup group = mGroups.get(i);
1011             CharSequence senderName = group.getSenderName();
1012             if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)) {
1013                 continue;
1014             }
1015             if (mIsOneToOne && mAvatarReplacement != null && group.getSender() != mUser) {
1016                 group.setAvatar(mAvatarReplacement);
1017             } else {
1018                 Icon cachedIcon = cachedAvatars.get(senderName);
1019                 if (cachedIcon == null) {
1020                     cachedIcon = mPeopleHelper.createAvatarSymbol(senderName,
1021                             uniqueNames.get(senderName), mLayoutColor);
1022                     cachedAvatars.put(senderName, cachedIcon);
1023                 }
1024                 group.setCreatedAvatar(cachedIcon, senderName, uniqueNames.get(senderName),
1025                         mLayoutColor);
1026             }
1027         }
1028     }
1029 
1030     /**
1031      * async version of {@link ConversationLayout#setLayoutColor}
1032      */
1033     @RemotableViewMethod
setLayoutColorAsync(int color)1034     public Runnable setLayoutColorAsync(int color) {
1035         if (!Flags.conversationStyleSetAvatarAsync()) {
1036             return () -> setLayoutColor(color);
1037         }
1038 
1039         mLayoutColor = color;
1040         return NotificationRunnables.NOOP;
1041     }
1042 
1043     @RemotableViewMethod(asyncImpl = "setLayoutColorAsync")
setLayoutColor(int color)1044     public void setLayoutColor(int color) {
1045         mLayoutColor = color;
1046     }
1047 
1048     /**
1049      * async version of {@link ConversationLayout#setIsOneToOne}
1050      */
1051     @RemotableViewMethod
setIsOneToOneAsync(boolean oneToOne)1052     public Runnable setIsOneToOneAsync(boolean oneToOne) {
1053         if (!Flags.conversationStyleSetAvatarAsync()) {
1054             return () -> setIsOneToOne(oneToOne);
1055         }
1056         mIsOneToOne = oneToOne;
1057         return NotificationRunnables.NOOP;
1058     }
1059 
1060     @RemotableViewMethod(asyncImpl = "setIsOneToOneAsync")
setIsOneToOne(boolean oneToOne)1061     public void setIsOneToOne(boolean oneToOne) {
1062         mIsOneToOne = oneToOne;
1063     }
1064 
1065     @RemotableViewMethod
setSenderTextColor(int color)1066     public void setSenderTextColor(int color) {
1067         mSenderTextColor = color;
1068         mConversationText.setTextColor(color);
1069     }
1070 
1071     /**
1072      * @param color the color of the notification background
1073      */
1074     @RemotableViewMethod
setNotificationBackgroundColor(int color)1075     public void setNotificationBackgroundColor(int color) {
1076         mNotificationBackgroundColor = color;
1077         applyNotificationBackgroundColor(mConversationIconBadgeBg);
1078     }
1079 
applyNotificationBackgroundColor(ImageView view)1080     private void applyNotificationBackgroundColor(ImageView view) {
1081         view.setImageTintList(ColorStateList.valueOf(mNotificationBackgroundColor));
1082     }
1083 
1084     @RemotableViewMethod
setMessageTextColor(int color)1085     public void setMessageTextColor(int color) {
1086         mMessageTextColor = color;
1087     }
1088 
setUser(Person user)1089     private void setUser(Person user) {
1090         mUser = user;
1091         if (mUser.getIcon() == null) {
1092             Icon userIcon = Icon.createWithResource(getContext(),
1093                     R.drawable.messaging_user);
1094             userIcon.setTint(mLayoutColor);
1095             mUser = mUser.toBuilder().setIcon(userIcon).build();
1096         }
1097     }
1098 
createGroupViews(List<List<MessagingMessage>> groups, List<Person> senders, boolean showSpinner)1099     private void createGroupViews(List<List<MessagingMessage>> groups,
1100             List<Person> senders, boolean showSpinner) {
1101         mGroups.clear();
1102         for (int groupIndex = 0; groupIndex < groups.size(); groupIndex++) {
1103             List<MessagingMessage> group = groups.get(groupIndex);
1104             MessagingGroup newGroup = null;
1105             // we'll just take the first group that exists or create one there is none
1106             for (int messageIndex = group.size() - 1; messageIndex >= 0; messageIndex--) {
1107                 MessagingMessage message = group.get(messageIndex);
1108                 newGroup = message.getGroup();
1109                 if (newGroup != null) {
1110                     break;
1111                 }
1112             }
1113             // Create a new group, adding it to the linear layout as well
1114             if (newGroup == null) {
1115                 newGroup = MessagingGroup.createGroup(mMessagingLinearLayout);
1116                 mAddedGroups.add(newGroup);
1117             } else if (newGroup.getParent() != mMessagingLinearLayout) {
1118                 throw new IllegalStateException(
1119                         "group parent was " + newGroup.getParent() + " but expected "
1120                                 + mMessagingLinearLayout);
1121             }
1122             newGroup.setImageDisplayLocation(mIsCollapsed
1123                     ? IMAGE_DISPLAY_LOCATION_EXTERNAL
1124                     : IMAGE_DISPLAY_LOCATION_INLINE);
1125             newGroup.setIsInConversation(true);
1126             newGroup.setLayoutColor(mLayoutColor);
1127             newGroup.setTextColors(mSenderTextColor, mMessageTextColor);
1128             Person sender = senders.get(groupIndex);
1129             CharSequence nameOverride = null;
1130             if (sender != mUser && mNameReplacement != null) {
1131                 nameOverride = mNameReplacement;
1132             }
1133             newGroup.setShowingAvatar(!mIsOneToOne && !mIsCollapsed);
1134             newGroup.setSingleLine(mIsCollapsed);
1135             newGroup.setSender(sender, nameOverride);
1136             newGroup.setSending(groupIndex == (groups.size() - 1) && showSpinner);
1137             mGroups.add(newGroup);
1138 
1139             // Reposition to the correct place (if we're re-using a group)
1140             if (mMessagingLinearLayout.indexOfChild(newGroup) != groupIndex) {
1141                 mMessagingLinearLayout.removeView(newGroup);
1142                 mMessagingLinearLayout.addView(newGroup, groupIndex);
1143             }
1144             newGroup.setMessages(group);
1145         }
1146     }
1147 
1148     /**
1149      * Finds groups and senders from the given messaging messages and fills outGroups and outSenders
1150      */
findGroups(List<MessagingMessage> historicMessages, List<MessagingMessage> messages, Person user, List<List<MessagingMessage>> outGroups, List<Person> outSenders)1151     private void findGroups(List<MessagingMessage> historicMessages,
1152             List<MessagingMessage> messages, Person user, List<List<MessagingMessage>> outGroups,
1153             List<Person> outSenders) {
1154         CharSequence currentSenderKey = null;
1155         List<MessagingMessage> currentGroup = null;
1156         int histSize = historicMessages.size();
1157         for (int i = 0; i < histSize + messages.size(); i++) {
1158             MessagingMessage message;
1159             if (i < histSize) {
1160                 message = historicMessages.get(i);
1161             } else {
1162                 message = messages.get(i - histSize);
1163             }
1164             boolean isNewGroup = currentGroup == null;
1165             Person sender =
1166                     message.getMessage() == null ? null : message.getMessage().getSenderPerson();
1167             CharSequence key = getKey(sender);
1168             isNewGroup |= !TextUtils.equals(key, currentSenderKey);
1169             if (isNewGroup) {
1170                 currentGroup = new ArrayList<>();
1171                 outGroups.add(currentGroup);
1172                 if (sender == null) {
1173                     sender = user;
1174                 } else {
1175                     // Remove all formatting from the sender name
1176                     sender = sender.toBuilder().setName(Objects.toString(sender.getName())).build();
1177                 }
1178                 outSenders.add(sender);
1179                 currentSenderKey = key;
1180             }
1181             currentGroup.add(message);
1182         }
1183     }
1184 
getKey(Person person)1185     private CharSequence getKey(Person person) {
1186         return person == null ? null : person.getKey() == null ? person.getName() : person.getKey();
1187     }
1188 
loadConversationHeaderData(boolean isOneToOne, CharSequence conversationTitle, Icon shortcutIcon, Icon largeIcon, List<MessagingMessage> messages, Person user, List<List<MessagingMessage>> groups, int layoutColor)1189     private ConversationHeaderData loadConversationHeaderData(boolean isOneToOne,
1190             CharSequence conversationTitle, Icon shortcutIcon, Icon largeIcon,
1191             List<MessagingMessage> messages,
1192             Person user,
1193             List<List<MessagingMessage>> groups, int layoutColor) {
1194         Icon conversationIcon = shortcutIcon;
1195         CharSequence conversationText = conversationTitle;
1196         final CharSequence userKey = getKey(user);
1197         if (isOneToOne) {
1198             for (int i = messages.size() - 1; i >= 0; i--) {
1199                 final Notification.MessagingStyle.Message message = messages.get(i).getMessage();
1200                 final Person sender = message.getSenderPerson();
1201                 final CharSequence senderKey = getKey(sender);
1202                 if ((sender != null && senderKey != userKey) || i == 0) {
1203                     if (conversationText == null || conversationText.length() == 0) {
1204                         conversationText = sender != null ? sender.getName() : "";
1205                     }
1206                     if (conversationIcon == null) {
1207                         conversationIcon = sender != null ? sender.getIcon()
1208                                 : mPeopleHelper.createAvatarSymbol(conversationText, "",
1209                                         layoutColor);
1210                     }
1211                     break;
1212                 }
1213             }
1214         }
1215         if (android.app.Flags.cleanUpSpansAndNewLines() && conversationText != null) {
1216             // remove formatting from title.
1217             conversationText = conversationText.toString();
1218         }
1219 
1220         if (conversationIcon == null) {
1221             conversationIcon = largeIcon;
1222         }
1223 
1224         if (isOneToOne || conversationIcon != null) {
1225             return new ConversationHeaderData(
1226                     conversationText,
1227                     new OneToOneConversationAvatarData(
1228                             resolveAvatarImageForOneToOne(conversationIcon)));
1229         }
1230 
1231         final List<List<Notification.MessagingStyle.Message>> groupMessages = new ArrayList<>();
1232         for (int i = 0; i < groups.size(); i++) {
1233             final List<Notification.MessagingStyle.Message> groupMessage = new ArrayList<>();
1234             for (int j = 0; j < groups.get(i).size(); j++) {
1235                 groupMessage.add(groups.get(i).get(j).getMessage());
1236             }
1237             groupMessages.add(groupMessage);
1238         }
1239 
1240         final PeopleHelper.NameToPrefixMap nameToPrefixMap =
1241                 mPeopleHelper.mapUniqueNamesToPrefixWithGroupList(groupMessages);
1242 
1243         Icon lastIcon = null;
1244         Icon secondLastIcon = null;
1245 
1246         CharSequence lastKey = null;
1247 
1248         for (int i = groups.size() - 1; i >= 0; i--) {
1249             final Notification.MessagingStyle.Message message = groups.get(i).get(0).getMessage();
1250             final Person sender =
1251                     message.getSenderPerson() != null ? message.getSenderPerson() : user;
1252             final CharSequence senderKey = getKey(sender);
1253             final boolean notUser = senderKey != userKey;
1254             final boolean notIncluded = senderKey != lastKey;
1255 
1256             if ((notUser && notIncluded) || (i == 0 && lastKey == null)) {
1257                 if (lastIcon == null) {
1258                     if (sender.getIcon() != null) {
1259                         lastIcon = sender.getIcon();
1260                     } else {
1261                         final CharSequence senderName =
1262                                 sender.getName() != null ? sender.getName() : "";
1263                         lastIcon = mPeopleHelper.createAvatarSymbol(
1264                                 senderName, nameToPrefixMap.getPrefix(senderName),
1265                                 layoutColor);
1266                     }
1267                     lastKey = senderKey;
1268                 } else {
1269                     if (sender.getIcon() != null) {
1270                         secondLastIcon = sender.getIcon();
1271                     } else {
1272                         final CharSequence senderName =
1273                                 sender.getName() != null ? sender.getName() : "";
1274                         secondLastIcon = mPeopleHelper.createAvatarSymbol(
1275                                 senderName, nameToPrefixMap.getPrefix(senderName),
1276                                 layoutColor);
1277                     }
1278                     break;
1279                 }
1280             }
1281         }
1282 
1283         if (lastIcon == null) {
1284             lastIcon = mPeopleHelper.createAvatarSymbol(
1285                     "", "", layoutColor);
1286         }
1287 
1288         if (secondLastIcon == null) {
1289             secondLastIcon = mPeopleHelper.createAvatarSymbol(
1290                     "", "", layoutColor);
1291         }
1292 
1293         return new ConversationHeaderData(
1294                 conversationText,
1295                 new GroupConversationAvatarData(resolveAvatarImageForFacePile(lastIcon),
1296                         resolveAvatarImageForFacePile(secondLastIcon)));
1297     }
1298 
1299     /**
1300      * One To One Conversation Avatars is loaded by CachingIconView(conversation icon view).
1301      */
1302     @Nullable
resolveAvatarImageForOneToOne(Icon conversationIcon)1303     private Drawable resolveAvatarImageForOneToOne(Icon conversationIcon) {
1304         final Drawable conversationIconDrawable =
1305                 tryLoadingSizeRestrictedIconForOneToOne(conversationIcon);
1306         if (conversationIconDrawable != null) {
1307             return conversationIconDrawable;
1308         }
1309         // when size restricted icon loading fails, we fallback to icons load drawable.
1310         return loadDrawableFromIcon(conversationIcon);
1311     }
1312 
1313     @Nullable
tryLoadingSizeRestrictedIconForOneToOne(Icon conversationIcon)1314     private Drawable tryLoadingSizeRestrictedIconForOneToOne(Icon conversationIcon) {
1315         try {
1316             return mConversationIconView.loadSizeRestrictedIcon(conversationIcon);
1317         } catch (Exception ex) {
1318             return null;
1319         }
1320     }
1321 
1322     /**
1323      * Group Avatar drawables are loaded by Icon.
1324      */
1325     @Nullable
resolveAvatarImageForFacePile(Icon conversationIcon)1326     private Drawable resolveAvatarImageForFacePile(Icon conversationIcon) {
1327         return loadDrawableFromIcon(conversationIcon);
1328     }
1329 
1330     @Nullable
loadDrawableFromIcon(Icon conversationIcon)1331     private Drawable loadDrawableFromIcon(Icon conversationIcon) {
1332         try {
1333             return conversationIcon.loadDrawable(getContext());
1334         } catch (Exception ex) {
1335             return null;
1336         }
1337     }
1338 
1339     /**
1340      * Creates new messages, reusing existing ones if they are available.
1341      *
1342      * @param newMessages the messages to parse.
1343      */
createMessages( List<Notification.MessagingStyle.Message> newMessages, boolean isHistoric, boolean usePrecomputedText)1344     private List<MessagingMessage> createMessages(
1345             List<Notification.MessagingStyle.Message> newMessages, boolean isHistoric,
1346             boolean usePrecomputedText) {
1347         List<MessagingMessage> result = new ArrayList<>();
1348         for (int i = 0; i < newMessages.size(); i++) {
1349             Notification.MessagingStyle.Message m = newMessages.get(i);
1350             MessagingMessage message = findAndRemoveMatchingMessage(m);
1351             if (message == null) {
1352                 message = MessagingMessage.createMessage(this, m,
1353                         mImageResolver, usePrecomputedText);
1354             }
1355             message.setIsHistoric(isHistoric);
1356             result.add(message);
1357         }
1358         return result;
1359     }
1360 
findAndRemoveMatchingMessage(Notification.MessagingStyle.Message m)1361     private MessagingMessage findAndRemoveMatchingMessage(Notification.MessagingStyle.Message m) {
1362         for (int i = 0; i < mMessages.size(); i++) {
1363             MessagingMessage existing = mMessages.get(i);
1364             if (existing.sameAs(m)) {
1365                 mMessages.remove(i);
1366                 return existing;
1367             }
1368         }
1369         for (int i = 0; i < mHistoricMessages.size(); i++) {
1370             MessagingMessage existing = mHistoricMessages.get(i);
1371             if (existing.sameAs(m)) {
1372                 mHistoricMessages.remove(i);
1373                 return existing;
1374             }
1375         }
1376         return null;
1377     }
1378 
showHistoricMessages(boolean show)1379     public void showHistoricMessages(boolean show) {
1380         mShowHistoricMessages = show;
1381         updateHistoricMessageVisibility();
1382     }
1383 
updateHistoricMessageVisibility()1384     private void updateHistoricMessageVisibility() {
1385         int numHistoric = mHistoricMessages.size();
1386         for (int i = 0; i < numHistoric; i++) {
1387             MessagingMessage existing = mHistoricMessages.get(i);
1388             existing.setVisibility(mShowHistoricMessages ? VISIBLE : GONE);
1389         }
1390         int numGroups = mGroups.size();
1391         for (int i = 0; i < numGroups; i++) {
1392             MessagingGroup group = mGroups.get(i);
1393             int visibleChildren = 0;
1394             List<MessagingMessage> messages = group.getMessages();
1395             int numGroupMessages = messages.size();
1396             for (int j = 0; j < numGroupMessages; j++) {
1397                 MessagingMessage message = messages.get(j);
1398                 if (message.getVisibility() != GONE) {
1399                     visibleChildren++;
1400                 }
1401             }
1402             if (visibleChildren > 0 && group.getVisibility() == GONE) {
1403                 group.setVisibility(VISIBLE);
1404             } else if (visibleChildren == 0 && group.getVisibility() != GONE) {
1405                 group.setVisibility(GONE);
1406             }
1407         }
1408     }
1409 
1410     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)1411     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
1412         super.onLayout(changed, left, top, right, bottom);
1413         if (!mAddedGroups.isEmpty()) {
1414             getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
1415                 @Override
1416                 public boolean onPreDraw() {
1417                     for (MessagingGroup group : mAddedGroups) {
1418                         if (!group.isShown()) {
1419                             continue;
1420                         }
1421                         MessagingPropertyAnimator.fadeIn(group.getAvatar());
1422                         MessagingPropertyAnimator.fadeIn(group.getSenderView());
1423                         MessagingPropertyAnimator.startLocalTranslationFrom(group,
1424                                 group.getHeight(), LINEAR_OUT_SLOW_IN);
1425                     }
1426                     mAddedGroups.clear();
1427                     getViewTreeObserver().removeOnPreDrawListener(this);
1428                     return true;
1429                 }
1430             });
1431         }
1432         mTouchDelegate.clear();
1433         if (mFeedbackIcon.getVisibility() == VISIBLE) {
1434             float width = Math.max(mMinTouchSize, mFeedbackIcon.getWidth());
1435             float height = Math.max(mMinTouchSize, mFeedbackIcon.getHeight());
1436             final Rect feedbackTouchRect = new Rect();
1437             feedbackTouchRect.left = (int) ((mFeedbackIcon.getLeft() + mFeedbackIcon.getRight())
1438                     / 2.0f - width / 2.0f);
1439             feedbackTouchRect.top = (int) ((mFeedbackIcon.getTop() + mFeedbackIcon.getBottom())
1440                     / 2.0f - height / 2.0f);
1441             feedbackTouchRect.bottom = (int) (feedbackTouchRect.top + height);
1442             feedbackTouchRect.right = (int) (feedbackTouchRect.left + width);
1443 
1444             getRelativeTouchRect(feedbackTouchRect, mFeedbackIcon);
1445             mTouchDelegate.add(new TouchDelegate(feedbackTouchRect, mFeedbackIcon));
1446         }
1447 
1448         setTouchDelegate(mTouchDelegate);
1449     }
1450 
getRelativeTouchRect(Rect touchRect, View view)1451     private void getRelativeTouchRect(Rect touchRect, View view) {
1452         ViewGroup viewGroup = (ViewGroup) view.getParent();
1453         while (viewGroup != this) {
1454             touchRect.offset(viewGroup.getLeft(), viewGroup.getTop());
1455             viewGroup = (ViewGroup) viewGroup.getParent();
1456         }
1457     }
1458 
getMessagingLinearLayout()1459     public MessagingLinearLayout getMessagingLinearLayout() {
1460         return mMessagingLinearLayout;
1461     }
1462 
getImageMessageContainer()1463     public @NonNull ViewGroup getImageMessageContainer() {
1464         return mImageMessageContainer;
1465     }
1466 
getMessagingGroups()1467     public ArrayList<MessagingGroup> getMessagingGroups() {
1468         return mGroups;
1469     }
1470 
updateExpandButton()1471     private void updateExpandButton() {
1472         int buttonGravity;
1473         ViewGroup newContainer;
1474         if (mIsCollapsed) {
1475             buttonGravity = Gravity.CENTER;
1476             // NOTE(b/182474419): In order for the touch target of the expand button to be the full
1477             // height of the notification, we would want the mExpandButtonContainer's height to be
1478             // set to WRAP_CONTENT (or 88dp) when in the collapsed state.  Unfortunately, that
1479             // causes an unstable remeasuring infinite loop when the unread count is visible,
1480             // causing the layout to occasionally hide the messages.  As an aside, that naive
1481             // solution also causes an undesirably large gap between content and smart replies.
1482             newContainer = mExpandButtonAndContentContainer;
1483         } else {
1484             buttonGravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
1485             newContainer = mExpandButtonContainerA11yContainer;
1486         }
1487         mExpandButton.setExpanded(!mIsCollapsed);
1488 
1489         // We need to make sure that the expand button is in the linearlayout pushing over the
1490         // content when collapsed, but allows the content to flow under it when expanded.
1491         if (newContainer != mExpandButtonContainer.getParent()) {
1492             ((ViewGroup) mExpandButtonContainer.getParent()).removeView(mExpandButtonContainer);
1493             newContainer.addView(mExpandButtonContainer);
1494         }
1495 
1496         // update if the expand button is centered
1497         LinearLayout.LayoutParams layoutParams =
1498                 (LinearLayout.LayoutParams) mExpandButton.getLayoutParams();
1499         layoutParams.gravity = buttonGravity;
1500         mExpandButton.setLayoutParams(layoutParams);
1501     }
1502 
updateContentEndPaddings()1503     private void updateContentEndPaddings() {
1504         // Let's make sure the conversation header can't run into the expand button when we're
1505         // collapsed and update the paddings of the content
1506         int headerPaddingEnd;
1507         int contentPaddingEnd;
1508         if (!mExpandable) {
1509             headerPaddingEnd = 0;
1510             contentPaddingEnd = mContentMarginEnd;
1511         } else if (mIsCollapsed) {
1512             headerPaddingEnd = 0;
1513             contentPaddingEnd = 0;
1514         } else {
1515             headerPaddingEnd = mNotificationHeaderExpandedPadding;
1516             contentPaddingEnd = mContentMarginEnd;
1517         }
1518         mConversationHeader.setPaddingRelative(
1519                 mConversationHeader.getPaddingStart(),
1520                 mConversationHeader.getPaddingTop(),
1521                 headerPaddingEnd,
1522                 mConversationHeader.getPaddingBottom());
1523 
1524         mContentContainer.setPaddingRelative(
1525                 mContentContainer.getPaddingStart(),
1526                 mContentContainer.getPaddingTop(),
1527                 contentPaddingEnd,
1528                 mContentContainer.getPaddingBottom());
1529     }
1530 
onAppNameVisibilityChanged()1531     private void onAppNameVisibilityChanged() {
1532         boolean appNameGone = mAppName.getVisibility() == GONE;
1533         if (appNameGone != mAppNameGone) {
1534             mAppNameGone = appNameGone;
1535             updateAppNameDividerVisibility();
1536         }
1537     }
1538 
updateAppNameDividerVisibility()1539     private void updateAppNameDividerVisibility() {
1540         mAppNameDivider.setVisibility(mAppNameGone ? GONE : VISIBLE);
1541     }
1542 
updateExpandability(boolean expandable, @Nullable OnClickListener onClickListener)1543     public void updateExpandability(boolean expandable, @Nullable OnClickListener onClickListener) {
1544         mExpandable = expandable;
1545         if (expandable) {
1546             mExpandButtonContainer.setVisibility(VISIBLE);
1547             mExpandButton.setOnClickListener(onClickListener);
1548             mConversationIconContainer.setOnClickListener(onClickListener);
1549         } else {
1550             mExpandButtonContainer.setVisibility(GONE);
1551             mConversationIconContainer.setOnClickListener(null);
1552         }
1553         mExpandButton.setVisibility(VISIBLE);
1554         updateContentEndPaddings();
1555     }
1556 
1557     @Override
setMessagingClippingDisabled(boolean clippingDisabled)1558     public void setMessagingClippingDisabled(boolean clippingDisabled) {
1559         mMessagingLinearLayout.setClipBounds(clippingDisabled ? null : mMessagingClipRect);
1560     }
1561 
1562     @Nullable
getConversationSenderName()1563     public CharSequence getConversationSenderName() {
1564         if (mGroups.isEmpty()) {
1565             return null;
1566         }
1567         final CharSequence name = mGroups.get(mGroups.size() - 1).getSenderName();
1568         return getResources().getString(R.string.conversation_single_line_name_display, name);
1569     }
1570 
isOneToOne()1571     public boolean isOneToOne() {
1572         return mIsOneToOne;
1573     }
1574 
1575     @Nullable
getConversationText()1576     public CharSequence getConversationText() {
1577         if (mMessages.isEmpty()) {
1578             return null;
1579         }
1580         final MessagingMessage messagingMessage = mMessages.get(mMessages.size() - 1);
1581         final CharSequence text = messagingMessage.getMessage() == null ? null
1582                 : messagingMessage.getMessage().getText();
1583         if (text == null && messagingMessage instanceof MessagingImageMessage) {
1584             final String unformatted =
1585                     getResources().getString(R.string.conversation_single_line_image_placeholder);
1586             SpannableString spannableString = new SpannableString(unformatted);
1587             spannableString.setSpan(
1588                     new StyleSpan(Typeface.ITALIC),
1589                     0,
1590                     spannableString.length(),
1591                     Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
1592             return spannableString;
1593         }
1594         return text;
1595     }
1596 
1597     @Nullable
getConversationIcon()1598     public Icon getConversationIcon() {
1599         return mConversationIcon;
1600     }
1601 
1602     @Nullable
getConversationHeaderData()1603     public ConversationHeaderData getConversationHeaderData() {
1604         return mConversationHeaderData;
1605     }
1606 
1607     private static class TouchDelegateComposite extends TouchDelegate {
1608         private final ArrayList<TouchDelegate> mDelegates = new ArrayList<>();
1609 
TouchDelegateComposite(View view)1610         private TouchDelegateComposite(View view) {
1611             super(new Rect(), view);
1612         }
1613 
add(TouchDelegate delegate)1614         public void add(TouchDelegate delegate) {
1615             mDelegates.add(delegate);
1616         }
1617 
clear()1618         public void clear() {
1619             mDelegates.clear();
1620         }
1621 
1622         @Override
onTouchEvent(MotionEvent event)1623         public boolean onTouchEvent(MotionEvent event) {
1624             float x = event.getX();
1625             float y = event.getY();
1626             for (TouchDelegate delegate : mDelegates) {
1627                 event.setLocation(x, y);
1628                 if (delegate.onTouchEvent(event)) {
1629                     return true;
1630                 }
1631             }
1632             return false;
1633         }
1634     }
1635 }
1636