1 /*
2  * Copyright (C) 2006 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.text;
18 
19 import static java.lang.annotation.RetentionPolicy.SOURCE;
20 
21 import android.annotation.FloatRange;
22 import android.annotation.IntDef;
23 import android.annotation.IntRange;
24 import android.annotation.NonNull;
25 import android.annotation.Nullable;
26 import android.annotation.PluralsRes;
27 import android.compat.annotation.UnsupportedAppUsage;
28 import android.content.Context;
29 import android.content.res.Resources;
30 import android.icu.lang.UCharacter;
31 import android.icu.text.CaseMap;
32 import android.icu.text.Edits;
33 import android.icu.util.ULocale;
34 import android.os.Parcel;
35 import android.os.Parcelable;
36 import android.sysprop.DisplayProperties;
37 import android.text.style.AbsoluteSizeSpan;
38 import android.text.style.AccessibilityClickableSpan;
39 import android.text.style.AccessibilityReplacementSpan;
40 import android.text.style.AccessibilityURLSpan;
41 import android.text.style.AlignmentSpan;
42 import android.text.style.BackgroundColorSpan;
43 import android.text.style.BulletSpan;
44 import android.text.style.CharacterStyle;
45 import android.text.style.EasyEditSpan;
46 import android.text.style.ForegroundColorSpan;
47 import android.text.style.LeadingMarginSpan;
48 import android.text.style.LineBackgroundSpan;
49 import android.text.style.LineHeightSpan;
50 import android.text.style.LocaleSpan;
51 import android.text.style.ParagraphStyle;
52 import android.text.style.QuoteSpan;
53 import android.text.style.RelativeSizeSpan;
54 import android.text.style.ReplacementSpan;
55 import android.text.style.ScaleXSpan;
56 import android.text.style.SpellCheckSpan;
57 import android.text.style.StrikethroughSpan;
58 import android.text.style.StyleSpan;
59 import android.text.style.SubscriptSpan;
60 import android.text.style.SuggestionRangeSpan;
61 import android.text.style.SuggestionSpan;
62 import android.text.style.SuperscriptSpan;
63 import android.text.style.TextAppearanceSpan;
64 import android.text.style.TtsSpan;
65 import android.text.style.TypefaceSpan;
66 import android.text.style.URLSpan;
67 import android.text.style.UnderlineSpan;
68 import android.text.style.UpdateAppearance;
69 import android.util.Log;
70 import android.util.Printer;
71 import android.view.View;
72 
73 import com.android.internal.util.ArrayUtils;
74 import com.android.internal.util.Preconditions;
75 
76 import java.lang.annotation.Retention;
77 import java.lang.reflect.Array;
78 import java.util.BitSet;
79 import java.util.Iterator;
80 import java.util.List;
81 import java.util.Locale;
82 import java.util.regex.Pattern;
83 
84 public class TextUtils {
85     private static final String TAG = "TextUtils";
86 
87     // Zero-width character used to fill ellipsized strings when codepoint length must be preserved.
88     /* package */ static final char ELLIPSIS_FILLER = '\uFEFF'; // ZERO WIDTH NO-BREAK SPACE
89 
90     // TODO: Based on CLDR data, these need to be localized for Dzongkha (dz) and perhaps
91     // Hong Kong Traditional Chinese (zh-Hant-HK), but that may need to depend on the actual word
92     // being ellipsized and not the locale.
93     private static final String ELLIPSIS_NORMAL = "\u2026"; // HORIZONTAL ELLIPSIS (…)
94     private static final String ELLIPSIS_TWO_DOTS = "\u2025"; // TWO DOT LEADER (‥)
95 
96     /** @hide */
97     public static final int LINE_FEED_CODE_POINT = 10;
98 
99     private static final int NBSP_CODE_POINT = 160;
100 
101     /**
102      * Flags for {@link #makeSafeForPresentation(String, int, float, int)}
103      *
104      * @hide
105      */
106     @Retention(SOURCE)
107     @IntDef(flag = true, prefix = "CLEAN_STRING_FLAG_",
108             value = {SAFE_STRING_FLAG_TRIM, SAFE_STRING_FLAG_SINGLE_LINE,
109                     SAFE_STRING_FLAG_FIRST_LINE})
110     public @interface SafeStringFlags {}
111 
112     /**
113      * Remove {@link Character#isWhitespace(int) whitespace} and non-breaking spaces from the edges
114      * of the label.
115      *
116      * @see #makeSafeForPresentation(String, int, float, int)
117      */
118     public static final int SAFE_STRING_FLAG_TRIM = 0x1;
119 
120     /**
121      * Force entire string into single line of text (no newlines). Cannot be set at the same time as
122      * {@link #SAFE_STRING_FLAG_FIRST_LINE}.
123      *
124      * @see #makeSafeForPresentation(String, int, float, int)
125      */
126     public static final int SAFE_STRING_FLAG_SINGLE_LINE = 0x2;
127 
128     /**
129      * Return only first line of text (truncate at first newline). Cannot be set at the same time as
130      * {@link #SAFE_STRING_FLAG_SINGLE_LINE}.
131      *
132      * @see #makeSafeForPresentation(String, int, float, int)
133      */
134     public static final int SAFE_STRING_FLAG_FIRST_LINE = 0x4;
135 
136     /** {@hide} */
137     @NonNull
getEllipsisString(@onNull TextUtils.TruncateAt method)138     public static String getEllipsisString(@NonNull TextUtils.TruncateAt method) {
139         return (method == TextUtils.TruncateAt.END_SMALL) ? ELLIPSIS_TWO_DOTS : ELLIPSIS_NORMAL;
140     }
141 
142 
TextUtils()143     private TextUtils() { /* cannot be instantiated */ }
144 
getChars(CharSequence s, int start, int end, char[] dest, int destoff)145     public static void getChars(CharSequence s, int start, int end,
146                                 char[] dest, int destoff) {
147         Class<? extends CharSequence> c = s.getClass();
148 
149         if (c == String.class)
150             ((String) s).getChars(start, end, dest, destoff);
151         else if (c == StringBuffer.class)
152             ((StringBuffer) s).getChars(start, end, dest, destoff);
153         else if (c == StringBuilder.class)
154             ((StringBuilder) s).getChars(start, end, dest, destoff);
155         else if (s instanceof GetChars)
156             ((GetChars) s).getChars(start, end, dest, destoff);
157         else {
158             for (int i = start; i < end; i++)
159                 dest[destoff++] = s.charAt(i);
160         }
161     }
162 
indexOf(CharSequence s, char ch)163     public static int indexOf(CharSequence s, char ch) {
164         return indexOf(s, ch, 0);
165     }
166 
indexOf(CharSequence s, char ch, int start)167     public static int indexOf(CharSequence s, char ch, int start) {
168         Class<? extends CharSequence> c = s.getClass();
169 
170         if (c == String.class)
171             return ((String) s).indexOf(ch, start);
172 
173         return indexOf(s, ch, start, s.length());
174     }
175 
indexOf(CharSequence s, char ch, int start, int end)176     public static int indexOf(CharSequence s, char ch, int start, int end) {
177         Class<? extends CharSequence> c = s.getClass();
178 
179         if (s instanceof GetChars || c == StringBuffer.class ||
180             c == StringBuilder.class || c == String.class) {
181             final int INDEX_INCREMENT = 500;
182             char[] temp = obtain(INDEX_INCREMENT);
183 
184             while (start < end) {
185                 int segend = start + INDEX_INCREMENT;
186                 if (segend > end)
187                     segend = end;
188 
189                 getChars(s, start, segend, temp, 0);
190 
191                 int count = segend - start;
192                 for (int i = 0; i < count; i++) {
193                     if (temp[i] == ch) {
194                         recycle(temp);
195                         return i + start;
196                     }
197                 }
198 
199                 start = segend;
200             }
201 
202             recycle(temp);
203             return -1;
204         }
205 
206         for (int i = start; i < end; i++)
207             if (s.charAt(i) == ch)
208                 return i;
209 
210         return -1;
211     }
212 
lastIndexOf(CharSequence s, char ch)213     public static int lastIndexOf(CharSequence s, char ch) {
214         return lastIndexOf(s, ch, s.length() - 1);
215     }
216 
lastIndexOf(CharSequence s, char ch, int last)217     public static int lastIndexOf(CharSequence s, char ch, int last) {
218         Class<? extends CharSequence> c = s.getClass();
219 
220         if (c == String.class)
221             return ((String) s).lastIndexOf(ch, last);
222 
223         return lastIndexOf(s, ch, 0, last);
224     }
225 
lastIndexOf(CharSequence s, char ch, int start, int last)226     public static int lastIndexOf(CharSequence s, char ch,
227                                   int start, int last) {
228         if (last < 0)
229             return -1;
230         if (last >= s.length())
231             last = s.length() - 1;
232 
233         int end = last + 1;
234 
235         Class<? extends CharSequence> c = s.getClass();
236 
237         if (s instanceof GetChars || c == StringBuffer.class ||
238             c == StringBuilder.class || c == String.class) {
239             final int INDEX_INCREMENT = 500;
240             char[] temp = obtain(INDEX_INCREMENT);
241 
242             while (start < end) {
243                 int segstart = end - INDEX_INCREMENT;
244                 if (segstart < start)
245                     segstart = start;
246 
247                 getChars(s, segstart, end, temp, 0);
248 
249                 int count = end - segstart;
250                 for (int i = count - 1; i >= 0; i--) {
251                     if (temp[i] == ch) {
252                         recycle(temp);
253                         return i + segstart;
254                     }
255                 }
256 
257                 end = segstart;
258             }
259 
260             recycle(temp);
261             return -1;
262         }
263 
264         for (int i = end - 1; i >= start; i--)
265             if (s.charAt(i) == ch)
266                 return i;
267 
268         return -1;
269     }
270 
indexOf(CharSequence s, CharSequence needle)271     public static int indexOf(CharSequence s, CharSequence needle) {
272         return indexOf(s, needle, 0, s.length());
273     }
274 
indexOf(CharSequence s, CharSequence needle, int start)275     public static int indexOf(CharSequence s, CharSequence needle, int start) {
276         return indexOf(s, needle, start, s.length());
277     }
278 
indexOf(CharSequence s, CharSequence needle, int start, int end)279     public static int indexOf(CharSequence s, CharSequence needle,
280                               int start, int end) {
281         int nlen = needle.length();
282         if (nlen == 0)
283             return start;
284 
285         char c = needle.charAt(0);
286 
287         for (;;) {
288             start = indexOf(s, c, start);
289             if (start > end - nlen) {
290                 break;
291             }
292 
293             if (start < 0) {
294                 return -1;
295             }
296 
297             if (regionMatches(s, start, needle, 0, nlen)) {
298                 return start;
299             }
300 
301             start++;
302         }
303         return -1;
304     }
305 
regionMatches(CharSequence one, int toffset, CharSequence two, int ooffset, int len)306     public static boolean regionMatches(CharSequence one, int toffset,
307                                         CharSequence two, int ooffset,
308                                         int len) {
309         int tempLen = 2 * len;
310         if (tempLen < len) {
311             // Integer overflow; len is unreasonably large
312             throw new IndexOutOfBoundsException();
313         }
314         char[] temp = obtain(tempLen);
315 
316         getChars(one, toffset, toffset + len, temp, 0);
317         getChars(two, ooffset, ooffset + len, temp, len);
318 
319         boolean match = true;
320         for (int i = 0; i < len; i++) {
321             if (temp[i] != temp[i + len]) {
322                 match = false;
323                 break;
324             }
325         }
326 
327         recycle(temp);
328         return match;
329     }
330 
331     /**
332      * Create a new String object containing the given range of characters
333      * from the source string.  This is different than simply calling
334      * {@link CharSequence#subSequence(int, int) CharSequence.subSequence}
335      * in that it does not preserve any style runs in the source sequence,
336      * allowing a more efficient implementation.
337      */
substring(CharSequence source, int start, int end)338     public static String substring(CharSequence source, int start, int end) {
339         if (source instanceof String)
340             return ((String) source).substring(start, end);
341         if (source instanceof StringBuilder)
342             return ((StringBuilder) source).substring(start, end);
343         if (source instanceof StringBuffer)
344             return ((StringBuffer) source).substring(start, end);
345 
346         char[] temp = obtain(end - start);
347         getChars(source, start, end, temp, 0);
348         String ret = new String(temp, 0, end - start);
349         recycle(temp);
350 
351         return ret;
352     }
353 
354 
355     /**
356      * Returns the longest prefix of a string for which the UTF-8 encoding fits into the given
357      * number of bytes, with the additional guarantee that the string is not truncated in the middle
358      * of a valid surrogate pair.
359      *
360      * <p>Unpaired surrogates are counted as taking 3 bytes of storage. However, a subsequent
361      * attempt to actually encode a string containing unpaired surrogates is likely to be rejected
362      * by the UTF-8 implementation.
363      *
364      * (copied from google/thirdparty)
365      *
366      * @param str a string
367      * @param maxbytes the maximum number of UTF-8 encoded bytes
368      * @return the beginning of the string, so that it uses at most maxbytes bytes in UTF-8
369      * @throws IndexOutOfBoundsException if maxbytes is negative
370      *
371      * @hide
372      */
truncateStringForUtf8Storage(String str, int maxbytes)373     public static String truncateStringForUtf8Storage(String str, int maxbytes) {
374         if (maxbytes < 0) {
375             throw new IndexOutOfBoundsException();
376         }
377 
378         int bytes = 0;
379         for (int i = 0, len = str.length(); i < len; i++) {
380             char c = str.charAt(i);
381             if (c < 0x80) {
382                 bytes += 1;
383             } else if (c < 0x800) {
384                 bytes += 2;
385             } else if (c < Character.MIN_SURROGATE
386                     || c > Character.MAX_SURROGATE
387                     || str.codePointAt(i) < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
388                 bytes += 3;
389             } else {
390                 bytes += 4;
391                 i += (bytes > maxbytes) ? 0 : 1;
392             }
393             if (bytes > maxbytes) {
394                 return str.substring(0, i);
395             }
396         }
397         return str;
398     }
399 
400 
401     /**
402      * Returns a string containing the tokens joined by delimiters.
403      *
404      * @param delimiter a CharSequence that will be inserted between the tokens. If null, the string
405      *     "null" will be used as the delimiter.
406      * @param tokens an array objects to be joined. Strings will be formed from the objects by
407      *     calling object.toString(). If tokens is null, a NullPointerException will be thrown. If
408      *     tokens is an empty array, an empty string will be returned.
409      */
join(@onNull CharSequence delimiter, @NonNull Object[] tokens)410     public static String join(@NonNull CharSequence delimiter, @NonNull Object[] tokens) {
411         final int length = tokens.length;
412         if (length == 0) {
413             return "";
414         }
415         final StringBuilder sb = new StringBuilder();
416         sb.append(tokens[0]);
417         for (int i = 1; i < length; i++) {
418             sb.append(delimiter);
419             sb.append(tokens[i]);
420         }
421         return sb.toString();
422     }
423 
424     /**
425      * Returns a string containing the tokens joined by delimiters.
426      *
427      * @param delimiter a CharSequence that will be inserted between the tokens. If null, the string
428      *     "null" will be used as the delimiter.
429      * @param tokens an array objects to be joined. Strings will be formed from the objects by
430      *     calling object.toString(). If tokens is null, a NullPointerException will be thrown. If
431      *     tokens is empty, an empty string will be returned.
432      */
join(@onNull CharSequence delimiter, @NonNull Iterable tokens)433     public static String join(@NonNull CharSequence delimiter, @NonNull Iterable tokens) {
434         final Iterator<?> it = tokens.iterator();
435         if (!it.hasNext()) {
436             return "";
437         }
438         final StringBuilder sb = new StringBuilder();
439         sb.append(it.next());
440         while (it.hasNext()) {
441             sb.append(delimiter);
442             sb.append(it.next());
443         }
444         return sb.toString();
445     }
446 
447     /**
448      *
449      * This method yields the same result as {@code text.split(expression, -1)} except that if
450      * {@code text.isEmpty()} then this method returns an empty array whereas
451      * {@code "".split(expression, -1)} would have returned an array with a single {@code ""}.
452      *
453      * The {@code -1} means that trailing empty Strings are not removed from the result; for
454      * example split("a,", ","  ) returns {"a", ""}. Note that whether a leading zero-width match
455      * can result in a leading {@code ""} depends on whether your app
456      * {@link android.content.pm.ApplicationInfo#targetSdkVersion targets an SDK version}
457      * {@code <= 28}; see {@link Pattern#split(CharSequence, int)}.
458      *
459      * @param text the string to split
460      * @param expression the regular expression to match
461      * @return an array of strings. The array will be empty if text is empty
462      *
463      * @throws NullPointerException if expression or text is null
464      */
split(String text, String expression)465     public static String[] split(String text, String expression) {
466         if (text.length() == 0) {
467             return EMPTY_STRING_ARRAY;
468         } else {
469             return text.split(expression, -1);
470         }
471     }
472 
473     /**
474      * Splits a string on a pattern. This method yields the same result as
475      * {@code pattern.split(text, -1)} except that if {@code text.isEmpty()} then this method
476      * returns an empty array whereas {@code pattern.split("", -1)} would have returned an array
477      * with a single {@code ""}.
478      *
479      * The {@code -1} means that trailing empty Strings are not removed from the result;
480      * Note that whether a leading zero-width match can result in a leading {@code ""} depends
481      * on whether your app {@link android.content.pm.ApplicationInfo#targetSdkVersion targets
482      * an SDK version} {@code <= 28}; see {@link Pattern#split(CharSequence, int)}.
483      *
484      * @param text the string to split
485      * @param pattern the regular expression to match
486      * @return an array of strings. The array will be empty if text is empty
487      *
488      * @throws NullPointerException if expression or text is null
489      */
split(String text, Pattern pattern)490     public static String[] split(String text, Pattern pattern) {
491         if (text.length() == 0) {
492             return EMPTY_STRING_ARRAY;
493         } else {
494             return pattern.split(text, -1);
495         }
496     }
497 
498     /**
499      * An interface for splitting strings according to rules that are opaque to the user of this
500      * interface. This also has less overhead than split, which uses regular expressions and
501      * allocates an array to hold the results.
502      *
503      * <p>The most efficient way to use this class is:
504      *
505      * <pre>
506      * // Once
507      * TextUtils.StringSplitter splitter = new TextUtils.SimpleStringSplitter(delimiter);
508      *
509      * // Once per string to split
510      * splitter.setString(string);
511      * for (String s : splitter) {
512      *     ...
513      * }
514      * </pre>
515      */
516     public interface StringSplitter extends Iterable<String> {
setString(String string)517         public void setString(String string);
518     }
519 
520     /**
521      * A simple string splitter.
522      *
523      * <p>If the final character in the string to split is the delimiter then no empty string will
524      * be returned for the empty string after that delimeter. That is, splitting <tt>"a,b,"</tt> on
525      * comma will return <tt>"a", "b"</tt>, not <tt>"a", "b", ""</tt>.
526      */
527     public static class SimpleStringSplitter implements StringSplitter, Iterator<String> {
528         private String mString;
529         private char mDelimiter;
530         private int mPosition;
531         private int mLength;
532 
533         /**
534          * Initializes the splitter. setString may be called later.
535          * @param delimiter the delimeter on which to split
536          */
SimpleStringSplitter(char delimiter)537         public SimpleStringSplitter(char delimiter) {
538             mDelimiter = delimiter;
539         }
540 
541         /**
542          * Sets the string to split
543          * @param string the string to split
544          */
setString(String string)545         public void setString(String string) {
546             mString = string;
547             mPosition = 0;
548             mLength = mString.length();
549         }
550 
iterator()551         public Iterator<String> iterator() {
552             return this;
553         }
554 
hasNext()555         public boolean hasNext() {
556             return mPosition < mLength;
557         }
558 
next()559         public String next() {
560             int end = mString.indexOf(mDelimiter, mPosition);
561             if (end == -1) {
562                 end = mLength;
563             }
564             String nextString = mString.substring(mPosition, end);
565             mPosition = end + 1; // Skip the delimiter.
566             return nextString;
567         }
568 
remove()569         public void remove() {
570             throw new UnsupportedOperationException();
571         }
572     }
573 
stringOrSpannedString(CharSequence source)574     public static CharSequence stringOrSpannedString(CharSequence source) {
575         if (source == null)
576             return null;
577         if (source instanceof SpannedString)
578             return source;
579         if (source instanceof Spanned)
580             return new SpannedString(source);
581 
582         return source.toString();
583     }
584 
585     /**
586      * Returns true if the string is null or 0-length.
587      * @param str the string to be examined
588      * @return true if str is null or zero length
589      */
isEmpty(@ullable CharSequence str)590     public static boolean isEmpty(@Nullable CharSequence str) {
591         return str == null || str.length() == 0;
592     }
593 
594     /** {@hide} */
nullIfEmpty(@ullable String str)595     public static String nullIfEmpty(@Nullable String str) {
596         return isEmpty(str) ? null : str;
597     }
598 
599     /** {@hide} */
emptyIfNull(@ullable String str)600     public static String emptyIfNull(@Nullable String str) {
601         return str == null ? "" : str;
602     }
603 
604     /** {@hide} */
firstNotEmpty(@ullable String a, @NonNull String b)605     public static String firstNotEmpty(@Nullable String a, @NonNull String b) {
606         return !isEmpty(a) ? a : Preconditions.checkStringNotEmpty(b);
607     }
608 
609     /** {@hide} */
length(@ullable String s)610     public static int length(@Nullable String s) {
611         return s != null ? s.length() : 0;
612     }
613 
614     /**
615      * @return interned string if it's null.
616      * @hide
617      */
safeIntern(String s)618     public static String safeIntern(String s) {
619         return (s != null) ? s.intern() : null;
620     }
621 
622     /**
623      * Returns the length that the specified CharSequence would have if
624      * spaces and ASCII control characters were trimmed from the start and end,
625      * as by {@link String#trim}.
626      */
getTrimmedLength(CharSequence s)627     public static int getTrimmedLength(CharSequence s) {
628         int len = s.length();
629 
630         int start = 0;
631         while (start < len && s.charAt(start) <= ' ') {
632             start++;
633         }
634 
635         int end = len;
636         while (end > start && s.charAt(end - 1) <= ' ') {
637             end--;
638         }
639 
640         return end - start;
641     }
642 
643     /**
644      * Returns true if a and b are equal, including if they are both null.
645      * <p><i>Note: In platform versions 1.1 and earlier, this method only worked well if
646      * both the arguments were instances of String.</i></p>
647      * @param a first CharSequence to check
648      * @param b second CharSequence to check
649      * @return true if a and b are equal
650      */
equals(CharSequence a, CharSequence b)651     public static boolean equals(CharSequence a, CharSequence b) {
652         if (a == b) return true;
653         int length;
654         if (a != null && b != null && (length = a.length()) == b.length()) {
655             if (a instanceof String && b instanceof String) {
656                 return a.equals(b);
657             } else {
658                 for (int i = 0; i < length; i++) {
659                     if (a.charAt(i) != b.charAt(i)) return false;
660                 }
661                 return true;
662             }
663         }
664         return false;
665     }
666 
667     /**
668      * This function only reverses individual {@code char}s and not their associated
669      * spans. It doesn't support surrogate pairs (that correspond to non-BMP code points), combining
670      * sequences or conjuncts either.
671      * @deprecated Do not use.
672      */
673     @Deprecated
getReverse(CharSequence source, int start, int end)674     public static CharSequence getReverse(CharSequence source, int start, int end) {
675         return new Reverser(source, start, end);
676     }
677 
678     private static class Reverser
679     implements CharSequence, GetChars
680     {
Reverser(CharSequence source, int start, int end)681         public Reverser(CharSequence source, int start, int end) {
682             mSource = source;
683             mStart = start;
684             mEnd = end;
685         }
686 
length()687         public int length() {
688             return mEnd - mStart;
689         }
690 
subSequence(int start, int end)691         public CharSequence subSequence(int start, int end) {
692             char[] buf = new char[end - start];
693 
694             getChars(start, end, buf, 0);
695             return new String(buf);
696         }
697 
698         @Override
toString()699         public String toString() {
700             return subSequence(0, length()).toString();
701         }
702 
charAt(int off)703         public char charAt(int off) {
704             return (char) UCharacter.getMirror(mSource.charAt(mEnd - 1 - off));
705         }
706 
707         @SuppressWarnings("deprecation")
getChars(int start, int end, char[] dest, int destoff)708         public void getChars(int start, int end, char[] dest, int destoff) {
709             TextUtils.getChars(mSource, start + mStart, end + mStart,
710                                dest, destoff);
711             AndroidCharacter.mirror(dest, 0, end - start);
712 
713             int len = end - start;
714             int n = (end - start) / 2;
715             for (int i = 0; i < n; i++) {
716                 char tmp = dest[destoff + i];
717 
718                 dest[destoff + i] = dest[destoff + len - i - 1];
719                 dest[destoff + len - i - 1] = tmp;
720             }
721         }
722 
723         private CharSequence mSource;
724         private int mStart;
725         private int mEnd;
726     }
727 
728     /** @hide */
729     public static final int ALIGNMENT_SPAN = 1;
730     /** @hide */
731     public static final int FIRST_SPAN = ALIGNMENT_SPAN;
732     /** @hide */
733     public static final int FOREGROUND_COLOR_SPAN = 2;
734     /** @hide */
735     public static final int RELATIVE_SIZE_SPAN = 3;
736     /** @hide */
737     public static final int SCALE_X_SPAN = 4;
738     /** @hide */
739     public static final int STRIKETHROUGH_SPAN = 5;
740     /** @hide */
741     public static final int UNDERLINE_SPAN = 6;
742     /** @hide */
743     public static final int STYLE_SPAN = 7;
744     /** @hide */
745     public static final int BULLET_SPAN = 8;
746     /** @hide */
747     public static final int QUOTE_SPAN = 9;
748     /** @hide */
749     public static final int LEADING_MARGIN_SPAN = 10;
750     /** @hide */
751     public static final int URL_SPAN = 11;
752     /** @hide */
753     public static final int BACKGROUND_COLOR_SPAN = 12;
754     /** @hide */
755     public static final int TYPEFACE_SPAN = 13;
756     /** @hide */
757     public static final int SUPERSCRIPT_SPAN = 14;
758     /** @hide */
759     public static final int SUBSCRIPT_SPAN = 15;
760     /** @hide */
761     public static final int ABSOLUTE_SIZE_SPAN = 16;
762     /** @hide */
763     public static final int TEXT_APPEARANCE_SPAN = 17;
764     /** @hide */
765     public static final int ANNOTATION = 18;
766     /** @hide */
767     public static final int SUGGESTION_SPAN = 19;
768     /** @hide */
769     public static final int SPELL_CHECK_SPAN = 20;
770     /** @hide */
771     public static final int SUGGESTION_RANGE_SPAN = 21;
772     /** @hide */
773     public static final int EASY_EDIT_SPAN = 22;
774     /** @hide */
775     public static final int LOCALE_SPAN = 23;
776     /** @hide */
777     public static final int TTS_SPAN = 24;
778     /** @hide */
779     public static final int ACCESSIBILITY_CLICKABLE_SPAN = 25;
780     /** @hide */
781     public static final int ACCESSIBILITY_URL_SPAN = 26;
782     /** @hide */
783     public static final int LINE_BACKGROUND_SPAN = 27;
784     /** @hide */
785     public static final int LINE_HEIGHT_SPAN = 28;
786     /** @hide */
787     public static final int ACCESSIBILITY_REPLACEMENT_SPAN = 29;
788     /** @hide */
789     public static final int LAST_SPAN = ACCESSIBILITY_REPLACEMENT_SPAN;
790 
791     /**
792      * Flatten a CharSequence and whatever styles can be copied across processes
793      * into the parcel.
794      */
writeToParcel(@ullable CharSequence cs, @NonNull Parcel p, int parcelableFlags)795     public static void writeToParcel(@Nullable CharSequence cs, @NonNull Parcel p,
796             int parcelableFlags) {
797         if (cs instanceof Spanned) {
798             p.writeInt(0);
799             p.writeString8(cs.toString());
800 
801             Spanned sp = (Spanned) cs;
802             Object[] os = sp.getSpans(0, cs.length(), Object.class);
803 
804             // note to people adding to this: check more specific types
805             // before more generic types.  also notice that it uses
806             // "if" instead of "else if" where there are interfaces
807             // so one object can be several.
808 
809             for (int i = 0; i < os.length; i++) {
810                 Object o = os[i];
811                 Object prop = os[i];
812 
813                 if (prop instanceof CharacterStyle) {
814                     prop = ((CharacterStyle) prop).getUnderlying();
815                 }
816 
817                 if (prop instanceof ParcelableSpan) {
818                     final ParcelableSpan ps = (ParcelableSpan) prop;
819                     final int spanTypeId = ps.getSpanTypeIdInternal();
820                     if (spanTypeId < FIRST_SPAN || spanTypeId > LAST_SPAN) {
821                         Log.e(TAG, "External class \"" + ps.getClass().getSimpleName()
822                                 + "\" is attempting to use the frameworks-only ParcelableSpan"
823                                 + " interface");
824                     } else {
825                         p.writeInt(spanTypeId);
826                         ps.writeToParcelInternal(p, parcelableFlags);
827                         writeWhere(p, sp, o);
828                     }
829                 }
830             }
831 
832             p.writeInt(0);
833         } else {
834             p.writeInt(1);
835             if (cs != null) {
836                 p.writeString8(cs.toString());
837             } else {
838                 p.writeString8(null);
839             }
840         }
841     }
842 
writeWhere(Parcel p, Spanned sp, Object o)843     private static void writeWhere(Parcel p, Spanned sp, Object o) {
844         p.writeInt(sp.getSpanStart(o));
845         p.writeInt(sp.getSpanEnd(o));
846         p.writeInt(sp.getSpanFlags(o));
847     }
848 
849     public static final Parcelable.Creator<CharSequence> CHAR_SEQUENCE_CREATOR
850             = new Parcelable.Creator<CharSequence>() {
851         /**
852          * Read and return a new CharSequence, possibly with styles,
853          * from the parcel.
854          */
855         public CharSequence createFromParcel(Parcel p) {
856             int kind = p.readInt();
857 
858             String string = p.readString8();
859             if (string == null) {
860                 return null;
861             }
862 
863             if (kind == 1) {
864                 return string;
865             }
866 
867             SpannableString sp = new SpannableString(string);
868 
869             while (true) {
870                 kind = p.readInt();
871 
872                 if (kind == 0)
873                     break;
874 
875                 switch (kind) {
876                 case ALIGNMENT_SPAN:
877                     readSpan(p, sp, new AlignmentSpan.Standard(p));
878                     break;
879 
880                 case FOREGROUND_COLOR_SPAN:
881                     readSpan(p, sp, new ForegroundColorSpan(p));
882                     break;
883 
884                 case RELATIVE_SIZE_SPAN:
885                     readSpan(p, sp, new RelativeSizeSpan(p));
886                     break;
887 
888                 case SCALE_X_SPAN:
889                     readSpan(p, sp, new ScaleXSpan(p));
890                     break;
891 
892                 case STRIKETHROUGH_SPAN:
893                     readSpan(p, sp, new StrikethroughSpan(p));
894                     break;
895 
896                 case UNDERLINE_SPAN:
897                     readSpan(p, sp, new UnderlineSpan(p));
898                     break;
899 
900                 case STYLE_SPAN:
901                     readSpan(p, sp, new StyleSpan(p));
902                     break;
903 
904                 case BULLET_SPAN:
905                     readSpan(p, sp, new BulletSpan(p));
906                     break;
907 
908                 case QUOTE_SPAN:
909                     readSpan(p, sp, new QuoteSpan(p));
910                     break;
911 
912                 case LEADING_MARGIN_SPAN:
913                     readSpan(p, sp, new LeadingMarginSpan.Standard(p));
914                     break;
915 
916                 case URL_SPAN:
917                     readSpan(p, sp, new URLSpan(p));
918                     break;
919 
920                 case BACKGROUND_COLOR_SPAN:
921                     readSpan(p, sp, new BackgroundColorSpan(p));
922                     break;
923 
924                 case TYPEFACE_SPAN:
925                     readSpan(p, sp, new TypefaceSpan(p));
926                     break;
927 
928                 case SUPERSCRIPT_SPAN:
929                     readSpan(p, sp, new SuperscriptSpan(p));
930                     break;
931 
932                 case SUBSCRIPT_SPAN:
933                     readSpan(p, sp, new SubscriptSpan(p));
934                     break;
935 
936                 case ABSOLUTE_SIZE_SPAN:
937                     readSpan(p, sp, new AbsoluteSizeSpan(p));
938                     break;
939 
940                 case TEXT_APPEARANCE_SPAN:
941                     readSpan(p, sp, new TextAppearanceSpan(p));
942                     break;
943 
944                 case ANNOTATION:
945                     readSpan(p, sp, new Annotation(p));
946                     break;
947 
948                 case SUGGESTION_SPAN:
949                     readSpan(p, sp, new SuggestionSpan(p));
950                     break;
951 
952                 case SPELL_CHECK_SPAN:
953                     readSpan(p, sp, new SpellCheckSpan(p));
954                     break;
955 
956                 case SUGGESTION_RANGE_SPAN:
957                     readSpan(p, sp, new SuggestionRangeSpan(p));
958                     break;
959 
960                 case EASY_EDIT_SPAN:
961                     readSpan(p, sp, new EasyEditSpan(p));
962                     break;
963 
964                 case LOCALE_SPAN:
965                     readSpan(p, sp, new LocaleSpan(p));
966                     break;
967 
968                 case TTS_SPAN:
969                     readSpan(p, sp, new TtsSpan(p));
970                     break;
971 
972                 case ACCESSIBILITY_CLICKABLE_SPAN:
973                     readSpan(p, sp, new AccessibilityClickableSpan(p));
974                     break;
975 
976                 case ACCESSIBILITY_URL_SPAN:
977                     readSpan(p, sp, new AccessibilityURLSpan(p));
978                     break;
979 
980                 case LINE_BACKGROUND_SPAN:
981                     readSpan(p, sp, new LineBackgroundSpan.Standard(p));
982                     break;
983 
984                 case LINE_HEIGHT_SPAN:
985                     readSpan(p, sp, new LineHeightSpan.Standard(p));
986                     break;
987 
988                 case ACCESSIBILITY_REPLACEMENT_SPAN:
989                     readSpan(p, sp, new AccessibilityReplacementSpan(p));
990                     break;
991 
992                 default:
993                     throw new RuntimeException("bogus span encoding " + kind);
994                 }
995             }
996 
997             return sp;
998         }
999 
1000         public CharSequence[] newArray(int size)
1001         {
1002             return new CharSequence[size];
1003         }
1004     };
1005 
1006     /**
1007      * Debugging tool to print the spans in a CharSequence.  The output will
1008      * be printed one span per line.  If the CharSequence is not a Spanned,
1009      * then the entire string will be printed on a single line.
1010      */
dumpSpans(CharSequence cs, Printer printer, String prefix)1011     public static void dumpSpans(CharSequence cs, Printer printer, String prefix) {
1012         if (cs instanceof Spanned) {
1013             Spanned sp = (Spanned) cs;
1014             Object[] os = sp.getSpans(0, cs.length(), Object.class);
1015 
1016             for (int i = 0; i < os.length; i++) {
1017                 Object o = os[i];
1018                 printer.println(prefix + cs.subSequence(sp.getSpanStart(o),
1019                         sp.getSpanEnd(o)) + ": "
1020                         + Integer.toHexString(System.identityHashCode(o))
1021                         + " " + o.getClass().getCanonicalName()
1022                          + " (" + sp.getSpanStart(o) + "-" + sp.getSpanEnd(o)
1023                          + ") fl=#" + sp.getSpanFlags(o));
1024             }
1025         } else {
1026             printer.println(prefix + cs + ": (no spans)");
1027         }
1028     }
1029 
1030     /**
1031      * Return a new CharSequence in which each of the source strings is
1032      * replaced by the corresponding element of the destinations.
1033      */
replace(CharSequence template, String[] sources, CharSequence[] destinations)1034     public static CharSequence replace(CharSequence template,
1035                                        String[] sources,
1036                                        CharSequence[] destinations) {
1037         SpannableStringBuilder tb = new SpannableStringBuilder(template);
1038 
1039         for (int i = 0; i < sources.length; i++) {
1040             int where = indexOf(tb, sources[i]);
1041 
1042             if (where >= 0)
1043                 tb.setSpan(sources[i], where, where + sources[i].length(),
1044                            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1045         }
1046 
1047         for (int i = 0; i < sources.length; i++) {
1048             int start = tb.getSpanStart(sources[i]);
1049             int end = tb.getSpanEnd(sources[i]);
1050 
1051             if (start >= 0) {
1052                 tb.replace(start, end, destinations[i]);
1053             }
1054         }
1055 
1056         return tb;
1057     }
1058 
1059     /**
1060      * Replace instances of "^1", "^2", etc. in the
1061      * <code>template</code> CharSequence with the corresponding
1062      * <code>values</code>.  "^^" is used to produce a single caret in
1063      * the output.  Only up to 9 replacement values are supported,
1064      * "^10" will be produce the first replacement value followed by a
1065      * '0'.
1066      *
1067      * @param template the input text containing "^1"-style
1068      * placeholder values.  This object is not modified; a copy is
1069      * returned.
1070      *
1071      * @param values CharSequences substituted into the template.  The
1072      * first is substituted for "^1", the second for "^2", and so on.
1073      *
1074      * @return the new CharSequence produced by doing the replacement
1075      *
1076      * @throws IllegalArgumentException if the template requests a
1077      * value that was not provided, or if more than 9 values are
1078      * provided.
1079      */
expandTemplate(CharSequence template, CharSequence... values)1080     public static CharSequence expandTemplate(CharSequence template,
1081                                               CharSequence... values) {
1082         if (values.length > 9) {
1083             throw new IllegalArgumentException("max of 9 values are supported");
1084         }
1085 
1086         SpannableStringBuilder ssb = new SpannableStringBuilder(template);
1087 
1088         try {
1089             int i = 0;
1090             while (i < ssb.length()) {
1091                 if (ssb.charAt(i) == '^') {
1092                     char next = ssb.charAt(i+1);
1093                     if (next == '^') {
1094                         ssb.delete(i+1, i+2);
1095                         ++i;
1096                         continue;
1097                     } else if (Character.isDigit(next)) {
1098                         int which = Character.getNumericValue(next) - 1;
1099                         if (which < 0) {
1100                             throw new IllegalArgumentException(
1101                                 "template requests value ^" + (which+1));
1102                         }
1103                         if (which >= values.length) {
1104                             throw new IllegalArgumentException(
1105                                 "template requests value ^" + (which+1) +
1106                                 "; only " + values.length + " provided");
1107                         }
1108                         ssb.replace(i, i+2, values[which]);
1109                         i += values[which].length();
1110                         continue;
1111                     }
1112                 }
1113                 ++i;
1114             }
1115         } catch (IndexOutOfBoundsException ignore) {
1116             // happens when ^ is the last character in the string.
1117         }
1118         return ssb;
1119     }
1120 
getOffsetBefore(CharSequence text, int offset)1121     public static int getOffsetBefore(CharSequence text, int offset) {
1122         if (offset == 0)
1123             return 0;
1124         if (offset == 1)
1125             return 0;
1126 
1127         char c = text.charAt(offset - 1);
1128 
1129         if (c >= '\uDC00' && c <= '\uDFFF') {
1130             char c1 = text.charAt(offset - 2);
1131 
1132             if (c1 >= '\uD800' && c1 <= '\uDBFF')
1133                 offset -= 2;
1134             else
1135                 offset -= 1;
1136         } else {
1137             offset -= 1;
1138         }
1139 
1140         if (text instanceof Spanned) {
1141             ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
1142                                                        ReplacementSpan.class);
1143 
1144             for (int i = 0; i < spans.length; i++) {
1145                 int start = ((Spanned) text).getSpanStart(spans[i]);
1146                 int end = ((Spanned) text).getSpanEnd(spans[i]);
1147 
1148                 if (start < offset && end > offset)
1149                     offset = start;
1150             }
1151         }
1152 
1153         return offset;
1154     }
1155 
getOffsetAfter(CharSequence text, int offset)1156     public static int getOffsetAfter(CharSequence text, int offset) {
1157         int len = text.length();
1158 
1159         if (offset == len)
1160             return len;
1161         if (offset == len - 1)
1162             return len;
1163 
1164         char c = text.charAt(offset);
1165 
1166         if (c >= '\uD800' && c <= '\uDBFF') {
1167             char c1 = text.charAt(offset + 1);
1168 
1169             if (c1 >= '\uDC00' && c1 <= '\uDFFF')
1170                 offset += 2;
1171             else
1172                 offset += 1;
1173         } else {
1174             offset += 1;
1175         }
1176 
1177         if (text instanceof Spanned) {
1178             ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
1179                                                        ReplacementSpan.class);
1180 
1181             for (int i = 0; i < spans.length; i++) {
1182                 int start = ((Spanned) text).getSpanStart(spans[i]);
1183                 int end = ((Spanned) text).getSpanEnd(spans[i]);
1184 
1185                 if (start < offset && end > offset)
1186                     offset = end;
1187             }
1188         }
1189 
1190         return offset;
1191     }
1192 
readSpan(Parcel p, Spannable sp, Object o)1193     private static void readSpan(Parcel p, Spannable sp, Object o) {
1194         sp.setSpan(o, p.readInt(), p.readInt(), p.readInt());
1195     }
1196 
1197     /**
1198      * Copies the spans from the region <code>start...end</code> in
1199      * <code>source</code> to the region
1200      * <code>destoff...destoff+end-start</code> in <code>dest</code>.
1201      * Spans in <code>source</code> that begin before <code>start</code>
1202      * or end after <code>end</code> but overlap this range are trimmed
1203      * as if they began at <code>start</code> or ended at <code>end</code>.
1204      *
1205      * @throws IndexOutOfBoundsException if any of the copied spans
1206      * are out of range in <code>dest</code>.
1207      */
copySpansFrom(Spanned source, int start, int end, Class kind, Spannable dest, int destoff)1208     public static void copySpansFrom(Spanned source, int start, int end,
1209                                      Class kind,
1210                                      Spannable dest, int destoff) {
1211         if (kind == null) {
1212             kind = Object.class;
1213         }
1214 
1215         Object[] spans = source.getSpans(start, end, kind);
1216 
1217         for (int i = 0; i < spans.length; i++) {
1218             int st = source.getSpanStart(spans[i]);
1219             int en = source.getSpanEnd(spans[i]);
1220             int fl = source.getSpanFlags(spans[i]);
1221 
1222             if (st < start)
1223                 st = start;
1224             if (en > end)
1225                 en = end;
1226 
1227             dest.setSpan(spans[i], st - start + destoff, en - start + destoff,
1228                          fl);
1229         }
1230     }
1231 
1232     /**
1233      * Transforms a CharSequences to uppercase, copying the sources spans and keeping them spans as
1234      * much as possible close to their relative original places. If uppercase string is identical
1235      * to the sources, the source itself is returned instead of being copied.
1236      *
1237      * If copySpans is set, source must be an instance of Spanned.
1238      *
1239      * {@hide}
1240      */
1241     @NonNull
toUpperCase(@ullable Locale locale, @NonNull CharSequence source, boolean copySpans)1242     public static CharSequence toUpperCase(@Nullable Locale locale, @NonNull CharSequence source,
1243             boolean copySpans) {
1244         final Edits edits = new Edits();
1245         if (!copySpans) { // No spans. Just uppercase the characters.
1246             final StringBuilder result = CaseMap.toUpper().apply(
1247                     locale, source, new StringBuilder(), edits);
1248             return edits.hasChanges() ? result : source;
1249         }
1250 
1251         final SpannableStringBuilder result = CaseMap.toUpper().apply(
1252                 locale, source, new SpannableStringBuilder(), edits);
1253         if (!edits.hasChanges()) {
1254             // No changes happened while capitalizing. We can return the source as it was.
1255             return source;
1256         }
1257 
1258         final Edits.Iterator iterator = edits.getFineIterator();
1259         final int sourceLength = source.length();
1260         final Spanned spanned = (Spanned) source;
1261         final Object[] spans = spanned.getSpans(0, sourceLength, Object.class);
1262         for (Object span : spans) {
1263             final int sourceStart = spanned.getSpanStart(span);
1264             final int sourceEnd = spanned.getSpanEnd(span);
1265             final int flags = spanned.getSpanFlags(span);
1266             // Make sure the indices are not at the end of the string, since in that case
1267             // iterator.findSourceIndex() would fail.
1268             final int destStart = sourceStart == sourceLength ? result.length() :
1269                     toUpperMapToDest(iterator, sourceStart);
1270             final int destEnd = sourceEnd == sourceLength ? result.length() :
1271                     toUpperMapToDest(iterator, sourceEnd);
1272             result.setSpan(span, destStart, destEnd, flags);
1273         }
1274         return result;
1275     }
1276 
1277     // helper method for toUpperCase()
toUpperMapToDest(Edits.Iterator iterator, int sourceIndex)1278     private static int toUpperMapToDest(Edits.Iterator iterator, int sourceIndex) {
1279         // Guaranteed to succeed if sourceIndex < source.length().
1280         iterator.findSourceIndex(sourceIndex);
1281         if (sourceIndex == iterator.sourceIndex()) {
1282             return iterator.destinationIndex();
1283         }
1284         // We handle the situation differently depending on if we are in the changed slice or an
1285         // unchanged one: In an unchanged slice, we can find the exact location the span
1286         // boundary was before and map there.
1287         //
1288         // But in a changed slice, we need to treat the whole destination slice as an atomic unit.
1289         // We adjust the span boundary to the end of that slice to reduce of the chance of adjacent
1290         // spans in the source overlapping in the result. (The choice for the end vs the beginning
1291         // is somewhat arbitrary, but was taken because we except to see slightly more spans only
1292         // affecting a base character compared to spans only affecting a combining character.)
1293         if (iterator.hasChange()) {
1294             return iterator.destinationIndex() + iterator.newLength();
1295         } else {
1296             // Move the index 1:1 along with this unchanged piece of text.
1297             return iterator.destinationIndex() + (sourceIndex - iterator.sourceIndex());
1298         }
1299     }
1300 
1301     public enum TruncateAt {
1302         START,
1303         MIDDLE,
1304         END,
1305         MARQUEE,
1306         /**
1307          * @hide
1308          */
1309         @UnsupportedAppUsage
1310         END_SMALL
1311     }
1312 
1313     public interface EllipsizeCallback {
1314         /**
1315          * This method is called to report that the specified region of
1316          * text was ellipsized away by a call to {@link #ellipsize}.
1317          */
ellipsized(int start, int end)1318         public void ellipsized(int start, int end);
1319     }
1320 
1321     /**
1322      * Returns the original text if it fits in the specified width
1323      * given the properties of the specified Paint,
1324      * or, if it does not fit, a truncated
1325      * copy with ellipsis character added at the specified edge or center.
1326      */
ellipsize(CharSequence text, TextPaint p, float avail, TruncateAt where)1327     public static CharSequence ellipsize(CharSequence text,
1328                                          TextPaint p,
1329                                          float avail, TruncateAt where) {
1330         return ellipsize(text, p, avail, where, false, null);
1331     }
1332 
1333     /**
1334      * Returns the original text if it fits in the specified width
1335      * given the properties of the specified Paint,
1336      * or, if it does not fit, a copy with ellipsis character added
1337      * at the specified edge or center.
1338      * If <code>preserveLength</code> is specified, the returned copy
1339      * will be padded with zero-width spaces to preserve the original
1340      * length and offsets instead of truncating.
1341      * If <code>callback</code> is non-null, it will be called to
1342      * report the start and end of the ellipsized range.  TextDirection
1343      * is determined by the first strong directional character.
1344      */
ellipsize(CharSequence text, TextPaint paint, float avail, TruncateAt where, boolean preserveLength, @Nullable EllipsizeCallback callback)1345     public static CharSequence ellipsize(CharSequence text,
1346                                          TextPaint paint,
1347                                          float avail, TruncateAt where,
1348                                          boolean preserveLength,
1349                                          @Nullable EllipsizeCallback callback) {
1350         return ellipsize(text, paint, avail, where, preserveLength, callback,
1351                 TextDirectionHeuristics.FIRSTSTRONG_LTR,
1352                 getEllipsisString(where));
1353     }
1354 
1355     /**
1356      * Returns the original text if it fits in the specified width
1357      * given the properties of the specified Paint,
1358      * or, if it does not fit, a copy with ellipsis character added
1359      * at the specified edge or center.
1360      * If <code>preserveLength</code> is specified, the returned copy
1361      * will be padded with zero-width spaces to preserve the original
1362      * length and offsets instead of truncating.
1363      * If <code>callback</code> is non-null, it will be called to
1364      * report the start and end of the ellipsized range.
1365      *
1366      * @hide
1367      */
ellipsize(CharSequence text, TextPaint paint, float avail, TruncateAt where, boolean preserveLength, @Nullable EllipsizeCallback callback, TextDirectionHeuristic textDir, String ellipsis)1368     public static CharSequence ellipsize(CharSequence text,
1369             TextPaint paint,
1370             float avail, TruncateAt where,
1371             boolean preserveLength,
1372             @Nullable EllipsizeCallback callback,
1373             TextDirectionHeuristic textDir, String ellipsis) {
1374 
1375         int len = text.length();
1376 
1377         MeasuredParagraph mt = null;
1378         try {
1379             mt = MeasuredParagraph.buildForMeasurement(paint, text, 0, text.length(), textDir, mt);
1380             float width = mt.getWholeWidth();
1381 
1382             if (width <= avail) {
1383                 if (callback != null) {
1384                     callback.ellipsized(0, 0);
1385                 }
1386 
1387                 return text;
1388             }
1389 
1390             // XXX assumes ellipsis string does not require shaping and
1391             // is unaffected by style
1392             float ellipsiswid = paint.measureText(ellipsis);
1393             avail -= ellipsiswid;
1394 
1395             int left = 0;
1396             int right = len;
1397             if (avail < 0) {
1398                 // it all goes
1399             } else if (where == TruncateAt.START) {
1400                 right = len - mt.breakText(len, false, avail);
1401             } else if (where == TruncateAt.END || where == TruncateAt.END_SMALL) {
1402                 left = mt.breakText(len, true, avail);
1403             } else {
1404                 right = len - mt.breakText(len, false, avail / 2);
1405                 avail -= mt.measure(right, len);
1406                 left = mt.breakText(right, true, avail);
1407             }
1408 
1409             if (callback != null) {
1410                 callback.ellipsized(left, right);
1411             }
1412 
1413             final char[] buf = mt.getChars();
1414             Spanned sp = text instanceof Spanned ? (Spanned) text : null;
1415 
1416             final int removed = right - left;
1417             final int remaining = len - removed;
1418             if (preserveLength) {
1419                 if (remaining > 0 && removed >= ellipsis.length()) {
1420                     ellipsis.getChars(0, ellipsis.length(), buf, left);
1421                     left += ellipsis.length();
1422                 } // else skip the ellipsis
1423                 for (int i = left; i < right; i++) {
1424                     buf[i] = ELLIPSIS_FILLER;
1425                 }
1426                 String s = new String(buf, 0, len);
1427                 if (sp == null) {
1428                     return s;
1429                 }
1430                 SpannableString ss = new SpannableString(s);
1431                 copySpansFrom(sp, 0, len, Object.class, ss, 0);
1432                 return ss;
1433             }
1434 
1435             if (remaining == 0) {
1436                 return "";
1437             }
1438 
1439             if (sp == null) {
1440                 StringBuilder sb = new StringBuilder(remaining + ellipsis.length());
1441                 sb.append(buf, 0, left);
1442                 sb.append(ellipsis);
1443                 sb.append(buf, right, len - right);
1444                 return sb.toString();
1445             }
1446 
1447             SpannableStringBuilder ssb = new SpannableStringBuilder();
1448             ssb.append(text, 0, left);
1449             ssb.append(ellipsis);
1450             ssb.append(text, right, len);
1451             return ssb;
1452         } finally {
1453             if (mt != null) {
1454                 mt.recycle();
1455             }
1456         }
1457     }
1458 
1459     /**
1460      * Formats a list of CharSequences by repeatedly inserting the separator between them,
1461      * but stopping when the resulting sequence is too wide for the specified width.
1462      *
1463      * This method actually tries to fit the maximum number of elements. So if {@code "A, 11 more"
1464      * fits}, {@code "A, B, 10 more"} doesn't fit, but {@code "A, B, C, 9 more"} fits again (due to
1465      * the glyphs for the digits being very wide, for example), it returns
1466      * {@code "A, B, C, 9 more"}. Because of this, this method may be inefficient for very long
1467      * lists.
1468      *
1469      * Note that the elements of the returned value, as well as the string for {@code moreId}, will
1470      * be bidi-wrapped using {@link BidiFormatter#unicodeWrap} based on the locale of the input
1471      * Context. If the input {@code Context} is null, the default BidiFormatter from
1472      * {@link BidiFormatter#getInstance()} will be used.
1473      *
1474      * @param context the {@code Context} to get the {@code moreId} resource from. If {@code null},
1475      *     an ellipsis (U+2026) would be used for {@code moreId}.
1476      * @param elements the list to format
1477      * @param separator a separator, such as {@code ", "}
1478      * @param paint the Paint with which to measure the text
1479      * @param avail the horizontal width available for the text (in pixels)
1480      * @param moreId the resource ID for the pluralized string to insert at the end of sequence when
1481      *     some of the elements don't fit.
1482      *
1483      * @return the formatted CharSequence. If even the shortest sequence (e.g. {@code "A, 11 more"})
1484      *     doesn't fit, it will return an empty string.
1485      */
1486 
listEllipsize(@ullable Context context, @Nullable List<CharSequence> elements, @NonNull String separator, @NonNull TextPaint paint, @FloatRange(from=0.0,fromInclusive=false) float avail, @PluralsRes int moreId)1487     public static CharSequence listEllipsize(@Nullable Context context,
1488             @Nullable List<CharSequence> elements, @NonNull String separator,
1489             @NonNull TextPaint paint, @FloatRange(from=0.0,fromInclusive=false) float avail,
1490             @PluralsRes int moreId) {
1491         if (elements == null) {
1492             return "";
1493         }
1494         final int totalLen = elements.size();
1495         if (totalLen == 0) {
1496             return "";
1497         }
1498 
1499         final Resources res;
1500         final BidiFormatter bidiFormatter;
1501         if (context == null) {
1502             res = null;
1503             bidiFormatter = BidiFormatter.getInstance();
1504         } else {
1505             res = context.getResources();
1506             bidiFormatter = BidiFormatter.getInstance(res.getConfiguration().getLocales().get(0));
1507         }
1508 
1509         final SpannableStringBuilder output = new SpannableStringBuilder();
1510         final int[] endIndexes = new int[totalLen];
1511         for (int i = 0; i < totalLen; i++) {
1512             output.append(bidiFormatter.unicodeWrap(elements.get(i)));
1513             if (i != totalLen - 1) {  // Insert a separator, except at the very end.
1514                 output.append(separator);
1515             }
1516             endIndexes[i] = output.length();
1517         }
1518 
1519         for (int i = totalLen - 1; i >= 0; i--) {
1520             // Delete the tail of the string, cutting back to one less element.
1521             output.delete(endIndexes[i], output.length());
1522 
1523             final int remainingElements = totalLen - i - 1;
1524             if (remainingElements > 0) {
1525                 CharSequence morePiece = (res == null) ?
1526                         ELLIPSIS_NORMAL :
1527                         res.getQuantityString(moreId, remainingElements, remainingElements);
1528                 morePiece = bidiFormatter.unicodeWrap(morePiece);
1529                 output.append(morePiece);
1530             }
1531 
1532             final float width = paint.measureText(output, 0, output.length());
1533             if (width <= avail) {  // The string fits.
1534                 return output;
1535             }
1536         }
1537         return "";  // Nothing fits.
1538     }
1539 
1540     /**
1541      * Converts a CharSequence of the comma-separated form "Andy, Bob,
1542      * Charles, David" that is too wide to fit into the specified width
1543      * into one like "Andy, Bob, 2 more".
1544      *
1545      * @param text the text to truncate
1546      * @param p the Paint with which to measure the text
1547      * @param avail the horizontal width available for the text (in pixels)
1548      * @param oneMore the string for "1 more" in the current locale
1549      * @param more the string for "%d more" in the current locale
1550      *
1551      * @deprecated Do not use. This is not internationalized, and has known issues
1552      * with right-to-left text, languages that have more than one plural form, languages
1553      * that use a different character as a comma-like separator, etc.
1554      * Use {@link #listEllipsize} instead.
1555      */
1556     @Deprecated
commaEllipsize(CharSequence text, TextPaint p, float avail, String oneMore, String more)1557     public static CharSequence commaEllipsize(CharSequence text,
1558                                               TextPaint p, float avail,
1559                                               String oneMore,
1560                                               String more) {
1561         return commaEllipsize(text, p, avail, oneMore, more,
1562                 TextDirectionHeuristics.FIRSTSTRONG_LTR);
1563     }
1564 
1565     /**
1566      * @hide
1567      */
1568     @Deprecated
commaEllipsize(CharSequence text, TextPaint p, float avail, String oneMore, String more, TextDirectionHeuristic textDir)1569     public static CharSequence commaEllipsize(CharSequence text, TextPaint p,
1570          float avail, String oneMore, String more, TextDirectionHeuristic textDir) {
1571 
1572         MeasuredParagraph mt = null;
1573         MeasuredParagraph tempMt = null;
1574         try {
1575             int len = text.length();
1576             mt = MeasuredParagraph.buildForMeasurement(p, text, 0, len, textDir, mt);
1577             final float width = mt.getWholeWidth();
1578             if (width <= avail) {
1579                 return text;
1580             }
1581 
1582             char[] buf = mt.getChars();
1583 
1584             int commaCount = 0;
1585             for (int i = 0; i < len; i++) {
1586                 if (buf[i] == ',') {
1587                     commaCount++;
1588                 }
1589             }
1590 
1591             int remaining = commaCount + 1;
1592 
1593             int ok = 0;
1594             String okFormat = "";
1595 
1596             int w = 0;
1597             int count = 0;
1598             float[] widths = mt.getWidths().getRawArray();
1599 
1600             for (int i = 0; i < len; i++) {
1601                 w += widths[i];
1602 
1603                 if (buf[i] == ',') {
1604                     count++;
1605 
1606                     String format;
1607                     // XXX should not insert spaces, should be part of string
1608                     // XXX should use plural rules and not assume English plurals
1609                     if (--remaining == 1) {
1610                         format = " " + oneMore;
1611                     } else {
1612                         format = " " + String.format(more, remaining);
1613                     }
1614 
1615                     // XXX this is probably ok, but need to look at it more
1616                     tempMt = MeasuredParagraph.buildForMeasurement(
1617                             p, format, 0, format.length(), textDir, tempMt);
1618                     float moreWid = tempMt.getWholeWidth();
1619 
1620                     if (w + moreWid <= avail) {
1621                         ok = i + 1;
1622                         okFormat = format;
1623                     }
1624                 }
1625             }
1626 
1627             SpannableStringBuilder out = new SpannableStringBuilder(okFormat);
1628             out.insert(0, text, 0, ok);
1629             return out;
1630         } finally {
1631             if (mt != null) {
1632                 mt.recycle();
1633             }
1634             if (tempMt != null) {
1635                 tempMt.recycle();
1636             }
1637         }
1638     }
1639 
1640     // Returns true if the character's presence could affect RTL layout.
1641     //
1642     // In order to be fast, the code is intentionally rough and quite conservative in its
1643     // considering inclusion of any non-BMP or surrogate characters or anything in the bidi
1644     // blocks or any bidi formatting characters with a potential to affect RTL layout.
1645     /* package */
couldAffectRtl(char c)1646     static boolean couldAffectRtl(char c) {
1647         return (0x0590 <= c && c <= 0x08FF) ||  // RTL scripts
1648                 c == 0x200E ||  // Bidi format character
1649                 c == 0x200F ||  // Bidi format character
1650                 (0x202A <= c && c <= 0x202E) ||  // Bidi format characters
1651                 (0x2066 <= c && c <= 0x2069) ||  // Bidi format characters
1652                 (0xD800 <= c && c <= 0xDFFF) ||  // Surrogate pairs
1653                 (0xFB1D <= c && c <= 0xFDFF) ||  // Hebrew and Arabic presentation forms
1654                 (0xFE70 <= c && c <= 0xFEFE);  // Arabic presentation forms
1655     }
1656 
1657     // Returns true if there is no character present that may potentially affect RTL layout.
1658     // Since this calls couldAffectRtl() above, it's also quite conservative, in the way that
1659     // it may return 'false' (needs bidi) although careful consideration may tell us it should
1660     // return 'true' (does not need bidi).
1661     /* package */
doesNotNeedBidi(char[] text, int start, int len)1662     static boolean doesNotNeedBidi(char[] text, int start, int len) {
1663         final int end = start + len;
1664         for (int i = start; i < end; i++) {
1665             if (couldAffectRtl(text[i])) {
1666                 return false;
1667             }
1668         }
1669         return true;
1670     }
1671 
obtain(int len)1672     /* package */ static char[] obtain(int len) {
1673         char[] buf;
1674 
1675         synchronized (sLock) {
1676             buf = sTemp;
1677             sTemp = null;
1678         }
1679 
1680         if (buf == null || buf.length < len)
1681             buf = ArrayUtils.newUnpaddedCharArray(len);
1682 
1683         return buf;
1684     }
1685 
recycle(char[] temp)1686     /* package */ static void recycle(char[] temp) {
1687         if (temp.length > 1000)
1688             return;
1689 
1690         synchronized (sLock) {
1691             sTemp = temp;
1692         }
1693     }
1694 
1695     /**
1696      * Html-encode the string.
1697      * @param s the string to be encoded
1698      * @return the encoded string
1699      */
htmlEncode(String s)1700     public static String htmlEncode(String s) {
1701         StringBuilder sb = new StringBuilder();
1702         char c;
1703         for (int i = 0; i < s.length(); i++) {
1704             c = s.charAt(i);
1705             switch (c) {
1706             case '<':
1707                 sb.append("&lt;"); //$NON-NLS-1$
1708                 break;
1709             case '>':
1710                 sb.append("&gt;"); //$NON-NLS-1$
1711                 break;
1712             case '&':
1713                 sb.append("&amp;"); //$NON-NLS-1$
1714                 break;
1715             case '\'':
1716                 //http://www.w3.org/TR/xhtml1
1717                 // The named character reference &apos; (the apostrophe, U+0027) was introduced in
1718                 // XML 1.0 but does not appear in HTML. Authors should therefore use &#39; instead
1719                 // of &apos; to work as expected in HTML 4 user agents.
1720                 sb.append("&#39;"); //$NON-NLS-1$
1721                 break;
1722             case '"':
1723                 sb.append("&quot;"); //$NON-NLS-1$
1724                 break;
1725             default:
1726                 sb.append(c);
1727             }
1728         }
1729         return sb.toString();
1730     }
1731 
1732     /**
1733      * Returns a CharSequence concatenating the specified CharSequences,
1734      * retaining their spans if any.
1735      *
1736      * If there are no parameters, an empty string will be returned.
1737      *
1738      * If the number of parameters is exactly one, that parameter is returned as output, even if it
1739      * is null.
1740      *
1741      * If the number of parameters is at least two, any null CharSequence among the parameters is
1742      * treated as if it was the string <code>"null"</code>.
1743      *
1744      * If there are paragraph spans in the source CharSequences that satisfy paragraph boundary
1745      * requirements in the sources but would no longer satisfy them in the concatenated
1746      * CharSequence, they may get extended in the resulting CharSequence or not retained.
1747      */
concat(CharSequence... text)1748     public static CharSequence concat(CharSequence... text) {
1749         if (text.length == 0) {
1750             return "";
1751         }
1752 
1753         if (text.length == 1) {
1754             return text[0];
1755         }
1756 
1757         boolean spanned = false;
1758         for (CharSequence piece : text) {
1759             if (piece instanceof Spanned) {
1760                 spanned = true;
1761                 break;
1762             }
1763         }
1764 
1765         if (spanned) {
1766             final SpannableStringBuilder ssb = new SpannableStringBuilder();
1767             for (CharSequence piece : text) {
1768                 // If a piece is null, we append the string "null" for compatibility with the
1769                 // behavior of StringBuilder and the behavior of the concat() method in earlier
1770                 // versions of Android.
1771                 ssb.append(piece == null ? "null" : piece);
1772             }
1773             return new SpannedString(ssb);
1774         } else {
1775             final StringBuilder sb = new StringBuilder();
1776             for (CharSequence piece : text) {
1777                 sb.append(piece);
1778             }
1779             return sb.toString();
1780         }
1781     }
1782 
1783     /**
1784      * Returns whether the given CharSequence contains any printable characters.
1785      */
isGraphic(CharSequence str)1786     public static boolean isGraphic(CharSequence str) {
1787         final int len = str.length();
1788         for (int cp, i=0; i<len; i+=Character.charCount(cp)) {
1789             cp = Character.codePointAt(str, i);
1790             int gc = Character.getType(cp);
1791             if (gc != Character.CONTROL
1792                     && gc != Character.FORMAT
1793                     && gc != Character.SURROGATE
1794                     && gc != Character.UNASSIGNED
1795                     && gc != Character.LINE_SEPARATOR
1796                     && gc != Character.PARAGRAPH_SEPARATOR
1797                     && gc != Character.SPACE_SEPARATOR) {
1798                 return true;
1799             }
1800         }
1801         return false;
1802     }
1803 
1804     /**
1805      * Returns whether this character is a printable character.
1806      *
1807      * This does not support non-BMP characters and should not be used.
1808      *
1809      * @deprecated Use {@link #isGraphic(CharSequence)} instead.
1810      */
1811     @Deprecated
isGraphic(char c)1812     public static boolean isGraphic(char c) {
1813         int gc = Character.getType(c);
1814         return     gc != Character.CONTROL
1815                 && gc != Character.FORMAT
1816                 && gc != Character.SURROGATE
1817                 && gc != Character.UNASSIGNED
1818                 && gc != Character.LINE_SEPARATOR
1819                 && gc != Character.PARAGRAPH_SEPARATOR
1820                 && gc != Character.SPACE_SEPARATOR;
1821     }
1822 
1823     /**
1824      * Returns whether the given CharSequence contains only digits.
1825      */
isDigitsOnly(CharSequence str)1826     public static boolean isDigitsOnly(CharSequence str) {
1827         final int len = str.length();
1828         for (int cp, i = 0; i < len; i += Character.charCount(cp)) {
1829             cp = Character.codePointAt(str, i);
1830             if (!Character.isDigit(cp)) {
1831                 return false;
1832             }
1833         }
1834         return true;
1835     }
1836 
1837     /**
1838      * @hide
1839      */
isPrintableAscii(final char c)1840     public static boolean isPrintableAscii(final char c) {
1841         final int asciiFirst = 0x20;
1842         final int asciiLast = 0x7E;  // included
1843         return (asciiFirst <= c && c <= asciiLast) || c == '\r' || c == '\n';
1844     }
1845 
1846     /**
1847      * @hide
1848      */
1849     @UnsupportedAppUsage
isPrintableAsciiOnly(final CharSequence str)1850     public static boolean isPrintableAsciiOnly(final CharSequence str) {
1851         final int len = str.length();
1852         for (int i = 0; i < len; i++) {
1853             if (!isPrintableAscii(str.charAt(i))) {
1854                 return false;
1855             }
1856         }
1857         return true;
1858     }
1859 
1860     /**
1861      * Capitalization mode for {@link #getCapsMode}: capitalize all
1862      * characters.  This value is explicitly defined to be the same as
1863      * {@link InputType#TYPE_TEXT_FLAG_CAP_CHARACTERS}.
1864      */
1865     public static final int CAP_MODE_CHARACTERS
1866             = InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS;
1867 
1868     /**
1869      * Capitalization mode for {@link #getCapsMode}: capitalize the first
1870      * character of all words.  This value is explicitly defined to be the same as
1871      * {@link InputType#TYPE_TEXT_FLAG_CAP_WORDS}.
1872      */
1873     public static final int CAP_MODE_WORDS
1874             = InputType.TYPE_TEXT_FLAG_CAP_WORDS;
1875 
1876     /**
1877      * Capitalization mode for {@link #getCapsMode}: capitalize the first
1878      * character of each sentence.  This value is explicitly defined to be the same as
1879      * {@link InputType#TYPE_TEXT_FLAG_CAP_SENTENCES}.
1880      */
1881     public static final int CAP_MODE_SENTENCES
1882             = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
1883 
1884     /**
1885      * Determine what caps mode should be in effect at the current offset in
1886      * the text.  Only the mode bits set in <var>reqModes</var> will be
1887      * checked.  Note that the caps mode flags here are explicitly defined
1888      * to match those in {@link InputType}.
1889      *
1890      * @param cs The text that should be checked for caps modes.
1891      * @param off Location in the text at which to check.
1892      * @param reqModes The modes to be checked: may be any combination of
1893      * {@link #CAP_MODE_CHARACTERS}, {@link #CAP_MODE_WORDS}, and
1894      * {@link #CAP_MODE_SENTENCES}.
1895      *
1896      * @return Returns the actual capitalization modes that can be in effect
1897      * at the current position, which is any combination of
1898      * {@link #CAP_MODE_CHARACTERS}, {@link #CAP_MODE_WORDS}, and
1899      * {@link #CAP_MODE_SENTENCES}.
1900      */
getCapsMode(CharSequence cs, int off, int reqModes)1901     public static int getCapsMode(CharSequence cs, int off, int reqModes) {
1902         if (off < 0) {
1903             return 0;
1904         }
1905 
1906         int i;
1907         char c;
1908         int mode = 0;
1909 
1910         if ((reqModes&CAP_MODE_CHARACTERS) != 0) {
1911             mode |= CAP_MODE_CHARACTERS;
1912         }
1913         if ((reqModes&(CAP_MODE_WORDS|CAP_MODE_SENTENCES)) == 0) {
1914             return mode;
1915         }
1916 
1917         // Back over allowed opening punctuation.
1918 
1919         for (i = off; i > 0; i--) {
1920             c = cs.charAt(i - 1);
1921 
1922             if (c != '"' && c != '\'' &&
1923                 Character.getType(c) != Character.START_PUNCTUATION) {
1924                 break;
1925             }
1926         }
1927 
1928         // Start of paragraph, with optional whitespace.
1929 
1930         int j = i;
1931         while (j > 0 && ((c = cs.charAt(j - 1)) == ' ' || c == '\t')) {
1932             j--;
1933         }
1934         if (j == 0 || cs.charAt(j - 1) == '\n') {
1935             return mode | CAP_MODE_WORDS;
1936         }
1937 
1938         // Or start of word if we are that style.
1939 
1940         if ((reqModes&CAP_MODE_SENTENCES) == 0) {
1941             if (i != j) mode |= CAP_MODE_WORDS;
1942             return mode;
1943         }
1944 
1945         // There must be a space if not the start of paragraph.
1946 
1947         if (i == j) {
1948             return mode;
1949         }
1950 
1951         // Back over allowed closing punctuation.
1952 
1953         for (; j > 0; j--) {
1954             c = cs.charAt(j - 1);
1955 
1956             if (c != '"' && c != '\'' &&
1957                 Character.getType(c) != Character.END_PUNCTUATION) {
1958                 break;
1959             }
1960         }
1961 
1962         if (j > 0) {
1963             c = cs.charAt(j - 1);
1964 
1965             if (c == '.' || c == '?' || c == '!') {
1966                 // Do not capitalize if the word ends with a period but
1967                 // also contains a period, in which case it is an abbreviation.
1968 
1969                 if (c == '.') {
1970                     for (int k = j - 2; k >= 0; k--) {
1971                         c = cs.charAt(k);
1972 
1973                         if (c == '.') {
1974                             return mode;
1975                         }
1976 
1977                         if (!Character.isLetter(c)) {
1978                             break;
1979                         }
1980                     }
1981                 }
1982 
1983                 return mode | CAP_MODE_SENTENCES;
1984             }
1985         }
1986 
1987         return mode;
1988     }
1989 
1990     /**
1991      * Does a comma-delimited list 'delimitedString' contain a certain item?
1992      * (without allocating memory)
1993      *
1994      * @hide
1995      */
delimitedStringContains( String delimitedString, char delimiter, String item)1996     public static boolean delimitedStringContains(
1997             String delimitedString, char delimiter, String item) {
1998         if (isEmpty(delimitedString) || isEmpty(item)) {
1999             return false;
2000         }
2001         int pos = -1;
2002         int length = delimitedString.length();
2003         while ((pos = delimitedString.indexOf(item, pos + 1)) != -1) {
2004             if (pos > 0 && delimitedString.charAt(pos - 1) != delimiter) {
2005                 continue;
2006             }
2007             int expectedDelimiterPos = pos + item.length();
2008             if (expectedDelimiterPos == length) {
2009                 // Match at end of string.
2010                 return true;
2011             }
2012             if (delimitedString.charAt(expectedDelimiterPos) == delimiter) {
2013                 return true;
2014             }
2015         }
2016         return false;
2017     }
2018 
2019     /**
2020      * Removes empty spans from the <code>spans</code> array.
2021      *
2022      * When parsing a Spanned using {@link Spanned#nextSpanTransition(int, int, Class)}, empty spans
2023      * will (correctly) create span transitions, and calling getSpans on a slice of text bounded by
2024      * one of these transitions will (correctly) include the empty overlapping span.
2025      *
2026      * However, these empty spans should not be taken into account when layouting or rendering the
2027      * string and this method provides a way to filter getSpans' results accordingly.
2028      *
2029      * @param spans A list of spans retrieved using {@link Spanned#getSpans(int, int, Class)} from
2030      * the <code>spanned</code>
2031      * @param spanned The Spanned from which spans were extracted
2032      * @return A subset of spans where empty spans ({@link Spanned#getSpanStart(Object)}  ==
2033      * {@link Spanned#getSpanEnd(Object)} have been removed. The initial order is preserved
2034      * @hide
2035      */
2036     @SuppressWarnings("unchecked")
removeEmptySpans(T[] spans, Spanned spanned, Class<T> klass)2037     public static <T> T[] removeEmptySpans(T[] spans, Spanned spanned, Class<T> klass) {
2038         T[] copy = null;
2039         int count = 0;
2040 
2041         for (int i = 0; i < spans.length; i++) {
2042             final T span = spans[i];
2043             final int start = spanned.getSpanStart(span);
2044             final int end = spanned.getSpanEnd(span);
2045 
2046             if (start == end) {
2047                 if (copy == null) {
2048                     copy = (T[]) Array.newInstance(klass, spans.length - 1);
2049                     System.arraycopy(spans, 0, copy, 0, i);
2050                     count = i;
2051                 }
2052             } else {
2053                 if (copy != null) {
2054                     copy[count] = span;
2055                     count++;
2056                 }
2057             }
2058         }
2059 
2060         if (copy != null) {
2061             T[] result = (T[]) Array.newInstance(klass, count);
2062             System.arraycopy(copy, 0, result, 0, count);
2063             return result;
2064         } else {
2065             return spans;
2066         }
2067     }
2068 
2069     /**
2070      * Pack 2 int values into a long, useful as a return value for a range
2071      * @see #unpackRangeStartFromLong(long)
2072      * @see #unpackRangeEndFromLong(long)
2073      * @hide
2074      */
2075     @UnsupportedAppUsage
packRangeInLong(int start, int end)2076     public static long packRangeInLong(int start, int end) {
2077         return (((long) start) << 32) | end;
2078     }
2079 
2080     /**
2081      * Get the start value from a range packed in a long by {@link #packRangeInLong(int, int)}
2082      * @see #unpackRangeEndFromLong(long)
2083      * @see #packRangeInLong(int, int)
2084      * @hide
2085      */
2086     @UnsupportedAppUsage
unpackRangeStartFromLong(long range)2087     public static int unpackRangeStartFromLong(long range) {
2088         return (int) (range >>> 32);
2089     }
2090 
2091     /**
2092      * Get the end value from a range packed in a long by {@link #packRangeInLong(int, int)}
2093      * @see #unpackRangeStartFromLong(long)
2094      * @see #packRangeInLong(int, int)
2095      * @hide
2096      */
2097     @UnsupportedAppUsage
unpackRangeEndFromLong(long range)2098     public static int unpackRangeEndFromLong(long range) {
2099         return (int) (range & 0x00000000FFFFFFFFL);
2100     }
2101 
2102     /**
2103      * Return the layout direction for a given Locale
2104      *
2105      * @param locale the Locale for which we want the layout direction. Can be null.
2106      * @return the layout direction. This may be one of:
2107      * {@link android.view.View#LAYOUT_DIRECTION_LTR} or
2108      * {@link android.view.View#LAYOUT_DIRECTION_RTL}.
2109      *
2110      * Be careful: this code will need to be updated when vertical scripts will be supported
2111      */
getLayoutDirectionFromLocale(Locale locale)2112     public static int getLayoutDirectionFromLocale(Locale locale) {
2113         return ((locale != null && !locale.equals(Locale.ROOT)
2114                         && ULocale.forLocale(locale).isRightToLeft())
2115                 // If forcing into RTL layout mode, return RTL as default
2116                 || DisplayProperties.debug_force_rtl().orElse(false))
2117             ? View.LAYOUT_DIRECTION_RTL
2118             : View.LAYOUT_DIRECTION_LTR;
2119     }
2120 
2121     /**
2122      * Simple alternative to {@link String#format} which purposefully supports
2123      * only a small handful of substitutions to improve execution speed.
2124      * Benchmarking reveals this optimized alternative performs 6.5x faster for
2125      * a typical format string.
2126      * <p>
2127      * Below is a summary of the limited grammar supported by this method; if
2128      * you need advanced features, please continue using {@link String#format}.
2129      * <ul>
2130      * <li>{@code %b} for {@code boolean}
2131      * <li>{@code %c} for {@code char}
2132      * <li>{@code %d} for {@code int} or {@code long}
2133      * <li>{@code %f} for {@code float} or {@code double}
2134      * <li>{@code %s} for {@code String}
2135      * <li>{@code %x} for hex representation of {@code int} or {@code long}
2136      * <li>{@code %%} for literal {@code %}
2137      * <li>{@code %04d} style grammar to specify the argument width, such as
2138      * {@code %04d} to prefix an {@code int} with zeros or {@code %10b} to
2139      * prefix a {@code boolean} with spaces
2140      * </ul>
2141      *
2142      * @throws IllegalArgumentException if the format string or arguments don't
2143      *             match the supported grammar described above.
2144      * @hide
2145      */
formatSimple(@onNull String format, Object... args)2146     public static @NonNull String formatSimple(@NonNull String format, Object... args) {
2147         final StringBuilder sb = new StringBuilder(format);
2148         int j = 0;
2149         for (int i = 0; i < sb.length(); ) {
2150             if (sb.charAt(i) == '%') {
2151                 char code = sb.charAt(i + 1);
2152 
2153                 // Decode any argument width request
2154                 char prefixChar = '\0';
2155                 int prefixLen = 0;
2156                 int consume = 2;
2157                 while ('0' <= code && code <= '9') {
2158                     if (prefixChar == '\0') {
2159                         prefixChar = (code == '0') ? '0' : ' ';
2160                     }
2161                     prefixLen *= 10;
2162                     prefixLen += Character.digit(code, 10);
2163                     consume += 1;
2164                     code = sb.charAt(i + consume - 1);
2165                 }
2166 
2167                 final String repl;
2168                 switch (code) {
2169                     case 'b': {
2170                         if (j == args.length) {
2171                             throw new IllegalArgumentException("Too few arguments");
2172                         }
2173                         final Object arg = args[j++];
2174                         if (arg instanceof Boolean) {
2175                             repl = Boolean.toString((boolean) arg);
2176                         } else {
2177                             repl = Boolean.toString(arg != null);
2178                         }
2179                         break;
2180                     }
2181                     case 'c':
2182                     case 'd':
2183                     case 'f':
2184                     case 's': {
2185                         if (j == args.length) {
2186                             throw new IllegalArgumentException("Too few arguments");
2187                         }
2188                         final Object arg = args[j++];
2189                         repl = String.valueOf(arg);
2190                         break;
2191                     }
2192                     case 'x': {
2193                         if (j == args.length) {
2194                             throw new IllegalArgumentException("Too few arguments");
2195                         }
2196                         final Object arg = args[j++];
2197                         if (arg instanceof Integer) {
2198                             repl = Integer.toHexString((int) arg);
2199                         } else if (arg instanceof Long) {
2200                             repl = Long.toHexString((long) arg);
2201                         } else {
2202                             throw new IllegalArgumentException(
2203                                     "Unsupported hex type " + arg.getClass());
2204                         }
2205                         break;
2206                     }
2207                     case '%': {
2208                         repl = "%";
2209                         break;
2210                     }
2211                     default: {
2212                         throw new IllegalArgumentException("Unsupported format code " + code);
2213                     }
2214                 }
2215 
2216                 sb.replace(i, i + consume, repl);
2217 
2218                 // Apply any argument width request
2219                 final int prefixInsert = (prefixChar == '0' && repl.charAt(0) == '-') ? 1 : 0;
2220                 for (int k = repl.length(); k < prefixLen; k++) {
2221                     sb.insert(i + prefixInsert, prefixChar);
2222                 }
2223                 i += Math.max(repl.length(), prefixLen);
2224             } else {
2225                 i++;
2226             }
2227         }
2228         if (j != args.length) {
2229             throw new IllegalArgumentException("Too many arguments");
2230         }
2231         return sb.toString();
2232     }
2233 
2234     /**
2235      * Returns whether or not the specified spanned text has a style span.
2236      * @hide
2237      */
hasStyleSpan(@onNull Spanned spanned)2238     public static boolean hasStyleSpan(@NonNull Spanned spanned) {
2239         Preconditions.checkArgument(spanned != null);
2240         final Class<?>[] styleClasses = {
2241                 CharacterStyle.class, ParagraphStyle.class, UpdateAppearance.class};
2242         for (Class<?> clazz : styleClasses) {
2243             if (spanned.nextSpanTransition(-1, spanned.length(), clazz) < spanned.length()) {
2244                 return true;
2245             }
2246         }
2247         return false;
2248     }
2249 
2250     /**
2251      * If the {@code charSequence} is instance of {@link Spanned}, creates a new copy and
2252      * {@link NoCopySpan}'s are removed from the copy. Otherwise the given {@code charSequence} is
2253      * returned as it is.
2254      *
2255      * @hide
2256      */
2257     @Nullable
trimNoCopySpans(@ullable CharSequence charSequence)2258     public static CharSequence trimNoCopySpans(@Nullable CharSequence charSequence) {
2259         if (charSequence != null && charSequence instanceof Spanned) {
2260             // SpannableStringBuilder copy constructor trims NoCopySpans.
2261             return new SpannableStringBuilder(charSequence);
2262         }
2263         return charSequence;
2264     }
2265 
2266     /**
2267      * Prepends {@code start} and appends {@code end} to a given {@link StringBuilder}
2268      *
2269      * @hide
2270      */
wrap(StringBuilder builder, String start, String end)2271     public static void wrap(StringBuilder builder, String start, String end) {
2272         builder.insert(0, start);
2273         builder.append(end);
2274     }
2275 
2276     /**
2277      * Intent size limitations prevent sending over a megabyte of data. Limit
2278      * text length to 100K characters - 200KB.
2279      */
2280     private static final int PARCEL_SAFE_TEXT_LENGTH = 100000;
2281 
2282     /**
2283      * Trims the text to {@link #PARCEL_SAFE_TEXT_LENGTH} length. Returns the string as it is if
2284      * the length() is smaller than {@link #PARCEL_SAFE_TEXT_LENGTH}. Used for text that is parceled
2285      * into a {@link Parcelable}.
2286      *
2287      * @hide
2288      */
2289     @Nullable
trimToParcelableSize(@ullable T text)2290     public static <T extends CharSequence> T trimToParcelableSize(@Nullable T text) {
2291         return trimToSize(text, PARCEL_SAFE_TEXT_LENGTH);
2292     }
2293 
2294     /**
2295      * Trims the text to {@code size} length. Returns the string as it is if the length() is
2296      * smaller than {@code size}. If chars at {@code size-1} and {@code size} is a surrogate
2297      * pair, returns a CharSequence of length {@code size-1}.
2298      *
2299      * @param size length of the result, should be greater than 0
2300      *
2301      * @hide
2302      */
2303     @Nullable
trimToSize(@ullable T text, @IntRange(from = 1) int size)2304     public static <T extends CharSequence> T trimToSize(@Nullable T text,
2305             @IntRange(from = 1) int size) {
2306         Preconditions.checkArgument(size > 0);
2307         if (TextUtils.isEmpty(text) || text.length() <= size) return text;
2308         if (Character.isHighSurrogate(text.charAt(size - 1))
2309                 && Character.isLowSurrogate(text.charAt(size))) {
2310             size = size - 1;
2311         }
2312         return (T) text.subSequence(0, size);
2313     }
2314 
2315     /**
2316      * Trims the {@code text} to the first {@code size} characters and adds an ellipsis if the
2317      * resulting string is shorter than the input. This will result in an output string which is
2318      * longer than {@code size} for most inputs.
2319      *
2320      * @param size length of the result, should be greater than 0
2321      *
2322      * @hide
2323      */
2324     @Nullable
trimToLengthWithEllipsis(@ullable T text, @IntRange(from = 1) int size)2325     public static <T extends CharSequence> T trimToLengthWithEllipsis(@Nullable T text,
2326             @IntRange(from = 1) int size) {
2327         T trimmed = trimToSize(text, size);
2328         if (text != null && trimmed.length() < text.length()) {
2329             trimmed = (T) (trimmed.toString() + "...");
2330         }
2331         return trimmed;
2332     }
2333 
2334     /** @hide */
isNewline(int codePoint)2335     public static boolean isNewline(int codePoint) {
2336         int type = Character.getType(codePoint);
2337         return type == Character.PARAGRAPH_SEPARATOR || type == Character.LINE_SEPARATOR
2338                 || codePoint == LINE_FEED_CODE_POINT;
2339     }
2340 
2341     /** @hide */
isWhitespace(int codePoint)2342     public static boolean isWhitespace(int codePoint) {
2343         return Character.isWhitespace(codePoint) || codePoint == NBSP_CODE_POINT;
2344     }
2345 
2346     /** @hide */
isWhitespaceExceptNewline(int codePoint)2347     public static boolean isWhitespaceExceptNewline(int codePoint) {
2348         return isWhitespace(codePoint) && !isNewline(codePoint);
2349     }
2350 
2351     /** @hide */
isPunctuation(int codePoint)2352     public static boolean isPunctuation(int codePoint) {
2353         int type = Character.getType(codePoint);
2354         return type == Character.CONNECTOR_PUNCTUATION
2355                 || type == Character.DASH_PUNCTUATION
2356                 || type == Character.END_PUNCTUATION
2357                 || type == Character.FINAL_QUOTE_PUNCTUATION
2358                 || type == Character.INITIAL_QUOTE_PUNCTUATION
2359                 || type == Character.OTHER_PUNCTUATION
2360                 || type == Character.START_PUNCTUATION;
2361     }
2362 
2363     /** @hide */
2364     @Nullable
withoutPrefix(@ullable String prefix, @Nullable String str)2365     public static String withoutPrefix(@Nullable String prefix, @Nullable String str) {
2366         if (prefix == null || str == null) return str;
2367         return str.startsWith(prefix) ? str.substring(prefix.length()) : str;
2368     }
2369 
2370     /**
2371      * Remove html, remove bad characters, and truncate string.
2372      *
2373      * <p>This method is meant to remove common mistakes and nefarious formatting from strings that
2374      * were loaded from untrusted sources (such as other packages).
2375      *
2376      * <p>This method first {@link Html#fromHtml treats the string like HTML} and then ...
2377      * <ul>
2378      * <li>Removes new lines or truncates at first new line
2379      * <li>Trims the white-space off the end
2380      * <li>Truncates the string
2381      * </ul>
2382      * ... if specified.
2383      *
2384      * @param unclean The input string
2385      * @param maxCharactersToConsider The maximum number of characters of {@code unclean} to
2386      *                                consider from the input string. {@code 0} disables this
2387      *                                feature.
2388      * @param ellipsizeDip Assuming maximum length of the string (in dip), assuming font size 42.
2389      *                     This is roughly 50 characters for {@code ellipsizeDip == 1000}.<br />
2390      *                     Usually ellipsizing should be left to the view showing the string. If a
2391      *                     string is used as an input to another string, it might be useful to
2392      *                     control the length of the input string though. {@code 0} disables this
2393      *                     feature.
2394      * @param flags Flags controlling cleaning behavior (Can be {@link #SAFE_STRING_FLAG_TRIM},
2395      *              {@link #SAFE_STRING_FLAG_SINGLE_LINE},
2396      *              and {@link #SAFE_STRING_FLAG_FIRST_LINE})
2397      *
2398      * @return The cleaned string
2399      */
makeSafeForPresentation(@onNull String unclean, @IntRange(from = 0) int maxCharactersToConsider, @FloatRange(from = 0) float ellipsizeDip, @SafeStringFlags int flags)2400     public static @NonNull CharSequence makeSafeForPresentation(@NonNull String unclean,
2401             @IntRange(from = 0) int maxCharactersToConsider,
2402             @FloatRange(from = 0) float ellipsizeDip, @SafeStringFlags int flags) {
2403         boolean onlyKeepFirstLine = ((flags & SAFE_STRING_FLAG_FIRST_LINE) != 0);
2404         boolean forceSingleLine = ((flags & SAFE_STRING_FLAG_SINGLE_LINE) != 0);
2405         boolean trim = ((flags & SAFE_STRING_FLAG_TRIM) != 0);
2406 
2407         Preconditions.checkNotNull(unclean);
2408         Preconditions.checkArgumentNonnegative(maxCharactersToConsider);
2409         Preconditions.checkArgumentNonNegative(ellipsizeDip, "ellipsizeDip");
2410         Preconditions.checkFlagsArgument(flags, SAFE_STRING_FLAG_TRIM
2411                 | SAFE_STRING_FLAG_SINGLE_LINE | SAFE_STRING_FLAG_FIRST_LINE);
2412         Preconditions.checkArgument(!(onlyKeepFirstLine && forceSingleLine),
2413                 "Cannot set SAFE_STRING_FLAG_SINGLE_LINE and SAFE_STRING_FLAG_FIRST_LINE at the"
2414                         + "same time");
2415 
2416         String shortString;
2417         if (maxCharactersToConsider > 0) {
2418             shortString = unclean.substring(0, Math.min(unclean.length(), maxCharactersToConsider));
2419         } else {
2420             shortString = unclean;
2421         }
2422 
2423         // Treat string as HTML. This
2424         // - converts HTML symbols: e.g. &szlig; -> ß
2425         // - applies some HTML tags: e.g. <br> -> \n
2426         // - removes invalid characters such as \b
2427         // - removes html styling, such as <b>
2428         // - applies html formatting: e.g. a<p>b</p>c -> a\n\nb\n\nc
2429         // - replaces some html tags by "object replacement" markers: <img> -> \ufffc
2430         // - Removes leading white space
2431         // - Removes all trailing white space beside a single space
2432         // - Collapses double white space
2433         StringWithRemovedChars gettingCleaned = new StringWithRemovedChars(
2434                 Html.fromHtml(shortString).toString());
2435 
2436         int firstNonWhiteSpace = -1;
2437         int firstTrailingWhiteSpace = -1;
2438 
2439         // Remove new lines (if requested) and control characters.
2440         int uncleanLength = gettingCleaned.length();
2441         for (int offset = 0; offset < uncleanLength; ) {
2442             int codePoint = gettingCleaned.codePointAt(offset);
2443             int type = Character.getType(codePoint);
2444             int codePointLen = Character.charCount(codePoint);
2445             boolean isNewline = isNewline(codePoint);
2446 
2447             if (onlyKeepFirstLine && isNewline) {
2448                 gettingCleaned.removeAllCharAfter(offset);
2449                 break;
2450             } else if (forceSingleLine && isNewline) {
2451                 gettingCleaned.removeRange(offset, offset + codePointLen);
2452             } else if (type == Character.CONTROL && !isNewline) {
2453                 gettingCleaned.removeRange(offset, offset + codePointLen);
2454             } else if (trim && !isWhitespace(codePoint)) {
2455                 // This is only executed if the code point is not removed
2456                 if (firstNonWhiteSpace == -1) {
2457                     firstNonWhiteSpace = offset;
2458                 }
2459                 firstTrailingWhiteSpace = offset + codePointLen;
2460             }
2461 
2462             offset += codePointLen;
2463         }
2464 
2465         if (trim) {
2466             // Remove leading and trailing white space
2467             if (firstNonWhiteSpace == -1) {
2468                 // No non whitespace found, remove all
2469                 gettingCleaned.removeAllCharAfter(0);
2470             } else {
2471                 if (firstNonWhiteSpace > 0) {
2472                     gettingCleaned.removeAllCharBefore(firstNonWhiteSpace);
2473                 }
2474                 if (firstTrailingWhiteSpace < uncleanLength) {
2475                     gettingCleaned.removeAllCharAfter(firstTrailingWhiteSpace);
2476                 }
2477             }
2478         }
2479 
2480         if (ellipsizeDip == 0) {
2481             return gettingCleaned.toString();
2482         } else {
2483             // Truncate
2484             final TextPaint paint = new TextPaint();
2485             paint.setTextSize(42);
2486 
2487             return TextUtils.ellipsize(gettingCleaned.toString(), paint, ellipsizeDip,
2488                     TextUtils.TruncateAt.END);
2489         }
2490     }
2491 
2492     /**
2493      * A special string manipulation class. Just records removals and executes the when onString()
2494      * is called.
2495      */
2496     private static class StringWithRemovedChars {
2497         /** The original string */
2498         private final String mOriginal;
2499 
2500         /**
2501          * One bit per char in string. If bit is set, character needs to be removed. If whole
2502          * bit field is not initialized nothing needs to be removed.
2503          */
2504         private BitSet mRemovedChars;
2505 
StringWithRemovedChars(@onNull String original)2506         StringWithRemovedChars(@NonNull String original) {
2507             mOriginal = original;
2508         }
2509 
2510         /**
2511          * Mark all chars in a range {@code [firstRemoved - firstNonRemoved[} (not including
2512          * firstNonRemoved) as removed.
2513          */
removeRange(int firstRemoved, int firstNonRemoved)2514         void removeRange(int firstRemoved, int firstNonRemoved) {
2515             if (mRemovedChars == null) {
2516                 mRemovedChars = new BitSet(mOriginal.length());
2517             }
2518 
2519             mRemovedChars.set(firstRemoved, firstNonRemoved);
2520         }
2521 
2522         /**
2523          * Remove all characters before {@code firstNonRemoved}.
2524          */
removeAllCharBefore(int firstNonRemoved)2525         void removeAllCharBefore(int firstNonRemoved) {
2526             if (mRemovedChars == null) {
2527                 mRemovedChars = new BitSet(mOriginal.length());
2528             }
2529 
2530             mRemovedChars.set(0, firstNonRemoved);
2531         }
2532 
2533         /**
2534          * Remove all characters after and including {@code firstRemoved}.
2535          */
removeAllCharAfter(int firstRemoved)2536         void removeAllCharAfter(int firstRemoved) {
2537             if (mRemovedChars == null) {
2538                 mRemovedChars = new BitSet(mOriginal.length());
2539             }
2540 
2541             mRemovedChars.set(firstRemoved, mOriginal.length());
2542         }
2543 
2544         @Override
toString()2545         public String toString() {
2546             // Common case, no chars removed
2547             if (mRemovedChars == null) {
2548                 return mOriginal;
2549             }
2550 
2551             StringBuilder sb = new StringBuilder(mOriginal.length());
2552             for (int i = 0; i < mOriginal.length(); i++) {
2553                 if (!mRemovedChars.get(i)) {
2554                     sb.append(mOriginal.charAt(i));
2555                 }
2556             }
2557 
2558             return sb.toString();
2559         }
2560 
2561         /**
2562          * Return length or the original string
2563          */
length()2564         int length() {
2565             return mOriginal.length();
2566         }
2567 
2568         /**
2569          * Return codePoint of original string at a certain {@code offset}
2570          */
codePointAt(int offset)2571         int codePointAt(int offset) {
2572             return mOriginal.codePointAt(offset);
2573         }
2574     }
2575 
2576     private static Object sLock = new Object();
2577 
2578     private static char[] sTemp = null;
2579 
2580     private static String[] EMPTY_STRING_ARRAY = new String[]{};
2581 }
2582