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 android.app.ActivityManager;
20 import android.content.ComponentCallbacks2;
21 import android.content.ContentResolver;
22 import android.content.ContentUris;
23 import android.content.Context;
24 import android.content.res.Configuration;
25 import android.content.res.Resources;
26 import android.database.Cursor;
27 import android.graphics.Bitmap;
28 import android.graphics.Canvas;
29 import android.graphics.Color;
30 import android.graphics.Paint;
31 import android.graphics.Paint.Style;
32 import android.graphics.drawable.BitmapDrawable;
33 import android.graphics.drawable.ColorDrawable;
34 import android.graphics.drawable.Drawable;
35 import android.graphics.drawable.TransitionDrawable;
36 import android.media.ThumbnailUtils;
37 import android.net.TrafficStats;
38 import android.net.Uri;
39 import android.net.Uri.Builder;
40 import android.os.Handler;
41 import android.os.Handler.Callback;
42 import android.os.HandlerThread;
43 import android.os.Message;
44 import android.provider.ContactsContract;
45 import android.provider.ContactsContract.Contacts;
46 import android.provider.ContactsContract.Contacts.Photo;
47 import android.provider.ContactsContract.Data;
48 import android.provider.ContactsContract.Directory;
49 import androidx.core.graphics.drawable.RoundedBitmapDrawable;
50 import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
51 import android.text.TextUtils;
52 import android.util.Log;
53 import android.util.LruCache;
54 import android.view.View;
55 import android.view.ViewGroup;
56 import android.widget.ImageView;
57 
58 import com.android.contacts.lettertiles.LetterTileDrawable;
59 import com.android.contacts.util.BitmapUtil;
60 import com.android.contacts.util.PermissionsUtil;
61 import com.android.contacts.util.TrafficStatsTags;
62 import com.android.contacts.util.UriUtils;
63 import com.android.contactsbind.util.UserAgentGenerator;
64 
65 import com.google.common.annotations.VisibleForTesting;
66 import com.google.common.collect.Lists;
67 import com.google.common.collect.Sets;
68 
69 import java.io.ByteArrayOutputStream;
70 import java.io.IOException;
71 import java.io.InputStream;
72 import java.lang.ref.Reference;
73 import java.lang.ref.SoftReference;
74 import java.net.HttpURLConnection;
75 import java.net.URL;
76 import java.util.Iterator;
77 import java.util.List;
78 import java.util.Map.Entry;
79 import java.util.Set;
80 import java.util.concurrent.ConcurrentHashMap;
81 import java.util.concurrent.atomic.AtomicInteger;
82 
83 /**
84  * Asynchronously loads contact photos and maintains a cache of photos.
85  */
86 public abstract class ContactPhotoManager implements ComponentCallbacks2 {
87     static final String TAG = "ContactPhotoManager";
88     static final boolean DEBUG = false; // Don't submit with true
89     static final boolean DEBUG_SIZES = false; // Don't submit with true
90 
91     /** Contact type constants used for default letter images */
92     public static final int TYPE_PERSON = LetterTileDrawable.TYPE_PERSON;
93     public static final int TYPE_BUSINESS = LetterTileDrawable.TYPE_BUSINESS;
94     public static final int TYPE_VOICEMAIL = LetterTileDrawable.TYPE_VOICEMAIL;
95     public static final int TYPE_DEFAULT = LetterTileDrawable.TYPE_DEFAULT;
96 
97     /** Scale and offset default constants used for default letter images */
98     public static final float SCALE_DEFAULT = 1.0f;
99     public static final float OFFSET_DEFAULT = 0.0f;
100 
101     public static final boolean IS_CIRCULAR_DEFAULT = false;
102 
103     /** Uri-related constants used for default letter images */
104     private static final String DISPLAY_NAME_PARAM_KEY = "display_name";
105     private static final String IDENTIFIER_PARAM_KEY = "identifier";
106     private static final String CONTACT_TYPE_PARAM_KEY = "contact_type";
107     private static final String SCALE_PARAM_KEY = "scale";
108     private static final String OFFSET_PARAM_KEY = "offset";
109     private static final String IS_CIRCULAR_PARAM_KEY = "is_circular";
110     private static final String DEFAULT_IMAGE_URI_SCHEME = "defaultimage";
111     private static final Uri DEFAULT_IMAGE_URI = Uri.parse(DEFAULT_IMAGE_URI_SCHEME + "://");
112 
113     // Static field used to cache the default letter avatar drawable that is created
114     // using a null {@link DefaultImageRequest}
115     private static Drawable sDefaultLetterAvatar = null;
116 
117     private static ContactPhotoManager sInstance;
118 
119     /**
120      * Given a {@link DefaultImageRequest}, returns a {@link Drawable}, that when drawn, will
121      * draw a letter tile avatar based on the request parameters defined in the
122      * {@link DefaultImageRequest}.
123      */
getDefaultAvatarDrawableForContact(Resources resources, boolean hires, DefaultImageRequest defaultImageRequest)124     public static Drawable getDefaultAvatarDrawableForContact(Resources resources, boolean hires,
125             DefaultImageRequest defaultImageRequest) {
126         if (defaultImageRequest == null) {
127             if (sDefaultLetterAvatar == null) {
128                 // Cache and return the letter tile drawable that is created by a null request,
129                 // so that it doesn't have to be recreated every time it is requested again.
130                 sDefaultLetterAvatar = LetterTileDefaultImageProvider.getDefaultImageForContact(
131                         resources, null);
132             }
133             return sDefaultLetterAvatar;
134         }
135         return LetterTileDefaultImageProvider.getDefaultImageForContact(resources,
136                 defaultImageRequest);
137     }
138 
139     /**
140      * Given a {@link DefaultImageRequest}, returns an Uri that can be used to request a
141      * letter tile avatar when passed to the {@link ContactPhotoManager}. The internal
142      * implementation of this uri is not guaranteed to remain the same across application
143      * versions, so the actual uri should never be persisted in long-term storage and reused.
144      *
145      * @param request A {@link DefaultImageRequest} object with the fields configured
146      * to return a
147      * @return A Uri that when later passed to the {@link ContactPhotoManager} via
148      * {@link #loadPhoto(ImageView, Uri, int, boolean, DefaultImageRequest)}, can be
149      * used to request a default contact image, drawn as a letter tile using the
150      * parameters as configured in the provided {@link DefaultImageRequest}
151      */
getDefaultAvatarUriForContact(DefaultImageRequest request)152     public static Uri getDefaultAvatarUriForContact(DefaultImageRequest request) {
153         final Builder builder = DEFAULT_IMAGE_URI.buildUpon();
154         if (request != null) {
155             if (!TextUtils.isEmpty(request.displayName)) {
156                 builder.appendQueryParameter(DISPLAY_NAME_PARAM_KEY, request.displayName);
157             }
158             if (!TextUtils.isEmpty(request.identifier)) {
159                 builder.appendQueryParameter(IDENTIFIER_PARAM_KEY, request.identifier);
160             }
161             if (request.contactType != TYPE_DEFAULT) {
162                 builder.appendQueryParameter(CONTACT_TYPE_PARAM_KEY,
163                         String.valueOf(request.contactType));
164             }
165             if (request.scale != SCALE_DEFAULT) {
166                 builder.appendQueryParameter(SCALE_PARAM_KEY, String.valueOf(request.scale));
167             }
168             if (request.offset != OFFSET_DEFAULT) {
169                 builder.appendQueryParameter(OFFSET_PARAM_KEY, String.valueOf(request.offset));
170             }
171             if (request.isCircular != IS_CIRCULAR_DEFAULT) {
172                 builder.appendQueryParameter(IS_CIRCULAR_PARAM_KEY,
173                         String.valueOf(request.isCircular));
174             }
175 
176         }
177         return builder.build();
178     }
179 
180     /**
181      * Adds a business contact type encoded fragment to the URL.  Used to ensure photo URLS
182      * from Nearby Places can be identified as business photo URLs rather than URLs for personal
183      * contact photos.
184      *
185      * @param photoUrl The photo URL to modify.
186      * @return URL with the contact type parameter added and set to TYPE_BUSINESS.
187      */
appendBusinessContactType(String photoUrl)188     public static String appendBusinessContactType(String photoUrl) {
189         Uri uri = Uri.parse(photoUrl);
190         Builder builder = uri.buildUpon();
191         builder.encodedFragment(String.valueOf(TYPE_BUSINESS));
192         return builder.build().toString();
193     }
194 
195     /**
196      * Removes the contact type information stored in the photo URI encoded fragment.
197      *
198      * @param photoUri The photo URI to remove the contact type from.
199      * @return The photo URI with contact type removed.
200      */
removeContactType(Uri photoUri)201     public static Uri removeContactType(Uri photoUri) {
202         String encodedFragment = photoUri.getEncodedFragment();
203         if (!TextUtils.isEmpty(encodedFragment)) {
204             Builder builder = photoUri.buildUpon();
205             builder.encodedFragment(null);
206             return builder.build();
207         }
208         return photoUri;
209     }
210 
211     /**
212      * Inspects a photo URI to determine if the photo URI represents a business.
213      *
214      * @param photoUri The URI to inspect.
215      * @return Whether the URI represents a business photo or not.
216      */
isBusinessContactUri(Uri photoUri)217     public static boolean isBusinessContactUri(Uri photoUri) {
218         if (photoUri == null) {
219             return false;
220         }
221 
222         String encodedFragment = photoUri.getEncodedFragment();
223         return !TextUtils.isEmpty(encodedFragment)
224                 && encodedFragment.equals(String.valueOf(TYPE_BUSINESS));
225     }
226 
getDefaultImageRequestFromUri(Uri uri)227     protected static DefaultImageRequest getDefaultImageRequestFromUri(Uri uri) {
228         final DefaultImageRequest request = new DefaultImageRequest(
229                 uri.getQueryParameter(DISPLAY_NAME_PARAM_KEY),
230                 uri.getQueryParameter(IDENTIFIER_PARAM_KEY), false);
231         try {
232             String contactType = uri.getQueryParameter(CONTACT_TYPE_PARAM_KEY);
233             if (!TextUtils.isEmpty(contactType)) {
234                 request.contactType = Integer.valueOf(contactType);
235             }
236 
237             String scale = uri.getQueryParameter(SCALE_PARAM_KEY);
238             if (!TextUtils.isEmpty(scale)) {
239                 request.scale = Float.valueOf(scale);
240             }
241 
242             String offset = uri.getQueryParameter(OFFSET_PARAM_KEY);
243             if (!TextUtils.isEmpty(offset)) {
244                 request.offset = Float.valueOf(offset);
245             }
246 
247             String isCircular = uri.getQueryParameter(IS_CIRCULAR_PARAM_KEY);
248             if (!TextUtils.isEmpty(isCircular)) {
249                 request.isCircular = Boolean.valueOf(isCircular);
250             }
251         } catch (NumberFormatException e) {
252             Log.w(TAG, "Invalid DefaultImageRequest image parameters provided, ignoring and using "
253                     + "defaults.");
254         }
255 
256         return request;
257     }
258 
isDefaultImageUri(Uri uri)259     protected boolean isDefaultImageUri(Uri uri) {
260         return DEFAULT_IMAGE_URI_SCHEME.equals(uri.getScheme());
261     }
262 
263     /**
264      * Contains fields used to contain contact details and other user-defined settings that might
265      * be used by the ContactPhotoManager to generate a default contact image. This contact image
266      * takes the form of a letter or bitmap drawn on top of a colored tile.
267      */
268     public static class DefaultImageRequest {
269         /**
270          * The contact's display name. The display name is used to
271          */
272         public String displayName;
273 
274         /**
275          * A unique and deterministic string that can be used to identify this contact. This is
276          * usually the contact's lookup key, but other contact details can be used as well,
277          * especially for non-local or temporary contacts that might not have a lookup key. This
278          * is used to determine the color of the tile.
279          */
280         public String identifier;
281 
282         /**
283          * The type of this contact. This contact type may be used to decide the kind of
284          * image to use in the case where a unique letter cannot be generated from the contact's
285          * display name and identifier. See:
286          * {@link #TYPE_PERSON}
287          * {@link #TYPE_BUSINESS}
288          * {@link #TYPE_PERSON}
289          * {@link #TYPE_DEFAULT}
290          */
291         public int contactType = TYPE_DEFAULT;
292 
293         /**
294          * The amount to scale the letter or bitmap to, as a ratio of its default size (from a
295          * range of 0.0f to 2.0f). The default value is 1.0f.
296          */
297         public float scale = SCALE_DEFAULT;
298 
299         /**
300          * The amount to vertically offset the letter or image to within the tile.
301          * The provided offset must be within the range of -0.5f to 0.5f.
302          * If set to -0.5f, the letter will be shifted upwards by 0.5 times the height of the canvas
303          * it is being drawn on, which means it will be drawn with the center of the letter starting
304          * at the top edge of the canvas.
305          * If set to 0.5f, the letter will be shifted downwards by 0.5 times the height of the
306          * canvas it is being drawn on, which means it will be drawn with the center of the letter
307          * starting at the bottom edge of the canvas.
308          * The default is 0.0f, which means the letter is drawn in the exact vertical center of
309          * the tile.
310          */
311         public float offset = OFFSET_DEFAULT;
312 
313         /**
314          * Whether or not to draw the default image as a circle, instead of as a square/rectangle.
315          */
316         public boolean isCircular = false;
317 
318         /**
319          * Used to indicate that a drawable that represents a contact without any contact details
320          * should be returned.
321          */
322         public static DefaultImageRequest EMPTY_DEFAULT_IMAGE_REQUEST = new DefaultImageRequest();
323 
324         /**
325          * Used to indicate that a drawable that represents a business without a business photo
326          * should be returned.
327          */
328         public static DefaultImageRequest EMPTY_DEFAULT_BUSINESS_IMAGE_REQUEST =
329                 new DefaultImageRequest(null, null, TYPE_BUSINESS, false);
330 
331         /**
332          * Used to indicate that a circular drawable that represents a contact without any contact
333          * details should be returned.
334          */
335         public static DefaultImageRequest EMPTY_CIRCULAR_DEFAULT_IMAGE_REQUEST =
336                 new DefaultImageRequest(null, null, true);
337 
338         /**
339          * Used to indicate that a circular drawable that represents a business without a business
340          * photo should be returned.
341          */
342         public static DefaultImageRequest EMPTY_CIRCULAR_BUSINESS_IMAGE_REQUEST =
343                 new DefaultImageRequest(null, null, TYPE_BUSINESS, true);
344 
DefaultImageRequest()345         public DefaultImageRequest() {
346         }
347 
DefaultImageRequest(String displayName, String identifier, boolean isCircular)348         public DefaultImageRequest(String displayName, String identifier, boolean isCircular) {
349             this(displayName, identifier, TYPE_DEFAULT, SCALE_DEFAULT, OFFSET_DEFAULT, isCircular);
350         }
351 
DefaultImageRequest(String displayName, String identifier, int contactType, boolean isCircular)352         public DefaultImageRequest(String displayName, String identifier, int contactType,
353                 boolean isCircular) {
354             this(displayName, identifier, contactType, SCALE_DEFAULT, OFFSET_DEFAULT, isCircular);
355         }
356 
DefaultImageRequest(String displayName, String identifier, int contactType, float scale, float offset, boolean isCircular)357         public DefaultImageRequest(String displayName, String identifier, int contactType,
358                 float scale, float offset, boolean isCircular) {
359             this.displayName = displayName;
360             this.identifier = identifier;
361             this.contactType = contactType;
362             this.scale = scale;
363             this.offset = offset;
364             this.isCircular = isCircular;
365         }
366     }
367 
368     public static abstract class DefaultImageProvider {
369         /**
370          * Applies the default avatar to the ImageView. Extent is an indicator for the size (width
371          * or height). If darkTheme is set, the avatar is one that looks better on dark background
372          *
373          * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a
374          * default letter tile avatar should be drawn.
375          */
applyDefaultImage(ImageView view, int extent, boolean darkTheme, DefaultImageRequest defaultImageRequest)376         public abstract void applyDefaultImage(ImageView view, int extent, boolean darkTheme,
377                 DefaultImageRequest defaultImageRequest);
378     }
379 
380     /**
381      * A default image provider that applies a letter tile consisting of a colored background
382      * and a letter in the foreground as the default image for a contact. The color of the
383      * background and the type of letter is decided based on the contact's details.
384      */
385     private static class LetterTileDefaultImageProvider extends DefaultImageProvider {
386         @Override
applyDefaultImage(ImageView view, int extent, boolean darkTheme, DefaultImageRequest defaultImageRequest)387         public void applyDefaultImage(ImageView view, int extent, boolean darkTheme,
388                 DefaultImageRequest defaultImageRequest) {
389             final Drawable drawable = getDefaultImageForContact(view.getResources(),
390                     defaultImageRequest);
391             view.setImageDrawable(drawable);
392         }
393 
getDefaultImageForContact(Resources resources, DefaultImageRequest defaultImageRequest)394         public static Drawable getDefaultImageForContact(Resources resources,
395                 DefaultImageRequest defaultImageRequest) {
396             final LetterTileDrawable drawable = new LetterTileDrawable(resources);
397             if (defaultImageRequest != null) {
398                 // If the contact identifier is null or empty, fallback to the
399                 // displayName. In that case, use {@code null} for the contact's
400                 // display name so that a default bitmap will be used instead of a
401                 // letter
402                 if (TextUtils.isEmpty(defaultImageRequest.identifier)) {
403                     drawable.setLetterAndColorFromContactDetails(null,
404                             defaultImageRequest.displayName);
405                 } else {
406                     drawable.setLetterAndColorFromContactDetails(defaultImageRequest.displayName,
407                             defaultImageRequest.identifier);
408                 }
409                 drawable.setContactType(defaultImageRequest.contactType);
410                 drawable.setScale(defaultImageRequest.scale);
411                 drawable.setOffset(defaultImageRequest.offset);
412                 drawable.setIsCircular(defaultImageRequest.isCircular);
413             }
414             return drawable;
415         }
416     }
417 
418     private static class BlankDefaultImageProvider extends DefaultImageProvider {
419         private static Drawable sDrawable;
420 
421         @Override
applyDefaultImage(ImageView view, int extent, boolean darkTheme, DefaultImageRequest defaultImageRequest)422         public void applyDefaultImage(ImageView view, int extent, boolean darkTheme,
423                 DefaultImageRequest defaultImageRequest) {
424             if (sDrawable == null) {
425                 Context context = view.getContext();
426                 sDrawable = new ColorDrawable(context.getResources().getColor(
427                         R.color.image_placeholder));
428             }
429             view.setImageDrawable(sDrawable);
430         }
431     }
432 
433     public static DefaultImageProvider DEFAULT_AVATAR = new LetterTileDefaultImageProvider();
434 
435     public static final DefaultImageProvider DEFAULT_BLANK = new BlankDefaultImageProvider();
436 
getInstance(Context context)437     public static ContactPhotoManager getInstance(Context context) {
438         if (sInstance == null) {
439             Context applicationContext = context.getApplicationContext();
440             sInstance = createContactPhotoManager(applicationContext);
441             applicationContext.registerComponentCallbacks(sInstance);
442             if (PermissionsUtil.hasContactsPermissions(context)) {
443                 sInstance.preloadPhotosInBackground();
444             }
445         }
446         return sInstance;
447     }
448 
createContactPhotoManager(Context context)449     public static synchronized ContactPhotoManager createContactPhotoManager(Context context) {
450         return new ContactPhotoManagerImpl(context);
451     }
452 
453     @VisibleForTesting
injectContactPhotoManagerForTesting(ContactPhotoManager photoManager)454     public static void injectContactPhotoManagerForTesting(ContactPhotoManager photoManager) {
455         sInstance = photoManager;
456     }
457 
458     /**
459      * Load thumbnail image into the supplied image view. If the photo is already cached,
460      * it is displayed immediately.  Otherwise a request is sent to load the photo
461      * from the database.
462      */
loadThumbnail(ImageView view, long photoId, boolean darkTheme, boolean isCircular, DefaultImageRequest defaultImageRequest, DefaultImageProvider defaultProvider)463     public abstract void loadThumbnail(ImageView view, long photoId, boolean darkTheme,
464             boolean isCircular, DefaultImageRequest defaultImageRequest,
465             DefaultImageProvider defaultProvider);
466 
467     /**
468      * Calls {@link #loadThumbnail(ImageView, long, boolean, DefaultImageRequest,
469      * DefaultImageProvider)} using the {@link DefaultImageProvider} {@link #DEFAULT_AVATAR}.
470      */
loadThumbnail(ImageView view, long photoId, boolean darkTheme, boolean isCircular, DefaultImageRequest defaultImageRequest)471     public final void loadThumbnail(ImageView view, long photoId, boolean darkTheme,
472             boolean isCircular, DefaultImageRequest defaultImageRequest) {
473         loadThumbnail(view, photoId, darkTheme, isCircular, defaultImageRequest, DEFAULT_AVATAR);
474     }
475 
476 
477     /**
478      * Load photo into the supplied image view. If the photo is already cached,
479      * it is displayed immediately. Otherwise a request is sent to load the photo
480      * from the location specified by the URI.
481      *
482      * @param view The target view
483      * @param photoUri The uri of the photo to load
484      * @param requestedExtent Specifies an approximate Max(width, height) of the targetView.
485      * This is useful if the source image can be a lot bigger that the target, so that the decoding
486      * is done using efficient sampling. If requestedExtent is specified, no sampling of the image
487      * is performed
488      * @param darkTheme Whether the background is dark. This is used for default avatars
489      * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default
490      * letter tile avatar should be drawn.
491      * @param defaultProvider The provider of default avatars (this is used if photoUri doesn't
492      * refer to an existing image)
493      */
loadPhoto(ImageView view, Uri photoUri, int requestedExtent, boolean darkTheme, boolean isCircular, DefaultImageRequest defaultImageRequest, DefaultImageProvider defaultProvider)494     public abstract void loadPhoto(ImageView view, Uri photoUri, int requestedExtent,
495             boolean darkTheme, boolean isCircular, DefaultImageRequest defaultImageRequest,
496             DefaultImageProvider defaultProvider);
497 
498     /**
499      * Calls {@link #loadPhoto(ImageView, Uri, int, boolean, DefaultImageRequest,
500      * DefaultImageProvider)} with {@link #DEFAULT_AVATAR} and {@code null} display names and
501      * lookup keys.
502      *
503      * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default
504      * letter tile avatar should be drawn.
505      */
loadPhoto(ImageView view, Uri photoUri, int requestedExtent, boolean darkTheme, boolean isCircular, DefaultImageRequest defaultImageRequest)506     public final void loadPhoto(ImageView view, Uri photoUri, int requestedExtent,
507             boolean darkTheme, boolean isCircular, DefaultImageRequest defaultImageRequest) {
508         loadPhoto(view, photoUri, requestedExtent, darkTheme, isCircular,
509                 defaultImageRequest, DEFAULT_AVATAR);
510     }
511 
512     /**
513      * Calls {@link #loadPhoto(ImageView, Uri, boolean, boolean, DefaultImageRequest,
514      * DefaultImageProvider)} with {@link #DEFAULT_AVATAR} and with the assumption, that
515      * the image is a thumbnail.
516      *
517      * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default
518      * letter tile avatar should be drawn.
519      */
loadDirectoryPhoto(ImageView view, Uri photoUri, boolean darkTheme, boolean isCircular, DefaultImageRequest defaultImageRequest)520     public final void loadDirectoryPhoto(ImageView view, Uri photoUri, boolean darkTheme,
521             boolean isCircular, DefaultImageRequest defaultImageRequest) {
522         loadPhoto(view, photoUri, -1, darkTheme, isCircular, defaultImageRequest, DEFAULT_AVATAR);
523     }
524 
525     /**
526      * Remove photo from the supplied image view. This also cancels current pending load request
527      * inside this photo manager.
528      */
removePhoto(ImageView view)529     public abstract void removePhoto(ImageView view);
530 
531     /**
532      * Cancels all pending requests to load photos asynchronously.
533      */
cancelPendingRequests(View fragmentRootView)534     public abstract void cancelPendingRequests(View fragmentRootView);
535 
536     /**
537      * Temporarily stops loading photos from the database.
538      */
pause()539     public abstract void pause();
540 
541     /**
542      * Resumes loading photos from the database.
543      */
resume()544     public abstract void resume();
545 
546     /**
547      * Marks all cached photos for reloading.  We can continue using cache but should
548      * also make sure the photos haven't changed in the background and notify the views
549      * if so.
550      */
refreshCache()551     public abstract void refreshCache();
552 
553     /**
554      * Stores the given bitmap directly in the LRU bitmap cache.
555      * @param photoUri The URI of the photo (for future requests).
556      * @param bitmap The bitmap.
557      * @param photoBytes The bytes that were parsed to create the bitmap.
558      */
cacheBitmap(Uri photoUri, Bitmap bitmap, byte[] photoBytes)559     public abstract void cacheBitmap(Uri photoUri, Bitmap bitmap, byte[] photoBytes);
560 
561     /**
562      * Initiates a background process that over time will fill up cache with
563      * preload photos.
564      */
preloadPhotosInBackground()565     public abstract void preloadPhotosInBackground();
566 
567     // ComponentCallbacks2
568     @Override
onConfigurationChanged(Configuration newConfig)569     public void onConfigurationChanged(Configuration newConfig) {
570     }
571 
572     // ComponentCallbacks2
573     @Override
onLowMemory()574     public void onLowMemory() {
575     }
576 
577     // ComponentCallbacks2
578     @Override
onTrimMemory(int level)579     public void onTrimMemory(int level) {
580     }
581 }
582 
583 class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
584     private static final String LOADER_THREAD_NAME = "ContactPhotoLoader";
585 
586     private static final int FADE_TRANSITION_DURATION = 200;
587 
588     /**
589      * Type of message sent by the UI thread to itself to indicate that some photos
590      * need to be loaded.
591      */
592     private static final int MESSAGE_REQUEST_LOADING = 1;
593 
594     /**
595      * Type of message sent by the loader thread to indicate that some photos have
596      * been loaded.
597      */
598     private static final int MESSAGE_PHOTOS_LOADED = 2;
599 
600     private static final String[] EMPTY_STRING_ARRAY = new String[0];
601 
602     private static final String[] COLUMNS = new String[] { Photo._ID, Photo.PHOTO };
603 
604     /**
605      * Placeholder object used to indicate that a bitmap for a given key could not
606      * be stored in the cache.
607      */
608     private static final BitmapHolder BITMAP_UNAVAILABLE;
609 
610     static {
611         BITMAP_UNAVAILABLE = new BitmapHolder(new byte[0], 0);
612         BITMAP_UNAVAILABLE.bitmapRef = new SoftReference<Bitmap>(null);
613     }
614 
615     /**
616      * Maintains the state of a particular photo.
617      */
618     private static class BitmapHolder {
619         final byte[] bytes;
620         final int originalSmallerExtent;
621 
622         volatile boolean fresh;
623         Bitmap bitmap;
624         Reference<Bitmap> bitmapRef;
625         int decodedSampleSize;
626 
BitmapHolder(byte[] bytes, int originalSmallerExtent)627         public BitmapHolder(byte[] bytes, int originalSmallerExtent) {
628             this.bytes = bytes;
629             this.fresh = true;
630             this.originalSmallerExtent = originalSmallerExtent;
631         }
632     }
633 
634     private final Context mContext;
635 
636     /**
637      * An LRU cache for bitmap holders. The cache contains bytes for photos just
638      * as they come from the database. Each holder has a soft reference to the
639      * actual bitmap.
640      */
641     private final LruCache<Object, BitmapHolder> mBitmapHolderCache;
642 
643     /**
644      * {@code true} if ALL entries in {@link #mBitmapHolderCache} are NOT fresh.
645      */
646     private volatile boolean mBitmapHolderCacheAllUnfresh = true;
647 
648     /**
649      * Cache size threshold at which bitmaps will not be preloaded.
650      */
651     private final int mBitmapHolderCacheRedZoneBytes;
652 
653     /**
654      * Level 2 LRU cache for bitmaps. This is a smaller cache that holds
655      * the most recently used bitmaps to save time on decoding
656      * them from bytes (the bytes are stored in {@link #mBitmapHolderCache}.
657      */
658     private final LruCache<Object, Bitmap> mBitmapCache;
659 
660     /**
661      * A map from ImageView to the corresponding photo ID or uri, encapsulated in a request.
662      * The request may swapped out before the photo loading request is started.
663      */
664     private final ConcurrentHashMap<ImageView, Request> mPendingRequests =
665             new ConcurrentHashMap<ImageView, Request>();
666 
667     /**
668      * Handler for messages sent to the UI thread.
669      */
670     private final Handler mMainThreadHandler = new Handler(this);
671 
672     /**
673      * Thread responsible for loading photos from the database. Created upon
674      * the first request.
675      */
676     private LoaderThread mLoaderThread;
677 
678     /**
679      * A gate to make sure we only send one instance of MESSAGE_PHOTOS_NEEDED at a time.
680      */
681     private boolean mLoadingRequested;
682 
683     /**
684      * Flag indicating if the image loading is paused.
685      */
686     private boolean mPaused;
687 
688     /** Cache size for {@link #mBitmapHolderCache} for devices with "large" RAM. */
689     private static final int HOLDER_CACHE_SIZE = 2000000;
690 
691     /** Cache size for {@link #mBitmapCache} for devices with "large" RAM. */
692     private static final int BITMAP_CACHE_SIZE = 36864 * 48; // 1728K
693 
694     /** Height/width of a thumbnail image */
695     private static int mThumbnailSize;
696 
697     /** For debug: How many times we had to reload cached photo for a stale entry */
698     private final AtomicInteger mStaleCacheOverwrite = new AtomicInteger();
699 
700     /** For debug: How many times we had to reload cached photo for a fresh entry.  Should be 0. */
701     private final AtomicInteger mFreshCacheOverwrite = new AtomicInteger();
702 
703     /**
704      * The user agent string to use when loading URI based photos.
705      */
706     private String mUserAgent;
707 
ContactPhotoManagerImpl(Context context)708     public ContactPhotoManagerImpl(Context context) {
709         mContext = context;
710 
711         final ActivityManager am = ((ActivityManager) context.getSystemService(
712                 Context.ACTIVITY_SERVICE));
713 
714         final float cacheSizeAdjustment = (am.isLowRamDevice()) ? 0.5f : 1.0f;
715 
716         final int bitmapCacheSize = (int) (cacheSizeAdjustment * BITMAP_CACHE_SIZE);
717         mBitmapCache = new LruCache<Object, Bitmap>(bitmapCacheSize) {
718             @Override protected int sizeOf(Object key, Bitmap value) {
719                 return value.getByteCount();
720             }
721 
722             @Override protected void entryRemoved(
723                     boolean evicted, Object key, Bitmap oldValue, Bitmap newValue) {
724                 if (DEBUG) dumpStats();
725             }
726         };
727         final int holderCacheSize = (int) (cacheSizeAdjustment * HOLDER_CACHE_SIZE);
728         mBitmapHolderCache = new LruCache<Object, BitmapHolder>(holderCacheSize) {
729             @Override protected int sizeOf(Object key, BitmapHolder value) {
730                 return value.bytes != null ? value.bytes.length : 0;
731             }
732 
733             @Override protected void entryRemoved(
734                     boolean evicted, Object key, BitmapHolder oldValue, BitmapHolder newValue) {
735                 if (DEBUG) dumpStats();
736             }
737         };
738         mBitmapHolderCacheRedZoneBytes = (int) (holderCacheSize * 0.75);
739         Log.i(TAG, "Cache adj: " + cacheSizeAdjustment);
740         if (DEBUG) {
741             Log.d(TAG, "Cache size: " + btk(mBitmapHolderCache.maxSize())
742                     + " + " + btk(mBitmapCache.maxSize()));
743         }
744 
745         mThumbnailSize = context.getResources().getDimensionPixelSize(
746                 R.dimen.contact_browser_list_item_photo_size);
747 
748         // Get a user agent string to use for URI photo requests.
749         mUserAgent = UserAgentGenerator.getUserAgent(context);
750         if (mUserAgent == null) {
751             mUserAgent = "";
752         }
753     }
754 
755     /** Converts bytes to K bytes, rounding up.  Used only for debug log. */
btk(int bytes)756     private static String btk(int bytes) {
757         return ((bytes + 1023) / 1024) + "K";
758     }
759 
safeDiv(int dividend, int divisor)760     private static final int safeDiv(int dividend, int divisor) {
761         return (divisor  == 0) ? 0 : (dividend / divisor);
762     }
763 
764     /**
765      * Dump cache stats on logcat.
766      */
dumpStats()767     private void dumpStats() {
768         if (!DEBUG) return;
769         {
770             int numHolders = 0;
771             int rawBytes = 0;
772             int bitmapBytes = 0;
773             int numBitmaps = 0;
774             for (BitmapHolder h : mBitmapHolderCache.snapshot().values()) {
775                 numHolders++;
776                 if (h.bytes != null) {
777                     rawBytes += h.bytes.length;
778                 }
779                 Bitmap b = h.bitmapRef != null ? h.bitmapRef.get() : null;
780                 if (b != null) {
781                     numBitmaps++;
782                     bitmapBytes += b.getByteCount();
783                 }
784             }
785             Log.d(TAG, "L1: " + btk(rawBytes) + " + " + btk(bitmapBytes) + " = "
786                     + btk(rawBytes + bitmapBytes) + ", " + numHolders + " holders, "
787                     + numBitmaps + " bitmaps, avg: "
788                     + btk(safeDiv(rawBytes, numHolders))
789                     + "," + btk(safeDiv(bitmapBytes,numBitmaps)));
790             Log.d(TAG, "L1 Stats: " + mBitmapHolderCache.toString()
791                     + ", overwrite: fresh=" + mFreshCacheOverwrite.get()
792                     + " stale=" + mStaleCacheOverwrite.get());
793         }
794 
795         {
796             int numBitmaps = 0;
797             int bitmapBytes = 0;
798             for (Bitmap b : mBitmapCache.snapshot().values()) {
799                 numBitmaps++;
800                 bitmapBytes += b.getByteCount();
801             }
802             Log.d(TAG, "L2: " + btk(bitmapBytes) + ", " + numBitmaps + " bitmaps"
803                     + ", avg: " + btk(safeDiv(bitmapBytes, numBitmaps)));
804             // We don't get from L2 cache, so L2 stats is meaningless.
805         }
806     }
807 
808     @Override
onTrimMemory(int level)809     public void onTrimMemory(int level) {
810         if (DEBUG) Log.d(TAG, "onTrimMemory: " + level);
811         if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
812             // Clear the caches.  Note all pending requests will be removed too.
813             clear();
814         }
815     }
816 
817     @Override
preloadPhotosInBackground()818     public void preloadPhotosInBackground() {
819         ensureLoaderThread();
820         mLoaderThread.requestPreloading();
821     }
822 
823     @Override
loadThumbnail(ImageView view, long photoId, boolean darkTheme, boolean isCircular, DefaultImageRequest defaultImageRequest, DefaultImageProvider defaultProvider)824     public void loadThumbnail(ImageView view, long photoId, boolean darkTheme, boolean isCircular,
825             DefaultImageRequest defaultImageRequest, DefaultImageProvider defaultProvider) {
826         if (photoId == 0) {
827             // No photo is needed
828             defaultProvider.applyDefaultImage(view, -1, darkTheme, defaultImageRequest);
829             mPendingRequests.remove(view);
830         } else {
831             if (DEBUG) Log.d(TAG, "loadPhoto request: " + photoId);
832             loadPhotoByIdOrUri(view, Request.createFromThumbnailId(photoId, darkTheme, isCircular,
833                     defaultProvider, defaultImageRequest));
834         }
835     }
836 
837     @Override
loadPhoto(ImageView view, Uri photoUri, int requestedExtent, boolean darkTheme, boolean isCircular, DefaultImageRequest defaultImageRequest, DefaultImageProvider defaultProvider)838     public void loadPhoto(ImageView view, Uri photoUri, int requestedExtent, boolean darkTheme,
839             boolean isCircular, DefaultImageRequest defaultImageRequest,
840             DefaultImageProvider defaultProvider) {
841         if (photoUri == null) {
842             // No photo is needed
843             defaultProvider.applyDefaultImage(view, requestedExtent, darkTheme,
844                     defaultImageRequest);
845             mPendingRequests.remove(view);
846         } else {
847             if (DEBUG) Log.d(TAG, "loadPhoto request: " + photoUri);
848             if (isDefaultImageUri(photoUri)) {
849                 createAndApplyDefaultImageForUri(view, photoUri, requestedExtent, darkTheme,
850                         isCircular, defaultProvider);
851             } else {
852                 loadPhotoByIdOrUri(view, Request.createFromUri(photoUri, requestedExtent,
853                         darkTheme, isCircular, defaultProvider, defaultImageRequest));
854             }
855         }
856     }
857 
createAndApplyDefaultImageForUri(ImageView view, Uri uri, int requestedExtent, boolean darkTheme, boolean isCircular, DefaultImageProvider defaultProvider)858     private void createAndApplyDefaultImageForUri(ImageView view, Uri uri, int requestedExtent,
859             boolean darkTheme, boolean isCircular, DefaultImageProvider defaultProvider) {
860         DefaultImageRequest request = getDefaultImageRequestFromUri(uri);
861         request.isCircular = isCircular;
862         defaultProvider.applyDefaultImage(view, requestedExtent, darkTheme, request);
863     }
864 
loadPhotoByIdOrUri(ImageView view, Request request)865     private void loadPhotoByIdOrUri(ImageView view, Request request) {
866         boolean loaded = loadCachedPhoto(view, request, false);
867         if (loaded) {
868             mPendingRequests.remove(view);
869         } else {
870             mPendingRequests.put(view, request);
871             if (!mPaused) {
872                 // Send a request to start loading photos
873                 requestLoading();
874             }
875         }
876     }
877 
878     @Override
removePhoto(ImageView view)879     public void removePhoto(ImageView view) {
880         view.setImageDrawable(null);
881         mPendingRequests.remove(view);
882     }
883 
884 
885     /**
886      * Cancels pending requests to load photos asynchronously for views inside
887      * {@param fragmentRootView}. If {@param fragmentRootView} is null, cancels all requests.
888      */
889     @Override
cancelPendingRequests(View fragmentRootView)890     public void cancelPendingRequests(View fragmentRootView) {
891         if (fragmentRootView == null) {
892             mPendingRequests.clear();
893             return;
894         }
895         final Iterator<Entry<ImageView, Request>> iterator = mPendingRequests.entrySet().iterator();
896         while (iterator.hasNext()) {
897             final ImageView imageView = iterator.next().getKey();
898             // If an ImageView is orphaned (currently scrap) or a child of fragmentRootView, then
899             // we can safely remove its request.
900             if (imageView.getParent() == null || isChildView(fragmentRootView, imageView)) {
901                 iterator.remove();
902             }
903         }
904     }
905 
isChildView(View parent, View potentialChild)906     private static boolean isChildView(View parent, View potentialChild) {
907         return potentialChild.getParent() != null && (potentialChild.getParent() == parent || (
908                 potentialChild.getParent() instanceof ViewGroup && isChildView(parent,
909                         (ViewGroup) potentialChild.getParent())));
910     }
911 
912     @Override
refreshCache()913     public void refreshCache() {
914         if (mBitmapHolderCacheAllUnfresh) {
915             if (DEBUG) Log.d(TAG, "refreshCache -- no fresh entries.");
916             return;
917         }
918         if (DEBUG) Log.d(TAG, "refreshCache");
919         mBitmapHolderCacheAllUnfresh = true;
920         for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
921             if (holder != BITMAP_UNAVAILABLE) {
922                 holder.fresh = false;
923             }
924         }
925     }
926 
927     /**
928      * Checks if the photo is present in cache.  If so, sets the photo on the view.
929      *
930      * @return false if the photo needs to be (re)loaded from the provider.
931      */
loadCachedPhoto(ImageView view, Request request, boolean fadeIn)932     private boolean loadCachedPhoto(ImageView view, Request request, boolean fadeIn) {
933         BitmapHolder holder = mBitmapHolderCache.get(request.getKey());
934         if (holder == null) {
935             // The bitmap has not been loaded ==> show default avatar
936             request.applyDefaultImage(view, request.mIsCircular);
937             return false;
938         }
939 
940         if (holder.bytes == null || holder.bytes.length == 0) {
941             request.applyDefaultImage(view, request.mIsCircular);
942             return holder.fresh;
943         }
944 
945         Bitmap cachedBitmap = holder.bitmapRef == null ? null : holder.bitmapRef.get();
946         if (cachedBitmap == null) {
947             if (holder.bytes.length < 8 * 1024) {
948                 // Small thumbnails are usually quick to inflate. Let's do that on the UI thread
949                 inflateBitmap(holder, request.getRequestedExtent());
950                 cachedBitmap = holder.bitmap;
951                 if (cachedBitmap == null) return false;
952             } else {
953                 // This is bigger data. Let's send that back to the Loader so that we can
954                 // inflate this in the background
955                 request.applyDefaultImage(view, request.mIsCircular);
956                 return false;
957             }
958         }
959 
960         final Drawable previousDrawable = view.getDrawable();
961         if (fadeIn && previousDrawable != null) {
962             final Drawable[] layers = new Drawable[2];
963             // Prevent cascade of TransitionDrawables.
964             if (previousDrawable instanceof TransitionDrawable) {
965                 final TransitionDrawable previousTransitionDrawable =
966                         (TransitionDrawable) previousDrawable;
967                 layers[0] = previousTransitionDrawable.getDrawable(
968                         previousTransitionDrawable.getNumberOfLayers() - 1);
969             } else {
970                 layers[0] = previousDrawable;
971             }
972             layers[1] = getDrawableForBitmap(mContext.getResources(), cachedBitmap, request);
973             TransitionDrawable drawable = new TransitionDrawable(layers);
974             view.setImageDrawable(drawable);
975             drawable.startTransition(FADE_TRANSITION_DURATION);
976         } else {
977             view.setImageDrawable(
978                     getDrawableForBitmap(mContext.getResources(), cachedBitmap, request));
979         }
980 
981         // Put the bitmap in the LRU cache. But only do this for images that are small enough
982         // (we require that at least six of those can be cached at the same time)
983         if (cachedBitmap.getByteCount() < mBitmapCache.maxSize() / 6) {
984             mBitmapCache.put(request.getKey(), cachedBitmap);
985         }
986 
987         // Soften the reference
988         holder.bitmap = null;
989 
990         return holder.fresh;
991     }
992 
993     /**
994      * Given a bitmap, returns a drawable that is configured to display the bitmap based on the
995      * specified request.
996      */
getDrawableForBitmap(Resources resources, Bitmap bitmap, Request request)997     private Drawable getDrawableForBitmap(Resources resources, Bitmap bitmap, Request request) {
998         if (request.mIsCircular) {
999             final RoundedBitmapDrawable drawable =
1000                     RoundedBitmapDrawableFactory.create(resources, bitmap);
1001             drawable.setAntiAlias(true);
1002             drawable.setCornerRadius(bitmap.getHeight() / 2);
1003             return drawable;
1004         } else {
1005             return new BitmapDrawable(resources, bitmap);
1006         }
1007     }
1008 
1009     /**
1010      * If necessary, decodes bytes stored in the holder to Bitmap.  As long as the
1011      * bitmap is held either by {@link #mBitmapCache} or by a soft reference in
1012      * the holder, it will not be necessary to decode the bitmap.
1013      */
inflateBitmap(BitmapHolder holder, int requestedExtent)1014     private static void inflateBitmap(BitmapHolder holder, int requestedExtent) {
1015         final int sampleSize =
1016                 BitmapUtil.findOptimalSampleSize(holder.originalSmallerExtent, requestedExtent);
1017         byte[] bytes = holder.bytes;
1018         if (bytes == null || bytes.length == 0) {
1019             return;
1020         }
1021 
1022         if (sampleSize == holder.decodedSampleSize) {
1023             // Check the soft reference.  If will be retained if the bitmap is also
1024             // in the LRU cache, so we don't need to check the LRU cache explicitly.
1025             if (holder.bitmapRef != null) {
1026                 holder.bitmap = holder.bitmapRef.get();
1027                 if (holder.bitmap != null) {
1028                     return;
1029                 }
1030             }
1031         }
1032 
1033         try {
1034             Bitmap bitmap = BitmapUtil.decodeBitmapFromBytes(bytes, sampleSize);
1035 
1036             // TODO: As a temporary workaround while framework support is being added to
1037             // clip non-square bitmaps into a perfect circle, manually crop the bitmap into
1038             // into a square if it will be displayed as a thumbnail so that it can be cropped
1039             // into a circle.
1040             final int height = bitmap.getHeight();
1041             final int width = bitmap.getWidth();
1042 
1043             // The smaller dimension of a scaled bitmap can range from anywhere from 0 to just
1044             // below twice the length of a thumbnail image due to the way we calculate the optimal
1045             // sample size.
1046             if (height != width && Math.min(height, width) <= mThumbnailSize * 2) {
1047                 final int dimension = Math.min(height, width);
1048                 bitmap = ThumbnailUtils.extractThumbnail(bitmap, dimension, dimension);
1049             }
1050             // make bitmap mutable and draw size onto it
1051             if (DEBUG_SIZES) {
1052                 Bitmap original = bitmap;
1053                 bitmap = bitmap.copy(bitmap.getConfig(), true);
1054                 original.recycle();
1055                 Canvas canvas = new Canvas(bitmap);
1056                 Paint paint = new Paint();
1057                 paint.setTextSize(16);
1058                 paint.setColor(Color.BLUE);
1059                 paint.setStyle(Style.FILL);
1060                 canvas.drawRect(0.0f, 0.0f, 50.0f, 20.0f, paint);
1061                 paint.setColor(Color.WHITE);
1062                 paint.setAntiAlias(true);
1063                 canvas.drawText(bitmap.getWidth() + "/" + sampleSize, 0, 15, paint);
1064             }
1065 
1066             holder.decodedSampleSize = sampleSize;
1067             holder.bitmap = bitmap;
1068             holder.bitmapRef = new SoftReference<Bitmap>(bitmap);
1069             if (DEBUG) {
1070                 Log.d(TAG, "inflateBitmap " + btk(bytes.length) + " -> "
1071                         + bitmap.getWidth() + "x" + bitmap.getHeight()
1072                         + ", " + btk(bitmap.getByteCount()));
1073             }
1074         } catch (OutOfMemoryError e) {
1075             // Do nothing - the photo will appear to be missing
1076         }
1077     }
1078 
clear()1079     public void clear() {
1080         if (DEBUG) Log.d(TAG, "clear");
1081         mPendingRequests.clear();
1082         mBitmapHolderCache.evictAll();
1083         mBitmapCache.evictAll();
1084     }
1085 
1086     @Override
pause()1087     public void pause() {
1088         mPaused = true;
1089     }
1090 
1091     @Override
resume()1092     public void resume() {
1093         mPaused = false;
1094         if (DEBUG) dumpStats();
1095         if (!mPendingRequests.isEmpty()) {
1096             requestLoading();
1097         }
1098     }
1099 
1100     /**
1101      * Sends a message to this thread itself to start loading images.  If the current
1102      * view contains multiple image views, all of those image views will get a chance
1103      * to request their respective photos before any of those requests are executed.
1104      * This allows us to load images in bulk.
1105      */
requestLoading()1106     private void requestLoading() {
1107         if (!mLoadingRequested) {
1108             mLoadingRequested = true;
1109             mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING);
1110         }
1111     }
1112 
1113     /**
1114      * Processes requests on the main thread.
1115      */
1116     @Override
handleMessage(Message msg)1117     public boolean handleMessage(Message msg) {
1118         switch (msg.what) {
1119             case MESSAGE_REQUEST_LOADING: {
1120                 mLoadingRequested = false;
1121                 if (!mPaused) {
1122                     ensureLoaderThread();
1123                     mLoaderThread.requestLoading();
1124                 }
1125                 return true;
1126             }
1127 
1128             case MESSAGE_PHOTOS_LOADED: {
1129                 if (!mPaused) {
1130                     processLoadedImages();
1131                 }
1132                 if (DEBUG) dumpStats();
1133                 return true;
1134             }
1135         }
1136         return false;
1137     }
1138 
ensureLoaderThread()1139     public void ensureLoaderThread() {
1140         if (mLoaderThread == null) {
1141             mLoaderThread = new LoaderThread(mContext.getContentResolver());
1142             mLoaderThread.start();
1143         }
1144     }
1145 
1146     /**
1147      * Goes over pending loading requests and displays loaded photos.  If some of the
1148      * photos still haven't been loaded, sends another request for image loading.
1149      */
processLoadedImages()1150     private void processLoadedImages() {
1151         final Iterator<Entry<ImageView, Request>> iterator = mPendingRequests.entrySet().iterator();
1152         while (iterator.hasNext()) {
1153             final Entry<ImageView, Request> entry = iterator.next();
1154             // TODO: Temporarily disable contact photo fading in, until issues with
1155             // RoundedBitmapDrawables overlapping the default image drawables are resolved.
1156             final boolean loaded = loadCachedPhoto(entry.getKey(), entry.getValue(), false);
1157             if (loaded) {
1158                 iterator.remove();
1159             }
1160         }
1161 
1162         softenCache();
1163 
1164         if (!mPendingRequests.isEmpty()) {
1165             requestLoading();
1166         }
1167     }
1168 
1169     /**
1170      * Removes strong references to loaded bitmaps to allow them to be garbage collected
1171      * if needed.  Some of the bitmaps will still be retained by {@link #mBitmapCache}.
1172      */
softenCache()1173     private void softenCache() {
1174         for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
1175             holder.bitmap = null;
1176         }
1177     }
1178 
1179     /**
1180      * Stores the supplied bitmap in cache.
1181      * bytes should be null to indicate a failure to load the photo. An empty byte[] signifies
1182      * a successful load but no photo was available.
1183      */
cacheBitmap(Object key, byte[] bytes, boolean preloading, int requestedExtent)1184     private void cacheBitmap(Object key, byte[] bytes, boolean preloading, int requestedExtent) {
1185         if (DEBUG) {
1186             BitmapHolder prev = mBitmapHolderCache.get(key);
1187             if (prev != null && prev.bytes != null) {
1188                 Log.d(TAG, "Overwriting cache: key=" + key + (prev.fresh ? " FRESH" : " stale"));
1189                 if (prev.fresh) {
1190                     mFreshCacheOverwrite.incrementAndGet();
1191                 } else {
1192                     mStaleCacheOverwrite.incrementAndGet();
1193                 }
1194             }
1195             Log.d(TAG, "Caching data: key=" + key + ", " +
1196                     (bytes == null ? "<null>" : btk(bytes.length)));
1197         }
1198         BitmapHolder holder = new BitmapHolder(bytes,
1199                 bytes == null ? -1 : BitmapUtil.getSmallerExtentFromBytes(bytes));
1200 
1201         // Unless this image is being preloaded, decode it right away while
1202         // we are still on the background thread.
1203         if (!preloading) {
1204             inflateBitmap(holder, requestedExtent);
1205         }
1206 
1207         if (bytes != null) {
1208             mBitmapHolderCache.put(key, holder);
1209             if (mBitmapHolderCache.get(key) != holder) {
1210                 Log.w(TAG, "Bitmap too big to fit in cache.");
1211                 mBitmapHolderCache.put(key, BITMAP_UNAVAILABLE);
1212             }
1213         } else {
1214             mBitmapHolderCache.put(key, BITMAP_UNAVAILABLE);
1215         }
1216 
1217         mBitmapHolderCacheAllUnfresh = false;
1218     }
1219 
1220     @Override
cacheBitmap(Uri photoUri, Bitmap bitmap, byte[] photoBytes)1221     public void cacheBitmap(Uri photoUri, Bitmap bitmap, byte[] photoBytes) {
1222         final int smallerExtent = Math.min(bitmap.getWidth(), bitmap.getHeight());
1223         // We can pretend here that the extent of the photo was the size that we originally
1224         // requested
1225         Request request = Request.createFromUri(photoUri, smallerExtent, false /* darkTheme */,
1226                 false /* isCircular */ , DEFAULT_AVATAR);
1227         BitmapHolder holder = new BitmapHolder(photoBytes, smallerExtent);
1228         holder.bitmapRef = new SoftReference<Bitmap>(bitmap);
1229         mBitmapHolderCache.put(request.getKey(), holder);
1230         mBitmapHolderCacheAllUnfresh = false;
1231         mBitmapCache.put(request.getKey(), bitmap);
1232     }
1233 
1234     /**
1235      * Populates an array of photo IDs that need to be loaded. Also decodes bitmaps that we have
1236      * already loaded
1237      */
obtainPhotoIdsAndUrisToLoad(Set<Long> photoIds, Set<String> photoIdsAsStrings, Set<Request> uris)1238     private void obtainPhotoIdsAndUrisToLoad(Set<Long> photoIds,
1239             Set<String> photoIdsAsStrings, Set<Request> uris) {
1240         photoIds.clear();
1241         photoIdsAsStrings.clear();
1242         uris.clear();
1243 
1244         boolean jpegsDecoded = false;
1245 
1246         /*
1247          * Since the call is made from the loader thread, the map could be
1248          * changing during the iteration. That's not really a problem:
1249          * ConcurrentHashMap will allow those changes to happen without throwing
1250          * exceptions. Since we may miss some requests in the situation of
1251          * concurrent change, we will need to check the map again once loading
1252          * is complete.
1253          */
1254         Iterator<Request> iterator = mPendingRequests.values().iterator();
1255         while (iterator.hasNext()) {
1256             Request request = iterator.next();
1257             final BitmapHolder holder = mBitmapHolderCache.get(request.getKey());
1258             if (holder == BITMAP_UNAVAILABLE) {
1259                 continue;
1260             }
1261             if (holder != null && holder.bytes != null && holder.fresh &&
1262                     (holder.bitmapRef == null || holder.bitmapRef.get() == null)) {
1263                 // This was previously loaded but we don't currently have the inflated Bitmap
1264                 inflateBitmap(holder, request.getRequestedExtent());
1265                 jpegsDecoded = true;
1266             } else {
1267                 if (holder == null || !holder.fresh) {
1268                     if (request.isUriRequest()) {
1269                         uris.add(request);
1270                     } else {
1271                         photoIds.add(request.getId());
1272                         photoIdsAsStrings.add(String.valueOf(request.mId));
1273                     }
1274                 }
1275             }
1276         }
1277 
1278         if (jpegsDecoded) mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
1279     }
1280 
1281     /**
1282      * The thread that performs loading of photos from the database.
1283      */
1284     private class LoaderThread extends HandlerThread implements Callback {
1285         private static final int BUFFER_SIZE = 1024*16;
1286         private static final int MESSAGE_PRELOAD_PHOTOS = 0;
1287         private static final int MESSAGE_LOAD_PHOTOS = 1;
1288 
1289         /**
1290          * A pause between preload batches that yields to the UI thread.
1291          */
1292         private static final int PHOTO_PRELOAD_DELAY = 1000;
1293 
1294         /**
1295          * Number of photos to preload per batch.
1296          */
1297         private static final int PRELOAD_BATCH = 25;
1298 
1299         /**
1300          * Maximum number of photos to preload.  If the cache size is 2Mb and
1301          * the expected average size of a photo is 4kb, then this number should be 2Mb/4kb = 500.
1302          */
1303         private static final int MAX_PHOTOS_TO_PRELOAD = 100;
1304 
1305         private final ContentResolver mResolver;
1306         private final StringBuilder mStringBuilder = new StringBuilder();
1307         private final Set<Long> mPhotoIds = Sets.newHashSet();
1308         private final Set<String> mPhotoIdsAsStrings = Sets.newHashSet();
1309         private final Set<Request> mPhotoUris = Sets.newHashSet();
1310         private final List<Long> mPreloadPhotoIds = Lists.newArrayList();
1311 
1312         private Handler mLoaderThreadHandler;
1313         private byte mBuffer[];
1314 
1315         private static final int PRELOAD_STATUS_NOT_STARTED = 0;
1316         private static final int PRELOAD_STATUS_IN_PROGRESS = 1;
1317         private static final int PRELOAD_STATUS_DONE = 2;
1318 
1319         private int mPreloadStatus = PRELOAD_STATUS_NOT_STARTED;
1320 
LoaderThread(ContentResolver resolver)1321         public LoaderThread(ContentResolver resolver) {
1322             super(LOADER_THREAD_NAME);
1323             mResolver = resolver;
1324         }
1325 
ensureHandler()1326         public void ensureHandler() {
1327             if (mLoaderThreadHandler == null) {
1328                 mLoaderThreadHandler = new Handler(getLooper(), this);
1329             }
1330         }
1331 
1332         /**
1333          * Kicks off preloading of the next batch of photos on the background thread.
1334          * Preloading will happen after a delay: we want to yield to the UI thread
1335          * as much as possible.
1336          * <p>
1337          * If preloading is already complete, does nothing.
1338          */
requestPreloading()1339         public void requestPreloading() {
1340             if (mPreloadStatus == PRELOAD_STATUS_DONE) {
1341                 return;
1342             }
1343 
1344             ensureHandler();
1345             if (mLoaderThreadHandler.hasMessages(MESSAGE_LOAD_PHOTOS)) {
1346                 return;
1347             }
1348 
1349             mLoaderThreadHandler.sendEmptyMessageDelayed(
1350                     MESSAGE_PRELOAD_PHOTOS, PHOTO_PRELOAD_DELAY);
1351         }
1352 
1353         /**
1354          * Sends a message to this thread to load requested photos.  Cancels a preloading
1355          * request, if any: we don't want preloading to impede loading of the photos
1356          * we need to display now.
1357          */
requestLoading()1358         public void requestLoading() {
1359             ensureHandler();
1360             mLoaderThreadHandler.removeMessages(MESSAGE_PRELOAD_PHOTOS);
1361             mLoaderThreadHandler.sendEmptyMessage(MESSAGE_LOAD_PHOTOS);
1362         }
1363 
1364         /**
1365          * Receives the above message, loads photos and then sends a message
1366          * to the main thread to process them.
1367          */
1368         @Override
handleMessage(Message msg)1369         public boolean handleMessage(Message msg) {
1370             switch (msg.what) {
1371                 case MESSAGE_PRELOAD_PHOTOS:
1372                     preloadPhotosInBackground();
1373                     break;
1374                 case MESSAGE_LOAD_PHOTOS:
1375                     loadPhotosInBackground();
1376                     break;
1377             }
1378             return true;
1379         }
1380 
1381         /**
1382          * The first time it is called, figures out which photos need to be preloaded.
1383          * Each subsequent call preloads the next batch of photos and requests
1384          * another cycle of preloading after a delay.  The whole process ends when
1385          * we either run out of photos to preload or fill up cache.
1386          */
preloadPhotosInBackground()1387         private void preloadPhotosInBackground() {
1388             if (mPreloadStatus == PRELOAD_STATUS_DONE) {
1389                 return;
1390             }
1391 
1392             if (mPreloadStatus == PRELOAD_STATUS_NOT_STARTED) {
1393                 queryPhotosForPreload();
1394                 if (mPreloadPhotoIds.isEmpty()) {
1395                     mPreloadStatus = PRELOAD_STATUS_DONE;
1396                 } else {
1397                     mPreloadStatus = PRELOAD_STATUS_IN_PROGRESS;
1398                 }
1399                 requestPreloading();
1400                 return;
1401             }
1402 
1403             if (mBitmapHolderCache.size() > mBitmapHolderCacheRedZoneBytes) {
1404                 mPreloadStatus = PRELOAD_STATUS_DONE;
1405                 return;
1406             }
1407 
1408             mPhotoIds.clear();
1409             mPhotoIdsAsStrings.clear();
1410 
1411             int count = 0;
1412             int preloadSize = mPreloadPhotoIds.size();
1413             while(preloadSize > 0 && mPhotoIds.size() < PRELOAD_BATCH) {
1414                 preloadSize--;
1415                 count++;
1416                 Long photoId = mPreloadPhotoIds.get(preloadSize);
1417                 mPhotoIds.add(photoId);
1418                 mPhotoIdsAsStrings.add(photoId.toString());
1419                 mPreloadPhotoIds.remove(preloadSize);
1420             }
1421 
1422             loadThumbnails(true);
1423 
1424             if (preloadSize == 0) {
1425                 mPreloadStatus = PRELOAD_STATUS_DONE;
1426             }
1427 
1428             if (Log.isLoggable(TAG, Log.VERBOSE)) {
1429                 Log.v(TAG, "Preloaded " + count + " photos.  Cached bytes: "
1430                         + mBitmapHolderCache.size());
1431             }
1432 
1433             requestPreloading();
1434         }
1435 
queryPhotosForPreload()1436         private void queryPhotosForPreload() {
1437             Cursor cursor = null;
1438             try {
1439                 Uri uri = Contacts.CONTENT_URI.buildUpon().appendQueryParameter(
1440                         ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT))
1441                         .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
1442                                 String.valueOf(MAX_PHOTOS_TO_PRELOAD))
1443                         .build();
1444                 cursor = mResolver.query(uri, new String[] { Contacts.PHOTO_ID },
1445                         Contacts.PHOTO_ID + " NOT NULL AND " + Contacts.PHOTO_ID + "!=0",
1446                         null,
1447                         Contacts.STARRED + " DESC, " + Contacts.LAST_TIME_CONTACTED + " DESC");
1448 
1449                 if (cursor != null) {
1450                     while (cursor.moveToNext()) {
1451                         // Insert them in reverse order, because we will be taking
1452                         // them from the end of the list for loading.
1453                         mPreloadPhotoIds.add(0, cursor.getLong(0));
1454                     }
1455                 }
1456             } finally {
1457                 if (cursor != null) {
1458                     cursor.close();
1459                 }
1460             }
1461         }
1462 
loadPhotosInBackground()1463         private void loadPhotosInBackground() {
1464             if (!PermissionsUtil.hasPermission(mContext,
1465                     android.Manifest.permission.READ_CONTACTS)) {
1466                 return;
1467             }
1468             obtainPhotoIdsAndUrisToLoad(mPhotoIds, mPhotoIdsAsStrings, mPhotoUris);
1469             loadThumbnails(false);
1470             loadUriBasedPhotos();
1471             requestPreloading();
1472         }
1473 
1474         /** Loads thumbnail photos with ids */
loadThumbnails(boolean preloading)1475         private void loadThumbnails(boolean preloading) {
1476             if (mPhotoIds.isEmpty()) {
1477                 return;
1478             }
1479 
1480             // Remove loaded photos from the preload queue: we don't want
1481             // the preloading process to load them again.
1482             if (!preloading && mPreloadStatus == PRELOAD_STATUS_IN_PROGRESS) {
1483                 for (Long id : mPhotoIds) {
1484                     mPreloadPhotoIds.remove(id);
1485                 }
1486                 if (mPreloadPhotoIds.isEmpty()) {
1487                     mPreloadStatus = PRELOAD_STATUS_DONE;
1488                 }
1489             }
1490 
1491             mStringBuilder.setLength(0);
1492             mStringBuilder.append(Photo._ID + " IN(");
1493             for (int i = 0; i < mPhotoIds.size(); i++) {
1494                 if (i != 0) {
1495                     mStringBuilder.append(',');
1496                 }
1497                 mStringBuilder.append('?');
1498             }
1499             mStringBuilder.append(')');
1500 
1501             Cursor cursor = null;
1502             try {
1503                 if (DEBUG) Log.d(TAG, "Loading " + TextUtils.join(",", mPhotoIdsAsStrings));
1504                 cursor = mResolver.query(Data.CONTENT_URI,
1505                         COLUMNS,
1506                         mStringBuilder.toString(),
1507                         mPhotoIdsAsStrings.toArray(EMPTY_STRING_ARRAY),
1508                         null);
1509 
1510                 if (cursor != null) {
1511                     while (cursor.moveToNext()) {
1512                         Long id = cursor.getLong(0);
1513                         byte[] bytes = cursor.getBlob(1);
1514                         if (bytes == null) {
1515                             bytes = new byte[0];
1516                         }
1517                         cacheBitmap(id, bytes, preloading, -1);
1518                         mPhotoIds.remove(id);
1519                     }
1520                 }
1521             } finally {
1522                 if (cursor != null) {
1523                     cursor.close();
1524                 }
1525             }
1526 
1527             // Remaining photos were not found in the contacts database (but might be in profile).
1528             for (Long id : mPhotoIds) {
1529                 if (ContactsContract.isProfileId(id)) {
1530                     Cursor profileCursor = null;
1531                     try {
1532                         profileCursor = mResolver.query(
1533                                 ContentUris.withAppendedId(Data.CONTENT_URI, id),
1534                                 COLUMNS, null, null, null);
1535                         if (profileCursor != null && profileCursor.moveToFirst()) {
1536                             byte[] bytes = profileCursor.getBlob(1);
1537                             if (bytes == null) {
1538                                 bytes = new byte[0];
1539                             }
1540                             cacheBitmap(profileCursor.getLong(0), bytes, preloading, -1);
1541                         } else {
1542                             // Couldn't load a photo this way either.
1543                             cacheBitmap(id, null, preloading, -1);
1544                         }
1545                     } finally {
1546                         if (profileCursor != null) {
1547                             profileCursor.close();
1548                         }
1549                     }
1550                 } else {
1551                     // Not a profile photo and not found - mark the cache accordingly
1552                     cacheBitmap(id, null, preloading, -1);
1553                 }
1554             }
1555 
1556             mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
1557         }
1558 
1559         /**
1560          * Loads photos referenced with Uris. Those can be remote thumbnails
1561          * (from directory searches), display photos etc
1562          */
loadUriBasedPhotos()1563         private void loadUriBasedPhotos() {
1564             for (Request uriRequest : mPhotoUris) {
1565                 // Keep the original URI and use this to key into the cache.  Failure to do so will
1566                 // result in an image being continually reloaded into cache if the original URI
1567                 // has a contact type encodedFragment (eg nearby places business photo URLs).
1568                 Uri originalUri = uriRequest.getUri();
1569 
1570                 // Strip off the "contact type" we added to the URI to ensure it was identifiable as
1571                 // a business photo -- there is no need to pass this on to the server.
1572                 Uri uri = ContactPhotoManager.removeContactType(originalUri);
1573 
1574                 if (mBuffer == null) {
1575                     mBuffer = new byte[BUFFER_SIZE];
1576                 }
1577                 try {
1578                     if (DEBUG) Log.d(TAG, "Loading " + uri);
1579                     final String scheme = uri.getScheme();
1580                     InputStream is = null;
1581                     if (scheme.equals("http") || scheme.equals("https")) {
1582                         TrafficStats.setThreadStatsTag(TrafficStatsTags.CONTACT_PHOTO_DOWNLOAD_TAG);
1583                         final HttpURLConnection connection =
1584                                 (HttpURLConnection) new URL(uri.toString()).openConnection();
1585 
1586                         // Include the user agent if it is specified.
1587                         if (!TextUtils.isEmpty(mUserAgent)) {
1588                             connection.setRequestProperty("User-Agent", mUserAgent);
1589                         }
1590                         try {
1591                             is = connection.getInputStream();
1592                         } catch (IOException e) {
1593                             connection.disconnect();
1594                             is = null;
1595                         }
1596                         TrafficStats.clearThreadStatsTag();
1597                     } else {
1598                         is = mResolver.openInputStream(uri);
1599                     }
1600                     if (is != null) {
1601                         ByteArrayOutputStream baos = new ByteArrayOutputStream();
1602                         try {
1603                             int size;
1604                             while ((size = is.read(mBuffer)) != -1) {
1605                                 baos.write(mBuffer, 0, size);
1606                             }
1607                         } finally {
1608                             is.close();
1609                         }
1610                         cacheBitmap(originalUri, baos.toByteArray(), false,
1611                                 uriRequest.getRequestedExtent());
1612                         mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
1613                     } else {
1614                         if (Log.isLoggable(TAG, Log.VERBOSE)) {
1615                             Log.v(TAG, "Cannot load photo " + uri);
1616                         }
1617                         cacheBitmap(originalUri, null, false, uriRequest.getRequestedExtent());
1618                     }
1619                 } catch (final Exception | OutOfMemoryError ex) {
1620                     if (Log.isLoggable(TAG, Log.VERBOSE)) {
1621                         Log.v(TAG, "Cannot load photo " + uri, ex);
1622                     }
1623                     cacheBitmap(originalUri, null, false, uriRequest.getRequestedExtent());
1624                 }
1625             }
1626         }
1627     }
1628 
1629     /**
1630      * A holder for either a Uri or an id and a flag whether this was requested for the dark or
1631      * light theme
1632      */
1633     private static final class Request {
1634         private final long mId;
1635         private final Uri mUri;
1636         private final boolean mDarkTheme;
1637         private final int mRequestedExtent;
1638         private final DefaultImageProvider mDefaultProvider;
1639         private final DefaultImageRequest mDefaultRequest;
1640         /**
1641          * Whether or not the contact photo is to be displayed as a circle
1642          */
1643         private final boolean mIsCircular;
1644 
Request(long id, Uri uri, int requestedExtent, boolean darkTheme, boolean isCircular, DefaultImageProvider defaultProvider, DefaultImageRequest defaultRequest)1645         private Request(long id, Uri uri, int requestedExtent, boolean darkTheme,
1646                 boolean isCircular, DefaultImageProvider defaultProvider,
1647                 DefaultImageRequest defaultRequest) {
1648             mId = id;
1649             mUri = uri;
1650             mDarkTheme = darkTheme;
1651             mIsCircular = isCircular;
1652             mRequestedExtent = requestedExtent;
1653             mDefaultProvider = defaultProvider;
1654             mDefaultRequest = defaultRequest;
1655         }
1656 
createFromThumbnailId(long id, boolean darkTheme, boolean isCircular, DefaultImageProvider defaultProvider, DefaultImageRequest defaultRequest)1657         public static Request createFromThumbnailId(long id, boolean darkTheme, boolean isCircular,
1658                 DefaultImageProvider defaultProvider, DefaultImageRequest defaultRequest) {
1659             return new Request(id, null /* no URI */, -1, darkTheme, isCircular, defaultProvider,
1660                     defaultRequest);
1661         }
1662 
createFromUri(Uri uri, int requestedExtent, boolean darkTheme, boolean isCircular, DefaultImageProvider defaultProvider)1663         public static Request createFromUri(Uri uri, int requestedExtent, boolean darkTheme,
1664                 boolean isCircular, DefaultImageProvider defaultProvider) {
1665             return createFromUri(uri, requestedExtent, darkTheme, isCircular, defaultProvider,
1666                     /* defaultRequest */ null);
1667         }
1668 
createFromUri(Uri uri, int requestedExtent, boolean darkTheme, boolean isCircular, DefaultImageProvider defaultProvider, DefaultImageRequest defaultRequest)1669         public static Request createFromUri(Uri uri, int requestedExtent, boolean darkTheme,
1670                 boolean isCircular, DefaultImageProvider defaultProvider,
1671                 DefaultImageRequest defaultRequest) {
1672             return new Request(0 /* no ID */, uri, requestedExtent, darkTheme, isCircular,
1673                     defaultProvider, defaultRequest);
1674         }
1675 
isUriRequest()1676         public boolean isUriRequest() {
1677             return mUri != null;
1678         }
1679 
getUri()1680         public Uri getUri() {
1681             return mUri;
1682         }
1683 
getId()1684         public long getId() {
1685             return mId;
1686         }
1687 
getRequestedExtent()1688         public int getRequestedExtent() {
1689             return mRequestedExtent;
1690         }
1691 
1692         @Override
hashCode()1693         public int hashCode() {
1694             final int prime = 31;
1695             int result = 1;
1696             result = prime * result + (int) (mId ^ (mId >>> 32));
1697             result = prime * result + mRequestedExtent;
1698             result = prime * result + ((mUri == null) ? 0 : mUri.hashCode());
1699             return result;
1700         }
1701 
1702         @Override
equals(Object obj)1703         public boolean equals(Object obj) {
1704             if (this == obj) return true;
1705             if (obj == null) return false;
1706             if (getClass() != obj.getClass()) return false;
1707             final Request that = (Request) obj;
1708             if (mId != that.mId) return false;
1709             if (mRequestedExtent != that.mRequestedExtent) return false;
1710             if (!UriUtils.areEqual(mUri, that.mUri)) return false;
1711             // Don't compare equality of mDarkTheme because it is only used in the default contact
1712             // photo case. When the contact does have a photo, the contact photo is the same
1713             // regardless of mDarkTheme, so we shouldn't need to put the photo request on the queue
1714             // twice.
1715             return true;
1716         }
1717 
getKey()1718         public Object getKey() {
1719             return mUri == null ? mId : mUri;
1720         }
1721 
1722         /**
1723          * Applies the default image to the current view. If the request is URI-based, looks for
1724          * the contact type encoded fragment to determine if this is a request for a business photo,
1725          * in which case we will load the default business photo.
1726          *
1727          * @param view The current image view to apply the image to.
1728          * @param isCircular Whether the image is circular or not.
1729          */
applyDefaultImage(ImageView view, boolean isCircular)1730         public void applyDefaultImage(ImageView view, boolean isCircular) {
1731             final DefaultImageRequest request;
1732 
1733             if (mDefaultRequest == null) {
1734                 if (isCircular) {
1735                     request = ContactPhotoManager.isBusinessContactUri(mUri)
1736                             ? DefaultImageRequest.EMPTY_CIRCULAR_BUSINESS_IMAGE_REQUEST
1737                             : DefaultImageRequest.EMPTY_CIRCULAR_DEFAULT_IMAGE_REQUEST;
1738                 } else {
1739                     request = ContactPhotoManager.isBusinessContactUri(mUri)
1740                             ? DefaultImageRequest.EMPTY_DEFAULT_BUSINESS_IMAGE_REQUEST
1741                             : DefaultImageRequest.EMPTY_DEFAULT_IMAGE_REQUEST;
1742                 }
1743             } else {
1744                 request = mDefaultRequest;
1745             }
1746             mDefaultProvider.applyDefaultImage(view, mRequestedExtent, mDarkTheme, request);
1747         }
1748     }
1749 }
1750