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