1 /* 2 * Copyright (c) 2016, 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 package com.android.car.stream.telecom; 17 18 import android.content.ContentResolver; 19 import android.content.ContentUris; 20 import android.content.Context; 21 import android.content.res.Resources; 22 import android.database.Cursor; 23 import android.graphics.Bitmap; 24 import android.graphics.BitmapFactory; 25 import android.graphics.Rect; 26 import android.net.Uri; 27 import android.provider.ContactsContract; 28 import android.support.annotation.Nullable; 29 import android.support.annotation.WorkerThread; 30 import android.telecom.Call; 31 import android.telecom.GatewayInfo; 32 import android.telephony.PhoneNumberUtils; 33 import android.telephony.TelephonyManager; 34 import android.text.TextUtils; 35 import android.util.LruCache; 36 import com.android.car.apps.common.CircleBitmapDrawable; 37 import com.android.car.apps.common.LetterTileDrawable; 38 import com.android.car.stream.R; 39 40 import java.io.InputStream; 41 import java.util.HashMap; 42 import java.util.Locale; 43 44 /** 45 * Telecom related utility methods. 46 */ 47 public class TelecomUtils { 48 private static final int LRU_CACHE_SIZE = 4194304; /** 4 mb **/ 49 50 private static final String[] CONTACT_ID_PROJECTION = new String[] { 51 ContactsContract.PhoneLookup.DISPLAY_NAME, 52 ContactsContract.PhoneLookup.TYPE, 53 ContactsContract.PhoneLookup.LABEL, 54 ContactsContract.PhoneLookup._ID 55 }; 56 57 private static String sVoicemailNumber; 58 59 private static LruCache<String, Bitmap> sContactPhotoNumberCache; 60 private static LruCache<Long, Bitmap> sContactPhotoIdCache; 61 private static HashMap<String, String> sContactNameCache; 62 private static HashMap<String, Integer> sContactIdCache; 63 private static HashMap<String, String> sFormattedNumberCache; 64 private static HashMap<String, String> sDisplayNameCache; 65 66 /** 67 * Create a round bitmap icon to represent the call. If a contact photo does not exist, 68 * a letter tile will be used instead. 69 */ 70 public static Bitmap createStreamCardSecondaryIcon(Context context, String number) { 71 Resources res = context.getResources(); 72 Bitmap largeIcon 73 = TelecomUtils.getContactPhotoFromNumber(context.getContentResolver(), number); 74 if (largeIcon == null) { 75 LetterTileDrawable ltd = new LetterTileDrawable(res); 76 String name = TelecomUtils.getDisplayName(context, number); 77 ltd.setContactDetails(name, number); 78 ltd.setIsCircular(true); 79 int size = res.getDimensionPixelSize(R.dimen.stream_card_secondary_icon_dimen); 80 largeIcon = ltd.toBitmap(size); 81 } 82 83 return new CircleBitmapDrawable(res, largeIcon) 84 .toBitmap(res.getDimensionPixelSize(R.dimen.stream_card_secondary_icon_dimen)); 85 } 86 87 88 /** 89 * Fetch contact photo by number from local cache. 90 * 91 * @param number 92 * @return Contact photo if it's in the cache, otherwise null. 93 */ 94 @Nullable 95 public static Bitmap getCachedContactPhotoFromNumber(String number) { 96 if (number == null) { 97 return null; 98 } 99 100 if (sContactPhotoNumberCache == null) { 101 sContactPhotoNumberCache = new LruCache<String, Bitmap>(LRU_CACHE_SIZE) { 102 @Override 103 protected int sizeOf(String key, Bitmap value) { 104 return value.getByteCount(); 105 } 106 }; 107 } 108 return sContactPhotoNumberCache.get(number); 109 } 110 111 @WorkerThread 112 public static Bitmap getContactPhotoFromNumber(ContentResolver contentResolver, String number) { 113 if (number == null) { 114 return null; 115 } 116 117 Bitmap photo = getCachedContactPhotoFromNumber(number); 118 if (photo != null) { 119 return photo; 120 } 121 122 int id = getContactIdFromNumber(contentResolver, number); 123 if (id == 0) { 124 return null; 125 } 126 photo = getContactPhotoFromId(contentResolver, id); 127 if (photo != null) { 128 sContactPhotoNumberCache.put(number, photo); 129 } 130 return photo; 131 } 132 133 /** 134 * Return the contact id for the given contact id 135 * @param id the contact id to get the photo for 136 * @return the contact photo if it is found, null otherwise. 137 */ 138 public static Bitmap getContactPhotoFromId(ContentResolver contentResolver, long id) { 139 if (sContactPhotoIdCache == null) { 140 sContactPhotoIdCache = new LruCache<Long, Bitmap>(LRU_CACHE_SIZE) { 141 @Override 142 protected int sizeOf(Long key, Bitmap value) { 143 return value.getByteCount(); 144 } 145 }; 146 } else if (sContactPhotoIdCache.get(id) != null) { 147 return sContactPhotoIdCache.get(id); 148 } 149 150 Uri photoUri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, id); 151 InputStream photoDataStream = ContactsContract.Contacts.openContactPhotoInputStream( 152 contentResolver, photoUri, true); 153 154 BitmapFactory.Options options = new BitmapFactory.Options(); 155 options.inPreferQualityOverSpeed = true; 156 // Scaling will be handled by later. We shouldn't scale multiple times to avoid 157 // quality lost due to multiple potential scaling up and down. 158 options.inScaled = false; 159 160 Rect nullPadding = null; 161 Bitmap photo = BitmapFactory.decodeStream(photoDataStream, nullPadding, options); 162 if (photo != null) { 163 photo.setDensity(Bitmap.DENSITY_NONE); 164 sContactPhotoIdCache.put(id, photo); 165 } 166 return photo; 167 } 168 169 /** 170 * Return the contact id for the given phone number. 171 * @param number Caller phone number 172 * @return the contact id if it is found, 0 otherwise. 173 */ 174 public static int getContactIdFromNumber(ContentResolver cr, String number) { 175 if (number == null || number.isEmpty()) { 176 return 0; 177 } 178 if (sContactIdCache == null) { 179 sContactIdCache = new HashMap<>(); 180 } else if (sContactIdCache.containsKey(number)) { 181 return sContactIdCache.get(number); 182 } 183 184 Uri uri = Uri.withAppendedPath( 185 ContactsContract.PhoneLookup.CONTENT_FILTER_URI, 186 Uri.encode(number)); 187 Cursor cursor = cr.query(uri, CONTACT_ID_PROJECTION, null, null, null); 188 189 try { 190 if (cursor != null && cursor.moveToFirst()) { 191 int id = cursor.getInt(cursor.getColumnIndex(ContactsContract.PhoneLookup._ID)); 192 sContactIdCache.put(number, id); 193 return id; 194 } 195 } 196 finally { 197 if (cursor != null) { 198 cursor.close(); 199 } 200 } 201 return 0; 202 } 203 204 public static String getDisplayName(Context context, String number) { 205 return getDisplayName(context, number, (Uri)null); 206 } 207 208 public static String getDisplayName(Context context, Call call) { 209 // A call might get created before its children are added. In that case, the display name 210 // would go from "Unknown" to "Conference call" therefore we don't want to cache it. 211 if (call.getChildren() != null && call.getChildren().size() > 0) { 212 return context.getString(R.string.conference_call); 213 } 214 return getDisplayName(context, getNumber(call), getGatewayInfoOriginalAddress(call)); 215 } 216 217 private static Uri getGatewayInfoOriginalAddress(Call call) { 218 if (call == null || call.getDetails() == null) { 219 return null; 220 } 221 GatewayInfo gatewayInfo = call.getDetails().getGatewayInfo(); 222 223 if (gatewayInfo != null && gatewayInfo.getOriginalAddress() != null) { 224 return gatewayInfo.getGatewayAddress(); 225 } 226 return null; 227 } 228 229 /** 230 * Return the phone number of the call. This CAN return null under certain circumstances such 231 * as if the incoming number is hidden. 232 */ 233 public static String getNumber(Call call) { 234 if (call == null || call.getDetails() == null) { 235 return null; 236 } 237 238 Uri gatewayInfoOriginalAddress = getGatewayInfoOriginalAddress(call); 239 if (gatewayInfoOriginalAddress != null) { 240 return gatewayInfoOriginalAddress.getSchemeSpecificPart(); 241 } 242 243 if (call.getDetails().getHandle() != null) { 244 return call.getDetails().getHandle().getSchemeSpecificPart(); 245 } 246 return null; 247 } 248 249 private static String getContactNameFromNumber(ContentResolver cr, String number) { 250 if (sContactNameCache == null) { 251 sContactNameCache = new HashMap<>(); 252 } else if (sContactNameCache.containsKey(number)) { 253 return sContactNameCache.get(number); 254 } 255 256 Uri uri = Uri.withAppendedPath( 257 ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)); 258 259 Cursor cursor = null; 260 String name = null; 261 try { 262 cursor = cr.query(uri, 263 new String[] {ContactsContract.PhoneLookup.DISPLAY_NAME}, null, null, null); 264 if (cursor != null && cursor.moveToFirst()) { 265 name = cursor.getString(0); 266 sContactNameCache.put(number, name); 267 } 268 } finally { 269 if (cursor != null) { 270 cursor.close(); 271 } 272 } 273 return name; 274 } 275 276 private static String getDisplayName( 277 Context context, String number, Uri gatewayOriginalAddress) { 278 if (sDisplayNameCache == null) { 279 sDisplayNameCache = new HashMap<>(); 280 } else { 281 if (sDisplayNameCache.containsKey(number)) { 282 return sDisplayNameCache.get(number); 283 } 284 } 285 286 if (TextUtils.isEmpty(number)) { 287 return context.getString(R.string.unknown); 288 } 289 ContentResolver cr = context.getContentResolver(); 290 String name; 291 if (number.equals(getVoicemailNumber(context))) { 292 name = context.getString(R.string.voicemail); 293 } else { 294 name = getContactNameFromNumber(cr, number); 295 } 296 297 if (name == null) { 298 name = getFormattedNumber(context, number); 299 } 300 if (name == null && gatewayOriginalAddress != null) { 301 name = gatewayOriginalAddress.getSchemeSpecificPart(); 302 } 303 if (name == null) { 304 name = context.getString(R.string.unknown); 305 } 306 sDisplayNameCache.put(number, name); 307 return name; 308 } 309 310 public static String getVoicemailNumber(Context context) { 311 if (sVoicemailNumber == null) { 312 TelephonyManager tm = 313 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); 314 sVoicemailNumber = tm.getVoiceMailNumber(); 315 } 316 return sVoicemailNumber; 317 } 318 319 public static String getFormattedNumber(Context context, @Nullable String number) { 320 if (TextUtils.isEmpty(number)) { 321 return ""; 322 } 323 324 if (sFormattedNumberCache == null) { 325 sFormattedNumberCache = new HashMap<>(); 326 } else { 327 if (sFormattedNumberCache.containsKey(number)) { 328 return sFormattedNumberCache.get(number); 329 } 330 } 331 332 String countryIso = getSimRegionCode(context); 333 String e164 = PhoneNumberUtils.formatNumberToE164(number, countryIso); 334 String formattedNumber = PhoneNumberUtils.formatNumber(number, e164, countryIso); 335 formattedNumber = TextUtils.isEmpty(formattedNumber) ? number : formattedNumber; 336 sFormattedNumberCache.put(number, formattedNumber); 337 return formattedNumber; 338 } 339 340 /** 341 * Wrapper around TelephonyManager.getSimCountryIso() that will fallback to locale or USA ISOs 342 * if it finds bogus data. 343 */ 344 private static String getSimRegionCode(Context context) { 345 TelephonyManager telephonyManager = 346 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); 347 348 // This can be null on some phones (and is null on robolectric default TelephonyManager) 349 String countryIso = telephonyManager.getSimCountryIso(); 350 if (TextUtils.isEmpty(countryIso) || countryIso.length() != 2) { 351 countryIso = Locale.getDefault().getCountry(); 352 if (countryIso == null || countryIso.length() != 2) { 353 countryIso = "US"; 354 } 355 } 356 357 return countryIso.toUpperCase(Locale.US); 358 } 359 }