ContactDrawable.java revision a71d756a0e80f63b8b4ebd91451c3a79929e1a4e
1/*
2 * Copyright (C) 2013 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.content.res.TypedArray;
20import android.graphics.Bitmap;
21import android.graphics.BitmapFactory;
22import android.graphics.Canvas;
23import android.graphics.ColorFilter;
24import android.graphics.Paint;
25import android.graphics.Paint.Align;
26import android.graphics.Rect;
27import android.graphics.Typeface;
28import android.graphics.drawable.Drawable;
29
30import com.android.oldbitmap.BitmapCache;
31import com.android.oldbitmap.DecodeTask.Request;
32import com.android.oldbitmap.ReusableBitmap;
33import com.android.mail.R;
34import com.android.mail.bitmap.ContactResolver.ContactDrawableInterface;
35
36/**
37 * A drawable that encapsulates all the functionality needed to display a contact image,
38 * including request creation/cancelling and data unbinding/re-binding. While no contact images
39 * can be shown, a default letter tile will be shown instead.
40 *
41 * <p/>
42 * The actual contact resolving and decoding is handled by {@link ContactResolver}.
43 */
44public class ContactDrawable extends Drawable implements ContactDrawableInterface {
45
46    private final BitmapCache mCache;
47    private final ContactResolver mContactResolver;
48
49    private ContactRequest mContactRequest;
50    private ReusableBitmap mBitmap;
51    private final Paint mPaint;
52    private int mScale;
53
54    /** Letter tile */
55    private static TypedArray sColors;
56    private static int sDefaultColor;
57    private static int sTileLetterFontSize;
58    private static int sTileLetterFontSizeSmall;
59    private static int sTileFontColor;
60    private static Bitmap DEFAULT_AVATAR;
61    /** Reusable components to avoid new allocations */
62    private static final Paint sPaint = new Paint();
63    private static final Rect sRect = new Rect();
64    private static final char[] sFirstChar = new char[1];
65
66    /** This should match the total number of colors defined in colors.xml for letter_tile_color */
67    private static final int NUM_OF_TILE_COLORS = 8;
68
69    public ContactDrawable(final Resources res, final BitmapCache cache,
70            final ContactResolver contactResolver) {
71        mCache = cache;
72        mContactResolver = contactResolver;
73        mPaint = new Paint();
74        mPaint.setFilterBitmap(true);
75        mPaint.setDither(true);
76
77        if (sColors == null) {
78            sColors = res.obtainTypedArray(R.array.letter_tile_colors);
79            sDefaultColor = res.getColor(R.color.letter_tile_default_color);
80            sTileLetterFontSize = res.getDimensionPixelSize(R.dimen.tile_letter_font_size);
81            sTileLetterFontSizeSmall = res
82                    .getDimensionPixelSize(R.dimen.tile_letter_font_size_small);
83            sTileFontColor = res.getColor(R.color.letter_tile_font_color);
84            DEFAULT_AVATAR = BitmapFactory.decodeResource(res, R.drawable.ic_generic_man);
85
86            sPaint.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL));
87            sPaint.setTextAlign(Align.CENTER);
88            sPaint.setAntiAlias(true);
89        }
90    }
91
92    @Override
93    public void draw(final Canvas canvas) {
94        final Rect bounds = getBounds();
95        if (!isVisible() || bounds.isEmpty()) {
96            return;
97        }
98
99        if (mBitmap != null && mBitmap.bmp != null) {
100            // Draw sender image.
101            drawBitmap(mBitmap.bmp, mBitmap.getLogicalWidth(), mBitmap.getLogicalHeight(), canvas);
102        } else {
103            // Draw letter tile.
104            drawLetterTile(canvas);
105        }
106    }
107
108    /**
109     * Draw the bitmap onto the canvas at the current bounds taking into account the current scale.
110     */
111    private void drawBitmap(final Bitmap bitmap, final int width, final int height,
112            final Canvas canvas) {
113        final Rect bounds = getBounds();
114
115        if (mScale != ContactGridDrawable.SCALE_TYPE_HALF) {
116            sRect.set(0, 0, width, height);
117        } else {
118            // For skinny bounds, draw the middle two quarters.
119            sRect.set(width / 4, 0, width / 4 * 3, height);
120        }
121        canvas.drawBitmap(bitmap, sRect, bounds, mPaint);
122    }
123
124    private void drawLetterTile(final Canvas canvas) {
125        if (mContactRequest == null) {
126            return;
127        }
128
129        // Draw background color.
130        final String email = mContactRequest.getEmail();
131        sPaint.setColor(pickColor(email));
132        sPaint.setAlpha(mPaint.getAlpha());
133        canvas.drawRect(getBounds(), sPaint);
134
135        // Draw letter/digit or generic avatar.
136        final String displayName = mContactRequest.getDisplayName();
137        final char firstChar = displayName.charAt(0);
138        final Rect bounds = getBounds();
139        if (isEnglishLetterOrDigit(firstChar)) {
140            // Draw letter or digit.
141            sFirstChar[0] = Character.toUpperCase(firstChar);
142            sPaint.setTextSize(mScale == ContactGridDrawable.SCALE_TYPE_ONE ? sTileLetterFontSize
143                    : sTileLetterFontSizeSmall);
144            sPaint.getTextBounds(sFirstChar, 0, 1, sRect);
145            sPaint.setColor(sTileFontColor);
146            canvas.drawText(sFirstChar, 0, 1, bounds.centerX(),
147                    bounds.centerY() + sRect.height() / 2, sPaint);
148        } else {
149            drawBitmap(DEFAULT_AVATAR, DEFAULT_AVATAR.getWidth(), DEFAULT_AVATAR.getHeight(),
150                    canvas);
151        }
152    }
153
154    private static int pickColor(final String email) {
155        // String.hashCode() implementation is not supposed to change across java versions, so
156        // this should guarantee the same email address always maps to the same color.
157        // The email should already have been normalized by the ContactRequest.
158        final int color = Math.abs(email.hashCode()) % NUM_OF_TILE_COLORS;
159        return sColors.getColor(color, sDefaultColor);
160    }
161
162    private static boolean isEnglishLetterOrDigit(final char c) {
163        return ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z') || ('0' <= c && c <= '9');
164    }
165
166    @Override
167    public void setAlpha(final int alpha) {
168        mPaint.setAlpha(alpha);
169    }
170
171    @Override
172    public void setColorFilter(final ColorFilter cf) {
173        mPaint.setColorFilter(cf);
174    }
175
176    @Override
177    public int getOpacity() {
178        return 0;
179    }
180
181    public void setDecodeDimensions(final int decodeWidth, final int decodeHeight) {
182        mCache.setPoolDimensions(decodeWidth, decodeHeight);
183    }
184
185    public void setScale(final int scale) {
186        mScale = scale;
187    }
188
189    public void unbind() {
190        setImage(null);
191    }
192
193    public void bind(final String name, final String email) {
194        setImage(new ContactRequest(name, email));
195    }
196
197    private void setImage(final ContactRequest contactRequest) {
198        if (mContactRequest != null && mContactRequest.equals(contactRequest)) {
199            return;
200        }
201
202        if (mBitmap != null) {
203            mBitmap.releaseReference();
204            mBitmap = null;
205        }
206
207        mContactResolver.remove(mContactRequest, this);
208        mContactRequest = contactRequest;
209
210        if (contactRequest == null) {
211            invalidateSelf();
212            return;
213        }
214
215        final ReusableBitmap cached = mCache.get(contactRequest, true /* incrementRefCount */);
216        if (cached != null) {
217            setBitmap(cached);
218        } else {
219            decode();
220        }
221    }
222
223    private void setBitmap(final ReusableBitmap bmp) {
224        if (mBitmap != null && mBitmap != bmp) {
225            mBitmap.releaseReference();
226        }
227        mBitmap = bmp;
228        invalidateSelf();
229    }
230
231    private void decode() {
232        if (mContactRequest == null) {
233            return;
234        }
235        // Add to batch.
236        mContactResolver.add(mContactRequest, this);
237    }
238
239    public void onDecodeComplete(final Request key, final ReusableBitmap result) {
240        final ContactRequest request = (ContactRequest) key;
241        // Remove from batch.
242        mContactResolver.remove(request, this);
243        if (request.equals(mContactRequest)) {
244            setBitmap(result);
245        } else {
246            // if the requests don't match (i.e. this request is stale), decrement the
247            // ref count to allow the bitmap to be pooled
248            if (result != null) {
249                result.releaseReference();
250            }
251        }
252    }
253}
254