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