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