Update the way of drawing letter tile

Added config to determine how many letters to show for contacts
that do not have avatars.

Bug: 142914836
Test: Manually
Change-Id: I16571467d65f210afabf9293fc408132ee4dd40e
diff --git a/car-apps-common/res/values/config.xml b/car-apps-common/res/values/config.xml
index 1b8514d..f274eb8 100644
--- a/car-apps-common/res/values/config.xml
+++ b/car-apps-common/res/values/config.xml
@@ -35,4 +35,10 @@
     <string name="config_letter_tile_font_family" translatable="false">sans-serif-light</string>
     <!-- Typeface.NORMAL=0; Typeface.BOLD=1; Typeface.ITALIC=2; Typeface.BOLD_ITALIC=3-->
     <integer name="config_letter_tile_text_style">0</integer>
+
+    <!-- This value will determine how many letters to show in a letter tile drawable for
+         the contacts that don't have avatars. The value can be 2 (show initials),
+         1 (show one letter) or 0 (show avatar anonymous icon)
+    -->
+    <integer name="config_number_of_letters_shown_for_avatar">1</integer>
 </resources>
diff --git a/car-apps-common/src/com/android/car/apps/common/LetterTileDrawable.java b/car-apps-common/src/com/android/car/apps/common/LetterTileDrawable.java
index 5abeefe..3a698b0 100644
--- a/car-apps-common/src/com/android/car/apps/common/LetterTileDrawable.java
+++ b/car-apps-common/src/com/android/car/apps/common/LetterTileDrawable.java
@@ -15,7 +15,6 @@
  */
 package com.android.car.apps.common;
 
-import android.annotation.Nullable;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.graphics.Bitmap;
@@ -29,6 +28,8 @@
 import android.graphics.drawable.Drawable;
 import android.text.TextUtils;
 
+import androidx.annotation.Nullable;
+
 /**
  * A drawable that encapsulates all the functionality needed to display a letter tile to
  * represent a contact image.
@@ -46,7 +47,6 @@
     /** Reusable components to avoid new allocations */
     private static final Paint sPaint = new Paint();
     private static final Rect sRect = new Rect();
-    private static final char[] sFirstChar = new char[1];
 
     /** Contact type constants */
     public static final int TYPE_PERSON = 1;
@@ -56,30 +56,43 @@
 
     private final Paint mPaint;
 
-    @Nullable private String mDisplayName;
+    private String mLetters;
     private int mColor;
     private int mContactType = TYPE_DEFAULT;
     private float mScale = 1.0f;
     private float mOffset = 0.0f;
     private boolean mIsCircle = false;
 
-    // TODO(rogerxue): the use pattern for this class is always:
-    // create LTD, setContactDetails(), setIsCircular(true). merge them into ctor.
+    /**
+     * A custom Drawable that draws letters on a colored background.
+     */
+    // The use pattern for this constructor is:
+    // create LTD, setContactDetails(), and setIsCircular(true) if needed.
     public LetterTileDrawable(final Resources res) {
+        this(res, null, null);
+    }
+
+    /**
+     * A custom Drawable that draws letters on a colored background.
+     */
+    // This constructor allows passing the letters and identifier directly. There is no need to
+    // call setContactDetails() again. setIsCircular(true) needs to be called separately if needed.
+    public LetterTileDrawable(final Resources res, @Nullable String letters,
+            @Nullable String identifier) {
         mPaint = new Paint();
         mPaint.setFilterBitmap(true);
         mPaint.setDither(true);
         setScale(0.7f);
 
         if (sColors == null) {
-            sDefaultColor = res.getColor(R.color.letter_tile_default_color);
+            sDefaultColor = res.getColor(R.color.letter_tile_default_color, null /* theme */);
             TypedArray ta = res.obtainTypedArray(R.array.letter_tile_colors);
             if (ta.length() == 0) {
                 // TODO(dnotario). Looks like robolectric shadow doesn't currently support
                 // obtainTypedArray and always returns length 0 array, which will make some code
                 // below that does a division by length of sColors choke. Workaround by creating
                 // an array of length 1. A more proper fix tracked by b/26518438.
-                sColors = new int[] { sDefaultColor };
+                sColors = new int[]{sDefaultColor};
 
             } else {
                 sColors = new int[ta.length()];
@@ -89,7 +102,7 @@
                 ta.recycle();
             }
 
-            sTileFontColor = res.getColor(R.color.letter_tile_font_color);
+            sTileFontColor = res.getColor(R.color.letter_tile_font_color, null /* theme */);
             sLetterToTileRatio = res.getFraction(R.fraction.letter_to_tile_ratio, 1, 1);
             // TODO: get images for business and voicemail
             sDefaultPersonAvatar = res.getDrawable(R.drawable.ic_person, null /* theme */);
@@ -101,6 +114,8 @@
             sPaint.setTextAlign(Align.CENTER);
             sPaint.setAntiAlias(true);
         }
+
+        setContactDetails(letters, identifier);
     }
 
     @Override
