1 /* 2 * Copyright (C) 2016 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.settingslib.drawable; 18 19 import static android.app.admin.DevicePolicyResources.Drawables.Style.SOLID_COLORED; 20 import static android.app.admin.DevicePolicyResources.Drawables.WORK_PROFILE_ICON; 21 import static android.app.admin.DevicePolicyResources.Drawables.WORK_PROFILE_USER_ICON; 22 23 import android.annotation.ColorInt; 24 import android.annotation.DrawableRes; 25 import android.app.admin.DevicePolicyManager; 26 import android.content.Context; 27 import android.content.res.ColorStateList; 28 import android.graphics.Bitmap; 29 import android.graphics.BitmapShader; 30 import android.graphics.Canvas; 31 import android.graphics.Color; 32 import android.graphics.ColorFilter; 33 import android.graphics.Matrix; 34 import android.graphics.Paint; 35 import android.graphics.PixelFormat; 36 import android.graphics.PorterDuff; 37 import android.graphics.PorterDuffColorFilter; 38 import android.graphics.PorterDuffXfermode; 39 import android.graphics.Rect; 40 import android.graphics.RectF; 41 import android.graphics.Shader; 42 import android.graphics.drawable.BitmapDrawable; 43 import android.graphics.drawable.Drawable; 44 import android.os.Build; 45 import android.os.UserHandle; 46 47 import androidx.annotation.NonNull; 48 import androidx.annotation.RequiresApi; 49 import androidx.annotation.VisibleForTesting; 50 51 import com.android.settingslib.utils.BuildCompatUtils; 52 53 /** 54 * Converts the user avatar icon to a circularly clipped one with an optional badge and frame 55 */ 56 public class UserIconDrawable extends Drawable implements Drawable.Callback { 57 58 private Drawable mUserDrawable; 59 private Bitmap mUserIcon; 60 private Bitmap mBitmap; // baked representation. Required for transparent border around badge 61 private final Paint mIconPaint = new Paint(); 62 private final Paint mPaint = new Paint(); 63 private final Matrix mIconMatrix = new Matrix(); 64 private float mIntrinsicRadius; 65 private float mDisplayRadius; 66 private float mPadding = 0; 67 private int mSize = 0; // custom "intrinsic" size for this drawable if non-zero 68 private boolean mInvalidated = true; 69 private ColorStateList mTintColor = null; 70 private PorterDuff.Mode mTintMode = PorterDuff.Mode.SRC_ATOP; 71 72 private float mFrameWidth; 73 private float mFramePadding; 74 private ColorStateList mFrameColor = null; 75 private Paint mFramePaint; 76 77 private Drawable mBadge; 78 private Paint mClearPaint; 79 private float mBadgeRadius; 80 private float mBadgeMargin; 81 82 /** 83 * Gets the system default managed-user badge as a drawable. This drawable is tint-able. 84 * For badging purpose, consider 85 * {@link android.content.pm.PackageManager#getUserBadgedDrawableForDensity(Drawable, UserHandle, Rect, int)}. 86 * 87 * @param context 88 * @return drawable containing just the badge 89 */ getManagedUserDrawable(Context context)90 public static Drawable getManagedUserDrawable(Context context) { 91 if (BuildCompatUtils.isAtLeastT()) { 92 return getUpdatableManagedUserDrawable(context); 93 } else { 94 return getDrawableForDisplayDensity( 95 context, com.android.internal.R.drawable.ic_corp_user_badge); 96 } 97 } 98 99 @RequiresApi(Build.VERSION_CODES.TIRAMISU) getUpdatableManagedUserDrawable(Context context)100 private static Drawable getUpdatableManagedUserDrawable(Context context) { 101 DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class); 102 return dpm.getResources().getDrawableForDensity( 103 WORK_PROFILE_USER_ICON, 104 SOLID_COLORED, 105 context.getResources().getDisplayMetrics().densityDpi, 106 /* default= */ () -> getDrawableForDisplayDensity( 107 context, com.android.internal.R.drawable.ic_corp_user_badge)); 108 } 109 getDrawableForDisplayDensity( Context context, @DrawableRes int drawable)110 private static Drawable getDrawableForDisplayDensity( 111 Context context, @DrawableRes int drawable) { 112 int density = context.getResources().getDisplayMetrics().densityDpi; 113 return context.getResources().getDrawableForDensity( 114 drawable, density, context.getTheme()); 115 } 116 117 /** 118 * Gets the preferred list-item size of this drawable. 119 * @param context 120 * @return size in pixels 121 */ getDefaultSize(Context context)122 public static int getDefaultSize(Context context) { 123 return context.getResources() 124 .getDimensionPixelSize(com.android.internal.R.dimen.user_icon_size); 125 } 126 UserIconDrawable()127 public UserIconDrawable() { 128 this(0); 129 } 130 131 /** 132 * Use this constructor if the drawable is intended to be placed in listviews 133 * @param intrinsicSize if 0, the intrinsic size will come from the icon itself 134 */ UserIconDrawable(int intrinsicSize)135 public UserIconDrawable(int intrinsicSize) { 136 super(); 137 mIconPaint.setAntiAlias(true); 138 mIconPaint.setFilterBitmap(true); 139 mPaint.setFilterBitmap(true); 140 mPaint.setAntiAlias(true); 141 if (intrinsicSize > 0) { 142 setBounds(0, 0, intrinsicSize, intrinsicSize); 143 setIntrinsicSize(intrinsicSize); 144 } 145 setIcon(null); 146 } 147 setIcon(Bitmap icon)148 public UserIconDrawable setIcon(Bitmap icon) { 149 if (mUserDrawable != null) { 150 mUserDrawable.setCallback(null); 151 mUserDrawable = null; 152 } 153 mUserIcon = icon; 154 if (mUserIcon == null) { 155 mIconPaint.setShader(null); 156 mBitmap = null; 157 } else { 158 mIconPaint.setShader(new BitmapShader(icon, Shader.TileMode.CLAMP, 159 Shader.TileMode.CLAMP)); 160 } 161 onBoundsChange(getBounds()); 162 return this; 163 } 164 setIconDrawable(Drawable icon)165 public UserIconDrawable setIconDrawable(Drawable icon) { 166 if (mUserDrawable != null) { 167 mUserDrawable.setCallback(null); 168 } 169 mUserIcon = null; 170 mUserDrawable = icon; 171 if (mUserDrawable == null) { 172 mBitmap = null; 173 } else { 174 mUserDrawable.setCallback(this); 175 } 176 onBoundsChange(getBounds()); 177 return this; 178 } 179 isEmpty()180 public boolean isEmpty() { 181 return getUserIcon() == null && getUserDrawable() == null; 182 } 183 setBadge(Drawable badge)184 public UserIconDrawable setBadge(Drawable badge) { 185 mBadge = badge; 186 if (mBadge != null) { 187 if (mClearPaint == null) { 188 mClearPaint = new Paint(); 189 mClearPaint.setAntiAlias(true); 190 mClearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); 191 mClearPaint.setStyle(Paint.Style.FILL); 192 } 193 // update metrics 194 onBoundsChange(getBounds()); 195 } else { 196 invalidateSelf(); 197 } 198 return this; 199 } 200 setBadgeIfManagedUser(Context context, int userId)201 public UserIconDrawable setBadgeIfManagedUser(Context context, int userId) { 202 Drawable badge = null; 203 if (userId != UserHandle.USER_NULL) { 204 DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class); 205 boolean isCorp = 206 dpm.getProfileOwnerAsUser(userId) != null // has an owner 207 && dpm.getProfileOwnerOrDeviceOwnerSupervisionComponent(UserHandle.of(userId)) 208 == null; // and has no supervisor 209 if (isCorp) { 210 badge = getManagementBadge(context); 211 } 212 } 213 return setBadge(badge); 214 } 215 216 /** 217 * Sets the managed badge to this user icon if the device has a device owner. 218 */ setBadgeIfManagedDevice(Context context)219 public UserIconDrawable setBadgeIfManagedDevice(Context context) { 220 DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class); 221 Drawable badge = null; 222 boolean deviceOwnerExists = dpm.getDeviceOwnerComponentOnAnyUser() != null; 223 if (deviceOwnerExists) { 224 badge = getManagementBadge(context); 225 } 226 return setBadge(badge); 227 } 228 getManagementBadge(Context context)229 private static Drawable getManagementBadge(Context context) { 230 if (BuildCompatUtils.isAtLeastT()) { 231 return getUpdatableManagementBadge(context); 232 } else { 233 return getDrawableForDisplayDensity( 234 context, com.android.internal.R.drawable.ic_corp_user_badge); 235 } 236 } 237 238 @RequiresApi(Build.VERSION_CODES.TIRAMISU) getUpdatableManagementBadge(Context context)239 private static Drawable getUpdatableManagementBadge(Context context) { 240 DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class); 241 return dpm.getResources().getDrawableForDensity( 242 WORK_PROFILE_ICON, 243 SOLID_COLORED, 244 context.getResources().getDisplayMetrics().densityDpi, 245 /* default= */ () -> getDrawableForDisplayDensity( 246 context, com.android.internal.R.drawable.ic_corp_badge_case)); 247 } 248 setBadgeRadius(float radius)249 public void setBadgeRadius(float radius) { 250 mBadgeRadius = radius; 251 onBoundsChange(getBounds()); 252 } 253 setBadgeMargin(float margin)254 public void setBadgeMargin(float margin) { 255 mBadgeMargin = margin; 256 onBoundsChange(getBounds()); 257 } 258 259 /** 260 * Sets global padding of icon/frame. Doesn't effect the badge. 261 * @param padding 262 */ setPadding(float padding)263 public void setPadding(float padding) { 264 mPadding = padding; 265 onBoundsChange(getBounds()); 266 } 267 initFramePaint()268 private void initFramePaint() { 269 if (mFramePaint == null) { 270 mFramePaint = new Paint(); 271 mFramePaint.setStyle(Paint.Style.STROKE); 272 mFramePaint.setAntiAlias(true); 273 } 274 } 275 setFrameWidth(float width)276 public void setFrameWidth(float width) { 277 initFramePaint(); 278 mFrameWidth = width; 279 mFramePaint.setStrokeWidth(width); 280 onBoundsChange(getBounds()); 281 } 282 setFramePadding(float padding)283 public void setFramePadding(float padding) { 284 initFramePaint(); 285 mFramePadding = padding; 286 onBoundsChange(getBounds()); 287 } 288 setFrameColor(int color)289 public void setFrameColor(int color) { 290 initFramePaint(); 291 mFramePaint.setColor(color); 292 invalidateSelf(); 293 } 294 setFrameColor(ColorStateList colorList)295 public void setFrameColor(ColorStateList colorList) { 296 initFramePaint(); 297 mFrameColor = colorList; 298 invalidateSelf(); 299 } 300 301 /** 302 * This sets the "intrinsic" size of this drawable. Useful for views which use the drawable's 303 * intrinsic size for layout. It is independent of the bounds. 304 * @param size if 0, the intrinsic size will be set to the displayed icon's size 305 */ setIntrinsicSize(int size)306 public void setIntrinsicSize(int size) { 307 mSize = size; 308 } 309 310 @Override draw(Canvas canvas)311 public void draw(Canvas canvas) { 312 if (mInvalidated) { 313 rebake(); 314 } 315 if (mBitmap != null) { 316 if (mTintColor == null) { 317 mPaint.setColorFilter(null); 318 } else { 319 int color = mTintColor.getColorForState(getState(), mTintColor.getDefaultColor()); 320 if (shouldUpdateColorFilter(color, mTintMode)) { 321 mPaint.setColorFilter(new PorterDuffColorFilter(color, mTintMode)); 322 } 323 } 324 325 canvas.drawBitmap(mBitmap, 0, 0, mPaint); 326 } 327 } 328 shouldUpdateColorFilter(@olorInt int color, PorterDuff.Mode mode)329 private boolean shouldUpdateColorFilter(@ColorInt int color, PorterDuff.Mode mode) { 330 ColorFilter colorFilter = mPaint.getColorFilter(); 331 if (colorFilter instanceof PorterDuffColorFilter) { 332 PorterDuffColorFilter porterDuffColorFilter = (PorterDuffColorFilter) colorFilter; 333 int currentColor = porterDuffColorFilter.getColor(); 334 PorterDuff.Mode currentMode = porterDuffColorFilter.getMode(); 335 return currentColor != color || currentMode != mode; 336 } else { 337 return true; 338 } 339 } 340 341 @Override setAlpha(int alpha)342 public void setAlpha(int alpha) { 343 mPaint.setAlpha(alpha); 344 super.invalidateSelf(); 345 } 346 347 @Override setColorFilter(ColorFilter colorFilter)348 public void setColorFilter(ColorFilter colorFilter) { 349 } 350 351 @Override setTintList(ColorStateList tintList)352 public void setTintList(ColorStateList tintList) { 353 mTintColor = tintList; 354 super.invalidateSelf(); 355 } 356 357 @Override setTintMode(@onNull PorterDuff.Mode mode)358 public void setTintMode(@NonNull PorterDuff.Mode mode) { 359 mTintMode = mode; 360 super.invalidateSelf(); 361 } 362 363 @Override getConstantState()364 public ConstantState getConstantState() { 365 return new BitmapDrawable(mBitmap).getConstantState(); 366 } 367 368 /** 369 * This 'bakes' the current state of this icon into a bitmap and removes/recycles the source 370 * bitmap/drawable. Use this when no more changes will be made and an intrinsic size is set. 371 * This effectively turns this into a static drawable. 372 */ bake()373 public UserIconDrawable bake() { 374 if (mSize <= 0) { 375 throw new IllegalStateException("Baking requires an explicit intrinsic size"); 376 } 377 onBoundsChange(new Rect(0, 0, mSize, mSize)); 378 rebake(); 379 mFrameColor = null; 380 mFramePaint = null; 381 mClearPaint = null; 382 if (mUserDrawable != null) { 383 mUserDrawable.setCallback(null); 384 mUserDrawable = null; 385 } else if (mUserIcon != null) { 386 mUserIcon.recycle(); 387 mUserIcon = null; 388 } 389 return this; 390 } 391 rebake()392 private void rebake() { 393 mInvalidated = false; 394 395 if (mBitmap == null || (mUserDrawable == null && mUserIcon == null)) { 396 return; 397 } 398 399 final Canvas canvas = new Canvas(mBitmap); 400 canvas.drawColor(0, PorterDuff.Mode.CLEAR); 401 402 if(mUserDrawable != null) { 403 mUserDrawable.draw(canvas); 404 } else if (mUserIcon != null) { 405 int saveId = canvas.save(); 406 canvas.concat(mIconMatrix); 407 canvas.drawCircle(mUserIcon.getWidth() * 0.5f, mUserIcon.getHeight() * 0.5f, 408 mIntrinsicRadius, mIconPaint); 409 canvas.restoreToCount(saveId); 410 } 411 if (mFrameColor != null) { 412 mFramePaint.setColor(mFrameColor.getColorForState(getState(), Color.TRANSPARENT)); 413 } 414 if ((mFrameWidth + mFramePadding) > 0.001f) { 415 float radius = mDisplayRadius - mPadding - mFrameWidth * 0.5f; 416 canvas.drawCircle(getBounds().exactCenterX(), getBounds().exactCenterY(), 417 radius, mFramePaint); 418 } 419 420 if ((mBadge != null) && (mBadgeRadius > 0.001f)) { 421 final float badgeDiameter = mBadgeRadius * 2f; 422 final float badgeTop = mBitmap.getHeight() - badgeDiameter; 423 float badgeLeft = mBitmap.getWidth() - badgeDiameter; 424 425 mBadge.setBounds((int) badgeLeft, (int) badgeTop, 426 (int) (badgeLeft + badgeDiameter), (int) (badgeTop + badgeDiameter)); 427 428 final float borderRadius = mBadge.getBounds().width() * 0.5f + mBadgeMargin; 429 canvas.drawCircle(badgeLeft + mBadgeRadius, badgeTop + mBadgeRadius, 430 borderRadius, mClearPaint); 431 mBadge.draw(canvas); 432 } 433 } 434 435 @Override onBoundsChange(Rect bounds)436 protected void onBoundsChange(Rect bounds) { 437 if (bounds.isEmpty() || (mUserIcon == null && mUserDrawable == null)) { 438 return; 439 } 440 441 // re-create bitmap if applicable 442 float newDisplayRadius = Math.min(bounds.width(), bounds.height()) * 0.5f; 443 int size = (int) (newDisplayRadius * 2); 444 if (mBitmap == null || size != ((int) (mDisplayRadius * 2))) { 445 mDisplayRadius = newDisplayRadius; 446 if (mBitmap != null) { 447 mBitmap.recycle(); 448 } 449 mBitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); 450 } 451 452 // update metrics 453 mDisplayRadius = Math.min(bounds.width(), bounds.height()) * 0.5f; 454 final float iconRadius = mDisplayRadius - mFrameWidth - mFramePadding - mPadding; 455 RectF dstRect = new RectF(bounds.exactCenterX() - iconRadius, 456 bounds.exactCenterY() - iconRadius, 457 bounds.exactCenterX() + iconRadius, 458 bounds.exactCenterY() + iconRadius); 459 if (mUserDrawable != null) { 460 Rect rounded = new Rect(); 461 dstRect.round(rounded); 462 mIntrinsicRadius = Math.min(mUserDrawable.getIntrinsicWidth(), 463 mUserDrawable.getIntrinsicHeight()) * 0.5f; 464 mUserDrawable.setBounds(rounded); 465 } else if (mUserIcon != null) { 466 // Build square-to-square transformation matrix 467 final float iconCX = mUserIcon.getWidth() * 0.5f; 468 final float iconCY = mUserIcon.getHeight() * 0.5f; 469 mIntrinsicRadius = Math.min(iconCX, iconCY); 470 RectF srcRect = new RectF(iconCX - mIntrinsicRadius, iconCY - mIntrinsicRadius, 471 iconCX + mIntrinsicRadius, iconCY + mIntrinsicRadius); 472 mIconMatrix.setRectToRect(srcRect, dstRect, Matrix.ScaleToFit.FILL); 473 } 474 475 invalidateSelf(); 476 } 477 478 @Override invalidateSelf()479 public void invalidateSelf() { 480 super.invalidateSelf(); 481 mInvalidated = true; 482 } 483 484 @Override isStateful()485 public boolean isStateful() { 486 return mFrameColor != null && mFrameColor.isStateful(); 487 } 488 489 @Override getOpacity()490 public int getOpacity() { 491 return PixelFormat.TRANSLUCENT; 492 } 493 494 @Override getIntrinsicWidth()495 public int getIntrinsicWidth() { 496 return (mSize <= 0 ? (int) mIntrinsicRadius * 2 : mSize); 497 } 498 499 @Override getIntrinsicHeight()500 public int getIntrinsicHeight() { 501 return getIntrinsicWidth(); 502 } 503 504 @Override invalidateDrawable(@onNull Drawable who)505 public void invalidateDrawable(@NonNull Drawable who) { 506 invalidateSelf(); 507 } 508 509 @Override scheduleDrawable(@onNull Drawable who, @NonNull Runnable what, long when)510 public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) { 511 scheduleSelf(what, when); 512 } 513 514 @Override unscheduleDrawable(@onNull Drawable who, @NonNull Runnable what)515 public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) { 516 unscheduleSelf(what); 517 } 518 519 @VisibleForTesting getUserDrawable()520 public Drawable getUserDrawable() { 521 return mUserDrawable; 522 } 523 524 @VisibleForTesting getUserIcon()525 public Bitmap getUserIcon() { 526 return mUserIcon; 527 } 528 529 @VisibleForTesting isInvalidated()530 public boolean isInvalidated() { 531 return mInvalidated; 532 } 533 534 @VisibleForTesting getBadge()535 public Drawable getBadge() { 536 return mBadge; 537 } 538 } 539