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