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