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.contacts.common; 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.contacts.common.util.BitmapUtil; 56 import com.android.contacts.common.util.TrafficStatsTags; 57 import com.android.contacts.common.util.UriUtils; 58 import com.android.dialer.common.LogUtil; 59 import com.android.dialer.util.PermissionsUtil; 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 #mBitmapHolderCache} for devices with "large" RAM. */ 99 private static final int HOLDER_CACHE_SIZE = 2000000; 100 /** Cache size for {@link #mBitmapCache} 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 mThumbnailSize; 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 mContext; 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> mBitmapHolderCache; 116 /** Cache size threshold at which bitmaps will not be preloaded. */ 117 private final int mBitmapHolderCacheRedZoneBytes; 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 * #mBitmapHolderCache}. 122 */ 123 private final LruCache<Object, Bitmap> mBitmapCache; 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> mPendingRequests = 129 new ConcurrentHashMap<ImageView, Request>(); 130 /** Handler for messages sent to the UI thread. */ 131 private final Handler mMainThreadHandler = new Handler(this); 132 /** For debug: How many times we had to reload cached photo for a stale entry */ 133 private final AtomicInteger mStaleCacheOverwrite = 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 mFreshCacheOverwrite = new AtomicInteger(); 136 /** {@code true} if ALL entries in {@link #mBitmapHolderCache} are NOT fresh. */ 137 private volatile boolean mBitmapHolderCacheAllUnfresh = true; 138 /** Thread responsible for loading photos from the database. Created upon the first request. */ 139 private LoaderThread mLoaderThread; 140 /** A gate to make sure we only send one instance of MESSAGE_PHOTOS_NEEDED at a time. */ 141 private boolean mLoadingRequested; 142 /** Flag indicating if the image loading is paused. */ 143 private boolean mPaused; 144 /** The user agent string to use when loading URI based photos. */ 145 private String mUserAgent; 146 ContactPhotoManagerImpl(Context context)147 public ContactPhotoManagerImpl(Context context) { 148 mContext = 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 mBitmapCache = 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 mBitmapHolderCache = 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 mBitmapHolderCacheRedZoneBytes = (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(mBitmapHolderCache.maxSize()) + " + " + btk(mBitmapCache.maxSize())); 194 } 195 196 mThumbnailSize = 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 mUserAgent = Bindings.get(context).getUserAgent(); 201 if (mUserAgent == null) { 202 mUserAgent = ""; 203 } 204 } 205 206 /** Converts bytes to K bytes, rounding up. Used only for debug log. */ btk(int bytes)207 private static String btk(int bytes) { 208 return ((bytes + 1023) / 1024) + "K"; 209 } 210 safeDiv(int dividend, int divisor)211 private static final int safeDiv(int dividend, int divisor) { 212 return (divisor == 0) ? 0 : (dividend / divisor); 213 } 214 isChildView(View parent, View potentialChild)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 #mBitmapCache} or by a soft reference in the holder, it will not be necessary 225 * to decode the bitmap. 226 */ inflateBitmap(BitmapHolder holder, int requestedExtent)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) <= mThumbnailSize * 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. */ dumpStats()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 : mBitmapHolderCache.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 + mBitmapHolderCache.toString() 340 + ", overwrite: fresh=" 341 + mFreshCacheOverwrite.get() 342 + " stale=" 343 + mStaleCacheOverwrite.get()); 344 } 345 346 { 347 int numBitmaps = 0; 348 int bitmapBytes = 0; 349 for (Bitmap b : mBitmapCache.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 onTrimMemory(int level)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 preloadPhotosInBackground()378 public void preloadPhotosInBackground() { 379 ensureLoaderThread(); 380 mLoaderThread.requestPreloading(); 381 } 382 383 @Override loadThumbnail( ImageView view, long photoId, boolean darkTheme, boolean isCircular, DefaultImageRequest defaultImageRequest, DefaultImageProvider defaultProvider)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 mPendingRequests.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 loadPhoto( ImageView view, Uri photoUri, int requestedExtent, boolean darkTheme, boolean isCircular, DefaultImageRequest defaultImageRequest, DefaultImageProvider defaultProvider)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 mPendingRequests.remove(view); 417 } else { 418 if (DEBUG) { 419 LogUtil.d("ContactPhotoManagerImpl.loadPhoto", "loadPhoto request: " + photoUri); 420 } 421 if (isDefaultImageUri(photoUri)) { 422 createAndApplyDefaultImageForUri( 423 view, photoUri, requestedExtent, darkTheme, isCircular, defaultProvider); 424 } else { 425 loadPhotoByIdOrUri( 426 view, 427 Request.createFromUri( 428 photoUri, requestedExtent, darkTheme, isCircular, defaultProvider)); 429 } 430 } 431 } 432 createAndApplyDefaultImageForUri( ImageView view, Uri uri, int requestedExtent, boolean darkTheme, boolean isCircular, DefaultImageProvider defaultProvider)433 private void createAndApplyDefaultImageForUri( 434 ImageView view, 435 Uri uri, 436 int requestedExtent, 437 boolean darkTheme, 438 boolean isCircular, 439 DefaultImageProvider defaultProvider) { 440 DefaultImageRequest request = getDefaultImageRequestFromUri(uri); 441 request.isCircular = isCircular; 442 defaultProvider.applyDefaultImage(view, requestedExtent, darkTheme, request); 443 } 444 loadPhotoByIdOrUri(ImageView view, Request request)445 private void loadPhotoByIdOrUri(ImageView view, Request request) { 446 boolean loaded = loadCachedPhoto(view, request, false); 447 if (loaded) { 448 mPendingRequests.remove(view); 449 } else { 450 mPendingRequests.put(view, request); 451 if (!mPaused) { 452 // Send a request to start loading photos 453 requestLoading(); 454 } 455 } 456 } 457 458 @Override removePhoto(ImageView view)459 public void removePhoto(ImageView view) { 460 view.setImageDrawable(null); 461 mPendingRequests.remove(view); 462 } 463 464 /** 465 * Cancels pending requests to load photos asynchronously for views inside {@param 466 * fragmentRootView}. If {@param fragmentRootView} is null, cancels all requests. 467 */ 468 @Override cancelPendingRequests(View fragmentRootView)469 public void cancelPendingRequests(View fragmentRootView) { 470 if (fragmentRootView == null) { 471 mPendingRequests.clear(); 472 return; 473 } 474 final Iterator<Entry<ImageView, Request>> iterator = mPendingRequests.entrySet().iterator(); 475 while (iterator.hasNext()) { 476 final ImageView imageView = iterator.next().getKey(); 477 // If an ImageView is orphaned (currently scrap) or a child of fragmentRootView, then 478 // we can safely remove its request. 479 if (imageView.getParent() == null || isChildView(fragmentRootView, imageView)) { 480 iterator.remove(); 481 } 482 } 483 } 484 485 @Override refreshCache()486 public void refreshCache() { 487 if (mBitmapHolderCacheAllUnfresh) { 488 if (DEBUG) { 489 LogUtil.d("ContactPhotoManagerImpl.refreshCache", "refreshCache -- no fresh entries."); 490 } 491 return; 492 } 493 if (DEBUG) { 494 LogUtil.d("ContactPhotoManagerImpl.refreshCache", "refreshCache"); 495 } 496 mBitmapHolderCacheAllUnfresh = true; 497 for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) { 498 if (holder != BITMAP_UNAVAILABLE) { 499 holder.fresh = false; 500 } 501 } 502 } 503 504 /** 505 * Checks if the photo is present in cache. If so, sets the photo on the view. 506 * 507 * @return false if the photo needs to be (re)loaded from the provider. 508 */ 509 @UiThread loadCachedPhoto(ImageView view, Request request, boolean fadeIn)510 private boolean loadCachedPhoto(ImageView view, Request request, boolean fadeIn) { 511 BitmapHolder holder = mBitmapHolderCache.get(request.getKey()); 512 if (holder == null) { 513 // The bitmap has not been loaded ==> show default avatar 514 request.applyDefaultImage(view, request.mIsCircular); 515 return false; 516 } 517 518 if (holder.bytes == null) { 519 request.applyDefaultImage(view, request.mIsCircular); 520 return holder.fresh; 521 } 522 523 Bitmap cachedBitmap = holder.bitmapRef == null ? null : holder.bitmapRef.get(); 524 if (cachedBitmap == null) { 525 request.applyDefaultImage(view, request.mIsCircular); 526 return false; 527 } 528 529 final Drawable previousDrawable = view.getDrawable(); 530 if (fadeIn && previousDrawable != null) { 531 final Drawable[] layers = new Drawable[2]; 532 // Prevent cascade of TransitionDrawables. 533 if (previousDrawable instanceof TransitionDrawable) { 534 final TransitionDrawable previousTransitionDrawable = (TransitionDrawable) previousDrawable; 535 layers[0] = 536 previousTransitionDrawable.getDrawable( 537 previousTransitionDrawable.getNumberOfLayers() - 1); 538 } else { 539 layers[0] = previousDrawable; 540 } 541 layers[1] = getDrawableForBitmap(mContext.getResources(), cachedBitmap, request); 542 TransitionDrawable drawable = new TransitionDrawable(layers); 543 view.setImageDrawable(drawable); 544 drawable.startTransition(FADE_TRANSITION_DURATION); 545 } else { 546 view.setImageDrawable(getDrawableForBitmap(mContext.getResources(), cachedBitmap, request)); 547 } 548 549 // Put the bitmap in the LRU cache. But only do this for images that are small enough 550 // (we require that at least six of those can be cached at the same time) 551 if (cachedBitmap.getByteCount() < mBitmapCache.maxSize() / 6) { 552 mBitmapCache.put(request.getKey(), cachedBitmap); 553 } 554 555 // Soften the reference 556 holder.bitmap = null; 557 558 return holder.fresh; 559 } 560 561 /** 562 * Given a bitmap, returns a drawable that is configured to display the bitmap based on the 563 * specified request. 564 */ getDrawableForBitmap(Resources resources, Bitmap bitmap, Request request)565 private Drawable getDrawableForBitmap(Resources resources, Bitmap bitmap, Request request) { 566 if (request.mIsCircular) { 567 final RoundedBitmapDrawable drawable = RoundedBitmapDrawableFactory.create(resources, bitmap); 568 drawable.setAntiAlias(true); 569 drawable.setCornerRadius(bitmap.getHeight() / 2); 570 return drawable; 571 } else { 572 return new BitmapDrawable(resources, bitmap); 573 } 574 } 575 clear()576 public void clear() { 577 if (DEBUG) { 578 LogUtil.d("ContactPhotoManagerImpl.clear", "clear"); 579 } 580 mPendingRequests.clear(); 581 mBitmapHolderCache.evictAll(); 582 mBitmapCache.evictAll(); 583 } 584 585 @Override pause()586 public void pause() { 587 mPaused = true; 588 } 589 590 @Override resume()591 public void resume() { 592 mPaused = false; 593 if (DEBUG) { 594 dumpStats(); 595 } 596 if (!mPendingRequests.isEmpty()) { 597 requestLoading(); 598 } 599 } 600 601 /** 602 * Sends a message to this thread itself to start loading images. If the current view contains 603 * multiple image views, all of those image views will get a chance to request their respective 604 * photos before any of those requests are executed. This allows us to load images in bulk. 605 */ requestLoading()606 private void requestLoading() { 607 if (!mLoadingRequested) { 608 mLoadingRequested = true; 609 mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING); 610 } 611 } 612 613 /** Processes requests on the main thread. */ 614 @Override handleMessage(Message msg)615 public boolean handleMessage(Message msg) { 616 switch (msg.what) { 617 case MESSAGE_REQUEST_LOADING: 618 { 619 mLoadingRequested = false; 620 if (!mPaused) { 621 ensureLoaderThread(); 622 mLoaderThread.requestLoading(); 623 } 624 return true; 625 } 626 627 case MESSAGE_PHOTOS_LOADED: 628 { 629 if (!mPaused) { 630 processLoadedImages(); 631 } 632 if (DEBUG) { 633 dumpStats(); 634 } 635 return true; 636 } 637 } 638 return false; 639 } 640 ensureLoaderThread()641 public void ensureLoaderThread() { 642 if (mLoaderThread == null) { 643 mLoaderThread = new LoaderThread(mContext.getContentResolver()); 644 mLoaderThread.start(); 645 } 646 } 647 648 /** 649 * Goes over pending loading requests and displays loaded photos. If some of the photos still 650 * haven't been loaded, sends another request for image loading. 651 */ processLoadedImages()652 private void processLoadedImages() { 653 final Iterator<Entry<ImageView, Request>> iterator = mPendingRequests.entrySet().iterator(); 654 while (iterator.hasNext()) { 655 final Entry<ImageView, Request> entry = iterator.next(); 656 // TODO: Temporarily disable contact photo fading in, until issues with 657 // RoundedBitmapDrawables overlapping the default image drawables are resolved. 658 final boolean loaded = loadCachedPhoto(entry.getKey(), entry.getValue(), false); 659 if (loaded) { 660 iterator.remove(); 661 } 662 } 663 664 softenCache(); 665 666 if (!mPendingRequests.isEmpty()) { 667 requestLoading(); 668 } 669 } 670 671 /** 672 * Removes strong references to loaded bitmaps to allow them to be garbage collected if needed. 673 * Some of the bitmaps will still be retained by {@link #mBitmapCache}. 674 */ softenCache()675 private void softenCache() { 676 for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) { 677 holder.bitmap = null; 678 } 679 } 680 681 /** Stores the supplied bitmap in cache. */ cacheBitmap(Object key, byte[] bytes, boolean preloading, int requestedExtent)682 private void cacheBitmap(Object key, byte[] bytes, boolean preloading, int requestedExtent) { 683 if (DEBUG) { 684 BitmapHolder prev = mBitmapHolderCache.get(key); 685 if (prev != null && prev.bytes != null) { 686 LogUtil.d( 687 "ContactPhotoManagerImpl.cacheBitmap", 688 "overwriting cache: key=" + key + (prev.fresh ? " FRESH" : " stale")); 689 if (prev.fresh) { 690 mFreshCacheOverwrite.incrementAndGet(); 691 } else { 692 mStaleCacheOverwrite.incrementAndGet(); 693 } 694 } 695 LogUtil.d( 696 "ContactPhotoManagerImpl.cacheBitmap", 697 "caching data: key=" + key + ", " + (bytes == null ? "<null>" : btk(bytes.length))); 698 } 699 BitmapHolder holder = 700 new BitmapHolder(bytes, bytes == null ? -1 : BitmapUtil.getSmallerExtentFromBytes(bytes)); 701 702 // Unless this image is being preloaded, decode it right away while 703 // we are still on the background thread. 704 if (!preloading) { 705 inflateBitmap(holder, requestedExtent); 706 } 707 708 if (bytes != null) { 709 mBitmapHolderCache.put(key, holder); 710 if (mBitmapHolderCache.get(key) != holder) { 711 LogUtil.w("ContactPhotoManagerImpl.cacheBitmap", "bitmap too big to fit in cache."); 712 mBitmapHolderCache.put(key, BITMAP_UNAVAILABLE); 713 } 714 } else { 715 mBitmapHolderCache.put(key, BITMAP_UNAVAILABLE); 716 } 717 718 mBitmapHolderCacheAllUnfresh = false; 719 } 720 721 /** 722 * Populates an array of photo IDs that need to be loaded. Also decodes bitmaps that we have 723 * already loaded 724 */ obtainPhotoIdsAndUrisToLoad( Set<Long> photoIds, Set<String> photoIdsAsStrings, Set<Request> uris)725 private void obtainPhotoIdsAndUrisToLoad( 726 Set<Long> photoIds, Set<String> photoIdsAsStrings, Set<Request> uris) { 727 photoIds.clear(); 728 photoIdsAsStrings.clear(); 729 uris.clear(); 730 731 boolean jpegsDecoded = false; 732 733 /* 734 * Since the call is made from the loader thread, the map could be 735 * changing during the iteration. That's not really a problem: 736 * ConcurrentHashMap will allow those changes to happen without throwing 737 * exceptions. Since we may miss some requests in the situation of 738 * concurrent change, we will need to check the map again once loading 739 * is complete. 740 */ 741 Iterator<Request> iterator = mPendingRequests.values().iterator(); 742 while (iterator.hasNext()) { 743 Request request = iterator.next(); 744 final BitmapHolder holder = mBitmapHolderCache.get(request.getKey()); 745 if (holder == BITMAP_UNAVAILABLE) { 746 continue; 747 } 748 if (holder != null 749 && holder.bytes != null 750 && holder.fresh 751 && (holder.bitmapRef == null || holder.bitmapRef.get() == null)) { 752 // This was previously loaded but we don't currently have the inflated Bitmap 753 inflateBitmap(holder, request.getRequestedExtent()); 754 jpegsDecoded = true; 755 } else { 756 if (holder == null || !holder.fresh) { 757 if (request.isUriRequest()) { 758 uris.add(request); 759 } else { 760 photoIds.add(request.getId()); 761 photoIdsAsStrings.add(String.valueOf(request.mId)); 762 } 763 } 764 } 765 } 766 767 if (jpegsDecoded) { 768 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED); 769 } 770 } 771 772 /** Maintains the state of a particular photo. */ 773 private static class BitmapHolder { 774 775 final byte[] bytes; 776 final int originalSmallerExtent; 777 778 volatile boolean fresh; 779 Bitmap bitmap; 780 Reference<Bitmap> bitmapRef; 781 int decodedSampleSize; 782 BitmapHolder(byte[] bytes, int originalSmallerExtent)783 public BitmapHolder(byte[] bytes, int originalSmallerExtent) { 784 this.bytes = bytes; 785 this.fresh = true; 786 this.originalSmallerExtent = originalSmallerExtent; 787 } 788 } 789 790 /** 791 * A holder for either a Uri or an id and a flag whether this was requested for the dark or light 792 * theme 793 */ 794 private static final class Request { 795 796 private final long mId; 797 private final Uri mUri; 798 private final boolean mDarkTheme; 799 private final int mRequestedExtent; 800 private final DefaultImageProvider mDefaultProvider; 801 /** Whether or not the contact photo is to be displayed as a circle */ 802 private final boolean mIsCircular; 803 Request( long id, Uri uri, int requestedExtent, boolean darkTheme, boolean isCircular, DefaultImageProvider defaultProvider)804 private Request( 805 long id, 806 Uri uri, 807 int requestedExtent, 808 boolean darkTheme, 809 boolean isCircular, 810 DefaultImageProvider defaultProvider) { 811 mId = id; 812 mUri = uri; 813 mDarkTheme = darkTheme; 814 mIsCircular = isCircular; 815 mRequestedExtent = requestedExtent; 816 mDefaultProvider = defaultProvider; 817 } 818 createFromThumbnailId( long id, boolean darkTheme, boolean isCircular, DefaultImageProvider defaultProvider)819 public static Request createFromThumbnailId( 820 long id, boolean darkTheme, boolean isCircular, DefaultImageProvider defaultProvider) { 821 return new Request(id, null /* no URI */, -1, darkTheme, isCircular, defaultProvider); 822 } 823 createFromUri( Uri uri, int requestedExtent, boolean darkTheme, boolean isCircular, DefaultImageProvider defaultProvider)824 public static Request createFromUri( 825 Uri uri, 826 int requestedExtent, 827 boolean darkTheme, 828 boolean isCircular, 829 DefaultImageProvider defaultProvider) { 830 return new Request( 831 0 /* no ID */, uri, requestedExtent, darkTheme, isCircular, defaultProvider); 832 } 833 isUriRequest()834 public boolean isUriRequest() { 835 return mUri != null; 836 } 837 getUri()838 public Uri getUri() { 839 return mUri; 840 } 841 getId()842 public long getId() { 843 return mId; 844 } 845 getRequestedExtent()846 public int getRequestedExtent() { 847 return mRequestedExtent; 848 } 849 850 @Override hashCode()851 public int hashCode() { 852 final int prime = 31; 853 int result = 1; 854 result = prime * result + (int) (mId ^ (mId >>> 32)); 855 result = prime * result + mRequestedExtent; 856 result = prime * result + ((mUri == null) ? 0 : mUri.hashCode()); 857 return result; 858 } 859 860 @Override equals(Object obj)861 public boolean equals(Object obj) { 862 if (this == obj) { 863 return true; 864 } 865 if (obj == null) { 866 return false; 867 } 868 if (getClass() != obj.getClass()) { 869 return false; 870 } 871 final Request that = (Request) obj; 872 if (mId != that.mId) { 873 return false; 874 } 875 if (mRequestedExtent != that.mRequestedExtent) { 876 return false; 877 } 878 if (!UriUtils.areEqual(mUri, that.mUri)) { 879 return false; 880 } 881 // Don't compare equality of mDarkTheme because it is only used in the default contact 882 // photo case. When the contact does have a photo, the contact photo is the same 883 // regardless of mDarkTheme, so we shouldn't need to put the photo request on the queue 884 // twice. 885 return true; 886 } 887 getKey()888 public Object getKey() { 889 return mUri == null ? mId : mUri; 890 } 891 892 /** 893 * Applies the default image to the current view. If the request is URI-based, looks for the 894 * contact type encoded fragment to determine if this is a request for a business photo, in 895 * which case we will load the default business photo. 896 * 897 * @param view The current image view to apply the image to. 898 * @param isCircular Whether the image is circular or not. 899 */ applyDefaultImage(ImageView view, boolean isCircular)900 public void applyDefaultImage(ImageView view, boolean isCircular) { 901 final DefaultImageRequest request; 902 903 if (isCircular) { 904 request = 905 ContactPhotoManager.isBusinessContactUri(mUri) 906 ? DefaultImageRequest.EMPTY_CIRCULAR_BUSINESS_IMAGE_REQUEST 907 : DefaultImageRequest.EMPTY_CIRCULAR_DEFAULT_IMAGE_REQUEST; 908 } else { 909 request = 910 ContactPhotoManager.isBusinessContactUri(mUri) 911 ? DefaultImageRequest.EMPTY_DEFAULT_BUSINESS_IMAGE_REQUEST 912 : DefaultImageRequest.EMPTY_DEFAULT_IMAGE_REQUEST; 913 } 914 mDefaultProvider.applyDefaultImage(view, mRequestedExtent, mDarkTheme, request); 915 } 916 } 917 918 /** The thread that performs loading of photos from the database. */ 919 private class LoaderThread extends HandlerThread implements Callback { 920 921 private static final int BUFFER_SIZE = 1024 * 16; 922 private static final int MESSAGE_PRELOAD_PHOTOS = 0; 923 private static final int MESSAGE_LOAD_PHOTOS = 1; 924 925 /** A pause between preload batches that yields to the UI thread. */ 926 private static final int PHOTO_PRELOAD_DELAY = 1000; 927 928 /** Number of photos to preload per batch. */ 929 private static final int PRELOAD_BATCH = 25; 930 931 /** 932 * Maximum number of photos to preload. If the cache size is 2Mb and the expected average size 933 * of a photo is 4kb, then this number should be 2Mb/4kb = 500. 934 */ 935 private static final int MAX_PHOTOS_TO_PRELOAD = 100; 936 937 private static final int PRELOAD_STATUS_NOT_STARTED = 0; 938 private static final int PRELOAD_STATUS_IN_PROGRESS = 1; 939 private static final int PRELOAD_STATUS_DONE = 2; 940 private final ContentResolver mResolver; 941 private final StringBuilder mStringBuilder = new StringBuilder(); 942 private final Set<Long> mPhotoIds = new HashSet<>(); 943 private final Set<String> mPhotoIdsAsStrings = new HashSet<>(); 944 private final Set<Request> mPhotoUris = new HashSet<>(); 945 private final List<Long> mPreloadPhotoIds = new ArrayList<>(); 946 private Handler mLoaderThreadHandler; 947 private byte[] mBuffer; 948 private int mPreloadStatus = PRELOAD_STATUS_NOT_STARTED; 949 LoaderThread(ContentResolver resolver)950 public LoaderThread(ContentResolver resolver) { 951 super(LOADER_THREAD_NAME); 952 mResolver = resolver; 953 } 954 ensureHandler()955 public void ensureHandler() { 956 if (mLoaderThreadHandler == null) { 957 mLoaderThreadHandler = new Handler(getLooper(), this); 958 } 959 } 960 961 /** 962 * Kicks off preloading of the next batch of photos on the background thread. Preloading will 963 * happen after a delay: we want to yield to the UI thread as much as possible. 964 * 965 * <p>If preloading is already complete, does nothing. 966 */ requestPreloading()967 public void requestPreloading() { 968 if (mPreloadStatus == PRELOAD_STATUS_DONE) { 969 return; 970 } 971 972 ensureHandler(); 973 if (mLoaderThreadHandler.hasMessages(MESSAGE_LOAD_PHOTOS)) { 974 return; 975 } 976 977 mLoaderThreadHandler.sendEmptyMessageDelayed(MESSAGE_PRELOAD_PHOTOS, PHOTO_PRELOAD_DELAY); 978 } 979 980 /** 981 * Sends a message to this thread to load requested photos. Cancels a preloading request, if 982 * any: we don't want preloading to impede loading of the photos we need to display now. 983 */ requestLoading()984 public void requestLoading() { 985 ensureHandler(); 986 mLoaderThreadHandler.removeMessages(MESSAGE_PRELOAD_PHOTOS); 987 mLoaderThreadHandler.sendEmptyMessage(MESSAGE_LOAD_PHOTOS); 988 } 989 990 /** 991 * Receives the above message, loads photos and then sends a message to the main thread to 992 * process them. 993 */ 994 @Override handleMessage(Message msg)995 public boolean handleMessage(Message msg) { 996 switch (msg.what) { 997 case MESSAGE_PRELOAD_PHOTOS: 998 preloadPhotosInBackground(); 999 break; 1000 case MESSAGE_LOAD_PHOTOS: 1001 loadPhotosInBackground(); 1002 break; 1003 } 1004 return true; 1005 } 1006 1007 /** 1008 * The first time it is called, figures out which photos need to be preloaded. Each subsequent 1009 * call preloads the next batch of photos and requests another cycle of preloading after a 1010 * delay. The whole process ends when we either run out of photos to preload or fill up cache. 1011 */ 1012 @WorkerThread preloadPhotosInBackground()1013 private void preloadPhotosInBackground() { 1014 if (!PermissionsUtil.hasPermission(mContext, android.Manifest.permission.READ_CONTACTS)) { 1015 return; 1016 } 1017 1018 if (mPreloadStatus == PRELOAD_STATUS_DONE) { 1019 return; 1020 } 1021 1022 if (mPreloadStatus == PRELOAD_STATUS_NOT_STARTED) { 1023 queryPhotosForPreload(); 1024 if (mPreloadPhotoIds.isEmpty()) { 1025 mPreloadStatus = PRELOAD_STATUS_DONE; 1026 } else { 1027 mPreloadStatus = PRELOAD_STATUS_IN_PROGRESS; 1028 } 1029 requestPreloading(); 1030 return; 1031 } 1032 1033 if (mBitmapHolderCache.size() > mBitmapHolderCacheRedZoneBytes) { 1034 mPreloadStatus = PRELOAD_STATUS_DONE; 1035 return; 1036 } 1037 1038 mPhotoIds.clear(); 1039 mPhotoIdsAsStrings.clear(); 1040 1041 int count = 0; 1042 int preloadSize = mPreloadPhotoIds.size(); 1043 while (preloadSize > 0 && mPhotoIds.size() < PRELOAD_BATCH) { 1044 preloadSize--; 1045 count++; 1046 Long photoId = mPreloadPhotoIds.get(preloadSize); 1047 mPhotoIds.add(photoId); 1048 mPhotoIdsAsStrings.add(photoId.toString()); 1049 mPreloadPhotoIds.remove(preloadSize); 1050 } 1051 1052 loadThumbnails(true); 1053 1054 if (preloadSize == 0) { 1055 mPreloadStatus = PRELOAD_STATUS_DONE; 1056 } 1057 1058 LogUtil.v( 1059 "ContactPhotoManagerImpl.preloadPhotosInBackground", 1060 "preloaded " + count + " photos. cached bytes: " + mBitmapHolderCache.size()); 1061 1062 requestPreloading(); 1063 } 1064 1065 @WorkerThread queryPhotosForPreload()1066 private void queryPhotosForPreload() { 1067 Cursor cursor = null; 1068 try { 1069 Uri uri = 1070 Contacts.CONTENT_URI 1071 .buildUpon() 1072 .appendQueryParameter( 1073 ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT)) 1074 .appendQueryParameter( 1075 ContactsContract.LIMIT_PARAM_KEY, String.valueOf(MAX_PHOTOS_TO_PRELOAD)) 1076 .build(); 1077 cursor = 1078 mResolver.query( 1079 uri, 1080 new String[] {Contacts.PHOTO_ID}, 1081 Contacts.PHOTO_ID + " NOT NULL AND " + Contacts.PHOTO_ID + "!=0", 1082 null, 1083 Contacts.STARRED + " DESC, " + Contacts.LAST_TIME_CONTACTED + " DESC"); 1084 1085 if (cursor != null) { 1086 while (cursor.moveToNext()) { 1087 // Insert them in reverse order, because we will be taking 1088 // them from the end of the list for loading. 1089 mPreloadPhotoIds.add(0, cursor.getLong(0)); 1090 } 1091 } 1092 } finally { 1093 if (cursor != null) { 1094 cursor.close(); 1095 } 1096 } 1097 } 1098 1099 @WorkerThread loadPhotosInBackground()1100 private void loadPhotosInBackground() { 1101 if (!PermissionsUtil.hasPermission(mContext, android.Manifest.permission.READ_CONTACTS)) { 1102 return; 1103 } 1104 obtainPhotoIdsAndUrisToLoad(mPhotoIds, mPhotoIdsAsStrings, mPhotoUris); 1105 loadThumbnails(false); 1106 loadUriBasedPhotos(); 1107 requestPreloading(); 1108 } 1109 1110 /** Loads thumbnail photos with ids */ 1111 @WorkerThread loadThumbnails(boolean preloading)1112 private void loadThumbnails(boolean preloading) { 1113 if (mPhotoIds.isEmpty()) { 1114 return; 1115 } 1116 1117 // Remove loaded photos from the preload queue: we don't want 1118 // the preloading process to load them again. 1119 if (!preloading && mPreloadStatus == PRELOAD_STATUS_IN_PROGRESS) { 1120 for (Long id : mPhotoIds) { 1121 mPreloadPhotoIds.remove(id); 1122 } 1123 if (mPreloadPhotoIds.isEmpty()) { 1124 mPreloadStatus = PRELOAD_STATUS_DONE; 1125 } 1126 } 1127 1128 mStringBuilder.setLength(0); 1129 mStringBuilder.append(Photo._ID + " IN("); 1130 for (int i = 0; i < mPhotoIds.size(); i++) { 1131 if (i != 0) { 1132 mStringBuilder.append(','); 1133 } 1134 mStringBuilder.append('?'); 1135 } 1136 mStringBuilder.append(')'); 1137 1138 Cursor cursor = null; 1139 try { 1140 if (DEBUG) { 1141 LogUtil.d( 1142 "ContactPhotoManagerImpl.loadThumbnails", 1143 "loading " + TextUtils.join(",", mPhotoIdsAsStrings)); 1144 } 1145 cursor = 1146 mResolver.query( 1147 Data.CONTENT_URI, 1148 COLUMNS, 1149 mStringBuilder.toString(), 1150 mPhotoIdsAsStrings.toArray(EMPTY_STRING_ARRAY), 1151 null); 1152 1153 if (cursor != null) { 1154 while (cursor.moveToNext()) { 1155 Long id = cursor.getLong(0); 1156 byte[] bytes = cursor.getBlob(1); 1157 cacheBitmap(id, bytes, preloading, -1); 1158 mPhotoIds.remove(id); 1159 } 1160 } 1161 } finally { 1162 if (cursor != null) { 1163 cursor.close(); 1164 } 1165 } 1166 1167 // Remaining photos were not found in the contacts database (but might be in profile). 1168 for (Long id : mPhotoIds) { 1169 if (ContactsContract.isProfileId(id)) { 1170 Cursor profileCursor = null; 1171 try { 1172 profileCursor = 1173 mResolver.query( 1174 ContentUris.withAppendedId(Data.CONTENT_URI, id), COLUMNS, null, null, null); 1175 if (profileCursor != null && profileCursor.moveToFirst()) { 1176 cacheBitmap(profileCursor.getLong(0), profileCursor.getBlob(1), preloading, -1); 1177 } else { 1178 // Couldn't load a photo this way either. 1179 cacheBitmap(id, null, preloading, -1); 1180 } 1181 } finally { 1182 if (profileCursor != null) { 1183 profileCursor.close(); 1184 } 1185 } 1186 } else { 1187 // Not a profile photo and not found - mark the cache accordingly 1188 cacheBitmap(id, null, preloading, -1); 1189 } 1190 } 1191 1192 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED); 1193 } 1194 1195 /** 1196 * Loads photos referenced with Uris. Those can be remote thumbnails (from directory searches), 1197 * display photos etc 1198 */ 1199 @WorkerThread loadUriBasedPhotos()1200 private void loadUriBasedPhotos() { 1201 for (Request uriRequest : mPhotoUris) { 1202 // Keep the original URI and use this to key into the cache. Failure to do so will 1203 // result in an image being continually reloaded into cache if the original URI 1204 // has a contact type encodedFragment (eg nearby places business photo URLs). 1205 Uri originalUri = uriRequest.getUri(); 1206 1207 // Strip off the "contact type" we added to the URI to ensure it was identifiable as 1208 // a business photo -- there is no need to pass this on to the server. 1209 Uri uri = ContactPhotoManager.removeContactType(originalUri); 1210 1211 if (mBuffer == null) { 1212 mBuffer = new byte[BUFFER_SIZE]; 1213 } 1214 try { 1215 if (DEBUG) { 1216 LogUtil.d("ContactPhotoManagerImpl.loadUriBasedPhotos", "loading " + uri); 1217 } 1218 final String scheme = uri.getScheme(); 1219 InputStream is = null; 1220 if (scheme.equals("http") || scheme.equals("https")) { 1221 TrafficStats.setThreadStatsTag(TrafficStatsTags.CONTACT_PHOTO_DOWNLOAD_TAG); 1222 final HttpURLConnection connection = 1223 (HttpURLConnection) new URL(uri.toString()).openConnection(); 1224 1225 // Include the user agent if it is specified. 1226 if (!TextUtils.isEmpty(mUserAgent)) { 1227 connection.setRequestProperty("User-Agent", mUserAgent); 1228 } 1229 try { 1230 is = connection.getInputStream(); 1231 } catch (IOException e) { 1232 connection.disconnect(); 1233 is = null; 1234 } 1235 TrafficStats.clearThreadStatsTag(); 1236 } else { 1237 is = mResolver.openInputStream(uri); 1238 } 1239 if (is != null) { 1240 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 1241 try { 1242 int size; 1243 while ((size = is.read(mBuffer)) != -1) { 1244 baos.write(mBuffer, 0, size); 1245 } 1246 } finally { 1247 is.close(); 1248 } 1249 cacheBitmap(originalUri, baos.toByteArray(), false, uriRequest.getRequestedExtent()); 1250 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED); 1251 } else { 1252 LogUtil.v("ContactPhotoManagerImpl.loadUriBasedPhotos", "cannot load photo " + uri); 1253 cacheBitmap(originalUri, null, false, uriRequest.getRequestedExtent()); 1254 } 1255 } catch (final Exception | OutOfMemoryError ex) { 1256 LogUtil.v("ContactPhotoManagerImpl.loadUriBasedPhotos", "cannot load photo " + uri, ex); 1257 cacheBitmap(originalUri, null, false, uriRequest.getRequestedExtent()); 1258 } 1259 } 1260 } 1261 } 1262 } 1263