blob: b43af19acea115835ecf66cac2148b1c0aac196b [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;
Yorke Leec4a2a232014-04-28 17:53:42 -070049import android.support.v4.graphics.drawable.RoundedBitmapDrawable;
Chris Craik93fb4352014-08-05 18:51:49 -070050import android.support.v4.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(
131 resources, null);
132 }
133 return sDefaultLetterAvatar;
134 }
135 return LetterTileDefaultImageProvider.getDefaultImageForContact(resources,
136 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(),
390 defaultImageRequest);
391 view.setImageDrawable(drawable);
392 }
393
394 public static Drawable getDefaultImageForContact(Resources resources,
395 DefaultImageRequest defaultImageRequest) {
396 final LetterTileDrawable drawable = new LetterTileDrawable(resources);
397 if (defaultImageRequest != null) {
398 // If the contact identifier is null or empty, fallback to the
399 // displayName. In that case, use {@code null} for the contact's
400 // display name so that a default bitmap will be used instead of a
401 // letter
402 if (TextUtils.isEmpty(defaultImageRequest.identifier)) {
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 */
463 public abstract void loadThumbnail(ImageView view, long photoId, boolean darkTheme,
Yorke Leec4a2a232014-04-28 17:53:42 -0700464 boolean isCircular, DefaultImageRequest defaultImageRequest,
465 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) {
473 loadThumbnail(view, photoId, darkTheme, isCircular, defaultImageRequest, DEFAULT_AVATAR);
Chiao Cheng86618002012-10-16 13:21:10 -0700474 }
475
Yorke Lee9df5e192014-02-12 14:58:25 -0800476
Chiao Cheng86618002012-10-16 13:21:10 -0700477 /**
478 * Load photo into the supplied image view. If the photo is already cached,
479 * it is displayed immediately. Otherwise a request is sent to load the photo
480 * from the location specified by the URI.
Yorke Lee9df5e192014-02-12 14:58:25 -0800481 *
Chiao Cheng86618002012-10-16 13:21:10 -0700482 * @param view The target view
483 * @param photoUri The uri of the photo to load
484 * @param requestedExtent Specifies an approximate Max(width, height) of the targetView.
485 * This is useful if the source image can be a lot bigger that the target, so that the decoding
486 * is done using efficient sampling. If requestedExtent is specified, no sampling of the image
487 * is performed
488 * @param darkTheme Whether the background is dark. This is used for default avatars
Yorke Lee9df5e192014-02-12 14:58:25 -0800489 * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default
490 * letter tile avatar should be drawn.
Chiao Cheng86618002012-10-16 13:21:10 -0700491 * @param defaultProvider The provider of default avatars (this is used if photoUri doesn't
492 * refer to an existing image)
493 */
494 public abstract void loadPhoto(ImageView view, Uri photoUri, int requestedExtent,
Yorke Leec4a2a232014-04-28 17:53:42 -0700495 boolean darkTheme, boolean isCircular, DefaultImageRequest defaultImageRequest,
Yorke Lee9df5e192014-02-12 14:58:25 -0800496 DefaultImageProvider defaultProvider);
Chiao Cheng86618002012-10-16 13:21:10 -0700497
498 /**
Yorke Lee9df5e192014-02-12 14:58:25 -0800499 * Calls {@link #loadPhoto(ImageView, Uri, int, boolean, DefaultImageRequest,
500 * DefaultImageProvider)} with {@link #DEFAULT_AVATAR} and {@code null} display names and
501 * lookup keys.
502 *
503 * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default
504 * letter tile avatar should be drawn.
Chiao Cheng86618002012-10-16 13:21:10 -0700505 */
506 public final void loadPhoto(ImageView view, Uri photoUri, int requestedExtent,
Yorke Leec4a2a232014-04-28 17:53:42 -0700507 boolean darkTheme, boolean isCircular, DefaultImageRequest defaultImageRequest) {
508 loadPhoto(view, photoUri, requestedExtent, darkTheme, isCircular,
509 defaultImageRequest, DEFAULT_AVATAR);
Chiao Cheng86618002012-10-16 13:21:10 -0700510 }
511
512 /**
Yorke Lee9df5e192014-02-12 14:58:25 -0800513 * Calls {@link #loadPhoto(ImageView, Uri, boolean, boolean, DefaultImageRequest,
514 * DefaultImageProvider)} with {@link #DEFAULT_AVATAR} and with the assumption, that
515 * the image is a thumbnail.
516 *
517 * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default
518 * letter tile avatar should be drawn.
Chiao Cheng86618002012-10-16 13:21:10 -0700519 */
Yorke Lee9df5e192014-02-12 14:58:25 -0800520 public final void loadDirectoryPhoto(ImageView view, Uri photoUri, boolean darkTheme,
Yorke Leec4a2a232014-04-28 17:53:42 -0700521 boolean isCircular, DefaultImageRequest defaultImageRequest) {
522 loadPhoto(view, photoUri, -1, darkTheme, isCircular, defaultImageRequest, DEFAULT_AVATAR);
Chiao Cheng86618002012-10-16 13:21:10 -0700523 }
524
525 /**
526 * Remove photo from the supplied image view. This also cancels current pending load request
527 * inside this photo manager.
528 */
529 public abstract void removePhoto(ImageView view);
530
531 /**
Tyler Gunn232df2f2014-04-15 15:30:58 -0700532 * Cancels all pending requests to load photos asynchronously.
533 */
Brian Attwellb92b6372014-07-21 23:39:35 -0700534 public abstract void cancelPendingRequests(View fragmentRootView);
Tyler Gunn232df2f2014-04-15 15:30:58 -0700535
536 /**
Chiao Cheng86618002012-10-16 13:21:10 -0700537 * Temporarily stops loading photos from the database.
538 */
539 public abstract void pause();
540
541 /**
542 * Resumes loading photos from the database.
543 */
544 public abstract void resume();
545
546 /**
547 * Marks all cached photos for reloading. We can continue using cache but should
548 * also make sure the photos haven't changed in the background and notify the views
549 * if so.
550 */
551 public abstract void refreshCache();
552
553 /**
554 * Stores the given bitmap directly in the LRU bitmap cache.
555 * @param photoUri The URI of the photo (for future requests).
556 * @param bitmap The bitmap.
557 * @param photoBytes The bytes that were parsed to create the bitmap.
558 */
559 public abstract void cacheBitmap(Uri photoUri, Bitmap bitmap, byte[] photoBytes);
560
561 /**
562 * Initiates a background process that over time will fill up cache with
563 * preload photos.
564 */
565 public abstract void preloadPhotosInBackground();
566
567 // ComponentCallbacks2
568 @Override
569 public void onConfigurationChanged(Configuration newConfig) {
570 }
571
572 // ComponentCallbacks2
573 @Override
574 public void onLowMemory() {
575 }
576
577 // ComponentCallbacks2
578 @Override
579 public void onTrimMemory(int level) {
580 }
581}
582
583class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
584 private static final String LOADER_THREAD_NAME = "ContactPhotoLoader";
585
586 private static final int FADE_TRANSITION_DURATION = 200;
587
588 /**
589 * Type of message sent by the UI thread to itself to indicate that some photos
590 * need to be loaded.
591 */
592 private static final int MESSAGE_REQUEST_LOADING = 1;
593
594 /**
595 * Type of message sent by the loader thread to indicate that some photos have
596 * been loaded.
597 */
598 private static final int MESSAGE_PHOTOS_LOADED = 2;
599
600 private static final String[] EMPTY_STRING_ARRAY = new String[0];
601
602 private static final String[] COLUMNS = new String[] { Photo._ID, Photo.PHOTO };
603
604 /**
Yorke Leee0c90b52015-05-27 12:14:57 -0700605 * Dummy object used to indicate that a bitmap for a given key could not be stored in the
606 * cache.
607 */
608 private static final BitmapHolder BITMAP_UNAVAILABLE;
609
610 static {
611 BITMAP_UNAVAILABLE = new BitmapHolder(new byte[0], 0);
612 BITMAP_UNAVAILABLE.bitmapRef = new SoftReference<Bitmap>(null);
613 }
614
615 /**
Chiao Cheng86618002012-10-16 13:21:10 -0700616 * Maintains the state of a particular photo.
617 */
618 private static class BitmapHolder {
619 final byte[] bytes;
620 final int originalSmallerExtent;
621
622 volatile boolean fresh;
623 Bitmap bitmap;
624 Reference<Bitmap> bitmapRef;
625 int decodedSampleSize;
626
627 public BitmapHolder(byte[] bytes, int originalSmallerExtent) {
628 this.bytes = bytes;
629 this.fresh = true;
630 this.originalSmallerExtent = originalSmallerExtent;
631 }
632 }
633
634 private final Context mContext;
635
636 /**
637 * An LRU cache for bitmap holders. The cache contains bytes for photos just
638 * as they come from the database. Each holder has a soft reference to the
639 * actual bitmap.
640 */
641 private final LruCache<Object, BitmapHolder> mBitmapHolderCache;
642
643 /**
644 * {@code true} if ALL entries in {@link #mBitmapHolderCache} are NOT fresh.
645 */
646 private volatile boolean mBitmapHolderCacheAllUnfresh = true;
647
648 /**
649 * Cache size threshold at which bitmaps will not be preloaded.
650 */
651 private final int mBitmapHolderCacheRedZoneBytes;
652
653 /**
654 * Level 2 LRU cache for bitmaps. This is a smaller cache that holds
655 * the most recently used bitmaps to save time on decoding
656 * them from bytes (the bytes are stored in {@link #mBitmapHolderCache}.
657 */
658 private final LruCache<Object, Bitmap> mBitmapCache;
659
660 /**
661 * A map from ImageView to the corresponding photo ID or uri, encapsulated in a request.
662 * The request may swapped out before the photo loading request is started.
663 */
664 private final ConcurrentHashMap<ImageView, Request> mPendingRequests =
665 new ConcurrentHashMap<ImageView, Request>();
666
667 /**
668 * Handler for messages sent to the UI thread.
669 */
670 private final Handler mMainThreadHandler = new Handler(this);
671
672 /**
673 * Thread responsible for loading photos from the database. Created upon
674 * the first request.
675 */
676 private LoaderThread mLoaderThread;
677
678 /**
679 * A gate to make sure we only send one instance of MESSAGE_PHOTOS_NEEDED at a time.
680 */
681 private boolean mLoadingRequested;
682
683 /**
684 * Flag indicating if the image loading is paused.
685 */
686 private boolean mPaused;
687
688 /** Cache size for {@link #mBitmapHolderCache} for devices with "large" RAM. */
689 private static final int HOLDER_CACHE_SIZE = 2000000;
690
691 /** Cache size for {@link #mBitmapCache} for devices with "large" RAM. */
692 private static final int BITMAP_CACHE_SIZE = 36864 * 48; // 1728K
693
Yorke Lee3b124482014-05-06 11:54:23 -0700694 /** Height/width of a thumbnail image */
695 private static int mThumbnailSize;
696
Chiao Cheng86618002012-10-16 13:21:10 -0700697 /** For debug: How many times we had to reload cached photo for a stale entry */
698 private final AtomicInteger mStaleCacheOverwrite = new AtomicInteger();
699
700 /** For debug: How many times we had to reload cached photo for a fresh entry. Should be 0. */
701 private final AtomicInteger mFreshCacheOverwrite = new AtomicInteger();
702
Tyler Gunn2005cbc2015-01-29 10:25:02 -0800703 /**
704 * The user agent string to use when loading URI based photos.
705 */
706 private String mUserAgent;
707
Chiao Cheng86618002012-10-16 13:21:10 -0700708 public ContactPhotoManagerImpl(Context context) {
709 mContext = context;
710
Yorke Leea2412222013-10-23 15:14:53 -0700711 final ActivityManager am = ((ActivityManager) context.getSystemService(
712 Context.ACTIVITY_SERVICE));
713
714 final float cacheSizeAdjustment = (am.isLowRamDevice()) ? 0.5f : 1.0f;
715
Chiao Cheng86618002012-10-16 13:21:10 -0700716 final int bitmapCacheSize = (int) (cacheSizeAdjustment * BITMAP_CACHE_SIZE);
717 mBitmapCache = new LruCache<Object, Bitmap>(bitmapCacheSize) {
718 @Override protected int sizeOf(Object key, Bitmap value) {
719 return value.getByteCount();
720 }
721
722 @Override protected void entryRemoved(
723 boolean evicted, Object key, Bitmap oldValue, Bitmap newValue) {
724 if (DEBUG) dumpStats();
725 }
726 };
727 final int holderCacheSize = (int) (cacheSizeAdjustment * HOLDER_CACHE_SIZE);
728 mBitmapHolderCache = new LruCache<Object, BitmapHolder>(holderCacheSize) {
729 @Override protected int sizeOf(Object key, BitmapHolder value) {
730 return value.bytes != null ? value.bytes.length : 0;
731 }
732
733 @Override protected void entryRemoved(
734 boolean evicted, Object key, BitmapHolder oldValue, BitmapHolder newValue) {
735 if (DEBUG) dumpStats();
736 }
737 };
738 mBitmapHolderCacheRedZoneBytes = (int) (holderCacheSize * 0.75);
739 Log.i(TAG, "Cache adj: " + cacheSizeAdjustment);
740 if (DEBUG) {
741 Log.d(TAG, "Cache size: " + btk(mBitmapHolderCache.maxSize())
742 + " + " + btk(mBitmapCache.maxSize()));
743 }
Yorke Lee3b124482014-05-06 11:54:23 -0700744
745 mThumbnailSize = context.getResources().getDimensionPixelSize(
746 R.dimen.contact_browser_list_item_photo_size);
Tyler Gunn2005cbc2015-01-29 10:25:02 -0800747
748 // Get a user agent string to use for URI photo requests.
749 mUserAgent = UserAgentGenerator.getUserAgent(context);
750 if (mUserAgent == null) {
751 mUserAgent = "";
752 }
Chiao Cheng86618002012-10-16 13:21:10 -0700753 }
754
755 /** Converts bytes to K bytes, rounding up. Used only for debug log. */
756 private static String btk(int bytes) {
757 return ((bytes + 1023) / 1024) + "K";
758 }
759
760 private static final int safeDiv(int dividend, int divisor) {
761 return (divisor == 0) ? 0 : (dividend / divisor);
762 }
763
764 /**
765 * Dump cache stats on logcat.
766 */
767 private void dumpStats() {
768 if (!DEBUG) return;
769 {
770 int numHolders = 0;
771 int rawBytes = 0;
772 int bitmapBytes = 0;
773 int numBitmaps = 0;
774 for (BitmapHolder h : mBitmapHolderCache.snapshot().values()) {
775 numHolders++;
776 if (h.bytes != null) {
777 rawBytes += h.bytes.length;
778 }
779 Bitmap b = h.bitmapRef != null ? h.bitmapRef.get() : null;
780 if (b != null) {
781 numBitmaps++;
782 bitmapBytes += b.getByteCount();
783 }
784 }
785 Log.d(TAG, "L1: " + btk(rawBytes) + " + " + btk(bitmapBytes) + " = "
786 + btk(rawBytes + bitmapBytes) + ", " + numHolders + " holders, "
787 + numBitmaps + " bitmaps, avg: "
788 + btk(safeDiv(rawBytes, numHolders))
789 + "," + btk(safeDiv(bitmapBytes,numBitmaps)));
790 Log.d(TAG, "L1 Stats: " + mBitmapHolderCache.toString()
791 + ", overwrite: fresh=" + mFreshCacheOverwrite.get()
792 + " stale=" + mStaleCacheOverwrite.get());
793 }
794
795 {
796 int numBitmaps = 0;
797 int bitmapBytes = 0;
798 for (Bitmap b : mBitmapCache.snapshot().values()) {
799 numBitmaps++;
800 bitmapBytes += b.getByteCount();
801 }
802 Log.d(TAG, "L2: " + btk(bitmapBytes) + ", " + numBitmaps + " bitmaps"
803 + ", avg: " + btk(safeDiv(bitmapBytes, numBitmaps)));
804 // We don't get from L2 cache, so L2 stats is meaningless.
805 }
806 }
807
808 @Override
809 public void onTrimMemory(int level) {
810 if (DEBUG) Log.d(TAG, "onTrimMemory: " + level);
811 if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
812 // Clear the caches. Note all pending requests will be removed too.
813 clear();
814 }
815 }
816
817 @Override
818 public void preloadPhotosInBackground() {
819 ensureLoaderThread();
820 mLoaderThread.requestPreloading();
821 }
822
823 @Override
Yorke Leec4a2a232014-04-28 17:53:42 -0700824 public void loadThumbnail(ImageView view, long photoId, boolean darkTheme, boolean isCircular,
Yorke Lee9df5e192014-02-12 14:58:25 -0800825 DefaultImageRequest defaultImageRequest, DefaultImageProvider defaultProvider) {
Chiao Cheng86618002012-10-16 13:21:10 -0700826 if (photoId == 0) {
827 // No photo is needed
Yorke Lee9df5e192014-02-12 14:58:25 -0800828 defaultProvider.applyDefaultImage(view, -1, darkTheme, defaultImageRequest);
Chiao Cheng86618002012-10-16 13:21:10 -0700829 mPendingRequests.remove(view);
830 } else {
831 if (DEBUG) Log.d(TAG, "loadPhoto request: " + photoId);
Yorke Leec4a2a232014-04-28 17:53:42 -0700832 loadPhotoByIdOrUri(view, Request.createFromThumbnailId(photoId, darkTheme, isCircular,
Gary Mai3a533282016-11-08 19:02:26 +0000833 defaultProvider, defaultImageRequest));
Chiao Cheng86618002012-10-16 13:21:10 -0700834 }
835 }
836
837 @Override
838 public void loadPhoto(ImageView view, Uri photoUri, int requestedExtent, boolean darkTheme,
Yorke Leec4a2a232014-04-28 17:53:42 -0700839 boolean isCircular, DefaultImageRequest defaultImageRequest,
840 DefaultImageProvider defaultProvider) {
Chiao Cheng86618002012-10-16 13:21:10 -0700841 if (photoUri == null) {
842 // No photo is needed
Yorke Lee9df5e192014-02-12 14:58:25 -0800843 defaultProvider.applyDefaultImage(view, requestedExtent, darkTheme,
844 defaultImageRequest);
Chiao Cheng86618002012-10-16 13:21:10 -0700845 mPendingRequests.remove(view);
846 } else {
847 if (DEBUG) Log.d(TAG, "loadPhoto request: " + photoUri);
Yorke Lee9df5e192014-02-12 14:58:25 -0800848 if (isDefaultImageUri(photoUri)) {
849 createAndApplyDefaultImageForUri(view, photoUri, requestedExtent, darkTheme,
Yorke Lee8269bb12014-05-19 10:08:46 -0700850 isCircular, defaultProvider);
Yorke Lee9df5e192014-02-12 14:58:25 -0800851 } else {
Yorke Lee9df5e192014-02-12 14:58:25 -0800852 loadPhotoByIdOrUri(view, Request.createFromUri(photoUri, requestedExtent,
Gary Maif682a8a2016-10-11 15:28:15 -0700853 darkTheme, isCircular, defaultProvider, defaultImageRequest));
Yorke Lee9df5e192014-02-12 14:58:25 -0800854 }
Chiao Cheng86618002012-10-16 13:21:10 -0700855 }
856 }
857
Yorke Lee9df5e192014-02-12 14:58:25 -0800858 private void createAndApplyDefaultImageForUri(ImageView view, Uri uri, int requestedExtent,
Yorke Lee8269bb12014-05-19 10:08:46 -0700859 boolean darkTheme, boolean isCircular, DefaultImageProvider defaultProvider) {
Yorke Lee9df5e192014-02-12 14:58:25 -0800860 DefaultImageRequest request = getDefaultImageRequestFromUri(uri);
Yorke Lee8269bb12014-05-19 10:08:46 -0700861 request.isCircular = isCircular;
Yorke Lee9df5e192014-02-12 14:58:25 -0800862 defaultProvider.applyDefaultImage(view, requestedExtent, darkTheme, request);
863 }
864
Chiao Cheng86618002012-10-16 13:21:10 -0700865 private void loadPhotoByIdOrUri(ImageView view, Request request) {
866 boolean loaded = loadCachedPhoto(view, request, false);
867 if (loaded) {
868 mPendingRequests.remove(view);
869 } else {
870 mPendingRequests.put(view, request);
871 if (!mPaused) {
872 // Send a request to start loading photos
873 requestLoading();
874 }
875 }
876 }
877
878 @Override
879 public void removePhoto(ImageView view) {
880 view.setImageDrawable(null);
881 mPendingRequests.remove(view);
882 }
883
Tyler Gunn232df2f2014-04-15 15:30:58 -0700884
885 /**
Brian Attwellb92b6372014-07-21 23:39:35 -0700886 * Cancels pending requests to load photos asynchronously for views inside
887 * {@param fragmentRootView}. If {@param fragmentRootView} is null, cancels all requests.
Tyler Gunn232df2f2014-04-15 15:30:58 -0700888 */
889 @Override
Brian Attwellb92b6372014-07-21 23:39:35 -0700890 public void cancelPendingRequests(View fragmentRootView) {
891 if (fragmentRootView == null) {
892 mPendingRequests.clear();
893 return;
894 }
Wenyi Wang3991d452016-04-12 10:57:42 -0700895 final Iterator<Entry<ImageView, Request>> iterator = mPendingRequests.entrySet().iterator();
896 while (iterator.hasNext()) {
897 final ImageView imageView = iterator.next().getKey();
Brian Attwellb92b6372014-07-21 23:39:35 -0700898 // If an ImageView is orphaned (currently scrap) or a child of fragmentRootView, then
899 // we can safely remove its request.
900 if (imageView.getParent() == null || isChildView(fragmentRootView, imageView)) {
Wenyi Wang3991d452016-04-12 10:57:42 -0700901 iterator.remove();
Brian Attwellb92b6372014-07-21 23:39:35 -0700902 }
903 }
904 }
905
906 private static boolean isChildView(View parent, View potentialChild) {
907 return potentialChild.getParent() != null && (potentialChild.getParent() == parent || (
908 potentialChild.getParent() instanceof ViewGroup && isChildView(parent,
909 (ViewGroup) potentialChild.getParent())));
Tyler Gunn232df2f2014-04-15 15:30:58 -0700910 }
911
Chiao Cheng86618002012-10-16 13:21:10 -0700912 @Override
913 public void refreshCache() {
914 if (mBitmapHolderCacheAllUnfresh) {
915 if (DEBUG) Log.d(TAG, "refreshCache -- no fresh entries.");
916 return;
917 }
918 if (DEBUG) Log.d(TAG, "refreshCache");
919 mBitmapHolderCacheAllUnfresh = true;
920 for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
Yorke Leee0c90b52015-05-27 12:14:57 -0700921 if (holder != BITMAP_UNAVAILABLE) {
922 holder.fresh = false;
923 }
Chiao Cheng86618002012-10-16 13:21:10 -0700924 }
925 }
926
927 /**
928 * Checks if the photo is present in cache. If so, sets the photo on the view.
929 *
930 * @return false if the photo needs to be (re)loaded from the provider.
931 */
932 private boolean loadCachedPhoto(ImageView view, Request request, boolean fadeIn) {
933 BitmapHolder holder = mBitmapHolderCache.get(request.getKey());
934 if (holder == null) {
935 // The bitmap has not been loaded ==> show default avatar
Yorke Leec4a2a232014-04-28 17:53:42 -0700936 request.applyDefaultImage(view, request.mIsCircular);
Chiao Cheng86618002012-10-16 13:21:10 -0700937 return false;
938 }
939
Gary Maif682a8a2016-10-11 15:28:15 -0700940 if (holder.bytes == null || holder.bytes.length == 0) {
Yorke Leec4a2a232014-04-28 17:53:42 -0700941 request.applyDefaultImage(view, request.mIsCircular);
Chiao Cheng86618002012-10-16 13:21:10 -0700942 return holder.fresh;
943 }
944
945 Bitmap cachedBitmap = holder.bitmapRef == null ? null : holder.bitmapRef.get();
946 if (cachedBitmap == null) {
947 if (holder.bytes.length < 8 * 1024) {
948 // Small thumbnails are usually quick to inflate. Let's do that on the UI thread
949 inflateBitmap(holder, request.getRequestedExtent());
950 cachedBitmap = holder.bitmap;
951 if (cachedBitmap == null) return false;
952 } else {
953 // This is bigger data. Let's send that back to the Loader so that we can
954 // inflate this in the background
Yorke Leec4a2a232014-04-28 17:53:42 -0700955 request.applyDefaultImage(view, request.mIsCircular);
Chiao Cheng86618002012-10-16 13:21:10 -0700956 return false;
957 }
958 }
959
960 final Drawable previousDrawable = view.getDrawable();
961 if (fadeIn && previousDrawable != null) {
962 final Drawable[] layers = new Drawable[2];
963 // Prevent cascade of TransitionDrawables.
964 if (previousDrawable instanceof TransitionDrawable) {
965 final TransitionDrawable previousTransitionDrawable =
966 (TransitionDrawable) previousDrawable;
967 layers[0] = previousTransitionDrawable.getDrawable(
968 previousTransitionDrawable.getNumberOfLayers() - 1);
969 } else {
970 layers[0] = previousDrawable;
971 }
Yorke Leec4a2a232014-04-28 17:53:42 -0700972 layers[1] = getDrawableForBitmap(mContext.getResources(), cachedBitmap, request);
Chiao Cheng86618002012-10-16 13:21:10 -0700973 TransitionDrawable drawable = new TransitionDrawable(layers);
974 view.setImageDrawable(drawable);
975 drawable.startTransition(FADE_TRANSITION_DURATION);
976 } else {
Yorke Leec4a2a232014-04-28 17:53:42 -0700977 view.setImageDrawable(
978 getDrawableForBitmap(mContext.getResources(), cachedBitmap, request));
Chiao Cheng86618002012-10-16 13:21:10 -0700979 }
980
981 // Put the bitmap in the LRU cache. But only do this for images that are small enough
982 // (we require that at least six of those can be cached at the same time)
983 if (cachedBitmap.getByteCount() < mBitmapCache.maxSize() / 6) {
984 mBitmapCache.put(request.getKey(), cachedBitmap);
985 }
986
987 // Soften the reference
988 holder.bitmap = null;
989
990 return holder.fresh;
991 }
992
993 /**
Yorke Leec4a2a232014-04-28 17:53:42 -0700994 * Given a bitmap, returns a drawable that is configured to display the bitmap based on the
995 * specified request.
996 */
997 private Drawable getDrawableForBitmap(Resources resources, Bitmap bitmap, Request request) {
998 if (request.mIsCircular) {
999 final RoundedBitmapDrawable drawable =
Chris Craik93fb4352014-08-05 18:51:49 -07001000 RoundedBitmapDrawableFactory.create(resources, bitmap);
Yorke Leec4a2a232014-04-28 17:53:42 -07001001 drawable.setAntiAlias(true);
1002 drawable.setCornerRadius(bitmap.getHeight() / 2);
1003 return drawable;
1004 } else {
1005 return new BitmapDrawable(resources, bitmap);
1006 }
1007 }
1008
1009 /**
Chiao Cheng86618002012-10-16 13:21:10 -07001010 * If necessary, decodes bytes stored in the holder to Bitmap. As long as the
1011 * bitmap is held either by {@link #mBitmapCache} or by a soft reference in
1012 * the holder, it will not be necessary to decode the bitmap.
1013 */
1014 private static void inflateBitmap(BitmapHolder holder, int requestedExtent) {
1015 final int sampleSize =
1016 BitmapUtil.findOptimalSampleSize(holder.originalSmallerExtent, requestedExtent);
1017 byte[] bytes = holder.bytes;
1018 if (bytes == null || bytes.length == 0) {
1019 return;
1020 }
1021
1022 if (sampleSize == holder.decodedSampleSize) {
1023 // Check the soft reference. If will be retained if the bitmap is also
1024 // in the LRU cache, so we don't need to check the LRU cache explicitly.
1025 if (holder.bitmapRef != null) {
1026 holder.bitmap = holder.bitmapRef.get();
1027 if (holder.bitmap != null) {
1028 return;
1029 }
1030 }
1031 }
1032
1033 try {
1034 Bitmap bitmap = BitmapUtil.decodeBitmapFromBytes(bytes, sampleSize);
1035
Yorke Lee3b124482014-05-06 11:54:23 -07001036 // TODO: As a temporary workaround while framework support is being added to
1037 // clip non-square bitmaps into a perfect circle, manually crop the bitmap into
1038 // into a square if it will be displayed as a thumbnail so that it can be cropped
1039 // into a circle.
1040 final int height = bitmap.getHeight();
1041 final int width = bitmap.getWidth();
Yorke Lee1ae8e742014-05-22 16:45:30 -07001042
1043 // The smaller dimension of a scaled bitmap can range from anywhere from 0 to just
1044 // below twice the length of a thumbnail image due to the way we calculate the optimal
1045 // sample size.
1046 if (height != width && Math.min(height, width) <= mThumbnailSize * 2) {
Yorke Lee3b124482014-05-06 11:54:23 -07001047 final int dimension = Math.min(height, width);
1048 bitmap = ThumbnailUtils.extractThumbnail(bitmap, dimension, dimension);
1049 }
Chiao Cheng86618002012-10-16 13:21:10 -07001050 // make bitmap mutable and draw size onto it
1051 if (DEBUG_SIZES) {
1052 Bitmap original = bitmap;
1053 bitmap = bitmap.copy(bitmap.getConfig(), true);
1054 original.recycle();
1055 Canvas canvas = new Canvas(bitmap);
1056 Paint paint = new Paint();
1057 paint.setTextSize(16);
1058 paint.setColor(Color.BLUE);
1059 paint.setStyle(Style.FILL);
1060 canvas.drawRect(0.0f, 0.0f, 50.0f, 20.0f, paint);
1061 paint.setColor(Color.WHITE);
1062 paint.setAntiAlias(true);
1063 canvas.drawText(bitmap.getWidth() + "/" + sampleSize, 0, 15, paint);
1064 }
1065
1066 holder.decodedSampleSize = sampleSize;
1067 holder.bitmap = bitmap;
1068 holder.bitmapRef = new SoftReference<Bitmap>(bitmap);
1069 if (DEBUG) {
1070 Log.d(TAG, "inflateBitmap " + btk(bytes.length) + " -> "
1071 + bitmap.getWidth() + "x" + bitmap.getHeight()
1072 + ", " + btk(bitmap.getByteCount()));
1073 }
1074 } catch (OutOfMemoryError e) {
1075 // Do nothing - the photo will appear to be missing
1076 }
1077 }
1078
1079 public void clear() {
1080 if (DEBUG) Log.d(TAG, "clear");
1081 mPendingRequests.clear();
1082 mBitmapHolderCache.evictAll();
1083 mBitmapCache.evictAll();
1084 }
1085
1086 @Override
1087 public void pause() {
1088 mPaused = true;
1089 }
1090
1091 @Override
1092 public void resume() {
1093 mPaused = false;
1094 if (DEBUG) dumpStats();
1095 if (!mPendingRequests.isEmpty()) {
1096 requestLoading();
1097 }
1098 }
1099
1100 /**
1101 * Sends a message to this thread itself to start loading images. If the current
1102 * view contains multiple image views, all of those image views will get a chance
1103 * to request their respective photos before any of those requests are executed.
1104 * This allows us to load images in bulk.
1105 */
1106 private void requestLoading() {
1107 if (!mLoadingRequested) {
1108 mLoadingRequested = true;
1109 mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING);
1110 }
1111 }
1112
1113 /**
1114 * Processes requests on the main thread.
1115 */
1116 @Override
1117 public boolean handleMessage(Message msg) {
1118 switch (msg.what) {
1119 case MESSAGE_REQUEST_LOADING: {
1120 mLoadingRequested = false;
1121 if (!mPaused) {
1122 ensureLoaderThread();
1123 mLoaderThread.requestLoading();
1124 }
1125 return true;
1126 }
1127
1128 case MESSAGE_PHOTOS_LOADED: {
1129 if (!mPaused) {
1130 processLoadedImages();
1131 }
1132 if (DEBUG) dumpStats();
1133 return true;
1134 }
1135 }
1136 return false;
1137 }
1138
1139 public void ensureLoaderThread() {
1140 if (mLoaderThread == null) {
1141 mLoaderThread = new LoaderThread(mContext.getContentResolver());
1142 mLoaderThread.start();
1143 }
1144 }
1145
1146 /**
1147 * Goes over pending loading requests and displays loaded photos. If some of the
1148 * photos still haven't been loaded, sends another request for image loading.
1149 */
1150 private void processLoadedImages() {
Wenyi Wang3991d452016-04-12 10:57:42 -07001151 final Iterator<Entry<ImageView, Request>> iterator = mPendingRequests.entrySet().iterator();
Chiao Cheng86618002012-10-16 13:21:10 -07001152 while (iterator.hasNext()) {
Wenyi Wang3991d452016-04-12 10:57:42 -07001153 final Entry<ImageView, Request> entry = iterator.next();
Yorke Leec4a2a232014-04-28 17:53:42 -07001154 // TODO: Temporarily disable contact photo fading in, until issues with
1155 // RoundedBitmapDrawables overlapping the default image drawables are resolved.
Wenyi Wang3991d452016-04-12 10:57:42 -07001156 final boolean loaded = loadCachedPhoto(entry.getKey(), entry.getValue(), false);
Chiao Cheng86618002012-10-16 13:21:10 -07001157 if (loaded) {
1158 iterator.remove();
1159 }
1160 }
1161
1162 softenCache();
1163
1164 if (!mPendingRequests.isEmpty()) {
1165 requestLoading();
1166 }
1167 }
1168
1169 /**
1170 * Removes strong references to loaded bitmaps to allow them to be garbage collected
1171 * if needed. Some of the bitmaps will still be retained by {@link #mBitmapCache}.
1172 */
1173 private void softenCache() {
1174 for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
1175 holder.bitmap = null;
1176 }
1177 }
1178
1179 /**
1180 * Stores the supplied bitmap in cache.
Gary Mai3a533282016-11-08 19:02:26 +00001181 * bytes should be null to indicate a failure to load the photo. An empty byte[] signifies
1182 * a successful load but no photo was available.
Chiao Cheng86618002012-10-16 13:21:10 -07001183 */
1184 private void cacheBitmap(Object key, byte[] bytes, boolean preloading, int requestedExtent) {
1185 if (DEBUG) {
1186 BitmapHolder prev = mBitmapHolderCache.get(key);
1187 if (prev != null && prev.bytes != null) {
1188 Log.d(TAG, "Overwriting cache: key=" + key + (prev.fresh ? " FRESH" : " stale"));
1189 if (prev.fresh) {
1190 mFreshCacheOverwrite.incrementAndGet();
1191 } else {
1192 mStaleCacheOverwrite.incrementAndGet();
1193 }
1194 }
1195 Log.d(TAG, "Caching data: key=" + key + ", " +
1196 (bytes == null ? "<null>" : btk(bytes.length)));
1197 }
1198 BitmapHolder holder = new BitmapHolder(bytes,
1199 bytes == null ? -1 : BitmapUtil.getSmallerExtentFromBytes(bytes));
1200
1201 // Unless this image is being preloaded, decode it right away while
1202 // we are still on the background thread.
1203 if (!preloading) {
1204 inflateBitmap(holder, requestedExtent);
1205 }
1206
Yorke Leee0c90b52015-05-27 12:14:57 -07001207 if (bytes != null) {
Walter Jang3a5f94f2016-11-08 17:11:28 +00001208 mBitmapHolderCache.put(key, holder);
Yorke Leee0c90b52015-05-27 12:14:57 -07001209 if (mBitmapHolderCache.get(key) != holder) {
1210 Log.w(TAG, "Bitmap too big to fit in cache.");
1211 mBitmapHolderCache.put(key, BITMAP_UNAVAILABLE);
1212 }
1213 } else {
1214 mBitmapHolderCache.put(key, BITMAP_UNAVAILABLE);
1215 }
1216
Chiao Cheng86618002012-10-16 13:21:10 -07001217 mBitmapHolderCacheAllUnfresh = false;
1218 }
1219
1220 @Override
1221 public void cacheBitmap(Uri photoUri, Bitmap bitmap, byte[] photoBytes) {
1222 final int smallerExtent = Math.min(bitmap.getWidth(), bitmap.getHeight());
1223 // We can pretend here that the extent of the photo was the size that we originally
1224 // requested
Yorke Leec4a2a232014-04-28 17:53:42 -07001225 Request request = Request.createFromUri(photoUri, smallerExtent, false /* darkTheme */,
1226 false /* isCircular */ , DEFAULT_AVATAR);
Chiao Cheng86618002012-10-16 13:21:10 -07001227 BitmapHolder holder = new BitmapHolder(photoBytes, smallerExtent);
1228 holder.bitmapRef = new SoftReference<Bitmap>(bitmap);
1229 mBitmapHolderCache.put(request.getKey(), holder);
1230 mBitmapHolderCacheAllUnfresh = false;
1231 mBitmapCache.put(request.getKey(), bitmap);
1232 }
1233
1234 /**
1235 * Populates an array of photo IDs that need to be loaded. Also decodes bitmaps that we have
1236 * already loaded
1237 */
1238 private void obtainPhotoIdsAndUrisToLoad(Set<Long> photoIds,
1239 Set<String> photoIdsAsStrings, Set<Request> uris) {
1240 photoIds.clear();
1241 photoIdsAsStrings.clear();
1242 uris.clear();
1243
1244 boolean jpegsDecoded = false;
1245
1246 /*
1247 * Since the call is made from the loader thread, the map could be
1248 * changing during the iteration. That's not really a problem:
1249 * ConcurrentHashMap will allow those changes to happen without throwing
1250 * exceptions. Since we may miss some requests in the situation of
1251 * concurrent change, we will need to check the map again once loading
1252 * is complete.
1253 */
1254 Iterator<Request> iterator = mPendingRequests.values().iterator();
1255 while (iterator.hasNext()) {
1256 Request request = iterator.next();
1257 final BitmapHolder holder = mBitmapHolderCache.get(request.getKey());
Yorke Lee4e0f6602015-06-01 17:10:40 -07001258 if (holder == BITMAP_UNAVAILABLE) {
1259 continue;
1260 }
Chiao Cheng86618002012-10-16 13:21:10 -07001261 if (holder != null && holder.bytes != null && holder.fresh &&
1262 (holder.bitmapRef == null || holder.bitmapRef.get() == null)) {
1263 // This was previously loaded but we don't currently have the inflated Bitmap
1264 inflateBitmap(holder, request.getRequestedExtent());
1265 jpegsDecoded = true;
1266 } else {
1267 if (holder == null || !holder.fresh) {
1268 if (request.isUriRequest()) {
1269 uris.add(request);
1270 } else {
1271 photoIds.add(request.getId());
1272 photoIdsAsStrings.add(String.valueOf(request.mId));
1273 }
1274 }
1275 }
1276 }
1277
1278 if (jpegsDecoded) mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
1279 }
1280
1281 /**
1282 * The thread that performs loading of photos from the database.
1283 */
1284 private class LoaderThread extends HandlerThread implements Callback {
1285 private static final int BUFFER_SIZE = 1024*16;
1286 private static final int MESSAGE_PRELOAD_PHOTOS = 0;
1287 private static final int MESSAGE_LOAD_PHOTOS = 1;
1288
1289 /**
1290 * A pause between preload batches that yields to the UI thread.
1291 */
1292 private static final int PHOTO_PRELOAD_DELAY = 1000;
1293
1294 /**
1295 * Number of photos to preload per batch.
1296 */
1297 private static final int PRELOAD_BATCH = 25;
1298
1299 /**
1300 * Maximum number of photos to preload. If the cache size is 2Mb and
1301 * the expected average size of a photo is 4kb, then this number should be 2Mb/4kb = 500.
1302 */
1303 private static final int MAX_PHOTOS_TO_PRELOAD = 100;
1304
1305 private final ContentResolver mResolver;
1306 private final StringBuilder mStringBuilder = new StringBuilder();
1307 private final Set<Long> mPhotoIds = Sets.newHashSet();
1308 private final Set<String> mPhotoIdsAsStrings = Sets.newHashSet();
1309 private final Set<Request> mPhotoUris = Sets.newHashSet();
1310 private final List<Long> mPreloadPhotoIds = Lists.newArrayList();
1311
1312 private Handler mLoaderThreadHandler;
1313 private byte mBuffer[];
1314
1315 private static final int PRELOAD_STATUS_NOT_STARTED = 0;
1316 private static final int PRELOAD_STATUS_IN_PROGRESS = 1;
1317 private static final int PRELOAD_STATUS_DONE = 2;
1318
1319 private int mPreloadStatus = PRELOAD_STATUS_NOT_STARTED;
1320
1321 public LoaderThread(ContentResolver resolver) {
1322 super(LOADER_THREAD_NAME);
1323 mResolver = resolver;
1324 }
1325
1326 public void ensureHandler() {
1327 if (mLoaderThreadHandler == null) {
1328 mLoaderThreadHandler = new Handler(getLooper(), this);
1329 }
1330 }
1331
1332 /**
1333 * Kicks off preloading of the next batch of photos on the background thread.
1334 * Preloading will happen after a delay: we want to yield to the UI thread
1335 * as much as possible.
1336 * <p>
1337 * If preloading is already complete, does nothing.
1338 */
1339 public void requestPreloading() {
1340 if (mPreloadStatus == PRELOAD_STATUS_DONE) {
1341 return;
1342 }
1343
1344 ensureHandler();
1345 if (mLoaderThreadHandler.hasMessages(MESSAGE_LOAD_PHOTOS)) {
1346 return;
1347 }
1348
1349 mLoaderThreadHandler.sendEmptyMessageDelayed(
1350 MESSAGE_PRELOAD_PHOTOS, PHOTO_PRELOAD_DELAY);
1351 }
1352
1353 /**
1354 * Sends a message to this thread to load requested photos. Cancels a preloading
1355 * request, if any: we don't want preloading to impede loading of the photos
1356 * we need to display now.
1357 */
1358 public void requestLoading() {
1359 ensureHandler();
1360 mLoaderThreadHandler.removeMessages(MESSAGE_PRELOAD_PHOTOS);
1361 mLoaderThreadHandler.sendEmptyMessage(MESSAGE_LOAD_PHOTOS);
1362 }
1363
1364 /**
1365 * Receives the above message, loads photos and then sends a message
1366 * to the main thread to process them.
1367 */
1368 @Override
1369 public boolean handleMessage(Message msg) {
1370 switch (msg.what) {
1371 case MESSAGE_PRELOAD_PHOTOS:
1372 preloadPhotosInBackground();
1373 break;
1374 case MESSAGE_LOAD_PHOTOS:
1375 loadPhotosInBackground();
1376 break;
1377 }
1378 return true;
1379 }
1380
1381 /**
1382 * The first time it is called, figures out which photos need to be preloaded.
1383 * Each subsequent call preloads the next batch of photos and requests
1384 * another cycle of preloading after a delay. The whole process ends when
1385 * we either run out of photos to preload or fill up cache.
1386 */
1387 private void preloadPhotosInBackground() {
1388 if (mPreloadStatus == PRELOAD_STATUS_DONE) {
1389 return;
1390 }
1391
1392 if (mPreloadStatus == PRELOAD_STATUS_NOT_STARTED) {
1393 queryPhotosForPreload();
1394 if (mPreloadPhotoIds.isEmpty()) {
1395 mPreloadStatus = PRELOAD_STATUS_DONE;
1396 } else {
1397 mPreloadStatus = PRELOAD_STATUS_IN_PROGRESS;
1398 }
1399 requestPreloading();
1400 return;
1401 }
1402
1403 if (mBitmapHolderCache.size() > mBitmapHolderCacheRedZoneBytes) {
1404 mPreloadStatus = PRELOAD_STATUS_DONE;
1405 return;
1406 }
1407
1408 mPhotoIds.clear();
1409 mPhotoIdsAsStrings.clear();
1410
1411 int count = 0;
1412 int preloadSize = mPreloadPhotoIds.size();
1413 while(preloadSize > 0 && mPhotoIds.size() < PRELOAD_BATCH) {
1414 preloadSize--;
1415 count++;
1416 Long photoId = mPreloadPhotoIds.get(preloadSize);
1417 mPhotoIds.add(photoId);
1418 mPhotoIdsAsStrings.add(photoId.toString());
1419 mPreloadPhotoIds.remove(preloadSize);
1420 }
1421
1422 loadThumbnails(true);
1423
1424 if (preloadSize == 0) {
1425 mPreloadStatus = PRELOAD_STATUS_DONE;
1426 }
1427
1428 Log.v(TAG, "Preloaded " + count + " photos. Cached bytes: "
1429 + mBitmapHolderCache.size());
1430
1431 requestPreloading();
1432 }
1433
1434 private void queryPhotosForPreload() {
1435 Cursor cursor = null;
1436 try {
1437 Uri uri = Contacts.CONTENT_URI.buildUpon().appendQueryParameter(
1438 ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT))
1439 .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
1440 String.valueOf(MAX_PHOTOS_TO_PRELOAD))
1441 .build();
1442 cursor = mResolver.query(uri, new String[] { Contacts.PHOTO_ID },
1443 Contacts.PHOTO_ID + " NOT NULL AND " + Contacts.PHOTO_ID + "!=0",
1444 null,
1445 Contacts.STARRED + " DESC, " + Contacts.LAST_TIME_CONTACTED + " DESC");
1446
1447 if (cursor != null) {
1448 while (cursor.moveToNext()) {
1449 // Insert them in reverse order, because we will be taking
1450 // them from the end of the list for loading.
1451 mPreloadPhotoIds.add(0, cursor.getLong(0));
1452 }
1453 }
1454 } finally {
1455 if (cursor != null) {
1456 cursor.close();
1457 }
1458 }
1459 }
1460
1461 private void loadPhotosInBackground() {
Yorke Leef5d819f2015-07-14 16:10:35 -07001462 if (!PermissionsUtil.hasPermission(mContext,
1463 android.Manifest.permission.READ_CONTACTS)) {
1464 return;
1465 }
Chiao Cheng86618002012-10-16 13:21:10 -07001466 obtainPhotoIdsAndUrisToLoad(mPhotoIds, mPhotoIdsAsStrings, mPhotoUris);
1467 loadThumbnails(false);
1468 loadUriBasedPhotos();
1469 requestPreloading();
1470 }
1471
1472 /** Loads thumbnail photos with ids */
1473 private void loadThumbnails(boolean preloading) {
1474 if (mPhotoIds.isEmpty()) {
1475 return;
1476 }
1477
1478 // Remove loaded photos from the preload queue: we don't want
1479 // the preloading process to load them again.
1480 if (!preloading && mPreloadStatus == PRELOAD_STATUS_IN_PROGRESS) {
1481 for (Long id : mPhotoIds) {
1482 mPreloadPhotoIds.remove(id);
1483 }
1484 if (mPreloadPhotoIds.isEmpty()) {
1485 mPreloadStatus = PRELOAD_STATUS_DONE;
1486 }
1487 }
1488
1489 mStringBuilder.setLength(0);
1490 mStringBuilder.append(Photo._ID + " IN(");
1491 for (int i = 0; i < mPhotoIds.size(); i++) {
1492 if (i != 0) {
1493 mStringBuilder.append(',');
1494 }
1495 mStringBuilder.append('?');
1496 }
1497 mStringBuilder.append(')');
1498
1499 Cursor cursor = null;
1500 try {
1501 if (DEBUG) Log.d(TAG, "Loading " + TextUtils.join(",", mPhotoIdsAsStrings));
1502 cursor = mResolver.query(Data.CONTENT_URI,
1503 COLUMNS,
1504 mStringBuilder.toString(),
1505 mPhotoIdsAsStrings.toArray(EMPTY_STRING_ARRAY),
1506 null);
1507
1508 if (cursor != null) {
1509 while (cursor.moveToNext()) {
1510 Long id = cursor.getLong(0);
1511 byte[] bytes = cursor.getBlob(1);
Gary Mai3a533282016-11-08 19:02:26 +00001512 if (bytes == null) {
1513 bytes = new byte[0];
1514 }
Chiao Cheng86618002012-10-16 13:21:10 -07001515 cacheBitmap(id, bytes, preloading, -1);
1516 mPhotoIds.remove(id);
1517 }
1518 }
1519 } finally {
1520 if (cursor != null) {
1521 cursor.close();
1522 }
1523 }
1524
1525 // Remaining photos were not found in the contacts database (but might be in profile).
1526 for (Long id : mPhotoIds) {
1527 if (ContactsContract.isProfileId(id)) {
1528 Cursor profileCursor = null;
1529 try {
1530 profileCursor = mResolver.query(
1531 ContentUris.withAppendedId(Data.CONTENT_URI, id),
1532 COLUMNS, null, null, null);
1533 if (profileCursor != null && profileCursor.moveToFirst()) {
Gary Mai3a533282016-11-08 19:02:26 +00001534 byte[] bytes = profileCursor.getBlob(1);
1535 if (bytes == null) {
1536 bytes = new byte[0];
1537 }
1538 cacheBitmap(profileCursor.getLong(0), bytes, preloading, -1);
Chiao Cheng86618002012-10-16 13:21:10 -07001539 } else {
1540 // Couldn't load a photo this way either.
1541 cacheBitmap(id, null, preloading, -1);
1542 }
1543 } finally {
1544 if (profileCursor != null) {
1545 profileCursor.close();
1546 }
1547 }
1548 } else {
1549 // Not a profile photo and not found - mark the cache accordingly
1550 cacheBitmap(id, null, preloading, -1);
1551 }
1552 }
1553
1554 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
1555 }
1556
1557 /**
1558 * Loads photos referenced with Uris. Those can be remote thumbnails
1559 * (from directory searches), display photos etc
1560 */
1561 private void loadUriBasedPhotos() {
1562 for (Request uriRequest : mPhotoUris) {
Tyler Gunn48c391d2014-05-09 16:08:21 -07001563 // Keep the original URI and use this to key into the cache. Failure to do so will
1564 // result in an image being continually reloaded into cache if the original URI
1565 // has a contact type encodedFragment (eg nearby places business photo URLs).
1566 Uri originalUri = uriRequest.getUri();
1567
1568 // Strip off the "contact type" we added to the URI to ensure it was identifiable as
1569 // a business photo -- there is no need to pass this on to the server.
1570 Uri uri = ContactPhotoManager.removeContactType(originalUri);
1571
Chiao Cheng86618002012-10-16 13:21:10 -07001572 if (mBuffer == null) {
1573 mBuffer = new byte[BUFFER_SIZE];
1574 }
1575 try {
1576 if (DEBUG) Log.d(TAG, "Loading " + uri);
Jay Shraunerd77fb672013-09-10 12:02:02 -07001577 final String scheme = uri.getScheme();
1578 InputStream is = null;
1579 if (scheme.equals("http") || scheme.equals("https")) {
Yorke Lee64953802015-05-28 09:43:01 -07001580 TrafficStats.setThreadStatsTag(TrafficStatsTags.CONTACT_PHOTO_DOWNLOAD_TAG);
Tyler Gunn2005cbc2015-01-29 10:25:02 -08001581 final HttpURLConnection connection =
1582 (HttpURLConnection) new URL(uri.toString()).openConnection();
1583
1584 // Include the user agent if it is specified.
1585 if (!TextUtils.isEmpty(mUserAgent)) {
1586 connection.setRequestProperty("User-Agent", mUserAgent);
1587 }
1588 try {
1589 is = connection.getInputStream();
1590 } catch (IOException e) {
1591 connection.disconnect();
1592 is = null;
1593 }
Yorke Lee64953802015-05-28 09:43:01 -07001594 TrafficStats.clearThreadStatsTag();
Jay Shraunerd77fb672013-09-10 12:02:02 -07001595 } else {
1596 is = mResolver.openInputStream(uri);
1597 }
Chiao Cheng86618002012-10-16 13:21:10 -07001598 if (is != null) {
1599 ByteArrayOutputStream baos = new ByteArrayOutputStream();
1600 try {
1601 int size;
1602 while ((size = is.read(mBuffer)) != -1) {
1603 baos.write(mBuffer, 0, size);
1604 }
1605 } finally {
1606 is.close();
1607 }
Tyler Gunn48c391d2014-05-09 16:08:21 -07001608 cacheBitmap(originalUri, baos.toByteArray(), false,
Chiao Cheng86618002012-10-16 13:21:10 -07001609 uriRequest.getRequestedExtent());
1610 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
1611 } else {
1612 Log.v(TAG, "Cannot load photo " + uri);
Tyler Gunn48c391d2014-05-09 16:08:21 -07001613 cacheBitmap(originalUri, null, false, uriRequest.getRequestedExtent());
Chiao Cheng86618002012-10-16 13:21:10 -07001614 }
Jay Shraunerd33af0b2014-05-27 15:30:10 -07001615 } catch (final Exception | OutOfMemoryError ex) {
Chiao Cheng86618002012-10-16 13:21:10 -07001616 Log.v(TAG, "Cannot load photo " + uri, ex);
Tyler Gunn48c391d2014-05-09 16:08:21 -07001617 cacheBitmap(originalUri, null, false, uriRequest.getRequestedExtent());
Chiao Cheng86618002012-10-16 13:21:10 -07001618 }
1619 }
1620 }
1621 }
1622
1623 /**
1624 * A holder for either a Uri or an id and a flag whether this was requested for the dark or
1625 * light theme
1626 */
1627 private static final class Request {
1628 private final long mId;
1629 private final Uri mUri;
1630 private final boolean mDarkTheme;
1631 private final int mRequestedExtent;
1632 private final DefaultImageProvider mDefaultProvider;
Gary Maif682a8a2016-10-11 15:28:15 -07001633 private final DefaultImageRequest mDefaultRequest;
Yorke Leec4a2a232014-04-28 17:53:42 -07001634 /**
1635 * Whether or not the contact photo is to be displayed as a circle
1636 */
1637 private final boolean mIsCircular;
Chiao Cheng86618002012-10-16 13:21:10 -07001638
1639 private Request(long id, Uri uri, int requestedExtent, boolean darkTheme,
Gary Maif682a8a2016-10-11 15:28:15 -07001640 boolean isCircular, DefaultImageProvider defaultProvider,
1641 DefaultImageRequest defaultRequest) {
Chiao Cheng86618002012-10-16 13:21:10 -07001642 mId = id;
1643 mUri = uri;
1644 mDarkTheme = darkTheme;
Yorke Leec4a2a232014-04-28 17:53:42 -07001645 mIsCircular = isCircular;
Chiao Cheng86618002012-10-16 13:21:10 -07001646 mRequestedExtent = requestedExtent;
1647 mDefaultProvider = defaultProvider;
Gary Maif682a8a2016-10-11 15:28:15 -07001648 mDefaultRequest = defaultRequest;
Chiao Cheng86618002012-10-16 13:21:10 -07001649 }
1650
Yorke Leec4a2a232014-04-28 17:53:42 -07001651 public static Request createFromThumbnailId(long id, boolean darkTheme, boolean isCircular,
Gary Mai3a533282016-11-08 19:02:26 +00001652 DefaultImageProvider defaultProvider, DefaultImageRequest defaultRequest) {
Gary Maif682a8a2016-10-11 15:28:15 -07001653 return new Request(id, null /* no URI */, -1, darkTheme, isCircular, defaultProvider,
Gary Mai3a533282016-11-08 19:02:26 +00001654 defaultRequest);
Chiao Cheng86618002012-10-16 13:21:10 -07001655 }
1656
1657 public static Request createFromUri(Uri uri, int requestedExtent, boolean darkTheme,
Yorke Leec4a2a232014-04-28 17:53:42 -07001658 boolean isCircular, DefaultImageProvider defaultProvider) {
Gary Maif682a8a2016-10-11 15:28:15 -07001659 return createFromUri(uri, requestedExtent, darkTheme, isCircular, defaultProvider,
1660 /* defaultRequest */ null);
1661 }
1662
1663 public static Request createFromUri(Uri uri, int requestedExtent, boolean darkTheme,
1664 boolean isCircular, DefaultImageProvider defaultProvider,
1665 DefaultImageRequest defaultRequest) {
Yorke Leec4a2a232014-04-28 17:53:42 -07001666 return new Request(0 /* no ID */, uri, requestedExtent, darkTheme, isCircular,
Gary Maif682a8a2016-10-11 15:28:15 -07001667 defaultProvider, defaultRequest);
Chiao Cheng86618002012-10-16 13:21:10 -07001668 }
1669
1670 public boolean isUriRequest() {
1671 return mUri != null;
1672 }
1673
1674 public Uri getUri() {
1675 return mUri;
1676 }
1677
1678 public long getId() {
1679 return mId;
1680 }
1681
1682 public int getRequestedExtent() {
1683 return mRequestedExtent;
1684 }
1685
1686 @Override
1687 public int hashCode() {
1688 final int prime = 31;
1689 int result = 1;
1690 result = prime * result + (int) (mId ^ (mId >>> 32));
1691 result = prime * result + mRequestedExtent;
1692 result = prime * result + ((mUri == null) ? 0 : mUri.hashCode());
1693 return result;
1694 }
1695
1696 @Override
1697 public boolean equals(Object obj) {
1698 if (this == obj) return true;
1699 if (obj == null) return false;
1700 if (getClass() != obj.getClass()) return false;
1701 final Request that = (Request) obj;
1702 if (mId != that.mId) return false;
1703 if (mRequestedExtent != that.mRequestedExtent) return false;
1704 if (!UriUtils.areEqual(mUri, that.mUri)) return false;
1705 // Don't compare equality of mDarkTheme because it is only used in the default contact
1706 // photo case. When the contact does have a photo, the contact photo is the same
1707 // regardless of mDarkTheme, so we shouldn't need to put the photo request on the queue
1708 // twice.
1709 return true;
1710 }
1711
1712 public Object getKey() {
1713 return mUri == null ? mId : mUri;
1714 }
1715
Tyler Gunn48c391d2014-05-09 16:08:21 -07001716 /**
1717 * Applies the default image to the current view. If the request is URI-based, looks for
1718 * the contact type encoded fragment to determine if this is a request for a business photo,
1719 * in which case we will load the default business photo.
1720 *
1721 * @param view The current image view to apply the image to.
1722 * @param isCircular Whether the image is circular or not.
1723 */
Yorke Leec4a2a232014-04-28 17:53:42 -07001724 public void applyDefaultImage(ImageView view, boolean isCircular) {
Tyler Gunn48c391d2014-05-09 16:08:21 -07001725 final DefaultImageRequest request;
1726
Gary Maif682a8a2016-10-11 15:28:15 -07001727 if (mDefaultRequest == null) {
1728 if (isCircular) {
1729 request = ContactPhotoManager.isBusinessContactUri(mUri)
1730 ? DefaultImageRequest.EMPTY_CIRCULAR_BUSINESS_IMAGE_REQUEST
1731 : DefaultImageRequest.EMPTY_CIRCULAR_DEFAULT_IMAGE_REQUEST;
1732 } else {
1733 request = ContactPhotoManager.isBusinessContactUri(mUri)
1734 ? DefaultImageRequest.EMPTY_DEFAULT_BUSINESS_IMAGE_REQUEST
1735 : DefaultImageRequest.EMPTY_DEFAULT_IMAGE_REQUEST;
1736 }
Tyler Gunn48c391d2014-05-09 16:08:21 -07001737 } else {
Gary Maif682a8a2016-10-11 15:28:15 -07001738 request = mDefaultRequest;
Tyler Gunn48c391d2014-05-09 16:08:21 -07001739 }
Yorke Leec4a2a232014-04-28 17:53:42 -07001740 mDefaultProvider.applyDefaultImage(view, mRequestedExtent, mDarkTheme, request);
Chiao Cheng86618002012-10-16 13:21:10 -07001741 }
1742 }
1743}