blob: 5b4c3802b3cebe9e97229736bdb61311b129ce43 [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
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}