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