TextUtils.java revision f3e64101bb2af1159518259229d5d429f8915787
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
17package android.text;
18
19import android.content.res.Resources;
20import android.os.Parcel;
21import android.os.Parcelable;
22import android.text.style.AbsoluteSizeSpan;
23import android.text.style.AlignmentSpan;
24import android.text.style.BackgroundColorSpan;
25import android.text.style.BulletSpan;
26import android.text.style.CharacterStyle;
27import android.text.style.EasyEditSpan;
28import android.text.style.ForegroundColorSpan;
29import android.text.style.LeadingMarginSpan;
30import android.text.style.MetricAffectingSpan;
31import android.text.style.QuoteSpan;
32import android.text.style.RelativeSizeSpan;
33import android.text.style.ReplacementSpan;
34import android.text.style.ScaleXSpan;
35import android.text.style.SpellCheckSpan;
36import android.text.style.StrikethroughSpan;
37import android.text.style.StyleSpan;
38import android.text.style.SubscriptSpan;
39import android.text.style.SuggestionRangeSpan;
40import android.text.style.SuggestionSpan;
41import android.text.style.SuperscriptSpan;
42import android.text.style.TextAppearanceSpan;
43import android.text.style.TypefaceSpan;
44import android.text.style.URLSpan;
45import android.text.style.UnderlineSpan;
46import android.util.Printer;
47
48import com.android.internal.R;
49import com.android.internal.util.ArrayUtils;
50
51import java.lang.reflect.Array;
52import java.util.Iterator;
53import java.util.regex.Pattern;
54
55public class TextUtils {
56
57    private TextUtils() { /* cannot be instantiated */ }
58
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
77    public static int indexOf(CharSequence s, char ch) {
78        return indexOf(s, ch, 0);
79    }
80
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
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
127    public static int lastIndexOf(CharSequence s, char ch) {
128        return lastIndexOf(s, ch, s.length() - 1);
129    }
130
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
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
185    public static int indexOf(CharSequence s, CharSequence needle) {
186        return indexOf(s, needle, 0, s.length());
187    }
188
189    public static int indexOf(CharSequence s, CharSequence needle, int start) {
190        return indexOf(s, needle, start, s.length());
191    }
192
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
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     */
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     */
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     */
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     */
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     */
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     */
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> {
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         */
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         */
394        public void setString(String string) {
395            mString = string;
396            mPosition = 0;
397            mLength = mString.length();
398        }
399
400        public Iterator<String> iterator() {
401            return this;
402        }
403
404        public boolean hasNext() {
405            return mPosition < mLength;
406        }
407
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
418        public void remove() {
419            throw new UnsupportedOperationException();
420        }
421    }
422
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     */
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     */
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     */
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
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    {
500        public Reverser(CharSequence source, int start, int end) {
501            mSource = source;
502            mStart = start;
503            mEnd = end;
504        }
505
506        public int length() {
507            return mEnd - mStart;
508        }
509
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
518        public String toString() {
519            return subSequence(0, length()).toString();
520        }
521
522        public char charAt(int off) {
523            return AndroidCharacter.getMirror(mSource.charAt(mEnd - 1 - off));
524        }
525
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     */
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
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     */
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     */
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     */
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
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
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
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     */
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         */
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     */
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     */
1040    public static CharSequence ellipsize(CharSequence text,
1041                                         TextPaint paint,
1042                                         float avail, TruncateAt where,
1043                                         boolean preserveLength,
1044                                         EllipsizeCallback callback) {
1045
1046        final String ellipsis = (where == TruncateAt.END_SMALL) ?
1047                Resources.getSystem().getString(R.string.ellipsis_two_dots) :
1048                Resources.getSystem().getString(R.string.ellipsis);
1049
1050        return ellipsize(text, paint, avail, where, preserveLength, callback,
1051                TextDirectionHeuristics.FIRSTSTRONG_LTR,
1052                ellipsis);
1053    }
1054
1055    /**
1056     * Returns the original text if it fits in the specified width
1057     * given the properties of the specified Paint,
1058     * or, if it does not fit, a copy with ellipsis character added
1059     * at the specified edge or center.
1060     * If <code>preserveLength</code> is specified, the returned copy
1061     * will be padded with zero-width spaces to preserve the original
1062     * length and offsets instead of truncating.
1063     * If <code>callback</code> is non-null, it will be called to
1064     * report the start and end of the ellipsized range.
1065     *
1066     * @hide
1067     */
1068    public static CharSequence ellipsize(CharSequence text,
1069            TextPaint paint,
1070            float avail, TruncateAt where,
1071            boolean preserveLength,
1072            EllipsizeCallback callback,
1073            TextDirectionHeuristic textDir, String ellipsis) {
1074
1075        int len = text.length();
1076
1077        MeasuredText mt = MeasuredText.obtain();
1078        try {
1079            float width = setPara(mt, paint, text, 0, text.length(), textDir);
1080
1081            if (width <= avail) {
1082                if (callback != null) {
1083                    callback.ellipsized(0, 0);
1084                }
1085
1086                return text;
1087            }
1088
1089            // XXX assumes ellipsis string does not require shaping and
1090            // is unaffected by style
1091            float ellipsiswid = paint.measureText(ellipsis);
1092            avail -= ellipsiswid;
1093
1094            int left = 0;
1095            int right = len;
1096            if (avail < 0) {
1097                // it all goes
1098            } else if (where == TruncateAt.START) {
1099                right = len - mt.breakText(len, false, avail);
1100            } else if (where == TruncateAt.END || where == TruncateAt.END_SMALL) {
1101                left = mt.breakText(len, true, avail);
1102            } else {
1103                right = len - mt.breakText(len, false, avail / 2);
1104                avail -= mt.measure(right, len);
1105                left = mt.breakText(right, true, avail);
1106            }
1107
1108            if (callback != null) {
1109                callback.ellipsized(left, right);
1110            }
1111
1112            char[] buf = mt.mChars;
1113            Spanned sp = text instanceof Spanned ? (Spanned) text : null;
1114
1115            int remaining = len - (right - left);
1116            if (preserveLength) {
1117                if (remaining > 0) { // else eliminate the ellipsis too
1118                    buf[left++] = ellipsis.charAt(0);
1119                }
1120                for (int i = left; i < right; i++) {
1121                    buf[i] = ZWNBS_CHAR;
1122                }
1123                String s = new String(buf, 0, len);
1124                if (sp == null) {
1125                    return s;
1126                }
1127                SpannableString ss = new SpannableString(s);
1128                copySpansFrom(sp, 0, len, Object.class, ss, 0);
1129                return ss;
1130            }
1131
1132            if (remaining == 0) {
1133                return "";
1134            }
1135
1136            if (sp == null) {
1137                StringBuilder sb = new StringBuilder(remaining + ellipsis.length());
1138                sb.append(buf, 0, left);
1139                sb.append(ellipsis);
1140                sb.append(buf, right, len - right);
1141                return sb.toString();
1142            }
1143
1144            SpannableStringBuilder ssb = new SpannableStringBuilder();
1145            ssb.append(text, 0, left);
1146            ssb.append(ellipsis);
1147            ssb.append(text, right, len);
1148            return ssb;
1149        } finally {
1150            MeasuredText.recycle(mt);
1151        }
1152    }
1153
1154    /**
1155     * Converts a CharSequence of the comma-separated form "Andy, Bob,
1156     * Charles, David" that is too wide to fit into the specified width
1157     * into one like "Andy, Bob, 2 more".
1158     *
1159     * @param text the text to truncate
1160     * @param p the Paint with which to measure the text
1161     * @param avail the horizontal width available for the text
1162     * @param oneMore the string for "1 more" in the current locale
1163     * @param more the string for "%d more" in the current locale
1164     */
1165    public static CharSequence commaEllipsize(CharSequence text,
1166                                              TextPaint p, float avail,
1167                                              String oneMore,
1168                                              String more) {
1169        return commaEllipsize(text, p, avail, oneMore, more,
1170                TextDirectionHeuristics.FIRSTSTRONG_LTR);
1171    }
1172
1173    /**
1174     * @hide
1175     */
1176    public static CharSequence commaEllipsize(CharSequence text, TextPaint p,
1177         float avail, String oneMore, String more, TextDirectionHeuristic textDir) {
1178
1179        MeasuredText mt = MeasuredText.obtain();
1180        try {
1181            int len = text.length();
1182            float width = setPara(mt, p, text, 0, len, textDir);
1183            if (width <= avail) {
1184                return text;
1185            }
1186
1187            char[] buf = mt.mChars;
1188
1189            int commaCount = 0;
1190            for (int i = 0; i < len; i++) {
1191                if (buf[i] == ',') {
1192                    commaCount++;
1193                }
1194            }
1195
1196            int remaining = commaCount + 1;
1197
1198            int ok = 0;
1199            String okFormat = "";
1200
1201            int w = 0;
1202            int count = 0;
1203            float[] widths = mt.mWidths;
1204
1205            MeasuredText tempMt = MeasuredText.obtain();
1206            for (int i = 0; i < len; i++) {
1207                w += widths[i];
1208
1209                if (buf[i] == ',') {
1210                    count++;
1211
1212                    String format;
1213                    // XXX should not insert spaces, should be part of string
1214                    // XXX should use plural rules and not assume English plurals
1215                    if (--remaining == 1) {
1216                        format = " " + oneMore;
1217                    } else {
1218                        format = " " + String.format(more, remaining);
1219                    }
1220
1221                    // XXX this is probably ok, but need to look at it more
1222                    tempMt.setPara(format, 0, format.length(), textDir);
1223                    float moreWid = tempMt.addStyleRun(p, tempMt.mLen, null);
1224
1225                    if (w + moreWid <= avail) {
1226                        ok = i + 1;
1227                        okFormat = format;
1228                    }
1229                }
1230            }
1231            MeasuredText.recycle(tempMt);
1232
1233            SpannableStringBuilder out = new SpannableStringBuilder(okFormat);
1234            out.insert(0, text, 0, ok);
1235            return out;
1236        } finally {
1237            MeasuredText.recycle(mt);
1238        }
1239    }
1240
1241    private static float setPara(MeasuredText mt, TextPaint paint,
1242            CharSequence text, int start, int end, TextDirectionHeuristic textDir) {
1243
1244        mt.setPara(text, start, end, textDir);
1245
1246        float width;
1247        Spanned sp = text instanceof Spanned ? (Spanned) text : null;
1248        int len = end - start;
1249        if (sp == null) {
1250            width = mt.addStyleRun(paint, len, null);
1251        } else {
1252            width = 0;
1253            int spanEnd;
1254            for (int spanStart = 0; spanStart < len; spanStart = spanEnd) {
1255                spanEnd = sp.nextSpanTransition(spanStart, len,
1256                        MetricAffectingSpan.class);
1257                MetricAffectingSpan[] spans = sp.getSpans(
1258                        spanStart, spanEnd, MetricAffectingSpan.class);
1259                spans = TextUtils.removeEmptySpans(spans, sp, MetricAffectingSpan.class);
1260                width += mt.addStyleRun(paint, spans, spanEnd - spanStart, null);
1261            }
1262        }
1263
1264        return width;
1265    }
1266
1267    private static final char FIRST_RIGHT_TO_LEFT = '\u0590';
1268
1269    /* package */
1270    static boolean doesNotNeedBidi(CharSequence s, int start, int end) {
1271        for (int i = start; i < end; i++) {
1272            if (s.charAt(i) >= FIRST_RIGHT_TO_LEFT) {
1273                return false;
1274            }
1275        }
1276        return true;
1277    }
1278
1279    /* package */
1280    static boolean doesNotNeedBidi(char[] text, int start, int len) {
1281        for (int i = start, e = i + len; i < e; i++) {
1282            if (text[i] >= FIRST_RIGHT_TO_LEFT) {
1283                return false;
1284            }
1285        }
1286        return true;
1287    }
1288
1289    /* package */ static char[] obtain(int len) {
1290        char[] buf;
1291
1292        synchronized (sLock) {
1293            buf = sTemp;
1294            sTemp = null;
1295        }
1296
1297        if (buf == null || buf.length < len)
1298            buf = new char[ArrayUtils.idealCharArraySize(len)];
1299
1300        return buf;
1301    }
1302
1303    /* package */ static void recycle(char[] temp) {
1304        if (temp.length > 1000)
1305            return;
1306
1307        synchronized (sLock) {
1308            sTemp = temp;
1309        }
1310    }
1311
1312    /**
1313     * Html-encode the string.
1314     * @param s the string to be encoded
1315     * @return the encoded string
1316     */
1317    public static String htmlEncode(String s) {
1318        StringBuilder sb = new StringBuilder();
1319        char c;
1320        for (int i = 0; i < s.length(); i++) {
1321            c = s.charAt(i);
1322            switch (c) {
1323            case '<':
1324                sb.append("&lt;"); //$NON-NLS-1$
1325                break;
1326            case '>':
1327                sb.append("&gt;"); //$NON-NLS-1$
1328                break;
1329            case '&':
1330                sb.append("&amp;"); //$NON-NLS-1$
1331                break;
1332            case '\'':
1333                //http://www.w3.org/TR/xhtml1
1334                // The named character reference &apos; (the apostrophe, U+0027) was introduced in
1335                // XML 1.0 but does not appear in HTML. Authors should therefore use &#39; instead
1336                // of &apos; to work as expected in HTML 4 user agents.
1337                sb.append("&#39;"); //$NON-NLS-1$
1338                break;
1339            case '"':
1340                sb.append("&quot;"); //$NON-NLS-1$
1341                break;
1342            default:
1343                sb.append(c);
1344            }
1345        }
1346        return sb.toString();
1347    }
1348
1349    /**
1350     * Returns a CharSequence concatenating the specified CharSequences,
1351     * retaining their spans if any.
1352     */
1353    public static CharSequence concat(CharSequence... text) {
1354        if (text.length == 0) {
1355            return "";
1356        }
1357
1358        if (text.length == 1) {
1359            return text[0];
1360        }
1361
1362        boolean spanned = false;
1363        for (int i = 0; i < text.length; i++) {
1364            if (text[i] instanceof Spanned) {
1365                spanned = true;
1366                break;
1367            }
1368        }
1369
1370        StringBuilder sb = new StringBuilder();
1371        for (int i = 0; i < text.length; i++) {
1372            sb.append(text[i]);
1373        }
1374
1375        if (!spanned) {
1376            return sb.toString();
1377        }
1378
1379        SpannableString ss = new SpannableString(sb);
1380        int off = 0;
1381        for (int i = 0; i < text.length; i++) {
1382            int len = text[i].length();
1383
1384            if (text[i] instanceof Spanned) {
1385                copySpansFrom((Spanned) text[i], 0, len, Object.class, ss, off);
1386            }
1387
1388            off += len;
1389        }
1390
1391        return new SpannedString(ss);
1392    }
1393
1394    /**
1395     * Returns whether the given CharSequence contains any printable characters.
1396     */
1397    public static boolean isGraphic(CharSequence str) {
1398        final int len = str.length();
1399        for (int i=0; i<len; i++) {
1400            int gc = Character.getType(str.charAt(i));
1401            if (gc != Character.CONTROL
1402                    && gc != Character.FORMAT
1403                    && gc != Character.SURROGATE
1404                    && gc != Character.UNASSIGNED
1405                    && gc != Character.LINE_SEPARATOR
1406                    && gc != Character.PARAGRAPH_SEPARATOR
1407                    && gc != Character.SPACE_SEPARATOR) {
1408                return true;
1409            }
1410        }
1411        return false;
1412    }
1413
1414    /**
1415     * Returns whether this character is a printable character.
1416     */
1417    public static boolean isGraphic(char c) {
1418        int gc = Character.getType(c);
1419        return     gc != Character.CONTROL
1420                && gc != Character.FORMAT
1421                && gc != Character.SURROGATE
1422                && gc != Character.UNASSIGNED
1423                && gc != Character.LINE_SEPARATOR
1424                && gc != Character.PARAGRAPH_SEPARATOR
1425                && gc != Character.SPACE_SEPARATOR;
1426    }
1427
1428    /**
1429     * Returns whether the given CharSequence contains only digits.
1430     */
1431    public static boolean isDigitsOnly(CharSequence str) {
1432        final int len = str.length();
1433        for (int i = 0; i < len; i++) {
1434            if (!Character.isDigit(str.charAt(i))) {
1435                return false;
1436            }
1437        }
1438        return true;
1439    }
1440
1441    /**
1442     * @hide
1443     */
1444    public static boolean isPrintableAscii(final char c) {
1445        final int asciiFirst = 0x20;
1446        final int asciiLast = 0x7E;  // included
1447        return (asciiFirst <= c && c <= asciiLast) || c == '\r' || c == '\n';
1448    }
1449
1450    /**
1451     * @hide
1452     */
1453    public static boolean isPrintableAsciiOnly(final CharSequence str) {
1454        final int len = str.length();
1455        for (int i = 0; i < len; i++) {
1456            if (!isPrintableAscii(str.charAt(i))) {
1457                return false;
1458            }
1459        }
1460        return true;
1461    }
1462
1463    /**
1464     * Capitalization mode for {@link #getCapsMode}: capitalize all
1465     * characters.  This value is explicitly defined to be the same as
1466     * {@link InputType#TYPE_TEXT_FLAG_CAP_CHARACTERS}.
1467     */
1468    public static final int CAP_MODE_CHARACTERS
1469            = InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS;
1470
1471    /**
1472     * Capitalization mode for {@link #getCapsMode}: capitalize the first
1473     * character of all words.  This value is explicitly defined to be the same as
1474     * {@link InputType#TYPE_TEXT_FLAG_CAP_WORDS}.
1475     */
1476    public static final int CAP_MODE_WORDS
1477            = InputType.TYPE_TEXT_FLAG_CAP_WORDS;
1478
1479    /**
1480     * Capitalization mode for {@link #getCapsMode}: capitalize the first
1481     * character of each sentence.  This value is explicitly defined to be the same as
1482     * {@link InputType#TYPE_TEXT_FLAG_CAP_SENTENCES}.
1483     */
1484    public static final int CAP_MODE_SENTENCES
1485            = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
1486
1487    /**
1488     * Determine what caps mode should be in effect at the current offset in
1489     * the text.  Only the mode bits set in <var>reqModes</var> will be
1490     * checked.  Note that the caps mode flags here are explicitly defined
1491     * to match those in {@link InputType}.
1492     *
1493     * @param cs The text that should be checked for caps modes.
1494     * @param off Location in the text at which to check.
1495     * @param reqModes The modes to be checked: may be any combination of
1496     * {@link #CAP_MODE_CHARACTERS}, {@link #CAP_MODE_WORDS}, and
1497     * {@link #CAP_MODE_SENTENCES}.
1498     *
1499     * @return Returns the actual capitalization modes that can be in effect
1500     * at the current position, which is any combination of
1501     * {@link #CAP_MODE_CHARACTERS}, {@link #CAP_MODE_WORDS}, and
1502     * {@link #CAP_MODE_SENTENCES}.
1503     */
1504    public static int getCapsMode(CharSequence cs, int off, int reqModes) {
1505        if (off < 0) {
1506            return 0;
1507        }
1508
1509        int i;
1510        char c;
1511        int mode = 0;
1512
1513        if ((reqModes&CAP_MODE_CHARACTERS) != 0) {
1514            mode |= CAP_MODE_CHARACTERS;
1515        }
1516        if ((reqModes&(CAP_MODE_WORDS|CAP_MODE_SENTENCES)) == 0) {
1517            return mode;
1518        }
1519
1520        // Back over allowed opening punctuation.
1521
1522        for (i = off; i > 0; i--) {
1523            c = cs.charAt(i - 1);
1524
1525            if (c != '"' && c != '\'' &&
1526                Character.getType(c) != Character.START_PUNCTUATION) {
1527                break;
1528            }
1529        }
1530
1531        // Start of paragraph, with optional whitespace.
1532
1533        int j = i;
1534        while (j > 0 && ((c = cs.charAt(j - 1)) == ' ' || c == '\t')) {
1535            j--;
1536        }
1537        if (j == 0 || cs.charAt(j - 1) == '\n') {
1538            return mode | CAP_MODE_WORDS;
1539        }
1540
1541        // Or start of word if we are that style.
1542
1543        if ((reqModes&CAP_MODE_SENTENCES) == 0) {
1544            if (i != j) mode |= CAP_MODE_WORDS;
1545            return mode;
1546        }
1547
1548        // There must be a space if not the start of paragraph.
1549
1550        if (i == j) {
1551            return mode;
1552        }
1553
1554        // Back over allowed closing punctuation.
1555
1556        for (; j > 0; j--) {
1557            c = cs.charAt(j - 1);
1558
1559            if (c != '"' && c != '\'' &&
1560                Character.getType(c) != Character.END_PUNCTUATION) {
1561                break;
1562            }
1563        }
1564
1565        if (j > 0) {
1566            c = cs.charAt(j - 1);
1567
1568            if (c == '.' || c == '?' || c == '!') {
1569                // Do not capitalize if the word ends with a period but
1570                // also contains a period, in which case it is an abbreviation.
1571
1572                if (c == '.') {
1573                    for (int k = j - 2; k >= 0; k--) {
1574                        c = cs.charAt(k);
1575
1576                        if (c == '.') {
1577                            return mode;
1578                        }
1579
1580                        if (!Character.isLetter(c)) {
1581                            break;
1582                        }
1583                    }
1584                }
1585
1586                return mode | CAP_MODE_SENTENCES;
1587            }
1588        }
1589
1590        return mode;
1591    }
1592
1593    /**
1594     * Does a comma-delimited list 'delimitedString' contain a certain item?
1595     * (without allocating memory)
1596     *
1597     * @hide
1598     */
1599    public static boolean delimitedStringContains(
1600            String delimitedString, char delimiter, String item) {
1601        if (isEmpty(delimitedString) || isEmpty(item)) {
1602            return false;
1603        }
1604        int pos = -1;
1605        int length = delimitedString.length();
1606        while ((pos = delimitedString.indexOf(item, pos + 1)) != -1) {
1607            if (pos > 0 && delimitedString.charAt(pos - 1) != delimiter) {
1608                continue;
1609            }
1610            int expectedDelimiterPos = pos + item.length();
1611            if (expectedDelimiterPos == length) {
1612                // Match at end of string.
1613                return true;
1614            }
1615            if (delimitedString.charAt(expectedDelimiterPos) == delimiter) {
1616                return true;
1617            }
1618        }
1619        return false;
1620    }
1621
1622    /**
1623     * Removes empty spans from the <code>spans</code> array.
1624     *
1625     * When parsing a Spanned using {@link Spanned#nextSpanTransition(int, int, Class)}, empty spans
1626     * will (correctly) create span transitions, and calling getSpans on a slice of text bounded by
1627     * one of these transitions will (correctly) include the empty overlapping span.
1628     *
1629     * However, these empty spans should not be taken into account when layouting or rendering the
1630     * string and this method provides a way to filter getSpans' results accordingly.
1631     *
1632     * @param spans A list of spans retrieved using {@link Spanned#getSpans(int, int, Class)} from
1633     * the <code>spanned</code>
1634     * @param spanned The Spanned from which spans were extracted
1635     * @return A subset of spans where empty spans ({@link Spanned#getSpanStart(Object)}  ==
1636     * {@link Spanned#getSpanEnd(Object)} have been removed. The initial order is preserved
1637     * @hide
1638     */
1639    @SuppressWarnings("unchecked")
1640    public static <T> T[] removeEmptySpans(T[] spans, Spanned spanned, Class<T> klass) {
1641        T[] copy = null;
1642        int count = 0;
1643
1644        for (int i = 0; i < spans.length; i++) {
1645            final T span = spans[i];
1646            final int start = spanned.getSpanStart(span);
1647            final int end = spanned.getSpanEnd(span);
1648
1649            if (start == end) {
1650                if (copy == null) {
1651                    copy = (T[]) Array.newInstance(klass, spans.length - 1);
1652                    System.arraycopy(spans, 0, copy, 0, i);
1653                    count = i;
1654                }
1655            } else {
1656                if (copy != null) {
1657                    copy[count] = span;
1658                    count++;
1659                }
1660            }
1661        }
1662
1663        if (copy != null) {
1664            T[] result = (T[]) Array.newInstance(klass, count);
1665            System.arraycopy(copy, 0, result, 0, count);
1666            return result;
1667        } else {
1668            return spans;
1669        }
1670    }
1671
1672    /**
1673     * Pack 2 int values into a long, useful as a return value for a range
1674     * @see #unpackRangeStartFromLong(long)
1675     * @see #unpackRangeEndFromLong(long)
1676     * @hide
1677     */
1678    public static long packRangeInLong(int start, int end) {
1679        return (((long) start) << 32) | end;
1680    }
1681
1682    /**
1683     * Get the start value from a range packed in a long by {@link #packRangeInLong(int, int)}
1684     * @see #unpackRangeEndFromLong(long)
1685     * @see #packRangeInLong(int, int)
1686     * @hide
1687     */
1688    public static int unpackRangeStartFromLong(long range) {
1689        return (int) (range >>> 32);
1690    }
1691
1692    /**
1693     * Get the end value from a range packed in a long by {@link #packRangeInLong(int, int)}
1694     * @see #unpackRangeStartFromLong(long)
1695     * @see #packRangeInLong(int, int)
1696     * @hide
1697     */
1698    public static int unpackRangeEndFromLong(long range) {
1699        return (int) (range & 0x00000000FFFFFFFFL);
1700    }
1701
1702    private static Object sLock = new Object();
1703    private static char[] sTemp = null;
1704
1705    private static String[] EMPTY_STRING_ARRAY = new String[]{};
1706
1707    private static final char ZWNBS_CHAR = '\uFEFF';
1708}
1709