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