• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 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.systemui.statusbar.notification.stack;
18 
19 import android.app.Notification;
20 import android.content.Context;
21 import android.content.res.Configuration;
22 import android.content.res.Resources;
23 import android.content.res.TypedArray;
24 import android.graphics.Canvas;
25 import android.graphics.Path;
26 import android.graphics.Path.Direction;
27 import android.graphics.drawable.ColorDrawable;
28 import android.os.Trace;
29 import android.service.notification.StatusBarNotification;
30 import android.util.AttributeSet;
31 import android.util.Log;
32 import android.view.ContextThemeWrapper;
33 import android.view.LayoutInflater;
34 import android.view.NotificationHeaderView;
35 import android.view.View;
36 import android.view.ViewGroup;
37 import android.widget.RemoteViews;
38 import android.widget.TextView;
39 
40 import androidx.annotation.NonNull;
41 import androidx.annotation.Nullable;
42 
43 import com.android.internal.annotations.VisibleForTesting;
44 import com.android.internal.widget.NotificationExpandButton;
45 import com.android.systemui.res.R;
46 import com.android.systemui.statusbar.CrossFadeHelper;
47 import com.android.systemui.statusbar.NotificationGroupingUtil;
48 import com.android.systemui.statusbar.notification.FeedbackIcon;
49 import com.android.systemui.statusbar.notification.NotificationFadeAware;
50 import com.android.systemui.statusbar.notification.NotificationUtils;
51 import com.android.systemui.statusbar.notification.Roundable;
52 import com.android.systemui.statusbar.notification.RoundableState;
53 import com.android.systemui.statusbar.notification.SourceType;
54 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
55 import com.android.systemui.statusbar.notification.row.ExpandableView;
56 import com.android.systemui.statusbar.notification.row.HybridGroupManager;
57 import com.android.systemui.statusbar.notification.row.HybridNotificationView;
58 import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation;
59 import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation;
60 import com.android.systemui.statusbar.notification.row.wrapper.NotificationHeaderViewWrapper;
61 import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper;
62 
63 import java.util.ArrayList;
64 import java.util.List;
65 
66 /**
67  * A container containing child notifications
68  */
69 public class NotificationChildrenContainer extends ViewGroup
70         implements NotificationFadeAware, Roundable {
71 
72     private static final String TAG = "NotificationChildrenContainer";
73 
74     @VisibleForTesting
75     static final int NUMBER_OF_CHILDREN_WHEN_COLLAPSED = 2;
76     @VisibleForTesting
77     static final int NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED = 5;
78     public static final int NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED = 8;
79     private static final AnimationProperties ALPHA_FADE_IN = new AnimationProperties() {
80         private AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha();
81 
82         @Override
83         public AnimationFilter getAnimationFilter() {
84             return mAnimationFilter;
85         }
86     }.setDuration(200);
87     private static final SourceType FROM_PARENT = SourceType.from("FromParent(NCC)");
88 
89     private final List<View> mDividers = new ArrayList<>();
90     private final List<ExpandableNotificationRow> mAttachedChildren = new ArrayList<>();
91     private final HybridGroupManager mHybridGroupManager;
92     private int mChildPadding;
93     private int mDividerHeight;
94     private float mDividerAlpha;
95     private int mNotificationHeaderMargin;
96 
97     private int mNotificationTopPadding;
98     private float mCollapsedBottomPadding;
99     private boolean mChildrenExpanded;
100     private ExpandableNotificationRow mContainingNotification;
101     private TextView mOverflowNumber;
102     private ViewState mGroupOverFlowState;
103     private int mRealHeight;
104     private boolean mUserLocked;
105     private int mActualHeight;
106     private boolean mNeverAppliedGroupState;
107     private int mHeaderHeight;
108 
109     /**
110      * Whether or not individual notifications that are part of this container will have shadows.
111      */
112     private boolean mEnableShadowOnChildNotifications;
113 
114     private NotificationHeaderView mGroupHeader;
115     private NotificationHeaderViewWrapper mGroupHeaderWrapper;
116     private NotificationHeaderView mMinimizedGroupHeader;
117     private NotificationHeaderViewWrapper mMinimizedGroupHeaderWrapper;
118     private NotificationGroupingUtil mGroupingUtil;
119     private ViewState mHeaderViewState;
120     private int mClipBottomAmount;
121     private boolean mIsMinimized;
122     private OnClickListener mHeaderClickListener;
123     private ViewGroup mCurrentHeader;
124     private boolean mIsConversation;
125     private Path mChildClipPath = null;
126     private final Path mHeaderPath = new Path();
127     private boolean mShowGroupCountInExpander;
128     private boolean mShowDividersWhenExpanded;
129     private boolean mHideDividersDuringExpand;
130     private int mTranslationForHeader;
131     private int mCurrentHeaderTranslation = 0;
132     private float mHeaderVisibleAmount = 1.0f;
133     private int mUntruncatedChildCount;
134     private boolean mContainingNotificationIsFaded = false;
135     private RoundableState mRoundableState;
136     private int mMinSingleLineHeight;
137 
138     private NotificationChildrenContainerLogger mLogger;
139 
NotificationChildrenContainer(Context context)140     public NotificationChildrenContainer(Context context) {
141         this(context, null);
142     }
143 
NotificationChildrenContainer(Context context, AttributeSet attrs)144     public NotificationChildrenContainer(Context context, AttributeSet attrs) {
145         this(context, attrs, 0);
146     }
147 
NotificationChildrenContainer(Context context, AttributeSet attrs, int defStyleAttr)148     public NotificationChildrenContainer(Context context, AttributeSet attrs, int defStyleAttr) {
149         this(context, attrs, defStyleAttr, 0);
150     }
151 
NotificationChildrenContainer( Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)152     public NotificationChildrenContainer(
153             Context context,
154             AttributeSet attrs,
155             int defStyleAttr,
156             int defStyleRes) {
157         super(context, attrs, defStyleAttr, defStyleRes);
158         mHybridGroupManager = new HybridGroupManager(getContext());
159         mRoundableState = new RoundableState(this, this, 0f);
160         initDimens();
161         setClipChildren(false);
162     }
163 
initDimens()164     private void initDimens() {
165         Resources res = getResources();
166         mChildPadding = res.getDimensionPixelOffset(R.dimen.notification_children_padding);
167         mDividerHeight = res.getDimensionPixelOffset(
168                 R.dimen.notification_children_container_divider_height);
169         mDividerAlpha = res.getFloat(R.dimen.notification_divider_alpha);
170         mNotificationHeaderMargin = res.getDimensionPixelOffset(
171                 R.dimen.notification_children_container_margin_top);
172         mNotificationTopPadding = res.getDimensionPixelOffset(
173                 R.dimen.notification_children_container_top_padding);
174         mHeaderHeight = mNotificationHeaderMargin + mNotificationTopPadding;
175         mCollapsedBottomPadding = res.getDimensionPixelOffset(
176                 R.dimen.notification_children_collapsed_bottom_padding);
177         mEnableShadowOnChildNotifications =
178                 res.getBoolean(R.bool.config_enableShadowOnChildNotifications);
179         mShowGroupCountInExpander =
180                 res.getBoolean(R.bool.config_showNotificationGroupCountInExpander);
181         mShowDividersWhenExpanded =
182                 res.getBoolean(R.bool.config_showDividersWhenGroupNotificationExpanded);
183         mHideDividersDuringExpand =
184                 res.getBoolean(R.bool.config_hideDividersDuringExpand);
185         mTranslationForHeader = res.getDimensionPixelOffset(
186                 com.android.internal.R.dimen.notification_content_margin)
187                 - mNotificationHeaderMargin;
188         mHybridGroupManager.initDimens();
189         mMinSingleLineHeight = getResources().getDimensionPixelSize(
190                 R.dimen.conversation_single_line_face_pile_size);
191     }
192 
193     @NonNull
194     @Override
getRoundableState()195     public RoundableState getRoundableState() {
196         return mRoundableState;
197     }
198 
199     @Override
getClipHeight()200     public int getClipHeight() {
201         return Math.max(mActualHeight - mClipBottomAmount, 0);
202     }
203 
204     @Override
onLayout(boolean changed, int l, int t, int r, int b)205     protected void onLayout(boolean changed, int l, int t, int r, int b) {
206         int childCount =
207                 Math.min(mAttachedChildren.size(), NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED);
208         for (int i = 0; i < childCount; i++) {
209             View child = mAttachedChildren.get(i);
210             // We need to layout all children even the GONE ones, such that the heights are
211             // calculated correctly as they are used to calculate how many we can fit on the screen
212             child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight());
213             mDividers.get(i).layout(0, 0, getWidth(), mDividerHeight);
214         }
215         if (mOverflowNumber != null) {
216             boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
217             int left = (isRtl ? 0 : getWidth() - mOverflowNumber.getMeasuredWidth());
218             int right = left + mOverflowNumber.getMeasuredWidth();
219             mOverflowNumber.layout(left, 0, right, mOverflowNumber.getMeasuredHeight());
220         }
221         if (mGroupHeader != null) {
222             mGroupHeader.layout(0, 0, mGroupHeader.getMeasuredWidth(),
223                     mGroupHeader.getMeasuredHeight());
224         }
225         if (mMinimizedGroupHeader != null) {
226             mMinimizedGroupHeader.layout(0, 0,
227                     mMinimizedGroupHeader.getMeasuredWidth(),
228                     mMinimizedGroupHeader.getMeasuredHeight());
229         }
230     }
231 
232     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)233     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
234         Trace.beginSection("NotificationChildrenContainer#onMeasure");
235         int heightMode = MeasureSpec.getMode(heightMeasureSpec);
236         boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY;
237         boolean isHeightLimited = heightMode == MeasureSpec.AT_MOST;
238         int size = MeasureSpec.getSize(heightMeasureSpec);
239         int newHeightSpec = heightMeasureSpec;
240         if (hasFixedHeight || isHeightLimited) {
241             newHeightSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST);
242         }
243         int width = MeasureSpec.getSize(widthMeasureSpec);
244         if (mOverflowNumber != null) {
245             mOverflowNumber.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST),
246                     newHeightSpec);
247         }
248         int dividerHeightSpec = MeasureSpec.makeMeasureSpec(mDividerHeight, MeasureSpec.EXACTLY);
249         int height = mNotificationHeaderMargin + mNotificationTopPadding;
250         int childCount =
251                 Math.min(mAttachedChildren.size(), NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED);
252         int collapsedChildren = getMaxAllowedVisibleChildren(true /* likeCollapsed */);
253         int overflowIndex = childCount > collapsedChildren ? collapsedChildren - 1 : -1;
254         for (int i = 0; i < childCount; i++) {
255             ExpandableNotificationRow child = mAttachedChildren.get(i);
256             // We need to measure all children even the GONE ones, such that the heights are
257             // calculated correctly as they are used to calculate how many we can fit on the screen.
258             boolean isOverflow = i == overflowIndex;
259             child.setSingleLineWidthIndention(isOverflow && mOverflowNumber != null
260                     ? mOverflowNumber.getMeasuredWidth() : 0);
261             child.measure(widthMeasureSpec, newHeightSpec);
262             // layout the divider
263             View divider = mDividers.get(i);
264             divider.measure(widthMeasureSpec, dividerHeightSpec);
265             if (child.getVisibility() != GONE) {
266                 height += child.getMeasuredHeight() + mDividerHeight;
267             }
268         }
269         mRealHeight = height;
270         if (heightMode != MeasureSpec.UNSPECIFIED) {
271             height = Math.min(height, size);
272         }
273 
274         int headerHeightSpec = MeasureSpec.makeMeasureSpec(mHeaderHeight, MeasureSpec.EXACTLY);
275         if (mGroupHeader != null) {
276             mGroupHeader.measure(widthMeasureSpec, headerHeightSpec);
277         }
278         if (mMinimizedGroupHeader != null) {
279             mMinimizedGroupHeader.measure(widthMeasureSpec, headerHeightSpec);
280         }
281 
282         setMeasuredDimension(width, height);
283         Trace.endSection();
284     }
285 
286     @Override
hasOverlappingRendering()287     public boolean hasOverlappingRendering() {
288         return false;
289     }
290 
291     @Override
pointInView(float localX, float localY, float slop)292     public boolean pointInView(float localX, float localY, float slop) {
293         return localX >= -slop && localY >= -slop && localX < ((mRight - mLeft) + slop) &&
294                 localY < (mRealHeight + slop);
295     }
296 
297     /**
298      * Set the untruncated number of children in the group so that the view can update the UI
299      * appropriately. Note that this may differ from the number of views attached as truncated
300      * children will not have views.
301      */
setUntruncatedChildCount(int childCount)302     public void setUntruncatedChildCount(int childCount) {
303         mUntruncatedChildCount = childCount;
304         updateGroupOverflow();
305     }
306 
307     /**
308      * Set the notification time in the group so that the view can show the latest event in the UI
309      * appropriately.
310      */
setNotificationGroupWhen(long whenMillis)311     public void setNotificationGroupWhen(long whenMillis) {
312         if (mGroupHeaderWrapper != null) {
313             mGroupHeaderWrapper.setNotificationWhen(whenMillis);
314         }
315         if (mMinimizedGroupHeaderWrapper != null) {
316             mMinimizedGroupHeaderWrapper.setNotificationWhen(whenMillis);
317         }
318     }
319 
320     /**
321      * Add a child notification to this view.
322      *
323      * @param row        the row to add
324      * @param childIndex the index to add it at, if -1 it will be added at the end
325      */
addNotification(ExpandableNotificationRow row, int childIndex)326     public void addNotification(ExpandableNotificationRow row, int childIndex) {
327         ensureRemovedFromTransientContainer(row);
328         int newIndex = childIndex < 0 ? mAttachedChildren.size() : childIndex;
329         mAttachedChildren.add(newIndex, row);
330         addView(row);
331         row.setUserLocked(mUserLocked);
332 
333         View divider = inflateDivider();
334         addView(divider);
335         mDividers.add(newIndex, divider);
336 
337         row.setContentTransformationAmount(0, false /* isLastChild */);
338         row.setNotificationFaded(mContainingNotificationIsFaded);
339 
340         // It doesn't make sense to keep old animations around, lets cancel them!
341         ExpandableViewState viewState = row.getViewState();
342         if (viewState != null) {
343             viewState.cancelAnimations(row);
344             row.cancelAppearDrawing();
345         }
346 
347         applyRoundnessAndInvalidate();
348     }
349 
350     private void ensureRemovedFromTransientContainer(View v) {
351         if (v.getParent() != null && v instanceof ExpandableView) {
352             // If the child is animating away, it will still have a parent, so detach it first
353             // TODO: We should really cancel the active animations here. This will
354             //  happen automatically when the view's intro animation starts, but
355             //  it's a fragile link.
356             ((ExpandableView) v).removeFromTransientContainerForAdditionTo(this);
357         }
358     }
359 
360     public void removeNotification(ExpandableNotificationRow row) {
361         int childIndex = mAttachedChildren.indexOf(row);
362         mAttachedChildren.remove(row);
363         removeView(row);
364 
365         final View divider = mDividers.remove(childIndex);
366         removeView(divider);
367         getOverlay().add(divider);
368         CrossFadeHelper.fadeOut(divider, new Runnable() {
369             @Override
370             public void run() {
371                 getOverlay().remove(divider);
372             }
373         });
374 
375         row.setSystemChildExpanded(false);
376         row.setNotificationFaded(false);
377         row.setUserLocked(false);
378         if (!row.isRemoved()) {
379             mGroupingUtil.restoreChildNotification(row);
380         }
381 
382         row.requestRoundnessReset(FROM_PARENT, /* animate = */ false);
383         applyRoundnessAndInvalidate();
384     }
385 
386     /**
387      * @return The number of notification children in the container.
388      */
389     public int getNotificationChildCount() {
390         return mAttachedChildren.size();
391     }
392 
393     /**
394      * Re-create the Notification header view
395      * @param listener OnClickListener of the header view
396      * @param isConversation if the notification group is a conversation group
397      */
398     public void recreateNotificationHeader(
399             OnClickListener listener,
400             boolean isConversation
401     ) {
402         // We don't want to inflate headers from the main thread when async inflation enabled
403         AsyncGroupHeaderViewInflation.assertInLegacyMode();
404         // TODO(b/217799515): remove traces from this function in a follow-up change
405         Trace.beginSection("NotifChildCont#recreateHeader");
406         mHeaderClickListener = listener;
407         mIsConversation = isConversation;
408         StatusBarNotification notification = mContainingNotification.getEntry().getSbn();
409         final Notification.Builder builder = Notification.Builder.recoverBuilder(getContext(),
410                 notification.getNotification());
411         Trace.beginSection("recreateHeader#makeNotificationGroupHeader");
412         RemoteViews header = builder.makeNotificationGroupHeader();
413         Trace.endSection();
414         if (mGroupHeader == null) {
415             Trace.beginSection("recreateHeader#apply");
416             mGroupHeader = (NotificationHeaderView) header.apply(getContext(), this);
417             Trace.endSection();
418             mGroupHeader.findViewById(com.android.internal.R.id.expand_button)
419                     .setVisibility(VISIBLE);
420             mGroupHeader.setOnClickListener(mHeaderClickListener);
421             mGroupHeaderWrapper =
422                     (NotificationHeaderViewWrapper) NotificationViewWrapper.wrap(
423                             getContext(),
424                             mGroupHeader,
425                             mContainingNotification);
426             mGroupHeaderWrapper.setOnRoundnessChangedListener(this::invalidate);
427             addView(mGroupHeader, 0);
428             invalidate();
429         } else {
430             Trace.beginSection("recreateHeader#reapply");
431             header.reapply(getContext(), mGroupHeader);
432             Trace.endSection();
433         }
434         mGroupHeaderWrapper.setExpanded(mChildrenExpanded);
435         mGroupHeaderWrapper.onContentUpdated(mContainingNotification);
436         recreateLowPriorityHeader(builder, isConversation);
437         updateHeaderVisibility(false /* animate */);
438         updateChildrenAppearance();
439         Trace.endSection();
440     }
441 
442     /**
443      * Update the expand state of the group header.
444      */
445     public void updateGroupHeaderExpandState() {
446         if (mGroupHeaderWrapper != null) {
447             mGroupHeaderWrapper.setExpanded(mChildrenExpanded);
448         }
449     }
450 
451     private void removeGroupHeader() {
452         if (mGroupHeader == null) {
453             return;
454         }
455         removeView(mGroupHeader);
456         mGroupHeader = null;
457         mGroupHeaderWrapper = null;
458     }
459 
460     private void removeLowPriorityGroupHeader() {
461         if (mMinimizedGroupHeader == null) {
462             return;
463         }
464         removeView(mMinimizedGroupHeader);
465         mMinimizedGroupHeader = null;
466         mMinimizedGroupHeaderWrapper = null;
467     }
468 
469     /**
470      * Set the group header view
471      * @param headerView view to set
472      * @param onClickListener OnClickListener of the header view
473      */
474     public void setGroupHeader(
475             NotificationHeaderView headerView,
476             OnClickListener onClickListener
477     ) {
478         if (AsyncGroupHeaderViewInflation.isUnexpectedlyInLegacyMode()) return;
479         mHeaderClickListener = onClickListener;
480 
481         removeGroupHeader();
482 
483         if (headerView == null) {
484             return;
485         }
486 
487         mGroupHeader = headerView;
488         mGroupHeader.findViewById(com.android.internal.R.id.expand_button)
489                 .setVisibility(VISIBLE);
490         mGroupHeader.setOnClickListener(mHeaderClickListener);
491         mGroupHeaderWrapper =
492                 (NotificationHeaderViewWrapper) NotificationViewWrapper.wrap(
493                         getContext(),
494                         mGroupHeader,
495                         mContainingNotification);
496         mGroupHeaderWrapper.setOnRoundnessChangedListener(this::invalidate);
497         addView(mGroupHeader, 0);
498         invalidate();
499 
500         mGroupHeaderWrapper.setExpanded(mChildrenExpanded);
501         mGroupHeaderWrapper.onContentUpdated(mContainingNotification);
502 
503         updateHeaderVisibility(false /* animate */);
504         updateChildrenAppearance();
505 
506         Trace.endSection();
507     }
508 
509     /**
510      * Set the low-priority group header view
511      * @param headerViewLowPriority header view to set
512      * @param onClickListener OnClickListener of the header view
513      */
514     public void setLowPriorityGroupHeader(
515             NotificationHeaderView headerViewLowPriority,
516             OnClickListener onClickListener
517     ) {
518         if (AsyncGroupHeaderViewInflation.isUnexpectedlyInLegacyMode()) return;
519         removeLowPriorityGroupHeader();
520         if (headerViewLowPriority == null) {
521             return;
522         }
523 
524         mMinimizedGroupHeader = headerViewLowPriority;
525         mMinimizedGroupHeader.findViewById(com.android.internal.R.id.expand_button)
526                 .setVisibility(VISIBLE);
527         mMinimizedGroupHeader.setOnClickListener(onClickListener);
528         mMinimizedGroupHeaderWrapper =
529                 (NotificationHeaderViewWrapper) NotificationViewWrapper.wrap(
530                         getContext(),
531                         mMinimizedGroupHeader,
532                         mContainingNotification);
533         mMinimizedGroupHeaderWrapper.setOnRoundnessChangedListener(this::invalidate);
534         addView(mMinimizedGroupHeader, 0);
535         invalidate();
536 
537         mMinimizedGroupHeaderWrapper.onContentUpdated(mContainingNotification);
538         updateHeaderVisibility(false /* animate */);
539         updateChildrenAppearance();
540     }
541 
542     /**
543      * Recreate the low-priority header.
544      *
545      * @param builder a builder to reuse. Otherwise the builder will be recovered.
546      */
547     @VisibleForTesting
548     void recreateLowPriorityHeader(Notification.Builder builder, boolean isConversation) {
549         AsyncGroupHeaderViewInflation.assertInLegacyMode();
550         RemoteViews header;
551         StatusBarNotification notification = mContainingNotification.getEntry().getSbn();
552         if (mIsMinimized) {
553             if (builder == null) {
554                 builder = Notification.Builder.recoverBuilder(getContext(),
555                         notification.getNotification());
556             }
557             header = builder.makeLowPriorityContentView(true /* useRegularSubtext */);
558             if (mMinimizedGroupHeader == null) {
559                 mMinimizedGroupHeader = (NotificationHeaderView) header.apply(getContext(),
560                         this);
561                 mMinimizedGroupHeader.findViewById(com.android.internal.R.id.expand_button)
562                         .setVisibility(VISIBLE);
563                 mMinimizedGroupHeader.setOnClickListener(mHeaderClickListener);
564                 mMinimizedGroupHeaderWrapper =
565                         (NotificationHeaderViewWrapper) NotificationViewWrapper.wrap(
566                                 getContext(),
567                                 mMinimizedGroupHeader,
568                                 mContainingNotification);
569                 mGroupHeaderWrapper.setOnRoundnessChangedListener(this::invalidate);
570                 addView(mMinimizedGroupHeader, 0);
571                 invalidate();
572             } else {
573                 header.reapply(getContext(), mMinimizedGroupHeader);
574             }
575             mMinimizedGroupHeaderWrapper.onContentUpdated(mContainingNotification);
576             resetHeaderVisibilityIfNeeded(mMinimizedGroupHeader, calculateDesiredHeader());
577         } else {
578             removeView(mMinimizedGroupHeader);
579             mMinimizedGroupHeader = null;
580             mMinimizedGroupHeaderWrapper = null;
581         }
582     }
583 
584     /**
585      * Update the appearance of the children to reduce redundancies.
586      */
587     public void updateChildrenAppearance() {
588         mGroupingUtil.updateChildrenAppearance();
589     }
590 
591     private void setExpandButtonNumber(NotificationViewWrapper wrapper) {
592         View expandButton = wrapper == null
593                 ? null : wrapper.getExpandButton();
594         if (expandButton instanceof NotificationExpandButton) {
595             ((NotificationExpandButton) expandButton).setNumber(mUntruncatedChildCount);
596         }
597     }
598 
599     public void updateGroupOverflow() {
600         if (mShowGroupCountInExpander) {
601             setExpandButtonNumber(mGroupHeaderWrapper);
602             setExpandButtonNumber(mMinimizedGroupHeaderWrapper);
603             return;
604         }
605         int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* likeCollapsed */);
606         if (mUntruncatedChildCount > maxAllowedVisibleChildren) {
607             int number = mUntruncatedChildCount - maxAllowedVisibleChildren;
608             mOverflowNumber = mHybridGroupManager.bindOverflowNumber(mOverflowNumber, number, this);
609             if (mGroupOverFlowState == null) {
610                 mGroupOverFlowState = new ViewState();
611                 mNeverAppliedGroupState = true;
612             }
613         } else if (mOverflowNumber != null) {
614             removeView(mOverflowNumber);
615             if (isShown() && isAttachedToWindow()) {
616                 final View removedOverflowNumber = mOverflowNumber;
617                 addTransientView(removedOverflowNumber, getTransientViewCount());
618                 CrossFadeHelper.fadeOut(removedOverflowNumber, new Runnable() {
619                     @Override
620                     public void run() {
621                         removeTransientView(removedOverflowNumber);
622                     }
623                 });
624             }
625             mOverflowNumber = null;
626             mGroupOverFlowState = null;
627         }
628     }
629 
630     @Override
631     protected void onConfigurationChanged(Configuration newConfig) {
632         super.onConfigurationChanged(newConfig);
633         updateGroupOverflow();
634     }
635 
636     private View inflateDivider() {
637         return LayoutInflater.from(mContext).inflate(
638                 R.layout.notification_children_divider, this, false);
639     }
640 
641     /**
642      * Get notification children that are attached currently.
643      */
644     public List<ExpandableNotificationRow> getAttachedChildren() {
645         return mAttachedChildren;
646     }
647 
648     /**
649      * Sets the alpha on the content, while leaving the background of the container itself as is.
650      *
651      * @param alpha alpha value to apply to the content
652      */
653     public void setContentAlpha(float alpha) {
654         if (mGroupHeader != null) {
655             for (int i = 0; i < mGroupHeader.getChildCount(); i++) {
656                 mGroupHeader.getChildAt(i).setAlpha(alpha);
657             }
658         }
659         for (ExpandableNotificationRow child : getAttachedChildren()) {
660             child.setContentAlpha(alpha);
661         }
662     }
663 
664     /**
665      * To be called any time the rows have been updated
666      */
667     public void updateExpansionStates() {
668         if (mChildrenExpanded || mUserLocked) {
669             // we don't modify it the group is expanded or if we are expanding it
670             return;
671         }
672         int size = mAttachedChildren.size();
673         for (int i = 0; i < size; i++) {
674             ExpandableNotificationRow child = mAttachedChildren.get(i);
675             child.setSystemChildExpanded(i == 0 && size == 1);
676         }
677     }
678 
679     /**
680      * @return the intrinsic size of this children container, i.e the natural fully expanded state
681      */
682     public int getIntrinsicHeight() {
683         int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren();
684         return getIntrinsicHeight(maxAllowedVisibleChildren);
685     }
686 
687     /**
688      * @return the intrinsic height with a number of children given
689      * in @param maxAllowedVisibleChildren
690      */
691     private int getIntrinsicHeight(float maxAllowedVisibleChildren) {
692         if (showingAsLowPriority()) {
693             if (AsyncGroupHeaderViewInflation.isEnabled()) {
694                 return mHeaderHeight;
695             } else {
696                 return mMinimizedGroupHeader.getHeight();
697             }
698         }
699         int intrinsicHeight = mNotificationHeaderMargin + mCurrentHeaderTranslation;
700         int visibleChildren = 0;
701         int childCount = mAttachedChildren.size();
702         boolean firstChild = true;
703         float expandFactor = 0;
704         if (mUserLocked) {
705             expandFactor = getGroupExpandFraction();
706         }
707         boolean childrenExpanded = mChildrenExpanded;
708         for (int i = 0; i < childCount; i++) {
709             if (visibleChildren >= maxAllowedVisibleChildren) {
710                 break;
711             }
712             if (!firstChild) {
713                 if (mUserLocked) {
714                     intrinsicHeight += NotificationUtils.interpolate(mChildPadding, mDividerHeight,
715                             expandFactor);
716                 } else {
717                     intrinsicHeight += childrenExpanded ? mDividerHeight : mChildPadding;
718                 }
719             } else {
720                 if (mUserLocked) {
721                     intrinsicHeight += NotificationUtils.interpolate(
722                             0,
723                             mNotificationTopPadding + mDividerHeight,
724                             expandFactor);
725                 } else {
726                     intrinsicHeight += childrenExpanded
727                             ? mNotificationTopPadding + mDividerHeight
728                             : 0;
729                 }
730                 firstChild = false;
731             }
732             ExpandableNotificationRow child = mAttachedChildren.get(i);
733             intrinsicHeight += child.getIntrinsicHeight();
734             visibleChildren++;
735         }
736         if (mUserLocked) {
737             intrinsicHeight += NotificationUtils.interpolate(mCollapsedBottomPadding, 0.0f,
738                     expandFactor);
739         } else if (!childrenExpanded) {
740             intrinsicHeight += mCollapsedBottomPadding;
741         }
742         return intrinsicHeight;
743     }
744 
745     /**
746      * Update the state of all its children based on a linear layout algorithm.
747      *
748      * @param parentState  the state of the parent
749      */
750     public void updateState(ExpandableViewState parentState) {
751         int childCount = mAttachedChildren.size();
752         int yPosition = mNotificationHeaderMargin + mCurrentHeaderTranslation;
753         boolean firstChild = true;
754         int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren();
755         int lastVisibleIndex = maxAllowedVisibleChildren - 1;
756         int firstOverflowIndex = lastVisibleIndex + 1;
757         float expandFactor = 0;
758         boolean expandingToExpandedGroup = mUserLocked && !showingAsLowPriority();
759         if (mUserLocked) {
760             expandFactor = getGroupExpandFraction();
761             firstOverflowIndex = getMaxAllowedVisibleChildren(true /* likeCollapsed */);
762         }
763 
764         boolean childrenExpandedAndNotAnimating = mChildrenExpanded
765                 && !mContainingNotification.isGroupExpansionChanging();
766         int launchTransitionCompensation = 0;
767         for (int i = 0; i < childCount; i++) {
768             ExpandableNotificationRow child = mAttachedChildren.get(i);
769             if (!firstChild) {
770                 if (expandingToExpandedGroup) {
771                     yPosition += NotificationUtils.interpolate(mChildPadding, mDividerHeight,
772                             expandFactor);
773                 } else {
774                     yPosition += mChildrenExpanded ? mDividerHeight : mChildPadding;
775                 }
776             } else {
777                 if (expandingToExpandedGroup) {
778                     yPosition += NotificationUtils.interpolate(
779                             0,
780                             mNotificationTopPadding + mDividerHeight,
781                             expandFactor);
782                 } else {
783                     yPosition += mChildrenExpanded ? mNotificationTopPadding + mDividerHeight : 0;
784                 }
785                 firstChild = false;
786             }
787 
788             ExpandableViewState childState = child.getViewState();
789             int intrinsicHeight = child.getIntrinsicHeight();
790             childState.height = intrinsicHeight;
791             childState.setYTranslation(yPosition + launchTransitionCompensation);
792             childState.hidden = false;
793             if (child.isExpandAnimationRunning() || mContainingNotification.hasExpandingChild()) {
794                 // Not modifying translationZ during launch animation. The translationZ of the
795                 // expanding child is handled inside ExpandableNotificationRow and the translationZ
796                 // of the other children inside the group should remain unchanged. In particular,
797                 // they should not take over the translationZ of the parent, since the parent has
798                 // a positive translationZ set only for the expanding child to be drawn above other
799                 // notifications.
800                 childState.setZTranslation(child.getTranslationZ());
801             } else if (childrenExpandedAndNotAnimating && mEnableShadowOnChildNotifications) {
802                 // When the group is expanded, the children cast the shadows rather than the parent
803                 // so use the parent's elevation here.
804                 childState.setZTranslation(parentState.getZTranslation());
805             } else {
806                 childState.setZTranslation(0);
807             }
808             childState.hideSensitive = parentState.hideSensitive;
809             childState.belowSpeedBump = parentState.belowSpeedBump;
810             childState.clipTopAmount = 0;
811             childState.setAlpha(0);
812             if (i < firstOverflowIndex) {
813                 childState.setAlpha(showingAsLowPriority() ? expandFactor : 1.0f);
814             } else if (expandFactor == 1.0f && i <= lastVisibleIndex) {
815                 childState.setAlpha(
816                         (mActualHeight - childState.getYTranslation()) / childState.height);
817                 childState.setAlpha(Math.max(0.0f, Math.min(1.0f, childState.getAlpha())));
818             }
819             childState.location = parentState.location;
820             childState.inShelf = parentState.inShelf;
821             yPosition += intrinsicHeight;
822         }
823         if (mOverflowNumber != null) {
824             ExpandableNotificationRow overflowView = mAttachedChildren.get(Math.min(
825                     getMaxAllowedVisibleChildren(true /* likeCollapsed */), childCount) - 1);
826             mGroupOverFlowState.copyFrom(overflowView.getViewState());
827 
828             if (!mChildrenExpanded) {
829                 HybridNotificationView alignView = overflowView.getSingleLineView();
830                 if (alignView != null) {
831                     View mirrorView = alignView.getTextView();
832                     if (mirrorView.getVisibility() == GONE) {
833                         mirrorView = alignView.getTitleView();
834                     }
835                     if (mirrorView.getVisibility() == GONE) {
836                         mirrorView = alignView;
837                     }
838                     mGroupOverFlowState.setAlpha(mirrorView.getAlpha());
839                     float yTranslation = mGroupOverFlowState.getYTranslation()
840                             + NotificationUtils.getRelativeYOffset(
841                             mirrorView, overflowView);
842                     mGroupOverFlowState.setYTranslation(yTranslation);
843                 }
844             } else {
845                 mGroupOverFlowState.setYTranslation(
846                         mGroupOverFlowState.getYTranslation() + mNotificationHeaderMargin);
847                 mGroupOverFlowState.setAlpha(0.0f);
848             }
849         }
850         if (mGroupHeader != null) {
851             if (mHeaderViewState == null) {
852                 mHeaderViewState = new ViewState();
853             }
854             mHeaderViewState.initFrom(mGroupHeader);
855 
856             if (mContainingNotification.hasExpandingChild()) {
857                 // Not modifying translationZ during expand animation.
858                 mHeaderViewState.setZTranslation(mGroupHeader.getTranslationZ());
859             } else if (childrenExpandedAndNotAnimating) {
860                 mHeaderViewState.setZTranslation(parentState.getZTranslation());
861             } else {
862                 mHeaderViewState.setZTranslation(0);
863             }
864             mHeaderViewState.setYTranslation(mCurrentHeaderTranslation);
865             mHeaderViewState.setAlpha(mHeaderVisibleAmount);
866             // The hiding is done automatically by the alpha, otherwise we'll pick it up again
867             // in the next frame with the initFrom call above and have an invisible header
868             mHeaderViewState.hidden = false;
869         }
870     }
871 
872     /**
873      * When moving into the bottom stack, the bottom visible child in an expanded group adjusts its
874      * height, children in the group after this are gone.
875      *
876      * @param child        the child who's height to adjust.
877      * @param parentHeight the height of the parent.
878      * @param childState   the state to update.
879      * @param yPosition    the yPosition of the view.
880      * @return true if children after this one should be hidden.
881      */
882     private boolean updateChildStateForExpandedGroup(
883             ExpandableNotificationRow child,
884             int parentHeight,
885             ExpandableViewState childState,
886             int yPosition) {
887         final int top = yPosition + child.getClipTopAmount();
888         final int intrinsicHeight = child.getIntrinsicHeight();
889         final int bottom = top + intrinsicHeight;
890         int newHeight = intrinsicHeight;
891         if (bottom >= parentHeight) {
892             // Child is either clipped or gone
893             newHeight = Math.max((parentHeight - top), 0);
894         }
895         childState.hidden = newHeight == 0;
896         childState.height = newHeight;
897         return childState.height != intrinsicHeight && !childState.hidden;
898     }
899 
900     @VisibleForTesting
901     int getMaxAllowedVisibleChildren() {
902         return getMaxAllowedVisibleChildren(false /* likeCollapsed */);
903     }
904 
905     @VisibleForTesting
906     int getMaxAllowedVisibleChildren(boolean likeCollapsed) {
907         if (!likeCollapsed && (mChildrenExpanded || mContainingNotification.isUserLocked())
908                 && !showingAsLowPriority()) {
909             return NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED;
910         }
911         if (mIsMinimized
912                 || (!mContainingNotification.isOnKeyguard() && mContainingNotification.isExpanded())
913                 || (mContainingNotification.isHeadsUpState()
914                 && mContainingNotification.canShowHeadsUp())) {
915             return NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED;
916         }
917         return NUMBER_OF_CHILDREN_WHEN_COLLAPSED;
918     }
919 
920     /**
921      * Applies state to children.
922      */
923     public void applyState() {
924         int childCount = mAttachedChildren.size();
925         ViewState tmpState = new ViewState();
926         float expandFraction = 0.0f;
927         if (mUserLocked) {
928             expandFraction = getGroupExpandFraction();
929         }
930         final boolean isExpanding = !showingAsLowPriority()
931                 && (mUserLocked || mContainingNotification.isGroupExpansionChanging());
932         final boolean dividersVisible = (mChildrenExpanded && mShowDividersWhenExpanded)
933                 || (isExpanding && !mHideDividersDuringExpand);
934         for (int i = 0; i < childCount; i++) {
935             ExpandableNotificationRow child = mAttachedChildren.get(i);
936             ExpandableViewState viewState = child.getViewState();
937             viewState.applyToView(child);
938 
939             // layout the divider
940             View divider = mDividers.get(i);
941             tmpState.initFrom(divider);
942             tmpState.setYTranslation(viewState.getYTranslation() - mDividerHeight);
943             float alpha = mChildrenExpanded && viewState.getAlpha() != 0 ? mDividerAlpha : 0;
944             if (mUserLocked && !showingAsLowPriority() && viewState.getAlpha() != 0) {
945                 alpha = NotificationUtils.interpolate(0, mDividerAlpha,
946                         Math.min(viewState.getAlpha(), expandFraction));
947             }
948             tmpState.hidden = !dividersVisible;
949             tmpState.setAlpha(alpha);
950             tmpState.applyToView(divider);
951             // There is no fake shadow to be drawn on the children
952             child.setFakeShadowIntensity(0.0f, 0.0f, 0, 0);
953         }
954         if (mGroupOverFlowState != null) {
955             mGroupOverFlowState.applyToView(mOverflowNumber);
956             mNeverAppliedGroupState = false;
957         }
958         if (mHeaderViewState != null) {
959             mHeaderViewState.applyToView(mGroupHeader);
960         }
961         updateChildrenClipping();
962     }
963 
964     private void updateChildrenClipping() {
965         if (mContainingNotification.hasExpandingChild()) {
966             return;
967         }
968         int childCount = mAttachedChildren.size();
969         int layoutEnd = mContainingNotification.getActualHeight() - mClipBottomAmount;
970         for (int i = 0; i < childCount; i++) {
971             ExpandableNotificationRow child = mAttachedChildren.get(i);
972             if (child.getVisibility() == GONE) {
973                 continue;
974             }
975             float childTop = child.getTranslationY();
976             float childBottom = childTop + child.getActualHeight();
977             boolean visible = true;
978             int clipBottomAmount = 0;
979             if (childTop > layoutEnd) {
980                 visible = false;
981             } else if (childBottom > layoutEnd) {
982                 clipBottomAmount = (int) (childBottom - layoutEnd);
983             }
984 
985             boolean isVisible = child.getVisibility() == VISIBLE;
986             if (visible != isVisible) {
987                 child.setVisibility(visible ? VISIBLE : INVISIBLE);
988             }
989 
990             child.setClipBottomAmount(clipBottomAmount);
991         }
992     }
993 
994     @Override
995     protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
996         boolean isCanvasChanged = false;
997 
998         Path clipPath = mChildClipPath;
999         if (clipPath != null) {
1000             final float translation;
1001             if (child instanceof ExpandableNotificationRow notificationRow) {
1002                 translation = notificationRow.getTranslation();
1003             } else {
1004                 translation = child.getTranslationX();
1005             }
1006 
1007             isCanvasChanged = true;
1008             canvas.save();
1009             if (translation != 0f) {
1010                 clipPath.offset(translation, 0f);
1011                 canvas.clipPath(clipPath);
1012                 clipPath.offset(-translation, 0f);
1013             } else {
1014                 canvas.clipPath(clipPath);
1015             }
1016         }
1017 
1018         if (child instanceof NotificationHeaderView
1019                 && mGroupHeaderWrapper.hasRoundedCorner()) {
1020             float[] radii = mGroupHeaderWrapper.getUpdatedRadii();
1021             mHeaderPath.reset();
1022             mHeaderPath.addRoundRect(
1023                     child.getLeft(),
1024                     child.getTop(),
1025                     child.getRight(),
1026                     child.getBottom(),
1027                     radii,
1028                     Direction.CW
1029             );
1030             if (!isCanvasChanged) {
1031                 isCanvasChanged = true;
1032                 canvas.save();
1033             }
1034             canvas.clipPath(mHeaderPath);
1035         }
1036 
1037         if (isCanvasChanged) {
1038             boolean result = super.drawChild(canvas, child, drawingTime);
1039             canvas.restore();
1040             return result;
1041         } else {
1042             // If there have been no changes to the canvas we can proceed as usual
1043             return super.drawChild(canvas, child, drawingTime);
1044         }
1045     }
1046 
1047 
1048     /**
1049      * This is called when the children expansion has changed and positions the children properly
1050      * for an appear animation.
1051      */
1052     public void prepareExpansionChanged() {
1053         // TODO: do something that makes sense, like placing the invisible views correctly
1054         return;
1055     }
1056 
1057     /**
1058      * Animate to a given state.
1059      */
1060     public void startAnimationToState(AnimationProperties properties) {
1061         int childCount = mAttachedChildren.size();
1062         ViewState tmpState = new ViewState();
1063         float expandFraction = getGroupExpandFraction();
1064         final boolean isExpanding = !showingAsLowPriority()
1065                 && (mUserLocked || mContainingNotification.isGroupExpansionChanging());
1066         final boolean dividersVisible = (mChildrenExpanded && mShowDividersWhenExpanded)
1067                 || (isExpanding && !mHideDividersDuringExpand);
1068         for (int i = childCount - 1; i >= 0; i--) {
1069             ExpandableNotificationRow child = mAttachedChildren.get(i);
1070             ExpandableViewState viewState = child.getViewState();
1071             viewState.animateTo(child, properties);
1072 
1073             // layout the divider
1074             View divider = mDividers.get(i);
1075             tmpState.initFrom(divider);
1076             tmpState.setYTranslation(viewState.getYTranslation() - mDividerHeight);
1077             float alpha = mChildrenExpanded && viewState.getAlpha() != 0 ? mDividerAlpha : 0;
1078             if (mUserLocked && !showingAsLowPriority() && viewState.getAlpha() != 0) {
1079                 alpha = NotificationUtils.interpolate(0, mDividerAlpha,
1080                         Math.min(viewState.getAlpha(), expandFraction));
1081             }
1082             tmpState.hidden = !dividersVisible;
1083             tmpState.setAlpha(alpha);
1084             tmpState.animateTo(divider, properties);
1085             // There is no fake shadow to be drawn on the children
1086             child.setFakeShadowIntensity(0.0f, 0.0f, 0, 0);
1087         }
1088         if (mOverflowNumber != null) {
1089             if (mNeverAppliedGroupState) {
1090                 float alpha = mGroupOverFlowState.getAlpha();
1091                 mGroupOverFlowState.setAlpha(0);
1092                 mGroupOverFlowState.applyToView(mOverflowNumber);
1093                 mGroupOverFlowState.setAlpha(alpha);
1094                 mNeverAppliedGroupState = false;
1095             }
1096             mGroupOverFlowState.animateTo(mOverflowNumber, properties);
1097         }
1098         if (mGroupHeader != null) {
1099             mHeaderViewState.applyToView(mGroupHeader);
1100         }
1101         updateChildrenClipping();
1102     }
1103 
1104     public ExpandableNotificationRow getViewAtPosition(float y) {
1105         // find the view under the pointer, accounting for GONE views
1106         final int count = mAttachedChildren.size();
1107         for (int childIdx = 0; childIdx < count; childIdx++) {
1108             ExpandableNotificationRow slidingChild = mAttachedChildren.get(childIdx);
1109             float childTop = slidingChild.getTranslationY();
1110             float top = childTop + Math.max(0, slidingChild.getClipTopAmount());
1111             float bottom = childTop + slidingChild.getActualHeight();
1112             if (y >= top && y <= bottom) {
1113                 return slidingChild;
1114             }
1115         }
1116         return null;
1117     }
1118 
1119     public void setChildrenExpanded(boolean childrenExpanded) {
1120         mChildrenExpanded = childrenExpanded;
1121         updateExpansionStates();
1122         if (mGroupHeaderWrapper != null) {
1123             mGroupHeaderWrapper.setExpanded(childrenExpanded);
1124         }
1125         final int count = mAttachedChildren.size();
1126         for (int childIdx = 0; childIdx < count; childIdx++) {
1127             ExpandableNotificationRow child = mAttachedChildren.get(childIdx);
1128             child.setChildrenExpanded(childrenExpanded, false);
1129         }
1130         updateHeaderTouchability();
1131     }
1132 
1133     public void setContainingNotification(ExpandableNotificationRow parent) {
1134         mContainingNotification = parent;
1135         mGroupingUtil = new NotificationGroupingUtil(mContainingNotification);
1136     }
1137 
1138     public ExpandableNotificationRow getContainingNotification() {
1139         return mContainingNotification;
1140     }
1141 
1142     public NotificationViewWrapper getNotificationViewWrapper() {
1143         return mGroupHeaderWrapper;
1144     }
1145 
1146     public NotificationViewWrapper getMinimizedGroupHeaderWrapper() {
1147         return mMinimizedGroupHeaderWrapper;
1148     }
1149 
1150     @VisibleForTesting
1151     public ViewGroup getCurrentHeaderView() {
1152         return mCurrentHeader;
1153     }
1154 
1155     public NotificationHeaderView getGroupHeader() {
1156         return mGroupHeader;
1157     }
1158 
1159     public NotificationHeaderView getMinimizedNotificationHeader() {
1160         return mMinimizedGroupHeader;
1161     }
1162 
1163     private void updateHeaderVisibility(boolean animate) {
1164         ViewGroup desiredHeader;
1165         ViewGroup currentHeader = mCurrentHeader;
1166         desiredHeader = calculateDesiredHeader();
1167 
1168         if (currentHeader == desiredHeader) {
1169             return;
1170         }
1171 
1172         if (AsyncGroupHeaderViewInflation.isEnabled() && desiredHeader == null) {
1173             return;
1174         }
1175 
1176         if (animate) {
1177             if (desiredHeader != null && currentHeader != null) {
1178                 currentHeader.setVisibility(VISIBLE);
1179                 desiredHeader.setVisibility(VISIBLE);
1180                 NotificationViewWrapper visibleWrapper = getWrapperForView(desiredHeader);
1181                 NotificationViewWrapper hiddenWrapper = getWrapperForView(currentHeader);
1182                 visibleWrapper.transformFrom(hiddenWrapper);
1183                 hiddenWrapper.transformTo(visibleWrapper, () -> updateHeaderVisibility(false));
1184                 startChildAlphaAnimations(desiredHeader == mGroupHeader);
1185             } else {
1186                 animate = false;
1187             }
1188         }
1189         if (!animate) {
1190             if (desiredHeader != null) {
1191                 getWrapperForView(desiredHeader).setVisible(true);
1192                 desiredHeader.setVisibility(VISIBLE);
1193             }
1194             if (currentHeader != null) {
1195                 // Wrapper can be null if we were a low priority notification
1196                 // and just destroyed it by calling setIsLowPriority(false)
1197                 NotificationViewWrapper wrapper = getWrapperForView(currentHeader);
1198                 if (wrapper != null) {
1199                     wrapper.setVisible(false);
1200                 }
1201                 currentHeader.setVisibility(INVISIBLE);
1202             }
1203         }
1204 
1205         resetHeaderVisibilityIfNeeded(mGroupHeader, desiredHeader);
1206         resetHeaderVisibilityIfNeeded(mMinimizedGroupHeader, desiredHeader);
1207 
1208         mCurrentHeader = desiredHeader;
1209     }
1210 
1211     private void resetHeaderVisibilityIfNeeded(View header, View desiredHeader) {
1212         if (header == null) {
1213             return;
1214         }
1215         if (header != mCurrentHeader && header != desiredHeader) {
1216             getWrapperForView(header).setVisible(false);
1217             header.setVisibility(INVISIBLE);
1218         }
1219         if (header == desiredHeader && header.getVisibility() != VISIBLE) {
1220             getWrapperForView(header).setVisible(true);
1221             header.setVisibility(VISIBLE);
1222         }
1223     }
1224 
1225     private ViewGroup calculateDesiredHeader() {
1226         ViewGroup desiredHeader;
1227         if (showingAsLowPriority()) {
1228             desiredHeader = mMinimizedGroupHeader;
1229         } else {
1230             desiredHeader = mGroupHeader;
1231         }
1232         return desiredHeader;
1233     }
1234 
1235     private void startChildAlphaAnimations(boolean toVisible) {
1236         float target = toVisible ? 1.0f : 0.0f;
1237         float start = 1.0f - target;
1238         int childCount = mAttachedChildren.size();
1239         for (int i = 0; i < childCount; i++) {
1240             if (i >= NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED) {
1241                 break;
1242             }
1243             ExpandableNotificationRow child = mAttachedChildren.get(i);
1244             child.setAlpha(start);
1245             ViewState viewState = new ViewState();
1246             viewState.initFrom(child);
1247             viewState.setAlpha(target);
1248             ALPHA_FADE_IN.setDelay(i * 50);
1249             viewState.animateTo(child, ALPHA_FADE_IN);
1250         }
1251     }
1252 
1253 
1254     private void updateHeaderTransformation() {
1255         if (mUserLocked && showingAsLowPriority()) {
1256             float fraction = getGroupExpandFraction();
1257             mGroupHeaderWrapper.transformFrom(mMinimizedGroupHeaderWrapper,
1258                     fraction);
1259             mGroupHeader.setVisibility(VISIBLE);
1260             mMinimizedGroupHeaderWrapper.transformTo(mGroupHeaderWrapper,
1261                     fraction);
1262         }
1263 
1264     }
1265 
1266     private NotificationViewWrapper getWrapperForView(View visibleHeader) {
1267         if (visibleHeader == mGroupHeader) {
1268             return mGroupHeaderWrapper;
1269         }
1270         return mMinimizedGroupHeaderWrapper;
1271     }
1272 
1273     /**
1274      * Called when a groups expansion changes to adjust the background of the header view.
1275      *
1276      * @param expanded whether the group is expanded.
1277      */
1278     public void updateHeaderForExpansion(boolean expanded) {
1279         if (mGroupHeader != null) {
1280             if (expanded) {
1281                 ColorDrawable cd = new ColorDrawable();
1282                 cd.setColor(mContainingNotification.calculateBgColor());
1283                 mGroupHeader.setHeaderBackgroundDrawable(cd);
1284             } else {
1285                 mGroupHeader.setHeaderBackgroundDrawable(null);
1286             }
1287         }
1288     }
1289 
1290     public int getMaxContentHeight() {
1291         if (showingAsLowPriority()) {
1292             return getMinHeight(NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED, true
1293                     /* likeHighPriority */);
1294         }
1295         int maxContentHeight = mNotificationHeaderMargin + mCurrentHeaderTranslation
1296                 + mNotificationTopPadding;
1297         int visibleChildren = 0;
1298         int childCount = mAttachedChildren.size();
1299         for (int i = 0; i < childCount; i++) {
1300             if (visibleChildren >= NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED) {
1301                 break;
1302             }
1303             ExpandableNotificationRow child = mAttachedChildren.get(i);
1304             float childHeight = child.isExpanded(true /* allowOnKeyguard */)
1305                     ? child.getMaxExpandHeight()
1306                     : child.getShowingLayout().getMinHeight(true /* likeGroupExpanded */);
1307             maxContentHeight += childHeight;
1308             visibleChildren++;
1309         }
1310         if (visibleChildren > 0) {
1311             maxContentHeight += visibleChildren * mDividerHeight;
1312         }
1313         return maxContentHeight;
1314     }
1315 
1316     public void setActualHeight(int actualHeight) {
1317         if (!mUserLocked) {
1318             return;
1319         }
1320         mActualHeight = actualHeight;
1321         float fraction = getGroupExpandFraction();
1322         boolean showingLowPriority = showingAsLowPriority();
1323         updateHeaderTransformation();
1324         int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* forceCollapsed */);
1325         int childCount = mAttachedChildren.size();
1326         for (int i = 0; i < childCount; i++) {
1327             ExpandableNotificationRow child = mAttachedChildren.get(i);
1328             float childHeight;
1329             if (showingLowPriority) {
1330                 childHeight = child.getShowingLayout().getMinHeight(false /* likeGroupExpanded */);
1331             } else if (child.isExpanded(true /* allowOnKeyguard */)) {
1332                 childHeight = child.getMaxExpandHeight();
1333             } else {
1334                 childHeight = child.getShowingLayout().getMinHeight(
1335                         true /* likeGroupExpanded */);
1336             }
1337             if (i < maxAllowedVisibleChildren) {
1338                 float singleLineHeight = child.getShowingLayout().getMinHeight(
1339                         false /* likeGroupExpanded */);
1340                 child.setActualHeight((int) NotificationUtils.interpolate(singleLineHeight,
1341                         childHeight, fraction), false);
1342             } else {
1343                 child.setActualHeight((int) childHeight, false);
1344             }
1345         }
1346     }
1347 
1348     public float getGroupExpandFraction() {
1349         int visibleChildrenExpandedHeight = showingAsLowPriority() ? getMaxContentHeight()
1350                 : getVisibleChildrenExpandHeight();
1351         int minExpandHeight = getCollapsedHeight();
1352         float factor = (mActualHeight - minExpandHeight)
1353                 / (float) (visibleChildrenExpandedHeight - minExpandHeight);
1354         return Math.max(0.0f, Math.min(1.0f, factor));
1355     }
1356 
1357     private int getVisibleChildrenExpandHeight() {
1358         int intrinsicHeight = mNotificationHeaderMargin + mCurrentHeaderTranslation
1359                 + mNotificationTopPadding + mDividerHeight;
1360         int visibleChildren = 0;
1361         int childCount = mAttachedChildren.size();
1362         int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* forceCollapsed */);
1363         for (int i = 0; i < childCount; i++) {
1364             if (visibleChildren >= maxAllowedVisibleChildren) {
1365                 break;
1366             }
1367             ExpandableNotificationRow child = mAttachedChildren.get(i);
1368             float childHeight = child.isExpanded(true /* allowOnKeyguard */)
1369                     ? child.getMaxExpandHeight()
1370                     : child.getShowingLayout().getMinHeight(true /* likeGroupExpanded */);
1371             intrinsicHeight += childHeight;
1372             visibleChildren++;
1373         }
1374         return intrinsicHeight;
1375     }
1376 
1377     public int getMinHeight() {
1378         return getMinHeight(NUMBER_OF_CHILDREN_WHEN_COLLAPSED, false /* likeHighPriority */);
1379     }
1380 
1381     public int getCollapsedHeight() {
1382         return getMinHeight(getMaxAllowedVisibleChildren(true /* forceCollapsed */),
1383                 false /* likeHighPriority */);
1384     }
1385 
1386     public int getCollapsedHeightWithoutHeader() {
1387         return getMinHeight(getMaxAllowedVisibleChildren(true /* forceCollapsed */),
1388                 false /* likeHighPriority */, 0);
1389     }
1390 
1391     /**
1392      * Get the minimum Height for this group.
1393      *
1394      * @param maxAllowedVisibleChildren the number of children that should be visible
1395      * @param likeHighPriority          if the height should be calculated as if it were not low
1396      *                                  priority
1397      */
1398     private int getMinHeight(int maxAllowedVisibleChildren, boolean likeHighPriority) {
1399         return getMinHeight(maxAllowedVisibleChildren, likeHighPriority, mCurrentHeaderTranslation);
1400     }
1401 
1402     /**
1403      * Get the minimum Height for this group.
1404      *
1405      * @param maxAllowedVisibleChildren the number of children that should be visible
1406      * @param likeHighPriority          if the height should be calculated as if it were not low
1407      *                                  priority
1408      * @param headerTranslation         the translation amount of the header
1409      */
1410     private int getMinHeight(
1411             int maxAllowedVisibleChildren,
1412             boolean likeHighPriority,
1413             int headerTranslation) {
1414         if (!likeHighPriority && showingAsLowPriority()) {
1415             if (AsyncGroupHeaderViewInflation.isEnabled()) {
1416                 return mHeaderHeight;
1417             }
1418             if (mMinimizedGroupHeader == null) {
1419                 Log.e(TAG, "getMinHeight: low priority header is null", new Exception());
1420                 return 0;
1421             }
1422             return mMinimizedGroupHeader.getHeight();
1423         }
1424         int minExpandHeight = mNotificationHeaderMargin + headerTranslation;
1425         int visibleChildren = 0;
1426         boolean firstChild = true;
1427         int childCount = mAttachedChildren.size();
1428         for (int i = 0; i < childCount; i++) {
1429             if (visibleChildren >= maxAllowedVisibleChildren) {
1430                 break;
1431             }
1432             if (!firstChild) {
1433                 minExpandHeight += mChildPadding;
1434             } else {
1435                 firstChild = false;
1436             }
1437             ExpandableNotificationRow child = mAttachedChildren.get(i);
1438             View singleLineView = child.getSingleLineView();
1439             if (singleLineView != null) {
1440                 minExpandHeight += singleLineView.getHeight();
1441             } else {
1442                 if (AsyncHybridViewInflation.isEnabled()) {
1443                     minExpandHeight += mMinSingleLineHeight;
1444                 } else {
1445                     Log.e(TAG, "getMinHeight: child " + child.getEntry().getKey()
1446                             + " single line view is null", new Exception());
1447                 }
1448             }
1449             visibleChildren++;
1450         }
1451         minExpandHeight += mCollapsedBottomPadding;
1452         return minExpandHeight;
1453     }
1454 
1455     public boolean showingAsLowPriority() {
1456         return mIsMinimized && !mContainingNotification.isExpanded();
1457     }
1458 
1459     public void reInflateViews(OnClickListener listener, StatusBarNotification notification) {
1460         if (!AsyncGroupHeaderViewInflation.isEnabled()) {
1461             // When Async header inflation is enabled, we do not reinflate headers because they are
1462             // inflated from the background thread
1463             if (mGroupHeader != null) {
1464                 removeView(mGroupHeader);
1465                 mGroupHeader = null;
1466             }
1467             if (mMinimizedGroupHeader != null) {
1468                 removeView(mMinimizedGroupHeader);
1469                 mMinimizedGroupHeader = null;
1470             }
1471             recreateNotificationHeader(listener, mIsConversation);
1472         }
1473         initDimens();
1474         for (int i = 0; i < mDividers.size(); i++) {
1475             View prevDivider = mDividers.get(i);
1476             int index = indexOfChild(prevDivider);
1477             removeView(prevDivider);
1478             View divider = inflateDivider();
1479             addView(divider, index);
1480             mDividers.set(i, divider);
1481         }
1482         removeView(mOverflowNumber);
1483         mOverflowNumber = null;
1484         mGroupOverFlowState = null;
1485         updateGroupOverflow();
1486     }
1487 
1488     public void setUserLocked(boolean userLocked) {
1489         mUserLocked = userLocked;
1490         if (!mUserLocked) {
1491             updateHeaderVisibility(false /* animate */);
1492         }
1493         int childCount = mAttachedChildren.size();
1494         for (int i = 0; i < childCount; i++) {
1495             ExpandableNotificationRow child = mAttachedChildren.get(i);
1496             child.setUserLocked(userLocked && !showingAsLowPriority());
1497         }
1498         updateHeaderTouchability();
1499     }
1500 
1501     private void updateHeaderTouchability() {
1502         if (mGroupHeader != null) {
1503             mGroupHeader.setAcceptAllTouches(mChildrenExpanded || mUserLocked);
1504         }
1505     }
1506 
1507     public void onNotificationUpdated() {
1508         if (mShowGroupCountInExpander) {
1509             // The overflow number is not used, so its color is irrelevant; skip this
1510             return;
1511         }
1512         int color = mContainingNotification.getNotificationColor();
1513         Resources.Theme theme = new ContextThemeWrapper(mContext,
1514                 com.android.internal.R.style.Theme_DeviceDefault_DayNight).getTheme();
1515         try (TypedArray ta = theme.obtainStyledAttributes(
1516                 new int[]{com.android.internal.R.attr.materialColorPrimary})) {
1517             color = ta.getColor(0, color);
1518         }
1519         mHybridGroupManager.setOverflowNumberColor(mOverflowNumber, color);
1520     }
1521 
1522     public int getPositionInLinearLayout(View childInGroup) {
1523         int position = mNotificationHeaderMargin + mCurrentHeaderTranslation
1524                 + mNotificationTopPadding;
1525 
1526         for (int i = 0; i < mAttachedChildren.size(); i++) {
1527             ExpandableNotificationRow child = mAttachedChildren.get(i);
1528             boolean notGone = child.getVisibility() != View.GONE;
1529             if (notGone) {
1530                 position += mDividerHeight;
1531             }
1532             if (child == childInGroup) {
1533                 return position;
1534             }
1535             if (notGone) {
1536                 position += child.getIntrinsicHeight();
1537             }
1538         }
1539         return 0;
1540     }
1541 
1542     public void setClipBottomAmount(int clipBottomAmount) {
1543         mClipBottomAmount = clipBottomAmount;
1544         updateChildrenClipping();
1545     }
1546 
1547     /**
1548      * Set whether the children container is minimized.
1549      */
1550     public void setIsMinimized(boolean isMinimized) {
1551         mIsMinimized = isMinimized;
1552         if (mContainingNotification != null) { /* we're not yet set up yet otherwise */
1553             if (!AsyncGroupHeaderViewInflation.isEnabled()) {
1554                 recreateLowPriorityHeader(null /* existingBuilder */, mIsConversation);
1555             }
1556             updateHeaderVisibility(false /* animate */);
1557         }
1558         if (mUserLocked) {
1559             setUserLocked(mUserLocked);
1560         }
1561     }
1562 
1563     /**
1564      * @return the view wrapper for the currently showing priority.
1565      */
1566     public NotificationViewWrapper getVisibleWrapper() {
1567         if (showingAsLowPriority()) {
1568             return mMinimizedGroupHeaderWrapper;
1569         }
1570         return mGroupHeaderWrapper;
1571     }
1572 
1573     public void onExpansionChanged() {
1574         if (mIsMinimized) {
1575             if (mUserLocked) {
1576                 setUserLocked(mUserLocked);
1577             }
1578             updateHeaderVisibility(true /* animate */);
1579         }
1580     }
1581 
1582     @VisibleForTesting
1583     public boolean isUserLocked() {
1584         return mUserLocked;
1585     }
1586 
1587     @Override
1588     public void applyRoundnessAndInvalidate() {
1589         boolean last = true;
1590         if (mGroupHeaderWrapper != null) {
1591             mGroupHeaderWrapper.requestTopRoundness(
1592                     /* value = */ getTopRoundness(),
1593                     /* sourceType = */ FROM_PARENT,
1594                     /* animate = */ false
1595             );
1596         }
1597         if (mMinimizedGroupHeaderWrapper != null) {
1598             mMinimizedGroupHeaderWrapper.requestTopRoundness(
1599                     /* value = */ getTopRoundness(),
1600                     /* sourceType = */ FROM_PARENT,
1601                     /* animate = */ false
1602             );
1603         }
1604         for (int i = mAttachedChildren.size() - 1; i >= 0; i--) {
1605             ExpandableNotificationRow child = mAttachedChildren.get(i);
1606             if (child.getVisibility() == View.GONE) {
1607                 continue;
1608             }
1609             child.requestRoundness(
1610                     /* top = */ 0f,
1611                     /* bottom = */ last ? getBottomRoundness() : 0f,
1612                     /* sourceType = */ FROM_PARENT,
1613                     /* animate = */ false);
1614             last = false;
1615         }
1616         Roundable.super.applyRoundnessAndInvalidate();
1617     }
1618 
1619     public void setHeaderVisibleAmount(float headerVisibleAmount) {
1620         mHeaderVisibleAmount = headerVisibleAmount;
1621         mCurrentHeaderTranslation = (int) ((1.0f - headerVisibleAmount) * mTranslationForHeader);
1622     }
1623 
1624     /**
1625      * Shows the given feedback icon, or hides the icon if null.
1626      */
1627     public void setFeedbackIcon(@Nullable FeedbackIcon icon) {
1628         if (mGroupHeaderWrapper != null) {
1629             mGroupHeaderWrapper.setFeedbackIcon(icon);
1630         }
1631         if (mMinimizedGroupHeaderWrapper != null) {
1632             mMinimizedGroupHeaderWrapper.setFeedbackIcon(icon);
1633         }
1634     }
1635 
1636     public void setRecentlyAudiblyAlerted(boolean audiblyAlertedRecently) {
1637         if (mGroupHeaderWrapper != null) {
1638             mGroupHeaderWrapper.setRecentlyAudiblyAlerted(audiblyAlertedRecently);
1639         }
1640         if (mMinimizedGroupHeaderWrapper != null) {
1641             mMinimizedGroupHeaderWrapper.setRecentlyAudiblyAlerted(audiblyAlertedRecently);
1642         }
1643     }
1644 
1645     @Override
1646     public void setNotificationFaded(boolean faded) {
1647         mContainingNotificationIsFaded = faded;
1648         if (mGroupHeaderWrapper != null) {
1649             mGroupHeaderWrapper.setNotificationFaded(faded);
1650         }
1651         if (mMinimizedGroupHeaderWrapper != null) {
1652             mMinimizedGroupHeaderWrapper.setNotificationFaded(faded);
1653         }
1654         for (ExpandableNotificationRow child : mAttachedChildren) {
1655             child.setNotificationFaded(faded);
1656         }
1657     }
1658 
1659     /**
1660      * Allow to define a path the clip the children in #drawChild()
1661      *
1662      * @param childClipPath path used to clip the children
1663      */
1664     public void setChildClipPath(@Nullable Path childClipPath) {
1665         mChildClipPath = childClipPath;
1666         invalidate();
1667     }
1668 
1669     public NotificationHeaderViewWrapper getNotificationHeaderWrapper() {
1670         return mGroupHeaderWrapper;
1671     }
1672 
1673     public void setLogger(NotificationChildrenContainerLogger logger) {
1674         mLogger = logger;
1675     }
1676 
1677     @Override
1678     public void addTransientView(View view, int index) {
1679         if (mLogger != null && view instanceof ExpandableNotificationRow) {
1680             mLogger.addTransientRow(
1681                     ((ExpandableNotificationRow) view).getEntry(),
1682                     getContainingNotification().getEntry(),
1683                     index
1684             );
1685         }
1686         super.addTransientView(view, index);
1687     }
1688 
1689     @Override
1690     public void removeTransientView(View view) {
1691         if (mLogger != null && view instanceof ExpandableNotificationRow) {
1692             mLogger.removeTransientRow(
1693                     ((ExpandableNotificationRow) view).getEntry(),
1694                     getContainingNotification().getEntry()
1695             );
1696         }
1697         super.removeTransientView(view);
1698     }
1699 
1700     public String debugString() {
1701         return TAG + " { "
1702                 + "visibility: " + getVisibility()
1703                 + ", alpha: " + getAlpha()
1704                 + ", translationY: " + getTranslationY()
1705                 + ", roundableState: " + getRoundableState().debugString() + "}";
1706     }
1707 }
1708