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