TextUtils.java revision 54b6cfa9a9e5b861a9930af873580d6dc20f773c
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 com.android.internal.R;
20
21import android.content.res.ColorStateList;
22import android.content.res.Resources;
23import android.os.Parcel;
24import android.os.Parcelable;
25import android.text.style.AbsoluteSizeSpan;
26import android.text.style.AlignmentSpan;
27import android.text.style.BackgroundColorSpan;
28import android.text.style.BulletSpan;
29import android.text.style.CharacterStyle;
30import android.text.style.ForegroundColorSpan;
31import android.text.style.LeadingMarginSpan;
32import android.text.style.MetricAffectingSpan;
33import android.text.style.QuoteSpan;
34import android.text.style.RelativeSizeSpan;
35import android.text.style.ReplacementSpan;
36import android.text.style.ScaleXSpan;
37import android.text.style.StrikethroughSpan;
38import android.text.style.StyleSpan;
39import android.text.style.SubscriptSpan;
40import android.text.style.SuperscriptSpan;
41import android.text.style.TextAppearanceSpan;
42import android.text.style.TypefaceSpan;
43import android.text.style.URLSpan;
44import android.text.style.UnderlineSpan;
45import com.android.internal.util.ArrayUtils;
46
47import java.util.regex.Pattern;
48import java.util.Iterator;
49
50public class TextUtils
51{
52    private TextUtils() { /* cannot be instantiated */ }
53
54    private static String[] EMPTY_STRING_ARRAY = new String[]{};
55
56    public static void getChars(CharSequence s, int start, int end,
57                                char[] dest, int destoff) {
58        Class c = s.getClass();
59
60        if (c == String.class)
61            ((String) s).getChars(start, end, dest, destoff);
62        else if (c == StringBuffer.class)
63            ((StringBuffer) s).getChars(start, end, dest, destoff);
64        else if (c == StringBuilder.class)
65            ((StringBuilder) s).getChars(start, end, dest, destoff);
66        else if (s instanceof GetChars)
67            ((GetChars) s).getChars(start, end, dest, destoff);
68        else {
69            for (int i = start; i < end; i++)
70                dest[destoff++] = s.charAt(i);
71        }
72    }
73
74    public static int indexOf(CharSequence s, char ch) {
75        return indexOf(s, ch, 0);
76    }
77
78    public static int indexOf(CharSequence s, char ch, int start) {
79        Class c = s.getClass();
80
81        if (c == String.class)
82            return ((String) s).indexOf(ch, start);
83
84        return indexOf(s, ch, start, s.length());
85    }
86
87    public static int indexOf(CharSequence s, char ch, int start, int end) {
88        Class c = s.getClass();
89
90        if (s instanceof GetChars || c == StringBuffer.class ||
91            c == StringBuilder.class || c == String.class) {
92            final int INDEX_INCREMENT = 500;
93            char[] temp = obtain(INDEX_INCREMENT);
94
95            while (start < end) {
96                int segend = start + INDEX_INCREMENT;
97                if (segend > end)
98                    segend = end;
99
100                getChars(s, start, segend, temp, 0);
101
102                int count = segend - start;
103                for (int i = 0; i < count; i++) {
104                    if (temp[i] == ch) {
105                        recycle(temp);
106                        return i + start;
107                    }
108                }
109
110                start = segend;
111            }
112
113            recycle(temp);
114            return -1;
115        }
116
117        for (int i = start; i < end; i++)
118            if (s.charAt(i) == ch)
119                return i;
120
121        return -1;
122    }
123
124    public static int lastIndexOf(CharSequence s, char ch) {
125        return lastIndexOf(s, ch, s.length() - 1);
126    }
127
128    public static int lastIndexOf(CharSequence s, char ch, int last) {
129        Class c = s.getClass();
130
131        if (c == String.class)
132            return ((String) s).lastIndexOf(ch, last);
133
134        return lastIndexOf(s, ch, 0, last);
135    }
136
137    public static int lastIndexOf(CharSequence s, char ch,
138                                  int start, int last) {
139        if (last < 0)
140            return -1;
141        if (last >= s.length())
142            last = s.length() - 1;
143
144        int end = last + 1;
145
146        Class c = s.getClass();
147
148        if (s instanceof GetChars || c == StringBuffer.class ||
149            c == StringBuilder.class || c == String.class) {
150            final int INDEX_INCREMENT = 500;
151            char[] temp = obtain(INDEX_INCREMENT);
152
153            while (start < end) {
154                int segstart = end - INDEX_INCREMENT;
155                if (segstart < start)
156                    segstart = start;
157
158                getChars(s, segstart, end, temp, 0);
159
160                int count = end - segstart;
161                for (int i = count - 1; i >= 0; i--) {
162                    if (temp[i] == ch) {
163                        recycle(temp);
164                        return i + segstart;
165                    }
166                }
167
168                end = segstart;
169            }
170
171            recycle(temp);
172            return -1;
173        }
174
175        for (int i = end - 1; i >= start; i--)
176            if (s.charAt(i) == ch)
177                return i;
178
179        return -1;
180    }
181
182    public static int indexOf(CharSequence s, CharSequence needle) {
183        return indexOf(s, needle, 0, s.length());
184    }
185
186    public static int indexOf(CharSequence s, CharSequence needle, int start) {
187        return indexOf(s, needle, start, s.length());
188    }
189
190    public static int indexOf(CharSequence s, CharSequence needle,
191                              int start, int end) {
192        int nlen = needle.length();
193        if (nlen == 0)
194            return start;
195
196        char c = needle.charAt(0);
197
198        for (;;) {
199            start = indexOf(s, c, start);
200            if (start > end - nlen) {
201                break;
202            }
203
204            if (start < 0) {
205                return -1;
206            }
207
208            if (regionMatches(s, start, needle, 0, nlen)) {
209                return start;
210            }
211
212            start++;
213        }
214        return -1;
215    }
216
217    public static boolean regionMatches(CharSequence one, int toffset,
218                                        CharSequence two, int ooffset,
219                                        int len) {
220        char[] temp = obtain(2 * len);
221
222        getChars(one, toffset, toffset + len, temp, 0);
223        getChars(two, ooffset, ooffset + len, temp, len);
224
225        boolean match = true;
226        for (int i = 0; i < len; i++) {
227            if (temp[i] != temp[i + len]) {
228                match = false;
229                break;
230            }
231        }
232
233        recycle(temp);
234        return match;
235    }
236
237    public static String substring(CharSequence source, int start, int end) {
238        if (source instanceof String)
239            return ((String) source).substring(start, end);
240        if (source instanceof StringBuilder)
241            return ((StringBuilder) source).substring(start, end);
242        if (source instanceof StringBuffer)
243            return ((StringBuffer) source).substring(start, end);
244
245        char[] temp = obtain(end - start);
246        getChars(source, start, end, temp, 0);
247        String ret = new String(temp, 0, end - start);
248        recycle(temp);
249
250        return ret;
251    }
252
253    /**
254     * Returns a string containing the tokens joined by delimiters.
255     * @param tokens an array objects to be joined. Strings will be formed from
256     *     the objects by calling object.toString().
257     */
258    public static String join(CharSequence delimiter, Object[] tokens) {
259        StringBuilder sb = new StringBuilder();
260        boolean firstTime = true;
261        for (Object token: tokens) {
262            if (firstTime) {
263                firstTime = false;
264            } else {
265                sb.append(delimiter);
266            }
267            sb.append(token);
268        }
269        return sb.toString();
270    }
271
272    /**
273     * Returns a string containing the tokens joined by delimiters.
274     * @param tokens an array objects to be joined. Strings will be formed from
275     *     the objects by calling object.toString().
276     */
277    public static String join(CharSequence delimiter, Iterable tokens) {
278        StringBuilder sb = new StringBuilder();
279        boolean firstTime = true;
280        for (Object token: tokens) {
281            if (firstTime) {
282                firstTime = false;
283            } else {
284                sb.append(delimiter);
285            }
286            sb.append(token);
287        }
288        return sb.toString();
289    }
290
291    /**
292     * String.split() returns [''] when the string to be split is empty. This returns []. This does
293     * not remove any empty strings from the result. For example split("a,", ","  ) returns {"a", ""}.
294     *
295     * @param text the string to split
296     * @param expression the regular expression to match
297     * @return an array of strings. The array will be empty if text is empty
298     *
299     * @throws NullPointerException if expression or text is null
300     */
301    public static String[] split(String text, String expression) {
302        if (text.length() == 0) {
303            return EMPTY_STRING_ARRAY;
304        } else {
305            return text.split(expression, -1);
306        }
307    }
308
309    /**
310     * Splits a string on a pattern. String.split() returns [''] when the string to be
311     * split is empty. This returns []. This does not remove any empty strings from the result.
312     * @param text the string to split
313     * @param pattern the regular expression to match
314     * @return an array of strings. The array will be empty if text is empty
315     *
316     * @throws NullPointerException if expression or text is null
317     */
318    public static String[] split(String text, Pattern pattern) {
319        if (text.length() == 0) {
320            return EMPTY_STRING_ARRAY;
321        } else {
322            return pattern.split(text, -1);
323        }
324    }
325
326    /**
327     * An interface for splitting strings according to rules that are opaque to the user of this
328     * interface. This also has less overhead than split, which uses regular expressions and
329     * allocates an array to hold the results.
330     *
331     * <p>The most efficient way to use this class is:
332     *
333     * <pre>
334     * // Once
335     * TextUtils.StringSplitter splitter = new TextUtils.SimpleStringSplitter(delimiter);
336     *
337     * // Once per string to split
338     * splitter.setString(string);
339     * for (String s : splitter) {
340     *     ...
341     * }
342     * </pre>
343     */
344    public interface StringSplitter extends Iterable<String> {
345        public void setString(String string);
346    }
347
348    /**
349     * A simple string splitter.
350     *
351     * <p>If the final character in the string to split is the delimiter then no empty string will
352     * be returned for the empty string after that delimeter. That is, splitting <tt>"a,b,"</tt> on
353     * comma will return <tt>"a", "b"</tt>, not <tt>"a", "b", ""</tt>.
354     */
355    public static class SimpleStringSplitter implements StringSplitter, Iterator<String> {
356        private String mString;
357        private char mDelimiter;
358        private int mPosition;
359        private int mLength;
360
361        /**
362         * Initializes the splitter. setString may be called later.
363         * @param delimiter the delimeter on which to split
364         */
365        public SimpleStringSplitter(char delimiter) {
366            mDelimiter = delimiter;
367        }
368
369        /**
370         * Sets the string to split
371         * @param string the string to split
372         */
373        public void setString(String string) {
374            mString = string;
375            mPosition = 0;
376            mLength = mString.length();
377        }
378
379        public Iterator<String> iterator() {
380            return this;
381        }
382
383        public boolean hasNext() {
384            return mPosition < mLength;
385        }
386
387        public String next() {
388            int end = mString.indexOf(mDelimiter, mPosition);
389            if (end == -1) {
390                end = mLength;
391            }
392            String nextString = mString.substring(mPosition, end);
393            mPosition = end + 1; // Skip the delimiter.
394            return nextString;
395        }
396
397        public void remove() {
398            throw new UnsupportedOperationException();
399        }
400    }
401
402    public static CharSequence stringOrSpannedString(CharSequence source) {
403        if (source == null)
404            return null;
405        if (source instanceof SpannedString)
406            return source;
407        if (source instanceof Spanned)
408            return new SpannedString(source);
409
410        return source.toString();
411    }
412
413    /**
414     * Returns true if the string is null or 0-length.
415     * @param str the string to be examined
416     * @return true if str is null or zero length
417     */
418    public static boolean isEmpty(CharSequence str) {
419        if (str == null || str.length() == 0)
420            return true;
421        else
422            return false;
423    }
424
425    /**
426     * Returns the length that the specified CharSequence would have if
427     * spaces and control characters were trimmed from the start and end,
428     * as by {@link String#trim}.
429     */
430    public static int getTrimmedLength(CharSequence s) {
431        int len = s.length();
432
433        int start = 0;
434        while (start < len && s.charAt(start) <= ' ') {
435            start++;
436        }
437
438        int end = len;
439        while (end > start && s.charAt(end - 1) <= ' ') {
440            end--;
441        }
442
443        return end - start;
444    }
445
446    /**
447     * Returns true if a and b are equal, including if they are both null.
448     *
449     * @param a first CharSequence to check
450     * @param b second CharSequence to check
451     * @return true if a and b are equal
452     */
453    public static boolean equals(CharSequence a, CharSequence b) {
454        return a == b || (a != null && a.equals(b));
455    }
456
457    // XXX currently this only reverses chars, not spans
458    public static CharSequence getReverse(CharSequence source,
459                                          int start, int end) {
460        return new Reverser(source, start, end);
461    }
462
463    private static class Reverser
464    implements CharSequence, GetChars
465    {
466        public Reverser(CharSequence source, int start, int end) {
467            mSource = source;
468            mStart = start;
469            mEnd = end;
470        }
471
472        public int length() {
473            return mEnd - mStart;
474        }
475
476        public CharSequence subSequence(int start, int end) {
477            char[] buf = new char[end - start];
478
479            getChars(start, end, buf, 0);
480            return new String(buf);
481        }
482
483        public String toString() {
484            return subSequence(0, length()).toString();
485        }
486
487        public char charAt(int off) {
488            return AndroidCharacter.getMirror(mSource.charAt(mEnd - 1 - off));
489        }
490
491        public void getChars(int start, int end, char[] dest, int destoff) {
492            TextUtils.getChars(mSource, start + mStart, end + mStart,
493                               dest, destoff);
494            AndroidCharacter.mirror(dest, 0, end - start);
495
496            int len = end - start;
497            int n = (end - start) / 2;
498            for (int i = 0; i < n; i++) {
499                char tmp = dest[destoff + i];
500
501                dest[destoff + i] = dest[destoff + len - i - 1];
502                dest[destoff + len - i - 1] = tmp;
503            }
504        }
505
506        private CharSequence mSource;
507        private int mStart;
508        private int mEnd;
509    }
510
511    private static final int ALIGNMENT_SPAN = 1;
512    private static final int FOREGROUND_COLOR_SPAN = 2;
513    private static final int RELATIVE_SIZE_SPAN = 3;
514    private static final int SCALE_X_SPAN = 4;
515    private static final int STRIKETHROUGH_SPAN = 5;
516    private static final int UNDERLINE_SPAN = 6;
517    private static final int STYLE_SPAN = 7;
518    private static final int BULLET_SPAN = 8;
519    private static final int QUOTE_SPAN = 9;
520    private static final int LEADING_MARGIN_SPAN = 10;
521    private static final int URL_SPAN = 11;
522    private static final int BACKGROUND_COLOR_SPAN = 12;
523    private static final int TYPEFACE_SPAN = 13;
524    private static final int SUPERSCRIPT_SPAN = 14;
525    private static final int SUBSCRIPT_SPAN = 15;
526    private static final int ABSOLUTE_SIZE_SPAN = 16;
527    private static final int TEXT_APPEARANCE_SPAN = 17;
528    private static final int ANNOTATION = 18;
529
530    /**
531     * Flatten a CharSequence and whatever styles can be copied across processes
532     * into the parcel.
533     */
534    public static void writeToParcel(CharSequence cs, Parcel p,
535            int parcelableFlags) {
536        if (cs instanceof Spanned) {
537            p.writeInt(0);
538            p.writeString(cs.toString());
539
540            Spanned sp = (Spanned) cs;
541            Object[] os = sp.getSpans(0, cs.length(), Object.class);
542
543            // note to people adding to this: check more specific types
544            // before more generic types.  also notice that it uses
545            // "if" instead of "else if" where there are interfaces
546            // so one object can be several.
547
548            for (int i = 0; i < os.length; i++) {
549                Object o = os[i];
550                Object prop = os[i];
551
552                if (prop instanceof CharacterStyle) {
553                    prop = ((CharacterStyle) prop).getUnderlying();
554                }
555
556                if (prop instanceof AlignmentSpan) {
557                    p.writeInt(ALIGNMENT_SPAN);
558                    p.writeString(((AlignmentSpan) prop).getAlignment().name());
559                    writeWhere(p, sp, o);
560                }
561
562                if (prop instanceof ForegroundColorSpan) {
563                    p.writeInt(FOREGROUND_COLOR_SPAN);
564                    p.writeInt(((ForegroundColorSpan) prop).getForegroundColor());
565                    writeWhere(p, sp, o);
566                }
567
568                if (prop instanceof RelativeSizeSpan) {
569                    p.writeInt(RELATIVE_SIZE_SPAN);
570                    p.writeFloat(((RelativeSizeSpan) prop).getSizeChange());
571                    writeWhere(p, sp, o);
572                }
573
574                if (prop instanceof ScaleXSpan) {
575                    p.writeInt(SCALE_X_SPAN);
576                    p.writeFloat(((ScaleXSpan) prop).getScaleX());
577                    writeWhere(p, sp, o);
578                }
579
580                if (prop instanceof StrikethroughSpan) {
581                    p.writeInt(STRIKETHROUGH_SPAN);
582                    writeWhere(p, sp, o);
583                }
584
585                if (prop instanceof UnderlineSpan) {
586                    p.writeInt(UNDERLINE_SPAN);
587                    writeWhere(p, sp, o);
588                }
589
590                if (prop instanceof StyleSpan) {
591                    p.writeInt(STYLE_SPAN);
592                    p.writeInt(((StyleSpan) prop).getStyle());
593                    writeWhere(p, sp, o);
594                }
595
596                if (prop instanceof LeadingMarginSpan) {
597                    if (prop instanceof BulletSpan) {
598                        p.writeInt(BULLET_SPAN);
599                        writeWhere(p, sp, o);
600                    } else if (prop instanceof QuoteSpan) {
601                        p.writeInt(QUOTE_SPAN);
602                        p.writeInt(((QuoteSpan) prop).getColor());
603                        writeWhere(p, sp, o);
604                    } else {
605                        p.writeInt(LEADING_MARGIN_SPAN);
606                        p.writeInt(((LeadingMarginSpan) prop).
607                                           getLeadingMargin(true));
608                        p.writeInt(((LeadingMarginSpan) prop).
609                                           getLeadingMargin(false));
610                        writeWhere(p, sp, o);
611                    }
612                }
613
614                if (prop instanceof URLSpan) {
615                    p.writeInt(URL_SPAN);
616                    p.writeString(((URLSpan) prop).getURL());
617                    writeWhere(p, sp, o);
618                }
619
620                if (prop instanceof BackgroundColorSpan) {
621                    p.writeInt(BACKGROUND_COLOR_SPAN);
622                    p.writeInt(((BackgroundColorSpan) prop).getBackgroundColor());
623                    writeWhere(p, sp, o);
624                }
625
626                if (prop instanceof TypefaceSpan) {
627                    p.writeInt(TYPEFACE_SPAN);
628                    p.writeString(((TypefaceSpan) prop).getFamily());
629                    writeWhere(p, sp, o);
630                }
631
632                if (prop instanceof SuperscriptSpan) {
633                    p.writeInt(SUPERSCRIPT_SPAN);
634                    writeWhere(p, sp, o);
635                }
636
637                if (prop instanceof SubscriptSpan) {
638                    p.writeInt(SUBSCRIPT_SPAN);
639                    writeWhere(p, sp, o);
640                }
641
642                if (prop instanceof AbsoluteSizeSpan) {
643                    p.writeInt(ABSOLUTE_SIZE_SPAN);
644                    p.writeInt(((AbsoluteSizeSpan) prop).getSize());
645                    writeWhere(p, sp, o);
646                }
647
648                if (prop instanceof TextAppearanceSpan) {
649                    TextAppearanceSpan tas = (TextAppearanceSpan) prop;
650                    p.writeInt(TEXT_APPEARANCE_SPAN);
651
652                    String tf = tas.getFamily();
653                    if (tf != null) {
654                        p.writeInt(1);
655                        p.writeString(tf);
656                    } else {
657                        p.writeInt(0);
658                    }
659
660                    p.writeInt(tas.getTextSize());
661                    p.writeInt(tas.getTextStyle());
662
663                    ColorStateList csl = tas.getTextColor();
664                    if (csl == null) {
665                        p.writeInt(0);
666                    } else {
667                        p.writeInt(1);
668                        csl.writeToParcel(p, parcelableFlags);
669                    }
670
671                    csl = tas.getLinkTextColor();
672                    if (csl == null) {
673                        p.writeInt(0);
674                    } else {
675                        p.writeInt(1);
676                        csl.writeToParcel(p, parcelableFlags);
677                    }
678
679                    writeWhere(p, sp, o);
680                }
681
682                if (prop instanceof Annotation) {
683                    p.writeInt(ANNOTATION);
684                    p.writeString(((Annotation) prop).getKey());
685                    p.writeString(((Annotation) prop).getValue());
686                    writeWhere(p, sp, o);
687                }
688            }
689
690            p.writeInt(0);
691        } else {
692            p.writeInt(1);
693            if (cs != null) {
694                p.writeString(cs.toString());
695            } else {
696                p.writeString(null);
697            }
698        }
699    }
700
701    private static void writeWhere(Parcel p, Spanned sp, Object o) {
702        p.writeInt(sp.getSpanStart(o));
703        p.writeInt(sp.getSpanEnd(o));
704        p.writeInt(sp.getSpanFlags(o));
705    }
706
707    public static final Parcelable.Creator<CharSequence> CHAR_SEQUENCE_CREATOR
708            = new Parcelable.Creator<CharSequence>()
709    {
710        /**
711         * Read and return a new CharSequence, possibly with styles,
712         * from the parcel.
713         */
714        public  CharSequence createFromParcel(Parcel p) {
715            int kind = p.readInt();
716
717            if (kind == 1)
718                return p.readString();
719
720            SpannableString sp = new SpannableString(p.readString());
721
722            while (true) {
723                kind = p.readInt();
724
725                if (kind == 0)
726                    break;
727
728                switch (kind) {
729                case ALIGNMENT_SPAN:
730                    readSpan(p, sp, new AlignmentSpan.Standard(
731                            Layout.Alignment.valueOf(p.readString())));
732                    break;
733
734                case FOREGROUND_COLOR_SPAN:
735                    readSpan(p, sp, new ForegroundColorSpan(p.readInt()));
736                    break;
737
738                case RELATIVE_SIZE_SPAN:
739                    readSpan(p, sp, new RelativeSizeSpan(p.readFloat()));
740                    break;
741
742                case SCALE_X_SPAN:
743                    readSpan(p, sp, new ScaleXSpan(p.readFloat()));
744                    break;
745
746                case STRIKETHROUGH_SPAN:
747                    readSpan(p, sp, new StrikethroughSpan());
748                    break;
749
750                case UNDERLINE_SPAN:
751                    readSpan(p, sp, new UnderlineSpan());
752                    break;
753
754                case STYLE_SPAN:
755                    readSpan(p, sp, new StyleSpan(p.readInt()));
756                    break;
757
758                case BULLET_SPAN:
759                    readSpan(p, sp, new BulletSpan());
760                    break;
761
762                case QUOTE_SPAN:
763                    readSpan(p, sp, new QuoteSpan(p.readInt()));
764                    break;
765
766                case LEADING_MARGIN_SPAN:
767                    readSpan(p, sp, new LeadingMarginSpan.Standard(p.readInt(),
768                                                                   p.readInt()));
769                break;
770
771                case URL_SPAN:
772                    readSpan(p, sp, new URLSpan(p.readString()));
773                    break;
774
775                case BACKGROUND_COLOR_SPAN:
776                    readSpan(p, sp, new BackgroundColorSpan(p.readInt()));
777                    break;
778
779                case TYPEFACE_SPAN:
780                    readSpan(p, sp, new TypefaceSpan(p.readString()));
781                    break;
782
783                case SUPERSCRIPT_SPAN:
784                    readSpan(p, sp, new SuperscriptSpan());
785                    break;
786
787                case SUBSCRIPT_SPAN:
788                    readSpan(p, sp, new SubscriptSpan());
789                    break;
790
791                case ABSOLUTE_SIZE_SPAN:
792                    readSpan(p, sp, new AbsoluteSizeSpan(p.readInt()));
793                    break;
794
795                case TEXT_APPEARANCE_SPAN:
796                    readSpan(p, sp, new TextAppearanceSpan(
797                        p.readInt() != 0
798                            ? p.readString()
799                            : null,
800                        p.readInt(),
801                        p.readInt(),
802                        p.readInt() != 0
803                            ? ColorStateList.CREATOR.createFromParcel(p)
804                            : null,
805                        p.readInt() != 0
806                            ? ColorStateList.CREATOR.createFromParcel(p)
807                            : null));
808                    break;
809
810                case ANNOTATION:
811                    readSpan(p, sp,
812                             new Annotation(p.readString(), p.readString()));
813                    break;
814
815                default:
816                    throw new RuntimeException("bogus span encoding " + kind);
817                }
818            }
819
820            return sp;
821        }
822
823        public CharSequence[] newArray(int size)
824        {
825            return new CharSequence[size];
826        }
827    };
828
829    /**
830     * Return a new CharSequence in which each of the source strings is
831     * replaced by the corresponding element of the destinations.
832     */
833    public static CharSequence replace(CharSequence template,
834                                       String[] sources,
835                                       CharSequence[] destinations) {
836        SpannableStringBuilder tb = new SpannableStringBuilder(template);
837
838        for (int i = 0; i < sources.length; i++) {
839            int where = indexOf(tb, sources[i]);
840
841            if (where >= 0)
842                tb.setSpan(sources[i], where, where + sources[i].length(),
843                           Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
844        }
845
846        for (int i = 0; i < sources.length; i++) {
847            int start = tb.getSpanStart(sources[i]);
848            int end = tb.getSpanEnd(sources[i]);
849
850            if (start >= 0) {
851                tb.replace(start, end, destinations[i]);
852            }
853        }
854
855        return tb;
856    }
857
858    /**
859     * Replace instances of "^1", "^2", etc. in the
860     * <code>template</code> CharSequence with the corresponding
861     * <code>values</code>.  "^^" is used to produce a single caret in
862     * the output.  Only up to 9 replacement values are supported,
863     * "^10" will be produce the first replacement value followed by a
864     * '0'.
865     *
866     * @param template the input text containing "^1"-style
867     * placeholder values.  This object is not modified; a copy is
868     * returned.
869     *
870     * @param values CharSequences substituted into the template.  The
871     * first is substituted for "^1", the second for "^2", and so on.
872     *
873     * @return the new CharSequence produced by doing the replacement
874     *
875     * @throws IllegalArgumentException if the template requests a
876     * value that was not provided, or if more than 9 values are
877     * provided.
878     */
879    public static CharSequence expandTemplate(CharSequence template,
880                                              CharSequence... values) {
881        if (values.length > 9) {
882            throw new IllegalArgumentException("max of 9 values are supported");
883        }
884
885        SpannableStringBuilder ssb = new SpannableStringBuilder(template);
886
887        try {
888            int i = 0;
889            while (i < ssb.length()) {
890                if (ssb.charAt(i) == '^') {
891                    char next = ssb.charAt(i+1);
892                    if (next == '^') {
893                        ssb.delete(i+1, i+2);
894                        ++i;
895                        continue;
896                    } else if (Character.isDigit(next)) {
897                        int which = Character.getNumericValue(next) - 1;
898                        if (which < 0) {
899                            throw new IllegalArgumentException(
900                                "template requests value ^" + (which+1));
901                        }
902                        if (which >= values.length) {
903                            throw new IllegalArgumentException(
904                                "template requests value ^" + (which+1) +
905                                "; only " + values.length + " provided");
906                        }
907                        ssb.replace(i, i+2, values[which]);
908                        i += values[which].length();
909                        continue;
910                    }
911                }
912                ++i;
913            }
914        } catch (IndexOutOfBoundsException ignore) {
915            // happens when ^ is the last character in the string.
916        }
917        return ssb;
918    }
919
920    public static int getOffsetBefore(CharSequence text, int offset) {
921        if (offset == 0)
922            return 0;
923        if (offset == 1)
924            return 0;
925
926        char c = text.charAt(offset - 1);
927
928        if (c >= '\uDC00' && c <= '\uDFFF') {
929            char c1 = text.charAt(offset - 2);
930
931            if (c1 >= '\uD800' && c1 <= '\uDBFF')
932                offset -= 2;
933            else
934                offset -= 1;
935        } else {
936            offset -= 1;
937        }
938
939        if (text instanceof Spanned) {
940            ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
941                                                       ReplacementSpan.class);
942
943            for (int i = 0; i < spans.length; i++) {
944                int start = ((Spanned) text).getSpanStart(spans[i]);
945                int end = ((Spanned) text).getSpanEnd(spans[i]);
946
947                if (start < offset && end > offset)
948                    offset = start;
949            }
950        }
951
952        return offset;
953    }
954
955    public static int getOffsetAfter(CharSequence text, int offset) {
956        int len = text.length();
957
958        if (offset == len)
959            return len;
960        if (offset == len - 1)
961            return len;
962
963        char c = text.charAt(offset);
964
965        if (c >= '\uD800' && c <= '\uDBFF') {
966            char c1 = text.charAt(offset + 1);
967
968            if (c1 >= '\uDC00' && c1 <= '\uDFFF')
969                offset += 2;
970            else
971                offset += 1;
972        } else {
973            offset += 1;
974        }
975
976        if (text instanceof Spanned) {
977            ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
978                                                       ReplacementSpan.class);
979
980            for (int i = 0; i < spans.length; i++) {
981                int start = ((Spanned) text).getSpanStart(spans[i]);
982                int end = ((Spanned) text).getSpanEnd(spans[i]);
983
984                if (start < offset && end > offset)
985                    offset = end;
986            }
987        }
988
989        return offset;
990    }
991
992    private static void readSpan(Parcel p, Spannable sp, Object o) {
993        sp.setSpan(o, p.readInt(), p.readInt(), p.readInt());
994    }
995
996    public static void copySpansFrom(Spanned source, int start, int end,
997                                     Class kind,
998                                     Spannable dest, int destoff) {
999        if (kind == null) {
1000            kind = Object.class;
1001        }
1002
1003        Object[] spans = source.getSpans(start, end, kind);
1004
1005        for (int i = 0; i < spans.length; i++) {
1006            int st = source.getSpanStart(spans[i]);
1007            int en = source.getSpanEnd(spans[i]);
1008            int fl = source.getSpanFlags(spans[i]);
1009
1010            if (st < start)
1011                st = start;
1012            if (en > end)
1013                en = end;
1014
1015            dest.setSpan(spans[i], st - start + destoff, en - start + destoff,
1016                         fl);
1017        }
1018    }
1019
1020    public enum TruncateAt {
1021        START,
1022        MIDDLE,
1023        END,
1024    }
1025
1026    public interface EllipsizeCallback {
1027        /**
1028         * This method is called to report that the specified region of
1029         * text was ellipsized away by a call to {@link #ellipsize}.
1030         */
1031        public void ellipsized(int start, int end);
1032    }
1033
1034    private static String sEllipsis = null;
1035
1036    /**
1037     * Returns the original text if it fits in the specified width
1038     * given the properties of the specified Paint,
1039     * or, if it does not fit, a truncated
1040     * copy with ellipsis character added at the specified edge or center.
1041     */
1042    public static CharSequence ellipsize(CharSequence text,
1043                                         TextPaint p,
1044                                         float avail, TruncateAt where) {
1045        return ellipsize(text, p, avail, where, false, null);
1046    }
1047
1048    /**
1049     * Returns the original text if it fits in the specified width
1050     * given the properties of the specified Paint,
1051     * or, if it does not fit, a copy with ellipsis character added
1052     * at the specified edge or center.
1053     * If <code>preserveLength</code> is specified, the returned copy
1054     * will be padded with zero-width spaces to preserve the original
1055     * length and offsets instead of truncating.
1056     * If <code>callback</code> is non-null, it will be called to
1057     * report the start and end of the ellipsized range.
1058     */
1059    public static CharSequence ellipsize(CharSequence text,
1060                                         TextPaint p,
1061                                         float avail, TruncateAt where,
1062                                         boolean preserveLength,
1063                                         EllipsizeCallback callback) {
1064        if (sEllipsis == null) {
1065            Resources r = Resources.getSystem();
1066            sEllipsis = r.getString(R.string.ellipsis);
1067        }
1068
1069        int len = text.length();
1070
1071        // Use Paint.breakText() for the non-Spanned case to avoid having
1072        // to allocate memory and accumulate the character widths ourselves.
1073
1074        if (!(text instanceof Spanned)) {
1075            float wid = p.measureText(text, 0, len);
1076
1077            if (wid <= avail) {
1078                if (callback != null) {
1079                    callback.ellipsized(0, 0);
1080                }
1081
1082                return text;
1083            }
1084
1085            float ellipsiswid = p.measureText(sEllipsis);
1086
1087            if (ellipsiswid > avail) {
1088                if (callback != null) {
1089                    callback.ellipsized(0, len);
1090                }
1091
1092                if (preserveLength) {
1093                    char[] buf = obtain(len);
1094                    for (int i = 0; i < len; i++) {
1095                        buf[i] = '\uFEFF';
1096                    }
1097                    String ret = new String(buf, 0, len);
1098                    recycle(buf);
1099                    return ret;
1100                } else {
1101                    return "";
1102                }
1103            }
1104
1105            if (where == TruncateAt.START) {
1106                int fit = p.breakText(text, 0, len, false,
1107                                      avail - ellipsiswid, null);
1108
1109                if (callback != null) {
1110                    callback.ellipsized(0, len - fit);
1111                }
1112
1113                if (preserveLength) {
1114                    return blank(text, 0, len - fit);
1115                } else {
1116                    return sEllipsis + text.toString().substring(len - fit, len);
1117                }
1118            } else if (where == TruncateAt.END) {
1119                int fit = p.breakText(text, 0, len, true,
1120                                      avail - ellipsiswid, null);
1121
1122                if (callback != null) {
1123                    callback.ellipsized(fit, len);
1124                }
1125
1126                if (preserveLength) {
1127                    return blank(text, fit, len);
1128                } else {
1129                    return text.toString().substring(0, fit) + sEllipsis;
1130                }
1131            } else /* where == TruncateAt.MIDDLE */ {
1132                int right = p.breakText(text, 0, len, false,
1133                                        (avail - ellipsiswid) / 2, null);
1134                float used = p.measureText(text, len - right, len);
1135                int left = p.breakText(text, 0, len - right, true,
1136                                       avail - ellipsiswid - used, null);
1137
1138                if (callback != null) {
1139                    callback.ellipsized(left, len - right);
1140                }
1141
1142                if (preserveLength) {
1143                    return blank(text, left, len - right);
1144                } else {
1145                    String s = text.toString();
1146                    return s.substring(0, left) + sEllipsis +
1147                           s.substring(len - right, len);
1148                }
1149            }
1150        }
1151
1152        // But do the Spanned cases by hand, because it's such a pain
1153        // to iterate the span transitions backwards and getTextWidths()
1154        // will give us the information we need.
1155
1156        // getTextWidths() always writes into the start of the array,
1157        // so measure each span into the first half and then copy the
1158        // results into the second half to use later.
1159
1160        float[] wid = new float[len * 2];
1161        TextPaint temppaint = new TextPaint();
1162        Spanned sp = (Spanned) text;
1163
1164        int next;
1165        for (int i = 0; i < len; i = next) {
1166            next = sp.nextSpanTransition(i, len, MetricAffectingSpan.class);
1167
1168            Styled.getTextWidths(p, temppaint, sp, i, next, wid, null);
1169            System.arraycopy(wid, 0, wid, len + i, next - i);
1170        }
1171
1172        float sum = 0;
1173        for (int i = 0; i < len; i++) {
1174            sum += wid[len + i];
1175        }
1176
1177        if (sum <= avail) {
1178            if (callback != null) {
1179                callback.ellipsized(0, 0);
1180            }
1181
1182            return text;
1183        }
1184
1185        float ellipsiswid = p.measureText(sEllipsis);
1186
1187        if (ellipsiswid > avail) {
1188            if (callback != null) {
1189                callback.ellipsized(0, len);
1190            }
1191
1192            if (preserveLength) {
1193                char[] buf = obtain(len);
1194                for (int i = 0; i < len; i++) {
1195                    buf[i] = '\uFEFF';
1196                }
1197                SpannableString ss = new SpannableString(new String(buf, 0, len));
1198                recycle(buf);
1199                copySpansFrom(sp, 0, len, Object.class, ss, 0);
1200                return ss;
1201            } else {
1202                return "";
1203            }
1204        }
1205
1206        if (where == TruncateAt.START) {
1207            sum = 0;
1208            int i;
1209
1210            for (i = len; i >= 0; i--) {
1211                float w = wid[len + i - 1];
1212
1213                if (w + sum + ellipsiswid > avail) {
1214                    break;
1215                }
1216
1217                sum += w;
1218            }
1219
1220            if (callback != null) {
1221                callback.ellipsized(0, i);
1222            }
1223
1224            if (preserveLength) {
1225                SpannableString ss = new SpannableString(blank(text, 0, i));
1226                copySpansFrom(sp, 0, len, Object.class, ss, 0);
1227                return ss;
1228            } else {
1229                SpannableStringBuilder out = new SpannableStringBuilder(sEllipsis);
1230                out.insert(1, text, i, len);
1231
1232                return out;
1233            }
1234        } else if (where == TruncateAt.END) {
1235            sum = 0;
1236            int i;
1237
1238            for (i = 0; i < len; i++) {
1239                float w = wid[len + i];
1240
1241                if (w + sum + ellipsiswid > avail) {
1242                    break;
1243                }
1244
1245                sum += w;
1246            }
1247
1248            if (callback != null) {
1249                callback.ellipsized(i, len);
1250            }
1251
1252            if (preserveLength) {
1253                SpannableString ss = new SpannableString(blank(text, i, len));
1254                copySpansFrom(sp, 0, len, Object.class, ss, 0);
1255                return ss;
1256            } else {
1257                SpannableStringBuilder out = new SpannableStringBuilder(sEllipsis);
1258                out.insert(0, text, 0, i);
1259
1260                return out;
1261            }
1262        } else /* where = TruncateAt.MIDDLE */ {
1263            float lsum = 0, rsum = 0;
1264            int left = 0, right = len;
1265
1266            float ravail = (avail - ellipsiswid) / 2;
1267            for (right = len; right >= 0; right--) {
1268                float w = wid[len + right - 1];
1269
1270                if (w + rsum > ravail) {
1271                    break;
1272                }
1273
1274                rsum += w;
1275            }
1276
1277            float lavail = avail - ellipsiswid - rsum;
1278            for (left = 0; left < right; left++) {
1279                float w = wid[len + left];
1280
1281                if (w + lsum > lavail) {
1282                    break;
1283                }
1284
1285                lsum += w;
1286            }
1287
1288            if (callback != null) {
1289                callback.ellipsized(left, right);
1290            }
1291
1292            if (preserveLength) {
1293                SpannableString ss = new SpannableString(blank(text, left, right));
1294                copySpansFrom(sp, 0, len, Object.class, ss, 0);
1295                return ss;
1296            } else {
1297                SpannableStringBuilder out = new SpannableStringBuilder(sEllipsis);
1298                out.insert(0, text, 0, left);
1299                out.insert(out.length(), text, right, len);
1300
1301                return out;
1302            }
1303        }
1304    }
1305
1306    private static String blank(CharSequence source, int start, int end) {
1307        int len = source.length();
1308        char[] buf = obtain(len);
1309
1310        if (start != 0) {
1311            getChars(source, 0, start, buf, 0);
1312        }
1313        if (end != len) {
1314            getChars(source, end, len, buf, end);
1315        }
1316
1317        if (start != end) {
1318            buf[start] = '\u2026';
1319
1320            for (int i = start + 1; i < end; i++) {
1321                buf[i] = '\uFEFF';
1322            }
1323        }
1324
1325        String ret = new String(buf, 0, len);
1326        recycle(buf);
1327
1328        return ret;
1329    }
1330
1331    /**
1332     * Converts a CharSequence of the comma-separated form "Andy, Bob,
1333     * Charles, David" that is too wide to fit into the specified width
1334     * into one like "Andy, Bob, 2 more".
1335     *
1336     * @param text the text to truncate
1337     * @param p the Paint with which to measure the text
1338     * @param avail the horizontal width available for the text
1339     * @param oneMore the string for "1 more" in the current locale
1340     * @param more the string for "%d more" in the current locale
1341     */
1342    public static CharSequence commaEllipsize(CharSequence text,
1343                                              TextPaint p, float avail,
1344                                              String oneMore,
1345                                              String more) {
1346        int len = text.length();
1347        char[] buf = new char[len];
1348        TextUtils.getChars(text, 0, len, buf, 0);
1349
1350        int commaCount = 0;
1351        for (int i = 0; i < len; i++) {
1352            if (buf[i] == ',') {
1353                commaCount++;
1354            }
1355        }
1356
1357        float[] wid;
1358
1359        if (text instanceof Spanned) {
1360            Spanned sp = (Spanned) text;
1361            TextPaint temppaint = new TextPaint();
1362            wid = new float[len * 2];
1363
1364            int next;
1365            for (int i = 0; i < len; i = next) {
1366                next = sp.nextSpanTransition(i, len, MetricAffectingSpan.class);
1367
1368                Styled.getTextWidths(p, temppaint, sp, i, next, wid, null);
1369                System.arraycopy(wid, 0, wid, len + i, next - i);
1370            }
1371
1372            System.arraycopy(wid, len, wid, 0, len);
1373        } else {
1374            wid = new float[len];
1375            p.getTextWidths(text, 0, len, wid);
1376        }
1377
1378        int ok = 0;
1379        int okRemaining = commaCount + 1;
1380        String okFormat = "";
1381
1382        int w = 0;
1383        int count = 0;
1384
1385        for (int i = 0; i < len; i++) {
1386            w += wid[i];
1387
1388            if (buf[i] == ',') {
1389                count++;
1390
1391                int remaining = commaCount - count + 1;
1392                float moreWid;
1393                String format;
1394
1395                if (remaining == 1) {
1396                    format = " " + oneMore;
1397                } else {
1398                    format = " " + String.format(more, remaining);
1399                }
1400
1401                moreWid = p.measureText(format);
1402
1403                if (w + moreWid <= avail) {
1404                    ok = i + 1;
1405                    okRemaining = remaining;
1406                    okFormat = format;
1407                }
1408            }
1409        }
1410
1411        if (w <= avail) {
1412            return text;
1413        } else {
1414            SpannableStringBuilder out = new SpannableStringBuilder(okFormat);
1415            out.insert(0, text, 0, ok);
1416            return out;
1417        }
1418    }
1419
1420    /* package */ static char[] obtain(int len) {
1421        char[] buf;
1422
1423        synchronized (sLock) {
1424            buf = sTemp;
1425            sTemp = null;
1426        }
1427
1428        if (buf == null || buf.length < len)
1429            buf = new char[ArrayUtils.idealCharArraySize(len)];
1430
1431        return buf;
1432    }
1433
1434    /* package */ static void recycle(char[] temp) {
1435        if (temp.length > 1000)
1436            return;
1437
1438        synchronized (sLock) {
1439            sTemp = temp;
1440        }
1441    }
1442
1443    /**
1444     * Html-encode the string.
1445     * @param s the string to be encoded
1446     * @return the encoded string
1447     */
1448    public static String htmlEncode(String s) {
1449        StringBuilder sb = new StringBuilder();
1450        char c;
1451        for (int i = 0; i < s.length(); i++) {
1452            c = s.charAt(i);
1453            switch (c) {
1454            case '<':
1455                sb.append("&lt;"); //$NON-NLS-1$
1456                break;
1457            case '>':
1458                sb.append("&gt;"); //$NON-NLS-1$
1459                break;
1460            case '&':
1461                sb.append("&amp;"); //$NON-NLS-1$
1462                break;
1463            case '\\':
1464                sb.append("&apos;"); //$NON-NLS-1$
1465                break;
1466            case '"':
1467                sb.append("&quot;"); //$NON-NLS-1$
1468                break;
1469            default:
1470                sb.append(c);
1471            }
1472        }
1473        return sb.toString();
1474    }
1475
1476    /**
1477     * Returns a CharSequence concatenating the specified CharSequences,
1478     * retaining their spans if any.
1479     */
1480    public static CharSequence concat(CharSequence... text) {
1481        if (text.length == 0) {
1482            return "";
1483        }
1484
1485        if (text.length == 1) {
1486            return text[0];
1487        }
1488
1489        boolean spanned = false;
1490        for (int i = 0; i < text.length; i++) {
1491            if (text[i] instanceof Spanned) {
1492                spanned = true;
1493                break;
1494            }
1495        }
1496
1497        StringBuilder sb = new StringBuilder();
1498        for (int i = 0; i < text.length; i++) {
1499            sb.append(text[i]);
1500        }
1501
1502        if (!spanned) {
1503            return sb.toString();
1504        }
1505
1506        SpannableString ss = new SpannableString(sb);
1507        int off = 0;
1508        for (int i = 0; i < text.length; i++) {
1509            int len = text[i].length();
1510
1511            if (text[i] instanceof Spanned) {
1512                copySpansFrom((Spanned) text[i], 0, len, Object.class, ss, off);
1513            }
1514
1515            off += len;
1516        }
1517
1518        return new SpannedString(ss);
1519    }
1520
1521    /**
1522     * Returns whether the given CharSequence contains any printable characters.
1523     */
1524    public static boolean isGraphic(CharSequence str) {
1525        final int len = str.length();
1526        for (int i=0; i<len; i++) {
1527            int gc = Character.getType(str.charAt(i));
1528            if (gc != Character.CONTROL
1529                    && gc != Character.FORMAT
1530                    && gc != Character.SURROGATE
1531                    && gc != Character.UNASSIGNED
1532                    && gc != Character.LINE_SEPARATOR
1533                    && gc != Character.PARAGRAPH_SEPARATOR
1534                    && gc != Character.SPACE_SEPARATOR) {
1535                return true;
1536            }
1537        }
1538        return false;
1539    }
1540
1541    /**
1542     * Returns whether this character is a printable character.
1543     */
1544    public static boolean isGraphic(char c) {
1545        int gc = Character.getType(c);
1546        return     gc != Character.CONTROL
1547                && gc != Character.FORMAT
1548                && gc != Character.SURROGATE
1549                && gc != Character.UNASSIGNED
1550                && gc != Character.LINE_SEPARATOR
1551                && gc != Character.PARAGRAPH_SEPARATOR
1552                && gc != Character.SPACE_SEPARATOR;
1553    }
1554
1555    /**
1556     * Returns whether the given CharSequence contains only digits.
1557     */
1558    public static boolean isDigitsOnly(CharSequence str) {
1559        final int len = str.length();
1560        for (int i = 0; i < len; i++) {
1561            if (!Character.isDigit(str.charAt(i))) {
1562                return false;
1563            }
1564        }
1565        return true;
1566    }
1567
1568    private static Object sLock = new Object();
1569    private static char[] sTemp = null;
1570}
1571