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