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