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; 18 19 import android.app.ActivityManager; 20 import android.content.ComponentCallbacks2; 21 import android.content.ContentResolver; 22 import android.content.ContentUris; 23 import android.content.Context; 24 import android.content.res.Configuration; 25 import android.content.res.Resources; 26 import android.database.Cursor; 27 import android.graphics.Bitmap; 28 import android.graphics.Canvas; 29 import android.graphics.Color; 30 import android.graphics.Paint; 31 import android.graphics.Paint.Style; 32 import android.graphics.drawable.BitmapDrawable; 33 import android.graphics.drawable.ColorDrawable; 34 import android.graphics.drawable.Drawable; 35 import android.graphics.drawable.TransitionDrawable; 36 import android.media.ThumbnailUtils; 37 import android.net.TrafficStats; 38 import android.net.Uri; 39 import android.net.Uri.Builder; 40 import android.os.Handler; 41 import android.os.Handler.Callback; 42 import android.os.HandlerThread; 43 import android.os.Message; 44 import android.provider.ContactsContract; 45 import android.provider.ContactsContract.Contacts; 46 import android.provider.ContactsContract.Contacts.Photo; 47 import android.provider.ContactsContract.Data; 48 import android.provider.ContactsContract.Directory; 49 import androidx.core.graphics.drawable.RoundedBitmapDrawable; 50 import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; 51 import android.text.TextUtils; 52 import android.util.Log; 53 import android.util.LruCache; 54 import android.view.View; 55 import android.view.ViewGroup; 56 import android.widget.ImageView; 57 58 import com.android.contacts.lettertiles.LetterTileDrawable; 59 import com.android.contacts.util.BitmapUtil; 60 import com.android.contacts.util.PermissionsUtil; 61 import com.android.contacts.util.TrafficStatsTags; 62 import com.android.contacts.util.UriUtils; 63 import com.android.contactsbind.util.UserAgentGenerator; 64 65 import com.google.common.annotations.VisibleForTesting; 66 import com.google.common.collect.Lists; 67 import com.google.common.collect.Sets; 68 69 import java.io.ByteArrayOutputStream; 70 import java.io.IOException; 71 import java.io.InputStream; 72 import java.lang.ref.Reference; 73 import java.lang.ref.SoftReference; 74 import java.net.HttpURLConnection; 75 import java.net.URL; 76 import java.util.Iterator; 77 import java.util.List; 78 import java.util.Map.Entry; 79 import java.util.Set; 80 import java.util.concurrent.ConcurrentHashMap; 81 import java.util.concurrent.atomic.AtomicInteger; 82 83 /** 84 * Asynchronously loads contact photos and maintains a cache of photos. 85 */ 86 public abstract class ContactPhotoManager implements ComponentCallbacks2 { 87 static final String TAG = "ContactPhotoManager"; 88 static final boolean DEBUG = false; // Don't submit with true 89 static final boolean DEBUG_SIZES = false; // Don't submit with true 90 91 /** Contact type constants used for default letter images */ 92 public static final int TYPE_PERSON = LetterTileDrawable.TYPE_PERSON; 93 public static final int TYPE_BUSINESS = LetterTileDrawable.TYPE_BUSINESS; 94 public static final int TYPE_VOICEMAIL = LetterTileDrawable.TYPE_VOICEMAIL; 95 public static final int TYPE_DEFAULT = LetterTileDrawable.TYPE_DEFAULT; 96 97 /** Scale and offset default constants used for default letter images */ 98 public static final float SCALE_DEFAULT = 1.0f; 99 public static final float OFFSET_DEFAULT = 0.0f; 100 101 public static final boolean IS_CIRCULAR_DEFAULT = false; 102 103 /** Uri-related constants used for default letter images */ 104 private static final String DISPLAY_NAME_PARAM_KEY = "display_name"; 105 private static final String IDENTIFIER_PARAM_KEY = "identifier"; 106 private static final String CONTACT_TYPE_PARAM_KEY = "contact_type"; 107 private static final String SCALE_PARAM_KEY = "scale"; 108 private static final String OFFSET_PARAM_KEY = "offset"; 109 private static final String IS_CIRCULAR_PARAM_KEY = "is_circular"; 110 private static final String DEFAULT_IMAGE_URI_SCHEME = "defaultimage"; 111 private static final Uri DEFAULT_IMAGE_URI = Uri.parse(DEFAULT_IMAGE_URI_SCHEME + "://"); 112 113 // Static field used to cache the default letter avatar drawable that is created 114 // using a null {@link DefaultImageRequest} 115 private static Drawable sDefaultLetterAvatar = null; 116 117 private static ContactPhotoManager sInstance; 118 119 /** 120 * Given a {@link DefaultImageRequest}, returns a {@link Drawable}, that when drawn, will 121 * draw a letter tile avatar based on the request parameters defined in the 122 * {@link DefaultImageRequest}. 123 */ getDefaultAvatarDrawableForContact(Resources resources, boolean hires, DefaultImageRequest defaultImageRequest)124 public static Drawable getDefaultAvatarDrawableForContact(Resources resources, boolean hires, 125 DefaultImageRequest defaultImageRequest) { 126 if (defaultImageRequest == null) { 127 if (sDefaultLetterAvatar == null) { 128 // Cache and return the letter tile drawable that is created by a null request, 129 // so that it doesn't have to be recreated every time it is requested again. 130 sDefaultLetterAvatar = LetterTileDefaultImageProvider.getDefaultImageForContact( 131 resources, null); 132 } 133 return sDefaultLetterAvatar; 134 } 135 return LetterTileDefaultImageProvider.getDefaultImageForContact(resources, 136 defaultImageRequest); 137 } 138 139 /** 140 * Given a {@link DefaultImageRequest}, returns an Uri that can be used to request a 141 * letter tile avatar when passed to the {@link ContactPhotoManager}. The internal 142 * implementation of this uri is not guaranteed to remain the same across application 143 * versions, so the actual uri should never be persisted in long-term storage and reused. 144 * 145 * @param request A {@link DefaultImageRequest} object with the fields configured 146 * to return a 147 * @return A Uri that when later passed to the {@link ContactPhotoManager} via 148 * {@link #loadPhoto(ImageView, Uri, int, boolean, DefaultImageRequest)}, can be 149 * used to request a default contact image, drawn as a letter tile using the 150 * parameters as configured in the provided {@link DefaultImageRequest} 151 */ getDefaultAvatarUriForContact(DefaultImageRequest request)152 public static Uri getDefaultAvatarUriForContact(DefaultImageRequest request) { 153 final Builder builder = DEFAULT_IMAGE_URI.buildUpon(); 154 if (request != null) { 155 if (!TextUtils.isEmpty(request.displayName)) { 156 builder.appendQueryParameter(DISPLAY_NAME_PARAM_KEY, request.displayName); 157 } 158 if (!TextUtils.isEmpty(request.identifier)) { 159 builder.appendQueryParameter(IDENTIFIER_PARAM_KEY, request.identifier); 160 } 161 if (request.contactType != TYPE_DEFAULT) { 162 builder.appendQueryParameter(CONTACT_TYPE_PARAM_KEY, 163 String.valueOf(request.contactType)); 164 } 165 if (request.scale != SCALE_DEFAULT) { 166 builder.appendQueryParameter(SCALE_PARAM_KEY, String.valueOf(request.scale)); 167 } 168 if (request.offset != OFFSET_DEFAULT) { 169 builder.appendQueryParameter(OFFSET_PARAM_KEY, String.valueOf(request.offset)); 170 } 171 if (request.isCircular != IS_CIRCULAR_DEFAULT) { 172 builder.appendQueryParameter(IS_CIRCULAR_PARAM_KEY, 173 String.valueOf(request.isCircular)); 174 } 175 176 } 177 return builder.build(); 178 } 179 180 /** 181 * Adds a business contact type encoded fragment to the URL. Used to ensure photo URLS 182 * from Nearby Places can be identified as business photo URLs rather than URLs for personal 183 * contact photos. 184 * 185 * @param photoUrl The photo URL to modify. 186 * @return URL with the contact type parameter added and set to TYPE_BUSINESS. 187 */ appendBusinessContactType(String photoUrl)188 public static String appendBusinessContactType(String photoUrl) { 189 Uri uri = Uri.parse(photoUrl); 190 Builder builder = uri.buildUpon(); 191 builder.encodedFragment(String.valueOf(TYPE_BUSINESS)); 192 return builder.build().toString(); 193 } 194 195 /** 196 * Removes the contact type information stored in the photo URI encoded fragment. 197 * 198 * @param photoUri The photo URI to remove the contact type from. 199 * @return The photo URI with contact type removed. 200 */ removeContactType(Uri photoUri)201 public static Uri removeContactType(Uri photoUri) { 202 String encodedFragment = photoUri.getEncodedFragment(); 203 if (!TextUtils.isEmpty(encodedFragment)) { 204 Builder builder = photoUri.buildUpon(); 205 builder.encodedFragment(null); 206 return builder.build(); 207 } 208 return photoUri; 209 } 210 211 /** 212 * Inspects a photo URI to determine if the photo URI represents a business. 213 * 214 * @param photoUri The URI to inspect. 215 * @return Whether the URI represents a business photo or not. 216 */ isBusinessContactUri(Uri photoUri)217 public static boolean isBusinessContactUri(Uri photoUri) { 218 if (photoUri == null) { 219 return false; 220 } 221 222 String encodedFragment = photoUri.getEncodedFragment(); 223 return !TextUtils.isEmpty(encodedFragment) 224 && encodedFragment.equals(String.valueOf(TYPE_BUSINESS)); 225 } 226 getDefaultImageRequestFromUri(Uri uri)227 protected static DefaultImageRequest getDefaultImageRequestFromUri(Uri uri) { 228 final DefaultImageRequest request = new DefaultImageRequest( 229 uri.getQueryParameter(DISPLAY_NAME_PARAM_KEY), 230 uri.getQueryParameter(IDENTIFIER_PARAM_KEY), false); 231 try { 232 String contactType = uri.getQueryParameter(CONTACT_TYPE_PARAM_KEY); 233 if (!TextUtils.isEmpty(contactType)) { 234 request.contactType = Integer.valueOf(contactType); 235 } 236 237 String scale = uri.getQueryParameter(SCALE_PARAM_KEY); 238 if (!TextUtils.isEmpty(scale)) { 239 request.scale = Float.valueOf(scale); 240 } 241 242 String offset = uri.getQueryParameter(OFFSET_PARAM_KEY); 243 if (!TextUtils.isEmpty(offset)) { 244 request.offset = Float.valueOf(offset); 245 } 246 247 String isCircular = uri.getQueryParameter(IS_CIRCULAR_PARAM_KEY); 248 if (!TextUtils.isEmpty(isCircular)) { 249 request.isCircular = Boolean.valueOf(isCircular); 250 } 251 } catch (NumberFormatException e) { 252 Log.w(TAG, "Invalid DefaultImageRequest image parameters provided, ignoring and using " 253 + "defaults."); 254 } 255 256 return request; 257 } 258 isDefaultImageUri(Uri uri)259 protected boolean isDefaultImageUri(Uri uri) { 260 return DEFAULT_IMAGE_URI_SCHEME.equals(uri.getScheme()); 261 } 262 263 /** 264 * Contains fields used to contain contact details and other user-defined settings that might 265 * be used by the ContactPhotoManager to generate a default contact image. This contact image 266 * takes the form of a letter or bitmap drawn on top of a colored tile. 267 */ 268 public static class DefaultImageRequest { 269 /** 270 * The contact's display name. The display name is used to 271 */ 272 public String displayName; 273 274 /** 275 * A unique and deterministic string that can be used to identify this contact. This is 276 * usually the contact's lookup key, but other contact details can be used as well, 277 * especially for non-local or temporary contacts that might not have a lookup key. This 278 * is used to determine the color of the tile. 279 */ 280 public String identifier; 281 282 /** 283 * The type of this contact. This contact type may be used to decide the kind of 284 * image to use in the case where a unique letter cannot be generated from the contact's 285 * display name and identifier. See: 286 * {@link #TYPE_PERSON} 287 * {@link #TYPE_BUSINESS} 288 * {@link #TYPE_PERSON} 289 * {@link #TYPE_DEFAULT} 290 */ 291 public int contactType = TYPE_DEFAULT; 292 293 /** 294 * The amount to scale the letter or bitmap to, as a ratio of its default size (from a 295 * range of 0.0f to 2.0f). The default value is 1.0f. 296 */ 297 public float scale = SCALE_DEFAULT; 298 299 /** 300 * The amount to vertically offset the letter or image to within the tile. 301 * The provided offset must be within the range of -0.5f to 0.5f. 302 * If set to -0.5f, the letter will be shifted upwards by 0.5 times the height of the canvas 303 * it is being drawn on, which means it will be drawn with the center of the letter starting 304 * at the top edge of the canvas. 305 * If set to 0.5f, the letter will be shifted downwards by 0.5 times the height of the 306 * canvas it is being drawn on, which means it will be drawn with the center of the letter 307 * starting at the bottom edge of the canvas. 308 * The default is 0.0f, which means the letter is drawn in the exact vertical center of 309 * the tile. 310 */ 311 public float offset = OFFSET_DEFAULT; 312 313 /** 314 * Whether or not to draw the default image as a circle, instead of as a square/rectangle. 315 */ 316 public boolean isCircular = false; 317 318 /** 319 * Used to indicate that a drawable that represents a contact without any contact details 320 * should be returned. 321 */ 322 public static DefaultImageRequest EMPTY_DEFAULT_IMAGE_REQUEST = new DefaultImageRequest(); 323 324 /** 325 * Used to indicate that a drawable that represents a business without a business photo 326 * should be returned. 327 */ 328 public static DefaultImageRequest EMPTY_DEFAULT_BUSINESS_IMAGE_REQUEST = 329 new DefaultImageRequest(null, null, TYPE_BUSINESS, false); 330 331 /** 332 * Used to indicate that a circular drawable that represents a contact without any contact 333 * details should be returned. 334 */ 335 public static DefaultImageRequest EMPTY_CIRCULAR_DEFAULT_IMAGE_REQUEST = 336 new DefaultImageRequest(null, null, true); 337 338 /** 339 * Used to indicate that a circular drawable that represents a business without a business 340 * photo should be returned. 341 */ 342 public static DefaultImageRequest EMPTY_CIRCULAR_BUSINESS_IMAGE_REQUEST = 343 new DefaultImageRequest(null, null, TYPE_BUSINESS, true); 344 DefaultImageRequest()345 public DefaultImageRequest() { 346 } 347 DefaultImageRequest(String displayName, String identifier, boolean isCircular)348 public DefaultImageRequest(String displayName, String identifier, boolean isCircular) { 349 this(displayName, identifier, TYPE_DEFAULT, SCALE_DEFAULT, OFFSET_DEFAULT, isCircular); 350 } 351 DefaultImageRequest(String displayName, String identifier, int contactType, boolean isCircular)352 public DefaultImageRequest(String displayName, String identifier, int contactType, 353 boolean isCircular) { 354 this(displayName, identifier, contactType, SCALE_DEFAULT, OFFSET_DEFAULT, isCircular); 355 } 356 DefaultImageRequest(String displayName, String identifier, int contactType, float scale, float offset, boolean isCircular)357 public DefaultImageRequest(String displayName, String identifier, int contactType, 358 float scale, float offset, boolean isCircular) { 359 this.displayName = displayName; 360 this.identifier = identifier; 361 this.contactType = contactType; 362 this.scale = scale; 363 this.offset = offset; 364 this.isCircular = isCircular; 365 } 366 } 367 368 public static abstract class DefaultImageProvider { 369 /** 370 * Applies the default avatar to the ImageView. Extent is an indicator for the size (width 371 * or height). If darkTheme is set, the avatar is one that looks better on dark background 372 * 373 * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a 374 * default letter tile avatar should be drawn. 375 */ applyDefaultImage(ImageView view, int extent, boolean darkTheme, DefaultImageRequest defaultImageRequest)376 public abstract void applyDefaultImage(ImageView view, int extent, boolean darkTheme, 377 DefaultImageRequest defaultImageRequest); 378 } 379 380 /** 381 * A default image provider that applies a letter tile consisting of a colored background 382 * and a letter in the foreground as the default image for a contact. The color of the 383 * background and the type of letter is decided based on the contact's details. 384 */ 385 private static class LetterTileDefaultImageProvider extends DefaultImageProvider { 386 @Override applyDefaultImage(ImageView view, int extent, boolean darkTheme, DefaultImageRequest defaultImageRequest)387 public void applyDefaultImage(ImageView view, int extent, boolean darkTheme, 388 DefaultImageRequest defaultImageRequest) { 389 final Drawable drawable = getDefaultImageForContact(view.getResources(), 390 defaultImageRequest); 391 view.setImageDrawable(drawable); 392 } 393 getDefaultImageForContact(Resources resources, DefaultImageRequest defaultImageRequest)394 public static Drawable getDefaultImageForContact(Resources resources, 395 DefaultImageRequest defaultImageRequest) { 396 final LetterTileDrawable drawable = new LetterTileDrawable(resources); 397 if (defaultImageRequest != null) { 398 // If the contact identifier is null or empty, fallback to the 399 // displayName. In that case, use {@code null} for the contact's 400 // display name so that a default bitmap will be used instead of a 401 // letter 402 if (TextUtils.isEmpty(defaultImageRequest.identifier)) { 403 drawable.setLetterAndColorFromContactDetails(null, 404 defaultImageRequest.displayName); 405 } else { 406 drawable.setLetterAndColorFromContactDetails(defaultImageRequest.displayName, 407 defaultImageRequest.identifier); 408 } 409 drawable.setContactType(defaultImageRequest.contactType); 410 drawable.setScale(defaultImageRequest.scale); 411 drawable.setOffset(defaultImageRequest.offset); 412 drawable.setIsCircular(defaultImageRequest.isCircular); 413 } 414 return drawable; 415 } 416 } 417 418 private static class BlankDefaultImageProvider extends DefaultImageProvider { 419 private static Drawable sDrawable; 420 421 @Override applyDefaultImage(ImageView view, int extent, boolean darkTheme, DefaultImageRequest defaultImageRequest)422 public void applyDefaultImage(ImageView view, int extent, boolean darkTheme, 423 DefaultImageRequest defaultImageRequest) { 424 if (sDrawable == null) { 425 Context context = view.getContext(); 426 sDrawable = new ColorDrawable(context.getResources().getColor( 427 R.color.image_placeholder)); 428 } 429 view.setImageDrawable(sDrawable); 430 } 431 } 432 433 public static DefaultImageProvider DEFAULT_AVATAR = new LetterTileDefaultImageProvider(); 434 435 public static final DefaultImageProvider DEFAULT_BLANK = new BlankDefaultImageProvider(); 436 getInstance(Context context)437 public static ContactPhotoManager getInstance(Context context) { 438 if (sInstance == null) { 439 Context applicationContext = context.getApplicationContext(); 440 sInstance = createContactPhotoManager(applicationContext); 441 applicationContext.registerComponentCallbacks(sInstance); 442 if (PermissionsUtil.hasContactsPermissions(context)) { 443 sInstance.preloadPhotosInBackground(); 444 } 445 } 446 return sInstance; 447 } 448 createContactPhotoManager(Context context)449 public static synchronized ContactPhotoManager createContactPhotoManager(Context context) { 450 return new ContactPhotoManagerImpl(context); 451 } 452 453 @VisibleForTesting injectContactPhotoManagerForTesting(ContactPhotoManager photoManager)454 public static void injectContactPhotoManagerForTesting(ContactPhotoManager photoManager) { 455 sInstance = photoManager; 456 } 457 458 /** 459 * Load thumbnail image into the supplied image view. If the photo is already cached, 460 * it is displayed immediately. Otherwise a request is sent to load the photo 461 * from the database. 462 */ loadThumbnail(ImageView view, long photoId, boolean darkTheme, boolean isCircular, DefaultImageRequest defaultImageRequest, DefaultImageProvider defaultProvider)463 public abstract void loadThumbnail(ImageView view, long photoId, boolean darkTheme, 464 boolean isCircular, DefaultImageRequest defaultImageRequest, 465 DefaultImageProvider defaultProvider); 466 467 /** 468 * Calls {@link #loadThumbnail(ImageView, long, boolean, DefaultImageRequest, 469 * DefaultImageProvider)} using the {@link DefaultImageProvider} {@link #DEFAULT_AVATAR}. 470 */ loadThumbnail(ImageView view, long photoId, boolean darkTheme, boolean isCircular, DefaultImageRequest defaultImageRequest)471 public final void loadThumbnail(ImageView view, long photoId, boolean darkTheme, 472 boolean isCircular, DefaultImageRequest defaultImageRequest) { 473 loadThumbnail(view, photoId, darkTheme, isCircular, defaultImageRequest, DEFAULT_AVATAR); 474 } 475 476 477 /** 478 * Load photo into the supplied image view. If the photo is already cached, 479 * it is displayed immediately. Otherwise a request is sent to load the photo 480 * from the location specified by the URI. 481 * 482 * @param view The target view 483 * @param photoUri The uri of the photo to load 484 * @param requestedExtent Specifies an approximate Max(width, height) of the targetView. 485 * This is useful if the source image can be a lot bigger that the target, so that the decoding 486 * is done using efficient sampling. If requestedExtent is specified, no sampling of the image 487 * is performed 488 * @param darkTheme Whether the background is dark. This is used for default avatars 489 * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default 490 * letter tile avatar should be drawn. 491 * @param defaultProvider The provider of default avatars (this is used if photoUri doesn't 492 * refer to an existing image) 493 */ loadPhoto(ImageView view, Uri photoUri, int requestedExtent, boolean darkTheme, boolean isCircular, DefaultImageRequest defaultImageRequest, DefaultImageProvider defaultProvider)494 public abstract void loadPhoto(ImageView view, Uri photoUri, int requestedExtent, 495 boolean darkTheme, boolean isCircular, DefaultImageRequest defaultImageRequest, 496 DefaultImageProvider defaultProvider); 497 498 /** 499 * Calls {@link #loadPhoto(ImageView, Uri, int, boolean, DefaultImageRequest, 500 * DefaultImageProvider)} with {@link #DEFAULT_AVATAR} and {@code null} display names and 501 * lookup keys. 502 * 503 * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default 504 * letter tile avatar should be drawn. 505 */ loadPhoto(ImageView view, Uri photoUri, int requestedExtent, boolean darkTheme, boolean isCircular, DefaultImageRequest defaultImageRequest)506 public final void loadPhoto(ImageView view, Uri photoUri, int requestedExtent, 507 boolean darkTheme, boolean isCircular, DefaultImageRequest defaultImageRequest) { 508 loadPhoto(view, photoUri, requestedExtent, darkTheme, isCircular, 509 defaultImageRequest, DEFAULT_AVATAR); 510 } 511 512 /** 513 * Calls {@link #loadPhoto(ImageView, Uri, boolean, boolean, DefaultImageRequest, 514 * DefaultImageProvider)} with {@link #DEFAULT_AVATAR} and with the assumption, that 515 * the image is a thumbnail. 516 * 517 * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default 518 * letter tile avatar should be drawn. 519 */ loadDirectoryPhoto(ImageView view, Uri photoUri, boolean darkTheme, boolean isCircular, DefaultImageRequest defaultImageRequest)520 public final void loadDirectoryPhoto(ImageView view, Uri photoUri, boolean darkTheme, 521 boolean isCircular, DefaultImageRequest defaultImageRequest) { 522 loadPhoto(view, photoUri, -1, darkTheme, isCircular, defaultImageRequest, DEFAULT_AVATAR); 523 } 524 525 /** 526 * Remove photo from the supplied image view. This also cancels current pending load request 527 * inside this photo manager. 528 */ removePhoto(ImageView view)529 public abstract void removePhoto(ImageView view); 530 531 /** 532 * Cancels all pending requests to load photos asynchronously. 533 */ cancelPendingRequests(View fragmentRootView)534 public abstract void cancelPendingRequests(View fragmentRootView); 535 536 /** 537 * Temporarily stops loading photos from the database. 538 */ pause()539 public abstract void pause(); 540 541 /** 542 * Resumes loading photos from the database. 543 */ resume()544 public abstract void resume(); 545 546 /** 547 * Marks all cached photos for reloading. We can continue using cache but should 548 * also make sure the photos haven't changed in the background and notify the views 549 * if so. 550 */ refreshCache()551 public abstract void refreshCache(); 552 553 /** 554 * Stores the given bitmap directly in the LRU bitmap cache. 555 * @param photoUri The URI of the photo (for future requests). 556 * @param bitmap The bitmap. 557 * @param photoBytes The bytes that were parsed to create the bitmap. 558 */ cacheBitmap(Uri photoUri, Bitmap bitmap, byte[] photoBytes)559 public abstract void cacheBitmap(Uri photoUri, Bitmap bitmap, byte[] photoBytes); 560 561 /** 562 * Initiates a background process that over time will fill up cache with 563 * preload photos. 564 */ preloadPhotosInBackground()565 public abstract void preloadPhotosInBackground(); 566 567 // ComponentCallbacks2 568 @Override onConfigurationChanged(Configuration newConfig)569 public void onConfigurationChanged(Configuration newConfig) { 570 } 571 572 // ComponentCallbacks2 573 @Override onLowMemory()574 public void onLowMemory() { 575 } 576 577 // ComponentCallbacks2 578 @Override onTrimMemory(int level)579 public void onTrimMemory(int level) { 580 } 581 } 582 583 class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback { 584 private static final String LOADER_THREAD_NAME = "ContactPhotoLoader"; 585 586 private static final int FADE_TRANSITION_DURATION = 200; 587 588 /** 589 * Type of message sent by the UI thread to itself to indicate that some photos 590 * need to be loaded. 591 */ 592 private static final int MESSAGE_REQUEST_LOADING = 1; 593 594 /** 595 * Type of message sent by the loader thread to indicate that some photos have 596 * been loaded. 597 */ 598 private static final int MESSAGE_PHOTOS_LOADED = 2; 599 600 private static final String[] EMPTY_STRING_ARRAY = new String[0]; 601 602 private static final String[] COLUMNS = new String[] { Photo._ID, Photo.PHOTO }; 603 604 /** 605 * Dummy object used to indicate that a bitmap for a given key could not be stored in the 606 * cache. 607 */ 608 private static final BitmapHolder BITMAP_UNAVAILABLE; 609 610 static { 611 BITMAP_UNAVAILABLE = new BitmapHolder(new byte[0], 0); 612 BITMAP_UNAVAILABLE.bitmapRef = new SoftReference<Bitmap>(null); 613 } 614 615 /** 616 * Maintains the state of a particular photo. 617 */ 618 private static class BitmapHolder { 619 final byte[] bytes; 620 final int originalSmallerExtent; 621 622 volatile boolean fresh; 623 Bitmap bitmap; 624 Reference<Bitmap> bitmapRef; 625 int decodedSampleSize; 626 BitmapHolder(byte[] bytes, int originalSmallerExtent)627 public BitmapHolder(byte[] bytes, int originalSmallerExtent) { 628 this.bytes = bytes; 629 this.fresh = true; 630 this.originalSmallerExtent = originalSmallerExtent; 631 } 632 } 633 634 private final Context mContext; 635 636 /** 637 * An LRU cache for bitmap holders. The cache contains bytes for photos just 638 * as they come from the database. Each holder has a soft reference to the 639 * actual bitmap. 640 */ 641 private final LruCache<Object, BitmapHolder> mBitmapHolderCache; 642 643 /** 644 * {@code true} if ALL entries in {@link #mBitmapHolderCache} are NOT fresh. 645 */ 646 private volatile boolean mBitmapHolderCacheAllUnfresh = true; 647 648 /** 649 * Cache size threshold at which bitmaps will not be preloaded. 650 */ 651 private final int mBitmapHolderCacheRedZoneBytes; 652 653 /** 654 * Level 2 LRU cache for bitmaps. This is a smaller cache that holds 655 * the most recently used bitmaps to save time on decoding 656 * them from bytes (the bytes are stored in {@link #mBitmapHolderCache}. 657 */ 658 private final LruCache<Object, Bitmap> mBitmapCache; 659 660 /** 661 * A map from ImageView to the corresponding photo ID or uri, encapsulated in a request. 662 * The request may swapped out before the photo loading request is started. 663 */ 664 private final ConcurrentHashMap<ImageView, Request> mPendingRequests = 665 new ConcurrentHashMap<ImageView, Request>(); 666 667 /** 668 * Handler for messages sent to the UI thread. 669 */ 670 private final Handler mMainThreadHandler = new Handler(this); 671 672 /** 673 * Thread responsible for loading photos from the database. Created upon 674 * the first request. 675 */ 676 private LoaderThread mLoaderThread; 677 678 /** 679 * A gate to make sure we only send one instance of MESSAGE_PHOTOS_NEEDED at a time. 680 */ 681 private boolean mLoadingRequested; 682 683 /** 684 * Flag indicating if the image loading is paused. 685 */ 686 private boolean mPaused; 687 688 /** Cache size for {@link #mBitmapHolderCache} for devices with "large" RAM. */ 689 private static final int HOLDER_CACHE_SIZE = 2000000; 690 691 /** Cache size for {@link #mBitmapCache} for devices with "large" RAM. */ 692 private static final int BITMAP_CACHE_SIZE = 36864 * 48; // 1728K 693 694 /** Height/width of a thumbnail image */ 695 private static int mThumbnailSize; 696 697 /** For debug: How many times we had to reload cached photo for a stale entry */ 698 private final AtomicInteger mStaleCacheOverwrite = new AtomicInteger(); 699 700 /** For debug: How many times we had to reload cached photo for a fresh entry. Should be 0. */ 701 private final AtomicInteger mFreshCacheOverwrite = new AtomicInteger(); 702 703 /** 704 * The user agent string to use when loading URI based photos. 705 */ 706 private String mUserAgent; 707 ContactPhotoManagerImpl(Context context)708 public ContactPhotoManagerImpl(Context context) { 709 mContext = context; 710 711 final ActivityManager am = ((ActivityManager) context.getSystemService( 712 Context.ACTIVITY_SERVICE)); 713 714 final float cacheSizeAdjustment = (am.isLowRamDevice()) ? 0.5f : 1.0f; 715 716 final int bitmapCacheSize = (int) (cacheSizeAdjustment * BITMAP_CACHE_SIZE); 717 mBitmapCache = new LruCache<Object, Bitmap>(bitmapCacheSize) { 718 @Override protected int sizeOf(Object key, Bitmap value) { 719 return value.getByteCount(); 720 } 721 722 @Override protected void entryRemoved( 723 boolean evicted, Object key, Bitmap oldValue, Bitmap newValue) { 724 if (DEBUG) dumpStats(); 725 } 726 }; 727 final int holderCacheSize = (int) (cacheSizeAdjustment * HOLDER_CACHE_SIZE); 728 mBitmapHolderCache = new LruCache<Object, BitmapHolder>(holderCacheSize) { 729 @Override protected int sizeOf(Object key, BitmapHolder value) { 730 return value.bytes != null ? value.bytes.length : 0; 731 } 732 733 @Override protected void entryRemoved( 734 boolean evicted, Object key, BitmapHolder oldValue, BitmapHolder newValue) { 735 if (DEBUG) dumpStats(); 736 } 737 }; 738 mBitmapHolderCacheRedZoneBytes = (int) (holderCacheSize * 0.75); 739 Log.i(TAG, "Cache adj: " + cacheSizeAdjustment); 740 if (DEBUG) { 741 Log.d(TAG, "Cache size: " + btk(mBitmapHolderCache.maxSize()) 742 + " + " + btk(mBitmapCache.maxSize())); 743 } 744 745 mThumbnailSize = context.getResources().getDimensionPixelSize( 746 R.dimen.contact_browser_list_item_photo_size); 747 748 // Get a user agent string to use for URI photo requests. 749 mUserAgent = UserAgentGenerator.getUserAgent(context); 750 if (mUserAgent == null) { 751 mUserAgent = ""; 752 } 753 } 754 755 /** Converts bytes to K bytes, rounding up. Used only for debug log. */ btk(int bytes)756 private static String btk(int bytes) { 757 return ((bytes + 1023) / 1024) + "K"; 758 } 759 safeDiv(int dividend, int divisor)760 private static final int safeDiv(int dividend, int divisor) { 761 return (divisor == 0) ? 0 : (dividend / divisor); 762 } 763 764 /** 765 * Dump cache stats on logcat. 766 */ dumpStats()767 private void dumpStats() { 768 if (!DEBUG) return; 769 { 770 int numHolders = 0; 771 int rawBytes = 0; 772 int bitmapBytes = 0; 773 int numBitmaps = 0; 774 for (BitmapHolder h : mBitmapHolderCache.snapshot().values()) { 775 numHolders++; 776 if (h.bytes != null) { 777 rawBytes += h.bytes.length; 778 } 779 Bitmap b = h.bitmapRef != null ? h.bitmapRef.get() : null; 780 if (b != null) { 781 numBitmaps++; 782 bitmapBytes += b.getByteCount(); 783 } 784 } 785 Log.d(TAG, "L1: " + btk(rawBytes) + " + " + btk(bitmapBytes) + " = " 786 + btk(rawBytes + bitmapBytes) + ", " + numHolders + " holders, " 787 + numBitmaps + " bitmaps, avg: " 788 + btk(safeDiv(rawBytes, numHolders)) 789 + "," + btk(safeDiv(bitmapBytes,numBitmaps))); 790 Log.d(TAG, "L1 Stats: " + mBitmapHolderCache.toString() 791 + ", overwrite: fresh=" + mFreshCacheOverwrite.get() 792 + " stale=" + mStaleCacheOverwrite.get()); 793 } 794 795 { 796 int numBitmaps = 0; 797 int bitmapBytes = 0; 798 for (Bitmap b : mBitmapCache.snapshot().values()) { 799 numBitmaps++; 800 bitmapBytes += b.getByteCount(); 801 } 802 Log.d(TAG, "L2: " + btk(bitmapBytes) + ", " + numBitmaps + " bitmaps" 803 + ", avg: " + btk(safeDiv(bitmapBytes, numBitmaps))); 804 // We don't get from L2 cache, so L2 stats is meaningless. 805 } 806 } 807 808 @Override onTrimMemory(int level)809 public void onTrimMemory(int level) { 810 if (DEBUG) Log.d(TAG, "onTrimMemory: " + level); 811 if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) { 812 // Clear the caches. Note all pending requests will be removed too. 813 clear(); 814 } 815 } 816 817 @Override preloadPhotosInBackground()818 public void preloadPhotosInBackground() { 819 ensureLoaderThread(); 820 mLoaderThread.requestPreloading(); 821 } 822 823 @Override loadThumbnail(ImageView view, long photoId, boolean darkTheme, boolean isCircular, DefaultImageRequest defaultImageRequest, DefaultImageProvider defaultProvider)824 public void loadThumbnail(ImageView view, long photoId, boolean darkTheme, boolean isCircular, 825 DefaultImageRequest defaultImageRequest, DefaultImageProvider defaultProvider) { 826 if (photoId == 0) { 827 // No photo is needed 828 defaultProvider.applyDefaultImage(view, -1, darkTheme, defaultImageRequest); 829 mPendingRequests.remove(view); 830 } else { 831 if (DEBUG) Log.d(TAG, "loadPhoto request: " + photoId); 832 loadPhotoByIdOrUri(view, Request.createFromThumbnailId(photoId, darkTheme, isCircular, 833 defaultProvider, defaultImageRequest)); 834 } 835 } 836 837 @Override loadPhoto(ImageView view, Uri photoUri, int requestedExtent, boolean darkTheme, boolean isCircular, DefaultImageRequest defaultImageRequest, DefaultImageProvider defaultProvider)838 public void loadPhoto(ImageView view, Uri photoUri, int requestedExtent, boolean darkTheme, 839 boolean isCircular, DefaultImageRequest defaultImageRequest, 840 DefaultImageProvider defaultProvider) { 841 if (photoUri == null) { 842 // No photo is needed 843 defaultProvider.applyDefaultImage(view, requestedExtent, darkTheme, 844 defaultImageRequest); 845 mPendingRequests.remove(view); 846 } else { 847 if (DEBUG) Log.d(TAG, "loadPhoto request: " + photoUri); 848 if (isDefaultImageUri(photoUri)) { 849 createAndApplyDefaultImageForUri(view, photoUri, requestedExtent, darkTheme, 850 isCircular, defaultProvider); 851 } else { 852 loadPhotoByIdOrUri(view, Request.createFromUri(photoUri, requestedExtent, 853 darkTheme, isCircular, defaultProvider, defaultImageRequest)); 854 } 855 } 856 } 857 createAndApplyDefaultImageForUri(ImageView view, Uri uri, int requestedExtent, boolean darkTheme, boolean isCircular, DefaultImageProvider defaultProvider)858 private void createAndApplyDefaultImageForUri(ImageView view, Uri uri, int requestedExtent, 859 boolean darkTheme, boolean isCircular, DefaultImageProvider defaultProvider) { 860 DefaultImageRequest request = getDefaultImageRequestFromUri(uri); 861 request.isCircular = isCircular; 862 defaultProvider.applyDefaultImage(view, requestedExtent, darkTheme, request); 863 } 864 loadPhotoByIdOrUri(ImageView view, Request request)865 private void loadPhotoByIdOrUri(ImageView view, Request request) { 866 boolean loaded = loadCachedPhoto(view, request, false); 867 if (loaded) { 868 mPendingRequests.remove(view); 869 } else { 870 mPendingRequests.put(view, request); 871 if (!mPaused) { 872 // Send a request to start loading photos 873 requestLoading(); 874 } 875 } 876 } 877 878 @Override removePhoto(ImageView view)879 public void removePhoto(ImageView view) { 880 view.setImageDrawable(null); 881 mPendingRequests.remove(view); 882 } 883 884 885 /** 886 * Cancels pending requests to load photos asynchronously for views inside 887 * {@param fragmentRootView}. If {@param fragmentRootView} is null, cancels all requests. 888 */ 889 @Override cancelPendingRequests(View fragmentRootView)890 public void cancelPendingRequests(View fragmentRootView) { 891 if (fragmentRootView == null) { 892 mPendingRequests.clear(); 893 return; 894 } 895 final Iterator<Entry<ImageView, Request>> iterator = mPendingRequests.entrySet().iterator(); 896 while (iterator.hasNext()) { 897 final ImageView imageView = iterator.next().getKey(); 898 // If an ImageView is orphaned (currently scrap) or a child of fragmentRootView, then 899 // we can safely remove its request. 900 if (imageView.getParent() == null || isChildView(fragmentRootView, imageView)) { 901 iterator.remove(); 902 } 903 } 904 } 905 isChildView(View parent, View potentialChild)906 private static boolean isChildView(View parent, View potentialChild) { 907 return potentialChild.getParent() != null && (potentialChild.getParent() == parent || ( 908 potentialChild.getParent() instanceof ViewGroup && isChildView(parent, 909 (ViewGroup) potentialChild.getParent()))); 910 } 911 912 @Override refreshCache()913 public void refreshCache() { 914 if (mBitmapHolderCacheAllUnfresh) { 915 if (DEBUG) Log.d(TAG, "refreshCache -- no fresh entries."); 916 return; 917 } 918 if (DEBUG) Log.d(TAG, "refreshCache"); 919 mBitmapHolderCacheAllUnfresh = true; 920 for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) { 921 if (holder != BITMAP_UNAVAILABLE) { 922 holder.fresh = false; 923 } 924 } 925 } 926 927 /** 928 * Checks if the photo is present in cache. If so, sets the photo on the view. 929 * 930 * @return false if the photo needs to be (re)loaded from the provider. 931 */ loadCachedPhoto(ImageView view, Request request, boolean fadeIn)932 private boolean loadCachedPhoto(ImageView view, Request request, boolean fadeIn) { 933 BitmapHolder holder = mBitmapHolderCache.get(request.getKey()); 934 if (holder == null) { 935 // The bitmap has not been loaded ==> show default avatar 936 request.applyDefaultImage(view, request.mIsCircular); 937 return false; 938 } 939 940 if (holder.bytes == null || holder.bytes.length == 0) { 941 request.applyDefaultImage(view, request.mIsCircular); 942 return holder.fresh; 943 } 944 945 Bitmap cachedBitmap = holder.bitmapRef == null ? null : holder.bitmapRef.get(); 946 if (cachedBitmap == null) { 947 if (holder.bytes.length < 8 * 1024) { 948 // Small thumbnails are usually quick to inflate. Let's do that on the UI thread 949 inflateBitmap(holder, request.getRequestedExtent()); 950 cachedBitmap = holder.bitmap; 951 if (cachedBitmap == null) return false; 952 } else { 953 // This is bigger data. Let's send that back to the Loader so that we can 954 // inflate this in the background 955 request.applyDefaultImage(view, request.mIsCircular); 956 return false; 957 } 958 } 959 960 final Drawable previousDrawable = view.getDrawable(); 961 if (fadeIn && previousDrawable != null) { 962 final Drawable[] layers = new Drawable[2]; 963 // Prevent cascade of TransitionDrawables. 964 if (previousDrawable instanceof TransitionDrawable) { 965 final TransitionDrawable previousTransitionDrawable = 966 (TransitionDrawable) previousDrawable; 967 layers[0] = previousTransitionDrawable.getDrawable( 968 previousTransitionDrawable.getNumberOfLayers() - 1); 969 } else { 970 layers[0] = previousDrawable; 971 } 972 layers[1] = getDrawableForBitmap(mContext.getResources(), cachedBitmap, request); 973 TransitionDrawable drawable = new TransitionDrawable(layers); 974 view.setImageDrawable(drawable); 975 drawable.startTransition(FADE_TRANSITION_DURATION); 976 } else { 977 view.setImageDrawable( 978 getDrawableForBitmap(mContext.getResources(), cachedBitmap, request)); 979 } 980 981 // Put the bitmap in the LRU cache. But only do this for images that are small enough 982 // (we require that at least six of those can be cached at the same time) 983 if (cachedBitmap.getByteCount() < mBitmapCache.maxSize() / 6) { 984 mBitmapCache.put(request.getKey(), cachedBitmap); 985 } 986 987 // Soften the reference 988 holder.bitmap = null; 989 990 return holder.fresh; 991 } 992 993 /** 994 * Given a bitmap, returns a drawable that is configured to display the bitmap based on the 995 * specified request. 996 */ getDrawableForBitmap(Resources resources, Bitmap bitmap, Request request)997 private Drawable getDrawableForBitmap(Resources resources, Bitmap bitmap, Request request) { 998 if (request.mIsCircular) { 999 final RoundedBitmapDrawable drawable = 1000 RoundedBitmapDrawableFactory.create(resources, bitmap); 1001 drawable.setAntiAlias(true); 1002 drawable.setCornerRadius(bitmap.getHeight() / 2); 1003 return drawable; 1004 } else { 1005 return new BitmapDrawable(resources, bitmap); 1006 } 1007 } 1008 1009 /** 1010 * If necessary, decodes bytes stored in the holder to Bitmap. As long as the 1011 * bitmap is held either by {@link #mBitmapCache} or by a soft reference in 1012 * the holder, it will not be necessary to decode the bitmap. 1013 */ inflateBitmap(BitmapHolder holder, int requestedExtent)1014 private static void inflateBitmap(BitmapHolder holder, int requestedExtent) { 1015 final int sampleSize = 1016 BitmapUtil.findOptimalSampleSize(holder.originalSmallerExtent, requestedExtent); 1017 byte[] bytes = holder.bytes; 1018 if (bytes == null || bytes.length == 0) { 1019 return; 1020 } 1021 1022 if (sampleSize == holder.decodedSampleSize) { 1023 // Check the soft reference. If will be retained if the bitmap is also 1024 // in the LRU cache, so we don't need to check the LRU cache explicitly. 1025 if (holder.bitmapRef != null) { 1026 holder.bitmap = holder.bitmapRef.get(); 1027 if (holder.bitmap != null) { 1028 return; 1029 } 1030 } 1031 } 1032 1033 try { 1034 Bitmap bitmap = BitmapUtil.decodeBitmapFromBytes(bytes, sampleSize); 1035 1036 // TODO: As a temporary workaround while framework support is being added to 1037 // clip non-square bitmaps into a perfect circle, manually crop the bitmap into 1038 // into a square if it will be displayed as a thumbnail so that it can be cropped 1039 // into a circle. 1040 final int height = bitmap.getHeight(); 1041 final int width = bitmap.getWidth(); 1042 1043 // The smaller dimension of a scaled bitmap can range from anywhere from 0 to just 1044 // below twice the length of a thumbnail image due to the way we calculate the optimal 1045 // sample size. 1046 if (height != width && Math.min(height, width) <= mThumbnailSize * 2) { 1047 final int dimension = Math.min(height, width); 1048 bitmap = ThumbnailUtils.extractThumbnail(bitmap, dimension, dimension); 1049 } 1050 // make bitmap mutable and draw size onto it 1051 if (DEBUG_SIZES) { 1052 Bitmap original = bitmap; 1053 bitmap = bitmap.copy(bitmap.getConfig(), true); 1054 original.recycle(); 1055 Canvas canvas = new Canvas(bitmap); 1056 Paint paint = new Paint(); 1057 paint.setTextSize(16); 1058 paint.setColor(Color.BLUE); 1059 paint.setStyle(Style.FILL); 1060 canvas.drawRect(0.0f, 0.0f, 50.0f, 20.0f, paint); 1061 paint.setColor(Color.WHITE); 1062 paint.setAntiAlias(true); 1063 canvas.drawText(bitmap.getWidth() + "/" + sampleSize, 0, 15, paint); 1064 } 1065 1066 holder.decodedSampleSize = sampleSize; 1067 holder.bitmap = bitmap; 1068 holder.bitmapRef = new SoftReference<Bitmap>(bitmap); 1069 if (DEBUG) { 1070 Log.d(TAG, "inflateBitmap " + btk(bytes.length) + " -> " 1071 + bitmap.getWidth() + "x" + bitmap.getHeight() 1072 + ", " + btk(bitmap.getByteCount())); 1073 } 1074 } catch (OutOfMemoryError e) { 1075 // Do nothing - the photo will appear to be missing 1076 } 1077 } 1078 clear()1079 public void clear() { 1080 if (DEBUG) Log.d(TAG, "clear"); 1081 mPendingRequests.clear(); 1082 mBitmapHolderCache.evictAll(); 1083 mBitmapCache.evictAll(); 1084 } 1085 1086 @Override pause()1087 public void pause() { 1088 mPaused = true; 1089 } 1090 1091 @Override resume()1092 public void resume() { 1093 mPaused = false; 1094 if (DEBUG) dumpStats(); 1095 if (!mPendingRequests.isEmpty()) { 1096 requestLoading(); 1097 } 1098 } 1099 1100 /** 1101 * Sends a message to this thread itself to start loading images. If the current 1102 * view contains multiple image views, all of those image views will get a chance 1103 * to request their respective photos before any of those requests are executed. 1104 * This allows us to load images in bulk. 1105 */ requestLoading()1106 private void requestLoading() { 1107 if (!mLoadingRequested) { 1108 mLoadingRequested = true; 1109 mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING); 1110 } 1111 } 1112 1113 /** 1114 * Processes requests on the main thread. 1115 */ 1116 @Override handleMessage(Message msg)1117 public boolean handleMessage(Message msg) { 1118 switch (msg.what) { 1119 case MESSAGE_REQUEST_LOADING: { 1120 mLoadingRequested = false; 1121 if (!mPaused) { 1122 ensureLoaderThread(); 1123 mLoaderThread.requestLoading(); 1124 } 1125 return true; 1126 } 1127 1128 case MESSAGE_PHOTOS_LOADED: { 1129 if (!mPaused) { 1130 processLoadedImages(); 1131 } 1132 if (DEBUG) dumpStats(); 1133 return true; 1134 } 1135 } 1136 return false; 1137 } 1138 ensureLoaderThread()1139 public void ensureLoaderThread() { 1140 if (mLoaderThread == null) { 1141 mLoaderThread = new LoaderThread(mContext.getContentResolver()); 1142 mLoaderThread.start(); 1143 } 1144 } 1145 1146 /** 1147 * Goes over pending loading requests and displays loaded photos. If some of the 1148 * photos still haven't been loaded, sends another request for image loading. 1149 */ processLoadedImages()1150 private void processLoadedImages() { 1151 final Iterator<Entry<ImageView, Request>> iterator = mPendingRequests.entrySet().iterator(); 1152 while (iterator.hasNext()) { 1153 final Entry<ImageView, Request> entry = iterator.next(); 1154 // TODO: Temporarily disable contact photo fading in, until issues with 1155 // RoundedBitmapDrawables overlapping the default image drawables are resolved. 1156 final boolean loaded = loadCachedPhoto(entry.getKey(), entry.getValue(), false); 1157 if (loaded) { 1158 iterator.remove(); 1159 } 1160 } 1161 1162 softenCache(); 1163 1164 if (!mPendingRequests.isEmpty()) { 1165 requestLoading(); 1166 } 1167 } 1168 1169 /** 1170 * Removes strong references to loaded bitmaps to allow them to be garbage collected 1171 * if needed. Some of the bitmaps will still be retained by {@link #mBitmapCache}. 1172 */ softenCache()1173 private void softenCache() { 1174 for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) { 1175 holder.bitmap = null; 1176 } 1177 } 1178 1179 /** 1180 * Stores the supplied bitmap in cache. 1181 * bytes should be null to indicate a failure to load the photo. An empty byte[] signifies 1182 * a successful load but no photo was available. 1183 */ cacheBitmap(Object key, byte[] bytes, boolean preloading, int requestedExtent)1184 private void cacheBitmap(Object key, byte[] bytes, boolean preloading, int requestedExtent) { 1185 if (DEBUG) { 1186 BitmapHolder prev = mBitmapHolderCache.get(key); 1187 if (prev != null && prev.bytes != null) { 1188 Log.d(TAG, "Overwriting cache: key=" + key + (prev.fresh ? " FRESH" : " stale")); 1189 if (prev.fresh) { 1190 mFreshCacheOverwrite.incrementAndGet(); 1191 } else { 1192 mStaleCacheOverwrite.incrementAndGet(); 1193 } 1194 } 1195 Log.d(TAG, "Caching data: key=" + key + ", " + 1196 (bytes == null ? "<null>" : btk(bytes.length))); 1197 } 1198 BitmapHolder holder = new BitmapHolder(bytes, 1199 bytes == null ? -1 : BitmapUtil.getSmallerExtentFromBytes(bytes)); 1200 1201 // Unless this image is being preloaded, decode it right away while 1202 // we are still on the background thread. 1203 if (!preloading) { 1204 inflateBitmap(holder, requestedExtent); 1205 } 1206 1207 if (bytes != null) { 1208 mBitmapHolderCache.put(key, holder); 1209 if (mBitmapHolderCache.get(key) != holder) { 1210 Log.w(TAG, "Bitmap too big to fit in cache."); 1211 mBitmapHolderCache.put(key, BITMAP_UNAVAILABLE); 1212 } 1213 } else { 1214 mBitmapHolderCache.put(key, BITMAP_UNAVAILABLE); 1215 } 1216 1217 mBitmapHolderCacheAllUnfresh = false; 1218 } 1219 1220 @Override cacheBitmap(Uri photoUri, Bitmap bitmap, byte[] photoBytes)1221 public void cacheBitmap(Uri photoUri, Bitmap bitmap, byte[] photoBytes) { 1222 final int smallerExtent = Math.min(bitmap.getWidth(), bitmap.getHeight()); 1223 // We can pretend here that the extent of the photo was the size that we originally 1224 // requested 1225 Request request = Request.createFromUri(photoUri, smallerExtent, false /* darkTheme */, 1226 false /* isCircular */ , DEFAULT_AVATAR); 1227 BitmapHolder holder = new BitmapHolder(photoBytes, smallerExtent); 1228 holder.bitmapRef = new SoftReference<Bitmap>(bitmap); 1229 mBitmapHolderCache.put(request.getKey(), holder); 1230 mBitmapHolderCacheAllUnfresh = false; 1231 mBitmapCache.put(request.getKey(), bitmap); 1232 } 1233 1234 /** 1235 * Populates an array of photo IDs that need to be loaded. Also decodes bitmaps that we have 1236 * already loaded 1237 */ obtainPhotoIdsAndUrisToLoad(Set<Long> photoIds, Set<String> photoIdsAsStrings, Set<Request> uris)1238 private void obtainPhotoIdsAndUrisToLoad(Set<Long> photoIds, 1239 Set<String> photoIdsAsStrings, Set<Request> uris) { 1240 photoIds.clear(); 1241 photoIdsAsStrings.clear(); 1242 uris.clear(); 1243 1244 boolean jpegsDecoded = false; 1245 1246 /* 1247 * Since the call is made from the loader thread, the map could be 1248 * changing during the iteration. That's not really a problem: 1249 * ConcurrentHashMap will allow those changes to happen without throwing 1250 * exceptions. Since we may miss some requests in the situation of 1251 * concurrent change, we will need to check the map again once loading 1252 * is complete. 1253 */ 1254 Iterator<Request> iterator = mPendingRequests.values().iterator(); 1255 while (iterator.hasNext()) { 1256 Request request = iterator.next(); 1257 final BitmapHolder holder = mBitmapHolderCache.get(request.getKey()); 1258 if (holder == BITMAP_UNAVAILABLE) { 1259 continue; 1260 } 1261 if (holder != null && holder.bytes != null && holder.fresh && 1262 (holder.bitmapRef == null || holder.bitmapRef.get() == null)) { 1263 // This was previously loaded but we don't currently have the inflated Bitmap 1264 inflateBitmap(holder, request.getRequestedExtent()); 1265 jpegsDecoded = true; 1266 } else { 1267 if (holder == null || !holder.fresh) { 1268 if (request.isUriRequest()) { 1269 uris.add(request); 1270 } else { 1271 photoIds.add(request.getId()); 1272 photoIdsAsStrings.add(String.valueOf(request.mId)); 1273 } 1274 } 1275 } 1276 } 1277 1278 if (jpegsDecoded) mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED); 1279 } 1280 1281 /** 1282 * The thread that performs loading of photos from the database. 1283 */ 1284 private class LoaderThread extends HandlerThread implements Callback { 1285 private static final int BUFFER_SIZE = 1024*16; 1286 private static final int MESSAGE_PRELOAD_PHOTOS = 0; 1287 private static final int MESSAGE_LOAD_PHOTOS = 1; 1288 1289 /** 1290 * A pause between preload batches that yields to the UI thread. 1291 */ 1292 private static final int PHOTO_PRELOAD_DELAY = 1000; 1293 1294 /** 1295 * Number of photos to preload per batch. 1296 */ 1297 private static final int PRELOAD_BATCH = 25; 1298 1299 /** 1300 * Maximum number of photos to preload. If the cache size is 2Mb and 1301 * the expected average size of a photo is 4kb, then this number should be 2Mb/4kb = 500. 1302 */ 1303 private static final int MAX_PHOTOS_TO_PRELOAD = 100; 1304 1305 private final ContentResolver mResolver; 1306 private final StringBuilder mStringBuilder = new StringBuilder(); 1307 private final Set<Long> mPhotoIds = Sets.newHashSet(); 1308 private final Set<String> mPhotoIdsAsStrings = Sets.newHashSet(); 1309 private final Set<Request> mPhotoUris = Sets.newHashSet(); 1310 private final List<Long> mPreloadPhotoIds = Lists.newArrayList(); 1311 1312 private Handler mLoaderThreadHandler; 1313 private byte mBuffer[]; 1314 1315 private static final int PRELOAD_STATUS_NOT_STARTED = 0; 1316 private static final int PRELOAD_STATUS_IN_PROGRESS = 1; 1317 private static final int PRELOAD_STATUS_DONE = 2; 1318 1319 private int mPreloadStatus = PRELOAD_STATUS_NOT_STARTED; 1320 LoaderThread(ContentResolver resolver)1321 public LoaderThread(ContentResolver resolver) { 1322 super(LOADER_THREAD_NAME); 1323 mResolver = resolver; 1324 } 1325 ensureHandler()1326 public void ensureHandler() { 1327 if (mLoaderThreadHandler == null) { 1328 mLoaderThreadHandler = new Handler(getLooper(), this); 1329 } 1330 } 1331 1332 /** 1333 * Kicks off preloading of the next batch of photos on the background thread. 1334 * Preloading will happen after a delay: we want to yield to the UI thread 1335 * as much as possible. 1336 * <p> 1337 * If preloading is already complete, does nothing. 1338 */ requestPreloading()1339 public void requestPreloading() { 1340 if (mPreloadStatus == PRELOAD_STATUS_DONE) { 1341 return; 1342 } 1343 1344 ensureHandler(); 1345 if (mLoaderThreadHandler.hasMessages(MESSAGE_LOAD_PHOTOS)) { 1346 return; 1347 } 1348 1349 mLoaderThreadHandler.sendEmptyMessageDelayed( 1350 MESSAGE_PRELOAD_PHOTOS, PHOTO_PRELOAD_DELAY); 1351 } 1352 1353 /** 1354 * Sends a message to this thread to load requested photos. Cancels a preloading 1355 * request, if any: we don't want preloading to impede loading of the photos 1356 * we need to display now. 1357 */ requestLoading()1358 public void requestLoading() { 1359 ensureHandler(); 1360 mLoaderThreadHandler.removeMessages(MESSAGE_PRELOAD_PHOTOS); 1361 mLoaderThreadHandler.sendEmptyMessage(MESSAGE_LOAD_PHOTOS); 1362 } 1363 1364 /** 1365 * Receives the above message, loads photos and then sends a message 1366 * to the main thread to process them. 1367 */ 1368 @Override handleMessage(Message msg)1369 public boolean handleMessage(Message msg) { 1370 switch (msg.what) { 1371 case MESSAGE_PRELOAD_PHOTOS: 1372 preloadPhotosInBackground(); 1373 break; 1374 case MESSAGE_LOAD_PHOTOS: 1375 loadPhotosInBackground(); 1376 break; 1377 } 1378 return true; 1379 } 1380 1381 /** 1382 * The first time it is called, figures out which photos need to be preloaded. 1383 * Each subsequent call preloads the next batch of photos and requests 1384 * another cycle of preloading after a delay. The whole process ends when 1385 * we either run out of photos to preload or fill up cache. 1386 */ preloadPhotosInBackground()1387 private void preloadPhotosInBackground() { 1388 if (mPreloadStatus == PRELOAD_STATUS_DONE) { 1389 return; 1390 } 1391 1392 if (mPreloadStatus == PRELOAD_STATUS_NOT_STARTED) { 1393 queryPhotosForPreload(); 1394 if (mPreloadPhotoIds.isEmpty()) { 1395 mPreloadStatus = PRELOAD_STATUS_DONE; 1396 } else { 1397 mPreloadStatus = PRELOAD_STATUS_IN_PROGRESS; 1398 } 1399 requestPreloading(); 1400 return; 1401 } 1402 1403 if (mBitmapHolderCache.size() > mBitmapHolderCacheRedZoneBytes) { 1404 mPreloadStatus = PRELOAD_STATUS_DONE; 1405 return; 1406 } 1407 1408 mPhotoIds.clear(); 1409 mPhotoIdsAsStrings.clear(); 1410 1411 int count = 0; 1412 int preloadSize = mPreloadPhotoIds.size(); 1413 while(preloadSize > 0 && mPhotoIds.size() < PRELOAD_BATCH) { 1414 preloadSize--; 1415 count++; 1416 Long photoId = mPreloadPhotoIds.get(preloadSize); 1417 mPhotoIds.add(photoId); 1418 mPhotoIdsAsStrings.add(photoId.toString()); 1419 mPreloadPhotoIds.remove(preloadSize); 1420 } 1421 1422 loadThumbnails(true); 1423 1424 if (preloadSize == 0) { 1425 mPreloadStatus = PRELOAD_STATUS_DONE; 1426 } 1427 1428 if (Log.isLoggable(TAG, Log.VERBOSE)) { 1429 Log.v(TAG, "Preloaded " + count + " photos. Cached bytes: " 1430 + mBitmapHolderCache.size()); 1431 } 1432 1433 requestPreloading(); 1434 } 1435 queryPhotosForPreload()1436 private void queryPhotosForPreload() { 1437 Cursor cursor = null; 1438 try { 1439 Uri uri = Contacts.CONTENT_URI.buildUpon().appendQueryParameter( 1440 ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT)) 1441 .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, 1442 String.valueOf(MAX_PHOTOS_TO_PRELOAD)) 1443 .build(); 1444 cursor = mResolver.query(uri, new String[] { Contacts.PHOTO_ID }, 1445 Contacts.PHOTO_ID + " NOT NULL AND " + Contacts.PHOTO_ID + "!=0", 1446 null, 1447 Contacts.STARRED + " DESC, " + Contacts.LAST_TIME_CONTACTED + " DESC"); 1448 1449 if (cursor != null) { 1450 while (cursor.moveToNext()) { 1451 // Insert them in reverse order, because we will be taking 1452 // them from the end of the list for loading. 1453 mPreloadPhotoIds.add(0, cursor.getLong(0)); 1454 } 1455 } 1456 } finally { 1457 if (cursor != null) { 1458 cursor.close(); 1459 } 1460 } 1461 } 1462 loadPhotosInBackground()1463 private void loadPhotosInBackground() { 1464 if (!PermissionsUtil.hasPermission(mContext, 1465 android.Manifest.permission.READ_CONTACTS)) { 1466 return; 1467 } 1468 obtainPhotoIdsAndUrisToLoad(mPhotoIds, mPhotoIdsAsStrings, mPhotoUris); 1469 loadThumbnails(false); 1470 loadUriBasedPhotos(); 1471 requestPreloading(); 1472 } 1473 1474 /** Loads thumbnail photos with ids */ loadThumbnails(boolean preloading)1475 private void loadThumbnails(boolean preloading) { 1476 if (mPhotoIds.isEmpty()) { 1477 return; 1478 } 1479 1480 // Remove loaded photos from the preload queue: we don't want 1481 // the preloading process to load them again. 1482 if (!preloading && mPreloadStatus == PRELOAD_STATUS_IN_PROGRESS) { 1483 for (Long id : mPhotoIds) { 1484 mPreloadPhotoIds.remove(id); 1485 } 1486 if (mPreloadPhotoIds.isEmpty()) { 1487 mPreloadStatus = PRELOAD_STATUS_DONE; 1488 } 1489 } 1490 1491 mStringBuilder.setLength(0); 1492 mStringBuilder.append(Photo._ID + " IN("); 1493 for (int i = 0; i < mPhotoIds.size(); i++) { 1494 if (i != 0) { 1495 mStringBuilder.append(','); 1496 } 1497 mStringBuilder.append('?'); 1498 } 1499 mStringBuilder.append(')'); 1500 1501 Cursor cursor = null; 1502 try { 1503 if (DEBUG) Log.d(TAG, "Loading " + TextUtils.join(",", mPhotoIdsAsStrings)); 1504 cursor = mResolver.query(Data.CONTENT_URI, 1505 COLUMNS, 1506 mStringBuilder.toString(), 1507 mPhotoIdsAsStrings.toArray(EMPTY_STRING_ARRAY), 1508 null); 1509 1510 if (cursor != null) { 1511 while (cursor.moveToNext()) { 1512 Long id = cursor.getLong(0); 1513 byte[] bytes = cursor.getBlob(1); 1514 if (bytes == null) { 1515 bytes = new byte[0]; 1516 } 1517 cacheBitmap(id, bytes, preloading, -1); 1518 mPhotoIds.remove(id); 1519 } 1520 } 1521 } finally { 1522 if (cursor != null) { 1523 cursor.close(); 1524 } 1525 } 1526 1527 // Remaining photos were not found in the contacts database (but might be in profile). 1528 for (Long id : mPhotoIds) { 1529 if (ContactsContract.isProfileId(id)) { 1530 Cursor profileCursor = null; 1531 try { 1532 profileCursor = mResolver.query( 1533 ContentUris.withAppendedId(Data.CONTENT_URI, id), 1534 COLUMNS, null, null, null); 1535 if (profileCursor != null && profileCursor.moveToFirst()) { 1536 byte[] bytes = profileCursor.getBlob(1); 1537 if (bytes == null) { 1538 bytes = new byte[0]; 1539 } 1540 cacheBitmap(profileCursor.getLong(0), bytes, preloading, -1); 1541 } else { 1542 // Couldn't load a photo this way either. 1543 cacheBitmap(id, null, preloading, -1); 1544 } 1545 } finally { 1546 if (profileCursor != null) { 1547 profileCursor.close(); 1548 } 1549 } 1550 } else { 1551 // Not a profile photo and not found - mark the cache accordingly 1552 cacheBitmap(id, null, preloading, -1); 1553 } 1554 } 1555 1556 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED); 1557 } 1558 1559 /** 1560 * Loads photos referenced with Uris. Those can be remote thumbnails 1561 * (from directory searches), display photos etc 1562 */ loadUriBasedPhotos()1563 private void loadUriBasedPhotos() { 1564 for (Request uriRequest : mPhotoUris) { 1565 // Keep the original URI and use this to key into the cache. Failure to do so will 1566 // result in an image being continually reloaded into cache if the original URI 1567 // has a contact type encodedFragment (eg nearby places business photo URLs). 1568 Uri originalUri = uriRequest.getUri(); 1569 1570 // Strip off the "contact type" we added to the URI to ensure it was identifiable as 1571 // a business photo -- there is no need to pass this on to the server. 1572 Uri uri = ContactPhotoManager.removeContactType(originalUri); 1573 1574 if (mBuffer == null) { 1575 mBuffer = new byte[BUFFER_SIZE]; 1576 } 1577 try { 1578 if (DEBUG) Log.d(TAG, "Loading " + uri); 1579 final String scheme = uri.getScheme(); 1580 InputStream is = null; 1581 if (scheme.equals("http") || scheme.equals("https")) { 1582 TrafficStats.setThreadStatsTag(TrafficStatsTags.CONTACT_PHOTO_DOWNLOAD_TAG); 1583 final HttpURLConnection connection = 1584 (HttpURLConnection) new URL(uri.toString()).openConnection(); 1585 1586 // Include the user agent if it is specified. 1587 if (!TextUtils.isEmpty(mUserAgent)) { 1588 connection.setRequestProperty("User-Agent", mUserAgent); 1589 } 1590 try { 1591 is = connection.getInputStream(); 1592 } catch (IOException e) { 1593 connection.disconnect(); 1594 is = null; 1595 } 1596 TrafficStats.clearThreadStatsTag(); 1597 } else { 1598 is = mResolver.openInputStream(uri); 1599 } 1600 if (is != null) { 1601 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 1602 try { 1603 int size; 1604 while ((size = is.read(mBuffer)) != -1) { 1605 baos.write(mBuffer, 0, size); 1606 } 1607 } finally { 1608 is.close(); 1609 } 1610 cacheBitmap(originalUri, baos.toByteArray(), false, 1611 uriRequest.getRequestedExtent()); 1612 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED); 1613 } else { 1614 if (Log.isLoggable(TAG, Log.VERBOSE)) { 1615 Log.v(TAG, "Cannot load photo " + uri); 1616 } 1617 cacheBitmap(originalUri, null, false, uriRequest.getRequestedExtent()); 1618 } 1619 } catch (final Exception | OutOfMemoryError ex) { 1620 if (Log.isLoggable(TAG, Log.VERBOSE)) { 1621 Log.v(TAG, "Cannot load photo " + uri, ex); 1622 } 1623 cacheBitmap(originalUri, null, false, uriRequest.getRequestedExtent()); 1624 } 1625 } 1626 } 1627 } 1628 1629 /** 1630 * A holder for either a Uri or an id and a flag whether this was requested for the dark or 1631 * light theme 1632 */ 1633 private static final class Request { 1634 private final long mId; 1635 private final Uri mUri; 1636 private final boolean mDarkTheme; 1637 private final int mRequestedExtent; 1638 private final DefaultImageProvider mDefaultProvider; 1639 private final DefaultImageRequest mDefaultRequest; 1640 /** 1641 * Whether or not the contact photo is to be displayed as a circle 1642 */ 1643 private final boolean mIsCircular; 1644 Request(long id, Uri uri, int requestedExtent, boolean darkTheme, boolean isCircular, DefaultImageProvider defaultProvider, DefaultImageRequest defaultRequest)1645 private Request(long id, Uri uri, int requestedExtent, boolean darkTheme, 1646 boolean isCircular, DefaultImageProvider defaultProvider, 1647 DefaultImageRequest defaultRequest) { 1648 mId = id; 1649 mUri = uri; 1650 mDarkTheme = darkTheme; 1651 mIsCircular = isCircular; 1652 mRequestedExtent = requestedExtent; 1653 mDefaultProvider = defaultProvider; 1654 mDefaultRequest = defaultRequest; 1655 } 1656 createFromThumbnailId(long id, boolean darkTheme, boolean isCircular, DefaultImageProvider defaultProvider, DefaultImageRequest defaultRequest)1657 public static Request createFromThumbnailId(long id, boolean darkTheme, boolean isCircular, 1658 DefaultImageProvider defaultProvider, DefaultImageRequest defaultRequest) { 1659 return new Request(id, null /* no URI */, -1, darkTheme, isCircular, defaultProvider, 1660 defaultRequest); 1661 } 1662 createFromUri(Uri uri, int requestedExtent, boolean darkTheme, boolean isCircular, DefaultImageProvider defaultProvider)1663 public static Request createFromUri(Uri uri, int requestedExtent, boolean darkTheme, 1664 boolean isCircular, DefaultImageProvider defaultProvider) { 1665 return createFromUri(uri, requestedExtent, darkTheme, isCircular, defaultProvider, 1666 /* defaultRequest */ null); 1667 } 1668 createFromUri(Uri uri, int requestedExtent, boolean darkTheme, boolean isCircular, DefaultImageProvider defaultProvider, DefaultImageRequest defaultRequest)1669 public static Request createFromUri(Uri uri, int requestedExtent, boolean darkTheme, 1670 boolean isCircular, DefaultImageProvider defaultProvider, 1671 DefaultImageRequest defaultRequest) { 1672 return new Request(0 /* no ID */, uri, requestedExtent, darkTheme, isCircular, 1673 defaultProvider, defaultRequest); 1674 } 1675 isUriRequest()1676 public boolean isUriRequest() { 1677 return mUri != null; 1678 } 1679 getUri()1680 public Uri getUri() { 1681 return mUri; 1682 } 1683 getId()1684 public long getId() { 1685 return mId; 1686 } 1687 getRequestedExtent()1688 public int getRequestedExtent() { 1689 return mRequestedExtent; 1690 } 1691 1692 @Override hashCode()1693 public int hashCode() { 1694 final int prime = 31; 1695 int result = 1; 1696 result = prime * result + (int) (mId ^ (mId >>> 32)); 1697 result = prime * result + mRequestedExtent; 1698 result = prime * result + ((mUri == null) ? 0 : mUri.hashCode()); 1699 return result; 1700 } 1701 1702 @Override equals(Object obj)1703 public boolean equals(Object obj) { 1704 if (this == obj) return true; 1705 if (obj == null) return false; 1706 if (getClass() != obj.getClass()) return false; 1707 final Request that = (Request) obj; 1708 if (mId != that.mId) return false; 1709 if (mRequestedExtent != that.mRequestedExtent) return false; 1710 if (!UriUtils.areEqual(mUri, that.mUri)) return false; 1711 // Don't compare equality of mDarkTheme because it is only used in the default contact 1712 // photo case. When the contact does have a photo, the contact photo is the same 1713 // regardless of mDarkTheme, so we shouldn't need to put the photo request on the queue 1714 // twice. 1715 return true; 1716 } 1717 getKey()1718 public Object getKey() { 1719 return mUri == null ? mId : mUri; 1720 } 1721 1722 /** 1723 * Applies the default image to the current view. If the request is URI-based, looks for 1724 * the contact type encoded fragment to determine if this is a request for a business photo, 1725 * in which case we will load the default business photo. 1726 * 1727 * @param view The current image view to apply the image to. 1728 * @param isCircular Whether the image is circular or not. 1729 */ applyDefaultImage(ImageView view, boolean isCircular)1730 public void applyDefaultImage(ImageView view, boolean isCircular) { 1731 final DefaultImageRequest request; 1732 1733 if (mDefaultRequest == null) { 1734 if (isCircular) { 1735 request = ContactPhotoManager.isBusinessContactUri(mUri) 1736 ? DefaultImageRequest.EMPTY_CIRCULAR_BUSINESS_IMAGE_REQUEST 1737 : DefaultImageRequest.EMPTY_CIRCULAR_DEFAULT_IMAGE_REQUEST; 1738 } else { 1739 request = ContactPhotoManager.isBusinessContactUri(mUri) 1740 ? DefaultImageRequest.EMPTY_DEFAULT_BUSINESS_IMAGE_REQUEST 1741 : DefaultImageRequest.EMPTY_DEFAULT_IMAGE_REQUEST; 1742 } 1743 } else { 1744 request = mDefaultRequest; 1745 } 1746 mDefaultProvider.applyDefaultImage(view, mRequestedExtent, mDarkTheme, request); 1747 } 1748 } 1749 } 1750