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