1 /* 2 * Copyright (C) 2016 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 17 package com.android.dialer.contactphoto; 18 19 import android.app.ActivityManager; 20 import android.content.ComponentCallbacks2; 21 import android.content.ContentResolver; 22 import android.content.ContentUris; 23 import android.content.Context; 24 import android.content.res.Resources; 25 import android.database.Cursor; 26 import android.graphics.Bitmap; 27 import android.graphics.Canvas; 28 import android.graphics.Color; 29 import android.graphics.Paint; 30 import android.graphics.Paint.Style; 31 import android.graphics.drawable.BitmapDrawable; 32 import android.graphics.drawable.Drawable; 33 import android.graphics.drawable.TransitionDrawable; 34 import android.media.ThumbnailUtils; 35 import android.net.TrafficStats; 36 import android.net.Uri; 37 import android.os.Handler; 38 import android.os.Handler.Callback; 39 import android.os.HandlerThread; 40 import android.os.Message; 41 import android.provider.ContactsContract; 42 import android.provider.ContactsContract.Contacts; 43 import android.provider.ContactsContract.Contacts.Photo; 44 import android.provider.ContactsContract.Data; 45 import android.provider.ContactsContract.Directory; 46 import android.support.annotation.UiThread; 47 import android.support.annotation.WorkerThread; 48 import android.support.v4.graphics.drawable.RoundedBitmapDrawable; 49 import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory; 50 import android.text.TextUtils; 51 import android.util.LruCache; 52 import android.view.View; 53 import android.view.ViewGroup; 54 import android.widget.ImageView; 55 import com.android.dialer.common.LogUtil; 56 import com.android.dialer.constants.Constants; 57 import com.android.dialer.constants.TrafficStatsTags; 58 import com.android.dialer.util.PermissionsUtil; 59 import com.android.dialer.util.UriUtils; 60 import java.io.ByteArrayOutputStream; 61 import java.io.IOException; 62 import java.io.InputStream; 63 import java.lang.ref.Reference; 64 import java.lang.ref.SoftReference; 65 import java.net.HttpURLConnection; 66 import java.net.URL; 67 import java.util.ArrayList; 68 import java.util.HashSet; 69 import java.util.Iterator; 70 import java.util.List; 71 import java.util.Map.Entry; 72 import java.util.Set; 73 import java.util.concurrent.ConcurrentHashMap; 74 import java.util.concurrent.atomic.AtomicInteger; 75 76 class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback { 77 78 private static final String LOADER_THREAD_NAME = "ContactPhotoLoader"; 79 80 private static final int FADE_TRANSITION_DURATION = 200; 81 82 /** 83 * Type of message sent by the UI thread to itself to indicate that some photos need to be loaded. 84 */ 85 private static final int MESSAGE_REQUEST_LOADING = 1; 86 87 /** Type of message sent by the loader thread to indicate that some photos have been loaded. */ 88 private static final int MESSAGE_PHOTOS_LOADED = 2; 89 90 private static final String[] EMPTY_STRING_ARRAY = new String[0]; 91 92 private static final String[] COLUMNS = new String[] {Photo._ID, Photo.PHOTO}; 93 94 /** 95 * Dummy object used to indicate that a bitmap for a given key could not be stored in the cache. 96 */ 97 private static final BitmapHolder BITMAP_UNAVAILABLE; 98 /** Cache size for {@link #bitmapHolderCache} for devices with "large" RAM. */ 99 private static final int HOLDER_CACHE_SIZE = 2000000; 100 /** Cache size for {@link #bitmapCache} for devices with "large" RAM. */ 101 private static final int BITMAP_CACHE_SIZE = 36864 * 48; // 1728K 102 /** Height/width of a thumbnail image */ 103 private static int thumbnailSize; 104 105 static { 106 BITMAP_UNAVAILABLE = new BitmapHolder(new byte[0], 0); 107 BITMAP_UNAVAILABLE.bitmapRef = new SoftReference<Bitmap>(null); 108 } 109 110 private final Context context; 111 /** 112 * An LRU cache for bitmap holders. The cache contains bytes for photos just as they come from the 113 * database. Each holder has a soft reference to the actual bitmap. 114 */ 115 private final LruCache<Object, BitmapHolder> bitmapHolderCache; 116 /** Cache size threshold at which bitmaps will not be preloaded. */ 117 private final int bitmapHolderCacheRedZoneBytes; 118 /** 119 * Level 2 LRU cache for bitmaps. This is a smaller cache that holds the most recently used 120 * bitmaps to save time on decoding them from bytes (the bytes are stored in {@link 121 * #bitmapHolderCache}. 122 */ 123 private final LruCache<Object, Bitmap> bitmapCache; 124 /** 125 * A map from ImageView to the corresponding photo ID or uri, encapsulated in a request. The 126 * request may swapped out before the photo loading request is started. 127 */ 128 private final ConcurrentHashMap<ImageView, Request> pendingRequests = 129 new ConcurrentHashMap<ImageView, Request>(); 130 /** Handler for messages sent to the UI thread. */ 131 private final Handler mainThreadHandler = new Handler(this); 132 /** For debug: How many times we had to reload cached photo for a stale entry */ 133 private final AtomicInteger staleCacheOverwrite = new AtomicInteger(); 134 /** For debug: How many times we had to reload cached photo for a fresh entry. Should be 0. */ 135 private final AtomicInteger freshCacheOverwrite = new AtomicInteger(); 136 /** {@code true} if ALL entries in {@link #bitmapHolderCache} are NOT fresh. */ 137 private volatile boolean bitmapHolderCacheAllUnfresh = true; 138 /** Thread responsible for loading photos from the database. Created upon the first request. */ 139 private LoaderThread loaderThread; 140 /** A gate to make sure we only send one instance of MESSAGE_PHOTOS_NEEDED at a time. */ 141 private boolean loadingRequested; 142 /** Flag indicating if the image loading is paused. */ 143 private boolean paused; 144 /** The user agent string to use when loading URI based photos. */ 145 private String userAgent; 146 147 public ContactPhotoManagerImpl(Context context) { 148 this.context = context; 149 150 final ActivityManager am = 151 ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE)); 152 153 final float cacheSizeAdjustment = (am.isLowRamDevice()) ? 0.5f : 1.0f; 154 155 final int bitmapCacheSize = (int) (cacheSizeAdjustment * BITMAP_CACHE_SIZE); 156 bitmapCache = 157 new LruCache<Object, Bitmap>(bitmapCacheSize) { 158 @Override 159 protected int sizeOf(Object key, Bitmap value) { 160 return value.getByteCount(); 161 } 162 163 @Override 164 protected void entryRemoved( 165 boolean evicted, Object key, Bitmap oldValue, Bitmap newValue) { 166 if (DEBUG) { 167 dumpStats(); 168 } 169 } 170 }; 171 final int holderCacheSize = (int) (cacheSizeAdjustment * HOLDER_CACHE_SIZE); 172 bitmapHolderCache = 173 new LruCache<Object, BitmapHolder>(holderCacheSize) { 174 @Override 175 protected int sizeOf(Object key, BitmapHolder value) { 176 return value.bytes != null ? value.bytes.length : 0; 177 } 178 179 @Override 180 protected void entryRemoved( 181 boolean evicted, Object key, BitmapHolder oldValue, BitmapHolder newValue) { 182 if (DEBUG) { 183 dumpStats(); 184 } 185 } 186 }; 187 bitmapHolderCacheRedZoneBytes = (int) (holderCacheSize * 0.75); 188 LogUtil.i( 189 "ContactPhotoManagerImpl.ContactPhotoManagerImpl", "cache adj: " + cacheSizeAdjustment); 190 if (DEBUG) { 191 LogUtil.d( 192 "ContactPhotoManagerImpl.ContactPhotoManagerImpl", 193 "Cache size: " + btk(bitmapHolderCache.maxSize()) + " + " + btk(bitmapCache.maxSize())); 194 } 195 196 thumbnailSize = 197 context.getResources().getDimensionPixelSize(R.dimen.contact_browser_list_item_photo_size); 198 199 // Get a user agent string to use for URI photo requests. 200 userAgent = Constants.get().getUserAgent(context); 201 if (userAgent == null) { 202 userAgent = ""; 203 } 204 } 205 206 /** Converts bytes to K bytes, rounding up. Used only for debug log. */ 207 private static String btk(int bytes) { 208 return ((bytes + 1023) / 1024) + "K"; 209 } 210 211 private static final int safeDiv(int dividend, int divisor) { 212 return (divisor == 0) ? 0 : (dividend / divisor); 213 } 214 215 private static boolean isChildView(View parent, View potentialChild) { 216 return potentialChild.getParent() != null 217 && (potentialChild.getParent() == parent 218 || (potentialChild.getParent() instanceof ViewGroup 219 && isChildView(parent, (ViewGroup) potentialChild.getParent()))); 220 } 221 222 /** 223 * If necessary, decodes bytes stored in the holder to Bitmap. As long as the bitmap is held 224 * either by {@link #bitmapCache} or by a soft reference in the holder, it will not be necessary 225 * to decode the bitmap. 226 */ 227 private static void inflateBitmap(BitmapHolder holder, int requestedExtent) { 228 final int sampleSize = 229 BitmapUtil.findOptimalSampleSize(holder.originalSmallerExtent, requestedExtent); 230 byte[] bytes = holder.bytes; 231 if (bytes == null || bytes.length == 0) { 232 return; 233 } 234 235 if (sampleSize == holder.decodedSampleSize) { 236 // Check the soft reference. If will be retained if the bitmap is also 237 // in the LRU cache, so we don't need to check the LRU cache explicitly. 238 if (holder.bitmapRef != null) { 239 holder.bitmap = holder.bitmapRef.get(); 240 if (holder.bitmap != null) { 241 return; 242 } 243 } 244 } 245 246 try { 247 Bitmap bitmap = BitmapUtil.decodeBitmapFromBytes(bytes, sampleSize); 248 249 // TODO: As a temporary workaround while framework support is being added to 250 // clip non-square bitmaps into a perfect circle, manually crop the bitmap into 251 // into a square if it will be displayed as a thumbnail so that it can be cropped 252 // into a circle. 253 final int height = bitmap.getHeight(); 254 final int width = bitmap.getWidth(); 255 256 // The smaller dimension of a scaled bitmap can range from anywhere from 0 to just 257 // below twice the length of a thumbnail image due to the way we calculate the optimal 258 // sample size. 259 if (height != width && Math.min(height, width) <= thumbnailSize * 2) { 260 final int dimension = Math.min(height, width); 261 bitmap = ThumbnailUtils.extractThumbnail(bitmap, dimension, dimension); 262 } 263 // make bitmap mutable and draw size onto it 264 if (DEBUG_SIZES) { 265 Bitmap original = bitmap; 266 bitmap = bitmap.copy(bitmap.getConfig(), true); 267 original.recycle(); 268 Canvas canvas = new Canvas(bitmap); 269 Paint paint = new Paint(); 270 paint.setTextSize(16); 271 paint.setColor(Color.BLUE); 272 paint.setStyle(Style.FILL); 273 canvas.drawRect(0.0f, 0.0f, 50.0f, 20.0f, paint); 274 paint.setColor(Color.WHITE); 275 paint.setAntiAlias(true); 276 canvas.drawText(bitmap.getWidth() + "/" + sampleSize, 0, 15, paint); 277 } 278 279 holder.decodedSampleSize = sampleSize; 280 holder.bitmap = bitmap; 281 holder.bitmapRef = new SoftReference<Bitmap>(bitmap); 282 if (DEBUG) { 283 LogUtil.d( 284 "ContactPhotoManagerImpl.inflateBitmap", 285 "inflateBitmap " 286 + btk(bytes.length) 287 + " -> " 288 + bitmap.getWidth() 289 + "x" 290 + bitmap.getHeight() 291 + ", " 292 + btk(bitmap.getByteCount())); 293 } 294 } catch (OutOfMemoryError e) { 295 // Do nothing - the photo will appear to be missing 296 } 297 } 298 299 /** Dump cache stats on logcat. */ 300 private void dumpStats() { 301 if (!DEBUG) { 302 return; 303 } 304 { 305 int numHolders = 0; 306 int rawBytes = 0; 307 int bitmapBytes = 0; 308 int numBitmaps = 0; 309 for (BitmapHolder h : bitmapHolderCache.snapshot().values()) { 310 numHolders++; 311 if (h.bytes != null) { 312 rawBytes += h.bytes.length; 313 } 314 Bitmap b = h.bitmapRef != null ? h.bitmapRef.get() : null; 315 if (b != null) { 316 numBitmaps++; 317 bitmapBytes += b.getByteCount(); 318 } 319 } 320 LogUtil.d( 321 "ContactPhotoManagerImpl.dumpStats", 322 "L1: " 323 + btk(rawBytes) 324 + " + " 325 + btk(bitmapBytes) 326 + " = " 327 + btk(rawBytes + bitmapBytes) 328 + ", " 329 + numHolders 330 + " holders, " 331 + numBitmaps 332 + " bitmaps, avg: " 333 + btk(safeDiv(rawBytes, numHolders)) 334 + "," 335 + btk(safeDiv(bitmapBytes, numBitmaps))); 336 LogUtil.d( 337 "ContactPhotoManagerImpl.dumpStats", 338 "L1 Stats: " 339 + bitmapHolderCache.toString() 340 + ", overwrite: fresh=" 341 + freshCacheOverwrite.get() 342 + " stale=" 343 + staleCacheOverwrite.get()); 344 } 345 346 { 347 int numBitmaps = 0; 348 int bitmapBytes = 0; 349 for (Bitmap b : bitmapCache.snapshot().values()) { 350 numBitmaps++; 351 bitmapBytes += b.getByteCount(); 352 } 353 LogUtil.d( 354 "ContactPhotoManagerImpl.dumpStats", 355 "L2: " 356 + btk(bitmapBytes) 357 + ", " 358 + numBitmaps 359 + " bitmaps" 360 + ", avg: " 361 + btk(safeDiv(bitmapBytes, numBitmaps))); 362 // We don't get from L2 cache, so L2 stats is meaningless. 363 } 364 } 365 366 @Override 367 public void onTrimMemory(int level) { 368 if (DEBUG) { 369 LogUtil.d("ContactPhotoManagerImpl.onTrimMemory", "onTrimMemory: " + level); 370 } 371 if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) { 372 // Clear the caches. Note all pending requests will be removed too. 373 clear(); 374 } 375 } 376 377 @Override 378 public void preloadPhotosInBackground() { 379 ensureLoaderThread(); 380 loaderThread.requestPreloading(); 381 } 382 383 @Override 384 public void loadThumbnail( 385 ImageView view, 386 long photoId, 387 boolean darkTheme, 388 boolean isCircular, 389 DefaultImageRequest defaultImageRequest, 390 DefaultImageProvider defaultProvider) { 391 if (photoId == 0) { 392 // No photo is needed 393 defaultProvider.applyDefaultImage(view, -1, darkTheme, defaultImageRequest); 394 pendingRequests.remove(view); 395 } else { 396 if (DEBUG) { 397 LogUtil.d("ContactPhotoManagerImpl.loadThumbnail", "loadPhoto request: " + photoId); 398 } 399 loadPhotoByIdOrUri( 400 view, Request.createFromThumbnailId(photoId, darkTheme, isCircular, defaultProvider)); 401 } 402 } 403 404 @Override 405 public void loadPhoto( 406 ImageView view, 407 Uri photoUri, 408 int requestedExtent, 409 boolean darkTheme, 410 boolean isCircular, 411 DefaultImageRequest defaultImageRequest, 412 DefaultImageProvider defaultProvider) { 413 if (photoUri == null) { 414 // No photo is needed 415 defaultProvider.applyDefaultImage(view, requestedExtent, darkTheme, defaultImageRequest); 416 pendingRequests.remove(view); 417 return; 418 } 419 if (isDrawableUri(photoUri)) { 420 view.setImageURI(photoUri); 421 pendingRequests.remove(view); 422 return; 423 } 424 if (DEBUG) { 425 LogUtil.d("ContactPhotoManagerImpl.loadPhoto", "loadPhoto request: " + photoUri); 426 } 427 428 if (isDefaultImageUri(photoUri)) { 429 createAndApplyDefaultImageForUri( 430 view, photoUri, requestedExtent, darkTheme, isCircular, defaultProvider); 431 } else { 432 loadPhotoByIdOrUri( 433 view, 434 Request.createFromUri(photoUri, requestedExtent, darkTheme, isCircular, defaultProvider)); 435 } 436 } 437 438 private static boolean isDrawableUri(Uri uri) { 439 if (!ContentResolver.SCHEME_ANDROID_RESOURCE.equals(uri.getScheme())) { 440 return false; 441 } 442 return uri.getPathSegments().get(0).equals("drawable"); 443 } 444 445 private void createAndApplyDefaultImageForUri( 446 ImageView view, 447 Uri uri, 448 int requestedExtent, 449 boolean darkTheme, 450 boolean isCircular, 451 DefaultImageProvider defaultProvider) { 452 DefaultImageRequest request = getDefaultImageRequestFromUri(uri); 453 request.isCircular = isCircular; 454 defaultProvider.applyDefaultImage(view, requestedExtent, darkTheme, request); 455 } 456 457 private void loadPhotoByIdOrUri(ImageView view, Request request) { 458 boolean loaded = loadCachedPhoto(view, request, false); 459 if (loaded) { 460 pendingRequests.remove(view); 461 } else { 462 pendingRequests.put(view, request); 463 if (!paused) { 464 // Send a request to start loading photos 465 requestLoading(); 466 } 467 } 468 } 469 470 @Override 471 public void removePhoto(ImageView view) { 472 view.setImageDrawable(null); 473 pendingRequests.remove(view); 474 } 475 476 /** 477 * Cancels pending requests to load photos asynchronously for views inside {@param 478 * fragmentRootView}. If {@param fragmentRootView} is null, cancels all requests. 479 */ 480 @Override 481 public void cancelPendingRequests(View fragmentRootView) { 482 if (fragmentRootView == null) { 483 pendingRequests.clear(); 484 return; 485 } 486 final Iterator<Entry<ImageView, Request>> iterator = pendingRequests.entrySet().iterator(); 487 while (iterator.hasNext()) { 488 final ImageView imageView = iterator.next().getKey(); 489 // If an ImageView is orphaned (currently scrap) or a child of fragmentRootView, then 490 // we can safely remove its request. 491 if (imageView.getParent() == null || isChildView(fragmentRootView, imageView)) { 492 iterator.remove(); 493 } 494 } 495 } 496 497 @Override 498 public void refreshCache() { 499 if (bitmapHolderCacheAllUnfresh) { 500 if (DEBUG) { 501 LogUtil.d("ContactPhotoManagerImpl.refreshCache", "refreshCache -- no fresh entries."); 502 } 503 return; 504 } 505 if (DEBUG) { 506 LogUtil.d("ContactPhotoManagerImpl.refreshCache", "refreshCache"); 507 } 508 bitmapHolderCacheAllUnfresh = true; 509 for (BitmapHolder holder : bitmapHolderCache.snapshot().values()) { 510 if (holder != BITMAP_UNAVAILABLE) { 511 holder.fresh = false; 512 } 513 } 514 } 515 516 /** 517 * Checks if the photo is present in cache. If so, sets the photo on the view. 518 * 519 * @return false if the photo needs to be (re)loaded from the provider. 520 */ 521 @UiThread 522 private boolean loadCachedPhoto(ImageView view, Request request, boolean fadeIn) { 523 BitmapHolder holder = bitmapHolderCache.get(request.getKey()); 524 if (holder == null) { 525 // The bitmap has not been loaded ==> show default avatar 526 request.applyDefaultImage(view, request.isCircular); 527 return false; 528 } 529 530 if (holder.bytes == null) { 531 request.applyDefaultImage(view, request.isCircular); 532 return holder.fresh; 533 } 534 535 Bitmap cachedBitmap = holder.bitmapRef == null ? null : holder.bitmapRef.get(); 536 if (cachedBitmap == null) { 537 request.applyDefaultImage(view, request.isCircular); 538 return false; 539 } 540 541 final Drawable previousDrawable = view.getDrawable(); 542 if (fadeIn && previousDrawable != null) { 543 final Drawable[] layers = new Drawable[2]; 544 // Prevent cascade of TransitionDrawables. 545 if (previousDrawable instanceof TransitionDrawable) { 546 final TransitionDrawable previousTransitionDrawable = (TransitionDrawable) previousDrawable; 547 layers[0] = 548 previousTransitionDrawable.getDrawable( 549 previousTransitionDrawable.getNumberOfLayers() - 1); 550 } else { 551 layers[0] = previousDrawable; 552 } 553 layers[1] = getDrawableForBitmap(context.getResources(), cachedBitmap, request); 554 TransitionDrawable drawable = new TransitionDrawable(layers); 555 view.setImageDrawable(drawable); 556 drawable.startTransition(FADE_TRANSITION_DURATION); 557 } else { 558 view.setImageDrawable(getDrawableForBitmap(context.getResources(), cachedBitmap, request)); 559 } 560 561 // Put the bitmap in the LRU cache. But only do this for images that are small enough 562 // (we require that at least six of those can be cached at the same time) 563 if (cachedBitmap.getByteCount() < bitmapCache.maxSize() / 6) { 564 bitmapCache.put(request.getKey(), cachedBitmap); 565 } 566 567 // Soften the reference 568 holder.bitmap = null; 569 570 return holder.fresh; 571 } 572 573 /** 574 * Given a bitmap, returns a drawable that is configured to display the bitmap based on the 575 * specified request. 576 */ 577 private Drawable getDrawableForBitmap(Resources resources, Bitmap bitmap, Request request) { 578 if (request.isCircular) { 579 final RoundedBitmapDrawable drawable = RoundedBitmapDrawableFactory.create(resources, bitmap); 580 drawable.setAntiAlias(true); 581 drawable.setCornerRadius(drawable.getIntrinsicHeight() / 2); 582 return drawable; 583 } else { 584 return new BitmapDrawable(resources, bitmap); 585 } 586 } 587 588 public void clear() { 589 if (DEBUG) { 590 LogUtil.d("ContactPhotoManagerImpl.clear", "clear"); 591 } 592 pendingRequests.clear(); 593 bitmapHolderCache.evictAll(); 594 bitmapCache.evictAll(); 595 } 596 597 @Override 598 public void pause() { 599 paused = true; 600 } 601 602 @Override 603 public void resume() { 604 paused = false; 605 if (DEBUG) { 606 dumpStats(); 607 } 608 if (!pendingRequests.isEmpty()) { 609 requestLoading(); 610 } 611 } 612 613 /** 614 * Sends a message to this thread itself to start loading images. If the current view contains 615 * multiple image views, all of those image views will get a chance to request their respective 616 * photos before any of those requests are executed. This allows us to load images in bulk. 617 */ 618 private void requestLoading() { 619 if (!loadingRequested) { 620 loadingRequested = true; 621 mainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING); 622 } 623 } 624 625 /** Processes requests on the main thread. */ 626 @Override 627 public boolean handleMessage(Message msg) { 628 switch (msg.what) { 629 case MESSAGE_REQUEST_LOADING: 630 { 631 loadingRequested = false; 632 if (!paused) { 633 ensureLoaderThread(); 634 loaderThread.requestLoading(); 635 } 636 return true; 637 } 638 639 case MESSAGE_PHOTOS_LOADED: 640 { 641 if (!paused) { 642 processLoadedImages(); 643 } 644 if (DEBUG) { 645 dumpStats(); 646 } 647 return true; 648 } 649 default: 650 return false; 651 } 652 } 653 654 public void ensureLoaderThread() { 655 if (loaderThread == null) { 656 loaderThread = new LoaderThread(context.getContentResolver()); 657 loaderThread.start(); 658 } 659 } 660 661 /** 662 * Goes over pending loading requests and displays loaded photos. If some of the photos still 663 * haven't been loaded, sends another request for image loading. 664 */ 665 private void processLoadedImages() { 666 final Iterator<Entry<ImageView, Request>> iterator = pendingRequests.entrySet().iterator(); 667 while (iterator.hasNext()) { 668 final Entry<ImageView, Request> entry = iterator.next(); 669 // TODO: Temporarily disable contact photo fading in, until issues with 670 // RoundedBitmapDrawables overlapping the default image drawables are resolved. 671 final boolean loaded = loadCachedPhoto(entry.getKey(), entry.getValue(), false); 672 if (loaded) { 673 iterator.remove(); 674 } 675 } 676 677 softenCache(); 678 679 if (!pendingRequests.isEmpty()) { 680 requestLoading(); 681 } 682 } 683 684 /** 685 * Removes strong references to loaded bitmaps to allow them to be garbage collected if needed. 686 * Some of the bitmaps will still be retained by {@link #bitmapCache}. 687 */ 688 private void softenCache() { 689 for (BitmapHolder holder : bitmapHolderCache.snapshot().values()) { 690 holder.bitmap = null; 691 } 692 } 693 694 /** Stores the supplied bitmap in cache. */ 695 private void cacheBitmap(Object key, byte[] bytes, boolean preloading, int requestedExtent) { 696 if (DEBUG) { 697 BitmapHolder prev = bitmapHolderCache.get(key); 698 if (prev != null && prev.bytes != null) { 699 LogUtil.d( 700 "ContactPhotoManagerImpl.cacheBitmap", 701 "overwriting cache: key=" + key + (prev.fresh ? " FRESH" : " stale")); 702 if (prev.fresh) { 703 freshCacheOverwrite.incrementAndGet(); 704 } else { 705 staleCacheOverwrite.incrementAndGet(); 706 } 707 } 708 LogUtil.d( 709 "ContactPhotoManagerImpl.cacheBitmap", 710 "caching data: key=" + key + ", " + (bytes == null ? "<null>" : btk(bytes.length))); 711 } 712 BitmapHolder holder = 713 new BitmapHolder(bytes, bytes == null ? -1 : BitmapUtil.getSmallerExtentFromBytes(bytes)); 714 715 // Unless this image is being preloaded, decode it right away while 716 // we are still on the background thread. 717 if (!preloading) { 718 inflateBitmap(holder, requestedExtent); 719 } 720 721 if (bytes != null) { 722 bitmapHolderCache.put(key, holder); 723 if (bitmapHolderCache.get(key) != holder) { 724 LogUtil.w("ContactPhotoManagerImpl.cacheBitmap", "bitmap too big to fit in cache."); 725 bitmapHolderCache.put(key, BITMAP_UNAVAILABLE); 726 } 727 } else { 728 bitmapHolderCache.put(key, BITMAP_UNAVAILABLE); 729 } 730 731 bitmapHolderCacheAllUnfresh = false; 732 } 733 734 /** 735 * Populates an array of photo IDs that need to be loaded. Also decodes bitmaps that we have 736 * already loaded 737 */ 738 private void obtainPhotoIdsAndUrisToLoad( 739 Set<Long> photoIds, Set<String> photoIdsAsStrings, Set<Request> uris) { 740 photoIds.clear(); 741 photoIdsAsStrings.clear(); 742 uris.clear(); 743 744 boolean jpegsDecoded = false; 745 746 /* 747 * Since the call is made from the loader thread, the map could be 748 * changing during the iteration. That's not really a problem: 749 * ConcurrentHashMap will allow those changes to happen without throwing 750 * exceptions. Since we may miss some requests in the situation of 751 * concurrent change, we will need to check the map again once loading 752 * is complete. 753 */ 754 Iterator<Request> iterator = pendingRequests.values().iterator(); 755 while (iterator.hasNext()) { 756 Request request = iterator.next(); 757 final BitmapHolder holder = bitmapHolderCache.get(request.getKey()); 758 if (holder == BITMAP_UNAVAILABLE) { 759 continue; 760 } 761 if (holder != null 762 && holder.bytes != null 763 && holder.fresh 764 && (holder.bitmapRef == null || holder.bitmapRef.get() == null)) { 765 // This was previously loaded but we don't currently have the inflated Bitmap 766 inflateBitmap(holder, request.getRequestedExtent()); 767 jpegsDecoded = true; 768 } else { 769 if (holder == null || !holder.fresh) { 770 if (request.isUriRequest()) { 771 uris.add(request); 772 } else { 773 photoIds.add(request.getId()); 774 photoIdsAsStrings.add(String.valueOf(request.id)); 775 } 776 } 777 } 778 } 779 780 if (jpegsDecoded) { 781 mainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED); 782 } 783 } 784 785 /** Maintains the state of a particular photo. */ 786 private static class BitmapHolder { 787 788 final byte[] bytes; 789 final int originalSmallerExtent; 790 791 volatile boolean fresh; 792 Bitmap bitmap; 793 Reference<Bitmap> bitmapRef; 794 int decodedSampleSize; 795 796 public BitmapHolder(byte[] bytes, int originalSmallerExtent) { 797 this.bytes = bytes; 798 this.fresh = true; 799 this.originalSmallerExtent = originalSmallerExtent; 800 } 801 } 802 803 /** 804 * A holder for either a Uri or an id and a flag whether this was requested for the dark or light 805 * theme 806 */ 807 private static final class Request { 808 809 private final long id; 810 private final Uri uri; 811 private final boolean darkTheme; 812 private final int requestedExtent; 813 private final DefaultImageProvider defaultProvider; 814 /** Whether or not the contact photo is to be displayed as a circle */ 815 private final boolean isCircular; 816 817 private Request( 818 long id, 819 Uri uri, 820 int requestedExtent, 821 boolean darkTheme, 822 boolean isCircular, 823 DefaultImageProvider defaultProvider) { 824 this.id = id; 825 this.uri = uri; 826 this.darkTheme = darkTheme; 827 this.isCircular = isCircular; 828 this.requestedExtent = requestedExtent; 829 this.defaultProvider = defaultProvider; 830 } 831 832 public static Request createFromThumbnailId( 833 long id, boolean darkTheme, boolean isCircular, DefaultImageProvider defaultProvider) { 834 return new Request(id, null /* no URI */, -1, darkTheme, isCircular, defaultProvider); 835 } 836 837 public static Request createFromUri( 838 Uri uri, 839 int requestedExtent, 840 boolean darkTheme, 841 boolean isCircular, 842 DefaultImageProvider defaultProvider) { 843 return new Request( 844 0 /* no ID */, uri, requestedExtent, darkTheme, isCircular, defaultProvider); 845 } 846 847 public boolean isUriRequest() { 848 return uri != null; 849 } 850 851 public Uri getUri() { 852 return uri; 853 } 854 855 public long getId() { 856 return id; 857 } 858 859 public int getRequestedExtent() { 860 return requestedExtent; 861 } 862 863 @Override 864 public int hashCode() { 865 final int prime = 31; 866 int result = 1; 867 result = prime * result + (int) (id ^ (id >>> 32)); 868 result = prime * result + requestedExtent; 869 result = prime * result + ((uri == null) ? 0 : uri.hashCode()); 870 return result; 871 } 872 873 @Override 874 public boolean equals(Object obj) { 875 if (this == obj) { 876 return true; 877 } 878 if (obj == null) { 879 return false; 880 } 881 if (getClass() != obj.getClass()) { 882 return false; 883 } 884 final Request that = (Request) obj; 885 if (id != that.id) { 886 return false; 887 } 888 if (requestedExtent != that.requestedExtent) { 889 return false; 890 } 891 if (!UriUtils.areEqual(uri, that.uri)) { 892 return false; 893 } 894 // Don't compare equality of mDarkTheme because it is only used in the default contact 895 // photo case. When the contact does have a photo, the contact photo is the same 896 // regardless of mDarkTheme, so we shouldn't need to put the photo request on the queue 897 // twice. 898 return true; 899 } 900 901 public Object getKey() { 902 return uri == null ? id : uri; 903 } 904 905 /** 906 * Applies the default image to the current view. If the request is URI-based, looks for the 907 * contact type encoded fragment to determine if this is a request for a business photo, in 908 * which case we will load the default business photo. 909 * 910 * @param view The current image view to apply the image to. 911 * @param isCircular Whether the image is circular or not. 912 */ 913 public void applyDefaultImage(ImageView view, boolean isCircular) { 914 final DefaultImageRequest request; 915 916 if (isCircular) { 917 request = 918 ContactPhotoManager.isBusinessContactUri(uri) 919 ? DefaultImageRequest.EMPTY_CIRCULAR_BUSINESS_IMAGE_REQUEST 920 : DefaultImageRequest.EMPTY_CIRCULAR_DEFAULT_IMAGE_REQUEST; 921 } else { 922 request = 923 ContactPhotoManager.isBusinessContactUri(uri) 924 ? DefaultImageRequest.EMPTY_DEFAULT_BUSINESS_IMAGE_REQUEST 925 : DefaultImageRequest.EMPTY_DEFAULT_IMAGE_REQUEST; 926 } 927 defaultProvider.applyDefaultImage(view, requestedExtent, darkTheme, request); 928 } 929 } 930 931 /** The thread that performs loading of photos from the database. */ 932 private class LoaderThread extends HandlerThread implements Callback { 933 934 private static final int BUFFER_SIZE = 1024 * 16; 935 private static final int MESSAGE_PRELOAD_PHOTOS = 0; 936 private static final int MESSAGE_LOAD_PHOTOS = 1; 937 938 /** A pause between preload batches that yields to the UI thread. */ 939 private static final int PHOTO_PRELOAD_DELAY = 1000; 940 941 /** Number of photos to preload per batch. */ 942 private static final int PRELOAD_BATCH = 25; 943 944 /** 945 * Maximum number of photos to preload. If the cache size is 2Mb and the expected average size 946 * of a photo is 4kb, then this number should be 2Mb/4kb = 500. 947 */ 948 private static final int MAX_PHOTOS_TO_PRELOAD = 100; 949 950 private static final int PRELOAD_STATUS_NOT_STARTED = 0; 951 private static final int PRELOAD_STATUS_IN_PROGRESS = 1; 952 private static final int PRELOAD_STATUS_DONE = 2; 953 private final ContentResolver resolver; 954 private final StringBuilder stringBuilder = new StringBuilder(); 955 private final Set<Long> photoIds = new HashSet<>(); 956 private final Set<String> photoIdsAsStrings = new HashSet<>(); 957 private final Set<Request> photoUris = new HashSet<>(); 958 private final List<Long> preloadPhotoIds = new ArrayList<>(); 959 private Handler loaderThreadHandler; 960 private byte[] buffer; 961 private int preloadStatus = PRELOAD_STATUS_NOT_STARTED; 962 963 public LoaderThread(ContentResolver resolver) { 964 super(LOADER_THREAD_NAME); 965 this.resolver = resolver; 966 } 967 968 public void ensureHandler() { 969 if (loaderThreadHandler == null) { 970 loaderThreadHandler = new Handler(getLooper(), this); 971 } 972 } 973 974 /** 975 * Kicks off preloading of the next batch of photos on the background thread. Preloading will 976 * happen after a delay: we want to yield to the UI thread as much as possible. 977 * 978 * <p>If preloading is already complete, does nothing. 979 */ 980 public void requestPreloading() { 981 if (preloadStatus == PRELOAD_STATUS_DONE) { 982 return; 983 } 984 985 ensureHandler(); 986 if (loaderThreadHandler.hasMessages(MESSAGE_LOAD_PHOTOS)) { 987 return; 988 } 989 990 loaderThreadHandler.sendEmptyMessageDelayed(MESSAGE_PRELOAD_PHOTOS, PHOTO_PRELOAD_DELAY); 991 } 992 993 /** 994 * Sends a message to this thread to load requested photos. Cancels a preloading request, if 995 * any: we don't want preloading to impede loading of the photos we need to display now. 996 */ 997 public void requestLoading() { 998 ensureHandler(); 999 loaderThreadHandler.removeMessages(MESSAGE_PRELOAD_PHOTOS); 1000 loaderThreadHandler.sendEmptyMessage(MESSAGE_LOAD_PHOTOS); 1001 } 1002 1003 /** 1004 * Receives the above message, loads photos and then sends a message to the main thread to 1005 * process them. 1006 */ 1007 @Override 1008 public boolean handleMessage(Message msg) { 1009 switch (msg.what) { 1010 case MESSAGE_PRELOAD_PHOTOS: 1011 preloadPhotosInBackground(); 1012 break; 1013 case MESSAGE_LOAD_PHOTOS: 1014 loadPhotosInBackground(); 1015 break; 1016 } 1017 return true; 1018 } 1019 1020 /** 1021 * The first time it is called, figures out which photos need to be preloaded. Each subsequent 1022 * call preloads the next batch of photos and requests another cycle of preloading after a 1023 * delay. The whole process ends when we either run out of photos to preload or fill up cache. 1024 */ 1025 @WorkerThread 1026 private void preloadPhotosInBackground() { 1027 if (!PermissionsUtil.hasPermission(context, android.Manifest.permission.READ_CONTACTS)) { 1028 return; 1029 } 1030 1031 if (preloadStatus == PRELOAD_STATUS_DONE) { 1032 return; 1033 } 1034 1035 if (preloadStatus == PRELOAD_STATUS_NOT_STARTED) { 1036 queryPhotosForPreload(); 1037 if (preloadPhotoIds.isEmpty()) { 1038 preloadStatus = PRELOAD_STATUS_DONE; 1039 } else { 1040 preloadStatus = PRELOAD_STATUS_IN_PROGRESS; 1041 } 1042 requestPreloading(); 1043 return; 1044 } 1045 1046 if (bitmapHolderCache.size() > bitmapHolderCacheRedZoneBytes) { 1047 preloadStatus = PRELOAD_STATUS_DONE; 1048 return; 1049 } 1050 1051 photoIds.clear(); 1052 photoIdsAsStrings.clear(); 1053 1054 int count = 0; 1055 int preloadSize = preloadPhotoIds.size(); 1056 while (preloadSize > 0 && photoIds.size() < PRELOAD_BATCH) { 1057 preloadSize--; 1058 count++; 1059 Long photoId = preloadPhotoIds.get(preloadSize); 1060 photoIds.add(photoId); 1061 photoIdsAsStrings.add(photoId.toString()); 1062 preloadPhotoIds.remove(preloadSize); 1063 } 1064 1065 loadThumbnails(true); 1066 1067 if (preloadSize == 0) { 1068 preloadStatus = PRELOAD_STATUS_DONE; 1069 } 1070 1071 LogUtil.v( 1072 "ContactPhotoManagerImpl.preloadPhotosInBackground", 1073 "preloaded " + count + " photos. cached bytes: " + bitmapHolderCache.size()); 1074 1075 requestPreloading(); 1076 } 1077 1078 @WorkerThread 1079 private void queryPhotosForPreload() { 1080 Cursor cursor = null; 1081 try { 1082 Uri uri = 1083 Contacts.CONTENT_URI 1084 .buildUpon() 1085 .appendQueryParameter( 1086 ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT)) 1087 .appendQueryParameter( 1088 ContactsContract.LIMIT_PARAM_KEY, String.valueOf(MAX_PHOTOS_TO_PRELOAD)) 1089 .build(); 1090 cursor = 1091 resolver.query( 1092 uri, 1093 new String[] {Contacts.PHOTO_ID}, 1094 Contacts.PHOTO_ID + " NOT NULL AND " + Contacts.PHOTO_ID + "!=0", 1095 null, 1096 Contacts.STARRED + " DESC, " + Contacts.LAST_TIME_CONTACTED + " DESC"); 1097 1098 if (cursor != null) { 1099 while (cursor.moveToNext()) { 1100 // Insert them in reverse order, because we will be taking 1101 // them from the end of the list for loading. 1102 preloadPhotoIds.add(0, cursor.getLong(0)); 1103 } 1104 } 1105 } finally { 1106 if (cursor != null) { 1107 cursor.close(); 1108 } 1109 } 1110 } 1111 1112 @WorkerThread 1113 private void loadPhotosInBackground() { 1114 if (!PermissionsUtil.hasPermission(context, android.Manifest.permission.READ_CONTACTS)) { 1115 return; 1116 } 1117 obtainPhotoIdsAndUrisToLoad(photoIds, photoIdsAsStrings, photoUris); 1118 loadThumbnails(false); 1119 loadUriBasedPhotos(); 1120 requestPreloading(); 1121 } 1122 1123 /** Loads thumbnail photos with ids */ 1124 @WorkerThread 1125 private void loadThumbnails(boolean preloading) { 1126 if (photoIds.isEmpty()) { 1127 return; 1128 } 1129 1130 // Remove loaded photos from the preload queue: we don't want 1131 // the preloading process to load them again. 1132 if (!preloading && preloadStatus == PRELOAD_STATUS_IN_PROGRESS) { 1133 for (Long id : photoIds) { 1134 preloadPhotoIds.remove(id); 1135 } 1136 if (preloadPhotoIds.isEmpty()) { 1137 preloadStatus = PRELOAD_STATUS_DONE; 1138 } 1139 } 1140 1141 stringBuilder.setLength(0); 1142 stringBuilder.append(Photo._ID + " IN("); 1143 for (int i = 0; i < photoIds.size(); i++) { 1144 if (i != 0) { 1145 stringBuilder.append(','); 1146 } 1147 stringBuilder.append('?'); 1148 } 1149 stringBuilder.append(')'); 1150 1151 Cursor cursor = null; 1152 try { 1153 if (DEBUG) { 1154 LogUtil.d( 1155 "ContactPhotoManagerImpl.loadThumbnails", 1156 "loading " + TextUtils.join(",", photoIdsAsStrings)); 1157 } 1158 cursor = 1159 resolver.query( 1160 Data.CONTENT_URI, 1161 COLUMNS, 1162 stringBuilder.toString(), 1163 photoIdsAsStrings.toArray(EMPTY_STRING_ARRAY), 1164 null); 1165 1166 if (cursor != null) { 1167 while (cursor.moveToNext()) { 1168 Long id = cursor.getLong(0); 1169 byte[] bytes = cursor.getBlob(1); 1170 cacheBitmap(id, bytes, preloading, -1); 1171 photoIds.remove(id); 1172 } 1173 } 1174 } finally { 1175 if (cursor != null) { 1176 cursor.close(); 1177 } 1178 } 1179 1180 // Remaining photos were not found in the contacts database (but might be in profile). 1181 for (Long id : photoIds) { 1182 if (ContactsContract.isProfileId(id)) { 1183 Cursor profileCursor = null; 1184 try { 1185 profileCursor = 1186 resolver.query( 1187 ContentUris.withAppendedId(Data.CONTENT_URI, id), COLUMNS, null, null, null); 1188 if (profileCursor != null && profileCursor.moveToFirst()) { 1189 cacheBitmap(profileCursor.getLong(0), profileCursor.getBlob(1), preloading, -1); 1190 } else { 1191 // Couldn't load a photo this way either. 1192 cacheBitmap(id, null, preloading, -1); 1193 } 1194 } finally { 1195 if (profileCursor != null) { 1196 profileCursor.close(); 1197 } 1198 } 1199 } else { 1200 // Not a profile photo and not found - mark the cache accordingly 1201 cacheBitmap(id, null, preloading, -1); 1202 } 1203 } 1204 1205 mainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED); 1206 } 1207 1208 /** 1209 * Loads photos referenced with Uris. Those can be remote thumbnails (from directory searches), 1210 * display photos etc 1211 */ 1212 @WorkerThread 1213 private void loadUriBasedPhotos() { 1214 for (Request uriRequest : photoUris) { 1215 // Keep the original URI and use this to key into the cache. Failure to do so will 1216 // result in an image being continually reloaded into cache if the original URI 1217 // has a contact type encodedFragment (eg nearby places business photo URLs). 1218 Uri originalUri = uriRequest.getUri(); 1219 1220 // Strip off the "contact type" we added to the URI to ensure it was identifiable as 1221 // a business photo -- there is no need to pass this on to the server. 1222 Uri uri = ContactPhotoManager.removeContactType(originalUri); 1223 1224 if (buffer == null) { 1225 buffer = new byte[BUFFER_SIZE]; 1226 } 1227 try { 1228 if (DEBUG) { 1229 LogUtil.d("ContactPhotoManagerImpl.loadUriBasedPhotos", "loading " + uri); 1230 } 1231 final String scheme = uri.getScheme(); 1232 InputStream is = null; 1233 if (scheme.equals("http") || scheme.equals("https")) { 1234 TrafficStats.setThreadStatsTag(TrafficStatsTags.CONTACT_PHOTO_DOWNLOAD_TAG); 1235 try { 1236 final HttpURLConnection connection = 1237 (HttpURLConnection) new URL(uri.toString()).openConnection(); 1238 1239 // Include the user agent if it is specified. 1240 if (!TextUtils.isEmpty(userAgent)) { 1241 connection.setRequestProperty("User-Agent", userAgent); 1242 } 1243 try { 1244 is = connection.getInputStream(); 1245 } catch (IOException e) { 1246 connection.disconnect(); 1247 is = null; 1248 } 1249 } finally { 1250 TrafficStats.clearThreadStatsTag(); 1251 } 1252 } else { 1253 is = resolver.openInputStream(uri); 1254 } 1255 if (is != null) { 1256 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 1257 try { 1258 int size; 1259 while ((size = is.read(buffer)) != -1) { 1260 baos.write(buffer, 0, size); 1261 } 1262 } finally { 1263 is.close(); 1264 } 1265 cacheBitmap(originalUri, baos.toByteArray(), false, uriRequest.getRequestedExtent()); 1266 mainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED); 1267 } else { 1268 LogUtil.v("ContactPhotoManagerImpl.loadUriBasedPhotos", "cannot load photo " + uri); 1269 cacheBitmap(originalUri, null, false, uriRequest.getRequestedExtent()); 1270 } 1271 } catch (final Exception | OutOfMemoryError ex) { 1272 LogUtil.v("ContactPhotoManagerImpl.loadUriBasedPhotos", "cannot load photo " + uri, ex); 1273 cacheBitmap(originalUri, null, false, uriRequest.getRequestedExtent()); 1274 } 1275 } 1276 } 1277 } 1278 } 1279