@@ -150,20 +165,16 @@
             canvas.drawRect(bounds, sPaint);
         }
 
-        // Draw letter/digit only if the first character is an english letter
-        if (!TextUtils.isEmpty(mDisplayName) && isEnglishLetter(mDisplayName.charAt(0))) {
-            // Draw letter or digit.
-            sFirstChar[0] = Character.toUpperCase(mDisplayName.charAt(0));
-
+        if (!TextUtils.isEmpty(mLetters)) {
             // Scale text by canvas bounds and user selected scaling factor
             sPaint.setTextSize(mScale * sLetterToTileRatio * minDimension);
             //sPaint.setTextSize(sTileLetterFontSize);
-            sPaint.getTextBounds(sFirstChar, 0, 1, sRect);
+            sPaint.getTextBounds(mLetters, 0, mLetters.length(), sRect);
             sPaint.setColor(sTileFontColor);
 
             // Draw the letter in the canvas, vertically shifted up or down by the user-defined
             // offset
-            canvas.drawText(sFirstChar, 0, 1, bounds.centerX(),
+            canvas.drawText(mLetters, 0, mLetters.length(), bounds.centerX(),
                     bounds.centerY() + mOffset * bounds.height() + sRect.height() / 2,
                     sPaint);
         } else {
@@ -204,10 +215,6 @@
         }
     }
 
-    private static boolean isEnglishLetter(final char c) {
-        return ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z');
-    }
-
     @Override
     public void setAlpha(final int alpha) {
         mPaint.setAlpha(alpha);
@@ -227,7 +234,7 @@
      * Scale the drawn letter tile to a ratio of its default size
      *
      * @param scale The ratio the letter tile should be scaled to as a percentage of its default
-     * size, from a scale of 0 to 2.0f. The default is 1.0f.
+     *              size, from a scale of 0 to 2.0f. The default is 1.0f.
      */
     public void setScale(float scale) {
         mScale = scale;
@@ -237,20 +244,26 @@
      * Assigns the vertical offset of the position of the letter tile to the ContactDrawable
      *
      * @param offset The provided offset must be within the range of -0.5f to 0.5f.
-     * If set to -0.5f, the letter will be shifted upwards by 0.5 times the height of the canvas
-     * it is being drawn on, which means it will be drawn with the center of the letter starting
-     * at the top edge of the canvas.
-     * If set to 0.5f, the letter will be shifted downwards by 0.5 times the height of the canvas
-     * it is being drawn on, which means it will be drawn with the center of the letter starting
-     * at the bottom edge of the canvas.
-     * The default is 0.0f.
+     *               If set to -0.5f, the letter will be shifted upwards by 0.5 times the height of
+     *               the canvas it is being drawn on, which means it will be drawn with the center
+     *               of the letter starting at the top edge of the canvas.
+     *               If set to 0.5f, the letter will be shifted downwards by 0.5 times the height of
+     *               the canvas it is being drawn on, which means it will be drawn with the center
+     *               of the letter starting at the bottom edge of the canvas.
+     *               The default is 0.0f.
      */
     public void setOffset(float offset) {
         mOffset = offset;
     }
 
-    public void setContactDetails(@Nullable String displayName, String identifier) {
-        mDisplayName = displayName;
+    /**
+     * Sets the details.
+     *
+     * @param letters    The letters need to be drawn
+     * @param identifier decides the color for the drawable.
+     */
+    public void setContactDetails(@Nullable String letters, @Nullable String identifier) {
+        mLetters = letters;
         mColor = pickColor(identifier);
     }
 
@@ -264,6 +277,7 @@
 
     /**
      * Convert the drawable to a bitmap.
+     *
      * @param size The target size of the bitmap.
      * @return A bitmap representation of the drawable.
      */
diff --git a/car-telephony-common/src/com/android/car/telephony/common/Contact.java b/car-telephony-common/src/com/android/car/telephony/common/Contact.java
index 90ae0f7..69904b4 100644
--- a/car-telephony-common/src/com/android/car/telephony/common/Contact.java
+++ b/car-telephony-common/src/com/android/car/telephony/common/Contact.java
@@ -161,6 +161,11 @@
     private PhoneNumber mPrimaryPhoneNumber;
 
     /**
+     * The initials of the contact's name.
+     */
+    private String mInitials;
+
+    /**
      * Parses a Contact entry for a Cursor loaded from the Contact Database.
      */
     public static Contact fromCursor(Context context, Cursor cursor) {
@@ -297,6 +302,19 @@
     }
 
     /**
+     * Returns the initials of the contact's name.
+     */
+    //TODO: update how to get initials after refactoring. Could use last name and first name to
+    // get initials after refactoring to avoid error for those names with prefix.
+    public String getInitials() {
+        if (mInitials == null) {
+            mInitials = TelecomUtils.getInitials(mDisplayName, mAltDisplayName);
+        }
+
+        return mInitials;
+    }
+
+    /**
      * Merges a Contact entry with another if they represent different numbers of the same contact.
      *
      * @return A merged contact.
diff --git a/car-telephony-common/src/com/android/car/telephony/common/TelecomUtils.java b/car-telephony-common/src/com/android/car/telephony/common/TelecomUtils.java
index f3a2b0e..25ac9d4 100644
--- a/car-telephony-common/src/com/android/car/telephony/common/TelecomUtils.java
+++ b/car-telephony-common/src/com/android/car/telephony/common/TelecomUtils.java
@@ -147,13 +147,15 @@
     public static final class PhoneNumberInfo {
         private final String mPhoneNumber;
         private final String mDisplayName;
+        private final String mInitials;
         private final Uri mAvatarUri;
         private final String mTypeLabel;
 
         public PhoneNumberInfo(String phoneNumber, String displayName,
-                Uri avatarUri, String typeLabel) {
+                String initials, Uri avatarUri, String typeLabel) {
             mPhoneNumber = phoneNumber;
             mDisplayName = displayName;
+            mInitials = initials;
             mAvatarUri = avatarUri;
             mTypeLabel = typeLabel;
         }
@@ -166,6 +168,16 @@
             return mDisplayName;
         }
 
+        /**
+         * Returns the initials of the contact related to the phone number. Returns null if there is
+         * no related contact.
+         */
+        @Nullable
+        public String getInitials() {
+            return mInitials;
+        }
+
+        @Nullable
         public Uri getAvatarUri() {
             return mAvatarUri;
         }
@@ -189,6 +201,7 @@
                     number,
                     context.getString(R.string.unknown),
                     null,
+                    null,
                     ""));
         }
 
@@ -196,6 +209,7 @@
             return CompletableFuture.completedFuture(new PhoneNumberInfo(
                     number,
                     context.getString(R.string.voicemail),
+                    null,
                     makeResourceUri(context, R.drawable.ic_voicemail),
                     ""));
         }
