1 /* 2 * Copyright (C) 2019 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.car.telephony.common; 18 19 import android.Manifest; 20 import android.content.ContentResolver; 21 import android.content.ContentUris; 22 import android.content.ContentValues; 23 import android.content.Context; 24 import android.content.pm.PackageManager; 25 import android.content.res.Resources; 26 import android.database.Cursor; 27 import android.net.Uri; 28 import android.provider.CallLog; 29 import android.provider.ContactsContract; 30 import android.provider.ContactsContract.CommonDataKinds.Phone; 31 import android.provider.ContactsContract.PhoneLookup; 32 import android.provider.Settings; 33 import android.telecom.Call; 34 import android.telephony.PhoneNumberUtils; 35 import android.telephony.TelephonyManager; 36 import android.text.TextUtils; 37 import android.util.Log; 38 import android.widget.ImageView; 39 40 import androidx.annotation.Nullable; 41 import androidx.core.util.Pair; 42 43 import com.android.car.apps.common.LetterTileDrawable; 44 45 import com.bumptech.glide.Glide; 46 import com.bumptech.glide.request.RequestOptions; 47 import com.google.i18n.phonenumbers.NumberParseException; 48 import com.google.i18n.phonenumbers.PhoneNumberUtil; 49 import com.google.i18n.phonenumbers.Phonenumber; 50 51 import java.util.ArrayList; 52 import java.util.List; 53 import java.util.Locale; 54 55 /** Helper methods. */ 56 public class TelecomUtils { 57 private static final String TAG = "CD.TelecomUtils"; 58 59 private static final String[] CONTACT_ID_PROJECTION = new String[]{ 60 PhoneLookup.DISPLAY_NAME, 61 PhoneLookup.TYPE, 62 PhoneLookup.LABEL, 63 PhoneLookup._ID 64 }; 65 66 private static String sVoicemailNumber; 67 private static TelephonyManager sTelephonyManager; 68 69 /** 70 * Return the label for the given phone number. 71 * 72 * @param number Caller phone number 73 * @return the label if it is found, empty string otherwise. 74 */ getTypeFromNumber(Context context, String number)75 public static CharSequence getTypeFromNumber(Context context, String number) { 76 if (Log.isLoggable(TAG, Log.DEBUG)) { 77 Log.d(TAG, "getTypeFromNumber, number: " + number); 78 } 79 80 String defaultLabel = ""; 81 if (TextUtils.isEmpty(number)) { 82 return defaultLabel; 83 } 84 85 ContentResolver cr = context.getContentResolver(); 86 Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)); 87 Cursor cursor = cr.query(uri, CONTACT_ID_PROJECTION, null, null, null); 88 89 try { 90 if (cursor != null && cursor.moveToFirst()) { 91 int typeColumn = cursor.getColumnIndex(PhoneLookup.TYPE); 92 int type = cursor.getInt(typeColumn); 93 int labelColumn = cursor.getColumnIndex(PhoneLookup.LABEL); 94 String label = cursor.getString(labelColumn); 95 CharSequence typeLabel = Phone.getTypeLabel(context.getResources(), type, label); 96 return typeLabel; 97 } 98 } finally { 99 if (cursor != null) { 100 cursor.close(); 101 } 102 } 103 return defaultLabel; 104 } 105 106 /** 107 * Get the voicemail number. 108 */ getVoicemailNumber(Context context)109 public static String getVoicemailNumber(Context context) { 110 if (sVoicemailNumber == null) { 111 sVoicemailNumber = getTelephonyManager(context).getVoiceMailNumber(); 112 } 113 return sVoicemailNumber; 114 } 115 116 /** 117 * Returns {@code true} if the given number is a voice mail number. 118 * 119 * @see TelephonyManager#getVoiceMailNumber() 120 */ isVoicemailNumber(Context context, String number)121 public static boolean isVoicemailNumber(Context context, String number) { 122 return !TextUtils.isEmpty(number) && number.equals(getVoicemailNumber(context)); 123 } 124 125 /** 126 * Get the {@link TelephonyManager} instance. 127 */ 128 // TODO(deanh): remove this, getSystemService is not slow. getTelephonyManager(Context context)129 public static TelephonyManager getTelephonyManager(Context context) { 130 if (sTelephonyManager == null) { 131 sTelephonyManager = 132 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); 133 } 134 return sTelephonyManager; 135 } 136 137 /** 138 * Format a number as a phone number. 139 */ getFormattedNumber(Context context, String number)140 public static String getFormattedNumber(Context context, String number) { 141 if (Log.isLoggable(TAG, Log.DEBUG)) { 142 Log.d(TAG, "getFormattedNumber: " + number); 143 } 144 if (number == null) { 145 return ""; 146 } 147 148 String countryIso = getIsoDefaultCountryNumber(context); 149 if (Log.isLoggable(TAG, Log.DEBUG)) { 150 Log.d(TAG, "PhoneNumberUtils.formatNumberToE16, number: " 151 + number + ", country: " + countryIso); 152 } 153 String e164 = PhoneNumberUtils.formatNumberToE164(number, countryIso); 154 String formattedNumber = PhoneNumberUtils.formatNumber(number, e164, countryIso); 155 formattedNumber = TextUtils.isEmpty(formattedNumber) ? number : formattedNumber; 156 if (Log.isLoggable(TAG, Log.DEBUG)) { 157 Log.d(TAG, "getFormattedNumber, result: " + formattedNumber); 158 } 159 return formattedNumber; 160 } 161 getIsoDefaultCountryNumber(Context context)162 private static String getIsoDefaultCountryNumber(Context context) { 163 String countryIso = getTelephonyManager(context).getSimCountryIso().toUpperCase(Locale.US); 164 if (countryIso.length() != 2) { 165 countryIso = Locale.getDefault().getCountry(); 166 if (countryIso == null || countryIso.length() != 2) { 167 countryIso = "US"; 168 } 169 } 170 171 return countryIso; 172 } 173 174 /** 175 * Creates a new instance of {@link Phonenumber#Phonenumber} base on the given number and sim 176 * card country code. Returns {@code null} if the number in an invalid number. 177 */ 178 @Nullable createI18nPhoneNumber(Context context, String number)179 public static Phonenumber.PhoneNumber createI18nPhoneNumber(Context context, String number) { 180 try { 181 return PhoneNumberUtil.getInstance().parse(number, getIsoDefaultCountryNumber(context)); 182 } catch (NumberParseException e) { 183 return null; 184 } 185 } 186 187 /** 188 * Get the display name and photo uri of the given number (e.g. if it's the voicemail number, 189 * return a string and a uri that represents voicemail, if it's a contact, get the contact's 190 * name and its avatar uri, etc). 191 * 192 * @return Pair of display name and contact's photo uri if found. Voicemail number uses drawable 193 * resource uri and null uri for other cases. 194 */ getDisplayNameAndAvatarUri(Context context, String number)195 public static Pair<String, Uri> getDisplayNameAndAvatarUri(Context context, String number) { 196 if (Log.isLoggable(TAG, Log.DEBUG)) { 197 Log.d(TAG, "getDisplayNameAndAvatarUri: " + number); 198 } 199 200 if (TextUtils.isEmpty(number)) { 201 return new Pair<>(context.getString(R.string.unknown), null); 202 } 203 204 if (isVoicemailNumber(context, number)) { 205 return new Pair<>( 206 context.getString(R.string.voicemail), 207 makeResourceUri(context, R.drawable.ic_voicemail)); 208 } 209 210 ContentResolver cr = context.getContentResolver(); 211 Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)); 212 213 Cursor cursor = null; 214 String name = null; 215 String photoUriString = null; 216 try { 217 cursor = cr.query(uri, new String[]{PhoneLookup.DISPLAY_NAME, PhoneLookup.PHOTO_URI}, 218 null, null, null); 219 if (cursor != null && cursor.moveToFirst()) { 220 name = cursor.getString(0); 221 photoUriString = cursor.getString(1); 222 } 223 } finally { 224 if (cursor != null) { 225 cursor.close(); 226 } 227 } 228 229 if (name == null) { 230 name = getFormattedNumber(context, number); 231 } 232 233 if (name == null) { 234 name = context.getString(R.string.unknown); 235 } 236 237 if (TextUtils.isEmpty(photoUriString)) { 238 return new Pair<>(name, null); 239 } 240 return new Pair<>(name, Uri.parse(photoUriString)); 241 } 242 243 /** 244 * @return A string representation of the call state that can be presented to a user. 245 */ callStateToUiString(Context context, int state)246 public static String callStateToUiString(Context context, int state) { 247 Resources res = context.getResources(); 248 switch (state) { 249 case Call.STATE_ACTIVE: 250 return res.getString(R.string.call_state_call_active); 251 case Call.STATE_HOLDING: 252 return res.getString(R.string.call_state_hold); 253 case Call.STATE_NEW: 254 case Call.STATE_CONNECTING: 255 return res.getString(R.string.call_state_connecting); 256 case Call.STATE_SELECT_PHONE_ACCOUNT: 257 case Call.STATE_DIALING: 258 return res.getString(R.string.call_state_dialing); 259 case Call.STATE_DISCONNECTED: 260 return res.getString(R.string.call_state_call_ended); 261 case Call.STATE_RINGING: 262 return res.getString(R.string.call_state_call_ringing); 263 case Call.STATE_DISCONNECTING: 264 return res.getString(R.string.call_state_call_ending); 265 default: 266 throw new IllegalStateException("Unknown Call State: " + state); 267 } 268 } 269 270 /** 271 * Returns true if the telephony network is available. 272 */ isNetworkAvailable(Context context)273 public static boolean isNetworkAvailable(Context context) { 274 TelephonyManager tm = 275 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); 276 return tm.getNetworkType() != TelephonyManager.NETWORK_TYPE_UNKNOWN 277 && tm.getSimState() == TelephonyManager.SIM_STATE_READY; 278 } 279 280 /** 281 * Returns true if airplane mode is on. 282 */ isAirplaneModeOn(Context context)283 public static boolean isAirplaneModeOn(Context context) { 284 return Settings.System.getInt(context.getContentResolver(), 285 Settings.Global.AIRPLANE_MODE_ON, 0) != 0; 286 } 287 288 /** 289 * Sets a Contact avatar onto the provided {@code icon}. The first letter of the contact's 290 * display name or {@code fallbackDisplayName} will be used as a fallback resource if avatar 291 * loading fails. 292 */ setContactBitmapAsync( Context context, final ImageView icon, @Nullable final Contact contact, @Nullable final String fallbackDisplayName)293 public static void setContactBitmapAsync( 294 Context context, 295 final ImageView icon, 296 @Nullable final Contact contact, 297 @Nullable final String fallbackDisplayName) { 298 Uri avatarUri = contact != null ? contact.getAvatarUri() : null; 299 String displayName = contact != null ? contact.getDisplayName() : fallbackDisplayName; 300 301 setContactBitmapAsync(context, icon, avatarUri, displayName); 302 } 303 304 /** 305 * Sets a Contact avatar onto the provided {@code icon}. The first letter of the contact's 306 * display name will be used as a fallback resource if avatar loading fails. 307 */ setContactBitmapAsync( Context context, final ImageView icon, final Uri avatarUri, final String displayName)308 public static void setContactBitmapAsync( 309 Context context, 310 final ImageView icon, 311 final Uri avatarUri, 312 final String displayName) { 313 LetterTileDrawable letterTileDrawable = createLetterTile(context, displayName); 314 315 if (avatarUri != null) { 316 Glide.with(context) 317 .load(avatarUri) 318 .apply(new RequestOptions().centerCrop().error(letterTileDrawable)) 319 .into(icon); 320 return; 321 } 322 323 // Use the letter tile as avatar if there is no avatar available from content provider. 324 icon.setScaleType(ImageView.ScaleType.CENTER_INSIDE); 325 icon.setImageDrawable(letterTileDrawable); 326 } 327 328 /** Create a {@link LetterTileDrawable} for the given display name. */ createLetterTile(Context context, String displayName)329 public static LetterTileDrawable createLetterTile(Context context, String displayName) { 330 LetterTileDrawable letterTileDrawable = new LetterTileDrawable(context.getResources()); 331 letterTileDrawable.setContactDetails(displayName, displayName); 332 return letterTileDrawable; 333 } 334 335 /** Set the given phone number as the primary phone number for its associated contact. */ setAsPrimaryPhoneNumber(Context context, PhoneNumber phoneNumber)336 public static void setAsPrimaryPhoneNumber(Context context, PhoneNumber phoneNumber) { 337 // Update the primary values in the data record. 338 ContentValues values = new ContentValues(1); 339 values.put(ContactsContract.Data.IS_SUPER_PRIMARY, 1); 340 values.put(ContactsContract.Data.IS_PRIMARY, 1); 341 342 context.getContentResolver().update( 343 ContentUris.withAppendedId(ContactsContract.Data.CONTENT_URI, phoneNumber.getId()), 344 values, null, null); 345 } 346 347 /** Add a contact to favorite or remove it from favorite. */ setAsFavoriteContact(Context context, Contact contact, boolean isFavorite)348 public static int setAsFavoriteContact(Context context, Contact contact, boolean isFavorite) { 349 if (contact.isStarred() == isFavorite) { 350 return 0; 351 } 352 353 ContentValues values = new ContentValues(1); 354 values.put(ContactsContract.Contacts.STARRED, isFavorite ? 1 : 0); 355 356 String where = ContactsContract.Contacts._ID + " = ?"; 357 String[] selectionArgs = new String[]{Long.toString(contact.getId())}; 358 return context.getContentResolver().update(ContactsContract.Contacts.CONTENT_URI, values, 359 where, selectionArgs); 360 } 361 362 /** 363 * Mark missed call log matching given phone number as read. If phone number string is not 364 * valid, it will mark all new missed call log as read. 365 */ markCallLogAsRead(Context context, String phoneNumberString)366 public static void markCallLogAsRead(Context context, String phoneNumberString) { 367 if (context.checkSelfPermission(Manifest.permission.WRITE_CALL_LOG) 368 != PackageManager.PERMISSION_GRANTED) { 369 Log.w(TAG, "Missing WRITE_CALL_LOG permission; not marking missed calls as read."); 370 return; 371 } 372 ContentValues contentValues = new ContentValues(); 373 contentValues.put(CallLog.Calls.NEW, 0); 374 contentValues.put(CallLog.Calls.IS_READ, 1); 375 376 List<String> selectionArgs = new ArrayList<>(); 377 StringBuilder where = new StringBuilder(); 378 where.append(CallLog.Calls.NEW); 379 where.append(" = 1 AND "); 380 where.append(CallLog.Calls.TYPE); 381 where.append(" = ?"); 382 selectionArgs.add(Integer.toString(CallLog.Calls.MISSED_TYPE)); 383 if (!TextUtils.isEmpty(phoneNumberString)) { 384 where.append(" AND "); 385 where.append(CallLog.Calls.NUMBER); 386 where.append(" = ?"); 387 selectionArgs.add(phoneNumberString); 388 } 389 String[] selectionArgsArray = new String[0]; 390 try { 391 context 392 .getContentResolver() 393 .update( 394 CallLog.Calls.CONTENT_URI, 395 contentValues, 396 where.toString(), 397 selectionArgs.toArray(selectionArgsArray)); 398 } catch (IllegalArgumentException e) { 399 Log.e(TAG, "markCallLogAsRead failed", e); 400 } 401 } 402 makeResourceUri(Context context, int resourceId)403 private static Uri makeResourceUri(Context context, int resourceId) { 404 return new Uri.Builder() 405 .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) 406 .encodedAuthority(context.getBasePackageName()) 407 .appendEncodedPath(String.valueOf(resourceId)) 408 .build(); 409 } 410 411 } 412