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