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