Solve BiDi problem when phone number is concatenated with RTL-string

Solves BiDi problem that might cause phone number to be displayed
incorrectly in the dialer. Issue might occur when a phone number
is concatenated with a custom number label containing an RTL-string.

Solved by changing affected labels from TextView to BidiTextView.

Removed function spanDisplayNumber in BottomRow which are now obsolete.

Updates the DialerBidiFormatter to prevent whitespace to be misplaced
within a BidiTextView.

BUG=78464687
TEST=Manual

Public-Origin-Change-Id: I789a6f76a5ca805c03a437813ce627eef7a0bf2e
Signed-off-by: Linyu He <linyuh@google.com>
Author: Torbjorn Eklund <torbjorn.eklund@sony.com>
Bug: 78464687
Test: Manual
PiperOrigin-RevId: 198823629
Change-Id: Id35c3ebf514609b4a1fe10a7ae5a297bde7c7456
diff --git a/java/com/android/dialer/callcomposer/CallComposerActivity.java b/java/com/android/dialer/callcomposer/CallComposerActivity.java
index 49bc29c..921bbba 100644
--- a/java/com/android/dialer/callcomposer/CallComposerActivity.java
+++ b/java/com/android/dialer/callcomposer/CallComposerActivity.java
@@ -78,6 +78,7 @@
 import com.android.dialer.telecom.TelecomUtil;
 import com.android.dialer.util.UriUtils;
 import com.android.dialer.util.ViewUtil;
+import com.android.dialer.widget.BidiTextView;
 import com.android.dialer.widget.DialerToolbar;
 import com.android.dialer.widget.LockableViewPager;
 import com.android.incallui.callpending.CallPendingActivity;
@@ -135,7 +136,7 @@
   private Long sessionId = Session.NO_SESSION_ID;
 
   private TextView nameView;
-  private TextView numberView;
+  private BidiTextView numberView;
   private QuickContactBadge contactPhoto;
   private RelativeLayout contactContainer;
   private DialerToolbar toolbar;
diff --git a/java/com/android/dialer/callcomposer/res/layout/call_composer_activity.xml b/java/com/android/dialer/callcomposer/res/layout/call_composer_activity.xml
index 49d278f..8b982b9 100644
--- a/java/com/android/dialer/callcomposer/res/layout/call_composer_activity.xml
+++ b/java/com/android/dialer/callcomposer/res/layout/call_composer_activity.xml
@@ -57,7 +57,7 @@
           android:textColor="?android:attr/textColorPrimaryInverse"
           android:textSize="@dimen/call_composer_name_text_size"/>
 
-        <TextView
+        <com.android.dialer.widget.BidiTextView
           android:id="@+id/phone_number"
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
diff --git a/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java b/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java
index 4da7fa0..84a60c1 100644
--- a/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java
+++ b/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java
@@ -54,7 +54,7 @@
   private final CallDetailsHeaderListener callDetailsHeaderListener;
   private final ImageView callbackButton;
   private final BidiTextView nameView;
-  private final TextView numberView;
+  private final BidiTextView numberView;
   private final TextView networkView;
   private final QuickContactBadge contactPhoto;
   private final Context context;
diff --git a/java/com/android/dialer/calldetails/res/layout/contact_container.xml b/java/com/android/dialer/calldetails/res/layout/contact_container.xml
old mode 100644
new mode 100755
index e918feb..4df81f1
--- a/java/com/android/dialer/calldetails/res/layout/contact_container.xml
+++ b/java/com/android/dialer/calldetails/res/layout/contact_container.xml
@@ -51,7 +51,7 @@
         android:layout_height="wrap_content"
         android:layout_marginStart="@dimen/photo_text_margin"/>
 
-    <TextView
+    <com.android.dialer.widget.BidiTextView
         android:id="@+id/phone_number"
         style="@style/Dialer.TextAppearance.Secondary.Ellipsize"
         android:layout_width="wrap_content"
