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 
17 package com.android.contacts.common.lettertiles;
18 
19 import android.content.res.Resources;
20 import android.content.res.TypedArray;
21 import android.graphics.Bitmap;
22 import android.graphics.Bitmap.Config;
23 import android.graphics.Canvas;
24 import android.graphics.ColorFilter;
25 import android.graphics.Outline;
26 import android.graphics.Paint;
27 import android.graphics.Paint.Align;
28 import android.graphics.Rect;
29 import android.graphics.Typeface;
30 import android.graphics.drawable.Drawable;
31 import android.support.annotation.IntDef;
32 import android.support.annotation.Nullable;
33 import android.text.TextUtils;
34 import com.android.contacts.common.R;
35 import com.android.dialer.common.Assert;
36 import java.lang.annotation.Retention;
37 import java.lang.annotation.RetentionPolicy;
38 
39 /**
40  * A drawable that encapsulates all the functionality needed to display a letter tile to represent a
41  * contact image.
42  */
43 public class LetterTileDrawable extends Drawable {
44 
45   /**
46    * ContactType indicates the avatar type of the contact. For a person or for the default when no
47    * name is provided, it is {@link #TYPE_DEFAULT}, otherwise, for a business it is {@link
48    * #TYPE_BUSINESS}, and voicemail contacts should use {@link #TYPE_VOICEMAIL}.
49    */
50   @Retention(RetentionPolicy.SOURCE)
51   @IntDef({TYPE_PERSON, TYPE_BUSINESS, TYPE_VOICEMAIL, TYPE_GENERIC_AVATAR, TYPE_SPAM})
52   public @interface ContactType {}
53 
54   /** Contact type constants */
55   public static final int TYPE_PERSON = 1;
56   public static final int TYPE_BUSINESS = 2;
57   public static final int TYPE_VOICEMAIL = 3;
58   /**
59    * A generic avatar that features the default icon, default color, and no letter. Useful for
60    * situations where a contact is anonymous.
61    */
62   public static final int TYPE_GENERIC_AVATAR = 4;
63   public static final int TYPE_SPAM = 5;
64   public static final int TYPE_CONFERENCE = 6;
65   @ContactType public static final int TYPE_DEFAULT = TYPE_PERSON;
66 
67   /**
68    * Shape indicates the letter tile shape. It can be either a {@link #SHAPE_CIRCLE}, otherwise, it
69    * is a {@link #SHAPE_RECTANGLE}.
70    */
71   @Retention(RetentionPolicy.SOURCE)
72   @IntDef({SHAPE_CIRCLE, SHAPE_RECTANGLE})
73   public @interface Shape {}
74 
75   /** Shape constants */
76   public static final int SHAPE_CIRCLE = 1;
77 
78   public static final int SHAPE_RECTANGLE = 2;
79 
80   /** 54% opacity */
81   private static final int ALPHA = 138;
82   /** 100% opacity */
83   private static final int SPAM_ALPHA = 255;
84   /** Default icon scale for vector drawable. */
85   private static final float VECTOR_ICON_SCALE = 0.7f;
86 
87   /** Reusable components to avoid new allocations */
88   private static final Paint sPaint = new Paint();
89 
90   private static final Rect sRect = new Rect();
91   private static final char[] sFirstChar = new char[1];
92   /** Letter tile */
93   private static TypedArray sColors;
94 
95   private static int sSpamColor;
96   private static int sDefaultColor;
97   private static int sTileFontColor;
98   private static float sLetterToTileRatio;
99   private static Drawable sDefaultPersonAvatar;
100   private static Drawable sDefaultBusinessAvatar;
101   private static Drawable sDefaultVoicemailAvatar;
102   private static Drawable sDefaultSpamAvatar;
103   private static Drawable sDefaultConferenceAvatar;
104 
105   private final Paint mPaint;
106   @ContactType private int mContactType = TYPE_DEFAULT;
107   private float mScale = 1.0f;
108   private float mOffset = 0.0f;
109   private boolean mIsCircle = false;
110 
111   private int mColor;
112   private Character mLetter = null;
113 
114   private String mDisplayName;
115 
LetterTileDrawable(final Resources res)116   public LetterTileDrawable(final Resources res) {
117     if (sColors == null) {
118       sColors = res.obtainTypedArray(R.array.letter_tile_colors);
119       sSpamColor = res.getColor(R.color.spam_contact_background);
120       sDefaultColor = res.getColor(R.color.letter_tile_default_color);
121       sTileFontColor = res.getColor(R.color.letter_tile_font_color);
122       sLetterToTileRatio = res.getFraction(R.dimen.letter_to_tile_ratio, 1, 1);
123       sDefaultPersonAvatar =
124           res.getDrawable(R.drawable.product_logo_avatar_anonymous_white_color_120, null);
125       sDefaultBusinessAvatar = res.getDrawable(R.drawable.quantum_ic_business_vd_theme_24, null);
126       sDefaultVoicemailAvatar = res.getDrawable(R.drawable.quantum_ic_voicemail_vd_theme_24, null);
127       sDefaultSpamAvatar = res.getDrawable(R.drawable.quantum_ic_report_vd_theme_24, null);
128       sDefaultConferenceAvatar = res.getDrawable(R.drawable.quantum_ic_group_vd_theme_24, null);
129       sPaint.setTypeface(
130           Typeface.create(res.getString(R.string.letter_tile_letter_font_family), Typeface.NORMAL));
131       sPaint.setTextAlign(Align.CENTER);
132       sPaint.setAntiAlias(true);
133     }
134     mPaint = new Paint();
135     mPaint.setFilterBitmap(true);
136     mPaint.setDither(true);
137     mColor = sDefaultColor;
138   }
139 
getScaledBounds(float scale, float offset)140   private Rect getScaledBounds(float scale, float offset) {
141     // The drawable should be drawn in the middle of the canvas without changing its width to
142     // height ratio.
143     final Rect destRect = copyBounds();
144     // Crop the destination bounds into a square, scaled and offset as appropriate
145     final int halfLength = (int) (scale * Math.min(destRect.width(), destRect.height()) / 2);
146 
147     destRect.set(
148         destRect.centerX() - halfLength,
149         (int) (destRect.centerY() - halfLength + offset * destRect.height()),
150         destRect.centerX() + halfLength,
151         (int) (destRect.centerY() + halfLength + offset * destRect.height()));
152     return destRect;
153   }
154 
getDrawableForContactType(int contactType)155   private Drawable getDrawableForContactType(int contactType) {
156     switch (contactType) {
157       case TYPE_BUSINESS:
158         mScale = VECTOR_ICON_SCALE;
159         return sDefaultBusinessAvatar;
160       case TYPE_VOICEMAIL:
161         mScale = VECTOR_ICON_SCALE;
162         return sDefaultVoicemailAvatar;
163       case TYPE_SPAM:
164         mScale = VECTOR_ICON_SCALE;
165         return sDefaultSpamAvatar;
166       case TYPE_CONFERENCE:
167         mScale = VECTOR_ICON_SCALE;
168         return sDefaultConferenceAvatar;
169       case TYPE_PERSON:
170       case TYPE_GENERIC_AVATAR:
171       default:
172         return sDefaultPersonAvatar;
173     }
174   }
175 
isEnglishLetter(final char c)176   private static boolean isEnglishLetter(final char c) {
177     return ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z');
178   }
179 
180   @Override
draw(final Canvas canvas)181   public void draw(final Canvas canvas) {
182     final Rect bounds = getBounds();
183     if (!isVisible() || bounds.isEmpty()) {
184       return;
185     }
186     // Draw letter tile.
187     drawLetterTile(canvas);
188   }
189 
getBitmap(int width, int height)190   public Bitmap getBitmap(int width, int height) {
191     Bitmap bitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888);
192     this.setBounds(0, 0, width, height);
193     Canvas canvas = new Canvas(bitmap);
194     this.draw(canvas);
195     return bitmap;
196   }
197 
drawLetterTile(final Canvas canvas)198   private void drawLetterTile(final Canvas canvas) {
199     // Draw background color.
200     sPaint.setColor(mColor);
201     sPaint.setAlpha(mPaint.getAlpha());
202 
203     final Rect bounds = getBounds();
204     final int minDimension = Math.min(bounds.width(), bounds.height());
205 
206     if (mIsCircle) {
207       canvas.drawCircle(bounds.centerX(), bounds.centerY(), minDimension / 2, sPaint);
208     } else {
209       canvas.drawRect(bounds, sPaint);
210     }
211 
212     // Draw letter/digit only if the first character is an english letter or there's a override
213     if (mLetter != null) {
214       // Draw letter or digit.
215       sFirstChar[0] = mLetter;
216 
217       // Scale text by canvas bounds and user selected scaling factor
218       sPaint.setTextSize(mScale * sLetterToTileRatio * minDimension);
219       sPaint.getTextBounds(sFirstChar, 0, 1, sRect);
220       sPaint.setTypeface(Typeface.create("sans-serif", Typeface.NORMAL));
221       sPaint.setColor(sTileFontColor);
222       sPaint.setAlpha(ALPHA);
223 
224       // Draw the letter in the canvas, vertically shifted up or down by the user-defined
225       // offset
226       canvas.drawText(
227           sFirstChar,
228           0,
229           1,
230           bounds.centerX(),
231           bounds.centerY() + mOffset * bounds.height() - sRect.exactCenterY(),
232           sPaint);
233     } else {
234       // Draw the default image if there is no letter/digit to be drawn
235       Drawable drawable = getDrawableForContactType(mContactType);
236       drawable.setBounds(getScaledBounds(mScale, mOffset));
237       drawable.setAlpha(drawable == sDefaultSpamAvatar ? SPAM_ALPHA : ALPHA);
238       drawable.draw(canvas);
239     }
240   }
241 
getColor()242   public int getColor() {
243     return mColor;
244   }
245 
setColor(int color)246   public LetterTileDrawable setColor(int color) {
247     mColor = color;
248     return this;
249   }
250 
251   /** Returns a deterministic color based on the provided contact identifier string. */
pickColor(final String identifier)252   private int pickColor(final String identifier) {
253     if (mContactType == TYPE_SPAM) {
254       return sSpamColor;
255     }
256 
257     if (mContactType == TYPE_VOICEMAIL
258         || mContactType == TYPE_BUSINESS
259         || TextUtils.isEmpty(identifier)) {
260       return sDefaultColor;
261     }
262 
263     // String.hashCode() implementation is not supposed to change across java versions, so
264     // this should guarantee the same email address always maps to the same color.
265     // The email should already have been normalized by the ContactRequest.
266     final int color = Math.abs(identifier.hashCode()) % sColors.length();
267     return sColors.getColor(color, sDefaultColor);
268   }
269 
270   @Override
setAlpha(final int alpha)271   public void setAlpha(final int alpha) {
272     mPaint.setAlpha(alpha);
273   }
274 
275   @Override
setColorFilter(final ColorFilter cf)276   public void setColorFilter(final ColorFilter cf) {
277     mPaint.setColorFilter(cf);
278   }
279 
280   @Override
getOpacity()281   public int getOpacity() {
282     return android.graphics.PixelFormat.OPAQUE;
283   }
284 
285   @Override
getOutline(Outline outline)286   public void getOutline(Outline outline) {
287     if (mIsCircle) {
288       outline.setOval(getBounds());
289     } else {
290       outline.setRect(getBounds());
291     }
292 
293     outline.setAlpha(1);
294   }
295 
296   /**
297    * Scale the drawn letter tile to a ratio of its default size
298    *
299    * @param scale The ratio the letter tile should be scaled to as a percentage of its default size,
300    *     from a scale of 0 to 2.0f. The default is 1.0f.
301    */
setScale(float scale)302   public LetterTileDrawable setScale(float scale) {
303     mScale = scale;
304     return this;
305   }
306 
307   /**
308    * Assigns the vertical offset of the position of the letter tile to the ContactDrawable
309    *
310    * @param offset The provided offset must be within the range of -0.5f to 0.5f. If set to -0.5f,
311    *     the letter will be shifted upwards by 0.5 times the height of the canvas it is being drawn
312    *     on, which means it will be drawn with the center of the letter starting at the top edge of
313    *     the canvas. If set to 0.5f, the letter will be shifted downwards by 0.5 times the height of
314    *     the canvas it is being drawn on, which means it will be drawn with the center of the letter
315    *     starting at the bottom edge of the canvas. The default is 0.0f.
316    */
setOffset(float offset)317   public LetterTileDrawable setOffset(float offset) {
318     Assert.checkArgument(offset >= -0.5f && offset <= 0.5f);
319     mOffset = offset;
320     return this;
321   }
322 
setLetter(Character letter)323   public LetterTileDrawable setLetter(Character letter) {
324     mLetter = letter;
325     return this;
326   }
327 
getLetter()328   public Character getLetter() {
329     return this.mLetter;
330   }
331 
setLetterAndColorFromContactDetails( final String displayName, final String identifier)332   private LetterTileDrawable setLetterAndColorFromContactDetails(
333       final String displayName, final String identifier) {
334     if (!TextUtils.isEmpty(displayName) && isEnglishLetter(displayName.charAt(0))) {
335       mLetter = Character.toUpperCase(displayName.charAt(0));
336     } else {
337       mLetter = null;
338     }
339     mColor = pickColor(identifier);
340     return this;
341   }
342 
setContactType(@ontactType int contactType)343   public LetterTileDrawable setContactType(@ContactType int contactType) {
344     mContactType = contactType;
345     return this;
346   }
347 
348   @ContactType
getContactType()349   public int getContactType() {
350     return this.mContactType;
351   }
352 
setIsCircular(boolean isCircle)353   public LetterTileDrawable setIsCircular(boolean isCircle) {
354     mIsCircle = isCircle;
355     return this;
356   }
357 
tileIsCircular()358   public boolean tileIsCircular() {
359     return this.mIsCircle;
360   }
361 
362   /**
363    * Creates a canonical letter tile for use across dialer fragments.
364    *
365    * @param displayName The display name to produce the letter in the tile. Null values or numbers
366    *     yield no letter.
367    * @param identifierForTileColor The string used to produce the tile color.
368    * @param shape The shape of the tile.
369    * @param contactType The type of contact, e.g. TYPE_VOICEMAIL.
370    * @return this
371    */
setCanonicalDialerLetterTileDetails( @ullable final String displayName, @Nullable final String identifierForTileColor, @Shape final int shape, final int contactType)372   public LetterTileDrawable setCanonicalDialerLetterTileDetails(
373       @Nullable final String displayName,
374       @Nullable final String identifierForTileColor,
375       @Shape final int shape,
376       final int contactType) {
377 
378     this.setIsCircular(shape == SHAPE_CIRCLE);
379 
380     /**
381      * We return quickly under the following conditions: 1. We are asked to draw a default tile, and
382      * no coloring information is provided, meaning no further initialization is necessary OR 2.
383      * We've already invoked this method before, set mDisplayName, and found that it has not
384      * changed. This is useful during events like hangup, when we lose the call state for special
385      * types of contacts, like voicemail. We keep track of the special case until we encounter a new
386      * display name.
387      */
388     if (contactType == TYPE_DEFAULT
389         && ((displayName == null && identifierForTileColor == null)
390             || (displayName != null && displayName.equals(mDisplayName)))) {
391       return this;
392     }
393 
394     this.mDisplayName = displayName;
395     setContactType(contactType);
396 
397     // Special contact types receive default color and no letter tile, but special iconography.
398     if (contactType != TYPE_PERSON) {
399       this.setLetterAndColorFromContactDetails(null, null);
400     } else {
401       if (identifierForTileColor != null) {
402         this.setLetterAndColorFromContactDetails(displayName, identifierForTileColor);
403       } else {
404         this.setLetterAndColorFromContactDetails(displayName, displayName);
405       }
406     }
407     return this;
408   }
409 }
410