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