Home | History | Annotate | Download | only in contacts
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.contacts;
     18 
     19 import android.app.ActivityManager;
     20 import android.content.ComponentCallbacks2;
     21 import android.content.ContentResolver;
     22 import android.content.ContentUris;
     23 import android.content.Context;
     24 import android.content.res.Configuration;
     25 import android.content.res.Resources;
     26 import android.database.Cursor;
     27 import android.graphics.Bitmap;
     28 import android.graphics.Canvas;
     29 import android.graphics.Color;
     30 import android.graphics.Paint;
     31 import android.graphics.Paint.Style;
     32 import android.graphics.drawable.BitmapDrawable;
     33 import android.graphics.drawable.ColorDrawable;
     34 import android.graphics.drawable.Drawable;
     35 import android.graphics.drawable.TransitionDrawable;
     36 import android.media.ThumbnailUtils;
     37 import android.net.TrafficStats;
     38 import android.net.Uri;
     39 import android.net.Uri.Builder;
     40 import android.os.Handler;
     41 import android.os.Handler.Callback;
     42 import android.os.HandlerThread;
     43 import android.os.Message;
     44 import android.provider.ContactsContract;
     45 import android.provider.ContactsContract.Contacts;
     46 import android.provider.ContactsContract.Contacts.Photo;
     47 import android.provider.ContactsContract.Data;
     48 import android.provider.ContactsContract.Directory;
     49 import 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.lettertiles.LetterTileDrawable;
     59 import com.android.contacts.util.BitmapUtil;
     60 import com.android.contacts.util.PermissionsUtil;
     61 import com.android.contacts.util.TrafficStatsTags;
     62 import com.android.contacts.util.UriUtils;
     63 import com.android.contactsbind.util.UserAgentGenerator;
     64 
     65 import com.google.common.annotations.VisibleForTesting;
     66 import com.google.common.collect.Lists;
     67 import com.google.common.collect.Sets;
     68 
     69 import java.io.ByteArrayOutputStream;
     70 import java.io.IOException;
     71 import java.io.InputStream;
     72 import java.lang.ref.Reference;
     73 import java.lang.ref.SoftReference;
     74 import java.net.HttpURLConnection;
     75 import java.net.URL;
     76 import java.util.Iterator;
     77 import java.util.List;
     78 import java.util.Map.Entry;
     79 import java.util.Set;
     80 import java.util.concurrent.ConcurrentHashMap;
     81 import java.util.concurrent.atomic.AtomicInteger;
     82 
     83 /**
     84  * Asynchronously loads contact photos and maintains a cache of photos.
     85  */
     86 public abstract class ContactPhotoManager implements ComponentCallbacks2 {
     87     static final String TAG = "ContactPhotoManager";
     88     static final boolean DEBUG = false; // Don't submit with true
     89     static final boolean DEBUG_SIZES = false; // Don't submit with true
     90 
     91     /** Contact type constants used for default letter images */
     92     public static final int TYPE_PERSON = LetterTileDrawable.TYPE_PERSON;
     93     public static final int TYPE_BUSINESS = LetterTileDrawable.TYPE_BUSINESS;
     94     public static final int TYPE_VOICEMAIL = LetterTileDrawable.TYPE_VOICEMAIL;
     95     public static final int TYPE_DEFAULT = LetterTileDrawable.TYPE_DEFAULT;
     96 
     97     /** Scale and offset default constants used for default letter images */
     98     public static final float SCALE_DEFAULT = 1.0f;
     99     public static final float OFFSET_DEFAULT = 0.0f;
    100 
    101     public static final boolean IS_CIRCULAR_DEFAULT = false;
    102 
    103     /** Uri-related constants used for default letter images */
    104     private static final String DISPLAY_NAME_PARAM_KEY = "display_name";
    105     private static final String IDENTIFIER_PARAM_KEY = "identifier";
    106     private static final String CONTACT_TYPE_PARAM_KEY = "contact_type";
    107     private static final String SCALE_PARAM_KEY = "scale";
    108     private static final String OFFSET_PARAM_KEY = "offset";
    109     private static final String IS_CIRCULAR_PARAM_KEY = "is_circular";
    110     private static final String DEFAULT_IMAGE_URI_SCHEME = "defaultimage";
    111     private static final Uri DEFAULT_IMAGE_URI = Uri.parse(DEFAULT_IMAGE_URI_SCHEME + "://");
    112 
    113     // Static field used to cache the default letter avatar drawable that is created
    114     // using a null {@link DefaultImageRequest}
    115     private static Drawable sDefaultLetterAvatar = null;
    116 
    117     private static ContactPhotoManager sInstance;
    118 
    119     /**
    120      * Given a {@link DefaultImageRequest}, returns a {@link Drawable}, that when drawn, will
    121      * draw a letter tile avatar based on the request parameters defined in the
    122      * {@link DefaultImageRequest}.
    123      */
    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, defaultImageRequest));
    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, defaultImageRequest));
    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 || holder.bytes.length == 0) {
    941             request.applyDefaultImage(view, request.mIsCircular);
    942             return holder.fresh;
    943         }
    944 
    945         Bitmap cachedBitmap = holder.bitmapRef == null ? null : holder.bitmapRef.get();
    946         if (cachedBitmap == null) {
    947             if (holder.bytes.length < 8 * 1024) {
    948                 // Small thumbnails are usually quick to inflate. Let's do that on the UI thread
    949                 inflateBitmap(holder, request.getRequestedExtent());
    950                 cachedBitmap = holder.bitmap;
    951                 if (cachedBitmap == null) return false;
    952             } else {
    953                 // This is bigger data. Let's send that back to the Loader so that we can
    954                 // inflate this in the background
    955                 request.applyDefaultImage(view, request.mIsCircular);
    956                 return false;
    957             }
    958         }
    959 
    960         final Drawable previousDrawable = view.getDrawable();
    961         if (fadeIn && previousDrawable != null) {
    962             final Drawable[] layers = new Drawable[2];
    963             // Prevent cascade of TransitionDrawables.
    964             if (previousDrawable instanceof TransitionDrawable) {
    965                 final TransitionDrawable previousTransitionDrawable =
    966                         (TransitionDrawable) previousDrawable;
    967                 layers[0] = previousTransitionDrawable.getDrawable(
    968                         previousTransitionDrawable.getNumberOfLayers() - 1);
    969             } else {
    970                 layers[0] = previousDrawable;
    971             }
    972             layers[1] = getDrawableForBitmap(mContext.getResources(), cachedBitmap, request);
    973             TransitionDrawable drawable = new TransitionDrawable(layers);
    974             view.setImageDrawable(drawable);
    975             drawable.startTransition(FADE_TRANSITION_DURATION);
    976         } else {
    977             view.setImageDrawable(
    978                     getDrawableForBitmap(mContext.getResources(), cachedBitmap, request));
    979         }
    980 
    981         // Put the bitmap in the LRU cache. But only do this for images that are small enough
    982         // (we require that at least six of those can be cached at the same time)
    983         if (cachedBitmap.getByteCount() < mBitmapCache.maxSize() / 6) {
    984             mBitmapCache.put(request.getKey(), cachedBitmap);
    985         }
    986 
    987         // Soften the reference
    988         holder.bitmap = null;
    989 
    990         return holder.fresh;
    991     }
    992 
    993     /**
    994      * Given a bitmap, returns a drawable that is configured to display the bitmap based on the
    995      * specified request.
    996      */
    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      * bytes should be null to indicate a failure to load the photo. An empty byte[] signifies
   1182      * a successful load but no photo was available.
   1183      */
   1184     private void cacheBitmap(Object key, byte[] bytes, boolean preloading, int requestedExtent) {
   1185         if (DEBUG) {
   1186             BitmapHolder prev = mBitmapHolderCache.get(key);
   1187             if (prev != null && prev.bytes != null) {
   1188                 Log.d(TAG, "Overwriting cache: key=" + key + (prev.fresh ? " FRESH" : " stale"));
   1189                 if (prev.fresh) {
   1190                     mFreshCacheOverwrite.incrementAndGet();
   1191                 } else {
   1192                     mStaleCacheOverwrite.incrementAndGet();
   1193                 }
   1194             }
   1195             Log.d(TAG, "Caching data: key=" + key + ", " +
   1196                     (bytes == null ? "<null>" : btk(bytes.length)));
   1197         }
   1198         BitmapHolder holder = new BitmapHolder(bytes,
   1199                 bytes == null ? -1 : BitmapUtil.getSmallerExtentFromBytes(bytes));
   1200 
   1201         // Unless this image is being preloaded, decode it right away while
   1202         // we are still on the background thread.
   1203         if (!preloading) {
   1204             inflateBitmap(holder, requestedExtent);
   1205         }
   1206 
   1207         if (bytes != null) {
   1208             mBitmapHolderCache.put(key, holder);
   1209             if (mBitmapHolderCache.get(key) != holder) {
   1210                 Log.w(TAG, "Bitmap too big to fit in cache.");
   1211                 mBitmapHolderCache.put(key, BITMAP_UNAVAILABLE);
   1212             }
   1213         } else {
   1214             mBitmapHolderCache.put(key, BITMAP_UNAVAILABLE);
   1215         }
   1216 
   1217         mBitmapHolderCacheAllUnfresh = false;
   1218     }
   1219 
   1220     @Override
   1221     public void cacheBitmap(Uri photoUri, Bitmap bitmap, byte[] photoBytes) {
   1222         final int smallerExtent = Math.min(bitmap.getWidth(), bitmap.getHeight());
   1223         // We can pretend here that the extent of the photo was the size that we originally
   1224         // requested
   1225         Request request = Request.createFromUri(photoUri, smallerExtent, false /* darkTheme */,
   1226                 false /* isCircular */ , DEFAULT_AVATAR);
   1227         BitmapHolder holder = new BitmapHolder(photoBytes, smallerExtent);
   1228         holder.bitmapRef = new SoftReference<Bitmap>(bitmap);
   1229         mBitmapHolderCache.put(request.getKey(), holder);
   1230         mBitmapHolderCacheAllUnfresh = false;
   1231         mBitmapCache.put(request.getKey(), bitmap);
   1232     }
   1233 
   1234     /**
   1235      * Populates an array of photo IDs that need to be loaded. Also decodes bitmaps that we have
   1236      * already loaded
   1237      */
   1238     private void obtainPhotoIdsAndUrisToLoad(Set<Long> photoIds,
   1239             Set<String> photoIdsAsStrings, Set<Request> uris) {
   1240         photoIds.clear();
   1241         photoIdsAsStrings.clear();
   1242         uris.clear();
   1243 
   1244         boolean jpegsDecoded = false;
   1245 
   1246         /*
   1247          * Since the call is made from the loader thread, the map could be
   1248          * changing during the iteration. That's not really a problem:
   1249          * ConcurrentHashMap will allow those changes to happen without throwing
   1250          * exceptions. Since we may miss some requests in the situation of
   1251          * concurrent change, we will need to check the map again once loading
   1252          * is complete.
   1253          */
   1254         Iterator<Request> iterator = mPendingRequests.values().iterator();
   1255         while (iterator.hasNext()) {
   1256             Request request = iterator.next();
   1257             final BitmapHolder holder = mBitmapHolderCache.get(request.getKey());
   1258             if (holder == BITMAP_UNAVAILABLE) {
   1259                 continue;
   1260             }
   1261             if (holder != null && holder.bytes != null && holder.fresh &&
   1262                     (holder.bitmapRef == null || holder.bitmapRef.get() == null)) {
   1263                 // This was previously loaded but we don't currently have the inflated Bitmap
   1264                 inflateBitmap(holder, request.getRequestedExtent());
   1265                 jpegsDecoded = true;
   1266             } else {
   1267                 if (holder == null || !holder.fresh) {
   1268                     if (request.isUriRequest()) {
   1269                         uris.add(request);
   1270                     } else {
   1271                         photoIds.add(request.getId());
   1272                         photoIdsAsStrings.add(String.valueOf(request.mId));
   1273                     }
   1274                 }
   1275             }
   1276         }
   1277 
   1278         if (jpegsDecoded) mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
   1279     }
   1280 
   1281     /**
   1282      * The thread that performs loading of photos from the database.
   1283      */
   1284     private class LoaderThread extends HandlerThread implements Callback {
   1285         private static final int BUFFER_SIZE = 1024*16;
   1286         private static final int MESSAGE_PRELOAD_PHOTOS = 0;
   1287         private static final int MESSAGE_LOAD_PHOTOS = 1;
   1288 
   1289         /**
   1290          * A pause between preload batches that yields to the UI thread.
   1291          */
   1292         private static final int PHOTO_PRELOAD_DELAY = 1000;
   1293 
   1294         /**
   1295          * Number of photos to preload per batch.
   1296          */
   1297         private static final int PRELOAD_BATCH = 25;
   1298 
   1299         /**
   1300          * Maximum number of photos to preload.  If the cache size is 2Mb and
   1301          * the expected average size of a photo is 4kb, then this number should be 2Mb/4kb = 500.
   1302          */
   1303         private static final int MAX_PHOTOS_TO_PRELOAD = 100;
   1304 
   1305         private final ContentResolver mResolver;
   1306         private final StringBuilder mStringBuilder = new StringBuilder();
   1307         private final Set<Long> mPhotoIds = Sets.newHashSet();
   1308         private final Set<String> mPhotoIdsAsStrings = Sets.newHashSet();
   1309         private final Set<Request> mPhotoUris = Sets.newHashSet();
   1310         private final List<Long> mPreloadPhotoIds = Lists.newArrayList();
   1311 
   1312         private Handler mLoaderThreadHandler;
   1313         private byte mBuffer[];
   1314 
   1315         private static final int PRELOAD_STATUS_NOT_STARTED = 0;
   1316         private static final int PRELOAD_STATUS_IN_PROGRESS = 1;
   1317         private static final int PRELOAD_STATUS_DONE = 2;
   1318 
   1319         private int mPreloadStatus = PRELOAD_STATUS_NOT_STARTED;
   1320 
   1321         public LoaderThread(ContentResolver resolver) {
   1322             super(LOADER_THREAD_NAME);
   1323             mResolver = resolver;
   1324         }
   1325 
   1326         public void ensureHandler() {
   1327             if (mLoaderThreadHandler == null) {
   1328                 mLoaderThreadHandler = new Handler(getLooper(), this);
   1329             }
   1330         }
   1331 
   1332         /**
   1333          * Kicks off preloading of the next batch of photos on the background thread.
   1334          * Preloading will happen after a delay: we want to yield to the UI thread
   1335          * as much as possible.
   1336          * <p>
   1337          * If preloading is already complete, does nothing.
   1338          */
   1339         public void requestPreloading() {
   1340             if (mPreloadStatus == PRELOAD_STATUS_DONE) {
   1341                 return;
   1342             }
   1343 
   1344             ensureHandler();
   1345             if (mLoaderThreadHandler.hasMessages(MESSAGE_LOAD_PHOTOS)) {
   1346                 return;
   1347             }
   1348 
   1349             mLoaderThreadHandler.sendEmptyMessageDelayed(
   1350                     MESSAGE_PRELOAD_PHOTOS, PHOTO_PRELOAD_DELAY);
   1351         }
   1352 
   1353         /**
   1354          * Sends a message to this thread to load requested photos.  Cancels a preloading
   1355          * request, if any: we don't want preloading to impede loading of the photos
   1356          * we need to display now.
   1357          */
   1358         public void requestLoading() {
   1359             ensureHandler();
   1360             mLoaderThreadHandler.removeMessages(MESSAGE_PRELOAD_PHOTOS);
   1361             mLoaderThreadHandler.sendEmptyMessage(MESSAGE_LOAD_PHOTOS);
   1362         }
   1363 
   1364         /**
   1365          * Receives the above message, loads photos and then sends a message
   1366          * to the main thread to process them.
   1367          */
   1368         @Override
   1369         public boolean handleMessage(Message msg) {
   1370             switch (msg.what) {
   1371                 case MESSAGE_PRELOAD_PHOTOS:
   1372                     preloadPhotosInBackground();
   1373                     break;
   1374                 case MESSAGE_LOAD_PHOTOS:
   1375                     loadPhotosInBackground();
   1376                     break;
   1377             }
   1378             return true;
   1379         }
   1380 
   1381         /**
   1382          * The first time it is called, figures out which photos need to be preloaded.
   1383          * Each subsequent call preloads the next batch of photos and requests
   1384          * another cycle of preloading after a delay.  The whole process ends when
   1385          * we either run out of photos to preload or fill up cache.
   1386          */
   1387         private void preloadPhotosInBackground() {
   1388             if (mPreloadStatus == PRELOAD_STATUS_DONE) {
   1389                 return;
   1390             }
   1391 
   1392             if (mPreloadStatus == PRELOAD_STATUS_NOT_STARTED) {
   1393                 queryPhotosForPreload();
   1394                 if (mPreloadPhotoIds.isEmpty()) {
   1395                     mPreloadStatus = PRELOAD_STATUS_DONE;
   1396                 } else {
   1397                     mPreloadStatus = PRELOAD_STATUS_IN_PROGRESS;
   1398                 }
   1399                 requestPreloading();
   1400                 return;
   1401             }
   1402 
   1403             if (mBitmapHolderCache.size() > mBitmapHolderCacheRedZoneBytes) {
   1404                 mPreloadStatus = PRELOAD_STATUS_DONE;
   1405                 return;
   1406             }
   1407 
   1408             mPhotoIds.clear();
   1409             mPhotoIdsAsStrings.clear();
   1410 
   1411             int count = 0;
   1412             int preloadSize = mPreloadPhotoIds.size();
   1413             while(preloadSize > 0 && mPhotoIds.size() < PRELOAD_BATCH) {
   1414                 preloadSize--;
   1415                 count++;
   1416                 Long photoId = mPreloadPhotoIds.get(preloadSize);
   1417                 mPhotoIds.add(photoId);
   1418                 mPhotoIdsAsStrings.add(photoId.toString());
   1419                 mPreloadPhotoIds.remove(preloadSize);
   1420             }
   1421 
   1422             loadThumbnails(true);
   1423 
   1424             if (preloadSize == 0) {
   1425                 mPreloadStatus = PRELOAD_STATUS_DONE;
   1426             }
   1427 
   1428             if (Log.isLoggable(TAG, Log.VERBOSE)) {
   1429                 Log.v(TAG, "Preloaded " + count + " photos.  Cached bytes: "
   1430                         + mBitmapHolderCache.size());
   1431             }
   1432 
   1433             requestPreloading();
   1434         }
   1435 
   1436         private void queryPhotosForPreload() {
   1437             Cursor cursor = null;
   1438             try {
   1439                 Uri uri = Contacts.CONTENT_URI.buildUpon().appendQueryParameter(
   1440                         ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT))
   1441                         .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
   1442                                 String.valueOf(MAX_PHOTOS_TO_PRELOAD))
   1443                         .build();
   1444                 cursor = mResolver.query(uri, new String[] { Contacts.PHOTO_ID },
   1445                         Contacts.PHOTO_ID + " NOT NULL AND " + Contacts.PHOTO_ID + "!=0",
   1446                         null,
   1447                         Contacts.STARRED + " DESC, " + Contacts.LAST_TIME_CONTACTED + " DESC");
   1448 
   1449                 if (cursor != null) {
   1450                     while (cursor.moveToNext()) {
   1451                         // Insert them in reverse order, because we will be taking
   1452                         // them from the end of the list for loading.
   1453                         mPreloadPhotoIds.add(0, cursor.getLong(0));
   1454                     }
   1455                 }
   1456             } finally {
   1457                 if (cursor != null) {
   1458                     cursor.close();
   1459                 }
   1460             }
   1461         }
   1462 
   1463         private void loadPhotosInBackground() {
   1464             if (!PermissionsUtil.hasPermission(mContext,
   1465                     android.Manifest.permission.READ_CONTACTS)) {
   1466                 return;
   1467             }
   1468             obtainPhotoIdsAndUrisToLoad(mPhotoIds, mPhotoIdsAsStrings, mPhotoUris);
   1469             loadThumbnails(false);
   1470             loadUriBasedPhotos();
   1471             requestPreloading();
   1472         }
   1473 
   1474         /** Loads thumbnail photos with ids */
   1475         private void loadThumbnails(boolean preloading) {
   1476             if (mPhotoIds.isEmpty()) {
   1477                 return;
   1478             }
   1479 
   1480             // Remove loaded photos from the preload queue: we don't want
   1481             // the preloading process to load them again.
   1482             if (!preloading && mPreloadStatus == PRELOAD_STATUS_IN_PROGRESS) {
   1483                 for (Long id : mPhotoIds) {
   1484                     mPreloadPhotoIds.remove(id);
   1485                 }
   1486                 if (mPreloadPhotoIds.isEmpty()) {
   1487                     mPreloadStatus = PRELOAD_STATUS_DONE;
   1488                 }
   1489             }
   1490 
   1491             mStringBuilder.setLength(0);
   1492             mStringBuilder.append(Photo._ID + " IN(");
   1493             for (int i = 0; i < mPhotoIds.size(); i++) {
   1494                 if (i != 0) {
   1495                     mStringBuilder.append(',');
   1496                 }
   1497                 mStringBuilder.append('?');
   1498             }
   1499             mStringBuilder.append(')');
   1500 
   1501             Cursor cursor = null;
   1502             try {
   1503                 if (DEBUG) Log.d(TAG, "Loading " + TextUtils.join(",", mPhotoIdsAsStrings));
   1504                 cursor = mResolver.query(Data.CONTENT_URI,
   1505                         COLUMNS,
   1506                         mStringBuilder.toString(),
   1507                         mPhotoIdsAsStrings.toArray(EMPTY_STRING_ARRAY),
   1508                         null);
   1509 
   1510                 if (cursor != null) {
   1511                     while (cursor.moveToNext()) {
   1512                         Long id = cursor.getLong(0);
   1513                         byte[] bytes = cursor.getBlob(1);
   1514                         if (bytes == null) {
   1515                             bytes = new byte[0];
   1516                         }
   1517                         cacheBitmap(id, bytes, preloading, -1);
   1518                         mPhotoIds.remove(id);
   1519                     }
   1520                 }
   1521             } finally {
   1522                 if (cursor != null) {
   1523                     cursor.close();
   1524                 }
   1525             }
   1526 
   1527             // Remaining photos were not found in the contacts database (but might be in profile).
   1528             for (Long id : mPhotoIds) {
   1529                 if (ContactsContract.isProfileId(id)) {
   1530                     Cursor profileCursor = null;
   1531                     try {
   1532                         profileCursor = mResolver.query(
   1533                                 ContentUris.withAppendedId(Data.CONTENT_URI, id),
   1534                                 COLUMNS, null, null, null);
   1535                         if (profileCursor != null && profileCursor.moveToFirst()) {
   1536                             byte[] bytes = profileCursor.getBlob(1);
   1537                             if (bytes == null) {
   1538                                 bytes = new byte[0];
   1539                             }
   1540                             cacheBitmap(profileCursor.getLong(0), bytes, preloading, -1);
   1541                         } else {
   1542                             // Couldn't load a photo this way either.
   1543                             cacheBitmap(id, null, preloading, -1);
   1544                         }
   1545                     } finally {
   1546                         if (profileCursor != null) {
   1547                             profileCursor.close();
   1548                         }
   1549                     }
   1550                 } else {
   1551                     // Not a profile photo and not found - mark the cache accordingly
   1552                     cacheBitmap(id, null, preloading, -1);
   1553                 }
   1554             }
   1555 
   1556             mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
   1557         }
   1558 
   1559         /**
   1560          * Loads photos referenced with Uris. Those can be remote thumbnails
   1561          * (from directory searches), display photos etc
   1562          */
   1563         private void loadUriBasedPhotos() {
   1564             for (Request uriRequest : mPhotoUris) {
   1565                 // Keep the original URI and use this to key into the cache.  Failure to do so will
   1566                 // result in an image being continually reloaded into cache if the original URI
   1567                 // has a contact type encodedFragment (eg nearby places business photo URLs).
   1568                 Uri originalUri = uriRequest.getUri();
   1569 
   1570                 // Strip off the "contact type" we added to the URI to ensure it was identifiable as
   1571                 // a business photo -- there is no need to pass this on to the server.
   1572                 Uri uri = ContactPhotoManager.removeContactType(originalUri);
   1573 
   1574                 if (mBuffer == null) {
   1575                     mBuffer = new byte[BUFFER_SIZE];
   1576                 }
   1577                 try {
   1578                     if (DEBUG) Log.d(TAG, "Loading " + uri);
   1579                     final String scheme = uri.getScheme();
   1580                     InputStream is = null;
   1581                     if (scheme.equals("http") || scheme.equals("https")) {
   1582                         TrafficStats.setThreadStatsTag(TrafficStatsTags.CONTACT_PHOTO_DOWNLOAD_TAG);
   1583                         final HttpURLConnection connection =
   1584                                 (HttpURLConnection) new URL(uri.toString()).openConnection();
   1585 
   1586                         // Include the user agent if it is specified.
   1587                         if (!TextUtils.isEmpty(mUserAgent)) {
   1588                             connection.setRequestProperty("User-Agent", mUserAgent);
   1589                         }
   1590                         try {
   1591                             is = connection.getInputStream();
   1592                         } catch (IOException e) {
   1593                             connection.disconnect();
   1594                             is = null;
   1595                         }
   1596                         TrafficStats.clearThreadStatsTag();
   1597                     } else {
   1598                         is = mResolver.openInputStream(uri);
   1599                     }
   1600                     if (is != null) {
   1601                         ByteArrayOutputStream baos = new ByteArrayOutputStream();
   1602                         try {
   1603                             int size;
   1604                             while ((size = is.read(mBuffer)) != -1) {
   1605                                 baos.write(mBuffer, 0, size);
   1606                             }
   1607                         } finally {
   1608                             is.close();
   1609                         }
   1610                         cacheBitmap(originalUri, baos.toByteArray(), false,
   1611                                 uriRequest.getRequestedExtent());
   1612                         mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
   1613                     } else {
   1614                         if (Log.isLoggable(TAG, Log.VERBOSE)) {
   1615                             Log.v(TAG, "Cannot load photo " + uri);
   1616                         }
   1617                         cacheBitmap(originalUri, null, false, uriRequest.getRequestedExtent());
   1618                     }
   1619                 } catch (final Exception | OutOfMemoryError ex) {
   1620                     if (Log.isLoggable(TAG, Log.VERBOSE)) {
   1621                         Log.v(TAG, "Cannot load photo " + uri, ex);
   1622                     }
   1623                     cacheBitmap(originalUri, null, false, uriRequest.getRequestedExtent());
   1624                 }
   1625             }
   1626         }
   1627     }
   1628 
   1629     /**
   1630      * A holder for either a Uri or an id and a flag whether this was requested for the dark or
   1631      * light theme
   1632      */
   1633     private static final class Request {
   1634         private final long mId;
   1635         private final Uri mUri;
   1636         private final boolean mDarkTheme;
   1637         private final int mRequestedExtent;
   1638         private final DefaultImageProvider mDefaultProvider;
   1639         private final DefaultImageRequest mDefaultRequest;
   1640         /**
   1641          * Whether or not the contact photo is to be displayed as a circle
   1642          */
   1643         private final boolean mIsCircular;
   1644 
   1645         private Request(long id, Uri uri, int requestedExtent, boolean darkTheme,
   1646                 boolean isCircular, DefaultImageProvider defaultProvider,
   1647                 DefaultImageRequest defaultRequest) {
   1648             mId = id;
   1649             mUri = uri;
   1650             mDarkTheme = darkTheme;
   1651             mIsCircular = isCircular;
   1652             mRequestedExtent = requestedExtent;
   1653             mDefaultProvider = defaultProvider;
   1654             mDefaultRequest = defaultRequest;
   1655         }
   1656 
   1657         public static Request createFromThumbnailId(long id, boolean darkTheme, boolean isCircular,
   1658                 DefaultImageProvider defaultProvider, DefaultImageRequest defaultRequest) {
   1659             return new Request(id, null /* no URI */, -1, darkTheme, isCircular, defaultProvider,
   1660                     defaultRequest);
   1661         }
   1662 
   1663         public static Request createFromUri(Uri uri, int requestedExtent, boolean darkTheme,
   1664                 boolean isCircular, DefaultImageProvider defaultProvider) {
   1665             return createFromUri(uri, requestedExtent, darkTheme, isCircular, defaultProvider,
   1666                     /* defaultRequest */ null);
   1667         }
   1668 
   1669         public static Request createFromUri(Uri uri, int requestedExtent, boolean darkTheme,
   1670                 boolean isCircular, DefaultImageProvider defaultProvider,
   1671                 DefaultImageRequest defaultRequest) {
   1672             return new Request(0 /* no ID */, uri, requestedExtent, darkTheme, isCircular,
   1673                     defaultProvider, defaultRequest);
   1674         }
   1675 
   1676         public boolean isUriRequest() {
   1677             return mUri != null;
   1678         }
   1679 
   1680         public Uri getUri() {
   1681             return mUri;
   1682         }
   1683 
   1684         public long getId() {
   1685             return mId;
   1686         }
   1687 
   1688         public int getRequestedExtent() {
   1689             return mRequestedExtent;
   1690         }
   1691 
   1692         @Override
   1693         public int hashCode() {
   1694             final int prime = 31;
   1695             int result = 1;
   1696             result = prime * result + (int) (mId ^ (mId >>> 32));
   1697             result = prime * result + mRequestedExtent;
   1698             result = prime * result + ((mUri == null) ? 0 : mUri.hashCode());
   1699             return result;
   1700         }
   1701 
   1702         @Override
   1703         public boolean equals(Object obj) {
   1704             if (this == obj) return true;
   1705             if (obj == null) return false;
   1706             if (getClass() != obj.getClass()) return false;
   1707             final Request that = (Request) obj;
   1708             if (mId != that.mId) return false;
   1709             if (mRequestedExtent != that.mRequestedExtent) return false;
   1710             if (!UriUtils.areEqual(mUri, that.mUri)) return false;
   1711             // Don't compare equality of mDarkTheme because it is only used in the default contact
   1712             // photo case. When the contact does have a photo, the contact photo is the same
   1713             // regardless of mDarkTheme, so we shouldn't need to put the photo request on the queue
   1714             // twice.
   1715             return true;
   1716         }
   1717 
   1718         public Object getKey() {
   1719             return mUri == null ? mId : mUri;
   1720         }
   1721 
   1722         /**
   1723          * Applies the default image to the current view. If the request is URI-based, looks for
   1724          * the contact type encoded fragment to determine if this is a request for a business photo,
   1725          * in which case we will load the default business photo.
   1726          *
   1727          * @param view The current image view to apply the image to.
   1728          * @param isCircular Whether the image is circular or not.
   1729          */
   1730         public void applyDefaultImage(ImageView view, boolean isCircular) {
   1731             final DefaultImageRequest request;
   1732 
   1733             if (mDefaultRequest == null) {
   1734                 if (isCircular) {
   1735                     request = ContactPhotoManager.isBusinessContactUri(mUri)
   1736                             ? DefaultImageRequest.EMPTY_CIRCULAR_BUSINESS_IMAGE_REQUEST
   1737                             : DefaultImageRequest.EMPTY_CIRCULAR_DEFAULT_IMAGE_REQUEST;
   1738                 } else {
   1739                     request = ContactPhotoManager.isBusinessContactUri(mUri)
   1740                             ? DefaultImageRequest.EMPTY_DEFAULT_BUSINESS_IMAGE_REQUEST
   1741                             : DefaultImageRequest.EMPTY_DEFAULT_IMAGE_REQUEST;
   1742                 }
   1743             } else {
   1744                 request = mDefaultRequest;
   1745             }
   1746             mDefaultProvider.applyDefaultImage(view, mRequestedExtent, mDarkTheme, request);
   1747         }
   1748     }
   1749 }
   1750