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