summaryrefslogtreecommitdiffstats
path: root/java/com/android/contacts/common/ContactPhotoManager.java
blob: 169348b25f17648fae2f56b96efc7bd5599efa76 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.contacts.common;

import android.content.ComponentCallbacks2;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.net.Uri.Builder;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.view.View;
import android.widget.ImageView;
import android.widget.QuickContactBadge;
import com.android.contacts.common.lettertiles.LetterTileDrawable;
import com.android.contacts.common.util.UriUtils;
import com.android.dialer.common.LogUtil;
import com.android.dialer.util.PermissionsUtil;

/** Asynchronously loads contact photos and maintains a cache of photos. */
public abstract class ContactPhotoManager implements ComponentCallbacks2 {

  /** Contact type constants used for default letter images */
  public static final int TYPE_PERSON = LetterTileDrawable.TYPE_PERSON;

  public static final int TYPE_SPAM = LetterTileDrawable.TYPE_SPAM;
  public static final int TYPE_BUSINESS = LetterTileDrawable.TYPE_BUSINESS;
  public static final int TYPE_VOICEMAIL = LetterTileDrawable.TYPE_VOICEMAIL;
  public static final int TYPE_DEFAULT = LetterTileDrawable.TYPE_DEFAULT;
  public static final int TYPE_GENERIC_AVATAR = LetterTileDrawable.TYPE_GENERIC_AVATAR;
  /** Scale and offset default constants used for default letter images */
  public static final float SCALE_DEFAULT = 1.0f;

  public static final float OFFSET_DEFAULT = 0.0f;
  public static final boolean IS_CIRCULAR_DEFAULT = false;
  // TODO: Use LogUtil.isVerboseEnabled for DEBUG branches instead of a lint check.
  // LINT.DoNotSubmitIf(true)
  static final boolean DEBUG = false;
  // LINT.DoNotSubmitIf(true)
  static final boolean DEBUG_SIZES = false;
  /** Uri-related constants used for default letter images */
  private static final String DISPLAY_NAME_PARAM_KEY = "display_name";

  private static final String IDENTIFIER_PARAM_KEY = "identifier";
  private static final String CONTACT_TYPE_PARAM_KEY = "contact_type";
  private static final String SCALE_PARAM_KEY = "scale";
  private static final String OFFSET_PARAM_KEY = "offset";
  private static final String IS_CIRCULAR_PARAM_KEY = "is_circular";
  private static final String DEFAULT_IMAGE_URI_SCHEME = "defaultimage";
  private static final Uri DEFAULT_IMAGE_URI = Uri.parse(DEFAULT_IMAGE_URI_SCHEME + "://");
  public static final DefaultImageProvider DEFAULT_AVATAR = new LetterTileDefaultImageProvider();
  private static ContactPhotoManager sInstance;

  /**
   * Given a {@link DefaultImageRequest}, returns an Uri that can be used to request a letter tile
   * avatar when passed to the {@link ContactPhotoManager}. The internal implementation of this uri
   * is not guaranteed to remain the same across application versions, so the actual uri should
   * never be persisted in long-term storage and reused.
   *
   * @param request A {@link DefaultImageRequest} object with the fields configured to return a
   * @return A Uri that when later passed to the {@link ContactPhotoManager} via {@link
   *     #loadPhoto(ImageView, Uri, int, boolean, boolean, DefaultImageRequest)}, can be used to
   *     request a default contact image, drawn as a letter tile using the parameters as configured
   *     in the provided {@link DefaultImageRequest}
   */
  public static Uri getDefaultAvatarUriForContact(DefaultImageRequest request) {
    final Builder builder = DEFAULT_IMAGE_URI.buildUpon();
    if (request != null) {
      if (!TextUtils.isEmpty(request.displayName)) {
        builder.appendQueryParameter(DISPLAY_NAME_PARAM_KEY, request.displayName);
      }
      if (!TextUtils.isEmpty(request.identifier)) {
        builder.appendQueryParameter(IDENTIFIER_PARAM_KEY, request.identifier);
      }
      if (request.contactType != TYPE_DEFAULT) {
        builder.appendQueryParameter(CONTACT_TYPE_PARAM_KEY, String.valueOf(request.contactType));
      }
      if (request.scale != SCALE_DEFAULT) {
        builder.appendQueryParameter(SCALE_PARAM_KEY, String.valueOf(request.scale));
      }
      if (request.offset != OFFSET_DEFAULT) {
        builder.appendQueryParameter(OFFSET_PARAM_KEY, String.valueOf(request.offset));
      }
      if (request.isCircular != IS_CIRCULAR_DEFAULT) {
        builder.appendQueryParameter(IS_CIRCULAR_PARAM_KEY, String.valueOf(request.isCircular));
      }
    }
    return builder.build();
  }

