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