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     /* package */ static final char[] ELLIPSIS_NORMAL = { '\u2026' }; // this is "..."
68     private static final String ELLIPSIS_STRING = new String(ELLIPSIS_NORMAL);
69 
70     /* package */ static final char[] ELLIPSIS_TWO_DOTS = { '\u2025' }; // this is ".."
71     private static final String ELLIPSIS_TWO_DOTS_STRING = new String(ELLIPSIS_TWO_DOTS);
72 
TextUtils()73     private TextUtils() { /* cannot be instantiated */ }
74 
getChars(CharSequence s, int start, int end, char[] dest, int destoff)75     public static void getChars(CharSequence s, int start, int end,
76                                 char[] dest, int destoff) {
77         Class<? extends CharSequence> c = s.getClass();
78 
79         if (c == String.class)
80             ((String) s).getChars(start, end, dest, destoff);
81         else if (c == StringBuffer.class)
82             ((StringBuffer) s).getChars(start, end, dest, destoff);
83         else if (c == StringBuilder.class)
84             ((StringBuilder) s).getChars(start, end, dest, destoff);
85         else if (s instanceof GetChars)
86             ((GetChars) s).getChars(start, end, dest, destoff);
87         else {
88             for (int i = start; i < end; i++)
89                 dest[destoff++] = s.charAt(i);
90         }
91     }
92 
indexOf(CharSequence s, char ch)93     public static int indexOf(CharSequence s, char ch) {
94         return indexOf(s, ch, 0);
95     }
96 
indexOf(CharSequence s, char ch, int start)97     public static int indexOf(CharSequence s, char ch, int start) {
98         Class<? extends CharSequence> c = s.getClass();
99 
100         if (c == String.class)
101             return ((String) s).indexOf(ch, start);
102 
103         return indexOf(s, ch, start, s.length());
104     }
105 
indexOf(CharSequence s, char ch, int start, int end)106     public static int indexOf(CharSequence s, char ch, int start, int end) {
107         Class<? extends CharSequence> c = s.getClass();
108 
109         if (s instanceof GetChars || c == StringBuffer.class ||
110             c == StringBuilder.class || c == String.class) {
111             final int INDEX_INCREMENT = 500;
112             char[] temp = obtain(INDEX_INCREMENT);
113 
114             while (start < end) {
115                 int segend = start + INDEX_INCREMENT;
116                 if (segend > end)
117                     segend = end;
118 
119                 getChars(s, start, segend, temp, 0);
120 
121                 int count = segend - start;
122                 for (int i = 0; i < count; i++) {
123                     if (temp[i] == ch) {
124                         recycle(temp);
125                         return i + start;
126                     }
127                 }
128 
129                 start = segend;
130             }
131 
132             recycle(temp);
133             return -1;
134         }
135 
136         for (int i = start; i < end; i++)
137             if (s.charAt(i) == ch)
138                 return i;
139 
140         return -1;
141     }
142 
lastIndexOf(CharSequence s, char ch)143     public static int lastIndexOf(CharSequence s, char ch) {
144         return lastIndexOf(s, ch, s.length() - 1);
145     }
146 
lastIndexOf(CharSequence s, char ch, int last)147     public static int lastIndexOf(CharSequence s, char ch, int last) {
148         Class<? extends CharSequence> c = s.getClass();
149 
150         if (c == String.class)
151             return ((String) s).lastIndexOf(ch, last);
152 
153         return lastIndexOf(s, ch, 0, last);
154     }
155 
lastIndexOf(CharSequence s, char ch, int start, int last)156     public static int lastIndexOf(CharSequence s, char ch,
157                                   int start, int last) {
158         if (last < 0)
159             return -1;
160         if (last >= s.length())
161             last = s.length() - 1;
162 
163         int end = last + 1;
164 
165         Class<? extends CharSequence> c = s.getClass();
166 
167         if (s instanceof GetChars || c == StringBuffer.class ||
168             c == StringBuilder.class || c == String.class) {
169             final int INDEX_INCREMENT = 500;
170             char[] temp = obtain(INDEX_INCREMENT);
171 
172             while (start < end) {
173                 int segstart = end - INDEX_INCREMENT;
174                 if (segstart < start)
175                     segstart = start;
176 
177                 getChars(s, segstart, end, temp, 0);
178 
179                 int count = end - segstart;
180                 for (int i = count - 1; i >= 0; i--) {
181                     if (temp[i] == ch) {
182                         recycle(temp);
183                         return i + segstart;
184                     }
185                 }
186 
187                 end = segstart;
188             }
189 
190             recycle(temp);
191             return -1;
192         }
193 
194         for (int i = end - 1; i >= start; i--)
195             if (s.charAt(i) == ch)
196                 return i;
197 
198         return -1;
199     }
200 
indexOf(CharSequence s, CharSequence needle)201     public static int indexOf(CharSequence s, CharSequence needle) {
202         return indexOf(s, needle, 0, s.length());
203     }
204 
indexOf(CharSequence s, CharSequence needle, int start)205     public static int indexOf(CharSequence s, CharSequence needle, int start) {
206         return indexOf(s, needle, start, s.length());
207     }
208 
indexOf(CharSequence s, CharSequence needle, int start, int end)209     public static int indexOf(CharSequence s, CharSequence needle,
210                               int start, int end) {
211         int nlen = needle.length();
212         if (nlen == 0)
213             return start;
214 
215         char c = needle.charAt(0);
216 
217         for (;;) {
218             start = indexOf(s, c, start);
219             if (start > end - nlen) {
220                 break;
221             }
222 
223             if (start < 0) {
224                 return -1;
225             }
226 
227             if (regionMatches(s, start, needle, 0, nlen)) {
228                 return start;
229             }
230 
231             start++;
232         }
233         return -1;
234     }
235 
regionMatches(CharSequence one, int toffset, CharSequence two, int ooffset, int len)236     public static boolean regionMatches(CharSequence one, int toffset,
237                                         CharSequence two, int ooffset,
238                                         int len) {
239         int tempLen = 2 * len;
240         if (tempLen < len) {
241             // Integer overflow; len is unreasonably large
242             throw new IndexOutOfBoundsException();
243         }
244         char[] temp = obtain(tempLen);
245 
246         getChars(one, toffset, toffset + len, temp, 0);
247         getChars(two, ooffset, ooffset + len, temp, len);
248 
249         boolean match = true;
250         for (int i = 0; i < len; i++) {
251             if (temp[i] != temp[i + len]) {
252                 match = false;
253                 break;
254             }
255         }
256 
257         recycle(temp);
258         return match;
259     }
260 
261     /**
262      * Create a new String object containing the given range of characters
263      * from the source string.  This is different than simply calling
264      * {@link CharSequence#subSequence(int, int) CharSequence.subSequence}
265      * in that it does not preserve any style runs in the source sequence,
266      * allowing a more efficient implementation.
267      */
substring(CharSequence source, int start, int end)268     public static String substring(CharSequence source, int start, int end) {
269         if (source instanceof String)
270             return ((String) source).substring(start, end);
271         if (source instanceof StringBuilder)
272             return ((StringBuilder) source).substring(start, end);
273         if (source instanceof StringBuffer)
274             return ((StringBuffer) source).substring(start, end);
275 
276         char[] temp = obtain(end - start);
277         getChars(source, start, end, temp, 0);
278         String ret = new String(temp, 0, end - start);
279         recycle(temp);
280 
281         return ret;
282     }
283 
284     /**
285      * Returns list of multiple {@link CharSequence} joined into a single
286      * {@link CharSequence} separated by localized delimiter such as ", ".
287      *
288      * @hide
289      */
join(Iterable<CharSequence> list)290     public static CharSequence join(Iterable<CharSequence> list) {
291         final CharSequence delimiter = Resources.getSystem().getText(R.string.list_delimeter);
292         return join(delimiter, list);
293     }
294 
295     /**
296      * Returns a string containing the tokens joined by delimiters.
297      * @param tokens an array objects to be joined. Strings will be formed from
298      *     the objects by calling object.toString().
299      */
join(CharSequence delimiter, Object[] tokens)300     public static String join(CharSequence delimiter, Object[] tokens) {
301         StringBuilder sb = new StringBuilder();
302         boolean firstTime = true;
303         for (Object token: tokens) {
304             if (firstTime) {
305                 firstTime = false;
306             } else {
307                 sb.append(delimiter);
308             }
309             sb.append(token);
310         }
311         return sb.toString();
312     }
313 
314     /**
315      * Returns a string containing the tokens joined by delimiters.
316      * @param tokens an array objects to be joined. Strings will be formed from
317      *     the objects by calling object.toString().
318      */
join(CharSequence delimiter, Iterable tokens)319     public static String join(CharSequence delimiter, Iterable tokens) {
320         StringBuilder sb = new StringBuilder();
321         boolean firstTime = true;
322         for (Object token: tokens) {
323             if (firstTime) {
324                 firstTime = false;
325             } else {
326                 sb.append(delimiter);
327             }
328             sb.append(token);
329         }
330         return sb.toString();
331     }
332 
333     /**
334      * String.split() returns [''] when the string to be split is empty. This returns []. This does
335      * not remove any empty strings from the result. For example split("a,", ","  ) returns {"a", ""}.
336      *
337      * @param text the string to split
338      * @param expression the regular expression to match
339      * @return an array of strings. The array will be empty if text is empty
340      *
341      * @throws NullPointerException if expression or text is null
342      */
split(String text, String expression)343     public static String[] split(String text, String expression) {
344         if (text.length() == 0) {
345             return EMPTY_STRING_ARRAY;
346         } else {
347             return text.split(expression, -1);
348         }
349     }
350 
351     /**
352      * Splits a string on a pattern. String.split() returns [''] when the string to be
353      * split is empty. This returns []. This does not remove any empty strings from the result.
354      * @param text the string to split
355      * @param pattern the regular expression to match
356      * @return an array of strings. The array will be empty if text is empty
357      *
358      * @throws NullPointerException if expression or text is null
359      */
split(String text, Pattern pattern)360     public static String[] split(String text, Pattern pattern) {
361         if (text.length() == 0) {
362             return EMPTY_STRING_ARRAY;
363         } else {
364             return pattern.split(text, -1);
365         }
366     }
367 
368     /**
369      * An interface for splitting strings according to rules that are opaque to the user of this
370      * interface. This also has less overhead than split, which uses regular expressions and
371      * allocates an array to hold the results.
372      *
373      * <p>The most efficient way to use this class is:
374      *
375      * <pre>
376      * // Once
377      * TextUtils.StringSplitter splitter = new TextUtils.SimpleStringSplitter(delimiter);
378      *
379      * // Once per string to split
380      * splitter.setString(string);
381      * for (String s : splitter) {
382      *     ...
383      * }
384      * </pre>
385      */
386     public interface StringSplitter extends Iterable<String> {
setString(String string)387         public void setString(String string);
388     }
389 
390     /**
391      * A simple string splitter.
392      *
393      * <p>If the final character in the string to split is the delimiter then no empty string will
394      * be returned for the empty string after that delimeter. That is, splitting <tt>"a,b,"</tt> on
395      * comma will return <tt>"a", "b"</tt>, not <tt>"a", "b", ""</tt>.
396      */
397     public static class SimpleStringSplitter implements StringSplitter, Iterator<String> {
398         private String mString;
399         private char mDelimiter;
400         private int mPosition;
401         private int mLength;
402 
403         /**
404          * Initializes the splitter. setString may be called later.
405          * @param delimiter the delimeter on which to split
406          */
SimpleStringSplitter(char delimiter)407         public SimpleStringSplitter(char delimiter) {
408             mDelimiter = delimiter;
409         }
410 
411         /**
412          * Sets the string to split
413          * @param string the string to split
414          */
setString(String string)415         public void setString(String string) {
416             mString = string;
417             mPosition = 0;
418             mLength = mString.length();
419         }
420 
iterator()421         public Iterator<String> iterator() {
422             return this;
423         }
424 
hasNext()425         public boolean hasNext() {
426             return mPosition < mLength;
427         }
428 
next()429         public String next() {
430             int end = mString.indexOf(mDelimiter, mPosition);
431             if (end == -1) {
432                 end = mLength;
433             }
434             String nextString = mString.substring(mPosition, end);
435             mPosition = end + 1; // Skip the delimiter.
436             return nextString;
437         }
438 
remove()439         public void remove() {
440             throw new UnsupportedOperationException();
441         }
442     }
443 
stringOrSpannedString(CharSequence source)444     public static CharSequence stringOrSpannedString(CharSequence source) {
445         if (source == null)
446             return null;
447         if (source instanceof SpannedString)
448             return source;
449         if (source instanceof Spanned)
450             return new SpannedString(source);
451 
452         return source.toString();
453     }
454 
455     /**
456      * Returns true if the string is null or 0-length.
457      * @param str the string to be examined
458      * @return true if str is null or zero length
459      */
isEmpty(CharSequence str)460     public static boolean isEmpty(CharSequence str) {
461         if (str == null || str.length() == 0)
462             return true;
463         else
464             return false;
465     }
466 
467     /**
468      * Returns the length that the specified CharSequence would have if
469      * spaces and control characters were trimmed from the start and end,
470      * as by {@link String#trim}.
471      */
getTrimmedLength(CharSequence s)472     public static int getTrimmedLength(CharSequence s) {
473         int len = s.length();
474 
475         int start = 0;
476         while (start < len && s.charAt(start) <= ' ') {
477             start++;
478         }
479 
480         int end = len;
481         while (end > start && s.charAt(end - 1) <= ' ') {
482             end--;
483         }
484 
485         return end - start;
486     }
487 
488     /**
489      * Returns true if a and b are equal, including if they are both null.
490      * <p><i>Note: In platform versions 1.1 and earlier, this method only worked well if
491      * both the arguments were instances of String.</i></p>
492      * @param a first CharSequence to check
493      * @param b second CharSequence to check
494      * @return true if a and b are equal
495      */
equals(CharSequence a, CharSequence b)496     public static boolean equals(CharSequence a, CharSequence b) {
497         if (a == b) return true;
498         int length;
499         if (a != null && b != null && (length = a.length()) == b.length()) {
500             if (a instanceof String && b instanceof String) {
501                 return a.equals(b);
502             } else {
503                 for (int i = 0; i < length; i++) {
504                     if (a.charAt(i) != b.charAt(i)) return false;
505                 }
506                 return true;
507             }
508         }
509         return false;
510     }
511 
512     // XXX currently this only reverses chars, not spans
getReverse(CharSequence source, int start, int end)513     public static CharSequence getReverse(CharSequence source,
514                                           int start, int end) {
515         return new Reverser(source, start, end);
516     }
517 
518     private static class Reverser
519     implements CharSequence, GetChars
520     {
Reverser(CharSequence source, int start, int end)521         public Reverser(CharSequence source, int start, int end) {
522             mSource = source;
523             mStart = start;
524             mEnd = end;
525         }
526 
length()527         public int length() {
528             return mEnd - mStart;
529         }
530 
subSequence(int start, int end)531         public CharSequence subSequence(int start, int end) {
532             char[] buf = new char[end - start];
533 
534             getChars(start, end, buf, 0);
535             return new String(buf);
536         }
537 
538         @Override
toString()539         public String toString() {
540             return subSequence(0, length()).toString();
541         }
542 
charAt(int off)543         public char charAt(int off) {
544             return AndroidCharacter.getMirror(mSource.charAt(mEnd - 1 - off));
545         }
546 
getChars(int start, int end, char[] dest, int destoff)547         public void getChars(int start, int end, char[] dest, int destoff) {
548             TextUtils.getChars(mSource, start + mStart, end + mStart,
549                                dest, destoff);
550             AndroidCharacter.mirror(dest, 0, end - start);
551 
552             int len = end - start;
553             int n = (end - start) / 2;
554             for (int i = 0; i < n; i++) {
555                 char tmp = dest[destoff + i];
556 
557                 dest[destoff + i] = dest[destoff + len - i - 1];
558                 dest[destoff + len - i - 1] = tmp;
559             }
560         }
561 
562         private CharSequence mSource;
563         private int mStart;
564         private int mEnd;
565     }
566 
567     /** @hide */
568     public static final int ALIGNMENT_SPAN = 1;
569     /** @hide */
570     public static final int FIRST_SPAN = ALIGNMENT_SPAN;
571     /** @hide */
572     public static final int FOREGROUND_COLOR_SPAN = 2;
573     /** @hide */
574     public static final int RELATIVE_SIZE_SPAN = 3;
575     /** @hide */
576     public static final int SCALE_X_SPAN = 4;
577     /** @hide */
578     public static final int STRIKETHROUGH_SPAN = 5;
579     /** @hide */
580     public static final int UNDERLINE_SPAN = 6;
581     /** @hide */
582     public static final int STYLE_SPAN = 7;
583     /** @hide */
584     public static final int BULLET_SPAN = 8;
585     /** @hide */
586     public static final int QUOTE_SPAN = 9;
587     /** @hide */
588     public static final int LEADING_MARGIN_SPAN = 10;
589     /** @hide */
590     public static final int URL_SPAN = 11;
591     /** @hide */
592     public static final int BACKGROUND_COLOR_SPAN = 12;
593     /** @hide */
594     public static final int TYPEFACE_SPAN = 13;
595     /** @hide */
596     public static final int SUPERSCRIPT_SPAN = 14;
597     /** @hide */
598     public static final int SUBSCRIPT_SPAN = 15;
599     /** @hide */
600     public static final int ABSOLUTE_SIZE_SPAN = 16;
601     /** @hide */
602     public static final int TEXT_APPEARANCE_SPAN = 17;
603     /** @hide */
604     public static final int ANNOTATION = 18;
605     /** @hide */
606     public static final int SUGGESTION_SPAN = 19;
607     /** @hide */
608     public static final int SPELL_CHECK_SPAN = 20;
609     /** @hide */
610     public static final int SUGGESTION_RANGE_SPAN = 21;
611     /** @hide */
612     public static final int EASY_EDIT_SPAN = 22;
613     /** @hide */
614     public static final int LOCALE_SPAN = 23;
615     /** @hide */
616     public static final int TTS_SPAN = 24;
617     /** @hide */
618     public static final int LAST_SPAN = TTS_SPAN;
619 
620     /**
621      * Flatten a CharSequence and whatever styles can be copied across processes
622      * into the parcel.
623      */
writeToParcel(CharSequence cs, Parcel p, int parcelableFlags)624     public static void writeToParcel(CharSequence cs, Parcel p,
625             int parcelableFlags) {
626         if (cs instanceof Spanned) {
627             p.writeInt(0);
628             p.writeString(cs.toString());
629 
630             Spanned sp = (Spanned) cs;
631             Object[] os = sp.getSpans(0, cs.length(), Object.class);
632 
633             // note to people adding to this: check more specific types
634             // before more generic types.  also notice that it uses
635             // "if" instead of "else if" where there are interfaces
636             // so one object can be several.
637 
638             for (int i = 0; i < os.length; i++) {
639                 Object o = os[i];
640                 Object prop = os[i];
641 
642                 if (prop instanceof CharacterStyle) {
643                     prop = ((CharacterStyle) prop).getUnderlying();
644                 }
645 
646                 if (prop instanceof ParcelableSpan) {
647                     ParcelableSpan ps = (ParcelableSpan)prop;
648                     int spanTypeId = ps.getSpanTypeId();
649                     if (spanTypeId < FIRST_SPAN || spanTypeId > LAST_SPAN) {
650                         Log.e(TAG, "external class \"" + ps.getClass().getSimpleName()
651                                 + "\" is attempting to use the frameworks-only ParcelableSpan"
652                                 + " interface");
653                     } else {
654                         p.writeInt(spanTypeId);
655                         ps.writeToParcel(p, parcelableFlags);
656                         writeWhere(p, sp, o);
657                     }
658                 }
659             }
660 
661             p.writeInt(0);
662         } else {
663             p.writeInt(1);
664             if (cs != null) {
665                 p.writeString(cs.toString());
666             } else {
667                 p.writeString(null);
668             }
669         }
670     }
671 
writeWhere(Parcel p, Spanned sp, Object o)672     private static void writeWhere(Parcel p, Spanned sp, Object o) {
673         p.writeInt(sp.getSpanStart(o));
674         p.writeInt(sp.getSpanEnd(o));
675         p.writeInt(sp.getSpanFlags(o));
676     }
677 
678     public static final Parcelable.Creator<CharSequence> CHAR_SEQUENCE_CREATOR
679             = new Parcelable.Creator<CharSequence>() {
680         /**
681          * Read and return a new CharSequence, possibly with styles,
682          * from the parcel.
683          */
684         public CharSequence createFromParcel(Parcel p) {
685             int kind = p.readInt();
686 
687             String string = p.readString();
688             if (string == null) {
689                 return null;
690             }
691 
692             if (kind == 1) {
693                 return string;
694             }
695 
696             SpannableString sp = new SpannableString(string);
697 
698             while (true) {
699                 kind = p.readInt();
700 
701                 if (kind == 0)
702                     break;
703 
704                 switch (kind) {
705                 case ALIGNMENT_SPAN:
706                     readSpan(p, sp, new AlignmentSpan.Standard(p));
707                     break;
708 
709                 case FOREGROUND_COLOR_SPAN:
710                     readSpan(p, sp, new ForegroundColorSpan(p));
711                     break;
712 
713                 case RELATIVE_SIZE_SPAN:
714                     readSpan(p, sp, new RelativeSizeSpan(p));
715                     break;
716 
717                 case SCALE_X_SPAN:
718                     readSpan(p, sp, new ScaleXSpan(p));
719                     break;
720 
721                 case STRIKETHROUGH_SPAN:
722                     readSpan(p, sp, new StrikethroughSpan(p));
723                     break;
724 
725                 case UNDERLINE_SPAN:
726                     readSpan(p, sp, new UnderlineSpan(p));
727                     break;
728 
729                 case STYLE_SPAN:
730                     readSpan(p, sp, new StyleSpan(p));
731                     break;
732 
733                 case BULLET_SPAN:
734                     readSpan(p, sp, new BulletSpan(p));
735                     break;
736 
737                 case QUOTE_SPAN:
738                     readSpan(p, sp, new QuoteSpan(p));
739                     break;
740 
741                 case LEADING_MARGIN_SPAN:
742                     readSpan(p, sp, new LeadingMarginSpan.Standard(p));
743                 break;
744 
745                 case URL_SPAN:
746                     readSpan(p, sp, new URLSpan(p));
747                     break;
748 
749                 case BACKGROUND_COLOR_SPAN:
750                     readSpan(p, sp, new BackgroundColorSpan(p));
751                     break;
752 
753                 case TYPEFACE_SPAN:
754                     readSpan(p, sp, new TypefaceSpan(p));
755                     break;
756 
757                 case SUPERSCRIPT_SPAN:
758                     readSpan(p, sp, new SuperscriptSpan(p));
759                     break;
760 
761                 case SUBSCRIPT_SPAN:
762                     readSpan(p, sp, new SubscriptSpan(p));
763                     break;
764 
765                 case ABSOLUTE_SIZE_SPAN:
766                     readSpan(p, sp, new AbsoluteSizeSpan(p));
767                     break;
768 
769                 case TEXT_APPEARANCE_SPAN:
770                     readSpan(p, sp, new TextAppearanceSpan(p));
771                     break;
772 
773                 case ANNOTATION:
774                     readSpan(p, sp, new Annotation(p));
775                     break;
776 
777                 case SUGGESTION_SPAN:
778                     readSpan(p, sp, new SuggestionSpan(p));
779                     break;
780 
781                 case SPELL_CHECK_SPAN:
782                     readSpan(p, sp, new SpellCheckSpan(p));
783                     break;
784 
785                 case SUGGESTION_RANGE_SPAN:
786                     readSpan(p, sp, new SuggestionRangeSpan(p));
787                     break;
788 
789                 case EASY_EDIT_SPAN:
790                     readSpan(p, sp, new EasyEditSpan(p));
791                     break;
792 
793                 case LOCALE_SPAN:
794                     readSpan(p, sp, new LocaleSpan(p));
795                     break;
796 
797                 case TTS_SPAN:
798                     readSpan(p, sp, new TtsSpan(p));
799                     break;
800 
801                 default:
802                     throw new RuntimeException("bogus span encoding " + kind);
803                 }
804             }
805 
806             return sp;
807         }
808 
809         public CharSequence[] newArray(int size)
810         {
811             return new CharSequence[size];
812         }
813     };
814 
815     /**
816      * Debugging tool to print the spans in a CharSequence.  The output will
817      * be printed one span per line.  If the CharSequence is not a Spanned,
818      * then the entire string will be printed on a single line.
819      */
dumpSpans(CharSequence cs, Printer printer, String prefix)820     public static void dumpSpans(CharSequence cs, Printer printer, String prefix) {
821         if (cs instanceof Spanned) {
822             Spanned sp = (Spanned) cs;
823             Object[] os = sp.getSpans(0, cs.length(), Object.class);
824 
825             for (int i = 0; i < os.length; i++) {
826                 Object o = os[i];
827                 printer.println(prefix + cs.subSequence(sp.getSpanStart(o),
828                         sp.getSpanEnd(o)) + ": "
829                         + Integer.toHexString(System.identityHashCode(o))
830                         + " " + o.getClass().getCanonicalName()
831                          + " (" + sp.getSpanStart(o) + "-" + sp.getSpanEnd(o)
832                          + ") fl=#" + sp.getSpanFlags(o));
833             }
834         } else {
835             printer.println(prefix + cs + ": (no spans)");
836         }
837     }
838 
839     /**
840      * Return a new CharSequence in which each of the source strings is
841      * replaced by the corresponding element of the destinations.
842      */
replace(CharSequence template, String[] sources, CharSequence[] destinations)843     public static CharSequence replace(CharSequence template,
844                                        String[] sources,
845                                        CharSequence[] destinations) {
846         SpannableStringBuilder tb = new SpannableStringBuilder(template);
847 
848         for (int i = 0; i < sources.length; i++) {
849             int where = indexOf(tb, sources[i]);
850 
851             if (where >= 0)
852                 tb.setSpan(sources[i], where, where + sources[i].length(),
853                            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
854         }
855 
856         for (int i = 0; i < sources.length; i++) {
857             int start = tb.getSpanStart(sources[i]);
858             int end = tb.getSpanEnd(sources[i]);
859 
860             if (start >= 0) {
861                 tb.replace(start, end, destinations[i]);
862             }
863         }
864 
865         return tb;
866     }
867 
868     /**
869      * Replace instances of "^1", "^2", etc. in the
870      * <code>template</code> CharSequence with the corresponding
871      * <code>values</code>.  "^^" is used to produce a single caret in
872      * the output.  Only up to 9 replacement values are supported,
873      * "^10" will be produce the first replacement value followed by a
874      * '0'.
875      *
876      * @param template the input text containing "^1"-style
877      * placeholder values.  This object is not modified; a copy is
878      * returned.
879      *
880      * @param values CharSequences substituted into the template.  The
881      * first is substituted for "^1", the second for "^2", and so on.
882      *
883      * @return the new CharSequence produced by doing the replacement
884      *
885      * @throws IllegalArgumentException if the template requests a
886      * value that was not provided, or if more than 9 values are
887      * provided.
888      */
expandTemplate(CharSequence template, CharSequence... values)889     public static CharSequence expandTemplate(CharSequence template,
890                                               CharSequence... values) {
891         if (values.length > 9) {
892             throw new IllegalArgumentException("max of 9 values are supported");
893         }
894 
895         SpannableStringBuilder ssb = new SpannableStringBuilder(template);
896 
897         try {
898             int i = 0;
899             while (i < ssb.length()) {
900                 if (ssb.charAt(i) == '^') {
901                     char next = ssb.charAt(i+1);
902                     if (next == '^') {
903                         ssb.delete(i+1, i+2);
904                         ++i;
905                         continue;
906                     } else if (Character.isDigit(next)) {
907                         int which = Character.getNumericValue(next) - 1;
908                         if (which < 0) {
909                             throw new IllegalArgumentException(
910                                 "template requests value ^" + (which+1));
911                         }
912                         if (which >= values.length) {
913                             throw new IllegalArgumentException(
914                                 "template requests value ^" + (which+1) +
915                                 "; only " + values.length + " provided");
916                         }
917                         ssb.replace(i, i+2, values[which]);
918                         i += values[which].length();
919                         continue;
920                     }
921                 }
922                 ++i;
923             }
924         } catch (IndexOutOfBoundsException ignore) {
925             // happens when ^ is the last character in the string.
926         }
927         return ssb;
928     }
929 
getOffsetBefore(CharSequence text, int offset)930     public static int getOffsetBefore(CharSequence text, int offset) {
931         if (offset == 0)
932             return 0;
933         if (offset == 1)
934             return 0;
935 
936         char c = text.charAt(offset - 1);
937 
938         if (c >= '\uDC00' && c <= '\uDFFF') {
939             char c1 = text.charAt(offset - 2);
940 
941             if (c1 >= '\uD800' && c1 <= '\uDBFF')
942                 offset -= 2;
943             else
944                 offset -= 1;
945         } else {
946             offset -= 1;
947         }
948 
949         if (text instanceof Spanned) {
950             ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
951                                                        ReplacementSpan.class);
952 
953             for (int i = 0; i < spans.length; i++) {
954                 int start = ((Spanned) text).getSpanStart(spans[i]);
955                 int end = ((Spanned) text).getSpanEnd(spans[i]);
956 
957                 if (start < offset && end > offset)
958                     offset = start;
959             }
960         }
961 
962         return offset;
963     }
964 
getOffsetAfter(CharSequence text, int offset)965     public static int getOffsetAfter(CharSequence text, int offset) {
966         int len = text.length();
967 
968         if (offset == len)
969             return len;
970         if (offset == len - 1)
971             return len;
972 
973         char c = text.charAt(offset);
974 
975         if (c >= '\uD800' && c <= '\uDBFF') {
976             char c1 = text.charAt(offset + 1);
977 
978             if (c1 >= '\uDC00' && c1 <= '\uDFFF')
979                 offset += 2;
980             else
981                 offset += 1;
982         } else {
983             offset += 1;
984         }
985 
986         if (text instanceof Spanned) {
987             ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
988                                                        ReplacementSpan.class);
989 
990             for (int i = 0; i < spans.length; i++) {
991                 int start = ((Spanned) text).getSpanStart(spans[i]);
992                 int end = ((Spanned) text).getSpanEnd(spans[i]);
993 
994                 if (start < offset && end > offset)
995                     offset = end;
996             }
997         }
998 
999         return offset;
1000     }
1001 
readSpan(Parcel p, Spannable sp, Object o)1002     private static void readSpan(Parcel p, Spannable sp, Object o) {
1003         sp.setSpan(o, p.readInt(), p.readInt(), p.readInt());
1004     }
1005 
1006     /**
1007      * Copies the spans from the region <code>start...end</code> in
1008      * <code>source</code> to the region
1009      * <code>destoff...destoff+end-start</code> in <code>dest</code>.
1010      * Spans in <code>source</code> that begin before <code>start</code>
1011      * or end after <code>end</code> but overlap this range are trimmed
1012      * as if they began at <code>start</code> or ended at <code>end</code>.
1013      *
1014      * @throws IndexOutOfBoundsException if any of the copied spans
1015      * are out of range in <code>dest</code>.
1016      */
copySpansFrom(Spanned source, int start, int end, Class kind, Spannable dest, int destoff)1017     public static void copySpansFrom(Spanned source, int start, int end,
1018                                      Class kind,
1019                                      Spannable dest, int destoff) {
1020         if (kind == null) {
1021             kind = Object.class;
1022         }
1023 
1024         Object[] spans = source.getSpans(start, end, kind);
1025 
1026         for (int i = 0; i < spans.length; i++) {
1027             int st = source.getSpanStart(spans[i]);
1028             int en = source.getSpanEnd(spans[i]);
1029             int fl = source.getSpanFlags(spans[i]);
1030 
1031             if (st < start)
1032                 st = start;
1033             if (en > end)
1034                 en = end;
1035 
1036             dest.setSpan(spans[i], st - start + destoff, en - start + destoff,
1037                          fl);
1038         }
1039     }
1040 
1041     public enum TruncateAt {
1042         START,
1043         MIDDLE,
1044         END,
1045         MARQUEE,
1046         /**
1047          * @hide
1048          */
1049         END_SMALL
1050     }
1051 
1052     public interface EllipsizeCallback {
1053         /**
1054          * This method is called to report that the specified region of
1055          * text was ellipsized away by a call to {@link #ellipsize}.
1056          */
ellipsized(int start, int end)1057         public void ellipsized(int start, int end);
1058     }
1059 
1060     /**
1061      * Returns the original text if it fits in the specified width
1062      * given the properties of the specified Paint,
1063      * or, if it does not fit, a truncated
1064      * copy with ellipsis character added at the specified edge or center.
1065      */
ellipsize(CharSequence text, TextPaint p, float avail, TruncateAt where)1066     public static CharSequence ellipsize(CharSequence text,
1067                                          TextPaint p,
1068                                          float avail, TruncateAt where) {
1069         return ellipsize(text, p, avail, where, false, null);
1070     }
1071 
1072     /**
1073      * Returns the original text if it fits in the specified width
1074      * given the properties of the specified Paint,
1075      * or, if it does not fit, a copy with ellipsis character added
1076      * at the specified edge or center.
1077      * If <code>preserveLength</code> is specified, the returned copy
1078      * will be padded with zero-width spaces to preserve the original
1079      * length and offsets instead of truncating.
1080      * If <code>callback</code> is non-null, it will be called to
1081      * report the start and end of the ellipsized range.  TextDirection
1082      * is determined by the first strong directional character.
1083      */
ellipsize(CharSequence text, TextPaint paint, float avail, TruncateAt where, boolean preserveLength, EllipsizeCallback callback)1084     public static CharSequence ellipsize(CharSequence text,
1085                                          TextPaint paint,
1086                                          float avail, TruncateAt where,
1087                                          boolean preserveLength,
1088                                          EllipsizeCallback callback) {
1089         return ellipsize(text, paint, avail, where, preserveLength, callback,
1090                 TextDirectionHeuristics.FIRSTSTRONG_LTR,
1091                 (where == TruncateAt.END_SMALL) ? ELLIPSIS_TWO_DOTS_STRING : ELLIPSIS_STRING);
1092     }
1093 
1094     /**
1095      * Returns the original text if it fits in the specified width
1096      * given the properties of the specified Paint,
1097      * or, if it does not fit, a copy with ellipsis character added
1098      * at the specified edge or center.
1099      * If <code>preserveLength</code> is specified, the returned copy
1100      * will be padded with zero-width spaces to preserve the original
1101      * length and offsets instead of truncating.
1102      * If <code>callback</code> is non-null, it will be called to
1103      * report the start and end of the ellipsized range.
1104      *
1105      * @hide
1106      */
ellipsize(CharSequence text, TextPaint paint, float avail, TruncateAt where, boolean preserveLength, EllipsizeCallback callback, TextDirectionHeuristic textDir, String ellipsis)1107     public static CharSequence ellipsize(CharSequence text,
1108             TextPaint paint,
1109             float avail, TruncateAt where,
1110             boolean preserveLength,
1111             EllipsizeCallback callback,
1112             TextDirectionHeuristic textDir, String ellipsis) {
1113 
1114         int len = text.length();
1115 
1116         MeasuredText mt = MeasuredText.obtain();
1117         try {
1118             float width = setPara(mt, paint, text, 0, text.length(), textDir);
1119 
1120             if (width <= avail) {
1121                 if (callback != null) {
1122                     callback.ellipsized(0, 0);
1123                 }
1124 
1125                 return text;
1126             }
1127 
1128             // XXX assumes ellipsis string does not require shaping and
1129             // is unaffected by style
1130             float ellipsiswid = paint.measureText(ellipsis);
1131             avail -= ellipsiswid;
1132 
1133             int left = 0;
1134             int right = len;
1135             if (avail < 0) {
1136                 // it all goes
1137             } else if (where == TruncateAt.START) {
1138                 right = len - mt.breakText(len, false, avail);
1139             } else if (where == TruncateAt.END || where == TruncateAt.END_SMALL) {
1140                 left = mt.breakText(len, true, avail);
1141             } else {
1142                 right = len - mt.breakText(len, false, avail / 2);
1143                 avail -= mt.measure(right, len);
1144                 left = mt.breakText(right, true, avail);
1145             }
1146 
1147             if (callback != null) {
1148                 callback.ellipsized(left, right);
1149             }
1150 
1151             char[] buf = mt.mChars;
1152             Spanned sp = text instanceof Spanned ? (Spanned) text : null;
1153 
1154             int remaining = len - (right - left);
1155             if (preserveLength) {
1156                 if (remaining > 0) { // else eliminate the ellipsis too
1157                     buf[left++] = ellipsis.charAt(0);
1158                 }
1159                 for (int i = left; i < right; i++) {
1160                     buf[i] = ZWNBS_CHAR;
1161                 }
1162                 String s = new String(buf, 0, len);
1163                 if (sp == null) {
1164                     return s;
1165                 }
1166                 SpannableString ss = new SpannableString(s);
1167                 copySpansFrom(sp, 0, len, Object.class, ss, 0);
1168                 return ss;
1169             }
1170 
1171             if (remaining == 0) {
1172                 return "";
1173             }
1174 
1175             if (sp == null) {
1176                 StringBuilder sb = new StringBuilder(remaining + ellipsis.length());
1177                 sb.append(buf, 0, left);
1178                 sb.append(ellipsis);
1179                 sb.append(buf, right, len - right);
1180                 return sb.toString();
1181             }
1182 
1183             SpannableStringBuilder ssb = new SpannableStringBuilder();
1184             ssb.append(text, 0, left);
1185             ssb.append(ellipsis);
1186             ssb.append(text, right, len);
1187             return ssb;
1188         } finally {
1189             MeasuredText.recycle(mt);
1190         }
1191     }
1192 
1193     /**
1194      * Converts a CharSequence of the comma-separated form "Andy, Bob,
1195      * Charles, David" that is too wide to fit into the specified width
1196      * into one like "Andy, Bob, 2 more".
1197      *
1198      * @param text the text to truncate
1199      * @param p the Paint with which to measure the text
1200      * @param avail the horizontal width available for the text
1201      * @param oneMore the string for "1 more" in the current locale
1202      * @param more the string for "%d more" in the current locale
1203      */
commaEllipsize(CharSequence text, TextPaint p, float avail, String oneMore, String more)1204     public static CharSequence commaEllipsize(CharSequence text,
1205                                               TextPaint p, float avail,
1206                                               String oneMore,
1207                                               String more) {
1208         return commaEllipsize(text, p, avail, oneMore, more,
1209                 TextDirectionHeuristics.FIRSTSTRONG_LTR);
1210     }
1211 
1212     /**
1213      * @hide
1214      */
commaEllipsize(CharSequence text, TextPaint p, float avail, String oneMore, String more, TextDirectionHeuristic textDir)1215     public static CharSequence commaEllipsize(CharSequence text, TextPaint p,
1216          float avail, String oneMore, String more, TextDirectionHeuristic textDir) {
1217 
1218         MeasuredText mt = MeasuredText.obtain();
1219         try {
1220             int len = text.length();
1221             float width = setPara(mt, p, text, 0, len, textDir);
1222             if (width <= avail) {
1223                 return text;
1224             }
1225 
1226             char[] buf = mt.mChars;
1227 
1228             int commaCount = 0;
1229             for (int i = 0; i < len; i++) {
1230                 if (buf[i] == ',') {
1231                     commaCount++;
1232                 }
1233             }
1234 
1235             int remaining = commaCount + 1;
1236 
1237             int ok = 0;
1238             String okFormat = "";
1239 
1240             int w = 0;
1241             int count = 0;
1242             float[] widths = mt.mWidths;
1243 
1244             MeasuredText tempMt = MeasuredText.obtain();
1245             for (int i = 0; i < len; i++) {
1246                 w += widths[i];
1247 
1248                 if (buf[i] == ',') {
1249                     count++;
1250 
1251                     String format;
1252                     // XXX should not insert spaces, should be part of string
1253                     // XXX should use plural rules and not assume English plurals
1254                     if (--remaining == 1) {
1255                         format = " " + oneMore;
1256                     } else {
1257                         format = " " + String.format(more, remaining);
1258                     }
1259 
1260                     // XXX this is probably ok, but need to look at it more
1261                     tempMt.setPara(format, 0, format.length(), textDir);
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