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