blob: 3d56e70cc57533210156fbe27a45a130a645c6cc [file] [log] [blame]
/*
* 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<String, Bitmap> sContactPhotoNumberCache;
private static LruCache<Long, Bitmap> sContactPhotoIdCache;
private static HashMap<String, String> sContactNameCache;
private static HashMap<String, Integer> sContactIdCache;
private static HashMap<String, String> sFormattedNumberCache;
private static HashMap<String, String> 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<String, Bitmap>(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<Long, Bitmap>(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);
}
}