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