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