/* * Copyright (c) 2016, 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.car.stream.telecom; import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; import android.content.res.Resources; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Rect; import android.net.Uri; import android.provider.ContactsContract; import android.support.annotation.Nullable; import android.support.annotation.WorkerThread; import android.telecom.Call; import android.telecom.GatewayInfo; import android.telephony.PhoneNumberUtils; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.LruCache; import com.android.car.apps.common.CircleBitmapDrawable; import com.android.car.apps.common.LetterTileDrawable; import com.android.car.stream.R; import java.io.InputStream; import java.util.HashMap; import java.util.Locale; /** * Telecom related utility methods. */ public class TelecomUtils { private static final int LRU_CACHE_SIZE = 4194304; /** 4 mb **/ private static final String[] CONTACT_ID_PROJECTION = new String[] { ContactsContract.PhoneLookup.DISPLAY_NAME, ContactsContract.PhoneLookup.TYPE, ContactsContract.PhoneLookup.LABEL, ContactsContract.PhoneLookup._ID }; private static String sVoicemailNumber; private static LruCache sContactPhotoNumberCache; private static LruCache sContactPhotoIdCache; private static HashMap sContactNameCache; private static HashMap sContactIdCache; private static HashMap sFormattedNumberCache; private static HashMap sDisplayNameCache; /** * Create a round bitmap icon to represent the call. If a contact photo does not exist, * a letter tile will be used instead. */ public static Bitmap createStreamCardSecondaryIcon(Context context, String number) { Resources res = context.getResources(); Bitmap largeIcon = TelecomUtils.getContactPhotoFromNumber(context.getContentResolver(), number); if (largeIcon == null) { LetterTileDrawable ltd = new LetterTileDrawable(res); String name = TelecomUtils.getDisplayName(context, number); ltd.setContactDetails(name, number); ltd.setIsCircular(true); int size = res.getDimensionPixelSize(R.dimen.stream_card_secondary_icon_dimen); largeIcon = ltd.toBitmap(size); } return new CircleBitmapDrawable(res, largeIcon) .toBitmap(res.getDimensionPixelSize(R.dimen.stream_card_secondary_icon_dimen)); } /** * Fetch contact photo by number from local cache. * * @param number * @return Contact photo if it's in the cache, otherwise null. */ @Nullable public static Bitmap getCachedContactPhotoFromNumber(String number) { if (number == null) { return null; } if (sContactPhotoNumberCache == null) { sContactPhotoNumberCache = new LruCache(LRU_CACHE_SIZE) { @Override protected int sizeOf(String key, Bitmap value) { return value.getByteCount(); } }; } return sContactPhotoNumberCache.get(number); } @WorkerThread public static Bitmap getContactPhotoFromNumber(ContentResolver contentResolver, String number) { if (number == null) { return null; } Bitmap photo = getCachedContactPhotoFromNumber(number); if (photo != null) { return photo; } int id = getContactIdFromNumber(contentResolver, number); if (id == 0) { return null; } photo = getContactPhotoFromId(contentResolver, id); if (photo != null) { sContactPhotoNumberCache.put(number, photo); } return photo; } /** * Return the contact id for the given contact id * @param id the contact id to get the photo for * @return the contact photo if it is found, null otherwise. */ public static Bitmap getContactPhotoFromId(ContentResolver contentResolver, long id) { if (sContactPhotoIdCache == null) { sContactPhotoIdCache = new LruCache(LRU_CACHE_SIZE) { @Override protected int sizeOf(Long key, Bitmap value) { return value.getByteCount(); } }; } else if (sContactPhotoIdCache.get(id) != null) { return sContactPhotoIdCache.get(id); } Uri photoUri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, id); InputStream photoDataStream = ContactsContract.Contacts.openContactPhotoInputStream( contentResolver, photoUri, true); BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferQualityOverSpeed = true; // Scaling will be handled by later. We shouldn't scale multiple times to avoid // quality lost due to multiple potential scaling up and down. options.inScaled = false; Rect nullPadding = null; Bitmap photo = BitmapFactory.decodeStream(photoDataStream, nullPadding, options); if (photo != null) { photo.setDensity(Bitmap.DENSITY_NONE); sContactPhotoIdCache.put(id, photo); } return photo; } /** * Return the contact id for the given phone number. * @param number Caller phone number * @return the contact id if it is found, 0 otherwise. */ public static int getContactIdFromNumber(ContentResolver cr, String number) { if (number == null || number.isEmpty()) { return 0; } if (sContactIdCache == null) { sContactIdCache = new HashMap<>(); } else if (sContactIdCache.containsKey(number)) { return sContactIdCache.get(number); } Uri uri = Uri.withAppendedPath( ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)); Cursor cursor = cr.query(uri, CONTACT_ID_PROJECTION, null, null, null); try { if (cursor != null && cursor.moveToFirst()) { int id = cursor.getInt(cursor.getColumnIndex(ContactsContract.PhoneLookup._ID)); sContactIdCache.put(number, id); return id; } } finally { if (cursor != null) { cursor.close(); } } return 0; } public static String getDisplayName(Context context, String number) { return getDisplayName(context, number, (Uri)null); } public static String getDisplayName(Context context, Call call) { // A call might get created before its children are added. In that case, the display name // would go from "Unknown" to "Conference call" therefore we don't want to cache it. if (call.getChildren() != null && call.getChildren().size() > 0) { return context.getString(R.string.conference_call); } return getDisplayName(context, getNumber(call), getGatewayInfoOriginalAddress(call)); } private static Uri getGatewayInfoOriginalAddress(Call call) { if (call == null || call.getDetails() == null) { return null; } GatewayInfo gatewayInfo = call.getDetails().getGatewayInfo(); if (gatewayInfo != null && gatewayInfo.getOriginalAddress() != null) { return gatewayInfo.getGatewayAddress(); } return null; } /** * Return the phone number of the call. This CAN return null under certain circumstances such * as if the incoming number is hidden. */ public static String getNumber(Call call) { if (call == null || call.getDetails() == null) { return null; } Uri gatewayInfoOriginalAddress = getGatewayInfoOriginalAddress(call); if (gatewayInfoOriginalAddress != null) { return gatewayInfoOriginalAddress.getSchemeSpecificPart(); } if (call.getDetails().getHandle() != null) { return call.getDetails().getHandle().getSchemeSpecificPart(); } return null; } private static String getContactNameFromNumber(ContentResolver cr, String number) { if (sContactNameCache == null) { sContactNameCache = new HashMap<>(); } else if (sContactNameCache.containsKey(number)) { return sContactNameCache.get(number); } Uri uri = Uri.withAppendedPath( ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)); Cursor cursor = null; String name = null; try { cursor = cr.query(uri, new String[] {ContactsContract.PhoneLookup.DISPLAY_NAME}, null, null, null); if (cursor != null && cursor.moveToFirst()) { name = cursor.getString(0); sContactNameCache.put(number, name); } } finally { if (cursor != null) { cursor.close(); } } return name; } private static String getDisplayName( Context context, String number, Uri gatewayOriginalAddress) { if (sDisplayNameCache == null) { sDisplayNameCache = new HashMap<>(); } else { if (sDisplayNameCache.containsKey(number)) { return sDisplayNameCache.get(number); } } if (TextUtils.isEmpty(number)) { return context.getString(R.string.unknown); } ContentResolver cr = context.getContentResolver(); String name; if (number.equals(getVoicemailNumber(context))) { name = context.getString(R.string.voicemail); } else { name = getContactNameFromNumber(cr, number); } if (name == null) { name = getFormattedNumber(context, number); } if (name == null && gatewayOriginalAddress != null) { name = gatewayOriginalAddress.getSchemeSpecificPart(); } if (name == null) { name = context.getString(R.string.unknown); } sDisplayNameCache.put(number, name); return name; } public static String getVoicemailNumber(Context context) { if (sVoicemailNumber == null) { TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); sVoicemailNumber = tm.getVoiceMailNumber(); } return sVoicemailNumber; } public static String getFormattedNumber(Context context, @Nullable String number) { if (TextUtils.isEmpty(number)) { return ""; } if (sFormattedNumberCache == null) { sFormattedNumberCache = new HashMap<>(); } else { if (sFormattedNumberCache.containsKey(number)) { return sFormattedNumberCache.get(number); } } String countryIso = getSimRegionCode(context); String e164 = PhoneNumberUtils.formatNumberToE164(number, countryIso); String formattedNumber = PhoneNumberUtils.formatNumber(number, e164, countryIso); formattedNumber = TextUtils.isEmpty(formattedNumber) ? number : formattedNumber; sFormattedNumberCache.put(number, formattedNumber); return formattedNumber; } /** * Wrapper around TelephonyManager.getSimCountryIso() that will fallback to locale or USA ISOs * if it finds bogus data. */ private static String getSimRegionCode(Context context) { TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); // This can be null on some phones (and is null on robolectric default TelephonyManager) String countryIso = telephonyManager.getSimCountryIso(); if (TextUtils.isEmpty(countryIso) || countryIso.length() != 2) { countryIso = Locale.getDefault().getCountry(); if (countryIso == null || countryIso.length() != 2) { countryIso = "US"; } } return countryIso.toUpperCase(Locale.US); } }