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