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