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