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