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