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