1/* 2 * Copyright (C) 2010 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.common; 18 19import android.content.ComponentCallbacks2; 20import android.content.Context; 21import android.content.res.Configuration; 22import android.content.res.Resources; 23import android.graphics.drawable.Drawable; 24import android.net.Uri; 25import android.net.Uri.Builder; 26import android.support.annotation.VisibleForTesting; 27import android.text.TextUtils; 28import android.view.View; 29import android.widget.ImageView; 30import android.widget.QuickContactBadge; 31import com.android.contacts.common.lettertiles.LetterTileDrawable; 32import com.android.contacts.common.util.UriUtils; 33import com.android.dialer.common.LogUtil; 34import com.android.dialer.util.PermissionsUtil; 35 36/** Asynchronously loads contact photos and maintains a cache of photos. */ 37public abstract class ContactPhotoManager implements ComponentCallbacks2 { 38 39 /** Contact type constants used for default letter images */ 40 public static final int TYPE_PERSON = LetterTileDrawable.TYPE_PERSON; 41 42 public static final int TYPE_SPAM = LetterTileDrawable.TYPE_SPAM; 43 public static final int TYPE_BUSINESS = LetterTileDrawable.TYPE_BUSINESS; 44 public static final int TYPE_VOICEMAIL = LetterTileDrawable.TYPE_VOICEMAIL; 45 public static final int TYPE_DEFAULT = LetterTileDrawable.TYPE_DEFAULT; 46 public static final int TYPE_GENERIC_AVATAR = LetterTileDrawable.TYPE_GENERIC_AVATAR; 47 /** Scale and offset default constants used for default letter images */ 48 public static final float SCALE_DEFAULT = 1.0f; 49 50 public static final float OFFSET_DEFAULT = 0.0f; 51 public static final boolean IS_CIRCULAR_DEFAULT = false; 52 // TODO: Use LogUtil.isVerboseEnabled for DEBUG branches instead of a lint check. 53 // LINT.DoNotSubmitIf(true) 54 static final boolean DEBUG = false; 55 // LINT.DoNotSubmitIf(true) 56 static final boolean DEBUG_SIZES = false; 57 /** Uri-related constants used for default letter images */ 58 private static final String DISPLAY_NAME_PARAM_KEY = "display_name"; 59 60 private static final String IDENTIFIER_PARAM_KEY = "identifier"; 61 private static final String CONTACT_TYPE_PARAM_KEY = "contact_type"; 62 private static final String SCALE_PARAM_KEY = "scale"; 63 private static final String OFFSET_PARAM_KEY = "offset"; 64 private static final String IS_CIRCULAR_PARAM_KEY = "is_circular"; 65 private static final String DEFAULT_IMAGE_URI_SCHEME = "defaultimage"; 66 private static final Uri DEFAULT_IMAGE_URI = Uri.parse(DEFAULT_IMAGE_URI_SCHEME + "://"); 67 public static final DefaultImageProvider DEFAULT_AVATAR = new LetterTileDefaultImageProvider(); 68 private static ContactPhotoManager sInstance; 69 70 /** 71 * Given a {@link DefaultImageRequest}, returns an Uri that can be used to request a letter tile 72 * avatar when passed to the {@link ContactPhotoManager}. The internal implementation of this uri 73 * is not guaranteed to remain the same across application versions, so the actual uri should 74 * never be persisted in long-term storage and reused. 75 * 76 * @param request A {@link DefaultImageRequest} object with the fields configured to return a 77 * @return A Uri that when later passed to the {@link ContactPhotoManager} via {@link 78 * #loadPhoto(ImageView, Uri, int, boolean, boolean, DefaultImageRequest)}, can be used to 79 * request a default contact image, drawn as a letter tile using the parameters as configured 80 * in the provided {@link DefaultImageRequest} 81 */ 82 public static Uri getDefaultAvatarUriForContact(DefaultImageRequest request) { 83 final Builder builder = DEFAULT_IMAGE_URI.buildUpon(); 84 if (request != null) { 85 if (!TextUtils.isEmpty(request.displayName)) { 86 builder.appendQueryParameter(DISPLAY_NAME_PARAM_KEY, request.displayName); 87 } 88 if (!TextUtils.isEmpty(request.identifier)) { 89 builder.appendQueryParameter(IDENTIFIER_PARAM_KEY, request.identifier); 90 } 91 if (request.contactType != TYPE_DEFAULT) { 92 builder.appendQueryParameter(CONTACT_TYPE_PARAM_KEY, String.valueOf(request.contactType)); 93 } 94 if (request.scale != SCALE_DEFAULT) { 95 builder.appendQueryParameter(SCALE_PARAM_KEY, String.valueOf(request.scale)); 96 } 97 if (request.offset != OFFSET_DEFAULT) { 98 builder.appendQueryParameter(OFFSET_PARAM_KEY, String.valueOf(request.offset)); 99 } 100 if (request.isCircular != IS_CIRCULAR_DEFAULT) { 101 builder.appendQueryParameter(IS_CIRCULAR_PARAM_KEY, String.valueOf(request.isCircular)); 102 } 103 } 104 return builder.build(); 105 } 106 107 /** 108 * Adds a business contact type encoded fragment to the URL. Used to ensure photo URLS from Nearby 109 * Places can be identified as business photo URLs rather than URLs for personal contact photos. 110 * 111 * @param photoUrl The photo URL to modify. 112 * @return URL with the contact type parameter added and set to TYPE_BUSINESS. 113 */ 114 public static String appendBusinessContactType(String photoUrl) { 115 Uri uri = Uri.parse(photoUrl); 116 Builder builder = uri.buildUpon(); 117 builder.encodedFragment(String.valueOf(TYPE_BUSINESS)); 118 return builder.build().toString(); 119 } 120 121 /** 122 * Removes the contact type information stored in the photo URI encoded fragment. 123 * 124 * @param photoUri The photo URI to remove the contact type from. 125 * @return The photo URI with contact type removed. 126 */ 127 public static Uri removeContactType(Uri photoUri) { 128 String encodedFragment = photoUri.getEncodedFragment(); 129 if (!TextUtils.isEmpty(encodedFragment)) { 130 Builder builder = photoUri.buildUpon(); 131 builder.encodedFragment(null); 132 return builder.build(); 133 } 134 return photoUri; 135 } 136 137 /** 138 * Inspects a photo URI to determine if the photo URI represents a business. 139 * 140 * @param photoUri The URI to inspect. 141 * @return Whether the URI represents a business photo or not. 142 */ 143 public static boolean isBusinessContactUri(Uri photoUri) { 144 if (photoUri == null) { 145 return false; 146 } 147 148 String encodedFragment = photoUri.getEncodedFragment(); 149 return !TextUtils.isEmpty(encodedFragment) 150 && encodedFragment.equals(String.valueOf(TYPE_BUSINESS)); 151 } 152 153 protected static DefaultImageRequest getDefaultImageRequestFromUri(Uri uri) { 154 final DefaultImageRequest request = 155 new DefaultImageRequest( 156 uri.getQueryParameter(DISPLAY_NAME_PARAM_KEY), 157 uri.getQueryParameter(IDENTIFIER_PARAM_KEY), 158 false); 159 try { 160 String contactType = uri.getQueryParameter(CONTACT_TYPE_PARAM_KEY); 161 if (!TextUtils.isEmpty(contactType)) { 162 request.contactType = Integer.valueOf(contactType); 163 } 164 165 String scale = uri.getQueryParameter(SCALE_PARAM_KEY); 166 if (!TextUtils.isEmpty(scale)) { 167 request.scale = Float.valueOf(scale); 168 } 169 170 String offset = uri.getQueryParameter(OFFSET_PARAM_KEY); 171 if (!TextUtils.isEmpty(offset)) { 172 request.offset = Float.valueOf(offset); 173 } 174 175 String isCircular = uri.getQueryParameter(IS_CIRCULAR_PARAM_KEY); 176 if (!TextUtils.isEmpty(isCircular)) { 177 request.isCircular = Boolean.valueOf(isCircular); 178 } 179 } catch (NumberFormatException e) { 180 LogUtil.w( 181 "ContactPhotoManager.getDefaultImageRequestFromUri", 182 "Invalid DefaultImageRequest image parameters provided, ignoring and using " 183 + "defaults."); 184 } 185 186 return request; 187 } 188 189 public static ContactPhotoManager getInstance(Context context) { 190 if (sInstance == null) { 191 Context applicationContext = context.getApplicationContext(); 192 sInstance = createContactPhotoManager(applicationContext); 193 applicationContext.registerComponentCallbacks(sInstance); 194 if (PermissionsUtil.hasContactsReadPermissions(context)) { 195 sInstance.preloadPhotosInBackground(); 196 } 197 } 198 return sInstance; 199 } 200 201 public static synchronized ContactPhotoManager createContactPhotoManager(Context context) { 202 return new ContactPhotoManagerImpl(context); 203 } 204 205 @VisibleForTesting 206 public static void injectContactPhotoManagerForTesting(ContactPhotoManager photoManager) { 207 sInstance = photoManager; 208 } 209 210 protected boolean isDefaultImageUri(Uri uri) { 211 return DEFAULT_IMAGE_URI_SCHEME.equals(uri.getScheme()); 212 } 213 214 /** 215 * Load thumbnail image into the supplied image view. If the photo is already cached, it is 216 * displayed immediately. Otherwise a request is sent to load the photo from the database. 217 */ 218 public abstract void loadThumbnail( 219 ImageView view, 220 long photoId, 221 boolean darkTheme, 222 boolean isCircular, 223 DefaultImageRequest defaultImageRequest, 224 DefaultImageProvider defaultProvider); 225 226 /** 227 * Calls {@link #loadThumbnail(ImageView, long, boolean, boolean, DefaultImageRequest, 228 * DefaultImageProvider)} using the {@link DefaultImageProvider} {@link #DEFAULT_AVATAR}. 229 */ 230 public final void loadThumbnail( 231 ImageView view, 232 long photoId, 233 boolean darkTheme, 234 boolean isCircular, 235 DefaultImageRequest defaultImageRequest) { 236 loadThumbnail(view, photoId, darkTheme, isCircular, defaultImageRequest, DEFAULT_AVATAR); 237 } 238 239 public final void loadDialerThumbnailOrPhoto( 240 QuickContactBadge badge, 241 Uri contactUri, 242 long photoId, 243 Uri photoUri, 244 String displayName, 245 int contactType) { 246 badge.assignContactUri(contactUri); 247 badge.setOverlay(null); 248 249 String lookupKey = contactUri == null ? null : UriUtils.getLookupKeyFromUri(contactUri); 250 ContactPhotoManager.DefaultImageRequest request = 251 new ContactPhotoManager.DefaultImageRequest( 252 displayName, lookupKey, contactType, true /* isCircular */); 253 if (photoId == 0 && photoUri != null) { 254 loadDirectoryPhoto(badge, photoUri, false /* darkTheme */, true /* isCircular */, request); 255 } else { 256 loadThumbnail(badge, photoId, false /* darkTheme */, true /* isCircular */, request); 257 } 258 } 259 260 /** 261 * Load photo into the supplied image view. If the photo is already cached, it is displayed 262 * immediately. Otherwise a request is sent to load the photo from the location specified by the 263 * URI. 264 * 265 * @param view The target view 266 * @param photoUri The uri of the photo to load 267 * @param requestedExtent Specifies an approximate Max(width, height) of the targetView. This is 268 * useful if the source image can be a lot bigger that the target, so that the decoding is 269 * done using efficient sampling. If requestedExtent is specified, no sampling of the image is 270 * performed 271 * @param darkTheme Whether the background is dark. This is used for default avatars 272 * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default 273 * letter tile avatar should be drawn. 274 * @param defaultProvider The provider of default avatars (this is used if photoUri doesn't refer 275 * to an existing image) 276 */ 277 public abstract void loadPhoto( 278 ImageView view, 279 Uri photoUri, 280 int requestedExtent, 281 boolean darkTheme, 282 boolean isCircular, 283 DefaultImageRequest defaultImageRequest, 284 DefaultImageProvider defaultProvider); 285 286 /** 287 * Calls {@link #loadPhoto(ImageView, Uri, int, boolean, boolean, DefaultImageRequest, 288 * DefaultImageProvider)} with {@link #DEFAULT_AVATAR} and {@code null} display names and lookup 289 * keys. 290 * 291 * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default 292 * letter tile avatar should be drawn. 293 */ 294 public final void loadPhoto( 295 ImageView view, 296 Uri photoUri, 297 int requestedExtent, 298 boolean darkTheme, 299 boolean isCircular, 300 DefaultImageRequest defaultImageRequest) { 301 loadPhoto( 302 view, 303 photoUri, 304 requestedExtent, 305 darkTheme, 306 isCircular, 307 defaultImageRequest, 308 DEFAULT_AVATAR); 309 } 310 311 /** 312 * Calls {@link #loadPhoto(ImageView, Uri, int, boolean, boolean, DefaultImageRequest, 313 * DefaultImageProvider)} with {@link #DEFAULT_AVATAR} and with the assumption, that the image is 314 * a thumbnail. 315 * 316 * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default 317 * letter tile avatar should be drawn. 318 */ 319 public final void loadDirectoryPhoto( 320 ImageView view, 321 Uri photoUri, 322 boolean darkTheme, 323 boolean isCircular, 324 DefaultImageRequest defaultImageRequest) { 325 loadPhoto(view, photoUri, -1, darkTheme, isCircular, defaultImageRequest, DEFAULT_AVATAR); 326 } 327 328 /** 329 * Remove photo from the supplied image view. This also cancels current pending load request 330 * inside this photo manager. 331 */ 332 public abstract void removePhoto(ImageView view); 333 334 /** Cancels all pending requests to load photos asynchronously. */ 335 public abstract void cancelPendingRequests(View fragmentRootView); 336 337 /** Temporarily stops loading photos from the database. */ 338 public abstract void pause(); 339 340 /** Resumes loading photos from the database. */ 341 public abstract void resume(); 342 343 /** 344 * Marks all cached photos for reloading. We can continue using cache but should also make sure 345 * the photos haven't changed in the background and notify the views if so. 346 */ 347 public abstract void refreshCache(); 348 349 /** Initiates a background process that over time will fill up cache with preload photos. */ 350 public abstract void preloadPhotosInBackground(); 351 352 // ComponentCallbacks2 353 @Override 354 public void onConfigurationChanged(Configuration newConfig) {} 355 356 // ComponentCallbacks2 357 @Override 358 public void onLowMemory() {} 359 360 // ComponentCallbacks2 361 @Override 362 public void onTrimMemory(int level) {} 363 364 /** 365 * Contains fields used to contain contact details and other user-defined settings that might be 366 * used by the ContactPhotoManager to generate a default contact image. This contact image takes 367 * the form of a letter or bitmap drawn on top of a colored tile. 368 */ 369 public static class DefaultImageRequest { 370 371 /** 372 * Used to indicate that a drawable that represents a contact without any contact details should 373 * be returned. 374 */ 375 public static final DefaultImageRequest EMPTY_DEFAULT_IMAGE_REQUEST = new DefaultImageRequest(); 376 /** 377 * Used to indicate that a drawable that represents a business without a business photo should 378 * be returned. 379 */ 380 public static final DefaultImageRequest EMPTY_DEFAULT_BUSINESS_IMAGE_REQUEST = 381 new DefaultImageRequest(null, null, TYPE_BUSINESS, false); 382 /** 383 * Used to indicate that a circular drawable that represents a contact without any contact 384 * details should be returned. 385 */ 386 public static final DefaultImageRequest EMPTY_CIRCULAR_DEFAULT_IMAGE_REQUEST = 387 new DefaultImageRequest(null, null, true); 388 /** 389 * Used to indicate that a circular drawable that represents a business without a business photo 390 * should be returned. 391 */ 392 public static final DefaultImageRequest EMPTY_CIRCULAR_BUSINESS_IMAGE_REQUEST = 393 new DefaultImageRequest(null, null, TYPE_BUSINESS, true); 394 /** The contact's display name. The display name is used to */ 395 public String displayName; 396 /** 397 * A unique and deterministic string that can be used to identify this contact. This is usually 398 * the contact's lookup key, but other contact details can be used as well, especially for 399 * non-local or temporary contacts that might not have a lookup key. This is used to determine 400 * the color of the tile. 401 */ 402 public String identifier; 403 /** 404 * The type of this contact. This contact type may be used to decide the kind of image to use in 405 * the case where a unique letter cannot be generated from the contact's display name and 406 * identifier. See: {@link #TYPE_PERSON} {@link #TYPE_BUSINESS} {@link #TYPE_PERSON} {@link 407 * #TYPE_DEFAULT} 408 */ 409 public int contactType = TYPE_DEFAULT; 410 /** 411 * The amount to scale the letter or bitmap to, as a ratio of its default size (from a range of 412 * 0.0f to 2.0f). The default value is 1.0f. 413 */ 414 public float scale = SCALE_DEFAULT; 415 /** 416 * The amount to vertically offset the letter or image to within the tile. The provided offset 417 * must be within the range of -0.5f to 0.5f. If set to -0.5f, the letter will be shifted 418 * upwards by 0.5 times the height of the canvas it is being drawn on, which means it will be 419 * drawn with the center of the letter starting at the top edge of the canvas. If set to 0.5f, 420 * the letter will be shifted downwards by 0.5 times the height of the canvas it is being drawn 421 * on, which means it will be drawn with the center of the letter starting at the bottom edge of 422 * the canvas. The default is 0.0f, which means the letter is drawn in the exact vertical center 423 * of the tile. 424 */ 425 public float offset = OFFSET_DEFAULT; 426 /** Whether or not to draw the default image as a circle, instead of as a square/rectangle. */ 427 public boolean isCircular = false; 428 429 public DefaultImageRequest() {} 430 431 public DefaultImageRequest(String displayName, String identifier, boolean isCircular) { 432 this(displayName, identifier, TYPE_DEFAULT, SCALE_DEFAULT, OFFSET_DEFAULT, isCircular); 433 } 434 435 public DefaultImageRequest( 436 String displayName, String identifier, int contactType, boolean isCircular) { 437 this(displayName, identifier, contactType, SCALE_DEFAULT, OFFSET_DEFAULT, isCircular); 438 } 439 440 public DefaultImageRequest( 441 String displayName, 442 String identifier, 443 int contactType, 444 float scale, 445 float offset, 446 boolean isCircular) { 447 this.displayName = displayName; 448 this.identifier = identifier; 449 this.contactType = contactType; 450 this.scale = scale; 451 this.offset = offset; 452 this.isCircular = isCircular; 453 } 454 } 455 456 public abstract static class DefaultImageProvider { 457 458 /** 459 * Applies the default avatar to the ImageView. Extent is an indicator for the size (width or 460 * height). If darkTheme is set, the avatar is one that looks better on dark background 461 * 462 * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default 463 * letter tile avatar should be drawn. 464 */ 465 public abstract void applyDefaultImage( 466 ImageView view, int extent, boolean darkTheme, DefaultImageRequest defaultImageRequest); 467 } 468 469 /** 470 * A default image provider that applies a letter tile consisting of a colored background and a 471 * letter in the foreground as the default image for a contact. The color of the background and 472 * the type of letter is decided based on the contact's details. 473 */ 474 private static class LetterTileDefaultImageProvider extends DefaultImageProvider { 475 476 public static Drawable getDefaultImageForContact( 477 Resources resources, DefaultImageRequest defaultImageRequest) { 478 final LetterTileDrawable drawable = new LetterTileDrawable(resources); 479 final int tileShape = 480 defaultImageRequest.isCircular 481 ? LetterTileDrawable.SHAPE_CIRCLE 482 : LetterTileDrawable.SHAPE_RECTANGLE; 483 if (defaultImageRequest != null) { 484 // If the contact identifier is null or empty, fallback to the 485 // displayName. In that case, use {@code null} for the contact's 486 // display name so that a default bitmap will be used instead of a 487 // letter 488 if (TextUtils.isEmpty(defaultImageRequest.identifier)) { 489 drawable.setCanonicalDialerLetterTileDetails( 490 null, defaultImageRequest.displayName, tileShape, defaultImageRequest.contactType); 491 } else { 492 drawable.setCanonicalDialerLetterTileDetails( 493 defaultImageRequest.displayName, 494 defaultImageRequest.identifier, 495 tileShape, 496 defaultImageRequest.contactType); 497 } 498 drawable.setScale(defaultImageRequest.scale); 499 drawable.setOffset(defaultImageRequest.offset); 500 } 501 return drawable; 502 } 503 504 @Override 505 public void applyDefaultImage( 506 ImageView view, int extent, boolean darkTheme, DefaultImageRequest defaultImageRequest) { 507 final Drawable drawable = getDefaultImageForContact(view.getResources(), defaultImageRequest); 508 view.setImageDrawable(drawable); 509 } 510 } 511} 512