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