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