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