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