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  /** 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   */
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   */
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   */
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   */
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
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
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
201  public static synchronized ContactPhotoManager createContactPhotoManager(Context context) {
202    return new ContactPhotoManagerImpl(context);
203  }
204
205  @VisibleForTesting
206  public static void injectContactPhotoManagerForTesting(ContactPhotoManager photoManager) {
207    sInstance = photoManager;
208  }
209
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   */
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   */
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
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   */
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   */
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   */
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   */
332  public abstract void removePhoto(ImageView view);
333
334  /** Cancels all pending requests to load photos asynchronously. */
335  public abstract void cancelPendingRequests(View fragmentRootView);
336
337  /** Temporarily stops loading photos from the database. */
338  public abstract void pause();
339
340  /** Resumes loading photos from the database. */
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   */
347  public abstract void refreshCache();
348
349  /** Initiates a background process that over time will fill up cache with preload photos. */
350  public abstract void preloadPhotosInBackground();
351
352  // ComponentCallbacks2
353  @Override
354  public void onConfigurationChanged(Configuration newConfig) {}
355
356  // ComponentCallbacks2
357  @Override
358  public void onLowMemory() {}
359
360  // ComponentCallbacks2
361  @Override
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
429    public DefaultImageRequest() {}
430
431    public DefaultImageRequest(String displayName, String identifier, boolean isCircular) {
432      this(displayName, identifier, TYPE_DEFAULT, SCALE_DEFAULT, OFFSET_DEFAULT, 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