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