Simplify & improve DialerBidiFormatter

Bug: 72162627,78464687
Test: DialerBidiFormatterTest + Manual testing
PiperOrigin-RevId: 198950604
Change-Id: Ia3d4d29b7c6a96a7facfeb5c41b17a6e7cabebf2
diff --git a/java/com/android/dialer/i18n/DialerBidiFormatter.java b/java/com/android/dialer/i18n/DialerBidiFormatter.java
index e882e06..440db17 100644
--- a/java/com/android/dialer/i18n/DialerBidiFormatter.java
+++ b/java/com/android/dialer/i18n/DialerBidiFormatter.java
@@ -17,141 +17,75 @@
 package com.android.dialer.i18n;
 
 import android.support.annotation.Nullable;
-import android.support.annotation.VisibleForTesting;
-import android.support.v4.text.BidiFormatter;
+import android.telephony.PhoneNumberUtils;
+import android.text.SpannableStringBuilder;
+import android.text.SpannedString;
 import android.text.TextUtils;
 import android.util.Patterns;
-import com.android.dialer.common.Assert;
-import com.google.auto.value.AutoValue;
-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
- * containing whitespaces.
- *
- * <p>Formatted phone numbers usually contain one or more whitespaces (e.g., "+1 650-253-0000",
- * "(650) 253-0000", etc). {@link BidiFormatter} mistakes such a number for tokens separated by
- * whitespaces. Therefore, these numbers can't be correctly shown in a RTL context (e.g., "+1
- * 650-253-0000" would be shown as "650-253-0000 1+".)
- */
+/** A formatter that applies bidirectional formatting to phone numbers in text. */
 public final class DialerBidiFormatter {
 
+  /** Unicode "Left-To-Right Embedding" (LRE) character. */
+  private static final char LRE = '\u202A';
+
+  /** Unicode "Pop Directional Formatting" (PDF) character. */
+  private static final char PDF = '\u202C';
+
   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.
+   * Divides the given text into segments, applies LTR formatting and adds TTS span to segments that
+   * are phone numbers, then reassembles the text.
    *
-   * <p>A segment of the text is either a substring matching {@link Patterns#PHONE} or one that does
-   * not.
+   * <p>Formatted phone numbers usually contain one or more whitespaces (e.g., "+1 650-253-0000",
+   * "(650) 253-0000", etc). The system mistakes such a number for tokens separated by whitespaces.
+   * Therefore, these numbers can't be correctly shown in a RTL context (e.g., "+1 650-253-0000"
+   * would be shown as "650-253-0000 1+".)
    *
-   * @see BidiFormatter#unicodeWrap(CharSequence)
+   * <p>This method wraps phone numbers with Unicode formatting characters LRE & PDF to ensure phone
+   * numbers are always shown as LTR strings.
+   *
+   * <p>Note that the regex used to find phone numbers ({@link Patterns#PHONE}) will also match any
+   * number. As this method also adds TTS span to segments that match {@link Patterns#PHONE}, extra
+   * actions need to be taken if you don't want a number to be read as a phone number by TalkBack.
    */
-  public static CharSequence unicodeWrap(@Nullable CharSequence text) {
+  public static CharSequence format(@Nullable CharSequence text) {
     if (TextUtils.isEmpty(text)) {
       return text;
     }
 
-    List<CharSequence> segments = segmentText(text);
+    SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder();
 
-    StringBuilder formattedText = new StringBuilder();
-    for (CharSequence segment : segments) {
-      formattedText.append(BidiFormatter.getInstance().unicodeWrap(segment));
-    }
+    // Find the start index and the end index of each segment matching the phone number pattern.
+    Matcher matcher = Patterns.PHONE.matcher(text.toString());
 
-    return formattedText.toString();
-  }
-
-  /**
-   * Segments the given text into a sequence of substrings using the following procedure.
-   *
-   * <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 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());
-    }
-
-    // Segment the text.
     int currIndex = 0;
-    for (Range segmentRange : segmentRanges) {
-      if (currIndex < segmentRange.getStart()) {
-        segments.add(text.subSequence(currIndex, segmentRange.getStart()));
+    while (matcher.find()) {
+      int start = matcher.start();
+      int end = matcher.end();
+
+      // Handle the case where the input text doesn't start with a phone number.
+      if (currIndex < start) {
+        spannableStringBuilder.append(text.subSequence(currIndex, start));
       }
 
-      segments.add(text.subSequence(segmentRange.getStart(), segmentRange.getEnd()));
-      currIndex = segmentRange.getEnd();
+      // For a phone number, wrap it with Unicode characters LRE & PDF so that it will always be
+      // shown as a LTR string.
+      spannableStringBuilder.append(
+          PhoneNumberUtils.createTtsSpannable(
+              TextUtils.concat(
+                  String.valueOf(LRE), text.subSequence(start, end), String.valueOf(PDF))));
+
+      currIndex = end;
     }
+
+    // Handle the case where the input doesn't end with a phone number.
     if (currIndex < text.length()) {
-      segments.add(text.subSequence(currIndex, text.length()));
+      spannableStringBuilder.append(text.subSequence(currIndex, text.length()));
     }
 
-    return segments;
-  }
-
-  /** Represents the start index (inclusive) and the end index (exclusive) of a text segment. */
-  @AutoValue
-  abstract static class Range {
-    static Builder newBuilder() {
-      return new AutoValue_DialerBidiFormatter_Range.Builder();
-    }
-
-    abstract int getStart();
-
-    abstract int getEnd();
-
-    @AutoValue.Builder
-    abstract static class Builder {
-      abstract Builder setStart(int start);
-
-      abstract Builder setEnd(int end);
-
-      abstract Range build();
-    }
+    return new SpannedString(spannableStringBuilder);
   }
 }
diff --git a/java/com/android/dialer/widget/BidiTextView.java b/java/com/android/dialer/widget/BidiTextView.java
index 6cf1aae..5dd15de 100644
--- a/java/com/android/dialer/widget/BidiTextView.java
+++ b/java/com/android/dialer/widget/BidiTextView.java
@@ -35,6 +35,6 @@
 
   @Override
   public void setText(CharSequence text, BufferType type) {
-    super.setText(DialerBidiFormatter.unicodeWrap(text), type);
+    super.setText(DialerBidiFormatter.format(text), type);
   }
 }