ContactPhotoManager.java revision bf9b213e047da2f39d2b7fd237aad1e08e46e813
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
17package com.android.contacts;
18
19import com.android.contacts.model.AccountTypeManager;
20import com.android.contacts.util.UriUtils;
21import com.google.android.collect.Lists;
22import com.google.android.collect.Sets;
23
24import android.content.ContentResolver;
25import android.content.ContentUris;
26import android.content.Context;
27import android.content.res.Resources;
28import android.database.Cursor;
29import android.graphics.Bitmap;
30import android.graphics.BitmapFactory;
31import android.graphics.drawable.ColorDrawable;
32import android.graphics.drawable.Drawable;
33import android.net.Uri;
34import android.os.Handler;
35import android.os.Handler.Callback;
36import android.os.HandlerThread;
37import android.os.Message;
38import android.provider.ContactsContract;
39import android.provider.ContactsContract.Contacts;
40import android.provider.ContactsContract.Contacts.Photo;
41import android.provider.ContactsContract.Data;
42import android.provider.ContactsContract.Directory;
43import android.util.Log;
44import android.util.LruCache;
45import android.widget.ImageView;
46
47import java.io.ByteArrayOutputStream;
48import java.io.InputStream;
49import java.lang.ref.Reference;
50import java.lang.ref.SoftReference;
51import java.util.Iterator;
52import java.util.List;
53import java.util.Set;
54import java.util.concurrent.ConcurrentHashMap;
55
56/**
57 * Asynchronously loads contact photos and maintains a cache of photos.
58 */
59public 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
182class 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