diff --git a/java/com/android/dialer/i18n/DialerBidiFormatter.java b/java/com/android/dialer/i18n/DialerBidiFormatter.java
index 4ebaa66..e882e06 100644
--- a/java/com/android/dialer/i18n/DialerBidiFormatter.java
+++ b/java/com/android/dialer/i18n/DialerBidiFormatter.java
@@ -26,6 +26,7 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 /**
  * An enhanced version of {@link BidiFormatter} that can recognize a formatted phone number
@@ -40,6 +41,9 @@
 
   private DialerBidiFormatter() {}
 
+  // Regular expression that matches a single space in the beginning or end of a string.
+  private static final String REGEXP_SURROUNDING_SPACE = "^[ ]|[ ]$";
+
   /**
    * Divides the given text into segments, applies {@link BidiFormatter#unicodeWrap(CharSequence)}
    * to each segment, and then reassembles the text.
@@ -65,19 +69,49 @@
   }
 
   /**
-   * Segments the given text using {@link Patterns#PHONE}.
+   * Segments the given text into a sequence of substrings using the following procedure.
    *
-   * <p>For example, "Mobile, +1 650-253-0000, 20 seconds" will be segmented into {"Mobile, ", "+1
-   * 650-253-0000", ", 20 seconds"}.
+   * <ol>
+   *   <li>Separate text matching {@link Patterns#PHONE} from others.
+   *       <p>For example: "Mobile, +1 650-253-0000, 20 seconds" will be segmented into<br>
+   *       {"Mobile, ", "+1 650-253-0000", ", 20 seconds"}
+   *   <li>For each substring produced by the previous step, separate a single whitespace at the
+   *       start/end of it from the rest of the substring.
+   *       <p>For example, the first substring "Mobile, " will be segmented into {"Mobile,", " "}.
+   * </ol>
+   *
+   * <p>The final result of segmenting "Mobile, +1 650-253-0000, 20 seconds" is<br>
+   * {"Mobile,", " ", "+1 650-253-0000", ", 20 seconds"}.
+   *
+   * <p>The reason for singling out the whitespace at the start/end of a substring is to prevent it
+   * from being misplaced in RTL context.
    */
   @VisibleForTesting
   static List<CharSequence> segmentText(CharSequence text) {
     Assert.checkArgument(!TextUtils.isEmpty(text));
 
+    // Separate text matching the phone number pattern from others.
+    List<CharSequence> segmentsSeparatingPhoneNumbers = segmentText(text, Patterns.PHONE);
+
+    // For each substring, separate a single whitespace at the start/end of it from the rest of the
+    // substring.
+    List<CharSequence> finalSegments = new ArrayList<>();
+    Pattern patternSurroundingSpace = Pattern.compile(REGEXP_SURROUNDING_SPACE);
+    for (CharSequence segment : segmentsSeparatingPhoneNumbers) {
+      finalSegments.addAll(segmentText(segment, patternSurroundingSpace));
+    }
+
+    return finalSegments;
+  }
+
+  /** Segments the given text into a sequence of substrings using the provided pattern. */
+  private static List<CharSequence> segmentText(CharSequence text, Pattern pattern) {
+    Assert.checkArgument(!TextUtils.isEmpty(text));
+
     List<CharSequence> segments = new ArrayList<>();
 
-    // Find the start index and the end index of each segment matching the phone number pattern.
-    Matcher matcher = Patterns.PHONE.matcher(text.toString());
+    // Find the start index and the end index of each segment matching the pattern.
+    Matcher matcher = pattern.matcher(text.toString());
     List<Range> segmentRanges = new ArrayList<>();
     while (matcher.find()) {
       segmentRanges.add(Range.newBuilder().setStart(matcher.start()).setEnd(matcher.end()).build());
diff --git a/java/com/android/dialer/widget/DialerToolbar.java b/java/com/android/dialer/widget/DialerToolbar.java
index 02ec8c0..40dabe8 100644
--- a/java/com/android/dialer/widget/DialerToolbar.java
+++ b/java/com/android/dialer/widget/DialerToolbar.java
@@ -29,13 +29,13 @@
 public class DialerToolbar extends Toolbar {
 
   private final TextView title;
-  private final TextView subtitle;
+  private final BidiTextView subtitle;
 
   public DialerToolbar(Context context, @Nullable AttributeSet attributeSet) {
     super(context, attributeSet);
     inflate(context, R.layout.dialer_toolbar, this);
     title = (TextView) findViewById(R.id.title);
-    subtitle = (TextView) findViewById(R.id.subtitle);
+    subtitle = (BidiTextView) findViewById(R.id.subtitle);
 
     setElevation(getResources().getDimensionPixelSize(R.dimen.toolbar_elevation));
     setBackgroundColor(ThemeComponent.get(context).theme().getColorPrimary());
diff --git a/java/com/android/dialer/widget/res/layout/dialer_toolbar.xml b/java/com/android/dialer/widget/res/layout/dialer_toolbar.xml
old mode 100644
new mode 100755
index a75176a..3b4d136
--- a/java/com/android/dialer/widget/res/layout/dialer_toolbar.xml
+++ b/java/com/android/dialer/widget/res/layout/dialer_toolbar.xml
@@ -31,7 +31,7 @@
         android:textColor="?android:attr/textColorPrimaryInverse"
         style="@style/Dialer.TextAppearance.Header2"/>
 
-    <TextView
+    <com.android.dialer.widget.BidiTextView
         android:id="@+id/subtitle"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
diff --git a/java/com/android/incallui/contactgrid/BottomRow.java b/java/com/android/incallui/contactgrid/BottomRow.java
index 7388c50..767d666 100644
--- a/java/com/android/incallui/contactgrid/BottomRow.java
+++ b/java/com/android/incallui/contactgrid/BottomRow.java
@@ -18,9 +18,6 @@
 
 import android.content.Context;
 import android.support.annotation.Nullable;
-import android.telephony.PhoneNumberUtils;
-import android.text.BidiFormatter;
-import android.text.TextDirectionHeuristics;
 import android.text.TextUtils;
 import com.android.incallui.call.state.DialerCallState;
 import com.android.incallui.incall.protocol.PrimaryCallState;
@@ -118,21 +115,15 @@
       return primaryInfo.location();
     }
     if (!primaryInfo.nameIsNumber() && !TextUtils.isEmpty(primaryInfo.number())) {
-      CharSequence spannedNumber = spanDisplayNumber(primaryInfo.number());
       if (primaryInfo.label() == null) {
-        return spannedNumber;
+        return primaryInfo.number();
       } else {
-        return TextUtils.concat(primaryInfo.label(), " ", spannedNumber);
+        return TextUtils.concat(primaryInfo.label(), " ", primaryInfo.number());
       }
     }
     return null;
   }
 
-  private static CharSequence spanDisplayNumber(String displayNumber) {
-    return PhoneNumberUtils.createTtsSpannable(
-        BidiFormatter.getInstance().unicodeWrap(displayNumber, TextDirectionHeuristics.LTR));
-  }
-
   private static boolean isIncoming(PrimaryCallState state) {
     return state.state() == DialerCallState.INCOMING
         || state.state() == DialerCallState.CALL_WAITING;
diff --git a/java/com/android/incallui/contactgrid/ContactGridManager.java b/java/com/android/incallui/contactgrid/ContactGridManager.java
index 86d7114..e375bd5 100644
--- a/java/com/android/incallui/contactgrid/ContactGridManager.java
+++ b/java/com/android/incallui/contactgrid/ContactGridManager.java
@@ -40,6 +40,7 @@
 import com.android.dialer.glidephotomanager.PhotoInfo;
 import com.android.dialer.lettertile.LetterTileDrawable;
 import com.android.dialer.util.DrawableConverter;
+import com.android.dialer.widget.BidiTextView;
 import com.android.incallui.incall.protocol.ContactPhotoType;
 import com.android.incallui.incall.protocol.PrimaryCallState;
 import com.android.incallui.incall.protocol.PrimaryInfo;
@@ -77,7 +78,7 @@
   private final TextView forwardedNumberView;
   private final ImageView spamIconImageView;
   private final ViewAnimator bottomTextSwitcher;
-  private final TextView bottomTextView;
+  private final BidiTextView bottomTextView;
   private final Chronometer bottomTimerView;
   private final Space topRowSpace;
   private int avatarSize;
diff --git a/java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_bottom_row.xml b/java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_bottom_row.xml
index ce355ea..73db413 100644
--- a/java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_bottom_row.xml
+++ b/java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_bottom_row.xml
@@ -53,7 +53,7 @@
       android:layout_height="wrap_content"
       android:layout_marginBottom="2dp"
       android:measureAllChildren="false">
-    <TextView
+    <com.android.dialer.widget.BidiTextView
         android:id="@+id/contactgrid_bottom_text"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"