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