1/*
2 * Copyright (C) 2014 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 */
16package com.android.mail.bitmap;
17
18import android.content.res.Resources;
19import android.graphics.Bitmap;
20import android.graphics.BitmapShader;
21import android.graphics.Canvas;
22import android.graphics.Color;
23import android.graphics.ColorFilter;
24import android.graphics.Matrix;
25import android.graphics.Paint;
26import android.graphics.Rect;
27import android.graphics.Shader;
28import android.graphics.drawable.Drawable;
29
30import com.android.bitmap.BitmapCache;
31import com.android.bitmap.RequestKey;
32import com.android.bitmap.ReusableBitmap;
33
34import com.android.mail.R;
35import com.android.mail.bitmap.ContactResolver.ContactDrawableInterface;
36
37/**
38 * A drawable that encapsulates all the functionality needed to display a contact image,
39 * including request creation/cancelling and data unbinding/re-binding.
40 * <p>
41 * The actual contact resolving and decoding is handled by {@link ContactResolver}.
42 * <p>
43 * For better performance, you should define a cache with {@link #setBitmapCache(BitmapCache)}.
44 */
45public abstract class AbstractAvatarDrawable extends Drawable implements ContactDrawableInterface {
46    protected final Resources mResources;
47
48    private BitmapCache mCache;
49    private ContactResolver mContactResolver;
50
51    protected ContactRequest mContactRequest;
52    protected ReusableBitmap mBitmap;
53
54    protected final float mBorderWidth;
55    protected final Paint mBitmapPaint;
56    protected final Paint mBorderPaint;
57    protected final Matrix mMatrix;
58
59    private int mDecodedWidth;
60    private int mDecodedHeight;
61
62    public AbstractAvatarDrawable(final Resources res) {
63        mResources = res;
64
65        mBitmapPaint = new Paint();
66        mBitmapPaint.setAntiAlias(true);
67        mBitmapPaint.setFilterBitmap(true);
68        mBitmapPaint.setDither(true);
69
70        mBorderWidth = res.getDimensionPixelSize(R.dimen.avatar_border_width);
71
72        mBorderPaint = new Paint();
73        mBorderPaint.setColor(Color.TRANSPARENT);
74        mBorderPaint.setStyle(Paint.Style.STROKE);
75        mBorderPaint.setStrokeWidth(mBorderWidth);
76        mBorderPaint.setAntiAlias(true);
77
78        mMatrix = new Matrix();
79    }
80
81    public void setBitmapCache(final BitmapCache cache) {
82        mCache = cache;
83    }
84
85    public void setContactResolver(final ContactResolver contactResolver) {
86        mContactResolver = contactResolver;
87    }
88
89    @Override
90    public void draw(final Canvas canvas) {
91        final Rect bounds = getBounds();
92        if (!isVisible() || bounds.isEmpty()) {
93            return;
94        }
95
96        if (mBitmap != null && mBitmap.bmp != null) {
97            // Draw sender image.
98            drawBitmap(mBitmap.bmp, mBitmap.getLogicalWidth(), mBitmap.getLogicalHeight(), canvas);
99        } else {
100            // Draw the default image
101            drawDefaultAvatar(canvas);
102        }
103    }
104
105    protected abstract void drawDefaultAvatar(Canvas canvas);
106
107    /**
108     * Draw the bitmap onto the canvas at the current bounds taking into account the current scale.
109     */
110    protected void drawBitmap(final Bitmap bitmap, final int width, final int height,
111            final Canvas canvas) {
112        final Rect bounds = getBounds();
113        // Draw bitmap through shader first.
114        final BitmapShader shader = new BitmapShader(bitmap, Shader.TileMode.CLAMP,
115                Shader.TileMode.CLAMP);
116        mMatrix.reset();
117
118        // Fit bitmap to bounds.
119        final float boundsWidth = (float) bounds.width();
120        final float boundsHeight = (float) bounds.height();
121        final float scale = Math.max(boundsWidth / width, boundsHeight / height);
122        mMatrix.postScale(scale, scale);
123
124        // Translate bitmap to dst bounds.
125        mMatrix.postTranslate(bounds.left, bounds.top);
126
127        shader.setLocalMatrix(mMatrix);
128        mBitmapPaint.setShader(shader);
129        drawCircle(canvas, bounds, mBitmapPaint);
130
131        // Then draw the border.
132        final float radius = bounds.width() / 2f - mBorderWidth / 2;
133        canvas.drawCircle(bounds.centerX(), bounds.centerY(), radius, mBorderPaint);
134    }
135
136    /**
137     * Draws the largest circle that fits within the given <code>bounds</code>.
138     *
139     * @param canvas the canvas on which to draw
140     * @param bounds the bounding box of the circle
141     * @param paint the paint with which to draw
142     */
143    protected static void drawCircle(Canvas canvas, Rect bounds, Paint paint) {
144        canvas.drawCircle(bounds.centerX(), bounds.centerY(), bounds.width() / 2, paint);
145    }
146
147    @Override
148    public int getDecodeWidth() {
149        return mDecodedWidth;
150    }
151
152    @Override
153    public int getDecodeHeight() {
154        return mDecodedHeight;
155    }
156
157    public void setDecodeDimensions(final int decodeWidth, final int decodeHeight) {
158        mDecodedWidth = decodeWidth;
159        mDecodedHeight = decodeHeight;
160    }
161
162    public void unbind() {
163        setImage(null);
164    }
165
166    public void bind(final String name, final String email) {
167        setImage(new ContactRequest(name, email));
168    }
169
170    private void setImage(final ContactRequest contactRequest) {
171        if (mContactRequest != null && mContactRequest.equals(contactRequest)) {
172            return;
173        }
174
175        if (mBitmap != null) {
176            mBitmap.releaseReference();
177            mBitmap = null;
178        }
179
180        if (mContactResolver != null) {
181            mContactResolver.remove(mContactRequest, this);
182        }
183
184        mContactRequest = contactRequest;
185
186        if (contactRequest == null) {
187            invalidateSelf();
188            return;
189        }
190
191        ReusableBitmap cached = null;
192        if (mCache != null) {
193            cached = mCache.get(contactRequest, true /* incrementRefCount */);
194        }
195
196        if (cached != null) {
197            setBitmap(cached);
198        } else {
199            decode();
200        }
201    }
202
203    private void decode() {
204        if (mContactRequest == null) {
205            return;
206        }
207        // Add to batch.
208        mContactResolver.add(mContactRequest, this);
209    }
210
211    private void setBitmap(final ReusableBitmap bmp) {
212        if (mBitmap != null && mBitmap != bmp) {
213            mBitmap.releaseReference();
214        }
215        mBitmap = bmp;
216        invalidateSelf();
217    }
218
219    @Override
220    public void onDecodeComplete(RequestKey key, ReusableBitmap result) {
221        final ContactRequest request = (ContactRequest) key;
222        // Remove from batch.
223        mContactResolver.remove(request, this);
224        if (request.equals(mContactRequest)) {
225            setBitmap(result);
226        } else {
227            // if the requests don't match (i.e. this request is stale), decrement the
228            // ref count to allow the bitmap to be pooled
229            if (result != null) {
230                result.releaseReference();
231            }
232        }
233    }
234
235    @Override
236    public void setAlpha(int alpha) {
237        mBitmapPaint.setAlpha(alpha);
238    }
239
240    @Override
241    public void setColorFilter(ColorFilter cf) {
242        mBitmapPaint.setColorFilter(cf);
243    }
244
245    @Override
246    public int getOpacity() {
247        return 0;
248    }
249}
250