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