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.startTransition(FADE_TRANSITION_DURATION);
    577         } else {
    578             view.setImageBitmap(cachedBitmap);
    579         }
    580 
    581         // Put the bitmap in the LRU cache. But only do this for images that are small enough
    582         // (we require that at least six of those can be cached at the same time)
    583         if (cachedBitmap.getByteCount() < mBitmapCache.maxSize() / 6) {
    584             mBitmapCache.put(request.getKey(), cachedBitmap);
    585         }
    586 
    587         // Soften the reference
    588         holder.bitmap = null;
    589 
    590         return holder.fresh;
    591     }
    592 
    593     /**
    594      * If necessary, decodes bytes stored in the holder to Bitmap.  As long as the
    595      * bitmap is held either by {@link #mBitmapCache} or by a soft reference in
    596      * the holder, it will not be necessary to decode the bitmap.
    597      */
    598     private static void inflateBitmap(BitmapHolder holder, int requestedExtent) {
    599         final int sampleSize =
    600                 BitmapUtil.findOptimalSampleSize(holder.originalSmallerExtent, requestedExtent);
    601         byte[] bytes = holder.bytes;
    602         if (bytes == null || bytes.length == 0) {
    603             return;
    604         }
    605 
    606         if (sampleSize == holder.decodedSampleSize) {
    607             // Check the soft reference.  If will be retained if the bitmap is also
    608             // in the LRU cache, so we don't need to check the LRU cache explicitly.
    609             if (holder.bitmapRef != null) {
    610                 holder.bitmap = holder.bitmapRef.get();
    611                 if (holder.bitmap != null) {
    612                     return;
    613                 }
    614             }
    615         }
    616 
    617         try {
    618             Bitmap bitmap = BitmapUtil.decodeBitmapFromBytes(bytes, sampleSize);
    619 
    620             // make bitmap mutable and draw size onto it
    621             if (DEBUG_SIZES) {
    622                 Bitmap original = bitmap;
    623                 bitmap = bitmap.copy(bitmap.getConfig(), true);
    624                 original.recycle();
    625                 Canvas canvas = new Canvas(bitmap);
    626                 Paint paint = new Paint();
    627                 paint.setTextSize(16);
    628                 paint.setColor(Color.BLUE);
    629                 paint.setStyle(Style.FILL);
    630                 canvas.drawRect(0.0f, 0.0f, 50.0f, 20.0f, paint);
    631                 paint.setColor(Color.WHITE);
    632                 paint.setAntiAlias(true);
    633                 canvas.drawText(bitmap.getWidth() + "/" + sampleSize, 0, 15, paint);
    634             }
    635 
    636             holder.decodedSampleSize = sampleSize;
    637             holder.bitmap = bitmap;
    638             holder.bitmapRef = new SoftReference<Bitmap>(bitmap);
    639             if (DEBUG) {
    640                 Log.d(TAG, "inflateBitmap " + btk(bytes.length) + " -> "
    641                         + bitmap.getWidth() + "x" + bitmap.getHeight()
    642                         + ", " + btk(bitmap.getByteCount()));
    643             }
    644         } catch (OutOfMemoryError e) {
    645             // Do nothing - the photo will appear to be missing
    646         }
    647     }
    648 
    649     public void clear() {
    650         if (DEBUG) Log.d(TAG, "clear");
    651         mPendingRequests.clear();
    652         mBitmapHolderCache.evictAll();
    653         mBitmapCache.evictAll();
    654     }
    655 
    656     @Override
    657     public void pause() {
    658         mPaused = true;
    659     }
    660 
    661     @Override
    662     public void resume() {
    663         mPaused = false;
    664         if (DEBUG) dumpStats();
    665         if (!mPendingRequests.isEmpty()) {
    666             requestLoading();
    667         }
    668     }
    669 
    670     /**
    671      * Sends a message to this thread itself to start loading images.  If the current
    672      * view contains multiple image views, all of those image views will get a chance
    673      * to request their respective photos before any of those requests are executed.
    674      * This allows us to load images in bulk.
    675      */
    676     private void requestLoading() {
    677         if (!mLoadingRequested) {
    678             mLoadingRequested = true;
    679             mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING);
    680         }
    681     }
    682 
    683     /**
    684      * Processes requests on the main thread.
    685      */
    686     @Override
    687     public boolean handleMessage(Message msg) {
    688         switch (msg.what) {
    689             case MESSAGE_REQUEST_LOADING: {
    690                 mLoadingRequested = false;
    691                 if (!mPaused) {
    692                     ensureLoaderThread();
    693                     mLoaderThread.requestLoading();
    694                 }
    695                 return true;
    696             }
    697 
    698             case MESSAGE_PHOTOS_LOADED: {
    699                 if (!mPaused) {
    700                     processLoadedImages();
    701                 }
    702                 if (DEBUG) dumpStats();
    703                 return true;
    704             }
    705         }
    706         return false;
    707     }
    708 
    709     public void ensureLoaderThread() {
    710         if (mLoaderThread == null) {
    711             mLoaderThread = new LoaderThread(mContext.getContentResolver());
    712             mLoaderThread.start();
    713         }
    714     }
    715 
    716     /**
    717      * Goes over pending loading requests and displays loaded photos.  If some of the
    718      * photos still haven't been loaded, sends another request for image loading.
    719      */
    720     private void processLoadedImages() {
    721         Iterator<ImageView> iterator = mPendingRequests.keySet().iterator();
    722         while (iterator.hasNext()) {
    723             ImageView view = iterator.next();
    724             Request key = mPendingRequests.get(view);
    725             boolean loaded = loadCachedPhoto(view, key, true);
    726             if (loaded) {
    727                 iterator.remove();
    728             }
    729         }
    730 
    731         softenCache();
    732 
    733         if (!mPendingRequests.isEmpty()) {
    734             requestLoading();
    735         }
    736     }
    737 
    738     /**
    739      * Removes strong references to loaded bitmaps to allow them to be garbage collected
    740      * if needed.  Some of the bitmaps will still be retained by {@link #mBitmapCache}.
    741      */
    742     private void softenCache() {
    743         for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
    744             holder.bitmap = null;
    745         }
    746     }
    747 
    748     /**
    749      * Stores the supplied bitmap in cache.
    750      */
    751     private void cacheBitmap(Object key, byte[] bytes, boolean preloading, int requestedExtent) {
    752         if (DEBUG) {
    753             BitmapHolder prev = mBitmapHolderCache.get(key);
    754             if (prev != null && prev.bytes != null) {
    755                 Log.d(TAG, "Overwriting cache: key=" + key + (prev.fresh ? " FRESH" : " stale"));
    756                 if (prev.fresh) {
    757                     mFreshCacheOverwrite.incrementAndGet();
    758                 } else {
    759                     mStaleCacheOverwrite.incrementAndGet();
    760                 }
    761             }
    762             Log.d(TAG, "Caching data: key=" + key + ", " +
    763                     (bytes == null ? "<null>" : btk(bytes.length)));
    764         }
    765         BitmapHolder holder = new BitmapHolder(bytes,
    766                 bytes == null ? -1 : BitmapUtil.getSmallerExtentFromBytes(bytes));
    767 
    768         // Unless this image is being preloaded, decode it right away while
    769         // we are still on the background thread.
    770         if (!preloading) {
    771             inflateBitmap(holder, requestedExtent);
    772         }
    773 
    774         mBitmapHolderCache.put(key, holder);
    775         mBitmapHolderCacheAllUnfresh = false;
    776     }
    777 
    778     @Override
    779     public void cacheBitmap(Uri photoUri, Bitmap bitmap, byte[] photoBytes) {
    780         final int smallerExtent = Math.min(bitmap.getWidth(), bitmap.getHeight());
    781         // We can pretend here that the extent of the photo was the size that we originally
    782         // requested
    783         Request request = Request.createFromUri(photoUri, smallerExtent, false, DEFAULT_AVATAR);
    784         BitmapHolder holder = new BitmapHolder(photoBytes, smallerExtent);
    785         holder.bitmapRef = new SoftReference<Bitmap>(bitmap);
    786         mBitmapHolderCache.put(request.getKey(), holder);
    787         mBitmapHolderCacheAllUnfresh = false;
    788         mBitmapCache.put(request.getKey(), bitmap);
    789     }
    790 
    791     /**
    792      * Populates an array of photo IDs that need to be loaded. Also decodes bitmaps that we have
    793      * already loaded
    794      */
    795     private void obtainPhotoIdsAndUrisToLoad(Set<Long> photoIds,
    796             Set<String> photoIdsAsStrings, Set<Request> uris) {
    797         photoIds.clear();
    798         photoIdsAsStrings.clear();
    799         uris.clear();
    800 
    801         boolean jpegsDecoded = false;
    802 
    803         /*
    804          * Since the call is made from the loader thread, the map could be
    805          * changing during the iteration. That's not really a problem:
    806          * ConcurrentHashMap will allow those changes to happen without throwing
    807          * exceptions. Since we may miss some requests in the situation of
    808          * concurrent change, we will need to check the map again once loading
    809          * is complete.
    810          */
    811         Iterator<Request> iterator = mPendingRequests.values().iterator();
    812         while (iterator.hasNext()) {
    813             Request request = iterator.next();
    814             final BitmapHolder holder = mBitmapHolderCache.get(request.getKey());
    815             if (holder != null && holder.bytes != null && holder.fresh &&
    816                     (holder.bitmapRef == null || holder.bitmapRef.get() == null)) {
    817                 // This was previously loaded but we don't currently have the inflated Bitmap
    818                 inflateBitmap(holder, request.getRequestedExtent());
    819                 jpegsDecoded = true;
    820             } else {
    821                 if (holder == null || !holder.fresh) {
    822                     if (request.isUriRequest()) {
    823                         uris.add(request);
    824                     } else {
    825                         photoIds.add(request.getId());
    826                         photoIdsAsStrings.add(String.valueOf(request.mId));
    827                     }
    828                 }
    829             }
    830         }
    831 
    832         if (jpegsDecoded) mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
    833     }
    834 
    835     /**
    836      * The thread that performs loading of photos from the database.
    837      */
    838     private class LoaderThread extends HandlerThread implements Callback {
    839         private static final int BUFFER_SIZE = 1024*16;
    840         private static final int MESSAGE_PRELOAD_PHOTOS = 0;
    841         private static final int MESSAGE_LOAD_PHOTOS = 1;
    842 
    843         /**
    844          * A pause between preload batches that yields to the UI thread.
    845          */
    846         private static final int PHOTO_PRELOAD_DELAY = 1000;
    847 
    848         /**
    849          * Number of photos to preload per batch.
    850          */
    851         private static final int PRELOAD_BATCH = 25;
    852 
    853         /**
    854          * Maximum number of photos to preload.  If the cache size is 2Mb and
    855          * the expected average size of a photo is 4kb, then this number should be 2Mb/4kb = 500.
    856          */
    857         private static final int MAX_PHOTOS_TO_PRELOAD = 100;
    858 
    859         private final ContentResolver mResolver;
    860         private final StringBuilder mStringBuilder = new StringBuilder();
    861         private final Set<Long> mPhotoIds = Sets.newHashSet();
    862         private final Set<String> mPhotoIdsAsStrings = Sets.newHashSet();
    863         private final Set<Request> mPhotoUris = Sets.newHashSet();
    864         private final List<Long> mPreloadPhotoIds = Lists.newArrayList();
    865 
    866         private Handler mLoaderThreadHandler;
    867         private byte mBuffer[];
    868 
    869         private static final int PRELOAD_STATUS_NOT_STARTED = 0;
    870         private static final int PRELOAD_STATUS_IN_PROGRESS = 1;
    871         private static final int PRELOAD_STATUS_DONE = 2;
    872 
    873         private int mPreloadStatus = PRELOAD_STATUS_NOT_STARTED;
    874 
    875         public LoaderThread(ContentResolver resolver) {
    876             super(LOADER_THREAD_NAME);
    877             mResolver = resolver;
    878         }
    879 
    880         public void ensureHandler() {
    881             if (mLoaderThreadHandler == null) {
    882                 mLoaderThreadHandler = new Handler(getLooper(), this);
    883             }
    884         }
    885 
    886         /**
    887          * Kicks off preloading of the next batch of photos on the background thread.
    888          * Preloading will happen after a delay: we want to yield to the UI thread
    889          * as much as possible.
    890          * <p>
    891          * If preloading is already complete, does nothing.
    892          */
    893         public void requestPreloading() {
    894             if (mPreloadStatus == PRELOAD_STATUS_DONE) {
    895                 return;
    896             }
    897 
    898             ensureHandler();
    899             if (mLoaderThreadHandler.hasMessages(MESSAGE_LOAD_PHOTOS)) {
    900                 return;
    901             }
    902 
    903             mLoaderThreadHandler.sendEmptyMessageDelayed(
    904                     MESSAGE_PRELOAD_PHOTOS, PHOTO_PRELOAD_DELAY);
    905         }
    906 
    907         /**
    908          * Sends a message to this thread to load requested photos.  Cancels a preloading
    909          * request, if any: we don't want preloading to impede loading of the photos
    910          * we need to display now.
    911          */
    912         public void requestLoading() {
    913             ensureHandler();
    914             mLoaderThreadHandler.removeMessages(MESSAGE_PRELOAD_PHOTOS);
    915             mLoaderThreadHandler.sendEmptyMessage(MESSAGE_LOAD_PHOTOS);
    916         }
    917 
    918         /**
    919          * Receives the above message, loads photos and then sends a message
    920          * to the main thread to process them.
    921          */
    922         @Override
    923         public boolean handleMessage(Message msg) {
    924             switch (msg.what) {
    925                 case MESSAGE_PRELOAD_PHOTOS:
    926                     preloadPhotosInBackground();
    927                     break;
    928                 case MESSAGE_LOAD_PHOTOS:
    929                     loadPhotosInBackground();
    930                     break;
    931             }
    932             return true;
    933         }
    934 
    935         /**
    936          * The first time it is called, figures out which photos need to be preloaded.
    937          * Each subsequent call preloads the next batch of photos and requests
    938          * another cycle of preloading after a delay.  The whole process ends when
    939          * we either run out of photos to preload or fill up cache.
    940          */
    941         private void preloadPhotosInBackground() {
    942             if (mPreloadStatus == PRELOAD_STATUS_DONE) {
    943                 return;
    944             }
    945 
    946             if (mPreloadStatus == PRELOAD_STATUS_NOT_STARTED) {
    947                 queryPhotosForPreload();
    948                 if (mPreloadPhotoIds.isEmpty()) {
    949                     mPreloadStatus = PRELOAD_STATUS_DONE;
    950                 } else {
    951                     mPreloadStatus = PRELOAD_STATUS_IN_PROGRESS;
    952                 }
    953                 requestPreloading();
    954                 return;
    955             }
    956 
    957             if (mBitmapHolderCache.size() > mBitmapHolderCacheRedZoneBytes) {
    958                 mPreloadStatus = PRELOAD_STATUS_DONE;
    959                 return;
    960             }
    961 
    962             mPhotoIds.clear();
    963             mPhotoIdsAsStrings.clear();
    964 
    965             int count = 0;
    966             int preloadSize = mPreloadPhotoIds.size();
    967             while(preloadSize > 0 && mPhotoIds.size() < PRELOAD_BATCH) {
    968                 preloadSize--;
    969                 count++;
    970                 Long photoId = mPreloadPhotoIds.get(preloadSize);
    971                 mPhotoIds.add(photoId);
    972                 mPhotoIdsAsStrings.add(photoId.toString());
    973                 mPreloadPhotoIds.remove(preloadSize);
    974             }
    975 
    976             loadThumbnails(true);
    977 
    978             if (preloadSize == 0) {
    979                 mPreloadStatus = PRELOAD_STATUS_DONE;
    980             }
    981 
    982             Log.v(TAG, "Preloaded " + count + " photos.  Cached bytes: "
    983                     + mBitmapHolderCache.size());
    984 
    985             requestPreloading();
    986         }
    987 
    988         private void queryPhotosForPreload() {
    989             Cursor cursor = null;
    990             try {
    991                 Uri uri = Contacts.CONTENT_URI.buildUpon().appendQueryParameter(
    992                         ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT))
    993                         .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
    994                                 String.valueOf(MAX_PHOTOS_TO_PRELOAD))
    995                         .build();
    996                 cursor = mResolver.query(uri, new String[] { Contacts.PHOTO_ID },
    997                         Contacts.PHOTO_ID + " NOT NULL AND " + Contacts.PHOTO_ID + "!=0",
    998                         null,
    999                         Contacts.STARRED + " DESC, " + Contacts.LAST_TIME_CONTACTED + " DESC");
   1000 
   1001                 if (cursor != null) {
   1002                     while (cursor.moveToNext()) {
   1003                         // Insert them in reverse order, because we will be taking
   1004                         // them from the end of the list for loading.
   1005                         mPreloadPhotoIds.add(0, cursor.getLong(0));
   1006                     }
   1007                 }
   1008             } finally {
   1009                 if (cursor != null) {
   1010                     cursor.close();
   1011                 }
   1012             }
   1013         }
   1014 
   1015         private void loadPhotosInBackground() {
   1016             obtainPhotoIdsAndUrisToLoad(mPhotoIds, mPhotoIdsAsStrings, mPhotoUris);
   1017             loadThumbnails(false);
   1018             loadUriBasedPhotos();
   1019             requestPreloading();
   1020         }
   1021 
   1022         /** Loads thumbnail photos with ids */
   1023         private void loadThumbnails(boolean preloading) {
   1024             if (mPhotoIds.isEmpty()) {
   1025                 return;
   1026             }
   1027 
   1028             // Remove loaded photos from the preload queue: we don't want
   1029             // the preloading process to load them again.
   1030             if (!preloading && mPreloadStatus == PRELOAD_STATUS_IN_PROGRESS) {
   1031                 for (Long id : mPhotoIds) {
   1032                     mPreloadPhotoIds.remove(id);
   1033                 }
   1034                 if (mPreloadPhotoIds.isEmpty()) {
   1035                     mPreloadStatus = PRELOAD_STATUS_DONE;
   1036                 }
   1037             }
   1038 
   1039             mStringBuilder.setLength(0);
   1040             mStringBuilder.append(Photo._ID + " IN(");
   1041             for (int i = 0; i < mPhotoIds.size(); i++) {
   1042                 if (i != 0) {
   1043                     mStringBuilder.append(',');
   1044                 }
   1045                 mStringBuilder.append('?');
   1046             }
   1047             mStringBuilder.append(')');
   1048 
   1049             Cursor cursor = null;
   1050             try {
   1051                 if (DEBUG) Log.d(TAG, "Loading " + TextUtils.join(",", mPhotoIdsAsStrings));
   1052                 cursor = mResolver.query(Data.CONTENT_URI,
   1053                         COLUMNS,
   1054                         mStringBuilder.toString(),
   1055                         mPhotoIdsAsStrings.toArray(EMPTY_STRING_ARRAY),
   1056                         null);
   1057 
   1058                 if (cursor != null) {
   1059                     while (cursor.moveToNext()) {
   1060                         Long id = cursor.getLong(0);
   1061                         byte[] bytes = cursor.getBlob(1);
   1062                         cacheBitmap(id, bytes, preloading, -1);
   1063                         mPhotoIds.remove(id);
   1064                     }
   1065                 }
   1066             } finally {
   1067                 if (cursor != null) {
   1068                     cursor.close();
   1069                 }
   1070             }
   1071 
   1072             // Remaining photos were not found in the contacts database (but might be in profile).
   1073             for (Long id : mPhotoIds) {
   1074                 if (ContactsContract.isProfileId(id)) {
   1075                     Cursor profileCursor = null;
   1076                     try {
   1077                         profileCursor = mResolver.query(
   1078                                 ContentUris.withAppendedId(Data.CONTENT_URI, id),
   1079                                 COLUMNS, null, null, null);
   1080                         if (profileCursor != null && profileCursor.moveToFirst()) {
   1081                             cacheBitmap(profileCursor.getLong(0), profileCursor.getBlob(1),
   1082                                     preloading, -1);
   1083                         } else {
   1084                             // Couldn't load a photo this way either.
   1085                             cacheBitmap(id, null, preloading, -1);
   1086                         }
   1087                     } finally {
   1088                         if (profileCursor != null) {
   1089                             profileCursor.close();
   1090                         }
   1091                     }
   1092                 } else {
   1093                     // Not a profile photo and not found - mark the cache accordingly
   1094                     cacheBitmap(id, null, preloading, -1);
   1095                 }
   1096             }
   1097 
   1098             mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
   1099         }
   1100 
   1101         /**
   1102          * Loads photos referenced with Uris. Those can be remote thumbnails
   1103          * (from directory searches), display photos etc
   1104          */
   1105         private void loadUriBasedPhotos() {
   1106             for (Request uriRequest : mPhotoUris) {
   1107                 Uri uri = uriRequest.getUri();
   1108                 if (mBuffer == null) {
   1109                     mBuffer = new byte[BUFFER_SIZE];
   1110                 }
   1111                 try {
   1112                     if (DEBUG) Log.d(TAG, "Loading " + uri);
   1113                     final String scheme = uri.getScheme();
   1114                     InputStream is = null;
   1115                     if (scheme.equals("http") || scheme.equals("https")) {
   1116                         is = new URL(uri.toString()).openStream();
   1117                     } else {
   1118                         is = mResolver.openInputStream(uri);
   1119                     }
   1120                     if (is != null) {
   1121                         ByteArrayOutputStream baos = new ByteArrayOutputStream();
   1122                         try {
   1123                             int size;
   1124                             while ((size = is.read(mBuffer)) != -1) {
   1125                                 baos.write(mBuffer, 0, size);
   1126                             }
   1127                         } finally {
   1128                             is.close();
   1129                         }
   1130                         cacheBitmap(uri, baos.toByteArray(), false,
   1131                                 uriRequest.getRequestedExtent());
   1132                         mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
   1133                     } else {
   1134                         Log.v(TAG, "Cannot load photo " + uri);
   1135                         cacheBitmap(uri, null, false, uriRequest.getRequestedExtent());
   1136                     }
   1137                 } catch (Exception ex) {
   1138                     Log.v(TAG, "Cannot load photo " + uri, ex);
   1139                     cacheBitmap(uri, null, false, uriRequest.getRequestedExtent());
   1140                 }
   1141             }
   1142         }
   1143     }
   1144 
   1145     /**
   1146      * A holder for either a Uri or an id and a flag whether this was requested for the dark or
   1147      * light theme
   1148      */
   1149     private static final class Request {
   1150         private final long mId;
   1151         private final Uri mUri;
   1152         private final boolean mDarkTheme;
   1153         private final int mRequestedExtent;
   1154         private final DefaultImageProvider mDefaultProvider;
   1155 
   1156         private Request(long id, Uri uri, int requestedExtent, boolean darkTheme,
   1157                 DefaultImageProvider defaultProvider) {
   1158             mId = id;
   1159             mUri = uri;
   1160             mDarkTheme = darkTheme;
   1161             mRequestedExtent = requestedExtent;
   1162             mDefaultProvider = defaultProvider;
   1163         }
   1164 
   1165         public static Request createFromThumbnailId(long id, boolean darkTheme,
   1166                 DefaultImageProvider defaultProvider) {
   1167             return new Request(id, null /* no URI */, -1, darkTheme, defaultProvider);
   1168         }
   1169 
   1170         public static Request createFromUri(Uri uri, int requestedExtent, boolean darkTheme,
   1171                 DefaultImageProvider defaultProvider) {
   1172             return new Request(0 /* no ID */, uri, requestedExtent, darkTheme, defaultProvider);
   1173         }
   1174 
   1175         public boolean isUriRequest() {
   1176             return mUri != null;
   1177         }
   1178 
   1179         public Uri getUri() {
   1180             return mUri;
   1181         }
   1182 
   1183         public long getId() {
   1184             return mId;
   1185         }
   1186 
   1187         public int getRequestedExtent() {
   1188             return mRequestedExtent;
   1189         }
   1190 
   1191         @Override
   1192         public int hashCode() {
   1193             final int prime = 31;
   1194             int result = 1;
   1195             result = prime * result + (int) (mId ^ (mId >>> 32));
   1196             result = prime * result + mRequestedExtent;
   1197             result = prime * result + ((mUri == null) ? 0 : mUri.hashCode());
   1198             return result;
   1199         }
   1200 
   1201         @Override
   1202         public boolean equals(Object obj) {
   1203             if (this == obj) return true;
   1204             if (obj == null) return false;
   1205             if (getClass() != obj.getClass()) return false;
   1206             final Request that = (Request) obj;
   1207             if (mId != that.mId) return false;
   1208             if (mRequestedExtent != that.mRequestedExtent) return false;
   1209             if (!UriUtils.areEqual(mUri, that.mUri)) return false;
   1210             // Don't compare equality of mDarkTheme because it is only used in the default contact
   1211             // photo case. When the contact does have a photo, the contact photo is the same
   1212             // regardless of mDarkTheme, so we shouldn't need to put the photo request on the queue
   1213             // twice.
   1214             return true;
   1215         }
   1216 
   1217         public Object getKey() {
   1218             return mUri == null ? mId : mUri;
   1219         }
   1220 
   1221         public void applyDefaultImage(ImageView view) {
   1222             mDefaultProvider.applyDefaultImage(view, mRequestedExtent, mDarkTheme);
   1223         }
   1224     }
   1225 }
   1226