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