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