1 /*
2  * Copyright (C) 2016 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 package com.android.systemui.statusbar.phone;
17 
18 import static com.android.systemui.statusbar.phone.HeadsUpAppearanceController.CONTENT_FADE_DELAY;
19 import static com.android.systemui.statusbar.phone.HeadsUpAppearanceController.CONTENT_FADE_DURATION;
20 
21 import android.content.Context;
22 import android.content.res.Configuration;
23 import android.graphics.Canvas;
24 import android.graphics.Color;
25 import android.graphics.Paint;
26 import android.graphics.Rect;
27 import android.graphics.drawable.Icon;
28 import android.util.AttributeSet;
29 import android.util.MathUtils;
30 import android.util.Property;
31 import android.view.ContextThemeWrapper;
32 import android.view.View;
33 import android.view.ViewGroup;
34 import android.view.animation.Interpolator;
35 
36 import androidx.annotation.Nullable;
37 import androidx.annotation.VisibleForTesting;
38 import androidx.collection.ArrayMap;
39 
40 import com.android.app.animation.Interpolators;
41 import com.android.internal.statusbar.StatusBarIcon;
42 import com.android.settingslib.Utils;
43 import com.android.systemui.res.R;
44 import com.android.systemui.statusbar.StatusBarIconView;
45 import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor;
46 import com.android.systemui.statusbar.notification.stack.AnimationFilter;
47 import com.android.systemui.statusbar.notification.stack.AnimationProperties;
48 import com.android.systemui.statusbar.notification.stack.ViewState;
49 
50 import java.util.ArrayList;
51 import java.util.HashMap;
52 import java.util.function.Consumer;
53 
54 /**
55  * A container for notification icons. It handles overflowing icons properly and positions them
56  * correctly on the screen.
57  */
58 public class NotificationIconContainer extends ViewGroup {
59     private static final int NO_VALUE = Integer.MIN_VALUE;
60     private static final String TAG = "NotificationIconContainer";
61     private static final boolean DEBUG = false;
62     private static final boolean DEBUG_OVERFLOW = false;
63     private static final int CANNED_ANIMATION_DURATION = 100;
64     private static final AnimationProperties DOT_ANIMATION_PROPERTIES = new AnimationProperties() {
65         private final AnimationFilter mAnimationFilter = new AnimationFilter().animateX();
66 
67         @Override
68         public AnimationFilter getAnimationFilter() {
69             return mAnimationFilter;
70         }
71     }.setDuration(200);
72 
73     private static final AnimationProperties ICON_ANIMATION_PROPERTIES = new AnimationProperties() {
74         private final AnimationFilter mAnimationFilter = new AnimationFilter()
75                 .animateX()
76                 .animateY()
77                 .animateAlpha()
78                 .animateScale();
79 
80         @Override
81         public AnimationFilter getAnimationFilter() {
82             return mAnimationFilter;
83         }
84 
85     }.setDuration(CANNED_ANIMATION_DURATION);
86 
87     /**
88      * Temporary AnimationProperties to avoid unnecessary allocations.
89      */
90     private static final AnimationProperties sTempProperties = new AnimationProperties() {
91         private final AnimationFilter mAnimationFilter = new AnimationFilter();
92 
93         @Override
94         public AnimationFilter getAnimationFilter() {
95             return mAnimationFilter;
96         }
97     };
98 
99     private static final AnimationProperties ADD_ICON_PROPERTIES = new AnimationProperties() {
100         private final AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha();
101 
102         @Override
103         public AnimationFilter getAnimationFilter() {
104             return mAnimationFilter;
105         }
106     }.setDuration(200).setDelay(50);
107 
108     /**
109      * The animation property used for all icons that were not isolated, when the isolation ends.
110      * This just fades the alpha and doesn't affect the movement and has a delay.
111      */
112     private static final AnimationProperties UNISOLATION_PROPERTY_OTHERS
113             = new AnimationProperties() {
114         private final AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha();
115 
116         @Override
117         public AnimationFilter getAnimationFilter() {
118             return mAnimationFilter;
119         }
120     }.setDuration(CONTENT_FADE_DURATION);
121 
122     /**
123      * The animation property used for the icon when its isolation ends.
124      * This animates the translation back to the right position.
125      */
126     private static final AnimationProperties UNISOLATION_PROPERTY = new AnimationProperties() {
127         private final AnimationFilter mAnimationFilter = new AnimationFilter().animateX();
128 
129         @Override
130         public AnimationFilter getAnimationFilter() {
131             return mAnimationFilter;
132         }
133     }.setDuration(CONTENT_FADE_DURATION);
134 
135     // TODO(b/278765923): Replace these with domain-agnostic state
136     /* Maximum number of icons on AOD when also showing overflow dot. */
137     private int mMaxIconsOnAod;
138     /* Maximum number of icons in short shelf on lockscreen when also showing overflow dot. */
139     private int mMaxIconsOnLockscreen;
140     /* Maximum number of icons in the status bar when also showing overflow dot. */
141     private int mMaxStaticIcons;
142     private boolean mDozing;
143     private boolean mOnLockScreen;
144     private int mSpeedBumpIndex = -1;
145 
146     private int mMaxIcons = Integer.MAX_VALUE;
147     private boolean mOverrideIconColor;
148     private boolean mIsStaticLayout = true;
149     private final HashMap<View, IconState> mIconStates = new HashMap<>();
150     private int mDotPadding;
151     private int mStaticDotDiameter;
152     private int mActualLayoutWidth = NO_VALUE;
153     private float mActualPaddingEnd = NO_VALUE;
154     private float mActualPaddingStart = NO_VALUE;
155     private boolean mChangingViewPositions;
156     private int mAddAnimationStartIndex = -1;
157     private int mCannedAnimationStartIndex = -1;
158     private int mIconSize;
159     private boolean mDisallowNextAnimation;
160     private boolean mAnimationsEnabled = true;
161     private ArrayMap<String, StatusBarIcon> mReplacingIcons;
162     private ArrayMap<String, ArrayList<StatusBarIcon>> mReplacingIconsLegacy;
163     // Keep track of the last visible icon so collapsed container can report on its location
164     private IconState mLastVisibleIconState;
165     private IconState mFirstVisibleIconState;
166     private float mVisualOverflowStart;
167     private boolean mIsShowingOverflowDot;
168     private StatusBarIconView mIsolatedIcon;
169     private Rect mIsolatedIconLocation;
170     private final int[] mAbsolutePosition = new int[2];
171     private View mIsolatedIconForAnimation;
172     private int mThemedTextColorPrimary;
173     private Runnable mIsolatedIconAnimationEndRunnable;
174     private boolean mUseIncreasedIconScale;
175 
NotificationIconContainer(Context context, AttributeSet attrs)176     public NotificationIconContainer(Context context, AttributeSet attrs) {
177         super(context, attrs);
178         initResources();
179         setWillNotDraw(!(DEBUG || DEBUG_OVERFLOW));
180     }
181 
initResources()182     private void initResources() {
183         mMaxIconsOnAod = getResources().getInteger(R.integer.max_notif_icons_on_aod);
184         mMaxIconsOnLockscreen = getResources().getInteger(R.integer.max_notif_icons_on_lockscreen);
185         mMaxStaticIcons = getResources().getInteger(R.integer.max_notif_static_icons);
186 
187         mDotPadding = getResources().getDimensionPixelSize(R.dimen.overflow_icon_dot_padding);
188         int staticDotRadius = getResources().getDimensionPixelSize(R.dimen.overflow_dot_radius);
189         mStaticDotDiameter = 2 * staticDotRadius;
190 
191         final Context themedContext = new ContextThemeWrapper(getContext(),
192                 com.android.internal.R.style.Theme_DeviceDefault_DayNight);
193         mThemedTextColorPrimary = Utils.getColorAttr(themedContext,
194                 com.android.internal.R.attr.textColorPrimary).getDefaultColor();
195     }
196 
197     @Override
onDraw(Canvas canvas)198     protected void onDraw(Canvas canvas) {
199         super.onDraw(canvas);
200         Paint paint = new Paint();
201         paint.setColor(Color.RED);
202         paint.setStyle(Paint.Style.STROKE);
203         canvas.drawRect(getActualPaddingStart(), 0, getLayoutEnd(), getHeight(), paint);
204 
205         if (DEBUG_OVERFLOW) {
206             if (mLastVisibleIconState == null) {
207                 return;
208             }
209 
210             int height = getHeight();
211             int end = getFinalTranslationX();
212 
213             // Visualize the "end" of the layout
214             paint.setColor(Color.BLUE);
215             canvas.drawLine(end, 0, end, height, paint);
216 
217             paint.setColor(Color.GREEN);
218             int lastIcon = (int) mLastVisibleIconState.getXTranslation();
219             canvas.drawLine(lastIcon, 0, lastIcon, height, paint);
220 
221             if (mFirstVisibleIconState != null) {
222                 int firstIcon = (int) mFirstVisibleIconState.getXTranslation();
223                 canvas.drawLine(firstIcon, 0, firstIcon, height, paint);
224             }
225 
226             paint.setColor(Color.RED);
227             canvas.drawLine(mVisualOverflowStart, 0, mVisualOverflowStart, height, paint);
228         }
229     }
230 
231     @Override
onConfigurationChanged(Configuration newConfig)232     protected void onConfigurationChanged(Configuration newConfig) {
233         super.onConfigurationChanged(newConfig);
234         initResources();
235     }
236 
237     @Override
hasOverlappingRendering()238     public boolean hasOverlappingRendering() {
239         // Does the same as "AlphaOptimizedFrameLayout".
240         return false;
241     }
242 
243     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)244     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
245         final int childCount = getChildCount();
246         final int maxVisibleIcons = getMaxVisibleIcons(childCount);
247         final int width = MeasureSpec.getSize(widthMeasureSpec);
248         final int childWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.UNSPECIFIED);
249         int totalWidth = (int) (getActualPaddingStart() + getActualPaddingEnd());
250         for (int i = 0; i < childCount; i++) {
251             View child = getChildAt(i);
252             measureChild(child, childWidthSpec, heightMeasureSpec);
253             if (i <= maxVisibleIcons) {
254                 totalWidth += child.getMeasuredWidth();
255             }
256         }
257         final int measuredWidth = resolveSize(totalWidth, widthMeasureSpec);
258         final int measuredHeight = MeasureSpec.getSize(heightMeasureSpec);
259         setMeasuredDimension(measuredWidth, measuredHeight);
260     }
261 
262     @Override
onLayout(boolean changed, int l, int t, int r, int b)263     protected void onLayout(boolean changed, int l, int t, int r, int b) {
264         float centerY = getHeight() / 2.0f;
265         // we layout all our children on the left at the top
266         mIconSize = 0;
267         for (int i = 0; i < getChildCount(); i++) {
268             View child = getChildAt(i);
269             // We need to layout all children even the GONE ones, such that the heights are
270             // calculated correctly as they are used to calculate how many we can fit on the screen
271             int width = child.getMeasuredWidth();
272             int height = child.getMeasuredHeight();
273             int top = (int) (centerY - height / 2.0f);
274             child.layout(0, top, width, top + height);
275             if (i == 0) {
276                 setIconSize(child.getWidth());
277             }
278         }
279         getLocationOnScreen(mAbsolutePosition);
280         if (mIsStaticLayout) {
281             updateState();
282         }
283     }
284 
285     @Override
toString()286     public String toString() {
287         if (NotificationIconContainerRefactor.isEnabled()) {
288             return super.toString()
289                     + " {"
290                     + " overrideIconColor=" + mOverrideIconColor
291                     + ", maxIcons=" + mMaxIcons
292                     + ", isStaticLayout=" + mIsStaticLayout
293                     + ", themedTextColorPrimary=#" + Integer.toHexString(mThemedTextColorPrimary)
294                     + " }";
295         } else {
296             return "NotificationIconContainer("
297                     + "dozing=" + mDozing + " onLockScreen=" + mOnLockScreen
298                     + " overrideIconColor=" + mOverrideIconColor
299                     + " speedBumpIndex=" + mSpeedBumpIndex
300                     + " themedTextColorPrimary=#" + Integer.toHexString(mThemedTextColorPrimary)
301                     + ')';
302         }
303     }
304 
305     @VisibleForTesting
setIconSize(int size)306     public void setIconSize(int size) {
307         mIconSize = size;
308     }
309 
updateState()310     private void updateState() {
311         resetViewStates();
312         calculateIconXTranslations();
313         applyIconStates();
314     }
315 
applyIconStates()316     public void applyIconStates() {
317         for (int i = 0; i < getChildCount(); i++) {
318             View child = getChildAt(i);
319             ViewState childState = mIconStates.get(child);
320             if (childState != null) {
321                 childState.applyToView(child);
322             }
323         }
324         mAddAnimationStartIndex = -1;
325         mCannedAnimationStartIndex = -1;
326         mDisallowNextAnimation = false;
327         mIsolatedIconForAnimation = null;
328     }
329 
330     @Override
onViewAdded(View child)331     public void onViewAdded(View child) {
332         super.onViewAdded(child);
333         boolean isReplacingIcon = isReplacingIcon(child);
334         if (!mChangingViewPositions) {
335             IconState v = new IconState(child);
336             if (isReplacingIcon) {
337                 v.justAdded = false;
338                 v.justReplaced = true;
339             }
340             mIconStates.put(child, v);
341         }
342         int childIndex = indexOfChild(child);
343         if (childIndex < getChildCount() - 1 && !isReplacingIcon
344             && mIconStates.get(getChildAt(childIndex + 1)).iconAppearAmount > 0.0f) {
345             if (mAddAnimationStartIndex < 0) {
346                 mAddAnimationStartIndex = childIndex;
347             } else {
348                 mAddAnimationStartIndex = Math.min(mAddAnimationStartIndex, childIndex);
349             }
350         }
351         if (child instanceof StatusBarIconView) {
352             if (NotificationIconContainerRefactor.isEnabled()) {
353                 if (!mChangingViewPositions) {
354                     ((StatusBarIconView) child).updateIconDimens();
355                 }
356             } else {
357                 ((StatusBarIconView) child).updateIconDimens();
358                 ((StatusBarIconView) child).setDozing(mDozing, false, 0);
359             }
360         }
361     }
362 
isReplacingIcon(View child)363     private boolean isReplacingIcon(View child) {
364         if (!(child instanceof StatusBarIconView)) {
365             return false;
366         }
367         StatusBarIconView iconView = (StatusBarIconView) child;
368         Icon sourceIcon = iconView.getSourceIcon();
369         String groupKey = iconView.getNotification().getGroupKey();
370         if (NotificationIconContainerRefactor.isEnabled()) {
371             if (mReplacingIcons == null) {
372                 return false;
373             }
374             StatusBarIcon replacedIcon = mReplacingIcons.get(groupKey);
375             return replacedIcon != null && sourceIcon.sameAs(replacedIcon.icon);
376         } else {
377             if (mReplacingIconsLegacy == null) {
378                 return false;
379             }
380             ArrayList<StatusBarIcon> statusBarIcons = mReplacingIconsLegacy.get(groupKey);
381             if (statusBarIcons != null) {
382                 StatusBarIcon replacedIcon = statusBarIcons.get(0);
383                 return sourceIcon.sameAs(replacedIcon.icon);
384             }
385             return false;
386         }
387     }
388 
389     @Override
onViewRemoved(View child)390     public void onViewRemoved(View child) {
391         super.onViewRemoved(child);
392 
393         if (child instanceof StatusBarIconView) {
394             boolean isReplacingIcon = isReplacingIcon(child);
395             final StatusBarIconView icon = (StatusBarIconView) child;
396             if (areAnimationsEnabled(icon) && icon.getVisibleState() != StatusBarIconView.STATE_HIDDEN
397                     && child.getVisibility() == VISIBLE && isReplacingIcon) {
398                 int animationStartIndex = findFirstViewIndexAfter(icon.getTranslationX());
399                 if (mAddAnimationStartIndex < 0) {
400                     mAddAnimationStartIndex = animationStartIndex;
401                 } else {
402                     mAddAnimationStartIndex = Math.min(mAddAnimationStartIndex, animationStartIndex);
403                 }
404             }
405             if (!mChangingViewPositions) {
406                 mIconStates.remove(child);
407                 if (areAnimationsEnabled(icon) && !isReplacingIcon) {
408                     addTransientView(icon, 0);
409                     boolean isIsolatedIcon = child == mIsolatedIcon;
410                     icon.setVisibleState(StatusBarIconView.STATE_HIDDEN, true /* animate */,
411                             () -> removeTransientView(icon),
412                             isIsolatedIcon ? CONTENT_FADE_DURATION : 0);
413                 }
414             }
415         }
416     }
417 
418     /**
419      * Removes all child {@link StatusBarIconView} instances from this container, immediately and
420      * without animation. This should be called when tearing down this container so that external
421      * icon views are not holding onto a reference thru {@link View#getParent()}.
422      */
detachAllIcons()423     public void detachAllIcons() {
424         boolean animsWereEnabled = mAnimationsEnabled;
425         boolean wasChangingPositions = mChangingViewPositions;
426         mAnimationsEnabled = false;
427         mChangingViewPositions = true;
428         removeAllViews();
429         mChangingViewPositions = wasChangingPositions;
430         mAnimationsEnabled = animsWereEnabled;
431     }
432 
areIconsOverflowing()433     public boolean areIconsOverflowing() {
434         return mIsShowingOverflowDot;
435     }
436 
areAnimationsEnabled(StatusBarIconView icon)437     private boolean areAnimationsEnabled(StatusBarIconView icon) {
438         return mAnimationsEnabled || icon == mIsolatedIcon;
439     }
440 
441     /**
442      * Finds the first view with a translation bigger then a given value
443      */
findFirstViewIndexAfter(float translationX)444     private int findFirstViewIndexAfter(float translationX) {
445         for (int i = 0; i < getChildCount(); i++) {
446             View view = getChildAt(i);
447             if (view.getTranslationX() > translationX) {
448                 return i;
449             }
450         }
451         return getChildCount();
452     }
453 
resetViewStates()454     public void resetViewStates() {
455         for (int i = 0; i < getChildCount(); i++) {
456             View view = getChildAt(i);
457             ViewState iconState = mIconStates.get(view);
458             iconState.initFrom(view);
459             iconState.setAlpha(mIsolatedIcon == null || view == mIsolatedIcon ? 1.0f : 0.0f);
460             iconState.hidden = false;
461         }
462     }
463 
464     /**
465      * @return Width of shelf for the given number of icons
466      */
calculateWidthFor(float numIcons)467     public float calculateWidthFor(float numIcons) {
468         if (numIcons == 0) {
469             return 0f;
470         }
471         final float contentWidth;
472         if (NotificationIconContainerRefactor.isEnabled()) {
473             contentWidth = mIconSize * numIcons;
474         } else {
475             contentWidth = mIconSize * MathUtils.min(numIcons, mMaxIconsOnLockscreen + 1);
476         }
477         return getActualPaddingStart() + contentWidth + getActualPaddingEnd();
478     }
479 
480     @VisibleForTesting
shouldForceOverflow(int i, int speedBumpIndex, float iconAppearAmount, int maxVisibleIcons)481     boolean shouldForceOverflow(int i, int speedBumpIndex, float iconAppearAmount,
482             int maxVisibleIcons) {
483         if (NotificationIconContainerRefactor.isEnabled()) {
484             return i >= maxVisibleIcons && iconAppearAmount > 0.0f;
485         } else {
486             return speedBumpIndex != -1 && i >= speedBumpIndex
487                     && iconAppearAmount > 0.0f || i >= maxVisibleIcons;
488         }
489     }
490 
491     @VisibleForTesting
isOverflowing(boolean isLastChild, float translationX, float layoutEnd, float iconSize)492     boolean isOverflowing(boolean isLastChild, float translationX, float layoutEnd,
493             float iconSize) {
494         if (isLastChild) {
495             return translationX + iconSize > layoutEnd;
496         } else {
497             // If the child is not the last child, we need to ensure that we have room for the next
498             // icon and the dot. The dot could be as large as an icon, so verify that we have room
499             // for 2 icons.
500             return translationX + iconSize * 2f > layoutEnd;
501         }
502     }
503 
504     /**
505      * Calculate the horizontal translations for each notification based on how much the icons
506      * are inserted into the notification container.
507      * If this is not a whole number, the fraction means by how much the icon is appearing.
508      */
calculateIconXTranslations()509     public void calculateIconXTranslations() {
510         float translationX = getActualPaddingStart();
511         int firstOverflowIndex = -1;
512         int childCount = getChildCount();
513         int maxVisibleIcons = getMaxVisibleIcons(childCount);
514         float layoutEnd = getLayoutEnd();
515         mVisualOverflowStart = 0;
516         mFirstVisibleIconState = null;
517         for (int i = 0; i < childCount; i++) {
518             View view = getChildAt(i);
519             IconState iconState = mIconStates.get(view);
520             if (iconState.iconAppearAmount == 1.0f) {
521                 // We only modify the xTranslation if it's fully inside of the container
522                 // since during the transition to the shelf, the translations are controlled
523                 // from the outside
524                 iconState.setXTranslation(translationX);
525             }
526             if (mFirstVisibleIconState == null) {
527                 mFirstVisibleIconState = iconState;
528             }
529             iconState.visibleState = iconState.hidden
530                     ? StatusBarIconView.STATE_HIDDEN
531                     : StatusBarIconView.STATE_ICON;
532 
533             final boolean forceOverflow = shouldForceOverflow(i, mSpeedBumpIndex,
534                     iconState.iconAppearAmount, maxVisibleIcons);
535             final boolean isOverflowing = forceOverflow || isOverflowing(
536                     /* isLastChild= */ i == childCount - 1, translationX, layoutEnd, mIconSize);
537 
538             // First icon to overflow.
539             if (firstOverflowIndex == -1 && isOverflowing) {
540                 firstOverflowIndex = i;
541                 mVisualOverflowStart = translationX;
542             }
543 
544             final float drawingScale = getDrawingScale(view);
545             translationX += iconState.iconAppearAmount * view.getWidth() * drawingScale;
546         }
547         mIsShowingOverflowDot = false;
548         if (firstOverflowIndex != -1) {
549             translationX = mVisualOverflowStart;
550             for (int i = firstOverflowIndex; i < childCount; i++) {
551                 View view = getChildAt(i);
552                 IconState iconState = mIconStates.get(view);
553                 int dotWidth = mStaticDotDiameter + mDotPadding;
554                 iconState.setXTranslation(translationX);
555                 if (!mIsShowingOverflowDot) {
556                     if (iconState.iconAppearAmount < 0.8f) {
557                         iconState.visibleState = StatusBarIconView.STATE_ICON;
558                     } else {
559                         iconState.visibleState = StatusBarIconView.STATE_DOT;
560                         mIsShowingOverflowDot = true;
561                     }
562                     translationX += dotWidth * iconState.iconAppearAmount;
563                     mLastVisibleIconState = iconState;
564                 } else {
565                     iconState.visibleState = StatusBarIconView.STATE_HIDDEN;
566                 }
567             }
568         } else if (childCount > 0) {
569             View lastChild = getChildAt(childCount - 1);
570             mLastVisibleIconState = mIconStates.get(lastChild);
571             mFirstVisibleIconState = mIconStates.get(getChildAt(0));
572         }
573         if (isLayoutRtl()) {
574             for (int i = 0; i < childCount; i++) {
575                 View view = getChildAt(i);
576                 IconState iconState = mIconStates.get(view);
577                 iconState.setXTranslation(
578                         getWidth() - iconState.getXTranslation() - view.getWidth());
579             }
580         }
581         if (mIsolatedIcon != null) {
582             IconState iconState = mIconStates.get(mIsolatedIcon);
583             if (iconState != null) {
584                 // Most of the time the icon isn't yet added when this is called but only happening
585                 // later. The isolated icon position left should equal to the mIsolatedIconLocation
586                 // to ensure the icon be put at the center of the HUN icon placeholder,
587                 // {@See HeadsUpAppearanceController#updateIsolatedIconLocation}.
588                 iconState.setXTranslation(mIsolatedIconLocation.left - mAbsolutePosition[0]);
589                 iconState.visibleState = StatusBarIconView.STATE_ICON;
590             }
591         }
592     }
593 
getDrawingScale(View view)594     private float getDrawingScale(View view) {
595         final boolean useIncreasedScale = NotificationIconContainerRefactor.isEnabled()
596                 ? mUseIncreasedIconScale
597                 : mOnLockScreen;
598         return useIncreasedScale && view instanceof StatusBarIconView
599                 ? ((StatusBarIconView) view).getIconScaleIncreased()
600                 : 1f;
601     }
602 
setUseIncreasedIconScale(boolean useIncreasedIconScale)603     public void setUseIncreasedIconScale(boolean useIncreasedIconScale) {
604         if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return;
605         mUseIncreasedIconScale = useIncreasedIconScale;
606     }
607 
getMaxVisibleIcons(int childCount)608     private int getMaxVisibleIcons(int childCount) {
609         if (NotificationIconContainerRefactor.isEnabled()) {
610             return mMaxIcons;
611         } else {
612             return mOnLockScreen ? mMaxIconsOnAod : mIsStaticLayout ? mMaxStaticIcons : childCount;
613         }
614     }
615 
getLayoutEnd()616     private float getLayoutEnd() {
617         return getActualWidth() - getActualPaddingEnd();
618     }
619 
getActualPaddingEnd()620     private float getActualPaddingEnd() {
621         if (mActualPaddingEnd == NO_VALUE) {
622             return getPaddingEnd();
623         }
624         return mActualPaddingEnd;
625     }
626 
627     /**
628      * @return the actual startPadding of this view
629      */
getActualPaddingStart()630     public float getActualPaddingStart() {
631         if (mActualPaddingStart == NO_VALUE) {
632             return getPaddingStart();
633         }
634         return mActualPaddingStart;
635     }
636 
637     /**
638      * Sets whether the layout should always show the same number of icons.
639      * If this is true, the icon positions will be updated on layout.
640      * If this if false, the layout is managed from the outside and layouting won't trigger a
641      * repositioning of the icons.
642      */
setIsStaticLayout(boolean isStaticLayout)643     public void setIsStaticLayout(boolean isStaticLayout) {
644         mIsStaticLayout = isStaticLayout;
645     }
646 
setActualLayoutWidth(int actualLayoutWidth)647     public void setActualLayoutWidth(int actualLayoutWidth) {
648         mActualLayoutWidth = actualLayoutWidth;
649         if (DEBUG) {
650             invalidate();
651         }
652     }
653 
setActualPaddingEnd(float paddingEnd)654     public void setActualPaddingEnd(float paddingEnd) {
655         mActualPaddingEnd = paddingEnd;
656         if (DEBUG) {
657             invalidate();
658         }
659     }
660 
setActualPaddingStart(float paddingStart)661     public void setActualPaddingStart(float paddingStart) {
662         mActualPaddingStart = paddingStart;
663         if (DEBUG) {
664             invalidate();
665         }
666     }
667 
getActualWidth()668     public int getActualWidth() {
669         if (mActualLayoutWidth == NO_VALUE) {
670             return getWidth();
671         }
672         return mActualLayoutWidth;
673     }
674 
getFinalTranslationX()675     public int getFinalTranslationX() {
676         if (mLastVisibleIconState == null) {
677             return 0;
678         }
679 
680         int translation = (int) (isLayoutRtl()
681                 ? getWidth() - mLastVisibleIconState.getXTranslation()
682                 : mLastVisibleIconState.getXTranslation() + mIconSize);
683 
684         // There's a chance that last translation goes beyond the edge maybe
685         return Math.min(getWidth(), translation);
686     }
687 
setChangingViewPositions(boolean changingViewPositions)688     public void setChangingViewPositions(boolean changingViewPositions) {
689         mChangingViewPositions = changingViewPositions;
690     }
691 
setDozing(boolean dozing, boolean animate, long delay)692     public void setDozing(boolean dozing, boolean animate, long delay) {
693         NotificationIconContainerRefactor.assertInLegacyMode();
694         setDozing(dozing, animate, delay, /* endRunnable= */ null);
695     }
696 
setDozing(boolean dozing, boolean animate, long delay, @Nullable Runnable endRunnable)697     private void setDozing(boolean dozing, boolean animate, long delay,
698             @Nullable Runnable endRunnable) {
699         NotificationIconContainerRefactor.assertInLegacyMode();
700         mDozing = dozing;
701         mDisallowNextAnimation |= !animate;
702         final int childCount = getChildCount();
703         // Track all the child invocations of setDozing, invoking the top-level endRunnable once
704         // they have all completed.
705         final Runnable onChildCompleted = endRunnable == null ? null : new Runnable() {
706             private int mPendingCallbacks = childCount;
707 
708             @Override
709             public void run() {
710                 if (--mPendingCallbacks == 0) {
711                     endRunnable.run();
712                 }
713             }
714         };
715         for (int i = 0; i < childCount; i++) {
716             View view = getChildAt(i);
717             if (view instanceof StatusBarIconView) {
718                 ((StatusBarIconView) view).setDozing(dozing, animate, delay, onChildCompleted);
719             } else if (onChildCompleted != null) {
720                 onChildCompleted.run();
721             }
722         }
723     }
724 
getIconState(StatusBarIconView icon)725     public IconState getIconState(StatusBarIconView icon) {
726         return mIconStates.get(icon);
727     }
728 
setSpeedBumpIndex(int speedBumpIndex)729     public void setSpeedBumpIndex(int speedBumpIndex) {
730         NotificationIconContainerRefactor.assertInLegacyMode();
731         mSpeedBumpIndex = speedBumpIndex;
732     }
733 
setMaxIconsAmount(int maxIcons)734     public void setMaxIconsAmount(int maxIcons) {
735         if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return;
736         mMaxIcons = maxIcons;
737     }
738 
getIconSize()739     public int getIconSize() {
740         return mIconSize;
741     }
742 
setAnimationsEnabled(boolean enabled)743     public void setAnimationsEnabled(boolean enabled) {
744         if (!enabled && mAnimationsEnabled) {
745             for (int i = 0; i < getChildCount(); i++) {
746                 View child = getChildAt(i);
747                 ViewState childState = mIconStates.get(child);
748                 if (childState != null) {
749                     childState.cancelAnimations(child);
750                     childState.applyToView(child);
751                 }
752             }
753         }
754         mAnimationsEnabled = enabled;
755     }
756 
setReplacingIconsLegacy(ArrayMap<String, ArrayList<StatusBarIcon>> replacingIcons)757     public void setReplacingIconsLegacy(ArrayMap<String, ArrayList<StatusBarIcon>> replacingIcons) {
758         NotificationIconContainerRefactor.assertInLegacyMode();
759         mReplacingIconsLegacy = replacingIcons;
760     }
761 
setReplacingIcons(ArrayMap<String, StatusBarIcon> replacingIcons)762     public void setReplacingIcons(ArrayMap<String, StatusBarIcon> replacingIcons) {
763         if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return;
764         mReplacingIcons = replacingIcons;
765     }
766 
767     @Deprecated
showIconIsolatedLegacy(StatusBarIconView icon, boolean animated)768     public void showIconIsolatedLegacy(StatusBarIconView icon, boolean animated) {
769         NotificationIconContainerRefactor.assertInLegacyMode();
770         if (animated) {
771             mIsolatedIconForAnimation = icon != null ? icon : mIsolatedIcon;
772         }
773         mIsolatedIcon = icon;
774         updateState();
775     }
776 
showIconIsolatedAnimated(StatusBarIconView icon, @Nullable Runnable onAnimationEnd)777     public void showIconIsolatedAnimated(StatusBarIconView icon,
778             @Nullable Runnable onAnimationEnd) {
779         if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return;
780         mIsolatedIconForAnimation = icon != null ? icon : mIsolatedIcon;
781         mIsolatedIconAnimationEndRunnable = onAnimationEnd;
782         showIconIsolated(icon);
783     }
784 
showIconIsolated(StatusBarIconView icon)785     public void showIconIsolated(StatusBarIconView icon) {
786         if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return;
787         mIsolatedIcon = icon;
788         updateState();
789     }
790 
setIsolatedIconLocation(Rect isolatedIconLocation, boolean requireUpdate)791     public void setIsolatedIconLocation(Rect isolatedIconLocation, boolean requireUpdate) {
792         mIsolatedIconLocation = isolatedIconLocation;
793         if (requireUpdate) {
794             updateState();
795         }
796     }
797 
798     /**
799      * Set whether the device is on the lockscreen and which lockscreen mode the device is
800      * configured to. Depending on these values, the layout of the AOD icons change.
801      */
setOnLockScreen(boolean onLockScreen)802     public void setOnLockScreen(boolean onLockScreen) {
803         NotificationIconContainerRefactor.assertInLegacyMode();
804         mOnLockScreen = onLockScreen;
805     }
806 
807     @Deprecated
setInNotificationIconShelf(boolean inShelf)808     public void setInNotificationIconShelf(boolean inShelf) {
809         NotificationIconContainerRefactor.assertInLegacyMode();
810         mOverrideIconColor = inShelf;
811     }
812 
setOverrideIconColor(boolean override)813     public void setOverrideIconColor(boolean override) {
814         if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return;
815         mOverrideIconColor = override;
816     }
817 
818     public class IconState extends ViewState {
819         public float iconAppearAmount = 1.0f;
820         public float clampedAppearAmount = 1.0f;
821         public int visibleState;
822         public boolean justAdded = true;
823         private boolean justReplaced;
824         public boolean needsCannedAnimation;
825         public int iconColor = StatusBarIconView.NO_COLOR;
826         public boolean noAnimations;
827         private final View mView;
828 
829         private final Consumer<Property> mCannedAnimationEndListener;
830 
IconState(View child)831         public IconState(View child) {
832             mView = child;
833             mCannedAnimationEndListener = (property) -> {
834                 // If we finished animating out of the shelf
835                 if (property == View.TRANSLATION_Y && iconAppearAmount == 0.0f
836                         && mView.getVisibility() == VISIBLE) {
837                     mView.setVisibility(INVISIBLE);
838                 }
839             };
840         }
841 
842         @Override
applyToView(View view)843         public void applyToView(View view) {
844             if (view instanceof StatusBarIconView) {
845                 StatusBarIconView icon = (StatusBarIconView) view;
846                 boolean animate = false;
847                 AnimationProperties animationProperties = null;
848                 final boolean animationsAllowed = animationsAllowed(icon);
849                 if (animationsAllowed) {
850                     if (justAdded || justReplaced) {
851                         super.applyToView(icon);
852                         if (justAdded && iconAppearAmount != 0.0f) {
853                             icon.setAlpha(0.0f);
854                             icon.setVisibleState(StatusBarIconView.STATE_HIDDEN,
855                                     false /* animate */);
856                             animationProperties = ADD_ICON_PROPERTIES;
857                             animate = true;
858                         }
859                     } else if (visibleState != icon.getVisibleState()) {
860                         animationProperties = DOT_ANIMATION_PROPERTIES;
861                         animate = true;
862                     }
863                     if (!animate && mAddAnimationStartIndex >= 0
864                             && indexOfChild(view) >= mAddAnimationStartIndex
865                             && (icon.getVisibleState() != StatusBarIconView.STATE_HIDDEN
866                             || visibleState != StatusBarIconView.STATE_HIDDEN)) {
867                         animationProperties = DOT_ANIMATION_PROPERTIES;
868                         animate = true;
869                     }
870                     if (needsCannedAnimation) {
871                         AnimationFilter animationFilter = sTempProperties.getAnimationFilter();
872                         animationFilter.reset();
873                         animationFilter.combineFilter(
874                                 ICON_ANIMATION_PROPERTIES.getAnimationFilter());
875                         sTempProperties.resetCustomInterpolators();
876                         sTempProperties.combineCustomInterpolators(ICON_ANIMATION_PROPERTIES);
877                         Interpolator interpolator;
878                         if (icon.showsConversation()) {
879                             interpolator = Interpolators.ICON_OVERSHOT_LESS;
880                         } else {
881                             interpolator = Interpolators.ICON_OVERSHOT;
882                         }
883                         sTempProperties.setCustomInterpolator(View.TRANSLATION_Y, interpolator);
884                         sTempProperties.setAnimationEndAction(mCannedAnimationEndListener);
885                         if (animationProperties != null) {
886                             animationFilter.combineFilter(animationProperties.getAnimationFilter());
887                             sTempProperties.combineCustomInterpolators(animationProperties);
888                         }
889                         animationProperties = sTempProperties;
890                         animationProperties.setDuration(CANNED_ANIMATION_DURATION);
891                         animate = true;
892                         mCannedAnimationStartIndex = indexOfChild(view);
893                     }
894                     if (!animate && mCannedAnimationStartIndex >= 0
895                             && indexOfChild(view) > mCannedAnimationStartIndex
896                             && (icon.getVisibleState() != StatusBarIconView.STATE_HIDDEN
897                             || visibleState != StatusBarIconView.STATE_HIDDEN)) {
898                         AnimationFilter animationFilter = sTempProperties.getAnimationFilter();
899                         animationFilter.reset();
900                         animationFilter.animateX();
901                         sTempProperties.resetCustomInterpolators();
902                         animationProperties = sTempProperties;
903                         animationProperties.setDuration(CANNED_ANIMATION_DURATION);
904                         animate = true;
905                     }
906                     if (mIsolatedIconForAnimation != null) {
907                         if (view == mIsolatedIconForAnimation) {
908                             animationProperties = UNISOLATION_PROPERTY;
909                             animationProperties.setDelay(
910                                     mIsolatedIcon != null ? CONTENT_FADE_DELAY : 0);
911                             Consumer<Property> endAction = getEndAction();
912                             if (endAction != null) {
913                                 animationProperties.setAnimationEndAction(endAction);
914                                 animationProperties.setAnimationCancelAction(endAction);
915                             }
916                         } else {
917                             animationProperties = UNISOLATION_PROPERTY_OTHERS;
918                             animationProperties.setDelay(
919                                     mIsolatedIcon == null ? CONTENT_FADE_DELAY : 0);
920                         }
921                         animate = true;
922                     }
923                 }
924                 icon.setVisibleState(visibleState, animationsAllowed);
925                 if (NotificationIconContainerRefactor.isEnabled()) {
926                     if (mOverrideIconColor) {
927                         icon.setIconColor(mThemedTextColorPrimary,
928                                 /* animate= */ needsCannedAnimation && animationsAllowed);
929                     }
930                 } else {
931                     icon.setIconColor(mOverrideIconColor ? mThemedTextColorPrimary : iconColor,
932                             needsCannedAnimation && animationsAllowed);
933                 }
934                 if (animate) {
935                     animateTo(icon, animationProperties);
936                 } else {
937                     super.applyToView(view);
938                 }
939                 sTempProperties.setAnimationEndAction(null);
940             }
941             justAdded = false;
942             justReplaced = false;
943             needsCannedAnimation = false;
944         }
945 
animationsAllowed(StatusBarIconView icon)946         private boolean animationsAllowed(StatusBarIconView icon) {
947             final boolean isLowPriorityIconChange =
948                     (visibleState == StatusBarIconView.STATE_HIDDEN
949                             && icon.getVisibleState() == StatusBarIconView.STATE_DOT)
950                     || (visibleState == StatusBarIconView.STATE_DOT
951                         && icon.getVisibleState() == StatusBarIconView.STATE_HIDDEN);
952             return areAnimationsEnabled(icon)
953                     && !mDisallowNextAnimation
954                     && !noAnimations
955                     && !isLowPriorityIconChange;
956         }
957 
958         @Nullable
getEndAction()959         private Consumer<Property> getEndAction() {
960             if (mIsolatedIconAnimationEndRunnable == null) return null;
961             final Runnable endRunnable = mIsolatedIconAnimationEndRunnable;
962             return prop -> {
963                 endRunnable.run();
964                 if (mIsolatedIconAnimationEndRunnable == endRunnable) {
965                     mIsolatedIconAnimationEndRunnable = null;
966                 }
967             };
968         }
969 
970         @Override
initFrom(View view)971         public void initFrom(View view) {
972             super.initFrom(view);
973             if (view instanceof StatusBarIconView) {
974                 iconColor = ((StatusBarIconView) view).getStaticDrawableColor();
975             }
976         }
977     }
978 }
979