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
68    private TextUtils() { /* cannot be instantiated */ }
69
70    public static void getChars(CharSequence s, int start, int end,
71                                char[] dest, int destoff) {
72        Class<? extends CharSequence> c = s.getClass();
73
74        if (c == String.class)
75            ((String) s).getChars(start, end, dest, destoff);
76        else if (c == StringBuffer.class)
77            ((StringBuffer) s).getChars(start, end, dest, destoff);
78        else if (c == StringBuilder.class)
79            ((StringBuilder) s).getChars(start, end, dest, destoff);
80        else if (s instanceof GetChars)
81            ((GetChars) s).getChars(start, end, dest, destoff);
82        else {
83            for (int i = start; i < end; i++)
84                dest[destoff++] = s.charAt(i);
85        }
86    }
87
88    public static int indexOf(CharSequence s, char ch) {
89        return indexOf(s, ch, 0);
90    }
91
92    public static int indexOf(CharSequence s, char ch, int start) {
93        Class<? extends CharSequence> c = s.getClass();
94
95        if (c == String.class)
96            return ((String) s).indexOf(ch, start);
97
98        return indexOf(s, ch, start, s.length());
99    }
100
101    public static int indexOf(CharSequence s, char ch, int start, int end) {
102        Class<? extends CharSequence> c = s.getClass();
103
104        if (s instanceof GetChars || c == StringBuffer.class ||
105            c == StringBuilder.class || c == String.class) {
106            final int INDEX_INCREMENT = 500;
107            char[] temp = obtain(INDEX_INCREMENT);
108
109            while (start < end) {
110                int segend = start + INDEX_INCREMENT;
111                if (segend > end)
112                    segend = end;
113
114                getChars(s, start, segend, temp, 0);
115
116                int count = segend - start;
117                for (int i = 0; i < count; i++) {
118                    if (temp[i] == ch) {
119                        recycle(temp);
120                        return i + start;
121                    }
122                }
123
124                start = segend;
125            }
126
127            recycle(temp);
128            return -1;
129        }
130
131        for (int i = start; i < end; i++)
132            if (s.charAt(i) == ch)
133                return i;
134
135        return -1;
136    }
137
138    public static int lastIndexOf(CharSequence s, char ch) {
139        return lastIndexOf(s, ch, s.length() - 1);
140    }
141
142    public static int lastIndexOf(CharSequence s, char ch, int last) {
143        Class<? extends CharSequence> c = s.getClass();
144
145        if (c == String.class)
146            return ((String) s).lastIndexOf(ch, last);
147
148        return lastIndexOf(s, ch, 0, last);
149    }
150
151    public static int lastIndexOf(CharSequence s, char ch,
152                                  int start, int last) {
153        if (last < 0)
154            return -1;
155        if (last >= s.length())
156            last = s.length() - 1;
157
158        int end = last + 1;
159
160        Class<? extends CharSequence> c = s.getClass();
161
162        if (s instanceof GetChars || c == StringBuffer.class ||
163            c == StringBuilder.class || c == String.class) {
164            final int INDEX_INCREMENT = 500;
165            char[] temp = obtain(INDEX_INCREMENT);
166
167            while (start < end) {
168                int segstart = end - INDEX_INCREMENT;
169                if (segstart < start)
170                    segstart = start;
171
172                getChars(s, segstart, end, temp, 0);
173
174                int count = end - segstart;
175                for (int i = count - 1; i >= 0; i--) {
176                    if (temp[i] == ch) {
177                        recycle(temp);
178                        return i + segstart;
179                    }
180                }
181
182                end = segstart;
183            }
184
185            recycle(temp);
186            return -1;
187        }
188
189        for (int i = end - 1; i >= start; i--)
190            if (s.charAt(i) == ch)
191                return i;
192
193        return -1;
194    }
195
196    public static int indexOf(CharSequence s, CharSequence needle) {
197        return indexOf(s, needle, 0, s.length());
198    }
199
200    public static int indexOf(CharSequence s, CharSequence needle, int start) {
201        return indexOf(s, needle, start, s.length());
202    }
203
204    public static int indexOf(CharSequence s, CharSequence needle,
205                              int start, int end) {
206        int nlen = needle.length();
207        if (nlen == 0)
208            return start;
209
210        char c = needle.charAt(0);
211
212        for (;;) {
213            start = indexOf(s, c, start);
214            if (start > end - nlen) {
215                break;
216            }
217
218            if (start < 0) {
219                return -1;
220            }
221
222            if (regionMatches(s, start, needle, 0, nlen)) {
223                return start;
224            }
225
226            start++;
227        }
228        return -1;
229    }
230
231    public static boolean regionMatches(CharSequence one, int toffset,
232                                        CharSequence two, int ooffset,
233                                        int len) {
234        int tempLen = 2 * len;
235        if (tempLen < len) {
236            // Integer overflow; len is unreasonably large
237            throw new IndexOutOfBoundsException();
238        }
239        char[] temp = obtain(tempLen);
240
241        getChars(one, toffset, toffset + len, temp, 0);
242        getChars(two, ooffset, ooffset + len, temp, len);
243
244        boolean match = true;
245        for (int i = 0; i < len; i++) {
246            if (temp[i] != temp[i + len]) {
247                match = false;
248                break;
249            }
250        }
251
252        recycle(temp);
253        return match;
254    }
255
256    /**
257     * Create a new String object containing the given range of characters
258     * from the source string.  This is different than simply calling
259     * {@link CharSequence#subSequence(int, int) CharSequence.subSequence}
260     * in that it does not preserve any style runs in the source sequence,
261     * allowing a more efficient implementation.
262     */
263    public static String substring(CharSequence source, int start, int end) {
264        if (source instanceof String)
265            return ((String) source).substring(start, end);
266        if (source instanceof StringBuilder)
267            return ((StringBuilder) source).substring(start, end);
268        if (source instanceof StringBuffer)
269            return ((StringBuffer) source).substring(start, end);
270
271        char[] temp = obtain(end - start);
272        getChars(source, start, end, temp, 0);
273        String ret = new String(temp, 0, end - start);
274        recycle(temp);
275
276        return ret;
277    }
278
279    /**
280     * Returns list of multiple {@link CharSequence} joined into a single
281     * {@link CharSequence} separated by localized delimiter such as ", ".
282     *
283     * @hide
284     */
285    public static CharSequence join(Iterable<CharSequence> list) {
286        final CharSequence delimiter = Resources.getSystem().getText(R.string.list_delimeter);
287        return join(delimiter, list);
288    }
289
290    /**
291     * Returns a string containing the tokens joined by delimiters.
292     * @param tokens an array objects to be joined. Strings will be formed from
293     *     the objects by calling object.toString().
294     */
295    public static String join(CharSequence delimiter, Object[] tokens) {
296        StringBuilder sb = new StringBuilder();
297        boolean firstTime = true;
298        for (Object token: tokens) {
299            if (firstTime) {
300                firstTime = false;
301            } else {
302                sb.append(delimiter);
303            }
304            sb.append(token);
305        }
306        return sb.toString();
307    }
308
309    /**
310     * Returns a string containing the tokens joined by delimiters.
311     * @param tokens an array objects to be joined. Strings will be formed from
312     *     the objects by calling object.toString().
313     */
314    public static String join(CharSequence delimiter, Iterable tokens) {
315        StringBuilder sb = new StringBuilder();
316        boolean firstTime = true;
317        for (Object token: tokens) {
318            if (firstTime) {
319                firstTime = false;
320            } else {
321                sb.append(delimiter);
322            }
323            sb.append(token);
324        }
325        return sb.toString();
326    }
327
328    /**
329     * String.split() returns [''] when the string to be split is empty. This returns []. This does
330     * not remove any empty strings from the result. For example split("a,", ","  ) returns {"a", ""}.
331     *
332     * @param text the string to split
333     * @param expression the regular expression to match
334     * @return an array of strings. The array will be empty if text is empty
335     *
336     * @throws NullPointerException if expression or text is null
337     */
338    public static String[] split(String text, String expression) {
339        if (text.length() == 0) {
340            return EMPTY_STRING_ARRAY;
341        } else {
342            return text.split(expression, -1);
343        }
344    }
345
346    /**
347     * Splits a string on a pattern. String.split() returns [''] when the string to be
348     * split is empty. This returns []. This does not remove any empty strings from the result.
349     * @param text the string to split
350     * @param pattern the regular expression to match
351     * @return an array of strings. The array will be empty if text is empty
352     *
353     * @throws NullPointerException if expression or text is null
354     */
355    public static String[] split(String text, Pattern pattern) {
356        if (text.length() == 0) {
357            return EMPTY_STRING_ARRAY;
358        } else {
359            return pattern.split(text, -1);
360        }
361    }
362
363    /**
364     * An interface for splitting strings according to rules that are opaque to the user of this
365     * interface. This also has less overhead than split, which uses regular expressions and
366     * allocates an array to hold the results.
367     *
368     * <p>The most efficient way to use this class is:
369     *
370     * <pre>
371     * // Once
372     * TextUtils.StringSplitter splitter = new TextUtils.SimpleStringSplitter(delimiter);
373     *
374     * // Once per string to split
375     * splitter.setString(string);
376     * for (String s : splitter) {
377     *     ...
378     * }
379     * </pre>
380     */
381    public interface StringSplitter extends Iterable<String> {
382        public void setString(String string);
383    }
384
385    /**
386     * A simple string splitter.
387     *
388     * <p>If the final character in the string to split is the delimiter then no empty string will
389     * be returned for the empty string after that delimeter. That is, splitting <tt>"a,b,"</tt> on
390     * comma will return <tt>"a", "b"</tt>, not <tt>"a", "b", ""</tt>.
391     */
392    public static class SimpleStringSplitter implements StringSplitter, Iterator<String> {
393        private String mString;
394        private char mDelimiter;
395        private int mPosition;
396        private int mLength;
397
398        /**
399         * Initializes the splitter. setString may be called later.
400         * @param delimiter the delimeter on which to split
401         */
402        public SimpleStringSplitter(char delimiter) {
403            mDelimiter = delimiter;
404        }
405
406        /**
407         * Sets the string to split
408         * @param string the string to split
409         */
410        public void setString(String string) {
411            mString = string;
412            mPosition = 0;
413            mLength = mString.length();
414        }
415
416        public Iterator<String> iterator() {
417            return this;
418        }
419
420        public boolean hasNext() {
421            return mPosition < mLength;
422        }
423
424        public String next() {
425            int end = mString.indexOf(mDelimiter, mPosition);
426            if (end == -1) {
427                end = mLength;
428            }
429            String nextString = mString.substring(mPosition, end);
430            mPosition = end + 1; // Skip the delimiter.
431            return nextString;
432        }
433
434        public void remove() {
435            throw new UnsupportedOperationException();
436        }
437    }
438
439    public static CharSequence stringOrSpannedString(CharSequence source) {
440        if (source == null)
441            return null;
442        if (source instanceof SpannedString)
443            return source;
444        if (source instanceof Spanned)
445            return new SpannedString(source);
446
447        return source.toString();
448    }
449
450    /**
451     * Returns true if the string is null or 0-length.
452     * @param str the string to be examined
453     * @return true if str is null or zero length
454     */
455    public static boolean isEmpty(CharSequence str) {
456        if (str == null || str.length() == 0)
457            return true;
458        else
459            return false;
460    }
461
462    /**
463     * Returns the length that the specified CharSequence would have if
464     * spaces and control characters were trimmed from the start and end,
465     * as by {@link String#trim}.
466     */
467    public static int getTrimmedLength(CharSequence s) {
468        int len = s.length();
469
470        int start = 0;
471        while (start < len && s.charAt(start) <= ' ') {
472            start++;
473        }
474
475        int end = len;
476        while (end > start && s.charAt(end - 1) <= ' ') {
477            end--;
478        }
479
480        return end - start;
481    }
482
483    /**
484     * Returns true if a and b are equal, including if they are both null.
485     * <p><i>Note: In platform versions 1.1 and earlier, this method only worked well if
486     * both the arguments were instances of String.</i></p>
487     * @param a first CharSequence to check
488     * @param b second CharSequence to check
489     * @return true if a and b are equal
490     */
491    public static boolean equals(CharSequence a, CharSequence b) {
492        if (a == b) return true;
493        int length;
494        if (a != null && b != null && (length = a.length()) == b.length()) {
495            if (a instanceof String && b instanceof String) {
496                return a.equals(b);
497            } else {
498                for (int i = 0; i < length; i++) {
499                    if (a.charAt(i) != b.charAt(i)) return false;
500                }
501                return true;
502            }
503        }
504        return false;
505    }
506
507    // XXX currently this only reverses chars, not spans
508    public static CharSequence getReverse(CharSequence source,
509                                          int start, int end) {
510        return new Reverser(source, start, end);
511    }
512
513    private static class Reverser
514    implements CharSequence, GetChars
515    {
516        public Reverser(CharSequence source, int start, int end) {
517            mSource = source;
518            mStart = start;
519            mEnd = end;
520        }
521
522        public int length() {
523            return mEnd - mStart;
524        }
525
526        public CharSequence subSequence(int start, int end) {
527            char[] buf = new char[end - start];
528
529            getChars(start, end, buf, 0);
530            return new String(buf);
531        }
532
533        @Override
534        public String toString() {
535            return subSequence(0, length()).toString();
536        }
537
538        public char charAt(int off) {
539            return AndroidCharacter.getMirror(mSource.charAt(mEnd - 1 - off));
540        }
541
542        public void getChars(int start, int end, char[] dest, int destoff) {
543            TextUtils.getChars(mSource, start + mStart, end + mStart,
544                               dest, destoff);
545            AndroidCharacter.mirror(dest, 0, end - start);
546
547            int len = end - start;
548            int n = (end - start) / 2;
549            for (int i = 0; i < n; i++) {
550                char tmp = dest[destoff + i];
551
552                dest[destoff + i] = dest[destoff + len - i - 1];
553                dest[destoff + len - i - 1] = tmp;
554            }
555        }
556
557        private CharSequence mSource;
558        private int mStart;
559        private int mEnd;
560    }
561
562    /** @hide */
563    public static final int ALIGNMENT_SPAN = 1;
564    /** @hide */
565    public static final int FIRST_SPAN = ALIGNMENT_SPAN;
566    /** @hide */
567    public static final int FOREGROUND_COLOR_SPAN = 2;
568    /** @hide */
569    public static final int RELATIVE_SIZE_SPAN = 3;
570    /** @hide */
571    public static final int SCALE_X_SPAN = 4;
572    /** @hide */
573    public static final int STRIKETHROUGH_SPAN = 5;
574    /** @hide */
575    public static final int UNDERLINE_SPAN = 6;
576    /** @hide */
577    public static final int STYLE_SPAN = 7;
578    /** @hide */
579    public static final int BULLET_SPAN = 8;
580    /** @hide */
581    public static final int QUOTE_SPAN = 9;
582    /** @hide */
583    public static final int LEADING_MARGIN_SPAN = 10;
584    /** @hide */
585    public static final int URL_SPAN = 11;
586    /** @hide */
587    public static final int BACKGROUND_COLOR_SPAN = 12;
588    /** @hide */
589    public static final int TYPEFACE_SPAN = 13;
590    /** @hide */
591    public static final int SUPERSCRIPT_SPAN = 14;
592    /** @hide */
593    public static final int SUBSCRIPT_SPAN = 15;
594    /** @hide */
595    public static final int ABSOLUTE_SIZE_SPAN = 16;
596    /** @hide */
597    public static final int TEXT_APPEARANCE_SPAN = 17;
598    /** @hide */
599    public static final int ANNOTATION = 18;
600    /** @hide */
601    public static final int SUGGESTION_SPAN = 19;
602    /** @hide */
603    public static final int SPELL_CHECK_SPAN = 20;
604    /** @hide */
605    public static final int SUGGESTION_RANGE_SPAN = 21;
606    /** @hide */
607    public static final int EASY_EDIT_SPAN = 22;
608    /** @hide */
609    public static final int LOCALE_SPAN = 23;
610    /** @hide */
611    public static final int TTS_SPAN = 24;
612    /** @hide */
613    public static final int LAST_SPAN = TTS_SPAN;
614
615    /**
616     * Flatten a CharSequence and whatever styles can be copied across processes
617     * into the parcel.
618     */
619    public static void writeToParcel(CharSequence cs, Parcel p,
620            int parcelableFlags) {
621        if (cs instanceof Spanned) {
622            p.writeInt(0);
623            p.writeString(cs.toString());
624
625            Spanned sp = (Spanned) cs;
626            Object[] os = sp.getSpans(0, cs.length(), Object.class);
627
628            // note to people adding to this: check more specific types
629            // before more generic types.  also notice that it uses
630            // "if" instead of "else if" where there are interfaces
631            // so one object can be several.
632
633            for (int i = 0; i < os.length; i++) {
634                Object o = os[i];
635                Object prop = os[i];
636
637                if (prop instanceof CharacterStyle) {
638                    prop = ((CharacterStyle) prop).getUnderlying();
639                }
640
641                if (prop instanceof ParcelableSpan) {
642                    ParcelableSpan ps = (ParcelableSpan)prop;
643                    int spanTypeId = ps.getSpanTypeId();
644                    if (spanTypeId < FIRST_SPAN || spanTypeId > LAST_SPAN) {
645                        Log.e(TAG, "external class \"" + ps.getClass().getSimpleName()
646                                + "\" is attempting to use the frameworks-only ParcelableSpan"
647                                + " interface");
648                    } else {
649                        p.writeInt(spanTypeId);
650                        ps.writeToParcel(p, parcelableFlags);
651                        writeWhere(p, sp, o);
652                    }
653                }
654            }
655
656            p.writeInt(0);
657        } else {
658            p.writeInt(1);
659            if (cs != null) {
660                p.writeString(cs.toString());
661            } else {
662                p.writeString(null);
663            }
664        }
665    }
666
667    private static void writeWhere(Parcel p, Spanned sp, Object o) {
668        p.writeInt(sp.getSpanStart(o));
669        p.writeInt(sp.getSpanEnd(o));
670        p.writeInt(sp.getSpanFlags(o));
671    }
672
673    public static final Parcelable.Creator<CharSequence> CHAR_SEQUENCE_CREATOR
674            = new Parcelable.Creator<CharSequence>() {
675        /**
676         * Read and return a new CharSequence, possibly with styles,
677         * from the parcel.
678         */
679        public CharSequence createFromParcel(Parcel p) {
680            int kind = p.readInt();
681
682            String string = p.readString();
683            if (string == null) {
684                return null;
685            }
686
687            if (kind == 1) {
688                return string;
689            }
690
691            SpannableString sp = new SpannableString(string);
692
693            while (true) {
694                kind = p.readInt();
695
696                if (kind == 0)
697                    break;
698
699                switch (kind) {
700                case ALIGNMENT_SPAN:
701                    readSpan(p, sp, new AlignmentSpan.Standard(p));
702                    break;
703
704                case FOREGROUND_COLOR_SPAN:
705                    readSpan(p, sp, new ForegroundColorSpan(p));
706                    break;
707
708                case RELATIVE_SIZE_SPAN:
709                    readSpan(p, sp, new RelativeSizeSpan(p));
710                    break;
711
712                case SCALE_X_SPAN:
713                    readSpan(p, sp, new ScaleXSpan(p));
714                    break;
715
716                case STRIKETHROUGH_SPAN:
717                    readSpan(p, sp, new StrikethroughSpan(p));
718                    break;
719
720                case UNDERLINE_SPAN:
721                    readSpan(p, sp, new UnderlineSpan(p));
722                    break;
723
724                case STYLE_SPAN:
725                    readSpan(p, sp, new StyleSpan(p));
726                    break;
727
728                case BULLET_SPAN:
729                    readSpan(p, sp, new BulletSpan(p));
730                    break;
731
732                case QUOTE_SPAN:
733                    readSpan(p, sp, new QuoteSpan(p));
734                    break;
735
736                case LEADING_MARGIN_SPAN:
737                    readSpan(p, sp, new LeadingMarginSpan.Standard(p));
738                break;
739
740                case URL_SPAN:
741                    readSpan(p, sp, new URLSpan(p));
742                    break;
743
744                case BACKGROUND_COLOR_SPAN:
745                    readSpan(p, sp, new BackgroundColorSpan(p));
746                    break;
747
748                case TYPEFACE_SPAN:
749                    readSpan(p, sp, new TypefaceSpan(p));
750                    break;
751
752                case SUPERSCRIPT_SPAN:
753                    readSpan(p, sp, new SuperscriptSpan(p));
754                    break;
755
756                case SUBSCRIPT_SPAN:
757                    readSpan(p, sp, new SubscriptSpan(p));
758                    break;
759
760                case ABSOLUTE_SIZE_SPAN:
761                    readSpan(p, sp, new AbsoluteSizeSpan(p));
762                    break;
763
764                case TEXT_APPEARANCE_SPAN:
765                    readSpan(p, sp, new TextAppearanceSpan(p));
766                    break;
767
768                case ANNOTATION:
769                    readSpan(p, sp, new Annotation(p));
770                    break;
771
772                case SUGGESTION_SPAN:
773                    readSpan(p, sp, new SuggestionSpan(p));
774                    break;
775
776                case SPELL_CHECK_SPAN:
777                    readSpan(p, sp, new SpellCheckSpan(p));
778                    break;
779
780                case SUGGESTION_RANGE_SPAN:
781                    readSpan(p, sp, new SuggestionRangeSpan(p));
782                    break;
783
784                case EASY_EDIT_SPAN:
785                    readSpan(p, sp, new EasyEditSpan(p));
786                    break;
787
788                case LOCALE_SPAN:
789                    readSpan(p, sp, new LocaleSpan(p));
790                    break;
791
792                case TTS_SPAN:
793                    readSpan(p, sp, new TtsSpan(p));
794                    break;
795
796                default:
797                    throw new RuntimeException("bogus span encoding " + kind);
798                }
799            }
800
801            return sp;
802        }
803
804        public CharSequence[] newArray(int size)
805        {
806            return new CharSequence[size];
807        }
808    };
809
810    /**
811     * Debugging tool to print the spans in a CharSequence.  The output will
812     * be printed one span per line.  If the CharSequence is not a Spanned,
813     * then the entire string will be printed on a single line.
814     */
815    public static void dumpSpans(CharSequence cs, Printer printer, String prefix) {
816        if (cs instanceof Spanned) {
817            Spanned sp = (Spanned) cs;
818            Object[] os = sp.getSpans(0, cs.length(), Object.class);
819
820            for (int i = 0; i < os.length; i++) {
821                Object o = os[i];
822                printer.println(prefix + cs.subSequence(sp.getSpanStart(o),
823                        sp.getSpanEnd(o)) + ": "
824                        + Integer.toHexString(System.identityHashCode(o))
825                        + " " + o.getClass().getCanonicalName()
826                         + " (" + sp.getSpanStart(o) + "-" + sp.getSpanEnd(o)
827                         + ") fl=#" + sp.getSpanFlags(o));
828            }
829        } else {
830            printer.println(prefix + cs + ": (no spans)");
831        }
832    }
833
834    /**
835     * Return a new CharSequence in which each of the source strings is
836     * replaced by the corresponding element of the destinations.
837     */
838    public static CharSequence replace(CharSequence template,
839                                       String[] sources,
840                                       CharSequence[] destinations) {
841        SpannableStringBuilder tb = new SpannableStringBuilder(template);
842
843        for (int i = 0; i < sources.length; i++) {
844            int where = indexOf(tb, sources[i]);
845
846            if (where >= 0)
847                tb.setSpan(sources[i], where, where + sources[i].length(),
848                           Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
849        }
850
851        for (int i = 0; i < sources.length; i++) {
852            int start = tb.getSpanStart(sources[i]);
853            int end = tb.getSpanEnd(sources[i]);
854
855            if (start >= 0) {
856                tb.replace(start, end, destinations[i]);
857            }
858        }
859
860        return tb;
861    }
862
863    /**
864     * Replace instances of "^1", "^2", etc. in the
865     * <code>template</code> CharSequence with the corresponding
866     * <code>values</code>.  "^^" is used to produce a single caret in
867     * the output.  Only up to 9 replacement values are supported,
868     * "^10" will be produce the first replacement value followed by a
869     * '0'.
870     *
871     * @param template the input text containing "^1"-style
872     * placeholder values.  This object is not modified; a copy is
873     * returned.
874     *
875     * @param values CharSequences substituted into the template.  The
876     * first is substituted for "^1", the second for "^2", and so on.
877     *
878     * @return the new CharSequence produced by doing the replacement
879     *
880     * @throws IllegalArgumentException if the template requests a
881     * value that was not provided, or if more than 9 values are
882     * provided.
883     */
884    public static CharSequence expandTemplate(CharSequence template,
885                                              CharSequence... values) {
886        if (values.length > 9) {
887            throw new IllegalArgumentException("max of 9 values are supported");
888        }
889
890        SpannableStringBuilder ssb = new SpannableStringBuilder(template);
891
892        try {
893            int i = 0;
894            while (i < ssb.length()) {
895                if (ssb.charAt(i) == '^') {
896                    char next = ssb.charAt(i+1);
897                    if (next == '^') {
898                        ssb.delete(i+1, i+2);
899                        ++i;
900                        continue;
901                    } else if (Character.isDigit(next)) {
902                        int which = Character.getNumericValue(next) - 1;
903                        if (which < 0) {
904                            throw new IllegalArgumentException(
905                                "template requests value ^" + (which+1));
906                        }
907                        if (which >= values.length) {
908                            throw new IllegalArgumentException(
909                                "template requests value ^" + (which+1) +
910                                "; only " + values.length + " provided");
911                        }
912                        ssb.replace(i, i+2, values[which]);
913                        i += values[which].length();
914                        continue;
915                    }
916                }
917                ++i;
918            }
919        } catch (IndexOutOfBoundsException ignore) {
920            // happens when ^ is the last character in the string.
921        }
922        return ssb;
923    }
924
925    public static int getOffsetBefore(CharSequence text, int offset) {
926        if (offset == 0)
927            return 0;
928        if (offset == 1)
929            return 0;
930
931        char c = text.charAt(offset - 1);
932
933        if (c >= '\uDC00' && c <= '\uDFFF') {
934            char c1 = text.charAt(offset - 2);
935
936            if (c1 >= '\uD800' && c1 <= '\uDBFF')
937                offset -= 2;
938            else
939                offset -= 1;
940        } else {
941            offset -= 1;
942        }
943
944        if (text instanceof Spanned) {
945            ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
946                                                       ReplacementSpan.class);
947
948            for (int i = 0; i < spans.length; i++) {
949                int start = ((Spanned) text).getSpanStart(spans[i]);
950                int end = ((Spanned) text).getSpanEnd(spans[i]);
951
952                if (start < offset && end > offset)
953                    offset = start;
954            }
955        }
956
957        return offset;
958    }
959
960    public static int getOffsetAfter(CharSequence text, int offset) {
961        int len = text.length();
962
963        if (offset == len)
964            return len;
965        if (offset == len - 1)
966            return len;
967
968        char c = text.charAt(offset);
969
970        if (c >= '\uD800' && c <= '\uDBFF') {
971            char c1 = text.charAt(offset + 1);
972
973            if (c1 >= '\uDC00' && c1 <= '\uDFFF')
974                offset += 2;
975            else
976                offset += 1;
977        } else {
978            offset += 1;
979        }
980
981        if (text instanceof Spanned) {
982            ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
983                                                       ReplacementSpan.class);
984
985            for (int i = 0; i < spans.length; i++) {
986                int start = ((Spanned) text).getSpanStart(spans[i]);
987                int end = ((Spanned) text).getSpanEnd(spans[i]);
988
989                if (start < offset && end > offset)
990                    offset = end;
991            }
992        }
993
994        return offset;
995    }
996
997    private static void readSpan(Parcel p, Spannable sp, Object o) {
998        sp.setSpan(o, p.readInt(), p.readInt(), p.readInt());
999    }
1000
1001    /**
1002     * Copies the spans from the region <code>start...end</code> in
1003     * <code>source</code> to the region
1004     * <code>destoff...destoff+end-start</code> in <code>dest</code>.
1005     * Spans in <code>source</code> that begin before <code>start</code>
1006     * or end after <code>end</code> but overlap this range are trimmed
1007     * as if they began at <code>start</code> or ended at <code>end</code>.
1008     *
1009     * @throws IndexOutOfBoundsException if any of the copied spans
1010     * are out of range in <code>dest</code>.
1011     */
1012    public static void copySpansFrom(Spanned source, int start, int end,
1013                                     Class kind,
1014                                     Spannable dest, int destoff) {
1015        if (kind == null) {
1016            kind = Object.class;
1017        }
1018
1019        Object[] spans = source.getSpans(start, end, kind);
1020
1021        for (int i = 0; i < spans.length; i++) {
1022            int st = source.getSpanStart(spans[i]);
1023            int en = source.getSpanEnd(spans[i]);
1024            int fl = source.getSpanFlags(spans[i]);
1025
1026            if (st < start)
1027                st = start;
1028            if (en > end)
1029                en = end;
1030
1031            dest.setSpan(spans[i], st - start + destoff, en - start + destoff,
1032                         fl);
1033        }
1034    }
1035
1036    public enum TruncateAt {
1037        START,
1038        MIDDLE,
1039        END,
1040        MARQUEE,
1041        /**
1042         * @hide
1043         */
1044        END_SMALL
1045    }
1046
1047    public interface EllipsizeCallback {
1048        /**
1049         * This method is called to report that the specified region of
1050         * text was ellipsized away by a call to {@link #ellipsize}.
1051         */
1052        public void ellipsized(int start, int end);
1053    }
1054
1055    /**
1056     * Returns the original text if it fits in the specified width
1057     * given the properties of the specified Paint,
1058     * or, if it does not fit, a truncated
1059     * copy with ellipsis character added at the specified edge or center.
1060     */
1061    public static CharSequence ellipsize(CharSequence text,
1062                                         TextPaint p,
1063                                         float avail, TruncateAt where) {
1064        return ellipsize(text, p, avail, where, false, null);
1065    }
1066
1067    /**
1068     * Returns the original text if it fits in the specified width
1069     * given the properties of the specified Paint,
1070     * or, if it does not fit, a copy with ellipsis character added
1071     * at the specified edge or center.
1072     * If <code>preserveLength</code> is specified, the returned copy
1073     * will be padded with zero-width spaces to preserve the original
1074     * length and offsets instead of truncating.
1075     * If <code>callback</code> is non-null, it will be called to
1076     * report the start and end of the ellipsized range.  TextDirection
1077     * is determined by the first strong directional character.
1078     */
1079    public static CharSequence ellipsize(CharSequence text,
1080                                         TextPaint paint,
1081                                         float avail, TruncateAt where,
1082                                         boolean preserveLength,
1083                                         EllipsizeCallback callback) {
1084
1085        final String ellipsis = (where == TruncateAt.END_SMALL) ?
1086                Resources.getSystem().getString(R.string.ellipsis_two_dots) :
1087                Resources.getSystem().getString(R.string.ellipsis);
1088
1089        return ellipsize(text, paint, avail, where, preserveLength, callback,
1090                TextDirectionHeuristics.FIRSTSTRONG_LTR,
1091                ellipsis);
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