DO NOT MERGE Make Group Conversation Titles RTL friendly

Bug: 159221409
Test: unit test
Change-Id: Ifce6b44ceefe1409bf989b036b68b1a5c16ccc93
diff --git a/car-messenger-common/Android.bp b/car-messenger-common/Android.bp
index 19ed50f..f40efac 100644
--- a/car-messenger-common/Android.bp
+++ b/car-messenger-common/Android.bp
@@ -19,6 +19,8 @@
 
     srcs: ["src/**/*.java"],
 
+    manifest: "AndroidManifest.xml",
+
     optimize: {
         enabled: false,
     },
@@ -33,5 +35,8 @@
         "car-apps-common-bp",
         "car-messenger-protos",
         "connected-device-protos",
+        "libphonenumber",
     ],
+
+    platform_apis: true,
 }
diff --git a/car-messenger-common/res/values/strings.xml b/car-messenger-common/res/values/strings.xml
index ff604e2..b19ffdb 100644
--- a/car-messenger-common/res/values/strings.xml
+++ b/car-messenger-common/res/values/strings.xml
@@ -41,6 +41,6 @@
     <string name="name_not_available">Name not available</string>
 
     <!-- Formats a group conversation's title for a message notification. The format is: <Sender of last message> mdot <Name of the conversation>.-->
-    <string name="group_conversation_title_separator" translatable="false">%1$s&#160;&#8226;&#160;%2$s</string>
+    <string name="group_conversation_title_separator" translatable="false">&#160;&#8226;&#160;</string>
 
 </resources>
diff --git a/car-messenger-common/src/com/android/car/messenger/common/BaseNotificationDelegate.java b/car-messenger-common/src/com/android/car/messenger/common/BaseNotificationDelegate.java
index ed42e98..b0a380c 100644
--- a/car-messenger-common/src/com/android/car/messenger/common/BaseNotificationDelegate.java
+++ b/car-messenger-common/src/com/android/car/messenger/common/BaseNotificationDelegate.java
@@ -230,9 +230,9 @@
             }
         });
         if (notificationInfo.isGroupConvo()) {
-            messagingStyle.setConversationTitle(
-                    mContext.getString(R.string.group_conversation_title_separator,
-                            lastMessage.getSenderName(), notificationInfo.getConvoTitle()));
+            messagingStyle.setConversationTitle(Utils.constructGroupConversationHeader(
+                    lastMessage.getSenderName(), notificationInfo.getConvoTitle(),
+                    mContext.getString(R.string.group_conversation_title_separator)));
         }
 
         // We are creating this notification for the first time.
diff --git a/car-messenger-common/src/com/android/car/messenger/common/Utils.java b/car-messenger-common/src/com/android/car/messenger/common/Utils.java
index 00e18f8..99ec52f 100644
--- a/car-messenger-common/src/com/android/car/messenger/common/Utils.java
+++ b/car-messenger-common/src/com/android/car/messenger/common/Utils.java
@@ -21,6 +21,8 @@
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
+import android.text.BidiFormatter;
+import android.text.TextDirectionHeuristics;
 import android.text.TextUtils;
 
 import androidx.annotation.Nullable;
@@ -34,6 +36,15 @@
 import com.android.car.messenger.NotificationMsgProto.NotificationMsg.MessagingStyleMessage;
 import com.android.car.messenger.NotificationMsgProto.NotificationMsg.Person;
 
+import com.google.i18n.phonenumbers.NumberParseException;
+import com.google.i18n.phonenumbers.PhoneNumberUtil;
+import com.google.i18n.phonenumbers.Phonenumber;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.Collectors;
+
 /** Utils methods for the car-messenger-common lib. **/
 public class Utils {
     private static final String TAG = "CMC.Utils";
@@ -246,4 +257,119 @@
         return initials.toString();
     }
 
