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