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