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