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