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.graphics.Bitmap; 28 import android.graphics.Canvas; 29 import android.graphics.drawable.Icon; 30 import android.net.Uri; 31 import android.provider.CallLog; 32 import android.provider.ContactsContract; 33 import android.provider.ContactsContract.CommonDataKinds.Phone; 34 import android.provider.ContactsContract.PhoneLookup; 35 import android.provider.Settings; 36 import android.telecom.Call; 37 import android.telephony.PhoneNumberUtils; 38 import android.telephony.TelephonyManager; 39 import android.text.BidiFormatter; 40 import android.text.TextDirectionHeuristics; 41 import android.text.TextUtils; 42 import android.widget.ImageView; 43 44 import androidx.annotation.Nullable; 45 import androidx.annotation.WorkerThread; 46 import androidx.core.content.ContextCompat; 47 import androidx.core.graphics.drawable.RoundedBitmapDrawable; 48 import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; 49 50 import com.android.car.apps.common.LetterTileDrawable; 51 import com.android.car.apps.common.log.L; 52 53 import com.bumptech.glide.Glide; 54 import com.bumptech.glide.request.RequestOptions; 55 import com.google.i18n.phonenumbers.NumberParseException; 56 import com.google.i18n.phonenumbers.PhoneNumberUtil; 57 import com.google.i18n.phonenumbers.Phonenumber; 58 59 import java.util.ArrayList; 60 import java.util.List; 61 import java.util.Locale; 62 import java.util.concurrent.CompletableFuture; 63 64 /** 65 * Helper methods. 66 */ 67 public class TelecomUtils { 68 private static final String TAG = "CD.TelecomUtils"; 69 private static final int PII_STRING_LENGTH = 4; 70 private static final String COUNTRY_US = "US"; 71 /** 72 * A reference to keep track of the soring method of sorting by the contact's first name. 73 */ 74 public static final Integer SORT_BY_FIRST_NAME = 1; 75 /** 76 * A reference to keep track of the soring method of sorting by the contact's last name. 77 */ 78 public static final Integer SORT_BY_LAST_NAME = 2; 79 80 private static String sVoicemailNumber; 81 private static TelephonyManager sTelephonyManager; 82 83 /** 84 * Get the voicemail number. 85 */ getVoicemailNumber(Context context)86 public static String getVoicemailNumber(Context context) { 87 if (sVoicemailNumber == null) { 88 sVoicemailNumber = getTelephonyManager(context).getVoiceMailNumber(); 89 } 90 return sVoicemailNumber; 91 } 92 93 /** 94 * Returns {@code true} if the given number is a voice mail number. 95 * 96 * @see TelephonyManager#getVoiceMailNumber() 97 */ isVoicemailNumber(Context context, String number)98 public static boolean isVoicemailNumber(Context context, String number) { 99 if (TextUtils.isEmpty(number)) { 100 return false; 101 } 102 103 if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE) 104 != PackageManager.PERMISSION_GRANTED) { 105 return false; 106 } 107 108 return number.equals(getVoicemailNumber(context)); 109 } 110 111 /** 112 * Get the {@link TelephonyManager} instance. 113 */ 114 // TODO(deanh): remove this, getSystemService is not slow. getTelephonyManager(Context context)115 public static TelephonyManager getTelephonyManager(Context context) { 116 if (sTelephonyManager == null) { 117 sTelephonyManager = 118 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); 119 } 120 return sTelephonyManager; 121 } 122 123 /** 124 * Format a number as a phone number. 125 */ getFormattedNumber(Context context, String number)126 public static String getFormattedNumber(Context context, String number) { 127 L.d(TAG, "getFormattedNumber: " + piiLog(number)); 128 if (number == null) { 129 return ""; 130 } 131 132 String countryIso = getCurrentCountryIsoFromLocale(context); 133 L.d(TAG, "PhoneNumberUtils.formatNumber, number: " + piiLog(number) 134 + ", country: " + countryIso); 135 136 String formattedNumber = PhoneNumberUtils.formatNumber(number, countryIso); 137 formattedNumber = TextUtils.isEmpty(formattedNumber) ? number : formattedNumber; 138 L.d(TAG, "getFormattedNumber, result: " + piiLog(formattedNumber)); 139 140 return formattedNumber; 141 } 142 143 /** 144 * @return The ISO 3166-1 two letters country code of the country the user is in. 145 */ getCurrentCountryIso(Context context, Locale locale)146 private static String getCurrentCountryIso(Context context, Locale locale) { 147 String countryIso = locale.getCountry(); 148 if (countryIso == null || countryIso.length() != 2) { 149 L.w(TAG, "Invalid locale, falling back to US"); 150 countryIso = COUNTRY_US; 151 } 152 return countryIso; 153 } 154 getCurrentCountryIso(Context context)155 private static String getCurrentCountryIso(Context context) { 156 return getCurrentCountryIso(context, Locale.getDefault()); 157 } 158 getCurrentCountryIsoFromLocale(Context context)159 private static String getCurrentCountryIsoFromLocale(Context context) { 160 String countryIso; 161 countryIso = context.getResources().getConfiguration().getLocales().get(0).getCountry(); 162 163 if (countryIso == null) { 164 L.w(TAG, "Invalid locale, falling back to US"); 165 countryIso = COUNTRY_US; 166 } 167 168 return countryIso; 169 } 170 171 /** 172 * Creates a new instance of {@link Phonenumber.PhoneNumber} base on the given number and sim 173 * card country code. Returns {@code null} if the number in an invalid number. 174 */ 175 @Nullable createI18nPhoneNumber(Context context, String number)176 public static Phonenumber.PhoneNumber createI18nPhoneNumber(Context context, String number) { 177 try { 178 return PhoneNumberUtil.getInstance().parse(number, getCurrentCountryIso(context)); 179 } catch (NumberParseException e) { 180 return null; 181 } 182 } 183 184 /** 185 * Contains all the info used to display a phone number on the screen. Returned by {@link 186 * #getPhoneNumberInfo(Context, String)} 187 */ 188 public static final class PhoneNumberInfo { 189 private final String mPhoneNumber; 190 private final String mDisplayName; 191 private final String mDisplayNameAlt; 192 private final String mInitials; 193 private final Uri mAvatarUri; 194 private final String mTypeLabel; 195 private final String mLookupKey; 196 PhoneNumberInfo(String phoneNumber, String displayName, String displayNameAlt, String initials, Uri avatarUri, String typeLabel, String lookupKey)197 public PhoneNumberInfo(String phoneNumber, String displayName, String displayNameAlt, 198 String initials, Uri avatarUri, String typeLabel, String lookupKey) { 199 mPhoneNumber = phoneNumber; 200 mDisplayName = displayName; 201 mDisplayNameAlt = displayNameAlt; 202 mInitials = initials; 203 mAvatarUri = avatarUri; 204 mTypeLabel = typeLabel; 205 mLookupKey = lookupKey; 206 } 207 getPhoneNumber()208 public String getPhoneNumber() { 209 return mPhoneNumber; 210 } 211 getDisplayName()212 public String getDisplayName() { 213 return mDisplayName; 214 } 215 getDisplayNameAlt()216 public String getDisplayNameAlt() { 217 return mDisplayNameAlt; 218 } 219 220 /** 221 * Returns the initials of the contact related to the phone number. Returns null if there is 222 * no related contact. 223 */ 224 @Nullable getInitials()225 public String getInitials() { 226 return mInitials; 227 } 228 229 @Nullable getAvatarUri()230 public Uri getAvatarUri() { 231 return mAvatarUri; 232 } 233 getTypeLabel()234 public String getTypeLabel() { 235 return mTypeLabel; 236 } 237 238 /** Returns the lookup key of the contact if any is found. */ 239 @Nullable getLookupKey()240 public String getLookupKey() { 241 return mLookupKey; 242 } 243 244 } 245 246 /** 247 * Gets all the info needed to properly display a phone number to the UI. (e.g. if it's the 248 * voicemail number, return a string and a uri that represents voicemail, if it's a contact, get 249 * the contact's name, its avatar uri, the phone number's label, etc). 250 */ getPhoneNumberInfo( Context context, String number)251 public static CompletableFuture<PhoneNumberInfo> getPhoneNumberInfo( 252 Context context, String number) { 253 return CompletableFuture.supplyAsync(() -> lookupNumberInBackground(context, number)); 254 } 255 256 /** Lookup phone number info in background. */ 257 @WorkerThread lookupNumberInBackground(Context context, String number)258 public static PhoneNumberInfo lookupNumberInBackground(Context context, String number) { 259 if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS) 260 != PackageManager.PERMISSION_GRANTED 261 || TextUtils.isEmpty(number)) { 262 String readableNumber = getReadableNumber(context, number); 263 return new PhoneNumberInfo(number, readableNumber, readableNumber, null, null, null, 264 null); 265 } 266 267 if (isVoicemailNumber(context, number)) { 268 return new PhoneNumberInfo( 269 number, 270 context.getString(R.string.voicemail), 271 context.getString(R.string.voicemail), 272 null, 273 makeResourceUri(context, R.drawable.ic_voicemail), 274 "", 275 null); 276 } 277 278 if (InMemoryPhoneBook.isInitialized()) { 279 Contact contact = InMemoryPhoneBook.get().lookupContactEntry(number); 280 if (contact != null) { 281 String name = contact.getDisplayName(); 282 String nameAlt = contact.getDisplayNameAlt(); 283 if (TextUtils.isEmpty(name)) { 284 name = getReadableNumber(context, number); 285 } 286 if (TextUtils.isEmpty(nameAlt)) { 287 nameAlt = name; 288 } 289 290 PhoneNumber phoneNumber = contact.getPhoneNumber(context, number); 291 CharSequence typeLabel = phoneNumber == null ? "" : phoneNumber.getReadableLabel( 292 context.getResources()); 293 294 return new PhoneNumberInfo( 295 number, 296 name, 297 nameAlt, 298 contact.getInitials(), 299 contact.getAvatarUri(), 300 typeLabel.toString(), 301 contact.getLookupKey()); 302 } 303 } else { 304 L.d(TAG, "InMemoryPhoneBook not initialized."); 305 } 306 307 String name = null; 308 String nameAlt = null; 309 String initials = null; 310 String photoUriString = null; 311 CharSequence typeLabel = ""; 312 String lookupKey = null; 313 314 ContentResolver cr = context.getContentResolver(); 315 try (Cursor cursor = cr.query( 316 Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)), 317 new String[]{ 318 PhoneLookup.DISPLAY_NAME, 319 PhoneLookup.DISPLAY_NAME_ALTERNATIVE, 320 PhoneLookup.PHOTO_URI, 321 PhoneLookup.TYPE, 322 PhoneLookup.LABEL, 323 PhoneLookup.LOOKUP_KEY, 324 }, 325 null, null, null)) { 326 327 if (cursor != null && cursor.moveToFirst()) { 328 int nameColumn = cursor.getColumnIndex(PhoneLookup.DISPLAY_NAME); 329 int altNameColumn = cursor.getColumnIndex(PhoneLookup.DISPLAY_NAME_ALTERNATIVE); 330 int photoUriColumn = cursor.getColumnIndex(PhoneLookup.PHOTO_URI); 331 int typeColumn = cursor.getColumnIndex(PhoneLookup.TYPE); 332 int labelColumn = cursor.getColumnIndex(PhoneLookup.LABEL); 333 int lookupKeyColumn = cursor.getColumnIndex(PhoneLookup.LOOKUP_KEY); 334 335 name = cursor.getString(nameColumn); 336 nameAlt = cursor.getString(altNameColumn); 337 photoUriString = cursor.getString(photoUriColumn); 338 initials = getInitials(name, nameAlt); 339 340 int type = cursor.getInt(typeColumn); 341 String label = cursor.getString(labelColumn); 342 typeLabel = Phone.getTypeLabel(context.getResources(), type, label); 343 344 lookupKey = cursor.getString(lookupKeyColumn); 345 } 346 } 347 348 if (TextUtils.isEmpty(name)) { 349 name = getReadableNumber(context, number); 350 } 351 if (TextUtils.isEmpty(nameAlt)) { 352 nameAlt = name; 353 } 354 355 return new PhoneNumberInfo( 356 number, 357 name, 358 nameAlt, 359 initials, 360 TextUtils.isEmpty(photoUriString) ? null : Uri.parse(photoUriString), 361 typeLabel.toString(), 362 lookupKey); 363 } 364 365 /** 366 * Formats the phone number and presents as "Unknown" if empty. 367 */ getReadableNumber(Context context, String number)368 public static String getReadableNumber(Context context, String number) { 369 String readableNumber = getFormattedNumber(context, number); 370 371 if (TextUtils.isEmpty(readableNumber)) { 372 readableNumber = context.getString(R.string.unknown); 373 } 374 return readableNumber; 375 } 376 377 /** 378 * @return A string representation of the call state that can be presented to a user. 379 */ callStateToUiString(Context context, int state)380 public static String callStateToUiString(Context context, int state) { 381 Resources res = context.getResources(); 382 switch (state) { 383 case Call.STATE_ACTIVE: 384 return res.getString(R.string.call_state_call_active); 385 case Call.STATE_HOLDING: 386 return res.getString(R.string.call_state_hold); 387 case Call.STATE_NEW: 388 case Call.STATE_CONNECTING: 389 return res.getString(R.string.call_state_connecting); 390 case Call.STATE_SELECT_PHONE_ACCOUNT: 391 case Call.STATE_DIALING: 392 return res.getString(R.string.call_state_dialing); 393 case Call.STATE_DISCONNECTED: 394 return res.getString(R.string.call_state_call_ended); 395 case Call.STATE_RINGING: 396 return res.getString(R.string.call_state_call_ringing); 397 case Call.STATE_DISCONNECTING: 398 return res.getString(R.string.call_state_call_ending); 399 default: 400 throw new IllegalStateException("Unknown Call State: " + state); 401 } 402 } 403 404 /** 405 * Returns true if the telephony network is available. 406 */ isNetworkAvailable(Context context)407 public static boolean isNetworkAvailable(Context context) { 408 TelephonyManager tm = 409 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); 410 return tm.getNetworkType() != TelephonyManager.NETWORK_TYPE_UNKNOWN 411 && tm.getSimState() == TelephonyManager.SIM_STATE_READY; 412 } 413 414 /** 415 * Returns true if airplane mode is on. 416 */ isAirplaneModeOn(Context context)417 public static boolean isAirplaneModeOn(Context context) { 418 return Settings.System.getInt(context.getContentResolver(), 419 Settings.Global.AIRPLANE_MODE_ON, 0) != 0; 420 } 421 422 /** 423 * Sets a Contact avatar onto the provided {@code icon}. The first letter or both letters of the 424 * contact's initials. 425 * 426 * @param sortMethod can be either {@link #SORT_BY_FIRST_NAME} or {@link #SORT_BY_LAST_NAME}. 427 */ setContactBitmapAsync( Context context, @Nullable final ImageView icon, @Nullable final Contact contact, Integer sortMethod)428 public static void setContactBitmapAsync( 429 Context context, 430 @Nullable final ImageView icon, 431 @Nullable final Contact contact, 432 Integer sortMethod) { 433 setContactBitmapAsync(context, icon, contact, null, sortMethod); 434 } 435 436 /** 437 * Sets a Contact avatar onto the provided {@code icon}. The first letter or both letters of the 438 * contact's initials. Will start with first name by default. 439 */ setContactBitmapAsync( Context context, @Nullable final ImageView icon, @Nullable final Contact contact, @Nullable final String fallbackDisplayName)440 public static void setContactBitmapAsync( 441 Context context, 442 @Nullable final ImageView icon, 443 @Nullable final Contact contact, 444 @Nullable final String fallbackDisplayName) { 445 setContactBitmapAsync(context, icon, contact, fallbackDisplayName, SORT_BY_FIRST_NAME); 446 } 447 448 /** 449 * Sets a Contact avatar onto the provided {@code icon}. The first letter or both letters of the 450 * contact's initials or {@code fallbackDisplayName} will be used as a fallback resource if 451 * avatar loading fails. 452 * 453 * @param sortMethod can be either {@link #SORT_BY_FIRST_NAME} or {@link #SORT_BY_LAST_NAME}. If 454 * the value is {@link #SORT_BY_FIRST_NAME}, the name and initials order will 455 * be first name first. Otherwise, the order will be last name first. 456 */ setContactBitmapAsync( Context context, @Nullable final ImageView icon, @Nullable final Contact contact, @Nullable final String fallbackDisplayName, Integer sortMethod)457 public static void setContactBitmapAsync( 458 Context context, 459 @Nullable final ImageView icon, 460 @Nullable final Contact contact, 461 @Nullable final String fallbackDisplayName, 462 Integer sortMethod) { 463 Uri avatarUri = contact != null ? contact.getAvatarUri() : null; 464 boolean startWithFirstName = isSortByFirstName(sortMethod); 465 String initials = contact != null 466 ? contact.getInitialsBasedOnDisplayOrder(startWithFirstName) 467 : (fallbackDisplayName == null ? null : getInitials(fallbackDisplayName, null)); 468 String identifier = contact == null ? fallbackDisplayName : contact.getDisplayName(); 469 470 setContactBitmapAsync(context, icon, avatarUri, initials, identifier); 471 } 472 473 /** 474 * Sets a Contact avatar onto the provided {@code icon}. A letter tile base on the contact's 475 * initials and identifier will be used as a fallback resource if avatar loading fails. 476 */ setContactBitmapAsync( Context context, @Nullable final ImageView icon, @Nullable final Uri avatarUri, @Nullable final String initials, @Nullable final String identifier)477 public static void setContactBitmapAsync( 478 Context context, 479 @Nullable final ImageView icon, 480 @Nullable final Uri avatarUri, 481 @Nullable final String initials, 482 @Nullable final String identifier) { 483 if (icon == null) { 484 return; 485 } 486 487 LetterTileDrawable letterTileDrawable = createLetterTile(context, initials, identifier); 488 489 Glide.with(context) 490 .load(avatarUri) 491 .apply(new RequestOptions().centerCrop().error(letterTileDrawable)) 492 .into(icon); 493 } 494 495 /** 496 * Create a {@link LetterTileDrawable} for the given initials. 497 * 498 * @param initials is the letters that will be drawn on the canvas. If it is null, then an 499 * avatar anonymous icon will be drawn 500 * @param identifier will decide the color for the drawable. If null, a default color will be 501 * used. 502 */ createLetterTile( Context context, @Nullable String initials, @Nullable String identifier)503 public static LetterTileDrawable createLetterTile( 504 Context context, 505 @Nullable String initials, 506 @Nullable String identifier) { 507 int numberOfLetter = context.getResources().getInteger( 508 R.integer.config_number_of_letters_shown_for_avatar); 509 String letters = initials != null 510 ? initials.substring(0, Math.min(initials.length(), numberOfLetter)) : null; 511 LetterTileDrawable letterTileDrawable = new LetterTileDrawable(context.getResources(), 512 letters, identifier); 513 return letterTileDrawable; 514 } 515 516 /** 517 * Set the given phone number as the primary phone number for its associated contact. 518 */ setAsPrimaryPhoneNumber(Context context, PhoneNumber phoneNumber)519 public static void setAsPrimaryPhoneNumber(Context context, PhoneNumber phoneNumber) { 520 if (context.checkSelfPermission(Manifest.permission.WRITE_CONTACTS) 521 != PackageManager.PERMISSION_GRANTED) { 522 L.w(TAG, "Missing WRITE_CONTACTS permission, not setting primary number."); 523 return; 524 } 525 // Update the primary values in the data record. 526 ContentValues values = new ContentValues(1); 527 values.put(ContactsContract.Data.IS_SUPER_PRIMARY, 1); 528 values.put(ContactsContract.Data.IS_PRIMARY, 1); 529 530 context.getContentResolver().update( 531 ContentUris.withAppendedId(ContactsContract.Data.CONTENT_URI, phoneNumber.getId()), 532 values, null, null); 533 } 534 535 /** 536 * Mark missed call log matching given phone number as read. If phone number string is not 537 * valid, it will mark all new missed call log as read. 538 */ markCallLogAsRead(Context context, String phoneNumberString)539 public static void markCallLogAsRead(Context context, String phoneNumberString) { 540 markCallLogAsRead(context, CallLog.Calls.NUMBER, phoneNumberString); 541 } 542 543 /** 544 * Mark missed call log matching given call log id as read. If phone number string is not 545 * valid, it will mark all new missed call log as read. 546 */ markCallLogAsRead(Context context, long callLogId)547 public static void markCallLogAsRead(Context context, long callLogId) { 548 markCallLogAsRead(context, CallLog.Calls._ID, 549 callLogId < 0 ? null : String.valueOf(callLogId)); 550 } 551 552 /** 553 * Mark missed call log matching given column name and selection argument as read. If the column 554 * name or the selection argument is not valid, mark all new missed call log as read. 555 */ markCallLogAsRead(Context context, String columnName, String selectionArg)556 private static void markCallLogAsRead(Context context, String columnName, 557 String selectionArg) { 558 if (context.checkSelfPermission(Manifest.permission.WRITE_CALL_LOG) 559 != PackageManager.PERMISSION_GRANTED) { 560 L.w(TAG, "Missing WRITE_CALL_LOG permission; not marking missed calls as read."); 561 return; 562 } 563 ContentValues contentValues = new ContentValues(); 564 contentValues.put(CallLog.Calls.NEW, 0); 565 contentValues.put(CallLog.Calls.IS_READ, 1); 566 567 List<String> selectionArgs = new ArrayList<>(); 568 StringBuilder where = new StringBuilder(); 569 where.append(CallLog.Calls.NEW); 570 where.append(" = 1 AND "); 571 where.append(CallLog.Calls.TYPE); 572 where.append(" = ?"); 573 selectionArgs.add(Integer.toString(CallLog.Calls.MISSED_TYPE)); 574 if (!TextUtils.isEmpty(columnName) && !TextUtils.isEmpty(selectionArg)) { 575 where.append(" AND "); 576 where.append(columnName); 577 where.append(" = ?"); 578 selectionArgs.add(selectionArg); 579 } 580 String[] selectionArgsArray = new String[0]; 581 try { 582 ContentResolver contentResolver = context.getContentResolver(); 583 contentResolver.update( 584 CallLog.Calls.CONTENT_URI, 585 contentValues, 586 where.toString(), 587 selectionArgs.toArray(selectionArgsArray)); 588 // #update doesn't notify change any more. Notify change to rerun query from database. 589 contentResolver.notifyChange(CallLog.Calls.CONTENT_URI, null); 590 } catch (IllegalArgumentException e) { 591 L.e(TAG, "markCallLogAsRead failed", e); 592 } 593 } 594 595 /** 596 * Returns the initials based on the name and nameAlt. 597 * 598 * @param name should be the display name of a contact. 599 * @param nameAlt should be alternative display name of a contact. 600 */ getInitials(String name, String nameAlt)601 public static String getInitials(String name, String nameAlt) { 602 StringBuilder initials = new StringBuilder(); 603 if (!TextUtils.isEmpty(name) && Character.isLetter(name.charAt(0))) { 604 initials.append(Character.toUpperCase(name.charAt(0))); 605 } 606 if (!TextUtils.isEmpty(nameAlt) 607 && !TextUtils.equals(name, nameAlt) 608 && Character.isLetter(nameAlt.charAt(0))) { 609 initials.append(Character.toUpperCase(nameAlt.charAt(0))); 610 } 611 return initials.toString(); 612 } 613 614 /** 615 * Creates a Letter Tile Icon that will display the given initials. If the initials are null, 616 * then an avatar anonymous icon will be drawn. 617 **/ createLetterTile(Context context, @Nullable String initials, String identifier, int avatarSize, float cornerRadiusPercent)618 public static Icon createLetterTile(Context context, @Nullable String initials, 619 String identifier, int avatarSize, float cornerRadiusPercent) { 620 LetterTileDrawable letterTileDrawable = TelecomUtils.createLetterTile(context, initials, 621 identifier); 622 RoundedBitmapDrawable roundedBitmapDrawable = RoundedBitmapDrawableFactory.create( 623 context.getResources(), letterTileDrawable.toBitmap(avatarSize)); 624 return createFromRoundedBitmapDrawable(roundedBitmapDrawable, avatarSize, 625 cornerRadiusPercent); 626 } 627 628 /** Creates an Icon based on the given roundedBitmapDrawable. **/ createFromRoundedBitmapDrawable(RoundedBitmapDrawable roundedBitmapDrawable, int avatarSize, float cornerRadiusPercent)629 public static Icon createFromRoundedBitmapDrawable(RoundedBitmapDrawable roundedBitmapDrawable, 630 int avatarSize, float cornerRadiusPercent) { 631 float radius = avatarSize * cornerRadiusPercent; 632 roundedBitmapDrawable.setCornerRadius(radius); 633 634 final Bitmap result = Bitmap.createBitmap(avatarSize, avatarSize, 635 Bitmap.Config.ARGB_8888); 636 final Canvas canvas = new Canvas(result); 637 roundedBitmapDrawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); 638 roundedBitmapDrawable.draw(canvas); 639 return Icon.createWithBitmap(result); 640 } 641 642 /** 643 * Sets the direction of a string, used for displaying phone numbers. 644 */ getBidiWrappedNumber(String string)645 public static String getBidiWrappedNumber(String string) { 646 return BidiFormatter.getInstance().unicodeWrap(string, TextDirectionHeuristics.LTR); 647 } 648 makeResourceUri(Context context, int resourceId)649 private static Uri makeResourceUri(Context context, int resourceId) { 650 return new Uri.Builder() 651 .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) 652 .encodedAuthority(context.getPackageName()) 653 .appendEncodedPath(String.valueOf(resourceId)) 654 .build(); 655 } 656 657 /** 658 * This is a workaround for Log.Pii(). It will only show the last {@link #PII_STRING_LENGTH} 659 * characters. 660 */ piiLog(Object pii)661 public static String piiLog(Object pii) { 662 String piiString = String.valueOf(pii); 663 return piiString.length() >= PII_STRING_LENGTH ? "*" + piiString.substring( 664 piiString.length() - PII_STRING_LENGTH) : piiString; 665 } 666 667 /** 668 * Returns true if contacts are sorted by their first names. Returns false if they are sorted by 669 * last names. 670 */ isSortByFirstName(Integer sortMethod)671 public static boolean isSortByFirstName(Integer sortMethod) { 672 return SORT_BY_FIRST_NAME.equals(sortMethod); 673 } 674 } 675