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