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