1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.contacts.common;
18 
19 import android.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 android.support.v4.graphics.drawable.RoundedBitmapDrawable;
50 import android.support.v4.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.common.lettertiles.LetterTileDrawable;
59 import com.android.contacts.common.util.BitmapUtil;
60 import com.android.contacts.common.util.PermissionsUtil;
61 import com.android.contacts.common.util.TrafficStatsTags;
62 import com.android.contacts.common.util.UriUtils;
63 import com.android.contacts.commonbind.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      * Dummy object used to indicate that a bitmap for a given key could not be stored in the
606      * 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));
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));
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) {
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      */
cacheBitmap(Object key, byte[] bytes, boolean preloading, int requestedExtent)1182     private void cacheBitmap(Object key, byte[] bytes, boolean preloading, int requestedExtent) {
1183         if (DEBUG) {
1184             BitmapHolder prev = mBitmapHolderCache.get(key);
1185             if (prev != null && prev.bytes != null) {
1186                 Log.d(TAG, "Overwriting cache: key=" + key + (prev.fresh ? " FRESH" : " stale"));
1187                 if (prev.fresh) {
1188                     mFreshCacheOverwrite.incrementAndGet();
1189                 } else {
1190                     mStaleCacheOverwrite.incrementAndGet();
1191                 }
1192             }
1193             Log.d(TAG, "Caching data: key=" + key + ", " +
1194                     (bytes == null ? "<null>" : btk(bytes.length)));
1195         }
1196         BitmapHolder holder = new BitmapHolder(bytes,
1197                 bytes == null ? -1 : BitmapUtil.getSmallerExtentFromBytes(bytes));
1198 
1199         // Unless this image is being preloaded, decode it right away while
1200         // we are still on the background thread.
1201         if (!preloading) {
1202             inflateBitmap(holder, requestedExtent);
1203         }
1204 
1205         if (bytes != null) {
1206             mBitmapHolderCache.put(key, holder);
1207             if (mBitmapHolderCache.get(key) != holder) {
1208                 Log.w(TAG, "Bitmap too big to fit in cache.");
1209                 mBitmapHolderCache.put(key, BITMAP_UNAVAILABLE);
1210             }
1211         } else {
1212             mBitmapHolderCache.put(key, BITMAP_UNAVAILABLE);
1213         }
1214 
1215         mBitmapHolderCacheAllUnfresh = false;
1216     }
1217 
1218     @Override
cacheBitmap(Uri photoUri, Bitmap bitmap, byte[] photoBytes)1219     public void cacheBitmap(Uri photoUri, Bitmap bitmap, byte[] photoBytes) {
1220         final int smallerExtent = Math.min(bitmap.getWidth(), bitmap.getHeight());
1221         // We can pretend here that the extent of the photo was the size that we originally
1222         // requested
1223         Request request = Request.createFromUri(photoUri, smallerExtent, false /* darkTheme */,
1224                 false /* isCircular */ , DEFAULT_AVATAR);
1225         BitmapHolder holder = new BitmapHolder(photoBytes, smallerExtent);
1226         holder.bitmapRef = new SoftReference<Bitmap>(bitmap);
1227         mBitmapHolderCache.put(request.getKey(), holder);
1228         mBitmapHolderCacheAllUnfresh = false;
1229         mBitmapCache.put(request.getKey(), bitmap);
1230     }
1231 
1232     /**
1233      * Populates an array of photo IDs that need to be loaded. Also decodes bitmaps that we have
1234      * already loaded
1235      */
obtainPhotoIdsAndUrisToLoad(Set<Long> photoIds, Set<String> photoIdsAsStrings, Set<Request> uris)1236     private void obtainPhotoIdsAndUrisToLoad(Set<Long> photoIds,
1237             Set<String> photoIdsAsStrings, Set<Request> uris) {
1238         photoIds.clear();
1239         photoIdsAsStrings.clear();
1240         uris.clear();
1241 
1242         boolean jpegsDecoded = false;
1243 
1244         /*
1245          * Since the call is made from the loader thread, the map could be
1246          * changing during the iteration. That's not really a problem:
1247          * ConcurrentHashMap will allow those changes to happen without throwing
1248          * exceptions. Since we may miss some requests in the situation of
1249          * concurrent change, we will need to check the map again once loading
1250          * is complete.
1251          */
1252         Iterator<Request> iterator = mPendingRequests.values().iterator();
1253         while (iterator.hasNext()) {
1254             Request request = iterator.next();
1255             final BitmapHolder holder = mBitmapHolderCache.get(request.getKey());
1256             if (holder == BITMAP_UNAVAILABLE) {
1257                 continue;
1258             }
1259             if (holder != null && holder.bytes != null && holder.fresh &&
1260                     (holder.bitmapRef == null || holder.bitmapRef.get() == null)) {
1261                 // This was previously loaded but we don't currently have the inflated Bitmap
1262                 inflateBitmap(holder, request.getRequestedExtent());
1263                 jpegsDecoded = true;
1264             } else {
1265                 if (holder == null || !holder.fresh) {
1266                     if (request.isUriRequest()) {
1267                         uris.add(request);
1268                     } else {
1269                         photoIds.add(request.getId());
1270                         photoIdsAsStrings.add(String.valueOf(request.mId));
1271                     }
1272                 }
1273             }
1274         }
1275 
1276         if (jpegsDecoded) mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
1277     }
1278 
1279     /**
1280      * The thread that performs loading of photos from the database.
1281      */
1282     private class LoaderThread extends HandlerThread implements Callback {
1283         private static final int BUFFER_SIZE = 1024*16;
1284         private static final int MESSAGE_PRELOAD_PHOTOS = 0;
1285         private static final int MESSAGE_LOAD_PHOTOS = 1;
1286 
1287         /**
1288          * A pause between preload batches that yields to the UI thread.
1289          */
1290         private static final int PHOTO_PRELOAD_DELAY = 1000;
1291 
1292         /**
1293          * Number of photos to preload per batch.
1294          */
1295         private static final int PRELOAD_BATCH = 25;
1296 
1297         /**
1298          * Maximum number of photos to preload.  If the cache size is 2Mb and
1299          * the expected average size of a photo is 4kb, then this number should be 2Mb/4kb = 500.
1300          */
1301         private static final int MAX_PHOTOS_TO_PRELOAD = 100;
1302 
1303         private final ContentResolver mResolver;
1304         private final StringBuilder mStringBuilder = new StringBuilder();
1305         private final Set<Long> mPhotoIds = Sets.newHashSet();
1306         private final Set<String> mPhotoIdsAsStrings = Sets.newHashSet();
1307         private final Set<Request> mPhotoUris = Sets.newHashSet();
1308         private final List<Long> mPreloadPhotoIds = Lists.newArrayList();
1309 
1310         private Handler mLoaderThreadHandler;
1311         private byte mBuffer[];
1312 
1313         private static final int PRELOAD_STATUS_NOT_STARTED = 0;
1314         private static final int PRELOAD_STATUS_IN_PROGRESS = 1;
1315         private static final int PRELOAD_STATUS_DONE = 2;
1316 
1317         private int mPreloadStatus = PRELOAD_STATUS_NOT_STARTED;
1318 
LoaderThread(ContentResolver resolver)1319         public LoaderThread(ContentResolver resolver) {
1320             super(LOADER_THREAD_NAME);
1321             mResolver = resolver;
1322         }
1323 
ensureHandler()1324         public void ensureHandler() {
1325             if (mLoaderThreadHandler == null) {
1326                 mLoaderThreadHandler = new Handler(getLooper(), this);
1327             }
1328         }
1329 
1330         /**
1331          * Kicks off preloading of the next batch of photos on the background thread.
1332          * Preloading will happen after a delay: we want to yield to the UI thread
1333          * as much as possible.
1334          * <p>
1335          * If preloading is already complete, does nothing.
1336          */
requestPreloading()1337         public void requestPreloading() {
1338             if (mPreloadStatus == PRELOAD_STATUS_DONE) {
1339                 return;
1340             }
1341 
1342             ensureHandler();
1343             if (mLoaderThreadHandler.hasMessages(MESSAGE_LOAD_PHOTOS)) {
1344                 return;
1345             }
1346 
1347             mLoaderThreadHandler.sendEmptyMessageDelayed(
1348                     MESSAGE_PRELOAD_PHOTOS, PHOTO_PRELOAD_DELAY);
1349         }
1350 
1351         /**
1352          * Sends a message to this thread to load requested photos.  Cancels a preloading
1353          * request, if any: we don't want preloading to impede loading of the photos
1354          * we need to display now.
1355          */
requestLoading()1356         public void requestLoading() {
1357             ensureHandler();
1358             mLoaderThreadHandler.removeMessages(MESSAGE_PRELOAD_PHOTOS);
1359             mLoaderThreadHandler.sendEmptyMessage(MESSAGE_LOAD_PHOTOS);
1360         }
1361 
1362         /**
1363          * Receives the above message, loads photos and then sends a message
1364          * to the main thread to process them.
1365          */
1366         @Override
handleMessage(Message msg)1367         public boolean handleMessage(Message msg) {
1368             switch (msg.what) {
1369                 case MESSAGE_PRELOAD_PHOTOS:
1370                     preloadPhotosInBackground();
1371                     break;
1372                 case MESSAGE_LOAD_PHOTOS:
1373                     loadPhotosInBackground();
1374                     break;
1375             }
1376             return true;
1377         }
1378 
1379         /**
1380          * The first time it is called, figures out which photos need to be preloaded.
1381          * Each subsequent call preloads the next batch of photos and requests
1382          * another cycle of preloading after a delay.  The whole process ends when
1383          * we either run out of photos to preload or fill up cache.
1384          */
preloadPhotosInBackground()1385         private void preloadPhotosInBackground() {
1386             if (mPreloadStatus == PRELOAD_STATUS_DONE) {
1387                 return;
1388             }
1389 
1390             if (mPreloadStatus == PRELOAD_STATUS_NOT_STARTED) {
1391                 queryPhotosForPreload();
1392                 if (mPreloadPhotoIds.isEmpty()) {
1393                     mPreloadStatus = PRELOAD_STATUS_DONE;
1394                 } else {
1395                     mPreloadStatus = PRELOAD_STATUS_IN_PROGRESS;
1396                 }
1397                 requestPreloading();
1398                 return;
1399             }
1400 
1401             if (mBitmapHolderCache.size() > mBitmapHolderCacheRedZoneBytes) {
1402                 mPreloadStatus = PRELOAD_STATUS_DONE;
1403                 return;
1404             }
1405 
1406             mPhotoIds.clear();
1407             mPhotoIdsAsStrings.clear();
1408 
1409             int count = 0;
1410             int preloadSize = mPreloadPhotoIds.size();
1411             while(preloadSize > 0 && mPhotoIds.size() < PRELOAD_BATCH) {
1412                 preloadSize--;
1413                 count++;
1414                 Long photoId = mPreloadPhotoIds.get(preloadSize);
1415                 mPhotoIds.add(photoId);
1416                 mPhotoIdsAsStrings.add(photoId.toString());
1417                 mPreloadPhotoIds.remove(preloadSize);
1418             }
1419 
1420             loadThumbnails(true);
1421 
1422             if (preloadSize == 0) {
1423                 mPreloadStatus = PRELOAD_STATUS_DONE;
1424             }
1425 
1426             Log.v(TAG, "Preloaded " + count + " photos.  Cached bytes: "
1427                     + mBitmapHolderCache.size());
1428 
1429             requestPreloading();
1430         }
1431 
queryPhotosForPreload()1432         private void queryPhotosForPreload() {
1433             Cursor cursor = null;
1434             try {
1435                 Uri uri = Contacts.CONTENT_URI.buildUpon().appendQueryParameter(
1436                         ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT))
1437                         .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
1438                                 String.valueOf(MAX_PHOTOS_TO_PRELOAD))
1439                         .build();
1440                 cursor = mResolver.query(uri, new String[] { Contacts.PHOTO_ID },
1441                         Contacts.PHOTO_ID + " NOT NULL AND " + Contacts.PHOTO_ID + "!=0",
1442                         null,
1443                         Contacts.STARRED + " DESC, " + Contacts.LAST_TIME_CONTACTED + " DESC");
1444 
1445                 if (cursor != null) {
1446                     while (cursor.moveToNext()) {
1447                         // Insert them in reverse order, because we will be taking
1448                         // them from the end of the list for loading.
1449                         mPreloadPhotoIds.add(0, cursor.getLong(0));
1450                     }
1451                 }
1452             } finally {
1453                 if (cursor != null) {
1454                     cursor.close();
1455                 }
1456             }
1457         }
1458 
loadPhotosInBackground()1459         private void loadPhotosInBackground() {
1460             if (!PermissionsUtil.hasPermission(mContext,
1461                     android.Manifest.permission.READ_CONTACTS)) {
1462                 return;
1463             }
1464             obtainPhotoIdsAndUrisToLoad(mPhotoIds, mPhotoIdsAsStrings, mPhotoUris);
1465             loadThumbnails(false);
1466             loadUriBasedPhotos();
1467             requestPreloading();
1468         }
1469 
1470         /** Loads thumbnail photos with ids */
loadThumbnails(boolean preloading)1471         private void loadThumbnails(boolean preloading) {
1472             if (mPhotoIds.isEmpty()) {
1473                 return;
1474             }
1475 
1476             // Remove loaded photos from the preload queue: we don't want
1477             // the preloading process to load them again.
1478             if (!preloading && mPreloadStatus == PRELOAD_STATUS_IN_PROGRESS) {
1479                 for (Long id : mPhotoIds) {
1480                     mPreloadPhotoIds.remove(id);
1481                 }
1482                 if (mPreloadPhotoIds.isEmpty()) {
1483                     mPreloadStatus = PRELOAD_STATUS_DONE;
1484                 }
1485             }
1486 
1487             mStringBuilder.setLength(0);
1488             mStringBuilder.append(Photo._ID + " IN(");
1489             for (int i = 0; i < mPhotoIds.size(); i++) {
1490                 if (i != 0) {
1491                     mStringBuilder.append(',');
1492                 }
1493                 mStringBuilder.append('?');
1494             }
1495             mStringBuilder.append(')');
1496 
1497             Cursor cursor = null;
1498             try {
1499                 if (DEBUG) Log.d(TAG, "Loading " + TextUtils.join(",", mPhotoIdsAsStrings));
1500                 cursor = mResolver.query(Data.CONTENT_URI,
1501                         COLUMNS,
1502                         mStringBuilder.toString(),
1503                         mPhotoIdsAsStrings.toArray(EMPTY_STRING_ARRAY),
1504                         null);
1505 
1506                 if (cursor != null) {
1507                     while (cursor.moveToNext()) {
1508                         Long id = cursor.getLong(0);
1509                         byte[] bytes = cursor.getBlob(1);
1510                         cacheBitmap(id, bytes, preloading, -1);
1511                         mPhotoIds.remove(id);
1512                     }
1513                 }
1514             } finally {
1515                 if (cursor != null) {
1516                     cursor.close();
1517                 }
1518             }
1519 
1520             // Remaining photos were not found in the contacts database (but might be in profile).
1521             for (Long id : mPhotoIds) {
1522                 if (ContactsContract.isProfileId(id)) {
1523                     Cursor profileCursor = null;
1524                     try {
1525                         profileCursor = mResolver.query(
1526                                 ContentUris.withAppendedId(Data.CONTENT_URI, id),
1527                                 COLUMNS, null, null, null);
1528                         if (profileCursor != null && profileCursor.moveToFirst()) {
1529                             cacheBitmap(profileCursor.getLong(0), profileCursor.getBlob(1),
1530                                     preloading, -1);
1531                         } else {
1532                             // Couldn't load a photo this way either.
1533                             cacheBitmap(id, null, preloading, -1);
1534                         }
1535                     } finally {
1536                         if (profileCursor != null) {
1537                             profileCursor.close();
1538                         }
1539                     }
1540                 } else {
1541                     // Not a profile photo and not found - mark the cache accordingly
1542                     cacheBitmap(id, null, preloading, -1);
1543                 }
1544             }
1545 
1546             mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
1547         }
1548 
1549         /**
1550          * Loads photos referenced with Uris. Those can be remote thumbnails
1551          * (from directory searches), display photos etc
1552          */
loadUriBasedPhotos()1553         private void loadUriBasedPhotos() {
1554             for (Request uriRequest : mPhotoUris) {
1555                 // Keep the original URI and use this to key into the cache.  Failure to do so will
1556                 // result in an image being continually reloaded into cache if the original URI
1557                 // has a contact type encodedFragment (eg nearby places business photo URLs).
1558                 Uri originalUri = uriRequest.getUri();
1559 
1560                 // Strip off the "contact type" we added to the URI to ensure it was identifiable as
1561                 // a business photo -- there is no need to pass this on to the server.
1562                 Uri uri = ContactPhotoManager.removeContactType(originalUri);
1563 
1564                 if (mBuffer == null) {
1565                     mBuffer = new byte[BUFFER_SIZE];
1566                 }
1567                 try {
1568                     if (DEBUG) Log.d(TAG, "Loading " + uri);
1569                     final String scheme = uri.getScheme();
1570                     InputStream is = null;
1571                     if (scheme.equals("http") || scheme.equals("https")) {
1572                         TrafficStats.setThreadStatsTag(TrafficStatsTags.CONTACT_PHOTO_DOWNLOAD_TAG);
1573                         final HttpURLConnection connection =
1574                                 (HttpURLConnection) new URL(uri.toString()).openConnection();
1575 
1576                         // Include the user agent if it is specified.
1577                         if (!TextUtils.isEmpty(mUserAgent)) {
1578                             connection.setRequestProperty("User-Agent", mUserAgent);
1579                         }
1580                         try {
1581                             is = connection.getInputStream();
1582                         } catch (IOException e) {
1583                             connection.disconnect();
1584                             is = null;
1585                         }
1586                         TrafficStats.clearThreadStatsTag();
1587                     } else {
1588                         is = mResolver.openInputStream(uri);
1589                     }
1590                     if (is != null) {
1591                         ByteArrayOutputStream baos = new ByteArrayOutputStream();
1592                         try {
1593                             int size;
1594                             while ((size = is.read(mBuffer)) != -1) {
1595                                 baos.write(mBuffer, 0, size);
1596                             }
1597                         } finally {
1598                             is.close();
1599                         }
1600                         cacheBitmap(originalUri, baos.toByteArray(), false,
1601                                 uriRequest.getRequestedExtent());
1602                         mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
1603                     } else {
1604                         Log.v(TAG, "Cannot load photo " + uri);
1605                         cacheBitmap(originalUri, null, false, uriRequest.getRequestedExtent());
1606                     }
1607                 } catch (final Exception | OutOfMemoryError ex) {
1608                     Log.v(TAG, "Cannot load photo " + uri, ex);
1609                     cacheBitmap(originalUri, null, false, uriRequest.getRequestedExtent());
1610                 }
1611             }
1612         }
1613     }
1614 
1615     /**
1616      * A holder for either a Uri or an id and a flag whether this was requested for the dark or
1617      * light theme
1618      */
1619     private static final class Request {
1620         private final long mId;
1621         private final Uri mUri;
1622         private final boolean mDarkTheme;
1623         private final int mRequestedExtent;
1624         private final DefaultImageProvider mDefaultProvider;
1625         /**
1626          * Whether or not the contact photo is to be displayed as a circle
1627          */
1628         private final boolean mIsCircular;
1629 
Request(long id, Uri uri, int requestedExtent, boolean darkTheme, boolean isCircular, DefaultImageProvider defaultProvider)1630         private Request(long id, Uri uri, int requestedExtent, boolean darkTheme,
1631                 boolean isCircular, DefaultImageProvider defaultProvider) {
1632             mId = id;
1633             mUri = uri;
1634             mDarkTheme = darkTheme;
1635             mIsCircular = isCircular;
1636             mRequestedExtent = requestedExtent;
1637             mDefaultProvider = defaultProvider;
1638         }
1639 
createFromThumbnailId(long id, boolean darkTheme, boolean isCircular, DefaultImageProvider defaultProvider)1640         public static Request createFromThumbnailId(long id, boolean darkTheme, boolean isCircular,
1641                 DefaultImageProvider defaultProvider) {
1642             return new Request(id, null /* no URI */, -1, darkTheme, isCircular, defaultProvider);
1643         }
1644 
createFromUri(Uri uri, int requestedExtent, boolean darkTheme, boolean isCircular, DefaultImageProvider defaultProvider)1645         public static Request createFromUri(Uri uri, int requestedExtent, boolean darkTheme,
1646                 boolean isCircular, DefaultImageProvider defaultProvider) {
1647             return new Request(0 /* no ID */, uri, requestedExtent, darkTheme, isCircular,
1648                     defaultProvider);
1649         }
1650 
isUriRequest()1651         public boolean isUriRequest() {
1652             return mUri != null;
1653         }
1654 
getUri()1655         public Uri getUri() {
1656             return mUri;
1657         }
1658 
getId()1659         public long getId() {
1660             return mId;
1661         }
1662 
getRequestedExtent()1663         public int getRequestedExtent() {
1664             return mRequestedExtent;
1665         }
1666 
1667         @Override
hashCode()1668         public int hashCode() {
1669             final int prime = 31;
1670             int result = 1;
1671             result = prime * result + (int) (mId ^ (mId >>> 32));
1672             result = prime * result + mRequestedExtent;
1673             result = prime * result + ((mUri == null) ? 0 : mUri.hashCode());
1674             return result;
1675         }
1676 
1677         @Override
equals(Object obj)1678         public boolean equals(Object obj) {
1679             if (this == obj) return true;
1680             if (obj == null) return false;
1681             if (getClass() != obj.getClass()) return false;
1682             final Request that = (Request) obj;
1683             if (mId != that.mId) return false;
1684             if (mRequestedExtent != that.mRequestedExtent) return false;
1685             if (!UriUtils.areEqual(mUri, that.mUri)) return false;
1686             // Don't compare equality of mDarkTheme because it is only used in the default contact
1687             // photo case. When the contact does have a photo, the contact photo is the same
1688             // regardless of mDarkTheme, so we shouldn't need to put the photo request on the queue
1689             // twice.
1690             return true;
1691         }
1692 
getKey()1693         public Object getKey() {
1694             return mUri == null ? mId : mUri;
1695         }
1696 
1697         /**
1698          * Applies the default image to the current view. If the request is URI-based, looks for
1699          * the contact type encoded fragment to determine if this is a request for a business photo,
1700          * in which case we will load the default business photo.
1701          *
1702          * @param view The current image view to apply the image to.
1703          * @param isCircular Whether the image is circular or not.
1704          */
applyDefaultImage(ImageView view, boolean isCircular)1705         public void applyDefaultImage(ImageView view, boolean isCircular) {
1706             final DefaultImageRequest request;
1707 
1708             if (isCircular) {
1709                 request = ContactPhotoManager.isBusinessContactUri(mUri)
1710                         ? DefaultImageRequest.EMPTY_CIRCULAR_BUSINESS_IMAGE_REQUEST
1711                         : DefaultImageRequest.EMPTY_CIRCULAR_DEFAULT_IMAGE_REQUEST;
1712             } else {
1713                 request = ContactPhotoManager.isBusinessContactUri(mUri)
1714                         ? DefaultImageRequest.EMPTY_DEFAULT_BUSINESS_IMAGE_REQUEST
1715                         : DefaultImageRequest.EMPTY_DEFAULT_IMAGE_REQUEST;
1716             }
1717             mDefaultProvider.applyDefaultImage(view, mRequestedExtent, mDarkTheme, request);
1718         }
1719     }
1720 }
1721