  /**
   * Adds a business contact type encoded fragment to the URL. Used to ensure photo URLS from Nearby
   * Places can be identified as business photo URLs rather than URLs for personal contact photos.
   *
   * @param photoUrl The photo URL to modify.
   * @return URL with the contact type parameter added and set to TYPE_BUSINESS.
   */
  public static String appendBusinessContactType(String photoUrl) {
    Uri uri = Uri.parse(photoUrl);
    Builder builder = uri.buildUpon();
    builder.encodedFragment(String.valueOf(TYPE_BUSINESS));
    return builder.build().toString();
  }

  /**
   * Removes the contact type information stored in the photo URI encoded fragment.
   *
   * @param photoUri The photo URI to remove the contact type from.
   * @return The photo URI with contact type removed.
   */
  public static Uri removeContactType(Uri photoUri) {
    String encodedFragment = photoUri.getEncodedFragment();
    if (!TextUtils.isEmpty(encodedFragment)) {
      Builder builder = photoUri.buildUpon();
      builder.encodedFragment(null);
      return builder.build();
    }
    return photoUri;
  }

  /**
   * Inspects a photo URI to determine if the photo URI represents a business.
   *
   * @param photoUri The URI to inspect.
   * @return Whether the URI represents a business photo or not.
   */
  public static boolean isBusinessContactUri(Uri photoUri) {
    if (photoUri == null) {
      return false;
    }

    String encodedFragment = photoUri.getEncodedFragment();
    return !TextUtils.isEmpty(encodedFragment)
        && encodedFragment.equals(String.valueOf(TYPE_BUSINESS));
  }

  protected static DefaultImageRequest getDefaultImageRequestFromUri(Uri uri) {
    final DefaultImageRequest request =
        new DefaultImageRequest(
            uri.getQueryParameter(DISPLAY_NAME_PARAM_KEY),
            uri.getQueryParameter(IDENTIFIER_PARAM_KEY),
            false);
    try {
      String contactType = uri.getQueryParameter(CONTACT_TYPE_PARAM_KEY);
      if (!TextUtils.isEmpty(contactType)) {
        request.contactType = Integer.valueOf(contactType);
      }

      String scale = uri.getQueryParameter(SCALE_PARAM_KEY);
      if (!TextUtils.isEmpty(scale)) {
        request.scale = Float.valueOf(scale);
      }

      String offset = uri.getQueryParameter(OFFSET_PARAM_KEY);
      if (!TextUtils.isEmpty(offset)) {
        request.offset = Float.valueOf(offset);
      }

      String isCircular = uri.getQueryParameter(IS_CIRCULAR_PARAM_KEY);
      if (!TextUtils.isEmpty(isCircular)) {
        request.isCircular = Boolean.valueOf(isCircular);
      }
    } catch (NumberFormatException e) {
      LogUtil.w(
          "ContactPhotoManager.getDefaultImageRequestFromUri",
          "Invalid DefaultImageRequest image parameters provided, ignoring and using "
              + "defaults.");
    }

    return request;
  }

