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.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 android.support.v4.graphics.drawable.RoundedBitmapDrawable; 50 import android.support.v4.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.common.lettertiles.LetterTileDrawable; 59 import com.android.contacts.common.util.BitmapUtil; 60 import com.android.contacts.common.util.PermissionsUtil; 61 import com.android.contacts.common.util.TrafficStatsTags; 62 import com.android.contacts.common.util.UriUtils; 63 import com.android.contacts.commonbind.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)); 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)); 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) { 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 */ cacheBitmap(Object key, byte[] bytes, boolean preloading, int requestedExtent)1182 private void cacheBitmap(Object key, byte[] bytes, boolean preloading, int requestedExtent) { 1183 if (DEBUG) { 1184 BitmapHolder prev = mBitmapHolderCache.get(key); 1185 if (prev != null && prev.bytes != null) { 1186 Log.d(TAG, "Overwriting cache: key=" + key + (prev.fresh ? " FRESH" : " stale")); 1187 if (prev.fresh) { 1188 mFreshCacheOverwrite.incrementAndGet(); 1189 } else { 1190 mStaleCacheOverwrite.incrementAndGet(); 1191 } 1192 } 1193 Log.d(TAG, "Caching data: key=" + key + ", " + 1194 (bytes == null ? "<null>" : btk(bytes.length))); 1195 } 1196 BitmapHolder holder = new BitmapHolder(bytes, 1197 bytes == null ? -1 : BitmapUtil.getSmallerExtentFromBytes(bytes)); 1198 1199 // Unless this image is being preloaded, decode it right away while 1200 // we are still on the background thread. 1201 if (!preloading) { 1202 inflateBitmap(holder, requestedExtent); 1203 } 1204 1205 if (bytes != null) { 1206 mBitmapHolderCache.put(key, holder); 1207 if (mBitmapHolderCache.get(key) != holder) { 1208 Log.w(TAG, "Bitmap too big to fit in cache."); 1209 mBitmapHolderCache.put(key, BITMAP_UNAVAILABLE); 1210 } 1211 } else { 1212 mBitmapHolderCache.put(key, BITMAP_UNAVAILABLE); 1213 } 1214 1215 mBitmapHolderCacheAllUnfresh = false; 1216 } 1217 1218 @Override cacheBitmap(Uri photoUri, Bitmap bitmap, byte[] photoBytes)1219 public void cacheBitmap(Uri photoUri, Bitmap bitmap, byte[] photoBytes) { 1220 final int smallerExtent = Math.min(bitmap.getWidth(), bitmap.getHeight()); 1221 // We can pretend here that the extent of the photo was the size that we originally 1222 // requested 1223 Request request = Request.createFromUri(photoUri, smallerExtent, false /* darkTheme */, 1224 false /* isCircular */ , DEFAULT_AVATAR); 1225 BitmapHolder holder = new BitmapHolder(photoBytes, smallerExtent); 1226 holder.bitmapRef = new SoftReference<Bitmap>(bitmap); 1227 mBitmapHolderCache.put(request.getKey(), holder); 1228 mBitmapHolderCacheAllUnfresh = false; 1229 mBitmapCache.put(request.getKey(), bitmap); 1230 } 1231 1232 /** 1233 * Populates an array of photo IDs that need to be loaded. Also decodes bitmaps that we have 1234 * already loaded 1235 */ obtainPhotoIdsAndUrisToLoad(Set<Long> photoIds, Set<String> photoIdsAsStrings, Set<Request> uris)1236 private void obtainPhotoIdsAndUrisToLoad(Set<Long> photoIds, 1237 Set<String> photoIdsAsStrings, Set<Request> uris) { 1238 photoIds.clear(); 1239 photoIdsAsStrings.clear(); 1240 uris.clear(); 1241 1242 boolean jpegsDecoded = false; 1243 1244 /* 1245 * Since the call is made from the loader thread, the map could be 1246 * changing during the iteration. That's not really a problem: 1247 * ConcurrentHashMap will allow those changes to happen without throwing 1248 * exceptions. Since we may miss some requests in the situation of 1249 * concurrent change, we will need to check the map again once loading 1250 * is complete. 1251 */ 1252 Iterator<Request> iterator = mPendingRequests.values().iterator(); 1253 while (iterator.hasNext()) { 1254 Request request = iterator.next(); 1255 final BitmapHolder holder = mBitmapHolderCache.get(request.getKey()); 1256 if (holder == BITMAP_UNAVAILABLE) { 1257 continue; 1258 } 1259 if (holder != null && holder.bytes != null && holder.fresh && 1260 (holder.bitmapRef == null || holder.bitmapRef.get() == null)) { 1261 // This was previously loaded but we don't currently have the inflated Bitmap 1262 inflateBitmap(holder, request.getRequestedExtent()); 1263 jpegsDecoded = true; 1264 } else { 1265 if (holder == null || !holder.fresh) { 1266 if (request.isUriRequest()) { 1267 uris.add(request); 1268 } else { 1269 photoIds.add(request.getId()); 1270 photoIdsAsStrings.add(String.valueOf(request.mId)); 1271 } 1272 } 1273 } 1274 } 1275 1276 if (jpegsDecoded) mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED); 1277 } 1278 1279 /** 1280 * The thread that performs loading of photos from the database. 1281 */ 1282 private class LoaderThread extends HandlerThread implements Callback { 1283 private static final int BUFFER_SIZE = 1024*16; 1284 private static final int MESSAGE_PRELOAD_PHOTOS = 0; 1285 private static final int MESSAGE_LOAD_PHOTOS = 1; 1286 1287 /** 1288 * A pause between preload batches that yields to the UI thread. 1289 */ 1290 private static final int PHOTO_PRELOAD_DELAY = 1000; 1291 1292 /** 1293 * Number of photos to preload per batch. 1294 */ 1295 private static final int PRELOAD_BATCH = 25; 1296 1297 /** 1298 * Maximum number of photos to preload. If the cache size is 2Mb and 1299 * the expected average size of a photo is 4kb, then this number should be 2Mb/4kb = 500. 1300 */ 1301 private static final int MAX_PHOTOS_TO_PRELOAD = 100; 1302 1303 private final ContentResolver mResolver; 1304 private final StringBuilder mStringBuilder = new StringBuilder(); 1305 private final Set<Long> mPhotoIds = Sets.newHashSet(); 1306 private final Set<String> mPhotoIdsAsStrings = Sets.newHashSet(); 1307 private final Set<Request> mPhotoUris = Sets.newHashSet(); 1308 private final List<Long> mPreloadPhotoIds = Lists.newArrayList(); 1309 1310 private Handler mLoaderThreadHandler; 1311 private byte mBuffer[]; 1312 1313 private static final int PRELOAD_STATUS_NOT_STARTED = 0; 1314 private static final int PRELOAD_STATUS_IN_PROGRESS = 1; 1315 private static final int PRELOAD_STATUS_DONE = 2; 1316 1317 private int mPreloadStatus = PRELOAD_STATUS_NOT_STARTED; 1318 LoaderThread(ContentResolver resolver)1319 public LoaderThread(ContentResolver resolver) { 1320 super(LOADER_THREAD_NAME); 1321 mResolver = resolver; 1322 } 1323 ensureHandler()1324 public void ensureHandler() { 1325 if (mLoaderThreadHandler == null) { 1326 mLoaderThreadHandler = new Handler(getLooper(), this); 1327 } 1328 } 1329 1330 /** 1331 * Kicks off preloading of the next batch of photos on the background thread. 1332 * Preloading will happen after a delay: we want to yield to the UI thread 1333 * as much as possible. 1334 * <p> 1335 * If preloading is already complete, does nothing. 1336 */ requestPreloading()1337 public void requestPreloading() { 1338 if (mPreloadStatus == PRELOAD_STATUS_DONE) { 1339 return; 1340 } 1341 1342 ensureHandler(); 1343 if (mLoaderThreadHandler.hasMessages(MESSAGE_LOAD_PHOTOS)) { 1344 return; 1345 } 1346 1347 mLoaderThreadHandler.sendEmptyMessageDelayed( 1348 MESSAGE_PRELOAD_PHOTOS, PHOTO_PRELOAD_DELAY); 1349 } 1350 1351 /** 1352 * Sends a message to this thread to load requested photos. Cancels a preloading 1353 * request, if any: we don't want preloading to impede loading of the photos 1354 * we need to display now. 1355 */ requestLoading()1356 public void requestLoading() { 1357 ensureHandler(); 1358 mLoaderThreadHandler.removeMessages(MESSAGE_PRELOAD_PHOTOS); 1359 mLoaderThreadHandler.sendEmptyMessage(MESSAGE_LOAD_PHOTOS); 1360 } 1361 1362 /** 1363 * Receives the above message, loads photos and then sends a message 1364 * to the main thread to process them. 1365 */ 1366 @Override handleMessage(Message msg)1367 public boolean handleMessage(Message msg) { 1368 switch (msg.what) { 1369 case MESSAGE_PRELOAD_PHOTOS: 1370 preloadPhotosInBackground(); 1371 break; 1372 case MESSAGE_LOAD_PHOTOS: 1373 loadPhotosInBackground(); 1374 break; 1375 } 1376 return true; 1377 } 1378 1379 /** 1380 * The first time it is called, figures out which photos need to be preloaded. 1381 * Each subsequent call preloads the next batch of photos and requests 1382 * another cycle of preloading after a delay. The whole process ends when 1383 * we either run out of photos to preload or fill up cache. 1384 */ preloadPhotosInBackground()1385 private void preloadPhotosInBackground() { 1386 if (mPreloadStatus == PRELOAD_STATUS_DONE) { 1387 return; 1388 } 1389 1390 if (mPreloadStatus == PRELOAD_STATUS_NOT_STARTED) { 1391 queryPhotosForPreload(); 1392 if (mPreloadPhotoIds.isEmpty()) { 1393 mPreloadStatus = PRELOAD_STATUS_DONE; 1394 } else { 1395 mPreloadStatus = PRELOAD_STATUS_IN_PROGRESS; 1396 } 1397 requestPreloading(); 1398 return; 1399 } 1400 1401 if (mBitmapHolderCache.size() > mBitmapHolderCacheRedZoneBytes) { 1402 mPreloadStatus = PRELOAD_STATUS_DONE; 1403 return; 1404 } 1405 1406 mPhotoIds.clear(); 1407 mPhotoIdsAsStrings.clear(); 1408 1409 int count = 0; 1410 int preloadSize = mPreloadPhotoIds.size(); 1411 while(preloadSize > 0 && mPhotoIds.size() < PRELOAD_BATCH) { 1412 preloadSize--; 1413 count++; 1414 Long photoId = mPreloadPhotoIds.get(preloadSize); 1415 mPhotoIds.add(photoId); 1416 mPhotoIdsAsStrings.add(photoId.toString()); 1417 mPreloadPhotoIds.remove(preloadSize); 1418 } 1419 1420 loadThumbnails(true); 1421 1422 if (preloadSize == 0) { 1423 mPreloadStatus = PRELOAD_STATUS_DONE; 1424 } 1425 1426 Log.v(TAG, "Preloaded " + count + " photos. Cached bytes: " 1427 + mBitmapHolderCache.size()); 1428 1429 requestPreloading(); 1430 } 1431 queryPhotosForPreload()1432 private void queryPhotosForPreload() { 1433 Cursor cursor = null; 1434 try { 1435 Uri uri = Contacts.CONTENT_URI.buildUpon().appendQueryParameter( 1436 ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT)) 1437 .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, 1438 String.valueOf(MAX_PHOTOS_TO_PRELOAD)) 1439 .build(); 1440 cursor = mResolver.query(uri, new String[] { Contacts.PHOTO_ID }, 1441 Contacts.PHOTO_ID + " NOT NULL AND " + Contacts.PHOTO_ID + "!=0", 1442 null, 1443 Contacts.STARRED + " DESC, " + Contacts.LAST_TIME_CONTACTED + " DESC"); 1444 1445 if (cursor != null) { 1446 while (cursor.moveToNext()) { 1447 // Insert them in reverse order, because we will be taking 1448 // them from the end of the list for loading. 1449 mPreloadPhotoIds.add(0, cursor.getLong(0)); 1450 } 1451 } 1452 } finally { 1453 if (cursor != null) { 1454 cursor.close(); 1455 } 1456 } 1457 } 1458 loadPhotosInBackground()1459 private void loadPhotosInBackground() { 1460 if (!PermissionsUtil.hasPermission(mContext, 1461 android.Manifest.permission.READ_CONTACTS)) { 1462 return; 1463 } 1464 obtainPhotoIdsAndUrisToLoad(mPhotoIds, mPhotoIdsAsStrings, mPhotoUris); 1465 loadThumbnails(false); 1466 loadUriBasedPhotos(); 1467 requestPreloading(); 1468 } 1469 1470 /** Loads thumbnail photos with ids */ loadThumbnails(boolean preloading)1471 private void loadThumbnails(boolean preloading) { 1472 if (mPhotoIds.isEmpty()) { 1473 return; 1474 } 1475 1476 // Remove loaded photos from the preload queue: we don't want 1477 // the preloading process to load them again. 1478 if (!preloading && mPreloadStatus == PRELOAD_STATUS_IN_PROGRESS) { 1479 for (Long id : mPhotoIds) { 1480 mPreloadPhotoIds.remove(id); 1481 } 1482 if (mPreloadPhotoIds.isEmpty()) { 1483 mPreloadStatus = PRELOAD_STATUS_DONE; 1484 } 1485 } 1486 1487 mStringBuilder.setLength(0); 1488 mStringBuilder.append(Photo._ID + " IN("); 1489 for (int i = 0; i < mPhotoIds.size(); i++) { 1490 if (i != 0) { 1491 mStringBuilder.append(','); 1492 } 1493 mStringBuilder.append('?'); 1494 } 1495 mStringBuilder.append(')'); 1496 1497 Cursor cursor = null; 1498 try { 1499 if (DEBUG) Log.d(TAG, "Loading " + TextUtils.join(",", mPhotoIdsAsStrings)); 1500 cursor = mResolver.query(Data.CONTENT_URI, 1501 COLUMNS, 1502 mStringBuilder.toString(), 1503 mPhotoIdsAsStrings.toArray(EMPTY_STRING_ARRAY), 1504 null); 1505 1506 if (cursor != null) { 1507 while (cursor.moveToNext()) { 1508 Long id = cursor.getLong(0); 1509 byte[] bytes = cursor.getBlob(1); 1510 cacheBitmap(id, bytes, preloading, -1); 1511 mPhotoIds.remove(id); 1512 } 1513 } 1514 } finally { 1515 if (cursor != null) { 1516 cursor.close(); 1517 } 1518 } 1519 1520 // Remaining photos were not found in the contacts database (but might be in profile). 1521 for (Long id : mPhotoIds) { 1522 if (ContactsContract.isProfileId(id)) { 1523 Cursor profileCursor = null; 1524 try { 1525 profileCursor = mResolver.query( 1526 ContentUris.withAppendedId(Data.CONTENT_URI, id), 1527 COLUMNS, null, null, null); 1528 if (profileCursor != null && profileCursor.moveToFirst()) { 1529 cacheBitmap(profileCursor.getLong(0), profileCursor.getBlob(1), 1530 preloading, -1); 1531 } else { 1532 // Couldn't load a photo this way either. 1533 cacheBitmap(id, null, preloading, -1); 1534 } 1535 } finally { 1536 if (profileCursor != null) { 1537 profileCursor.close(); 1538 } 1539 } 1540 } else { 1541 // Not a profile photo and not found - mark the cache accordingly 1542 cacheBitmap(id, null, preloading, -1); 1543 } 1544 } 1545 1546 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED); 1547 } 1548 1549 /** 1550 * Loads photos referenced with Uris. Those can be remote thumbnails 1551 * (from directory searches), display photos etc 1552 */ loadUriBasedPhotos()1553 private void loadUriBasedPhotos() { 1554 for (Request uriRequest : mPhotoUris) { 1555 // Keep the original URI and use this to key into the cache. Failure to do so will 1556 // result in an image being continually reloaded into cache if the original URI 1557 // has a contact type encodedFragment (eg nearby places business photo URLs). 1558 Uri originalUri = uriRequest.getUri(); 1559 1560 // Strip off the "contact type" we added to the URI to ensure it was identifiable as 1561 // a business photo -- there is no need to pass this on to the server. 1562 Uri uri = ContactPhotoManager.removeContactType(originalUri); 1563 1564 if (mBuffer == null) { 1565 mBuffer = new byte[BUFFER_SIZE]; 1566 } 1567 try { 1568 if (DEBUG) Log.d(TAG, "Loading " + uri); 1569 final String scheme = uri.getScheme(); 1570 InputStream is = null; 1571 if (scheme.equals("http") || scheme.equals("https")) { 1572 TrafficStats.setThreadStatsTag(TrafficStatsTags.CONTACT_PHOTO_DOWNLOAD_TAG); 1573 final HttpURLConnection connection = 1574 (HttpURLConnection) new URL(uri.toString()).openConnection(); 1575 1576 // Include the user agent if it is specified. 1577 if (!TextUtils.isEmpty(mUserAgent)) { 1578 connection.setRequestProperty("User-Agent", mUserAgent); 1579 } 1580 try { 1581 is = connection.getInputStream(); 1582 } catch (IOException e) { 1583 connection.disconnect(); 1584 is = null; 1585 } 1586 TrafficStats.clearThreadStatsTag(); 1587 } else { 1588 is = mResolver.openInputStream(uri); 1589 } 1590 if (is != null) { 1591 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 1592 try { 1593 int size; 1594 while ((size = is.read(mBuffer)) != -1) { 1595 baos.write(mBuffer, 0, size); 1596 } 1597 } finally { 1598 is.close(); 1599 } 1600 cacheBitmap(originalUri, baos.toByteArray(), false, 1601 uriRequest.getRequestedExtent()); 1602 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED); 1603 } else { 1604 Log.v(TAG, "Cannot load photo " + uri); 1605 cacheBitmap(originalUri, null, false, uriRequest.getRequestedExtent()); 1606 } 1607 } catch (final Exception | OutOfMemoryError ex) { 1608 Log.v(TAG, "Cannot load photo " + uri, ex); 1609 cacheBitmap(originalUri, null, false, uriRequest.getRequestedExtent()); 1610 } 1611 } 1612 } 1613 } 1614 1615 /** 1616 * A holder for either a Uri or an id and a flag whether this was requested for the dark or 1617 * light theme 1618 */ 1619 private static final class Request { 1620 private final long mId; 1621 private final Uri mUri; 1622 private final boolean mDarkTheme; 1623 private final int mRequestedExtent; 1624 private final DefaultImageProvider mDefaultProvider; 1625 /** 1626 * Whether or not the contact photo is to be displayed as a circle 1627 */ 1628 private final boolean mIsCircular; 1629 Request(long id, Uri uri, int requestedExtent, boolean darkTheme, boolean isCircular, DefaultImageProvider defaultProvider)1630 private Request(long id, Uri uri, int requestedExtent, boolean darkTheme, 1631 boolean isCircular, DefaultImageProvider defaultProvider) { 1632 mId = id; 1633 mUri = uri; 1634 mDarkTheme = darkTheme; 1635 mIsCircular = isCircular; 1636 mRequestedExtent = requestedExtent; 1637 mDefaultProvider = defaultProvider; 1638 } 1639 createFromThumbnailId(long id, boolean darkTheme, boolean isCircular, DefaultImageProvider defaultProvider)1640 public static Request createFromThumbnailId(long id, boolean darkTheme, boolean isCircular, 1641 DefaultImageProvider defaultProvider) { 1642 return new Request(id, null /* no URI */, -1, darkTheme, isCircular, defaultProvider); 1643 } 1644 createFromUri(Uri uri, int requestedExtent, boolean darkTheme, boolean isCircular, DefaultImageProvider defaultProvider)1645 public static Request createFromUri(Uri uri, int requestedExtent, boolean darkTheme, 1646 boolean isCircular, DefaultImageProvider defaultProvider) { 1647 return new Request(0 /* no ID */, uri, requestedExtent, darkTheme, isCircular, 1648 defaultProvider); 1649 } 1650 isUriRequest()1651 public boolean isUriRequest() { 1652 return mUri != null; 1653 } 1654 getUri()1655 public Uri getUri() { 1656 return mUri; 1657 } 1658 getId()1659 public long getId() { 1660 return mId; 1661 } 1662 getRequestedExtent()1663 public int getRequestedExtent() { 1664 return mRequestedExtent; 1665 } 1666 1667 @Override hashCode()1668 public int hashCode() { 1669 final int prime = 31; 1670 int result = 1; 1671 result = prime * result + (int) (mId ^ (mId >>> 32)); 1672 result = prime * result + mRequestedExtent; 1673 result = prime * result + ((mUri == null) ? 0 : mUri.hashCode()); 1674 return result; 1675 } 1676 1677 @Override equals(Object obj)1678 public boolean equals(Object obj) { 1679 if (this == obj) return true; 1680 if (obj == null) return false; 1681 if (getClass() != obj.getClass()) return false; 1682 final Request that = (Request) obj; 1683 if (mId != that.mId) return false; 1684 if (mRequestedExtent != that.mRequestedExtent) return false; 1685 if (!UriUtils.areEqual(mUri, that.mUri)) return false; 1686 // Don't compare equality of mDarkTheme because it is only used in the default contact 1687 // photo case. When the contact does have a photo, the contact photo is the same 1688 // regardless of mDarkTheme, so we shouldn't need to put the photo request on the queue 1689 // twice. 1690 return true; 1691 } 1692 getKey()1693 public Object getKey() { 1694 return mUri == null ? mId : mUri; 1695 } 1696 1697 /** 1698 * Applies the default image to the current view. If the request is URI-based, looks for 1699 * the contact type encoded fragment to determine if this is a request for a business photo, 1700 * in which case we will load the default business photo. 1701 * 1702 * @param view The current image view to apply the image to. 1703 * @param isCircular Whether the image is circular or not. 1704 */ applyDefaultImage(ImageView view, boolean isCircular)1705 public void applyDefaultImage(ImageView view, boolean isCircular) { 1706 final DefaultImageRequest request; 1707 1708 if (isCircular) { 1709 request = ContactPhotoManager.isBusinessContactUri(mUri) 1710 ? DefaultImageRequest.EMPTY_CIRCULAR_BUSINESS_IMAGE_REQUEST 1711 : DefaultImageRequest.EMPTY_CIRCULAR_DEFAULT_IMAGE_REQUEST; 1712 } else { 1713 request = ContactPhotoManager.isBusinessContactUri(mUri) 1714 ? DefaultImageRequest.EMPTY_DEFAULT_BUSINESS_IMAGE_REQUEST 1715 : DefaultImageRequest.EMPTY_DEFAULT_IMAGE_REQUEST; 1716 } 1717 mDefaultProvider.applyDefaultImage(view, mRequestedExtent, mDarkTheme, request); 1718 } 1719 } 1720 } 1721