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