Add bidi wrapping support for CharSequences

Code for doing this in the system has already been added in Nougat.
Porting to support library.

Bug: 30476952
Change-Id: Ibbd81bfc15d4754669d47eb4844c8733c46d1bcb
diff --git a/api/current.txt b/api/current.txt
index b2101c7..6b9a183 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -5683,11 +5683,16 @@
     method public static android.support.v4.text.BidiFormatter getInstance(java.util.Locale);
     method public boolean getStereoReset();
     method public boolean isRtl(java.lang.String);
+    method public boolean isRtl(java.lang.CharSequence);
     method public boolean isRtlContext();
     method public java.lang.String unicodeWrap(java.lang.String, android.support.v4.text.TextDirectionHeuristicCompat, boolean);
+    method public java.lang.CharSequence unicodeWrap(java.lang.CharSequence, android.support.v4.text.TextDirectionHeuristicCompat, boolean);
     method public java.lang.String unicodeWrap(java.lang.String, android.support.v4.text.TextDirectionHeuristicCompat);
+    method public java.lang.CharSequence unicodeWrap(java.lang.CharSequence, android.support.v4.text.TextDirectionHeuristicCompat);
     method public java.lang.String unicodeWrap(java.lang.String, boolean);
+    method public java.lang.CharSequence unicodeWrap(java.lang.CharSequence, boolean);
     method public java.lang.String unicodeWrap(java.lang.String);
