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