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