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 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.Configuration; 25 import android.content.res.Resources; 26 import android.database.Cursor; 27 import android.graphics.Bitmap; 28 import android.graphics.Canvas; 29 import android.graphics.Color; 30 import android.graphics.Paint; 31 import android.graphics.Paint.Style; 32 import android.graphics.drawable.BitmapDrawable; 33 import android.graphics.drawable.ColorDrawable; 34 import android.graphics.drawable.Drawable; 35 import android.graphics.drawable.TransitionDrawable; 36 import android.media.ThumbnailUtils; 37 import android.net.TrafficStats; 38 import android.net.Uri; 39 import android.net.Uri.Builder; 40 import android.os.Handler; 41 import android.os.Handler.Callback; 42 import android.os.HandlerThread; 43 import android.os.Message; 44 import android.provider.ContactsContract; 45 import android.provider.ContactsContract.Contacts; 46 import android.provider.ContactsContract.Contacts.Photo; 47 import android.provider.ContactsContract.Data; 48 import android.provider.ContactsContract.Directory; 49 import android.support.v4.graphics.drawable.RoundedBitmapDrawable; 50 import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory; 51 import android.text.TextUtils; 52 import android.util.Log; 53 import android.util.LruCache; 54 import android.view.View; 55 import android.view.ViewGroup; 56 import android.widget.ImageView; 57 58 import com.android.contacts.common.lettertiles.LetterTileDrawable; 59 import com.android.contacts.common.util.BitmapUtil; 60 import com.android.contacts.common.util.PermissionsUtil; 61 import com.android.contacts.common.util.TrafficStatsTags; 62 import com.android.contacts.common.util.UriUtils; 63 import com.android.contacts.commonbind.util.UserAgentGenerator; 64 65 import com.google.common.annotations.VisibleForTesting; 66 import com.google.common.collect.Lists; 67 import com.google.common.collect.Sets; 68 69 import java.io.ByteArrayOutputStream; 70 import java.io.IOException; 71 import java.io.InputStream; 72 import java.lang.ref.Reference; 73 import java.lang.ref.SoftReference; 74 import java.net.HttpURLConnection; 75 import java.net.URL; 76 import java.util.Iterator; 77 import java.util.List; 78 import java.util.Map.Entry; 79 import java.util.Set; 80 import java.util.concurrent.ConcurrentHashMap; 81 import java.util.concurrent.atomic.AtomicInteger; 82 83 /** 84 * Asynchronously loads contact photos and maintains a cache of photos. 85 */ 86 public abstract class ContactPhotoManager implements ComponentCallbacks2 { 87 static final String TAG = "ContactPhotoManager"; 88 static final boolean DEBUG = false; // Don't submit with true 89 static final boolean DEBUG_SIZES = false; // Don't submit with true 90 91 /** Contact type constants used for default letter images */ 92 public static final int TYPE_PERSON = LetterTileDrawable.TYPE_PERSON; 93 public static final int TYPE_BUSINESS = LetterTileDrawable.TYPE_BUSINESS; 94 public static final int TYPE_VOICEMAIL = LetterTileDrawable.TYPE_VOICEMAIL; 95 public static final int TYPE_DEFAULT = LetterTileDrawable.TYPE_DEFAULT; 96 97 /** Scale and offset default constants used for default letter images */ 98 public static final float SCALE_DEFAULT = 1.0f; 99 public static final float OFFSET_DEFAULT = 0.0f; 100 101 public static final boolean IS_CIRCULAR_DEFAULT = false; 102 103 /** Uri-related constants used for default letter images */ 104 private static final String DISPLAY_NAME_PARAM_KEY = "display_name"; 105 private static final String IDENTIFIER_PARAM_KEY = "identifier"; 106 private static final String CONTACT_TYPE_PARAM_KEY = "contact_type"; 107 private static final String SCALE_PARAM_KEY = "scale"; 108 private static final String OFFSET_PARAM_KEY = "offset"; 109 private static final String IS_CIRCULAR_PARAM_KEY = "is_circular"; 110 private static final String DEFAULT_IMAGE_URI_SCHEME = "defaultimage"; 111 private static final Uri DEFAULT_IMAGE_URI = Uri.parse(DEFAULT_IMAGE_URI_SCHEME + "://"); 112 113 // Static field used to cache the default letter avatar drawable that is created 114 // using a null {@link DefaultImageRequest} 115 private static Drawable sDefaultLetterAvatar = null; 116 117 private static ContactPhotoManager sInstance; 118 119 /** 120 * Given a {@link DefaultImageRequest}, returns a {@link Drawable}, that when drawn, will 121 * draw a letter tile avatar based on the request parameters defined in the 122 * {@link DefaultImageRequest}. 123 */ 124 public static Drawable getDefaultAvatarDrawableForContact(Resources resources, boolean hires, 125 DefaultImageRequest defaultImageRequest) { 126 if (defaultImageRequest == null) { 127 if (sDefaultLetterAvatar == null) { 128 // Cache and return the letter tile drawable that is created by a null request, 129 // so that it doesn't have to be recreated every time it is requested again. 130 sDefaultLetterAvatar = LetterTileDefaultImageProvider.getDefaultImageForContact( 131 resources, null); 132 } 133 return sDefaultLetterAvatar; 134 } 135 return LetterTileDefaultImageProvider.getDefaultImageForContact(resources, 136 defaultImageRequest); 137 } 138 139 /** 140 * Given a {@link DefaultImageRequest}, returns an Uri that can be used to request a 141 * letter tile avatar when passed to the {@link ContactPhotoManager}. The internal 142 * implementation of this uri is not guaranteed to remain the same across application 143 * versions, so the actual uri should never be persisted in long-term storage and reused. 144 * 145 * @param request A {@link DefaultImageRequest} object with the fields configured 146 * to return a 147 * @return A Uri that when later passed to the {@link ContactPhotoManager} via 148 * {@link #loadPhoto(ImageView, Uri, int, boolean, DefaultImageRequest)}, can be 149 * used to request a default contact image, drawn as a letter tile using the 150 * parameters as configured in the provided {@link DefaultImageRequest} 151 */ 152 public static Uri getDefaultAvatarUriForContact(DefaultImageRequest request) { 153 final Builder builder = DEFAULT_IMAGE_URI.buildUpon(); 154 if (request != null) { 155 if (!TextUtils.isEmpty(request.displayName)) { 156 builder.appendQueryParameter(DISPLAY_NAME_PARAM_KEY, request.displayName); 157 } 158 if (!TextUtils.isEmpty(request.identifier)) { 159 builder.appendQueryParameter(IDENTIFIER_PARAM_KEY, request.identifier); 160 } 161 if (request.contactType != TYPE_DEFAULT) { 162 builder.appendQueryParameter(CONTACT_TYPE_PARAM_KEY, 163 String.valueOf(request.contactType)); 164 } 165 if (request.scale != SCALE_DEFAULT) { 166 builder.appendQueryParameter(SCALE_PARAM_KEY, String.valueOf(request.scale)); 167 } 168 if (request.offset != OFFSET_DEFAULT) { 169 builder.appendQueryParameter(OFFSET_PARAM_KEY, String.valueOf(request.offset)); 170 } 171 if (request.isCircular != IS_CIRCULAR_DEFAULT) { 172 builder.appendQueryParameter(IS_CIRCULAR_PARAM_KEY, 173 String.valueOf(request.isCircular)); 174 } 175 176 } 177 return builder.build(); 178 } 179 180 /** 181 * Adds a business contact type encoded fragment to the URL. Used to ensure photo URLS 182 * from Nearby Places can be identified as business photo URLs rather than URLs for personal 183 * contact photos. 184 * 185 * @param photoUrl The photo URL to modify. 186 * @return URL with the contact type parameter added and set to TYPE_BUSINESS. 187 */ 188 public static String appendBusinessContactType(String photoUrl) { 189 Uri uri = Uri.parse(photoUrl); 190 Builder builder = uri.buildUpon(); 191 builder.encodedFragment(String.valueOf(TYPE_BUSINESS)); 192 return builder.build().toString(); 193 } 194 195 /** 196 * Removes the contact type information stored in the photo URI encoded fragment. 197 * 198 * @param photoUri The photo URI to remove the contact type from. 199 * @return The photo URI with contact type removed. 200 */ 201 public static Uri removeContactType(Uri photoUri) { 202 String encodedFragment = photoUri.getEncodedFragment(); 203 if (!TextUtils.isEmpty(encodedFragment)) { 204 Builder builder = photoUri.buildUpon(); 205 builder.encodedFragment(null); 206 return builder.build(); 207 } 208 return photoUri; 209 } 210 211 /** 212 * Inspects a photo URI to determine if the photo URI represents a business. 213 * 214 * @param photoUri The URI to inspect. 215 * @return Whether the URI represents a business photo or not. 216 */ 217 public static boolean isBusinessContactUri(Uri photoUri) { 218 if (photoUri == null) { 219 return false; 220 } 221 222 String encodedFragment = photoUri.getEncodedFragment(); 223 return !TextUtils.isEmpty(encodedFragment) 224 && encodedFragment.equals(String.valueOf(TYPE_BUSINESS)); 225 } 226 227 protected static DefaultImageRequest getDefaultImageRequestFromUri(Uri uri) { 228 final DefaultImageRequest request = new DefaultImageRequest( 229 uri.getQueryParameter(DISPLAY_NAME_PARAM_KEY), 230 uri.getQueryParameter(IDENTIFIER_PARAM_KEY), false); 231 try { 232 String contactType = uri.getQueryParameter(CONTACT_TYPE_PARAM_KEY); 233 if (!TextUtils.isEmpty(contactType)) { 234 request.contactType = Integer.valueOf(contactType); 235 } 236 237 String scale = uri.getQueryParameter(SCALE_PARAM_KEY); 238 if (!TextUtils.isEmpty(scale)) { 239 request.scale = Float.valueOf(scale); 240 } 241 242 String offset = uri.getQueryParameter(OFFSET_PARAM_KEY); 243 if (!TextUtils.isEmpty(offset)) { 244 request.offset = Float.valueOf(offset); 245 } 246 247 String isCircular = uri.getQueryParameter(IS_CIRCULAR_PARAM_KEY); 248 if (!TextUtils.isEmpty(isCircular)) { 249 request.isCircular = Boolean.valueOf(isCircular); 250 } 251 } catch (NumberFormatException e) { 252 Log.w(TAG, "Invalid DefaultImageRequest image parameters provided, ignoring and using " 253 + "defaults."); 254 } 255 256 return request; 257 } 258 259 protected boolean isDefaultImageUri(Uri uri) { 260 return DEFAULT_IMAGE_URI_SCHEME.equals(uri.getScheme()); 261 } 262 263 /** 264 * Contains fields used to contain contact details and other user-defined settings that might 265 * be used by the ContactPhotoManager to generate a default contact image. This contact image 266 * takes the form of a letter or bitmap drawn on top of a colored tile. 267 */ 268 public static class DefaultImageRequest { 269 /** 270 * The contact's display name. The display name is used to 271 */ 272 public String displayName; 273 274 /** 275 * A unique and deterministic string that can be used to identify this contact. This is 276 * usually the contact's lookup key, but other contact details can be used as well, 277 * especially for non-local or temporary contacts that might not have a lookup key. This 278 * is used to determine the color of the tile. 279 */ 280 public String identifier; 281 282 /** 283 * The type of this contact. This contact type may be used to decide the kind of 284 * image to use in the case where a unique letter cannot be generated from the contact's 285 * display name and identifier. See: 286 * {@link #TYPE_PERSON} 287 * {@link #TYPE_BUSINESS} 288 * {@link #TYPE_PERSON} 289 * {@link #TYPE_DEFAULT} 290 */ 291 public int contactType = TYPE_DEFAULT; 292 293 /** 294 * The amount to scale the letter or bitmap to, as a ratio of its default size (from a 295 * range of 0.0f to 2.0f). The default value is 1.0f. 296 */ 297 public float scale = SCALE_DEFAULT; 298 299 /** 300 * The amount to vertically offset the letter or image to within the tile. 301 * The provided offset must be within the range of -0.5f to 0.5f. 302 * If set to -0.5f, the letter will be shifted upwards by 0.5 times the height of the canvas 303 * it is being drawn on, which means it will be drawn with the center of the letter starting 304 * at the top edge of the canvas. 305 * If set to 0.5f, the letter will be shifted downwards by 0.5 times the height of the 306 * canvas it is being drawn on, which means it will be drawn with the center of the letter 307 * starting at the bottom edge of the canvas. 308 * The default is 0.0f, which means the letter is drawn in the exact vertical center of 309 * the tile. 310 */ 311 public float offset = OFFSET_DEFAULT; 312 313 /** 314 * Whether or not to draw the default image as a circle, instead of as a square/rectangle. 315 */ 316 public boolean isCircular = false; 317 318 /** 319 * Used to indicate that a drawable that represents a contact without any contact details 320 * should be returned. 321 */ 322 public static DefaultImageRequest EMPTY_DEFAULT_IMAGE_REQUEST = new DefaultImageRequest(); 323 324 /** 325 * Used to indicate that a drawable that represents a business without a business photo 326 * should be returned. 327 */ 328 public static DefaultImageRequest EMPTY_DEFAULT_BUSINESS_IMAGE_REQUEST = 329 new DefaultImageRequest(null, null, TYPE_BUSINESS, false); 330 331 /** 332 * Used to indicate that a circular drawable that represents a contact without any contact 333 * details should be returned. 334 */ 335 public static DefaultImageRequest EMPTY_CIRCULAR_DEFAULT_IMAGE_REQUEST = 336 new DefaultImageRequest(null, null, true); 337 338 /** 339 * Used to indicate that a circular drawable that represents a business without a business 340 * photo should be returned. 341 */ 342 public static DefaultImageRequest EMPTY_CIRCULAR_BUSINESS_IMAGE_REQUEST = 343 new DefaultImageRequest(null, null, TYPE_BUSINESS, true); 344 345 public DefaultImageRequest() { 346 } 347 348 public DefaultImageRequest(String displayName, String identifier, boolean isCircular) { 349 this(displayName, identifier, TYPE_DEFAULT, SCALE_DEFAULT, OFFSET_DEFAULT, isCircular); 350 } 351 352 public DefaultImageRequest(String displayName, String identifier, int contactType, 353 boolean isCircular) { 354 this(displayName, identifier, contactType, SCALE_DEFAULT, OFFSET_DEFAULT, isCircular); 355 } 356 357 public DefaultImageRequest(String displayName, String identifier, int contactType, 358 float scale, float offset, boolean isCircular) { 359 this.displayName = displayName; 360 this.identifier = identifier; 361 this.contactType = contactType; 362 this.scale = scale; 363 this.offset = offset; 364 this.isCircular = isCircular; 365 } 366 } 367 368 public static abstract class DefaultImageProvider { 369 /** 370 * Applies the default avatar to the ImageView. Extent is an indicator for the size (width 371 * or height). If darkTheme is set, the avatar is one that looks better on dark background 372 * 373 * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a 374 * default letter tile avatar should be drawn. 375 */ 376 public abstract void applyDefaultImage(ImageView view, int extent, boolean darkTheme, 377 DefaultImageRequest defaultImageRequest); 378 } 379 380 /** 381 * A default image provider that applies a letter tile consisting of a colored background 382 * and a letter in the foreground as the default image for a contact. The color of the 383 * background and the type of letter is decided based on the contact's details. 384 */ 385 private static class LetterTileDefaultImageProvider extends DefaultImageProvider { 386 @Override 387 public void applyDefaultImage(ImageView view, int extent, boolean darkTheme, 388 DefaultImageRequest defaultImageRequest) { 389 final Drawable drawable = getDefaultImageForContact(view.getResources(), 390 defaultImageRequest); 391 view.setImageDrawable(drawable); 392 } 393 394 public static Drawable getDefaultImageForContact(Resources resources, 395 DefaultImageRequest defaultImageRequest) { 396 final LetterTileDrawable drawable = new LetterTileDrawable(resources); 397 if (defaultImageRequest != null) { 398 // If the contact identifier is null or empty, fallback to the 399 // displayName. In that case, use {@code null} for the contact's 400 // display name so that a default bitmap will be used instead of a 401 // letter 402 if (TextUtils.isEmpty(defaultImageRequest.identifier)) { 403 drawable.setLetterAndColorFromContactDetails(null, 404 defaultImageRequest.displayName); 405 } else { 406 drawable.setLetterAndColorFromContactDetails(defaultImageRequest.displayName, 407 defaultImageRequest.identifier); 408 } 409 drawable.setContactType(defaultImageRequest.contactType); 410 drawable.setScale(defaultImageRequest.scale); 411 drawable.setOffset(defaultImageRequest.offset); 412 drawable.setIsCircular(defaultImageRequest.isCircular); 413 } 414 return drawable; 415 } 416 } 417 418 private static class BlankDefaultImageProvider extends DefaultImageProvider { 419 private static Drawable sDrawable; 420 421 @Override 422 public void applyDefaultImage(ImageView view, int extent, boolean darkTheme, 423 DefaultImageRequest defaultImageRequest) { 424 if (sDrawable == null) { 425 Context context = view.getContext(); 426 sDrawable = new ColorDrawable(context.getResources().getColor( 427 R.color.image_placeholder)); 428 } 429 view.setImageDrawable(sDrawable); 430 } 431 } 432 433 public static DefaultImageProvider DEFAULT_AVATAR = new LetterTileDefaultImageProvider(); 434 435 public static final DefaultImageProvider DEFAULT_BLANK = new BlankDefaultImageProvider(); 436 437 public static ContactPhotoManager getInstance(Context context) { 438 if (sInstance == null) { 439 Context applicationContext = context.getApplicationContext(); 440 sInstance = createContactPhotoManager(applicationContext); 441 applicationContext.registerComponentCallbacks(sInstance); 442 if (PermissionsUtil.hasContactsPermissions(context)) { 443 sInstance.preloadPhotosInBackground(); 444 } 445 } 446 return sInstance; 447 } 448 449 public static synchronized ContactPhotoManager createContactPhotoManager(Context context) { 450 return new ContactPhotoManagerImpl(context); 451 } 452 453 @VisibleForTesting 454 public static void injectContactPhotoManagerForTesting(ContactPhotoManager photoManager) { 455 sInstance = photoManager; 456 } 457 458 /** 459 * Load thumbnail image into the supplied image view. If the photo is already cached, 460 * it is displayed immediately. Otherwise a request is sent to load the photo 461 * from the database. 462 */ 463 public abstract void loadThumbnail(ImageView view, long photoId, boolean darkTheme, 464 boolean isCircular, DefaultImageRequest defaultImageRequest, 465 DefaultImageProvider defaultProvider); 466 467 /** 468 * Calls {@link #loadThumbnail(ImageView, long, boolean, DefaultImageRequest, 469 * DefaultImageProvider)} using the {@link DefaultImageProvider} {@link #DEFAULT_AVATAR}. 470 */ 471 public final void loadThumbnail(ImageView view, long photoId, boolean darkTheme, 472 boolean isCircular, DefaultImageRequest defaultImageRequest) { 473 loadThumbnail(view, photoId, darkTheme, isCircular, defaultImageRequest, DEFAULT_AVATAR); 474 } 475 476 477 /** 478 * Load photo into the supplied image view. If the photo is already cached, 479 * it is displayed immediately. Otherwise a request is sent to load the photo 480 * from the location specified by the URI. 481 * 482 * @param view The target view 483 * @param photoUri The uri of the photo to load 484 * @param requestedExtent Specifies an approximate Max(width, height) of the targetView. 485 * This is useful if the source image can be a lot bigger that the target, so that the decoding 486 * is done using efficient sampling. If requestedExtent is specified, no sampling of the image 487 * is performed 488 * @param darkTheme Whether the background is dark. This is used for default avatars 489 * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default 490 * letter tile avatar should be drawn. 491 * @param defaultProvider The provider of default avatars (this is used if photoUri doesn't 492 * refer to an existing image) 493 */ 494 public abstract void loadPhoto(ImageView view, Uri photoUri, int requestedExtent, 495 boolean darkTheme, boolean isCircular, DefaultImageRequest defaultImageRequest, 496 DefaultImageProvider defaultProvider); 497 498 /** 499 * Calls {@link #loadPhoto(ImageView, Uri, int, boolean, DefaultImageRequest, 500 * DefaultImageProvider)} with {@link #DEFAULT_AVATAR} and {@code null} display names and 501 * lookup keys. 502 * 503 * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default 504 * letter tile avatar should be drawn. 505 */ 506 public final void loadPhoto(ImageView view, Uri photoUri, int requestedExtent, 507 boolean darkTheme, boolean isCircular, DefaultImageRequest defaultImageRequest) { 508 loadPhoto(view, photoUri, requestedExtent, darkTheme, isCircular, 509 defaultImageRequest, DEFAULT_AVATAR); 510 } 511 512 /** 513 * Calls {@link #loadPhoto(ImageView, Uri, boolean, boolean, DefaultImageRequest, 514 * DefaultImageProvider)} with {@link #DEFAULT_AVATAR} and with the assumption, that 515 * the image is a thumbnail. 516 * 517 * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default 518 * letter tile avatar should be drawn. 519 */ 520 public final void loadDirectoryPhoto(ImageView view, Uri photoUri, boolean darkTheme, 521 boolean isCircular, DefaultImageRequest defaultImageRequest) { 522 loadPhoto(view, photoUri, -1, darkTheme, isCircular, defaultImageRequest, DEFAULT_AVATAR); 523 } 524 525 /** 526 * Remove photo from the supplied image view. This also cancels current pending load request 527 * inside this photo manager. 528 */ 529 public abstract void removePhoto(ImageView view); 530 531 /** 532 * Cancels all pending requests to load photos asynchronously. 533 */ 534 public abstract void cancelPendingRequests(View fragmentRootView); 535 536 /** 537 * Temporarily stops loading photos from the database. 538 */ 539 public abstract void pause(); 540 541 /** 542 * Resumes loading photos from the database. 543 */ 544 public abstract void resume(); 545 546 /** 547 * Marks all cached photos for reloading. We can continue using cache but should 548 * also make sure the photos haven't changed in the background and notify the views 549 * if so. 550 */ 551 public abstract void refreshCache(); 552 553 /** 554 * Stores the given bitmap directly in the LRU bitmap cache. 555 * @param photoUri The URI of the photo (for future requests). 556 * @param bitmap The bitmap. 557 * @param photoBytes The bytes that were parsed to create the bitmap. 558 */ 559 public abstract void cacheBitmap(Uri photoUri, Bitmap bitmap, byte[] photoBytes); 560 561 /** 562 * Initiates a background process that over time will fill up cache with 563 * preload photos. 564 */ 565 public abstract void preloadPhotosInBackground(); 566 567 // ComponentCallbacks2 568 @Override 569 public void onConfigurationChanged(Configuration newConfig) { 570 } 571 572 // ComponentCallbacks2 573 @Override 574 public void onLowMemory() { 575 } 576 577 // ComponentCallbacks2 578 @Override 579 public void onTrimMemory(int level) { 580 } 581 } 582 583 class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback { 584 private static final String LOADER_THREAD_NAME = "ContactPhotoLoader"; 585 586 private static final int FADE_TRANSITION_DURATION = 200; 587 588 /** 589 * Type of message sent by the UI thread to itself to indicate that some photos 590 * need to be loaded. 591 */ 592 private static final int MESSAGE_REQUEST_LOADING = 1; 593 594 /** 595 * Type of message sent by the loader thread to indicate that some photos have 596 * been loaded. 597 */ 598 private static final int MESSAGE_PHOTOS_LOADED = 2; 599 600 private static final String[] EMPTY_STRING_ARRAY = new String[0]; 601 602 private static final String[] COLUMNS = new String[] { Photo._ID, Photo.PHOTO }; 603 604 /** 605 * Dummy object used to indicate that a bitmap for a given key could not be stored in the 606 * cache. 607 */ 608 private static final BitmapHolder BITMAP_UNAVAILABLE; 609 610 static { 611 BITMAP_UNAVAILABLE = new BitmapHolder(new byte[0], 0); 612 BITMAP_UNAVAILABLE.bitmapRef = new SoftReference<Bitmap>(null); 613 } 614 615 /** 616 * Maintains the state of a particular photo. 617 */ 618 private static class BitmapHolder { 619 final byte[] bytes; 620 final int originalSmallerExtent; 621 622 volatile boolean fresh; 623 Bitmap bitmap; 624 Reference<Bitmap> bitmapRef; 625 int decodedSampleSize; 626 627 public BitmapHolder(byte[] bytes, int originalSmallerExtent) { 628 this.bytes = bytes; 629 this.fresh = true; 630 this.originalSmallerExtent = originalSmallerExtent; 631 } 632 } 633 634 private final Context mContext; 635 636 /** 637 * An LRU cache for bitmap holders. The cache contains bytes for photos just 638 * as they come from the database. Each holder has a soft reference to the 639 * actual bitmap. 640 */ 641 private final LruCache<Object, BitmapHolder> mBitmapHolderCache; 642 643 /** 644 * {@code true} if ALL entries in {@link #mBitmapHolderCache} are NOT fresh. 645 */ 646 private volatile boolean mBitmapHolderCacheAllUnfresh = true; 647 648 /** 649 * Cache size threshold at which bitmaps will not be preloaded. 650 */ 651 private final int mBitmapHolderCacheRedZoneBytes; 652 653 /** 654 * Level 2 LRU cache for bitmaps. This is a smaller cache that holds 655 * the most recently used bitmaps to save time on decoding 656 * them from bytes (the bytes are stored in {@link #mBitmapHolderCache}. 657 */ 658 private final LruCache<Object, Bitmap> mBitmapCache; 659 660 /** 661 * A map from ImageView to the corresponding photo ID or uri, encapsulated in a request. 662 * The request may swapped out before the photo loading request is started. 663 */ 664 private final ConcurrentHashMap<ImageView, Request> mPendingRequests = 665 new ConcurrentHashMap<ImageView, Request>(); 666 667 /** 668 * Handler for messages sent to the UI thread. 669 */ 670 private final Handler mMainThreadHandler = new Handler(this); 671 672 /** 673 * Thread responsible for loading photos from the database. Created upon 674 * the first request. 675 */ 676 private LoaderThread mLoaderThread; 677 678 /** 679 * A gate to make sure we only send one instance of MESSAGE_PHOTOS_NEEDED at a time. 680 */ 681 private boolean mLoadingRequested; 682 683 /** 684 * Flag indicating if the image loading is paused. 685 */ 686 private boolean mPaused; 687 688 /** Cache size for {@link #mBitmapHolderCache} for devices with "large" RAM. */ 689 private static final int HOLDER_CACHE_SIZE = 2000000; 690 691 /** Cache size for {@link #mBitmapCache} for devices with "large" RAM. */ 692 private static final int BITMAP_CACHE_SIZE = 36864 * 48; // 1728K 693 694 /** Height/width of a thumbnail image */ 695 private static int mThumbnailSize; 696 697 /** For debug: How many times we had to reload cached photo for a stale entry */ 698 private final AtomicInteger mStaleCacheOverwrite = new AtomicInteger(); 699 700 /** For debug: How many times we had to reload cached photo for a fresh entry. Should be 0. */ 701 private final AtomicInteger mFreshCacheOverwrite = new AtomicInteger(); 702 703 /** 704 * The user agent string to use when loading URI based photos. 705 */ 706 private String mUserAgent; 707 708 public ContactPhotoManagerImpl(Context context) { 709 mContext = context; 710 711 final ActivityManager am = ((ActivityManager) context.getSystemService( 712 Context.ACTIVITY_SERVICE)); 713 714 final float cacheSizeAdjustment = (am.isLowRamDevice()) ? 0.5f : 1.0f; 715 716 final int bitmapCacheSize = (int) (cacheSizeAdjustment * BITMAP_CACHE_SIZE); 717 mBitmapCache = new LruCache<Object, Bitmap>(bitmapCacheSize) { 718 @Override protected int sizeOf(Object key, Bitmap value) { 719 return value.getByteCount(); 720 } 721 722 @Override protected void entryRemoved( 723 boolean evicted, Object key, Bitmap oldValue, Bitmap newValue) { 724 if (DEBUG) dumpStats(); 725 } 726 }; 727 final int holderCacheSize = (int) (cacheSizeAdjustment * HOLDER_CACHE_SIZE); 728 mBitmapHolderCache = new LruCache<Object, BitmapHolder>(holderCacheSize) { 729 @Override protected int sizeOf(Object key, BitmapHolder value) { 730 return value.bytes != null ? value.bytes.length : 0; 731 } 732 733 @Override protected void entryRemoved( 734 boolean evicted, Object key, BitmapHolder oldValue, BitmapHolder newValue) { 735 if (DEBUG) dumpStats(); 736 } 737 }; 738 mBitmapHolderCacheRedZoneBytes = (int) (holderCacheSize * 0.75); 739 Log.i(TAG, "Cache adj: " + cacheSizeAdjustment); 740 if (DEBUG) { 741 Log.d(TAG, "Cache size: " + btk(mBitmapHolderCache.maxSize()) 742 + " + " + btk(mBitmapCache.maxSize())); 743 } 744 745 mThumbnailSize = context.getResources().getDimensionPixelSize( 746 R.dimen.contact_browser_list_item_photo_size); 747 748 // Get a user agent string to use for URI photo requests. 749 mUserAgent = UserAgentGenerator.getUserAgent(context); 750 if (mUserAgent == null) { 751 mUserAgent = ""; 752 } 753 } 754 755 /** Converts bytes to K bytes, rounding up. Used only for debug log. */ 756 private static String btk(int bytes) { 757 return ((bytes + 1023) / 1024) + "K"; 758 } 759 760 private static final int safeDiv(int dividend, int divisor) { 761 return (divisor == 0) ? 0 : (dividend / divisor); 762 } 763 764 /** 765 * Dump cache stats on logcat. 766 */ 767 private void dumpStats() { 768 if (!DEBUG) return; 769 { 770 int numHolders = 0; 771 int rawBytes = 0; 772 int bitmapBytes = 0; 773 int numBitmaps = 0; 774 for (BitmapHolder h : mBitmapHolderCache.snapshot().values()) { 775 numHolders++; 776 if (h.bytes != null) { 777 rawBytes += h.bytes.length; 778 } 779 Bitmap b = h.bitmapRef != null ? h.bitmapRef.get() : null; 780 if (b != null) { 781 numBitmaps++; 782 bitmapBytes += b.getByteCount(); 783 } 784 } 785 Log.d(TAG, "L1: " + btk(rawBytes) + " + " + btk(bitmapBytes) + " = " 786 + btk(rawBytes + bitmapBytes) + ", " + numHolders + " holders, " 787 + numBitmaps + " bitmaps, avg: " 788 + btk(safeDiv(rawBytes, numHolders)) 789 + "," + btk(safeDiv(bitmapBytes,numBitmaps))); 790 Log.d(TAG, "L1 Stats: " + mBitmapHolderCache.toString() 791 + ", overwrite: fresh=" + mFreshCacheOverwrite.get() 792 + " stale=" + mStaleCacheOverwrite.get()); 793 } 794 795 { 796 int numBitmaps = 0; 797 int bitmapBytes = 0; 798 for (Bitmap b : mBitmapCache.snapshot().values()) { 799 numBitmaps++; 800 bitmapBytes += b.getByteCount(); 801 } 802 Log.d(TAG, "L2: " + btk(bitmapBytes) + ", " + numBitmaps + " bitmaps" 803 + ", avg: " + btk(safeDiv(bitmapBytes, numBitmaps))); 804 // We don't get from L2 cache, so L2 stats is meaningless. 805 } 806 } 807 808 @Override 809 public void onTrimMemory(int level) { 810 if (DEBUG) Log.d(TAG, "onTrimMemory: " + level); 811 if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) { 812 // Clear the caches. Note all pending requests will be removed too. 813 clear(); 814 } 815 } 816 817 @Override 818 public void preloadPhotosInBackground() { 819 ensureLoaderThread(); 820 mLoaderThread.requestPreloading(); 821 } 822 823 @Override 824 public void loadThumbnail(ImageView view, long photoId, boolean darkTheme, boolean isCircular, 825 DefaultImageRequest defaultImageRequest, DefaultImageProvider defaultProvider) { 826 if (photoId == 0) { 827 // No photo is needed 828 defaultProvider.applyDefaultImage(view, -1, darkTheme, defaultImageRequest); 829 mPendingRequests.remove(view); 830 } else { 831 if (DEBUG) Log.d(TAG, "loadPhoto request: " + photoId); 832 loadPhotoByIdOrUri(view, Request.createFromThumbnailId(photoId, darkTheme, isCircular, 833 defaultProvider)); 834 } 835 } 836 837 @Override 838 public void loadPhoto(ImageView view, Uri photoUri, int requestedExtent, boolean darkTheme, 839 boolean isCircular, DefaultImageRequest defaultImageRequest, 840 DefaultImageProvider defaultProvider) { 841 if (photoUri == null) { 842 // No photo is needed 843 defaultProvider.applyDefaultImage(view, requestedExtent, darkTheme, 844 defaultImageRequest); 845 mPendingRequests.remove(view); 846 } else { 847 if (DEBUG) Log.d(TAG, "loadPhoto request: " + photoUri); 848 if (isDefaultImageUri(photoUri)) { 849 createAndApplyDefaultImageForUri(view, photoUri, requestedExtent, darkTheme, 850 isCircular, defaultProvider); 851 } else { 852 loadPhotoByIdOrUri(view, Request.createFromUri(photoUri, requestedExtent, 853 darkTheme, isCircular, defaultProvider)); 854 } 855 } 856 } 857 858 private void createAndApplyDefaultImageForUri(ImageView view, Uri uri, int requestedExtent, 859 boolean darkTheme, boolean isCircular, DefaultImageProvider defaultProvider) { 860 DefaultImageRequest request = getDefaultImageRequestFromUri(uri); 861 request.isCircular = isCircular; 862 defaultProvider.applyDefaultImage(view, requestedExtent, darkTheme, request); 863 } 864 865 private void loadPhotoByIdOrUri(ImageView view, Request request) { 866 boolean loaded = loadCachedPhoto(view, request, false); 867 if (loaded) { 868 mPendingRequests.remove(view); 869 } else { 870 mPendingRequests.put(view, request); 871 if (!mPaused) { 872 // Send a request to start loading photos 873 requestLoading(); 874 } 875 } 876 } 877 878 @Override 879 public void removePhoto(ImageView view) { 880 view.setImageDrawable(null); 881 mPendingRequests.remove(view); 882 } 883 884 885 /** 886 * Cancels pending requests to load photos asynchronously for views inside 887 * {@param fragmentRootView}. If {@param fragmentRootView} is null, cancels all requests. 888 */ 889 @Override 890 public void cancelPendingRequests(View fragmentRootView) { 891 if (fragmentRootView == null) { 892 mPendingRequests.clear(); 893 return; 894 } 895 final Iterator<Entry<ImageView, Request>> iterator = mPendingRequests.entrySet().iterator(); 896 while (iterator.hasNext()) { 897 final ImageView imageView = iterator.next().getKey(); 898 // If an ImageView is orphaned (currently scrap) or a child of fragmentRootView, then 899 // we can safely remove its request. 900 if (imageView.getParent() == null || isChildView(fragmentRootView, imageView)) { 901 iterator.remove(); 902 } 903 } 904 } 905 906 private static boolean isChildView(View parent, View potentialChild) { 907 return potentialChild.getParent() != null && (potentialChild.getParent() == parent || ( 908 potentialChild.getParent() instanceof ViewGroup && isChildView(parent, 909 (ViewGroup) potentialChild.getParent()))); 910 } 911 912 @Override 913 public void refreshCache() { 914 if (mBitmapHolderCacheAllUnfresh) { 915 if (DEBUG) Log.d(TAG, "refreshCache -- no fresh entries."); 916 return; 917 } 918 if (DEBUG) Log.d(TAG, "refreshCache"); 919 mBitmapHolderCacheAllUnfresh = true; 920 for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) { 921 if (holder != BITMAP_UNAVAILABLE) { 922 holder.fresh = false; 923 } 924 } 925 } 926 927 /** 928 * Checks if the photo is present in cache. If so, sets the photo on the view. 929 * 930 * @return false if the photo needs to be (re)loaded from the provider. 931 */ 932 private boolean loadCachedPhoto(ImageView view, Request request, boolean fadeIn) { 933 BitmapHolder holder = mBitmapHolderCache.get(request.getKey()); 934 if (holder == null) { 935 // The bitmap has not been loaded ==> show default avatar 936 request.applyDefaultImage(view, request.mIsCircular); 937 return false; 938 } 939 940 if (holder.bytes == null) { 941 request.applyDefaultImage(view, request.mIsCircular); 942 return holder.fresh; 943 } 944 945 Bitmap cachedBitmap = holder.bitmapRef == null ? null : holder.bitmapRef.get(); 946 if (cachedBitmap == null) { 947 if (holder.bytes.length < 8 * 1024) { 948 // Small thumbnails are usually quick to inflate. Let's do that on the UI thread 949 inflateBitmap(holder, request.getRequestedExtent()); 950 cachedBitmap = holder.bitmap; 951 if (cachedBitmap == null) return false; 952 } else { 953 // This is bigger data. Let's send that back to the Loader so that we can 954 // inflate this in the background 955 request.applyDefaultImage(view, request.mIsCircular); 956 return false; 957 } 958 } 959 960 final Drawable previousDrawable = view.getDrawable(); 961 if (fadeIn && previousDrawable != null) { 962 final Drawable[] layers = new Drawable[2]; 963 // Prevent cascade of TransitionDrawables. 964 if (previousDrawable instanceof TransitionDrawable) { 965 final TransitionDrawable previousTransitionDrawable = 966 (TransitionDrawable) previousDrawable; 967 layers[0] = previousTransitionDrawable.getDrawable( 968 previousTransitionDrawable.getNumberOfLayers() - 1); 969 } else { 970 layers[0] = previousDrawable; 971 } 972 layers[1] = getDrawableForBitmap(mContext.getResources(), cachedBitmap, request); 973 TransitionDrawable drawable = new TransitionDrawable(layers); 974 view.setImageDrawable(drawable); 975 drawable.startTransition(FADE_TRANSITION_DURATION); 976 } else { 977 view.setImageDrawable( 978 getDrawableForBitmap(mContext.getResources(), cachedBitmap, request)); 979 } 980 981 // Put the bitmap in the LRU cache. But only do this for images that are small enough 982 // (we require that at least six of those can be cached at the same time) 983 if (cachedBitmap.getByteCount() < mBitmapCache.maxSize() / 6) { 984 mBitmapCache.put(request.getKey(), cachedBitmap); 985 } 986 987 // Soften the reference 988 holder.bitmap = null; 989 990 return holder.fresh; 991 } 992 993 /** 994 * Given a bitmap, returns a drawable that is configured to display the bitmap based on the 995 * specified request. 996 */ 997 private Drawable getDrawableForBitmap(Resources resources, Bitmap bitmap, Request request) { 998 if (request.mIsCircular) { 999 final RoundedBitmapDrawable drawable = 1000 RoundedBitmapDrawableFactory.create(resources, bitmap); 1001 drawable.setAntiAlias(true); 1002 drawable.setCornerRadius(bitmap.getHeight() / 2); 1003 return drawable; 1004 } else { 1005 return new BitmapDrawable(resources, bitmap); 1006 } 1007 } 1008 1009 /** 1010 * If necessary, decodes bytes stored in the holder to Bitmap. As long as the 1011 * bitmap is held either by {@link #mBitmapCache} or by a soft reference in 1012 * the holder, it will not be necessary to decode the bitmap. 1013 */ 1014 private static void inflateBitmap(BitmapHolder holder, int requestedExtent) { 1015 final int sampleSize = 1016 BitmapUtil.findOptimalSampleSize(holder.originalSmallerExtent, requestedExtent); 1017 byte[] bytes = holder.bytes; 1018 if (bytes == null || bytes.length == 0) { 1019 return; 1020 } 1021 1022 if (sampleSize == holder.decodedSampleSize) { 1023 // Check the soft reference. If will be retained if the bitmap is also 1024 // in the LRU cache, so we don't need to check the LRU cache explicitly. 1025 if (holder.bitmapRef != null) { 1026 holder.bitmap = holder.bitmapRef.get(); 1027 if (holder.bitmap != null) { 1028 return; 1029 } 1030 } 1031 } 1032 1033 try { 1034 Bitmap bitmap = BitmapUtil.decodeBitmapFromBytes(bytes, sampleSize); 1035 1036 // TODO: As a temporary workaround while framework support is being added to 1037 // clip non-square bitmaps into a perfect circle, manually crop the bitmap into 1038 // into a square if it will be displayed as a thumbnail so that it can be cropped 1039 // into a circle. 1040 final int height = bitmap.getHeight(); 1041 final int width = bitmap.getWidth(); 1042 1043 // The smaller dimension of a scaled bitmap can range from anywhere from 0 to just 1044 // below twice the length of a thumbnail image due to the way we calculate the optimal 1045 // sample size. 1046 if (height != width && Math.min(height, width) <= mThumbnailSize * 2) { 1047 final int dimension = Math.min(height, width); 1048 bitmap = ThumbnailUtils.extractThumbnail(bitmap, dimension, dimension); 1049 } 1050 // make bitmap mutable and draw size onto it 1051 if (DEBUG_SIZES) { 1052 Bitmap original = bitmap; 1053 bitmap = bitmap.copy(bitmap.getConfig(), true); 1054 original.recycle(); 1055 Canvas canvas = new Canvas(bitmap); 1056 Paint paint = new Paint(); 1057 paint.setTextSize(16); 1058 paint.setColor(Color.BLUE); 1059 paint.setStyle(Style.FILL); 1060 canvas.drawRect(0.0f, 0.0f, 50.0f, 20.0f, paint); 1061 paint.setColor(Color.WHITE); 1062 paint.setAntiAlias(true); 1063 canvas.drawText(bitmap.getWidth() + "/" + sampleSize, 0, 15, paint); 1064 } 1065 1066 holder.decodedSampleSize = sampleSize; 1067 holder.bitmap = bitmap; 1068 holder.bitmapRef = new SoftReference<Bitmap>(bitmap); 1069 if (DEBUG) { 1070 Log.d(TAG, "inflateBitmap " + btk(bytes.length) + " -> " 1071 + bitmap.getWidth() + "x" + bitmap.getHeight() 1072 + ", " + btk(bitmap.getByteCount())); 1073 } 1074 } catch (OutOfMemoryError e) { 1075 // Do nothing - the photo will appear to be missing 1076 } 1077 } 1078 1079 public void clear() { 1080 if (DEBUG) Log.d(TAG, "clear"); 1081 mPendingRequests.clear(); 1082 mBitmapHolderCache.evictAll(); 1083 mBitmapCache.evictAll(); 1084 } 1085 1086 @Override 1087 public void pause() { 1088 mPaused = true; 1089 } 1090 1091 @Override 1092 public void resume() { 1093 mPaused = false; 1094 if (DEBUG) dumpStats(); 1095 if (!mPendingRequests.isEmpty()) { 1096 requestLoading(); 1097 } 1098 } 1099 1100 /** 1101 * Sends a message to this thread itself to start loading images. If the current 1102 * view contains multiple image views, all of those image views will get a chance 1103 * to request their respective photos before any of those requests are executed. 1104 * This allows us to load images in bulk. 1105 */ 1106 private void requestLoading() { 1107 if (!mLoadingRequested) { 1108 mLoadingRequested = true; 1109 mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING); 1110 } 1111 } 1112 1113 /** 1114 * Processes requests on the main thread. 1115 */ 1116 @Override 1117 public boolean handleMessage(Message msg) { 1118 switch (msg.what) { 1119 case MESSAGE_REQUEST_LOADING: { 1120 mLoadingRequested = false; 1121 if (!mPaused) { 1122 ensureLoaderThread(); 1123 mLoaderThread.requestLoading(); 1124 } 1125 return true; 1126 } 1127 1128 case MESSAGE_PHOTOS_LOADED: { 1129 if (!mPaused) { 1130 processLoadedImages(); 1131 } 1132 if (DEBUG) dumpStats(); 1133 return true; 1134 } 1135 } 1136 return false; 1137 } 1138 1139 public void ensureLoaderThread() { 1140 if (mLoaderThread == null) { 1141 mLoaderThread = new LoaderThread(mContext.getContentResolver()); 1142 mLoaderThread.start(); 1143 } 1144 } 1145 1146 /** 1147 * Goes over pending loading requests and displays loaded photos. If some of the 1148 * photos still haven't been loaded, sends another request for image loading. 1149 */ 1150 private void processLoadedImages() { 1151 final Iterator<Entry<ImageView, Request>> iterator = mPendingRequests.entrySet().iterator(); 1152 while (iterator.hasNext()) { 1153 final Entry<ImageView, Request> entry = iterator.next(); 1154 // TODO: Temporarily disable contact photo fading in, until issues with 1155 // RoundedBitmapDrawables overlapping the default image drawables are resolved. 1156 final boolean loaded = loadCachedPhoto(entry.getKey(), entry.getValue(), false); 1157 if (loaded) { 1158 iterator.remove(); 1159 } 1160 } 1161 1162 softenCache(); 1163 1164 if (!mPendingRequests.isEmpty()) { 1165 requestLoading(); 1166 } 1167 } 1168 1169 /** 1170 * Removes strong references to loaded bitmaps to allow them to be garbage collected 1171 * if needed. Some of the bitmaps will still be retained by {@link #mBitmapCache}. 1172 */ 1173 private void softenCache() { 1174 for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) { 1175 holder.bitmap = null; 1176 } 1177 } 1178 1179 /** 1180 * Stores the supplied bitmap in cache. 1181 */ 1182 private void cacheBitmap(Object key, byte[] bytes, boolean preloading, int requestedExtent) { 1183 if (DEBUG) { 1184 BitmapHolder prev = mBitmapHolderCache.get(key); 1185 if (prev != null && prev.bytes != null) { 1186 Log.d(TAG, "Overwriting cache: key=" + key + (prev.fresh ? " FRESH" : " stale")); 1187 if (prev.fresh) { 1188 mFreshCacheOverwrite.incrementAndGet(); 1189 } else { 1190 mStaleCacheOverwrite.incrementAndGet(); 1191 } 1192 } 1193 Log.d(TAG, "Caching data: key=" + key + ", " + 1194 (bytes == null ? "<null>" : btk(bytes.length))); 1195 } 1196 BitmapHolder holder = new BitmapHolder(bytes, 1197 bytes == null ? -1 : BitmapUtil.getSmallerExtentFromBytes(bytes)); 1198 1199 // Unless this image is being preloaded, decode it right away while 1200 // we are still on the background thread. 1201 if (!preloading) { 1202 inflateBitmap(holder, requestedExtent); 1203 } 1204 1205 if (bytes != null) { 1206 mBitmapHolderCache.put(key, holder); 1207 if (mBitmapHolderCache.get(key) != holder) { 1208 Log.w(TAG, "Bitmap too big to fit in cache."); 1209 mBitmapHolderCache.put(key, BITMAP_UNAVAILABLE); 1210 } 1211 } else { 1212 mBitmapHolderCache.put(key, BITMAP_UNAVAILABLE); 1213 } 1214 1215 mBitmapHolderCacheAllUnfresh = false; 1216 } 1217 1218 @Override 1219 public void cacheBitmap(Uri photoUri, Bitmap bitmap, byte[] photoBytes) { 1220 final int smallerExtent = Math.min(bitmap.getWidth(), bitmap.getHeight()); 1221 // We can pretend here that the extent of the photo was the size that we originally 1222 // requested 1223 Request request = Request.createFromUri(photoUri, smallerExtent, false /* darkTheme */, 1224 false /* isCircular */ , DEFAULT_AVATAR); 1225 BitmapHolder holder = new BitmapHolder(photoBytes, smallerExtent); 1226 holder.bitmapRef = new SoftReference<Bitmap>(bitmap); 1227 mBitmapHolderCache.put(request.getKey(), holder); 1228 mBitmapHolderCacheAllUnfresh = false; 1229 mBitmapCache.put(request.getKey(), bitmap); 1230 } 1231 1232 /** 1233 * Populates an array of photo IDs that need to be loaded. Also decodes bitmaps that we have 1234 * already loaded 1235 */ 1236 private void obtainPhotoIdsAndUrisToLoad(Set<Long> photoIds, 1237 Set<String> photoIdsAsStrings, Set<Request> uris) { 1238 photoIds.clear(); 1239 photoIdsAsStrings.clear(); 1240 uris.clear(); 1241 1242 boolean jpegsDecoded = false; 1243 1244 /* 1245 * Since the call is made from the loader thread, the map could be 1246 * changing during the iteration. That's not really a problem: 1247 * ConcurrentHashMap will allow those changes to happen without throwing 1248 * exceptions. Since we may miss some requests in the situation of 1249 * concurrent change, we will need to check the map again once loading 1250 * is complete. 1251 */ 1252 Iterator<Request> iterator = mPendingRequests.values().iterator(); 1253 while (iterator.hasNext()) { 1254 Request request = iterator.next(); 1255 final BitmapHolder holder = mBitmapHolderCache.get(request.getKey()); 1256 if (holder == BITMAP_UNAVAILABLE) { 1257 continue; 1258 } 1259 if (holder != null && holder.bytes != null && holder.fresh && 1260 (holder.bitmapRef == null || holder.bitmapRef.get() == null)) { 1261 // This was previously loaded but we don't currently have the inflated Bitmap 1262 inflateBitmap(holder, request.getRequestedExtent()); 1263 jpegsDecoded = true; 1264 } else { 1265 if (holder == null || !holder.fresh) { 1266 if (request.isUriRequest()) { 1267 uris.add(request); 1268 } else { 1269 photoIds.add(request.getId()); 1270 photoIdsAsStrings.add(String.valueOf(request.mId)); 1271 } 1272 } 1273 } 1274 } 1275 1276 if (jpegsDecoded) mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED); 1277 } 1278 1279 /** 1280 * The thread that performs loading of photos from the database. 1281 */ 1282 private class LoaderThread extends HandlerThread implements Callback { 1283 private static final int BUFFER_SIZE = 1024*16; 1284 private static final int MESSAGE_PRELOAD_PHOTOS = 0; 1285 private static final int MESSAGE_LOAD_PHOTOS = 1; 1286 1287 /** 1288 * A pause between preload batches that yields to the UI thread. 1289 */ 1290 private static final int PHOTO_PRELOAD_DELAY = 1000; 1291 1292 /** 1293 * Number of photos to preload per batch. 1294 */ 1295 private static final int PRELOAD_BATCH = 25; 1296 1297 /** 1298 * Maximum number of photos to preload. If the cache size is 2Mb and 1299 * the expected average size of a photo is 4kb, then this number should be 2Mb/4kb = 500. 1300 */ 1301 private static final int MAX_PHOTOS_TO_PRELOAD = 100; 1302 1303 private final ContentResolver mResolver; 1304 private final StringBuilder mStringBuilder = new StringBuilder(); 1305 private final Set<Long> mPhotoIds = Sets.newHashSet(); 1306 private final Set<String> mPhotoIdsAsStrings = Sets.newHashSet(); 1307 private final Set<Request> mPhotoUris = Sets.newHashSet(); 1308 private final List<Long> mPreloadPhotoIds = Lists.newArrayList(); 1309 1310 private Handler mLoaderThreadHandler; 1311 private byte mBuffer[]; 1312 1313 private static final int PRELOAD_STATUS_NOT_STARTED = 0; 1314 private static final int PRELOAD_STATUS_IN_PROGRESS = 1; 1315 private static final int PRELOAD_STATUS_DONE = 2; 1316 1317 private int mPreloadStatus = PRELOAD_STATUS_NOT_STARTED; 1318 1319 public LoaderThread(ContentResolver resolver) { 1320 super(LOADER_THREAD_NAME); 1321 mResolver = resolver; 1322 } 1323 1324 public void ensureHandler() { 1325 if (mLoaderThreadHandler == null) { 1326 mLoaderThreadHandler = new Handler(getLooper(), this); 1327 } 1328 } 1329 1330 /** 1331 * Kicks off preloading of the next batch of photos on the background thread. 1332 * Preloading will happen after a delay: we want to yield to the UI thread 1333 * as much as possible. 1334 * <p> 1335 * If preloading is already complete, does nothing. 1336 */ 1337 public void requestPreloading() { 1338 if (mPreloadStatus == PRELOAD_STATUS_DONE) { 1339 return; 1340 } 1341 1342 ensureHandler(); 1343 if (mLoaderThreadHandler.hasMessages(MESSAGE_LOAD_PHOTOS)) { 1344 return; 1345 } 1346 1347 mLoaderThreadHandler.sendEmptyMessageDelayed( 1348 MESSAGE_PRELOAD_PHOTOS, PHOTO_PRELOAD_DELAY); 1349 } 1350 1351 /** 1352 * Sends a message to this thread to load requested photos. Cancels a preloading 1353 * request, if any: we don't want preloading to impede loading of the photos 1354 * we need to display now. 1355 */ 1356 public void requestLoading() { 1357 ensureHandler(); 1358 mLoaderThreadHandler.removeMessages(MESSAGE_PRELOAD_PHOTOS); 1359 mLoaderThreadHandler.sendEmptyMessage(MESSAGE_LOAD_PHOTOS); 1360 } 1361 1362 /** 1363 * Receives the above message, loads photos and then sends a message 1364 * to the main thread to process them. 1365 */ 1366 @Override 1367 public boolean handleMessage(Message msg) { 1368 switch (msg.what) { 1369 case MESSAGE_PRELOAD_PHOTOS: 1370 preloadPhotosInBackground(); 1371 break; 1372 case MESSAGE_LOAD_PHOTOS: 1373 loadPhotosInBackground(); 1374 break; 1375 } 1376 return true; 1377 } 1378 1379 /** 1380 * The first time it is called, figures out which photos need to be preloaded. 1381 * Each subsequent call preloads the next batch of photos and requests 1382 * another cycle of preloading after a delay. The whole process ends when 1383 * we either run out of photos to preload or fill up cache. 1384 */ 1385 private void preloadPhotosInBackground() { 1386 if (mPreloadStatus == PRELOAD_STATUS_DONE) { 1387 return; 1388 } 1389 1390 if (mPreloadStatus == PRELOAD_STATUS_NOT_STARTED) { 1391 queryPhotosForPreload(); 1392 if (mPreloadPhotoIds.isEmpty()) { 1393 mPreloadStatus = PRELOAD_STATUS_DONE; 1394 } else { 1395 mPreloadStatus = PRELOAD_STATUS_IN_PROGRESS; 1396 } 1397 requestPreloading(); 1398 return; 1399 } 1400 1401 if (mBitmapHolderCache.size() > mBitmapHolderCacheRedZoneBytes) { 1402 mPreloadStatus = PRELOAD_STATUS_DONE; 1403 return; 1404 } 1405 1406 mPhotoIds.clear(); 1407 mPhotoIdsAsStrings.clear(); 1408 1409 int count = 0; 1410 int preloadSize = mPreloadPhotoIds.size(); 1411 while(preloadSize > 0 && mPhotoIds.size() < PRELOAD_BATCH) { 1412 preloadSize--; 1413 count++; 1414 Long photoId = mPreloadPhotoIds.get(preloadSize); 1415 mPhotoIds.add(photoId); 1416 mPhotoIdsAsStrings.add(photoId.toString()); 1417 mPreloadPhotoIds.remove(preloadSize); 1418 } 1419 1420 loadThumbnails(true); 1421 1422 if (preloadSize == 0) { 1423 mPreloadStatus = PRELOAD_STATUS_DONE; 1424 } 1425 1426 Log.v(TAG, "Preloaded " + count + " photos. Cached bytes: " 1427 + mBitmapHolderCache.size()); 1428 1429 requestPreloading(); 1430 } 1431 1432 private void queryPhotosForPreload() { 1433 Cursor cursor = null; 1434 try { 1435 Uri uri = Contacts.CONTENT_URI.buildUpon().appendQueryParameter( 1436 ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT)) 1437 .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, 1438 String.valueOf(MAX_PHOTOS_TO_PRELOAD)) 1439 .build(); 1440 cursor = mResolver.query(uri, new String[] { Contacts.PHOTO_ID }, 1441 Contacts.PHOTO_ID + " NOT NULL AND " + Contacts.PHOTO_ID + "!=0", 1442 null, 1443 Contacts.STARRED + " DESC, " + Contacts.LAST_TIME_CONTACTED + " DESC"); 1444 1445 if (cursor != null) { 1446 while (cursor.moveToNext()) { 1447 // Insert them in reverse order, because we will be taking 1448 // them from the end of the list for loading. 1449 mPreloadPhotoIds.add(0, cursor.getLong(0)); 1450 } 1451 } 1452 } finally { 1453 if (cursor != null) { 1454 cursor.close(); 1455 } 1456 } 1457 } 1458 1459 private void loadPhotosInBackground() { 1460 if (!PermissionsUtil.hasPermission(mContext, 1461 android.Manifest.permission.READ_CONTACTS)) { 1462 return; 1463 } 1464 obtainPhotoIdsAndUrisToLoad(mPhotoIds, mPhotoIdsAsStrings, mPhotoUris); 1465 loadThumbnails(false); 1466 loadUriBasedPhotos(); 1467 requestPreloading(); 1468 } 1469 1470 /** Loads thumbnail photos with ids */ 1471 private void loadThumbnails(boolean preloading) { 1472 if (mPhotoIds.isEmpty()) { 1473 return; 1474 } 1475 1476 // Remove loaded photos from the preload queue: we don't want 1477 // the preloading process to load them again. 1478 if (!preloading && mPreloadStatus == PRELOAD_STATUS_IN_PROGRESS) { 1479 for (Long id : mPhotoIds) { 1480 mPreloadPhotoIds.remove(id); 1481 } 1482 if (mPreloadPhotoIds.isEmpty()) { 1483 mPreloadStatus = PRELOAD_STATUS_DONE; 1484 } 1485 } 1486 1487 mStringBuilder.setLength(0); 1488 mStringBuilder.append(Photo._ID + " IN("); 1489 for (int i = 0; i < mPhotoIds.size(); i++) { 1490 if (i != 0) { 1491 mStringBuilder.append(','); 1492 } 1493 mStringBuilder.append('?'); 1494 } 1495 mStringBuilder.append(')'); 1496 1497 Cursor cursor = null; 1498 try { 1499 if (DEBUG) Log.d(TAG, "Loading " + TextUtils.join(",", mPhotoIdsAsStrings)); 1500 cursor = mResolver.query(Data.CONTENT_URI, 1501 COLUMNS, 1502 mStringBuilder.toString(), 1503 mPhotoIdsAsStrings.toArray(EMPTY_STRING_ARRAY), 1504 null); 1505 1506 if (cursor != null) { 1507 while (cursor.moveToNext()) { 1508 Long id = cursor.getLong(0); 1509 byte[] bytes = cursor.getBlob(1); 1510 cacheBitmap(id, bytes, preloading, -1); 1511 mPhotoIds.remove(id); 1512 } 1513 } 1514 } finally { 1515 if (cursor != null) { 1516 cursor.close(); 1517 } 1518 } 1519 1520 // Remaining photos were not found in the contacts database (but might be in profile). 1521 for (Long id : mPhotoIds) { 1522 if (ContactsContract.isProfileId(id)) { 1523 Cursor profileCursor = null; 1524 try { 1525 profileCursor = mResolver.query( 1526 ContentUris.withAppendedId(Data.CONTENT_URI, id), 1527 COLUMNS, null, null, null); 1528 if (profileCursor != null && profileCursor.moveToFirst()) { 1529 cacheBitmap(profileCursor.getLong(0), profileCursor.getBlob(1), 1530 preloading, -1); 1531 } else { 1532 // Couldn't load a photo this way either. 1533 cacheBitmap(id, null, preloading, -1); 1534 } 1535 } finally { 1536 if (profileCursor != null) { 1537 profileCursor.close(); 1538 } 1539 } 1540 } else { 1541 // Not a profile photo and not found - mark the cache accordingly 1542 cacheBitmap(id, null, preloading, -1); 1543 } 1544 } 1545 1546 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED); 1547 } 1548 1549 /** 1550 * Loads photos referenced with Uris. Those can be remote thumbnails 1551 * (from directory searches), display photos etc 1552 */ 1553 private void loadUriBasedPhotos() { 1554 for (Request uriRequest : mPhotoUris) { 1555 // Keep the original URI and use this to key into the cache. Failure to do so will 1556 // result in an image being continually reloaded into cache if the original URI 1557 // has a contact type encodedFragment (eg nearby places business photo URLs). 1558 Uri originalUri = uriRequest.getUri(); 1559 1560 // Strip off the "contact type" we added to the URI to ensure it was identifiable as 1561 // a business photo -- there is no need to pass this on to the server. 1562 Uri uri = ContactPhotoManager.removeContactType(originalUri); 1563 1564 if (mBuffer == null) { 1565 mBuffer = new byte[BUFFER_SIZE]; 1566 } 1567 try { 1568 if (DEBUG) Log.d(TAG, "Loading " + uri); 1569 final String scheme = uri.getScheme(); 1570 InputStream is = null; 1571 if (scheme.equals("http") || scheme.equals("https")) { 1572 TrafficStats.setThreadStatsTag(TrafficStatsTags.CONTACT_PHOTO_DOWNLOAD_TAG); 1573 final HttpURLConnection connection = 1574 (HttpURLConnection) new URL(uri.toString()).openConnection(); 1575 1576 // Include the user agent if it is specified. 1577 if (!TextUtils.isEmpty(mUserAgent)) { 1578 connection.setRequestProperty("User-Agent", mUserAgent); 1579 } 1580 try { 1581 is = connection.getInputStream(); 1582 } catch (IOException e) { 1583 connection.disconnect(); 1584 is = null; 1585 } 1586 TrafficStats.clearThreadStatsTag(); 1587 } else { 1588 is = mResolver.openInputStream(uri); 1589 } 1590 if (is != null) { 1591 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 1592 try { 1593 int size; 1594 while ((size = is.read(mBuffer)) != -1) { 1595 baos.write(mBuffer, 0, size); 1596 } 1597 } finally { 1598 is.close(); 1599 } 1600 cacheBitmap(originalUri, baos.toByteArray(), false, 1601 uriRequest.getRequestedExtent()); 1602 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED); 1603 } else { 1604 Log.v(TAG, "Cannot load photo " + uri); 1605 cacheBitmap(originalUri, null, false, uriRequest.getRequestedExtent()); 1606 } 1607 } catch (final Exception | OutOfMemoryError ex) { 1608 Log.v(TAG, "Cannot load photo " + uri, ex); 1609 cacheBitmap(originalUri, null, false, uriRequest.getRequestedExtent()); 1610 } 1611 } 1612 } 1613 } 1614 1615 /** 1616 * A holder for either a Uri or an id and a flag whether this was requested for the dark or 1617 * light theme 1618 */ 1619 private static final class Request { 1620 private final long mId; 1621 private final Uri mUri; 1622 private final boolean mDarkTheme; 1623 private final int mRequestedExtent; 1624 private final DefaultImageProvider mDefaultProvider; 1625 /** 1626 * Whether or not the contact photo is to be displayed as a circle 1627 */ 1628 private final boolean mIsCircular; 1629 1630 private Request(long id, Uri uri, int requestedExtent, boolean darkTheme, 1631 boolean isCircular, DefaultImageProvider defaultProvider) { 1632 mId = id; 1633 mUri = uri; 1634 mDarkTheme = darkTheme; 1635 mIsCircular = isCircular; 1636 mRequestedExtent = requestedExtent; 1637 mDefaultProvider = defaultProvider; 1638 } 1639 1640 public static Request createFromThumbnailId(long id, boolean darkTheme, boolean isCircular, 1641 DefaultImageProvider defaultProvider) { 1642 return new Request(id, null /* no URI */, -1, darkTheme, isCircular, defaultProvider); 1643 } 1644 1645 public static Request createFromUri(Uri uri, int requestedExtent, boolean darkTheme, 1646 boolean isCircular, DefaultImageProvider defaultProvider) { 1647 return new Request(0 /* no ID */, uri, requestedExtent, darkTheme, isCircular, 1648 defaultProvider); 1649 } 1650 1651 public boolean isUriRequest() { 1652 return mUri != null; 1653 } 1654 1655 public Uri getUri() { 1656 return mUri; 1657 } 1658 1659 public long getId() { 1660 return mId; 1661 } 1662 1663 public int getRequestedExtent() { 1664 return mRequestedExtent; 1665 } 1666 1667 @Override 1668 public int hashCode() { 1669 final int prime = 31; 1670 int result = 1; 1671 result = prime * result + (int) (mId ^ (mId >>> 32)); 1672 result = prime * result + mRequestedExtent; 1673 result = prime * result + ((mUri == null) ? 0 : mUri.hashCode()); 1674 return result; 1675 } 1676 1677 @Override 1678 public boolean equals(Object obj) { 1679 if (this == obj) return true; 1680 if (obj == null) return false; 1681 if (getClass() != obj.getClass()) return false; 1682 final Request that = (Request) obj; 1683 if (mId != that.mId) return false; 1684 if (mRequestedExtent != that.mRequestedExtent) return false; 1685 if (!UriUtils.areEqual(mUri, that.mUri)) return false; 1686 // Don't compare equality of mDarkTheme because it is only used in the default contact 1687 // photo case. When the contact does have a photo, the contact photo is the same 1688 // regardless of mDarkTheme, so we shouldn't need to put the photo request on the queue 1689 // twice. 1690 return true; 1691 } 1692 1693 public Object getKey() { 1694 return mUri == null ? mId : mUri; 1695 } 1696 1697 /** 1698 * Applies the default image to the current view. If the request is URI-based, looks for 1699 * the contact type encoded fragment to determine if this is a request for a business photo, 1700 * in which case we will load the default business photo. 1701 * 1702 * @param view The current image view to apply the image to. 1703 * @param isCircular Whether the image is circular or not. 1704 */ 1705 public void applyDefaultImage(ImageView view, boolean isCircular) { 1706 final DefaultImageRequest request; 1707 1708 if (isCircular) { 1709 request = ContactPhotoManager.isBusinessContactUri(mUri) 1710 ? DefaultImageRequest.EMPTY_CIRCULAR_BUSINESS_IMAGE_REQUEST 1711 : DefaultImageRequest.EMPTY_CIRCULAR_DEFAULT_IMAGE_REQUEST; 1712 } else { 1713 request = ContactPhotoManager.isBusinessContactUri(mUri) 1714 ? DefaultImageRequest.EMPTY_DEFAULT_BUSINESS_IMAGE_REQUEST 1715 : DefaultImageRequest.EMPTY_DEFAULT_IMAGE_REQUEST; 1716 } 1717 mDefaultProvider.applyDefaultImage(view, mRequestedExtent, mDarkTheme, request); 1718 } 1719 } 1720 } 1721