@@ -223,6 +237,7 @@
                 return CompletableFuture.completedFuture(new PhoneNumberInfo(
                         number,
                         name,
+                        contact.getInitials(),
                         contact.getAvatarUri(),
                         typeLabel.toString()));
             }
@@ -230,13 +245,16 @@
 
         return CompletableFuture.supplyAsync(() -> {
             String name = null;
+            String nameAlt = null;
             String photoUriString = null;
             CharSequence typeLabel = "";
             ContentResolver cr = context.getContentResolver();
+            String initials;
             try (Cursor cursor = cr.query(
                     Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)),
-                    new String[] {
+                    new String[]{
                             PhoneLookup.DISPLAY_NAME,
+                            PhoneLookup.DISPLAY_NAME_ALTERNATIVE,
                             PhoneLookup.PHOTO_URI,
                             PhoneLookup.TYPE,
                             PhoneLookup.LABEL,
@@ -244,14 +262,23 @@
                     null, null, null)) {
 
                 if (cursor != null && cursor.moveToFirst()) {
-                    name = cursor.getString(0);
-                    photoUriString = cursor.getString(1);
-                    int type = cursor.getInt(2);
-                    String label = cursor.getString(3);
+                    int nameColumn = cursor.getColumnIndex(PhoneLookup.DISPLAY_NAME);
+                    int altNameColumn = cursor.getColumnIndex(PhoneLookup.DISPLAY_NAME_ALTERNATIVE);
+                    int photoUriColumn = cursor.getColumnIndex(PhoneLookup.PHOTO_URI);
+                    int typeColumn = cursor.getColumnIndex(PhoneLookup.TYPE);
+                    int labelColumn = cursor.getColumnIndex(PhoneLookup.LABEL);
+
+                    name = cursor.getString(nameColumn);
+                    nameAlt = cursor.getString(altNameColumn);
+                    photoUriString = cursor.getString(photoUriColumn);
+                    int type = cursor.getInt(typeColumn);
+                    String label = cursor.getString(labelColumn);
                     typeLabel = Phone.getTypeLabel(context.getResources(), type, label);
                 }
             }
 
+            initials = getInitials(name, nameAlt);
+
             if (name == null) {
                 name = getFormattedNumber(context, number);
             }
@@ -260,7 +287,7 @@
                 name = context.getString(R.string.unknown);
             }
 
-            return new PhoneNumberInfo(number, name,
+            return new PhoneNumberInfo(number, name, initials,
                     TextUtils.isEmpty(photoUriString) ? null : Uri.parse(photoUriString),
                     typeLabel.toString());
         });
