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