Home | History | Annotate | Download | only in contacts
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.contacts;
     18 
     19 import com.android.contacts.model.AccountTypeManager;
     20 import com.android.contacts.util.UriUtils;
     21 import com.google.android.collect.Lists;
     22 import com.google.android.collect.Sets;
     23 
     24 import android.content.ContentResolver;
     25 import android.content.ContentUris;
     26 import android.content.Context;
     27 import android.content.res.Resources;
     28 import android.database.Cursor;
     29 import android.graphics.Bitmap;
     30 import android.graphics.BitmapFactory;
     31 import android.graphics.drawable.ColorDrawable;
     32 import android.graphics.drawable.Drawable;
     33 import android.net.Uri;
     34 import android.os.Handler;
     35 import android.os.Handler.Callback;
     36 import android.os.HandlerThread;
     37 import android.os.Message;
     38 import android.provider.ContactsContract;
     39 import android.provider.ContactsContract.Contacts;
     40 import android.provider.ContactsContract.Contacts.Photo;
     41 import android.provider.ContactsContract.Data;
     42 import android.provider.ContactsContract.Directory;
     43 import android.util.Log;
     44 import android.util.LruCache;
     45 import android.widget.ImageView;
     46 
     47 import java.io.ByteArrayOutputStream;
     48 import java.io.InputStream;
     49 import java.lang.ref.Reference;
     50 import java.lang.ref.SoftReference;
     51 import java.util.Iterator;
     52 import java.util.List;
     53 import java.util.Set;
     54 import java.util.concurrent.ConcurrentHashMap;
     55 
     56 /**
     57  * Asynchronously loads contact photos and maintains a cache of photos.
     58  */
     59 public abstract class ContactPhotoManager {
     60 
     61     static final String TAG = "ContactPhotoManager";
     62 
     63     public static final String CONTACT_PHOTO_SERVICE = "contactPhotos";
     64 
     65     public static int getDefaultAvatarResId(boolean hires, boolean darkTheme) {
     66         if (hires && darkTheme) return R.drawable.ic_contact_picture_180_holo_dark;
     67         if (hires) return R.drawable.ic_contact_picture_180_holo_light;
     68         if (darkTheme) return R.drawable.ic_contact_picture_holo_dark;
     69         return R.drawable.ic_contact_picture_holo_light;
     70     }
     71 
     72     public static abstract class DefaultImageProvider {
     73         public abstract void applyDefaultImage(ImageView view, boolean hires, boolean darkTheme);
     74     }
     75 
     76     private static class AvatarDefaultImageProvider extends DefaultImageProvider {
     77         @Override
     78         public void applyDefaultImage(ImageView view, boolean hires, boolean darkTheme) {
     79             view.setImageResource(getDefaultAvatarResId(hires, darkTheme));
     80         }
     81     }
     82 
     83     private static class BlankDefaultImageProvider extends DefaultImageProvider {
     84         private static Drawable sDrawable;
     85 
     86         @Override
     87         public void applyDefaultImage(ImageView view, boolean hires, boolean darkTheme) {
     88             if (sDrawable == null) {
     89                 Context context = view.getContext();
     90                 sDrawable = new ColorDrawable(context.getResources().getColor(
     91                         R.color.image_placeholder));
     92             }
     93             view.setImageDrawable(sDrawable);
     94         }
     95     }
     96 
     97     public static final DefaultImageProvider DEFAULT_AVATER = new AvatarDefaultImageProvider();
     98 
     99     public static final DefaultImageProvider DEFAULT_BLANK = new BlankDefaultImageProvider();
    100 
    101     /**
    102      * Requests the singleton instance of {@link AccountTypeManager} with data bound from
    103      * the available authenticators. This method can safely be called from the UI thread.
    104      */
    105     public static ContactPhotoManager getInstance(Context context) {
    106         Context applicationContext = context.getApplicationContext();
    107         ContactPhotoManager service =
    108                 (ContactPhotoManager) applicationContext.getSystemService(CONTACT_PHOTO_SERVICE);
    109         if (service == null) {
    110             service = createContactPhotoManager(applicationContext);
    111             Log.e(TAG, "No contact photo service in context: " + applicationContext);
    112         }
    113         return service;
    114     }
    115 
    116     public static synchronized ContactPhotoManager createContactPhotoManager(Context context) {
    117         return new ContactPhotoManagerImpl(context);
    118     }
    119 
    120     /**
    121      * Load photo into the supplied image view.  If the photo is already cached,
    122      * it is displayed immediately.  Otherwise a request is sent to load the photo
    123      * from the database.
    124      */
    125     public abstract void loadPhoto(ImageView view, long photoId, boolean hires, boolean darkTheme,
    126             DefaultImageProvider defaultProvider);
    127 
    128     /**
    129      * Calls {@link #loadPhoto(ImageView, long, boolean, boolean, DefaultImageProvider)} with
    130      * {@link #DEFAULT_AVATER}.
    131      */
    132     public final void loadPhoto(ImageView view, long photoId, boolean hires, boolean darkTheme) {
    133         loadPhoto(view, photoId, hires, darkTheme, DEFAULT_AVATER);
    134     }
    135 
    136     /**
    137      * Load photo into the supplied image view.  If the photo is already cached,
    138      * it is displayed immediately.  Otherwise a request is sent to load the photo
    139      * from the location specified by the URI.
    140      */
    141     public abstract void loadPhoto(ImageView view, Uri photoUri, boolean hires, boolean darkTheme,
    142             DefaultImageProvider defaultProvider);
    143 
    144     /**
    145      * Calls {@link #loadPhoto(ImageView, Uri, boolean, boolean, DefaultImageProvider)} with
    146      * {@link #DEFAULT_AVATER}.
    147      */
    148     public final void loadPhoto(ImageView view, Uri photoUri, boolean hires, boolean darkTheme) {
    149         loadPhoto(view, photoUri, hires, darkTheme, DEFAULT_AVATER);
    150     }
    151 
    152     /**
    153      * Remove photo from the supplied image view. This also cancels current pending load request
    154      * inside this photo manager.
    155      */
    156     public abstract void removePhoto(ImageView view);
    157 
    158     /**
    159      * Temporarily stops loading photos from the database.
    160      */
    161     public abstract void pause();
    162 
    163     /**
    164      * Resumes loading photos from the database.
    165      */
    166     public abstract void resume();
    167 
    168     /**
    169      * Marks all cached photos for reloading.  We can continue using cache but should
    170      * also make sure the photos haven't changed in the background and notify the views
    171      * if so.
    172      */
    173     public abstract void refreshCache();
    174 
    175     /**
    176      * Initiates a background process that over time will fill up cache with
    177      * preload photos.
    178      */
    179     public abstract void preloadPhotosInBackground();
    180 }
    181 
    182 class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
    183     private static final String LOADER_THREAD_NAME = "ContactPhotoLoader";
    184 
    185     /**
    186      * Type of message sent by the UI thread to itself to indicate that some photos
    187      * need to be loaded.
    188      */
    189     private static final int MESSAGE_REQUEST_LOADING = 1;
    190 
    191     /**
    192      * Type of message sent by the loader thread to indicate that some photos have
    193      * been loaded.
    194      */
    195     private static final int MESSAGE_PHOTOS_LOADED = 2;
    196 
    197     private static final String[] EMPTY_STRING_ARRAY = new String[0];
    198 
    199     private static final String[] COLUMNS = new String[] { Photo._ID, Photo.PHOTO };
    200 
    201     /**
    202      * Maintains the state of a particular photo.
    203      */
    204     private static class BitmapHolder {
    205         final byte[] bytes;
    206 
    207         volatile boolean fresh;
    208         Bitmap bitmap;
    209         Reference<Bitmap> bitmapRef;
    210 
    211         public BitmapHolder(byte[] bytes) {
    212             this.bytes = bytes;
    213             this.fresh = true;
    214         }
    215     }
    216 
    217     private final Context mContext;
    218 
    219     /**
    220      * An LRU cache for bitmap holders. The cache contains bytes for photos just
    221      * as they come from the database. Each holder has a soft reference to the
    222      * actual bitmap.
    223      */
    224     private final LruCache<Object, BitmapHolder> mBitmapHolderCache;
    225 
    226     /**
    227      * Cache size threshold at which bitmaps will not be preloaded.
    228      */
    229     private final int mBitmapHolderCacheRedZoneBytes;
    230 
    231     /**
    232      * Level 2 LRU cache for bitmaps. This is a smaller cache that holds
    233      * the most recently used bitmaps to save time on decoding
    234      * them from bytes (the bytes are stored in {@link #mBitmapHolderCache}.
    235      */
    236     private final LruCache<Object, Bitmap> mBitmapCache;
    237 
    238     /**
    239      * A map from ImageView to the corresponding photo ID or uri, encapsulated in a request.
    240      * The request may swapped out before the photo loading request is started.
    241      */
    242     private final ConcurrentHashMap<ImageView, Request> mPendingRequests =
    243             new ConcurrentHashMap<ImageView, Request>();
    244 
    245     /**
    246      * Handler for messages sent to the UI thread.
    247      */
    248     private final Handler mMainThreadHandler = new Handler(this);
    249 
    250     /**
    251      * Thread responsible for loading photos from the database. Created upon
    252      * the first request.
    253      */
    254     private LoaderThread mLoaderThread;
    255 
    256     /**
    257      * A gate to make sure we only send one instance of MESSAGE_PHOTOS_NEEDED at a time.
    258      */
    259     private boolean mLoadingRequested;
    260 
    261     /**
    262      * Flag indicating if the image loading is paused.
    263      */
    264     private boolean mPaused;
    265 
    266     public ContactPhotoManagerImpl(Context context) {
    267         mContext = context;
    268 
    269         Resources resources = context.getResources();
    270         mBitmapCache = new LruCache<Object, Bitmap>(
    271                 resources.getInteger(R.integer.config_photo_cache_max_bitmaps));
    272         int maxBytes = resources.getInteger(R.integer.config_photo_cache_max_bytes);
    273         mBitmapHolderCache = new LruCache<Object, BitmapHolder>(maxBytes) {
    274             @Override protected int sizeOf(Object key, BitmapHolder value) {
    275                 return value.bytes != null ? value.bytes.length : 0;
    276             }
    277         };
    278         mBitmapHolderCacheRedZoneBytes = (int) (maxBytes * 0.75);
    279     }
    280 
    281     @Override
    282     public void preloadPhotosInBackground() {
    283         ensureLoaderThread();
    284         mLoaderThread.requestPreloading();
    285     }
    286 
    287     @Override
    288     public void loadPhoto(ImageView view, long photoId, boolean hires, boolean darkTheme,
    289             DefaultImageProvider defaultProvider) {
    290         if (photoId == 0) {
    291             // No photo is needed
    292             defaultProvider.applyDefaultImage(view, hires, darkTheme);
    293             mPendingRequests.remove(view);
    294         } else {
    295             loadPhotoByIdOrUri(view, Request.createFromId(photoId, hires, darkTheme,
    296                     defaultProvider));
    297         }
    298     }
    299 
    300     @Override
    301     public void loadPhoto(ImageView view, Uri photoUri, boolean hires, boolean darkTheme,
    302             DefaultImageProvider defaultProvider) {
    303         if (photoUri == null) {
    304             // No photo is needed
    305             defaultProvider.applyDefaultImage(view, hires, darkTheme);
    306             mPendingRequests.remove(view);
    307         } else {
    308             loadPhotoByIdOrUri(view, Request.createFromUri(photoUri, hires, darkTheme,
    309                     defaultProvider));
    310         }
    311     }
    312 
    313     private void loadPhotoByIdOrUri(ImageView view, Request request) {
    314         boolean loaded = loadCachedPhoto(view, request);
    315         if (loaded) {
    316             mPendingRequests.remove(view);
    317         } else {
    318             mPendingRequests.put(view, request);
    319             if (!mPaused) {
    320                 // Send a request to start loading photos
    321                 requestLoading();
    322             }
    323         }
    324     }
    325 
    326     @Override
    327     public void removePhoto(ImageView view) {
    328         view.setImageDrawable(null);
    329         mPendingRequests.remove(view);
    330     }
    331 
    332     @Override
    333     public void refreshCache() {
    334         for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
    335             holder.fresh = false;
    336         }
    337     }
    338 
    339     /**
    340      * Checks if the photo is present in cache.  If so, sets the photo on the view.
    341      *
    342      * @return false if the photo needs to be (re)loaded from the provider.
    343      */
    344     private boolean loadCachedPhoto(ImageView view, Request request) {
    345         BitmapHolder holder = mBitmapHolderCache.get(request.getKey());
    346         if (holder == null) {
    347             // The bitmap has not been loaded - should display the placeholder image.
    348             request.applyDefaultImage(view);
    349             return false;
    350         }
    351 
    352         if (holder.bytes == null) {
    353             request.applyDefaultImage(view);
    354             return holder.fresh;
    355         }
    356 
    357         // Optionally decode bytes into a bitmap
    358         inflateBitmap(holder);
    359 
    360         view.setImageBitmap(holder.bitmap);
    361 
    362         if (holder.bitmap != null) {
    363             // Put the bitmap in the LRU cache
    364             mBitmapCache.put(request, holder.bitmap);
    365         }
    366 
    367         // Soften the reference
    368         holder.bitmap = null;
    369 
    370         return holder.fresh;
    371     }
    372 
    373     /**
    374      * If necessary, decodes bytes stored in the holder to Bitmap.  As long as the
    375      * bitmap is held either by {@link #mBitmapCache} or by a soft reference in
    376      * the holder, it will not be necessary to decode the bitmap.
    377      */
    378     private void inflateBitmap(BitmapHolder holder) {
    379         byte[] bytes = holder.bytes;
    380         if (bytes == null || bytes.length == 0) {
    381             return;
    382         }
    383 
    384         // Check the soft reference.  If will be retained if the bitmap is also
    385         // in the LRU cache, so we don't need to check the LRU cache explicitly.
    386         if (holder.bitmapRef != null) {
    387             holder.bitmap = holder.bitmapRef.get();
    388             if (holder.bitmap != null) {
    389                 return;
    390             }
    391         }
    392 
    393         try {
    394             Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, null);
    395             holder.bitmap = bitmap;
    396             holder.bitmapRef = new SoftReference<Bitmap>(bitmap);
    397         } catch (OutOfMemoryError e) {
    398             // Do nothing - the photo will appear to be missing
    399         }
    400     }
    401 
    402     public void clear() {
    403         mPendingRequests.clear();
    404         mBitmapHolderCache.evictAll();
    405     }
    406 
    407     @Override
    408     public void pause() {
    409         mPaused = true;
    410     }
    411 
    412     @Override
    413     public void resume() {
    414         mPaused = false;
    415         if (!mPendingRequests.isEmpty()) {
    416             requestLoading();
    417         }
    418     }
    419 
    420     /**
    421      * Sends a message to this thread itself to start loading images.  If the current
    422      * view contains multiple image views, all of those image views will get a chance
    423      * to request their respective photos before any of those requests are executed.
    424      * This allows us to load images in bulk.
    425      */
    426     private void requestLoading() {
    427         if (!mLoadingRequested) {
    428             mLoadingRequested = true;
    429             mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING);
    430         }
    431     }
    432 
    433     /**
    434      * Processes requests on the main thread.
    435      */
    436     @Override
    437     public boolean handleMessage(Message msg) {
    438         switch (msg.what) {
    439             case MESSAGE_REQUEST_LOADING: {
    440                 mLoadingRequested = false;
    441                 if (!mPaused) {
    442                     ensureLoaderThread();
    443                     mLoaderThread.requestLoading();
    444                 }
    445                 return true;
    446             }
    447 
    448             case MESSAGE_PHOTOS_LOADED: {
    449                 if (!mPaused) {
    450                     processLoadedImages();
    451                 }
    452                 return true;
    453             }
    454         }
    455         return false;
    456     }
    457 
    458     public void ensureLoaderThread() {
    459         if (mLoaderThread == null) {
    460             mLoaderThread = new LoaderThread(mContext.getContentResolver());
    461             mLoaderThread.start();
    462         }
    463     }
    464 
    465     /**
    466      * Goes over pending loading requests and displays loaded photos.  If some of the
    467      * photos still haven't been loaded, sends another request for image loading.
    468      */
    469     private void processLoadedImages() {
    470         Iterator<ImageView> iterator = mPendingRequests.keySet().iterator();
    471         while (iterator.hasNext()) {
    472             ImageView view = iterator.next();
    473             Request key = mPendingRequests.get(view);
    474             boolean loaded = loadCachedPhoto(view, key);
    475             if (loaded) {
    476                 iterator.remove();
    477             }
    478         }
    479 
    480         softenCache();
    481 
    482         if (!mPendingRequests.isEmpty()) {
    483             requestLoading();
    484         }
    485     }
    486 
    487     /**
    488      * Removes strong references to loaded bitmaps to allow them to be garbage collected
    489      * if needed.  Some of the bitmaps will still be retained by {@link #mBitmapCache}.
    490      */
    491     private void softenCache() {
    492         for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
    493             holder.bitmap = null;
    494         }
    495     }
    496 
    497     /**
    498      * Stores the supplied bitmap in cache.
    499      */
    500     private void cacheBitmap(Object key, byte[] bytes, boolean preloading) {
    501         BitmapHolder holder = new BitmapHolder(bytes);
    502         holder.fresh = true;
    503 
    504         // Unless this image is being preloaded, decode it right away while
    505         // we are still on the background thread.
    506         if (!preloading) {
    507             inflateBitmap(holder);
    508         }
    509 
    510         mBitmapHolderCache.put(key, holder);
    511     }
    512 
    513     /**
    514      * Populates an array of photo IDs that need to be loaded.
    515      */
    516     private void obtainPhotoIdsAndUrisToLoad(Set<Long> photoIds,
    517             Set<String> photoIdsAsStrings, Set<Uri> uris) {
    518         photoIds.clear();
    519         photoIdsAsStrings.clear();
    520         uris.clear();
    521 
    522         /*
    523          * Since the call is made from the loader thread, the map could be
    524          * changing during the iteration. That's not really a problem:
    525          * ConcurrentHashMap will allow those changes to happen without throwing
    526          * exceptions. Since we may miss some requests in the situation of
    527          * concurrent change, we will need to check the map again once loading
    528          * is complete.
    529          */
    530         Iterator<Request> iterator = mPendingRequests.values().iterator();
    531         while (iterator.hasNext()) {
    532             Request request = iterator.next();
    533             BitmapHolder holder = mBitmapHolderCache.get(request);
    534             if (holder == null || !holder.fresh) {
    535                 if (request.isUriRequest()) {
    536                     uris.add(request.mUri);
    537                 } else {
    538                     photoIds.add(request.mId);
    539                     photoIdsAsStrings.add(String.valueOf(request.mId));
    540                 }
    541             }
    542         }
    543     }
    544 
    545     /**
    546      * The thread that performs loading of photos from the database.
    547      */
    548     private class LoaderThread extends HandlerThread implements Callback {
    549         private static final int BUFFER_SIZE = 1024*16;
    550         private static final int MESSAGE_PRELOAD_PHOTOS = 0;
    551         private static final int MESSAGE_LOAD_PHOTOS = 1;
    552 
    553         /**
    554          * A pause between preload batches that yields to the UI thread.
    555          */
    556         private static final int PHOTO_PRELOAD_DELAY = 1000;
    557 
    558         /**
    559          * Number of photos to preload per batch.
    560          */
    561         private static final int PRELOAD_BATCH = 25;
    562 
    563         /**
    564          * Maximum number of photos to preload.  If the cache size is 2Mb and
    565          * the expected average size of a photo is 4kb, then this number should be 2Mb/4kb = 500.
    566          */
    567         private static final int MAX_PHOTOS_TO_PRELOAD = 100;
    568 
    569         private final ContentResolver mResolver;
    570         private final StringBuilder mStringBuilder = new StringBuilder();
    571         private final Set<Long> mPhotoIds = Sets.newHashSet();
    572         private final Set<String> mPhotoIdsAsStrings = Sets.newHashSet();
    573         private final Set<Uri> mPhotoUris = Sets.newHashSet();
    574         private final List<Long> mPreloadPhotoIds = Lists.newArrayList();
    575 
    576         private Handler mLoaderThreadHandler;
    577         private byte mBuffer[];
    578 
    579         private static final int PRELOAD_STATUS_NOT_STARTED = 0;
    580         private static final int PRELOAD_STATUS_IN_PROGRESS = 1;
    581         private static final int PRELOAD_STATUS_DONE = 2;
    582 
    583         private int mPreloadStatus = PRELOAD_STATUS_NOT_STARTED;
    584 
    585         public LoaderThread(ContentResolver resolver) {
    586             super(LOADER_THREAD_NAME);
    587             mResolver = resolver;
    588         }
    589 
    590         public void ensureHandler() {
    591             if (mLoaderThreadHandler == null) {
    592                 mLoaderThreadHandler = new Handler(getLooper(), this);
    593             }
    594         }
    595 
    596         /**
    597          * Kicks off preloading of the next batch of photos on the background thread.
    598          * Preloading will happen after a delay: we want to yield to the UI thread
    599          * as much as possible.
    600          * <p>
    601          * If preloading is already complete, does nothing.
    602          */
    603         public void requestPreloading() {
    604             if (mPreloadStatus == PRELOAD_STATUS_DONE) {
    605                 return;
    606             }
    607 
    608             ensureHandler();
    609             if (mLoaderThreadHandler.hasMessages(MESSAGE_LOAD_PHOTOS)) {
    610                 return;
    611             }
    612 
    613             mLoaderThreadHandler.sendEmptyMessageDelayed(
    614                     MESSAGE_PRELOAD_PHOTOS, PHOTO_PRELOAD_DELAY);
    615         }
    616 
    617         /**
    618          * Sends a message to this thread to load requested photos.  Cancels a preloading
    619          * request, if any: we don't want preloading to impede loading of the photos
    620          * we need to display now.
    621          */
    622         public void requestLoading() {
    623             ensureHandler();
    624             mLoaderThreadHandler.removeMessages(MESSAGE_PRELOAD_PHOTOS);
    625             mLoaderThreadHandler.sendEmptyMessage(MESSAGE_LOAD_PHOTOS);
    626         }
    627 
    628         /**
    629          * Receives the above message, loads photos and then sends a message
    630          * to the main thread to process them.
    631          */
    632         @Override
    633         public boolean handleMessage(Message msg) {
    634             switch (msg.what) {
    635                 case MESSAGE_PRELOAD_PHOTOS:
    636                     preloadPhotosInBackground();
    637                     break;
    638                 case MESSAGE_LOAD_PHOTOS:
    639                     loadPhotosInBackground();
    640                     break;
    641             }
    642             return true;
    643         }
    644 
    645         /**
    646          * The first time it is called, figures out which photos need to be preloaded.
    647          * Each subsequent call preloads the next batch of photos and requests
    648          * another cycle of preloading after a delay.  The whole process ends when
    649          * we either run out of photos to preload or fill up cache.
    650          */
    651         private void preloadPhotosInBackground() {
    652             if (mPreloadStatus == PRELOAD_STATUS_DONE) {
    653                 return;
    654             }
    655 
    656             if (mPreloadStatus == PRELOAD_STATUS_NOT_STARTED) {
    657                 queryPhotosForPreload();
    658                 if (mPreloadPhotoIds.isEmpty()) {
    659                     mPreloadStatus = PRELOAD_STATUS_DONE;
    660                 } else {
    661                     mPreloadStatus = PRELOAD_STATUS_IN_PROGRESS;
    662                 }
    663                 requestPreloading();
    664                 return;
    665             }
    666 
    667             if (mBitmapHolderCache.size() > mBitmapHolderCacheRedZoneBytes) {
    668                 mPreloadStatus = PRELOAD_STATUS_DONE;
    669                 return;
    670             }
    671 
    672             mPhotoIds.clear();
    673             mPhotoIdsAsStrings.clear();
    674 
    675             int count = 0;
    676             int preloadSize = mPreloadPhotoIds.size();
    677             while(preloadSize > 0 && mPhotoIds.size() < PRELOAD_BATCH) {
    678                 preloadSize--;
    679                 count++;
    680                 Long photoId = mPreloadPhotoIds.get(preloadSize);
    681                 mPhotoIds.add(photoId);
    682                 mPhotoIdsAsStrings.add(photoId.toString());
    683                 mPreloadPhotoIds.remove(preloadSize);
    684             }
    685 
    686             loadPhotosFromDatabase(true);
    687 
    688             if (preloadSize == 0) {
    689                 mPreloadStatus = PRELOAD_STATUS_DONE;
    690             }
    691 
    692             Log.v(TAG, "Preloaded " + count + " photos.  Cached bytes: "
    693                     + mBitmapHolderCache.size());
    694 
    695             requestPreloading();
    696         }
    697 
    698         private void queryPhotosForPreload() {
    699             Cursor cursor = null;
    700             try {
    701                 Uri uri = Contacts.CONTENT_URI.buildUpon().appendQueryParameter(
    702                         ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT))
    703                         .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
    704                                 String.valueOf(MAX_PHOTOS_TO_PRELOAD))
    705                         .build();
    706                 cursor = mResolver.query(uri, new String[] { Contacts.PHOTO_ID },
    707                         Contacts.PHOTO_ID + " NOT NULL AND " + Contacts.PHOTO_ID + "!=0",
    708                         null,
    709                         Contacts.STARRED + " DESC, " + Contacts.LAST_TIME_CONTACTED + " DESC");
    710 
    711                 if (cursor != null) {
    712                     while (cursor.moveToNext()) {
    713                         // Insert them in reverse order, because we will be taking
    714                         // them from the end of the list for loading.
    715                         mPreloadPhotoIds.add(0, cursor.getLong(0));
    716                     }
    717                 }
    718             } finally {
    719                 if (cursor != null) {
    720                     cursor.close();
    721                 }
    722             }
    723         }
    724 
    725         private void loadPhotosInBackground() {
    726             obtainPhotoIdsAndUrisToLoad(mPhotoIds, mPhotoIdsAsStrings, mPhotoUris);
    727             loadPhotosFromDatabase(false);
    728             loadRemotePhotos();
    729             requestPreloading();
    730         }
    731 
    732         private void loadPhotosFromDatabase(boolean preloading) {
    733             if (mPhotoIds.isEmpty()) {
    734                 return;
    735             }
    736 
    737             // Remove loaded photos from the preload queue: we don't want
    738             // the preloading process to load them again.
    739             if (!preloading && mPreloadStatus == PRELOAD_STATUS_IN_PROGRESS) {
    740                 for (Long id : mPhotoIds) {
    741                     mPreloadPhotoIds.remove(id);
    742                 }
    743                 if (mPreloadPhotoIds.isEmpty()) {
    744                     mPreloadStatus = PRELOAD_STATUS_DONE;
    745                 }
    746             }
    747 
    748             mStringBuilder.setLength(0);
    749             mStringBuilder.append(Photo._ID + " IN(");
    750             for (int i = 0; i < mPhotoIds.size(); i++) {
    751                 if (i != 0) {
    752                     mStringBuilder.append(',');
    753                 }
    754                 mStringBuilder.append('?');
    755             }
    756             mStringBuilder.append(')');
    757 
    758             Cursor cursor = null;
    759             try {
    760                 cursor = mResolver.query(Data.CONTENT_URI,
    761                         COLUMNS,
    762                         mStringBuilder.toString(),
    763                         mPhotoIdsAsStrings.toArray(EMPTY_STRING_ARRAY),
    764                         null);
    765 
    766                 if (cursor != null) {
    767                     while (cursor.moveToNext()) {
    768                         Long id = cursor.getLong(0);
    769                         byte[] bytes = cursor.getBlob(1);
    770                         cacheBitmap(id, bytes, preloading);
    771                         mPhotoIds.remove(id);
    772                     }
    773                 }
    774             } finally {
    775                 if (cursor != null) {
    776                     cursor.close();
    777                 }
    778             }
    779 
    780             // Remaining photos were not found in the contacts database (but might be in profile).
    781             for (Long id : mPhotoIds) {
    782                 if (ContactsContract.isProfileId(id)) {
    783                     Cursor profileCursor = null;
    784                     try {
    785                         profileCursor = mResolver.query(
    786                                 ContentUris.withAppendedId(Data.CONTENT_URI, id),
    787                                 COLUMNS, null, null, null);
    788                         if (profileCursor != null && profileCursor.moveToFirst()) {
    789                             cacheBitmap(profileCursor.getLong(0), profileCursor.getBlob(1),
    790                                     preloading);
    791                         } else {
    792                             // Couldn't load a photo this way either.
    793                             cacheBitmap(id, null, preloading);
    794                         }
    795                     } finally {
    796                         if (profileCursor != null) {
    797                             profileCursor.close();
    798                         }
    799                     }
    800                 } else {
    801                     // Not a profile photo and not found - mark the cache accordingly
    802                     cacheBitmap(id, null, preloading);
    803                 }
    804             }
    805 
    806             mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
    807         }
    808 
    809         private void loadRemotePhotos() {
    810             for (Uri uri : mPhotoUris) {
    811                 if (mBuffer == null) {
    812                     mBuffer = new byte[BUFFER_SIZE];
    813                 }
    814                 try {
    815                     InputStream is = mResolver.openInputStream(uri);
    816                     if (is != null) {
    817                         ByteArrayOutputStream baos = new ByteArrayOutputStream();
    818                         try {
    819                             int size;
    820                             while ((size = is.read(mBuffer)) != -1) {
    821                                 baos.write(mBuffer, 0, size);
    822                             }
    823                         } finally {
    824                             is.close();
    825                         }
    826                         cacheBitmap(uri, baos.toByteArray(), false);
    827                         mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
    828                     } else {
    829                         Log.v(TAG, "Cannot load photo " + uri);
    830                         cacheBitmap(uri, null, false);
    831                     }
    832                 } catch (Exception ex) {
    833                     Log.v(TAG, "Cannot load photo " + uri, ex);
    834                     cacheBitmap(uri, null, false);
    835                 }
    836             }
    837         }
    838     }
    839 
    840     /**
    841      * A holder for either a Uri or an id and a flag whether this was requested for the dark or
    842      * light theme
    843      */
    844     private static final class Request {
    845         private final long mId;
    846         private final Uri mUri;
    847         private final boolean mDarkTheme;
    848         private final boolean mHires;
    849         private final DefaultImageProvider mDefaultProvider;
    850 
    851         private Request(long id, Uri uri, boolean hires, boolean darkTheme,
    852                 DefaultImageProvider defaultProvider) {
    853             mId = id;
    854             mUri = uri;
    855             mDarkTheme = darkTheme;
    856             mHires = hires;
    857             mDefaultProvider = defaultProvider;
    858         }
    859 
    860         public static Request createFromId(long id, boolean hires, boolean darkTheme,
    861                 DefaultImageProvider defaultProvider) {
    862             return new Request(id, null /* no URI */, hires, darkTheme, defaultProvider);
    863         }
    864 
    865         public static Request createFromUri(Uri uri, boolean hires, boolean darkTheme,
    866                 DefaultImageProvider defaultProvider) {
    867             return new Request(0 /* no ID */, uri, hires, darkTheme, defaultProvider);
    868         }
    869 
    870         public boolean isDarkTheme() {
    871             return mDarkTheme;
    872         }
    873 
    874         public boolean isHires() {
    875             return mHires;
    876         }
    877 
    878         public boolean isUriRequest() {
    879             return mUri != null;
    880         }
    881 
    882         @Override
    883         public int hashCode() {
    884             if (mUri != null) return mUri.hashCode();
    885 
    886             // copied over from Long.hashCode()
    887             return (int) (mId ^ (mId >>> 32));
    888         }
    889 
    890         @Override
    891         public boolean equals(Object o) {
    892             if (!(o instanceof Request)) return false;
    893             final Request that = (Request) o;
    894             // Don't compare equality of mHires and mDarkTheme fields because these are only used
    895             // in the default contact photo case. When the contact does have a photo, the contact
    896             // photo is the same regardless of mHires and mDarkTheme, so we shouldn't need to put
    897             // the photo request on the queue twice.
    898             return mId == that.mId && UriUtils.areEqual(mUri, that.mUri);
    899         }
    900 
    901         public Object getKey() {
    902             return mUri == null ? mId : mUri;
    903         }
    904 
    905         public void applyDefaultImage(ImageView view) {
    906             mDefaultProvider.applyDefaultImage(view, mHires, mDarkTheme);
    907         }
    908     }
    909 }
    910