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