1 /*
2  * Copyright (C) 2018 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.dialer.glidephotomanager.impl;
18 
19 import android.content.ContentUris;
20 import android.content.Context;
21 import android.graphics.drawable.Drawable;
22 import android.net.Uri;
23 import android.provider.ContactsContract.Contacts;
24 import android.provider.ContactsContract.Data;
25 import android.support.annotation.MainThread;
26 import android.support.annotation.Nullable;
27 import android.telecom.TelecomManager;
28 import android.text.TextUtils;
29 import android.widget.ImageView;
30 import android.widget.QuickContactBadge;
31 import com.android.dialer.common.Assert;
32 import com.android.dialer.glide.GlideApp;
33 import com.android.dialer.glide.GlideRequest;
34 import com.android.dialer.glide.GlideRequests;
35 import com.android.dialer.glidephotomanager.GlidePhotoManager;
36 import com.android.dialer.glidephotomanager.PhotoInfo;
37 import com.android.dialer.i18n.DialerBidiFormatter;
38 import com.android.dialer.inject.ApplicationContext;
39 import com.android.dialer.lettertile.LetterTileDrawable;
40 import java.util.List;
41 import javax.inject.Inject;
42 
43 /** Implementation of {@link GlidePhotoManager} */
44 public class GlidePhotoManagerImpl implements GlidePhotoManager {
45 
46   private static final int LOOKUP_URI_PATH_SEGMENTS =
47       Contacts.CONTENT_LOOKUP_URI.getPathSegments().size();
48 
49   private final Context appContext;
50 
51   @Inject
GlidePhotoManagerImpl(@pplicationContext Context appContext)52   public GlidePhotoManagerImpl(@ApplicationContext Context appContext) {
53     this.appContext = appContext;
54   }
55 
56   @MainThread
57   @Override
loadQuickContactBadge(QuickContactBadge badge, PhotoInfo photoInfo)58   public void loadQuickContactBadge(QuickContactBadge badge, PhotoInfo photoInfo) {
59     Assert.isMainThread();
60     badge.assignContactUri(
61         TextUtils.isEmpty(photoInfo.getLookupUri())
62             ? DefaultLookupUriGenerator.generateUri(photoInfo)
63             : parseUri(photoInfo.getLookupUri()));
64     badge.setOverlay(null);
65     loadContactPhoto(badge, photoInfo);
66   }
67 
68   @MainThread
69   @Override
loadContactPhoto(ImageView imageView, PhotoInfo photoInfo)70   public void loadContactPhoto(ImageView imageView, PhotoInfo photoInfo) {
71     Assert.isMainThread();
72     imageView.setContentDescription(
73         TextUtils.expandTemplate(
74             appContext.getText(R.string.a11y_glide_photo_manager_contact_photo_description),
75             // The display name in "photoInfo" can be a contact name, a number, or a mixture of text
76             // and a phone number. We use DialerBidiFormatter to wrap the phone number with TTS
77             // span.
78             DialerBidiFormatter.format(photoInfo.getName())));
79     GlideRequest<Drawable> request = buildRequest(GlideApp.with(imageView), photoInfo);
80     request.into(imageView);
81   }
82 
buildRequest(GlideRequests requestManager, PhotoInfo photoInfo)83   private GlideRequest<Drawable> buildRequest(GlideRequests requestManager, PhotoInfo photoInfo) {
84     // Warning: Glide ignores extra attributes on BitmapDrawable such as tint and draw the bitmap
85     // directly so be sure not to set tint in the XML of any drawable referenced below.
86 
87     GlideRequest<Drawable> request;
88     boolean circleCrop = true; // Photos are cropped to a circle by default.
89 
90     if (photoInfo.getIsBlocked()) {
91       // Whether the number is blocked takes precedence over the spam status.
92       request = requestManager.load(R.drawable.ic_block_grey_48dp);
93 
94     } else if (photoInfo.getIsSpam()) {
95       request = requestManager.load(R.drawable.quantum_ic_report_vd_red_24);
96       circleCrop = false; // The spam icon is an octagon so we don't crop it.
97 
98     } else if (!TextUtils.isEmpty(photoInfo.getPhotoUri())) {
99       request = requestManager.load(parseUri(photoInfo.getPhotoUri()));
100 
101     } else if (photoInfo.getPhotoId() != 0) {
102       request =
103           requestManager.load(ContentUris.withAppendedId(Data.CONTENT_URI, photoInfo.getPhotoId()));
104 
105     } else {
106       // load null to indicate fallback should be used.
107       request = requestManager.load((Object) null);
108     }
109 
110     LetterTileDrawable defaultDrawable = getDefaultDrawable(photoInfo);
111     request
112         .placeholder(defaultDrawable) // when the photo is still loading.
113         .fallback(defaultDrawable); // when there's nothing to load.
114 
115     if (circleCrop) {
116       request.circleCrop();
117     }
118 
119     return request;
120   }
121 
122   /**
123    * Generate the default drawable when photos are not available. Used when the photo is loading or
124    * no photo is available.
125    */
getDefaultDrawable(PhotoInfo photoInfo)126   private LetterTileDrawable getDefaultDrawable(PhotoInfo photoInfo) {
127     LetterTileDrawable letterTileDrawable = new LetterTileDrawable(appContext.getResources());
128     String displayName;
129     String identifier;
130     if (TextUtils.isEmpty(photoInfo.getLookupUri())) {
131       // Use generic avatar instead of letter for non-contacts.
132       displayName = null;
133       identifier =
134           TextUtils.isEmpty(photoInfo.getName())
135               ? photoInfo.getFormattedNumber()
136               : photoInfo.getName();
137     } else {
138       displayName = photoInfo.getName();
139       identifier = getIdentifier(photoInfo.getLookupUri());
140     }
141     letterTileDrawable.setCanonicalDialerLetterTileDetails(
142         displayName,
143         identifier,
144         LetterTileDrawable.SHAPE_CIRCLE,
145         LetterTileDrawable.getContactTypeFromPrimitives(
146             photoInfo.getIsVoicemail(),
147             photoInfo.getIsSpam(),
148             photoInfo.getIsBusiness(),
149             TelecomManager.PRESENTATION_ALLOWED, // TODO(twyen):implement
150             photoInfo.getIsConference()));
151     return letterTileDrawable;
152   }
153 
154   @Nullable
parseUri(@ullable String uri)155   private static Uri parseUri(@Nullable String uri) {
156     return TextUtils.isEmpty(uri) ? null : Uri.parse(uri);
157   }
158 
159   /**
160    * Return the "lookup key" inside the lookup URI. If the URI does not contain the key (i.e, JSON
161    * based prepopulated URIs for non-contact entries), the URI itself is returned.
162    *
163    * <p>The lookup URI has the format of Contacts.CONTENT_LOOKUP_URI/lookupKey/rowId. For JSON based
164    * URI, it would be Contacts.CONTENT_LOOKUP_URI/encoded#JSON
165    */
getIdentifier(String lookupUri)166   private static String getIdentifier(String lookupUri) {
167     if (!lookupUri.startsWith(Contacts.CONTENT_LOOKUP_URI.toString())) {
168       return lookupUri;
169     }
170 
171     List<String> segments = Uri.parse(lookupUri).getPathSegments();
172     if (segments.size() < LOOKUP_URI_PATH_SEGMENTS) {
173       return lookupUri;
174     }
175     String lookupKey = segments.get(LOOKUP_URI_PATH_SEGMENTS);
176     if ("encoded".equals(lookupKey)) {
177       return lookupUri;
178     }
179     return lookupKey;
180   }
181 }
182