1 /*
2  * Copyright (C) 2014 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.row;
18 
19 import static com.android.systemui.Flags.notificationColorUpdateLogger;
20 
21 import android.animation.AnimatorListenerAdapter;
22 import android.content.Context;
23 import android.content.res.Configuration;
24 import android.graphics.Paint;
25 import android.graphics.Rect;
26 import android.util.AttributeSet;
27 import android.util.IndentingPrintWriter;
28 import android.util.Log;
29 import android.view.View;
30 import android.view.ViewGroup;
31 import android.view.ViewParent;
32 import android.widget.FrameLayout;
33 
34 import androidx.annotation.NonNull;
35 import androidx.annotation.Nullable;
36 
37 import com.android.app.animation.Interpolators;
38 import com.android.systemui.Dumpable;
39 import com.android.systemui.res.R;
40 import com.android.systemui.statusbar.StatusBarIconView;
41 import com.android.systemui.statusbar.notification.Roundable;
42 import com.android.systemui.statusbar.notification.RoundableState;
43 import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor;
44 import com.android.systemui.statusbar.notification.stack.ExpandableViewState;
45 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
46 import com.android.systemui.util.Compile;
47 import com.android.systemui.util.DumpUtilsKt;
48 
49 import java.io.PrintWriter;
50 import java.util.ArrayList;
51 import java.util.List;
52 
53 /**
54  * An abstract view for expandable views.
55  */
56 public abstract class ExpandableView extends FrameLayout implements Dumpable, Roundable {
57     private static final String TAG = "ExpandableView";
58     /** whether the dump() for this class should include verbose details */
59     protected static final boolean DUMP_VERBOSE = Compile.IS_DEBUG
60             && (Log.isLoggable(TAG, Log.VERBOSE) || notificationColorUpdateLogger());
61 
62     private RoundableState mRoundableState = null;
63     protected OnHeightChangedListener mOnHeightChangedListener;
64     private int mActualHeight;
65     protected int mClipTopAmount;
66     protected int mClipBottomAmount;
67     protected int mMinimumHeightForClipping = 0;
68     protected float mExtraWidthForClipping = 0;
69     private ArrayList<View> mMatchParentViews = new ArrayList<View>();
70     private static Rect mClipRect = new Rect();
71     private boolean mWillBeGone;
72     private boolean mClipToActualHeight = true;
73     private boolean mChangingPosition = false;
74     private ViewGroup mTransientContainer;
75 
76     // Needs to be added as transient view when removed from parent, because it's in animation
77     private boolean mInRemovalAnimation;
78     private boolean mInShelf;
79     private boolean mTransformingInShelf;
80     protected float mContentTransformationAmount;
81     protected boolean mIsLastChild;
82     protected int mContentShift;
83     @NonNull private final ExpandableViewState mViewState;
84     private float mContentTranslation;
85     protected boolean mLastInSection;
86     protected boolean mFirstInSection;
87 
ExpandableView(Context context, AttributeSet attrs)88     public ExpandableView(Context context, AttributeSet attrs) {
89         super(context, attrs);
90         mViewState = createExpandableViewState();
91         initDimens();
92     }
93 
94     @Override
getRoundableState()95     public RoundableState getRoundableState() {
96         if (mRoundableState == null) {
97             mRoundableState = new RoundableState(this, this, 0f);
98         }
99         return mRoundableState;
100     }
101 
102     @Override
getClipHeight()103     public int getClipHeight() {
104         int clipHeight = Math.max(mActualHeight - mClipTopAmount - mClipBottomAmount, 0);
105         return Math.max(clipHeight, mMinimumHeightForClipping);
106     }
107 
initDimens()108     private void initDimens() {
109         mContentShift = getResources().getDimensionPixelSize(
110                 R.dimen.shelf_transform_content_shift);
111     }
112 
113     @Override
onConfigurationChanged(Configuration newConfig)114     protected void onConfigurationChanged(Configuration newConfig) {
115         super.onConfigurationChanged(newConfig);
116         initDimens();
117     }
118 
119     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)120     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
121         final int givenHeight = MeasureSpec.getSize(heightMeasureSpec);
122         final int viewHorizontalPadding = getPaddingStart() + getPaddingEnd();
123 
124         // Max height is as large as possible, unless otherwise requested
125         int ownMaxHeight = Integer.MAX_VALUE;
126         int heightMode = MeasureSpec.getMode(heightMeasureSpec);
127         if (heightMode != MeasureSpec.UNSPECIFIED && givenHeight != 0) {
128             // Set our max height to what was requested from the parent
129             ownMaxHeight = Math.min(givenHeight, ownMaxHeight);
130         }
131 
132         // height of the largest child
133         int maxChildHeight = 0;
134         int atMostOwnMaxHeightSpec = MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.AT_MOST);
135         int childCount = getChildCount();
136         for (int i = 0; i < childCount; i++) {
137             View child = getChildAt(i);
138             if (child.getVisibility() == GONE) {
139                 continue;
140             }
141             int childHeightSpec = atMostOwnMaxHeightSpec;
142             ViewGroup.LayoutParams layoutParams = child.getLayoutParams();
143             if (layoutParams.height != ViewGroup.LayoutParams.MATCH_PARENT) {
144                 if (layoutParams.height >= 0) {
145                     // If an actual height is set, cap it to the max height
146                     childHeightSpec = MeasureSpec.makeMeasureSpec(
147                             Math.min(layoutParams.height, ownMaxHeight),
148                             MeasureSpec.EXACTLY);
149                 }
150                 child.measure(getChildMeasureSpec(
151                         widthMeasureSpec, viewHorizontalPadding, layoutParams.width),
152                         childHeightSpec);
153                 int childHeight = child.getMeasuredHeight();
154                 maxChildHeight = Math.max(maxChildHeight, childHeight);
155             } else {
156                 mMatchParentViews.add(child);
157             }
158         }
159 
160         // Set our own height to the given height, or the height of the largest child
161         int ownHeight = heightMode == MeasureSpec.EXACTLY
162                 ? givenHeight
163                 : Math.min(ownMaxHeight, maxChildHeight);
164         int exactlyOwnHeightSpec = MeasureSpec.makeMeasureSpec(ownHeight, MeasureSpec.EXACTLY);
165 
166         // Now that we know our own height, measure the children that are MATCH_PARENT
167         for (View child : mMatchParentViews) {
168             child.measure(getChildMeasureSpec(
169                     widthMeasureSpec, viewHorizontalPadding, child.getLayoutParams().width),
170                     exactlyOwnHeightSpec);
171         }
172         mMatchParentViews.clear();
173 
174         // Finish up
175         int width = MeasureSpec.getSize(widthMeasureSpec);
176         setMeasuredDimension(width, ownHeight);
177     }
178 
179     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)180     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
181         super.onLayout(changed, left, top, right, bottom);
182         updateClipping();
183     }
184 
185     @Override
pointInView(float localX, float localY, float slop)186     public boolean pointInView(float localX, float localY, float slop) {
187         float top = Math.max(0, mClipTopAmount);
188         float bottom = mActualHeight;
189         return localX >= -slop && localY >= top - slop && localX < ((mRight - mLeft) + slop) &&
190                 localY < (bottom + slop);
191     }
192 
193     /**
194      * @return if this view needs to be clipped to the shelf
195      */
needsClippingToShelf()196     public boolean needsClippingToShelf() {
197         return true;
198     }
199 
200 
isPinned()201     public boolean isPinned() {
202         return false;
203     }
204 
isHeadsUpAnimatingAway()205     public boolean isHeadsUpAnimatingAway() {
206         return false;
207     }
208 
209     /**
210      * Sets the actual height of this notification. This is different than the laid out
211      * {@link View#getHeight()}, as we want to avoid layouting during scrolling and expanding.
212      *
213      * @param actualHeight The height of this notification.
214      * @param notifyListeners Whether the listener should be informed about the change.
215      */
setActualHeight(int actualHeight, boolean notifyListeners)216     public void setActualHeight(int actualHeight, boolean notifyListeners) {
217         if (mActualHeight != actualHeight) {
218             mActualHeight = actualHeight;
219             updateClipping();
220             if (notifyListeners) {
221                 notifyHeightChanged(false  /* needsAnimation */);
222             }
223         }
224     }
225 
setActualHeight(int actualHeight)226     public void setActualHeight(int actualHeight) {
227         setActualHeight(actualHeight, true /* notifyListeners */);
228     }
229 
230     /**
231      * See {@link #setActualHeight}.
232      *
233      * @return The current actual height of this notification.
234      */
getActualHeight()235     public int getActualHeight() {
236         return mActualHeight;
237     }
238 
isExpandAnimationRunning()239     public boolean isExpandAnimationRunning() {
240         return false;
241     }
242 
243     /**
244      * @return The maximum height of this notification.
245      */
getMaxContentHeight()246     public int getMaxContentHeight() {
247         return getHeight();
248     }
249 
250     /**
251      * @return The minimum content height of this notification. This also respects the temporary
252      * states of the view.
253      */
getMinHeight()254     public int getMinHeight() {
255         return getMinHeight(false /* ignoreTemporaryStates */);
256     }
257 
258     /**
259      * Get the minimum height of this view.
260      *
261      * @param ignoreTemporaryStates should temporary states be ignored like the guts or heads-up.
262      *
263      * @return The minimum height that this view needs.
264      */
getMinHeight(boolean ignoreTemporaryStates)265     public int getMinHeight(boolean ignoreTemporaryStates) {
266         return getHeight();
267     }
268 
269     /**
270      * @return The collapsed height of this view. Note that this might be different
271      * than {@link #getMinHeight()} because some elements like groups may have different sizes when
272      * they are system expanded.
273      */
getCollapsedHeight()274     public int getCollapsedHeight() {
275         return getHeight();
276     }
277 
isRemoved()278     public boolean isRemoved() {
279         return false;
280     }
281 
282     /**
283      * See {@link #setHideSensitive}. This is a variant which notifies this view in advance about
284      * the upcoming state of hiding sensitive notifications. It gets called at the very beginning
285      * of a stack scroller update such that the updated intrinsic height (which is dependent on
286      * whether private or public layout is showing) gets taken into account into all layout
287      * calculations.
288      */
setHideSensitiveForIntrinsicHeight(boolean hideSensitive)289     public void setHideSensitiveForIntrinsicHeight(boolean hideSensitive) {
290     }
291 
292     /**
293      * Sets whether the notification should hide its private contents if it is sensitive.
294      */
setHideSensitive(boolean hideSensitive, boolean animated, long delay, long duration)295     public void setHideSensitive(boolean hideSensitive, boolean animated, long delay,
296             long duration) {
297     }
298 
getHeightWithoutLockscreenConstraints()299     public int getHeightWithoutLockscreenConstraints() {
300         // ExpandableNotificationRow overrides this.
301         return getHeight();
302     }
303 
304     /**
305      * @return The desired notification height.
306      */
getIntrinsicHeight()307     public int getIntrinsicHeight() {
308         return getHeight();
309     }
310 
311     /**
312      * Sets the amount this view should be clipped from the top. This is used when an expanded
313      * notification is scrolling in the top or bottom stack.
314      *
315      * @param clipTopAmount The amount of pixels this view should be clipped from top.
316      */
setClipTopAmount(int clipTopAmount)317     public void setClipTopAmount(int clipTopAmount) {
318         mClipTopAmount = clipTopAmount;
319         updateClipping();
320     }
321 
322     /**
323      * Set the amount the the notification is clipped on the bottom in addition to the regular
324      * clipping. This is mainly used to clip something in a non-animated way without changing the
325      * actual height of the notification and is purely visual.
326      *
327      * @param clipBottomAmount the amount to clip.
328      */
setClipBottomAmount(int clipBottomAmount)329     public void setClipBottomAmount(int clipBottomAmount) {
330         mClipBottomAmount = clipBottomAmount;
331         updateClipping();
332     }
333 
getClipTopAmount()334     public int getClipTopAmount() {
335         return mClipTopAmount;
336     }
337 
getClipBottomAmount()338     public int getClipBottomAmount() {
339         return mClipBottomAmount;
340     }
341 
setOnHeightChangedListener(OnHeightChangedListener listener)342     public void setOnHeightChangedListener(OnHeightChangedListener listener) {
343         mOnHeightChangedListener = listener;
344     }
345 
346     /**
347      * @return Whether we can expand this views content.
348      */
isContentExpandable()349     public boolean isContentExpandable() {
350         return false;
351     }
352 
notifyHeightChanged(boolean needsAnimation)353     public void notifyHeightChanged(boolean needsAnimation) {
354         if (mOnHeightChangedListener != null) {
355             mOnHeightChangedListener.onHeightChanged(this, needsAnimation);
356         }
357     }
358 
isTransparent()359     public boolean isTransparent() {
360         return false;
361     }
362 
363     /**
364      * Perform a remove animation on this view.
365      *
366      * @param duration             The duration of the remove animation.
367      * @param delay                The delay of the animation
368      * @param translationDirection The direction value from [-1 ... 1] indicating in which the
369      *                             animation should be performed. A value of -1 means that The
370      *                             remove animation should be performed upwards,
371      *                             such that the  child appears to be going away to the top. 1
372      *                             Should mean the opposite.
373      * @param isHeadsUpAnimation   Is this a headsUp animation.
374      * @param onFinishedRunnable   A runnable which should be run when the animation is finished.
375      * @param animationListener    An animation listener to add to the animation.
376      * @return The additional delay, in milliseconds, that this view needs to add before the
377      * animation starts.
378      */
performRemoveAnimation(long duration, long delay, float translationDirection, boolean isHeadsUpAnimation, Runnable onStartedRunnable, Runnable onFinishedRunnable, AnimatorListenerAdapter animationListener, ClipSide clipSide)379     public abstract long performRemoveAnimation(long duration,
380             long delay, float translationDirection, boolean isHeadsUpAnimation,
381             Runnable onStartedRunnable,
382             Runnable onFinishedRunnable,
383             AnimatorListenerAdapter animationListener, ClipSide clipSide);
384 
385     public enum ClipSide {
386         TOP,
387         BOTTOM
388     }
389 
performAddAnimation(long delay, long duration, boolean isHeadsUpAppear)390     public void performAddAnimation(long delay, long duration, boolean isHeadsUpAppear) {
391         performAddAnimation(delay, duration, isHeadsUpAppear, null);
392     }
393 
performAddAnimation(long delay, long duration, boolean isHeadsUpAppear, Runnable onEndRunnable)394     public abstract void performAddAnimation(long delay, long duration, boolean isHeadsUpAppear,
395             Runnable onEndRunnable);
396 
397     /**
398      * Set the notification appearance to be below the speed bump.
399      * @param below true if it is below.
400      */
setBelowSpeedBump(boolean below)401     public void setBelowSpeedBump(boolean below) {
402         NotificationIconContainerRefactor.assertInLegacyMode();
403     }
404 
getPinnedHeadsUpHeight()405     public int getPinnedHeadsUpHeight() {
406         return getIntrinsicHeight();
407     }
408 
409 
410     /**
411      * Sets the translation of the view.
412      */
setTranslation(float translation)413     public void setTranslation(float translation) {
414         setTranslationX(translation);
415     }
416 
417     /**
418      * Gets the translation of the view.
419      */
getTranslation()420     public float getTranslation() {
421         return getTranslationX();
422     }
423 
onHeightReset()424     public void onHeightReset() {
425         if (mOnHeightChangedListener != null) {
426             mOnHeightChangedListener.onReset(this);
427         }
428     }
429 
430     /**
431      * This method returns the drawing rect for the view which is different from the regular
432      * drawing rect, since we layout all children in the {@link NotificationStackScrollLayout} at
433      * position 0 and usually the translation is neglected. Since we are manually clipping this
434      * view,we also need to subtract the clipTopAmount from the top. This is needed in order to
435      * ensure that accessibility and focusing work correctly.
436      *
437      * @param outRect The (scrolled) drawing bounds of the view.
438      */
439     @Override
getDrawingRect(Rect outRect)440     public void getDrawingRect(Rect outRect) {
441         super.getDrawingRect(outRect);
442         outRect.left += getTranslationX();
443         outRect.right += getTranslationX();
444         outRect.bottom = (int) (outRect.top + getTranslationY() + getActualHeight());
445         outRect.top += getTranslationY() + getClipTopAmount();
446     }
447 
448     @Override
getBoundsOnScreen(Rect outRect, boolean clipToParent)449     public void getBoundsOnScreen(Rect outRect, boolean clipToParent) {
450         super.getBoundsOnScreen(outRect, clipToParent);
451         if (getTop() + getTranslationY() < 0) {
452             // We got clipped to the parent here - make sure we undo that.
453             outRect.top += getTop() + getTranslationY();
454         }
455         outRect.bottom = outRect.top + getActualHeight();
456         outRect.top += Math.max(0, getClipTopAmount());
457     }
458 
isSummaryWithChildren()459     public boolean isSummaryWithChildren() {
460         return false;
461     }
462 
areChildrenExpanded()463     public boolean areChildrenExpanded() {
464         return false;
465     }
466 
updateClipping()467     protected void updateClipping() {
468         if (mClipToActualHeight && shouldClipToActualHeight()) {
469             int top = getClipTopAmount();
470             int bottom = Math.max(Math.max(getActualHeight()
471                     - mClipBottomAmount, top), mMinimumHeightForClipping);
472             mClipRect.set(Integer.MIN_VALUE, top, Integer.MAX_VALUE, bottom);
473             setClipBounds(mClipRect);
474         } else {
475             setClipBounds(null);
476         }
477     }
478 
setMinimumHeightForClipping(int minimumHeightForClipping)479     public void setMinimumHeightForClipping(int minimumHeightForClipping) {
480         mMinimumHeightForClipping = minimumHeightForClipping;
481         updateClipping();
482     }
483 
setExtraWidthForClipping(float extraWidthForClipping)484     public void setExtraWidthForClipping(float extraWidthForClipping) {
485         mExtraWidthForClipping = extraWidthForClipping;
486     }
487 
getHeaderVisibleAmount()488     public float getHeaderVisibleAmount() {
489         return 1.0f;
490     }
491 
shouldClipToActualHeight()492     protected boolean shouldClipToActualHeight() {
493         return true;
494     }
495 
setClipToActualHeight(boolean clipToActualHeight)496     public void setClipToActualHeight(boolean clipToActualHeight) {
497         mClipToActualHeight = clipToActualHeight;
498         updateClipping();
499     }
500 
willBeGone()501     public boolean willBeGone() {
502         return mWillBeGone;
503     }
504 
setWillBeGone(boolean willBeGone)505     public void setWillBeGone(boolean willBeGone) {
506         mWillBeGone = willBeGone;
507     }
508 
509     @Override
setLayerType(int layerType, Paint paint)510     public void setLayerType(int layerType, Paint paint) {
511         // Allow resetting the layerType to NONE regardless of overlappingRendering
512         if (layerType == LAYER_TYPE_NONE || hasOverlappingRendering()) {
513             super.setLayerType(layerType, paint);
514         }
515     }
516 
517     @Override
hasOverlappingRendering()518     public boolean hasOverlappingRendering() {
519         // Otherwise it will be clipped
520         return super.hasOverlappingRendering() && getActualHeight() <= getHeight();
521     }
522 
mustStayOnScreen()523     public boolean mustStayOnScreen() {
524         return false;
525     }
526 
setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd, int outlineTranslation)527     public void setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd,
528             int outlineTranslation) {
529     }
530 
getOutlineAlpha()531     public float getOutlineAlpha() {
532         return 0.0f;
533     }
534 
getOutlineTranslation()535     public int getOutlineTranslation() {
536         return 0;
537     }
538 
setChangingPosition(boolean changingPosition)539     public void setChangingPosition(boolean changingPosition) {
540         mChangingPosition = changingPosition;
541     }
542 
isChangingPosition()543     public boolean isChangingPosition() {
544         return mChangingPosition;
545     }
546 
547     /**
548      * Called when removing a view from its transient container, such as at the end of an animation.
549      * Generally, when operating on ExpandableView instances, this should be used rather than
550      * {@link ExpandableView#removeTransientView(View)} to ensure that the
551      * {@link #getTransientContainer() transient container} is correctly reset.
552      */
removeFromTransientContainer()553     public void removeFromTransientContainer() {
554         final ViewGroup transientContainer = getTransientContainer();
555         if (transientContainer == null) {
556             return;
557         }
558         final ViewParent parent = getParent();
559         if (parent != transientContainer) {
560             Log.w(TAG, "Expandable view " + this
561                     + " has transient container " + transientContainer
562                     + " but different parent " + parent);
563             setTransientContainer(null);
564             return;
565         }
566         transientContainer.removeTransientView(this);
567         setTransientContainer(null);
568     }
569 
570     /**
571      * Called before adding this view to a group, which would always throw an exception if this view
572      * has a different parent, so clean up the transient container and throw an exception if the
573      * parent isn't a transient container.  Provide as much detail as possible in the crash.
574      */
removeFromTransientContainerForAdditionTo(ViewGroup newParent)575     public void removeFromTransientContainerForAdditionTo(ViewGroup newParent) {
576         final ViewParent parent = getParent();
577         final ViewGroup transientContainer = getTransientContainer();
578         if (parent == null || parent == newParent) {
579             // If this view's current parent is null or the same as the new parent, the add will
580             // succeed as long as it's a true child, so just make sure the view isn't transient.
581             removeFromTransientContainer();
582             return;
583         }
584         if (transientContainer == null) {
585             throw new IllegalStateException("Can't add view " + this + " to container " + newParent
586                     + "; current parent " + parent + " is not a transient container");
587         }
588         if (transientContainer != parent) {
589             // Crash with details before addView() crashes without any; the view is being added
590             // to a different parent, and the transient container isn't the parent, so we can't
591             // even (safely) clean that up.
592             throw new IllegalStateException("Expandable view " + this
593                     + " has transient container " + transientContainer
594                     + " but different parent " + parent);
595         }
596         Log.w(TAG, "Removing view " + this + " from transient container "
597                 + transientContainer + " in preparation for moving to parent " + newParent);
598         transientContainer.removeTransientView(this);
599         setTransientContainer(null);
600     }
601 
setTransientContainer(ViewGroup transientContainer)602     public void setTransientContainer(ViewGroup transientContainer) {
603         mTransientContainer = transientContainer;
604     }
605 
getTransientContainer()606     public ViewGroup getTransientContainer() {
607         return mTransientContainer;
608     }
609 
610     /**
611      * Add the view to a transient container.
612      */
addToTransientContainer(ViewGroup container, int index)613     public void addToTransientContainer(ViewGroup container, int index) {
614         container.addTransientView(this, index);
615         setTransientContainer(container);
616     }
617 
618     /**
619      * @return If the view is in a process of removal animation.
620      */
inRemovalAnimation()621     public boolean inRemovalAnimation() {
622         return mInRemovalAnimation;
623     }
624 
setInRemovalAnimation(boolean inRemovalAnimation)625     public void setInRemovalAnimation(boolean inRemovalAnimation) {
626         mInRemovalAnimation = inRemovalAnimation;
627     }
628 
629     /**
630      * @return true if the group's expansion state is changing, false otherwise.
631      */
isGroupExpansionChanging()632     public boolean isGroupExpansionChanging() {
633         return false;
634     }
635 
isGroupExpanded()636     public boolean isGroupExpanded() {
637         return false;
638     }
639 
setHeadsUpIsVisible()640     public void setHeadsUpIsVisible() {
641     }
642 
showingPulsing()643     public boolean showingPulsing() {
644         return false;
645     }
646 
isHeadsUpState()647     public boolean isHeadsUpState() {
648         return false;
649     }
650 
isChildInGroup()651     public boolean isChildInGroup() {
652         return false;
653     }
654 
setActualHeightAnimating(boolean animating)655     public void setActualHeightAnimating(boolean animating) {}
656 
657     @NonNull
createExpandableViewState()658     protected ExpandableViewState createExpandableViewState() {
659         return new ExpandableViewState();
660     }
661 
662     /** Sets {@link ExpandableViewState} to default state. */
resetViewState()663     public ExpandableViewState resetViewState() {
664         // initialize with the default values of the view
665         mViewState.height = getIntrinsicHeight();
666         mViewState.gone = getVisibility() == View.GONE;
667         mViewState.setAlpha(1f);
668         mViewState.notGoneIndex = -1;
669         mViewState.setXTranslation(getTranslationX());
670         mViewState.hidden = false;
671         mViewState.setScaleX(getScaleX());
672         mViewState.setScaleY(getScaleY());
673         mViewState.inShelf = false;
674         mViewState.headsUpIsVisible = false;
675 
676         // handling reset for child notifications
677         if (this instanceof ExpandableNotificationRow row) {
678             List<ExpandableNotificationRow> children = row.getAttachedChildren();
679             if (row.isSummaryWithChildren() && children != null) {
680                 for (ExpandableNotificationRow childRow : children) {
681                     childRow.resetViewState();
682                 }
683             }
684         }
685 
686         return mViewState;
687     }
688 
689     /**
690      * Get the {@link ExpandableViewState} associated with the view.
691      *
692      * @return the ExpandableView's view state.
693      */
getViewState()694     @NonNull public ExpandableViewState getViewState() {
695         return mViewState;
696     }
697 
698     /** Applies internal {@link ExpandableViewState} to this view. */
applyViewState()699     public void applyViewState() {
700         if (!mViewState.gone) {
701             mViewState.applyToView(this);
702         }
703     }
704 
705     /**
706      * @return whether the current view doesn't add height to the overall content. This means that
707      * if it is added to a list of items, its content will still have the same height.
708      * An example is the notification shelf, that is always placed on top of another view.
709      */
hasNoContentHeight()710     public boolean hasNoContentHeight() {
711         return false;
712     }
713 
714     /**
715      * @param inShelf whether the view is currently fully in the notification shelf.
716      */
setInShelf(boolean inShelf)717     public void setInShelf(boolean inShelf) {
718         mInShelf = inShelf;
719     }
720 
721     /**
722      * @return true if the view is currently fully in the notification shelf.
723      */
isInShelf()724     public boolean isInShelf() {
725         return mInShelf;
726     }
727 
getShelfIcon()728     public @Nullable StatusBarIconView getShelfIcon() {
729         return null;
730     }
731 
732     /**
733      * @return get the transformation target of the shelf, which usually is the icon
734      */
getShelfTransformationTarget()735     public View getShelfTransformationTarget() {
736         return null;
737     }
738 
739     /**
740      * Get the relative top padding of a view relative to this view. This recursively walks up the
741      * hierarchy and does the corresponding measuring.
742      *
743      * @param view the view to get the padding for. The requested view has to be a child of this
744      *             notification.
745      * @return the toppadding
746      */
getRelativeTopPadding(View view)747     public int getRelativeTopPadding(View view) {
748         int topPadding = 0;
749         while (view.getParent() instanceof ViewGroup) {
750             topPadding += view.getTop();
751             view = (View) view.getParent();
752             if (view == this) {
753                 return topPadding;
754             }
755         }
756         return topPadding;
757     }
758 
759 
760     /**
761      * Get the relative start padding of a view relative to this view. This recursively walks up the
762      * hierarchy and does the corresponding measuring.
763      *
764      * @param view the view to get the padding for. The requested view has to be a child of this
765      *             notification.
766      * @return the start padding
767      */
getRelativeStartPadding(View view)768     public int getRelativeStartPadding(View view) {
769         boolean isRtl = isLayoutRtl();
770         int startPadding = 0;
771         while (view.getParent() instanceof ViewGroup) {
772             View parent = (View) view.getParent();
773             startPadding += isRtl ? parent.getWidth() - view.getRight() : view.getLeft();
774             view = parent;
775             if (view == this) {
776                 return startPadding;
777             }
778         }
779         return startPadding;
780     }
781 
782     /**
783      * Set how much this notification is transformed into the shelf.
784      *
785      * @param contentTransformationAmount A value from 0 to 1 indicating how much we are transformed
786      *                                 to the content away
787      * @param isLastChild is this the last child in the list. If true, then the transformation is
788      *                    different since its content fades out.
789      */
setContentTransformationAmount(float contentTransformationAmount, boolean isLastChild)790     public void setContentTransformationAmount(float contentTransformationAmount,
791             boolean isLastChild) {
792         boolean changeTransformation = isLastChild != mIsLastChild;
793         changeTransformation |= mContentTransformationAmount != contentTransformationAmount;
794         mIsLastChild = isLastChild;
795         mContentTransformationAmount = contentTransformationAmount;
796         if (changeTransformation) {
797             updateContentTransformation();
798         }
799     }
800 
801     /**
802      * Update the content representation based on the amount we are transformed into the shelf.
803      */
updateContentTransformation()804     protected void updateContentTransformation() {
805         float translationY = -mContentTransformationAmount * getContentTransformationShift();
806         float contentAlpha = 1.0f - mContentTransformationAmount;
807         contentAlpha = Math.min(contentAlpha / 0.5f, 1.0f);
808         contentAlpha = Interpolators.ALPHA_OUT.getInterpolation(contentAlpha);
809         if (mIsLastChild) {
810             translationY *= 0.4f;
811         }
812         mContentTranslation = translationY;
813         applyContentTransformation(contentAlpha, translationY);
814     }
815 
816     /**
817      * @return how much the content shifts up when going into the shelf
818      */
getContentTransformationShift()819     protected float getContentTransformationShift() {
820         return mContentShift;
821     }
822 
823     /**
824      * Apply the contentTransformation when going into the shelf.
825      *
826      * @param contentAlpha The alpha that should be applied
827      * @param translationY the translationY that should be applied
828      */
applyContentTransformation(float contentAlpha, float translationY)829     protected void applyContentTransformation(float contentAlpha, float translationY) {
830     }
831 
832     /**
833      * @param transformingInShelf whether the view is currently transforming into the shelf in an
834      *                            animated way
835      */
setTransformingInShelf(boolean transformingInShelf)836     public void setTransformingInShelf(boolean transformingInShelf) {
837         mTransformingInShelf = transformingInShelf;
838     }
839 
isTransformingIntoShelf()840     public boolean isTransformingIntoShelf() {
841         return mTransformingInShelf;
842     }
843 
isAboveShelf()844     public boolean isAboveShelf() {
845         return false;
846     }
847 
hasExpandingChild()848     public boolean hasExpandingChild() {
849         return false;
850     }
851 
852     @Override
dump(PrintWriter pwOriginal, String[] args)853     public void dump(PrintWriter pwOriginal, String[] args) {
854         IndentingPrintWriter pw = DumpUtilsKt.asIndenting(pwOriginal);
855         pw.println(getClass().getSimpleName());
856         DumpUtilsKt.withIncreasedIndent(pw, () -> {
857             ExpandableViewState viewState = getViewState();
858             if (viewState == null) {
859                 pw.println("no viewState!!!");
860             } else {
861                 viewState.dump(pw, args);
862                 pw.println();
863             }
864             if (DUMP_VERBOSE) {
865                 pw.println("mInRemovalAnimation: " + mInRemovalAnimation);
866                 pw.println("mClipTopAmount: " + mClipTopAmount);
867                 pw.println("mClipBottomAmount " + mClipBottomAmount);
868                 pw.println("mClipToActualHeight: " + mClipToActualHeight);
869                 pw.println("mExtraWidthForClipping: " + mExtraWidthForClipping);
870                 pw.println("mMinimumHeightForClipping: " + mMinimumHeightForClipping);
871                 pw.println("getClipBounds(): " + getClipBounds());
872             }
873         });
874     }
875 
876     /**
877      * return the amount that the content is translated
878      */
getContentTranslation()879     public float getContentTranslation() {
880         return mContentTranslation;
881     }
882 
883     /** Sets whether this view is the first notification in a section. */
setFirstInSection(boolean firstInSection)884     public void setFirstInSection(boolean firstInSection) {
885         mFirstInSection = firstInSection;
886     }
887 
888     /** Sets whether this view is the last notification in a section. */
setLastInSection(boolean lastInSection)889     public void setLastInSection(boolean lastInSection) {
890         mLastInSection = lastInSection;
891     }
892 
isLastInSection()893     public boolean isLastInSection() {
894         return mLastInSection;
895     }
896 
isFirstInSection()897     public boolean isFirstInSection() {
898         return mFirstInSection;
899     }
900 
getHeadsUpHeightWithoutHeader()901     public int getHeadsUpHeightWithoutHeader() {
902         return getHeight();
903     }
904 
905     /**
906      * A listener notifying when {@link #getActualHeight} changes.
907      */
908     public interface OnHeightChangedListener {
909 
910         /**
911          * @param view the view for which the height changed, or {@code null} if just the top
912          *             padding or the padding between the elements changed
913          * @param needsAnimation whether the view height needs to be animated
914          */
onHeightChanged(ExpandableView view, boolean needsAnimation)915         void onHeightChanged(ExpandableView view, boolean needsAnimation);
916 
917         /**
918          * Called when the view is reset and therefore the height will change abruptly
919          *
920          * @param view The view which was reset.
921          */
onReset(ExpandableView view)922         void onReset(ExpandableView view);
923     }
924 }
925