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