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