@@ -312,31 +339,50 @@
     }
 
     /**
-     * Sets a Contact avatar onto the provided {@code icon}. The first letter of the contact's
-     * display name or {@code fallbackDisplayName} will be used as a fallback resource if avatar
-     * loading fails.
+     * Sets a Contact avatar onto the provided {@code icon}. The first letter or both letters
+     * of the contact's initials.
      */
     public static void setContactBitmapAsync(
             Context context,
-            final ImageView icon,
-            @Nullable final Contact contact,
-            @Nullable final String fallbackDisplayName) {
-        Uri avatarUri = contact != null ? contact.getAvatarUri() : null;
-        String displayName = contact != null ? contact.getDisplayName() : fallbackDisplayName;
-
-        setContactBitmapAsync(context, icon, avatarUri, displayName);
+            @Nullable final ImageView icon,
+            @Nullable final Contact contact) {
+        setContactBitmapAsync(context, icon, contact, null);
     }
 
     /**
-     * Sets a Contact avatar onto the provided {@code icon}. The first letter of the contact's
-     * display name will be used as a fallback resource if avatar loading fails.
+     * Sets a Contact avatar onto the provided {@code icon}. The first letter or both letters
+     * of the contact's initials or {@code fallbackDisplayName} will be used as a fallback resource
+     * if avatar loading fails.
      */
     public static void setContactBitmapAsync(
             Context context,
-            final ImageView icon,
-            final Uri avatarUri,
-            final String displayName) {
-        LetterTileDrawable letterTileDrawable = createLetterTile(context, displayName);
+            @Nullable final ImageView icon,
+            @Nullable final Contact contact,
+            @Nullable final String fallbackDisplayName) {
+        Uri avatarUri = contact != null ? contact.getAvatarUri() : null;
+        String initials = contact != null
+                ? contact.getInitials() : fallbackDisplayName.substring(0, 1);
+        String identifier = TextUtils.isEmpty(initials) ? fallbackDisplayName : initials;
+
+        setContactBitmapAsync(context, icon, avatarUri, initials, identifier);
+    }
+
+    /**
+     * Sets a Contact avatar onto the provided {@code icon}. A letter tile base on the contact's
+     * initials and identifier will be used as a fallback resource if avatar loading fails.
+     */
+    public static void setContactBitmapAsync(
+            Context context,
+            @Nullable final ImageView icon,
+            @Nullable final Uri avatarUri,
+            @Nullable final String initials,
+            final String identifier) {
+        if (icon == null) {
+            return;
+        }
+
+        LetterTileDrawable letterTileDrawable = createLetterTile(context, initials,
+                TextUtils.isEmpty(initials) ? identifier : initials);
 
         Glide.with(context)
                 .load(avatarUri)
@@ -344,11 +390,25 @@
                 .into(icon);
     }
 
-    /** Create a {@link LetterTileDrawable} for the given display name. */
-    public static LetterTileDrawable createLetterTile(Context context, String displayName) {
-        LetterTileDrawable letterTileDrawable = new LetterTileDrawable(context.getResources());
-        letterTileDrawable.setContactDetails(displayName, displayName);
-        return  letterTileDrawable;
+    /**
+     * Create a {@link LetterTileDrawable} for the given initials.
+     *
+     * @param initials   is the letters that will be drawn on the canvas. If it is null, then
+     *                   an avatar anonymous icon will be drawn
+     * @param identifier will decide the color for the drawable. If null, a default color will
+     *                   be used.
+     */
+    public static LetterTileDrawable createLetterTile(
+            Context context,
+            @Nullable String initials,
+            @Nullable String identifier) {
+        int numberOfLetter = context.getResources().getInteger(
+                R.integer.config_number_of_letters_shown_for_avatar);
+        String letters = initials != null
+                ? initials.substring(0, Math.min(initials.length(), numberOfLetter)) : null;
+        LetterTileDrawable letterTileDrawable = new LetterTileDrawable(context.getResources(),
+                letters, identifier);
+        return letterTileDrawable;
     }
 
     /** Set the given phone number as the primary phone number for its associated contact. */
@@ -419,6 +479,19 @@
         }
     }
 
+    static String getInitials(String name, String nameAlt) {
+        StringBuilder initials = new StringBuilder();
+        if (!TextUtils.isEmpty(name) && Character.isLetter(name.charAt(0))) {
+            initials.append(Character.toUpperCase(name.charAt(0)));
+        }
+        if (!TextUtils.isEmpty(nameAlt)
+                && !TextUtils.equals(name, nameAlt)
+                && Character.isLetter(nameAlt.charAt(0))) {
+            initials.append(Character.toUpperCase(nameAlt.charAt(0)));
+        }
+        return initials.toString();
+    }
+
     private static Uri makeResourceUri(Context context, int resourceId) {
         return new Uri.Builder()
                 .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)