• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.GradientDrawable;
38 import android.graphics.drawable.Icon;
39 import android.os.Bundle;
40 import android.os.Parcelable;
41 import android.text.Spannable;
42 import android.text.SpannableString;
43 import android.text.TextUtils;
44 import android.text.style.StyleSpan;
45 import android.util.ArrayMap;
46 import android.util.AttributeSet;
47 import android.util.DisplayMetrics;
48 import android.view.Gravity;
49 import android.view.MotionEvent;
50 import android.view.RemotableViewMethod;
51 import android.view.TouchDelegate;
52 import android.view.View;
53 import android.view.ViewGroup;
54 import android.view.ViewTreeObserver;
55 import android.view.animation.Interpolator;
56 import android.view.animation.PathInterpolator;
57 import android.widget.FrameLayout;
58 import android.widget.ImageView;
59 import android.widget.LinearLayout;
60 import android.widget.RemoteViews;
61 import android.widget.TextView;
62 
63 import com.android.internal.R;
64 
65 import java.util.ArrayList;
66 import java.util.List;
67 import java.util.Map;
68 import java.util.Objects;
69 
70 /**
71  * A custom-built layout for the Notification.MessagingStyle allows dynamic addition and removal
72  * messages and adapts the layout accordingly.
73  */
74 @RemoteViews.RemoteView
75 public class ConversationLayout extends FrameLayout
76         implements ImageMessageConsumer, IMessagingLayout {
77 
78     public static final Interpolator LINEAR_OUT_SLOW_IN = new PathInterpolator(0f, 0f, 0.2f, 1f);
79     public static final Interpolator FAST_OUT_LINEAR_IN = new PathInterpolator(0.4f, 0f, 1f, 1f);
80     public static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f);
81     public static final Interpolator OVERSHOOT = new PathInterpolator(0.4f, 0f, 0.2f, 1.4f);
82     public static final OnLayoutChangeListener MESSAGING_PROPERTY_ANIMATOR
83             = new MessagingPropertyAnimator();
84     public static final int IMPORTANCE_ANIM_GROW_DURATION = 250;
85     public static final int IMPORTANCE_ANIM_SHRINK_DURATION = 200;
86     public static final int IMPORTANCE_ANIM_SHRINK_DELAY = 25;
87     private final PeopleHelper mPeopleHelper = new PeopleHelper();
88     private List<MessagingMessage> mMessages = new ArrayList<>();
89     private List<MessagingMessage> mHistoricMessages = new ArrayList<>();
90     private MessagingLinearLayout mMessagingLinearLayout;
91     private boolean mShowHistoricMessages;
92     private ArrayList<MessagingGroup> mGroups = new ArrayList<>();
93     private int mLayoutColor;
94     private int mSenderTextColor;
95     private int mMessageTextColor;
96     private Icon mAvatarReplacement;
97     private boolean mIsOneToOne;
98     private ArrayList<MessagingGroup> mAddedGroups = new ArrayList<>();
99     private Person mUser;
100     private CharSequence mNameReplacement;
101     private boolean mIsCollapsed;
102     private ImageResolver mImageResolver;
103     private CachingIconView mConversationIconView;
104     private View mConversationIconContainer;
105     private int mConversationIconTopPaddingExpandedGroup;
106     private int mConversationIconTopPadding;
107     private int mExpandedGroupMessagePadding;
108     private TextView mConversationText;
109     private View mConversationIconBadge;
110     private CachingIconView mConversationIconBadgeBg;
111     private Icon mLargeIcon;
112     private View mExpandButtonContainer;
113     private ViewGroup mExpandButtonAndContentContainer;
114     private ViewGroup mExpandButtonContainerA11yContainer;
115     private NotificationExpandButton mExpandButton;
116     private MessagingLinearLayout mImageMessageContainer;
117     private int mBadgeProtrusion;
118     private int mConversationAvatarSize;
119     private int mConversationAvatarSizeExpanded;
120     private CachingIconView mIcon;
121     private CachingIconView mImportanceRingView;
122     private int mExpandedGroupBadgeProtrusion;
123     private int mExpandedGroupBadgeProtrusionFacePile;
124     private View mConversationFacePile;
125     private int mNotificationBackgroundColor;
126     private CharSequence mFallbackChatName;
127     private CharSequence mFallbackGroupChatName;
128     private CharSequence mConversationTitle;
129     private int mMessageSpacingStandard;
130     private int mMessageSpacingGroup;
131     private int mNotificationHeaderExpandedPadding;
132     private View mConversationHeader;
133     private View mContentContainer;
134     private boolean mExpandable = true;
135     private int mContentMarginEnd;
136     private Rect mMessagingClipRect;
137     private ObservableTextView mAppName;
138     private NotificationActionListLayout mActions;
139     private boolean mAppNameGone;
140     private int mFacePileAvatarSize;
141     private int mFacePileAvatarSizeExpandedGroup;
142     private int mFacePileProtectionWidth;
143     private int mFacePileProtectionWidthExpanded;
144     private boolean mImportantConversation;
145     private View mFeedbackIcon;
146     private float mMinTouchSize;
147     private Icon mConversationIcon;
148     private Icon mShortcutIcon;
149     private View mAppNameDivider;
150     private TouchDelegateComposite mTouchDelegate = new TouchDelegateComposite(this);
151     private ArrayList<MessagingLinearLayout.MessagingChild> mToRecycle = new ArrayList<>();
152 
ConversationLayout(@onNull Context context)153     public ConversationLayout(@NonNull Context context) {
154         super(context);
155     }
156 
ConversationLayout(@onNull Context context, @Nullable AttributeSet attrs)157     public ConversationLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
158         super(context, attrs);
159     }
160 
ConversationLayout(@onNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr)161     public ConversationLayout(@NonNull Context context, @Nullable AttributeSet attrs,
162             @AttrRes int defStyleAttr) {
163         super(context, attrs, defStyleAttr);
164     }
165 
ConversationLayout(@onNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes)166     public ConversationLayout(@NonNull Context context, @Nullable AttributeSet attrs,
167             @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
168         super(context, attrs, defStyleAttr, defStyleRes);
169     }
170 
171     @Override
onFinishInflate()172     protected void onFinishInflate() {
173         super.onFinishInflate();
174         mPeopleHelper.init(getContext());
175         mMessagingLinearLayout = findViewById(R.id.notification_messaging);
176         mActions = findViewById(R.id.actions);
177         mImageMessageContainer = findViewById(R.id.conversation_image_message_container);
178         // We still want to clip, but only on the top, since views can temporarily out of bounds
179         // during transitions.
180         DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
181         int size = Math.max(displayMetrics.widthPixels, displayMetrics.heightPixels);
182         mMessagingClipRect = new Rect(0, 0, size, size);
183         setMessagingClippingDisabled(false);
184         mConversationIconView = findViewById(R.id.conversation_icon);
185         mConversationIconContainer = findViewById(R.id.conversation_icon_container);
186         mIcon = findViewById(R.id.icon);
187         mFeedbackIcon = findViewById(com.android.internal.R.id.feedback);
188         mMinTouchSize = 48 * getResources().getDisplayMetrics().density;
189         mImportanceRingView = findViewById(R.id.conversation_icon_badge_ring);
190         mConversationIconBadge = findViewById(R.id.conversation_icon_badge);
191         mConversationIconBadgeBg = findViewById(R.id.conversation_icon_badge_bg);
192         mIcon.setOnVisibilityChangedListener((visibility) -> {
193 
194             // Let's hide the background directly or in an animated way
195             boolean isGone = visibility == GONE;
196             int oldVisibility = mConversationIconBadgeBg.getVisibility();
197             boolean wasGone = oldVisibility == GONE;
198             if (wasGone != isGone) {
199                 // Keep the badge gone state in sync with the icon. This is necessary in cases
200                 // Where the icon is being hidden externally like in group children.
201                 mConversationIconBadgeBg.animate().cancel();
202                 mConversationIconBadgeBg.setVisibility(visibility);
203             }
204 
205             // Let's handle the importance ring which can also be be gone normally
206             oldVisibility = mImportanceRingView.getVisibility();
207             wasGone = oldVisibility == GONE;
208             visibility = !mImportantConversation ? GONE : visibility;
209             boolean isRingGone = visibility == GONE;
210             if (wasGone != isRingGone) {
211                 // Keep the badge visibility in sync with the icon. This is necessary in cases
212                 // Where the icon is being hidden externally like in group children.
213                 mImportanceRingView.animate().cancel();
214                 mImportanceRingView.setVisibility(visibility);
215             }
216 
217             oldVisibility = mConversationIconBadge.getVisibility();
218             wasGone = oldVisibility == GONE;
219             if (wasGone != isGone) {
220                 mConversationIconBadge.animate().cancel();
221                 mConversationIconBadge.setVisibility(visibility);
222             }
223         });
224         // When the small icon is gone, hide the rest of the badge
225         mIcon.setOnForceHiddenChangedListener((forceHidden) -> {
226             mPeopleHelper.animateViewForceHidden(mConversationIconBadgeBg, forceHidden);
227             mPeopleHelper.animateViewForceHidden(mImportanceRingView, forceHidden);
228         });
229 
230         // When the conversation icon is gone, hide the whole badge
231         mConversationIconView.setOnForceHiddenChangedListener((forceHidden) -> {
232             mPeopleHelper.animateViewForceHidden(mConversationIconBadgeBg, forceHidden);
233             mPeopleHelper.animateViewForceHidden(mImportanceRingView, forceHidden);
234             mPeopleHelper.animateViewForceHidden(mIcon, forceHidden);
235         });
236         mConversationText = findViewById(R.id.conversation_text);
237         mExpandButtonContainer = findViewById(R.id.expand_button_container);
238         mExpandButtonContainerA11yContainer =
239                 findViewById(R.id.expand_button_a11y_container);
240         mConversationHeader = findViewById(R.id.conversation_header);
241         mContentContainer = findViewById(R.id.notification_action_list_margin_target);
242         mExpandButtonAndContentContainer = findViewById(R.id.expand_button_and_content_container);
243         mExpandButton = findViewById(R.id.expand_button);
244         mMessageSpacingStandard = getResources().getDimensionPixelSize(
245                 R.dimen.notification_messaging_spacing);
246         mMessageSpacingGroup = getResources().getDimensionPixelSize(
247                 R.dimen.notification_messaging_spacing_conversation_group);
248         mNotificationHeaderExpandedPadding = getResources().getDimensionPixelSize(
249                 R.dimen.conversation_header_expanded_padding_end);
250         mContentMarginEnd = getResources().getDimensionPixelSize(
251                 R.dimen.notification_content_margin_end);
252         mBadgeProtrusion = getResources().getDimensionPixelSize(
253                 R.dimen.conversation_badge_protrusion);
254         mConversationAvatarSize = getResources().getDimensionPixelSize(
255                 R.dimen.conversation_avatar_size);
256         mConversationAvatarSizeExpanded = getResources().getDimensionPixelSize(
257                 R.dimen.conversation_avatar_size_group_expanded);
258         mConversationIconTopPaddingExpandedGroup = getResources().getDimensionPixelSize(
259                 R.dimen.conversation_icon_container_top_padding_small_avatar);
260         mConversationIconTopPadding = getResources().getDimensionPixelSize(
261                 R.dimen.conversation_icon_container_top_padding);
262         mExpandedGroupMessagePadding = getResources().getDimensionPixelSize(
263                 R.dimen.expanded_group_conversation_message_padding);
264         mExpandedGroupBadgeProtrusion = getResources().getDimensionPixelSize(
265                 R.dimen.conversation_badge_protrusion_group_expanded);
266         mExpandedGroupBadgeProtrusionFacePile = getResources().getDimensionPixelSize(
267                 R.dimen.conversation_badge_protrusion_group_expanded_face_pile);
268         mConversationFacePile = findViewById(R.id.conversation_face_pile);
269         mFacePileAvatarSize = getResources().getDimensionPixelSize(
270                 R.dimen.conversation_face_pile_avatar_size);
271         mFacePileAvatarSizeExpandedGroup = getResources().getDimensionPixelSize(
272                 R.dimen.conversation_face_pile_avatar_size_group_expanded);
273         mFacePileProtectionWidth = getResources().getDimensionPixelSize(
274                 R.dimen.conversation_face_pile_protection_width);
275         mFacePileProtectionWidthExpanded = getResources().getDimensionPixelSize(
276                 R.dimen.conversation_face_pile_protection_width_expanded);
277         mFallbackChatName = getResources().getString(
278                 R.string.conversation_title_fallback_one_to_one);
279         mFallbackGroupChatName = getResources().getString(
280                 R.string.conversation_title_fallback_group_chat);
281         mAppName = findViewById(R.id.app_name_text);
282         mAppNameDivider = findViewById(R.id.app_name_divider);
283         mAppNameGone = mAppName.getVisibility() == GONE;
284         mAppName.setOnVisibilityChangedListener((visibility) -> {
285             onAppNameVisibilityChanged();
286         });
287     }
288 
289     @RemotableViewMethod
setAvatarReplacement(Icon icon)290     public void setAvatarReplacement(Icon icon) {
291         mAvatarReplacement = icon;
292     }
293 
294     @RemotableViewMethod
setNameReplacement(CharSequence nameReplacement)295     public void setNameReplacement(CharSequence nameReplacement) {
296         mNameReplacement = nameReplacement;
297     }
298 
299     /** Sets this conversation as "important", adding some additional UI treatment. */
300     @RemotableViewMethod
setIsImportantConversation(boolean isImportantConversation)301     public void setIsImportantConversation(boolean isImportantConversation) {
302         setIsImportantConversation(isImportantConversation, false);
303     }
304 
305     /** @hide **/
setIsImportantConversation(boolean isImportantConversation, boolean animate)306     public void setIsImportantConversation(boolean isImportantConversation, boolean animate) {
307         mImportantConversation = isImportantConversation;
308         mImportanceRingView.setVisibility(isImportantConversation && mIcon.getVisibility() != GONE
309                 ? VISIBLE : GONE);
310 
311         if (animate && isImportantConversation) {
312             GradientDrawable ring = (GradientDrawable) mImportanceRingView.getDrawable();
313             ring.mutate();
314             GradientDrawable bg = (GradientDrawable) mConversationIconBadgeBg.getDrawable();
315             bg.mutate();
316             int ringColor = getResources()
317                     .getColor(R.color.conversation_important_highlight);
318             int standardThickness = getResources()
319                     .getDimensionPixelSize(R.dimen.importance_ring_stroke_width);
320             int largeThickness = getResources()
321                     .getDimensionPixelSize(R.dimen.importance_ring_anim_max_stroke_width);
322             int standardSize = getResources().getDimensionPixelSize(
323                     R.dimen.importance_ring_size);
324             int baseSize = standardSize - standardThickness * 2;
325             int bgSize = getResources()
326                     .getDimensionPixelSize(R.dimen.conversation_icon_size_badged);
327 
328             ValueAnimator.AnimatorUpdateListener animatorUpdateListener = animation -> {
329                 int strokeWidth = Math.round((float) animation.getAnimatedValue());
330                 ring.setStroke(strokeWidth, ringColor);
331                 int newSize = baseSize + strokeWidth * 2;
332                 ring.setSize(newSize, newSize);
333                 mImportanceRingView.invalidate();
334             };
335 
336             ValueAnimator growAnimation = ValueAnimator.ofFloat(0, largeThickness);
337             growAnimation.setInterpolator(LINEAR_OUT_SLOW_IN);
338             growAnimation.setDuration(IMPORTANCE_ANIM_GROW_DURATION);
339             growAnimation.addUpdateListener(animatorUpdateListener);
340 
341             ValueAnimator shrinkAnimation =
342                     ValueAnimator.ofFloat(largeThickness, standardThickness);
343             shrinkAnimation.setDuration(IMPORTANCE_ANIM_SHRINK_DURATION);
344             shrinkAnimation.setStartDelay(IMPORTANCE_ANIM_SHRINK_DELAY);
345             shrinkAnimation.setInterpolator(OVERSHOOT);
346             shrinkAnimation.addUpdateListener(animatorUpdateListener);
347             shrinkAnimation.addListener(new AnimatorListenerAdapter() {
348                 @Override
349                 public void onAnimationStart(Animator animation) {
350                     // Shrink the badge bg so that it doesn't peek behind the animation
351                     bg.setSize(baseSize, baseSize);
352                     mConversationIconBadgeBg.invalidate();
353                 }
354 
355                 @Override
356                 public void onAnimationEnd(Animator animation) {
357                     // Reset bg back to normal size
358                     bg.setSize(bgSize, bgSize);
359                     mConversationIconBadgeBg.invalidate();
360                 }
361             });
362 
363             AnimatorSet anims = new AnimatorSet();
364             anims.playSequentially(growAnimation, shrinkAnimation);
365             anims.start();
366         }
367     }
368 
isImportantConversation()369     public boolean isImportantConversation() {
370         return mImportantConversation;
371     }
372 
373     /**
374      * Set this layout to show the collapsed representation.
375      *
376      * @param isCollapsed is it collapsed
377      */
378     @RemotableViewMethod
setIsCollapsed(boolean isCollapsed)379     public void setIsCollapsed(boolean isCollapsed) {
380         mIsCollapsed = isCollapsed;
381         mMessagingLinearLayout.setMaxDisplayedLines(isCollapsed ? 1 : Integer.MAX_VALUE);
382         updateExpandButton();
383         updateContentEndPaddings();
384     }
385 
386     @RemotableViewMethod
setData(Bundle extras)387     public void setData(Bundle extras) {
388         Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES);
389         List<Notification.MessagingStyle.Message> newMessages
390                 = Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages);
391         Parcelable[] histMessages = extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES);
392         List<Notification.MessagingStyle.Message> newHistoricMessages
393                 = Notification.MessagingStyle.Message.getMessagesFromBundleArray(histMessages);
394 
395         // mUser now set (would be nice to avoid the side effect but WHATEVER)
396         setUser(extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON));
397 
398         // Append remote input history to newMessages (again, side effect is lame but WHATEVS)
399         RemoteInputHistoryItem[] history = (RemoteInputHistoryItem[])
400                 extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
401         addRemoteInputHistoryToMessages(newMessages, history);
402 
403         boolean showSpinner =
404                 extras.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false);
405         // bind it, baby
406         bind(newMessages, newHistoricMessages, showSpinner);
407 
408         int unreadCount = extras.getInt(Notification.EXTRA_CONVERSATION_UNREAD_MESSAGE_COUNT);
409         setUnreadCount(unreadCount);
410     }
411 
412     @Override
setImageResolver(ImageResolver resolver)413     public void setImageResolver(ImageResolver resolver) {
414         mImageResolver = resolver;
415     }
416 
417     /** @hide */
setUnreadCount(int unreadCount)418     public void setUnreadCount(int unreadCount) {
419         mExpandButton.setNumber(unreadCount);
420     }
421 
addRemoteInputHistoryToMessages( List<Notification.MessagingStyle.Message> newMessages, RemoteInputHistoryItem[] remoteInputHistory)422     private void addRemoteInputHistoryToMessages(
423             List<Notification.MessagingStyle.Message> newMessages,
424             RemoteInputHistoryItem[] remoteInputHistory) {
425         if (remoteInputHistory == null || remoteInputHistory.length == 0) {
426             return;
427         }
428         for (int i = remoteInputHistory.length - 1; i >= 0; i--) {
429             RemoteInputHistoryItem historyMessage = remoteInputHistory[i];
430             Notification.MessagingStyle.Message message = new Notification.MessagingStyle.Message(
431                     historyMessage.getText(), 0, (Person) null, true /* remoteHistory */);
432             if (historyMessage.getUri() != null) {
433                 message.setData(historyMessage.getMimeType(), historyMessage.getUri());
434             }
435             newMessages.add(message);
436         }
437     }
438 
bind(List<Notification.MessagingStyle.Message> newMessages, List<Notification.MessagingStyle.Message> newHistoricMessages, boolean showSpinner)439     private void bind(List<Notification.MessagingStyle.Message> newMessages,
440             List<Notification.MessagingStyle.Message> newHistoricMessages,
441             boolean showSpinner) {
442         // convert MessagingStyle.Message to MessagingMessage, re-using ones from a previous binding
443         // if they exist
444         List<MessagingMessage> historicMessages = createMessages(newHistoricMessages,
445                 true /* isHistoric */);
446         List<MessagingMessage> messages = createMessages(newMessages, false /* isHistoric */);
447 
448         // Copy our groups, before they get clobbered
449         ArrayList<MessagingGroup> oldGroups = new ArrayList<>(mGroups);
450 
451         // Add our new MessagingMessages to groups
452         List<List<MessagingMessage>> groups = new ArrayList<>();
453         List<Person> senders = new ArrayList<>();
454 
455         // Lets first find the groups (populate `groups` and `senders`)
456         findGroups(historicMessages, messages, groups, senders);
457 
458         // Let's now create the views and reorder them accordingly
459         //   side-effect: updates mGroups, mAddedGroups
460         createGroupViews(groups, senders, showSpinner);
461 
462         // Let's first check which groups were removed altogether and remove them in one animation
463         removeGroups(oldGroups);
464 
465         // Let's remove the remaining messages
466         for (MessagingMessage message : mMessages) {
467             message.removeMessage(mToRecycle);
468         }
469         for (MessagingMessage historicMessage : mHistoricMessages) {
470             historicMessage.removeMessage(mToRecycle);
471         }
472 
473         mMessages = messages;
474         mHistoricMessages = historicMessages;
475 
476         updateHistoricMessageVisibility();
477         updateTitleAndNamesDisplay();
478 
479         updateConversationLayout();
480 
481         // Recycle everything at the end of the update, now that we know it's no longer needed.
482         for (MessagingLinearLayout.MessagingChild child : mToRecycle) {
483             child.recycle();
484         }
485         mToRecycle.clear();
486     }
487 
488     /**
489      * Update the layout according to the data provided (i.e mIsOneToOne, expanded etc);
490      */
updateConversationLayout()491     private void updateConversationLayout() {
492         // Set avatar and name
493         CharSequence conversationText = mConversationTitle;
494         mConversationIcon = mShortcutIcon;
495         if (mIsOneToOne) {
496             // Let's resolve the icon / text from the last sender
497             CharSequence userKey = getKey(mUser);
498             for (int i = mGroups.size() - 1; i >= 0; i--) {
499                 MessagingGroup messagingGroup = mGroups.get(i);
500                 Person messageSender = messagingGroup.getSender();
501                 if ((messageSender != null && !TextUtils.equals(userKey, getKey(messageSender)))
502                         || i == 0) {
503                     if (TextUtils.isEmpty(conversationText)) {
504                         // We use the sendername as header text if no conversation title is provided
505                         // (This usually happens for most 1:1 conversations)
506                         conversationText = messagingGroup.getSenderName();
507                     }
508                     if (mConversationIcon == null) {
509                         Icon avatarIcon = messagingGroup.getAvatarIcon();
510                         if (avatarIcon == null) {
511                             avatarIcon = mPeopleHelper.createAvatarSymbol(conversationText, "",
512                                     mLayoutColor);
513                         }
514                         mConversationIcon = avatarIcon;
515                     }
516                     break;
517                 }
518             }
519         }
520         if (mConversationIcon == null) {
521             mConversationIcon = mLargeIcon;
522         }
523         if (mIsOneToOne || mConversationIcon != null) {
524             mConversationIconView.setVisibility(VISIBLE);
525             mConversationFacePile.setVisibility(GONE);
526             mConversationIconView.setImageIcon(mConversationIcon);
527         } else {
528             mConversationIconView.setVisibility(GONE);
529             // This will also inflate it!
530             mConversationFacePile.setVisibility(VISIBLE);
531             // rebind the value to the inflated view instead of the stub
532             mConversationFacePile = findViewById(R.id.conversation_face_pile);
533             bindFacePile();
534         }
535         if (TextUtils.isEmpty(conversationText)) {
536             conversationText = mIsOneToOne ? mFallbackChatName : mFallbackGroupChatName;
537         }
538         mConversationText.setText(conversationText);
539         // Update if the groups can hide the sender if they are first (applies to 1:1 conversations)
540         // This needs to happen after all of the above o update all of the groups
541         mPeopleHelper.maybeHideFirstSenderName(mGroups, mIsOneToOne, conversationText);
542         updateAppName();
543         updateIconPositionAndSize();
544         updateImageMessages();
545         updatePaddingsBasedOnContentAvailability();
546         updateActionListPadding();
547         updateAppNameDividerVisibility();
548     }
549 
updateActionListPadding()550     private void updateActionListPadding() {
551         if (mActions != null) {
552             mActions.setCollapsibleIndentDimen(R.dimen.call_notification_collapsible_indent);
553         }
554     }
555 
updateImageMessages()556     private void updateImageMessages() {
557         View newMessage = null;
558         if (mIsCollapsed && mGroups.size() > 0) {
559 
560             // When collapsed, we're displaying the image message in a dedicated container
561             // on the right of the layout instead of inline. Let's add the isolated image there
562             MessagingGroup messagingGroup = mGroups.get(mGroups.size() -1);
563             MessagingImageMessage isolatedMessage = messagingGroup.getIsolatedMessage();
564             if (isolatedMessage != null) {
565                 newMessage = isolatedMessage.getView();
566             }
567         }
568         // Remove all messages that don't belong into the image layout
569         View previousMessage = mImageMessageContainer.getChildAt(0);
570         if (previousMessage != newMessage) {
571             mImageMessageContainer.removeView(previousMessage);
572             if (newMessage != null) {
573                 mImageMessageContainer.addView(newMessage);
574             }
575         }
576         mImageMessageContainer.setVisibility(newMessage != null ? VISIBLE : GONE);
577     }
578 
bindFacePile(ImageView bottomBackground, ImageView bottomView, ImageView topView)579     public void bindFacePile(ImageView bottomBackground, ImageView bottomView, ImageView topView) {
580         applyNotificationBackgroundColor(bottomBackground);
581         // Let's find the two last conversations:
582         Icon secondLastIcon = null;
583         CharSequence lastKey = null;
584         Icon lastIcon = null;
585         CharSequence userKey = getKey(mUser);
586         for (int i = mGroups.size() - 1; i >= 0; i--) {
587             MessagingGroup messagingGroup = mGroups.get(i);
588             Person messageSender = messagingGroup.getSender();
589             boolean notUser = messageSender != null
590                     && !TextUtils.equals(userKey, getKey(messageSender));
591             boolean notIncluded = messageSender != null
592                     && !TextUtils.equals(lastKey, getKey(messageSender));
593             if ((notUser && notIncluded)
594                     || (i == 0 && lastKey == null)) {
595                 if (lastIcon == null) {
596                     lastIcon = messagingGroup.getAvatarIcon();
597                     lastKey = getKey(messageSender);
598                 } else {
599                     secondLastIcon = messagingGroup.getAvatarIcon();
600                     break;
601                 }
602             }
603         }
604         if (lastIcon == null) {
605             lastIcon = mPeopleHelper.createAvatarSymbol(" ", "", mLayoutColor);
606         }
607         bottomView.setImageIcon(lastIcon);
608         if (secondLastIcon == null) {
609             secondLastIcon = mPeopleHelper.createAvatarSymbol("", "", mLayoutColor);
610         }
611         topView.setImageIcon(secondLastIcon);
612     }
613 
bindFacePile()614     private void bindFacePile() {
615         ImageView bottomBackground = mConversationFacePile.findViewById(
616                 R.id.conversation_face_pile_bottom_background);
617         ImageView bottomView = mConversationFacePile.findViewById(
618                 R.id.conversation_face_pile_bottom);
619         ImageView topView = mConversationFacePile.findViewById(
620                 R.id.conversation_face_pile_top);
621 
622         bindFacePile(bottomBackground, bottomView, topView);
623 
624         int conversationAvatarSize;
625         int facepileAvatarSize;
626         int facePileBackgroundSize;
627         if (mIsCollapsed) {
628             conversationAvatarSize = mConversationAvatarSize;
629             facepileAvatarSize = mFacePileAvatarSize;
630             facePileBackgroundSize = facepileAvatarSize + 2 * mFacePileProtectionWidth;
631         } else {
632             conversationAvatarSize = mConversationAvatarSizeExpanded;
633             facepileAvatarSize = mFacePileAvatarSizeExpandedGroup;
634             facePileBackgroundSize = facepileAvatarSize + 2 * mFacePileProtectionWidthExpanded;
635         }
636         LayoutParams layoutParams = (LayoutParams) mConversationFacePile.getLayoutParams();
637         layoutParams.width = conversationAvatarSize;
638         layoutParams.height = conversationAvatarSize;
639         mConversationFacePile.setLayoutParams(layoutParams);
640 
641         layoutParams = (LayoutParams) bottomView.getLayoutParams();
642         layoutParams.width = facepileAvatarSize;
643         layoutParams.height = facepileAvatarSize;
644         bottomView.setLayoutParams(layoutParams);
645 
646         layoutParams = (LayoutParams) topView.getLayoutParams();
647         layoutParams.width = facepileAvatarSize;
648         layoutParams.height = facepileAvatarSize;
649         topView.setLayoutParams(layoutParams);
650 
651         layoutParams = (LayoutParams) bottomBackground.getLayoutParams();
652         layoutParams.width = facePileBackgroundSize;
653         layoutParams.height = facePileBackgroundSize;
654         bottomBackground.setLayoutParams(layoutParams);
655     }
656 
updateAppName()657     private void updateAppName() {
658         mAppName.setVisibility(mIsCollapsed ? GONE : VISIBLE);
659     }
660 
shouldHideAppName()661     public boolean shouldHideAppName() {
662         return mIsCollapsed;
663     }
664 
665     /**
666      * update the icon position and sizing
667      */
updateIconPositionAndSize()668     private void updateIconPositionAndSize() {
669         int badgeProtrusion;
670         int conversationAvatarSize;
671         if (mIsOneToOne || mIsCollapsed) {
672             badgeProtrusion = mBadgeProtrusion;
673             conversationAvatarSize = mConversationAvatarSize;
674         } else {
675             badgeProtrusion = mConversationFacePile.getVisibility() == VISIBLE
676                     ? mExpandedGroupBadgeProtrusionFacePile
677                     : mExpandedGroupBadgeProtrusion;
678             conversationAvatarSize = mConversationAvatarSizeExpanded;
679         }
680 
681         if (mConversationIconView.getVisibility() == VISIBLE) {
682             LayoutParams layoutParams = (LayoutParams) mConversationIconView.getLayoutParams();
683             layoutParams.width = conversationAvatarSize;
684             layoutParams.height = conversationAvatarSize;
685             layoutParams.leftMargin = badgeProtrusion;
686             layoutParams.rightMargin = badgeProtrusion;
687             layoutParams.bottomMargin = badgeProtrusion;
688             mConversationIconView.setLayoutParams(layoutParams);
689         }
690 
691         if (mConversationFacePile.getVisibility() == VISIBLE) {
692             LayoutParams layoutParams = (LayoutParams) mConversationFacePile.getLayoutParams();
693             layoutParams.leftMargin = badgeProtrusion;
694             layoutParams.rightMargin = badgeProtrusion;
695             layoutParams.bottomMargin = badgeProtrusion;
696             mConversationFacePile.setLayoutParams(layoutParams);
697         }
698     }
699 
updatePaddingsBasedOnContentAvailability()700     private void updatePaddingsBasedOnContentAvailability() {
701         // groups have avatars that need more spacing
702         mMessagingLinearLayout.setSpacing(
703                 mIsOneToOne ? mMessageSpacingStandard : mMessageSpacingGroup);
704 
705         int messagingPadding = mIsOneToOne || mIsCollapsed
706                 ? 0
707                 // Add some extra padding to the messages, since otherwise it will overlap with the
708                 // group
709                 : mExpandedGroupMessagePadding;
710 
711         int iconPadding = mIsOneToOne || mIsCollapsed
712                 ? mConversationIconTopPadding
713                 : mConversationIconTopPaddingExpandedGroup;
714 
715         mConversationIconContainer.setPaddingRelative(
716                 mConversationIconContainer.getPaddingStart(),
717                 iconPadding,
718                 mConversationIconContainer.getPaddingEnd(),
719                 mConversationIconContainer.getPaddingBottom());
720 
721         mMessagingLinearLayout.setPaddingRelative(
722                 mMessagingLinearLayout.getPaddingStart(),
723                 messagingPadding,
724                 mMessagingLinearLayout.getPaddingEnd(),
725                 mMessagingLinearLayout.getPaddingBottom());
726     }
727 
728     @RemotableViewMethod
setLargeIcon(Icon largeIcon)729     public void setLargeIcon(Icon largeIcon) {
730         mLargeIcon = largeIcon;
731     }
732 
733     @RemotableViewMethod
setShortcutIcon(Icon shortcutIcon)734     public void setShortcutIcon(Icon shortcutIcon) {
735         mShortcutIcon = shortcutIcon;
736     }
737 
738     /**
739      * Sets the conversation title of this conversation.
740      *
741      * @param conversationTitle the conversation title
742      */
743     @RemotableViewMethod
setConversationTitle(CharSequence conversationTitle)744     public void setConversationTitle(CharSequence conversationTitle) {
745         // Remove formatting from the title.
746         mConversationTitle = conversationTitle != null ? conversationTitle.toString() : null;
747     }
748 
getConversationTitle()749     public CharSequence getConversationTitle() {
750         return mConversationText.getText();
751     }
752 
removeGroups(ArrayList<MessagingGroup> oldGroups)753     private void removeGroups(ArrayList<MessagingGroup> oldGroups) {
754         int size = oldGroups.size();
755         for (int i = 0; i < size; i++) {
756             MessagingGroup group = oldGroups.get(i);
757             if (!mGroups.contains(group)) {
758                 List<MessagingMessage> messages = group.getMessages();
759                 boolean wasShown = group.isShown();
760                 mMessagingLinearLayout.removeView(group);
761                 if (wasShown && !MessagingLinearLayout.isGone(group)) {
762                     mMessagingLinearLayout.addTransientView(group, 0);
763                     group.removeGroupAnimated(() -> {
764                         mMessagingLinearLayout.removeTransientView(group);
765                         group.recycle();
766                     });
767                 } else {
768                     // Defer recycling until after the update is done, since we may still need the
769                     // old group around to perform other updates.
770                     mToRecycle.add(group);
771                 }
772                 mMessages.removeAll(messages);
773                 mHistoricMessages.removeAll(messages);
774             }
775         }
776     }
777 
updateTitleAndNamesDisplay()778     private void updateTitleAndNamesDisplay() {
779         // Map of unique names to their prefix
780         Map<CharSequence, String> uniqueNames = mPeopleHelper.mapUniqueNamesToPrefix(mGroups);
781 
782         // Now that we have the correct symbols, let's look what we have cached
783         ArrayMap<CharSequence, Icon> cachedAvatars = new ArrayMap<>();
784         for (int i = 0; i < mGroups.size(); i++) {
785             // Let's now set the avatars
786             MessagingGroup group = mGroups.get(i);
787             boolean isOwnMessage = group.getSender() == mUser;
788             CharSequence senderName = group.getSenderName();
789             if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)
790                     || (mIsOneToOne && mAvatarReplacement != null && !isOwnMessage)) {
791                 continue;
792             }
793             String symbol = uniqueNames.get(senderName);
794             Icon cachedIcon = group.getAvatarSymbolIfMatching(senderName,
795                     symbol, mLayoutColor);
796             if (cachedIcon != null) {
797                 cachedAvatars.put(senderName, cachedIcon);
798             }
799         }
800 
801         for (int i = 0; i < mGroups.size(); i++) {
802             // Let's now set the avatars
803             MessagingGroup group = mGroups.get(i);
804             CharSequence senderName = group.getSenderName();
805             if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)) {
806                 continue;
807             }
808             if (mIsOneToOne && mAvatarReplacement != null && group.getSender() != mUser) {
809                 group.setAvatar(mAvatarReplacement);
810             } else {
811                 Icon cachedIcon = cachedAvatars.get(senderName);
812                 if (cachedIcon == null) {
813                     cachedIcon = mPeopleHelper.createAvatarSymbol(senderName,
814                             uniqueNames.get(senderName), mLayoutColor);
815                     cachedAvatars.put(senderName, cachedIcon);
816                 }
817                 group.setCreatedAvatar(cachedIcon, senderName, uniqueNames.get(senderName),
818                         mLayoutColor);
819             }
820         }
821     }
822 
823     @RemotableViewMethod
setLayoutColor(int color)824     public void setLayoutColor(int color) {
825         mLayoutColor = color;
826     }
827 
828     @RemotableViewMethod
setIsOneToOne(boolean oneToOne)829     public void setIsOneToOne(boolean oneToOne) {
830         mIsOneToOne = oneToOne;
831     }
832 
833     @RemotableViewMethod
setSenderTextColor(int color)834     public void setSenderTextColor(int color) {
835         mSenderTextColor = color;
836         mConversationText.setTextColor(color);
837     }
838 
839     /**
840      * @param color the color of the notification background
841      */
842     @RemotableViewMethod
setNotificationBackgroundColor(int color)843     public void setNotificationBackgroundColor(int color) {
844         mNotificationBackgroundColor = color;
845         applyNotificationBackgroundColor(mConversationIconBadgeBg);
846     }
847 
applyNotificationBackgroundColor(ImageView view)848     private void applyNotificationBackgroundColor(ImageView view) {
849         view.setImageTintList(ColorStateList.valueOf(mNotificationBackgroundColor));
850     }
851 
852     @RemotableViewMethod
setMessageTextColor(int color)853     public void setMessageTextColor(int color) {
854         mMessageTextColor = color;
855     }
856 
setUser(Person user)857     private void setUser(Person user) {
858         mUser = user;
859         if (mUser.getIcon() == null) {
860             Icon userIcon = Icon.createWithResource(getContext(),
861                     R.drawable.messaging_user);
862             userIcon.setTint(mLayoutColor);
863             mUser = mUser.toBuilder().setIcon(userIcon).build();
864         }
865     }
866 
createGroupViews(List<List<MessagingMessage>> groups, List<Person> senders, boolean showSpinner)867     private void createGroupViews(List<List<MessagingMessage>> groups,
868             List<Person> senders, boolean showSpinner) {
869         mGroups.clear();
870         for (int groupIndex = 0; groupIndex < groups.size(); groupIndex++) {
871             List<MessagingMessage> group = groups.get(groupIndex);
872             MessagingGroup newGroup = null;
873             // we'll just take the first group that exists or create one there is none
874             for (int messageIndex = group.size() - 1; messageIndex >= 0; messageIndex--) {
875                 MessagingMessage message = group.get(messageIndex);
876                 newGroup = message.getGroup();
877                 if (newGroup != null) {
878                     break;
879                 }
880             }
881             // Create a new group, adding it to the linear layout as well
882             if (newGroup == null) {
883                 newGroup = MessagingGroup.createGroup(mMessagingLinearLayout);
884                 mAddedGroups.add(newGroup);
885             } else if (newGroup.getParent() != mMessagingLinearLayout) {
886                 throw new IllegalStateException(
887                         "group parent was " + newGroup.getParent() + " but expected "
888                                 + mMessagingLinearLayout);
889             }
890             newGroup.setImageDisplayLocation(mIsCollapsed
891                     ? IMAGE_DISPLAY_LOCATION_EXTERNAL
892                     : IMAGE_DISPLAY_LOCATION_INLINE);
893             newGroup.setIsInConversation(true);
894             newGroup.setLayoutColor(mLayoutColor);
895             newGroup.setTextColors(mSenderTextColor, mMessageTextColor);
896             Person sender = senders.get(groupIndex);
897             CharSequence nameOverride = null;
898             if (sender != mUser && mNameReplacement != null) {
899                 nameOverride = mNameReplacement;
900             }
901             newGroup.setShowingAvatar(!mIsOneToOne && !mIsCollapsed);
902             newGroup.setSingleLine(mIsCollapsed);
903             newGroup.setSender(sender, nameOverride);
904             newGroup.setSending(groupIndex == (groups.size() - 1) && showSpinner);
905             mGroups.add(newGroup);
906 
907             // Reposition to the correct place (if we're re-using a group)
908             if (mMessagingLinearLayout.indexOfChild(newGroup) != groupIndex) {
909                 mMessagingLinearLayout.removeView(newGroup);
910                 mMessagingLinearLayout.addView(newGroup, groupIndex);
911             }
912             newGroup.setMessages(group);
913         }
914     }
915 
findGroups(List<MessagingMessage> historicMessages, List<MessagingMessage> messages, List<List<MessagingMessage>> groups, List<Person> senders)916     private void findGroups(List<MessagingMessage> historicMessages,
917             List<MessagingMessage> messages, List<List<MessagingMessage>> groups,
918             List<Person> senders) {
919         CharSequence currentSenderKey = null;
920         List<MessagingMessage> currentGroup = null;
921         int histSize = historicMessages.size();
922         for (int i = 0; i < histSize + messages.size(); i++) {
923             MessagingMessage message;
924             if (i < histSize) {
925                 message = historicMessages.get(i);
926             } else {
927                 message = messages.get(i - histSize);
928             }
929             boolean isNewGroup = currentGroup == null;
930             Person sender = message.getMessage().getSenderPerson();
931             CharSequence key = getKey(sender);
932             isNewGroup |= !TextUtils.equals(key, currentSenderKey);
933             if (isNewGroup) {
934                 currentGroup = new ArrayList<>();
935                 groups.add(currentGroup);
936                 if (sender == null) {
937                     sender = mUser;
938                 } else {
939                     // Remove all formatting from the sender name
940                     sender = sender.toBuilder().setName(Objects.toString(sender.getName())).build();
941                 }
942                 senders.add(sender);
943                 currentSenderKey = key;
944             }
945             currentGroup.add(message);
946         }
947     }
948 
getKey(Person person)949     private CharSequence getKey(Person person) {
950         return person == null ? null : person.getKey() == null ? person.getName() : person.getKey();
951     }
952 
953     /**
954      * Creates new messages, reusing existing ones if they are available.
955      *
956      * @param newMessages the messages to parse.
957      */
createMessages( List<Notification.MessagingStyle.Message> newMessages, boolean historic)958     private List<MessagingMessage> createMessages(
959             List<Notification.MessagingStyle.Message> newMessages, boolean historic) {
960         List<MessagingMessage> result = new ArrayList<>();
961         for (int i = 0; i < newMessages.size(); i++) {
962             Notification.MessagingStyle.Message m = newMessages.get(i);
963             MessagingMessage message = findAndRemoveMatchingMessage(m);
964             if (message == null) {
965                 message = MessagingMessage.createMessage(this, m, mImageResolver);
966             }
967             message.setIsHistoric(historic);
968             result.add(message);
969         }
970         return result;
971     }
972 
findAndRemoveMatchingMessage(Notification.MessagingStyle.Message m)973     private MessagingMessage findAndRemoveMatchingMessage(Notification.MessagingStyle.Message m) {
974         for (int i = 0; i < mMessages.size(); i++) {
975             MessagingMessage existing = mMessages.get(i);
976             if (existing.sameAs(m)) {
977                 mMessages.remove(i);
978                 return existing;
979             }
980         }
981         for (int i = 0; i < mHistoricMessages.size(); i++) {
982             MessagingMessage existing = mHistoricMessages.get(i);
983             if (existing.sameAs(m)) {
984                 mHistoricMessages.remove(i);
985                 return existing;
986             }
987         }
988         return null;
989     }
990 
showHistoricMessages(boolean show)991     public void showHistoricMessages(boolean show) {
992         mShowHistoricMessages = show;
993         updateHistoricMessageVisibility();
994     }
995 
updateHistoricMessageVisibility()996     private void updateHistoricMessageVisibility() {
997         int numHistoric = mHistoricMessages.size();
998         for (int i = 0; i < numHistoric; i++) {
999             MessagingMessage existing = mHistoricMessages.get(i);
1000             existing.setVisibility(mShowHistoricMessages ? VISIBLE : GONE);
1001         }
1002         int numGroups = mGroups.size();
1003         for (int i = 0; i < numGroups; i++) {
1004             MessagingGroup group = mGroups.get(i);
1005             int visibleChildren = 0;
1006             List<MessagingMessage> messages = group.getMessages();
1007             int numGroupMessages = messages.size();
1008             for (int j = 0; j < numGroupMessages; j++) {
1009                 MessagingMessage message = messages.get(j);
1010                 if (message.getVisibility() != GONE) {
1011                     visibleChildren++;
1012                 }
1013             }
1014             if (visibleChildren > 0 && group.getVisibility() == GONE) {
1015                 group.setVisibility(VISIBLE);
1016             } else if (visibleChildren == 0 && group.getVisibility() != GONE)   {
1017                 group.setVisibility(GONE);
1018             }
1019         }
1020     }
1021 
1022     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)1023     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
1024         super.onLayout(changed, left, top, right, bottom);
1025         if (!mAddedGroups.isEmpty()) {
1026             getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
1027                 @Override
1028                 public boolean onPreDraw() {
1029                     for (MessagingGroup group : mAddedGroups) {
1030                         if (!group.isShown()) {
1031                             continue;
1032                         }
1033                         MessagingPropertyAnimator.fadeIn(group.getAvatar());
1034                         MessagingPropertyAnimator.fadeIn(group.getSenderView());
1035                         MessagingPropertyAnimator.startLocalTranslationFrom(group,
1036                                 group.getHeight(), LINEAR_OUT_SLOW_IN);
1037                     }
1038                     mAddedGroups.clear();
1039                     getViewTreeObserver().removeOnPreDrawListener(this);
1040                     return true;
1041                 }
1042             });
1043         }
1044         mTouchDelegate.clear();
1045         if (mFeedbackIcon.getVisibility() == VISIBLE) {
1046             float width = Math.max(mMinTouchSize, mFeedbackIcon.getWidth());
1047             float height = Math.max(mMinTouchSize, mFeedbackIcon.getHeight());
1048             final Rect feedbackTouchRect = new Rect();
1049             feedbackTouchRect.left = (int) ((mFeedbackIcon.getLeft() + mFeedbackIcon.getRight())
1050                     / 2.0f - width / 2.0f);
1051             feedbackTouchRect.top = (int) ((mFeedbackIcon.getTop() + mFeedbackIcon.getBottom())
1052                     / 2.0f - height / 2.0f);
1053             feedbackTouchRect.bottom = (int) (feedbackTouchRect.top + height);
1054             feedbackTouchRect.right = (int) (feedbackTouchRect.left + width);
1055 
1056             getRelativeTouchRect(feedbackTouchRect, mFeedbackIcon);
1057             mTouchDelegate.add(new TouchDelegate(feedbackTouchRect, mFeedbackIcon));
1058         }
1059 
1060         setTouchDelegate(mTouchDelegate);
1061     }
1062 
getRelativeTouchRect(Rect touchRect, View view)1063     private void getRelativeTouchRect(Rect touchRect, View view) {
1064         ViewGroup viewGroup = (ViewGroup) view.getParent();
1065         while (viewGroup != this) {
1066             touchRect.offset(viewGroup.getLeft(), viewGroup.getTop());
1067             viewGroup = (ViewGroup) viewGroup.getParent();
1068         }
1069     }
1070 
getMessagingLinearLayout()1071     public MessagingLinearLayout getMessagingLinearLayout() {
1072         return mMessagingLinearLayout;
1073     }
1074 
getImageMessageContainer()1075     public @NonNull ViewGroup getImageMessageContainer() {
1076         return mImageMessageContainer;
1077     }
1078 
getMessagingGroups()1079     public ArrayList<MessagingGroup> getMessagingGroups() {
1080         return mGroups;
1081     }
1082 
updateExpandButton()1083     private void updateExpandButton() {
1084         int buttonGravity;
1085         ViewGroup newContainer;
1086         if (mIsCollapsed) {
1087             buttonGravity = Gravity.CENTER;
1088             // NOTE(b/182474419): In order for the touch target of the expand button to be the full
1089             // height of the notification, we would want the mExpandButtonContainer's height to be
1090             // set to WRAP_CONTENT (or 88dp) when in the collapsed state.  Unfortunately, that
1091             // causes an unstable remeasuring infinite loop when the unread count is visible,
1092             // causing the layout to occasionally hide the messages.  As an aside, that naive
1093             // solution also causes an undesirably large gap between content and smart replies.
1094             newContainer = mExpandButtonAndContentContainer;
1095         } else {
1096             buttonGravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
1097             newContainer = mExpandButtonContainerA11yContainer;
1098         }
1099         mExpandButton.setExpanded(!mIsCollapsed);
1100 
1101         // We need to make sure that the expand button is in the linearlayout pushing over the
1102         // content when collapsed, but allows the content to flow under it when expanded.
1103         if (newContainer != mExpandButtonContainer.getParent()) {
1104             ((ViewGroup) mExpandButtonContainer.getParent()).removeView(mExpandButtonContainer);
1105             newContainer.addView(mExpandButtonContainer);
1106         }
1107 
1108         // update if the expand button is centered
1109         LinearLayout.LayoutParams layoutParams =
1110                 (LinearLayout.LayoutParams) mExpandButton.getLayoutParams();
1111         layoutParams.gravity = buttonGravity;
1112         mExpandButton.setLayoutParams(layoutParams);
1113     }
1114 
updateContentEndPaddings()1115     private void updateContentEndPaddings() {
1116         // Let's make sure the conversation header can't run into the expand button when we're
1117         // collapsed and update the paddings of the content
1118         int headerPaddingEnd;
1119         int contentPaddingEnd;
1120         if (!mExpandable) {
1121             headerPaddingEnd = 0;
1122             contentPaddingEnd = mContentMarginEnd;
1123         } else if (mIsCollapsed) {
1124             headerPaddingEnd = 0;
1125             contentPaddingEnd = 0;
1126         } else {
1127             headerPaddingEnd = mNotificationHeaderExpandedPadding;
1128             contentPaddingEnd = mContentMarginEnd;
1129         }
1130         mConversationHeader.setPaddingRelative(
1131                 mConversationHeader.getPaddingStart(),
1132                 mConversationHeader.getPaddingTop(),
1133                 headerPaddingEnd,
1134                 mConversationHeader.getPaddingBottom());
1135 
1136         mContentContainer.setPaddingRelative(
1137                 mContentContainer.getPaddingStart(),
1138                 mContentContainer.getPaddingTop(),
1139                 contentPaddingEnd,
1140                 mContentContainer.getPaddingBottom());
1141     }
1142 
onAppNameVisibilityChanged()1143     private void onAppNameVisibilityChanged() {
1144         boolean appNameGone = mAppName.getVisibility() == GONE;
1145         if (appNameGone != mAppNameGone) {
1146             mAppNameGone = appNameGone;
1147             updateAppNameDividerVisibility();
1148         }
1149     }
1150 
updateAppNameDividerVisibility()1151     private void updateAppNameDividerVisibility() {
1152         mAppNameDivider.setVisibility(mAppNameGone ? GONE : VISIBLE);
1153     }
1154 
updateExpandability(boolean expandable, @Nullable OnClickListener onClickListener)1155     public void updateExpandability(boolean expandable, @Nullable OnClickListener onClickListener) {
1156         mExpandable = expandable;
1157         if (expandable) {
1158             mExpandButtonContainer.setVisibility(VISIBLE);
1159             mExpandButton.setOnClickListener(onClickListener);
1160             mConversationIconContainer.setOnClickListener(onClickListener);
1161         } else {
1162             mExpandButtonContainer.setVisibility(GONE);
1163             mConversationIconContainer.setOnClickListener(null);
1164         }
1165         mExpandButton.setVisibility(VISIBLE);
1166         updateContentEndPaddings();
1167     }
1168 
1169     @Override
setMessagingClippingDisabled(boolean clippingDisabled)1170     public void setMessagingClippingDisabled(boolean clippingDisabled) {
1171         mMessagingLinearLayout.setClipBounds(clippingDisabled ? null : mMessagingClipRect);
1172     }
1173 
1174     @Nullable
getConversationSenderName()1175     public CharSequence getConversationSenderName() {
1176         if (mGroups.isEmpty()) {
1177             return null;
1178         }
1179         final CharSequence name = mGroups.get(mGroups.size() - 1).getSenderName();
1180         return getResources().getString(R.string.conversation_single_line_name_display, name);
1181     }
1182 
isOneToOne()1183     public boolean isOneToOne() {
1184         return mIsOneToOne;
1185     }
1186 
1187     @Nullable
getConversationText()1188     public CharSequence getConversationText() {
1189         if (mMessages.isEmpty()) {
1190             return null;
1191         }
1192         final MessagingMessage messagingMessage = mMessages.get(mMessages.size() - 1);
1193         final CharSequence text = messagingMessage.getMessage().getText();
1194         if (text == null && messagingMessage instanceof MessagingImageMessage) {
1195             final String unformatted =
1196                     getResources().getString(R.string.conversation_single_line_image_placeholder);
1197             SpannableString spannableString = new SpannableString(unformatted);
1198             spannableString.setSpan(
1199                     new StyleSpan(Typeface.ITALIC),
1200                     0,
1201                     spannableString.length(),
1202                     Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
1203             return spannableString;
1204         }
1205         return text;
1206     }
1207 
1208     @Nullable
getConversationIcon()1209     public Icon getConversationIcon() {
1210         return mConversationIcon;
1211     }
1212 
1213     private static class TouchDelegateComposite extends TouchDelegate {
1214         private final ArrayList<TouchDelegate> mDelegates = new ArrayList<>();
1215 
TouchDelegateComposite(View view)1216         private TouchDelegateComposite(View view) {
1217             super(new Rect(), view);
1218         }
1219 
add(TouchDelegate delegate)1220         public void add(TouchDelegate delegate) {
1221             mDelegates.add(delegate);
1222         }
1223 
clear()1224         public void clear() {
1225             mDelegates.clear();
1226         }
1227 
1228         @Override
onTouchEvent(MotionEvent event)1229         public boolean onTouchEvent(MotionEvent event) {
1230             float x = event.getX();
1231             float y = event.getY();
1232             for (TouchDelegate delegate: mDelegates) {
1233                 event.setLocation(x, y);
1234                 if (delegate.onTouchEvent(event)) {
1235                     return true;
1236                 }
1237             }
1238             return false;
1239         }
1240     }
1241 }
1242