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