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