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