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