• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
getDefaultAvatarResId(boolean hires, boolean darkTheme)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 {
applyDefaultImage(ImageView view, boolean hires, boolean darkTheme)73         public abstract void applyDefaultImage(ImageView view, boolean hires, boolean darkTheme);
74     }
75 
76     private static class AvatarDefaultImageProvider extends DefaultImageProvider {
77         @Override
applyDefaultImage(ImageView view, boolean hires, boolean darkTheme)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
applyDefaultImage(ImageView view, boolean hires, boolean darkTheme)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      */
getInstance(Context context)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 
createContactPhotoManager(Context context)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      */
loadPhoto(ImageView view, long photoId, boolean hires, boolean darkTheme, DefaultImageProvider defaultProvider)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      */
loadPhoto(ImageView view, long photoId, boolean hires, boolean darkTheme)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      */
loadPhoto(ImageView view, Uri photoUri, boolean hires, boolean darkTheme, DefaultImageProvider defaultProvider)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      */
loadPhoto(ImageView view, Uri photoUri, boolean hires, boolean darkTheme)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      */
removePhoto(ImageView view)156     public abstract void removePhoto(ImageView view);
157 
158     /**
159      * Temporarily stops loading photos from the database.
160      */
pause()161     public abstract void pause();
162 
163     /**
164      * Resumes loading photos from the database.
165      */
resume()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      */
refreshCache()173     public abstract void refreshCache();
174 
175     /**
176      * Initiates a background process that over time will fill up cache with
177      * preload photos.
178      */
preloadPhotosInBackground()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 
BitmapHolder(byte[] bytes)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 
ContactPhotoManagerImpl(Context context)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
preloadPhotosInBackground()282     public void preloadPhotosInBackground() {
283         ensureLoaderThread();
284         mLoaderThread.requestPreloading();
285     }
286 
287     @Override
loadPhoto(ImageView view, long photoId, boolean hires, boolean darkTheme, DefaultImageProvider defaultProvider)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
loadPhoto(ImageView view, Uri photoUri, boolean hires, boolean darkTheme, DefaultImageProvider defaultProvider)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 
loadPhotoByIdOrUri(ImageView view, Request request)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
removePhoto(ImageView view)327     public void removePhoto(ImageView view) {
328         view.setImageDrawable(null);
329         mPendingRequests.remove(view);
330     }
331 
332     @Override
refreshCache()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      */
loadCachedPhoto(ImageView view, Request request)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      */
inflateBitmap(BitmapHolder holder)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 
clear()402     public void clear() {
403         mPendingRequests.clear();
404         mBitmapHolderCache.evictAll();
405     }
406 
407     @Override
pause()408     public void pause() {
409         mPaused = true;
410     }
411 
412     @Override
resume()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      */
requestLoading()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
handleMessage(Message msg)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 
ensureLoaderThread()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      */
processLoadedImages()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      */
softenCache()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      */
cacheBitmap(Object key, byte[] bytes, boolean preloading)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      */
obtainPhotoIdsAndUrisToLoad(Set<Long> photoIds, Set<String> photoIdsAsStrings, Set<Uri> uris)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 
LoaderThread(ContentResolver resolver)585         public LoaderThread(ContentResolver resolver) {
586             super(LOADER_THREAD_NAME);
587             mResolver = resolver;
588         }
589 
ensureHandler()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          */
requestPreloading()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          */
requestLoading()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
handleMessage(Message msg)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          */
preloadPhotosInBackground()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 
queryPhotosForPreload()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 
loadPhotosInBackground()725         private void loadPhotosInBackground() {
726             obtainPhotoIdsAndUrisToLoad(mPhotoIds, mPhotoIdsAsStrings, mPhotoUris);
727             loadPhotosFromDatabase(false);
728             loadRemotePhotos();
729             requestPreloading();
730         }
731 
loadPhotosFromDatabase(boolean preloading)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 
loadRemotePhotos()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 
Request(long id, Uri uri, boolean hires, boolean darkTheme, DefaultImageProvider defaultProvider)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 
createFromId(long id, boolean hires, boolean darkTheme, DefaultImageProvider defaultProvider)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 
createFromUri(Uri uri, boolean hires, boolean darkTheme, DefaultImageProvider defaultProvider)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 
isDarkTheme()870         public boolean isDarkTheme() {
871             return mDarkTheme;
872         }
873 
isHires()874         public boolean isHires() {
875             return mHires;
876         }
877 
isUriRequest()878         public boolean isUriRequest() {
879             return mUri != null;
880         }
881 
882         @Override
hashCode()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
equals(Object o)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 
getKey()901         public Object getKey() {
902             return mUri == null ? mId : mUri;
903         }
904 
applyDefaultImage(ImageView view)905         public void applyDefaultImage(ImageView view) {
906             mDefaultProvider.applyDefaultImage(view, mHires, mDarkTheme);
907         }
908     }
909 }
910