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