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