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