Correctly display phone numbers containing whitespaces in RTL context.

Bug: 74421656
Test: DialerBidiFormatterTest
PiperOrigin-RevId: 190154072
Change-Id: Ic7cb3be702dd28b07b6e5e1e6d89f75f0bb12655
diff --git a/java/com/android/contacts/common/list/ContactTileView.java b/java/com/android/contacts/common/list/ContactTileView.java
index cfd52f3..072e07d 100644
--- a/java/com/android/contacts/common/list/ContactTileView.java
+++ b/java/com/android/contacts/common/list/ContactTileView.java
@@ -22,7 +22,6 @@
 import android.view.View;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
-import android.widget.TextView;
 import com.android.contacts.common.MoreContactUtils;
 import com.android.contacts.common.R;
 import com.android.dialer.callintent.CallInitiationType;
@@ -30,6 +29,7 @@
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.contactphoto.ContactPhotoManager;
 import com.android.dialer.contactphoto.ContactPhotoManager.DefaultImageRequest;
+import com.android.dialer.widget.BidiTextView;
 
 /** A ContactTile displays a contact's picture and name */
 public abstract class ContactTileView extends FrameLayout {
@@ -38,7 +38,7 @@
   protected Listener mListener;
   private Uri mLookupUri;
   private ImageView mPhoto;
-  private TextView mName;
+  private BidiTextView mName;
   private ContactPhotoManager mPhotoManager = null;
 
   public ContactTileView(Context context, AttributeSet attrs) {
@@ -48,7 +48,7 @@
   @Override
   protected void onFinishInflate() {
     super.onFinishInflate();
-    mName = (TextView) findViewById(R.id.contact_tile_name);
+    mName = (BidiTextView) findViewById(R.id.contact_tile_name);
     mPhoto = (ImageView) findViewById(R.id.contact_tile_image);
 
     OnClickListener listener = createClickListener();
diff --git a/java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java b/java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java
index 8b7a92b..71cbc8c 100644
--- a/java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java
+++ b/java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java
@@ -21,11 +21,12 @@
 import android.widget.TextView;
 import com.android.dialer.app.R;
 import com.android.dialer.calllogutils.CallTypeIconsView;
+import com.android.dialer.widget.BidiTextView;
 
 /** Encapsulates the views that are used to display the details of a phone call in the call log. */
 public final class PhoneCallDetailsViews {
 
-  public final TextView nameView;
+  public final BidiTextView nameView;
   public final View callTypeView;
   public final CallTypeIconsView callTypeIcons;
   public final TextView callLocationAndDate;
@@ -36,7 +37,7 @@
   public final TextView callAccountLabel;
 
   private PhoneCallDetailsViews(
-      TextView nameView,
+      BidiTextView nameView,
       View callTypeView,
       CallTypeIconsView callTypeIcons,
       TextView callLocationAndDate,
@@ -65,7 +66,7 @@
    */
   public static PhoneCallDetailsViews fromView(View view) {
     return new PhoneCallDetailsViews(
-        (TextView) view.findViewById(R.id.name),
+        (BidiTextView) view.findViewById(R.id.name),
         view.findViewById(R.id.call_type),
         (CallTypeIconsView) view.findViewById(R.id.call_type_icons),
         (TextView) view.findViewById(R.id.call_location_and_date),
@@ -78,7 +79,7 @@
 
   public static PhoneCallDetailsViews createForTest(Context context) {
     return new PhoneCallDetailsViews(
-        new TextView(context),
+        new BidiTextView(context),
         new View(context),
         new CallTypeIconsView(context),
         new TextView(context),
diff --git a/java/com/android/dialer/app/list/PhoneFavoriteSquareTileView.java b/java/com/android/dialer/app/list/PhoneFavoriteSquareTileView.java
index 330a361..6096ca8 100644
--- a/java/com/android/dialer/app/list/PhoneFavoriteSquareTileView.java
+++ b/java/com/android/dialer/app/list/PhoneFavoriteSquareTileView.java
@@ -28,12 +28,11 @@
 import com.android.dialer.compat.CompatUtils;
 import com.android.dialer.logging.InteractionEvent;
 import com.android.dialer.logging.Logger;
+import com.android.dialer.widget.BidiTextView;
 
 /** Displays the contact's picture overlaid with their name and number type in a tile. */
 public class PhoneFavoriteSquareTileView extends PhoneFavoriteTileView {
 
-  private static final String TAG = PhoneFavoriteSquareTileView.class.getSimpleName();
-
   private final float heightToWidthRatio;
 
   private ImageButton secondaryButton;
@@ -50,11 +49,12 @@
   @Override
   protected void onFinishInflate() {
     super.onFinishInflate();
-    final TextView nameView = (TextView) findViewById(R.id.contact_tile_name);
+    BidiTextView nameView = findViewById(R.id.contact_tile_name);
     nameView.setElegantTextHeight(false);
-    final TextView phoneTypeView = (TextView) findViewById(R.id.contact_tile_phone_type);
+
+    TextView phoneTypeView = findViewById(R.id.contact_tile_phone_type);
     phoneTypeView.setElegantTextHeight(false);
-    secondaryButton = (ImageButton) findViewById(R.id.contact_tile_secondary_button);
+    secondaryButton = findViewById(R.id.contact_tile_secondary_button);
   }
 
   @Override
diff --git a/java/com/android/dialer/app/res/layout/call_log_list_item.xml b/java/com/android/dialer/app/res/layout/call_log_list_item.xml
index acaa820..d111110 100644
--- a/java/com/android/dialer/app/res/layout/call_log_list_item.xml
+++ b/java/com/android/dialer/app/res/layout/call_log_list_item.xml
@@ -93,7 +93,7 @@
           android:gravity="center_vertical"
           android:layout_marginStart="@dimen/call_log_list_item_info_margin_start">
 
-          <TextView
+          <com.android.dialer.widget.BidiTextView
             android:id="@+id/name"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
diff --git a/java/com/android/dialer/app/res/layout/phone_favorite_tile_view.xml b/java/com/android/dialer/app/res/layout/phone_favorite_tile_view.xml
index d2712e9..3aeba98 100644
--- a/java/com/android/dialer/app/res/layout/phone_favorite_tile_view.xml
+++ b/java/com/android/dialer/app/res/layout/phone_favorite_tile_view.xml
@@ -63,7 +63,7 @@
           android:layout_height="wrap_content"
           android:gravity="center_vertical"
           android:orientation="horizontal">
-        <TextView
+        <com.android.dialer.widget.BidiTextView
             android:id="@+id/contact_tile_name"
             android:layout_width="0dp"
             android:layout_height="wrap_content"
diff --git a/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java b/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java
index e4fded1..cb84a28 100644
--- a/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java
+++ b/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java
@@ -40,6 +40,7 @@
 import com.android.dialer.glidephotomanager.GlidePhotoManagerComponent;
 import com.android.dialer.logging.InteractionEvent;
 import com.android.dialer.logging.Logger;
+import com.android.dialer.widget.BidiTextView;
 
 /**
  * ViewHolder for the header in {@link OldCallDetailsActivity} or {@link CallDetailsActivity}.
@@ -51,7 +52,7 @@
 
   private final CallDetailsHeaderListener callDetailsHeaderListener;
   private final ImageView callbackButton;
-  private final TextView nameView;
+  private final BidiTextView nameView;
   private final TextView numberView;
   private final TextView networkView;
   private final QuickContactBadge contactPhoto;
diff --git a/java/com/android/dialer/calldetails/res/layout/contact_container.xml b/java/com/android/dialer/calldetails/res/layout/contact_container.xml
index 5f531ab..9506183 100644
--- a/java/com/android/dialer/calldetails/res/layout/contact_container.xml
+++ b/java/com/android/dialer/calldetails/res/layout/contact_container.xml
@@ -44,7 +44,7 @@
       android:minHeight="@dimen/call_details_contact_photo_size"
       android:orientation="vertical">
 
-    <TextView
+    <com.android.dialer.widget.BidiTextView
         android:id="@+id/contact_name"
         style="@style/PrimaryText"
         android:layout_width="wrap_content"
diff --git a/java/com/android/dialer/contactsfragment/ContactViewHolder.java b/java/com/android/dialer/contactsfragment/ContactViewHolder.java
index 2730c0d..e188332 100644
--- a/java/com/android/dialer/contactsfragment/ContactViewHolder.java
+++ b/java/com/android/dialer/contactsfragment/ContactViewHolder.java
@@ -28,12 +28,13 @@
 import com.android.dialer.contactsfragment.ContactsFragment.OnContactSelectedListener;
 import com.android.dialer.logging.InteractionEvent;
 import com.android.dialer.logging.Logger;
+import com.android.dialer.widget.BidiTextView;
 
 /** View holder for a contact. */
 final class ContactViewHolder extends RecyclerView.ViewHolder implements OnClickListener {
 
   private final TextView header;
-  private final TextView name;
+  private final BidiTextView name;
   private final QuickContactBadge photo;
   private final Context context;
   private final OnContactSelectedListener onContactSelectedListener;
diff --git a/java/com/android/dialer/contactsfragment/res/layout/contact_row.xml b/java/com/android/dialer/contactsfragment/res/layout/contact_row.xml
index 9e829fe..b65a8c8 100644
--- a/java/com/android/dialer/contactsfragment/res/layout/contact_row.xml
+++ b/java/com/android/dialer/contactsfragment/res/layout/contact_row.xml
@@ -41,7 +41,7 @@
         android:layout_height="@dimen/photo_size"
         android:clickable="false"/>
 
-    <TextView
+    <com.android.dialer.widget.BidiTextView
         android:id="@+id/contact_name"
         android:layout_width="wrap_content"
         android:layout_height="match_parent"
diff --git a/java/com/android/dialer/i18n/DialerBidiFormatter.java b/java/com/android/dialer/i18n/DialerBidiFormatter.java
new file mode 100644
index 0000000..4ebaa66
--- /dev/null
+++ b/java/com/android/dialer/i18n/DialerBidiFormatter.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2018 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.dialer.i18n;
+
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.text.BidiFormatter;
+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;
+
+/**
+ * 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+".)
+ */
+public final class DialerBidiFormatter {
+
+  private DialerBidiFormatter() {}
+
+  /**
+   * Divides the given text into segments, applies {@link BidiFormatter#unicodeWrap(CharSequence)}
+   * to each segment, and then reassembles the text.
+   *
+   * <p>A segment of the text is either a substring matching {@link Patterns#PHONE} or one that does
+   * not.
+   *
+   * @see BidiFormatter#unicodeWrap(CharSequence)
+   */
+  public static CharSequence unicodeWrap(@Nullable CharSequence text) {
+    if (TextUtils.isEmpty(text)) {
+      return text;
+    }
+
+    List<CharSequence> segments = segmentText(text);
+
+    StringBuilder formattedText = new StringBuilder();
+    for (CharSequence segment : segments) {
+      formattedText.append(BidiFormatter.getInstance().unicodeWrap(segment));
+    }
+
+    return formattedText.toString();
+  }
+
+  /**
+   * Segments the given text using {@link Patterns#PHONE}.
+   *
+   * <p>For example, "Mobile, +1 650-253-0000, 20 seconds" will be segmented into {"Mobile, ", "+1
+   * 650-253-0000", ", 20 seconds"}.
+   */
+  @VisibleForTesting
+  static List<CharSequence> segmentText(CharSequence text) {
+    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());
+    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()));
+      }
+
+      segments.add(text.subSequence(segmentRange.getStart(), segmentRange.getEnd()));
+      currIndex = segmentRange.getEnd();
+    }
+    if (currIndex < text.length()) {
+      segments.add(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();
+    }
+  }
+}
diff --git a/java/com/android/dialer/searchfragment/common/res/layout/search_contact_row.xml b/java/com/android/dialer/searchfragment/common/res/layout/search_contact_row.xml
index 9be7fa0..a0d9dd2 100644
--- a/java/com/android/dialer/searchfragment/common/res/layout/search_contact_row.xml
+++ b/java/com/android/dialer/searchfragment/common/res/layout/search_contact_row.xml
@@ -39,7 +39,7 @@
       android:layout_centerVertical="true"
       android:layout_marginStart="8dp">
 
-    <TextView
+    <com.android.dialer.widget.BidiTextView
         android:id="@+id/primary"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
@@ -47,7 +47,7 @@
         android:fontFamily="sans-serif"
         style="@style/PrimaryText"/>
 
-    <TextView
+    <com.android.dialer.widget.BidiTextView
         android:id="@+id/secondary"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
diff --git a/java/com/android/dialer/searchfragment/cp2/SearchContactViewHolder.java b/java/com/android/dialer/searchfragment/cp2/SearchContactViewHolder.java
index e36df4b..9d18e07 100644
--- a/java/com/android/dialer/searchfragment/cp2/SearchContactViewHolder.java
+++ b/java/com/android/dialer/searchfragment/cp2/SearchContactViewHolder.java
@@ -30,7 +30,6 @@
 import android.view.View.OnClickListener;
 import android.widget.ImageView;
 import android.widget.QuickContactBadge;
-import android.widget.TextView;
 import com.android.dialer.common.Assert;
 import com.android.dialer.contactphoto.ContactPhotoManager;
 import com.android.dialer.dialercontact.DialerContact;
@@ -44,6 +43,7 @@
 import com.android.dialer.searchfragment.common.R;
 import com.android.dialer.searchfragment.common.RowClickListener;
 import com.android.dialer.searchfragment.common.SearchCursor;
+import com.android.dialer.widget.BidiTextView;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
@@ -67,8 +67,8 @@
 
   private final RowClickListener listener;
   private final QuickContactBadge photo;
-  private final TextView nameOrNumberView;
-  private final TextView numberView;
+  private final BidiTextView nameOrNumberView;
+  private final BidiTextView numberView;
   private final ImageView callToActionView;
   private final Context context;
 
diff --git a/java/com/android/dialer/widget/BidiTextView.java b/java/com/android/dialer/widget/BidiTextView.java
new file mode 100644
index 0000000..6cf1aae
--- /dev/null
+++ b/java/com/android/dialer/widget/BidiTextView.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2018 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.dialer.widget;
+
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.util.AttributeSet;
+import android.widget.TextView;
+import com.android.dialer.i18n.DialerBidiFormatter;
+
+/** A {@link TextView} that applies bidirectional formatting to its text. */
+public final class BidiTextView extends TextView {
+
+  public BidiTextView(Context context) {
+    super(context);
+  }
+
+  public BidiTextView(Context context, @Nullable AttributeSet attrs) {
+    super(context, attrs);
+  }
+
+  @Override
+  public void setText(CharSequence text, BufferType type) {
+    super.setText(DialerBidiFormatter.unicodeWrap(text), type);
+  }
+}