Home | History | Annotate | Download | only in photomanager
      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      */
     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