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