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