• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2006 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.text;
18 
19 import android.content.res.Resources;
20 import android.os.Parcel;
21 import android.os.Parcelable;
22 import android.os.SystemProperties;
23 import android.provider.Settings;
24 import android.text.style.AbsoluteSizeSpan;
25 import android.text.style.AlignmentSpan;
26 import android.text.style.BackgroundColorSpan;
27 import android.text.style.BulletSpan;
28 import android.text.style.CharacterStyle;
29 import android.text.style.EasyEditSpan;
30 import android.text.style.ForegroundColorSpan;
31 import android.text.style.LeadingMarginSpan;
32 import android.text.style.LocaleSpan;
33 import android.text.style.MetricAffectingSpan;
34 import android.text.style.QuoteSpan;
35 import android.text.style.RelativeSizeSpan;
36 import android.text.style.ReplacementSpan;
37 import android.text.style.ScaleXSpan;
38 import android.text.style.SpellCheckSpan;
39 import android.text.style.StrikethroughSpan;
40 import android.text.style.StyleSpan;
41 import android.text.style.SubscriptSpan;
42 import android.text.style.SuggestionRangeSpan;
43 import android.text.style.SuggestionSpan;
44 import android.text.style.SuperscriptSpan;
45 import android.text.style.TextAppearanceSpan;
46 import android.text.style.TtsSpan;
47 import android.text.style.TypefaceSpan;
48 import android.text.style.URLSpan;
49 import android.text.style.UnderlineSpan;
50 import android.util.Log;
51 import android.util.Printer;
52 import android.view.View;
53 
54 import com.android.internal.R;
55 import com.android.internal.util.ArrayUtils;
56 
57 import libcore.icu.ICU;
58 
59 import java.lang.reflect.Array;
60 import java.util.Iterator;
61 import java.util.Locale;
62 import java.util.regex.Pattern;
63 
64 public class TextUtils {
65     private static final String TAG = "TextUtils";
66 
67 
TextUtils()68     private TextUtils() { /* cannot be instantiated */ }
69 
getChars(CharSequence s, int start, int end, char[] dest, int destoff)70     public static void getChars(CharSequence s, int start, int end,
71                                 char[] dest, int destoff) {
72         Class<? extends CharSequence> c = s.getClass();
73 
74         if (c == String.class)
75             ((String) s).getChars(start, end, dest, destoff);
76         else if (c == StringBuffer.class)
77             ((StringBuffer) s).getChars(start, end, dest, destoff);
78         else if (c == StringBuilder.class)
79             ((StringBuilder) s).getChars(start, end, dest, destoff);
80         else if (s instanceof GetChars)
81             ((GetChars) s).getChars(start, end, dest, destoff);
82         else {
83             for (int i = start; i < end; i++)
84                 dest[destoff++] = s.charAt(i);
85         }
86     }
87 
indexOf(CharSequence s, char ch)88     public static int indexOf(CharSequence s, char ch) {
89         return indexOf(s, ch, 0);
90     }
91 
indexOf(CharSequence s, char ch, int start)92     public static int indexOf(CharSequence s, char ch, int start) {
93         Class<? extends CharSequence> c = s.getClass();
94 
95         if (c == String.class)
96             return ((String) s).indexOf(ch, start);
97 
98         return indexOf(s, ch, start, s.length());
99     }
100 
indexOf(CharSequence s, char ch, int start, int end)101     public static int indexOf(CharSequence s, char ch, int start, int end) {
102         Class<? extends CharSequence> c = s.getClass();
103 
104         if (s instanceof GetChars || c == StringBuffer.class ||
105             c == StringBuilder.class || c == String.class) {
106             final int INDEX_INCREMENT = 500;
107             char[] temp = obtain(INDEX_INCREMENT);
108 
109             while (start < end) {
110                 int segend = start + INDEX_INCREMENT;
111                 if (segend > end)
112                     segend = end;
113 
114                 getChars(s, start, segend, temp, 0);
115 
116                 int count = segend - start;
117                 for (int i = 0; i < count; i++) {
118                     if (temp[i] == ch) {
119                         recycle(temp);
120                         return i + start;
121                     }
122                 }
123 
124                 start = segend;
125             }
126 
127             recycle(temp);
128             return -1;
129         }
130 
131         for (int i = start; i < end; i++)
132             if (s.charAt(i) == ch)
133                 return i;
134 
135         return -1;
136     }
137 
lastIndexOf(CharSequence s, char ch)138     public static int lastIndexOf(CharSequence s, char ch) {
139         return lastIndexOf(s, ch, s.length() - 1);
140     }
141 
lastIndexOf(CharSequence s, char ch, int last)142     public static int lastIndexOf(CharSequence s, char ch, int last) {
143         Class<? extends CharSequence> c = s.getClass();
144 
145         if (c == String.class)
146             return ((String) s).lastIndexOf(ch, last);
147 
148         return lastIndexOf(s, ch, 0, last);
149     }
150 
lastIndexOf(CharSequence s, char ch, int start, int last)151     public static int lastIndexOf(CharSequence s, char ch,
152                                   int start, int last) {
153         if (last < 0)
154             return -1;
155         if (last >= s.length())
156             last = s.length() - 1;
157 
158         int end = last + 1;
159 
160         Class<? extends CharSequence> c = s.getClass();
161 
162         if (s instanceof GetChars || c == StringBuffer.class ||
163             c == StringBuilder.class || c == String.class) {
164             final int INDEX_INCREMENT = 500;
165             char[] temp = obtain(INDEX_INCREMENT);
166 
167             while (start < end) {
168                 int segstart = end - INDEX_INCREMENT;
169                 if (segstart < start)
170                     segstart = start;
171 
172                 getChars(s, segstart, end, temp, 0);
173 
174                 int count = end - segstart;
175                 for (int i = count - 1; i >= 0; i--) {
176                     if (temp[i] == ch) {
177                         recycle(temp);
178                         return i + segstart;
179                     }
180                 }
181 
182                 end = segstart;
183             }
184 
185             recycle(temp);
186             return -1;
187         }
188 
189         for (int i = end - 1; i >= start; i--)
190             if (s.charAt(i) == ch)
191                 return i;
192 
193         return -1;
194     }
195 
indexOf(CharSequence s, CharSequence needle)196     public static int indexOf(CharSequence s, CharSequence needle) {
197         return indexOf(s, needle, 0, s.length());
198     }
199 
indexOf(CharSequence s, CharSequence needle, int start)200     public static int indexOf(CharSequence s, CharSequence needle, int start) {
201         return indexOf(s, needle, start, s.length());
202     }
203 
indexOf(CharSequence s, CharSequence needle, int start, int end)204     public static int indexOf(CharSequence s, CharSequence needle,
205                               int start, int end) {
206         int nlen = needle.length();
207         if (nlen == 0)
208             return start;
209 
210         char c = needle.charAt(0);
211 
212         for (;;) {
213             start = indexOf(s, c, start);
214             if (start > end - nlen) {
215                 break;
216             }
217 
218             if (start < 0) {
219                 return -1;
220             }
221 
222             if (regionMatches(s, start, needle, 0, nlen)) {
223                 return start;
224             }
225 
226             start++;
227         }
228         return -1;
229     }
230 
regionMatches(CharSequence one, int toffset, CharSequence two, int ooffset, int len)231     public static boolean regionMatches(CharSequence one, int toffset,
232                                         CharSequence two, int ooffset,
233                                         int len) {
234         int tempLen = 2 * len;
235         if (tempLen < len) {
236             // Integer overflow; len is unreasonably large
237             throw new IndexOutOfBoundsException();
238         }
239         char[] temp = obtain(tempLen);
240 
241         getChars(one, toffset, toffset + len, temp, 0);
242         getChars(two, ooffset, ooffset + len, temp, len);
243 
244         boolean match = true;
245         for (int i = 0; i < len; i++) {
246             if (temp[i] != temp[i + len]) {
247                 match = false;
248                 break;
249             }
250         }
251 
252         recycle(temp);
253         return match;
254     }
255 
256     /**
257      * Create a new String object containing the given range of characters
258      * from the source string.  This is different than simply calling
259      * {@link CharSequence#subSequence(int, int) CharSequence.subSequence}
260      * in that it does not preserve any style runs in the source sequence,
261      * allowing a more efficient implementation.
262      */
substring(CharSequence source, int start, int end)263     public static String substring(CharSequence source, int start, int end) {
264         if (source instanceof String)
265             return ((String) source).substring(start, end);
266         if (source instanceof StringBuilder)
267             return ((StringBuilder) source).substring(start, end);
268         if (source instanceof StringBuffer)
269             return ((StringBuffer) source).substring(start, end);
270 
271         char[] temp = obtain(end - start);
272         getChars(source, start, end, temp, 0);
273         String ret = new String(temp, 0, end - start);
274         recycle(temp);
275 
276         return ret;
277     }
278 
279     /**
280      * Returns list of multiple {@link CharSequence} joined into a single
281      * {@link CharSequence} separated by localized delimiter such as ", ".
282      *
283      * @hide
284      */
join(Iterable<CharSequence> list)285     public static CharSequence join(Iterable<CharSequence> list) {
286         final CharSequence delimiter = Resources.getSystem().getText(R.string.list_delimeter);
287         return join(delimiter, list);
288     }
289 
290     /**
291      * Returns a string containing the tokens joined by delimiters.
292      * @param tokens an array objects to be joined. Strings will be formed from
293      *     the objects by calling object.toString().
294      */
join(CharSequence delimiter, Object[] tokens)295     public static String join(CharSequence delimiter, Object[] tokens) {
296         StringBuilder sb = new StringBuilder();
297         boolean firstTime = true;
298         for (Object token: tokens) {
299             if (firstTime) {
300                 firstTime = false;
301             } else {
302                 sb.append(delimiter);
303             }
304             sb.append(token);
305         }
306         return sb.toString();
307     }
308 
309     /**
310      * Returns a string containing the tokens joined by delimiters.
311      * @param tokens an array objects to be joined. Strings will be formed from
312      *     the objects by calling object.toString().
313      */
join(CharSequence delimiter, Iterable tokens)314     public static String join(CharSequence delimiter, Iterable tokens) {
315         StringBuilder sb = new StringBuilder();
316         boolean firstTime = true;
317         for (Object token: tokens) {
318             if (firstTime) {
319                 firstTime = false;
320             } else {
321                 sb.append(delimiter);
322             }
323             sb.append(token);
324         }
325         return sb.toString();
326     }
327 
328     /**
329      * String.split() returns [''] when the string to be split is empty. This returns []. This does
330      * not remove any empty strings from the result. For example split("a,", ","  ) returns {"a", ""}.
331      *
332      * @param text the string to split
333      * @param expression the regular expression to match
334      * @return an array of strings. The array will be empty if text is empty
335      *
336      * @throws NullPointerException if expression or text is null
337      */
split(String text, String expression)338     public static String[] split(String text, String expression) {
339         if (text.length() == 0) {
340             return EMPTY_STRING_ARRAY;
341         } else {
342             return text.split(expression, -1);
343         }
344     }
345 
346     /**
347      * Splits a string on a pattern. String.split() returns [''] when the string to be
348      * split is empty. This returns []. This does not remove any empty strings from the result.
349      * @param text the string to split
350      * @param pattern the regular expression to match
351      * @return an array of strings. The array will be empty if text is empty
352      *
353      * @throws NullPointerException if expression or text is null
354      */
split(String text, Pattern pattern)355     public static String[] split(String text, Pattern pattern) {
356         if (text.length() == 0) {
357             return EMPTY_STRING_ARRAY;
358         } else {
359             return pattern.split(text, -1);
360         }
361     }
362 
363     /**
364      * An interface for splitting strings according to rules that are opaque to the user of this
365      * interface. This also has less overhead than split, which uses regular expressions and
366      * allocates an array to hold the results.
367      *
368      * <p>The most efficient way to use this class is:
369      *
370      * <pre>
371      * // Once
372      * TextUtils.StringSplitter splitter = new TextUtils.SimpleStringSplitter(delimiter);
373      *
374      * // Once per string to split
375      * splitter.setString(string);
376      * for (String s : splitter) {
377      *     ...
378      * }
379      * </pre>
380      */
381     public interface StringSplitter extends Iterable<String> {
setString(String string)382         public void setString(String string);
383     }
384 
385     /**
386      * A simple string splitter.
387      *
388      * <p>If the final character in the string to split is the delimiter then no empty string will
389      * be returned for the empty string after that delimeter. That is, splitting <tt>"a,b,"</tt> on
390      * comma will return <tt>"a", "b"</tt>, not <tt>"a", "b", ""</tt>.
391      */
392     public static class SimpleStringSplitter implements StringSplitter, Iterator<String> {
393         private String mString;
394         private char mDelimiter;
395         private int mPosition;
396         private int mLength;
397 
398         /**
399          * Initializes the splitter. setString may be called later.
400          * @param delimiter the delimeter on which to split
401          */
SimpleStringSplitter(char delimiter)402         public SimpleStringSplitter(char delimiter) {
403             mDelimiter = delimiter;
404         }
405 
406         /**
407          * Sets the string to split
408          * @param string the string to split
409          */
setString(String string)410         public void setString(String string) {
411             mString = string;
412             mPosition = 0;
413             mLength = mString.length();
414         }
415 
iterator()416         public Iterator<String> iterator() {
417             return this;
418         }
419 
hasNext()420         public boolean hasNext() {
421             return mPosition < mLength;
422         }
423 
next()424         public String next() {
425             int end = mString.indexOf(mDelimiter, mPosition);
426             if (end == -1) {
427                 end = mLength;
428             }
429             String nextString = mString.substring(mPosition, end);
430             mPosition = end + 1; // Skip the delimiter.
431             return nextString;
432         }
433 
remove()434         public void remove() {
435             throw new UnsupportedOperationException();
436         }
437     }
438 
stringOrSpannedString(CharSequence source)439     public static CharSequence stringOrSpannedString(CharSequence source) {
440         if (source == null)
441             return null;
442         if (source instanceof SpannedString)
443             return source;
444         if (source instanceof Spanned)
445             return new SpannedString(source);
446 
447         return source.toString();
448     }
449 
450     /**
451      * Returns true if the string is null or 0-length.
452      * @param str the string to be examined
453      * @return true if str is null or zero length
454      */
isEmpty(CharSequence str)455     public static boolean isEmpty(CharSequence str) {
456         if (str == null || str.length() == 0)
457             return true;
458         else
459             return false;
460     }
461 
462     /**
463      * Returns the length that the specified CharSequence would have if
464      * spaces and control characters were trimmed from the start and end,
465      * as by {@link String#trim}.
466      */
getTrimmedLength(CharSequence s)467     public static int getTrimmedLength(CharSequence s) {
468         int len = s.length();
469 
470         int start = 0;
471         while (start < len && s.charAt(start) <= ' ') {
472             start++;
473         }
474 
475         int end = len;
476         while (end > start && s.charAt(end - 1) <= ' ') {
477             end--;
478         }
479 
480         return end - start;
481     }
482 
483     /**
484      * Returns true if a and b are equal, including if they are both null.
485      * <p><i>Note: In platform versions 1.1 and earlier, this method only worked well if
486      * both the arguments were instances of String.</i></p>
487      * @param a first CharSequence to check
488      * @param b second CharSequence to check
489      * @return true if a and b are equal
490      */
equals(CharSequence a, CharSequence b)491     public static boolean equals(CharSequence a, CharSequence b) {
492         if (a == b) return true;
493         int length;
494         if (a != null && b != null && (length = a.length()) == b.length()) {
495             if (a instanceof String && b instanceof String) {
496                 return a.equals(b);
497             } else {
498                 for (int i = 0; i < length; i++) {
499                     if (a.charAt(i) != b.charAt(i)) return false;
500                 }
501                 return true;
502             }
503         }
504         return false;
505     }
506 
507     // XXX currently this only reverses chars, not spans
getReverse(CharSequence source, int start, int end)508     public static CharSequence getReverse(CharSequence source,
509                                           int start, int end) {
510         return new Reverser(source, start, end);
511     }
512 
513     private static class Reverser
514     implements CharSequence, GetChars
515     {
Reverser(CharSequence source, int start, int end)516         public Reverser(CharSequence source, int start, int end) {
517             mSource = source;
518             mStart = start;
519             mEnd = end;
520         }
521 
length()522         public int length() {
523             return mEnd - mStart;
524         }
525 
subSequence(int start, int end)526         public CharSequence subSequence(int start, int end) {
527             char[] buf = new char[end - start];
528 
529             getChars(start, end, buf, 0);
530             return new String(buf);
531         }
532 
533         @Override
toString()534         public String toString() {
535             return subSequence(0, length()).toString();
536         }
537 
charAt(int off)538         public char charAt(int off) {
539             return AndroidCharacter.getMirror(mSource.charAt(mEnd - 1 - off));
540         }
541 
getChars(int start, int end, char[] dest, int destoff)542         public void getChars(int start, int end, char[] dest, int destoff) {
543             TextUtils.getChars(mSource, start + mStart, end + mStart,
544                                dest, destoff);
545             AndroidCharacter.mirror(dest, 0, end - start);
546 
547             int len = end - start;
548             int n = (end - start) / 2;
549             for (int i = 0; i < n; i++) {
550                 char tmp = dest[destoff + i];
551 
552                 dest[destoff + i] = dest[destoff + len - i - 1];
553                 dest[destoff + len - i - 1] = tmp;
554             }
555         }
556 
557         private CharSequence mSource;
558         private int mStart;
559         private int mEnd;
560     }
561 
562     /** @hide */
563     public static final int ALIGNMENT_SPAN = 1;
564     /** @hide */
565     public static final int FIRST_SPAN = ALIGNMENT_SPAN;
566     /** @hide */
567     public static final int FOREGROUND_COLOR_SPAN = 2;
568     /** @hide */
569     public static final int RELATIVE_SIZE_SPAN = 3;
570     /** @hide */
571     public static final int SCALE_X_SPAN = 4;
572     /** @hide */
573     public static final int STRIKETHROUGH_SPAN = 5;
574     /** @hide */
575     public static final int UNDERLINE_SPAN = 6;
576     /** @hide */
577     public static final int STYLE_SPAN = 7;
578     /** @hide */
579     public static final int BULLET_SPAN = 8;
580     /** @hide */
581     public static final int QUOTE_SPAN = 9;
582     /** @hide */
583     public static final int LEADING_MARGIN_SPAN = 10;
584     /** @hide */
585     public static final int URL_SPAN = 11;
586     /** @hide */
587     public static final int BACKGROUND_COLOR_SPAN = 12;
588     /** @hide */
589     public static final int TYPEFACE_SPAN = 13;
590     /** @hide */
591     public static final int SUPERSCRIPT_SPAN = 14;
592     /** @hide */
593     public static final int SUBSCRIPT_SPAN = 15;
594     /** @hide */
595     public static final int ABSOLUTE_SIZE_SPAN = 16;
596     /** @hide */
597     public static final int TEXT_APPEARANCE_SPAN = 17;
598     /** @hide */
599     public static final int ANNOTATION = 18;
600     /** @hide */
601     public static final int SUGGESTION_SPAN = 19;
602     /** @hide */
603     public static final int SPELL_CHECK_SPAN = 20;
604     /** @hide */
605     public static final int SUGGESTION_RANGE_SPAN = 21;
606     /** @hide */
607     public static final int EASY_EDIT_SPAN = 22;
608     /** @hide */
609     public static final int LOCALE_SPAN = 23;
610     /** @hide */
611     public static final int TTS_SPAN = 24;
612     /** @hide */
613     public static final int LAST_SPAN = TTS_SPAN;
614 
615     /**
616      * Flatten a CharSequence and whatever styles can be copied across processes
617      * into the parcel.
618      */
writeToParcel(CharSequence cs, Parcel p, int parcelableFlags)619     public static void writeToParcel(CharSequence cs, Parcel p,
620             int parcelableFlags) {
621         if (cs instanceof Spanned) {
622             p.writeInt(0);
623             p.writeString(cs.toString());
624 
625             Spanned sp = (Spanned) cs;
626             Object[] os = sp.getSpans(0, cs.length(), Object.class);
627 
628             // note to people adding to this: check more specific types
629             // before more generic types.  also notice that it uses
630             // "if" instead of "else if" where there are interfaces
631             // so one object can be several.
632 
633             for (int i = 0; i < os.length; i++) {
634                 Object o = os[i];
635                 Object prop = os[i];
636 
637                 if (prop instanceof CharacterStyle) {
638                     prop = ((CharacterStyle) prop).getUnderlying();
639                 }
640 
641                 if (prop instanceof ParcelableSpan) {
642                     ParcelableSpan ps = (ParcelableSpan)prop;
643                     int spanTypeId = ps.getSpanTypeId();
644                     if (spanTypeId < FIRST_SPAN || spanTypeId > LAST_SPAN) {
645                         Log.e(TAG, "external class \"" + ps.getClass().getSimpleName()
646                                 + "\" is attempting to use the frameworks-only ParcelableSpan"
647                                 + " interface");
648                     } else {
649                         p.writeInt(spanTypeId);
650                         ps.writeToParcel(p, parcelableFlags);
651                         writeWhere(p, sp, o);
652                     }
653                 }
654             }
655 
656             p.writeInt(0);
657         } else {
658             p.writeInt(1);
659             if (cs != null) {
660                 p.writeString(cs.toString());
661             } else {
662                 p.writeString(null);
663             }
664         }
665     }
666 
writeWhere(Parcel p, Spanned sp, Object o)667     private static void writeWhere(Parcel p, Spanned sp, Object o) {
668         p.writeInt(sp.getSpanStart(o));
669         p.writeInt(sp.getSpanEnd(o));
670         p.writeInt(sp.getSpanFlags(o));
671     }
672 
673     public static final Parcelable.Creator<CharSequence> CHAR_SEQUENCE_CREATOR
674             = new Parcelable.Creator<CharSequence>() {
675         /**
676          * Read and return a new CharSequence, possibly with styles,
677          * from the parcel.
678          */
679         public CharSequence createFromParcel(Parcel p) {
680             int kind = p.readInt();
681 
682             String string = p.readString();
683             if (string == null) {
684                 return null;
685             }
686 
687             if (kind == 1) {
688                 return string;
689             }
690 
691             SpannableString sp = new SpannableString(string);
692 
693             while (true) {
694                 kind = p.readInt();
695 
696                 if (kind == 0)
697                     break;
698 
699                 switch (kind) {
700                 case ALIGNMENT_SPAN:
701                     readSpan(p, sp, new AlignmentSpan.Standard(p));
702                     break;
703 
704                 case FOREGROUND_COLOR_SPAN:
705                     readSpan(p, sp, new ForegroundColorSpan(p));
706                     break;
707 
708                 case RELATIVE_SIZE_SPAN:
709                     readSpan(p, sp, new RelativeSizeSpan(p));
710                     break;
711 
712                 case SCALE_X_SPAN:
713                     readSpan(p, sp, new ScaleXSpan(p));
714                     break;
715 
716                 case STRIKETHROUGH_SPAN:
717                     readSpan(p, sp, new StrikethroughSpan(p));
718                     break;
719 
720                 case UNDERLINE_SPAN:
721                     readSpan(p, sp, new UnderlineSpan(p));
722                     break;
723 
724                 case STYLE_SPAN:
725                     readSpan(p, sp, new StyleSpan(p));
726                     break;
727 
728                 case BULLET_SPAN:
729                     readSpan(p, sp, new BulletSpan(p));
730                     break;
731 
732                 case QUOTE_SPAN:
733                     readSpan(p, sp, new QuoteSpan(p));
734                     break;
735 
736                 case LEADING_MARGIN_SPAN:
737                     readSpan(p, sp, new LeadingMarginSpan.Standard(p));
738                 break;
739 
740                 case URL_SPAN:
741                     readSpan(p, sp, new URLSpan(p));
742                     break;
743 
744                 case BACKGROUND_COLOR_SPAN:
745                     readSpan(p, sp, new BackgroundColorSpan(p));
746                     break;
747 
748                 case TYPEFACE_SPAN:
749                     readSpan(p, sp, new TypefaceSpan(p));
750                     break;
751 
752                 case SUPERSCRIPT_SPAN:
753                     readSpan(p, sp, new SuperscriptSpan(p));
754                     break;
755 
756                 case SUBSCRIPT_SPAN:
757                     readSpan(p, sp, new SubscriptSpan(p));
758                     break;
759 
760                 case ABSOLUTE_SIZE_SPAN:
761                     readSpan(p, sp, new AbsoluteSizeSpan(p));
762                     break;
763 
764                 case TEXT_APPEARANCE_SPAN:
765                     readSpan(p, sp, new TextAppearanceSpan(p));
766                     break;
767 
768                 case ANNOTATION:
769                     readSpan(p, sp, new Annotation(p));
770                     break;
771 
772                 case SUGGESTION_SPAN:
773                     readSpan(p, sp, new SuggestionSpan(p));
774                     break;
775 
776                 case SPELL_CHECK_SPAN:
777                     readSpan(p, sp, new SpellCheckSpan(p));
778                     break;
779 
780                 case SUGGESTION_RANGE_SPAN:
781                     readSpan(p, sp, new SuggestionRangeSpan(p));
782                     break;
783 
784                 case EASY_EDIT_SPAN:
785                     readSpan(p, sp, new EasyEditSpan(p));
786                     break;
787 
788                 case LOCALE_SPAN:
789                     readSpan(p, sp, new LocaleSpan(p));
790                     break;
791 
792                 case TTS_SPAN:
793                     readSpan(p, sp, new TtsSpan(p));
794                     break;
795 
796                 default:
797                     throw new RuntimeException("bogus span encoding " + kind);
798                 }
799             }
800 
801             return sp;
802         }
803 
804         public CharSequence[] newArray(int size)
805         {
806             return new CharSequence[size];
807         }
808     };
809 
810     /**
811      * Debugging tool to print the spans in a CharSequence.  The output will
812      * be printed one span per line.  If the CharSequence is not a Spanned,
813      * then the entire string will be printed on a single line.
814      */
dumpSpans(CharSequence cs, Printer printer, String prefix)815     public static void dumpSpans(CharSequence cs, Printer printer, String prefix) {
816         if (cs instanceof Spanned) {
817             Spanned sp = (Spanned) cs;
818             Object[] os = sp.getSpans(0, cs.length(), Object.class);
819 
820             for (int i = 0; i < os.length; i++) {
821                 Object o = os[i];
822                 printer.println(prefix + cs.subSequence(sp.getSpanStart(o),
823                         sp.getSpanEnd(o)) + ": "
824                         + Integer.toHexString(System.identityHashCode(o))
825                         + " " + o.getClass().getCanonicalName()
826                          + " (" + sp.getSpanStart(o) + "-" + sp.getSpanEnd(o)
827                          + ") fl=#" + sp.getSpanFlags(o));
828             }
829         } else {
830             printer.println(prefix + cs + ": (no spans)");
831         }
832     }
833 
834     /**
835      * Return a new CharSequence in which each of the source strings is
836      * replaced by the corresponding element of the destinations.
837      */
replace(CharSequence template, String[] sources, CharSequence[] destinations)838     public static CharSequence replace(CharSequence template,
839                                        String[] sources,
840                                        CharSequence[] destinations) {
841         SpannableStringBuilder tb = new SpannableStringBuilder(template);
842 
843         for (int i = 0; i < sources.length; i++) {
844             int where = indexOf(tb, sources[i]);
845 
846             if (where >= 0)
847                 tb.setSpan(sources[i], where, where + sources[i].length(),
848                            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
849         }
850 
851         for (int i = 0; i < sources.length; i++) {
852             int start = tb.getSpanStart(sources[i]);
853             int end = tb.getSpanEnd(sources[i]);
854 
855             if (start >= 0) {
856                 tb.replace(start, end, destinations[i]);
857             }
858         }
859 
860         return tb;
861     }
862 
863     /**
864      * Replace instances of "^1", "^2", etc. in the
865      * <code>template</code> CharSequence with the corresponding
866      * <code>values</code>.  "^^" is used to produce a single caret in
867      * the output.  Only up to 9 replacement values are supported,
868      * "^10" will be produce the first replacement value followed by a
869      * '0'.
870      *
871      * @param template the input text containing "^1"-style
872      * placeholder values.  This object is not modified; a copy is
873      * returned.
874      *
875      * @param values CharSequences substituted into the template.  The
876      * first is substituted for "^1", the second for "^2", and so on.
877      *
878      * @return the new CharSequence produced by doing the replacement
879      *
880      * @throws IllegalArgumentException if the template requests a
881      * value that was not provided, or if more than 9 values are
882      * provided.
883      */
expandTemplate(CharSequence template, CharSequence... values)884     public static CharSequence expandTemplate(CharSequence template,
885                                               CharSequence... values) {
886         if (values.length > 9) {
887             throw new IllegalArgumentException("max of 9 values are supported");
888         }
889 
890         SpannableStringBuilder ssb = new SpannableStringBuilder(template);
891 
892         try {
893             int i = 0;
894             while (i < ssb.length()) {
895                 if (ssb.charAt(i) == '^') {
896                     char next = ssb.charAt(i+1);
897                     if (next == '^') {
898                         ssb.delete(i+1, i+2);
899                         ++i;
900                         continue;
901                     } else if (Character.isDigit(next)) {
902                         int which = Character.getNumericValue(next) - 1;
903                         if (which < 0) {
904                             throw new IllegalArgumentException(
905                                 "template requests value ^" + (which+1));
906                         }
907                         if (which >= values.length) {
908                             throw new IllegalArgumentException(
909                                 "template requests value ^" + (which+1) +
910                                 "; only " + values.length + " provided");
911                         }
912                         ssb.replace(i, i+2, values[which]);
913                         i += values[which].length();
914                         continue;
915                     }
916                 }
917                 ++i;
918             }
919         } catch (IndexOutOfBoundsException ignore) {
920             // happens when ^ is the last character in the string.
921         }
922         return ssb;
923     }
924 
getOffsetBefore(CharSequence text, int offset)925     public static int getOffsetBefore(CharSequence text, int offset) {
926         if (offset == 0)
927             return 0;
928         if (offset == 1)
929             return 0;
930 
931         char c = text.charAt(offset - 1);
932 
933         if (c >= '\uDC00' && c <= '\uDFFF') {
934             char c1 = text.charAt(offset - 2);
935 
936             if (c1 >= '\uD800' && c1 <= '\uDBFF')
937                 offset -= 2;
938             else
939                 offset -= 1;
940         } else {
941             offset -= 1;
942         }
943 
944         if (text instanceof Spanned) {
945             ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
946                                                        ReplacementSpan.class);
947 
948             for (int i = 0; i < spans.length; i++) {
949                 int start = ((Spanned) text).getSpanStart(spans[i]);
950                 int end = ((Spanned) text).getSpanEnd(spans[i]);
951 
952                 if (start < offset && end > offset)
953                     offset = start;
954             }
955         }
956 
957         return offset;
958     }
959 
getOffsetAfter(CharSequence text, int offset)960     public static int getOffsetAfter(CharSequence text, int offset) {
961         int len = text.length();
962 
963         if (offset == len)
964             return len;
965         if (offset == len - 1)
966             return len;
967 
968         char c = text.charAt(offset);
969 
970         if (c >= '\uD800' && c <= '\uDBFF') {
971             char c1 = text.charAt(offset + 1);
972 
973             if (c1 >= '\uDC00' && c1 <= '\uDFFF')
974                 offset += 2;
975             else
976                 offset += 1;
977         } else {
978             offset += 1;
979         }
980 
981         if (text instanceof Spanned) {
982             ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
983                                                        ReplacementSpan.class);
984 
985             for (int i = 0; i < spans.length; i++) {
986                 int start = ((Spanned) text).getSpanStart(spans[i]);
987                 int end = ((Spanned) text).getSpanEnd(spans[i]);
988 
989                 if (start < offset && end > offset)
990                     offset = end;
991             }
992         }
993 
994         return offset;
995     }
996 
readSpan(Parcel p, Spannable sp, Object o)997     private static void readSpan(Parcel p, Spannable sp, Object o) {
998         sp.setSpan(o, p.readInt(), p.readInt(), p.readInt());
999     }
1000 
1001     /**
1002      * Copies the spans from the region <code>start...end</code> in
1003      * <code>source</code> to the region
1004      * <code>destoff...destoff+end-start</code> in <code>dest</code>.
1005      * Spans in <code>source</code> that begin before <code>start</code>
1006      * or end after <code>end</code> but overlap this range are trimmed
1007      * as if they began at <code>start</code> or ended at <code>end</code>.
1008      *
1009      * @throws IndexOutOfBoundsException if any of the copied spans
1010      * are out of range in <code>dest</code>.
1011      */
copySpansFrom(Spanned source, int start, int end, Class kind, Spannable dest, int destoff)1012     public static void copySpansFrom(Spanned source, int start, int end,
1013                                      Class kind,
1014                                      Spannable dest, int destoff) {
1015         if (kind == null) {
1016             kind = Object.class;
1017         }
1018 
1019         Object[] spans = source.getSpans(start, end, kind);
1020 
1021         for (int i = 0; i < spans.length; i++) {
1022             int st = source.getSpanStart(spans[i]);
1023             int en = source.getSpanEnd(spans[i]);
1024             int fl = source.getSpanFlags(spans[i]);
1025 
1026             if (st < start)
1027                 st = start;
1028             if (en > end)
1029                 en = end;
1030 
1031             dest.setSpan(spans[i], st - start + destoff, en - start + destoff,
1032                          fl);
1033         }
1034     }
1035 
1036     public enum TruncateAt {
1037         START,
1038         MIDDLE,
1039         END,
1040         MARQUEE,
1041         /**
1042          * @hide
1043          */
1044         END_SMALL
1045     }
1046 
1047     public interface EllipsizeCallback {
1048         /**
1049          * This method is called to report that the specified region of
1050          * text was ellipsized away by a call to {@link #ellipsize}.
1051          */
ellipsized(int start, int end)1052         public void ellipsized(int start, int end);
1053     }
1054 
1055     /**
1056      * Returns the original text if it fits in the specified width
1057      * given the properties of the specified Paint,
1058      * or, if it does not fit, a truncated
1059      * copy with ellipsis character added at the specified edge or center.
1060      */
ellipsize(CharSequence text, TextPaint p, float avail, TruncateAt where)1061     public static CharSequence ellipsize(CharSequence text,
1062                                          TextPaint p,
1063                                          float avail, TruncateAt where) {
1064         return ellipsize(text, p, avail, where, false, null);
1065     }
1066 
1067     /**
1068      * Returns the original text if it fits in the specified width
1069      * given the properties of the specified Paint,
1070      * or, if it does not fit, a copy with ellipsis character added
1071      * at the specified edge or center.
1072      * If <code>preserveLength</code> is specified, the returned copy
1073      * will be padded with zero-width spaces to preserve the original
1074      * length and offsets instead of truncating.
1075      * If <code>callback</code> is non-null, it will be called to
1076      * report the start and end of the ellipsized range.  TextDirection
1077      * is determined by the first strong directional character.
1078      */
ellipsize(CharSequence text, TextPaint paint, float avail, TruncateAt where, boolean preserveLength, EllipsizeCallback callback)1079     public static CharSequence ellipsize(CharSequence text,
1080                                          TextPaint paint,
1081                                          float avail, TruncateAt where,
1082                                          boolean preserveLength,
1083                                          EllipsizeCallback callback) {
1084 
1085         final String ellipsis = (where == TruncateAt.END_SMALL) ?
1086                 Resources.getSystem().getString(R.string.ellipsis_two_dots) :
1087                 Resources.getSystem().getString(R.string.ellipsis);
1088 
1089         return ellipsize(text, paint, avail, where, preserveLength, callback,
1090                 TextDirectionHeuristics.FIRSTSTRONG_LTR,
1091                 ellipsis);
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);
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);
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 i=0; i<len; i++) {
1439             int gc = Character.getType(str.charAt(i));
1440             if (gc != Character.CONTROL
1441                     && gc != Character.FORMAT
1442                     && gc != Character.SURROGATE
1443                     && gc != Character.UNASSIGNED
1444                     && gc != Character.LINE_SEPARATOR
1445                     && gc != Character.PARAGRAPH_SEPARATOR
1446                     && gc != Character.SPACE_SEPARATOR) {
1447                 return true;
1448             }
1449         }
1450         return false;
1451     }
1452 
1453     /**
1454      * Returns whether this character is a printable character.
1455      */
isGraphic(char c)1456     public static boolean isGraphic(char c) {
1457         int gc = Character.getType(c);
1458         return     gc != Character.CONTROL
1459                 && gc != Character.FORMAT
1460                 && gc != Character.SURROGATE
1461                 && gc != Character.UNASSIGNED
1462                 && gc != Character.LINE_SEPARATOR
1463                 && gc != Character.PARAGRAPH_SEPARATOR
1464                 && gc != Character.SPACE_SEPARATOR;
1465     }
1466 
1467     /**
1468      * Returns whether the given CharSequence contains only digits.
1469      */
isDigitsOnly(CharSequence str)1470     public static boolean isDigitsOnly(CharSequence str) {
1471         final int len = str.length();
1472         for (int i = 0; i < len; i++) {
1473             if (!Character.isDigit(str.charAt(i))) {
1474                 return false;
1475             }
1476         }
1477         return true;
1478     }
1479 
1480     /**
1481      * @hide
1482      */
isPrintableAscii(final char c)1483     public static boolean isPrintableAscii(final char c) {
1484         final int asciiFirst = 0x20;
1485         final int asciiLast = 0x7E;  // included
1486         return (asciiFirst <= c && c <= asciiLast) || c == '\r' || c == '\n';
1487     }
1488 
1489     /**
1490      * @hide
1491      */
isPrintableAsciiOnly(final CharSequence str)1492     public static boolean isPrintableAsciiOnly(final CharSequence str) {
1493         final int len = str.length();
1494         for (int i = 0; i < len; i++) {
1495             if (!isPrintableAscii(str.charAt(i))) {
1496                 return false;
1497             }
1498         }
1499         return true;
1500     }
1501 
1502     /**
1503      * Capitalization mode for {@link #getCapsMode}: capitalize all
1504      * characters.  This value is explicitly defined to be the same as
1505      * {@link InputType#TYPE_TEXT_FLAG_CAP_CHARACTERS}.
1506      */
1507     public static final int CAP_MODE_CHARACTERS
1508             = InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS;
1509 
1510     /**
1511      * Capitalization mode for {@link #getCapsMode}: capitalize the first
1512      * character of all words.  This value is explicitly defined to be the same as
1513      * {@link InputType#TYPE_TEXT_FLAG_CAP_WORDS}.
1514      */
1515     public static final int CAP_MODE_WORDS
1516             = InputType.TYPE_TEXT_FLAG_CAP_WORDS;
1517 
1518     /**
1519      * Capitalization mode for {@link #getCapsMode}: capitalize the first
1520      * character of each sentence.  This value is explicitly defined to be the same as
1521      * {@link InputType#TYPE_TEXT_FLAG_CAP_SENTENCES}.
1522      */
1523     public static final int CAP_MODE_SENTENCES
1524             = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
1525 
1526     /**
1527      * Determine what caps mode should be in effect at the current offset in
1528      * the text.  Only the mode bits set in <var>reqModes</var> will be
1529      * checked.  Note that the caps mode flags here are explicitly defined
1530      * to match those in {@link InputType}.
1531      *
1532      * @param cs The text that should be checked for caps modes.
1533      * @param off Location in the text at which to check.
1534      * @param reqModes The modes to be checked: may be any combination of
1535      * {@link #CAP_MODE_CHARACTERS}, {@link #CAP_MODE_WORDS}, and
1536      * {@link #CAP_MODE_SENTENCES}.
1537      *
1538      * @return Returns the actual capitalization modes that can be in effect
1539      * at the current position, which is any combination of
1540      * {@link #CAP_MODE_CHARACTERS}, {@link #CAP_MODE_WORDS}, and
1541      * {@link #CAP_MODE_SENTENCES}.
1542      */
getCapsMode(CharSequence cs, int off, int reqModes)1543     public static int getCapsMode(CharSequence cs, int off, int reqModes) {
1544         if (off < 0) {
1545             return 0;
1546         }
1547 
1548         int i;
1549         char c;
1550         int mode = 0;
1551 
1552         if ((reqModes&CAP_MODE_CHARACTERS) != 0) {
1553             mode |= CAP_MODE_CHARACTERS;
1554         }
1555         if ((reqModes&(CAP_MODE_WORDS|CAP_MODE_SENTENCES)) == 0) {
1556             return mode;
1557         }
1558 
1559         // Back over allowed opening punctuation.
1560 
1561         for (i = off; i > 0; i--) {
1562             c = cs.charAt(i - 1);
1563 
1564             if (c != '"' && c != '\'' &&
1565                 Character.getType(c) != Character.START_PUNCTUATION) {
1566                 break;
1567             }
1568         }
1569 
1570         // Start of paragraph, with optional whitespace.
1571 
1572         int j = i;
1573         while (j > 0 && ((c = cs.charAt(j - 1)) == ' ' || c == '\t')) {
1574             j--;
1575         }
1576         if (j == 0 || cs.charAt(j - 1) == '\n') {
1577             return mode | CAP_MODE_WORDS;
1578         }
1579 
1580         // Or start of word if we are that style.
1581 
1582         if ((reqModes&CAP_MODE_SENTENCES) == 0) {
1583             if (i != j) mode |= CAP_MODE_WORDS;
1584             return mode;
1585         }
1586 
1587         // There must be a space if not the start of paragraph.
1588 
1589         if (i == j) {
1590             return mode;
1591         }
1592 
1593         // Back over allowed closing punctuation.
1594 
1595         for (; j > 0; j--) {
1596             c = cs.charAt(j - 1);
1597 
1598             if (c != '"' && c != '\'' &&
1599                 Character.getType(c) != Character.END_PUNCTUATION) {
1600                 break;
1601             }
1602         }
1603 
1604         if (j > 0) {
1605             c = cs.charAt(j - 1);
1606 
1607             if (c == '.' || c == '?' || c == '!') {
1608                 // Do not capitalize if the word ends with a period but
1609                 // also contains a period, in which case it is an abbreviation.
1610 
1611                 if (c == '.') {
1612                     for (int k = j - 2; k >= 0; k--) {
1613                         c = cs.charAt(k);
1614 
1615                         if (c == '.') {
1616                             return mode;
1617                         }
1618 
1619                         if (!Character.isLetter(c)) {
1620                             break;
1621                         }
1622                     }
1623                 }
1624 
1625                 return mode | CAP_MODE_SENTENCES;
1626             }
1627         }
1628 
1629         return mode;
1630     }
1631 
1632     /**
1633      * Does a comma-delimited list 'delimitedString' contain a certain item?
1634      * (without allocating memory)
1635      *
1636      * @hide
1637      */
delimitedStringContains( String delimitedString, char delimiter, String item)1638     public static boolean delimitedStringContains(
1639             String delimitedString, char delimiter, String item) {
1640         if (isEmpty(delimitedString) || isEmpty(item)) {
1641             return false;
1642         }
1643         int pos = -1;
1644         int length = delimitedString.length();
1645         while ((pos = delimitedString.indexOf(item, pos + 1)) != -1) {
1646             if (pos > 0 && delimitedString.charAt(pos - 1) != delimiter) {
1647                 continue;
1648             }
1649             int expectedDelimiterPos = pos + item.length();
1650             if (expectedDelimiterPos == length) {
1651                 // Match at end of string.
1652                 return true;
1653             }
1654             if (delimitedString.charAt(expectedDelimiterPos) == delimiter) {
1655                 return true;
1656             }
1657         }
1658         return false;
1659     }
1660 
1661     /**
1662      * Removes empty spans from the <code>spans</code> array.
1663      *
1664      * When parsing a Spanned using {@link Spanned#nextSpanTransition(int, int, Class)}, empty spans
1665      * will (correctly) create span transitions, and calling getSpans on a slice of text bounded by
1666      * one of these transitions will (correctly) include the empty overlapping span.
1667      *
1668      * However, these empty spans should not be taken into account when layouting or rendering the
1669      * string and this method provides a way to filter getSpans' results accordingly.
1670      *
1671      * @param spans A list of spans retrieved using {@link Spanned#getSpans(int, int, Class)} from
1672      * the <code>spanned</code>
1673      * @param spanned The Spanned from which spans were extracted
1674      * @return A subset of spans where empty spans ({@link Spanned#getSpanStart(Object)}  ==
1675      * {@link Spanned#getSpanEnd(Object)} have been removed. The initial order is preserved
1676      * @hide
1677      */
1678     @SuppressWarnings("unchecked")
removeEmptySpans(T[] spans, Spanned spanned, Class<T> klass)1679     public static <T> T[] removeEmptySpans(T[] spans, Spanned spanned, Class<T> klass) {
1680         T[] copy = null;
1681         int count = 0;
1682 
1683         for (int i = 0; i < spans.length; i++) {
1684             final T span = spans[i];
1685             final int start = spanned.getSpanStart(span);
1686             final int end = spanned.getSpanEnd(span);
1687 
1688             if (start == end) {
1689                 if (copy == null) {
1690                     copy = (T[]) Array.newInstance(klass, spans.length - 1);
1691                     System.arraycopy(spans, 0, copy, 0, i);
1692                     count = i;
1693                 }
1694             } else {
1695                 if (copy != null) {
1696                     copy[count] = span;
1697                     count++;
1698                 }
1699             }
1700         }
1701 
1702         if (copy != null) {
1703             T[] result = (T[]) Array.newInstance(klass, count);
1704             System.arraycopy(copy, 0, result, 0, count);
1705             return result;
1706         } else {
1707             return spans;
1708         }
1709     }
1710 
1711     /**
1712      * Pack 2 int values into a long, useful as a return value for a range
1713      * @see #unpackRangeStartFromLong(long)
1714      * @see #unpackRangeEndFromLong(long)
1715      * @hide
1716      */
packRangeInLong(int start, int end)1717     public static long packRangeInLong(int start, int end) {
1718         return (((long) start) << 32) | end;
1719     }
1720 
1721     /**
1722      * Get the start value from a range packed in a long by {@link #packRangeInLong(int, int)}
1723      * @see #unpackRangeEndFromLong(long)
1724      * @see #packRangeInLong(int, int)
1725      * @hide
1726      */
unpackRangeStartFromLong(long range)1727     public static int unpackRangeStartFromLong(long range) {
1728         return (int) (range >>> 32);
1729     }
1730 
1731     /**
1732      * Get the end value from a range packed in a long by {@link #packRangeInLong(int, int)}
1733      * @see #unpackRangeStartFromLong(long)
1734      * @see #packRangeInLong(int, int)
1735      * @hide
1736      */
unpackRangeEndFromLong(long range)1737     public static int unpackRangeEndFromLong(long range) {
1738         return (int) (range & 0x00000000FFFFFFFFL);
1739     }
1740 
1741     /**
1742      * Return the layout direction for a given Locale
1743      *
1744      * @param locale the Locale for which we want the layout direction. Can be null.
1745      * @return the layout direction. This may be one of:
1746      * {@link android.view.View#LAYOUT_DIRECTION_LTR} or
1747      * {@link android.view.View#LAYOUT_DIRECTION_RTL}.
1748      *
1749      * Be careful: this code will need to be updated when vertical scripts will be supported
1750      */
getLayoutDirectionFromLocale(Locale locale)1751     public static int getLayoutDirectionFromLocale(Locale locale) {
1752         if (locale != null && !locale.equals(Locale.ROOT)) {
1753             final String scriptSubtag = ICU.addLikelySubtags(locale).getScript();
1754             if (scriptSubtag == null) return getLayoutDirectionFromFirstChar(locale);
1755 
1756             if (scriptSubtag.equalsIgnoreCase(ARAB_SCRIPT_SUBTAG) ||
1757                     scriptSubtag.equalsIgnoreCase(HEBR_SCRIPT_SUBTAG)) {
1758                 return View.LAYOUT_DIRECTION_RTL;
1759             }
1760         }
1761         // If forcing into RTL layout mode, return RTL as default, else LTR
1762         return SystemProperties.getBoolean(Settings.Global.DEVELOPMENT_FORCE_RTL, false)
1763                 ? View.LAYOUT_DIRECTION_RTL
1764                 : View.LAYOUT_DIRECTION_LTR;
1765     }
1766 
1767     /**
1768      * Fallback algorithm to detect the locale direction. Rely on the fist char of the
1769      * localized locale name. This will not work if the localized locale name is in English
1770      * (this is the case for ICU 4.4 and "Urdu" script)
1771      *
1772      * @param locale
1773      * @return the layout direction. This may be one of:
1774      * {@link View#LAYOUT_DIRECTION_LTR} or
1775      * {@link View#LAYOUT_DIRECTION_RTL}.
1776      *
1777      * Be careful: this code will need to be updated when vertical scripts will be supported
1778      *
1779      * @hide
1780      */
getLayoutDirectionFromFirstChar(Locale locale)1781     private static int getLayoutDirectionFromFirstChar(Locale locale) {
1782         switch(Character.getDirectionality(locale.getDisplayName(locale).charAt(0))) {
1783             case Character.DIRECTIONALITY_RIGHT_TO_LEFT:
1784             case Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC:
1785                 return View.LAYOUT_DIRECTION_RTL;
1786 
1787             case Character.DIRECTIONALITY_LEFT_TO_RIGHT:
1788             default:
1789                 return View.LAYOUT_DIRECTION_LTR;
1790         }
1791     }
1792 
1793     private static Object sLock = new Object();
1794 
1795     private static char[] sTemp = null;
1796 
1797     private static String[] EMPTY_STRING_ARRAY = new String[]{};
1798 
1799     private static final char ZWNBS_CHAR = '\uFEFF';
1800 
1801     private static String ARAB_SCRIPT_SUBTAG = "Arab";
1802     private static String HEBR_SCRIPT_SUBTAG = "Hebr";
1803 }
1804