blob: 1508d10ea6afeb614035b2264844211b15a8a1fe [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
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080019import android.content.res.Resources;
20import android.os.Parcel;
21import android.os.Parcelable;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080022import android.text.style.AbsoluteSizeSpan;
23import android.text.style.AlignmentSpan;
24import android.text.style.BackgroundColorSpan;
25import android.text.style.BulletSpan;
26import android.text.style.CharacterStyle;
Luca Zanoline6d36822011-08-30 18:04:34 +010027import android.text.style.EasyEditSpan;
Gilles Debunne0eea6682011-08-29 13:30:31 -070028import android.text.style.ForegroundColorSpan;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080029import android.text.style.LeadingMarginSpan;
Victoria Leasedf8ef4b2012-08-17 15:34:01 -070030import android.text.style.LocaleSpan;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080031import android.text.style.MetricAffectingSpan;
32import android.text.style.QuoteSpan;
33import android.text.style.RelativeSizeSpan;
34import android.text.style.ReplacementSpan;
35import android.text.style.ScaleXSpan;
Gilles Debunne28294cc2011-08-24 12:02:05 -070036import android.text.style.SpellCheckSpan;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080037import android.text.style.StrikethroughSpan;
38import android.text.style.StyleSpan;
39import android.text.style.SubscriptSpan;
Gilles Debunne28294cc2011-08-24 12:02:05 -070040import android.text.style.SuggestionRangeSpan;
Gilles Debunnea00972a2011-04-13 16:07:31 -070041import android.text.style.SuggestionSpan;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080042import android.text.style.SuperscriptSpan;
43import android.text.style.TextAppearanceSpan;
44import android.text.style.TypefaceSpan;
45import android.text.style.URLSpan;
46import android.text.style.UnderlineSpan;
47import android.util.Printer;
48
Fabrice Di Megliod3d9f3f2012-09-18 12:55:32 -070049import android.view.View;
Doug Feltcb3791202011-07-07 11:57:48 -070050import com.android.internal.R;
51import com.android.internal.util.ArrayUtils;
Fabrice Di Megliod3d9f3f2012-09-18 12:55:32 -070052import libcore.icu.ICU;
Doug Feltcb3791202011-07-07 11:57:48 -070053
Gilles Debunne1e3ac182011-03-08 14:22:34 -080054import java.lang.reflect.Array;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080055import java.util.Iterator;
Fabrice Di Megliod3d9f3f2012-09-18 12:55:32 -070056import java.util.Locale;
Doug Felte8e45f22010-03-29 14:58:40 -070057import java.util.regex.Pattern;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080058
59public class TextUtils {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080060
Fabrice Di Megliocb332642011-09-23 19:08:04 -070061 private TextUtils() { /* cannot be instantiated */ }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080062
63 public static void getChars(CharSequence s, int start, int end,
64 char[] dest, int destoff) {
Gilles Debunne1e3ac182011-03-08 14:22:34 -080065 Class<? extends CharSequence> c = s.getClass();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080066
67 if (c == String.class)
68 ((String) s).getChars(start, end, dest, destoff);
69 else if (c == StringBuffer.class)
70 ((StringBuffer) s).getChars(start, end, dest, destoff);
71 else if (c == StringBuilder.class)
72 ((StringBuilder) s).getChars(start, end, dest, destoff);
73 else if (s instanceof GetChars)
74 ((GetChars) s).getChars(start, end, dest, destoff);
75 else {
76 for (int i = start; i < end; i++)
77 dest[destoff++] = s.charAt(i);
78 }
79 }
80
81 public static int indexOf(CharSequence s, char ch) {
82 return indexOf(s, ch, 0);
83 }
84
85 public static int indexOf(CharSequence s, char ch, int start) {
Gilles Debunne1e3ac182011-03-08 14:22:34 -080086 Class<? extends CharSequence> c = s.getClass();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080087
88 if (c == String.class)
89 return ((String) s).indexOf(ch, start);
90
91 return indexOf(s, ch, start, s.length());
92 }
93
94 public static int indexOf(CharSequence s, char ch, int start, int end) {
Gilles Debunne1e3ac182011-03-08 14:22:34 -080095 Class<? extends CharSequence> c = s.getClass();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080096
97 if (s instanceof GetChars || c == StringBuffer.class ||
98 c == StringBuilder.class || c == String.class) {
99 final int INDEX_INCREMENT = 500;
100 char[] temp = obtain(INDEX_INCREMENT);
101
102 while (start < end) {
103 int segend = start + INDEX_INCREMENT;
104 if (segend > end)
105 segend = end;
106
107 getChars(s, start, segend, temp, 0);
108
109 int count = segend - start;
110 for (int i = 0; i < count; i++) {
111 if (temp[i] == ch) {
112 recycle(temp);
113 return i + start;
114 }
115 }
116
117 start = segend;
118 }
119
120 recycle(temp);
121 return -1;
122 }
123
124 for (int i = start; i < end; i++)
125 if (s.charAt(i) == ch)
126 return i;
127
128 return -1;
129 }
130
131 public static int lastIndexOf(CharSequence s, char ch) {
132 return lastIndexOf(s, ch, s.length() - 1);
133 }
134
135 public static int lastIndexOf(CharSequence s, char ch, int last) {
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800136 Class<? extends CharSequence> c = s.getClass();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800137
138 if (c == String.class)
139 return ((String) s).lastIndexOf(ch, last);
140
141 return lastIndexOf(s, ch, 0, last);
142 }
143
144 public static int lastIndexOf(CharSequence s, char ch,
145 int start, int last) {
146 if (last < 0)
147 return -1;
148 if (last >= s.length())
149 last = s.length() - 1;
150
151 int end = last + 1;
152
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800153 Class<? extends CharSequence> c = s.getClass();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800154
155 if (s instanceof GetChars || c == StringBuffer.class ||
156 c == StringBuilder.class || c == String.class) {
157 final int INDEX_INCREMENT = 500;
158 char[] temp = obtain(INDEX_INCREMENT);
159
160 while (start < end) {
161 int segstart = end - INDEX_INCREMENT;
162 if (segstart < start)
163 segstart = start;
164
165 getChars(s, segstart, end, temp, 0);
166
167 int count = end - segstart;
168 for (int i = count - 1; i >= 0; i--) {
169 if (temp[i] == ch) {
170 recycle(temp);
171 return i + segstart;
172 }
173 }
174
175 end = segstart;
176 }
177
178 recycle(temp);
179 return -1;
180 }
181
182 for (int i = end - 1; i >= start; i--)
183 if (s.charAt(i) == ch)
184 return i;
185
186 return -1;
187 }
188
189 public static int indexOf(CharSequence s, CharSequence needle) {
190 return indexOf(s, needle, 0, s.length());
191 }
192
193 public static int indexOf(CharSequence s, CharSequence needle, int start) {
194 return indexOf(s, needle, start, s.length());
195 }
196
197 public static int indexOf(CharSequence s, CharSequence needle,
198 int start, int end) {
199 int nlen = needle.length();
200 if (nlen == 0)
201 return start;
202
203 char c = needle.charAt(0);
204
205 for (;;) {
206 start = indexOf(s, c, start);
207 if (start > end - nlen) {
208 break;
209 }
210
211 if (start < 0) {
212 return -1;
213 }
214
215 if (regionMatches(s, start, needle, 0, nlen)) {
216 return start;
217 }
218
219 start++;
220 }
221 return -1;
222 }
223
224 public static boolean regionMatches(CharSequence one, int toffset,
225 CharSequence two, int ooffset,
226 int len) {
227 char[] temp = obtain(2 * len);
228
229 getChars(one, toffset, toffset + len, temp, 0);
230 getChars(two, ooffset, ooffset + len, temp, len);
231
232 boolean match = true;
233 for (int i = 0; i < len; i++) {
234 if (temp[i] != temp[i + len]) {
235 match = false;
236 break;
237 }
238 }
239
240 recycle(temp);
241 return match;
242 }
243
244 /**
245 * Create a new String object containing the given range of characters
246 * from the source string. This is different than simply calling
247 * {@link CharSequence#subSequence(int, int) CharSequence.subSequence}
248 * in that it does not preserve any style runs in the source sequence,
249 * allowing a more efficient implementation.
250 */
251 public static String substring(CharSequence source, int start, int end) {
252 if (source instanceof String)
253 return ((String) source).substring(start, end);
254 if (source instanceof StringBuilder)
255 return ((StringBuilder) source).substring(start, end);
256 if (source instanceof StringBuffer)
257 return ((StringBuffer) source).substring(start, end);
258
259 char[] temp = obtain(end - start);
260 getChars(source, start, end, temp, 0);
261 String ret = new String(temp, 0, end - start);
262 recycle(temp);
263
264 return ret;
265 }
266
267 /**
Jeff Sharkeyfa4d7752011-08-17 15:08:27 -0700268 * Returns list of multiple {@link CharSequence} joined into a single
269 * {@link CharSequence} separated by localized delimiter such as ", ".
270 *
271 * @hide
272 */
273 public static CharSequence join(Iterable<CharSequence> list) {
274 final CharSequence delimiter = Resources.getSystem().getText(R.string.list_delimeter);
275 return join(delimiter, list);
276 }
277
278 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800279 * Returns a string containing the tokens joined by delimiters.
280 * @param tokens an array objects to be joined. Strings will be formed from
281 * the objects by calling object.toString().
282 */
283 public static String join(CharSequence delimiter, Object[] tokens) {
284 StringBuilder sb = new StringBuilder();
285 boolean firstTime = true;
286 for (Object token: tokens) {
287 if (firstTime) {
288 firstTime = false;
289 } else {
290 sb.append(delimiter);
291 }
292 sb.append(token);
293 }
294 return sb.toString();
295 }
296
297 /**
298 * Returns a string containing the tokens joined by delimiters.
299 * @param tokens an array objects to be joined. Strings will be formed from
300 * the objects by calling object.toString().
301 */
302 public static String join(CharSequence delimiter, Iterable tokens) {
303 StringBuilder sb = new StringBuilder();
304 boolean firstTime = true;
305 for (Object token: tokens) {
306 if (firstTime) {
307 firstTime = false;
308 } else {
309 sb.append(delimiter);
310 }
311 sb.append(token);
312 }
313 return sb.toString();
314 }
315
316 /**
317 * String.split() returns [''] when the string to be split is empty. This returns []. This does
318 * not remove any empty strings from the result. For example split("a,", "," ) returns {"a", ""}.
319 *
320 * @param text the string to split
321 * @param expression 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, String expression) {
327 if (text.length() == 0) {
328 return EMPTY_STRING_ARRAY;
329 } else {
330 return text.split(expression, -1);
331 }
332 }
333
334 /**
335 * Splits a string on a pattern. String.split() returns [''] when the string to be
336 * split is empty. This returns []. This does not remove any empty strings from the result.
337 * @param text the string to split
338 * @param pattern the regular expression to match
339 * @return an array of strings. The array will be empty if text is empty
340 *
341 * @throws NullPointerException if expression or text is null
342 */
343 public static String[] split(String text, Pattern pattern) {
344 if (text.length() == 0) {
345 return EMPTY_STRING_ARRAY;
346 } else {
347 return pattern.split(text, -1);
348 }
349 }
350
351 /**
352 * An interface for splitting strings according to rules that are opaque to the user of this
353 * interface. This also has less overhead than split, which uses regular expressions and
354 * allocates an array to hold the results.
355 *
356 * <p>The most efficient way to use this class is:
357 *
358 * <pre>
359 * // Once
360 * TextUtils.StringSplitter splitter = new TextUtils.SimpleStringSplitter(delimiter);
361 *
362 * // Once per string to split
363 * splitter.setString(string);
364 * for (String s : splitter) {
365 * ...
366 * }
367 * </pre>
368 */
369 public interface StringSplitter extends Iterable<String> {
370 public void setString(String string);
371 }
372
373 /**
374 * A simple string splitter.
375 *
376 * <p>If the final character in the string to split is the delimiter then no empty string will
377 * be returned for the empty string after that delimeter. That is, splitting <tt>"a,b,"</tt> on
378 * comma will return <tt>"a", "b"</tt>, not <tt>"a", "b", ""</tt>.
379 */
380 public static class SimpleStringSplitter implements StringSplitter, Iterator<String> {
381 private String mString;
382 private char mDelimiter;
383 private int mPosition;
384 private int mLength;
385
386 /**
387 * Initializes the splitter. setString may be called later.
388 * @param delimiter the delimeter on which to split
389 */
390 public SimpleStringSplitter(char delimiter) {
391 mDelimiter = delimiter;
392 }
393
394 /**
395 * Sets the string to split
396 * @param string the string to split
397 */
398 public void setString(String string) {
399 mString = string;
400 mPosition = 0;
401 mLength = mString.length();
402 }
403
404 public Iterator<String> iterator() {
405 return this;
406 }
407
408 public boolean hasNext() {
409 return mPosition < mLength;
410 }
411
412 public String next() {
413 int end = mString.indexOf(mDelimiter, mPosition);
414 if (end == -1) {
415 end = mLength;
416 }
417 String nextString = mString.substring(mPosition, end);
418 mPosition = end + 1; // Skip the delimiter.
419 return nextString;
420 }
421
422 public void remove() {
423 throw new UnsupportedOperationException();
424 }
425 }
426
427 public static CharSequence stringOrSpannedString(CharSequence source) {
428 if (source == null)
429 return null;
430 if (source instanceof SpannedString)
431 return source;
432 if (source instanceof Spanned)
433 return new SpannedString(source);
434
435 return source.toString();
436 }
437
438 /**
439 * Returns true if the string is null or 0-length.
440 * @param str the string to be examined
441 * @return true if str is null or zero length
442 */
443 public static boolean isEmpty(CharSequence str) {
444 if (str == null || str.length() == 0)
445 return true;
446 else
447 return false;
448 }
449
450 /**
451 * Returns the length that the specified CharSequence would have if
452 * spaces and control characters were trimmed from the start and end,
453 * as by {@link String#trim}.
454 */
455 public static int getTrimmedLength(CharSequence s) {
456 int len = s.length();
457
458 int start = 0;
459 while (start < len && s.charAt(start) <= ' ') {
460 start++;
461 }
462
463 int end = len;
464 while (end > start && s.charAt(end - 1) <= ' ') {
465 end--;
466 }
467
468 return end - start;
469 }
470
471 /**
472 * Returns true if a and b are equal, including if they are both null.
473 * <p><i>Note: In platform versions 1.1 and earlier, this method only worked well if
474 * both the arguments were instances of String.</i></p>
475 * @param a first CharSequence to check
476 * @param b second CharSequence to check
477 * @return true if a and b are equal
478 */
479 public static boolean equals(CharSequence a, CharSequence b) {
480 if (a == b) return true;
481 int length;
482 if (a != null && b != null && (length = a.length()) == b.length()) {
483 if (a instanceof String && b instanceof String) {
484 return a.equals(b);
485 } else {
486 for (int i = 0; i < length; i++) {
487 if (a.charAt(i) != b.charAt(i)) return false;
488 }
489 return true;
490 }
491 }
492 return false;
493 }
494
495 // XXX currently this only reverses chars, not spans
496 public static CharSequence getReverse(CharSequence source,
497 int start, int end) {
498 return new Reverser(source, start, end);
499 }
500
501 private static class Reverser
502 implements CharSequence, GetChars
503 {
504 public Reverser(CharSequence source, int start, int end) {
505 mSource = source;
506 mStart = start;
507 mEnd = end;
508 }
509
510 public int length() {
511 return mEnd - mStart;
512 }
513
514 public CharSequence subSequence(int start, int end) {
515 char[] buf = new char[end - start];
516
517 getChars(start, end, buf, 0);
518 return new String(buf);
519 }
520
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800521 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800522 public String toString() {
523 return subSequence(0, length()).toString();
524 }
525
526 public char charAt(int off) {
527 return AndroidCharacter.getMirror(mSource.charAt(mEnd - 1 - off));
528 }
529
530 public void getChars(int start, int end, char[] dest, int destoff) {
531 TextUtils.getChars(mSource, start + mStart, end + mStart,
532 dest, destoff);
533 AndroidCharacter.mirror(dest, 0, end - start);
534
535 int len = end - start;
536 int n = (end - start) / 2;
537 for (int i = 0; i < n; i++) {
538 char tmp = dest[destoff + i];
539
540 dest[destoff + i] = dest[destoff + len - i - 1];
541 dest[destoff + len - i - 1] = tmp;
542 }
543 }
544
545 private CharSequence mSource;
546 private int mStart;
547 private int mEnd;
548 }
549
550 /** @hide */
551 public static final int ALIGNMENT_SPAN = 1;
552 /** @hide */
553 public static final int FOREGROUND_COLOR_SPAN = 2;
554 /** @hide */
555 public static final int RELATIVE_SIZE_SPAN = 3;
556 /** @hide */
557 public static final int SCALE_X_SPAN = 4;
558 /** @hide */
559 public static final int STRIKETHROUGH_SPAN = 5;
560 /** @hide */
561 public static final int UNDERLINE_SPAN = 6;
562 /** @hide */
563 public static final int STYLE_SPAN = 7;
564 /** @hide */
565 public static final int BULLET_SPAN = 8;
566 /** @hide */
567 public static final int QUOTE_SPAN = 9;
568 /** @hide */
569 public static final int LEADING_MARGIN_SPAN = 10;
570 /** @hide */
571 public static final int URL_SPAN = 11;
572 /** @hide */
573 public static final int BACKGROUND_COLOR_SPAN = 12;
574 /** @hide */
575 public static final int TYPEFACE_SPAN = 13;
576 /** @hide */
577 public static final int SUPERSCRIPT_SPAN = 14;
578 /** @hide */
579 public static final int SUBSCRIPT_SPAN = 15;
580 /** @hide */
581 public static final int ABSOLUTE_SIZE_SPAN = 16;
582 /** @hide */
583 public static final int TEXT_APPEARANCE_SPAN = 17;
584 /** @hide */
585 public static final int ANNOTATION = 18;
satokadb43582011-03-09 10:08:47 +0900586 /** @hide */
Gilles Debunnea00972a2011-04-13 16:07:31 -0700587 public static final int SUGGESTION_SPAN = 19;
Gilles Debunne28294cc2011-08-24 12:02:05 -0700588 /** @hide */
589 public static final int SPELL_CHECK_SPAN = 20;
590 /** @hide */
591 public static final int SUGGESTION_RANGE_SPAN = 21;
Luca Zanoline6d36822011-08-30 18:04:34 +0100592 /** @hide */
593 public static final int EASY_EDIT_SPAN = 22;
Victoria Leasedf8ef4b2012-08-17 15:34:01 -0700594 /** @hide */
595 public static final int LOCALE_SPAN = 23;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800596
597 /**
598 * Flatten a CharSequence and whatever styles can be copied across processes
599 * into the parcel.
600 */
601 public static void writeToParcel(CharSequence cs, Parcel p,
602 int parcelableFlags) {
603 if (cs instanceof Spanned) {
604 p.writeInt(0);
605 p.writeString(cs.toString());
606
607 Spanned sp = (Spanned) cs;
608 Object[] os = sp.getSpans(0, cs.length(), Object.class);
609
610 // note to people adding to this: check more specific types
611 // before more generic types. also notice that it uses
612 // "if" instead of "else if" where there are interfaces
613 // so one object can be several.
614
615 for (int i = 0; i < os.length; i++) {
616 Object o = os[i];
617 Object prop = os[i];
618
619 if (prop instanceof CharacterStyle) {
620 prop = ((CharacterStyle) prop).getUnderlying();
621 }
622
623 if (prop instanceof ParcelableSpan) {
624 ParcelableSpan ps = (ParcelableSpan)prop;
625 p.writeInt(ps.getSpanTypeId());
626 ps.writeToParcel(p, parcelableFlags);
627 writeWhere(p, sp, o);
628 }
629 }
630
631 p.writeInt(0);
632 } else {
633 p.writeInt(1);
634 if (cs != null) {
635 p.writeString(cs.toString());
636 } else {
637 p.writeString(null);
638 }
639 }
640 }
641
642 private static void writeWhere(Parcel p, Spanned sp, Object o) {
643 p.writeInt(sp.getSpanStart(o));
644 p.writeInt(sp.getSpanEnd(o));
645 p.writeInt(sp.getSpanFlags(o));
646 }
647
648 public static final Parcelable.Creator<CharSequence> CHAR_SEQUENCE_CREATOR
649 = new Parcelable.Creator<CharSequence>() {
650 /**
651 * Read and return a new CharSequence, possibly with styles,
652 * from the parcel.
653 */
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800654 public CharSequence createFromParcel(Parcel p) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800655 int kind = p.readInt();
656
Martin Wallgrencee20512011-04-07 14:45:43 +0200657 String string = p.readString();
658 if (string == null) {
659 return null;
660 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800661
Martin Wallgrencee20512011-04-07 14:45:43 +0200662 if (kind == 1) {
663 return string;
664 }
665
666 SpannableString sp = new SpannableString(string);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800667
668 while (true) {
669 kind = p.readInt();
670
671 if (kind == 0)
672 break;
673
674 switch (kind) {
675 case ALIGNMENT_SPAN:
676 readSpan(p, sp, new AlignmentSpan.Standard(p));
677 break;
678
679 case FOREGROUND_COLOR_SPAN:
680 readSpan(p, sp, new ForegroundColorSpan(p));
681 break;
682
683 case RELATIVE_SIZE_SPAN:
684 readSpan(p, sp, new RelativeSizeSpan(p));
685 break;
686
687 case SCALE_X_SPAN:
688 readSpan(p, sp, new ScaleXSpan(p));
689 break;
690
691 case STRIKETHROUGH_SPAN:
692 readSpan(p, sp, new StrikethroughSpan(p));
693 break;
694
695 case UNDERLINE_SPAN:
696 readSpan(p, sp, new UnderlineSpan(p));
697 break;
698
699 case STYLE_SPAN:
700 readSpan(p, sp, new StyleSpan(p));
701 break;
702
703 case BULLET_SPAN:
704 readSpan(p, sp, new BulletSpan(p));
705 break;
706
707 case QUOTE_SPAN:
708 readSpan(p, sp, new QuoteSpan(p));
709 break;
710
711 case LEADING_MARGIN_SPAN:
712 readSpan(p, sp, new LeadingMarginSpan.Standard(p));
713 break;
714
715 case URL_SPAN:
716 readSpan(p, sp, new URLSpan(p));
717 break;
718
719 case BACKGROUND_COLOR_SPAN:
720 readSpan(p, sp, new BackgroundColorSpan(p));
721 break;
722
723 case TYPEFACE_SPAN:
724 readSpan(p, sp, new TypefaceSpan(p));
725 break;
726
727 case SUPERSCRIPT_SPAN:
728 readSpan(p, sp, new SuperscriptSpan(p));
729 break;
730
731 case SUBSCRIPT_SPAN:
732 readSpan(p, sp, new SubscriptSpan(p));
733 break;
734
735 case ABSOLUTE_SIZE_SPAN:
736 readSpan(p, sp, new AbsoluteSizeSpan(p));
737 break;
738
739 case TEXT_APPEARANCE_SPAN:
740 readSpan(p, sp, new TextAppearanceSpan(p));
741 break;
742
743 case ANNOTATION:
744 readSpan(p, sp, new Annotation(p));
745 break;
746
Gilles Debunnea00972a2011-04-13 16:07:31 -0700747 case SUGGESTION_SPAN:
748 readSpan(p, sp, new SuggestionSpan(p));
749 break;
750
Gilles Debunne28294cc2011-08-24 12:02:05 -0700751 case SPELL_CHECK_SPAN:
752 readSpan(p, sp, new SpellCheckSpan(p));
753 break;
754
755 case SUGGESTION_RANGE_SPAN:
Gilles Debunne0eea6682011-08-29 13:30:31 -0700756 readSpan(p, sp, new SuggestionRangeSpan(p));
Gilles Debunne28294cc2011-08-24 12:02:05 -0700757 break;
Gilles Debunnee90bed12011-08-30 14:28:27 -0700758
Luca Zanoline6d36822011-08-30 18:04:34 +0100759 case EASY_EDIT_SPAN:
Gilles Debunne0eea6682011-08-29 13:30:31 -0700760 readSpan(p, sp, new EasyEditSpan());
Luca Zanoline6d36822011-08-30 18:04:34 +0100761 break;
762
Victoria Leasedf8ef4b2012-08-17 15:34:01 -0700763 case LOCALE_SPAN:
764 readSpan(p, sp, new LocaleSpan(p));
765 break;
766
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800767 default:
768 throw new RuntimeException("bogus span encoding " + kind);
769 }
770 }
771
772 return sp;
773 }
774
775 public CharSequence[] newArray(int size)
776 {
777 return new CharSequence[size];
778 }
779 };
780
781 /**
782 * Debugging tool to print the spans in a CharSequence. The output will
783 * be printed one span per line. If the CharSequence is not a Spanned,
784 * then the entire string will be printed on a single line.
785 */
786 public static void dumpSpans(CharSequence cs, Printer printer, String prefix) {
787 if (cs instanceof Spanned) {
788 Spanned sp = (Spanned) cs;
789 Object[] os = sp.getSpans(0, cs.length(), Object.class);
790
791 for (int i = 0; i < os.length; i++) {
792 Object o = os[i];
793 printer.println(prefix + cs.subSequence(sp.getSpanStart(o),
794 sp.getSpanEnd(o)) + ": "
795 + Integer.toHexString(System.identityHashCode(o))
796 + " " + o.getClass().getCanonicalName()
797 + " (" + sp.getSpanStart(o) + "-" + sp.getSpanEnd(o)
798 + ") fl=#" + sp.getSpanFlags(o));
799 }
800 } else {
801 printer.println(prefix + cs + ": (no spans)");
802 }
803 }
804
805 /**
806 * Return a new CharSequence in which each of the source strings is
807 * replaced by the corresponding element of the destinations.
808 */
809 public static CharSequence replace(CharSequence template,
810 String[] sources,
811 CharSequence[] destinations) {
812 SpannableStringBuilder tb = new SpannableStringBuilder(template);
813
814 for (int i = 0; i < sources.length; i++) {
815 int where = indexOf(tb, sources[i]);
816
817 if (where >= 0)
818 tb.setSpan(sources[i], where, where + sources[i].length(),
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800819 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800820 }
821
822 for (int i = 0; i < sources.length; i++) {
823 int start = tb.getSpanStart(sources[i]);
824 int end = tb.getSpanEnd(sources[i]);
825
826 if (start >= 0) {
827 tb.replace(start, end, destinations[i]);
828 }
829 }
830
831 return tb;
832 }
833
834 /**
835 * Replace instances of "^1", "^2", etc. in the
836 * <code>template</code> CharSequence with the corresponding
837 * <code>values</code>. "^^" is used to produce a single caret in
838 * the output. Only up to 9 replacement values are supported,
839 * "^10" will be produce the first replacement value followed by a
840 * '0'.
841 *
842 * @param template the input text containing "^1"-style
843 * placeholder values. This object is not modified; a copy is
844 * returned.
845 *
846 * @param values CharSequences substituted into the template. The
847 * first is substituted for "^1", the second for "^2", and so on.
848 *
849 * @return the new CharSequence produced by doing the replacement
850 *
851 * @throws IllegalArgumentException if the template requests a
852 * value that was not provided, or if more than 9 values are
853 * provided.
854 */
855 public static CharSequence expandTemplate(CharSequence template,
856 CharSequence... values) {
857 if (values.length > 9) {
858 throw new IllegalArgumentException("max of 9 values are supported");
859 }
860
861 SpannableStringBuilder ssb = new SpannableStringBuilder(template);
862
863 try {
864 int i = 0;
865 while (i < ssb.length()) {
866 if (ssb.charAt(i) == '^') {
867 char next = ssb.charAt(i+1);
868 if (next == '^') {
869 ssb.delete(i+1, i+2);
870 ++i;
871 continue;
872 } else if (Character.isDigit(next)) {
873 int which = Character.getNumericValue(next) - 1;
874 if (which < 0) {
875 throw new IllegalArgumentException(
876 "template requests value ^" + (which+1));
877 }
878 if (which >= values.length) {
879 throw new IllegalArgumentException(
880 "template requests value ^" + (which+1) +
881 "; only " + values.length + " provided");
882 }
883 ssb.replace(i, i+2, values[which]);
884 i += values[which].length();
885 continue;
886 }
887 }
888 ++i;
889 }
890 } catch (IndexOutOfBoundsException ignore) {
891 // happens when ^ is the last character in the string.
892 }
893 return ssb;
894 }
895
896 public static int getOffsetBefore(CharSequence text, int offset) {
897 if (offset == 0)
898 return 0;
899 if (offset == 1)
900 return 0;
901
902 char c = text.charAt(offset - 1);
903
904 if (c >= '\uDC00' && c <= '\uDFFF') {
905 char c1 = text.charAt(offset - 2);
906
907 if (c1 >= '\uD800' && c1 <= '\uDBFF')
908 offset -= 2;
909 else
910 offset -= 1;
911 } else {
912 offset -= 1;
913 }
914
915 if (text instanceof Spanned) {
916 ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
917 ReplacementSpan.class);
918
919 for (int i = 0; i < spans.length; i++) {
920 int start = ((Spanned) text).getSpanStart(spans[i]);
921 int end = ((Spanned) text).getSpanEnd(spans[i]);
922
923 if (start < offset && end > offset)
924 offset = start;
925 }
926 }
927
928 return offset;
929 }
930
931 public static int getOffsetAfter(CharSequence text, int offset) {
932 int len = text.length();
933
934 if (offset == len)
935 return len;
936 if (offset == len - 1)
937 return len;
938
939 char c = text.charAt(offset);
940
941 if (c >= '\uD800' && c <= '\uDBFF') {
942 char c1 = text.charAt(offset + 1);
943
944 if (c1 >= '\uDC00' && c1 <= '\uDFFF')
945 offset += 2;
946 else
947 offset += 1;
948 } else {
949 offset += 1;
950 }
951
952 if (text instanceof Spanned) {
953 ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
954 ReplacementSpan.class);
955
956 for (int i = 0; i < spans.length; i++) {
957 int start = ((Spanned) text).getSpanStart(spans[i]);
958 int end = ((Spanned) text).getSpanEnd(spans[i]);
959
960 if (start < offset && end > offset)
961 offset = end;
962 }
963 }
964
965 return offset;
966 }
967
968 private static void readSpan(Parcel p, Spannable sp, Object o) {
969 sp.setSpan(o, p.readInt(), p.readInt(), p.readInt());
970 }
971
Daisuke Miyakawac1d27482009-05-25 17:37:41 +0900972 /**
973 * Copies the spans from the region <code>start...end</code> in
974 * <code>source</code> to the region
975 * <code>destoff...destoff+end-start</code> in <code>dest</code>.
976 * Spans in <code>source</code> that begin before <code>start</code>
977 * or end after <code>end</code> but overlap this range are trimmed
978 * as if they began at <code>start</code> or ended at <code>end</code>.
979 *
980 * @throws IndexOutOfBoundsException if any of the copied spans
981 * are out of range in <code>dest</code>.
982 */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800983 public static void copySpansFrom(Spanned source, int start, int end,
984 Class kind,
985 Spannable dest, int destoff) {
986 if (kind == null) {
987 kind = Object.class;
988 }
989
990 Object[] spans = source.getSpans(start, end, kind);
991
992 for (int i = 0; i < spans.length; i++) {
993 int st = source.getSpanStart(spans[i]);
994 int en = source.getSpanEnd(spans[i]);
995 int fl = source.getSpanFlags(spans[i]);
996
997 if (st < start)
998 st = start;
999 if (en > end)
1000 en = end;
1001
1002 dest.setSpan(spans[i], st - start + destoff, en - start + destoff,
1003 fl);
1004 }
1005 }
1006
1007 public enum TruncateAt {
1008 START,
1009 MIDDLE,
1010 END,
1011 MARQUEE,
Fabrice Di Megliocb332642011-09-23 19:08:04 -07001012 /**
1013 * @hide
1014 */
1015 END_SMALL
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001016 }
1017
1018 public interface EllipsizeCallback {
1019 /**
1020 * This method is called to report that the specified region of
1021 * text was ellipsized away by a call to {@link #ellipsize}.
1022 */
1023 public void ellipsized(int start, int end);
1024 }
1025
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001026 /**
1027 * Returns the original text if it fits in the specified width
1028 * given the properties of the specified Paint,
1029 * or, if it does not fit, a truncated
1030 * copy with ellipsis character added at the specified edge or center.
1031 */
1032 public static CharSequence ellipsize(CharSequence text,
1033 TextPaint p,
1034 float avail, TruncateAt where) {
1035 return ellipsize(text, p, avail, where, false, null);
1036 }
1037
1038 /**
1039 * Returns the original text if it fits in the specified width
1040 * given the properties of the specified Paint,
Doug Felte8e45f22010-03-29 14:58:40 -07001041 * or, if it does not fit, a copy with ellipsis character added
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001042 * at the specified edge or center.
1043 * If <code>preserveLength</code> is specified, the returned copy
1044 * will be padded with zero-width spaces to preserve the original
1045 * length and offsets instead of truncating.
1046 * If <code>callback</code> is non-null, it will be called to
Doug Feltcb3791202011-07-07 11:57:48 -07001047 * report the start and end of the ellipsized range. TextDirection
1048 * is determined by the first strong directional character.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001049 */
1050 public static CharSequence ellipsize(CharSequence text,
Doug Felte8e45f22010-03-29 14:58:40 -07001051 TextPaint paint,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001052 float avail, TruncateAt where,
1053 boolean preserveLength,
1054 EllipsizeCallback callback) {
Fabrice Di Megliof3e64102012-07-31 13:39:06 -07001055
1056 final String ellipsis = (where == TruncateAt.END_SMALL) ?
1057 Resources.getSystem().getString(R.string.ellipsis_two_dots) :
1058 Resources.getSystem().getString(R.string.ellipsis);
1059
Doug Feltcb3791202011-07-07 11:57:48 -07001060 return ellipsize(text, paint, avail, where, preserveLength, callback,
Fabrice Di Megliocb332642011-09-23 19:08:04 -07001061 TextDirectionHeuristics.FIRSTSTRONG_LTR,
Fabrice Di Megliof3e64102012-07-31 13:39:06 -07001062 ellipsis);
Doug Feltcb3791202011-07-07 11:57:48 -07001063 }
1064
1065 /**
1066 * Returns the original text if it fits in the specified width
1067 * given the properties of the specified Paint,
1068 * or, if it does not fit, a copy with ellipsis character added
1069 * at the specified edge or center.
1070 * If <code>preserveLength</code> is specified, the returned copy
1071 * will be padded with zero-width spaces to preserve the original
1072 * length and offsets instead of truncating.
1073 * If <code>callback</code> is non-null, it will be called to
1074 * report the start and end of the ellipsized range.
1075 *
1076 * @hide
1077 */
1078 public static CharSequence ellipsize(CharSequence text,
1079 TextPaint paint,
1080 float avail, TruncateAt where,
1081 boolean preserveLength,
1082 EllipsizeCallback callback,
Fabrice Di Megliocb332642011-09-23 19:08:04 -07001083 TextDirectionHeuristic textDir, String ellipsis) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001084
1085 int len = text.length();
1086
Doug Felte8e45f22010-03-29 14:58:40 -07001087 MeasuredText mt = MeasuredText.obtain();
1088 try {
Doug Feltcb3791202011-07-07 11:57:48 -07001089 float width = setPara(mt, paint, text, 0, text.length(), textDir);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001090
Doug Felte8e45f22010-03-29 14:58:40 -07001091 if (width <= avail) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001092 if (callback != null) {
1093 callback.ellipsized(0, 0);
1094 }
1095
1096 return text;
1097 }
1098
Doug Felte8e45f22010-03-29 14:58:40 -07001099 // XXX assumes ellipsis string does not require shaping and
1100 // is unaffected by style
Fabrice Di Megliocb332642011-09-23 19:08:04 -07001101 float ellipsiswid = paint.measureText(ellipsis);
Doug Felte8e45f22010-03-29 14:58:40 -07001102 avail -= ellipsiswid;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001103
Doug Felte8e45f22010-03-29 14:58:40 -07001104 int left = 0;
1105 int right = len;
1106 if (avail < 0) {
1107 // it all goes
1108 } else if (where == TruncateAt.START) {
Gilles Debunnec70e7a02012-02-23 18:05:55 -08001109 right = len - mt.breakText(len, false, avail);
Fabrice Di Megliocb332642011-09-23 19:08:04 -07001110 } else if (where == TruncateAt.END || where == TruncateAt.END_SMALL) {
Gilles Debunnec70e7a02012-02-23 18:05:55 -08001111 left = mt.breakText(len, true, avail);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001112 } else {
Gilles Debunnec70e7a02012-02-23 18:05:55 -08001113 right = len - mt.breakText(len, false, avail / 2);
Doug Felte8e45f22010-03-29 14:58:40 -07001114 avail -= mt.measure(right, len);
Gilles Debunnec70e7a02012-02-23 18:05:55 -08001115 left = mt.breakText(right, true, avail);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001116 }
1117
1118 if (callback != null) {
1119 callback.ellipsized(left, right);
1120 }
1121
Doug Felte8e45f22010-03-29 14:58:40 -07001122 char[] buf = mt.mChars;
1123 Spanned sp = text instanceof Spanned ? (Spanned) text : null;
1124
1125 int remaining = len - (right - left);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001126 if (preserveLength) {
Doug Felte8e45f22010-03-29 14:58:40 -07001127 if (remaining > 0) { // else eliminate the ellipsis too
Fabrice Di Megliocb332642011-09-23 19:08:04 -07001128 buf[left++] = ellipsis.charAt(0);
Doug Felte8e45f22010-03-29 14:58:40 -07001129 }
1130 for (int i = left; i < right; i++) {
Fabrice Di Megliocb332642011-09-23 19:08:04 -07001131 buf[i] = ZWNBS_CHAR;
Doug Felte8e45f22010-03-29 14:58:40 -07001132 }
1133 String s = new String(buf, 0, len);
1134 if (sp == null) {
1135 return s;
1136 }
1137 SpannableString ss = new SpannableString(s);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001138 copySpansFrom(sp, 0, len, Object.class, ss, 0);
1139 return ss;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001140 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001141
Doug Felte8e45f22010-03-29 14:58:40 -07001142 if (remaining == 0) {
1143 return "";
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001144 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001145
Doug Felte8e45f22010-03-29 14:58:40 -07001146 if (sp == null) {
Fabrice Di Megliocb332642011-09-23 19:08:04 -07001147 StringBuilder sb = new StringBuilder(remaining + ellipsis.length());
Doug Felte8e45f22010-03-29 14:58:40 -07001148 sb.append(buf, 0, left);
Fabrice Di Megliocb332642011-09-23 19:08:04 -07001149 sb.append(ellipsis);
Doug Felte8e45f22010-03-29 14:58:40 -07001150 sb.append(buf, right, len - right);
1151 return sb.toString();
1152 }
1153
1154 SpannableStringBuilder ssb = new SpannableStringBuilder();
1155 ssb.append(text, 0, left);
Fabrice Di Megliocb332642011-09-23 19:08:04 -07001156 ssb.append(ellipsis);
Doug Felte8e45f22010-03-29 14:58:40 -07001157 ssb.append(text, right, len);
1158 return ssb;
1159 } finally {
1160 MeasuredText.recycle(mt);
1161 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001162 }
1163
1164 /**
1165 * Converts a CharSequence of the comma-separated form "Andy, Bob,
1166 * Charles, David" that is too wide to fit into the specified width
1167 * into one like "Andy, Bob, 2 more".
1168 *
1169 * @param text the text to truncate
1170 * @param p the Paint with which to measure the text
1171 * @param avail the horizontal width available for the text
1172 * @param oneMore the string for "1 more" in the current locale
1173 * @param more the string for "%d more" in the current locale
1174 */
1175 public static CharSequence commaEllipsize(CharSequence text,
1176 TextPaint p, float avail,
1177 String oneMore,
1178 String more) {
Doug Feltcb3791202011-07-07 11:57:48 -07001179 return commaEllipsize(text, p, avail, oneMore, more,
1180 TextDirectionHeuristics.FIRSTSTRONG_LTR);
1181 }
1182
1183 /**
1184 * @hide
1185 */
1186 public static CharSequence commaEllipsize(CharSequence text, TextPaint p,
1187 float avail, String oneMore, String more, TextDirectionHeuristic textDir) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001188
Doug Felte8e45f22010-03-29 14:58:40 -07001189 MeasuredText mt = MeasuredText.obtain();
1190 try {
1191 int len = text.length();
Doug Feltcb3791202011-07-07 11:57:48 -07001192 float width = setPara(mt, p, text, 0, len, textDir);
Doug Felte8e45f22010-03-29 14:58:40 -07001193 if (width <= avail) {
1194 return text;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001195 }
1196
Doug Felte8e45f22010-03-29 14:58:40 -07001197 char[] buf = mt.mChars;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001198
Doug Felte8e45f22010-03-29 14:58:40 -07001199 int commaCount = 0;
1200 for (int i = 0; i < len; i++) {
1201 if (buf[i] == ',') {
1202 commaCount++;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001203 }
1204 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001205
Doug Felte8e45f22010-03-29 14:58:40 -07001206 int remaining = commaCount + 1;
1207
1208 int ok = 0;
Doug Felte8e45f22010-03-29 14:58:40 -07001209 String okFormat = "";
1210
1211 int w = 0;
1212 int count = 0;
1213 float[] widths = mt.mWidths;
1214
Doug Felte8e45f22010-03-29 14:58:40 -07001215 MeasuredText tempMt = MeasuredText.obtain();
1216 for (int i = 0; i < len; i++) {
1217 w += widths[i];
1218
1219 if (buf[i] == ',') {
1220 count++;
1221
1222 String format;
1223 // XXX should not insert spaces, should be part of string
1224 // XXX should use plural rules and not assume English plurals
1225 if (--remaining == 1) {
1226 format = " " + oneMore;
1227 } else {
1228 format = " " + String.format(more, remaining);
1229 }
1230
1231 // XXX this is probably ok, but need to look at it more
Doug Feltcb3791202011-07-07 11:57:48 -07001232 tempMt.setPara(format, 0, format.length(), textDir);
Brian Muramatsu4c8ad6e2011-01-27 18:13:39 -08001233 float moreWid = tempMt.addStyleRun(p, tempMt.mLen, null);
Doug Felte8e45f22010-03-29 14:58:40 -07001234
1235 if (w + moreWid <= avail) {
1236 ok = i + 1;
Doug Felte8e45f22010-03-29 14:58:40 -07001237 okFormat = format;
1238 }
1239 }
1240 }
1241 MeasuredText.recycle(tempMt);
1242
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001243 SpannableStringBuilder out = new SpannableStringBuilder(okFormat);
1244 out.insert(0, text, 0, ok);
1245 return out;
Doug Felte8e45f22010-03-29 14:58:40 -07001246 } finally {
1247 MeasuredText.recycle(mt);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001248 }
1249 }
1250
Doug Felte8e45f22010-03-29 14:58:40 -07001251 private static float setPara(MeasuredText mt, TextPaint paint,
Doug Feltcb3791202011-07-07 11:57:48 -07001252 CharSequence text, int start, int end, TextDirectionHeuristic textDir) {
Doug Felte8e45f22010-03-29 14:58:40 -07001253
Doug Feltcb3791202011-07-07 11:57:48 -07001254 mt.setPara(text, start, end, textDir);
Doug Felte8e45f22010-03-29 14:58:40 -07001255
1256 float width;
1257 Spanned sp = text instanceof Spanned ? (Spanned) text : null;
1258 int len = end - start;
1259 if (sp == null) {
1260 width = mt.addStyleRun(paint, len, null);
1261 } else {
1262 width = 0;
1263 int spanEnd;
1264 for (int spanStart = 0; spanStart < len; spanStart = spanEnd) {
1265 spanEnd = sp.nextSpanTransition(spanStart, len,
1266 MetricAffectingSpan.class);
1267 MetricAffectingSpan[] spans = sp.getSpans(
1268 spanStart, spanEnd, MetricAffectingSpan.class);
Gilles Debunne1e3ac182011-03-08 14:22:34 -08001269 spans = TextUtils.removeEmptySpans(spans, sp, MetricAffectingSpan.class);
Doug Felte8e45f22010-03-29 14:58:40 -07001270 width += mt.addStyleRun(paint, spans, spanEnd - spanStart, null);
1271 }
1272 }
1273
1274 return width;
1275 }
1276
1277 private static final char FIRST_RIGHT_TO_LEFT = '\u0590';
1278
1279 /* package */
1280 static boolean doesNotNeedBidi(CharSequence s, int start, int end) {
1281 for (int i = start; i < end; i++) {
1282 if (s.charAt(i) >= FIRST_RIGHT_TO_LEFT) {
1283 return false;
1284 }
1285 }
1286 return true;
1287 }
1288
1289 /* package */
1290 static boolean doesNotNeedBidi(char[] text, int start, int len) {
1291 for (int i = start, e = i + len; i < e; i++) {
1292 if (text[i] >= FIRST_RIGHT_TO_LEFT) {
1293 return false;
1294 }
1295 }
1296 return true;
1297 }
1298
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001299 /* package */ static char[] obtain(int len) {
1300 char[] buf;
1301
1302 synchronized (sLock) {
1303 buf = sTemp;
1304 sTemp = null;
1305 }
1306
1307 if (buf == null || buf.length < len)
1308 buf = new char[ArrayUtils.idealCharArraySize(len)];
1309
1310 return buf;
1311 }
1312
1313 /* package */ static void recycle(char[] temp) {
1314 if (temp.length > 1000)
1315 return;
1316
1317 synchronized (sLock) {
1318 sTemp = temp;
1319 }
1320 }
1321
1322 /**
1323 * Html-encode the string.
1324 * @param s the string to be encoded
1325 * @return the encoded string
1326 */
1327 public static String htmlEncode(String s) {
1328 StringBuilder sb = new StringBuilder();
1329 char c;
1330 for (int i = 0; i < s.length(); i++) {
1331 c = s.charAt(i);
1332 switch (c) {
1333 case '<':
1334 sb.append("&lt;"); //$NON-NLS-1$
1335 break;
1336 case '>':
1337 sb.append("&gt;"); //$NON-NLS-1$
1338 break;
1339 case '&':
1340 sb.append("&amp;"); //$NON-NLS-1$
1341 break;
1342 case '\'':
Marc Blankf4832da2012-02-13 10:11:50 -08001343 //http://www.w3.org/TR/xhtml1
1344 // The named character reference &apos; (the apostrophe, U+0027) was introduced in
1345 // XML 1.0 but does not appear in HTML. Authors should therefore use &#39; instead
1346 // of &apos; to work as expected in HTML 4 user agents.
1347 sb.append("&#39;"); //$NON-NLS-1$
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001348 break;
1349 case '"':
1350 sb.append("&quot;"); //$NON-NLS-1$
1351 break;
1352 default:
1353 sb.append(c);
1354 }
1355 }
1356 return sb.toString();
1357 }
1358
1359 /**
1360 * Returns a CharSequence concatenating the specified CharSequences,
1361 * retaining their spans if any.
1362 */
1363 public static CharSequence concat(CharSequence... text) {
1364 if (text.length == 0) {
1365 return "";
1366 }
1367
1368 if (text.length == 1) {
1369 return text[0];
1370 }
1371
1372 boolean spanned = false;
1373 for (int i = 0; i < text.length; i++) {
1374 if (text[i] instanceof Spanned) {
1375 spanned = true;
1376 break;
1377 }
1378 }
1379
1380 StringBuilder sb = new StringBuilder();
1381 for (int i = 0; i < text.length; i++) {
1382 sb.append(text[i]);
1383 }
1384
1385 if (!spanned) {
1386 return sb.toString();
1387 }
1388
1389 SpannableString ss = new SpannableString(sb);
1390 int off = 0;
1391 for (int i = 0; i < text.length; i++) {
1392 int len = text[i].length();
1393
1394 if (text[i] instanceof Spanned) {
1395 copySpansFrom((Spanned) text[i], 0, len, Object.class, ss, off);
1396 }
1397
1398 off += len;
1399 }
1400
1401 return new SpannedString(ss);
1402 }
1403
1404 /**
1405 * Returns whether the given CharSequence contains any printable characters.
1406 */
1407 public static boolean isGraphic(CharSequence str) {
1408 final int len = str.length();
1409 for (int i=0; i<len; i++) {
1410 int gc = Character.getType(str.charAt(i));
1411 if (gc != Character.CONTROL
1412 && gc != Character.FORMAT
1413 && gc != Character.SURROGATE
1414 && gc != Character.UNASSIGNED
1415 && gc != Character.LINE_SEPARATOR
1416 && gc != Character.PARAGRAPH_SEPARATOR
1417 && gc != Character.SPACE_SEPARATOR) {
1418 return true;
1419 }
1420 }
1421 return false;
1422 }
1423
1424 /**
1425 * Returns whether this character is a printable character.
1426 */
1427 public static boolean isGraphic(char c) {
1428 int gc = Character.getType(c);
1429 return gc != Character.CONTROL
1430 && gc != Character.FORMAT
1431 && gc != Character.SURROGATE
1432 && gc != Character.UNASSIGNED
1433 && gc != Character.LINE_SEPARATOR
1434 && gc != Character.PARAGRAPH_SEPARATOR
1435 && gc != Character.SPACE_SEPARATOR;
1436 }
1437
1438 /**
1439 * Returns whether the given CharSequence contains only digits.
1440 */
1441 public static boolean isDigitsOnly(CharSequence str) {
1442 final int len = str.length();
1443 for (int i = 0; i < len; i++) {
1444 if (!Character.isDigit(str.charAt(i))) {
1445 return false;
1446 }
1447 }
1448 return true;
1449 }
1450
1451 /**
Daisuke Miyakawa973afa92009-12-03 10:43:45 +09001452 * @hide
1453 */
1454 public static boolean isPrintableAscii(final char c) {
1455 final int asciiFirst = 0x20;
1456 final int asciiLast = 0x7E; // included
1457 return (asciiFirst <= c && c <= asciiLast) || c == '\r' || c == '\n';
1458 }
1459
1460 /**
1461 * @hide
1462 */
1463 public static boolean isPrintableAsciiOnly(final CharSequence str) {
1464 final int len = str.length();
1465 for (int i = 0; i < len; i++) {
1466 if (!isPrintableAscii(str.charAt(i))) {
1467 return false;
1468 }
1469 }
1470 return true;
1471 }
1472
1473 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001474 * Capitalization mode for {@link #getCapsMode}: capitalize all
1475 * characters. This value is explicitly defined to be the same as
1476 * {@link InputType#TYPE_TEXT_FLAG_CAP_CHARACTERS}.
1477 */
1478 public static final int CAP_MODE_CHARACTERS
1479 = InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS;
Doug Felte8e45f22010-03-29 14:58:40 -07001480
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001481 /**
1482 * Capitalization mode for {@link #getCapsMode}: capitalize the first
1483 * character of all words. This value is explicitly defined to be the same as
1484 * {@link InputType#TYPE_TEXT_FLAG_CAP_WORDS}.
1485 */
1486 public static final int CAP_MODE_WORDS
1487 = InputType.TYPE_TEXT_FLAG_CAP_WORDS;
Doug Felte8e45f22010-03-29 14:58:40 -07001488
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001489 /**
1490 * Capitalization mode for {@link #getCapsMode}: capitalize the first
1491 * character of each sentence. This value is explicitly defined to be the same as
1492 * {@link InputType#TYPE_TEXT_FLAG_CAP_SENTENCES}.
1493 */
1494 public static final int CAP_MODE_SENTENCES
1495 = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
Doug Felte8e45f22010-03-29 14:58:40 -07001496
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001497 /**
1498 * Determine what caps mode should be in effect at the current offset in
1499 * the text. Only the mode bits set in <var>reqModes</var> will be
1500 * checked. Note that the caps mode flags here are explicitly defined
1501 * to match those in {@link InputType}.
Doug Felte8e45f22010-03-29 14:58:40 -07001502 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001503 * @param cs The text that should be checked for caps modes.
1504 * @param off Location in the text at which to check.
1505 * @param reqModes The modes to be checked: may be any combination of
1506 * {@link #CAP_MODE_CHARACTERS}, {@link #CAP_MODE_WORDS}, and
1507 * {@link #CAP_MODE_SENTENCES}.
Mark Wagner60919952010-03-01 09:24:59 -08001508 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001509 * @return Returns the actual capitalization modes that can be in effect
1510 * at the current position, which is any combination of
1511 * {@link #CAP_MODE_CHARACTERS}, {@link #CAP_MODE_WORDS}, and
1512 * {@link #CAP_MODE_SENTENCES}.
1513 */
1514 public static int getCapsMode(CharSequence cs, int off, int reqModes) {
Mark Wagner60919952010-03-01 09:24:59 -08001515 if (off < 0) {
1516 return 0;
1517 }
1518
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001519 int i;
1520 char c;
1521 int mode = 0;
1522
1523 if ((reqModes&CAP_MODE_CHARACTERS) != 0) {
1524 mode |= CAP_MODE_CHARACTERS;
1525 }
1526 if ((reqModes&(CAP_MODE_WORDS|CAP_MODE_SENTENCES)) == 0) {
1527 return mode;
1528 }
1529
1530 // Back over allowed opening punctuation.
1531
1532 for (i = off; i > 0; i--) {
1533 c = cs.charAt(i - 1);
1534
1535 if (c != '"' && c != '\'' &&
1536 Character.getType(c) != Character.START_PUNCTUATION) {
1537 break;
1538 }
1539 }
1540
1541 // Start of paragraph, with optional whitespace.
1542
1543 int j = i;
1544 while (j > 0 && ((c = cs.charAt(j - 1)) == ' ' || c == '\t')) {
1545 j--;
1546 }
1547 if (j == 0 || cs.charAt(j - 1) == '\n') {
1548 return mode | CAP_MODE_WORDS;
1549 }
1550
1551 // Or start of word if we are that style.
1552
1553 if ((reqModes&CAP_MODE_SENTENCES) == 0) {
1554 if (i != j) mode |= CAP_MODE_WORDS;
1555 return mode;
1556 }
1557
1558 // There must be a space if not the start of paragraph.
1559
1560 if (i == j) {
1561 return mode;
1562 }
1563
1564 // Back over allowed closing punctuation.
1565
1566 for (; j > 0; j--) {
1567 c = cs.charAt(j - 1);
1568
1569 if (c != '"' && c != '\'' &&
1570 Character.getType(c) != Character.END_PUNCTUATION) {
1571 break;
1572 }
1573 }
1574
1575 if (j > 0) {
1576 c = cs.charAt(j - 1);
1577
1578 if (c == '.' || c == '?' || c == '!') {
1579 // Do not capitalize if the word ends with a period but
1580 // also contains a period, in which case it is an abbreviation.
1581
1582 if (c == '.') {
1583 for (int k = j - 2; k >= 0; k--) {
1584 c = cs.charAt(k);
1585
1586 if (c == '.') {
1587 return mode;
1588 }
1589
1590 if (!Character.isLetter(c)) {
1591 break;
1592 }
1593 }
1594 }
1595
1596 return mode | CAP_MODE_SENTENCES;
1597 }
1598 }
1599
1600 return mode;
1601 }
Doug Felte8e45f22010-03-29 14:58:40 -07001602
Brad Fitzpatrick11fe1812010-09-10 16:07:52 -07001603 /**
1604 * Does a comma-delimited list 'delimitedString' contain a certain item?
1605 * (without allocating memory)
1606 *
1607 * @hide
1608 */
1609 public static boolean delimitedStringContains(
1610 String delimitedString, char delimiter, String item) {
1611 if (isEmpty(delimitedString) || isEmpty(item)) {
1612 return false;
1613 }
1614 int pos = -1;
1615 int length = delimitedString.length();
1616 while ((pos = delimitedString.indexOf(item, pos + 1)) != -1) {
1617 if (pos > 0 && delimitedString.charAt(pos - 1) != delimiter) {
1618 continue;
1619 }
1620 int expectedDelimiterPos = pos + item.length();
1621 if (expectedDelimiterPos == length) {
1622 // Match at end of string.
1623 return true;
1624 }
1625 if (delimitedString.charAt(expectedDelimiterPos) == delimiter) {
1626 return true;
1627 }
1628 }
1629 return false;
1630 }
1631
Gilles Debunne1e3ac182011-03-08 14:22:34 -08001632 /**
1633 * Removes empty spans from the <code>spans</code> array.
1634 *
1635 * When parsing a Spanned using {@link Spanned#nextSpanTransition(int, int, Class)}, empty spans
1636 * will (correctly) create span transitions, and calling getSpans on a slice of text bounded by
1637 * one of these transitions will (correctly) include the empty overlapping span.
1638 *
1639 * However, these empty spans should not be taken into account when layouting or rendering the
1640 * string and this method provides a way to filter getSpans' results accordingly.
1641 *
1642 * @param spans A list of spans retrieved using {@link Spanned#getSpans(int, int, Class)} from
1643 * the <code>spanned</code>
1644 * @param spanned The Spanned from which spans were extracted
1645 * @return A subset of spans where empty spans ({@link Spanned#getSpanStart(Object)} ==
1646 * {@link Spanned#getSpanEnd(Object)} have been removed. The initial order is preserved
1647 * @hide
1648 */
1649 @SuppressWarnings("unchecked")
1650 public static <T> T[] removeEmptySpans(T[] spans, Spanned spanned, Class<T> klass) {
1651 T[] copy = null;
1652 int count = 0;
1653
1654 for (int i = 0; i < spans.length; i++) {
1655 final T span = spans[i];
1656 final int start = spanned.getSpanStart(span);
1657 final int end = spanned.getSpanEnd(span);
1658
1659 if (start == end) {
1660 if (copy == null) {
1661 copy = (T[]) Array.newInstance(klass, spans.length - 1);
1662 System.arraycopy(spans, 0, copy, 0, i);
1663 count = i;
1664 }
1665 } else {
1666 if (copy != null) {
1667 copy[count] = span;
1668 count++;
1669 }
1670 }
1671 }
1672
1673 if (copy != null) {
1674 T[] result = (T[]) Array.newInstance(klass, count);
1675 System.arraycopy(copy, 0, result, 0, count);
1676 return result;
1677 } else {
1678 return spans;
1679 }
1680 }
1681
Gilles Debunne6c488de2012-03-01 16:20:35 -08001682 /**
1683 * Pack 2 int values into a long, useful as a return value for a range
1684 * @see #unpackRangeStartFromLong(long)
1685 * @see #unpackRangeEndFromLong(long)
1686 * @hide
1687 */
1688 public static long packRangeInLong(int start, int end) {
1689 return (((long) start) << 32) | end;
1690 }
1691
1692 /**
1693 * Get the start value from a range packed in a long by {@link #packRangeInLong(int, int)}
1694 * @see #unpackRangeEndFromLong(long)
1695 * @see #packRangeInLong(int, int)
1696 * @hide
1697 */
1698 public static int unpackRangeStartFromLong(long range) {
1699 return (int) (range >>> 32);
1700 }
1701
1702 /**
1703 * Get the end value from a range packed in a long by {@link #packRangeInLong(int, int)}
1704 * @see #unpackRangeStartFromLong(long)
1705 * @see #packRangeInLong(int, int)
1706 * @hide
1707 */
1708 public static int unpackRangeEndFromLong(long range) {
1709 return (int) (range & 0x00000000FFFFFFFFL);
1710 }
1711
Fabrice Di Megliod3d9f3f2012-09-18 12:55:32 -07001712 /**
1713 * Return the layout direction for a given Locale
1714 *
1715 * @param locale the Locale for which we want the layout direction. Can be null.
1716 * @return the layout direction. This may be one of:
1717 * {@link android.view.View#LAYOUT_DIRECTION_LTR} or
1718 * {@link android.view.View#LAYOUT_DIRECTION_RTL}.
1719 *
1720 * Be careful: this code will need to be updated when vertical scripts will be supported
1721 */
1722 public static int getLayoutDirectionFromLocale(Locale locale) {
1723 if (locale != null && !locale.equals(Locale.ROOT)) {
1724 final String scriptSubtag = ICU.getScript(ICU.addLikelySubtags(locale.toString()));
1725 if (scriptSubtag == null) return getLayoutDirectionFromFirstChar(locale);
1726
1727 if (scriptSubtag.equalsIgnoreCase(ARAB_SCRIPT_SUBTAG) ||
1728 scriptSubtag.equalsIgnoreCase(HEBR_SCRIPT_SUBTAG)) {
1729 return View.LAYOUT_DIRECTION_RTL;
1730 }
1731 }
1732
1733 return View.LAYOUT_DIRECTION_LTR;
1734 }
1735
1736 /**
1737 * Fallback algorithm to detect the locale direction. Rely on the fist char of the
1738 * localized locale name. This will not work if the localized locale name is in English
1739 * (this is the case for ICU 4.4 and "Urdu" script)
1740 *
1741 * @param locale
1742 * @return the layout direction. This may be one of:
1743 * {@link View#LAYOUT_DIRECTION_LTR} or
1744 * {@link View#LAYOUT_DIRECTION_RTL}.
1745 *
1746 * Be careful: this code will need to be updated when vertical scripts will be supported
1747 *
1748 * @hide
1749 */
1750 private static int getLayoutDirectionFromFirstChar(Locale locale) {
1751 switch(Character.getDirectionality(locale.getDisplayName(locale).charAt(0))) {
1752 case Character.DIRECTIONALITY_RIGHT_TO_LEFT:
1753 case Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC:
1754 return View.LAYOUT_DIRECTION_RTL;
1755
1756 case Character.DIRECTIONALITY_LEFT_TO_RIGHT:
1757 default:
1758 return View.LAYOUT_DIRECTION_LTR;
1759 }
1760 }
1761
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001762 private static Object sLock = new Object();
Fabrice Di Megliod3d9f3f2012-09-18 12:55:32 -07001763
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001764 private static char[] sTemp = null;
Fabrice Di Megliocb332642011-09-23 19:08:04 -07001765
1766 private static String[] EMPTY_STRING_ARRAY = new String[]{};
1767
1768 private static final char ZWNBS_CHAR = '\uFEFF';
Fabrice Di Megliod3d9f3f2012-09-18 12:55:32 -07001769
1770 private static String ARAB_SCRIPT_SUBTAG = "Arab";
1771 private static String HEBR_SCRIPT_SUBTAG = "Hebr";
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001772}