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