+    method public java.lang.CharSequence unicodeWrap(java.lang.CharSequence);
   }
 
   public static final class BidiFormatter.Builder {
diff --git a/core-utils/java/android/support/v4/text/BidiFormatter.java b/core-utils/java/android/support/v4/text/BidiFormatter.java
index 0eb86b2..b3b8b1c 100644
--- a/core-utils/java/android/support/v4/text/BidiFormatter.java
+++ b/core-utils/java/android/support/v4/text/BidiFormatter.java
@@ -17,6 +17,7 @@
 package android.support.v4.text;
 
 import android.support.v4.view.ViewCompat;
+import android.text.SpannableStringBuilder;
 
 import java.util.Locale;
 
@@ -280,20 +281,21 @@
 
     /**
      * Returns a Unicode bidi mark matching the context directionality (LRM or RLM) if either the
-     * overall or the exit directionality of a given string is opposite to the context directionality.
-     * Putting this after the string (including its directionality declaration wrapping) prevents it
-     * from "sticking" to other opposite-directionality text or a number appearing after it inline
-     * with only neutral content in between. Otherwise returns the empty string. While the exit
-     * directionality is determined by scanning the end of the string, the overall directionality is
-     * given explicitly by a heuristic to estimate the {@code str}'s directionality.
+     * overall or the exit directionality of a given CharSequence is opposite to the context
+     * directionality. Putting this after the CharSequence (including its directionality
+     * declaration wrapping) prevents it from "sticking" to other opposite-directionality text or a
+     * number appearing after it inline with only neutral content in between. Otherwise returns
+     * the empty string. While the exit directionality is determined by scanning the end of the
+     * CharSequence, the overall directionality is given explicitly by a heuristic to estimate the
+     * {@code str}'s directionality.
      *
-     * @param str String after which the mark may need to appear.
+     * @param str CharSequence after which the mark may need to appear.
      * @param heuristic The text direction heuristic that will be used to estimate the {@code str}'s
      *                  directionality.
      * @return LRM for RTL text in LTR context; RLM for LTR text in RTL context;
-     *     else, the empty string.
+     *     else, the empty .
      */
-    private String markAfter(String str, TextDirectionHeuristicCompat heuristic) {
+    private String markAfter(CharSequence str, TextDirectionHeuristicCompat heuristic) {
         final boolean isRtl = heuristic.isRtl(str, 0, str.length());
         // getExitDir() is called only if needed (short-circuit).
         if (!mIsRtlContext && (isRtl || getExitDir(str) == DIR_RTL)) {
@@ -307,20 +309,21 @@
 
     /**
      * Returns a Unicode bidi mark matching the context directionality (LRM or RLM) if either the
-     * overall or the entry directionality of a given string is opposite to the context
-     * directionality. Putting this before the string (including its directionality declaration
-     * wrapping) prevents it from "sticking" to other opposite-directionality text appearing before
-     * it inline with only neutral content in between. Otherwise returns the empty string. While the
-     * entry directionality is determined by scanning the beginning of the string, the overall
-     * directionality is given explicitly by a heuristic to estimate the {@code str}'s directionality.
+     * overall or the entry directionality of a given CharSequence is opposite to the context
+     * directionality. Putting this before the CharSequence (including its directionality
+     * declaration wrapping) prevents it from "sticking" to other opposite-directionality text
+     * appearing before it inline with only neutral content in between. Otherwise returns the
+     * empty string. While the entry directionality is determined by scanning the beginning of the
+     * CharSequence, the overall directionality is given explicitly by a heuristic to estimate the
+     * {@code str}'s directionality.
      *
-     * @param str String before which the mark may need to appear.
+     * @param str CharSequence before which the mark may need to appear.
      * @param heuristic The text direction heuristic that will be used to estimate the {@code str}'s
      *                  directionality.
      * @return LRM for RTL text in LTR context; RLM for LTR text in RTL context;
      *     else, the empty string.
      */
-    private String markBefore(String str, TextDirectionHeuristicCompat heuristic) {
+    private String markBefore(CharSequence str, TextDirectionHeuristicCompat heuristic) {
         final boolean isRtl = heuristic.isRtl(str, 0, str.length());
         // getEntryDir() is called only if needed (short-circuit).
         if (!mIsRtlContext && (isRtl || getEntryDir(str) == DIR_RTL)) {
@@ -340,6 +343,17 @@
      *          false.
      */
     public boolean isRtl(String str) {
+        return isRtl((CharSequence) str);
+    }
+
+    /**
+     * Operates like {@link #isRtl(String)}, but takes a CharSequence instead of a string.
+     *
+     * @param str CharSequence whose directionality is to be estimated.
+     * @return true if {@code str}'s estimated overall directionality is RTL. Otherwise returns
+     *          false.
+     */
+    public boolean isRtl(CharSequence str) {
         return mDefaultTextDirectionHeuristicCompat.isRtl(str, 0, str.length());
     }
 
@@ -374,8 +388,28 @@
      */
     public String unicodeWrap(String str, TextDirectionHeuristicCompat heuristic, boolean isolate) {
         if (str == null) return null;
+        return unicodeWrap((CharSequence) str, heuristic, isolate).toString();
+    }
+
+    /**
+     * Operates like {@link #unicodeWrap(String,
+     * android.support.v4.text.TextDirectionHeuristicCompat, boolean)}, but takes a CharSequence
+     * instead of a string
+     *
+     * @param str The input CharSequence.
+     * @param heuristic The algorithm to be used to estimate the CharSequence's overall direction.
+     *        See {@link android.support.v4.text.TextDirectionHeuristicsCompat} for pre-defined
+     *        heuristics.
+     * @param isolate Whether to directionally isolate the CharSequence to prevent it from garbling
+     *     the content around it
+     * @return Input CharSequence after applying the above processing. {@code null} if {@code str}
+     *     is {@code null}.
+     */
+    public CharSequence unicodeWrap(CharSequence str, TextDirectionHeuristicCompat heuristic,
+            boolean isolate) {
+        if (str == null) return null;
         final boolean isRtl = heuristic.isRtl(str, 0, str.length());
-        StringBuilder result = new StringBuilder();
+        SpannableStringBuilder result = new SpannableStringBuilder();
         if (getStereoReset() && isolate) {
             result.append(markBefore(str,
                     isRtl ? TextDirectionHeuristicsCompat.RTL : TextDirectionHeuristicsCompat.LTR));
@@ -391,7 +425,7 @@
             result.append(markAfter(str,
                     isRtl ? TextDirectionHeuristicsCompat.RTL : TextDirectionHeuristicsCompat.LTR));
         }
-        return result.toString();
+        return result;
     }
 
     /**
@@ -407,6 +441,21 @@
     }
 
     /**
+     * Operates like {@link #unicodeWrap(CharSequence,
+     * android.support.v4.text.TextDirectionHeuristicCompat, boolean)}, but assumes {@code isolate}
+     * is true.
+     *
+     * @param str The input CharSequence.
+     * @param heuristic The algorithm to be used to estimate the CharSequence's overall direction.
+     *        See {@link android.support.v4.text.TextDirectionHeuristicsCompat} for pre-defined
+     *        heuristics.
+     * @return Input CharSequence after applying the above processing.
+     */
+    public CharSequence unicodeWrap(CharSequence str, TextDirectionHeuristicCompat heuristic) {
+        return unicodeWrap(str, heuristic, true /* isolate */);
+    }
+
+    /**
      * Operates like {@link #unicodeWrap(String, android.support.v4.text.TextDirectionHeuristicCompat, boolean)}, but uses the
      * formatter's default direction estimation algorithm.
      *
@@ -420,6 +469,20 @@
     }
 
     /**
+     * Operates like {@link #unicodeWrap(CharSequence,
+     * android.support.v4.text.TextDirectionHeuristicCompat, boolean)}, but uses the formatter's
+     * default direction estimation algorithm.
+     *
+     * @param str The input CharSequence.
+     * @param isolate Whether to directionally isolate the CharSequence to prevent it from garbling
+     *     the content around it
+     * @return Input CharSequence after applying the above processing.
+     */
+    public CharSequence unicodeWrap(CharSequence str, boolean isolate) {
+        return unicodeWrap(str, mDefaultTextDirectionHeuristicCompat, isolate);
+    }
+
+    /**
      * Operates like {@link #unicodeWrap(String, android.support.v4.text.TextDirectionHeuristicCompat, boolean)}, but uses the
      * formatter's default direction estimation algorithm and assumes {@code isolate} is true.
      *
@@ -431,6 +494,18 @@
     }
 
     /**
+     * Operates like {@link #unicodeWrap(CharSequence,
+     * android.support.v4.text.TextDirectionHeuristicCompat, boolean)}, but uses the formatter's
+     * default direction estimation algorithm and assumes {@code isolate} is true.
+     *
+     * @param str The input CharSequence.
+     * @return Input CharSequence after applying the above processing.
+     */
+    public CharSequence unicodeWrap(CharSequence str) {
+        return unicodeWrap(str, mDefaultTextDirectionHeuristicCompat, true /* isolate */);
+    }
+
+    /**
      * Helper method to return true if the Locale directionality is RTL.
      *
      * @param locale The Locale whose directionality will be checked to be RTL or LTR
@@ -461,7 +536,7 @@
      *
      * @param str the string to check.
      */
-    private static int getExitDir(String str) {
+    private static int getExitDir(CharSequence str) {
         return new DirectionalityEstimator(str, false /* isHtml */).getExitDir();
     }
 
@@ -478,7 +553,7 @@
      *
      * @param str the string to check.
      */
-    private static int getEntryDir(String str) {
+    private static int getEntryDir(CharSequence str) {
         return new DirectionalityEstimator(str, false /* isHtml */).getEntryDir();
     }
 
@@ -516,7 +591,7 @@
         /**
          * The text to be scanned.
          */
-        private final String text;
+        private final CharSequence text;
 
         /**
          * Whether the text to be scanned is to be treated as HTML, i.e. skipping over tags and
@@ -549,7 +624,7 @@
          * @param isHtml Whether the text to be scanned is to be treated as HTML, i.e. skipping over
          *     tags and entities.
          */
-        DirectionalityEstimator(String text, boolean isHtml) {
+        DirectionalityEstimator(CharSequence text, boolean isHtml) {
             this.text = text;
             this.isHtml = isHtml;
             length = text.length();
diff --git a/core-utils/tests/java/android/support/v4/text/BidiFormatterTest.java b/core-utils/tests/java/android/support/v4/text/BidiFormatterTest.java
index 6dc2042..e6e35fc 100644
--- a/core-utils/tests/java/android/support/v4/text/BidiFormatterTest.java
+++ b/core-utils/tests/java/android/support/v4/text/BidiFormatterTest.java
@@ -18,6 +18,9 @@
 
 import android.support.test.runner.AndroidJUnit4;
 import android.test.suitebuilder.annotation.SmallTest;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.style.RelativeSizeSpan;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -39,7 +42,7 @@
             new BidiFormatter.Builder(true /* RTL context */).stereoReset(false).build();
 
     private static final String EN = "abba";
-    private static final String HE = "\u05e0\u05e1";
+    private static final String HE = "\u05E0\u05E1";
 
     private static final String LRM = "\u200E";
     private static final String RLM = "\u200F";
@@ -191,4 +194,49 @@
                 RTL_FMT_EXIT_RESET.unicodeWrap(HE + EN + HE, TextDirectionHeuristicsCompat.LTR,
                         false));
     }
+
+    @Test
+    public void testCharSequenceApis() {
+        final CharSequence CS_HE = new SpannableString(HE);
+        assertEquals(true, BidiFormatter.getInstance(true).isRtl(CS_HE));
+
+        final SpannableString CS_EN_HE = new SpannableString(EN + HE);
+        final Object RELATIVE_SIZE_SPAN = new RelativeSizeSpan(1.2f);
+        CS_EN_HE.setSpan(RELATIVE_SIZE_SPAN, 0, EN.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+
+        Spanned wrapped;
+        Object[] spans;
+
+        wrapped = (Spanned) LTR_FMT.unicodeWrap(CS_EN_HE);
+        assertEquals(EN + HE + LRM, wrapped.toString());
+        spans = wrapped.getSpans(0, wrapped.length(), Object.class);
+        assertEquals(1, spans.length);
+        assertEquals(RELATIVE_SIZE_SPAN, spans[0]);
+        assertEquals(0, wrapped.getSpanStart(RELATIVE_SIZE_SPAN));
+        assertEquals(EN.length(), wrapped.getSpanEnd(RELATIVE_SIZE_SPAN));
+
+        wrapped = (Spanned) LTR_FMT.unicodeWrap(CS_EN_HE, TextDirectionHeuristicsCompat.LTR);
+        assertEquals(EN + HE + LRM, wrapped.toString());
+        spans = wrapped.getSpans(0, wrapped.length(), Object.class);
+        assertEquals(1, spans.length);
+        assertEquals(RELATIVE_SIZE_SPAN, spans[0]);
+        assertEquals(0, wrapped.getSpanStart(RELATIVE_SIZE_SPAN));
+        assertEquals(EN.length(), wrapped.getSpanEnd(RELATIVE_SIZE_SPAN));
+
+        wrapped = (Spanned) LTR_FMT.unicodeWrap(CS_EN_HE, false);
+        assertEquals(EN + HE, wrapped.toString());
+        spans = wrapped.getSpans(0, wrapped.length(), Object.class);
+        assertEquals(1, spans.length);
+        assertEquals(RELATIVE_SIZE_SPAN, spans[0]);
+        assertEquals(0, wrapped.getSpanStart(RELATIVE_SIZE_SPAN));
+        assertEquals(EN.length(), wrapped.getSpanEnd(RELATIVE_SIZE_SPAN));
+
+        wrapped = (Spanned) LTR_FMT.unicodeWrap(CS_EN_HE, TextDirectionHeuristicsCompat.LTR, false);
+        assertEquals(EN + HE, wrapped.toString());
+        spans = wrapped.getSpans(0, wrapped.length(), Object.class);
+        assertEquals(1, spans.length);
+        assertEquals(RELATIVE_SIZE_SPAN, spans[0]);
+        assertEquals(0, wrapped.getSpanStart(RELATIVE_SIZE_SPAN));
+        assertEquals(EN.length(), wrapped.getSpanEnd(RELATIVE_SIZE_SPAN));
+    }
 }