blob: afb22acef433fed0fcffce833af75c99b371b0ef [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
Daisuke Miyakawac1d27482009-05-25 17:37:41 +0900919 /**
920 * Copies the spans from the region <code>start...end</code> in
921 * <code>source</code> to the region
922 * <code>destoff...destoff+end-start</code> in <code>dest</code>.
923 * Spans in <code>source</code> that begin before <code>start</code>
924 * or end after <code>end</code> but overlap this range are trimmed
925 * as if they began at <code>start</code> or ended at <code>end</code>.
926 *
927 * @throws IndexOutOfBoundsException if any of the copied spans
928 * are out of range in <code>dest</code>.
929 */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800930 public static void copySpansFrom(Spanned source, int start, int end,
931 Class kind,
932 Spannable dest, int destoff) {
933 if (kind == null) {
934 kind = Object.class;
935 }
936
937 Object[] spans = source.getSpans(start, end, kind);
938
939 for (int i = 0; i < spans.length; i++) {
940 int st = source.getSpanStart(spans[i]);
941 int en = source.getSpanEnd(spans[i]);
942 int fl = source.getSpanFlags(spans[i]);
943
944 if (st < start)
945 st = start;
946 if (en > end)
947 en = end;
948
949 dest.setSpan(spans[i], st - start + destoff, en - start + destoff,
950 fl);
951 }
952 }
953
954 public enum TruncateAt {
955 START,
956 MIDDLE,
957 END,
958 MARQUEE,
959 }
960
961 public interface EllipsizeCallback {
962 /**
963 * This method is called to report that the specified region of
964 * text was ellipsized away by a call to {@link #ellipsize}.
965 */
966 public void ellipsized(int start, int end);
967 }
968
969 private static String sEllipsis = null;
970
971 /**
972 * Returns the original text if it fits in the specified width
973 * given the properties of the specified Paint,
974 * or, if it does not fit, a truncated
975 * copy with ellipsis character added at the specified edge or center.
976 */
977 public static CharSequence ellipsize(CharSequence text,
978 TextPaint p,
979 float avail, TruncateAt where) {
980 return ellipsize(text, p, avail, where, false, null);
981 }
982
983 /**
984 * Returns the original text if it fits in the specified width
985 * given the properties of the specified Paint,
986 * or, if it does not fit, a copy with ellipsis character added
987 * at the specified edge or center.
988 * If <code>preserveLength</code> is specified, the returned copy
989 * will be padded with zero-width spaces to preserve the original
990 * length and offsets instead of truncating.
991 * If <code>callback</code> is non-null, it will be called to
992 * report the start and end of the ellipsized range.
993 */
994 public static CharSequence ellipsize(CharSequence text,
995 TextPaint p,
996 float avail, TruncateAt where,
997 boolean preserveLength,
998 EllipsizeCallback callback) {
999 if (sEllipsis == null) {
1000 Resources r = Resources.getSystem();
1001 sEllipsis = r.getString(R.string.ellipsis);
1002 }
1003
1004 int len = text.length();
1005
1006 // Use Paint.breakText() for the non-Spanned case to avoid having
1007 // to allocate memory and accumulate the character widths ourselves.
1008
1009 if (!(text instanceof Spanned)) {
1010 float wid = p.measureText(text, 0, len);
1011
1012 if (wid <= avail) {
1013 if (callback != null) {
1014 callback.ellipsized(0, 0);
1015 }
1016
1017 return text;
1018 }
1019
1020 float ellipsiswid = p.measureText(sEllipsis);
1021
1022 if (ellipsiswid > avail) {
1023 if (callback != null) {
1024 callback.ellipsized(0, len);
1025 }
1026
1027 if (preserveLength) {
1028 char[] buf = obtain(len);
1029 for (int i = 0; i < len; i++) {
1030 buf[i] = '\uFEFF';
1031 }
1032 String ret = new String(buf, 0, len);
1033 recycle(buf);
1034 return ret;
1035 } else {
1036 return "";
1037 }
1038 }
1039
1040 if (where == TruncateAt.START) {
1041 int fit = p.breakText(text, 0, len, false,
1042 avail - ellipsiswid, null);
1043
1044 if (callback != null) {
1045 callback.ellipsized(0, len - fit);
1046 }
1047
1048 if (preserveLength) {
1049 return blank(text, 0, len - fit);
1050 } else {
1051 return sEllipsis + text.toString().substring(len - fit, len);
1052 }
1053 } else if (where == TruncateAt.END) {
1054 int fit = p.breakText(text, 0, len, true,
1055 avail - ellipsiswid, null);
1056
1057 if (callback != null) {
1058 callback.ellipsized(fit, len);
1059 }
1060
1061 if (preserveLength) {
1062 return blank(text, fit, len);
1063 } else {
1064 return text.toString().substring(0, fit) + sEllipsis;
1065 }
1066 } else /* where == TruncateAt.MIDDLE */ {
1067 int right = p.breakText(text, 0, len, false,
1068 (avail - ellipsiswid) / 2, null);
1069 float used = p.measureText(text, len - right, len);
1070 int left = p.breakText(text, 0, len - right, true,
1071 avail - ellipsiswid - used, null);
1072
1073 if (callback != null) {
1074 callback.ellipsized(left, len - right);
1075 }
1076
1077 if (preserveLength) {
1078 return blank(text, left, len - right);
1079 } else {
1080 String s = text.toString();
1081 return s.substring(0, left) + sEllipsis +
1082 s.substring(len - right, len);
1083 }
1084 }
1085 }
1086
1087 // But do the Spanned cases by hand, because it's such a pain
1088 // to iterate the span transitions backwards and getTextWidths()
1089 // will give us the information we need.
1090
1091 // getTextWidths() always writes into the start of the array,
1092 // so measure each span into the first half and then copy the
1093 // results into the second half to use later.
1094
1095 float[] wid = new float[len * 2];
1096 TextPaint temppaint = new TextPaint();
1097 Spanned sp = (Spanned) text;
1098
1099 int next;
1100 for (int i = 0; i < len; i = next) {
1101 next = sp.nextSpanTransition(i, len, MetricAffectingSpan.class);
1102
1103 Styled.getTextWidths(p, temppaint, sp, i, next, wid, null);
1104 System.arraycopy(wid, 0, wid, len + i, next - i);
1105 }
1106
1107 float sum = 0;
1108 for (int i = 0; i < len; i++) {
1109 sum += wid[len + i];
1110 }
1111
1112 if (sum <= avail) {
1113 if (callback != null) {
1114 callback.ellipsized(0, 0);
1115 }
1116
1117 return text;
1118 }
1119
1120 float ellipsiswid = p.measureText(sEllipsis);
1121
1122 if (ellipsiswid > avail) {
1123 if (callback != null) {
1124 callback.ellipsized(0, len);
1125 }
1126
1127 if (preserveLength) {
1128 char[] buf = obtain(len);
1129 for (int i = 0; i < len; i++) {
1130 buf[i] = '\uFEFF';
1131 }
1132 SpannableString ss = new SpannableString(new String(buf, 0, len));
1133 recycle(buf);
1134 copySpansFrom(sp, 0, len, Object.class, ss, 0);
1135 return ss;
1136 } else {
1137 return "";
1138 }
1139 }
1140
1141 if (where == TruncateAt.START) {
1142 sum = 0;
1143 int i;
1144
1145 for (i = len; i >= 0; i--) {
1146 float w = wid[len + i - 1];
1147
1148 if (w + sum + ellipsiswid > avail) {
1149 break;
1150 }
1151
1152 sum += w;
1153 }
1154
1155 if (callback != null) {
1156 callback.ellipsized(0, i);
1157 }
1158
1159 if (preserveLength) {
1160 SpannableString ss = new SpannableString(blank(text, 0, i));
1161 copySpansFrom(sp, 0, len, Object.class, ss, 0);
1162 return ss;
1163 } else {
1164 SpannableStringBuilder out = new SpannableStringBuilder(sEllipsis);
1165 out.insert(1, text, i, len);
1166
1167 return out;
1168 }
1169 } else if (where == TruncateAt.END) {
1170 sum = 0;
1171 int i;
1172
1173 for (i = 0; i < len; i++) {
1174 float w = wid[len + i];
1175
1176 if (w + sum + ellipsiswid > avail) {
1177 break;
1178 }
1179
1180 sum += w;
1181 }
1182
1183 if (callback != null) {
1184 callback.ellipsized(i, len);
1185 }
1186
1187 if (preserveLength) {
1188 SpannableString ss = new SpannableString(blank(text, i, len));
1189 copySpansFrom(sp, 0, len, Object.class, ss, 0);
1190 return ss;
1191 } else {
1192 SpannableStringBuilder out = new SpannableStringBuilder(sEllipsis);
1193 out.insert(0, text, 0, i);
1194
1195 return out;
1196 }
1197 } else /* where = TruncateAt.MIDDLE */ {
1198 float lsum = 0, rsum = 0;
1199 int left = 0, right = len;
1200
1201 float ravail = (avail - ellipsiswid) / 2;
1202 for (right = len; right >= 0; right--) {
1203 float w = wid[len + right - 1];
1204
1205 if (w + rsum > ravail) {
1206 break;
1207 }
1208
1209 rsum += w;
1210 }
1211
1212 float lavail = avail - ellipsiswid - rsum;
1213 for (left = 0; left < right; left++) {
1214 float w = wid[len + left];
1215
1216 if (w + lsum > lavail) {
1217 break;
1218 }
1219
1220 lsum += w;
1221 }
1222
1223 if (callback != null) {
1224 callback.ellipsized(left, right);
1225 }
1226
1227 if (preserveLength) {
1228 SpannableString ss = new SpannableString(blank(text, left, right));
1229 copySpansFrom(sp, 0, len, Object.class, ss, 0);
1230 return ss;
1231 } else {
1232 SpannableStringBuilder out = new SpannableStringBuilder(sEllipsis);
1233 out.insert(0, text, 0, left);
1234 out.insert(out.length(), text, right, len);
1235
1236 return out;
1237 }
1238 }
1239 }
1240
1241 private static String blank(CharSequence source, int start, int end) {
1242 int len = source.length();
1243 char[] buf = obtain(len);
1244
1245 if (start != 0) {
1246 getChars(source, 0, start, buf, 0);
1247 }
1248 if (end != len) {
1249 getChars(source, end, len, buf, end);
1250 }
1251
1252 if (start != end) {
1253 buf[start] = '\u2026';
1254
1255 for (int i = start + 1; i < end; i++) {
1256 buf[i] = '\uFEFF';
1257 }
1258 }
1259
1260 String ret = new String(buf, 0, len);
1261 recycle(buf);
1262
1263 return ret;
1264 }
1265
1266 /**
1267 * Converts a CharSequence of the comma-separated form "Andy, Bob,
1268 * Charles, David" that is too wide to fit into the specified width
1269 * into one like "Andy, Bob, 2 more".
1270 *
1271 * @param text the text to truncate
1272 * @param p the Paint with which to measure the text
1273 * @param avail the horizontal width available for the text
1274 * @param oneMore the string for "1 more" in the current locale
1275 * @param more the string for "%d more" in the current locale
1276 */
1277 public static CharSequence commaEllipsize(CharSequence text,
1278 TextPaint p, float avail,
1279 String oneMore,
1280 String more) {
1281 int len = text.length();
1282 char[] buf = new char[len];
1283 TextUtils.getChars(text, 0, len, buf, 0);
1284
1285 int commaCount = 0;
1286 for (int i = 0; i < len; i++) {
1287 if (buf[i] == ',') {
1288 commaCount++;
1289 }
1290 }
1291
1292 float[] wid;
1293
1294 if (text instanceof Spanned) {
1295 Spanned sp = (Spanned) text;
1296 TextPaint temppaint = new TextPaint();
1297 wid = new float[len * 2];
1298
1299 int next;
1300 for (int i = 0; i < len; i = next) {
1301 next = sp.nextSpanTransition(i, len, MetricAffectingSpan.class);
1302
1303 Styled.getTextWidths(p, temppaint, sp, i, next, wid, null);
1304 System.arraycopy(wid, 0, wid, len + i, next - i);
1305 }
1306
1307 System.arraycopy(wid, len, wid, 0, len);
1308 } else {
1309 wid = new float[len];
1310 p.getTextWidths(text, 0, len, wid);
1311 }
1312
1313 int ok = 0;
1314 int okRemaining = commaCount + 1;
1315 String okFormat = "";
1316
1317 int w = 0;
1318 int count = 0;
1319
1320 for (int i = 0; i < len; i++) {
1321 w += wid[i];
1322
1323 if (buf[i] == ',') {
1324 count++;
1325
1326 int remaining = commaCount - count + 1;
1327 float moreWid;
1328 String format;
1329
1330 if (remaining == 1) {
1331 format = " " + oneMore;
1332 } else {
1333 format = " " + String.format(more, remaining);
1334 }
1335
1336 moreWid = p.measureText(format);
1337
1338 if (w + moreWid <= avail) {
1339 ok = i + 1;
1340 okRemaining = remaining;
1341 okFormat = format;
1342 }
1343 }
1344 }
1345
1346 if (w <= avail) {
1347 return text;
1348 } else {
1349 SpannableStringBuilder out = new SpannableStringBuilder(okFormat);
1350 out.insert(0, text, 0, ok);
1351 return out;
1352 }
1353 }
1354
1355 /* package */ static char[] obtain(int len) {
1356 char[] buf;
1357
1358 synchronized (sLock) {
1359 buf = sTemp;
1360 sTemp = null;
1361 }
1362
1363 if (buf == null || buf.length < len)
1364 buf = new char[ArrayUtils.idealCharArraySize(len)];
1365
1366 return buf;
1367 }
1368
1369 /* package */ static void recycle(char[] temp) {
1370 if (temp.length > 1000)
1371 return;
1372
1373 synchronized (sLock) {
1374 sTemp = temp;
1375 }
1376 }
1377
1378 /**
1379 * Html-encode the string.
1380 * @param s the string to be encoded
1381 * @return the encoded string
1382 */
1383 public static String htmlEncode(String s) {
1384 StringBuilder sb = new StringBuilder();
1385 char c;
1386 for (int i = 0; i < s.length(); i++) {
1387 c = s.charAt(i);
1388 switch (c) {
1389 case '<':
1390 sb.append("&lt;"); //$NON-NLS-1$
1391 break;
1392 case '>':
1393 sb.append("&gt;"); //$NON-NLS-1$
1394 break;
1395 case '&':
1396 sb.append("&amp;"); //$NON-NLS-1$
1397 break;
1398 case '\'':
1399 sb.append("&apos;"); //$NON-NLS-1$
1400 break;
1401 case '"':
1402 sb.append("&quot;"); //$NON-NLS-1$
1403 break;
1404 default:
1405 sb.append(c);
1406 }
1407 }
1408 return sb.toString();
1409 }
1410
1411 /**
1412 * Returns a CharSequence concatenating the specified CharSequences,
1413 * retaining their spans if any.
1414 */
1415 public static CharSequence concat(CharSequence... text) {
1416 if (text.length == 0) {
1417 return "";
1418 }
1419
1420 if (text.length == 1) {
1421 return text[0];
1422 }
1423
1424 boolean spanned = false;
1425 for (int i = 0; i < text.length; i++) {
1426 if (text[i] instanceof Spanned) {
1427 spanned = true;
1428 break;
1429 }
1430 }
1431
1432 StringBuilder sb = new StringBuilder();
1433 for (int i = 0; i < text.length; i++) {
1434 sb.append(text[i]);
1435 }
1436
1437 if (!spanned) {
1438 return sb.toString();
1439 }
1440
1441 SpannableString ss = new SpannableString(sb);
1442 int off = 0;
1443 for (int i = 0; i < text.length; i++) {
1444 int len = text[i].length();
1445
1446 if (text[i] instanceof Spanned) {
1447 copySpansFrom((Spanned) text[i], 0, len, Object.class, ss, off);
1448 }
1449
1450 off += len;
1451 }
1452
1453 return new SpannedString(ss);
1454 }
1455
1456 /**
1457 * Returns whether the given CharSequence contains any printable characters.
1458 */
1459 public static boolean isGraphic(CharSequence str) {
1460 final int len = str.length();
1461 for (int i=0; i<len; i++) {
1462 int gc = Character.getType(str.charAt(i));
1463 if (gc != Character.CONTROL
1464 && gc != Character.FORMAT
1465 && gc != Character.SURROGATE
1466 && gc != Character.UNASSIGNED
1467 && gc != Character.LINE_SEPARATOR
1468 && gc != Character.PARAGRAPH_SEPARATOR
1469 && gc != Character.SPACE_SEPARATOR) {
1470 return true;
1471 }
1472 }
1473 return false;
1474 }
1475
1476 /**
1477 * Returns whether this character is a printable character.
1478 */
1479 public static boolean isGraphic(char c) {
1480 int gc = Character.getType(c);
1481 return gc != Character.CONTROL
1482 && gc != Character.FORMAT
1483 && gc != Character.SURROGATE
1484 && gc != Character.UNASSIGNED
1485 && gc != Character.LINE_SEPARATOR
1486 && gc != Character.PARAGRAPH_SEPARATOR
1487 && gc != Character.SPACE_SEPARATOR;
1488 }
1489
1490 /**
1491 * Returns whether the given CharSequence contains only digits.
1492 */
1493 public static boolean isDigitsOnly(CharSequence str) {
1494 final int len = str.length();
1495 for (int i = 0; i < len; i++) {
1496 if (!Character.isDigit(str.charAt(i))) {
1497 return false;
1498 }
1499 }
1500 return true;
1501 }
1502
1503 /**
Daisuke Miyakawa973afa92009-12-03 10:43:45 +09001504 * @hide
1505 */
1506 public static boolean isPrintableAscii(final char c) {
1507 final int asciiFirst = 0x20;
1508 final int asciiLast = 0x7E; // included
1509 return (asciiFirst <= c && c <= asciiLast) || c == '\r' || c == '\n';
1510 }
1511
1512 /**
1513 * @hide
1514 */
1515 public static boolean isPrintableAsciiOnly(final CharSequence str) {
1516 final int len = str.length();
1517 for (int i = 0; i < len; i++) {
1518 if (!isPrintableAscii(str.charAt(i))) {
1519 return false;
1520 }
1521 }
1522 return true;
1523 }
1524
1525 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001526 * Capitalization mode for {@link #getCapsMode}: capitalize all
1527 * characters. This value is explicitly defined to be the same as
1528 * {@link InputType#TYPE_TEXT_FLAG_CAP_CHARACTERS}.
1529 */
1530 public static final int CAP_MODE_CHARACTERS
1531 = InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS;
1532
1533 /**
1534 * Capitalization mode for {@link #getCapsMode}: capitalize the first
1535 * character of all words. This value is explicitly defined to be the same as
1536 * {@link InputType#TYPE_TEXT_FLAG_CAP_WORDS}.
1537 */
1538 public static final int CAP_MODE_WORDS
1539 = InputType.TYPE_TEXT_FLAG_CAP_WORDS;
1540
1541 /**
1542 * Capitalization mode for {@link #getCapsMode}: capitalize the first
1543 * character of each sentence. This value is explicitly defined to be the same as
1544 * {@link InputType#TYPE_TEXT_FLAG_CAP_SENTENCES}.
1545 */
1546 public static final int CAP_MODE_SENTENCES
1547 = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
1548
1549 /**
1550 * Determine what caps mode should be in effect at the current offset in
1551 * the text. Only the mode bits set in <var>reqModes</var> will be
1552 * checked. Note that the caps mode flags here are explicitly defined
1553 * to match those in {@link InputType}.
1554 *
1555 * @param cs The text that should be checked for caps modes.
1556 * @param off Location in the text at which to check.
1557 * @param reqModes The modes to be checked: may be any combination of
1558 * {@link #CAP_MODE_CHARACTERS}, {@link #CAP_MODE_WORDS}, and
1559 * {@link #CAP_MODE_SENTENCES}.
1560 *
1561 * @return Returns the actual capitalization modes that can be in effect
1562 * at the current position, which is any combination of
1563 * {@link #CAP_MODE_CHARACTERS}, {@link #CAP_MODE_WORDS}, and
1564 * {@link #CAP_MODE_SENTENCES}.
1565 */
1566 public static int getCapsMode(CharSequence cs, int off, int reqModes) {
1567 int i;
1568 char c;
1569 int mode = 0;
1570
1571 if ((reqModes&CAP_MODE_CHARACTERS) != 0) {
1572 mode |= CAP_MODE_CHARACTERS;
1573 }
1574 if ((reqModes&(CAP_MODE_WORDS|CAP_MODE_SENTENCES)) == 0) {
1575 return mode;
1576 }
1577
1578 // Back over allowed opening punctuation.
1579
1580 for (i = off; i > 0; i--) {
1581 c = cs.charAt(i - 1);
1582
1583 if (c != '"' && c != '\'' &&
1584 Character.getType(c) != Character.START_PUNCTUATION) {
1585 break;
1586 }
1587 }
1588
1589 // Start of paragraph, with optional whitespace.
1590
1591 int j = i;
1592 while (j > 0 && ((c = cs.charAt(j - 1)) == ' ' || c == '\t')) {
1593 j--;
1594 }
1595 if (j == 0 || cs.charAt(j - 1) == '\n') {
1596 return mode | CAP_MODE_WORDS;
1597 }
1598
1599 // Or start of word if we are that style.
1600
1601 if ((reqModes&CAP_MODE_SENTENCES) == 0) {
1602 if (i != j) mode |= CAP_MODE_WORDS;
1603 return mode;
1604 }
1605
1606 // There must be a space if not the start of paragraph.
1607
1608 if (i == j) {
1609 return mode;
1610 }
1611
1612 // Back over allowed closing punctuation.
1613
1614 for (; j > 0; j--) {
1615 c = cs.charAt(j - 1);
1616
1617 if (c != '"' && c != '\'' &&
1618 Character.getType(c) != Character.END_PUNCTUATION) {
1619 break;
1620 }
1621 }
1622
1623 if (j > 0) {
1624 c = cs.charAt(j - 1);
1625
1626 if (c == '.' || c == '?' || c == '!') {
1627 // Do not capitalize if the word ends with a period but
1628 // also contains a period, in which case it is an abbreviation.
1629
1630 if (c == '.') {
1631 for (int k = j - 2; k >= 0; k--) {
1632 c = cs.charAt(k);
1633
1634 if (c == '.') {
1635 return mode;
1636 }
1637
1638 if (!Character.isLetter(c)) {
1639 break;
1640 }
1641 }
1642 }
1643
1644 return mode | CAP_MODE_SENTENCES;
1645 }
1646 }
1647
1648 return mode;
1649 }
1650
1651 private static Object sLock = new Object();
1652 private static char[] sTemp = null;
1653}