• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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