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