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