+    /**
+     * Creates a Header for a group conversation, where the senderName and groupName are both shown,
+     * separated by a delimiter.
+     *
+     * @param senderName Sender's name.
+     * @param groupName  Group conversation's name.
+     * @param delimiter  delimiter that separates each element.
+     */
+    public static String constructGroupConversationHeader(String senderName, String groupName,
+            String delimiter) {
+        return constructGroupConversationHeader(senderName, groupName, delimiter,
+                BidiFormatter.getInstance());
+    }
+
+    /**
+     * Creates a Header for a group conversation, where the senderName and groupName are both shown,
+     * separated by a delimiter.
+     *
+     * @param senderName Sender's name.
+     * @param groupName  Group conversation's name.
+     * @param delimiter  delimiter that separates each element.
+     * @param bidiFormatter  formatter for the context's locale.
+     */
+    public static String constructGroupConversationHeader(String senderName, String groupName,
+            String delimiter, BidiFormatter bidiFormatter) {
+        String formattedSenderName = bidiFormatter.unicodeWrap(senderName,
+                TextDirectionHeuristics.FIRSTSTRONG_LTR);
+        String formattedGroupName = bidiFormatter.unicodeWrap(groupName,
+                TextDirectionHeuristics.LOCALE);
+        String title = String.join(delimiter, formattedSenderName, formattedGroupName);
+        return bidiFormatter.unicodeWrap(title, TextDirectionHeuristics.LOCALE);
+    }
+
+    /**
+     * Given a name of all the participants in a group conversation (some names might be phone
+     * numbers), this function creates the conversation title by putting the names in alphabetical
+     * order first, then adding any phone numbers. This title should not exceed the
+     * conversationTitleLength, so not all participants' names are guaranteed to be
+     * in the conversation title.
+     */
+    public static String constructGroupConversationTitle(List<String> names, String delimiter,
+            int conversationTitleLength) {
+        return constructGroupConversationTitle(names, delimiter, conversationTitleLength,
+                BidiFormatter.getInstance());
+    }
+
+    /**
+     * Given a name of all the participants in a group conversation (some names might be phone
+     * numbers), this function creates the conversation title by putting the names in alphabetical
+     * order first, then adding any phone numbers. This title should not exceed the
+     * conversationTitleLength, so not all participants' names are guaranteed to be
+     * in the conversation title.
+     */
+    public static String constructGroupConversationTitle(List<String> names, String delimiter,
+            int conversationTitleLength, BidiFormatter bidiFormatter) {
+        List<String> sortedNames = getSortedSubsetNames(names, conversationTitleLength,
+                delimiter.length());
+        String formattedDelimiter = bidiFormatter.unicodeWrap(delimiter,
+                TextDirectionHeuristics.LOCALE);
+
+        String conversationName = sortedNames.stream().map(name -> bidiFormatter.unicodeWrap(name,
+                TextDirectionHeuristics.FIRSTSTRONG_LTR))
+                .collect(Collectors.joining(formattedDelimiter));
+        return bidiFormatter.unicodeWrap(conversationName, TextDirectionHeuristics.LOCALE);
+    }
+
+    /**
+     * Sorts the list, and returns the first elements whose total length is less than the given
+     * conversationTitleLength.
+     */
+    private static List<String> getSortedSubsetNames(List<String> names,
+            int conversationTitleLength,
+            int delimiterLength) {
+        Collections.sort(names, Utils.ALPHA_THEN_NUMERIC_COMPARATOR);
+        int namesCounter = 0;
+        int indexCounter = 0;
+        while (namesCounter < conversationTitleLength && indexCounter < names.size()) {
+            namesCounter = namesCounter + names.get(indexCounter).length() + delimiterLength;
+            indexCounter = indexCounter + 1;
+        }
+        return names.subList(0, indexCounter);
+    }
+
+    /** Comparator that sorts names alphabetically first, then phone numbers numerically. **/
+    public static final Comparator<String> ALPHA_THEN_NUMERIC_COMPARATOR =
+            new Comparator<String>() {
+                private boolean isPhoneNumber(String input) {
+                    PhoneNumberUtil util = PhoneNumberUtil.getInstance();
+                    try {
+                        Phonenumber.PhoneNumber phoneNumber = util.parse(input, /* defaultRegion */
+                                null);
+                        return util.isValidNumber(phoneNumber);
+                    } catch (NumberParseException e) {
+                        return false;
+                    }
+                }
+
+                private boolean isOfSameType(String o1, String o2) {
+                    boolean isO1PhoneNumber = isPhoneNumber(o1);
+                    boolean isO2PhoneNumber = isPhoneNumber(o2);
+                    return isO1PhoneNumber == isO2PhoneNumber;
+                }
+
+                @Override
+                public int compare(String o1, String o2) {
+                    // if both are names, sort based on names.
+                    // if both are number, sort numerically.
+                    // if one is phone number and the other is a name, give name precedence.
+                    if (!isOfSameType(o1, o2)) {
+                        return isPhoneNumber(o1) ? 1 : -1;
+                    } else {
+                        return o1.compareTo(o2);
+                    }
+                }
+            };
 }
