Home | History | Annotate | Download | only in common
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.contacts.common;
     18 
     19 import android.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      */
    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      */
    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      */
    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      */
    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      */
    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 
    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 
    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 
    345         public DefaultImageRequest() {
    346         }
    347 
    348         public DefaultImageRequest(String displayName, String identifier, boolean isCircular) {
    349             this(displayName, identifier, TYPE_DEFAULT, SCALE_DEFAULT, OFFSET_DEFAULT, isCircular);
    350         }
    351 
    352         public DefaultImageRequest(String displayName, String identifier, int contactType,
    353                 boolean isCircular) {
    354             this(displayName, identifier, contactType, SCALE_DEFAULT, OFFSET_DEFAULT, isCircular);
    355         }
    356 
    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          */
    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
    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 
    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
    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 
    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 
    449     public static synchronized ContactPhotoManager createContactPhotoManager(Context context) {
    450         return new ContactPhotoManagerImpl(context);
    451     }
    452 
    453     @VisibleForTesting
    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      */
    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     */
    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      */
    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      */
    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      */
    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      */
    529     public abstract void removePhoto(ImageView view);
    530 
    531     /**
    532      * Cancels all pending requests to load photos asynchronously.
    533      */
    534     public abstract void cancelPendingRequests(View fragmentRootView);
    535 
    536     /**
    537      * Temporarily stops loading photos from the database.
    538      */
    539     public abstract void pause();
    540 
    541     /**
    542      * Resumes loading photos from the database.
    543      */
    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      */
    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      */
    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      */
    565     public abstract void preloadPhotosInBackground();
    566 
    567     // ComponentCallbacks2
    568     @Override
    569     public void onConfigurationChanged(Configuration newConfig) {
    570     }
    571 
    572     // ComponentCallbacks2
    573     @Override
    574     public void onLowMemory() {
    575     }
    576 
    577     // ComponentCallbacks2
    578     @Override
    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 
    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 
    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. */
    756     private static String btk(int bytes) {
    757         return ((bytes + 1023) / 1024) + "K";
    758     }
    759 
    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      */
    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
    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
    818     public void preloadPhotosInBackground() {
    819         ensureLoaderThread();
    820         mLoaderThread.requestPreloading();
    821     }
    822 
    823     @Override
    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
    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 
    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 
    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
    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
    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 
    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
    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      */
    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      */
    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      */
   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 
   1079     public void clear() {
   1080         if (DEBUG) Log.d(TAG, "clear");
   1081         mPendingRequests.clear();
   1082         mBitmapHolderCache.evictAll();
   1083         mBitmapCache.evictAll();
   1084     }
   1085 
   1086     @Override
   1087     public void pause() {
   1088         mPaused = true;
   1089     }
   1090 
   1091     @Override
   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      */
   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
   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 
   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      */
   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      */
   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      */
   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
   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      */
   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 
   1319         public LoaderThread(ContentResolver resolver) {
   1320             super(LOADER_THREAD_NAME);
   1321             mResolver = resolver;
   1322         }
   1323 
   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          */
   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          */
   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
   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          */
   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 
   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 
   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 */
   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          */
   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 
   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 
   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 
   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 
   1651         public boolean isUriRequest() {
   1652             return mUri != null;
   1653         }
   1654 
   1655         public Uri getUri() {
   1656             return mUri;
   1657         }
   1658 
   1659         public long getId() {
   1660             return mId;
   1661         }
   1662 
   1663         public int getRequestedExtent() {
   1664             return mRequestedExtent;
   1665         }
   1666 
   1667         @Override
   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
   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 
   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          */
   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