Refactor ellipsis attributes and methods

Mostly changed to avoid repetition of code and remove the assumptions
about ellipsis being one code unit. The code for multi-code unit
ellipsis is not triggered yet, but is done in preparation for
potential future locale-dependent cases.

Test: bit CtsTextTestCases:*
Test: bit CtsWidgetTestCases:android.widget.cts.TextViewTest
Test: bit CtsWidgetTestCases:android.widget.cts.EditTextTest
Test: bit CtsWidgetTestCases:android.widget.cts.CheckedTextViewTest
Test: bit CtsWidgetTestCases:android.widget.cts.AutoCompleteTextViewTest
Test: bit CtsWidgetTestCases:android.widget.cts.MultiAutoCompleteTextViewTest
Test: adb shell am instrument -w -e package android.text com.android.frameworks.coretests/android.support.test.runner.AndroidJUnitRunner

Change-Id: Id1dfdc503f87fabed2447d55ab2107eee0eccd08
diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java
index a233ba1..0f910cc 100644
--- a/core/java/android/text/Layout.java
+++ b/core/java/android/text/Layout.java
@@ -2036,35 +2036,27 @@
         }
     }
 
-    private char getEllipsisChar(TextUtils.TruncateAt method) {
-        return (method == TextUtils.TruncateAt.END_SMALL) ?
-                TextUtils.ELLIPSIS_TWO_DOTS[0] :
-                TextUtils.ELLIPSIS_NORMAL[0];
-    }
-
     private void ellipsize(int start, int end, int line,
                            char[] dest, int destoff, TextUtils.TruncateAt method) {
-        int ellipsisCount = getEllipsisCount(line);
-
+        final int ellipsisCount = getEllipsisCount(line);
         if (ellipsisCount == 0) {
             return;
         }
+        final int ellipsisStart = getEllipsisStart(line);
+        final int lineStart = getLineStart(line);
 
-        int ellipsisStart = getEllipsisStart(line);
-        int linestart = getLineStart(line);
-
-        for (int i = ellipsisStart; i < ellipsisStart + ellipsisCount; i++) {
-            char c;
-
-            if (i == ellipsisStart) {
-                c = getEllipsisChar(method); // ellipsis
+        final String ellipsisString = TextUtils.getEllipsisString(method);
+        final int ellipsisStringLen = ellipsisString.length();
+        for (int i = 0; i < ellipsisCount; i++) {
+            final char c;
+            if (i < ellipsisStringLen && ellipsisCount <= ellipsisStringLen) {
+                c = ellipsisString.charAt(i);
             } else {
-                c = '\uFEFF'; // 0-width space
+                c = TextUtils.ELLIPSIS_FILLER;
             }
 
-            int a = i + linestart;
-
-            if (a >= start && a < end) {
+            final int a = i + ellipsisStart + lineStart;
+            if (start <= a && a < end) {
                 dest[destoff + a - start] = c;
             }
         }
diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java
index a8c6aa6..c5bd29e 100644
--- a/core/java/android/text/StaticLayout.java
+++ b/core/java/android/text/StaticLayout.java
@@ -781,8 +781,8 @@
                     && (ellipsize == TextUtils.TruncateAt.END
                         || (mMaximumVisibleLineCount == 1
                                 && ellipsize != TextUtils.TruncateAt.MARQUEE));
-            if (remainingLineCount > 0 && remainingLineCount < breakCount &&
-                    ellipsisMayBeApplied) {
+            if (0 < remainingLineCount && remainingLineCount < breakCount
+                    && ellipsisMayBeApplied) {
                 // Calculate width and flag.
                 float width = 0;
                 int flag = 0;
@@ -1053,9 +1053,7 @@
             return;
         }
 
-        float ellipsisWidth = paint.measureText(
-                (where == TextUtils.TruncateAt.END_SMALL) ?
-                        TextUtils.ELLIPSIS_TWO_DOTS : TextUtils.ELLIPSIS_NORMAL, 0, 1);
+        float ellipsisWidth = paint.measureText(TextUtils.getEllipsisString(where));
         int ellipsisStart = 0;
         int ellipsisCount = 0;
         int len = lineEnd - lineStart;
diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java
index 440c88e..ba5eaec 100644
--- a/core/java/android/text/TextUtils.java
+++ b/core/java/android/text/TextUtils.java
@@ -77,12 +77,21 @@
 public class TextUtils {
     private static final String TAG = "TextUtils";
 
-    /* package */ static final char[] ELLIPSIS_NORMAL = { '\u2026' }; // this is "..."
-    /** {@hide} */
-    public static final String ELLIPSIS_STRING = new String(ELLIPSIS_NORMAL);
+    // Zero-width character used to fill ellipsized strings when codepoint lenght must be preserved.
+    /* package */ static final char ELLIPSIS_FILLER = '\uFEFF'; // ZERO WIDTH NO-BREAK SPACE
 
-    /* package */ static final char[] ELLIPSIS_TWO_DOTS = { '\u2025' }; // this is ".."
-    private static final String ELLIPSIS_TWO_DOTS_STRING = new String(ELLIPSIS_TWO_DOTS);
+    // TODO: Based on CLDR data, these need to be localized for Dzongkha (dz) and perhaps
+    // Hong Kong Traditional Chinese (zh-Hant-HK), but that may need to depend on the actual word
+    // being ellipsized and not the locale.
+    private static final String ELLIPSIS_NORMAL = "\u2026"; // HORIZONTAL ELLIPSIS (…)
+    private static final String ELLIPSIS_TWO_DOTS = "\u2025"; // TWO DOT LEADER (‥)
+
+    /** {@hide} */
+    @NonNull
+    public static String getEllipsisString(@NonNull TextUtils.TruncateAt method) {
+        return (method == TextUtils.TruncateAt.END_SMALL) ? ELLIPSIS_TWO_DOTS : ELLIPSIS_NORMAL;
+    }
+
 
     private TextUtils() { /* cannot be instantiated */ }
 
@@ -1190,10 +1199,10 @@
                                          TextPaint paint,
                                          float avail, TruncateAt where,
                                          boolean preserveLength,
-                                         EllipsizeCallback callback) {
+                                         @Nullable EllipsizeCallback callback) {
         return ellipsize(text, paint, avail, where, preserveLength, callback,
                 TextDirectionHeuristics.FIRSTSTRONG_LTR,
-                (where == TruncateAt.END_SMALL) ? ELLIPSIS_TWO_DOTS_STRING : ELLIPSIS_STRING);
+                getEllipsisString(where));
     }
 
     /**
@@ -1213,7 +1222,7 @@
             TextPaint paint,
             float avail, TruncateAt where,
             boolean preserveLength,
-            EllipsizeCallback callback,
+            @Nullable EllipsizeCallback callback,
             TextDirectionHeuristic textDir, String ellipsis) {
 
         int len = text.length();
@@ -1256,13 +1265,15 @@
             char[] buf = mt.mChars;
             Spanned sp = text instanceof Spanned ? (Spanned) text : null;
 
-            int remaining = len - (right - left);
+            final int removed = right - left;
+            final int remaining = len - removed;
             if (preserveLength) {
-                if (remaining > 0) { // else eliminate the ellipsis too
-                    buf[left++] = ellipsis.charAt(0);
-                }
+                if (remaining > 0 && removed >= ellipsis.length()) {
+                    ellipsis.getChars(0, ellipsis.length(), buf, left);
+                    left += ellipsis.length();
+                } // else skip the ellipsis
                 for (int i = left; i < right; i++) {
-                    buf[i] = ZWNBS_CHAR;
+                    buf[i] = ELLIPSIS_FILLER;
                 }
                 String s = new String(buf, 0, len);
                 if (sp == null) {
@@ -1362,7 +1373,7 @@
             final int remainingElements = totalLen - i - 1;
             if (remainingElements > 0) {
                 CharSequence morePiece = (res == null) ?
-                        ELLIPSIS_STRING :
+                        ELLIPSIS_NORMAL :
                         res.getQuantityString(moreId, remainingElements, remainingElements);
                 morePiece = bidiFormatter.unicodeWrap(morePiece);
                 output.append(morePiece);
@@ -2029,6 +2040,4 @@
     private static char[] sTemp = null;
 
     private static String[] EMPTY_STRING_ARRAY = new String[]{};
-
-    private static final char ZWNBS_CHAR = '\uFEFF';
 }
diff --git a/core/java/com/android/internal/app/LocaleHelper.java b/core/java/com/android/internal/app/LocaleHelper.java
index d26be91..386aa84 100644
--- a/core/java/com/android/internal/app/LocaleHelper.java
+++ b/core/java/com/android/internal/app/LocaleHelper.java
@@ -181,7 +181,7 @@
             // Hong Kong Traditional Chinese (zh_Hant_HK) and Dzongkha (dz). But that has two
             // problems: it's expensive to extract it, and in case the output string becomes
             // automatically ellipsized, it can result in weird output.
-            localeNames[maxLocales] = TextUtils.ELLIPSIS_STRING;
+            localeNames[maxLocales] = TextUtils.getEllipsisString(TextUtils.TruncateAt.END);
         }
 
         ListFormatter lfn = ListFormatter.getInstance(dispLocale);
diff --git a/core/tests/coretests/src/android/text/TextUtilsTest.java b/core/tests/coretests/src/android/text/TextUtilsTest.java
index 472b3e2..4e155bd 100644
--- a/core/tests/coretests/src/android/text/TextUtilsTest.java
+++ b/core/tests/coretests/src/android/text/TextUtilsTest.java
@@ -341,6 +341,66 @@
     }
 
     @Test
+    public void testEllipsize_multiCodepoint() {
+        final TextPaint paint = new TextPaint();
+        final float wordWidth = paint.measureText("MMMM");
+
+        // Establish the ground rules first, for single-codepoint cases.
+        final String ellipsis = "."; // one full stop character
+        assertEquals(
+                "MM.\uFEFF",
+                TextUtils.ellipsize("MMMM", paint, 0.7f * wordWidth,
+                        TextUtils.TruncateAt.END, true /* preserve length */,
+                        null /* no callback */, TextDirectionHeuristics.LTR,
+                        ellipsis));
+        assertEquals(
+                "MM.",
+                TextUtils.ellipsize("MMMM", paint, 0.7f * wordWidth,
+                        TextUtils.TruncateAt.END, false /* preserve length */,
+                        null /* no callback */, TextDirectionHeuristics.LTR,
+                        ellipsis));
+        assertEquals(
+                "M.",
+                TextUtils.ellipsize("MM", paint, 0.45f * wordWidth,
+                        TextUtils.TruncateAt.END, true /* preserve length */,
+                        null /* no callback */, TextDirectionHeuristics.LTR,
+                        ellipsis));
+        assertEquals(
+                "M.",
+                TextUtils.ellipsize("MM", paint, 0.45f * wordWidth,
+                        TextUtils.TruncateAt.END, false /* preserve length */,
+                        null /* no callback */, TextDirectionHeuristics.LTR,
+                        ellipsis));
+
+        // Now check the differences for multi-codepoint ellipsis.
+        final String longEllipsis = ".."; // two full stop characters
+        assertEquals(
+                "MM..",
+                TextUtils.ellipsize("MMMM", paint, 0.7f * wordWidth,
+                        TextUtils.TruncateAt.END, true /* preserve length */,
+                        null /* no callback */, TextDirectionHeuristics.LTR,
+                        longEllipsis));
+        assertEquals(
+                "MM..",
+                TextUtils.ellipsize("MMMM", paint, 0.7f * wordWidth,
+                        TextUtils.TruncateAt.END, false /* preserve length */,
+                        null /* no callback */, TextDirectionHeuristics.LTR,
+                        longEllipsis));
+        assertEquals(
+                "M\uFEFF",
+                TextUtils.ellipsize("MM", paint, 0.45f * wordWidth,
+                        TextUtils.TruncateAt.END, true /* preserve length */,
+                        null /* no callback */, TextDirectionHeuristics.LTR,
+                        longEllipsis));
+        assertEquals(
+                "M..",
+                TextUtils.ellipsize("MM", paint, 0.45f * wordWidth,
+                        TextUtils.TruncateAt.END, false /* preserve length */,
+                        null /* no callback */, TextDirectionHeuristics.LTR,
+                        longEllipsis));
+    }
+
+    @Test
     public void testDelimitedStringContains() {
         assertFalse(TextUtils.delimitedStringContains("", ',', null));
         assertFalse(TextUtils.delimitedStringContains(null, ',', ""));