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