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