1/* 2 * Copyright (C) 2013 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.mail.photomanager; 18 19import android.content.ComponentCallbacks2; 20import android.content.ContentResolver; 21import android.content.Context; 22import android.content.res.Configuration; 23import android.graphics.Bitmap; 24import android.os.Handler; 25import android.os.Handler.Callback; 26import android.os.HandlerThread; 27import android.os.Message; 28import android.os.Process; 29import android.util.LruCache; 30 31import com.android.mail.ui.ImageCanvas; 32import com.android.mail.utils.LogUtils; 33import com.android.mail.utils.Utils; 34import com.google.common.base.Objects; 35import com.google.common.collect.Lists; 36 37import java.util.Collection; 38import java.util.Collections; 39import java.util.HashMap; 40import java.util.HashSet; 41import java.util.List; 42import java.util.Map; 43import java.util.PriorityQueue; 44import java.util.concurrent.atomic.AtomicInteger; 45 46/** 47 * Asynchronously loads photos and maintains a cache of photos 48 */ 49public abstract class PhotoManager implements ComponentCallbacks2, Callback { 50 /** 51 * Get the default image provider that draws while the photo is being 52 * loaded. 53 */ 54 protected abstract DefaultImageProvider getDefaultImageProvider(); 55 56 /** 57 * Generate a hashcode unique to each request. 58 */ 59 protected abstract int getHash(PhotoIdentifier id, ImageCanvas view); 60 61 /** 62 * Return a specific implementation of PhotoLoaderThread. 63 */ 64 protected abstract PhotoLoaderThread getLoaderThread(ContentResolver contentResolver); 65 66 /** 67 * Subclasses can implement this method to alert callbacks that images finished loading. 68 * @param request The original request made. 69 * @param success True if we successfully loaded the image from cache. False if we fell back 70 * to the default image. 71 */ 72 protected void onImageDrawn(final Request request, final boolean success) { 73 // Subclasses can choose to do something about this 74 } 75 76 /** 77 * Subclasses can implement this method to alert callbacks that images started loading. 78 * @param request The original request made. 79 */ 80 protected void onImageLoadStarted(final Request request) { 81 // Subclasses can choose to do something about this 82 } 83 84 /** 85 * Subclasses can implement this method to determine whether a previously loaded bitmap can 86 * be reused for a new canvas size. 87 * @param prevWidth The width of the previously loaded bitmap. 88 * @param prevHeight The height of the previously loaded bitmap. 89 * @param newWidth The width of the canvas this request is drawing on. 90 * @param newHeight The height of the canvas this request is drawing on. 91 * @return 92 */ 93 protected boolean isSizeCompatible(int prevWidth, int prevHeight, int newWidth, int newHeight) { 94 return true; 95 } 96 97 protected final Context getContext() { 98 return mContext; 99 } 100 101 static final String TAG = "PhotoManager"; 102 static final boolean DEBUG = false; // Don't submit with true 103 static final boolean DEBUG_SIZES = false; // Don't submit with true 104 105 private static final String LOADER_THREAD_NAME = "PhotoLoader"; 106 107 /** 108 * Type of message sent by the UI thread to itself to indicate that some photos 109 * need to be loaded. 110 */ 111 private static final int MESSAGE_REQUEST_LOADING = 1; 112 113 /** 114 * Type of message sent by the loader thread to indicate that some photos have 115 * been loaded. 116 */ 117 private static final int MESSAGE_PHOTOS_LOADED = 2; 118 119 /** 120 * Type of message sent by the loader thread to indicate that 121 */ 122 private static final int MESSAGE_PHOTO_LOADING = 3; 123 124 public interface DefaultImageProvider { 125 /** 126 * Applies the default avatar to the DividedImageView. Extent is an 127 * indicator for the size (width or height). If darkTheme is set, the 128 * avatar is one that looks better on dark background 129 * @param id 130 */ 131 public void applyDefaultImage(PhotoIdentifier id, ImageCanvas view, int extent); 132 } 133 134 /** 135 * Maintains the state of a particular photo. 136 */ 137 protected static class BitmapHolder { 138 byte[] bytes; 139 int width; 140 int height; 141 142 volatile boolean fresh; 143 144 public BitmapHolder(byte[] bytes, int width, int height) { 145 this.bytes = bytes; 146 this.width = width; 147 this.height = height; 148 this.fresh = true; 149 } 150 151 @Override 152 public String toString() { 153 final StringBuilder sb = new StringBuilder("{"); 154 sb.append(super.toString()); 155 sb.append(" bytes="); 156 sb.append(bytes); 157 sb.append(" size="); 158 sb.append(bytes == null ? 0 : bytes.length); 159 sb.append(" width="); 160 sb.append(width); 161 sb.append(" height="); 162 sb.append(height); 163 sb.append(" fresh="); 164 sb.append(fresh); 165 sb.append("}"); 166 return sb.toString(); 167 } 168 } 169 170 // todo:ath caches should be member vars 171 /** 172 * An LRU cache for bitmap holders. The cache contains bytes for photos just 173 * as they come from the database. Each holder has a soft reference to the 174 * actual bitmap. The keys are decided by the implementation. 175 */ 176 private static final LruCache<Object, BitmapHolder> sBitmapHolderCache; 177 178 /** 179 * Level 2 LRU cache for bitmaps. This is a smaller cache that holds 180 * the most recently used bitmaps to save time on decoding 181 * them from bytes (the bytes are stored in {@link #sBitmapHolderCache}. 182 * The keys are decided by the implementation. 183 */ 184 private static final LruCache<BitmapIdentifier, Bitmap> sBitmapCache; 185 186 /** Cache size for {@link #sBitmapHolderCache} for devices with "large" RAM. */ 187 private static final int HOLDER_CACHE_SIZE = 2000000; 188 189 /** Cache size for {@link #sBitmapCache} for devices with "large" RAM. */ 190 private static final int BITMAP_CACHE_SIZE = 1024 * 1024 * 8; // 8MB 191 192 /** For debug: How many times we had to reload cached photo for a stale entry */ 193 private static final AtomicInteger sStaleCacheOverwrite = new AtomicInteger(); 194 195 /** For debug: How many times we had to reload cached photo for a fresh entry. Should be 0. */ 196 private static final AtomicInteger sFreshCacheOverwrite = new AtomicInteger(); 197 198 static { 199 final float cacheSizeAdjustment = 200 (MemoryUtils.getTotalMemorySize() >= MemoryUtils.LARGE_RAM_THRESHOLD) ? 201 1.0f : 0.5f; 202 final int holderCacheSize = (int) (cacheSizeAdjustment * HOLDER_CACHE_SIZE); 203 sBitmapHolderCache = new LruCache<Object, BitmapHolder>(holderCacheSize) { 204 @Override protected int sizeOf(Object key, BitmapHolder value) { 205 return value.bytes != null ? value.bytes.length : 0; 206 } 207 208 @Override protected void entryRemoved( 209 boolean evicted, Object key, BitmapHolder oldValue, BitmapHolder newValue) { 210 if (DEBUG) dumpStats(); 211 } 212 }; 213 final int bitmapCacheSize = (int) (cacheSizeAdjustment * BITMAP_CACHE_SIZE); 214 sBitmapCache = new LruCache<BitmapIdentifier, Bitmap>(bitmapCacheSize) { 215 @Override protected int sizeOf(BitmapIdentifier key, Bitmap value) { 216 return value.getByteCount(); 217 } 218 219 @Override protected void entryRemoved( 220 boolean evicted, BitmapIdentifier key, Bitmap oldValue, Bitmap newValue) { 221 if (DEBUG) dumpStats(); 222 } 223 }; 224 LogUtils.i(TAG, "Cache adj: " + cacheSizeAdjustment); 225 if (DEBUG) { 226 LogUtils.d(TAG, "Cache size: " + btk(sBitmapHolderCache.maxSize()) 227 + " + " + btk(sBitmapCache.maxSize())); 228 } 229 } 230 231 /** 232 * A map from ImageCanvas hashcode to the corresponding photo ID or uri, 233 * encapsulated in a request. The request may swapped out before the photo 234 * loading request is started. 235 */ 236 private final Map<Integer, Request> mPendingRequests = Collections.synchronizedMap( 237 new HashMap<Integer, Request>()); 238 239 /** 240 * Handler for messages sent to the UI thread. 241 */ 242 private final Handler mMainThreadHandler = new Handler(this); 243 244 /** 245 * Thread responsible for loading photos from the database. Created upon 246 * the first request. 247 */ 248 private PhotoLoaderThread mLoaderThread; 249 250 /** 251 * A gate to make sure we only send one instance of MESSAGE_PHOTOS_NEEDED at a time. 252 */ 253 private boolean mLoadingRequested; 254 255 /** 256 * Flag indicating if the image loading is paused. 257 */ 258 private boolean mPaused; 259 260 private final Context mContext; 261 262 public PhotoManager(Context context) { 263 mContext = context; 264 } 265 266 public void loadThumbnail(PhotoIdentifier id, ImageCanvas view) { 267 loadThumbnail(id, view, null); 268 } 269 270 /** 271 * Load an image 272 * 273 * @param dimensions Preferred dimensions 274 */ 275 public void loadThumbnail(final PhotoIdentifier id, final ImageCanvas view, 276 final ImageCanvas.Dimensions dimensions) { 277 Utils.traceBeginSection("Load thumbnail"); 278 final DefaultImageProvider defaultProvider = getDefaultImageProvider(); 279 final Request request = new Request(id, defaultProvider, view, dimensions); 280 final int hashCode = request.hashCode(); 281 282 if (!id.isValid()) { 283 // No photo is needed 284 request.applyDefaultImage(); 285 onImageDrawn(request, false); 286 mPendingRequests.remove(hashCode); 287 } else if (mPendingRequests.containsKey(hashCode)) { 288 LogUtils.d(TAG, "load request dropped for %s", id); 289 } else { 290 if (DEBUG) LogUtils.v(TAG, "loadPhoto request: %s", id.getKey()); 291 loadPhoto(hashCode, request); 292 } 293 Utils.traceEndSection(); 294 } 295 296 private void loadPhoto(int hashCode, Request request) { 297 if (DEBUG) { 298 LogUtils.v(TAG, "NEW IMAGE REQUEST key=%s r=%s thread=%s", 299 request.getKey(), 300 request, 301 Thread.currentThread()); 302 } 303 304 boolean loaded = loadCachedPhoto(request, false); 305 if (loaded) { 306 if (DEBUG) { 307 LogUtils.v(TAG, "image request, cache hit. request queue size=%s", 308 mPendingRequests.size()); 309 } 310 } else { 311 if (DEBUG) { 312 LogUtils.d(TAG, "image request, cache miss: key=%s", request.getKey()); 313 } 314 mPendingRequests.put(hashCode, request); 315 if (!mPaused) { 316 // Send a request to start loading photos 317 requestLoading(); 318 } 319 } 320 } 321 322 /** 323 * Remove photo from the supplied image view. This also cancels current pending load request 324 * inside this photo manager. 325 */ 326 public void removePhoto(int hashcode) { 327 Request r = mPendingRequests.remove(hashcode); 328 if (r != null) { 329 LogUtils.d(TAG, "removed request %s", r.getKey()); 330 } 331 } 332 333 private void ensureLoaderThread() { 334 if (mLoaderThread == null) { 335 mLoaderThread = getLoaderThread(mContext.getContentResolver()); 336 mLoaderThread.start(); 337 } 338 } 339 340 /** 341 * Checks if the photo is present in cache. If so, sets the photo on the view. 342 * 343 * @param request Determines which image to load from cache. 344 * @param afterLoaderThreadFinished Pass true if calling after the LoaderThread has run. Pass 345 * false if the Loader Thread hasn't made any attempts to 346 * load images yet. 347 * @return false if the photo needs to be (re)loaded from the provider. 348 */ 349 private boolean loadCachedPhoto(final Request request, 350 final boolean afterLoaderThreadFinished) { 351 Utils.traceBeginSection("Load cached photo"); 352 final Bitmap cached = getCachedPhoto(request.bitmapKey); 353 if (cached != null) { 354 if (DEBUG) { 355 LogUtils.v(TAG, "%s, key=%s decodedSize=%s thread=%s", 356 afterLoaderThreadFinished ? "DECODED IMG READ" 357 : "DECODED IMG CACHE HIT", 358 request.getKey(), 359 cached.getByteCount(), 360 Thread.currentThread()); 361 } 362 if (request.getView().getGeneration() == request.viewGeneration) { 363 request.getView().drawImage(cached, request.getKey()); 364 onImageDrawn(request, true); 365 } 366 Utils.traceEndSection(); 367 return true; 368 } 369 370 // We couldn't load the requested image, so try to load a replacement. 371 // This removes the flicker from SIMPLE to BEST transition. 372 final Object replacementKey = request.getPhotoIdentifier().getKeyToShowInsteadOfDefault(); 373 if (replacementKey != null) { 374 final BitmapIdentifier replacementBitmapKey = new BitmapIdentifier(replacementKey, 375 request.bitmapKey.w, request.bitmapKey.h); 376 final Bitmap cachedReplacement = getCachedPhoto(replacementBitmapKey); 377 if (cachedReplacement != null) { 378 if (DEBUG) { 379 LogUtils.v(TAG, "%s, key=%s decodedSize=%s thread=%s", 380 afterLoaderThreadFinished ? "DECODED IMG READ" 381 : "DECODED IMG CACHE HIT", 382 replacementKey, 383 cachedReplacement.getByteCount(), 384 Thread.currentThread()); 385 } 386 if (request.getView().getGeneration() == request.viewGeneration) { 387 request.getView().drawImage(cachedReplacement, request.getKey()); 388 onImageDrawn(request, true); 389 } 390 Utils.traceEndSection(); 391 return false; 392 } 393 } 394 395 // We couldn't load any image, so draw a default image 396 request.applyDefaultImage(); 397 398 final BitmapHolder holder = sBitmapHolderCache.get(request.getKey()); 399 // Check if we loaded null bytes, which means we meant to not draw anything. 400 if (holder != null && holder.bytes == null) { 401 onImageDrawn(request, holder.fresh); 402 Utils.traceEndSection(); 403 return holder.fresh; 404 } 405 Utils.traceEndSection(); 406 return false; 407 } 408 409 /** 410 * Takes care of retrieving the Bitmap from both the decoded and holder caches. 411 */ 412 private static Bitmap getCachedPhoto(BitmapIdentifier bitmapKey) { 413 Utils.traceBeginSection("Get cached photo"); 414 final Bitmap cached = sBitmapCache.get(bitmapKey); 415 Utils.traceEndSection(); 416 return cached; 417 } 418 419 /** 420 * Temporarily stops loading photos from the database. 421 */ 422 public void pause() { 423 LogUtils.d(TAG, "%s paused.", getClass().getName()); 424 mPaused = true; 425 } 426 427 /** 428 * Resumes loading photos from the database. 429 */ 430 public void resume() { 431 LogUtils.d(TAG, "%s resumed.", getClass().getName()); 432 mPaused = false; 433 if (DEBUG) dumpStats(); 434 if (!mPendingRequests.isEmpty()) { 435 requestLoading(); 436 } 437 } 438 439 /** 440 * Sends a message to this thread itself to start loading images. If the current 441 * view contains multiple image views, all of those image views will get a chance 442 * to request their respective photos before any of those requests are executed. 443 * This allows us to load images in bulk. 444 */ 445 private void requestLoading() { 446 if (!mLoadingRequested) { 447 mLoadingRequested = true; 448 mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING); 449 } 450 } 451 452 /** 453 * Processes requests on the main thread. 454 */ 455 @Override 456 public boolean handleMessage(final Message msg) { 457 switch (msg.what) { 458 case MESSAGE_REQUEST_LOADING: { 459 mLoadingRequested = false; 460 if (!mPaused) { 461 ensureLoaderThread(); 462 mLoaderThread.requestLoading(); 463 } 464 return true; 465 } 466 467 case MESSAGE_PHOTOS_LOADED: { 468 processLoadedImages(); 469 if (DEBUG) dumpStats(); 470 return true; 471 } 472 473 case MESSAGE_PHOTO_LOADING: { 474 final int hashcode = msg.arg1; 475 final Request request = mPendingRequests.get(hashcode); 476 onImageLoadStarted(request); 477 return true; 478 } 479 } 480 return false; 481 } 482 483 /** 484 * Goes over pending loading requests and displays loaded photos. If some of the 485 * photos still haven't been loaded, sends another request for image loading. 486 */ 487 private void processLoadedImages() { 488 Utils.traceBeginSection("process loaded images"); 489 final List<Integer> toRemove = Lists.newArrayList(); 490 for (final Integer hash : mPendingRequests.keySet()) { 491 final Request request = mPendingRequests.get(hash); 492 final boolean loaded = loadCachedPhoto(request, true); 493 // Request can go through multiple attempts if the LoaderThread fails to load any 494 // images for it, or if the images it loads are evicted from the cache before we 495 // could access them in the main thread. 496 if (loaded || request.attempts > 2) { 497 toRemove.add(hash); 498 } 499 } 500 for (final Integer key : toRemove) { 501 mPendingRequests.remove(key); 502 } 503 504 if (!mPaused && !mPendingRequests.isEmpty()) { 505 LogUtils.d(TAG, "Finished loading batch. %d still have to be loaded.", 506 mPendingRequests.size()); 507 requestLoading(); 508 } 509 Utils.traceEndSection(); 510 } 511 512 /** 513 * Stores the supplied bitmap in cache. 514 */ 515 private static void cacheBitmapHolder(final String cacheKey, final BitmapHolder holder) { 516 if (DEBUG) { 517 BitmapHolder prev = sBitmapHolderCache.get(cacheKey); 518 if (prev != null && prev.bytes != null) { 519 LogUtils.d(TAG, "Overwriting cache: key=" + cacheKey 520 + (prev.fresh ? " FRESH" : " stale")); 521 if (prev.fresh) { 522 sFreshCacheOverwrite.incrementAndGet(); 523 } else { 524 sStaleCacheOverwrite.incrementAndGet(); 525 } 526 } 527 LogUtils.d(TAG, "Caching data: key=" + cacheKey + ", " 528 + (holder.bytes == null ? "<null>" : btk(holder.bytes.length))); 529 } 530 531 sBitmapHolderCache.put(cacheKey, holder); 532 } 533 534 protected static void cacheBitmap(final BitmapIdentifier bitmapKey, final Bitmap bitmap) { 535 sBitmapCache.put(bitmapKey, bitmap); 536 } 537 538 // ComponentCallbacks2 539 @Override 540 public void onConfigurationChanged(Configuration newConfig) { 541 } 542 543 // ComponentCallbacks2 544 @Override 545 public void onLowMemory() { 546 } 547 548 // ComponentCallbacks2 549 @Override 550 public void onTrimMemory(int level) { 551 if (DEBUG) LogUtils.d(TAG, "onTrimMemory: " + level); 552 if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) { 553 // Clear the caches. Note all pending requests will be removed too. 554 clear(); 555 } 556 } 557 558 public void clear() { 559 if (DEBUG) LogUtils.d(TAG, "clear"); 560 mPendingRequests.clear(); 561 sBitmapHolderCache.evictAll(); 562 sBitmapCache.evictAll(); 563 } 564 565 /** 566 * Dump cache stats on logcat. 567 */ 568 private static void dumpStats() { 569 if (!DEBUG) { 570 return; 571 } 572 int numHolders = 0; 573 int rawBytes = 0; 574 int bitmapBytes = 0; 575 int numBitmaps = 0; 576 for (BitmapHolder h : sBitmapHolderCache.snapshot().values()) { 577 numHolders++; 578 if (h.bytes != null) { 579 rawBytes += h.bytes.length; 580 numBitmaps++; 581 } 582 } 583 LogUtils.d(TAG, 584 "L1: " + btk(rawBytes) + " + " + btk(bitmapBytes) + " = " 585 + btk(rawBytes + bitmapBytes) + ", " + numHolders + " holders, " 586 + numBitmaps + " bitmaps, avg: " + btk(safeDiv(rawBytes, numBitmaps))); 587 LogUtils.d(TAG, "L1 Stats: %s, overwrite: fresh=%s stale=%s", sBitmapHolderCache, 588 sFreshCacheOverwrite.get(), sStaleCacheOverwrite.get()); 589 590 numBitmaps = 0; 591 bitmapBytes = 0; 592 for (Bitmap b : sBitmapCache.snapshot().values()) { 593 numBitmaps++; 594 bitmapBytes += b.getByteCount(); 595 } 596 LogUtils.d(TAG, "L2: " + btk(bitmapBytes) + ", " + numBitmaps + " bitmaps" + ", avg: " 597 + btk(safeDiv(bitmapBytes, numBitmaps))); 598 // We don't get from L2 cache, so L2 stats is meaningless. 599 } 600 601 /** Converts bytes to K bytes, rounding up. Used only for debug log. */ 602 private static String btk(int bytes) { 603 return ((bytes + 1023) / 1024) + "K"; 604 } 605 606 private static final int safeDiv(int dividend, int divisor) { 607 return (divisor == 0) ? 0 : (dividend / divisor); 608 } 609 610 public static abstract class PhotoIdentifier implements Comparable<PhotoIdentifier> { 611 /** 612 * If this returns false, the PhotoManager will not attempt to load the 613 * bitmap. Instead, the default image provider will be used. 614 */ 615 public abstract boolean isValid(); 616 617 /** 618 * Identifies this request. 619 */ 620 public abstract Object getKey(); 621 622 /** 623 * Replacement key to try to load from cache instead of drawing the default image. This 624 * is useful when we've already loaded a SIMPLE rendition, and are now loading the BEST 625 * rendition. We want the BEST image to appear seamlessly on top of the existing SIMPLE 626 * image. 627 */ 628 public Object getKeyToShowInsteadOfDefault() { 629 return null; 630 } 631 } 632 633 /** 634 * The thread that performs loading of photos from the database. 635 */ 636 protected abstract class PhotoLoaderThread extends HandlerThread implements Callback { 637 638 /** 639 * Return photos mapped from {@link Request#getKey()} to the photo for 640 * that request. 641 */ 642 protected abstract Map<String, BitmapHolder> loadPhotos(Collection<Request> requests); 643 644 private static final int MESSAGE_LOAD_PHOTOS = 0; 645 646 private final ContentResolver mResolver; 647 648 private Handler mLoaderThreadHandler; 649 650 public PhotoLoaderThread(ContentResolver resolver) { 651 super(LOADER_THREAD_NAME, Process.THREAD_PRIORITY_BACKGROUND); 652 mResolver = resolver; 653 } 654 655 protected ContentResolver getResolver() { 656 return mResolver; 657 } 658 659 public void ensureHandler() { 660 if (mLoaderThreadHandler == null) { 661 mLoaderThreadHandler = new Handler(getLooper(), this); 662 } 663 } 664 665 /** 666 * Sends a message to this thread to load requested photos. Cancels a preloading 667 * request, if any: we don't want preloading to impede loading of the photos 668 * we need to display now. 669 */ 670 public void requestLoading() { 671 ensureHandler(); 672 mLoaderThreadHandler.sendEmptyMessage(MESSAGE_LOAD_PHOTOS); 673 } 674 675 /** 676 * Receives the above message, loads photos and then sends a message 677 * to the main thread to process them. 678 */ 679 @Override 680 public boolean handleMessage(Message msg) { 681 switch (msg.what) { 682 case MESSAGE_LOAD_PHOTOS: 683 loadPhotosInBackground(); 684 break; 685 } 686 return true; 687 } 688 689 /** 690 * Subclasses may specify the maximum number of requests to be given at a time to 691 * #loadPhotos(). For batch count N, the UI will be updated with up to N images at a time. 692 * 693 * @return A positive integer if you would like to limit the number of 694 * items in a single batch. 695 */ 696 protected int getMaxBatchCount() { 697 return -1; 698 } 699 700 private void loadPhotosInBackground() { 701 Utils.traceBeginSection("pre processing"); 702 final Collection<Request> loadRequests = new HashSet<PhotoManager.Request>(); 703 final Collection<Request> decodeRequests = new HashSet<PhotoManager.Request>(); 704 final PriorityQueue<Request> requests; 705 synchronized (mPendingRequests) { 706 requests = new PriorityQueue<Request>(mPendingRequests.values()); 707 } 708 709 int batchCount = 0; 710 int maxBatchCount = getMaxBatchCount(); 711 while (!requests.isEmpty()) { 712 Request request = requests.poll(); 713 final BitmapHolder holder = sBitmapHolderCache 714 .get(request.getKey()); 715 if (holder == null || holder.bytes == null || !holder.fresh || !isSizeCompatible( 716 holder.width, holder.height, request.bitmapKey.w, request.bitmapKey.h)) { 717 loadRequests.add(request); 718 decodeRequests.add(request); 719 batchCount++; 720 721 final Message msg = Message.obtain(); 722 msg.what = MESSAGE_PHOTO_LOADING; 723 msg.arg1 = request.hashCode(); 724 mMainThreadHandler.sendMessage(msg); 725 } else { 726 // Even if the image load is already done, this particular decode configuration 727 // may not yet have run. Be sure to add it to the queue. 728 if (sBitmapCache.get(request.bitmapKey) == null) { 729 decodeRequests.add(request); 730 } 731 } 732 request.attempts++; 733 if (maxBatchCount > 0 && batchCount >= maxBatchCount) { 734 break; 735 } 736 } 737 Utils.traceEndSection(); 738 739 Utils.traceBeginSection("load photos"); 740 // Ask subclass to do the actual loading 741 final Map<String, BitmapHolder> photosMap = loadPhotos(loadRequests); 742 Utils.traceEndSection(); 743 744 if (DEBUG) { 745 LogUtils.d(TAG, 746 "worker thread completed read request batch. inputN=%s outputN=%s", 747 loadRequests.size(), 748 photosMap.size()); 749 } 750 Utils.traceBeginSection("post processing"); 751 for (String cacheKey : photosMap.keySet()) { 752 if (DEBUG) { 753 LogUtils.d(TAG, 754 "worker thread completed read request key=%s byteCount=%s thread=%s", 755 cacheKey, 756 photosMap.get(cacheKey) == null ? 0 757 : photosMap.get(cacheKey).bytes.length, 758 Thread.currentThread()); 759 } 760 cacheBitmapHolder(cacheKey, photosMap.get(cacheKey)); 761 } 762 763 for (Request r : decodeRequests) { 764 if (sBitmapCache.get(r.bitmapKey) != null) { 765 continue; 766 } 767 768 final Object cacheKey = r.getKey(); 769 final BitmapHolder holder = sBitmapHolderCache.get(cacheKey); 770 if (holder == null || holder.bytes == null || !holder.fresh || !isSizeCompatible( 771 holder.width, holder.height, r.bitmapKey.w, r.bitmapKey.h)) { 772 continue; 773 } 774 775 final int w = r.bitmapKey.w; 776 final int h = r.bitmapKey.h; 777 final byte[] src = holder.bytes; 778 779 if (w == 0 || h == 0) { 780 LogUtils.e(TAG, new Error(), "bad dimensions for request=%s w/h=%s/%s", 781 r, w, h); 782 } 783 784 final Bitmap decoded = BitmapUtil.decodeByteArrayWithCenterCrop(src, w, h); 785 if (DEBUG) { 786 LogUtils.i(TAG, 787 "worker thread completed decode bmpKey=%s decoded=%s holder=%s", 788 r.bitmapKey, decoded, holder); 789 } 790 791 if (decoded != null) { 792 cacheBitmap(r.bitmapKey, decoded); 793 } 794 } 795 Utils.traceEndSection(); 796 797 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED); 798 } 799 800 protected String createInQuery(String value, int itemCount) { 801 // Build first query 802 StringBuilder query = new StringBuilder().append(value + " IN ("); 803 appendQuestionMarks(query, itemCount); 804 query.append(')'); 805 return query.toString(); 806 } 807 808 protected void appendQuestionMarks(StringBuilder query, int itemCount) { 809 boolean first = true; 810 for (int i = 0; i < itemCount; i++) { 811 if (first) { 812 first = false; 813 } else { 814 query.append(','); 815 } 816 query.append('?'); 817 } 818 } 819 } 820 821 /** 822 * An object to uniquely identify a combination of (Request + decoded size). Multiple requests 823 * may require the same src image, but want to decode it into different sizes. 824 */ 825 public static final class BitmapIdentifier { 826 public final Object key; 827 public final int w; 828 public final int h; 829 830 // OK to be static as long as all Requests are created on the same 831 // thread 832 private static final ImageCanvas.Dimensions sWorkDims = new ImageCanvas.Dimensions(); 833 834 public static BitmapIdentifier getBitmapKey(PhotoIdentifier id, ImageCanvas view, 835 ImageCanvas.Dimensions dimensions) { 836 final int width; 837 final int height; 838 if (dimensions != null) { 839 width = dimensions.width; 840 height = dimensions.height; 841 } else { 842 view.getDesiredDimensions(id.getKey(), sWorkDims); 843 width = sWorkDims.width; 844 height = sWorkDims.height; 845 } 846 return new BitmapIdentifier(id.getKey(), width, height); 847 } 848 849 public BitmapIdentifier(Object key, int w, int h) { 850 this.key = key; 851 this.w = w; 852 this.h = h; 853 } 854 855 @Override 856 public int hashCode() { 857 int hash = 19; 858 hash = 31 * hash + key.hashCode(); 859 hash = 31 * hash + w; 860 hash = 31 * hash + h; 861 return hash; 862 } 863 864 @Override 865 public boolean equals(Object obj) { 866 if (obj == null || obj.getClass() != getClass()) { 867 return false; 868 } else if (obj == this) { 869 return true; 870 } 871 final BitmapIdentifier o = (BitmapIdentifier) obj; 872 return Objects.equal(key, o.key) && w == o.w && h == o.h; 873 } 874 875 @Override 876 public String toString() { 877 final StringBuilder sb = new StringBuilder("{"); 878 sb.append(super.toString()); 879 sb.append(" key="); 880 sb.append(key); 881 sb.append(" w="); 882 sb.append(w); 883 sb.append(" h="); 884 sb.append(h); 885 sb.append("}"); 886 return sb.toString(); 887 } 888 } 889 890 /** 891 * A holder for a contact photo request. 892 */ 893 public final class Request implements Comparable<Request> { 894 private final int mRequestedExtent; 895 private final DefaultImageProvider mDefaultProvider; 896 private final PhotoIdentifier mPhotoIdentifier; 897 private final ImageCanvas mView; 898 public final BitmapIdentifier bitmapKey; 899 public final int viewGeneration; 900 public int attempts; 901 902 private Request(final PhotoIdentifier photoIdentifier, 903 final DefaultImageProvider defaultProvider, final ImageCanvas view, 904 final ImageCanvas.Dimensions dimensions) { 905 mPhotoIdentifier = photoIdentifier; 906 mRequestedExtent = -1; 907 mDefaultProvider = defaultProvider; 908 mView = view; 909 viewGeneration = view.getGeneration(); 910 911 bitmapKey = BitmapIdentifier.getBitmapKey(photoIdentifier, mView, dimensions); 912 } 913 914 public ImageCanvas getView() { 915 return mView; 916 } 917 918 public PhotoIdentifier getPhotoIdentifier() { 919 return mPhotoIdentifier; 920 } 921 922 /** 923 * @see PhotoIdentifier#getKey() 924 */ 925 public Object getKey() { 926 return mPhotoIdentifier.getKey(); 927 } 928 929 @Override 930 public int hashCode() { 931 return getHash(mPhotoIdentifier, mView); 932 } 933 934 @Override 935 public boolean equals(Object obj) { 936 if (this == obj) return true; 937 if (obj == null) return false; 938 if (getClass() != obj.getClass()) return false; 939 final Request that = (Request) obj; 940 if (mRequestedExtent != that.mRequestedExtent) return false; 941 if (!Objects.equal(mPhotoIdentifier, that.mPhotoIdentifier)) return false; 942 if (!Objects.equal(mView, that.mView)) return false; 943 // Don't compare equality of mDarkTheme because it is only used in the default contact 944 // photo case. When the contact does have a photo, the contact photo is the same 945 // regardless of mDarkTheme, so we shouldn't need to put the photo request on the queue 946 // twice. 947 return true; 948 } 949 950 @Override 951 public String toString() { 952 final StringBuilder sb = new StringBuilder("{"); 953 sb.append(super.toString()); 954 sb.append(" key="); 955 sb.append(getKey()); 956 sb.append(" id="); 957 sb.append(mPhotoIdentifier); 958 sb.append(" mView="); 959 sb.append(mView); 960 sb.append(" mExtent="); 961 sb.append(mRequestedExtent); 962 sb.append(" bitmapKey="); 963 sb.append(bitmapKey); 964 sb.append(" viewGeneration="); 965 sb.append(viewGeneration); 966 sb.append("}"); 967 return sb.toString(); 968 } 969 970 public void applyDefaultImage() { 971 if (mView.getGeneration() != viewGeneration) { 972 // This can legitimately happen when an ImageCanvas is reused and re-purposed to 973 // house a new set of images (e.g. by ListView recycling). 974 // Ignore this now-stale request. 975 if (DEBUG) { 976 LogUtils.d(TAG, 977 "ImageCanvas skipping applyDefaultImage; no longer contains" + 978 " item=%s canvas=%s", getKey(), mView); 979 } 980 } 981 mDefaultProvider.applyDefaultImage(mPhotoIdentifier, mView, mRequestedExtent); 982 } 983 984 @Override 985 public int compareTo(Request another) { 986 // Hold off on loading Requests which have failed before so it don't hold up others 987 if (attempts - another.attempts != 0) { 988 return attempts - another.attempts; 989 } 990 return mPhotoIdentifier.compareTo(another.mPhotoIdentifier); 991 } 992 } 993} 994