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.annotation.Nullable;
20import android.content.res.Resources;
21import android.icu.util.ULocale;
22import android.os.Parcel;
23import android.os.Parcelable;
24import android.os.SystemProperties;
25import android.provider.Settings;
26import android.text.style.AbsoluteSizeSpan;
27import android.text.style.AlignmentSpan;
28import android.text.style.BackgroundColorSpan;
29import android.text.style.BulletSpan;
30import android.text.style.CharacterStyle;
31import android.text.style.EasyEditSpan;
32import android.text.style.ForegroundColorSpan;
33import android.text.style.LeadingMarginSpan;
34import android.text.style.LocaleSpan;
35import android.text.style.MetricAffectingSpan;
36import android.text.style.QuoteSpan;
37import android.text.style.RelativeSizeSpan;
38import android.text.style.ReplacementSpan;
39import android.text.style.ScaleXSpan;
40import android.text.style.SpellCheckSpan;
41import android.text.style.StrikethroughSpan;
42import android.text.style.StyleSpan;
43import android.text.style.SubscriptSpan;
44import android.text.style.SuggestionRangeSpan;
45import android.text.style.SuggestionSpan;
46import android.text.style.SuperscriptSpan;
47import android.text.style.TextAppearanceSpan;
48import android.text.style.TtsSpan;
49import android.text.style.TypefaceSpan;
50import android.text.style.URLSpan;
51import android.text.style.UnderlineSpan;
52import android.util.Log;
53import android.util.Printer;
54import android.view.View;
55
56import com.android.internal.R;
57import com.android.internal.util.ArrayUtils;
58
59import libcore.icu.ICU;
60
61import java.lang.reflect.Array;
62import java.util.Iterator;
63import java.util.Locale;
64import java.util.regex.Pattern;
65
66public class TextUtils {
67    private static final String TAG = "TextUtils";
68
69    /* package */ static final char[] ELLIPSIS_NORMAL = { '\u2026' }; // this is "..."
70    /** {@hide} */
71    public static final String ELLIPSIS_STRING = new String(ELLIPSIS_NORMAL);
72
73    /* package */ static final char[] ELLIPSIS_TWO_DOTS = { '\u2025' }; // this is ".."
74    private static final String ELLIPSIS_TWO_DOTS_STRING = new String(ELLIPSIS_TWO_DOTS);
75
76    private TextUtils() { /* cannot be instantiated */ }
77
78    public static void getChars(CharSequence s, int start, int end,
79                                char[] dest, int destoff) {
80        Class<? extends CharSequence> c = s.getClass();
81
82        if (c == String.class)
83            ((String) s).getChars(start, end, dest, destoff);
84        else if (c == StringBuffer.class)
85            ((StringBuffer) s).getChars(start, end, dest, destoff);
86        else if (c == StringBuilder.class)
87            ((StringBuilder) s).getChars(start, end, dest, destoff);
88        else if (s instanceof GetChars)
89            ((GetChars) s).getChars(start, end, dest, destoff);
90        else {
91            for (int i = start; i < end; i++)
92                dest[destoff++] = s.charAt(i);
93        }
94    }
95
96    public static int indexOf(CharSequence s, char ch) {
97        return indexOf(s, ch, 0);
98    }
99
100    public static int indexOf(CharSequence s, char ch, int start) {
101        Class<? extends CharSequence> c = s.getClass();
102
103        if (c == String.class)
104            return ((String) s).indexOf(ch, start);
105
106        return indexOf(s, ch, start, s.length());
107    }
108
109    public static int indexOf(CharSequence s, char ch, int start, int end) {
110        Class<? extends CharSequence> c = s.getClass();
111
112        if (s instanceof GetChars || c == StringBuffer.class ||
113            c == StringBuilder.class || c == String.class) {
114            final int INDEX_INCREMENT = 500;
115            char[] temp = obtain(INDEX_INCREMENT);
116
117            while (start < end) {
118                int segend = start + INDEX_INCREMENT;
119                if (segend > end)
120                    segend = end;
121
122                getChars(s, start, segend, temp, 0);
123
124                int count = segend - start;
125                for (int i = 0; i < count; i++) {
126                    if (temp[i] == ch) {
127                        recycle(temp);
128                        return i + start;
129                    }
130                }
131
132                start = segend;
133            }
134
135            recycle(temp);
136            return -1;
137        }
138
139        for (int i = start; i < end; i++)
140            if (s.charAt(i) == ch)
141                return i;
142
143        return -1;
144    }
145
146    public static int lastIndexOf(CharSequence s, char ch) {
147        return lastIndexOf(s, ch, s.length() - 1);
148    }
149
150    public static int lastIndexOf(CharSequence s, char ch, int last) {
151        Class<? extends CharSequence> c = s.getClass();
152
153        if (c == String.class)
154            return ((String) s).lastIndexOf(ch, last);
155
156        return lastIndexOf(s, ch, 0, last);
157    }
158
159    public static int lastIndexOf(CharSequence s, char ch,
160                                  int start, int last) {
161        if (last < 0)
162            return -1;
163        if (last >= s.length())
164            last = s.length() - 1;
165
166        int end = last + 1;
167
168        Class<? extends CharSequence> c = s.getClass();
169
170        if (s instanceof GetChars || c == StringBuffer.class ||
171            c == StringBuilder.class || c == String.class) {
172            final int INDEX_INCREMENT = 500;
173            char[] temp = obtain(INDEX_INCREMENT);
174
175            while (start < end) {
176                int segstart = end - INDEX_INCREMENT;
177                if (segstart < start)
178                    segstart = start;
179
180                getChars(s, segstart, end, temp, 0);
181
182                int count = end - segstart;
183                for (int i = count - 1; i >= 0; i--) {
184                    if (temp[i] == ch) {
185                        recycle(temp);
186                        return i + segstart;
187                    }
188                }
189
190                end = segstart;
191            }
192
193            recycle(temp);
194            return -1;
195        }
196
197        for (int i = end - 1; i >= start; i--)
198            if (s.charAt(i) == ch)
199                return i;
200
201        return -1;
202    }
203
204    public static int indexOf(CharSequence s, CharSequence needle) {
205        return indexOf(s, needle, 0, s.length());
206    }
207
208    public static int indexOf(CharSequence s, CharSequence needle, int start) {
209        return indexOf(s, needle, start, s.length());
210    }
211
212    public static int indexOf(CharSequence s, CharSequence needle,
213                              int start, int end) {
214        int nlen = needle.length();
215        if (nlen == 0)
216            return start;
217
218        char c = needle.charAt(0);
219
220        for (;;) {
221            start = indexOf(s, c, start);
222            if (start > end - nlen) {
223                break;
224            }
225
226            if (start < 0) {
227                return -1;
228            }
229
230            if (regionMatches(s, start, needle, 0, nlen)) {
231                return start;
232            }
233
234            start++;
235        }
236        return -1;
237    }
238
239    public static boolean regionMatches(CharSequence one, int toffset,
240                                        CharSequence two, int ooffset,
241                                        int len) {
242        int tempLen = 2 * len;
243        if (tempLen < len) {
244            // Integer overflow; len is unreasonably large
245            throw new IndexOutOfBoundsException();
246        }
247        char[] temp = obtain(tempLen);
248
249        getChars(one, toffset, toffset + len, temp, 0);
250        getChars(two, ooffset, ooffset + len, temp, len);
251
252        boolean match = true;
253        for (int i = 0; i < len; i++) {
254            if (temp[i] != temp[i + len]) {
255                match = false;
256                break;
257            }
258        }
259
260        recycle(temp);
261        return match;
262    }
263
264    /**
265     * Create a new String object containing the given range of characters
266     * from the source string.  This is different than simply calling
267     * {@link CharSequence#subSequence(int, int) CharSequence.subSequence}
268     * in that it does not preserve any style runs in the source sequence,
269     * allowing a more efficient implementation.
270     */
271    public static String substring(CharSequence source, int start, int end) {
272        if (source instanceof String)
273            return ((String) source).substring(start, end);
274        if (source instanceof StringBuilder)
275            return ((StringBuilder) source).substring(start, end);
276        if (source instanceof StringBuffer)
277            return ((StringBuffer) source).substring(start, end);
278
279        char[] temp = obtain(end - start);
280        getChars(source, start, end, temp, 0);
281        String ret = new String(temp, 0, end - start);
282        recycle(temp);
283
284        return ret;
285    }
286
287    /**
288     * Returns a string containing the tokens joined by delimiters.
289     * @param tokens an array objects to be joined. Strings will be formed from
290     *     the objects by calling object.toString().
291     */
292    public static String join(CharSequence delimiter, Object[] tokens) {
293        StringBuilder sb = new StringBuilder();
294        boolean firstTime = true;
295        for (Object token: tokens) {
296            if (firstTime) {
297                firstTime = false;
298            } else {
299                sb.append(delimiter);
300            }
301            sb.append(token);
302        }
303        return sb.toString();
304    }
305
306    /**
307     * Returns a string containing the tokens joined by delimiters.
308     * @param tokens an array objects to be joined. Strings will be formed from
309     *     the objects by calling object.toString().
310     */
311    public static String join(CharSequence delimiter, Iterable tokens) {
312        StringBuilder sb = new StringBuilder();
313        Iterator<?> it = tokens.iterator();
314        if (it.hasNext()) {
315            sb.append(it.next());
316            while (it.hasNext()) {
317                sb.append(delimiter);
318                sb.append(it.next());
319            }
320        }
321        return sb.toString();
322    }
323
324    /**
325     * String.split() returns [''] when the string to be split is empty. This returns []. This does
326     * not remove any empty strings from the result. For example split("a,", ","  ) returns {"a", ""}.
327     *
328     * @param text the string to split
329     * @param expression the regular expression to match
330     * @return an array of strings. The array will be empty if text is empty
331     *
332     * @throws NullPointerException if expression or text is null
333     */
334    public static String[] split(String text, String expression) {
335        if (text.length() == 0) {
336            return EMPTY_STRING_ARRAY;
337        } else {
338            return text.split(expression, -1);
339        }
340    }
341
342    /**
343     * Splits a string on a pattern. String.split() returns [''] when the string to be
344     * split is empty. This returns []. This does not remove any empty strings from the result.
345     * @param text the string to split
346     * @param pattern the regular expression to match
347     * @return an array of strings. The array will be empty if text is empty
348     *
349     * @throws NullPointerException if expression or text is null
350     */
351    public static String[] split(String text, Pattern pattern) {
352        if (text.length() == 0) {
353            return EMPTY_STRING_ARRAY;
354        } else {
355            return pattern.split(text, -1);
356        }
357    }
358
359    /**
360     * An interface for splitting strings according to rules that are opaque to the user of this
361     * interface. This also has less overhead than split, which uses regular expressions and
362     * allocates an array to hold the results.
363     *
364     * <p>The most efficient way to use this class is:
365     *
366     * <pre>
367     * // Once
368     * TextUtils.StringSplitter splitter = new TextUtils.SimpleStringSplitter(delimiter);
369     *
370     * // Once per string to split
371     * splitter.setString(string);
372     * for (String s : splitter) {
373     *     ...
374     * }
375     * </pre>
376     */
377    public interface StringSplitter extends Iterable<String> {
378        public void setString(String string);
379    }
380
381    /**
382     * A simple string splitter.
383     *
384     * <p>If the final character in the string to split is the delimiter then no empty string will
385     * be returned for the empty string after that delimeter. That is, splitting <tt>"a,b,"</tt> on
386     * comma will return <tt>"a", "b"</tt>, not <tt>"a", "b", ""</tt>.
387     */
388    public static class SimpleStringSplitter implements StringSplitter, Iterator<String> {
389        private String mString;
390        private char mDelimiter;
391        private int mPosition;
392        private int mLength;
393
394        /**
395         * Initializes the splitter. setString may be called later.
396         * @param delimiter the delimeter on which to split
397         */
398        public SimpleStringSplitter(char delimiter) {
399            mDelimiter = delimiter;
400        }
401
402        /**
403         * Sets the string to split
404         * @param string the string to split
405         */
406        public void setString(String string) {
407            mString = string;
408            mPosition = 0;
409            mLength = mString.length();
410        }
411
412        public Iterator<String> iterator() {
413            return this;
414        }
415
416        public boolean hasNext() {
417            return mPosition < mLength;
418        }
419
420        public String next() {
421            int end = mString.indexOf(mDelimiter, mPosition);
422            if (end == -1) {
423                end = mLength;
424            }
425            String nextString = mString.substring(mPosition, end);
426            mPosition = end + 1; // Skip the delimiter.
427            return nextString;
428        }
429
430        public void remove() {
431            throw new UnsupportedOperationException();
432        }
433    }
434
435    public static CharSequence stringOrSpannedString(CharSequence source) {
436        if (source == null)
437            return null;
438        if (source instanceof SpannedString)
439            return source;
440        if (source instanceof Spanned)
441            return new SpannedString(source);
442
443        return source.toString();
444    }
445
446    /**
447     * Returns true if the string is null or 0-length.
448     * @param str the string to be examined
449     * @return true if str is null or zero length
450     */
451    public static boolean isEmpty(@Nullable CharSequence str) {
452        if (str == null || str.length() == 0)
453            return true;
454        else
455            return false;
456    }
457
458    /** {@hide} */
459    public static String nullIfEmpty(@Nullable String str) {
460        return isEmpty(str) ? null : str;
461    }
462
463    /**
464     * Returns the length that the specified CharSequence would have if
465     * spaces and ASCII control characters were trimmed from the start and end,
466     * as by {@link String#trim}.
467     */
468    public static int getTrimmedLength(CharSequence s) {
469        int len = s.length();
470
471        int start = 0;
472        while (start < len && s.charAt(start) <= ' ') {
473            start++;
474        }
475
476        int end = len;
477        while (end > start && s.charAt(end - 1) <= ' ') {
478            end--;
479        }
480
481        return end - start;
482    }
483
484    /**
485     * Returns true if a and b are equal, including if they are both null.
486     * <p><i>Note: In platform versions 1.1 and earlier, this method only worked well if
487     * both the arguments were instances of String.</i></p>
488     * @param a first CharSequence to check
489     * @param b second CharSequence to check
490     * @return true if a and b are equal
491     */
492    public static boolean equals(CharSequence a, CharSequence b) {
493        if (a == b) return true;
494        int length;
495        if (a != null && b != null && (length = a.length()) == b.length()) {
496            if (a instanceof String && b instanceof String) {
497                return a.equals(b);
498            } else {
499                for (int i = 0; i < length; i++) {
500                    if (a.charAt(i) != b.charAt(i)) return false;
501                }
502                return true;
503            }
504        }
505        return false;
506    }
507
508    /**
509     * This function only reverses individual {@code char}s and not their associated
510     * spans. It doesn't support surrogate pairs (that correspond to non-BMP code points), combining
511     * sequences or conjuncts either.
512     * @deprecated Do not use.
513     */
514    @Deprecated
515    public static CharSequence getReverse(CharSequence source, int start, int end) {
516        return new Reverser(source, start, end);
517    }
518
519    private static class Reverser
520    implements CharSequence, GetChars
521    {
522        public Reverser(CharSequence source, int start, int end) {
523            mSource = source;
524            mStart = start;
525            mEnd = end;
526        }
527
528        public int length() {
529            return mEnd - mStart;
530        }
531
532        public CharSequence subSequence(int start, int end) {
533            char[] buf = new char[end - start];
534
535            getChars(start, end, buf, 0);
536            return new String(buf);
537        }
538
539        @Override
540        public String toString() {
541            return subSequence(0, length()).toString();
542        }
543
544        public char charAt(int off) {
545            return AndroidCharacter.getMirror(mSource.charAt(mEnd - 1 - off));
546        }
547
548        public void getChars(int start, int end, char[] dest, int destoff) {
549            TextUtils.getChars(mSource, start + mStart, end + mStart,
550                               dest, destoff);
551            AndroidCharacter.mirror(dest, 0, end - start);
552
553            int len = end - start;
554            int n = (end - start) / 2;
555            for (int i = 0; i < n; i++) {
556                char tmp = dest[destoff + i];
557
558                dest[destoff + i] = dest[destoff + len - i - 1];
559                dest[destoff + len - i - 1] = tmp;
560            }
561        }
562
563        private CharSequence mSource;
564        private int mStart;
565        private int mEnd;
566    }
567
568    /** @hide */
569    public static final int ALIGNMENT_SPAN = 1;
570    /** @hide */
571    public static final int FIRST_SPAN = ALIGNMENT_SPAN;
572    /** @hide */
573    public static final int FOREGROUND_COLOR_SPAN = 2;
574    /** @hide */
575    public static final int RELATIVE_SIZE_SPAN = 3;
576    /** @hide */
577    public static final int SCALE_X_SPAN = 4;
578    /** @hide */
579    public static final int STRIKETHROUGH_SPAN = 5;
580    /** @hide */
581    public static final int UNDERLINE_SPAN = 6;
582    /** @hide */
583    public static final int STYLE_SPAN = 7;
584    /** @hide */
585    public static final int BULLET_SPAN = 8;
586    /** @hide */
587    public static final int QUOTE_SPAN = 9;
588    /** @hide */
589    public static final int LEADING_MARGIN_SPAN = 10;
590    /** @hide */
591    public static final int URL_SPAN = 11;
592    /** @hide */
593    public static final int BACKGROUND_COLOR_SPAN = 12;
594    /** @hide */
595    public static final int TYPEFACE_SPAN = 13;
596    /** @hide */
597    public static final int SUPERSCRIPT_SPAN = 14;
598    /** @hide */
599    public static final int SUBSCRIPT_SPAN = 15;
600    /** @hide */
601    public static final int ABSOLUTE_SIZE_SPAN = 16;
602    /** @hide */
603    public static final int TEXT_APPEARANCE_SPAN = 17;
604    /** @hide */
605    public static final int ANNOTATION = 18;
606    /** @hide */
607    public static final int SUGGESTION_SPAN = 19;
608    /** @hide */
609    public static final int SPELL_CHECK_SPAN = 20;
610    /** @hide */
611    public static final int SUGGESTION_RANGE_SPAN = 21;
612    /** @hide */
613    public static final int EASY_EDIT_SPAN = 22;
614    /** @hide */
615    public static final int LOCALE_SPAN = 23;
616    /** @hide */
617    public static final int TTS_SPAN = 24;
618    /** @hide */
619    public static final int LAST_SPAN = TTS_SPAN;
620
621    /**
622     * Flatten a CharSequence and whatever styles can be copied across processes
623     * into the parcel.
624     */
625    public static void writeToParcel(CharSequence cs, Parcel p, 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                    final ParcelableSpan ps = (ParcelableSpan) prop;
648                    final int spanTypeId = ps.getSpanTypeIdInternal();
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.writeToParcelInternal(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, null);
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, null);
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 cp, i=0; i<len; i+=Character.charCount(cp)) {
1439            cp = Character.codePointAt(str, i);
1440            int gc = Character.getType(cp);
1441            if (gc != Character.CONTROL
1442                    && gc != Character.FORMAT
1443                    && gc != Character.SURROGATE
1444                    && gc != Character.UNASSIGNED
1445                    && gc != Character.LINE_SEPARATOR
1446                    && gc != Character.PARAGRAPH_SEPARATOR
1447                    && gc != Character.SPACE_SEPARATOR) {
1448                return true;
1449            }
1450        }
1451        return false;
1452    }
1453
1454    /**
1455     * Returns whether this character is a printable character.
1456     *
1457     * This does not support non-BMP characters and should not be used.
1458     *
1459     * @deprecated Use {@link #isGraphic(CharSequence)} instead.
1460     */
1461    @Deprecated
1462    public static boolean isGraphic(char c) {
1463        int gc = Character.getType(c);
1464        return     gc != Character.CONTROL
1465                && gc != Character.FORMAT
1466                && gc != Character.SURROGATE
1467                && gc != Character.UNASSIGNED
1468                && gc != Character.LINE_SEPARATOR
1469                && gc != Character.PARAGRAPH_SEPARATOR
1470                && gc != Character.SPACE_SEPARATOR;
1471    }
1472
1473    /**
1474     * Returns whether the given CharSequence contains only digits.
1475     */
1476    public static boolean isDigitsOnly(CharSequence str) {
1477        final int len = str.length();
1478        for (int cp, i = 0; i < len; i += Character.charCount(cp)) {
1479            cp = Character.codePointAt(str, i);
1480            if (!Character.isDigit(cp)) {
1481                return false;
1482            }
1483        }
1484        return true;
1485    }
1486
1487    /**
1488     * @hide
1489     */
1490    public static boolean isPrintableAscii(final char c) {
1491        final int asciiFirst = 0x20;
1492        final int asciiLast = 0x7E;  // included
1493        return (asciiFirst <= c && c <= asciiLast) || c == '\r' || c == '\n';
1494    }
1495
1496    /**
1497     * @hide
1498     */
1499    public static boolean isPrintableAsciiOnly(final CharSequence str) {
1500        final int len = str.length();
1501        for (int i = 0; i < len; i++) {
1502            if (!isPrintableAscii(str.charAt(i))) {
1503                return false;
1504            }
1505        }
1506        return true;
1507    }
1508
1509    /**
1510     * Capitalization mode for {@link #getCapsMode}: capitalize all
1511     * characters.  This value is explicitly defined to be the same as
1512     * {@link InputType#TYPE_TEXT_FLAG_CAP_CHARACTERS}.
1513     */
1514    public static final int CAP_MODE_CHARACTERS
1515            = InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS;
1516
1517    /**
1518     * Capitalization mode for {@link #getCapsMode}: capitalize the first
1519     * character of all words.  This value is explicitly defined to be the same as
1520     * {@link InputType#TYPE_TEXT_FLAG_CAP_WORDS}.
1521     */
1522    public static final int CAP_MODE_WORDS
1523            = InputType.TYPE_TEXT_FLAG_CAP_WORDS;
1524
1525    /**
1526     * Capitalization mode for {@link #getCapsMode}: capitalize the first
1527     * character of each sentence.  This value is explicitly defined to be the same as
1528     * {@link InputType#TYPE_TEXT_FLAG_CAP_SENTENCES}.
1529     */
1530    public static final int CAP_MODE_SENTENCES
1531            = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
1532
1533    /**
1534     * Determine what caps mode should be in effect at the current offset in
1535     * the text.  Only the mode bits set in <var>reqModes</var> will be
1536     * checked.  Note that the caps mode flags here are explicitly defined
1537     * to match those in {@link InputType}.
1538     *
1539     * @param cs The text that should be checked for caps modes.
1540     * @param off Location in the text at which to check.
1541     * @param reqModes The modes to be checked: may be any combination of
1542     * {@link #CAP_MODE_CHARACTERS}, {@link #CAP_MODE_WORDS}, and
1543     * {@link #CAP_MODE_SENTENCES}.
1544     *
1545     * @return Returns the actual capitalization modes that can be in effect
1546     * at the current position, which is any combination of
1547     * {@link #CAP_MODE_CHARACTERS}, {@link #CAP_MODE_WORDS}, and
1548     * {@link #CAP_MODE_SENTENCES}.
1549     */
1550    public static int getCapsMode(CharSequence cs, int off, int reqModes) {
1551        if (off < 0) {
1552            return 0;
1553        }
1554
1555        int i;
1556        char c;
1557        int mode = 0;
1558
1559        if ((reqModes&CAP_MODE_CHARACTERS) != 0) {
1560            mode |= CAP_MODE_CHARACTERS;
1561        }
1562        if ((reqModes&(CAP_MODE_WORDS|CAP_MODE_SENTENCES)) == 0) {
1563            return mode;
1564        }
1565
1566        // Back over allowed opening punctuation.
1567
1568        for (i = off; i > 0; i--) {
1569            c = cs.charAt(i - 1);
1570
1571            if (c != '"' && c != '\'' &&
1572                Character.getType(c) != Character.START_PUNCTUATION) {
1573                break;
1574            }
1575        }
1576
1577        // Start of paragraph, with optional whitespace.
1578
1579        int j = i;
1580        while (j > 0 && ((c = cs.charAt(j - 1)) == ' ' || c == '\t')) {
1581            j--;
1582        }
1583        if (j == 0 || cs.charAt(j - 1) == '\n') {
1584            return mode | CAP_MODE_WORDS;
1585        }
1586
1587        // Or start of word if we are that style.
1588
1589        if ((reqModes&CAP_MODE_SENTENCES) == 0) {
1590            if (i != j) mode |= CAP_MODE_WORDS;
1591            return mode;
1592        }
1593
1594        // There must be a space if not the start of paragraph.
1595
1596        if (i == j) {
1597            return mode;
1598        }
1599
1600        // Back over allowed closing punctuation.
1601
1602        for (; j > 0; j--) {
1603            c = cs.charAt(j - 1);
1604
1605            if (c != '"' && c != '\'' &&
1606                Character.getType(c) != Character.END_PUNCTUATION) {
1607                break;
1608            }
1609        }
1610
1611        if (j > 0) {
1612            c = cs.charAt(j - 1);
1613
1614            if (c == '.' || c == '?' || c == '!') {
1615                // Do not capitalize if the word ends with a period but
1616                // also contains a period, in which case it is an abbreviation.
1617
1618                if (c == '.') {
1619                    for (int k = j - 2; k >= 0; k--) {
1620                        c = cs.charAt(k);
1621
1622                        if (c == '.') {
1623                            return mode;
1624                        }
1625
1626                        if (!Character.isLetter(c)) {
1627                            break;
1628                        }
1629                    }
1630                }
1631
1632                return mode | CAP_MODE_SENTENCES;
1633            }
1634        }
1635
1636        return mode;
1637    }
1638
1639    /**
1640     * Does a comma-delimited list 'delimitedString' contain a certain item?
1641     * (without allocating memory)
1642     *
1643     * @hide
1644     */
1645    public static boolean delimitedStringContains(
1646            String delimitedString, char delimiter, String item) {
1647        if (isEmpty(delimitedString) || isEmpty(item)) {
1648            return false;
1649        }
1650        int pos = -1;
1651        int length = delimitedString.length();
1652        while ((pos = delimitedString.indexOf(item, pos + 1)) != -1) {
1653            if (pos > 0 && delimitedString.charAt(pos - 1) != delimiter) {
1654                continue;
1655            }
1656            int expectedDelimiterPos = pos + item.length();
1657            if (expectedDelimiterPos == length) {
1658                // Match at end of string.
1659                return true;
1660            }
1661            if (delimitedString.charAt(expectedDelimiterPos) == delimiter) {
1662                return true;
1663            }
1664        }
1665        return false;
1666    }
1667
1668    /**
1669     * Removes empty spans from the <code>spans</code> array.
1670     *
1671     * When parsing a Spanned using {@link Spanned#nextSpanTransition(int, int, Class)}, empty spans
1672     * will (correctly) create span transitions, and calling getSpans on a slice of text bounded by
1673     * one of these transitions will (correctly) include the empty overlapping span.
1674     *
1675     * However, these empty spans should not be taken into account when layouting or rendering the
1676     * string and this method provides a way to filter getSpans' results accordingly.
1677     *
1678     * @param spans A list of spans retrieved using {@link Spanned#getSpans(int, int, Class)} from
1679     * the <code>spanned</code>
1680     * @param spanned The Spanned from which spans were extracted
1681     * @return A subset of spans where empty spans ({@link Spanned#getSpanStart(Object)}  ==
1682     * {@link Spanned#getSpanEnd(Object)} have been removed. The initial order is preserved
1683     * @hide
1684     */
1685    @SuppressWarnings("unchecked")
1686    public static <T> T[] removeEmptySpans(T[] spans, Spanned spanned, Class<T> klass) {
1687        T[] copy = null;
1688        int count = 0;
1689
1690        for (int i = 0; i < spans.length; i++) {
1691            final T span = spans[i];
1692            final int start = spanned.getSpanStart(span);
1693            final int end = spanned.getSpanEnd(span);
1694
1695            if (start == end) {
1696                if (copy == null) {
1697                    copy = (T[]) Array.newInstance(klass, spans.length - 1);
1698                    System.arraycopy(spans, 0, copy, 0, i);
1699                    count = i;
1700                }
1701            } else {
1702                if (copy != null) {
1703                    copy[count] = span;
1704                    count++;
1705                }
1706            }
1707        }
1708
1709        if (copy != null) {
1710            T[] result = (T[]) Array.newInstance(klass, count);
1711            System.arraycopy(copy, 0, result, 0, count);
1712            return result;
1713        } else {
1714            return spans;
1715        }
1716    }
1717
1718    /**
1719     * Pack 2 int values into a long, useful as a return value for a range
1720     * @see #unpackRangeStartFromLong(long)
1721     * @see #unpackRangeEndFromLong(long)
1722     * @hide
1723     */
1724    public static long packRangeInLong(int start, int end) {
1725        return (((long) start) << 32) | end;
1726    }
1727
1728    /**
1729     * Get the start value from a range packed in a long by {@link #packRangeInLong(int, int)}
1730     * @see #unpackRangeEndFromLong(long)
1731     * @see #packRangeInLong(int, int)
1732     * @hide
1733     */
1734    public static int unpackRangeStartFromLong(long range) {
1735        return (int) (range >>> 32);
1736    }
1737
1738    /**
1739     * Get the end value from a range packed in a long by {@link #packRangeInLong(int, int)}
1740     * @see #unpackRangeStartFromLong(long)
1741     * @see #packRangeInLong(int, int)
1742     * @hide
1743     */
1744    public static int unpackRangeEndFromLong(long range) {
1745        return (int) (range & 0x00000000FFFFFFFFL);
1746    }
1747
1748    /**
1749     * Return the layout direction for a given Locale
1750     *
1751     * @param locale the Locale for which we want the layout direction. Can be null.
1752     * @return the layout direction. This may be one of:
1753     * {@link android.view.View#LAYOUT_DIRECTION_LTR} or
1754     * {@link android.view.View#LAYOUT_DIRECTION_RTL}.
1755     *
1756     * Be careful: this code will need to be updated when vertical scripts will be supported
1757     */
1758    public static int getLayoutDirectionFromLocale(Locale locale) {
1759        return ((locale != null && !locale.equals(Locale.ROOT)
1760                        && ULocale.forLocale(locale).isRightToLeft())
1761                // If forcing into RTL layout mode, return RTL as default
1762                || SystemProperties.getBoolean(Settings.Global.DEVELOPMENT_FORCE_RTL, false))
1763            ? View.LAYOUT_DIRECTION_RTL
1764            : View.LAYOUT_DIRECTION_LTR;
1765    }
1766
1767    /**
1768     * Return localized string representing the given number of selected items.
1769     *
1770     * @hide
1771     */
1772    public static CharSequence formatSelectedCount(int count) {
1773        return Resources.getSystem().getQuantityString(R.plurals.selected_count, count, count);
1774    }
1775
1776    private static Object sLock = new Object();
1777
1778    private static char[] sTemp = null;
1779
1780    private static String[] EMPTY_STRING_ARRAY = new String[]{};
1781
1782    private static final char ZWNBS_CHAR = '\uFEFF';
1783}
1784