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