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 String readableNumber = getReadableNumber(context, number); 262 return new PhoneNumberInfo(number, readableNumber, readableNumber, null, null, null, 263 null); 264 } 265 266 if (TextUtils.isEmpty(number)) { 267 return new PhoneNumberInfo( 268 number, 269 context.getString(R.string.unknown), 270 context.getString(R.string.unknown), 271 null, 272 null, 273 "", 274 null); 275 } 276 277 if (isVoicemailNumber(context, number)) { 278 return new PhoneNumberInfo( 279 number, 280 context.getString(R.string.voicemail), 281 context.getString(R.string.voicemail), 282 null, 283 makeResourceUri(context, R.drawable.ic_voicemail), 284 "", 285 null); 286 } 287 288 if (InMemoryPhoneBook.isInitialized()) { 289 Contact contact = InMemoryPhoneBook.get().lookupContactEntry(number); 290 if (contact != null) { 291 String name = contact.getDisplayName(); 292 String nameAlt = contact.getDisplayNameAlt(); 293 if (TextUtils.isEmpty(name)) { 294 name = getReadableNumber(context, number); 295 } 296 if (TextUtils.isEmpty(nameAlt)) { 297 nameAlt = name; 298 } 299 300 PhoneNumber phoneNumber = contact.getPhoneNumber(context, number); 301 CharSequence typeLabel = phoneNumber == null ? "" : phoneNumber.getReadableLabel( 302 context.getResources()); 303 304 return new PhoneNumberInfo( 305 number, 306 name, 307 nameAlt, 308 contact.getInitials(), 309 contact.getAvatarUri(), 310 typeLabel.toString(), 311 contact.getLookupKey()); 312 } 313 } else { 314 L.d(TAG, "InMemoryPhoneBook not initialized."); 315 } 316 317 String name = null; 318 String nameAlt = null; 319 String initials = null; 320 String photoUriString = null; 321 CharSequence typeLabel = ""; 322 String lookupKey = null; 323 324 ContentResolver cr = context.getContentResolver(); 325 try (Cursor cursor = cr.query( 326 Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)), 327 new String[]{ 328 PhoneLookup.DISPLAY_NAME, 329 PhoneLookup.DISPLAY_NAME_ALTERNATIVE, 330 PhoneLookup.PHOTO_URI, 331 PhoneLookup.TYPE, 332 PhoneLookup.LABEL, 333 PhoneLookup.LOOKUP_KEY, 334 }, 335 null, null, null)) { 336 337 if (cursor != null && cursor.moveToFirst()) { 338 int nameColumn = cursor.getColumnIndex(PhoneLookup.DISPLAY_NAME); 339 int altNameColumn = cursor.getColumnIndex(PhoneLookup.DISPLAY_NAME_ALTERNATIVE); 340 int photoUriColumn = cursor.getColumnIndex(PhoneLookup.PHOTO_URI); 341 int typeColumn = cursor.getColumnIndex(PhoneLookup.TYPE); 342 int labelColumn = cursor.getColumnIndex(PhoneLookup.LABEL); 343 int lookupKeyColumn = cursor.getColumnIndex(PhoneLookup.LOOKUP_KEY); 344 345 name = cursor.getString(nameColumn); 346 nameAlt = cursor.getString(altNameColumn); 347 photoUriString = cursor.getString(photoUriColumn); 348 initials = getInitials(name, nameAlt); 349 350 int type = cursor.getInt(typeColumn); 351 String label = cursor.getString(labelColumn); 352 typeLabel = Phone.getTypeLabel(context.getResources(), type, label); 353 354 lookupKey = cursor.getString(lookupKeyColumn); 355 } 356 } 357 358 if (TextUtils.isEmpty(name)) { 359 name = getReadableNumber(context, number); 360 } 361 if (TextUtils.isEmpty(nameAlt)) { 362 nameAlt = name; 363 } 364 365 return new PhoneNumberInfo( 366 number, 367 name, 368 nameAlt, 369 initials, 370 TextUtils.isEmpty(photoUriString) ? null : Uri.parse(photoUriString), 371 typeLabel.toString(), 372 lookupKey); 373 } 374 getReadableNumber(Context context, String number)375 private static String getReadableNumber(Context context, String number) { 376 String readableNumber = getFormattedNumber(context, number); 377 378 if (readableNumber == null) { 379 readableNumber = context.getString(R.string.unknown); 380 } 381 return readableNumber; 382 } 383 384 /** 385 * @return A string representation of the call state that can be presented to a user. 386 */ callStateToUiString(Context context, int state)387 public static String callStateToUiString(Context context, int state) { 388 Resources res = context.getResources(); 389 switch (state) { 390 case Call.STATE_ACTIVE: 391 return res.getString(R.string.call_state_call_active); 392 case Call.STATE_HOLDING: 393 return res.getString(R.string.call_state_hold); 394 case Call.STATE_NEW: 395 case Call.STATE_CONNECTING: 396 return res.getString(R.string.call_state_connecting); 397 case Call.STATE_SELECT_PHONE_ACCOUNT: 398 case Call.STATE_DIALING: 399 return res.getString(R.string.call_state_dialing); 400 case Call.STATE_DISCONNECTED: 401 return res.getString(R.string.call_state_call_ended); 402 case Call.STATE_RINGING: 403 return res.getString(R.string.call_state_call_ringing); 404 case Call.STATE_DISCONNECTING: 405 return res.getString(R.string.call_state_call_ending); 406 default: 407 throw new IllegalStateException("Unknown Call State: " + state); 408 } 409 } 410 411 /** 412 * Returns true if the telephony network is available. 413 */ isNetworkAvailable(Context context)414 public static boolean isNetworkAvailable(Context context) { 415 TelephonyManager tm = 416 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); 417 return tm.getNetworkType() != TelephonyManager.NETWORK_TYPE_UNKNOWN 418 && tm.getSimState() == TelephonyManager.SIM_STATE_READY; 419 } 420 421 /** 422 * Returns true if airplane mode is on. 423 */ isAirplaneModeOn(Context context)424 public static boolean isAirplaneModeOn(Context context) { 425 return Settings.System.getInt(context.getContentResolver(), 426 Settings.Global.AIRPLANE_MODE_ON, 0) != 0; 427 } 428 429 /** 430 * Sets a Contact avatar onto the provided {@code icon}. The first letter or both letters of the 431 * contact's initials. 432 * 433 * @param sortMethod can be either {@link #SORT_BY_FIRST_NAME} or {@link #SORT_BY_LAST_NAME}. 434 */ setContactBitmapAsync( Context context, @Nullable final ImageView icon, @Nullable final Contact contact, Integer sortMethod)435 public static void setContactBitmapAsync( 436 Context context, 437 @Nullable final ImageView icon, 438 @Nullable final Contact contact, 439 Integer sortMethod) { 440 setContactBitmapAsync(context, icon, contact, null, sortMethod); 441 } 442 443 /** 444 * Sets a Contact avatar onto the provided {@code icon}. The first letter or both letters of the 445 * contact's initials. Will start with first name by default. 446 */ setContactBitmapAsync( Context context, @Nullable final ImageView icon, @Nullable final Contact contact, @Nullable final String fallbackDisplayName)447 public static void setContactBitmapAsync( 448 Context context, 449 @Nullable final ImageView icon, 450 @Nullable final Contact contact, 451 @Nullable final String fallbackDisplayName) { 452 setContactBitmapAsync(context, icon, contact, fallbackDisplayName, SORT_BY_FIRST_NAME); 453 } 454 455 /** 456 * Sets a Contact avatar onto the provided {@code icon}. The first letter or both letters of the 457 * contact's initials or {@code fallbackDisplayName} will be used as a fallback resource if 458 * avatar loading fails. 459 * 460 * @param sortMethod can be either {@link #SORT_BY_FIRST_NAME} or {@link #SORT_BY_LAST_NAME}. If 461 * the value is {@link #SORT_BY_FIRST_NAME}, the name and initials order will 462 * be first name first. Otherwise, the order will be last name first. 463 */ setContactBitmapAsync( Context context, @Nullable final ImageView icon, @Nullable final Contact contact, @Nullable final String fallbackDisplayName, Integer sortMethod)464 public static void setContactBitmapAsync( 465 Context context, 466 @Nullable final ImageView icon, 467 @Nullable final Contact contact, 468 @Nullable final String fallbackDisplayName, 469 Integer sortMethod) { 470 Uri avatarUri = contact != null ? contact.getAvatarUri() : null; 471 boolean startWithFirstName = isSortByFirstName(sortMethod); 472 String initials = contact != null 473 ? contact.getInitialsBasedOnDisplayOrder(startWithFirstName) 474 : (fallbackDisplayName == null ? null : getInitials(fallbackDisplayName, null)); 475 String identifier = contact == null ? fallbackDisplayName : contact.getDisplayName(); 476 477 setContactBitmapAsync(context, icon, avatarUri, initials, identifier); 478 } 479 480 /** 481 * Sets a Contact avatar onto the provided {@code icon}. A letter tile base on the contact's 482 * initials and identifier will be used as a fallback resource if avatar loading fails. 483 */ setContactBitmapAsync( Context context, @Nullable final ImageView icon, @Nullable final Uri avatarUri, @Nullable final String initials, @Nullable final String identifier)484 public static void setContactBitmapAsync( 485 Context context, 486 @Nullable final ImageView icon, 487 @Nullable final Uri avatarUri, 488 @Nullable final String initials, 489 @Nullable final String identifier) { 490 if (icon == null) { 491 return; 492 } 493 494 LetterTileDrawable letterTileDrawable = createLetterTile(context, initials, identifier); 495 496 Glide.with(context) 497 .load(avatarUri) 498 .apply(new RequestOptions().centerCrop().error(letterTileDrawable)) 499 .into(icon); 500 } 501 502 /** 503 * Create a {@link LetterTileDrawable} for the given initials. 504 * 505 * @param initials is the letters that will be drawn on the canvas. If it is null, then an 506 * avatar anonymous icon will be drawn 507 * @param identifier will decide the color for the drawable. If null, a default color will be 508 * used. 509 */ createLetterTile( Context context, @Nullable String initials, @Nullable String identifier)510 public static LetterTileDrawable createLetterTile( 511 Context context, 512 @Nullable String initials, 513 @Nullable String identifier) { 514 int numberOfLetter = context.getResources().getInteger( 515 R.integer.config_number_of_letters_shown_for_avatar); 516 String letters = initials != null 517 ? initials.substring(0, Math.min(initials.length(), numberOfLetter)) : null; 518 LetterTileDrawable letterTileDrawable = new LetterTileDrawable(context.getResources(), 519 letters, identifier); 520 return letterTileDrawable; 521 } 522 523 /** 524 * Set the given phone number as the primary phone number for its associated contact. 525 */ setAsPrimaryPhoneNumber(Context context, PhoneNumber phoneNumber)526 public static void setAsPrimaryPhoneNumber(Context context, PhoneNumber phoneNumber) { 527 if (context.checkSelfPermission(Manifest.permission.WRITE_CONTACTS) 528 != PackageManager.PERMISSION_GRANTED) { 529 L.w(TAG, "Missing WRITE_CONTACTS permission, not setting primary number."); 530 return; 531 } 532 // Update the primary values in the data record. 533 ContentValues values = new ContentValues(1); 534 values.put(ContactsContract.Data.IS_SUPER_PRIMARY, 1); 535 values.put(ContactsContract.Data.IS_PRIMARY, 1); 536 537 context.getContentResolver().update( 538 ContentUris.withAppendedId(ContactsContract.Data.CONTENT_URI, phoneNumber.getId()), 539 values, null, null); 540 } 541 542 /** 543 * Mark missed call log matching given phone number as read. If phone number string is not 544 * valid, it will mark all new missed call log as read. 545 */ markCallLogAsRead(Context context, String phoneNumberString)546 public static void markCallLogAsRead(Context context, String phoneNumberString) { 547 markCallLogAsRead(context, CallLog.Calls.NUMBER, phoneNumberString); 548 } 549 550 /** 551 * Mark missed call log matching given call log id as read. If phone number string is not 552 * valid, it will mark all new missed call log as read. 553 */ markCallLogAsRead(Context context, long callLogId)554 public static void markCallLogAsRead(Context context, long callLogId) { 555 markCallLogAsRead(context, CallLog.Calls._ID, 556 callLogId < 0 ? null : String.valueOf(callLogId)); 557 } 558 559 /** 560 * Mark missed call log matching given column name and selection argument as read. If the column 561 * name or the selection argument is not valid, mark all new missed call log as read. 562 */ markCallLogAsRead(Context context, String columnName, String selectionArg)563 private static void markCallLogAsRead(Context context, String columnName, 564 String selectionArg) { 565 if (context.checkSelfPermission(Manifest.permission.WRITE_CALL_LOG) 566 != PackageManager.PERMISSION_GRANTED) { 567 L.w(TAG, "Missing WRITE_CALL_LOG permission; not marking missed calls as read."); 568 return; 569 } 570 ContentValues contentValues = new ContentValues(); 571 contentValues.put(CallLog.Calls.NEW, 0); 572 contentValues.put(CallLog.Calls.IS_READ, 1); 573 574 List<String> selectionArgs = new ArrayList<>(); 575 StringBuilder where = new StringBuilder(); 576 where.append(CallLog.Calls.NEW); 577 where.append(" = 1 AND "); 578 where.append(CallLog.Calls.TYPE); 579 where.append(" = ?"); 580 selectionArgs.add(Integer.toString(CallLog.Calls.MISSED_TYPE)); 581 if (!TextUtils.isEmpty(columnName) && !TextUtils.isEmpty(selectionArg)) { 582 where.append(" AND "); 583 where.append(columnName); 584 where.append(" = ?"); 585 selectionArgs.add(selectionArg); 586 } 587 String[] selectionArgsArray = new String[0]; 588 try { 589 ContentResolver contentResolver = context.getContentResolver(); 590 contentResolver.update( 591 CallLog.Calls.CONTENT_URI, 592 contentValues, 593 where.toString(), 594 selectionArgs.toArray(selectionArgsArray)); 595 // #update doesn't notify change any more. Notify change to rerun query from database. 596 contentResolver.notifyChange(CallLog.Calls.CONTENT_URI, null); 597 } catch (IllegalArgumentException e) { 598 L.e(TAG, "markCallLogAsRead failed", e); 599 } 600 } 601 602 /** 603 * Returns the initials based on the name and nameAlt. 604 * 605 * @param name should be the display name of a contact. 606 * @param nameAlt should be alternative display name of a contact. 607 */ getInitials(String name, String nameAlt)608 public static String getInitials(String name, String nameAlt) { 609 StringBuilder initials = new StringBuilder(); 610 if (!TextUtils.isEmpty(name) && Character.isLetter(name.charAt(0))) { 611 initials.append(Character.toUpperCase(name.charAt(0))); 612 } 613 if (!TextUtils.isEmpty(nameAlt) 614 && !TextUtils.equals(name, nameAlt) 615 && Character.isLetter(nameAlt.charAt(0))) { 616 initials.append(Character.toUpperCase(nameAlt.charAt(0))); 617 } 618 return initials.toString(); 619 } 620 621 /** 622 * Creates a Letter Tile Icon that will display the given initials. If the initials are null, 623 * then an avatar anonymous icon will be drawn. 624 **/ createLetterTile(Context context, @Nullable String initials, String identifier, int avatarSize, float cornerRadiusPercent)625 public static Icon createLetterTile(Context context, @Nullable String initials, 626 String identifier, int avatarSize, float cornerRadiusPercent) { 627 LetterTileDrawable letterTileDrawable = TelecomUtils.createLetterTile(context, initials, 628 identifier); 629 RoundedBitmapDrawable roundedBitmapDrawable = RoundedBitmapDrawableFactory.create( 630 context.getResources(), letterTileDrawable.toBitmap(avatarSize)); 631 return createFromRoundedBitmapDrawable(roundedBitmapDrawable, avatarSize, 632 cornerRadiusPercent); 633 } 634 635 /** Creates an Icon based on the given roundedBitmapDrawable. **/ createFromRoundedBitmapDrawable(RoundedBitmapDrawable roundedBitmapDrawable, int avatarSize, float cornerRadiusPercent)636 public static Icon createFromRoundedBitmapDrawable(RoundedBitmapDrawable roundedBitmapDrawable, 637 int avatarSize, float cornerRadiusPercent) { 638 float radius = avatarSize * cornerRadiusPercent; 639 roundedBitmapDrawable.setCornerRadius(radius); 640 641 final Bitmap result = Bitmap.createBitmap(avatarSize, avatarSize, 642 Bitmap.Config.ARGB_8888); 643 final Canvas canvas = new Canvas(result); 644 roundedBitmapDrawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); 645 roundedBitmapDrawable.draw(canvas); 646 return Icon.createWithBitmap(result); 647 } 648 649 /** 650 * Sets the direction of a string, used for displaying phone numbers. 651 */ getBidiWrappedNumber(String string)652 public static String getBidiWrappedNumber(String string) { 653 return BidiFormatter.getInstance().unicodeWrap(string, TextDirectionHeuristics.LTR); 654 } 655 makeResourceUri(Context context, int resourceId)656 private static Uri makeResourceUri(Context context, int resourceId) { 657 return new Uri.Builder() 658 .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) 659 .encodedAuthority(context.getPackageName()) 660 .appendEncodedPath(String.valueOf(resourceId)) 661 .build(); 662 } 663 664 /** 665 * This is a workaround for Log.Pii(). It will only show the last {@link #PII_STRING_LENGTH} 666 * characters. 667 */ piiLog(Object pii)668 public static String piiLog(Object pii) { 669 String piiString = String.valueOf(pii); 670 return piiString.length() >= PII_STRING_LENGTH ? "*" + piiString.substring( 671 piiString.length() - PII_STRING_LENGTH) : piiString; 672 } 673 674 /** 675 * Returns true if contacts are sorted by their first names. Returns false if they are sorted by 676 * last names. 677 */ isSortByFirstName(Integer sortMethod)678 public static boolean isSortByFirstName(Integer sortMethod) { 679 return SORT_BY_FIRST_NAME.equals(sortMethod); 680 } 681 } 682