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 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 
    157     public StatusBarIconView(Context context, String slot, StatusBarNotification sbn) {
    158         this(context, slot, sbn, false);
    159     }
    160 
    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()} */
    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 
    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 
    203     private void updateIconScaleForSystemIcons() {
    204         mIconScale = SYSTEM_ICON_SCALE;
    205     }
    206 
    207     public float getIconScaleFullyDark() {
    208         return (float) mStatusBarIconDrawingSizeDark / mStatusBarIconDrawingSize;
    209     }
    210 
    211     public float getIconScale() {
    212         return mIconScale;
    213     }
    214 
    215     @Override
    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 
    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 
    241     public void setNotification(StatusBarNotification notification) {
    242         mNotification = notification;
    243         if (notification != null) {
    244             setContentDescription(notification.getNotification());
    245         }
    246     }
    247 
    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 
    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 
    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      */
    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 
    325     public void updateDrawable() {
    326         updateDrawable(true /* with clear */);
    327     }
    328 
    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 
    352     public Icon getSourceIcon() {
    353         return mIcon.icon;
    354     }
    355 
    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      */
    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 
    387     public StatusBarIcon getStatusBarIcon() {
    388         return mIcon;
    389     }
    390 
    391     @Override
    392     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
    393         super.onInitializeAccessibilityEvent(event);
    394         if (mNotification != null) {
    395             event.setParcelableData(mNotification.getNotification());
    396         }
    397     }
    398 
    399     @Override
    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
    408     public void onRtlPropertiesChanged(int layoutDirection) {
    409         super.onRtlPropertiesChanged(layoutDirection);
    410         updateDrawable();
    411     }
    412 
    413     @Override
    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
    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 
    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 
    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 
    491     public String toString() {
    492         return "StatusBarIconView(slot=" + mSlot + " icon=" + mIcon
    493             + " notification=" + mNotification + ")";
    494     }
    495 
    496     public StatusBarNotification getNotification() {
    497         return mNotification;
    498     }
    499 
    500     public String getSlot() {
    501         return mSlot;
    502     }
    503 
    504 
    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      */
    538     public void setDecorColor(int iconTint) {
    539         mDecorColor = iconTint;
    540         updateDecorColor();
    541     }
    542 
    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      */
    558     public void setStaticDrawableColor(int color) {
    559         mDrawableColor = color;
    560         setColorInternal(color);
    561         updateContrastedStaticColor();
    562         mIconColor = color;
    563         mDozer.setColor(color);
    564     }
    565 
    566     private void setColorInternal(int color) {
    567         mCurrentSetColor = color;
    568         updateIconColor();
    569     }
    570 
    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      */
    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 
    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 
    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      */
    640     int getContrastedStaticDrawableColor(int backgroundColor) {
    641         if (mCachedContrastBackgroundColor != backgroundColor) {
    642             mCachedContrastBackgroundColor = backgroundColor;
    643             updateContrastedStaticColor();
    644         }
    645         return mContrastedDrawableColor;
    646     }
    647 
    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
    671     public void setVisibleState(int state) {
    672         setVisibleState(state, true /* animate */, null /* endRunnable */);
    673     }
    674 
    675     public void setVisibleState(int state, boolean animate) {
    676         setVisibleState(state, animate, null);
    677     }
    678 
    679     @Override
    680     public boolean hasOverlappingRendering() {
    681         return false;
    682     }
    683 
    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      */
    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 
    770     private void runRunnable(Runnable runnable) {
    771         if (runnable != null) {
    772             runnable.run();
    773         }
    774     }
    775 
    776     public void setIconAppearAmount(float iconAppearAmount) {
    777         if (mIconAppearAmount != iconAppearAmount) {
    778             mIconAppearAmount = iconAppearAmount;
    779             invalidate();
    780         }
    781     }
    782 
    783     public float getIconAppearAmount() {
    784         return mIconAppearAmount;
    785     }
    786 
    787     public int getVisibleState() {
    788         return mVisibleState;
    789     }
    790 
    791     public void setDotAppearAmount(float dotAppearAmount) {
    792         if (mDotAppearAmount != dotAppearAmount) {
    793             mDotAppearAmount = dotAppearAmount;
    794             invalidate();
    795         }
    796     }
    797 
    798     @Override
    799     public void setVisibility(int visibility) {
    800         super.setVisibility(visibility);
    801         if (mOnVisibilityChangedListener != null) {
    802             mOnVisibilityChangedListener.onVisibilityChanged(visibility);
    803         }
    804     }
    805 
    806     public float getDotAppearAmount() {
    807         return mDotAppearAmount;
    808     }
    809 
    810     public void setOnVisibilityChangedListener(OnVisibilityChangedListener listener) {
    811         mOnVisibilityChangedListener = listener;
    812     }
    813 
    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 
    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
    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 
    848     public void setIsInShelf(boolean isInShelf) {
    849         mIsInShelf = isInShelf;
    850     }
    851 
    852     public boolean isInShelf() {
    853         return mIsInShelf;
    854     }
    855 
    856     @Override
    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 
    866     private void updatePivot() {
    867         setPivotX((1 - mIconScale) / 2.0f * getWidth());
    868         setPivotY((getHeight() - mIconScale * getWidth()) / 2.0f);
    869     }
    870 
    871     public void executeOnLayout(Runnable runnable) {
    872         mLayoutRunnable = runnable;
    873     }
    874 
    875     public void setDismissed() {
    876         mDismissed = true;
    877         if (mOnDismissListener != null) {
    878             mOnDismissListener.run();
    879         }
    880     }
    881 
    882     public boolean isDismissed() {
    883         return mDismissed;
    884     }
    885 
    886     public void setOnDismissListener(Runnable onDismissListener) {
    887         mOnDismissListener = onDismissListener;
    888     }
    889 
    890     @Override
    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
    899     public boolean isIconVisible() {
    900         return mIcon != null && mIcon.visible;
    901     }
    902 
    903     @Override
    904     public boolean isIconBlocked() {
    905         return mBlocked;
    906     }
    907 
    908     public interface OnVisibilityChangedListener {
    909         void onVisibilityChanged(int newVisibility);
    910     }
    911 }
    912