• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.mail.photomanager;
18 
19 import android.content.ComponentCallbacks2;
20 import android.content.ContentResolver;
21 import android.content.Context;
22 import android.content.res.Configuration;
23 import android.graphics.Bitmap;
24 import android.os.Handler;
25 import android.os.Handler.Callback;
26 import android.os.HandlerThread;
27 import android.os.Message;
28 import android.os.Process;
29 import android.util.LruCache;
30 
31 import com.android.mail.ui.ImageCanvas;
32 import com.android.mail.utils.LogUtils;
33 import com.android.mail.utils.Utils;
34 import com.google.common.base.Objects;
35 import com.google.common.collect.Lists;
36 
37 import java.util.Collection;
38 import java.util.Collections;
39 import java.util.HashMap;
40 import java.util.HashSet;
41 import java.util.List;
42 import java.util.Map;
43 import java.util.PriorityQueue;
44 import java.util.concurrent.atomic.AtomicInteger;
45 
46 /**
47  * Asynchronously loads photos and maintains a cache of photos
48  */
49 public abstract class PhotoManager implements ComponentCallbacks2, Callback {
50     /**
51      * Get the default image provider that draws while the photo is being
52      * loaded.
53      */
getDefaultImageProvider()54     protected abstract DefaultImageProvider getDefaultImageProvider();
55 
56     /**
57      * Generate a hashcode unique to each request.
58      */
getHash(PhotoIdentifier id, ImageCanvas view)59     protected abstract int getHash(PhotoIdentifier id, ImageCanvas view);
60 
61     /**
62      * Return a specific implementation of PhotoLoaderThread.
63      */
getLoaderThread(ContentResolver contentResolver)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      */
onImageDrawn(final Request request, final boolean success)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      */
onImageLoadStarted(final Request request)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      */
isSizeCompatible(int prevWidth, int prevHeight, int newWidth, int newHeight)93     protected boolean isSizeCompatible(int prevWidth, int prevHeight, int newWidth, int newHeight) {
94         return true;
95     }
96 
getContext()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          */
applyDefaultImage(PhotoIdentifier id, ImageCanvas view, int extent)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 
BitmapHolder(byte[] bytes, int width, int height)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
toString()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         };
LogUtils.i(TAG, "Cache adj: " + cacheSizeAdjustment)224         LogUtils.i(TAG, "Cache adj: " + cacheSizeAdjustment);
225         if (DEBUG) {
LogUtils.d(TAG, "Cache size: " + btk(sBitmapHolderCache.maxSize()) + " + " + btk(sBitmapCache.maxSize()))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 
PhotoManager(Context context)262     public PhotoManager(Context context) {
263         mContext = context;
264     }
265 
loadThumbnail(PhotoIdentifier id, ImageCanvas view)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      */
loadThumbnail(final PhotoIdentifier id, final ImageCanvas view, final ImageCanvas.Dimensions dimensions)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 
loadPhoto(int hashCode, Request request)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      */
removePhoto(int hashcode)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 
ensureLoaderThread()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      */
loadCachedPhoto(final Request request, final boolean afterLoaderThreadFinished)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      */
getCachedPhoto(BitmapIdentifier bitmapKey)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      */
pause()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      */
resume()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      */
requestLoading()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
handleMessage(final Message msg)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      */
processLoadedImages()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      */
cacheBitmapHolder(final String cacheKey, final BitmapHolder holder)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 
cacheBitmap(final BitmapIdentifier bitmapKey, final Bitmap bitmap)534     protected static void cacheBitmap(final BitmapIdentifier bitmapKey, final Bitmap bitmap) {
535         sBitmapCache.put(bitmapKey, bitmap);
536     }
537 
538     // ComponentCallbacks2
539     @Override
onConfigurationChanged(Configuration newConfig)540     public void onConfigurationChanged(Configuration newConfig) {
541     }
542 
543     // ComponentCallbacks2
544     @Override
onLowMemory()545     public void onLowMemory() {
546     }
547 
548     // ComponentCallbacks2
549     @Override
onTrimMemory(int level)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 
clear()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      */
dumpStats()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. */
btk(int bytes)602     private static String btk(int bytes) {
603         return ((bytes + 1023) / 1024) + "K";
604     }
605 
safeDiv(int dividend, int divisor)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          */
isValid()615         public abstract boolean isValid();
616 
617         /**
618          * Identifies this request.
619          */
getKey()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          */
getKeyToShowInsteadOfDefault()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          */
loadPhotos(Collection<Request> requests)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 
PhotoLoaderThread(ContentResolver resolver)650         public PhotoLoaderThread(ContentResolver resolver) {
651             super(LOADER_THREAD_NAME, Process.THREAD_PRIORITY_BACKGROUND);
652             mResolver = resolver;
653         }
654 
getResolver()655         protected ContentResolver getResolver() {
656             return mResolver;
657         }
658 
ensureHandler()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          */
requestLoading()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
handleMessage(Message msg)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          */
getMaxBatchCount()696         protected int getMaxBatchCount() {
697             return -1;
698         }
699 
loadPhotosInBackground()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 
createInQuery(String value, int itemCount)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 
appendQuestionMarks(StringBuilder query, int itemCount)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 
getBitmapKey(PhotoIdentifier id, ImageCanvas view, ImageCanvas.Dimensions dimensions)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 
BitmapIdentifier(Object key, int w, int h)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
hashCode()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
equals(Object obj)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
toString()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 
Request(final PhotoIdentifier photoIdentifier, final DefaultImageProvider defaultProvider, final ImageCanvas view, final ImageCanvas.Dimensions dimensions)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 
getView()914         public ImageCanvas getView() {
915             return mView;
916         }
917 
getPhotoIdentifier()918         public PhotoIdentifier getPhotoIdentifier() {
919             return mPhotoIdentifier;
920         }
921 
922         /**
923          * @see PhotoIdentifier#getKey()
924          */
getKey()925         public Object getKey() {
926             return mPhotoIdentifier.getKey();
927         }
928 
929         @Override
hashCode()930         public int hashCode() {
931             return getHash(mPhotoIdentifier, mView);
932         }
933 
934         @Override
equals(Object obj)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
toString()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 
applyDefaultImage()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
compareTo(Request another)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