  public static ContactPhotoManager getInstance(Context context) {
    if (sInstance == null) {
      Context applicationContext = context.getApplicationContext();
      sInstance = createContactPhotoManager(applicationContext);
      applicationContext.registerComponentCallbacks(sInstance);
      if (PermissionsUtil.hasContactsReadPermissions(context)) {
        sInstance.preloadPhotosInBackground();
      }
    }
    return sInstance;
  }

  public static synchronized ContactPhotoManager createContactPhotoManager(Context context) {
    return new ContactPhotoManagerImpl(context);
  }

  @VisibleForTesting
  public static void injectContactPhotoManagerForTesting(ContactPhotoManager photoManager) {
    sInstance = photoManager;
  }

  protected boolean isDefaultImageUri(Uri uri) {
    return DEFAULT_IMAGE_URI_SCHEME.equals(uri.getScheme());
  }

  /**
   * Load thumbnail image into the supplied image view. If the photo is already cached, it is
   * displayed immediately. Otherwise a request is sent to load the photo from the database.
   */
  public abstract void loadThumbnail(
      ImageView view,
      long photoId,
      boolean darkTheme,
      boolean isCircular,
      DefaultImageRequest defaultImageRequest,
      DefaultImageProvider defaultProvider);

  /**
   * Calls {@link #loadThumbnail(ImageView, long, boolean, boolean, DefaultImageRequest,
   * DefaultImageProvider)} using the {@link DefaultImageProvider} {@link #DEFAULT_AVATAR}.
   */
  public final void loadThumbnail(
      ImageView view,
      long photoId,
      boolean darkTheme,
      boolean isCircular,
      DefaultImageRequest defaultImageRequest) {
    loadThumbnail(view, photoId, darkTheme, isCircular, defaultImageRequest, DEFAULT_AVATAR);
  }

  public final void loadDialerThumbnailOrPhoto(
      QuickContactBadge badge,
      Uri contactUri,
      long photoId,
      Uri photoUri,
      String displayName,
      int contactType) {
    badge.assignContactUri(contactUri);
    badge.setOverlay(null);

    String lookupKey = contactUri == null ? null : UriUtils.getLookupKeyFromUri(contactUri);
    ContactPhotoManager.DefaultImageRequest request =
        new ContactPhotoManager.DefaultImageRequest(
            displayName, lookupKey, contactType, true /* isCircular */);
    if (photoId == 0 && photoUri != null) {
      loadDirectoryPhoto(badge, photoUri, false /* darkTheme */, true /* isCircular */, request);
    } else {
      loadThumbnail(badge, photoId, false /* darkTheme */, true /* isCircular */, request);
    }
  }

  /**
   * Load photo into the supplied image view. If the photo is already cached, it is displayed
   * immediately. Otherwise a request is sent to load the photo from the location specified by the
   * URI.
   *
   * @param view The target view
   * @param photoUri The uri of the photo to load
   * @param requestedExtent Specifies an approximate Max(width, height) of the targetView. This is
   *     useful if the source image can be a lot bigger that the target, so that the decoding is
   *     done using efficient sampling. If requestedExtent is specified, no sampling of the image is
   *     performed
   * @param darkTheme Whether the background is dark. This is used for default avatars
   * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default
   *     letter tile avatar should be drawn.
   * @param defaultProvider The provider of default avatars (this is used if photoUri doesn't refer
   *     to an existing image)
   */
  public abstract void loadPhoto(
      ImageView view,
      Uri photoUri,
      int requestedExtent,
      boolean darkTheme,
      boolean isCircular,
      DefaultImageRequest defaultImageRequest,
      DefaultImageProvider defaultProvider);

  /**
   * Calls {@link #loadPhoto(ImageView, Uri, int, boolean, boolean, DefaultImageRequest,
   * DefaultImageProvider)} with {@link #DEFAULT_AVATAR} and {@code null} display names and lookup
   * keys.
   *
   * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default
   *     letter tile avatar should be drawn.
   */
  public final void loadPhoto(
      ImageView view,
      Uri photoUri,
      int requestedExtent,
      boolean darkTheme,
      boolean isCircular,
      DefaultImageRequest defaultImageRequest) {
    loadPhoto(
        view,
        photoUri,
        requestedExtent,
        darkTheme,
        isCircular,
        defaultImageRequest,
        DEFAULT_AVATAR);
  }