diff --git a/car-messenger-common/tests/unit/Android.bp b/car-messenger-common/tests/unit/Android.bp
new file mode 100644
index 0000000..c5d29fa
--- /dev/null
+++ b/car-messenger-common/tests/unit/Android.bp
@@ -0,0 +1,39 @@
+//
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+android_test {
+    name: "car-messenger-common-lib-unit-tests",
+
+    srcs: ["src/**/*.java"],
+
+    libs: [
+        "android.test.runner",
+        "android.test.base",
+        "android.test.mock",
+    ],
+
+    static_libs: [
+        "android.car",
+        "androidx.test.core",
+        "androidx.test.ext.junit",
+        "androidx.test.rules",
+        "car-messenger-common",
+        "mockito-target-extended-minus-junit4",
+        "truth-prebuilt",
+    ],
+
+    platform_apis: true,
+}
\ No newline at end of file
diff --git a/car-messenger-common/tests/unit/AndroidManifest.xml b/car-messenger-common/tests/unit/AndroidManifest.xml
new file mode 100644
index 0000000..eefce32
--- /dev/null
+++ b/car-messenger-common/tests/unit/AndroidManifest.xml
@@ -0,0 +1,36 @@
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.car.messenger.common.tests.unit">
+    <uses-permission android:name="android.car.permission.ACCESS_CAR_PROJECTION_STATUS"/>
+    <application android:testOnly="true"
+                 android:debuggable="true"
+                 xmlns:tools="http://schemas.android.com/tools">
+        <uses-library android:name="android.test.runner" />
+        <!-- Workaround for b/113294940 -->
+        <provider
+            android:name="androidx.lifecycle.ProcessLifecycleOwnerInitializer"
+            tools:replace="android:authorities"
+            android:authorities="${applicationId}.lifecycle"
+            android:exported="false"
+            android:multiprocess="true" />
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.car.messenger.common.tests.unit"
+                     android:label="Car Messenger Lib Test Cases" />
+</manifest>
\ No newline at end of file
diff --git a/car-messenger-common/tests/unit/README.md b/car-messenger-common/tests/unit/README.md
new file mode 100644
index 0000000..12bb628
--- /dev/null
+++ b/car-messenger-common/tests/unit/README.md
@@ -0,0 +1,24 @@
+# Instructions for running unit tests
+
+### Build unit test module
+
+`m car-messenger-common-lib-unit-tests`
+
+### Install resulting apk on device
+
+`adb install -r -t $OUT/testcases/car-messenger-common-lib-unit-tests/arm64/car-messenger-common-lib-unit-tests.apk`
+
+### Run all tests
+
+`adb shell am instrument -w com.android.car.messenger.common.tests.unit`
+
+### Run tests in a class
+
+`adb shell am instrument -w -e class com.android.car.messenger.common.<classPath> com.android.car.messenger.common.tests.unit`
+
+### Run a specific test
+
+`adb shell am instrument -w -e class com.android.car.messenger.common.<classPath>#<testMethod> com.android.car.messenger.common.tests.unit`
+
+More general information can be found at
+http://developer.android.com/reference/android/support/test/runner/AndroidJUnitRunner.html
\ No newline at end of file
diff --git a/car-messenger-common/tests/unit/src/com.android.car.messenger.common/UtilsTest.java b/car-messenger-common/tests/unit/src/com.android.car.messenger.common/UtilsTest.java
new file mode 100644
index 0000000..acdffbd
--- /dev/null
+++ b/car-messenger-common/tests/unit/src/com.android.car.messenger.common/UtilsTest.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.messenger.common;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.text.BidiFormatter;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class UtilsTest {
+
+    private static final String ARABIC_NAME = "جﺗﺧ";
+    private static final List<String> NAMES = Arrays.asList("+1-650-900-1234", "Logan.", "Emily",
+            "Christopher", "!Sam", ARABIC_NAME);
+    private static final String NAME_DELIMITER = "، ";
+    private static final String TITLE_DELIMITER = " : ";
+    private static final int TITLE_LENGTH = 30;
+    private static final BidiFormatter RTL_FORMATTER = BidiFormatter.getInstance(/* rtlContext= */
+            true);
+
+    @Test
+    public void testNameWithMultipleNumbers() {
+        // Ensure that a group name with many phone numbers sorts the phone numbers correctly.
+        List<String> senderNames = Arrays.asList("+1-650-900-1234", "+1-650-900-1111",
+                "+1-100-200-1234");
+        String actual = Utils.constructGroupConversationTitle(senderNames, NAME_DELIMITER,
+                TITLE_LENGTH + 20);
+        String expected = "+1-100-200-1234، +1-650-900-1111، +1-650-900-1234";
+        assertThat(actual).isEqualTo(expected);
+    }
+
+    @Test
+    public void testNameWithInternationalNumbers() {
+        // Ensure that a group name with many phone numbers sorts the phone numbers correctly.
+        List<String> senderNames = Arrays.asList("+44-20-7183-8750", "+1-650-900-1111",
+                "+1-100-200-1234");
+        String actual = Utils.constructGroupConversationTitle(senderNames, NAME_DELIMITER,
+                TITLE_LENGTH + 20);
+        String expected = "+1-100-200-1234، +1-650-900-1111، +44-20-7183-8750";
+        assertThat(actual).isEqualTo(expected);
+    }
+
+    @Test
+    public void testNameConstructorLtr() {
+        String actual = Utils.constructGroupConversationTitle(NAMES, NAME_DELIMITER, TITLE_LENGTH);
+        assertThat(actual).isEqualTo("!Sam، Christopher، Emily، Logan.");
+    }
+
+    @Test
+    public void testNameConstructorLtr_longerTitle() {
+        String actual = Utils.constructGroupConversationTitle(NAMES, NAME_DELIMITER,
+                TITLE_LENGTH + 5);
+        assertThat(actual).isEqualTo(
+                "!Sam، Christopher، Emily، Logan.، \u200E\u202Bجﺗﺧ\u202C\u200E");
+
+    }
+
+    @Test
+    public void testTitleConstructorLtr() {
+        String actual = Utils.constructGroupConversationHeader("Christopher",
+                "!Sam، Emily، Logan.، +1-650-900-1234", TITLE_DELIMITER);
+        String expected = "Christopher : !Sam، Emily، Logan.، +1-650-900-1234";
+        assertThat(actual).isEqualTo(expected);
+    }
+
+    @Test
+    public void testTitleConstructorLtr_with_rtlName() {
+        String actual = Utils.constructGroupConversationHeader(ARABIC_NAME, "!Sam، Logan.، جﺗﺧ",
+                TITLE_DELIMITER);
+        // Note: the Group name doesn't have the RTL tag because in the function we format the
+        // entire group name string, not each name in the string.
+        String expected = "\u200E\u202Bجﺗﺧ\u202C\u200E : !Sam، Logan.، جﺗﺧ\u200E";
+        assertThat(actual).isEqualTo(expected);
+    }
+
+    @Test
+    public void testTitleConstructorLtr_with_phoneNumber() {
+        String actual = Utils.constructGroupConversationHeader("+1-650-900-1234",
+                "!Sam، Logan.، جﺗﺧ",
+                TITLE_DELIMITER);
+        // Note: the Group name doesn't have the RTL tag because in the function we format the
+        // entire group name string, not each name in the string.
+        String expected = "+1-650-900-1234 : !Sam، Logan.، جﺗﺧ\u200E";
+        assertThat(actual).isEqualTo(expected);
+    }
+
+    /**
+     * NOTE for all the RTL tests done below: When BidiFormatter is unicode-wrapping strings, they
+     * are actually adding invisible Unicode characters to denote whether a section is RTL, LTR,
+     * etc. These invisible characters are NOT visible on the terminal output, or if you copy
+     * paste the string to most HTML pages. They ARE visible when you paste them in certain
+     * text editors like IntelliJ, or there are some online tools that provide this as well.
+     *
+     * Therefore, in most of these RTL tests (and some of the LTR tests) you will see the
+     * invisible characters in the expected strings. Here's a couple of the characters, and what
+     * they're used for:
+     * \u200F is the RTL mark
+     * \u200E is the LTR mark
+     * \u202A marks the start of LTR embedding
+     * \u202B marks the start of RTL embedding
+     * \u202C pops the directional formatting - Must be used to end an embedding
+     */
+    @Test
+    public void testNameWithInternationalNumbers_rtl() {
+        // Ensure that a group name with many phone numbers sorts the phone numbers correctly.
+        List<String> senderNames = Arrays.asList("+44-20-7183-8750", "+1-650-900-1111",
+                "+1-100-200-1234");
+        String actual = Utils.constructGroupConversationTitle(senderNames, NAME_DELIMITER,
+                TITLE_LENGTH + 20, RTL_FORMATTER);
+        String expected = "\u200F\u202A\u200F\u202A+1-100-200-1234\u202C\u200F\u200F\u202A، "
+                + "\u202C\u200F\u200F\u202A+1-650-900-1111\u202C\u200F\u200F\u202A، "
+                + "\u202C\u200F\u200F\u202A+44-20-7183-8750\u202C\u200F\u202C\u200F";
+        assertThat(actual).isEqualTo(expected);
+    }
+
+    @Test
+    public void testNameConstructorRtl() {
+        String actual = Utils.constructGroupConversationTitle(NAMES, NAME_DELIMITER, TITLE_LENGTH,
+                /* isRtl */ RTL_FORMATTER);
+
+        String expected =
+                "\u200F\u202A\u200F\u202A!Sam\u202C\u200F\u200F\u202A، \u202C\u200F"
+                        + "\u200F\u202AChristopher\u202C\u200F\u200F\u202A، \u202C\u200F"
+                        + "\u200F\u202AEmily\u202C\u200F\u200F\u202A، "
+                        + "\u202C\u200F\u200F\u202ALogan.\u202C\u200F\u202C\u200F";
+        assertThat(actual).isEqualTo(expected);
+    }
+
+    @Test
+    public void testNameConstructorRtl_longerTitle() {
+        String actual = Utils.constructGroupConversationTitle(NAMES, NAME_DELIMITER,
+                TITLE_LENGTH + 5, /* isRtl */ RTL_FORMATTER);
+
+        String expected =
+                "\u200F\u202A\u200F\u202A!Sam\u202C\u200F\u200F\u202A، "
+                        + "\u202C\u200F\u200F\u202AChristopher\u202C\u200F\u200F"
+                        + "\u202A، \u202C\u200F\u200F\u202AEmily\u202C\u200F\u200F\u202A، "
+                        + "\u202C\u200F\u200F\u202ALogan.\u202C\u200F\u200F\u202A، "
+                        + "\u202C\u200Fجﺗﺧ\u202C\u200F";
+        assertThat(actual).isEqualTo(expected);
+    }
+
+    @Test
+    public void testTitleConstructorRtl_with_rtlName() {
+        String actual = Utils.constructGroupConversationHeader(ARABIC_NAME, "!Sam، Logan.، جﺗﺧ",
+                TITLE_DELIMITER, RTL_FORMATTER);
+        // Note: the Group name doesn't have the RTL tag because in the function we format the
+        // entire group name string, not each name in the string.
+        // Also, note that the sender's name, which is RTL still has LTR embedded because we wrap
+        // it with FIRSTSTRONG_LTR.
+        String expected = "\u200F\u202Aجﺗﺧ : \u200F\u202A!Sam، Logan.، جﺗﺧ\u202C\u200F\u202C"
+                + "\u200F";
+        assertThat(actual).isEqualTo(expected);
+    }
+
+
+    @Test
+    public void testTitleConstructorRtl_with_phoneNumber() {
+        String actual = Utils.constructGroupConversationHeader("+1-650-900-1234",
+                "!Sam، Logan.، جﺗﺧ",
+                TITLE_DELIMITER, RTL_FORMATTER);
+        // Note: the Group name doesn't have the RTL tag because in the function we format the
+        // entire group name string, not each name in the string.
+        String expected = "\u200F\u202A\u200F\u202A+1-650-900-1234\u202C\u200F : "
+                + "\u200F\u202A!Sam، Logan.، جﺗﺧ\u202C\u200F\u202C\u200F";
+        assertThat(actual).isEqualTo(expected);
+    }
+
+    @Test
+    public void testTitleConstructorRtl() {
+        String actual = Utils.constructGroupConversationHeader("Christopher",
+                "+1-650-900-1234، Logan.، Emily، Christopher، !Sam", TITLE_DELIMITER, /* isRtl */
+                RTL_FORMATTER).trim();
+
+        String expected =
+                "\u200F\u202A\u200F\u202AChristopher\u202C\u200F : \u200F\u202A+1-650-900-1234، "
+                        + "Logan.، Emily، Christopher، !Sam\u202C\u200F\u202C\u200F";
+
+        assertThat(actual).isEqualTo(expected);
+    }
+}