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