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 17 package com.android.contacts.common; 18 19 import android.content.ComponentCallbacks2; 20 import android.content.Context; 21 import android.content.res.Configuration; 22 import android.content.res.Resources; 23 import android.graphics.drawable.Drawable; 24 import android.net.Uri; 25 import android.net.Uri.Builder; 26 import android.support.annotation.VisibleForTesting; 27 import android.text.TextUtils; 28 import android.view.View; 29 import android.widget.ImageView; 30 import android.widget.QuickContactBadge; 31 import com.android.contacts.common.lettertiles.LetterTileDrawable; 32 import com.android.contacts.common.util.UriUtils; 33 import com.android.dialer.common.LogUtil; 34 import com.android.dialer.util.PermissionsUtil; 35 36 /** Asynchronously loads contact photos and maintains a cache of photos. */ 37 public 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 */ getDefaultAvatarUriForContact(DefaultImageRequest request)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 */ appendBusinessContactType(String photoUrl)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 */ removeContactType(Uri photoUri)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 */ isBusinessContactUri(Uri photoUri)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 getDefaultImageRequestFromUri(Uri uri)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 getInstance(Context context)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 createContactPhotoManager(Context context)201 public static synchronized ContactPhotoManager createContactPhotoManager(Context context) { 202 return new ContactPhotoManagerImpl(context); 203 } 204 205 @VisibleForTesting injectContactPhotoManagerForTesting(ContactPhotoManager photoManager)206 public static void injectContactPhotoManagerForTesting(ContactPhotoManager photoManager) { 207 sInstance = photoManager; 208 } 209 isDefaultImageUri(Uri uri)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 */ loadThumbnail( ImageView view, long photoId, boolean darkTheme, boolean isCircular, DefaultImageRequest defaultImageRequest, DefaultImageProvider defaultProvider)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 */ loadThumbnail( ImageView view, long photoId, boolean darkTheme, boolean isCircular, DefaultImageRequest defaultImageRequest)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 loadDialerThumbnailOrPhoto( QuickContactBadge badge, Uri contactUri, long photoId, Uri photoUri, String displayName, int contactType)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 */ loadPhoto( ImageView view, Uri photoUri, int requestedExtent, boolean darkTheme, boolean isCircular, DefaultImageRequest defaultImageRequest, DefaultImageProvider defaultProvider)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 */ loadPhoto( ImageView view, Uri photoUri, int requestedExtent, boolean darkTheme, boolean isCircular, DefaultImageRequest defaultImageRequest)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 */ loadDirectoryPhoto( ImageView view, Uri photoUri, boolean darkTheme, boolean isCircular, DefaultImageRequest defaultImageRequest)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 */ removePhoto(ImageView view)332 public abstract void removePhoto(ImageView view); 333 334 /** Cancels all pending requests to load photos asynchronously. */ cancelPendingRequests(View fragmentRootView)335 public abstract void cancelPendingRequests(View fragmentRootView); 336 337 /** Temporarily stops loading photos from the database. */ pause()338 public abstract void pause(); 339 340 /** Resumes loading photos from the database. */ resume()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 */ refreshCache()347 public abstract void refreshCache(); 348 349 /** Initiates a background process that over time will fill up cache with preload photos. */ preloadPhotosInBackground()350 public abstract void preloadPhotosInBackground(); 351 352 // ComponentCallbacks2 353 @Override onConfigurationChanged(Configuration newConfig)354 public void onConfigurationChanged(Configuration newConfig) {} 355 356 // ComponentCallbacks2 357 @Override onLowMemory()358 public void onLowMemory() {} 359 360 // ComponentCallbacks2 361 @Override onTrimMemory(int level)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 DefaultImageRequest()429 public DefaultImageRequest() {} 430 DefaultImageRequest(String displayName, String identifier, boolean isCircular)431 public DefaultImageRequest(String displayName, String identifier, boolean isCircular) { 432 this(displayName, identifier, TYPE_DEFAULT, SCALE_DEFAULT, OFFSET_DEFAULT, isCircular); 433 } 434 DefaultImageRequest( String displayName, String identifier, int contactType, boolean isCircular)435 public DefaultImageRequest( 436 String displayName, String identifier, int contactType, boolean isCircular) { 437 this(displayName, identifier, contactType, SCALE_DEFAULT, OFFSET_DEFAULT, isCircular); 438 } 439 DefaultImageRequest( String displayName, String identifier, int contactType, float scale, float offset, boolean isCircular)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 */ applyDefaultImage( ImageView view, int extent, boolean darkTheme, DefaultImageRequest defaultImageRequest)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 getDefaultImageForContact( Resources resources, DefaultImageRequest defaultImageRequest)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 applyDefaultImage( ImageView view, int extent, boolean darkTheme, DefaultImageRequest defaultImageRequest)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