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