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