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