  /**
   * Calls {@link #loadPhoto(ImageView, Uri, int, boolean, boolean, DefaultImageRequest,
   * DefaultImageProvider)} with {@link #DEFAULT_AVATAR} and with the assumption, that the image is
   * a thumbnail.
   *
   * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default
   *     letter tile avatar should be drawn.
   */
  public final void loadDirectoryPhoto(
      ImageView view,
      Uri photoUri,
      boolean darkTheme,
      boolean isCircular,
      DefaultImageRequest defaultImageRequest) {
    loadPhoto(view, photoUri, -1, darkTheme, isCircular, defaultImageRequest, DEFAULT_AVATAR);
  }

  /**
   * Remove photo from the supplied image view. This also cancels current pending load request
   * inside this photo manager.
   */
  public abstract void removePhoto(ImageView view);

  /** Cancels all pending requests to load photos asynchronously. */
  public abstract void cancelPendingRequests(View fragmentRootView);

  /** Temporarily stops loading photos from the database. */
  public abstract void pause();

  /** Resumes loading photos from the database. */
  public abstract void resume();

  /**
   * Marks all cached photos for reloading. We can continue using cache but should also make sure
   * the photos haven't changed in the background and notify the views if so.
   */
  public abstract void refreshCache();

  /** Initiates a background process that over time will fill up cache with preload photos. */
  public abstract void preloadPhotosInBackground();

  // ComponentCallbacks2
  @Override
  public void onConfigurationChanged(Configuration newConfig) {}

  // ComponentCallbacks2
  @Override
  public void onLowMemory() {}

  // ComponentCallbacks2
  @Override
  public void onTrimMemory(int level) {}

  /**
   * Contains fields used to contain contact details and other user-defined settings that might be
   * used by the ContactPhotoManager to generate a default contact image. This contact image takes
   * the form of a letter or bitmap drawn on top of a colored tile.
   */
  public static class DefaultImageRequest {

    /**
     * Used to indicate that a drawable that represents a contact without any contact details should
     * be returned.
     */
    public static final DefaultImageRequest EMPTY_DEFAULT_IMAGE_REQUEST = new DefaultImageRequest();
    /**
     * Used to indicate that a drawable that represents a business without a business photo should
     * be returned.
     */
    public static final DefaultImageRequest EMPTY_DEFAULT_BUSINESS_IMAGE_REQUEST =
        new DefaultImageRequest(null, null, TYPE_BUSINESS, false);
    /**
     * Used to indicate that a circular drawable that represents a contact without any contact
     * details should be returned.
     */
    public static final DefaultImageRequest EMPTY_CIRCULAR_DEFAULT_IMAGE_REQUEST =
        new DefaultImageRequest(null, null, true);
    /**
     * Used to indicate that a circular drawable that represents a business without a business photo
     * should be returned.
     */
    public static final DefaultImageRequest EMPTY_CIRCULAR_BUSINESS_IMAGE_REQUEST =
        new DefaultImageRequest(null, null, TYPE_BUSINESS, true);
    /** The contact's display name. The display name is used to */
    public String displayName;
    /**
     * A unique and deterministic string that can be used to identify this contact. This is usually
     * the contact's lookup key, but other contact details can be used as well, especially for
     * non-local or temporary contacts that might not have a lookup key. This is used to determine
     * the color of the tile.
     */
    public String identifier;
    /**
     * The type of this contact. This contact type may be used to decide the kind of image to use in
     * the case where a unique letter cannot be generated from the contact's display name and
     * identifier. See: {@link #TYPE_PERSON} {@link #TYPE_BUSINESS} {@link #TYPE_PERSON} {@link
     * #TYPE_DEFAULT}
     */
    public int contactType = TYPE_DEFAULT;
    /**
     * The amount to scale the letter or bitmap to, as a ratio of its default size (from a range of
     * 0.0f to 2.0f). The default value is 1.0f.
     */
    public float scale = SCALE_DEFAULT;
    /**
     * The amount to vertically offset the letter or image to within the tile. The provided offset
     * must be within the range of -0.5f to 0.5f. If set to -0.5f, the letter will be shifted
     * upwards by 0.5 times the height of the canvas it is being drawn on, which means it will be
     * drawn with the center of the letter starting at the top edge of the canvas. If set to 0.5f,
     * the letter will be shifted downwards by 0.5 times the height of the canvas it is being drawn
     * on, which means it will be drawn with the center of the letter starting at the bottom edge of
     * the canvas. The default is 0.0f, which means the letter is drawn in the exact vertical center
     * of the tile.
     */
    public float offset = OFFSET_DEFAULT;
    /** Whether or not to draw the default image as a circle, instead of as a square/rectangle. */
    public boolean isCircular = false;

