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