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