1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.dialer.contactphoto;
18 
19 import android.content.ComponentCallbacks2;
20 import android.content.Context;
21 import android.content.res.Configuration;
22 import android.content.res.Resources;
23 import android.graphics.drawable.Drawable;
24 import android.net.Uri;
25 import android.net.Uri.Builder;
26 import android.support.annotation.VisibleForTesting;
27 import android.text.TextUtils;
28 import android.view.View;
29 import android.widget.ImageView;
30 import android.widget.QuickContactBadge;
31 import com.android.dialer.common.LogUtil;
32 import com.android.dialer.lettertile.LetterTileDrawable;
33 import com.android.dialer.util.PermissionsUtil;
34 import com.android.dialer.util.UriUtils;
35 
36 /** Asynchronously loads contact photos and maintains a cache of photos. */
37 public abstract class ContactPhotoManager implements ComponentCallbacks2 {
38 
39   /** Scale and offset default constants used for default letter images */
40   public static final float SCALE_DEFAULT = 1.0f;
41 
42   public static final float OFFSET_DEFAULT = 0.0f;
43   public static final boolean IS_CIRCULAR_DEFAULT = false;
44   // TODO: Use LogUtil.isVerboseEnabled for DEBUG branches instead of a lint check.
45   // LINT.DoNotSubmitIf(true)
46   static final boolean DEBUG = false;
47   // LINT.DoNotSubmitIf(true)
48   static final boolean DEBUG_SIZES = false;
49   /** Uri-related constants used for default letter images */
50   private static final String DISPLAY_NAME_PARAM_KEY = "display_name";
51 
52   private static final String IDENTIFIER_PARAM_KEY = "identifier";
53   private static final String CONTACT_TYPE_PARAM_KEY = "contact_type";
54   private static final String SCALE_PARAM_KEY = "scale";
55   private static final String OFFSET_PARAM_KEY = "offset";
56   private static final String IS_CIRCULAR_PARAM_KEY = "is_circular";
57   private static final String DEFAULT_IMAGE_URI_SCHEME = "defaultimage";
58   private static final Uri DEFAULT_IMAGE_URI = Uri.parse(DEFAULT_IMAGE_URI_SCHEME + "://");
59   public static final DefaultImageProvider DEFAULT_AVATAR = new LetterTileDefaultImageProvider();
60   private static ContactPhotoManager instance;
61 
62   /**
63    * Given a {@link DefaultImageRequest}, returns an Uri that can be used to request a letter tile
64    * avatar when passed to the {@link ContactPhotoManager}. The internal implementation of this uri
65    * is not guaranteed to remain the same across application versions, so the actual uri should
66    * never be persisted in long-term storage and reused.
67    *
68    * @param request A {@link DefaultImageRequest} object with the fields configured to return a
69    * @return A Uri that when later passed to the {@link ContactPhotoManager} via {@link
70    *     #loadPhoto(ImageView, Uri, int, boolean, boolean, DefaultImageRequest)}, can be used to
71    *     request a default contact image, drawn as a letter tile using the parameters as configured
72    *     in the provided {@link DefaultImageRequest}
73    */
getDefaultAvatarUriForContact(DefaultImageRequest request)74   public static Uri getDefaultAvatarUriForContact(DefaultImageRequest request) {
75     final Builder builder = DEFAULT_IMAGE_URI.buildUpon();
76     if (request != null) {
77       if (!TextUtils.isEmpty(request.displayName)) {
78         builder.appendQueryParameter(DISPLAY_NAME_PARAM_KEY, request.displayName);
79       }
80       if (!TextUtils.isEmpty(request.identifier)) {
81         builder.appendQueryParameter(IDENTIFIER_PARAM_KEY, request.identifier);
82       }
83       if (request.contactType != LetterTileDrawable.TYPE_DEFAULT) {
84         builder.appendQueryParameter(CONTACT_TYPE_PARAM_KEY, String.valueOf(request.contactType));
85       }
86       if (request.scale != SCALE_DEFAULT) {
87         builder.appendQueryParameter(SCALE_PARAM_KEY, String.valueOf(request.scale));
88       }
89       if (request.offset != OFFSET_DEFAULT) {
90         builder.appendQueryParameter(OFFSET_PARAM_KEY, String.valueOf(request.offset));
91       }
92       if (request.isCircular != IS_CIRCULAR_DEFAULT) {
93         builder.appendQueryParameter(IS_CIRCULAR_PARAM_KEY, String.valueOf(request.isCircular));
94       }
95     }
96     return builder.build();
97   }
98 
99   /**
100    * Adds a business contact type encoded fragment to the URL. Used to ensure photo URLS from Nearby
101    * Places can be identified as business photo URLs rather than URLs for personal contact photos.
102    *
103    * @param photoUrl The photo URL to modify.
104    * @return URL with the contact type parameter added and set to TYPE_BUSINESS.
105    */
appendBusinessContactType(String photoUrl)106   public static String appendBusinessContactType(String photoUrl) {
107     Uri uri = Uri.parse(photoUrl);
108     Builder builder = uri.buildUpon();
109     builder.encodedFragment(String.valueOf(LetterTileDrawable.TYPE_BUSINESS));
110     return builder.build().toString();
111   }
112 
113   /**
114    * Removes the contact type information stored in the photo URI encoded fragment.
115    *
116    * @param photoUri The photo URI to remove the contact type from.
117    * @return The photo URI with contact type removed.
118    */
removeContactType(Uri photoUri)119   public static Uri removeContactType(Uri photoUri) {
120     String encodedFragment = photoUri.getEncodedFragment();
121     if (!TextUtils.isEmpty(encodedFragment)) {
122       Builder builder = photoUri.buildUpon();
123       builder.encodedFragment(null);
124       return builder.build();
125     }
126     return photoUri;
127   }
128 
129   /**
130    * Inspects a photo URI to determine if the photo URI represents a business.
131    *
132    * @param photoUri The URI to inspect.
133    * @return Whether the URI represents a business photo or not.
134    */
isBusinessContactUri(Uri photoUri)135   public static boolean isBusinessContactUri(Uri photoUri) {
136     if (photoUri == null) {
137       return false;
138     }
139 
140     String encodedFragment = photoUri.getEncodedFragment();
141     return !TextUtils.isEmpty(encodedFragment)
142         && encodedFragment.equals(String.valueOf(LetterTileDrawable.TYPE_BUSINESS));
143   }
144 
getDefaultImageRequestFromUri(Uri uri)145   protected static DefaultImageRequest getDefaultImageRequestFromUri(Uri uri) {
146     final DefaultImageRequest request =
147         new DefaultImageRequest(
148             uri.getQueryParameter(DISPLAY_NAME_PARAM_KEY),
149             uri.getQueryParameter(IDENTIFIER_PARAM_KEY),
150             false);
151     try {
152       String contactType = uri.getQueryParameter(CONTACT_TYPE_PARAM_KEY);
153       if (!TextUtils.isEmpty(contactType)) {
154         request.contactType = Integer.valueOf(contactType);
155       }
156 
157       String scale = uri.getQueryParameter(SCALE_PARAM_KEY);
158       if (!TextUtils.isEmpty(scale)) {
159         request.scale = Float.valueOf(scale);
160       }
161 
162       String offset = uri.getQueryParameter(OFFSET_PARAM_KEY);
163       if (!TextUtils.isEmpty(offset)) {
164         request.offset = Float.valueOf(offset);
165       }
166 
167       String isCircular = uri.getQueryParameter(IS_CIRCULAR_PARAM_KEY);
168       if (!TextUtils.isEmpty(isCircular)) {
169         request.isCircular = Boolean.valueOf(isCircular);
170       }
171     } catch (NumberFormatException e) {
172       LogUtil.w(
173           "ContactPhotoManager.getDefaultImageRequestFromUri",
174           "Invalid DefaultImageRequest image parameters provided, ignoring and using "
175               + "defaults.");
176     }
177 
178     return request;
179   }
180 
getInstance(Context context)181   public static ContactPhotoManager getInstance(Context context) {
182     if (instance == null) {
183       Context applicationContext = context.getApplicationContext();
184       instance = createContactPhotoManager(applicationContext);
185       applicationContext.registerComponentCallbacks(instance);
186       if (PermissionsUtil.hasContactsReadPermissions(context)) {
187         instance.preloadPhotosInBackground();
188       }
189     }
190     return instance;
191   }
192 
createContactPhotoManager(Context context)193   public static synchronized ContactPhotoManager createContactPhotoManager(Context context) {
194     return new ContactPhotoManagerImpl(context);
195   }
196 
197   @VisibleForTesting
injectContactPhotoManagerForTesting(ContactPhotoManager photoManager)198   public static void injectContactPhotoManagerForTesting(ContactPhotoManager photoManager) {
199     instance = photoManager;
200   }
201 
isDefaultImageUri(Uri uri)202   protected boolean isDefaultImageUri(Uri uri) {
203     return DEFAULT_IMAGE_URI_SCHEME.equals(uri.getScheme());
204   }
205 
206   /**
207    * Load thumbnail image into the supplied image view. If the photo is already cached, it is
208    * displayed immediately. Otherwise a request is sent to load the photo from the database.
209    */
loadThumbnail( ImageView view, long photoId, boolean darkTheme, boolean isCircular, DefaultImageRequest defaultImageRequest, DefaultImageProvider defaultProvider)210   public abstract void loadThumbnail(
211       ImageView view,
212       long photoId,
213       boolean darkTheme,
214       boolean isCircular,
215       DefaultImageRequest defaultImageRequest,
216       DefaultImageProvider defaultProvider);
217 
218   /**
219    * Calls {@link #loadThumbnail(ImageView, long, boolean, boolean, DefaultImageRequest,
220    * DefaultImageProvider)} using the {@link DefaultImageProvider} {@link #DEFAULT_AVATAR}.
221    */
loadThumbnail( ImageView view, long photoId, boolean darkTheme, boolean isCircular, DefaultImageRequest defaultImageRequest)222   public final void loadThumbnail(
223       ImageView view,
224       long photoId,
225       boolean darkTheme,
226       boolean isCircular,
227       DefaultImageRequest defaultImageRequest) {
228     loadThumbnail(view, photoId, darkTheme, isCircular, defaultImageRequest, DEFAULT_AVATAR);
229   }
230 
loadDialerThumbnailOrPhoto( QuickContactBadge badge, Uri contactUri, long photoId, Uri photoUri, String displayName, int contactType)231   public final void loadDialerThumbnailOrPhoto(
232       QuickContactBadge badge,
233       Uri contactUri,
234       long photoId,
235       Uri photoUri,
236       String displayName,
237       int contactType) {
238     badge.assignContactUri(contactUri);
239     badge.setOverlay(null);
240 
241     badge.setContentDescription(
242         badge.getContext().getString(R.string.description_quick_contact_for, displayName));
243 
244     String lookupKey = contactUri == null ? null : UriUtils.getLookupKeyFromUri(contactUri);
245     ContactPhotoManager.DefaultImageRequest request =
246         new ContactPhotoManager.DefaultImageRequest(
247             displayName, lookupKey, contactType, true /* isCircular */);
248     if (photoId == 0 && photoUri != null) {
249       loadDirectoryPhoto(badge, photoUri, false /* darkTheme */, true /* isCircular */, request);
250     } else {
251       loadThumbnail(badge, photoId, false /* darkTheme */, true /* isCircular */, request);
252     }
253   }
254 
255   /**
256    * Load photo into the supplied image view. If the photo is already cached, it is displayed
257    * immediately. Otherwise a request is sent to load the photo from the location specified by the
258    * URI.
259    *
260    * @param view The target view
261    * @param photoUri The uri of the photo to load
262    * @param requestedExtent Specifies an approximate Max(width, height) of the targetView. This is
263    *     useful if the source image can be a lot bigger that the target, so that the decoding is
264    *     done using efficient sampling. If requestedExtent is specified, no sampling of the image is
265    *     performed
266    * @param darkTheme Whether the background is dark. This is used for default avatars
267    * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default
268    *     letter tile avatar should be drawn.
269    * @param defaultProvider The provider of default avatars (this is used if photoUri doesn't refer
270    *     to an existing image)
271    */
loadPhoto( ImageView view, Uri photoUri, int requestedExtent, boolean darkTheme, boolean isCircular, DefaultImageRequest defaultImageRequest, DefaultImageProvider defaultProvider)272   public abstract void loadPhoto(
273       ImageView view,
274       Uri photoUri,
275       int requestedExtent,
276       boolean darkTheme,
277       boolean isCircular,
278       DefaultImageRequest defaultImageRequest,
279       DefaultImageProvider defaultProvider);
280 
281   /**
282    * Calls {@link #loadPhoto(ImageView, Uri, int, boolean, boolean, DefaultImageRequest,
283    * DefaultImageProvider)} with {@link #DEFAULT_AVATAR} and {@code null} display names and lookup
284    * keys.
285    *
286    * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default
287    *     letter tile avatar should be drawn.
288    */
loadPhoto( ImageView view, Uri photoUri, int requestedExtent, boolean darkTheme, boolean isCircular, DefaultImageRequest defaultImageRequest)289   public final void loadPhoto(
290       ImageView view,
291       Uri photoUri,
292       int requestedExtent,
293       boolean darkTheme,
294       boolean isCircular,
295       DefaultImageRequest defaultImageRequest) {
296     loadPhoto(
297         view,
298         photoUri,
299         requestedExtent,
300         darkTheme,
301         isCircular,
302         defaultImageRequest,
303         DEFAULT_AVATAR);
304   }
305 
306   /**
307    * Calls {@link #loadPhoto(ImageView, Uri, int, boolean, boolean, DefaultImageRequest,
308    * DefaultImageProvider)} with {@link #DEFAULT_AVATAR} and with the assumption, that the image is
309    * a thumbnail.
310    *
311    * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default
312    *     letter tile avatar should be drawn.
313    */
loadDirectoryPhoto( ImageView view, Uri photoUri, boolean darkTheme, boolean isCircular, DefaultImageRequest defaultImageRequest)314   public final void loadDirectoryPhoto(
315       ImageView view,
316       Uri photoUri,
317       boolean darkTheme,
318       boolean isCircular,
319       DefaultImageRequest defaultImageRequest) {
320     loadPhoto(view, photoUri, -1, darkTheme, isCircular, defaultImageRequest, DEFAULT_AVATAR);
321   }
322 
323   /**
324    * Remove photo from the supplied image view. This also cancels current pending load request
325    * inside this photo manager.
326    */
removePhoto(ImageView view)327   public abstract void removePhoto(ImageView view);
328 
329   /** Cancels all pending requests to load photos asynchronously. */
cancelPendingRequests(View fragmentRootView)330   public abstract void cancelPendingRequests(View fragmentRootView);
331 
332   /** Temporarily stops loading photos from the database. */
pause()333   public abstract void pause();
334 
335   /** Resumes loading photos from the database. */
resume()336   public abstract void resume();
337 
338   /**
339    * Marks all cached photos for reloading. We can continue using cache but should also make sure
340    * the photos haven't changed in the background and notify the views if so.
341    */
refreshCache()342   public abstract void refreshCache();
343 
344   /** Initiates a background process that over time will fill up cache with preload photos. */
preloadPhotosInBackground()345   public abstract void preloadPhotosInBackground();
346 
347   // ComponentCallbacks2
348   @Override
onConfigurationChanged(Configuration newConfig)349   public void onConfigurationChanged(Configuration newConfig) {}
350 
351   // ComponentCallbacks2
352   @Override
onLowMemory()353   public void onLowMemory() {}
354 
355   // ComponentCallbacks2
356   @Override
onTrimMemory(int level)357   public void onTrimMemory(int level) {}
358 
359   /**
360    * Contains fields used to contain contact details and other user-defined settings that might be
361    * used by the ContactPhotoManager to generate a default contact image. This contact image takes
362    * the form of a letter or bitmap drawn on top of a colored tile.
363    */
364   public static class DefaultImageRequest {
365 
366     /**
367      * Used to indicate that a drawable that represents a contact without any contact details should
368      * be returned.
369      */
370     public static final DefaultImageRequest EMPTY_DEFAULT_IMAGE_REQUEST = new DefaultImageRequest();
371     /**
372      * Used to indicate that a drawable that represents a business without a business photo should
373      * be returned.
374      */
375     public static final DefaultImageRequest EMPTY_DEFAULT_BUSINESS_IMAGE_REQUEST =
376         new DefaultImageRequest(null, null, LetterTileDrawable.TYPE_BUSINESS, false);
377     /**
378      * Used to indicate that a circular drawable that represents a contact without any contact
379      * details should be returned.
380      */
381     public static final DefaultImageRequest EMPTY_CIRCULAR_DEFAULT_IMAGE_REQUEST =
382         new DefaultImageRequest(null, null, true);
383     /**
384      * Used to indicate that a circular drawable that represents a business without a business photo
385      * should be returned.
386      */
387     public static final DefaultImageRequest EMPTY_CIRCULAR_BUSINESS_IMAGE_REQUEST =
388         new DefaultImageRequest(null, null, LetterTileDrawable.TYPE_BUSINESS, true);
389     /** The contact's display name. The display name is used to */
390     public String displayName;
391     /**
392      * A unique and deterministic string that can be used to identify this contact. This is usually
393      * the contact's lookup key, but other contact details can be used as well, especially for
394      * non-local or temporary contacts that might not have a lookup key. This is used to determine
395      * the color of the tile.
396      */
397     public String identifier;
398     /**
399      * The type of this contact. This contact type may be used to decide the kind of image to use in
400      * the case where a unique letter cannot be generated from the contact's display name and
401      * identifier.
402      */
403     public @LetterTileDrawable.ContactType int contactType = LetterTileDrawable.TYPE_DEFAULT;
404     /**
405      * The amount to scale the letter or bitmap to, as a ratio of its default size (from a range of
406      * 0.0f to 2.0f). The default value is 1.0f.
407      */
408     public float scale = SCALE_DEFAULT;
409     /**
410      * The amount to vertically offset the letter or image to within the tile. The provided offset
411      * must be within the range of -0.5f to 0.5f. If set to -0.5f, the letter will be shifted
412      * upwards by 0.5 times the height of the canvas it is being drawn on, which means it will be
413      * drawn with the center of the letter starting at the top edge of the canvas. If set to 0.5f,
414      * the letter will be shifted downwards by 0.5 times the height of the canvas it is being drawn
415      * on, which means it will be drawn with the center of the letter starting at the bottom edge of
416      * the canvas. The default is 0.0f, which means the letter is drawn in the exact vertical center
417      * of the tile.
418      */
419     public float offset = OFFSET_DEFAULT;
420     /** Whether or not to draw the default image as a circle, instead of as a square/rectangle. */
421     public boolean isCircular = false;
422 
DefaultImageRequest()423     public DefaultImageRequest() {}
424 
DefaultImageRequest(String displayName, String identifier, boolean isCircular)425     public DefaultImageRequest(String displayName, String identifier, boolean isCircular) {
426       this(
427           displayName,
428           identifier,
429           LetterTileDrawable.TYPE_DEFAULT,
430           SCALE_DEFAULT,
431           OFFSET_DEFAULT,
432           isCircular);
433     }
434 
DefaultImageRequest( String displayName, String identifier, int contactType, boolean isCircular)435     public DefaultImageRequest(
436         String displayName, String identifier, int contactType, boolean isCircular) {
437       this(displayName, identifier, contactType, SCALE_DEFAULT, OFFSET_DEFAULT, isCircular);
438     }
439 
DefaultImageRequest( String displayName, String identifier, int contactType, float scale, float offset, boolean isCircular)440     public DefaultImageRequest(
441         String displayName,
442         String identifier,
443         int contactType,
444         float scale,
445         float offset,
446         boolean isCircular) {
447       this.displayName = displayName;
448       this.identifier = identifier;
449       this.contactType = contactType;
450       this.scale = scale;
451       this.offset = offset;
452       this.isCircular = isCircular;
453     }
454   }
455 
456   public abstract static class DefaultImageProvider {
457 
458     /**
459      * Applies the default avatar to the ImageView. Extent is an indicator for the size (width or
460      * height). If darkTheme is set, the avatar is one that looks better on dark background
461      *
462      * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default
463      *     letter tile avatar should be drawn.
464      */
applyDefaultImage( ImageView view, int extent, boolean darkTheme, DefaultImageRequest defaultImageRequest)465     public abstract void applyDefaultImage(
466         ImageView view, int extent, boolean darkTheme, DefaultImageRequest defaultImageRequest);
467   }
468 
469   /**
470    * A default image provider that applies a letter tile consisting of a colored background and a
471    * letter in the foreground as the default image for a contact. The color of the background and
472    * the type of letter is decided based on the contact's details.
473    */
474   private static class LetterTileDefaultImageProvider extends DefaultImageProvider {
475 
getDefaultImageForContact( Resources resources, DefaultImageRequest defaultImageRequest)476     public static Drawable getDefaultImageForContact(
477         Resources resources, DefaultImageRequest defaultImageRequest) {
478       final LetterTileDrawable drawable = new LetterTileDrawable(resources);
479       final int tileShape =
480           defaultImageRequest.isCircular
481               ? LetterTileDrawable.SHAPE_CIRCLE
482               : LetterTileDrawable.SHAPE_RECTANGLE;
483       if (defaultImageRequest != null) {
484         // If the contact identifier is null or empty, fallback to the
485         // displayName. In that case, use {@code null} for the contact's
486         // display name so that a default bitmap will be used instead of a
487         // letter
488         if (TextUtils.isEmpty(defaultImageRequest.identifier)) {
489           drawable.setCanonicalDialerLetterTileDetails(
490               null, defaultImageRequest.displayName, tileShape, defaultImageRequest.contactType);
491         } else {
492           drawable.setCanonicalDialerLetterTileDetails(
493               defaultImageRequest.displayName,
494               defaultImageRequest.identifier,
495               tileShape,
496               defaultImageRequest.contactType);
497         }
498         drawable.setScale(defaultImageRequest.scale);
499         drawable.setOffset(defaultImageRequest.offset);
500       }
501       return drawable;
502     }
503 
504     @Override
applyDefaultImage( ImageView view, int extent, boolean darkTheme, DefaultImageRequest defaultImageRequest)505     public void applyDefaultImage(
506         ImageView view, int extent, boolean darkTheme, DefaultImageRequest defaultImageRequest) {
507       final Drawable drawable = getDefaultImageForContact(view.getResources(), defaultImageRequest);
508       view.setImageDrawable(drawable);
509     }
510   }
511 }
512