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