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