blob: 4e1afaae574c73e5c95963c49dcf432f638fc685 [file] [log] [blame]
Chiao Cheng86618002012-10-16 13:21:10 -07001/*
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
Gary Mai0a49afa2016-12-05 15:53:58 -080017package com.android.contacts;
Chiao Cheng86618002012-10-16 13:21:10 -070018
Yorke Leea2412222013-10-23 15:14:53 -070019import android.app.ActivityManager;
Chiao Cheng86618002012-10-16 13:21:10 -070020import android.content.ComponentCallbacks2;
21import android.content.ContentResolver;
22import android.content.ContentUris;
23import android.content.Context;
24import android.content.res.Configuration;
25import android.content.res.Resources;
26import android.database.Cursor;
27import android.graphics.Bitmap;
28import android.graphics.Canvas;
29import android.graphics.Color;
30import android.graphics.Paint;
31import android.graphics.Paint.Style;
32import android.graphics.drawable.BitmapDrawable;
33import android.graphics.drawable.ColorDrawable;
34import android.graphics.drawable.Drawable;
35import android.graphics.drawable.TransitionDrawable;
Yorke Lee3b124482014-05-06 11:54:23 -070036import android.media.ThumbnailUtils;
Yorke Lee64953802015-05-28 09:43:01 -070037import android.net.TrafficStats;
Chiao Cheng86618002012-10-16 13:21:10 -070038import android.net.Uri;
Yorke Lee9df5e192014-02-12 14:58:25 -080039import android.net.Uri.Builder;
Chiao Cheng86618002012-10-16 13:21:10 -070040import android.os.Handler;
41import android.os.Handler.Callback;
42import android.os.HandlerThread;
43import android.os.Message;
44import android.provider.ContactsContract;
45import android.provider.ContactsContract.Contacts;
46import android.provider.ContactsContract.Contacts.Photo;
47import android.provider.ContactsContract.Data;
48import android.provider.ContactsContract.Directory;
Aravind Sreekumar71212852018-04-06 15:47:45 -070049import androidx.core.graphics.drawable.RoundedBitmapDrawable;
50import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
Chiao Cheng86618002012-10-16 13:21:10 -070051import android.text.TextUtils;
52import android.util.Log;
53import android.util.LruCache;
Brian Attwellb92b6372014-07-21 23:39:35 -070054import android.view.View;
55import android.view.ViewGroup;
Chiao Cheng86618002012-10-16 13:21:10 -070056import android.widget.ImageView;
57
Gary Mai69c182a2016-12-05 13:07:03 -080058import com.android.contacts.lettertiles.LetterTileDrawable;
59import com.android.contacts.util.BitmapUtil;
60import com.android.contacts.util.PermissionsUtil;
61import com.android.contacts.util.TrafficStatsTags;
62import com.android.contacts.util.UriUtils;
Walter Jange837ae32016-08-15 12:50:37 -070063import com.android.contactsbind.util.UserAgentGenerator;
Yorke Lee9df5e192014-02-12 14:58:25 -080064
Andrew Lee56933f62015-03-27 15:16:57 -070065import com.google.common.annotations.VisibleForTesting;
Chiao Cheng86618002012-10-16 13:21:10 -070066import com.google.common.collect.Lists;
67import com.google.common.collect.Sets;
68
69import java.io.ByteArrayOutputStream;
Tyler Gunn2005cbc2015-01-29 10:25:02 -080070import java.io.IOException;
Chiao Cheng86618002012-10-16 13:21:10 -070071import java.io.InputStream;
72import java.lang.ref.Reference;
73import java.lang.ref.SoftReference;
Tyler Gunn2005cbc2015-01-29 10:25:02 -080074import java.net.HttpURLConnection;
Jay Shraunerd77fb672013-09-10 12:02:02 -070075import java.net.URL;
Chiao Cheng86618002012-10-16 13:21:10 -070076import java.util.Iterator;
77import java.util.List;
Wenyi Wang3991d452016-04-12 10:57:42 -070078import java.util.Map.Entry;
Chiao Cheng86618002012-10-16 13:21:10 -070079import java.util.Set;
80import java.util.concurrent.ConcurrentHashMap;
81import java.util.concurrent.atomic.AtomicInteger;
82
83/**
84 * Asynchronously loads contact photos and maintains a cache of photos.
85 */
86public 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
Yorke Lee9df5e192014-02-12 14:58:25 -080091 /** 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
Yorke Leec4a2a232014-04-28 17:53:42 -0700101 public static final boolean IS_CIRCULAR_DEFAULT = false;
102
Yorke Lee9df5e192014-02-12 14:58:25 -0800103 /** 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";
Yorke Leec4a2a232014-04-28 17:53:42 -0700109 private static final String IS_CIRCULAR_PARAM_KEY = "is_circular";
Yorke Lee9df5e192014-02-12 14:58:25 -0800110 private static final String DEFAULT_IMAGE_URI_SCHEME = "defaultimage";
111 private static final Uri DEFAULT_IMAGE_URI = Uri.parse(DEFAULT_IMAGE_URI_SCHEME + "://");
Chiao Cheng86618002012-10-16 13:21:10 -0700112
Yorke Lee9df5e192014-02-12 14:58:25 -0800113 // 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;
Chiao Cheng86618002012-10-16 13:21:10 -0700116
Andrew Lee56933f62015-03-27 15:16:57 -0700117 private static ContactPhotoManager sInstance;
118
Yorke Lee9df5e192014-02-12 14:58:25 -0800119 /**
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(
Yingren Wang1f9f8512019-02-22 14:16:35 +0800131 resources, null);
Yorke Lee9df5e192014-02-12 14:58:25 -0800132 }
133 return sDefaultLetterAvatar;
134 }
135 return LetterTileDefaultImageProvider.getDefaultImageForContact(resources,
Yingren Wang1f9f8512019-02-22 14:16:35 +0800136 defaultImageRequest);
Chiao Cheng86618002012-10-16 13:21:10 -0700137 }
138
Yorke Lee9df5e192014-02-12 14:58:25 -0800139 /**
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 }
Yorke Leec4a2a232014-04-28 17:53:42 -0700171 if (request.isCircular != IS_CIRCULAR_DEFAULT) {
172 builder.appendQueryParameter(IS_CIRCULAR_PARAM_KEY,
173 String.valueOf(request.isCircular));
174 }
Yorke Lee9df5e192014-02-12 14:58:25 -0800175
176 }
177 return builder.build();
178 }
179
Tyler Gunn48c391d2014-05-09 16:08:21 -0700180 /**
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
Yorke Lee9df5e192014-02-12 14:58:25 -0800227 protected static DefaultImageRequest getDefaultImageRequestFromUri(Uri uri) {
228 final DefaultImageRequest request = new DefaultImageRequest(
229 uri.getQueryParameter(DISPLAY_NAME_PARAM_KEY),
Yorke Leec4a2a232014-04-28 17:53:42 -0700230 uri.getQueryParameter(IDENTIFIER_PARAM_KEY), false);
Yorke Lee9df5e192014-02-12 14:58:25 -0800231 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 }
Yorke Leec4a2a232014-04-28 17:53:42 -0700246
247 String isCircular = uri.getQueryParameter(IS_CIRCULAR_PARAM_KEY);
248 if (!TextUtils.isEmpty(isCircular)) {
249 request.isCircular = Boolean.valueOf(isCircular);
250 }
Yorke Lee9df5e192014-02-12 14:58:25 -0800251 } 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;
Yorke Leec4a2a232014-04-28 17:53:42 -0700273
Yorke Lee9df5e192014-02-12 14:58:25 -0800274 /**
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;
Yorke Leec4a2a232014-04-28 17:53:42 -0700281
Yorke Lee9df5e192014-02-12 14:58:25 -0800282 /**
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;
Yorke Leec4a2a232014-04-28 17:53:42 -0700292
Yorke Lee9df5e192014-02-12 14:58:25 -0800293 /**
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;
Yorke Leec4a2a232014-04-28 17:53:42 -0700298
Yorke Lee9df5e192014-02-12 14:58:25 -0800299 /**
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
Yorke Leec4a2a232014-04-28 17:53:42 -0700313 /**
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 /**
Tyler Gunn48c391d2014-05-09 16:08:21 -0700325 * 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 /**
Yorke Leec4a2a232014-04-28 17:53:42 -0700332 * 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
Tyler Gunn48c391d2014-05-09 16:08:21 -0700338 /**
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
Yorke Lee9df5e192014-02-12 14:58:25 -0800345 public DefaultImageRequest() {
346 }
347
Yorke Leec4a2a232014-04-28 17:53:42 -0700348 public DefaultImageRequest(String displayName, String identifier, boolean isCircular) {
349 this(displayName, identifier, TYPE_DEFAULT, SCALE_DEFAULT, OFFSET_DEFAULT, isCircular);
Yorke Lee9df5e192014-02-12 14:58:25 -0800350 }
351
352 public DefaultImageRequest(String displayName, String identifier, int contactType,
Yorke Leec4a2a232014-04-28 17:53:42 -0700353 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) {
Yorke Lee9df5e192014-02-12 14:58:25 -0800359 this.displayName = displayName;
360 this.identifier = identifier;
361 this.contactType = contactType;
362 this.scale = scale;
363 this.offset = offset;
Yorke Leec4a2a232014-04-28 17:53:42 -0700364 this.isCircular = isCircular;
Yorke Lee9df5e192014-02-12 14:58:25 -0800365 }
Chiao Cheng86618002012-10-16 13:21:10 -0700366 }
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
Yorke Lee9df5e192014-02-12 14:58:25 -0800372 *
373 * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a
374 * default letter tile avatar should be drawn.
Chiao Cheng86618002012-10-16 13:21:10 -0700375 */
Yorke Lee9df5e192014-02-12 14:58:25 -0800376 public abstract void applyDefaultImage(ImageView view, int extent, boolean darkTheme,
377 DefaultImageRequest defaultImageRequest);
Chiao Cheng86618002012-10-16 13:21:10 -0700378 }
379
Yorke Lee9df5e192014-02-12 14:58:25 -0800380 /**
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 {
Chiao Cheng86618002012-10-16 13:21:10 -0700386 @Override
Yorke Lee9df5e192014-02-12 14:58:25 -0800387 public void applyDefaultImage(ImageView view, int extent, boolean darkTheme,
388 DefaultImageRequest defaultImageRequest) {
389 final Drawable drawable = getDefaultImageForContact(view.getResources(),
Yingren Wang1f9f8512019-02-22 14:16:35 +0800390 defaultImageRequest);
Yorke Lee9df5e192014-02-12 14:58:25 -0800391 view.setImageDrawable(drawable);
392 }
393
394 public static Drawable getDefaultImageForContact(Resources resources,
Yingren Wang1f9f8512019-02-22 14:16:35 +0800395 DefaultImageRequest defaultImageRequest) {
396 final LetterTileDrawable drawable = new LetterTileDrawable(resources);
Yorke Lee9df5e192014-02-12 14:58:25 -0800397 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)) {
Ta-wei Yen75bd9342015-11-05 12:58:43 -0800403 drawable.setLetterAndColorFromContactDetails(null,
404 defaultImageRequest.displayName);
Yorke Lee9df5e192014-02-12 14:58:25 -0800405 } else {
Ta-wei Yen75bd9342015-11-05 12:58:43 -0800406 drawable.setLetterAndColorFromContactDetails(defaultImageRequest.displayName,
Yorke Lee9df5e192014-02-12 14:58:25 -0800407 defaultImageRequest.identifier);
408 }
409 drawable.setContactType(defaultImageRequest.contactType);
410 drawable.setScale(defaultImageRequest.scale);
411 drawable.setOffset(defaultImageRequest.offset);
Yorke Leec4a2a232014-04-28 17:53:42 -0700412 drawable.setIsCircular(defaultImageRequest.isCircular);
Yorke Lee9df5e192014-02-12 14:58:25 -0800413 }
414 return drawable;
Chiao Cheng86618002012-10-16 13:21:10 -0700415 }
416 }
417
418 private static class BlankDefaultImageProvider extends DefaultImageProvider {
419 private static Drawable sDrawable;
420
421 @Override
Yorke Lee9df5e192014-02-12 14:58:25 -0800422 public void applyDefaultImage(ImageView view, int extent, boolean darkTheme,
423 DefaultImageRequest defaultImageRequest) {
Chiao Cheng86618002012-10-16 13:21:10 -0700424 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
Yorke Lee9df5e192014-02-12 14:58:25 -0800433 public static DefaultImageProvider DEFAULT_AVATAR = new LetterTileDefaultImageProvider();
Chiao Cheng86618002012-10-16 13:21:10 -0700434
435 public static final DefaultImageProvider DEFAULT_BLANK = new BlankDefaultImageProvider();
436
437 public static ContactPhotoManager getInstance(Context context) {
Andrew Lee56933f62015-03-27 15:16:57 -0700438 if (sInstance == null) {
439 Context applicationContext = context.getApplicationContext();
440 sInstance = createContactPhotoManager(applicationContext);
441 applicationContext.registerComponentCallbacks(sInstance);
Yorke Lee3a52e632015-05-21 11:04:14 -0700442 if (PermissionsUtil.hasContactsPermissions(context)) {
443 sInstance.preloadPhotosInBackground();
444 }
Chiao Cheng86618002012-10-16 13:21:10 -0700445 }
Andrew Lee56933f62015-03-27 15:16:57 -0700446 return sInstance;
Chiao Cheng86618002012-10-16 13:21:10 -0700447 }
448
449 public static synchronized ContactPhotoManager createContactPhotoManager(Context context) {
450 return new ContactPhotoManagerImpl(context);
451 }
452
Andrew Lee56933f62015-03-27 15:16:57 -0700453 @VisibleForTesting
454 public static void injectContactPhotoManagerForTesting(ContactPhotoManager photoManager) {
455 sInstance = photoManager;
456 }
457
Chiao Cheng86618002012-10-16 13:21:10 -0700458 /**
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 */
Yingren Wang1f9f8512019-02-22 14:16:35 +0800463 public abstract void loadThumbnail(ImageView view, long photoId, boolean darkTheme,
464 boolean isCircular, DefaultImageRequest defaultImageRequest,
Yorke Leec4a2a232014-04-28 17:53:42 -0700465 DefaultImageProvider defaultProvider);
Chiao Cheng86618002012-10-16 13:21:10 -0700466
467 /**
Yorke Lee9df5e192014-02-12 14:58:25 -0800468 * Calls {@link #loadThumbnail(ImageView, long, boolean, DefaultImageRequest,
469 * DefaultImageProvider)} using the {@link DefaultImageProvider} {@link #DEFAULT_AVATAR}.
Gary Mai3a533282016-11-08 19:02:26 +0000470 */
Yorke Lee9df5e192014-02-12 14:58:25 -0800471 public final void loadThumbnail(ImageView view, long photoId, boolean darkTheme,
Yorke Leec4a2a232014-04-28 17:53:42 -0700472 boolean isCircular, DefaultImageRequest defaultImageRequest) {
Yingren Wang1f9f8512019-02-22 14:16:35 +0800473 loadThumbnail(view, photoId, darkTheme, isCircular, defaultImageRequest, DEFAULT_AVATAR);
Chiao Cheng86618002012-10-16 13:21:10 -0700474 }
475
476 /**
477 * Load photo into the supplied image view. If the photo is already cached,
478 * it is displayed immediately. Otherwise a request is sent to load the photo
479 * from the location specified by the URI.
Yorke Lee9df5e192014-02-12 14:58:25 -0800480 *
Chiao Cheng86618002012-10-16 13:21:10 -0700481 * @param view The target view
482 * @param photoUri The uri of the photo to load
483 * @param requestedExtent Specifies an approximate Max(width, height) of the targetView.
484 * This is useful if the source image can be a lot bigger that the target, so that the decoding
485 * is done using efficient sampling. If requestedExtent is specified, no sampling of the image
486 * is performed
487 * @param darkTheme Whether the background is dark. This is used for default avatars
Yorke Lee9df5e192014-02-12 14:58:25 -0800488 * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default
489 * letter tile avatar should be drawn.
Chiao Cheng86618002012-10-16 13:21:10 -0700490 * @param defaultProvider The provider of default avatars (this is used if photoUri doesn't
491 * refer to an existing image)
492 */
Yingren Wang1f9f8512019-02-22 14:16:35 +0800493 public abstract void loadPhoto(ImageView view, Uri photoUri, int requestedExtent,
494 boolean darkTheme, boolean isCircular, DefaultImageRequest defaultImageRequest,
495 DefaultImageProvider defaultProvider);
Chiao Cheng86618002012-10-16 13:21:10 -0700496
497 /**
Yorke Lee9df5e192014-02-12 14:58:25 -0800498 * Calls {@link #loadPhoto(ImageView, Uri, int, boolean, DefaultImageRequest,
499 * DefaultImageProvider)} with {@link #DEFAULT_AVATAR} and {@code null} display names and
500 * lookup keys.
501 *
502 * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default
503 * letter tile avatar should be drawn.
Chiao Cheng86618002012-10-16 13:21:10 -0700504 */
505 public final void loadPhoto(ImageView view, Uri photoUri, int requestedExtent,
Yorke Leec4a2a232014-04-28 17:53:42 -0700506 boolean darkTheme, boolean isCircular, DefaultImageRequest defaultImageRequest) {
Yingren Wang1f9f8512019-02-22 14:16:35 +0800507 loadPhoto(view, photoUri, requestedExtent, darkTheme, isCircular,
Yorke Leec4a2a232014-04-28 17:53:42 -0700508 defaultImageRequest, DEFAULT_AVATAR);
Chiao Cheng86618002012-10-16 13:21:10 -0700509 }
510
511 /**
Yorke Lee9df5e192014-02-12 14:58:25 -0800512 * Calls {@link #loadPhoto(ImageView, Uri, boolean, boolean, DefaultImageRequest,
513 * DefaultImageProvider)} with {@link #DEFAULT_AVATAR} and with the assumption, that
514 * the image is a thumbnail.
515 *
516 * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default
517 * letter tile avatar should be drawn.
Chiao Cheng86618002012-10-16 13:21:10 -0700518 */
Yorke Lee9df5e192014-02-12 14:58:25 -0800519 public final void loadDirectoryPhoto(ImageView view, Uri photoUri, boolean darkTheme,
Yorke Leec4a2a232014-04-28 17:53:42 -0700520 boolean isCircular, DefaultImageRequest defaultImageRequest) {
Yingren Wang1f9f8512019-02-22 14:16:35 +0800521 loadPhoto(view, photoUri, -1, darkTheme, isCircular, defaultImageRequest, DEFAULT_AVATAR);
Chiao Cheng86618002012-10-16 13:21:10 -0700522 }
523
524 /**
525 * Remove photo from the supplied image view. This also cancels current pending load request
526 * inside this photo manager.
527 */
528 public abstract void removePhoto(ImageView view);
529
530 /**
Tyler Gunn232df2f2014-04-15 15:30:58 -0700531 * Cancels all pending requests to load photos asynchronously.
532 */
Brian Attwellb92b6372014-07-21 23:39:35 -0700533 public abstract void cancelPendingRequests(View fragmentRootView);
Tyler Gunn232df2f2014-04-15 15:30:58 -0700534
535 /**
Chiao Cheng86618002012-10-16 13:21:10 -0700536 * Temporarily stops loading photos from the database.
537 */
538 public abstract void pause();
539
540 /**
541 * Resumes loading photos from the database.
542 */
543 public abstract void resume();
544
545 /**
546 * Marks all cached photos for reloading. We can continue using cache but should
547 * also make sure the photos haven't changed in the background and notify the views
548 * if so.
549 */
550 public abstract void refreshCache();
551
552 /**
553 * Stores the given bitmap directly in the LRU bitmap cache.
554 * @param photoUri The URI of the photo (for future requests).
555 * @param bitmap The bitmap.
556 * @param photoBytes The bytes that were parsed to create the bitmap.
557 */
558 public abstract void cacheBitmap(Uri photoUri, Bitmap bitmap, byte[] photoBytes);
559
560 /**
561 * Initiates a background process that over time will fill up cache with
562 * preload photos.
563 */
564 public abstract void preloadPhotosInBackground();
565
566 // ComponentCallbacks2
567 @Override
568 public void onConfigurationChanged(Configuration newConfig) {
569 }
570
571 // ComponentCallbacks2
572 @Override
573 public void onLowMemory() {
574 }
575
576 // ComponentCallbacks2
577 @Override
578 public void onTrimMemory(int level) {
579 }
580}
581
582class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
583 private static final String LOADER_THREAD_NAME = "ContactPhotoLoader";
584
585 private static final int FADE_TRANSITION_DURATION = 200;
586
587 /**
588 * Type of message sent by the UI thread to itself to indicate that some photos
589 * need to be loaded.
590 */
591 private static final int MESSAGE_REQUEST_LOADING = 1;
592
593 /**
594 * Type of message sent by the loader thread to indicate that some photos have
595 * been loaded.
596 */
597 private static final int MESSAGE_PHOTOS_LOADED = 2;
598
599 private static final String[] EMPTY_STRING_ARRAY = new String[0];
600
601 private static final String[] COLUMNS = new String[] { Photo._ID, Photo.PHOTO };
602
603 /**
Yorke Leee0c90b52015-05-27 12:14:57 -0700604 * Dummy object used to indicate that a bitmap for a given key could not be stored in the
605 * cache.
606 */
607 private static final BitmapHolder BITMAP_UNAVAILABLE;
608
609 static {
610 BITMAP_UNAVAILABLE = new BitmapHolder(new byte[0], 0);
611 BITMAP_UNAVAILABLE.bitmapRef = new SoftReference<Bitmap>(null);
612 }
613
614 /**
Chiao Cheng86618002012-10-16 13:21:10 -0700615 * Maintains the state of a particular photo.
616 */
617 private static class BitmapHolder {
618 final byte[] bytes;
619 final int originalSmallerExtent;
620
621 volatile boolean fresh;
622 Bitmap bitmap;
623 Reference<Bitmap> bitmapRef;
624 int decodedSampleSize;
625
626 public BitmapHolder(byte[] bytes, int originalSmallerExtent) {
627 this.bytes = bytes;
628 this.fresh = true;
629 this.originalSmallerExtent = originalSmallerExtent;
630 }
631 }
632
633 private final Context mContext;
634
635 /**
636 * An LRU cache for bitmap holders. The cache contains bytes for photos just
637 * as they come from the database. Each holder has a soft reference to the
638 * actual bitmap.
639 */
640 private final LruCache<Object, BitmapHolder> mBitmapHolderCache;
641
642 /**
643 * {@code true} if ALL entries in {@link #mBitmapHolderCache} are NOT fresh.
644 */
645 private volatile boolean mBitmapHolderCacheAllUnfresh = true;
646
647 /**
648 * Cache size threshold at which bitmaps will not be preloaded.
649 */
650 private final int mBitmapHolderCacheRedZoneBytes;
651
652 /**
653 * Level 2 LRU cache for bitmaps. This is a smaller cache that holds
654 * the most recently used bitmaps to save time on decoding
655 * them from bytes (the bytes are stored in {@link #mBitmapHolderCache}.
656 */
657 private final LruCache<Object, Bitmap> mBitmapCache;
658
659 /**
660 * A map from ImageView to the corresponding photo ID or uri, encapsulated in a request.
661 * The request may swapped out before the photo loading request is started.
662 */
663 private final ConcurrentHashMap<ImageView, Request> mPendingRequests =
664 new ConcurrentHashMap<ImageView, Request>();
665
666 /**
667 * Handler for messages sent to the UI thread.
668 */
669 private final Handler mMainThreadHandler = new Handler(this);
670
671 /**
672 * Thread responsible for loading photos from the database. Created upon
673 * the first request.
674 */
675 private LoaderThread mLoaderThread;
676
677 /**
678 * A gate to make sure we only send one instance of MESSAGE_PHOTOS_NEEDED at a time.
679 */
680 private boolean mLoadingRequested;
681
682 /**
683 * Flag indicating if the image loading is paused.
684 */
685 private boolean mPaused;
686
687 /** Cache size for {@link #mBitmapHolderCache} for devices with "large" RAM. */
688 private static final int HOLDER_CACHE_SIZE = 2000000;
689
690 /** Cache size for {@link #mBitmapCache} for devices with "large" RAM. */
691 private static final int BITMAP_CACHE_SIZE = 36864 * 48; // 1728K
692
Yorke Lee3b124482014-05-06 11:54:23 -0700693 /** Height/width of a thumbnail image */
694 private static int mThumbnailSize;
695
Chiao Cheng86618002012-10-16 13:21:10 -0700696 /** For debug: How many times we had to reload cached photo for a stale entry */
697 private final AtomicInteger mStaleCacheOverwrite = new AtomicInteger();
698
699 /** For debug: How many times we had to reload cached photo for a fresh entry. Should be 0. */
700 private final AtomicInteger mFreshCacheOverwrite = new AtomicInteger();
701
Tyler Gunn2005cbc2015-01-29 10:25:02 -0800702 /**
703 * The user agent string to use when loading URI based photos.
704 */
705 private String mUserAgent;
706
Chiao Cheng86618002012-10-16 13:21:10 -0700707 public ContactPhotoManagerImpl(Context context) {
708 mContext = context;
709
Yorke Leea2412222013-10-23 15:14:53 -0700710 final ActivityManager am = ((ActivityManager) context.getSystemService(
711 Context.ACTIVITY_SERVICE));
712
713 final float cacheSizeAdjustment = (am.isLowRamDevice()) ? 0.5f : 1.0f;
714
Chiao Cheng86618002012-10-16 13:21:10 -0700715 final int bitmapCacheSize = (int) (cacheSizeAdjustment * BITMAP_CACHE_SIZE);
716 mBitmapCache = new LruCache<Object, Bitmap>(bitmapCacheSize) {
717 @Override protected int sizeOf(Object key, Bitmap value) {
718 return value.getByteCount();
719 }
720
721 @Override protected void entryRemoved(
722 boolean evicted, Object key, Bitmap oldValue, Bitmap newValue) {
723 if (DEBUG) dumpStats();
724 }
725 };
726 final int holderCacheSize = (int) (cacheSizeAdjustment * HOLDER_CACHE_SIZE);
727 mBitmapHolderCache = new LruCache<Object, BitmapHolder>(holderCacheSize) {
728 @Override protected int sizeOf(Object key, BitmapHolder value) {
729 return value.bytes != null ? value.bytes.length : 0;
730 }
731
732 @Override protected void entryRemoved(
733 boolean evicted, Object key, BitmapHolder oldValue, BitmapHolder newValue) {
734 if (DEBUG) dumpStats();
735 }
736 };
737 mBitmapHolderCacheRedZoneBytes = (int) (holderCacheSize * 0.75);
738 Log.i(TAG, "Cache adj: " + cacheSizeAdjustment);
739 if (DEBUG) {
740 Log.d(TAG, "Cache size: " + btk(mBitmapHolderCache.maxSize())
741 + " + " + btk(mBitmapCache.maxSize()));
742 }
Yorke Lee3b124482014-05-06 11:54:23 -0700743
744 mThumbnailSize = context.getResources().getDimensionPixelSize(
745 R.dimen.contact_browser_list_item_photo_size);
Tyler Gunn2005cbc2015-01-29 10:25:02 -0800746
747 // Get a user agent string to use for URI photo requests.
748 mUserAgent = UserAgentGenerator.getUserAgent(context);
749 if (mUserAgent == null) {
750 mUserAgent = "";
751 }
Chiao Cheng86618002012-10-16 13:21:10 -0700752 }
753
754 /** Converts bytes to K bytes, rounding up. Used only for debug log. */
755 private static String btk(int bytes) {
756 return ((bytes + 1023) / 1024) + "K";
757 }
758
759 private static final int safeDiv(int dividend, int divisor) {
760 return (divisor == 0) ? 0 : (dividend / divisor);
761 }
762
763 /**
764 * Dump cache stats on logcat.
765 */
766 private void dumpStats() {
767 if (!DEBUG) return;
768 {
769 int numHolders = 0;
770 int rawBytes = 0;
771 int bitmapBytes = 0;
772 int numBitmaps = 0;
773 for (BitmapHolder h : mBitmapHolderCache.snapshot().values()) {
774 numHolders++;
775 if (h.bytes != null) {
776 rawBytes += h.bytes.length;
777 }
778 Bitmap b = h.bitmapRef != null ? h.bitmapRef.get() : null;
779 if (b != null) {
780 numBitmaps++;
781 bitmapBytes += b.getByteCount();
782 }
783 }
784 Log.d(TAG, "L1: " + btk(rawBytes) + " + " + btk(bitmapBytes) + " = "
785 + btk(rawBytes + bitmapBytes) + ", " + numHolders + " holders, "
786 + numBitmaps + " bitmaps, avg: "
787 + btk(safeDiv(rawBytes, numHolders))
788 + "," + btk(safeDiv(bitmapBytes,numBitmaps)));
789 Log.d(TAG, "L1 Stats: " + mBitmapHolderCache.toString()
790 + ", overwrite: fresh=" + mFreshCacheOverwrite.get()
791 + " stale=" + mStaleCacheOverwrite.get());
792 }
793
794 {
795 int numBitmaps = 0;
796 int bitmapBytes = 0;
797 for (Bitmap b : mBitmapCache.snapshot().values()) {
798 numBitmaps++;
799 bitmapBytes += b.getByteCount();
800 }
801 Log.d(TAG, "L2: " + btk(bitmapBytes) + ", " + numBitmaps + " bitmaps"
802 + ", avg: " + btk(safeDiv(bitmapBytes, numBitmaps)));
803 // We don't get from L2 cache, so L2 stats is meaningless.
804 }
805 }
806
807 @Override
808 public void onTrimMemory(int level) {
809 if (DEBUG) Log.d(TAG, "onTrimMemory: " + level);
810 if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
811 // Clear the caches. Note all pending requests will be removed too.
812 clear();
813 }
814 }
815
816 @Override
817 public void preloadPhotosInBackground() {
818 ensureLoaderThread();
819 mLoaderThread.requestPreloading();
820 }
821
822 @Override
Yingren Wang1f9f8512019-02-22 14:16:35 +0800823 public void loadThumbnail(ImageView view, long photoId, boolean darkTheme, boolean isCircular,
824 DefaultImageRequest defaultImageRequest, DefaultImageProvider defaultProvider) {
Chiao Cheng86618002012-10-16 13:21:10 -0700825 if (photoId == 0) {
826 // No photo is needed
Yingren Wang1f9f8512019-02-22 14:16:35 +0800827 defaultProvider.applyDefaultImage(view, -1, darkTheme, defaultImageRequest);
Chiao Cheng86618002012-10-16 13:21:10 -0700828 mPendingRequests.remove(view);
829 } else {
830 if (DEBUG) Log.d(TAG, "loadPhoto request: " + photoId);
Yorke Leec4a2a232014-04-28 17:53:42 -0700831 loadPhotoByIdOrUri(view, Request.createFromThumbnailId(photoId, darkTheme, isCircular,
Gary Mai3a533282016-11-08 19:02:26 +0000832 defaultProvider, defaultImageRequest));
Chiao Cheng86618002012-10-16 13:21:10 -0700833 }
834 }
835
836 @Override
Yingren Wang1f9f8512019-02-22 14:16:35 +0800837 public void loadPhoto(ImageView view, Uri photoUri, int requestedExtent, boolean darkTheme,
838 boolean isCircular, DefaultImageRequest defaultImageRequest,
Yorke Leec4a2a232014-04-28 17:53:42 -0700839 DefaultImageProvider defaultProvider) {
Chiao Cheng86618002012-10-16 13:21:10 -0700840 if (photoUri == null) {
841 // No photo is needed
Yingren Wang1f9f8512019-02-22 14:16:35 +0800842 defaultProvider.applyDefaultImage(view, requestedExtent, darkTheme,
Yorke Lee9df5e192014-02-12 14:58:25 -0800843 defaultImageRequest);
Chiao Cheng86618002012-10-16 13:21:10 -0700844 mPendingRequests.remove(view);
845 } else {
846 if (DEBUG) Log.d(TAG, "loadPhoto request: " + photoUri);
Yorke Lee9df5e192014-02-12 14:58:25 -0800847 if (isDefaultImageUri(photoUri)) {
Yingren Wang1f9f8512019-02-22 14:16:35 +0800848 createAndApplyDefaultImageForUri(view, photoUri, requestedExtent, darkTheme,
849 isCircular, defaultProvider);
Yorke Lee9df5e192014-02-12 14:58:25 -0800850 } else {
Yorke Lee9df5e192014-02-12 14:58:25 -0800851 loadPhotoByIdOrUri(view, Request.createFromUri(photoUri, requestedExtent,
Gary Maif682a8a2016-10-11 15:28:15 -0700852 darkTheme, isCircular, defaultProvider, defaultImageRequest));
Yorke Lee9df5e192014-02-12 14:58:25 -0800853 }
Chiao Cheng86618002012-10-16 13:21:10 -0700854 }
855 }
856
Yingren Wang1f9f8512019-02-22 14:16:35 +0800857 private void createAndApplyDefaultImageForUri(ImageView view, Uri uri, int requestedExtent,
858 boolean darkTheme, boolean isCircular, DefaultImageProvider defaultProvider) {
Yorke Lee9df5e192014-02-12 14:58:25 -0800859 DefaultImageRequest request = getDefaultImageRequestFromUri(uri);
Yorke Lee8269bb12014-05-19 10:08:46 -0700860 request.isCircular = isCircular;
Yingren Wang1f9f8512019-02-22 14:16:35 +0800861 defaultProvider.applyDefaultImage(view, requestedExtent, darkTheme, request);
Yorke Lee9df5e192014-02-12 14:58:25 -0800862 }
863
Chiao Cheng86618002012-10-16 13:21:10 -0700864 private void loadPhotoByIdOrUri(ImageView view, Request request) {
865 boolean loaded = loadCachedPhoto(view, request, false);
866 if (loaded) {
867 mPendingRequests.remove(view);
868 } else {
869 mPendingRequests.put(view, request);
870 if (!mPaused) {
871 // Send a request to start loading photos
872 requestLoading();
873 }
874 }
875 }
876
877 @Override
878 public void removePhoto(ImageView view) {
879 view.setImageDrawable(null);
880 mPendingRequests.remove(view);
881 }
882
Tyler Gunn232df2f2014-04-15 15:30:58 -0700883
884 /**
Brian Attwellb92b6372014-07-21 23:39:35 -0700885 * Cancels pending requests to load photos asynchronously for views inside
886 * {@param fragmentRootView}. If {@param fragmentRootView} is null, cancels all requests.
Tyler Gunn232df2f2014-04-15 15:30:58 -0700887 */
888 @Override
Brian Attwellb92b6372014-07-21 23:39:35 -0700889 public void cancelPendingRequests(View fragmentRootView) {
890 if (fragmentRootView == null) {
891 mPendingRequests.clear();
892 return;
893 }
Wenyi Wang3991d452016-04-12 10:57:42 -0700894 final Iterator<Entry<ImageView, Request>> iterator = mPendingRequests.entrySet().iterator();
895 while (iterator.hasNext()) {
896 final ImageView imageView = iterator.next().getKey();
Brian Attwellb92b6372014-07-21 23:39:35 -0700897 // If an ImageView is orphaned (currently scrap) or a child of fragmentRootView, then
898 // we can safely remove its request.
899 if (imageView.getParent() == null || isChildView(fragmentRootView, imageView)) {
Wenyi Wang3991d452016-04-12 10:57:42 -0700900 iterator.remove();
Brian Attwellb92b6372014-07-21 23:39:35 -0700901 }
902 }
903 }
904
905 private static boolean isChildView(View parent, View potentialChild) {
906 return potentialChild.getParent() != null && (potentialChild.getParent() == parent || (
907 potentialChild.getParent() instanceof ViewGroup && isChildView(parent,
908 (ViewGroup) potentialChild.getParent())));
Tyler Gunn232df2f2014-04-15 15:30:58 -0700909 }
910
Chiao Cheng86618002012-10-16 13:21:10 -0700911 @Override
912 public void refreshCache() {
913 if (mBitmapHolderCacheAllUnfresh) {
914 if (DEBUG) Log.d(TAG, "refreshCache -- no fresh entries.");
915 return;
916 }
917 if (DEBUG) Log.d(TAG, "refreshCache");
918 mBitmapHolderCacheAllUnfresh = true;
919 for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
Yorke Leee0c90b52015-05-27 12:14:57 -0700920 if (holder != BITMAP_UNAVAILABLE) {
921 holder.fresh = false;
922 }
Chiao Cheng86618002012-10-16 13:21:10 -0700923 }
924 }
925
926 /**
927 * Checks if the photo is present in cache. If so, sets the photo on the view.
928 *
929 * @return false if the photo needs to be (re)loaded from the provider.
930 */
931 private boolean loadCachedPhoto(ImageView view, Request request, boolean fadeIn) {
932 BitmapHolder holder = mBitmapHolderCache.get(request.getKey());
933 if (holder == null) {
934 // The bitmap has not been loaded ==> show default avatar
Yorke Leec4a2a232014-04-28 17:53:42 -0700935 request.applyDefaultImage(view, request.mIsCircular);
Chiao Cheng86618002012-10-16 13:21:10 -0700936 return false;
937 }
938
Gary Maif682a8a2016-10-11 15:28:15 -0700939 if (holder.bytes == null || holder.bytes.length == 0) {
Yorke Leec4a2a232014-04-28 17:53:42 -0700940 request.applyDefaultImage(view, request.mIsCircular);
Chiao Cheng86618002012-10-16 13:21:10 -0700941 return holder.fresh;
942 }
943
944 Bitmap cachedBitmap = holder.bitmapRef == null ? null : holder.bitmapRef.get();
945 if (cachedBitmap == null) {
946 if (holder.bytes.length < 8 * 1024) {
947 // Small thumbnails are usually quick to inflate. Let's do that on the UI thread
948 inflateBitmap(holder, request.getRequestedExtent());
949 cachedBitmap = holder.bitmap;
950 if (cachedBitmap == null) return false;
951 } else {
952 // This is bigger data. Let's send that back to the Loader so that we can
953 // inflate this in the background
Yorke Leec4a2a232014-04-28 17:53:42 -0700954 request.applyDefaultImage(view, request.mIsCircular);
Chiao Cheng86618002012-10-16 13:21:10 -0700955 return false;
956 }
957 }
958
959 final Drawable previousDrawable = view.getDrawable();
960 if (fadeIn && previousDrawable != null) {
961 final Drawable[] layers = new Drawable[2];
962 // Prevent cascade of TransitionDrawables.
963 if (previousDrawable instanceof TransitionDrawable) {
964 final TransitionDrawable previousTransitionDrawable =
965 (TransitionDrawable) previousDrawable;
966 layers[0] = previousTransitionDrawable.getDrawable(
967 previousTransitionDrawable.getNumberOfLayers() - 1);
968 } else {
969 layers[0] = previousDrawable;
970 }
Yorke Leec4a2a232014-04-28 17:53:42 -0700971 layers[1] = getDrawableForBitmap(mContext.getResources(), cachedBitmap, request);
Chiao Cheng86618002012-10-16 13:21:10 -0700972 TransitionDrawable drawable = new TransitionDrawable(layers);
973 view.setImageDrawable(drawable);
974 drawable.startTransition(FADE_TRANSITION_DURATION);
975 } else {
Yorke Leec4a2a232014-04-28 17:53:42 -0700976 view.setImageDrawable(
977 getDrawableForBitmap(mContext.getResources(), cachedBitmap, request));
Chiao Cheng86618002012-10-16 13:21:10 -0700978 }
979
980 // Put the bitmap in the LRU cache. But only do this for images that are small enough
981 // (we require that at least six of those can be cached at the same time)
982 if (cachedBitmap.getByteCount() < mBitmapCache.maxSize() / 6) {
983 mBitmapCache.put(request.getKey(), cachedBitmap);
984 }
985
986 // Soften the reference
987 holder.bitmap = null;
988
989 return holder.fresh;
990 }
991
992 /**
Yorke Leec4a2a232014-04-28 17:53:42 -0700993 * Given a bitmap, returns a drawable that is configured to display the bitmap based on the
994 * specified request.
995 */
996 private Drawable getDrawableForBitmap(Resources resources, Bitmap bitmap, Request request) {
997 if (request.mIsCircular) {
998 final RoundedBitmapDrawable drawable =
Chris Craik93fb4352014-08-05 18:51:49 -0700999 RoundedBitmapDrawableFactory.create(resources, bitmap);
Yorke Leec4a2a232014-04-28 17:53:42 -07001000 drawable.setAntiAlias(true);
1001 drawable.setCornerRadius(bitmap.getHeight() / 2);
1002 return drawable;
1003 } else {
1004 return new BitmapDrawable(resources, bitmap);
1005 }
1006 }
1007
1008 /**
Chiao Cheng86618002012-10-16 13:21:10 -07001009 * If necessary, decodes bytes stored in the holder to Bitmap. As long as the
1010 * bitmap is held either by {@link #mBitmapCache} or by a soft reference in
1011 * the holder, it will not be necessary to decode the bitmap.
1012 */
1013 private static void inflateBitmap(BitmapHolder holder, int requestedExtent) {
1014 final int sampleSize =
1015 BitmapUtil.findOptimalSampleSize(holder.originalSmallerExtent, requestedExtent);
1016 byte[] bytes = holder.bytes;
1017 if (bytes == null || bytes.length == 0) {
1018 return;
1019 }
1020
1021 if (sampleSize == holder.decodedSampleSize) {
1022 // Check the soft reference. If will be retained if the bitmap is also
1023 // in the LRU cache, so we don't need to check the LRU cache explicitly.
1024 if (holder.bitmapRef != null) {
1025 holder.bitmap = holder.bitmapRef.get();
1026 if (holder.bitmap != null) {
1027 return;
1028 }
1029 }
1030 }
1031
1032 try {
1033 Bitmap bitmap = BitmapUtil.decodeBitmapFromBytes(bytes, sampleSize);
1034
Yorke Lee3b124482014-05-06 11:54:23 -07001035 // TODO: As a temporary workaround while framework support is being added to
1036 // clip non-square bitmaps into a perfect circle, manually crop the bitmap into
1037 // into a square if it will be displayed as a thumbnail so that it can be cropped
1038 // into a circle.
1039 final int height = bitmap.getHeight();
1040 final int width = bitmap.getWidth();
Yorke Lee1ae8e742014-05-22 16:45:30 -07001041
1042 // The smaller dimension of a scaled bitmap can range from anywhere from 0 to just
1043 // below twice the length of a thumbnail image due to the way we calculate the optimal
1044 // sample size.
1045 if (height != width && Math.min(height, width) <= mThumbnailSize * 2) {
Yorke Lee3b124482014-05-06 11:54:23 -07001046 final int dimension = Math.min(height, width);
1047 bitmap = ThumbnailUtils.extractThumbnail(bitmap, dimension, dimension);
1048 }
Chiao Cheng86618002012-10-16 13:21:10 -07001049 // make bitmap mutable and draw size onto it
1050 if (DEBUG_SIZES) {
1051 Bitmap original = bitmap;
1052 bitmap = bitmap.copy(bitmap.getConfig(), true);
1053 original.recycle();
1054 Canvas canvas = new Canvas(bitmap);
1055 Paint paint = new Paint();
1056 paint.setTextSize(16);
1057 paint.setColor(Color.BLUE);
1058 paint.setStyle(Style.FILL);
1059 canvas.drawRect(0.0f, 0.0f, 50.0f, 20.0f, paint);
1060 paint.setColor(Color.WHITE);
1061 paint.setAntiAlias(true);
1062 canvas.drawText(bitmap.getWidth() + "/" + sampleSize, 0, 15, paint);
1063 }
1064
1065 holder.decodedSampleSize = sampleSize;
1066 holder.bitmap = bitmap;
1067 holder.bitmapRef = new SoftReference<Bitmap>(bitmap);
1068 if (DEBUG) {
1069 Log.d(TAG, "inflateBitmap " + btk(bytes.length) + " -> "
1070 + bitmap.getWidth() + "x" + bitmap.getHeight()
1071 + ", " + btk(bitmap.getByteCount()));
1072 }
1073 } catch (OutOfMemoryError e) {
1074 // Do nothing - the photo will appear to be missing
1075 }
1076 }
1077
1078 public void clear() {
1079 if (DEBUG) Log.d(TAG, "clear");
1080 mPendingRequests.clear();
1081 mBitmapHolderCache.evictAll();
1082 mBitmapCache.evictAll();
1083 }
1084
1085 @Override
1086 public void pause() {
1087 mPaused = true;
1088 }
1089
1090 @Override
1091 public void resume() {
1092 mPaused = false;
1093 if (DEBUG) dumpStats();
1094 if (!mPendingRequests.isEmpty()) {
1095 requestLoading();
1096 }
1097 }
1098
1099 /**
1100 * Sends a message to this thread itself to start loading images. If the current
1101 * view contains multiple image views, all of those image views will get a chance
1102 * to request their respective photos before any of those requests are executed.
1103 * This allows us to load images in bulk.
1104 */
1105 private void requestLoading() {
1106 if (!mLoadingRequested) {
1107 mLoadingRequested = true;
1108 mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING);
1109 }
1110 }
1111
1112 /**
1113 * Processes requests on the main thread.
1114 */
1115 @Override
1116 public boolean handleMessage(Message msg) {
1117 switch (msg.what) {
1118 case MESSAGE_REQUEST_LOADING: {
1119 mLoadingRequested = false;
1120 if (!mPaused) {
1121 ensureLoaderThread();
1122 mLoaderThread.requestLoading();
1123 }
1124 return true;
1125 }
1126
1127 case MESSAGE_PHOTOS_LOADED: {
1128 if (!mPaused) {
1129 processLoadedImages();
1130 }
1131 if (DEBUG) dumpStats();
1132 return true;
1133 }
1134 }
1135 return false;
1136 }
1137
1138 public void ensureLoaderThread() {
1139 if (mLoaderThread == null) {
1140 mLoaderThread = new LoaderThread(mContext.getContentResolver());
1141 mLoaderThread.start();
1142 }
1143 }
1144
1145 /**
1146 * Goes over pending loading requests and displays loaded photos. If some of the
1147 * photos still haven't been loaded, sends another request for image loading.
1148 */
1149 private void processLoadedImages() {
Wenyi Wang3991d452016-04-12 10:57:42 -07001150 final Iterator<Entry<ImageView, Request>> iterator = mPendingRequests.entrySet().iterator();
Chiao Cheng86618002012-10-16 13:21:10 -07001151 while (iterator.hasNext()) {
Wenyi Wang3991d452016-04-12 10:57:42 -07001152 final Entry<ImageView, Request> entry = iterator.next();
Yorke Leec4a2a232014-04-28 17:53:42 -07001153 // TODO: Temporarily disable contact photo fading in, until issues with
1154 // RoundedBitmapDrawables overlapping the default image drawables are resolved.
Wenyi Wang3991d452016-04-12 10:57:42 -07001155 final boolean loaded = loadCachedPhoto(entry.getKey(), entry.getValue(), false);
Chiao Cheng86618002012-10-16 13:21:10 -07001156 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 */
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.
Gary Mai3a533282016-11-08 19:02:26 +00001180 * bytes should be null to indicate a failure to load the photo. An empty byte[] signifies
1181 * a successful load but no photo was available.
Chiao Cheng86618002012-10-16 13:21:10 -07001182 */
1183 private void cacheBitmap(Object key, byte[] bytes, boolean preloading, int requestedExtent) {
1184 if (DEBUG) {
1185 BitmapHolder prev = mBitmapHolderCache.get(key);
1186 if (prev != null && prev.bytes != null) {
1187 Log.d(TAG, "Overwriting cache: key=" + key + (prev.fresh ? " FRESH" : " stale"));
1188 if (prev.fresh) {
1189 mFreshCacheOverwrite.incrementAndGet();
1190 } else {
1191 mStaleCacheOverwrite.incrementAndGet();
1192 }
1193 }
1194 Log.d(TAG, "Caching data: key=" + key + ", " +
1195 (bytes == null ? "<null>" : btk(bytes.length)));
1196 }
1197 BitmapHolder holder = new BitmapHolder(bytes,
1198 bytes == null ? -1 : BitmapUtil.getSmallerExtentFromBytes(bytes));
1199
1200 // Unless this image is being preloaded, decode it right away while
1201 // we are still on the background thread.
1202 if (!preloading) {
1203 inflateBitmap(holder, requestedExtent);
1204 }
1205
Yorke Leee0c90b52015-05-27 12:14:57 -07001206 if (bytes != null) {
Walter Jang3a5f94f2016-11-08 17:11:28 +00001207 mBitmapHolderCache.put(key, holder);
Yorke Leee0c90b52015-05-27 12:14:57 -07001208 if (mBitmapHolderCache.get(key) != holder) {
1209 Log.w(TAG, "Bitmap too big to fit in cache.");
1210 mBitmapHolderCache.put(key, BITMAP_UNAVAILABLE);
1211 }
1212 } else {
1213 mBitmapHolderCache.put(key, BITMAP_UNAVAILABLE);
1214 }
1215
Chiao Cheng86618002012-10-16 13:21:10 -07001216 mBitmapHolderCacheAllUnfresh = false;
1217 }
1218
1219 @Override
1220 public void cacheBitmap(Uri photoUri, Bitmap bitmap, byte[] photoBytes) {
1221 final int smallerExtent = Math.min(bitmap.getWidth(), bitmap.getHeight());
1222 // We can pretend here that the extent of the photo was the size that we originally
1223 // requested
Yorke Leec4a2a232014-04-28 17:53:42 -07001224 Request request = Request.createFromUri(photoUri, smallerExtent, false /* darkTheme */,
1225 false /* isCircular */ , DEFAULT_AVATAR);
Chiao Cheng86618002012-10-16 13:21:10 -07001226 BitmapHolder holder = new BitmapHolder(photoBytes, smallerExtent);
1227 holder.bitmapRef = new SoftReference<Bitmap>(bitmap);
1228 mBitmapHolderCache.put(request.getKey(), holder);
1229 mBitmapHolderCacheAllUnfresh = false;
1230 mBitmapCache.put(request.getKey(), bitmap);
1231 }
1232
1233 /**
1234 * Populates an array of photo IDs that need to be loaded. Also decodes bitmaps that we have
1235 * already loaded
1236 */
1237 private void obtainPhotoIdsAndUrisToLoad(Set<Long> photoIds,
1238 Set<String> photoIdsAsStrings, Set<Request> uris) {
1239 photoIds.clear();
1240 photoIdsAsStrings.clear();
1241 uris.clear();
1242
1243 boolean jpegsDecoded = false;
1244
1245 /*
1246 * Since the call is made from the loader thread, the map could be
1247 * changing during the iteration. That's not really a problem:
1248 * ConcurrentHashMap will allow those changes to happen without throwing
1249 * exceptions. Since we may miss some requests in the situation of
1250 * concurrent change, we will need to check the map again once loading
1251 * is complete.
1252 */
1253 Iterator<Request> iterator = mPendingRequests.values().iterator();
1254 while (iterator.hasNext()) {
1255 Request request = iterator.next();
1256 final BitmapHolder holder = mBitmapHolderCache.get(request.getKey());
Yorke Lee4e0f6602015-06-01 17:10:40 -07001257 if (holder == BITMAP_UNAVAILABLE) {
1258 continue;
1259 }
Chiao Cheng86618002012-10-16 13:21:10 -07001260 if (holder != null && holder.bytes != null && holder.fresh &&
1261 (holder.bitmapRef == null || holder.bitmapRef.get() == null)) {
1262 // This was previously loaded but we don't currently have the inflated Bitmap
1263 inflateBitmap(holder, request.getRequestedExtent());
1264 jpegsDecoded = true;
1265 } else {
1266 if (holder == null || !holder.fresh) {
1267 if (request.isUriRequest()) {
1268 uris.add(request);
1269 } else {
1270 photoIds.add(request.getId());
1271 photoIdsAsStrings.add(String.valueOf(request.mId));
1272 }
1273 }
1274 }
1275 }
1276
1277 if (jpegsDecoded) mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
1278 }
1279
1280 /**
1281 * The thread that performs loading of photos from the database.
1282 */
1283 private class LoaderThread extends HandlerThread implements Callback {
1284 private static final int BUFFER_SIZE = 1024*16;
1285 private static final int MESSAGE_PRELOAD_PHOTOS = 0;
1286 private static final int MESSAGE_LOAD_PHOTOS = 1;
1287
1288 /**
1289 * A pause between preload batches that yields to the UI thread.
1290 */
1291 private static final int PHOTO_PRELOAD_DELAY = 1000;
1292
1293 /**
1294 * Number of photos to preload per batch.
1295 */
1296 private static final int PRELOAD_BATCH = 25;
1297
1298 /**
1299 * Maximum number of photos to preload. If the cache size is 2Mb and
1300 * the expected average size of a photo is 4kb, then this number should be 2Mb/4kb = 500.
1301 */
1302 private static final int MAX_PHOTOS_TO_PRELOAD = 100;
1303
1304 private final ContentResolver mResolver;
1305 private final StringBuilder mStringBuilder = new StringBuilder();
1306 private final Set<Long> mPhotoIds = Sets.newHashSet();
1307 private final Set<String> mPhotoIdsAsStrings = Sets.newHashSet();
1308 private final Set<Request> mPhotoUris = Sets.newHashSet();
1309 private final List<Long> mPreloadPhotoIds = Lists.newArrayList();
1310
1311 private Handler mLoaderThreadHandler;
1312 private byte mBuffer[];
1313
1314 private static final int PRELOAD_STATUS_NOT_STARTED = 0;
1315 private static final int PRELOAD_STATUS_IN_PROGRESS = 1;
1316 private static final int PRELOAD_STATUS_DONE = 2;
1317
1318 private int mPreloadStatus = PRELOAD_STATUS_NOT_STARTED;
1319
1320 public LoaderThread(ContentResolver resolver) {
1321 super(LOADER_THREAD_NAME);
1322 mResolver = resolver;
1323 }
1324
1325 public void ensureHandler() {
1326 if (mLoaderThreadHandler == null) {
1327 mLoaderThreadHandler = new Handler(getLooper(), this);
1328 }
1329 }
1330
1331 /**
1332 * Kicks off preloading of the next batch of photos on the background thread.
1333 * Preloading will happen after a delay: we want to yield to the UI thread
1334 * as much as possible.
1335 * <p>
1336 * If preloading is already complete, does nothing.
1337 */
1338 public void requestPreloading() {
1339 if (mPreloadStatus == PRELOAD_STATUS_DONE) {
1340 return;
1341 }
1342
1343 ensureHandler();
1344 if (mLoaderThreadHandler.hasMessages(MESSAGE_LOAD_PHOTOS)) {
1345 return;
1346 }
1347
1348 mLoaderThreadHandler.sendEmptyMessageDelayed(
1349 MESSAGE_PRELOAD_PHOTOS, PHOTO_PRELOAD_DELAY);
1350 }
1351
1352 /**
1353 * Sends a message to this thread to load requested photos. Cancels a preloading
1354 * request, if any: we don't want preloading to impede loading of the photos
1355 * we need to display now.
1356 */
1357 public void requestLoading() {
1358 ensureHandler();
1359 mLoaderThreadHandler.removeMessages(MESSAGE_PRELOAD_PHOTOS);
1360 mLoaderThreadHandler.sendEmptyMessage(MESSAGE_LOAD_PHOTOS);
1361 }
1362
1363 /**
1364 * Receives the above message, loads photos and then sends a message
1365 * to the main thread to process them.
1366 */
1367 @Override
1368 public boolean handleMessage(Message msg) {
1369 switch (msg.what) {
1370 case MESSAGE_PRELOAD_PHOTOS:
1371 preloadPhotosInBackground();
1372 break;
1373 case MESSAGE_LOAD_PHOTOS:
1374 loadPhotosInBackground();
1375 break;
1376 }
1377 return true;
1378 }
1379
1380 /**
1381 * The first time it is called, figures out which photos need to be preloaded.
1382 * Each subsequent call preloads the next batch of photos and requests
1383 * another cycle of preloading after a delay. The whole process ends when
1384 * we either run out of photos to preload or fill up cache.
1385 */
1386 private void preloadPhotosInBackground() {
1387 if (mPreloadStatus == PRELOAD_STATUS_DONE) {
1388 return;
1389 }
1390
1391 if (mPreloadStatus == PRELOAD_STATUS_NOT_STARTED) {
1392 queryPhotosForPreload();
1393 if (mPreloadPhotoIds.isEmpty()) {
1394 mPreloadStatus = PRELOAD_STATUS_DONE;
1395 } else {
1396 mPreloadStatus = PRELOAD_STATUS_IN_PROGRESS;
1397 }
1398 requestPreloading();
1399 return;
1400 }
1401
1402 if (mBitmapHolderCache.size() > mBitmapHolderCacheRedZoneBytes) {
1403 mPreloadStatus = PRELOAD_STATUS_DONE;
1404 return;
1405 }
1406
1407 mPhotoIds.clear();
1408 mPhotoIdsAsStrings.clear();
1409
1410 int count = 0;
1411 int preloadSize = mPreloadPhotoIds.size();
1412 while(preloadSize > 0 && mPhotoIds.size() < PRELOAD_BATCH) {
1413 preloadSize--;
1414 count++;
1415 Long photoId = mPreloadPhotoIds.get(preloadSize);
1416 mPhotoIds.add(photoId);
1417 mPhotoIdsAsStrings.add(photoId.toString());
1418 mPreloadPhotoIds.remove(preloadSize);
1419 }
1420
1421 loadThumbnails(true);
1422
1423 if (preloadSize == 0) {
1424 mPreloadStatus = PRELOAD_STATUS_DONE;
1425 }
1426
Wenyi Wang57a0e982017-03-24 16:02:44 -07001427 if (Log.isLoggable(TAG, Log.VERBOSE)) {
1428 Log.v(TAG, "Preloaded " + count + " photos. Cached bytes: "
1429 + mBitmapHolderCache.size());
1430 }
Chiao Cheng86618002012-10-16 13:21:10 -07001431
1432 requestPreloading();
1433 }
1434
1435 private void queryPhotosForPreload() {
1436 Cursor cursor = null;
1437 try {
1438 Uri uri = Contacts.CONTENT_URI.buildUpon().appendQueryParameter(
1439 ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT))
1440 .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
1441 String.valueOf(MAX_PHOTOS_TO_PRELOAD))
1442 .build();
1443 cursor = mResolver.query(uri, new String[] { Contacts.PHOTO_ID },
1444 Contacts.PHOTO_ID + " NOT NULL AND " + Contacts.PHOTO_ID + "!=0",
1445 null,
1446 Contacts.STARRED + " DESC, " + Contacts.LAST_TIME_CONTACTED + " DESC");
1447
1448 if (cursor != null) {
1449 while (cursor.moveToNext()) {
1450 // Insert them in reverse order, because we will be taking
1451 // them from the end of the list for loading.
1452 mPreloadPhotoIds.add(0, cursor.getLong(0));
1453 }
1454 }
1455 } finally {
1456 if (cursor != null) {
1457 cursor.close();
1458 }
1459 }
1460 }
1461
1462 private void loadPhotosInBackground() {
Yorke Leef5d819f2015-07-14 16:10:35 -07001463 if (!PermissionsUtil.hasPermission(mContext,
1464 android.Manifest.permission.READ_CONTACTS)) {
1465 return;
1466 }
Chiao Cheng86618002012-10-16 13:21:10 -07001467 obtainPhotoIdsAndUrisToLoad(mPhotoIds, mPhotoIdsAsStrings, mPhotoUris);
1468 loadThumbnails(false);
1469 loadUriBasedPhotos();
1470 requestPreloading();
1471 }
1472
1473 /** Loads thumbnail photos with ids */
1474 private void loadThumbnails(boolean preloading) {
1475 if (mPhotoIds.isEmpty()) {
1476 return;
1477 }
1478
1479 // Remove loaded photos from the preload queue: we don't want
1480 // the preloading process to load them again.
1481 if (!preloading && mPreloadStatus == PRELOAD_STATUS_IN_PROGRESS) {
1482 for (Long id : mPhotoIds) {
1483 mPreloadPhotoIds.remove(id);
1484 }
1485 if (mPreloadPhotoIds.isEmpty()) {
1486 mPreloadStatus = PRELOAD_STATUS_DONE;
1487 }
1488 }
1489
1490 mStringBuilder.setLength(0);
1491 mStringBuilder.append(Photo._ID + " IN(");
1492 for (int i = 0; i < mPhotoIds.size(); i++) {
1493 if (i != 0) {
1494 mStringBuilder.append(',');
1495 }
1496 mStringBuilder.append('?');
1497 }
1498 mStringBuilder.append(')');
1499
1500 Cursor cursor = null;
1501 try {
1502 if (DEBUG) Log.d(TAG, "Loading " + TextUtils.join(",", mPhotoIdsAsStrings));
1503 cursor = mResolver.query(Data.CONTENT_URI,
1504 COLUMNS,
1505 mStringBuilder.toString(),
1506 mPhotoIdsAsStrings.toArray(EMPTY_STRING_ARRAY),
1507 null);
1508
1509 if (cursor != null) {
1510 while (cursor.moveToNext()) {
1511 Long id = cursor.getLong(0);
1512 byte[] bytes = cursor.getBlob(1);
Gary Mai3a533282016-11-08 19:02:26 +00001513 if (bytes == null) {
1514 bytes = new byte[0];
1515 }
Chiao Cheng86618002012-10-16 13:21:10 -07001516 cacheBitmap(id, bytes, preloading, -1);
1517 mPhotoIds.remove(id);
1518 }
1519 }
1520 } finally {
1521 if (cursor != null) {
1522 cursor.close();
1523 }
1524 }
1525
1526 // Remaining photos were not found in the contacts database (but might be in profile).
1527 for (Long id : mPhotoIds) {
1528 if (ContactsContract.isProfileId(id)) {
1529 Cursor profileCursor = null;
1530 try {
1531 profileCursor = mResolver.query(
1532 ContentUris.withAppendedId(Data.CONTENT_URI, id),
1533 COLUMNS, null, null, null);
1534 if (profileCursor != null && profileCursor.moveToFirst()) {
Gary Mai3a533282016-11-08 19:02:26 +00001535 byte[] bytes = profileCursor.getBlob(1);
1536 if (bytes == null) {
1537 bytes = new byte[0];
1538 }
1539 cacheBitmap(profileCursor.getLong(0), bytes, preloading, -1);
Chiao Cheng86618002012-10-16 13:21:10 -07001540 } else {
1541 // Couldn't load a photo this way either.
1542 cacheBitmap(id, null, preloading, -1);
1543 }
1544 } finally {
1545 if (profileCursor != null) {
1546 profileCursor.close();
1547 }
1548 }
1549 } else {
1550 // Not a profile photo and not found - mark the cache accordingly
1551 cacheBitmap(id, null, preloading, -1);
1552 }
1553 }
1554
1555 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
1556 }
1557
1558 /**
1559 * Loads photos referenced with Uris. Those can be remote thumbnails
1560 * (from directory searches), display photos etc
1561 */
1562 private void loadUriBasedPhotos() {
1563 for (Request uriRequest : mPhotoUris) {
Tyler Gunn48c391d2014-05-09 16:08:21 -07001564 // Keep the original URI and use this to key into the cache. Failure to do so will
1565 // result in an image being continually reloaded into cache if the original URI
1566 // has a contact type encodedFragment (eg nearby places business photo URLs).
1567 Uri originalUri = uriRequest.getUri();
1568
1569 // Strip off the "contact type" we added to the URI to ensure it was identifiable as
1570 // a business photo -- there is no need to pass this on to the server.
1571 Uri uri = ContactPhotoManager.removeContactType(originalUri);
1572
Chiao Cheng86618002012-10-16 13:21:10 -07001573 if (mBuffer == null) {
1574 mBuffer = new byte[BUFFER_SIZE];
1575 }
1576 try {
1577 if (DEBUG) Log.d(TAG, "Loading " + uri);
Jay Shraunerd77fb672013-09-10 12:02:02 -07001578 final String scheme = uri.getScheme();
1579 InputStream is = null;
1580 if (scheme.equals("http") || scheme.equals("https")) {
Yorke Lee64953802015-05-28 09:43:01 -07001581 TrafficStats.setThreadStatsTag(TrafficStatsTags.CONTACT_PHOTO_DOWNLOAD_TAG);
Tyler Gunn2005cbc2015-01-29 10:25:02 -08001582 final HttpURLConnection connection =
1583 (HttpURLConnection) new URL(uri.toString()).openConnection();
1584
1585 // Include the user agent if it is specified.
1586 if (!TextUtils.isEmpty(mUserAgent)) {
1587 connection.setRequestProperty("User-Agent", mUserAgent);
1588 }
1589 try {
1590 is = connection.getInputStream();
1591 } catch (IOException e) {
1592 connection.disconnect();
1593 is = null;
1594 }
Yorke Lee64953802015-05-28 09:43:01 -07001595 TrafficStats.clearThreadStatsTag();
Jay Shraunerd77fb672013-09-10 12:02:02 -07001596 } else {
1597 is = mResolver.openInputStream(uri);
1598 }
Chiao Cheng86618002012-10-16 13:21:10 -07001599 if (is != null) {
1600 ByteArrayOutputStream baos = new ByteArrayOutputStream();
1601 try {
1602 int size;
1603 while ((size = is.read(mBuffer)) != -1) {
1604 baos.write(mBuffer, 0, size);
1605 }
1606 } finally {
1607 is.close();
1608 }
Tyler Gunn48c391d2014-05-09 16:08:21 -07001609 cacheBitmap(originalUri, baos.toByteArray(), false,
Chiao Cheng86618002012-10-16 13:21:10 -07001610 uriRequest.getRequestedExtent());
1611 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
1612 } else {
Wenyi Wang57a0e982017-03-24 16:02:44 -07001613 if (Log.isLoggable(TAG, Log.VERBOSE)) {
1614 Log.v(TAG, "Cannot load photo " + uri);
1615 }
Tyler Gunn48c391d2014-05-09 16:08:21 -07001616 cacheBitmap(originalUri, null, false, uriRequest.getRequestedExtent());
Chiao Cheng86618002012-10-16 13:21:10 -07001617 }
Jay Shraunerd33af0b2014-05-27 15:30:10 -07001618 } catch (final Exception | OutOfMemoryError ex) {
Wenyi Wang57a0e982017-03-24 16:02:44 -07001619 if (Log.isLoggable(TAG, Log.VERBOSE)) {
1620 Log.v(TAG, "Cannot load photo " + uri, ex);
1621 }
Tyler Gunn48c391d2014-05-09 16:08:21 -07001622 cacheBitmap(originalUri, null, false, uriRequest.getRequestedExtent());
Chiao Cheng86618002012-10-16 13:21:10 -07001623 }
1624 }
1625 }
1626 }
1627
1628 /**
1629 * A holder for either a Uri or an id and a flag whether this was requested for the dark or
1630 * light theme
1631 */
1632 private static final class Request {
1633 private final long mId;
1634 private final Uri mUri;
1635 private final boolean mDarkTheme;
1636 private final int mRequestedExtent;
1637 private final DefaultImageProvider mDefaultProvider;
Gary Maif682a8a2016-10-11 15:28:15 -07001638 private final DefaultImageRequest mDefaultRequest;
Yorke Leec4a2a232014-04-28 17:53:42 -07001639 /**
1640 * Whether or not the contact photo is to be displayed as a circle
1641 */
1642 private final boolean mIsCircular;
Chiao Cheng86618002012-10-16 13:21:10 -07001643
1644 private Request(long id, Uri uri, int requestedExtent, boolean darkTheme,
Gary Maif682a8a2016-10-11 15:28:15 -07001645 boolean isCircular, DefaultImageProvider defaultProvider,
1646 DefaultImageRequest defaultRequest) {
Chiao Cheng86618002012-10-16 13:21:10 -07001647 mId = id;
1648 mUri = uri;
1649 mDarkTheme = darkTheme;
Yorke Leec4a2a232014-04-28 17:53:42 -07001650 mIsCircular = isCircular;
Chiao Cheng86618002012-10-16 13:21:10 -07001651 mRequestedExtent = requestedExtent;
1652 mDefaultProvider = defaultProvider;
Gary Maif682a8a2016-10-11 15:28:15 -07001653 mDefaultRequest = defaultRequest;
Chiao Cheng86618002012-10-16 13:21:10 -07001654 }
1655
Yorke Leec4a2a232014-04-28 17:53:42 -07001656 public static Request createFromThumbnailId(long id, boolean darkTheme, boolean isCircular,
Gary Mai3a533282016-11-08 19:02:26 +00001657 DefaultImageProvider defaultProvider, DefaultImageRequest defaultRequest) {
Gary Maif682a8a2016-10-11 15:28:15 -07001658 return new Request(id, null /* no URI */, -1, darkTheme, isCircular, defaultProvider,
Gary Mai3a533282016-11-08 19:02:26 +00001659 defaultRequest);
Chiao Cheng86618002012-10-16 13:21:10 -07001660 }
1661
1662 public static Request createFromUri(Uri uri, int requestedExtent, boolean darkTheme,
Yorke Leec4a2a232014-04-28 17:53:42 -07001663 boolean isCircular, DefaultImageProvider defaultProvider) {
Gary Maif682a8a2016-10-11 15:28:15 -07001664 return createFromUri(uri, requestedExtent, darkTheme, isCircular, defaultProvider,
1665 /* defaultRequest */ null);
1666 }
1667
1668 public static Request createFromUri(Uri uri, int requestedExtent, boolean darkTheme,
1669 boolean isCircular, DefaultImageProvider defaultProvider,
1670 DefaultImageRequest defaultRequest) {
Yorke Leec4a2a232014-04-28 17:53:42 -07001671 return new Request(0 /* no ID */, uri, requestedExtent, darkTheme, isCircular,
Gary Maif682a8a2016-10-11 15:28:15 -07001672 defaultProvider, defaultRequest);
Chiao Cheng86618002012-10-16 13:21:10 -07001673 }
1674
1675 public boolean isUriRequest() {
1676 return mUri != null;
1677 }
1678
1679 public Uri getUri() {
1680 return mUri;
1681 }
1682
1683 public long getId() {
1684 return mId;
1685 }
1686
1687 public int getRequestedExtent() {
1688 return mRequestedExtent;
1689 }
1690
1691 @Override
1692 public int hashCode() {
1693 final int prime = 31;
1694 int result = 1;
1695 result = prime * result + (int) (mId ^ (mId >>> 32));
1696 result = prime * result + mRequestedExtent;
1697 result = prime * result + ((mUri == null) ? 0 : mUri.hashCode());
1698 return result;
1699 }
1700
1701 @Override
1702 public boolean equals(Object obj) {
1703 if (this == obj) return true;
1704 if (obj == null) return false;
1705 if (getClass() != obj.getClass()) return false;
1706 final Request that = (Request) obj;
1707 if (mId != that.mId) return false;
1708 if (mRequestedExtent != that.mRequestedExtent) return false;
1709 if (!UriUtils.areEqual(mUri, that.mUri)) return false;
1710 // Don't compare equality of mDarkTheme because it is only used in the default contact
1711 // photo case. When the contact does have a photo, the contact photo is the same
1712 // regardless of mDarkTheme, so we shouldn't need to put the photo request on the queue
1713 // twice.
1714 return true;
1715 }
1716
1717 public Object getKey() {
1718 return mUri == null ? mId : mUri;
1719 }
1720
Tyler Gunn48c391d2014-05-09 16:08:21 -07001721 /**
1722 * Applies the default image to the current view. If the request is URI-based, looks for
1723 * the contact type encoded fragment to determine if this is a request for a business photo,
1724 * in which case we will load the default business photo.
1725 *
1726 * @param view The current image view to apply the image to.
1727 * @param isCircular Whether the image is circular or not.
1728 */
Yorke Leec4a2a232014-04-28 17:53:42 -07001729 public void applyDefaultImage(ImageView view, boolean isCircular) {
Tyler Gunn48c391d2014-05-09 16:08:21 -07001730 final DefaultImageRequest request;
1731
Gary Maif682a8a2016-10-11 15:28:15 -07001732 if (mDefaultRequest == null) {
1733 if (isCircular) {
1734 request = ContactPhotoManager.isBusinessContactUri(mUri)
1735 ? DefaultImageRequest.EMPTY_CIRCULAR_BUSINESS_IMAGE_REQUEST
1736 : DefaultImageRequest.EMPTY_CIRCULAR_DEFAULT_IMAGE_REQUEST;
1737 } else {
1738 request = ContactPhotoManager.isBusinessContactUri(mUri)
1739 ? DefaultImageRequest.EMPTY_DEFAULT_BUSINESS_IMAGE_REQUEST
1740 : DefaultImageRequest.EMPTY_DEFAULT_IMAGE_REQUEST;
1741 }
Tyler Gunn48c391d2014-05-09 16:08:21 -07001742 } else {
Gary Maif682a8a2016-10-11 15:28:15 -07001743 request = mDefaultRequest;
Tyler Gunn48c391d2014-05-09 16:08:21 -07001744 }
Yorke Leec4a2a232014-04-28 17:53:42 -07001745 mDefaultProvider.applyDefaultImage(view, mRequestedExtent, mDarkTheme, request);
Chiao Cheng86618002012-10-16 13:21:10 -07001746 }
1747 }
1748}