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
17package com.android.contacts.common;
18
19import android.content.ComponentCallbacks2;
20import android.content.Context;
21import android.content.res.Configuration;
22import android.content.res.Resources;
23import android.graphics.drawable.Drawable;
24import android.net.Uri;
25import android.net.Uri.Builder;
26import android.support.annotation.VisibleForTesting;
27import android.text.TextUtils;
28import android.view.View;
29import android.widget.ImageView;
30import android.widget.QuickContactBadge;
31import com.android.contacts.common.lettertiles.LetterTileDrawable;
32import com.android.contacts.common.util.UriUtils;
33import com.android.dialer.common.LogUtil;
34import com.android.dialer.util.PermissionsUtil;
35
36/** Asynchronously loads contact photos and maintains a cache of photos. */
37public 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 sInstance;
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   */
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   */
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   */
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   */
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
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
181  public static ContactPhotoManager getInstance(Context context) {
182    if (sInstance == null) {
183      Context applicationContext = context.getApplicationContext();
184      sInstance = createContactPhotoManager(applicationContext);
185      applicationContext.registerComponentCallbacks(sInstance);
186      if (PermissionsUtil.hasContactsReadPermissions(context)) {
187        sInstance.preloadPhotosInBackground();
188      }
189    }
190    return sInstance;
191  }
192
193  public static synchronized ContactPhotoManager createContactPhotoManager(Context context) {
194    return new ContactPhotoManagerImpl(context);
195  }
196
197  @VisibleForTesting
198  public static void injectContactPhotoManagerForTesting(ContactPhotoManager photoManager) {
199    sInstance = photoManager;
200  }
201
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   */
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   */
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
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   */
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   */
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   */
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   */
327  public abstract void removePhoto(ImageView view);
328
329  /** Cancels all pending requests to load photos asynchronously. */
330  public abstract void cancelPendingRequests(View fragmentRootView);
331
332  /** Temporarily stops loading photos from the database. */
333  public abstract void pause();
334
335  /** Resumes loading photos from the database. */
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   */
342  public abstract void refreshCache();
343
344  /** Initiates a background process that over time will fill up cache with preload photos. */
345  public abstract void preloadPhotosInBackground();
346
347  // ComponentCallbacks2
348  @Override
349  public void onConfigurationChanged(Configuration newConfig) {}
350
351  // ComponentCallbacks2
352  @Override
353  public void onLowMemory() {}
354
355  // ComponentCallbacks2
356  @Override
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
423    public DefaultImageRequest() {}
424
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
435    public DefaultImageRequest(
436        String displayName, String identifier, int contactType, boolean isCircular) {
437      this(displayName, identifier, contactType, SCALE_DEFAULT, OFFSET_DEFAULT, isCircular);
438    }
439
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     */
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
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
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