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 17package com.android.contacts; 18 19import com.android.contacts.model.AccountTypeManager; 20import com.android.contacts.util.MemoryUtils; 21import com.android.contacts.util.UriUtils; 22import com.google.android.collect.Lists; 23import com.google.android.collect.Sets; 24 25import android.content.ComponentCallbacks2; 26import android.content.ContentResolver; 27import android.content.ContentUris; 28import android.content.Context; 29import android.content.res.Configuration; 30import android.database.Cursor; 31import android.graphics.Bitmap; 32import android.graphics.BitmapFactory; 33import android.graphics.drawable.ColorDrawable; 34import android.graphics.drawable.Drawable; 35import android.net.Uri; 36import android.os.Handler; 37import android.os.Handler.Callback; 38import android.os.HandlerThread; 39import android.os.Message; 40import android.provider.ContactsContract; 41import android.provider.ContactsContract.Contacts; 42import android.provider.ContactsContract.Contacts.Photo; 43import android.provider.ContactsContract.Data; 44import android.provider.ContactsContract.Directory; 45import android.text.TextUtils; 46import android.util.Log; 47import android.util.LruCache; 48import android.widget.ImageView; 49 50import java.io.ByteArrayOutputStream; 51import java.io.InputStream; 52import java.lang.ref.Reference; 53import java.lang.ref.SoftReference; 54import java.util.Iterator; 55import java.util.List; 56import java.util.Set; 57import java.util.concurrent.ConcurrentHashMap; 58import java.util.concurrent.atomic.AtomicInteger; 59 60/** 61 * Asynchronously loads contact photos and maintains a cache of photos. 62 */ 63public abstract class ContactPhotoManager implements ComponentCallbacks2 { 64 static final String TAG = "ContactPhotoManager"; 65 static final boolean DEBUG = false; // Don't submit with true 66 67 public static final String CONTACT_PHOTO_SERVICE = "contactPhotos"; 68 69 public static int getDefaultAvatarResId(boolean hires, boolean darkTheme) { 70 if (hires && darkTheme) return R.drawable.ic_contact_picture_180_holo_dark; 71 if (hires) return R.drawable.ic_contact_picture_180_holo_light; 72 if (darkTheme) return R.drawable.ic_contact_picture_holo_dark; 73 return R.drawable.ic_contact_picture_holo_light; 74 } 75 76 public static abstract class DefaultImageProvider { 77 public abstract void applyDefaultImage(ImageView view, boolean hires, boolean darkTheme); 78 } 79 80 private static class AvatarDefaultImageProvider extends DefaultImageProvider { 81 @Override 82 public void applyDefaultImage(ImageView view, boolean hires, boolean darkTheme) { 83 view.setImageResource(getDefaultAvatarResId(hires, darkTheme)); 84 } 85 } 86 87 private static class BlankDefaultImageProvider extends DefaultImageProvider { 88 private static Drawable sDrawable; 89 90 @Override 91 public void applyDefaultImage(ImageView view, boolean hires, boolean darkTheme) { 92 if (sDrawable == null) { 93 Context context = view.getContext(); 94 sDrawable = new ColorDrawable(context.getResources().getColor( 95 R.color.image_placeholder)); 96 } 97 view.setImageDrawable(sDrawable); 98 } 99 } 100 101 public static final DefaultImageProvider DEFAULT_AVATER = new AvatarDefaultImageProvider(); 102 103 public static final DefaultImageProvider DEFAULT_BLANK = new BlankDefaultImageProvider(); 104 105 /** 106 * Requests the singleton instance of {@link AccountTypeManager} with data bound from 107 * the available authenticators. This method can safely be called from the UI thread. 108 */ 109 public static ContactPhotoManager getInstance(Context context) { 110 Context applicationContext = context.getApplicationContext(); 111 ContactPhotoManager service = 112 (ContactPhotoManager) applicationContext.getSystemService(CONTACT_PHOTO_SERVICE); 113 if (service == null) { 114 service = createContactPhotoManager(applicationContext); 115 Log.e(TAG, "No contact photo service in context: " + applicationContext); 116 } 117 return service; 118 } 119 120 public static synchronized ContactPhotoManager createContactPhotoManager(Context context) { 121 return new ContactPhotoManagerImpl(context); 122 } 123 124 /** 125 * Load photo into the supplied image view. If the photo is already cached, 126 * it is displayed immediately. Otherwise a request is sent to load the photo 127 * from the database. 128 */ 129 public abstract void loadPhoto(ImageView view, long photoId, boolean hires, boolean darkTheme, 130 DefaultImageProvider defaultProvider); 131 132 /** 133 * Calls {@link #loadPhoto(ImageView, long, boolean, boolean, DefaultImageProvider)} with 134 * {@link #DEFAULT_AVATER}. 135 */ 136 public final void loadPhoto(ImageView view, long photoId, boolean hires, boolean darkTheme) { 137 loadPhoto(view, photoId, hires, darkTheme, DEFAULT_AVATER); 138 } 139 140 /** 141 * Load photo into the supplied image view. If the photo is already cached, 142 * it is displayed immediately. Otherwise a request is sent to load the photo 143 * from the location specified by the URI. 144 */ 145 public abstract void loadPhoto(ImageView view, Uri photoUri, boolean hires, boolean darkTheme, 146 DefaultImageProvider defaultProvider); 147 148 /** 149 * Calls {@link #loadPhoto(ImageView, Uri, boolean, boolean, DefaultImageProvider)} with 150 * {@link #DEFAULT_AVATER}. 151 */ 152 public final void loadPhoto(ImageView view, Uri photoUri, boolean hires, boolean darkTheme) { 153 loadPhoto(view, photoUri, hires, darkTheme, DEFAULT_AVATER); 154 } 155 156 /** 157 * Remove photo from the supplied image view. This also cancels current pending load request 158 * inside this photo manager. 159 */ 160 public abstract void removePhoto(ImageView view); 161 162 /** 163 * Temporarily stops loading photos from the database. 164 */ 165 public abstract void pause(); 166 167 /** 168 * Resumes loading photos from the database. 169 */ 170 public abstract void resume(); 171 172 /** 173 * Marks all cached photos for reloading. We can continue using cache but should 174 * also make sure the photos haven't changed in the background and notify the views 175 * if so. 176 */ 177 public abstract void refreshCache(); 178 179 /** 180 * Initiates a background process that over time will fill up cache with 181 * preload photos. 182 */ 183 public abstract void preloadPhotosInBackground(); 184 185 // ComponentCallbacks2 186 @Override 187 public void onConfigurationChanged(Configuration newConfig) { 188 } 189 190 // ComponentCallbacks2 191 @Override 192 public void onLowMemory() { 193 } 194 195 // ComponentCallbacks2 196 @Override 197 public void onTrimMemory(int level) { 198 } 199} 200 201class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback { 202 private static final String LOADER_THREAD_NAME = "ContactPhotoLoader"; 203 204 /** 205 * Type of message sent by the UI thread to itself to indicate that some photos 206 * need to be loaded. 207 */ 208 private static final int MESSAGE_REQUEST_LOADING = 1; 209 210 /** 211 * Type of message sent by the loader thread to indicate that some photos have 212 * been loaded. 213 */ 214 private static final int MESSAGE_PHOTOS_LOADED = 2; 215 216 private static final String[] EMPTY_STRING_ARRAY = new String[0]; 217 218 private static final String[] COLUMNS = new String[] { Photo._ID, Photo.PHOTO }; 219 220 /** 221 * Maintains the state of a particular photo. 222 */ 223 private static class BitmapHolder { 224 final byte[] bytes; 225 226 volatile boolean fresh; 227 Bitmap bitmap; 228 Reference<Bitmap> bitmapRef; 229 230 public BitmapHolder(byte[] bytes) { 231 this.bytes = bytes; 232 this.fresh = true; 233 } 234 } 235 236 private final Context mContext; 237 238 /** 239 * An LRU cache for bitmap holders. The cache contains bytes for photos just 240 * as they come from the database. Each holder has a soft reference to the 241 * actual bitmap. 242 */ 243 private final LruCache<Object, BitmapHolder> mBitmapHolderCache; 244 245 /** 246 * Cache size threshold at which bitmaps will not be preloaded. 247 */ 248 private final int mBitmapHolderCacheRedZoneBytes; 249 250 /** 251 * Level 2 LRU cache for bitmaps. This is a smaller cache that holds 252 * the most recently used bitmaps to save time on decoding 253 * them from bytes (the bytes are stored in {@link #mBitmapHolderCache}. 254 */ 255 private final LruCache<Object, Bitmap> mBitmapCache; 256 257 /** 258 * A map from ImageView to the corresponding photo ID or uri, encapsulated in a request. 259 * The request may swapped out before the photo loading request is started. 260 */ 261 private final ConcurrentHashMap<ImageView, Request> mPendingRequests = 262 new ConcurrentHashMap<ImageView, Request>(); 263 264 /** 265 * Handler for messages sent to the UI thread. 266 */ 267 private final Handler mMainThreadHandler = new Handler(this); 268 269 /** 270 * Thread responsible for loading photos from the database. Created upon 271 * the first request. 272 */ 273 private LoaderThread mLoaderThread; 274 275 /** 276 * A gate to make sure we only send one instance of MESSAGE_PHOTOS_NEEDED at a time. 277 */ 278 private boolean mLoadingRequested; 279 280 /** 281 * Flag indicating if the image loading is paused. 282 */ 283 private boolean mPaused; 284 285 /** Cache size for {@link #mBitmapHolderCache} for devices with "large" RAM. */ 286 private static final int HOLDER_CACHE_SIZE = 2000000; 287 288 /** Cache size for {@link #mBitmapCache} for devices with "large" RAM. */ 289 private static final int BITMAP_CACHE_SIZE = 36864 * 48; // 1728K 290 291 private static final int LARGE_RAM_THRESHOLD = 640 * 1024 * 1024; 292 293 /** For debug: How many times we had to reload cached photo for a stale entry */ 294 private final AtomicInteger mStaleCacheOverwrite = new AtomicInteger(); 295 296 /** For debug: How many times we had to reload cached photo for a fresh entry. Should be 0. */ 297 private final AtomicInteger mFreshCacheOverwrite = new AtomicInteger(); 298 299 public ContactPhotoManagerImpl(Context context) { 300 mContext = context; 301 302 final float cacheSizeAdjustment = 303 (MemoryUtils.getTotalMemorySize() >= LARGE_RAM_THRESHOLD) ? 1.0f : 0.5f; 304 final int bitmapCacheSize = (int) (cacheSizeAdjustment * BITMAP_CACHE_SIZE); 305 mBitmapCache = new LruCache<Object, Bitmap>(bitmapCacheSize) { 306 @Override protected int sizeOf(Object key, Bitmap value) { 307 return value.getByteCount(); 308 } 309 310 @Override protected void entryRemoved( 311 boolean evicted, Object key, Bitmap oldValue, Bitmap newValue) { 312 if (DEBUG) dumpStats(); 313 } 314 }; 315 final int holderCacheSize = (int) (cacheSizeAdjustment * HOLDER_CACHE_SIZE); 316 mBitmapHolderCache = new LruCache<Object, BitmapHolder>(holderCacheSize) { 317 @Override protected int sizeOf(Object key, BitmapHolder value) { 318 return value.bytes != null ? value.bytes.length : 0; 319 } 320 321 @Override protected void entryRemoved( 322 boolean evicted, Object key, BitmapHolder oldValue, BitmapHolder newValue) { 323 if (DEBUG) dumpStats(); 324 } 325 }; 326 mBitmapHolderCacheRedZoneBytes = (int) (holderCacheSize * 0.75); 327 Log.i(TAG, "Cache adj: " + cacheSizeAdjustment); 328 if (DEBUG) { 329 Log.d(TAG, "Cache size: " + btk(mBitmapHolderCache.maxSize()) 330 + " + " + btk(mBitmapCache.maxSize())); 331 } 332 } 333 334 /** Converts bytes to K bytes, rounding up. Used only for debug log. */ 335 private static String btk(int bytes) { 336 return ((bytes + 1023) / 1024) + "K"; 337 } 338 339 private static final int safeDiv(int dividend, int divisor) { 340 return (divisor == 0) ? 0 : (dividend / divisor); 341 } 342 343 /** 344 * Dump cache stats on logcat. 345 */ 346 private void dumpStats() { 347 if (!DEBUG) return; 348 { 349 int numHolders = 0; 350 int rawBytes = 0; 351 int bitmapBytes = 0; 352 int numBitmaps = 0; 353 for (BitmapHolder h : mBitmapHolderCache.snapshot().values()) { 354 numHolders++; 355 if (h.bytes != null) { 356 rawBytes += h.bytes.length; 357 } 358 Bitmap b = h.bitmapRef != null ? h.bitmapRef.get() : null; 359 if (b != null) { 360 numBitmaps++; 361 bitmapBytes += b.getByteCount(); 362 } 363 } 364 Log.d(TAG, "L1: " + btk(rawBytes) + " + " + btk(bitmapBytes) + " = " 365 + btk(rawBytes + bitmapBytes) + ", " + numHolders + " holders, " 366 + numBitmaps + " bitmaps, avg: " 367 + btk(safeDiv(rawBytes, numHolders)) 368 + "," + btk(safeDiv(bitmapBytes,numBitmaps))); 369 Log.d(TAG, "L1 Stats: " + mBitmapHolderCache.toString() 370 + ", overwrite: fresh=" + mFreshCacheOverwrite.get() 371 + " stale=" + mStaleCacheOverwrite.get()); 372 } 373 374 { 375 int numBitmaps = 0; 376 int bitmapBytes = 0; 377 for (Bitmap b : mBitmapCache.snapshot().values()) { 378 numBitmaps++; 379 bitmapBytes += b.getByteCount(); 380 } 381 Log.d(TAG, "L2: " + btk(bitmapBytes) + ", " + numBitmaps + " bitmaps" 382 + ", avg: " + btk(safeDiv(bitmapBytes, numBitmaps))); 383 // We don't get from L2 cache, so L2 stats is meaningless. 384 } 385 } 386 387 @Override 388 public void onTrimMemory(int level) { 389 if (DEBUG) Log.d(TAG, "onTrimMemory: " + level); 390 if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) { 391 // Clear the caches. Note all pending requests will be removed too. 392 clear(); 393 } 394 } 395 396 @Override 397 public void preloadPhotosInBackground() { 398 ensureLoaderThread(); 399 mLoaderThread.requestPreloading(); 400 } 401 402 @Override 403 public void loadPhoto(ImageView view, long photoId, boolean hires, boolean darkTheme, 404 DefaultImageProvider defaultProvider) { 405 if (photoId == 0) { 406 // No photo is needed 407 defaultProvider.applyDefaultImage(view, hires, darkTheme); 408 mPendingRequests.remove(view); 409 } else { 410 if (DEBUG) Log.d(TAG, "loadPhoto request: " + photoId); 411 loadPhotoByIdOrUri(view, Request.createFromId(photoId, hires, darkTheme, 412 defaultProvider)); 413 } 414 } 415 416 @Override 417 public void loadPhoto(ImageView view, Uri photoUri, boolean hires, boolean darkTheme, 418 DefaultImageProvider defaultProvider) { 419 if (photoUri == null) { 420 // No photo is needed 421 defaultProvider.applyDefaultImage(view, hires, darkTheme); 422 mPendingRequests.remove(view); 423 } else { 424 if (DEBUG) Log.d(TAG, "loadPhoto request: " + photoUri); 425 loadPhotoByIdOrUri(view, Request.createFromUri(photoUri, hires, darkTheme, 426 defaultProvider)); 427 } 428 } 429 430 private void loadPhotoByIdOrUri(ImageView view, Request request) { 431 boolean loaded = loadCachedPhoto(view, request); 432 if (loaded) { 433 mPendingRequests.remove(view); 434 } else { 435 mPendingRequests.put(view, request); 436 if (!mPaused) { 437 // Send a request to start loading photos 438 requestLoading(); 439 } 440 } 441 } 442 443 @Override 444 public void removePhoto(ImageView view) { 445 view.setImageDrawable(null); 446 mPendingRequests.remove(view); 447 } 448 449 @Override 450 public void refreshCache() { 451 if (DEBUG) Log.d(TAG, "refreshCache"); 452 for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) { 453 holder.fresh = false; 454 } 455 } 456 457 /** 458 * Checks if the photo is present in cache. If so, sets the photo on the view. 459 * 460 * @return false if the photo needs to be (re)loaded from the provider. 461 */ 462 private boolean loadCachedPhoto(ImageView view, Request request) { 463 BitmapHolder holder = mBitmapHolderCache.get(request.getKey()); 464 if (holder == null) { 465 // The bitmap has not been loaded - should display the placeholder image. 466 request.applyDefaultImage(view); 467 return false; 468 } 469 470 if (holder.bytes == null) { 471 request.applyDefaultImage(view); 472 return holder.fresh; 473 } 474 475 // Optionally decode bytes into a bitmap 476 inflateBitmap(holder); 477 478 view.setImageBitmap(holder.bitmap); 479 480 if (holder.bitmap != null) { 481 // Put the bitmap in the LRU cache 482 mBitmapCache.put(request, holder.bitmap); 483 } 484 485 // Soften the reference 486 holder.bitmap = null; 487 488 return holder.fresh; 489 } 490 491 /** 492 * If necessary, decodes bytes stored in the holder to Bitmap. As long as the 493 * bitmap is held either by {@link #mBitmapCache} or by a soft reference in 494 * the holder, it will not be necessary to decode the bitmap. 495 */ 496 private static void inflateBitmap(BitmapHolder holder) { 497 byte[] bytes = holder.bytes; 498 if (bytes == null || bytes.length == 0) { 499 return; 500 } 501 502 // Check the soft reference. If will be retained if the bitmap is also 503 // in the LRU cache, so we don't need to check the LRU cache explicitly. 504 if (holder.bitmapRef != null) { 505 holder.bitmap = holder.bitmapRef.get(); 506 if (holder.bitmap != null) { 507 return; 508 } 509 } 510 511 try { 512 Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, null); 513 holder.bitmap = bitmap; 514 holder.bitmapRef = new SoftReference<Bitmap>(bitmap); 515 if (DEBUG) { 516 Log.d(TAG, "inflateBitmap " + btk(bytes.length) + " -> " 517 + bitmap.getWidth() + "x" + bitmap.getHeight() 518 + ", " + btk(bitmap.getByteCount())); 519 } 520 } catch (OutOfMemoryError e) { 521 // Do nothing - the photo will appear to be missing 522 } 523 } 524 525 public void clear() { 526 if (DEBUG) Log.d(TAG, "clear"); 527 mPendingRequests.clear(); 528 mBitmapHolderCache.evictAll(); 529 mBitmapCache.evictAll(); 530 } 531 532 @Override 533 public void pause() { 534 mPaused = true; 535 } 536 537 @Override 538 public void resume() { 539 mPaused = false; 540 if (DEBUG) dumpStats(); 541 if (!mPendingRequests.isEmpty()) { 542 requestLoading(); 543 } 544 } 545 546 /** 547 * Sends a message to this thread itself to start loading images. If the current 548 * view contains multiple image views, all of those image views will get a chance 549 * to request their respective photos before any of those requests are executed. 550 * This allows us to load images in bulk. 551 */ 552 private void requestLoading() { 553 if (!mLoadingRequested) { 554 mLoadingRequested = true; 555 mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING); 556 } 557 } 558 559 /** 560 * Processes requests on the main thread. 561 */ 562 @Override 563 public boolean handleMessage(Message msg) { 564 switch (msg.what) { 565 case MESSAGE_REQUEST_LOADING: { 566 mLoadingRequested = false; 567 if (!mPaused) { 568 ensureLoaderThread(); 569 mLoaderThread.requestLoading(); 570 } 571 return true; 572 } 573 574 case MESSAGE_PHOTOS_LOADED: { 575 if (!mPaused) { 576 processLoadedImages(); 577 } 578 if (DEBUG) dumpStats(); 579 return true; 580 } 581 } 582 return false; 583 } 584 585 public void ensureLoaderThread() { 586 if (mLoaderThread == null) { 587 mLoaderThread = new LoaderThread(mContext.getContentResolver()); 588 mLoaderThread.start(); 589 } 590 } 591 592 /** 593 * Goes over pending loading requests and displays loaded photos. If some of the 594 * photos still haven't been loaded, sends another request for image loading. 595 */ 596 private void processLoadedImages() { 597 Iterator<ImageView> iterator = mPendingRequests.keySet().iterator(); 598 while (iterator.hasNext()) { 599 ImageView view = iterator.next(); 600 Request key = mPendingRequests.get(view); 601 boolean loaded = loadCachedPhoto(view, key); 602 if (loaded) { 603 iterator.remove(); 604 } 605 } 606 607 softenCache(); 608 609 if (!mPendingRequests.isEmpty()) { 610 requestLoading(); 611 } 612 } 613 614 /** 615 * Removes strong references to loaded bitmaps to allow them to be garbage collected 616 * if needed. Some of the bitmaps will still be retained by {@link #mBitmapCache}. 617 */ 618 private void softenCache() { 619 for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) { 620 holder.bitmap = null; 621 } 622 } 623 624 /** 625 * Stores the supplied bitmap in cache. 626 */ 627 private void cacheBitmap(Object key, byte[] bytes, boolean preloading) { 628 if (DEBUG) { 629 BitmapHolder prev = mBitmapHolderCache.get(key); 630 if (prev != null && prev.bytes != null) { 631 Log.d(TAG, "Overwriting cache: key=" + key + (prev.fresh ? " FRESH" : " stale")); 632 if (prev.fresh) { 633 mFreshCacheOverwrite.incrementAndGet(); 634 } else { 635 mStaleCacheOverwrite.incrementAndGet(); 636 } 637 } 638 Log.d(TAG, "Caching data: key=" + key + ", " + btk(bytes.length)); 639 } 640 BitmapHolder holder = new BitmapHolder(bytes); 641 holder.fresh = true; 642 643 // Unless this image is being preloaded, decode it right away while 644 // we are still on the background thread. 645 if (!preloading) { 646 inflateBitmap(holder); 647 } 648 649 mBitmapHolderCache.put(key, holder); 650 } 651 652 /** 653 * Populates an array of photo IDs that need to be loaded. 654 */ 655 private void obtainPhotoIdsAndUrisToLoad(Set<Long> photoIds, 656 Set<String> photoIdsAsStrings, Set<Uri> uris) { 657 photoIds.clear(); 658 photoIdsAsStrings.clear(); 659 uris.clear(); 660 661 /* 662 * Since the call is made from the loader thread, the map could be 663 * changing during the iteration. That's not really a problem: 664 * ConcurrentHashMap will allow those changes to happen without throwing 665 * exceptions. Since we may miss some requests in the situation of 666 * concurrent change, we will need to check the map again once loading 667 * is complete. 668 */ 669 Iterator<Request> iterator = mPendingRequests.values().iterator(); 670 while (iterator.hasNext()) { 671 Request request = iterator.next(); 672 BitmapHolder holder = mBitmapHolderCache.get(request); 673 if (holder == null || !holder.fresh) { 674 if (request.isUriRequest()) { 675 uris.add(request.mUri); 676 } else { 677 photoIds.add(request.mId); 678 photoIdsAsStrings.add(String.valueOf(request.mId)); 679 } 680 } 681 } 682 } 683 684 /** 685 * The thread that performs loading of photos from the database. 686 */ 687 private class LoaderThread extends HandlerThread implements Callback { 688 private static final int BUFFER_SIZE = 1024*16; 689 private static final int MESSAGE_PRELOAD_PHOTOS = 0; 690 private static final int MESSAGE_LOAD_PHOTOS = 1; 691 692 /** 693 * A pause between preload batches that yields to the UI thread. 694 */ 695 private static final int PHOTO_PRELOAD_DELAY = 1000; 696 697 /** 698 * Number of photos to preload per batch. 699 */ 700 private static final int PRELOAD_BATCH = 25; 701 702 /** 703 * Maximum number of photos to preload. If the cache size is 2Mb and 704 * the expected average size of a photo is 4kb, then this number should be 2Mb/4kb = 500. 705 */ 706 private static final int MAX_PHOTOS_TO_PRELOAD = 100; 707 708 private final ContentResolver mResolver; 709 private final StringBuilder mStringBuilder = new StringBuilder(); 710 private final Set<Long> mPhotoIds = Sets.newHashSet(); 711 private final Set<String> mPhotoIdsAsStrings = Sets.newHashSet(); 712 private final Set<Uri> mPhotoUris = Sets.newHashSet(); 713 private final List<Long> mPreloadPhotoIds = Lists.newArrayList(); 714 715 private Handler mLoaderThreadHandler; 716 private byte mBuffer[]; 717 718 private static final int PRELOAD_STATUS_NOT_STARTED = 0; 719 private static final int PRELOAD_STATUS_IN_PROGRESS = 1; 720 private static final int PRELOAD_STATUS_DONE = 2; 721 722 private int mPreloadStatus = PRELOAD_STATUS_NOT_STARTED; 723 724 public LoaderThread(ContentResolver resolver) { 725 super(LOADER_THREAD_NAME); 726 mResolver = resolver; 727 } 728 729 public void ensureHandler() { 730 if (mLoaderThreadHandler == null) { 731 mLoaderThreadHandler = new Handler(getLooper(), this); 732 } 733 } 734 735 /** 736 * Kicks off preloading of the next batch of photos on the background thread. 737 * Preloading will happen after a delay: we want to yield to the UI thread 738 * as much as possible. 739 * <p> 740 * If preloading is already complete, does nothing. 741 */ 742 public void requestPreloading() { 743 if (mPreloadStatus == PRELOAD_STATUS_DONE) { 744 return; 745 } 746 747 ensureHandler(); 748 if (mLoaderThreadHandler.hasMessages(MESSAGE_LOAD_PHOTOS)) { 749 return; 750 } 751 752 mLoaderThreadHandler.sendEmptyMessageDelayed( 753 MESSAGE_PRELOAD_PHOTOS, PHOTO_PRELOAD_DELAY); 754 } 755 756 /** 757 * Sends a message to this thread to load requested photos. Cancels a preloading 758 * request, if any: we don't want preloading to impede loading of the photos 759 * we need to display now. 760 */ 761 public void requestLoading() { 762 ensureHandler(); 763 mLoaderThreadHandler.removeMessages(MESSAGE_PRELOAD_PHOTOS); 764 mLoaderThreadHandler.sendEmptyMessage(MESSAGE_LOAD_PHOTOS); 765 } 766 767 /** 768 * Receives the above message, loads photos and then sends a message 769 * to the main thread to process them. 770 */ 771 @Override 772 public boolean handleMessage(Message msg) { 773 switch (msg.what) { 774 case MESSAGE_PRELOAD_PHOTOS: 775 preloadPhotosInBackground(); 776 break; 777 case MESSAGE_LOAD_PHOTOS: 778 loadPhotosInBackground(); 779 break; 780 } 781 return true; 782 } 783 784 /** 785 * The first time it is called, figures out which photos need to be preloaded. 786 * Each subsequent call preloads the next batch of photos and requests 787 * another cycle of preloading after a delay. The whole process ends when 788 * we either run out of photos to preload or fill up cache. 789 */ 790 private void preloadPhotosInBackground() { 791 if (mPreloadStatus == PRELOAD_STATUS_DONE) { 792 return; 793 } 794 795 if (mPreloadStatus == PRELOAD_STATUS_NOT_STARTED) { 796 queryPhotosForPreload(); 797 if (mPreloadPhotoIds.isEmpty()) { 798 mPreloadStatus = PRELOAD_STATUS_DONE; 799 } else { 800 mPreloadStatus = PRELOAD_STATUS_IN_PROGRESS; 801 } 802 requestPreloading(); 803 return; 804 } 805 806 if (mBitmapHolderCache.size() > mBitmapHolderCacheRedZoneBytes) { 807 mPreloadStatus = PRELOAD_STATUS_DONE; 808 return; 809 } 810 811 mPhotoIds.clear(); 812 mPhotoIdsAsStrings.clear(); 813 814 int count = 0; 815 int preloadSize = mPreloadPhotoIds.size(); 816 while(preloadSize > 0 && mPhotoIds.size() < PRELOAD_BATCH) { 817 preloadSize--; 818 count++; 819 Long photoId = mPreloadPhotoIds.get(preloadSize); 820 mPhotoIds.add(photoId); 821 mPhotoIdsAsStrings.add(photoId.toString()); 822 mPreloadPhotoIds.remove(preloadSize); 823 } 824 825 loadPhotosFromDatabase(true); 826 827 if (preloadSize == 0) { 828 mPreloadStatus = PRELOAD_STATUS_DONE; 829 } 830 831 Log.v(TAG, "Preloaded " + count + " photos. Cached bytes: " 832 + mBitmapHolderCache.size()); 833 834 requestPreloading(); 835 } 836 837 private void queryPhotosForPreload() { 838 Cursor cursor = null; 839 try { 840 Uri uri = Contacts.CONTENT_URI.buildUpon().appendQueryParameter( 841 ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT)) 842 .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, 843 String.valueOf(MAX_PHOTOS_TO_PRELOAD)) 844 .build(); 845 cursor = mResolver.query(uri, new String[] { Contacts.PHOTO_ID }, 846 Contacts.PHOTO_ID + " NOT NULL AND " + Contacts.PHOTO_ID + "!=0", 847 null, 848 Contacts.STARRED + " DESC, " + Contacts.LAST_TIME_CONTACTED + " DESC"); 849 850 if (cursor != null) { 851 while (cursor.moveToNext()) { 852 // Insert them in reverse order, because we will be taking 853 // them from the end of the list for loading. 854 mPreloadPhotoIds.add(0, cursor.getLong(0)); 855 } 856 } 857 } finally { 858 if (cursor != null) { 859 cursor.close(); 860 } 861 } 862 } 863 864 private void loadPhotosInBackground() { 865 obtainPhotoIdsAndUrisToLoad(mPhotoIds, mPhotoIdsAsStrings, mPhotoUris); 866 loadPhotosFromDatabase(false); 867 loadRemotePhotos(); 868 requestPreloading(); 869 } 870 871 private void loadPhotosFromDatabase(boolean preloading) { 872 if (mPhotoIds.isEmpty()) { 873 return; 874 } 875 876 // Remove loaded photos from the preload queue: we don't want 877 // the preloading process to load them again. 878 if (!preloading && mPreloadStatus == PRELOAD_STATUS_IN_PROGRESS) { 879 for (Long id : mPhotoIds) { 880 mPreloadPhotoIds.remove(id); 881 } 882 if (mPreloadPhotoIds.isEmpty()) { 883 mPreloadStatus = PRELOAD_STATUS_DONE; 884 } 885 } 886 887 mStringBuilder.setLength(0); 888 mStringBuilder.append(Photo._ID + " IN("); 889 for (int i = 0; i < mPhotoIds.size(); i++) { 890 if (i != 0) { 891 mStringBuilder.append(','); 892 } 893 mStringBuilder.append('?'); 894 } 895 mStringBuilder.append(')'); 896 897 Cursor cursor = null; 898 try { 899 if (DEBUG) Log.d(TAG, "Loading " + TextUtils.join(",", mPhotoIdsAsStrings)); 900 cursor = mResolver.query(Data.CONTENT_URI, 901 COLUMNS, 902 mStringBuilder.toString(), 903 mPhotoIdsAsStrings.toArray(EMPTY_STRING_ARRAY), 904 null); 905 906 if (cursor != null) { 907 while (cursor.moveToNext()) { 908 Long id = cursor.getLong(0); 909 byte[] bytes = cursor.getBlob(1); 910 cacheBitmap(id, bytes, preloading); 911 mPhotoIds.remove(id); 912 } 913 } 914 } finally { 915 if (cursor != null) { 916 cursor.close(); 917 } 918 } 919 920 // Remaining photos were not found in the contacts database (but might be in profile). 921 for (Long id : mPhotoIds) { 922 if (ContactsContract.isProfileId(id)) { 923 Cursor profileCursor = null; 924 try { 925 profileCursor = mResolver.query( 926 ContentUris.withAppendedId(Data.CONTENT_URI, id), 927 COLUMNS, null, null, null); 928 if (profileCursor != null && profileCursor.moveToFirst()) { 929 cacheBitmap(profileCursor.getLong(0), profileCursor.getBlob(1), 930 preloading); 931 } else { 932 // Couldn't load a photo this way either. 933 cacheBitmap(id, null, preloading); 934 } 935 } finally { 936 if (profileCursor != null) { 937 profileCursor.close(); 938 } 939 } 940 } else { 941 // Not a profile photo and not found - mark the cache accordingly 942 cacheBitmap(id, null, preloading); 943 } 944 } 945 946 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED); 947 } 948 949 private void loadRemotePhotos() { 950 for (Uri uri : mPhotoUris) { 951 if (mBuffer == null) { 952 mBuffer = new byte[BUFFER_SIZE]; 953 } 954 try { 955 if (DEBUG) Log.d(TAG, "Loading " + uri); 956 InputStream is = mResolver.openInputStream(uri); 957 if (is != null) { 958 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 959 try { 960 int size; 961 while ((size = is.read(mBuffer)) != -1) { 962 baos.write(mBuffer, 0, size); 963 } 964 } finally { 965 is.close(); 966 } 967 cacheBitmap(uri, baos.toByteArray(), false); 968 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED); 969 } else { 970 Log.v(TAG, "Cannot load photo " + uri); 971 cacheBitmap(uri, null, false); 972 } 973 } catch (Exception ex) { 974 Log.v(TAG, "Cannot load photo " + uri, ex); 975 cacheBitmap(uri, null, false); 976 } 977 } 978 } 979 } 980 981 /** 982 * A holder for either a Uri or an id and a flag whether this was requested for the dark or 983 * light theme 984 */ 985 private static final class Request { 986 private final long mId; 987 private final Uri mUri; 988 private final boolean mDarkTheme; 989 private final boolean mHires; 990 private final DefaultImageProvider mDefaultProvider; 991 992 private Request(long id, Uri uri, boolean hires, boolean darkTheme, 993 DefaultImageProvider defaultProvider) { 994 mId = id; 995 mUri = uri; 996 mDarkTheme = darkTheme; 997 mHires = hires; 998 mDefaultProvider = defaultProvider; 999 } 1000 1001 public static Request createFromId(long id, boolean hires, boolean darkTheme, 1002 DefaultImageProvider defaultProvider) { 1003 return new Request(id, null /* no URI */, hires, darkTheme, defaultProvider); 1004 } 1005 1006 public static Request createFromUri(Uri uri, boolean hires, boolean darkTheme, 1007 DefaultImageProvider defaultProvider) { 1008 return new Request(0 /* no ID */, uri, hires, darkTheme, defaultProvider); 1009 } 1010 1011 public boolean isDarkTheme() { 1012 return mDarkTheme; 1013 } 1014 1015 public boolean isHires() { 1016 return mHires; 1017 } 1018 1019 public boolean isUriRequest() { 1020 return mUri != null; 1021 } 1022 1023 @Override 1024 public int hashCode() { 1025 if (mUri != null) return mUri.hashCode(); 1026 1027 // copied over from Long.hashCode() 1028 return (int) (mId ^ (mId >>> 32)); 1029 } 1030 1031 @Override 1032 public boolean equals(Object o) { 1033 if (!(o instanceof Request)) return false; 1034 final Request that = (Request) o; 1035 // Don't compare equality of mHires and mDarkTheme fields because these are only used 1036 // in the default contact photo case. When the contact does have a photo, the contact 1037 // photo is the same regardless of mHires and mDarkTheme, so we shouldn't need to put 1038 // the photo request on the queue twice. 1039 return mId == that.mId && UriUtils.areEqual(mUri, that.mUri); 1040 } 1041 1042 public Object getKey() { 1043 return mUri == null ? mId : mUri; 1044 } 1045 1046 public void applyDefaultImage(ImageView view) { 1047 mDefaultProvider.applyDefaultImage(view, mHires, mDarkTheme); 1048 } 1049 } 1050} 1051