1 /*
2  * Copyright (C) 2008 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;
18 
19 import static com.android.systemui.plugins.DarkIconDispatcher.getTint;
20 
21 import android.animation.Animator;
22 import android.animation.AnimatorListenerAdapter;
23 import android.animation.ObjectAnimator;
24 import android.animation.ValueAnimator;
25 import android.app.Notification;
26 import android.content.Context;
27 import android.content.pm.ApplicationInfo;
28 import android.content.res.ColorStateList;
29 import android.content.res.Configuration;
30 import android.content.res.Resources;
31 import android.graphics.Canvas;
32 import android.graphics.Color;
33 import android.graphics.ColorMatrixColorFilter;
34 import android.graphics.Paint;
35 import android.graphics.Rect;
36 import android.graphics.drawable.Drawable;
37 import android.graphics.drawable.Icon;
38 import android.os.Parcelable;
39 import android.os.UserHandle;
40 import android.service.notification.StatusBarNotification;
41 import android.text.TextUtils;
42 import android.util.AttributeSet;
43 import android.util.FloatProperty;
44 import android.util.Log;
45 import android.util.Property;
46 import android.util.TypedValue;
47 import android.view.ViewDebug;
48 import android.view.accessibility.AccessibilityEvent;
49 import android.view.animation.Interpolator;
50 
51 import androidx.core.graphics.ColorUtils;
52 
53 import com.android.internal.statusbar.StatusBarIcon;
54 import com.android.internal.util.ContrastColorUtil;
55 import com.android.systemui.Interpolators;
56 import com.android.systemui.R;
57 import com.android.systemui.statusbar.notification.NotificationIconDozeHelper;
58 import com.android.systemui.statusbar.notification.NotificationUtils;
59 
60 import java.text.NumberFormat;
61 import java.util.Arrays;
62 
63 public class StatusBarIconView extends AnimatedImageView implements StatusIconDisplayable {
64     public static final int NO_COLOR = 0;
65 
66     /**
67      * Multiply alpha values with (1+DARK_ALPHA_BOOST) when dozing. The chosen value boosts
68      * everything above 30% to 50%, making it appear on 1bit color depths.
69      */
70     private static final float DARK_ALPHA_BOOST = 0.67f;
71     /**
72      * Status icons are currently drawn with the intention of being 17dp tall, but we
73      * want to scale them (in a way that doesn't require an asset dump) down 2dp. So
74      * 17dp * (15 / 17) = 15dp, the new height. After the first call to {@link #reloadDimens} all
75      * values will be in px.
76      */
77     private float mSystemIconDesiredHeight = 15f;
78     private float mSystemIconIntrinsicHeight = 17f;
79     private float mSystemIconDefaultScale = mSystemIconDesiredHeight / mSystemIconIntrinsicHeight;
80     private final int ANIMATION_DURATION_FAST = 100;
81 
82     public static final int STATE_ICON = 0;
83     public static final int STATE_DOT = 1;
84     public static final int STATE_HIDDEN = 2;
85 
86     private static final String TAG = "StatusBarIconView";
87     private static final Property<StatusBarIconView, Float> ICON_APPEAR_AMOUNT
88             = new FloatProperty<StatusBarIconView>("iconAppearAmount") {
89 
90         @Override
91         public void setValue(StatusBarIconView object, float value) {
92             object.setIconAppearAmount(value);
93         }
94 
95         @Override
96         public Float get(StatusBarIconView object) {
97             return object.getIconAppearAmount();
98         }
99     };
100     private static final Property<StatusBarIconView, Float> DOT_APPEAR_AMOUNT
101             = new FloatProperty<StatusBarIconView>("dot_appear_amount") {
102 
103         @Override
104         public void setValue(StatusBarIconView object, float value) {
105             object.setDotAppearAmount(value);
106         }
107 
108         @Override
109         public Float get(StatusBarIconView object) {
110             return object.getDotAppearAmount();
111         }
112     };
113 
114     private boolean mAlwaysScaleIcon;
115     private int mStatusBarIconDrawingSizeIncreased = 1;
116     private int mStatusBarIconDrawingSize = 1;
117     private int mStatusBarIconSize = 1;
118     private StatusBarIcon mIcon;
119     @ViewDebug.ExportedProperty private String mSlot;
120     private Drawable mNumberBackground;
121     private Paint mNumberPain;
122     private int mNumberX;
123     private int mNumberY;
124     private String mNumberText;
125     private StatusBarNotification mNotification;
126     private final boolean mBlocked;
127     private int mDensity;
128     private boolean mNightMode;
129     private float mIconScale = 1.0f;
130     private final Paint mDotPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
131     private float mDotRadius;
132     private int mStaticDotRadius;
133     private int mVisibleState = STATE_ICON;
134     private float mIconAppearAmount = 1.0f;
135     private ObjectAnimator mIconAppearAnimator;
136     private ObjectAnimator mDotAnimator;
137     private float mDotAppearAmount;
138     private OnVisibilityChangedListener mOnVisibilityChangedListener;
139     private int mDrawableColor;
140     private int mIconColor;
141     private int mDecorColor;
142     private float mDozeAmount;
143     private ValueAnimator mColorAnimator;
144     private int mCurrentSetColor = NO_COLOR;
145     private int mAnimationStartColor = NO_COLOR;
146     private final ValueAnimator.AnimatorUpdateListener mColorUpdater
147             = animation -> {
148         int newColor = NotificationUtils.interpolateColors(mAnimationStartColor, mIconColor,
149                 animation.getAnimatedFraction());
150         setColorInternal(newColor);
151     };
152     private final NotificationIconDozeHelper mDozer;
153     private int mContrastedDrawableColor;
154     private int mCachedContrastBackgroundColor = NO_COLOR;
155     private float[] mMatrix;
156     private ColorMatrixColorFilter mMatrixColorFilter;
157     private boolean mIsInShelf;
158     private Runnable mLayoutRunnable;
159     private boolean mDismissed;
160     private Runnable mOnDismissListener;
161     private boolean mIncreasedSize;
162 
StatusBarIconView(Context context, String slot, StatusBarNotification sbn)163     public StatusBarIconView(Context context, String slot, StatusBarNotification sbn) {
164         this(context, slot, sbn, false);
165     }
166 
StatusBarIconView(Context context, String slot, StatusBarNotification sbn, boolean blocked)167     public StatusBarIconView(Context context, String slot, StatusBarNotification sbn,
168             boolean blocked) {
169         super(context);
170         mDozer = new NotificationIconDozeHelper(context);
171         mBlocked = blocked;
172         mSlot = slot;
173         mNumberPain = new Paint();
174         mNumberPain.setTextAlign(Paint.Align.CENTER);
175         mNumberPain.setColor(context.getColor(R.drawable.notification_number_text_color));
176         mNumberPain.setAntiAlias(true);
177         setNotification(sbn);
178         setScaleType(ScaleType.CENTER);
179         mDensity = context.getResources().getDisplayMetrics().densityDpi;
180         Configuration configuration = context.getResources().getConfiguration();
181         mNightMode = (configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK)
182                 == Configuration.UI_MODE_NIGHT_YES;
183         initializeDecorColor();
184         reloadDimens();
185         maybeUpdateIconScaleDimens();
186     }
187 
188     /** Should always be preceded by {@link #reloadDimens()} */
maybeUpdateIconScaleDimens()189     private void maybeUpdateIconScaleDimens() {
190         // We do not resize and scale system icons (on the right), only notification icons (on the
191         // left).
192         if (mNotification != null || mAlwaysScaleIcon) {
193             updateIconScaleForNotifications();
194         } else {
195             updateIconScaleForSystemIcons();
196         }
197     }
198 
updateIconScaleForNotifications()199     private void updateIconScaleForNotifications() {
200         final float imageBounds = mIncreasedSize ?
201                 mStatusBarIconDrawingSizeIncreased : mStatusBarIconDrawingSize;
202         final int outerBounds = mStatusBarIconSize;
203         mIconScale = imageBounds / (float)outerBounds;
204         updatePivot();
205     }
206 
207     // Makes sure that all icons are scaled to the same height (15dp). If we cannot get a height
208     // for the icon, it uses the default SCALE (15f / 17f) which is the old behavior
updateIconScaleForSystemIcons()209     private void updateIconScaleForSystemIcons() {
210         float iconHeight = getIconHeight();
211         if (iconHeight != 0) {
212             mIconScale = mSystemIconDesiredHeight / iconHeight;
213         } else {
214             mIconScale = mSystemIconDefaultScale;
215         }
216     }
217 
getIconHeight()218     private float getIconHeight() {
219         Drawable d = getDrawable();
220         if (d != null) {
221             return (float) getDrawable().getIntrinsicHeight();
222         } else {
223             return mSystemIconIntrinsicHeight;
224         }
225     }
226 
getIconScaleIncreased()227     public float getIconScaleIncreased() {
228         return (float) mStatusBarIconDrawingSizeIncreased / mStatusBarIconDrawingSize;
229     }
230 
getIconScale()231     public float getIconScale() {
232         return mIconScale;
233     }
234 
235     @Override
onConfigurationChanged(Configuration newConfig)236     protected void onConfigurationChanged(Configuration newConfig) {
237         super.onConfigurationChanged(newConfig);
238         int density = newConfig.densityDpi;
239         if (density != mDensity) {
240             mDensity = density;
241             reloadDimens();
242             updateDrawable();
243             maybeUpdateIconScaleDimens();
244         }
245         boolean nightMode = (newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK)
246                 == Configuration.UI_MODE_NIGHT_YES;
247         if (nightMode != mNightMode) {
248             mNightMode = nightMode;
249             initializeDecorColor();
250         }
251     }
252 
reloadDimens()253     private void reloadDimens() {
254         boolean applyRadius = mDotRadius == mStaticDotRadius;
255         Resources res = getResources();
256         mStaticDotRadius = res.getDimensionPixelSize(R.dimen.overflow_dot_radius);
257         mStatusBarIconSize = res.getDimensionPixelSize(R.dimen.status_bar_icon_size);
258         mStatusBarIconDrawingSizeIncreased =
259                 res.getDimensionPixelSize(R.dimen.status_bar_icon_drawing_size_dark);
260         mStatusBarIconDrawingSize =
261                 res.getDimensionPixelSize(R.dimen.status_bar_icon_drawing_size);
262         if (applyRadius) {
263             mDotRadius = mStaticDotRadius;
264         }
265         mSystemIconDesiredHeight = res.getDimension(
266                 com.android.internal.R.dimen.status_bar_system_icon_size);
267         mSystemIconIntrinsicHeight = res.getDimension(
268                 com.android.internal.R.dimen.status_bar_system_icon_intrinsic_size);
269         mSystemIconDefaultScale = mSystemIconDesiredHeight / mSystemIconIntrinsicHeight;
270     }
271 
setNotification(StatusBarNotification notification)272     public void setNotification(StatusBarNotification notification) {
273         mNotification = notification;
274         if (notification != null) {
275             setContentDescription(notification.getNotification());
276         }
277         maybeUpdateIconScaleDimens();
278     }
279 
StatusBarIconView(Context context, AttributeSet attrs)280     public StatusBarIconView(Context context, AttributeSet attrs) {
281         super(context, attrs);
282         mDozer = new NotificationIconDozeHelper(context);
283         mBlocked = false;
284         mAlwaysScaleIcon = true;
285         reloadDimens();
286         maybeUpdateIconScaleDimens();
287         mDensity = context.getResources().getDisplayMetrics().densityDpi;
288     }
289 
streq(String a, String b)290     private static boolean streq(String a, String b) {
291         if (a == b) {
292             return true;
293         }
294         if (a == null && b != null) {
295             return false;
296         }
297         if (a != null && b == null) {
298             return false;
299         }
300         return a.equals(b);
301     }
302 
equalIcons(Icon a, Icon b)303     public boolean equalIcons(Icon a, Icon b) {
304         if (a == b) return true;
305         if (a.getType() != b.getType()) return false;
306         switch (a.getType()) {
307             case Icon.TYPE_RESOURCE:
308                 return a.getResPackage().equals(b.getResPackage()) && a.getResId() == b.getResId();
309             case Icon.TYPE_URI:
310                 return a.getUriString().equals(b.getUriString());
311             default:
312                 return false;
313         }
314     }
315     /**
316      * Returns whether the set succeeded.
317      */
set(StatusBarIcon icon)318     public boolean set(StatusBarIcon icon) {
319         final boolean iconEquals = mIcon != null && equalIcons(mIcon.icon, icon.icon);
320         final boolean levelEquals = iconEquals
321                 && mIcon.iconLevel == icon.iconLevel;
322         final boolean visibilityEquals = mIcon != null
323                 && mIcon.visible == icon.visible;
324         final boolean numberEquals = mIcon != null
325                 && mIcon.number == icon.number;
326         mIcon = icon.clone();
327         setContentDescription(icon.contentDescription);
328         if (!iconEquals) {
329             if (!updateDrawable(false /* no clear */)) return false;
330             // we have to clear the grayscale tag since it may have changed
331             setTag(R.id.icon_is_grayscale, null);
332             // Maybe set scale based on icon height
333             maybeUpdateIconScaleDimens();
334         }
335         if (!levelEquals) {
336             setImageLevel(icon.iconLevel);
337         }
338 
339         if (!numberEquals) {
340             if (icon.number > 0 && getContext().getResources().getBoolean(
341                         R.bool.config_statusBarShowNumber)) {
342                 if (mNumberBackground == null) {
343                     mNumberBackground = getContext().getResources().getDrawable(
344                             R.drawable.ic_notification_overlay);
345                 }
346                 placeNumber();
347             } else {
348                 mNumberBackground = null;
349                 mNumberText = null;
350             }
351             invalidate();
352         }
353         if (!visibilityEquals) {
354             setVisibility(icon.visible && !mBlocked ? VISIBLE : GONE);
355         }
356         return true;
357     }
358 
updateDrawable()359     public void updateDrawable() {
360         updateDrawable(true /* with clear */);
361     }
362 
updateDrawable(boolean withClear)363     private boolean updateDrawable(boolean withClear) {
364         if (mIcon == null) {
365             return false;
366         }
367         Drawable drawable;
368         try {
369             drawable = getIcon(mIcon);
370         } catch (OutOfMemoryError e) {
371             Log.w(TAG, "OOM while inflating " + mIcon.icon + " for slot " + mSlot);
372             return false;
373         }
374 
375         if (drawable == null) {
376             Log.w(TAG, "No icon for slot " + mSlot + "; " + mIcon.icon);
377             return false;
378         }
379         if (withClear) {
380             setImageDrawable(null);
381         }
382         setImageDrawable(drawable);
383         return true;
384     }
385 
getSourceIcon()386     public Icon getSourceIcon() {
387         return mIcon.icon;
388     }
389 
getIcon(StatusBarIcon icon)390     private Drawable getIcon(StatusBarIcon icon) {
391         return getIcon(getContext(), icon);
392     }
393 
394     /**
395      * Returns the right icon to use for this item
396      *
397      * @param context Context to use to get resources
398      * @return Drawable for this item, or null if the package or item could not
399      *         be found
400      */
getIcon(Context context, StatusBarIcon statusBarIcon)401     public static Drawable getIcon(Context context, StatusBarIcon statusBarIcon) {
402         int userId = statusBarIcon.user.getIdentifier();
403         if (userId == UserHandle.USER_ALL) {
404             userId = UserHandle.USER_SYSTEM;
405         }
406 
407         Drawable icon = statusBarIcon.icon.loadDrawableAsUser(context, userId);
408 
409         TypedValue typedValue = new TypedValue();
410         context.getResources().getValue(R.dimen.status_bar_icon_scale_factor, typedValue, true);
411         float scaleFactor = typedValue.getFloat();
412 
413         // No need to scale the icon, so return it as is.
414         if (scaleFactor == 1.f) {
415             return icon;
416         }
417 
418         return new ScalingDrawableWrapper(icon, scaleFactor);
419     }
420 
getStatusBarIcon()421     public StatusBarIcon getStatusBarIcon() {
422         return mIcon;
423     }
424 
425     @Override
onInitializeAccessibilityEvent(AccessibilityEvent event)426     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
427         super.onInitializeAccessibilityEvent(event);
428         if (mNotification != null) {
429             event.setParcelableData(mNotification.getNotification());
430         }
431     }
432 
433     @Override
onSizeChanged(int w, int h, int oldw, int oldh)434     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
435         super.onSizeChanged(w, h, oldw, oldh);
436         if (mNumberBackground != null) {
437             placeNumber();
438         }
439     }
440 
441     @Override
onRtlPropertiesChanged(int layoutDirection)442     public void onRtlPropertiesChanged(int layoutDirection) {
443         super.onRtlPropertiesChanged(layoutDirection);
444         updateDrawable();
445     }
446 
447     @Override
onDraw(Canvas canvas)448     protected void onDraw(Canvas canvas) {
449         if (mIconAppearAmount > 0.0f) {
450             canvas.save();
451             canvas.scale(mIconScale * mIconAppearAmount, mIconScale * mIconAppearAmount,
452                     getWidth() / 2, getHeight() / 2);
453             super.onDraw(canvas);
454             canvas.restore();
455         }
456 
457         if (mNumberBackground != null) {
458             mNumberBackground.draw(canvas);
459             canvas.drawText(mNumberText, mNumberX, mNumberY, mNumberPain);
460         }
461         if (mDotAppearAmount != 0.0f) {
462             float radius;
463             float alpha = Color.alpha(mDecorColor) / 255.f;
464             if (mDotAppearAmount <= 1.0f) {
465                 radius = mDotRadius * mDotAppearAmount;
466             } else {
467                 float fadeOutAmount = mDotAppearAmount - 1.0f;
468                 alpha = alpha * (1.0f - fadeOutAmount);
469                 radius = NotificationUtils.interpolate(mDotRadius, getWidth() / 4, fadeOutAmount);
470             }
471             mDotPaint.setAlpha((int) (alpha * 255));
472             canvas.drawCircle(mStatusBarIconSize / 2, getHeight() / 2, radius, mDotPaint);
473         }
474     }
475 
476     @Override
debug(int depth)477     protected void debug(int depth) {
478         super.debug(depth);
479         Log.d("View", debugIndent(depth) + "slot=" + mSlot);
480         Log.d("View", debugIndent(depth) + "icon=" + mIcon);
481     }
482 
placeNumber()483     void placeNumber() {
484         final String str;
485         final int tooBig = getContext().getResources().getInteger(
486                 android.R.integer.status_bar_notification_info_maxnum);
487         if (mIcon.number > tooBig) {
488             str = getContext().getResources().getString(
489                         android.R.string.status_bar_notification_info_overflow);
490         } else {
491             NumberFormat f = NumberFormat.getIntegerInstance();
492             str = f.format(mIcon.number);
493         }
494         mNumberText = str;
495 
496         final int w = getWidth();
497         final int h = getHeight();
498         final Rect r = new Rect();
499         mNumberPain.getTextBounds(str, 0, str.length(), r);
500         final int tw = r.right - r.left;
501         final int th = r.bottom - r.top;
502         mNumberBackground.getPadding(r);
503         int dw = r.left + tw + r.right;
504         if (dw < mNumberBackground.getMinimumWidth()) {
505             dw = mNumberBackground.getMinimumWidth();
506         }
507         mNumberX = w-r.right-((dw-r.right-r.left)/2);
508         int dh = r.top + th + r.bottom;
509         if (dh < mNumberBackground.getMinimumWidth()) {
510             dh = mNumberBackground.getMinimumWidth();
511         }
512         mNumberY = h-r.bottom-((dh-r.top-th-r.bottom)/2);
513         mNumberBackground.setBounds(w-dw, h-dh, w, h);
514     }
515 
setContentDescription(Notification notification)516     private void setContentDescription(Notification notification) {
517         if (notification != null) {
518             String d = contentDescForNotification(mContext, notification);
519             if (!TextUtils.isEmpty(d)) {
520                 setContentDescription(d);
521             }
522         }
523     }
524 
toString()525     public String toString() {
526         return "StatusBarIconView(slot=" + mSlot + " icon=" + mIcon
527             + " notification=" + mNotification + ")";
528     }
529 
getNotification()530     public StatusBarNotification getNotification() {
531         return mNotification;
532     }
533 
getSlot()534     public String getSlot() {
535         return mSlot;
536     }
537 
538 
contentDescForNotification(Context c, Notification n)539     public static String contentDescForNotification(Context c, Notification n) {
540         String appName = "";
541         try {
542             Notification.Builder builder = Notification.Builder.recoverBuilder(c, n);
543             appName = builder.loadHeaderAppName();
544         } catch (RuntimeException e) {
545             Log.e(TAG, "Unable to recover builder", e);
546             // Trying to get the app name from the app info instead.
547             Parcelable appInfo = n.extras.getParcelable(
548                     Notification.EXTRA_BUILDER_APPLICATION_INFO);
549             if (appInfo instanceof ApplicationInfo) {
550                 appName = String.valueOf(((ApplicationInfo) appInfo).loadLabel(
551                         c.getPackageManager()));
552             }
553         }
554 
555         CharSequence title = n.extras.getCharSequence(Notification.EXTRA_TITLE);
556         CharSequence text = n.extras.getCharSequence(Notification.EXTRA_TEXT);
557         CharSequence ticker = n.tickerText;
558 
559         // Some apps just put the app name into the title
560         CharSequence titleOrText = TextUtils.equals(title, appName) ? text : title;
561 
562         CharSequence desc = !TextUtils.isEmpty(titleOrText) ? titleOrText
563                 : !TextUtils.isEmpty(ticker) ? ticker : "";
564 
565         return c.getString(R.string.accessibility_desc_notification_icon, appName, desc);
566     }
567 
568     /**
569      * Set the color that is used to draw decoration like the overflow dot. This will not be applied
570      * to the drawable.
571      */
setDecorColor(int iconTint)572     public void setDecorColor(int iconTint) {
573         mDecorColor = iconTint;
574         updateDecorColor();
575     }
576 
initializeDecorColor()577     private void initializeDecorColor() {
578         if (mNotification != null) {
579             setDecorColor(getContext().getColor(mNightMode
580                     ? com.android.internal.R.color.notification_default_color_dark
581                     : com.android.internal.R.color.notification_default_color_light));
582         }
583     }
584 
updateDecorColor()585     private void updateDecorColor() {
586         int color = NotificationUtils.interpolateColors(mDecorColor, Color.WHITE, mDozeAmount);
587         if (mDotPaint.getColor() != color) {
588             mDotPaint.setColor(color);
589 
590             if (mDotAppearAmount != 0) {
591                 invalidate();
592             }
593         }
594     }
595 
596     /**
597      * Set the static color that should be used for the drawable of this icon if it's not
598      * transitioning this also immediately sets the color.
599      */
setStaticDrawableColor(int color)600     public void setStaticDrawableColor(int color) {
601         mDrawableColor = color;
602         setColorInternal(color);
603         updateContrastedStaticColor();
604         mIconColor = color;
605         mDozer.setColor(color);
606     }
607 
setColorInternal(int color)608     private void setColorInternal(int color) {
609         mCurrentSetColor = color;
610         updateIconColor();
611     }
612 
updateIconColor()613     private void updateIconColor() {
614         if (mCurrentSetColor != NO_COLOR) {
615             if (mMatrixColorFilter == null) {
616                 mMatrix = new float[4 * 5];
617                 mMatrixColorFilter = new ColorMatrixColorFilter(mMatrix);
618             }
619             int color = NotificationUtils.interpolateColors(
620                     mCurrentSetColor, Color.WHITE, mDozeAmount);
621             updateTintMatrix(mMatrix, color, DARK_ALPHA_BOOST * mDozeAmount);
622             mMatrixColorFilter.setColorMatrixArray(mMatrix);
623             setColorFilter(null);  // setColorFilter only invalidates if the instance changed.
624             setColorFilter(mMatrixColorFilter);
625         } else {
626             mDozer.updateGrayscale(this, mDozeAmount);
627         }
628     }
629 
630     /**
631      * Updates {@param array} such that it represents a matrix that changes RGB to {@param color}
632      * and multiplies the alpha channel with the color's alpha+{@param alphaBoost}.
633      */
updateTintMatrix(float[] array, int color, float alphaBoost)634     private static void updateTintMatrix(float[] array, int color, float alphaBoost) {
635         Arrays.fill(array, 0);
636         array[4] = Color.red(color);
637         array[9] = Color.green(color);
638         array[14] = Color.blue(color);
639         array[18] = Color.alpha(color) / 255f + alphaBoost;
640     }
641 
setIconColor(int iconColor, boolean animate)642     public void setIconColor(int iconColor, boolean animate) {
643         if (mIconColor != iconColor) {
644             mIconColor = iconColor;
645             if (mColorAnimator != null) {
646                 mColorAnimator.cancel();
647             }
648             if (mCurrentSetColor == iconColor) {
649                 return;
650             }
651             if (animate && mCurrentSetColor != NO_COLOR) {
652                 mAnimationStartColor = mCurrentSetColor;
653                 mColorAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
654                 mColorAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
655                 mColorAnimator.setDuration(ANIMATION_DURATION_FAST);
656                 mColorAnimator.addUpdateListener(mColorUpdater);
657                 mColorAnimator.addListener(new AnimatorListenerAdapter() {
658                     @Override
659                     public void onAnimationEnd(Animator animation) {
660                         mColorAnimator = null;
661                         mAnimationStartColor = NO_COLOR;
662                     }
663                 });
664                 mColorAnimator.start();
665             } else {
666                 setColorInternal(iconColor);
667             }
668         }
669     }
670 
getStaticDrawableColor()671     public int getStaticDrawableColor() {
672         return mDrawableColor;
673     }
674 
675     /**
676      * A drawable color that passes GAR on a specific background.
677      * This value is cached.
678      *
679      * @param backgroundColor Background to test against.
680      * @return GAR safe version of {@link StatusBarIconView#getStaticDrawableColor()}.
681      */
getContrastedStaticDrawableColor(int backgroundColor)682     int getContrastedStaticDrawableColor(int backgroundColor) {
683         if (mCachedContrastBackgroundColor != backgroundColor) {
684             mCachedContrastBackgroundColor = backgroundColor;
685             updateContrastedStaticColor();
686         }
687         return mContrastedDrawableColor;
688     }
689 
updateContrastedStaticColor()690     private void updateContrastedStaticColor() {
691         if (Color.alpha(mCachedContrastBackgroundColor) != 255) {
692             mContrastedDrawableColor = mDrawableColor;
693             return;
694         }
695         // We'll modify the color if it doesn't pass GAR
696         int contrastedColor = mDrawableColor;
697         if (!ContrastColorUtil.satisfiesTextContrast(mCachedContrastBackgroundColor,
698                 contrastedColor)) {
699             float[] hsl = new float[3];
700             ColorUtils.colorToHSL(mDrawableColor, hsl);
701             // This is basically a light grey, pushing the color will only distort it.
702             // Best thing to do in here is to fallback to the default color.
703             if (hsl[1] < 0.2f) {
704                 contrastedColor = Notification.COLOR_DEFAULT;
705             }
706             boolean isDark = !ContrastColorUtil.isColorLight(mCachedContrastBackgroundColor);
707             contrastedColor = ContrastColorUtil.resolveContrastColor(mContext,
708                     contrastedColor, mCachedContrastBackgroundColor, isDark);
709         }
710         mContrastedDrawableColor = contrastedColor;
711     }
712 
713     @Override
setVisibleState(int state)714     public void setVisibleState(int state) {
715         setVisibleState(state, true /* animate */, null /* endRunnable */);
716     }
717 
setVisibleState(int state, boolean animate)718     public void setVisibleState(int state, boolean animate) {
719         setVisibleState(state, animate, null);
720     }
721 
722     @Override
hasOverlappingRendering()723     public boolean hasOverlappingRendering() {
724         return false;
725     }
726 
setVisibleState(int visibleState, boolean animate, Runnable endRunnable)727     public void setVisibleState(int visibleState, boolean animate, Runnable endRunnable) {
728         setVisibleState(visibleState, animate, endRunnable, 0);
729     }
730 
731     /**
732      * Set the visibleState of this view.
733      *
734      * @param visibleState The new state.
735      * @param animate Should we animate?
736      * @param endRunnable The runnable to run at the end.
737      * @param duration The duration of an animation or 0 if the default should be taken.
738      */
setVisibleState(int visibleState, boolean animate, Runnable endRunnable, long duration)739     public void setVisibleState(int visibleState, boolean animate, Runnable endRunnable,
740             long duration) {
741         boolean runnableAdded = false;
742         if (visibleState != mVisibleState) {
743             mVisibleState = visibleState;
744             if (mIconAppearAnimator != null) {
745                 mIconAppearAnimator.cancel();
746             }
747             if (mDotAnimator != null) {
748                 mDotAnimator.cancel();
749             }
750             if (animate) {
751                 float targetAmount = 0.0f;
752                 Interpolator interpolator = Interpolators.FAST_OUT_LINEAR_IN;
753                 if (visibleState == STATE_ICON) {
754                     targetAmount = 1.0f;
755                     interpolator = Interpolators.LINEAR_OUT_SLOW_IN;
756                 }
757                 float currentAmount = getIconAppearAmount();
758                 if (targetAmount != currentAmount) {
759                     mIconAppearAnimator = ObjectAnimator.ofFloat(this, ICON_APPEAR_AMOUNT,
760                             currentAmount, targetAmount);
761                     mIconAppearAnimator.setInterpolator(interpolator);
762                     mIconAppearAnimator.setDuration(duration == 0 ? ANIMATION_DURATION_FAST
763                             : duration);
764                     mIconAppearAnimator.addListener(new AnimatorListenerAdapter() {
765                         @Override
766                         public void onAnimationEnd(Animator animation) {
767                             mIconAppearAnimator = null;
768                             runRunnable(endRunnable);
769                         }
770                     });
771                     mIconAppearAnimator.start();
772                     runnableAdded = true;
773                 }
774 
775                 targetAmount = visibleState == STATE_ICON ? 2.0f : 0.0f;
776                 interpolator = Interpolators.FAST_OUT_LINEAR_IN;
777                 if (visibleState == STATE_DOT) {
778                     targetAmount = 1.0f;
779                     interpolator = Interpolators.LINEAR_OUT_SLOW_IN;
780                 }
781                 currentAmount = getDotAppearAmount();
782                 if (targetAmount != currentAmount) {
783                     mDotAnimator = ObjectAnimator.ofFloat(this, DOT_APPEAR_AMOUNT,
784                             currentAmount, targetAmount);
785                     mDotAnimator.setInterpolator(interpolator);;
786                     mDotAnimator.setDuration(duration == 0 ? ANIMATION_DURATION_FAST
787                             : duration);
788                     final boolean runRunnable = !runnableAdded;
789                     mDotAnimator.addListener(new AnimatorListenerAdapter() {
790                         @Override
791                         public void onAnimationEnd(Animator animation) {
792                             mDotAnimator = null;
793                             if (runRunnable) {
794                                 runRunnable(endRunnable);
795                             }
796                         }
797                     });
798                     mDotAnimator.start();
799                     runnableAdded = true;
800                 }
801             } else {
802                 setIconAppearAmount(visibleState == STATE_ICON ? 1.0f : 0.0f);
803                 setDotAppearAmount(visibleState == STATE_DOT ? 1.0f
804                         : visibleState == STATE_ICON ? 2.0f
805                         : 0.0f);
806             }
807         }
808         if (!runnableAdded) {
809             runRunnable(endRunnable);
810         }
811     }
812 
runRunnable(Runnable runnable)813     private void runRunnable(Runnable runnable) {
814         if (runnable != null) {
815             runnable.run();
816         }
817     }
818 
setIconAppearAmount(float iconAppearAmount)819     public void setIconAppearAmount(float iconAppearAmount) {
820         if (mIconAppearAmount != iconAppearAmount) {
821             mIconAppearAmount = iconAppearAmount;
822             invalidate();
823         }
824     }
825 
getIconAppearAmount()826     public float getIconAppearAmount() {
827         return mIconAppearAmount;
828     }
829 
getVisibleState()830     public int getVisibleState() {
831         return mVisibleState;
832     }
833 
setDotAppearAmount(float dotAppearAmount)834     public void setDotAppearAmount(float dotAppearAmount) {
835         if (mDotAppearAmount != dotAppearAmount) {
836             mDotAppearAmount = dotAppearAmount;
837             invalidate();
838         }
839     }
840 
841     @Override
setVisibility(int visibility)842     public void setVisibility(int visibility) {
843         super.setVisibility(visibility);
844         if (mOnVisibilityChangedListener != null) {
845             mOnVisibilityChangedListener.onVisibilityChanged(visibility);
846         }
847     }
848 
getDotAppearAmount()849     public float getDotAppearAmount() {
850         return mDotAppearAmount;
851     }
852 
setOnVisibilityChangedListener(OnVisibilityChangedListener listener)853     public void setOnVisibilityChangedListener(OnVisibilityChangedListener listener) {
854         mOnVisibilityChangedListener = listener;
855     }
856 
setDozing(boolean dozing, boolean fade, long delay)857     public void setDozing(boolean dozing, boolean fade, long delay) {
858         mDozer.setDozing(f -> {
859             mDozeAmount = f;
860             updateDecorColor();
861             updateIconColor();
862             updateAllowAnimation();
863         }, dozing, fade, delay, this);
864     }
865 
updateAllowAnimation()866     private void updateAllowAnimation() {
867         if (mDozeAmount == 0 || mDozeAmount == 1) {
868             setAllowAnimation(mDozeAmount == 0);
869         }
870     }
871 
872     /**
873      * This method returns the drawing rect for the view which is different from the regular
874      * drawing rect, since we layout all children at position 0 and usually the translation is
875      * neglected. The standard implementation doesn't account for translation.
876      *
877      * @param outRect The (scrolled) drawing bounds of the view.
878      */
879     @Override
getDrawingRect(Rect outRect)880     public void getDrawingRect(Rect outRect) {
881         super.getDrawingRect(outRect);
882         float translationX = getTranslationX();
883         float translationY = getTranslationY();
884         outRect.left += translationX;
885         outRect.right += translationX;
886         outRect.top += translationY;
887         outRect.bottom += translationY;
888     }
889 
setIsInShelf(boolean isInShelf)890     public void setIsInShelf(boolean isInShelf) {
891         mIsInShelf = isInShelf;
892     }
893 
isInShelf()894     public boolean isInShelf() {
895         return mIsInShelf;
896     }
897 
898     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)899     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
900         super.onLayout(changed, left, top, right, bottom);
901         if (mLayoutRunnable != null) {
902             mLayoutRunnable.run();
903             mLayoutRunnable = null;
904         }
905         updatePivot();
906     }
907 
updatePivot()908     private void updatePivot() {
909         setPivotX((1 - mIconScale) / 2.0f * getWidth());
910         setPivotY((getHeight() - mIconScale * getWidth()) / 2.0f);
911     }
912 
executeOnLayout(Runnable runnable)913     public void executeOnLayout(Runnable runnable) {
914         mLayoutRunnable = runnable;
915     }
916 
setDismissed()917     public void setDismissed() {
918         mDismissed = true;
919         if (mOnDismissListener != null) {
920             mOnDismissListener.run();
921         }
922     }
923 
isDismissed()924     public boolean isDismissed() {
925         return mDismissed;
926     }
927 
setOnDismissListener(Runnable onDismissListener)928     public void setOnDismissListener(Runnable onDismissListener) {
929         mOnDismissListener = onDismissListener;
930     }
931 
932     @Override
onDarkChanged(Rect area, float darkIntensity, int tint)933     public void onDarkChanged(Rect area, float darkIntensity, int tint) {
934         int areaTint = getTint(area, this, tint);
935         ColorStateList color = ColorStateList.valueOf(areaTint);
936         setImageTintList(color);
937         setDecorColor(areaTint);
938     }
939 
940     @Override
isIconVisible()941     public boolean isIconVisible() {
942         return mIcon != null && mIcon.visible;
943     }
944 
945     @Override
isIconBlocked()946     public boolean isIconBlocked() {
947         return mBlocked;
948     }
949 
setIncreasedSize(boolean increasedSize)950     public void setIncreasedSize(boolean increasedSize) {
951         mIncreasedSize = increasedSize;
952         maybeUpdateIconScaleDimens();
953     }
954 
955     public interface OnVisibilityChangedListener {
onVisibilityChanged(int newVisibility)956         void onVisibilityChanged(int newVisibility);
957     }
958 }
959