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