blob: f3a2b0e3e3a541fbbe414ede9d1767cf57caa3f0 [file] [log] [blame]
/*
* Copyright (C) 2019 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.telephony.common;
import android.Manifest;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.database.Cursor;
import android.net.Uri;
import android.provider.CallLog;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.PhoneLookup;
import android.provider.Settings;
import android.telecom.Call;
import android.telephony.PhoneNumberUtils;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;
import android.widget.ImageView;
import androidx.annotation.Nullable;
import com.android.car.apps.common.LetterTileDrawable;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
import com.google.i18n.phonenumbers.NumberParseException;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import com.google.i18n.phonenumbers.Phonenumber;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.CompletableFuture;
/** Helper methods. */
public class TelecomUtils {
private static final String TAG = "CD.TelecomUtils";
private static String sVoicemailNumber;
private static TelephonyManager sTelephonyManager;
/**
* Get the voicemail number.
*/
public static String getVoicemailNumber(Context context) {
if (sVoicemailNumber == null) {
sVoicemailNumber = getTelephonyManager(context).getVoiceMailNumber();
}
return sVoicemailNumber;
}
/**
* Returns {@code true} if the given number is a voice mail number.
*
* @see TelephonyManager#getVoiceMailNumber()
*/
public static boolean isVoicemailNumber(Context context, String number) {
return !TextUtils.isEmpty(number) && number.equals(getVoicemailNumber(context));
}
/**
* Get the {@link TelephonyManager} instance.
*/
// TODO(deanh): remove this, getSystemService is not slow.
public static TelephonyManager getTelephonyManager(Context context) {
if (sTelephonyManager == null) {
sTelephonyManager =
(TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
}
return sTelephonyManager;
}
/**
* Format a number as a phone number.
*/
public static String getFormattedNumber(Context context, String number) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "getFormattedNumber: " + number);
}
if (number == null) {
return "";
}
String countryIso = getIsoDefaultCountryNumber(context);
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "PhoneNumberUtils.formatNumberToE16, number: "
+ number + ", country: " + countryIso);
}
String e164 = PhoneNumberUtils.formatNumberToE164(number, countryIso);
String formattedNumber = PhoneNumberUtils.formatNumber(number, e164, countryIso);
formattedNumber = TextUtils.isEmpty(formattedNumber) ? number : formattedNumber;
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "getFormattedNumber, result: " + formattedNumber);
}
return formattedNumber;
}
private static String getIsoDefaultCountryNumber(Context context) {
String countryIso = getTelephonyManager(context).getSimCountryIso().toUpperCase(Locale.US);
if (countryIso.length() != 2) {
countryIso = Locale.getDefault().getCountry();
if (countryIso == null || countryIso.length() != 2) {
countryIso = "US";
}
}
return countryIso;
}
/**
* Creates a new instance of {@link Phonenumber#Phonenumber} base on the given number and sim
* card country code. Returns {@code null} if the number in an invalid number.
*/
@Nullable
public static Phonenumber.PhoneNumber createI18nPhoneNumber(Context context, String number) {
try {
return PhoneNumberUtil.getInstance().parse(number, getIsoDefaultCountryNumber(context));
} catch (NumberParseException e) {
return null;
}
}
/**
* Contains all the info used to display a phone number on the screen.
* Returned by {@link #getPhoneNumberInfo(Context, String)}
*/
public static final class PhoneNumberInfo {
private final String mPhoneNumber;
private final String mDisplayName;
private final Uri mAvatarUri;
private final String mTypeLabel;
public PhoneNumberInfo(String phoneNumber, String displayName,
Uri avatarUri, String typeLabel) {
mPhoneNumber = phoneNumber;
mDisplayName = displayName;
mAvatarUri = avatarUri;
mTypeLabel = typeLabel;
}
public String getPhoneNumber() {
return mPhoneNumber;
}
public String getDisplayName() {
return mDisplayName;
}
public Uri getAvatarUri() {
return mAvatarUri;
}
public String getTypeLabel() {
return mTypeLabel;
}
}
/**
* Gets all the info needed to properly display a phone number to the UI. (e.g. if it's the
* voicemail number, return a string and a uri that represents voicemail, if it's a contact, get
* the contact's name, its avatar uri, the phone number's label, etc).
*/
public static CompletableFuture<PhoneNumberInfo> getPhoneNumberInfo(
Context context, String number) {
if (TextUtils.isEmpty(number)) {
return CompletableFuture.completedFuture(new PhoneNumberInfo(
number,
context.getString(R.string.unknown),
null,
""));
}
if (isVoicemailNumber(context, number)) {
return CompletableFuture.completedFuture(new PhoneNumberInfo(
number,
context.getString(R.string.voicemail),
makeResourceUri(context, R.drawable.ic_voicemail),
""));
}
if (InMemoryPhoneBook.isInitialized()) {
Contact contact = InMemoryPhoneBook.get().lookupContactEntry(number);
if (contact != null) {
String name = contact.getDisplayName();
if (name == null) {
name = getFormattedNumber(context, number);
}
if (name == null) {
name = context.getString(R.string.unknown);
}
PhoneNumber phoneNumber = contact.getPhoneNumber(context, number);
CharSequence typeLabel = "";
if (phoneNumber != null) {
typeLabel = Phone.getTypeLabel(context.getResources(),
phoneNumber.getType(),
phoneNumber.getLabel());
}
return CompletableFuture.completedFuture(new PhoneNumberInfo(
number,
name,
contact.getAvatarUri(),
typeLabel.toString()));
}
}
return CompletableFuture.supplyAsync(() -> {
String name = null;
String photoUriString = null;
CharSequence typeLabel = "";
ContentResolver cr = context.getContentResolver();
try (Cursor cursor = cr.query(
Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)),
new String[] {
PhoneLookup.DISPLAY_NAME,
PhoneLookup.PHOTO_URI,
PhoneLookup.TYPE,
PhoneLookup.LABEL,
},
null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
name = cursor.getString(0);
photoUriString = cursor.getString(1);
int type = cursor.getInt(2);
String label = cursor.getString(3);
typeLabel = Phone.getTypeLabel(context.getResources(), type, label);
}
}
if (name == null) {
name = getFormattedNumber(context, number);
}
if (name == null) {
name = context.getString(R.string.unknown);
}
return new PhoneNumberInfo(number, name,
TextUtils.isEmpty(photoUriString) ? null : Uri.parse(photoUriString),
typeLabel.toString());
});
}
/**
* @return A string representation of the call state that can be presented to a user.
*/
public static String callStateToUiString(Context context, int state) {
Resources res = context.getResources();
switch (state) {
case Call.STATE_ACTIVE:
return res.getString(R.string.call_state_call_active);
case Call.STATE_HOLDING:
return res.getString(R.string.call_state_hold);
case Call.STATE_NEW:
case Call.STATE_CONNECTING:
return res.getString(R.string.call_state_connecting);
case Call.STATE_SELECT_PHONE_ACCOUNT:
case Call.STATE_DIALING:
return res.getString(R.string.call_state_dialing);
case Call.STATE_DISCONNECTED:
return res.getString(R.string.call_state_call_ended);
case Call.STATE_RINGING:
return res.getString(R.string.call_state_call_ringing);
case Call.STATE_DISCONNECTING:
return res.getString(R.string.call_state_call_ending);
default:
throw new IllegalStateException("Unknown Call State: " + state);
}
}
/**
* Returns true if the telephony network is available.
*/
public static boolean isNetworkAvailable(Context context) {
TelephonyManager tm =
(TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
return tm.getNetworkType() != TelephonyManager.NETWORK_TYPE_UNKNOWN
&& tm.getSimState() == TelephonyManager.SIM_STATE_READY;
}
/**
* Returns true if airplane mode is on.
*/
public static boolean isAirplaneModeOn(Context context) {
return Settings.System.getInt(context.getContentResolver(),
Settings.Global.AIRPLANE_MODE_ON, 0) != 0;
}
/**
* Sets a Contact avatar onto the provided {@code icon}. The first letter of the contact's
* display name or {@code fallbackDisplayName} will be used as a fallback resource if avatar
* loading fails.
*/
public static void setContactBitmapAsync(
Context context,
final ImageView icon,
@Nullable final Contact contact,
@Nullable final String fallbackDisplayName) {
Uri avatarUri = contact != null ? contact.getAvatarUri() : null;
String displayName = contact != null ? contact.getDisplayName() : fallbackDisplayName;
setContactBitmapAsync(context, icon, avatarUri, displayName);
}
/**
* Sets a Contact avatar onto the provided {@code icon}. The first letter of the contact's
* display name will be used as a fallback resource if avatar loading fails.
*/
public static void setContactBitmapAsync(
Context context,
final ImageView icon,
final Uri avatarUri,
final String displayName) {
LetterTileDrawable letterTileDrawable = createLetterTile(context, displayName);
Glide.with(context)
.load(avatarUri)
.apply(new RequestOptions().centerCrop().error(letterTileDrawable))
.into(icon);
}
/** Create a {@link LetterTileDrawable} for the given display name. */
public static LetterTileDrawable createLetterTile(Context context, String displayName) {
LetterTileDrawable letterTileDrawable = new LetterTileDrawable(context.getResources());
letterTileDrawable.setContactDetails(displayName, displayName);
return letterTileDrawable;
}
/** Set the given phone number as the primary phone number for its associated contact. */
public static void setAsPrimaryPhoneNumber(Context context, PhoneNumber phoneNumber) {
// Update the primary values in the data record.
ContentValues values = new ContentValues(1);
values.put(ContactsContract.Data.IS_SUPER_PRIMARY, 1);
values.put(ContactsContract.Data.IS_PRIMARY, 1);
context.getContentResolver().update(
ContentUris.withAppendedId(ContactsContract.Data.CONTENT_URI, phoneNumber.getId()),
values, null, null);
}
/** Add a contact to favorite or remove it from favorite. */
public static int setAsFavoriteContact(Context context, Contact contact, boolean isFavorite) {
if (contact.isStarred() == isFavorite) {
return 0;
}
ContentValues values = new ContentValues(1);
values.put(ContactsContract.Contacts.STARRED, isFavorite ? 1 : 0);
String where = ContactsContract.Contacts._ID + " = ?";
String[] selectionArgs = new String[]{Long.toString(contact.getId())};
return context.getContentResolver().update(ContactsContract.Contacts.CONTENT_URI, values,
where, selectionArgs);
}
/**
* Mark missed call log matching given phone number as read. If phone number string is not
* valid, it will mark all new missed call log as read.
*/
public static void markCallLogAsRead(Context context, String phoneNumberString) {
if (context.checkSelfPermission(Manifest.permission.WRITE_CALL_LOG)
!= PackageManager.PERMISSION_GRANTED) {
Log.w(TAG, "Missing WRITE_CALL_LOG permission; not marking missed calls as read.");
return;
}
ContentValues contentValues = new ContentValues();
contentValues.put(CallLog.Calls.NEW, 0);
contentValues.put(CallLog.Calls.IS_READ, 1);
List<String> selectionArgs = new ArrayList<>();
StringBuilder where = new StringBuilder();
where.append(CallLog.Calls.NEW);
where.append(" = 1 AND ");
where.append(CallLog.Calls.TYPE);
where.append(" = ?");
selectionArgs.add(Integer.toString(CallLog.Calls.MISSED_TYPE));
if (!TextUtils.isEmpty(phoneNumberString)) {
where.append(" AND ");
where.append(CallLog.Calls.NUMBER);
where.append(" = ?");
selectionArgs.add(phoneNumberString);
}
String[] selectionArgsArray = new String[0];
try {
context
.getContentResolver()
.update(
CallLog.Calls.CONTENT_URI,
contentValues,
where.toString(),
selectionArgs.toArray(selectionArgsArray));
} catch (IllegalArgumentException e) {
Log.e(TAG, "markCallLogAsRead failed", e);
}
}
private static Uri makeResourceUri(Context context, int resourceId) {
return new Uri.Builder()
.scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
.encodedAuthority(context.getBasePackageName())
.appendEncodedPath(String.valueOf(resourceId))
.build();
}
}