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