Home | History | Annotate | Download | only in common
      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   /** 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