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 */
16
17package com.android.contacts.common.lettertiles;
18
19import android.content.res.Resources;
20import android.content.res.TypedArray;
21import android.graphics.Bitmap;
22import android.graphics.BitmapFactory;
23import android.graphics.Canvas;
24import android.graphics.ColorFilter;
25import android.graphics.Paint;
26import android.graphics.Paint.Align;
27import android.graphics.Rect;
28import android.graphics.Typeface;
29import android.graphics.drawable.Drawable;
30import android.text.TextUtils;
31import android.util.Log;
32
33import com.android.contacts.common.R;
34import com.android.contacts.common.util.BitmapUtil;
35
36import junit.framework.Assert;
37
38/**
39 * A drawable that encapsulates all the functionality needed to display a letter tile to
40 * represent a contact image.
41 */
42public class LetterTileDrawable extends Drawable {
43
44    private final String TAG = LetterTileDrawable.class.getSimpleName();
45
46    private final Paint mPaint;
47
48    /** Letter tile */
49    private static TypedArray sColors;
50    private static int sDefaultColor;
51    private static int sTileFontColor;
52    private static float sLetterToTileRatio;
53    private static Bitmap DEFAULT_PERSON_AVATAR;
54    private static Bitmap DEFAULT_BUSINESS_AVATAR;
55    private static Bitmap DEFAULT_VOICEMAIL_AVATAR;
56
57    /** Reusable components to avoid new allocations */
58    private static final Paint sPaint = new Paint();
59    private static final Rect sRect = new Rect();
60    private static final char[] sFirstChar = new char[1];
61
62    /** Contact type constants */
63    public static final int TYPE_PERSON = 1;
64    public static final int TYPE_BUSINESS = 2;
65    public static final int TYPE_VOICEMAIL = 3;
66    public static final int TYPE_DEFAULT = TYPE_PERSON;
67
68    private String mDisplayName;
69    private String mIdentifier;
70    private int mContactType = TYPE_DEFAULT;
71    private float mScale = 1.0f;
72    private float mOffset = 0.0f;
73
74    /** This should match the total number of colors defined in colors.xml for letter_tile_color */
75    private static final int NUM_OF_TILE_COLORS = 8;
76
77    public LetterTileDrawable(final Resources res) {
78        mPaint = new Paint();
79        mPaint.setFilterBitmap(true);
80        mPaint.setDither(true);
81
82        if (sColors == null) {
83            sColors = res.obtainTypedArray(R.array.letter_tile_colors);
84            sDefaultColor = res.getColor(R.color.letter_tile_default_color);
85            sTileFontColor = res.getColor(R.color.letter_tile_font_color);
86            sLetterToTileRatio = res.getFraction(R.dimen.letter_to_tile_ratio, 1, 1);
87            DEFAULT_PERSON_AVATAR = BitmapFactory.decodeResource(res,
88                    R.drawable.ic_list_item_avatar);
89            DEFAULT_BUSINESS_AVATAR = BitmapFactory.decodeResource(res,
90                    R.drawable.ic_list_item_businessavatar);
91            DEFAULT_VOICEMAIL_AVATAR = BitmapFactory.decodeResource(res,
92                    R.drawable.ic_voicemail_avatar);
93            sPaint.setTypeface(Typeface.create(
94                    res.getString(R.string.letter_tile_letter_font_family), Typeface.NORMAL));
95            sPaint.setTextAlign(Align.CENTER);
96            sPaint.setAntiAlias(true);
97        }
98    }
99
100    @Override
101    public void draw(final Canvas canvas) {
102        final Rect bounds = getBounds();
103        if (!isVisible() || bounds.isEmpty()) {
104            return;
105        }
106        // Draw letter tile.
107        drawLetterTile(canvas);
108    }
109
110    /**
111     * Draw the bitmap onto the canvas at the current bounds taking into account the current scale.
112     */
113    private void drawBitmap(final Bitmap bitmap, final int width, final int height,
114            final Canvas canvas) {
115        // The bitmap should be drawn in the middle of the canvas without changing its width to
116        // height ratio.
117        final Rect destRect = copyBounds();
118
119        // Crop the destination bounds into a square, scaled and offset as appropriate
120        final int halfLength = (int) (mScale * Math.min(destRect.width(), destRect.height()) / 2);
121
122        destRect.set(destRect.centerX() - halfLength,
123                (int) (destRect.centerY() - halfLength + mOffset * destRect.height()),
124                destRect.centerX() + halfLength,
125                (int) (destRect.centerY() + halfLength + mOffset * destRect.height()));
126
127        // Source rectangle remains the entire bounds of the source bitmap.
128        sRect.set(0, 0, width, height);
129
130        canvas.drawBitmap(bitmap, sRect, destRect, mPaint);
131    }
132
133    private void drawLetterTile(final Canvas canvas) {
134        // Draw background color.
135        sPaint.setColor(pickColor(mIdentifier));
136
137        sPaint.setAlpha(mPaint.getAlpha());
138        canvas.drawRect(getBounds(), sPaint);
139
140        // Draw letter/digit only if the first character is an english letter
141        if (mDisplayName != null && isEnglishLetter(mDisplayName.charAt(0))) {
142            // Draw letter or digit.
143            sFirstChar[0] = Character.toUpperCase(mDisplayName.charAt(0));
144
145            // Scale text by canvas bounds and user selected scaling factor
146            final int minDimension = Math.min(getBounds().width(), getBounds().height());
147            sPaint.setTextSize(mScale * sLetterToTileRatio * minDimension);
148            //sPaint.setTextSize(sTileLetterFontSize);
149            sPaint.getTextBounds(sFirstChar, 0, 1, sRect);
150            sPaint.setColor(sTileFontColor);
151            final Rect bounds = getBounds();
152
153            // Draw the letter in the canvas, vertically shifted up or down by the user-defined
154            // offset
155            canvas.drawText(sFirstChar, 0, 1, bounds.centerX(),
156                    bounds.centerY() + mOffset * bounds.height() + sRect.height() / 2,
157                    sPaint);
158        } else {
159            // Draw the default image if there is no letter/digit to be drawn
160            final Bitmap bitmap = getBitmapForContactType(mContactType);
161            drawBitmap(bitmap, bitmap.getWidth(), bitmap.getHeight(),
162                        canvas);
163        }
164    }
165
166    /**
167     * Returns a deterministic color based on the provided contact identifier string.
168     */
169    private int pickColor(final String identifier) {
170        if (TextUtils.isEmpty(identifier) || mContactType == TYPE_VOICEMAIL) {
171            return sDefaultColor;
172        }
173        // String.hashCode() implementation is not supposed to change across java versions, so
174        // this should guarantee the same email address always maps to the same color.
175        // The email should already have been normalized by the ContactRequest.
176        final int color = Math.abs(identifier.hashCode()) % NUM_OF_TILE_COLORS;
177        return sColors.getColor(color, sDefaultColor);
178    }
179
180    private static Bitmap getBitmapForContactType(int contactType) {
181        switch (contactType) {
182            case TYPE_PERSON:
183                return DEFAULT_PERSON_AVATAR;
184            case TYPE_BUSINESS:
185                return DEFAULT_BUSINESS_AVATAR;
186            case TYPE_VOICEMAIL:
187                return DEFAULT_VOICEMAIL_AVATAR;
188            default:
189                return DEFAULT_PERSON_AVATAR;
190        }
191    }
192
193    private static boolean isEnglishLetter(final char c) {
194        return ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z');
195    }
196
197    @Override
198    public void setAlpha(final int alpha) {
199        mPaint.setAlpha(alpha);
200    }
201
202    @Override
203    public void setColorFilter(final ColorFilter cf) {
204        mPaint.setColorFilter(cf);
205    }
206
207    @Override
208    public int getOpacity() {
209        return android.graphics.PixelFormat.OPAQUE;
210    }
211
212    /**
213     * Scale the drawn letter tile to a ratio of its default size
214     *
215     * @param scale The ratio the letter tile should be scaled to as a percentage of its default
216     * size, from a scale of 0 to 2.0f. The default is 1.0f.
217     */
218    public void setScale(float scale) {
219        mScale = scale;
220    }
221
222    /**
223     * Assigns the vertical offset of the position of the letter tile to the ContactDrawable
224     *
225     * @param offset The provided offset must be within the range of -0.5f to 0.5f.
226     * If set to -0.5f, the letter will be shifted upwards by 0.5 times the height of the canvas
227     * it is being drawn on, which means it will be drawn with the center of the letter starting
228     * at the top edge of the canvas.
229     * If set to 0.5f, the letter will be shifted downwards by 0.5 times the height of the canvas
230     * it is being drawn on, which means it will be drawn with the center of the letter starting
231     * at the bottom edge of the canvas.
232     * The default is 0.0f.
233     */
234    public void setOffset(float offset) {
235        Assert.assertTrue(offset >= -0.5f && offset <= 0.5f);
236        mOffset = offset;
237    }
238
239    public void setContactDetails(final String displayName, final String identifier) {
240        mDisplayName = displayName;
241        mIdentifier = identifier;
242    }
243
244    public void setContactType(int contactType) {
245        mContactType = contactType;
246    }
247}
248