    public DefaultImageRequest() {}

    public DefaultImageRequest(String displayName, String identifier, boolean isCircular) {
      this(displayName, identifier, TYPE_DEFAULT, SCALE_DEFAULT, OFFSET_DEFAULT, isCircular);
    }

    public DefaultImageRequest(
        String displayName, String identifier, int contactType, boolean isCircular) {
      this(displayName, identifier, contactType, SCALE_DEFAULT, OFFSET_DEFAULT, isCircular);
    }

    public DefaultImageRequest(
        String displayName,
        String identifier,
        int contactType,
        float scale,
        float offset,
        boolean isCircular) {
      this.displayName = displayName;
      this.identifier = identifier;
      this.contactType = contactType;
      this.scale = scale;
      this.offset = offset;
      this.isCircular = isCircular;
    }
  }

  public abstract static class DefaultImageProvider {

    /**
     * Applies the default avatar to the ImageView. Extent is an indicator for the size (width or
     * height). If darkTheme is set, the avatar is one that looks better on dark background
     *
     * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default
     *     letter tile avatar should be drawn.
     */
    public abstract void applyDefaultImage(
        ImageView view, int extent, boolean darkTheme, DefaultImageRequest defaultImageRequest);
  }

  /**
   * A default image provider that applies a letter tile consisting of a colored background and a
   * letter in the foreground as the default image for a contact. The color of the background and
   * the type of letter is decided based on the contact's details.
   */
  private static class LetterTileDefaultImageProvider extends DefaultImageProvider {

    public static Drawable getDefaultImageForContact(
        Resources resources, DefaultImageRequest defaultImageRequest) {
      final LetterTileDrawable drawable = new LetterTileDrawable(resources);
      final int tileShape =
          defaultImageRequest.isCircular
              ? LetterTileDrawable.SHAPE_CIRCLE
              : LetterTileDrawable.SHAPE_RECTANGLE;
      if (defaultImageRequest != null) {
        // If the contact identifier is null or empty, fallback to the
        // displayName. In that case, use {@code null} for the contact's
        // display name so that a default bitmap will be used instead of a
        // letter
        if (TextUtils.isEmpty(defaultImageRequest.identifier)) {
          drawable.setCanonicalDialerLetterTileDetails(
              null, defaultImageRequest.displayName, tileShape, defaultImageRequest.contactType);
        } else {
          drawable.setCanonicalDialerLetterTileDetails(
              defaultImageRequest.displayName,
              defaultImageRequest.identifier,
              tileShape,
              defaultImageRequest.contactType);
        }
        drawable.setScale(defaultImageRequest.scale);
        drawable.setOffset(defaultImageRequest.offset);
      }
      return drawable;
    }

    @Override
    public void applyDefaultImage(
        ImageView view, int extent, boolean darkTheme, DefaultImageRequest defaultImageRequest) {
      final Drawable drawable = getDefaultImageForContact(view.getResources(), defaultImageRequest);
      view.setImageDrawable(drawable);
    }
  }
}