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 static java.lang.annotation.RetentionPolicy.SOURCE; 20 21 import android.annotation.FloatRange; 22 import android.annotation.IntDef; 23 import android.annotation.IntRange; 24 import android.annotation.NonNull; 25 import android.annotation.Nullable; 26 import android.annotation.PluralsRes; 27 import android.compat.annotation.UnsupportedAppUsage; 28 import android.content.Context; 29 import android.content.res.Resources; 30 import android.graphics.Typeface; 31 import android.icu.lang.UCharacter; 32 import android.icu.text.CaseMap; 33 import android.icu.text.Edits; 34 import android.icu.util.ULocale; 35 import android.os.Parcel; 36 import android.os.Parcelable; 37 import android.sysprop.DisplayProperties; 38 import android.text.style.AbsoluteSizeSpan; 39 import android.text.style.AccessibilityClickableSpan; 40 import android.text.style.AccessibilityReplacementSpan; 41 import android.text.style.AccessibilityURLSpan; 42 import android.text.style.AlignmentSpan; 43 import android.text.style.BackgroundColorSpan; 44 import android.text.style.BulletSpan; 45 import android.text.style.CharacterStyle; 46 import android.text.style.EasyEditSpan; 47 import android.text.style.ForegroundColorSpan; 48 import android.text.style.LeadingMarginSpan; 49 import android.text.style.LineBackgroundSpan; 50 import android.text.style.LineBreakConfigSpan; 51 import android.text.style.LineHeightSpan; 52 import android.text.style.LocaleSpan; 53 import android.text.style.ParagraphStyle; 54 import android.text.style.QuoteSpan; 55 import android.text.style.RelativeSizeSpan; 56 import android.text.style.ReplacementSpan; 57 import android.text.style.ScaleXSpan; 58 import android.text.style.SpellCheckSpan; 59 import android.text.style.StrikethroughSpan; 60 import android.text.style.StyleSpan; 61 import android.text.style.SubscriptSpan; 62 import android.text.style.SuggestionRangeSpan; 63 import android.text.style.SuggestionSpan; 64 import android.text.style.SuperscriptSpan; 65 import android.text.style.TextAppearanceSpan; 66 import android.text.style.TtsSpan; 67 import android.text.style.TypefaceSpan; 68 import android.text.style.URLSpan; 69 import android.text.style.UnderlineSpan; 70 import android.text.style.UpdateAppearance; 71 import android.util.EmptyArray; 72 import android.util.Log; 73 import android.util.Printer; 74 import android.view.View; 75 76 import com.android.internal.util.ArrayUtils; 77 import com.android.internal.util.Preconditions; 78 79 import java.lang.annotation.Retention; 80 import java.lang.reflect.Array; 81 import java.util.BitSet; 82 import java.util.Iterator; 83 import java.util.List; 84 import java.util.Locale; 85 import java.util.regex.Pattern; 86 87 public class TextUtils { 88 private static final String TAG = "TextUtils"; 89 90 // Zero-width character used to fill ellipsized strings when codepoint length must be preserved. 91 /* package */ static final char ELLIPSIS_FILLER = '\uFEFF'; // ZERO WIDTH NO-BREAK SPACE 92 93 // TODO: Based on CLDR data, these need to be localized for Dzongkha (dz) and perhaps 94 // Hong Kong Traditional Chinese (zh-Hant-HK), but that may need to depend on the actual word 95 // being ellipsized and not the locale. 96 private static final String ELLIPSIS_NORMAL = "\u2026"; // HORIZONTAL ELLIPSIS (…) 97 private static final String ELLIPSIS_TWO_DOTS = "\u2025"; // TWO DOT LEADER (‥) 98 99 /** @hide */ 100 public static final int LINE_FEED_CODE_POINT = 10; 101 102 private static final int NBSP_CODE_POINT = 160; 103 104 /** 105 * Flags for {@link #makeSafeForPresentation(String, int, float, int)} 106 * 107 * @hide 108 */ 109 @Retention(SOURCE) 110 @IntDef(flag = true, prefix = "CLEAN_STRING_FLAG_", 111 value = {SAFE_STRING_FLAG_TRIM, SAFE_STRING_FLAG_SINGLE_LINE, 112 SAFE_STRING_FLAG_FIRST_LINE}) 113 public @interface SafeStringFlags {} 114 115 /** 116 * Remove {@link Character#isWhitespace(int) whitespace} and non-breaking spaces from the edges 117 * of the label. 118 * 119 * @see #makeSafeForPresentation(String, int, float, int) 120 */ 121 public static final int SAFE_STRING_FLAG_TRIM = 0x1; 122 123 /** 124 * Force entire string into single line of text (no newlines). Cannot be set at the same time as 125 * {@link #SAFE_STRING_FLAG_FIRST_LINE}. 126 * 127 * @see #makeSafeForPresentation(String, int, float, int) 128 */ 129 public static final int SAFE_STRING_FLAG_SINGLE_LINE = 0x2; 130 131 /** 132 * Return only first line of text (truncate at first newline). Cannot be set at the same time as 133 * {@link #SAFE_STRING_FLAG_SINGLE_LINE}. 134 * 135 * @see #makeSafeForPresentation(String, int, float, int) 136 */ 137 public static final int SAFE_STRING_FLAG_FIRST_LINE = 0x4; 138 139 /** {@hide} */ 140 @NonNull getEllipsisString(@onNull TextUtils.TruncateAt method)141 public static String getEllipsisString(@NonNull TextUtils.TruncateAt method) { 142 return (method == TextUtils.TruncateAt.END_SMALL) ? ELLIPSIS_TWO_DOTS : ELLIPSIS_NORMAL; 143 } 144 TextUtils()145 private TextUtils() { /* cannot be instantiated */ } 146 147 @android.ravenwood.annotation.RavenwoodKeep getChars(CharSequence s, int start, int end, char[] dest, int destoff)148 public static void getChars(CharSequence s, int start, int end, 149 char[] dest, int destoff) { 150 Class<? extends CharSequence> c = s.getClass(); 151 152 if (c == String.class) 153 ((String) s).getChars(start, end, dest, destoff); 154 else if (c == StringBuffer.class) 155 ((StringBuffer) s).getChars(start, end, dest, destoff); 156 else if (c == StringBuilder.class) 157 ((StringBuilder) s).getChars(start, end, dest, destoff); 158 else if (s instanceof GetChars) 159 ((GetChars) s).getChars(start, end, dest, destoff); 160 else { 161 for (int i = start; i < end; i++) 162 dest[destoff++] = s.charAt(i); 163 } 164 } 165 166 @android.ravenwood.annotation.RavenwoodKeep indexOf(CharSequence s, char ch)167 public static int indexOf(CharSequence s, char ch) { 168 return indexOf(s, ch, 0); 169 } 170 171 @android.ravenwood.annotation.RavenwoodKeep indexOf(CharSequence s, char ch, int start)172 public static int indexOf(CharSequence s, char ch, int start) { 173 Class<? extends CharSequence> c = s.getClass(); 174 175 if (c == String.class) 176 return ((String) s).indexOf(ch, start); 177 178 return indexOf(s, ch, start, s.length()); 179 } 180 181 @android.ravenwood.annotation.RavenwoodKeep indexOf(CharSequence s, char ch, int start, int end)182 public static int indexOf(CharSequence s, char ch, int start, int end) { 183 Class<? extends CharSequence> c = s.getClass(); 184 185 if (s instanceof GetChars || c == StringBuffer.class || 186 c == StringBuilder.class || c == String.class) { 187 final int INDEX_INCREMENT = 500; 188 char[] temp = obtain(INDEX_INCREMENT); 189 190 while (start < end) { 191 int segend = start + INDEX_INCREMENT; 192 if (segend > end) 193 segend = end; 194 195 getChars(s, start, segend, temp, 0); 196 197 int count = segend - start; 198 for (int i = 0; i < count; i++) { 199 if (temp[i] == ch) { 200 recycle(temp); 201 return i + start; 202 } 203 } 204 205 start = segend; 206 } 207 208 recycle(temp); 209 return -1; 210 } 211 212 for (int i = start; i < end; i++) 213 if (s.charAt(i) == ch) 214 return i; 215 216 return -1; 217 } 218 219 @android.ravenwood.annotation.RavenwoodKeep lastIndexOf(CharSequence s, char ch)220 public static int lastIndexOf(CharSequence s, char ch) { 221 return lastIndexOf(s, ch, s.length() - 1); 222 } 223 224 @android.ravenwood.annotation.RavenwoodKeep lastIndexOf(CharSequence s, char ch, int last)225 public static int lastIndexOf(CharSequence s, char ch, int last) { 226 Class<? extends CharSequence> c = s.getClass(); 227 228 if (c == String.class) 229 return ((String) s).lastIndexOf(ch, last); 230 231 return lastIndexOf(s, ch, 0, last); 232 } 233 234 @android.ravenwood.annotation.RavenwoodKeep lastIndexOf(CharSequence s, char ch, int start, int last)235 public static int lastIndexOf(CharSequence s, char ch, 236 int start, int last) { 237 if (last < 0) 238 return -1; 239 if (last >= s.length()) 240 last = s.length() - 1; 241 242 int end = last + 1; 243 244 Class<? extends CharSequence> c = s.getClass(); 245 246 if (s instanceof GetChars || c == StringBuffer.class || 247 c == StringBuilder.class || c == String.class) { 248 final int INDEX_INCREMENT = 500; 249 char[] temp = obtain(INDEX_INCREMENT); 250 251 while (start < end) { 252 int segstart = end - INDEX_INCREMENT; 253 if (segstart < start) 254 segstart = start; 255 256 getChars(s, segstart, end, temp, 0); 257 258 int count = end - segstart; 259 for (int i = count - 1; i >= 0; i--) { 260 if (temp[i] == ch) { 261 recycle(temp); 262 return i + segstart; 263 } 264 } 265 266 end = segstart; 267 } 268 269 recycle(temp); 270 return -1; 271 } 272 273 for (int i = end - 1; i >= start; i--) 274 if (s.charAt(i) == ch) 275 return i; 276 277 return -1; 278 } 279 280 @android.ravenwood.annotation.RavenwoodKeep indexOf(CharSequence s, CharSequence needle)281 public static int indexOf(CharSequence s, CharSequence needle) { 282 return indexOf(s, needle, 0, s.length()); 283 } 284 285 @android.ravenwood.annotation.RavenwoodKeep indexOf(CharSequence s, CharSequence needle, int start)286 public static int indexOf(CharSequence s, CharSequence needle, int start) { 287 return indexOf(s, needle, start, s.length()); 288 } 289 290 @android.ravenwood.annotation.RavenwoodKeep indexOf(CharSequence s, CharSequence needle, int start, int end)291 public static int indexOf(CharSequence s, CharSequence needle, 292 int start, int end) { 293 int nlen = needle.length(); 294 if (nlen == 0) 295 return start; 296 297 char c = needle.charAt(0); 298 299 for (;;) { 300 start = indexOf(s, c, start); 301 if (start > end - nlen) { 302 break; 303 } 304 305 if (start < 0) { 306 return -1; 307 } 308 309 if (regionMatches(s, start, needle, 0, nlen)) { 310 return start; 311 } 312 313 start++; 314 } 315 return -1; 316 } 317 318 @android.ravenwood.annotation.RavenwoodKeep regionMatches(CharSequence one, int toffset, CharSequence two, int ooffset, int len)319 public static boolean regionMatches(CharSequence one, int toffset, 320 CharSequence two, int ooffset, 321 int len) { 322 int tempLen = 2 * len; 323 if (tempLen < len) { 324 // Integer overflow; len is unreasonably large 325 throw new IndexOutOfBoundsException(); 326 } 327 char[] temp = obtain(tempLen); 328 329 getChars(one, toffset, toffset + len, temp, 0); 330 getChars(two, ooffset, ooffset + len, temp, len); 331 332 boolean match = true; 333 for (int i = 0; i < len; i++) { 334 if (temp[i] != temp[i + len]) { 335 match = false; 336 break; 337 } 338 } 339 340 recycle(temp); 341 return match; 342 } 343 344 /** 345 * Create a new String object containing the given range of characters 346 * from the source string. This is different than simply calling 347 * {@link CharSequence#subSequence(int, int) CharSequence.subSequence} 348 * in that it does not preserve any style runs in the source sequence, 349 * allowing a more efficient implementation. 350 */ 351 @android.ravenwood.annotation.RavenwoodKeep substring(CharSequence source, int start, int end)352 public static String substring(CharSequence source, int start, int end) { 353 if (source instanceof String) 354 return ((String) source).substring(start, end); 355 if (source instanceof StringBuilder) 356 return ((StringBuilder) source).substring(start, end); 357 if (source instanceof StringBuffer) 358 return ((StringBuffer) source).substring(start, end); 359 360 char[] temp = obtain(end - start); 361 getChars(source, start, end, temp, 0); 362 String ret = new String(temp, 0, end - start); 363 recycle(temp); 364 365 return ret; 366 } 367 368 369 /** 370 * Returns the longest prefix of a string for which the UTF-8 encoding fits into the given 371 * number of bytes, with the additional guarantee that the string is not truncated in the middle 372 * of a valid surrogate pair. 373 * 374 * <p>Unpaired surrogates are counted as taking 3 bytes of storage. However, a subsequent 375 * attempt to actually encode a string containing unpaired surrogates is likely to be rejected 376 * by the UTF-8 implementation. 377 * 378 * (copied from google/thirdparty) 379 * 380 * @param str a string 381 * @param maxbytes the maximum number of UTF-8 encoded bytes 382 * @return the beginning of the string, so that it uses at most maxbytes bytes in UTF-8 383 * @throws IndexOutOfBoundsException if maxbytes is negative 384 * 385 * @hide 386 */ truncateStringForUtf8Storage(String str, int maxbytes)387 public static String truncateStringForUtf8Storage(String str, int maxbytes) { 388 if (maxbytes < 0) { 389 throw new IndexOutOfBoundsException(); 390 } 391 392 int bytes = 0; 393 for (int i = 0, len = str.length(); i < len; i++) { 394 char c = str.charAt(i); 395 if (c < 0x80) { 396 bytes += 1; 397 } else if (c < 0x800) { 398 bytes += 2; 399 } else if (c < Character.MIN_SURROGATE 400 || c > Character.MAX_SURROGATE 401 || str.codePointAt(i) < Character.MIN_SUPPLEMENTARY_CODE_POINT) { 402 bytes += 3; 403 } else { 404 bytes += 4; 405 i += (bytes > maxbytes) ? 0 : 1; 406 } 407 if (bytes > maxbytes) { 408 return str.substring(0, i); 409 } 410 } 411 return str; 412 } 413 414 415 /** 416 * Returns a string containing the tokens joined by delimiters. 417 * 418 * @param delimiter a CharSequence that will be inserted between the tokens. If null, the string 419 * "null" will be used as the delimiter. 420 * @param tokens an array objects to be joined. Strings will be formed from the objects by 421 * calling object.toString(). If tokens is null, a NullPointerException will be thrown. If 422 * tokens is an empty array, an empty string will be returned. 423 */ 424 @android.ravenwood.annotation.RavenwoodKeep join(@onNull CharSequence delimiter, @NonNull Object[] tokens)425 public static String join(@NonNull CharSequence delimiter, @NonNull Object[] tokens) { 426 final int length = tokens.length; 427 if (length == 0) { 428 return ""; 429 } 430 final StringBuilder sb = new StringBuilder(); 431 sb.append(tokens[0]); 432 for (int i = 1; i < length; i++) { 433 sb.append(delimiter); 434 sb.append(tokens[i]); 435 } 436 return sb.toString(); 437 } 438 439 /** 440 * Returns a string containing the tokens joined by delimiters. 441 * 442 * @param delimiter a CharSequence that will be inserted between the tokens. If null, the string 443 * "null" will be used as the delimiter. 444 * @param tokens an array objects to be joined. Strings will be formed from the objects by 445 * calling object.toString(). If tokens is null, a NullPointerException will be thrown. If 446 * tokens is empty, an empty string will be returned. 447 */ 448 @android.ravenwood.annotation.RavenwoodKeep join(@onNull CharSequence delimiter, @NonNull Iterable tokens)449 public static String join(@NonNull CharSequence delimiter, @NonNull Iterable tokens) { 450 final Iterator<?> it = tokens.iterator(); 451 if (!it.hasNext()) { 452 return ""; 453 } 454 final StringBuilder sb = new StringBuilder(); 455 sb.append(it.next()); 456 while (it.hasNext()) { 457 sb.append(delimiter); 458 sb.append(it.next()); 459 } 460 return sb.toString(); 461 } 462 463 /** 464 * 465 * This method yields the same result as {@code text.split(expression, -1)} except that if 466 * {@code text.isEmpty()} then this method returns an empty array whereas 467 * {@code "".split(expression, -1)} would have returned an array with a single {@code ""}. 468 * 469 * The {@code -1} means that trailing empty Strings are not removed from the result; for 470 * example split("a,", "," ) returns {"a", ""}. Note that whether a leading zero-width match 471 * can result in a leading {@code ""} depends on whether your app 472 * {@link android.content.pm.ApplicationInfo#targetSdkVersion targets an SDK version} 473 * {@code <= 28}; see {@link Pattern#split(CharSequence, int)}. 474 * 475 * @param text the string to split 476 * @param expression the regular expression to match 477 * @return an array of strings. The array will be empty if text is empty 478 * 479 * @throws NullPointerException if expression or text is null 480 */ 481 @android.ravenwood.annotation.RavenwoodKeep split(String text, String expression)482 public static String[] split(String text, String expression) { 483 if (text.length() == 0) { 484 return EmptyArray.STRING; 485 } else { 486 return text.split(expression, -1); 487 } 488 } 489 490 /** 491 * Splits a string on a pattern. This method yields the same result as 492 * {@code pattern.split(text, -1)} except that if {@code text.isEmpty()} then this method 493 * returns an empty array whereas {@code pattern.split("", -1)} would have returned an array 494 * with a single {@code ""}. 495 * 496 * The {@code -1} means that trailing empty Strings are not removed from the result; 497 * Note that whether a leading zero-width match can result in a leading {@code ""} depends 498 * on whether your app {@link android.content.pm.ApplicationInfo#targetSdkVersion targets 499 * an SDK version} {@code <= 28}; see {@link Pattern#split(CharSequence, int)}. 500 * 501 * @param text the string to split 502 * @param pattern the regular expression to match 503 * @return an array of strings. The array will be empty if text is empty 504 * 505 * @throws NullPointerException if expression or text is null 506 */ 507 @android.ravenwood.annotation.RavenwoodKeep split(String text, Pattern pattern)508 public static String[] split(String text, Pattern pattern) { 509 if (text.length() == 0) { 510 return EmptyArray.STRING; 511 } else { 512 return pattern.split(text, -1); 513 } 514 } 515 516 /** 517 * An interface for splitting strings according to rules that are opaque to the user of this 518 * interface. This also has less overhead than split, which uses regular expressions and 519 * allocates an array to hold the results. 520 * 521 * <p>The most efficient way to use this class is: 522 * 523 * <pre> 524 * // Once 525 * TextUtils.StringSplitter splitter = new TextUtils.SimpleStringSplitter(delimiter); 526 * 527 * // Once per string to split 528 * splitter.setString(string); 529 * for (String s : splitter) { 530 * ... 531 * } 532 * </pre> 533 */ 534 public interface StringSplitter extends Iterable<String> { setString(String string)535 public void setString(String string); 536 } 537 538 /** 539 * A simple string splitter. 540 * 541 * <p>If the final character in the string to split is the delimiter then no empty string will 542 * be returned for the empty string after that delimeter. That is, splitting <tt>"a,b,"</tt> on 543 * comma will return <tt>"a", "b"</tt>, not <tt>"a", "b", ""</tt>. 544 */ 545 @android.ravenwood.annotation.RavenwoodKeepWholeClass 546 public static class SimpleStringSplitter implements StringSplitter, Iterator<String> { 547 private String mString; 548 private char mDelimiter; 549 private int mPosition; 550 private int mLength; 551 552 /** 553 * Initializes the splitter. setString may be called later. 554 * @param delimiter the delimeter on which to split 555 */ SimpleStringSplitter(char delimiter)556 public SimpleStringSplitter(char delimiter) { 557 mDelimiter = delimiter; 558 } 559 560 /** 561 * Sets the string to split 562 * @param string the string to split 563 */ setString(String string)564 public void setString(String string) { 565 mString = string; 566 mPosition = 0; 567 mLength = mString.length(); 568 } 569 iterator()570 public Iterator<String> iterator() { 571 return this; 572 } 573 hasNext()574 public boolean hasNext() { 575 return mPosition < mLength; 576 } 577 next()578 public String next() { 579 int end = mString.indexOf(mDelimiter, mPosition); 580 if (end == -1) { 581 end = mLength; 582 } 583 String nextString = mString.substring(mPosition, end); 584 mPosition = end + 1; // Skip the delimiter. 585 return nextString; 586 } 587 remove()588 public void remove() { 589 throw new UnsupportedOperationException(); 590 } 591 } 592 stringOrSpannedString(CharSequence source)593 public static CharSequence stringOrSpannedString(CharSequence source) { 594 if (source == null) 595 return null; 596 if (source instanceof SpannedString) 597 return source; 598 if (source instanceof Spanned) 599 return new SpannedString(source); 600 601 return source.toString(); 602 } 603 604 /** 605 * Returns true if the string is null or 0-length. 606 * @param str the string to be examined 607 * @return true if str is null or zero length 608 */ 609 @android.ravenwood.annotation.RavenwoodKeep isEmpty(@ullable CharSequence str)610 public static boolean isEmpty(@Nullable CharSequence str) { 611 return str == null || str.length() == 0; 612 } 613 614 /** {@hide} */ 615 @android.ravenwood.annotation.RavenwoodKeep nullIfEmpty(@ullable String str)616 public static String nullIfEmpty(@Nullable String str) { 617 return isEmpty(str) ? null : str; 618 } 619 620 /** {@hide} */ 621 @android.ravenwood.annotation.RavenwoodKeep emptyIfNull(@ullable String str)622 public static String emptyIfNull(@Nullable String str) { 623 return str == null ? "" : str; 624 } 625 626 /** {@hide} */ 627 @android.ravenwood.annotation.RavenwoodKeep firstNotEmpty(@ullable String a, @NonNull String b)628 public static String firstNotEmpty(@Nullable String a, @NonNull String b) { 629 return !isEmpty(a) ? a : Preconditions.checkStringNotEmpty(b); 630 } 631 632 /** {@hide} */ 633 @android.ravenwood.annotation.RavenwoodKeep length(@ullable String s)634 public static int length(@Nullable String s) { 635 return s != null ? s.length() : 0; 636 } 637 638 /** 639 * @return interned string if it's null. 640 * @hide 641 */ 642 @android.ravenwood.annotation.RavenwoodKeep safeIntern(String s)643 public static String safeIntern(String s) { 644 return (s != null) ? s.intern() : null; 645 } 646 647 /** 648 * Returns the length that the specified CharSequence would have if 649 * spaces and ASCII control characters were trimmed from the start and end, 650 * as by {@link String#trim}. 651 */ 652 @android.ravenwood.annotation.RavenwoodKeep getTrimmedLength(CharSequence s)653 public static int getTrimmedLength(CharSequence s) { 654 int len = s.length(); 655 656 int start = 0; 657 while (start < len && s.charAt(start) <= ' ') { 658 start++; 659 } 660 661 int end = len; 662 while (end > start && s.charAt(end - 1) <= ' ') { 663 end--; 664 } 665 666 return end - start; 667 } 668 669 /** 670 * Returns true if a and b are equal, including if they are both null. 671 * <p><i>Note: In platform versions 1.1 and earlier, this method only worked well if 672 * both the arguments were instances of String.</i></p> 673 * @param a first CharSequence to check 674 * @param b second CharSequence to check 675 * @return true if a and b are equal 676 */ 677 @android.ravenwood.annotation.RavenwoodKeep equals(CharSequence a, CharSequence b)678 public static boolean equals(CharSequence a, CharSequence b) { 679 if (a == b) return true; 680 int length; 681 if (a != null && b != null && (length = a.length()) == b.length()) { 682 if (a instanceof String && b instanceof String) { 683 return a.equals(b); 684 } else { 685 for (int i = 0; i < length; i++) { 686 if (a.charAt(i) != b.charAt(i)) return false; 687 } 688 return true; 689 } 690 } 691 return false; 692 } 693 694 /** 695 * This function only reverses individual {@code char}s and not their associated 696 * spans. It doesn't support surrogate pairs (that correspond to non-BMP code points), combining 697 * sequences or conjuncts either. 698 * @deprecated Do not use. 699 */ 700 @Deprecated getReverse(CharSequence source, int start, int end)701 public static CharSequence getReverse(CharSequence source, int start, int end) { 702 return new Reverser(source, start, end); 703 } 704 705 private static class Reverser 706 implements CharSequence, GetChars 707 { Reverser(CharSequence source, int start, int end)708 public Reverser(CharSequence source, int start, int end) { 709 mSource = source; 710 mStart = start; 711 mEnd = end; 712 } 713 length()714 public int length() { 715 return mEnd - mStart; 716 } 717 subSequence(int start, int end)718 public CharSequence subSequence(int start, int end) { 719 char[] buf = new char[end - start]; 720 721 getChars(start, end, buf, 0); 722 return new String(buf); 723 } 724 725 @Override toString()726 public String toString() { 727 return subSequence(0, length()).toString(); 728 } 729 charAt(int off)730 public char charAt(int off) { 731 return (char) UCharacter.getMirror(mSource.charAt(mEnd - 1 - off)); 732 } 733 734 @SuppressWarnings("deprecation") getChars(int start, int end, char[] dest, int destoff)735 public void getChars(int start, int end, char[] dest, int destoff) { 736 TextUtils.getChars(mSource, start + mStart, end + mStart, 737 dest, destoff); 738 AndroidCharacter.mirror(dest, 0, end - start); 739 740 int len = end - start; 741 int n = (end - start) / 2; 742 for (int i = 0; i < n; i++) { 743 char tmp = dest[destoff + i]; 744 745 dest[destoff + i] = dest[destoff + len - i - 1]; 746 dest[destoff + len - i - 1] = tmp; 747 } 748 } 749 750 private CharSequence mSource; 751 private int mStart; 752 private int mEnd; 753 } 754 755 /** @hide */ 756 public static final int ALIGNMENT_SPAN = 1; 757 /** @hide */ 758 public static final int FIRST_SPAN = ALIGNMENT_SPAN; 759 /** @hide */ 760 public static final int FOREGROUND_COLOR_SPAN = 2; 761 /** @hide */ 762 public static final int RELATIVE_SIZE_SPAN = 3; 763 /** @hide */ 764 public static final int SCALE_X_SPAN = 4; 765 /** @hide */ 766 public static final int STRIKETHROUGH_SPAN = 5; 767 /** @hide */ 768 public static final int UNDERLINE_SPAN = 6; 769 /** @hide */ 770 public static final int STYLE_SPAN = 7; 771 /** @hide */ 772 public static final int BULLET_SPAN = 8; 773 /** @hide */ 774 public static final int QUOTE_SPAN = 9; 775 /** @hide */ 776 public static final int LEADING_MARGIN_SPAN = 10; 777 /** @hide */ 778 public static final int URL_SPAN = 11; 779 /** @hide */ 780 public static final int BACKGROUND_COLOR_SPAN = 12; 781 /** @hide */ 782 public static final int TYPEFACE_SPAN = 13; 783 /** @hide */ 784 public static final int SUPERSCRIPT_SPAN = 14; 785 /** @hide */ 786 public static final int SUBSCRIPT_SPAN = 15; 787 /** @hide */ 788 public static final int ABSOLUTE_SIZE_SPAN = 16; 789 /** @hide */ 790 public static final int TEXT_APPEARANCE_SPAN = 17; 791 /** @hide */ 792 public static final int ANNOTATION = 18; 793 /** @hide */ 794 public static final int SUGGESTION_SPAN = 19; 795 /** @hide */ 796 public static final int SPELL_CHECK_SPAN = 20; 797 /** @hide */ 798 public static final int SUGGESTION_RANGE_SPAN = 21; 799 /** @hide */ 800 public static final int EASY_EDIT_SPAN = 22; 801 /** @hide */ 802 public static final int LOCALE_SPAN = 23; 803 /** @hide */ 804 public static final int TTS_SPAN = 24; 805 /** @hide */ 806 public static final int ACCESSIBILITY_CLICKABLE_SPAN = 25; 807 /** @hide */ 808 public static final int ACCESSIBILITY_URL_SPAN = 26; 809 /** @hide */ 810 public static final int LINE_BACKGROUND_SPAN = 27; 811 /** @hide */ 812 public static final int LINE_HEIGHT_SPAN = 28; 813 /** @hide */ 814 public static final int ACCESSIBILITY_REPLACEMENT_SPAN = 29; 815 /** @hide */ 816 public static final int LINE_BREAK_CONFIG_SPAN = 30; 817 /** @hide */ 818 public static final int LAST_SPAN = LINE_BREAK_CONFIG_SPAN; 819 820 /** 821 * Flatten a CharSequence and whatever styles can be copied across processes 822 * into the parcel. 823 */ writeToParcel(@ullable CharSequence cs, @NonNull Parcel p, int parcelableFlags)824 public static void writeToParcel(@Nullable CharSequence cs, @NonNull Parcel p, 825 int parcelableFlags) { 826 if (cs instanceof Spanned) { 827 p.writeInt(0); 828 p.writeString8(cs.toString()); 829 830 Spanned sp = (Spanned) cs; 831 Object[] os = sp.getSpans(0, cs.length(), Object.class); 832 833 // note to people adding to this: check more specific types 834 // before more generic types. also notice that it uses 835 // "if" instead of "else if" where there are interfaces 836 // so one object can be several. 837 838 for (int i = 0; i < os.length; i++) { 839 Object o = os[i]; 840 Object prop = os[i]; 841 842 if (prop instanceof CharacterStyle) { 843 prop = ((CharacterStyle) prop).getUnderlying(); 844 } 845 846 if (prop instanceof ParcelableSpan) { 847 final ParcelableSpan ps = (ParcelableSpan) prop; 848 final int spanTypeId = ps.getSpanTypeIdInternal(); 849 if (spanTypeId < FIRST_SPAN || spanTypeId > LAST_SPAN) { 850 Log.e(TAG, "External class \"" + ps.getClass().getSimpleName() 851 + "\" is attempting to use the frameworks-only ParcelableSpan" 852 + " interface"); 853 } else { 854 p.writeInt(spanTypeId); 855 ps.writeToParcelInternal(p, parcelableFlags); 856 writeWhere(p, sp, o); 857 } 858 } 859 } 860 861 p.writeInt(0); 862 } else { 863 p.writeInt(1); 864 if (cs != null) { 865 p.writeString8(cs.toString()); 866 } else { 867 p.writeString8(null); 868 } 869 } 870 } 871 writeWhere(Parcel p, Spanned sp, Object o)872 private static void writeWhere(Parcel p, Spanned sp, Object o) { 873 p.writeInt(sp.getSpanStart(o)); 874 p.writeInt(sp.getSpanEnd(o)); 875 p.writeInt(sp.getSpanFlags(o)); 876 } 877 878 public static final Parcelable.Creator<CharSequence> CHAR_SEQUENCE_CREATOR 879 = new Parcelable.Creator<CharSequence>() { 880 /** 881 * Read and return a new CharSequence, possibly with styles, 882 * from the parcel. 883 */ 884 public CharSequence createFromParcel(Parcel p) { 885 int kind = p.readInt(); 886 887 String string = p.readString8(); 888 if (string == null) { 889 return null; 890 } 891 892 if (kind == 1) { 893 return string; 894 } 895 896 SpannableString sp = new SpannableString(string); 897 898 while (true) { 899 kind = p.readInt(); 900 901 if (kind == 0) 902 break; 903 904 final Object span; 905 switch (kind) { 906 case ALIGNMENT_SPAN: 907 span = new AlignmentSpan.Standard(p); 908 break; 909 910 case FOREGROUND_COLOR_SPAN: 911 span = new ForegroundColorSpan(p); 912 break; 913 914 case RELATIVE_SIZE_SPAN: 915 span = new RelativeSizeSpan(p); 916 break; 917 918 case SCALE_X_SPAN: 919 span = new ScaleXSpan(p); 920 break; 921 922 case STRIKETHROUGH_SPAN: 923 span = new StrikethroughSpan(p); 924 break; 925 926 case UNDERLINE_SPAN: 927 span = new UnderlineSpan(p); 928 break; 929 930 case STYLE_SPAN: 931 span = new StyleSpan(p); 932 break; 933 934 case BULLET_SPAN: 935 span = new BulletSpan(p); 936 break; 937 938 case QUOTE_SPAN: 939 span = new QuoteSpan(p); 940 break; 941 942 case LEADING_MARGIN_SPAN: 943 span = new LeadingMarginSpan.Standard(p); 944 break; 945 946 case URL_SPAN: 947 span = new URLSpan(p); 948 break; 949 950 case BACKGROUND_COLOR_SPAN: 951 span = new BackgroundColorSpan(p); 952 break; 953 954 case TYPEFACE_SPAN: 955 span = new TypefaceSpan(p); 956 break; 957 958 case SUPERSCRIPT_SPAN: 959 span = new SuperscriptSpan(p); 960 break; 961 962 case SUBSCRIPT_SPAN: 963 span = new SubscriptSpan(p); 964 break; 965 966 case ABSOLUTE_SIZE_SPAN: 967 span = new AbsoluteSizeSpan(p); 968 break; 969 970 case TEXT_APPEARANCE_SPAN: 971 span = new TextAppearanceSpan(p); 972 break; 973 974 case ANNOTATION: 975 span = new Annotation(p); 976 break; 977 978 case SUGGESTION_SPAN: 979 span = new SuggestionSpan(p); 980 break; 981 982 case SPELL_CHECK_SPAN: 983 span = new SpellCheckSpan(p); 984 break; 985 986 case SUGGESTION_RANGE_SPAN: 987 span = new SuggestionRangeSpan(p); 988 break; 989 990 case EASY_EDIT_SPAN: 991 span = new EasyEditSpan(p); 992 break; 993 994 case LOCALE_SPAN: 995 span = new LocaleSpan(p); 996 break; 997 998 case TTS_SPAN: 999 span = new TtsSpan(p); 1000 break; 1001 1002 case ACCESSIBILITY_CLICKABLE_SPAN: 1003 span = new AccessibilityClickableSpan(p); 1004 break; 1005 1006 case ACCESSIBILITY_URL_SPAN: 1007 span = new AccessibilityURLSpan(p); 1008 break; 1009 1010 case LINE_BACKGROUND_SPAN: 1011 span = new LineBackgroundSpan.Standard(p); 1012 break; 1013 1014 case LINE_HEIGHT_SPAN: 1015 span = new LineHeightSpan.Standard(p); 1016 break; 1017 1018 case ACCESSIBILITY_REPLACEMENT_SPAN: 1019 span = new AccessibilityReplacementSpan(p); 1020 break; 1021 1022 case LINE_BREAK_CONFIG_SPAN: 1023 span = LineBreakConfigSpan.CREATOR.createFromParcel(p); 1024 break; 1025 1026 default: 1027 throw new RuntimeException("bogus span encoding " + kind); 1028 } 1029 readSpan(p, sp, span); 1030 } 1031 1032 return sp; 1033 } 1034 1035 public CharSequence[] newArray(int size) 1036 { 1037 return new CharSequence[size]; 1038 } 1039 }; 1040 1041 /** 1042 * Debugging tool to print the spans in a CharSequence. The output will 1043 * be printed one span per line. If the CharSequence is not a Spanned, 1044 * then the entire string will be printed on a single line. 1045 */ dumpSpans(CharSequence cs, Printer printer, String prefix)1046 public static void dumpSpans(CharSequence cs, Printer printer, String prefix) { 1047 if (cs instanceof Spanned) { 1048 Spanned sp = (Spanned) cs; 1049 Object[] os = sp.getSpans(0, cs.length(), Object.class); 1050 1051 for (int i = 0; i < os.length; i++) { 1052 Object o = os[i]; 1053 printer.println(prefix + cs.subSequence(sp.getSpanStart(o), 1054 sp.getSpanEnd(o)) + ": " 1055 + Integer.toHexString(System.identityHashCode(o)) 1056 + " " + o.getClass().getCanonicalName() 1057 + " (" + sp.getSpanStart(o) + "-" + sp.getSpanEnd(o) 1058 + ") fl=#" + sp.getSpanFlags(o)); 1059 } 1060 } else { 1061 printer.println(prefix + cs + ": (no spans)"); 1062 } 1063 } 1064 1065 /** 1066 * Return a new CharSequence in which each of the source strings is 1067 * replaced by the corresponding element of the destinations. 1068 */ replace(CharSequence template, String[] sources, CharSequence[] destinations)1069 public static CharSequence replace(CharSequence template, 1070 String[] sources, 1071 CharSequence[] destinations) { 1072 SpannableStringBuilder tb = new SpannableStringBuilder(template); 1073 1074 for (int i = 0; i < sources.length; i++) { 1075 int where = indexOf(tb, sources[i]); 1076 1077 if (where >= 0) 1078 tb.setSpan(sources[i], where, where + sources[i].length(), 1079 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 1080 } 1081 1082 for (int i = 0; i < sources.length; i++) { 1083 int start = tb.getSpanStart(sources[i]); 1084 int end = tb.getSpanEnd(sources[i]); 1085 1086 if (start >= 0) { 1087 tb.replace(start, end, destinations[i]); 1088 } 1089 } 1090 1091 return tb; 1092 } 1093 1094 /** 1095 * Replace instances of "^1", "^2", etc. in the 1096 * <code>template</code> CharSequence with the corresponding 1097 * <code>values</code>. "^^" is used to produce a single caret in 1098 * the output. Only up to 9 replacement values are supported, 1099 * "^10" will be produce the first replacement value followed by a 1100 * '0'. 1101 * 1102 * @param template the input text containing "^1"-style 1103 * placeholder values. This object is not modified; a copy is 1104 * returned. 1105 * 1106 * @param values CharSequences substituted into the template. The 1107 * first is substituted for "^1", the second for "^2", and so on. 1108 * 1109 * @return the new CharSequence produced by doing the replacement 1110 * 1111 * @throws IllegalArgumentException if the template requests a 1112 * value that was not provided, or if more than 9 values are 1113 * provided. 1114 */ expandTemplate(CharSequence template, CharSequence... values)1115 public static CharSequence expandTemplate(CharSequence template, 1116 CharSequence... values) { 1117 if (values.length > 9) { 1118 throw new IllegalArgumentException("max of 9 values are supported"); 1119 } 1120 1121 SpannableStringBuilder ssb = new SpannableStringBuilder(template); 1122 1123 try { 1124 int i = 0; 1125 while (i < ssb.length()) { 1126 if (ssb.charAt(i) == '^') { 1127 char next = ssb.charAt(i+1); 1128 if (next == '^') { 1129 ssb.delete(i+1, i+2); 1130 ++i; 1131 continue; 1132 } else if (Character.isDigit(next)) { 1133 int which = Character.getNumericValue(next) - 1; 1134 if (which < 0) { 1135 throw new IllegalArgumentException( 1136 "template requests value ^" + (which+1)); 1137 } 1138 if (which >= values.length) { 1139 throw new IllegalArgumentException( 1140 "template requests value ^" + (which+1) + 1141 "; only " + values.length + " provided"); 1142 } 1143 ssb.replace(i, i+2, values[which]); 1144 i += values[which].length(); 1145 continue; 1146 } 1147 } 1148 ++i; 1149 } 1150 } catch (IndexOutOfBoundsException ignore) { 1151 // happens when ^ is the last character in the string. 1152 } 1153 return ssb; 1154 } 1155 getOffsetBefore(CharSequence text, int offset)1156 public static int getOffsetBefore(CharSequence text, int offset) { 1157 if (offset == 0) 1158 return 0; 1159 if (offset == 1) 1160 return 0; 1161 1162 char c = text.charAt(offset - 1); 1163 1164 if (c >= '\uDC00' && c <= '\uDFFF') { 1165 char c1 = text.charAt(offset - 2); 1166 1167 if (c1 >= '\uD800' && c1 <= '\uDBFF') 1168 offset -= 2; 1169 else 1170 offset -= 1; 1171 } else { 1172 offset -= 1; 1173 } 1174 1175 if (text instanceof Spanned) { 1176 ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset, 1177 ReplacementSpan.class); 1178 1179 for (int i = 0; i < spans.length; i++) { 1180 int start = ((Spanned) text).getSpanStart(spans[i]); 1181 int end = ((Spanned) text).getSpanEnd(spans[i]); 1182 1183 if (start < offset && end > offset) 1184 offset = start; 1185 } 1186 } 1187 1188 return offset; 1189 } 1190 getOffsetAfter(CharSequence text, int offset)1191 public static int getOffsetAfter(CharSequence text, int offset) { 1192 int len = text.length(); 1193 1194 if (offset == len) 1195 return len; 1196 if (offset == len - 1) 1197 return len; 1198 1199 char c = text.charAt(offset); 1200 1201 if (c >= '\uD800' && c <= '\uDBFF') { 1202 char c1 = text.charAt(offset + 1); 1203 1204 if (c1 >= '\uDC00' && c1 <= '\uDFFF') 1205 offset += 2; 1206 else 1207 offset += 1; 1208 } else { 1209 offset += 1; 1210 } 1211 1212 if (text instanceof Spanned) { 1213 ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset, 1214 ReplacementSpan.class); 1215 1216 for (int i = 0; i < spans.length; i++) { 1217 int start = ((Spanned) text).getSpanStart(spans[i]); 1218 int end = ((Spanned) text).getSpanEnd(spans[i]); 1219 1220 if (start < offset && end > offset) 1221 offset = end; 1222 } 1223 } 1224 1225 return offset; 1226 } 1227 readSpan(Parcel p, Spannable sp, Object o)1228 private static void readSpan(Parcel p, Spannable sp, Object o) { 1229 sp.setSpan(o, p.readInt(), p.readInt(), p.readInt()); 1230 } 1231 1232 /** 1233 * Copies the spans from the region <code>start...end</code> in 1234 * <code>source</code> to the region 1235 * <code>destoff...destoff+end-start</code> in <code>dest</code>. 1236 * Spans in <code>source</code> that begin before <code>start</code> 1237 * or end after <code>end</code> but overlap this range are trimmed 1238 * as if they began at <code>start</code> or ended at <code>end</code>. 1239 * 1240 * @throws IndexOutOfBoundsException if any of the copied spans 1241 * are out of range in <code>dest</code>. 1242 */ copySpansFrom(Spanned source, int start, int end, Class kind, Spannable dest, int destoff)1243 public static void copySpansFrom(Spanned source, int start, int end, 1244 Class kind, 1245 Spannable dest, int destoff) { 1246 if (kind == null) { 1247 kind = Object.class; 1248 } 1249 1250 Object[] spans = source.getSpans(start, end, kind); 1251 1252 for (int i = 0; i < spans.length; i++) { 1253 int st = source.getSpanStart(spans[i]); 1254 int en = source.getSpanEnd(spans[i]); 1255 int fl = source.getSpanFlags(spans[i]); 1256 1257 if (st < start) 1258 st = start; 1259 if (en > end) 1260 en = end; 1261 1262 dest.setSpan(spans[i], st - start + destoff, en - start + destoff, 1263 fl); 1264 } 1265 } 1266 1267 /** 1268 * Transforms a CharSequences to uppercase, copying the sources spans and keeping them spans as 1269 * much as possible close to their relative original places. If uppercase string is identical 1270 * to the sources, the source itself is returned instead of being copied. 1271 * 1272 * If copySpans is set, source must be an instance of Spanned. 1273 * 1274 * {@hide} 1275 */ 1276 @NonNull toUpperCase(@ullable Locale locale, @NonNull CharSequence source, boolean copySpans)1277 public static CharSequence toUpperCase(@Nullable Locale locale, @NonNull CharSequence source, 1278 boolean copySpans) { 1279 final Edits edits = new Edits(); 1280 if (!copySpans) { // No spans. Just uppercase the characters. 1281 final StringBuilder result = CaseMap.toUpper().apply( 1282 locale, source, new StringBuilder(), edits); 1283 return edits.hasChanges() ? result : source; 1284 } 1285 1286 final SpannableStringBuilder result = CaseMap.toUpper().apply( 1287 locale, source, new SpannableStringBuilder(), edits); 1288 if (!edits.hasChanges()) { 1289 // No changes happened while capitalizing. We can return the source as it was. 1290 return source; 1291 } 1292 1293 final Edits.Iterator iterator = edits.getFineIterator(); 1294 final int sourceLength = source.length(); 1295 final Spanned spanned = (Spanned) source; 1296 final Object[] spans = spanned.getSpans(0, sourceLength, Object.class); 1297 for (Object span : spans) { 1298 final int sourceStart = spanned.getSpanStart(span); 1299 final int sourceEnd = spanned.getSpanEnd(span); 1300 final int flags = spanned.getSpanFlags(span); 1301 // Make sure the indices are not at the end of the string, since in that case 1302 // iterator.findSourceIndex() would fail. 1303 final int destStart = sourceStart == sourceLength ? result.length() : 1304 toUpperMapToDest(iterator, sourceStart); 1305 final int destEnd = sourceEnd == sourceLength ? result.length() : 1306 toUpperMapToDest(iterator, sourceEnd); 1307 result.setSpan(span, destStart, destEnd, flags); 1308 } 1309 return result; 1310 } 1311 1312 // helper method for toUpperCase() toUpperMapToDest(Edits.Iterator iterator, int sourceIndex)1313 private static int toUpperMapToDest(Edits.Iterator iterator, int sourceIndex) { 1314 // Guaranteed to succeed if sourceIndex < source.length(). 1315 iterator.findSourceIndex(sourceIndex); 1316 if (sourceIndex == iterator.sourceIndex()) { 1317 return iterator.destinationIndex(); 1318 } 1319 // We handle the situation differently depending on if we are in the changed slice or an 1320 // unchanged one: In an unchanged slice, we can find the exact location the span 1321 // boundary was before and map there. 1322 // 1323 // But in a changed slice, we need to treat the whole destination slice as an atomic unit. 1324 // We adjust the span boundary to the end of that slice to reduce of the chance of adjacent 1325 // spans in the source overlapping in the result. (The choice for the end vs the beginning 1326 // is somewhat arbitrary, but was taken because we except to see slightly more spans only 1327 // affecting a base character compared to spans only affecting a combining character.) 1328 if (iterator.hasChange()) { 1329 return iterator.destinationIndex() + iterator.newLength(); 1330 } else { 1331 // Move the index 1:1 along with this unchanged piece of text. 1332 return iterator.destinationIndex() + (sourceIndex - iterator.sourceIndex()); 1333 } 1334 } 1335 1336 public enum TruncateAt { 1337 START, 1338 MIDDLE, 1339 END, 1340 MARQUEE, 1341 /** 1342 * @hide 1343 */ 1344 @UnsupportedAppUsage 1345 END_SMALL 1346 } 1347 1348 public interface EllipsizeCallback { 1349 /** 1350 * This method is called to report that the specified region of 1351 * text was ellipsized away by a call to {@link #ellipsize}. 1352 */ ellipsized(int start, int end)1353 public void ellipsized(int start, int end); 1354 } 1355 1356 /** 1357 * Returns the original text if it fits in the specified width 1358 * given the properties of the specified Paint, 1359 * or, if it does not fit, a truncated 1360 * copy with ellipsis character added at the specified edge or center. 1361 */ ellipsize(CharSequence text, TextPaint p, float avail, TruncateAt where)1362 public static CharSequence ellipsize(CharSequence text, 1363 TextPaint p, 1364 float avail, TruncateAt where) { 1365 return ellipsize(text, p, avail, where, false, null); 1366 } 1367 1368 /** 1369 * Returns the original text if it fits in the specified width 1370 * given the properties of the specified Paint, 1371 * or, if it does not fit, a copy with ellipsis character added 1372 * at the specified edge or center. 1373 * If <code>preserveLength</code> is specified, the returned copy 1374 * will be padded with zero-width spaces to preserve the original 1375 * length and offsets instead of truncating. 1376 * If <code>callback</code> is non-null, it will be called to 1377 * report the start and end of the ellipsized range. TextDirection 1378 * is determined by the first strong directional character. 1379 */ ellipsize(CharSequence text, TextPaint paint, float avail, TruncateAt where, boolean preserveLength, @Nullable EllipsizeCallback callback)1380 public static CharSequence ellipsize(CharSequence text, 1381 TextPaint paint, 1382 float avail, TruncateAt where, 1383 boolean preserveLength, 1384 @Nullable EllipsizeCallback callback) { 1385 return ellipsize(text, paint, avail, where, preserveLength, callback, 1386 TextDirectionHeuristics.FIRSTSTRONG_LTR, 1387 getEllipsisString(where)); 1388 } 1389 1390 /** 1391 * Returns the original text if it fits in the specified width 1392 * given the properties of the specified Paint, 1393 * or, if it does not fit, a copy with ellipsis character added 1394 * at the specified edge or center. 1395 * If <code>preserveLength</code> is specified, the returned copy 1396 * will be padded with zero-width spaces to preserve the original 1397 * length and offsets instead of truncating. 1398 * If <code>callback</code> is non-null, it will be called to 1399 * report the start and end of the ellipsized range. 1400 * 1401 * @hide 1402 */ ellipsize(CharSequence text, TextPaint paint, float avail, TruncateAt where, boolean preserveLength, @Nullable EllipsizeCallback callback, TextDirectionHeuristic textDir, String ellipsis)1403 public static CharSequence ellipsize(CharSequence text, 1404 TextPaint paint, 1405 float avail, TruncateAt where, 1406 boolean preserveLength, 1407 @Nullable EllipsizeCallback callback, 1408 TextDirectionHeuristic textDir, String ellipsis) { 1409 1410 int len = text.length(); 1411 1412 MeasuredParagraph mt = null; 1413 try { 1414 mt = MeasuredParagraph.buildForMeasurement(paint, text, 0, text.length(), textDir, mt); 1415 float width = mt.getWholeWidth(); 1416 1417 if (width <= avail) { 1418 if (callback != null) { 1419 callback.ellipsized(0, 0); 1420 } 1421 1422 return text; 1423 } 1424 1425 // XXX assumes ellipsis string does not require shaping and 1426 // is unaffected by style 1427 float ellipsiswid = paint.measureText(ellipsis); 1428 avail -= ellipsiswid; 1429 1430 int left = 0; 1431 int right = len; 1432 if (avail < 0) { 1433 // it all goes 1434 } else if (where == TruncateAt.START) { 1435 right = len - mt.breakText(len, false, avail); 1436 } else if (where == TruncateAt.END || where == TruncateAt.END_SMALL) { 1437 left = mt.breakText(len, true, avail); 1438 } else { 1439 right = len - mt.breakText(len, false, avail / 2); 1440 avail -= mt.measure(right, len); 1441 left = mt.breakText(right, true, avail); 1442 } 1443 1444 if (callback != null) { 1445 callback.ellipsized(left, right); 1446 } 1447 1448 final char[] buf = mt.getChars(); 1449 Spanned sp = text instanceof Spanned ? (Spanned) text : null; 1450 1451 final int removed = right - left; 1452 final int remaining = len - removed; 1453 if (preserveLength) { 1454 if (remaining > 0 && removed >= ellipsis.length()) { 1455 ellipsis.getChars(0, ellipsis.length(), buf, left); 1456 left += ellipsis.length(); 1457 } // else skip the ellipsis 1458 for (int i = left; i < right; i++) { 1459 buf[i] = ELLIPSIS_FILLER; 1460 } 1461 String s = new String(buf, 0, len); 1462 if (sp == null) { 1463 return s; 1464 } 1465 SpannableString ss = new SpannableString(s); 1466 copySpansFrom(sp, 0, len, Object.class, ss, 0); 1467 return ss; 1468 } 1469 1470 if (remaining == 0) { 1471 return ""; 1472 } 1473 1474 if (sp == null) { 1475 StringBuilder sb = new StringBuilder(remaining + ellipsis.length()); 1476 sb.append(buf, 0, left); 1477 sb.append(ellipsis); 1478 sb.append(buf, right, len - right); 1479 return sb.toString(); 1480 } 1481 1482 SpannableStringBuilder ssb = new SpannableStringBuilder(); 1483 ssb.append(text, 0, left); 1484 ssb.append(ellipsis); 1485 ssb.append(text, right, len); 1486 return ssb; 1487 } finally { 1488 if (mt != null) { 1489 mt.recycle(); 1490 } 1491 } 1492 } 1493 1494 /** 1495 * Formats a list of CharSequences by repeatedly inserting the separator between them, 1496 * but stopping when the resulting sequence is too wide for the specified width. 1497 * 1498 * This method actually tries to fit the maximum number of elements. So if {@code "A, 11 more" 1499 * fits}, {@code "A, B, 10 more"} doesn't fit, but {@code "A, B, C, 9 more"} fits again (due to 1500 * the glyphs for the digits being very wide, for example), it returns 1501 * {@code "A, B, C, 9 more"}. Because of this, this method may be inefficient for very long 1502 * lists. 1503 * 1504 * Note that the elements of the returned value, as well as the string for {@code moreId}, will 1505 * be bidi-wrapped using {@link BidiFormatter#unicodeWrap} based on the locale of the input 1506 * Context. If the input {@code Context} is null, the default BidiFormatter from 1507 * {@link BidiFormatter#getInstance()} will be used. 1508 * 1509 * @param context the {@code Context} to get the {@code moreId} resource from. If {@code null}, 1510 * an ellipsis (U+2026) would be used for {@code moreId}. 1511 * @param elements the list to format 1512 * @param separator a separator, such as {@code ", "} 1513 * @param paint the Paint with which to measure the text 1514 * @param avail the horizontal width available for the text (in pixels) 1515 * @param moreId the resource ID for the pluralized string to insert at the end of sequence when 1516 * some of the elements don't fit. 1517 * 1518 * @return the formatted CharSequence. If even the shortest sequence (e.g. {@code "A, 11 more"}) 1519 * doesn't fit, it will return an empty string. 1520 */ 1521 listEllipsize(@ullable Context context, @Nullable List<CharSequence> elements, @NonNull String separator, @NonNull TextPaint paint, @FloatRange(from=0.0,fromInclusive=false) float avail, @PluralsRes int moreId)1522 public static CharSequence listEllipsize(@Nullable Context context, 1523 @Nullable List<CharSequence> elements, @NonNull String separator, 1524 @NonNull TextPaint paint, @FloatRange(from=0.0,fromInclusive=false) float avail, 1525 @PluralsRes int moreId) { 1526 if (elements == null) { 1527 return ""; 1528 } 1529 final int totalLen = elements.size(); 1530 if (totalLen == 0) { 1531 return ""; 1532 } 1533 1534 final Resources res; 1535 final BidiFormatter bidiFormatter; 1536 if (context == null) { 1537 res = null; 1538 bidiFormatter = BidiFormatter.getInstance(); 1539 } else { 1540 res = context.getResources(); 1541 bidiFormatter = BidiFormatter.getInstance(res.getConfiguration().getLocales().get(0)); 1542 } 1543 1544 final SpannableStringBuilder output = new SpannableStringBuilder(); 1545 final int[] endIndexes = new int[totalLen]; 1546 for (int i = 0; i < totalLen; i++) { 1547 output.append(bidiFormatter.unicodeWrap(elements.get(i))); 1548 if (i != totalLen - 1) { // Insert a separator, except at the very end. 1549 output.append(separator); 1550 } 1551 endIndexes[i] = output.length(); 1552 } 1553 1554 for (int i = totalLen - 1; i >= 0; i--) { 1555 // Delete the tail of the string, cutting back to one less element. 1556 output.delete(endIndexes[i], output.length()); 1557 1558 final int remainingElements = totalLen - i - 1; 1559 if (remainingElements > 0) { 1560 CharSequence morePiece = (res == null) ? 1561 ELLIPSIS_NORMAL : 1562 res.getQuantityString(moreId, remainingElements, remainingElements); 1563 morePiece = bidiFormatter.unicodeWrap(morePiece); 1564 output.append(morePiece); 1565 } 1566 1567 final float width = paint.measureText(output, 0, output.length()); 1568 if (width <= avail) { // The string fits. 1569 return output; 1570 } 1571 } 1572 return ""; // Nothing fits. 1573 } 1574 1575 /** 1576 * Converts a CharSequence of the comma-separated form "Andy, Bob, 1577 * Charles, David" that is too wide to fit into the specified width 1578 * into one like "Andy, Bob, 2 more". 1579 * 1580 * @param text the text to truncate 1581 * @param p the Paint with which to measure the text 1582 * @param avail the horizontal width available for the text (in pixels) 1583 * @param oneMore the string for "1 more" in the current locale 1584 * @param more the string for "%d more" in the current locale 1585 * 1586 * @deprecated Do not use. This is not internationalized, and has known issues 1587 * with right-to-left text, languages that have more than one plural form, languages 1588 * that use a different character as a comma-like separator, etc. 1589 * Use {@link #listEllipsize} instead. 1590 */ 1591 @Deprecated commaEllipsize(CharSequence text, TextPaint p, float avail, String oneMore, String more)1592 public static CharSequence commaEllipsize(CharSequence text, 1593 TextPaint p, float avail, 1594 String oneMore, 1595 String more) { 1596 return commaEllipsize(text, p, avail, oneMore, more, 1597 TextDirectionHeuristics.FIRSTSTRONG_LTR); 1598 } 1599 1600 /** 1601 * @hide 1602 */ 1603 @Deprecated commaEllipsize(CharSequence text, TextPaint p, float avail, String oneMore, String more, TextDirectionHeuristic textDir)1604 public static CharSequence commaEllipsize(CharSequence text, TextPaint p, 1605 float avail, String oneMore, String more, TextDirectionHeuristic textDir) { 1606 1607 MeasuredParagraph mt = null; 1608 MeasuredParagraph tempMt = null; 1609 try { 1610 int len = text.length(); 1611 mt = MeasuredParagraph.buildForMeasurement(p, text, 0, len, textDir, mt); 1612 final float width = mt.getWholeWidth(); 1613 if (width <= avail) { 1614 return text; 1615 } 1616 1617 char[] buf = mt.getChars(); 1618 1619 int commaCount = 0; 1620 for (int i = 0; i < len; i++) { 1621 if (buf[i] == ',') { 1622 commaCount++; 1623 } 1624 } 1625 1626 int remaining = commaCount + 1; 1627 1628 int ok = 0; 1629 String okFormat = ""; 1630 1631 int w = 0; 1632 int count = 0; 1633 float[] widths = mt.getWidths().getRawArray(); 1634 1635 for (int i = 0; i < len; i++) { 1636 w += widths[i]; 1637 1638 if (buf[i] == ',') { 1639 count++; 1640 1641 String format; 1642 // XXX should not insert spaces, should be part of string 1643 // XXX should use plural rules and not assume English plurals 1644 if (--remaining == 1) { 1645 format = " " + oneMore; 1646 } else { 1647 format = " " + String.format(more, remaining); 1648 } 1649 1650 // XXX this is probably ok, but need to look at it more 1651 tempMt = MeasuredParagraph.buildForMeasurement( 1652 p, format, 0, format.length(), textDir, tempMt); 1653 float moreWid = tempMt.getWholeWidth(); 1654 1655 if (w + moreWid <= avail) { 1656 ok = i + 1; 1657 okFormat = format; 1658 } 1659 } 1660 } 1661 1662 SpannableStringBuilder out = new SpannableStringBuilder(okFormat); 1663 out.insert(0, text, 0, ok); 1664 return out; 1665 } finally { 1666 if (mt != null) { 1667 mt.recycle(); 1668 } 1669 if (tempMt != null) { 1670 tempMt.recycle(); 1671 } 1672 } 1673 } 1674 1675 // Returns true if the character's presence could affect RTL layout. 1676 // 1677 // In order to be fast, the code is intentionally rough and quite conservative in its 1678 // considering inclusion of any non-BMP or surrogate characters or anything in the bidi 1679 // blocks or any bidi formatting characters with a potential to affect RTL layout. 1680 /* package */ couldAffectRtl(char c)1681 static boolean couldAffectRtl(char c) { 1682 return (0x0590 <= c && c <= 0x08FF) || // RTL scripts 1683 c == 0x200E || // Bidi format character 1684 c == 0x200F || // Bidi format character 1685 (0x202A <= c && c <= 0x202E) || // Bidi format characters 1686 (0x2066 <= c && c <= 0x2069) || // Bidi format characters 1687 (0xD800 <= c && c <= 0xDFFF) || // Surrogate pairs 1688 (0xFB1D <= c && c <= 0xFDFF) || // Hebrew and Arabic presentation forms 1689 (0xFE70 <= c && c <= 0xFEFE); // Arabic presentation forms 1690 } 1691 1692 // Returns true if there is no character present that may potentially affect RTL layout. 1693 // Since this calls couldAffectRtl() above, it's also quite conservative, in the way that 1694 // it may return 'false' (needs bidi) although careful consideration may tell us it should 1695 // return 'true' (does not need bidi). 1696 /* package */ doesNotNeedBidi(char[] text, int start, int len)1697 static boolean doesNotNeedBidi(char[] text, int start, int len) { 1698 final int end = start + len; 1699 for (int i = start; i < end; i++) { 1700 if (couldAffectRtl(text[i])) { 1701 return false; 1702 } 1703 } 1704 return true; 1705 } 1706 1707 @android.ravenwood.annotation.RavenwoodReplace obtain(int len)1708 /* package */ static char[] obtain(int len) { 1709 char[] buf; 1710 1711 synchronized (sLock) { 1712 buf = sTemp; 1713 sTemp = null; 1714 } 1715 1716 if (buf == null || buf.length < len) 1717 buf = ArrayUtils.newUnpaddedCharArray(len); 1718 1719 return buf; 1720 } 1721 obtain$ravenwood(int len)1722 /* package */ static char[] obtain$ravenwood(int len) { 1723 return new char[len]; 1724 } 1725 1726 @android.ravenwood.annotation.RavenwoodReplace recycle(char[] temp)1727 /* package */ static void recycle(char[] temp) { 1728 if (temp.length > 1000) 1729 return; 1730 1731 synchronized (sLock) { 1732 sTemp = temp; 1733 } 1734 } 1735 recycle$ravenwood(char[] temp)1736 /* package */ static void recycle$ravenwood(char[] temp) { 1737 // Handled by typical GC 1738 } 1739 1740 /** 1741 * Html-encode the string. 1742 * @param s the string to be encoded 1743 * @return the encoded string 1744 */ 1745 @android.ravenwood.annotation.RavenwoodKeep htmlEncode(String s)1746 public static String htmlEncode(String s) { 1747 StringBuilder sb = new StringBuilder(); 1748 char c; 1749 for (int i = 0; i < s.length(); i++) { 1750 c = s.charAt(i); 1751 switch (c) { 1752 case '<': 1753 sb.append("<"); //$NON-NLS-1$ 1754 break; 1755 case '>': 1756 sb.append(">"); //$NON-NLS-1$ 1757 break; 1758 case '&': 1759 sb.append("&"); //$NON-NLS-1$ 1760 break; 1761 case '\'': 1762 //http://www.w3.org/TR/xhtml1 1763 // The named character reference ' (the apostrophe, U+0027) was introduced in 1764 // XML 1.0 but does not appear in HTML. Authors should therefore use ' instead 1765 // of ' to work as expected in HTML 4 user agents. 1766 sb.append("'"); //$NON-NLS-1$ 1767 break; 1768 case '"': 1769 sb.append("""); //$NON-NLS-1$ 1770 break; 1771 default: 1772 sb.append(c); 1773 } 1774 } 1775 return sb.toString(); 1776 } 1777 1778 /** 1779 * Returns a CharSequence concatenating the specified CharSequences, 1780 * retaining their spans if any. 1781 * 1782 * If there are no parameters, an empty string will be returned. 1783 * 1784 * If the number of parameters is exactly one, that parameter is returned as output, even if it 1785 * is null. 1786 * 1787 * If the number of parameters is at least two, any null CharSequence among the parameters is 1788 * treated as if it was the string <code>"null"</code>. 1789 * 1790 * If there are paragraph spans in the source CharSequences that satisfy paragraph boundary 1791 * requirements in the sources but would no longer satisfy them in the concatenated 1792 * CharSequence, they may get extended in the resulting CharSequence or not retained. 1793 */ concat(CharSequence... text)1794 public static CharSequence concat(CharSequence... text) { 1795 if (text.length == 0) { 1796 return ""; 1797 } 1798 1799 if (text.length == 1) { 1800 return text[0]; 1801 } 1802 1803 boolean spanned = false; 1804 for (CharSequence piece : text) { 1805 if (piece instanceof Spanned) { 1806 spanned = true; 1807 break; 1808 } 1809 } 1810 1811 if (spanned) { 1812 final SpannableStringBuilder ssb = new SpannableStringBuilder(); 1813 for (CharSequence piece : text) { 1814 // If a piece is null, we append the string "null" for compatibility with the 1815 // behavior of StringBuilder and the behavior of the concat() method in earlier 1816 // versions of Android. 1817 ssb.append(piece == null ? "null" : piece); 1818 } 1819 return new SpannedString(ssb); 1820 } else { 1821 final StringBuilder sb = new StringBuilder(); 1822 for (CharSequence piece : text) { 1823 sb.append(piece); 1824 } 1825 return sb.toString(); 1826 } 1827 } 1828 1829 /** 1830 * Returns whether the given CharSequence contains any printable characters. 1831 */ 1832 @android.ravenwood.annotation.RavenwoodKeep isGraphic(CharSequence str)1833 public static boolean isGraphic(CharSequence str) { 1834 final int len = str.length(); 1835 for (int cp, i=0; i<len; i+=Character.charCount(cp)) { 1836 cp = Character.codePointAt(str, i); 1837 int gc = Character.getType(cp); 1838 if (gc != Character.CONTROL 1839 && gc != Character.FORMAT 1840 && gc != Character.SURROGATE 1841 && gc != Character.UNASSIGNED 1842 && gc != Character.LINE_SEPARATOR 1843 && gc != Character.PARAGRAPH_SEPARATOR 1844 && gc != Character.SPACE_SEPARATOR) { 1845 return true; 1846 } 1847 } 1848 return false; 1849 } 1850 1851 /** 1852 * Returns whether this character is a printable character. 1853 * 1854 * This does not support non-BMP characters and should not be used. 1855 * 1856 * @deprecated Use {@link #isGraphic(CharSequence)} instead. 1857 */ 1858 @Deprecated 1859 @android.ravenwood.annotation.RavenwoodKeep isGraphic(char c)1860 public static boolean isGraphic(char c) { 1861 int gc = Character.getType(c); 1862 return gc != Character.CONTROL 1863 && gc != Character.FORMAT 1864 && gc != Character.SURROGATE 1865 && gc != Character.UNASSIGNED 1866 && gc != Character.LINE_SEPARATOR 1867 && gc != Character.PARAGRAPH_SEPARATOR 1868 && gc != Character.SPACE_SEPARATOR; 1869 } 1870 1871 /** 1872 * Returns whether the given CharSequence contains only digits. 1873 */ 1874 @android.ravenwood.annotation.RavenwoodKeep isDigitsOnly(CharSequence str)1875 public static boolean isDigitsOnly(CharSequence str) { 1876 final int len = str.length(); 1877 for (int cp, i = 0; i < len; i += Character.charCount(cp)) { 1878 cp = Character.codePointAt(str, i); 1879 if (!Character.isDigit(cp)) { 1880 return false; 1881 } 1882 } 1883 return true; 1884 } 1885 1886 /** 1887 * @hide 1888 */ 1889 @android.ravenwood.annotation.RavenwoodKeep isPrintableAscii(final char c)1890 public static boolean isPrintableAscii(final char c) { 1891 final int asciiFirst = 0x20; 1892 final int asciiLast = 0x7E; // included 1893 return (asciiFirst <= c && c <= asciiLast) || c == '\r' || c == '\n'; 1894 } 1895 1896 /** 1897 * @hide 1898 */ 1899 @UnsupportedAppUsage 1900 @android.ravenwood.annotation.RavenwoodKeep isPrintableAsciiOnly(final CharSequence str)1901 public static boolean isPrintableAsciiOnly(final CharSequence str) { 1902 final int len = str.length(); 1903 for (int i = 0; i < len; i++) { 1904 if (!isPrintableAscii(str.charAt(i))) { 1905 return false; 1906 } 1907 } 1908 return true; 1909 } 1910 1911 /** 1912 * Capitalization mode for {@link #getCapsMode}: capitalize all 1913 * characters. This value is explicitly defined to be the same as 1914 * {@link InputType#TYPE_TEXT_FLAG_CAP_CHARACTERS}. 1915 */ 1916 public static final int CAP_MODE_CHARACTERS 1917 = InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS; 1918 1919 /** 1920 * Capitalization mode for {@link #getCapsMode}: capitalize the first 1921 * character of all words. This value is explicitly defined to be the same as 1922 * {@link InputType#TYPE_TEXT_FLAG_CAP_WORDS}. 1923 */ 1924 public static final int CAP_MODE_WORDS 1925 = InputType.TYPE_TEXT_FLAG_CAP_WORDS; 1926 1927 /** 1928 * Capitalization mode for {@link #getCapsMode}: capitalize the first 1929 * character of each sentence. This value is explicitly defined to be the same as 1930 * {@link InputType#TYPE_TEXT_FLAG_CAP_SENTENCES}. 1931 */ 1932 public static final int CAP_MODE_SENTENCES 1933 = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES; 1934 1935 /** 1936 * Determine what caps mode should be in effect at the current offset in 1937 * the text. Only the mode bits set in <var>reqModes</var> will be 1938 * checked. Note that the caps mode flags here are explicitly defined 1939 * to match those in {@link InputType}. 1940 * 1941 * @param cs The text that should be checked for caps modes. 1942 * @param off Location in the text at which to check. 1943 * @param reqModes The modes to be checked: may be any combination of 1944 * {@link #CAP_MODE_CHARACTERS}, {@link #CAP_MODE_WORDS}, and 1945 * {@link #CAP_MODE_SENTENCES}. 1946 * 1947 * @return Returns the actual capitalization modes that can be in effect 1948 * at the current position, which is any combination of 1949 * {@link #CAP_MODE_CHARACTERS}, {@link #CAP_MODE_WORDS}, and 1950 * {@link #CAP_MODE_SENTENCES}. 1951 */ 1952 @android.ravenwood.annotation.RavenwoodKeep getCapsMode(CharSequence cs, int off, int reqModes)1953 public static int getCapsMode(CharSequence cs, int off, int reqModes) { 1954 if (off < 0) { 1955 return 0; 1956 } 1957 1958 int i; 1959 char c; 1960 int mode = 0; 1961 1962 if ((reqModes&CAP_MODE_CHARACTERS) != 0) { 1963 mode |= CAP_MODE_CHARACTERS; 1964 } 1965 if ((reqModes&(CAP_MODE_WORDS|CAP_MODE_SENTENCES)) == 0) { 1966 return mode; 1967 } 1968 1969 // Back over allowed opening punctuation. 1970 1971 for (i = off; i > 0; i--) { 1972 c = cs.charAt(i - 1); 1973 1974 if (c != '"' && c != '\'' && 1975 Character.getType(c) != Character.START_PUNCTUATION) { 1976 break; 1977 } 1978 } 1979 1980 // Start of paragraph, with optional whitespace. 1981 1982 int j = i; 1983 while (j > 0 && ((c = cs.charAt(j - 1)) == ' ' || c == '\t')) { 1984 j--; 1985 } 1986 if (j == 0 || cs.charAt(j - 1) == '\n') { 1987 return mode | CAP_MODE_WORDS; 1988 } 1989 1990 // Or start of word if we are that style. 1991 1992 if ((reqModes&CAP_MODE_SENTENCES) == 0) { 1993 if (i != j) mode |= CAP_MODE_WORDS; 1994 return mode; 1995 } 1996 1997 // There must be a space if not the start of paragraph. 1998 1999 if (i == j) { 2000 return mode; 2001 } 2002 2003 // Back over allowed closing punctuation. 2004 2005 for (; j > 0; j--) { 2006 c = cs.charAt(j - 1); 2007 2008 if (c != '"' && c != '\'' && 2009 Character.getType(c) != Character.END_PUNCTUATION) { 2010 break; 2011 } 2012 } 2013 2014 if (j > 0) { 2015 c = cs.charAt(j - 1); 2016 2017 if (c == '.' || c == '?' || c == '!') { 2018 // Do not capitalize if the word ends with a period but 2019 // also contains a period, in which case it is an abbreviation. 2020 2021 if (c == '.') { 2022 for (int k = j - 2; k >= 0; k--) { 2023 c = cs.charAt(k); 2024 2025 if (c == '.') { 2026 return mode; 2027 } 2028 2029 if (!Character.isLetter(c)) { 2030 break; 2031 } 2032 } 2033 } 2034 2035 return mode | CAP_MODE_SENTENCES; 2036 } 2037 } 2038 2039 return mode; 2040 } 2041 2042 /** 2043 * Does a comma-delimited list 'delimitedString' contain a certain item? 2044 * (without allocating memory) 2045 * 2046 * @hide 2047 */ delimitedStringContains( String delimitedString, char delimiter, String item)2048 public static boolean delimitedStringContains( 2049 String delimitedString, char delimiter, String item) { 2050 if (isEmpty(delimitedString) || isEmpty(item)) { 2051 return false; 2052 } 2053 int pos = -1; 2054 int length = delimitedString.length(); 2055 while ((pos = delimitedString.indexOf(item, pos + 1)) != -1) { 2056 if (pos > 0 && delimitedString.charAt(pos - 1) != delimiter) { 2057 continue; 2058 } 2059 int expectedDelimiterPos = pos + item.length(); 2060 if (expectedDelimiterPos == length) { 2061 // Match at end of string. 2062 return true; 2063 } 2064 if (delimitedString.charAt(expectedDelimiterPos) == delimiter) { 2065 return true; 2066 } 2067 } 2068 return false; 2069 } 2070 2071 /** 2072 * Removes empty spans from the <code>spans</code> array. 2073 * 2074 * When parsing a Spanned using {@link Spanned#nextSpanTransition(int, int, Class)}, empty spans 2075 * will (correctly) create span transitions, and calling getSpans on a slice of text bounded by 2076 * one of these transitions will (correctly) include the empty overlapping span. 2077 * 2078 * However, these empty spans should not be taken into account when layouting or rendering the 2079 * string and this method provides a way to filter getSpans' results accordingly. 2080 * 2081 * @param spans A list of spans retrieved using {@link Spanned#getSpans(int, int, Class)} from 2082 * the <code>spanned</code> 2083 * @param spanned The Spanned from which spans were extracted 2084 * @return A subset of spans where empty spans ({@link Spanned#getSpanStart(Object)} == 2085 * {@link Spanned#getSpanEnd(Object)} have been removed. The initial order is preserved 2086 * @hide 2087 */ 2088 @SuppressWarnings("unchecked") removeEmptySpans(T[] spans, Spanned spanned, Class<T> klass)2089 public static <T> T[] removeEmptySpans(T[] spans, Spanned spanned, Class<T> klass) { 2090 T[] copy = null; 2091 int count = 0; 2092 2093 for (int i = 0; i < spans.length; i++) { 2094 final T span = spans[i]; 2095 final int start = spanned.getSpanStart(span); 2096 final int end = spanned.getSpanEnd(span); 2097 2098 if (start == end) { 2099 if (copy == null) { 2100 copy = (T[]) Array.newInstance(klass, spans.length - 1); 2101 System.arraycopy(spans, 0, copy, 0, i); 2102 count = i; 2103 } 2104 } else { 2105 if (copy != null) { 2106 copy[count] = span; 2107 count++; 2108 } 2109 } 2110 } 2111 2112 if (copy != null) { 2113 T[] result = (T[]) Array.newInstance(klass, count); 2114 System.arraycopy(copy, 0, result, 0, count); 2115 return result; 2116 } else { 2117 return spans; 2118 } 2119 } 2120 2121 /** 2122 * Pack 2 int values into a long, useful as a return value for a range 2123 * @see #unpackRangeStartFromLong(long) 2124 * @see #unpackRangeEndFromLong(long) 2125 * @hide 2126 */ 2127 @UnsupportedAppUsage packRangeInLong(int start, int end)2128 public static long packRangeInLong(int start, int end) { 2129 return (((long) start) << 32) | end; 2130 } 2131 2132 /** 2133 * Get the start value from a range packed in a long by {@link #packRangeInLong(int, int)} 2134 * @see #unpackRangeEndFromLong(long) 2135 * @see #packRangeInLong(int, int) 2136 * @hide 2137 */ 2138 @UnsupportedAppUsage unpackRangeStartFromLong(long range)2139 public static int unpackRangeStartFromLong(long range) { 2140 return (int) (range >>> 32); 2141 } 2142 2143 /** 2144 * Get the end value from a range packed in a long by {@link #packRangeInLong(int, int)} 2145 * @see #unpackRangeStartFromLong(long) 2146 * @see #packRangeInLong(int, int) 2147 * @hide 2148 */ 2149 @UnsupportedAppUsage unpackRangeEndFromLong(long range)2150 public static int unpackRangeEndFromLong(long range) { 2151 return (int) (range & 0x00000000FFFFFFFFL); 2152 } 2153 2154 /** 2155 * Return the layout direction for a given Locale 2156 * 2157 * @param locale the Locale for which we want the layout direction. Can be null. 2158 * @return the layout direction. This may be one of: 2159 * {@link android.view.View#LAYOUT_DIRECTION_LTR} or 2160 * {@link android.view.View#LAYOUT_DIRECTION_RTL}. 2161 * 2162 * Be careful: this code will need to be updated when vertical scripts will be supported 2163 */ getLayoutDirectionFromLocale(Locale locale)2164 public static int getLayoutDirectionFromLocale(Locale locale) { 2165 return ((locale != null && !locale.equals(Locale.ROOT) 2166 && ULocale.forLocale(locale).isRightToLeft()) 2167 // If forcing into RTL layout mode, return RTL as default 2168 || DisplayProperties.debug_force_rtl().orElse(false)) 2169 ? View.LAYOUT_DIRECTION_RTL 2170 : View.LAYOUT_DIRECTION_LTR; 2171 } 2172 2173 /** 2174 * Simple alternative to {@link String#format} which purposefully supports 2175 * only a small handful of substitutions to improve execution speed. 2176 * Benchmarking reveals this optimized alternative performs 6.5x faster for 2177 * a typical format string. 2178 * <p> 2179 * Below is a summary of the limited grammar supported by this method; if 2180 * you need advanced features, please continue using {@link String#format}. 2181 * <ul> 2182 * <li>{@code %b} for {@code boolean} 2183 * <li>{@code %c} for {@code char} 2184 * <li>{@code %d} for {@code int} or {@code long} 2185 * <li>{@code %f} for {@code float} or {@code double} 2186 * <li>{@code %s} for {@code String} 2187 * <li>{@code %x} for hex representation of {@code int} or {@code long} 2188 * <li>{@code %%} for literal {@code %} 2189 * <li>{@code %04d} style grammar to specify the argument width, such as 2190 * {@code %04d} to prefix an {@code int} with zeros or {@code %10b} to 2191 * prefix a {@code boolean} with spaces 2192 * </ul> 2193 * 2194 * @throws IllegalArgumentException if the format string or arguments don't 2195 * match the supported grammar described above. 2196 * @hide 2197 */ 2198 @android.ravenwood.annotation.RavenwoodKeep formatSimple(@onNull String format, Object... args)2199 public static @NonNull String formatSimple(@NonNull String format, Object... args) { 2200 final StringBuilder sb = new StringBuilder(format); 2201 int j = 0; 2202 for (int i = 0; i < sb.length(); ) { 2203 if (sb.charAt(i) == '%') { 2204 char code = sb.charAt(i + 1); 2205 2206 // Decode any argument width request 2207 char prefixChar = '\0'; 2208 int prefixLen = 0; 2209 int consume = 2; 2210 while ('0' <= code && code <= '9') { 2211 if (prefixChar == '\0') { 2212 prefixChar = (code == '0') ? '0' : ' '; 2213 } 2214 prefixLen *= 10; 2215 prefixLen += Character.digit(code, 10); 2216 consume += 1; 2217 code = sb.charAt(i + consume - 1); 2218 } 2219 2220 final String repl; 2221 switch (code) { 2222 case 'b': { 2223 if (j == args.length) { 2224 throw new IllegalArgumentException("Too few arguments"); 2225 } 2226 final Object arg = args[j++]; 2227 if (arg instanceof Boolean) { 2228 repl = Boolean.toString((boolean) arg); 2229 } else { 2230 repl = Boolean.toString(arg != null); 2231 } 2232 break; 2233 } 2234 case 'c': 2235 case 'd': 2236 case 'f': 2237 case 's': { 2238 if (j == args.length) { 2239 throw new IllegalArgumentException("Too few arguments"); 2240 } 2241 final Object arg = args[j++]; 2242 repl = String.valueOf(arg); 2243 break; 2244 } 2245 case 'x': { 2246 if (j == args.length) { 2247 throw new IllegalArgumentException("Too few arguments"); 2248 } 2249 final Object arg = args[j++]; 2250 if (arg instanceof Integer) { 2251 repl = Integer.toHexString((int) arg); 2252 } else if (arg instanceof Long) { 2253 repl = Long.toHexString((long) arg); 2254 } else { 2255 throw new IllegalArgumentException( 2256 "Unsupported hex type " + arg.getClass()); 2257 } 2258 break; 2259 } 2260 case '%': { 2261 repl = "%"; 2262 break; 2263 } 2264 default: { 2265 throw new IllegalArgumentException("Unsupported format code " + code); 2266 } 2267 } 2268 2269 sb.replace(i, i + consume, repl); 2270 2271 // Apply any argument width request 2272 final int prefixInsert = (prefixChar == '0' && repl.charAt(0) == '-') ? 1 : 0; 2273 for (int k = repl.length(); k < prefixLen; k++) { 2274 sb.insert(i + prefixInsert, prefixChar); 2275 } 2276 i += Math.max(repl.length(), prefixLen); 2277 } else { 2278 i++; 2279 } 2280 } 2281 if (j != args.length) { 2282 throw new IllegalArgumentException("Too many arguments"); 2283 } 2284 return sb.toString(); 2285 } 2286 2287 /** 2288 * Returns whether or not the specified spanned text has a style span. 2289 * @hide 2290 */ hasStyleSpan(@onNull Spanned spanned)2291 public static boolean hasStyleSpan(@NonNull Spanned spanned) { 2292 Preconditions.checkArgument(spanned != null); 2293 final Class<?>[] styleClasses = { 2294 CharacterStyle.class, ParagraphStyle.class, UpdateAppearance.class}; 2295 for (Class<?> clazz : styleClasses) { 2296 if (spanned.nextSpanTransition(-1, spanned.length(), clazz) < spanned.length()) { 2297 return true; 2298 } 2299 } 2300 return false; 2301 } 2302 2303 /** 2304 * If the {@code charSequence} is instance of {@link Spanned}, creates a new copy and 2305 * {@link NoCopySpan}'s are removed from the copy. Otherwise the given {@code charSequence} is 2306 * returned as it is. 2307 * 2308 * @hide 2309 */ 2310 @Nullable trimNoCopySpans(@ullable CharSequence charSequence)2311 public static CharSequence trimNoCopySpans(@Nullable CharSequence charSequence) { 2312 if (charSequence != null && charSequence instanceof Spanned) { 2313 // SpannableStringBuilder copy constructor trims NoCopySpans. 2314 return new SpannableStringBuilder(charSequence); 2315 } 2316 return charSequence; 2317 } 2318 2319 /** 2320 * Prepends {@code start} and appends {@code end} to a given {@link StringBuilder} 2321 * 2322 * @hide 2323 */ wrap(StringBuilder builder, String start, String end)2324 public static void wrap(StringBuilder builder, String start, String end) { 2325 builder.insert(0, start); 2326 builder.append(end); 2327 } 2328 2329 /** 2330 * Intent size limitations prevent sending over a megabyte of data. Limit 2331 * text length to 100K characters - 200KB. 2332 */ 2333 private static final int PARCEL_SAFE_TEXT_LENGTH = 100000; 2334 2335 /** 2336 * Trims the text to {@link #PARCEL_SAFE_TEXT_LENGTH} length. Returns the string as it is if 2337 * the length() is smaller than {@link #PARCEL_SAFE_TEXT_LENGTH}. Used for text that is parceled 2338 * into a {@link Parcelable}. 2339 * 2340 * @hide 2341 */ 2342 @Nullable trimToParcelableSize(@ullable T text)2343 public static <T extends CharSequence> T trimToParcelableSize(@Nullable T text) { 2344 return trimToSize(text, PARCEL_SAFE_TEXT_LENGTH); 2345 } 2346 2347 /** 2348 * Trims the text to {@code size} length. Returns the string as it is if the length() is 2349 * smaller than {@code size}. If chars at {@code size-1} and {@code size} is a surrogate 2350 * pair, returns a CharSequence of length {@code size-1}. 2351 * 2352 * @param size length of the result, should be greater than 0 2353 * 2354 * @hide 2355 */ 2356 @Nullable trimToSize(@ullable T text, @IntRange(from = 1) int size)2357 public static <T extends CharSequence> T trimToSize(@Nullable T text, 2358 @IntRange(from = 1) int size) { 2359 Preconditions.checkArgument(size > 0); 2360 if (TextUtils.isEmpty(text) || text.length() <= size) return text; 2361 if (Character.isHighSurrogate(text.charAt(size - 1)) 2362 && Character.isLowSurrogate(text.charAt(size))) { 2363 size = size - 1; 2364 } 2365 return (T) text.subSequence(0, size); 2366 } 2367 2368 /** 2369 * Trims the {@code text} to the first {@code size} characters and adds an ellipsis if the 2370 * resulting string is shorter than the input. This will result in an output string which is 2371 * longer than {@code size} for most inputs. 2372 * 2373 * @param size length of the result, should be greater than 0 2374 * 2375 * @hide 2376 */ 2377 @Nullable trimToLengthWithEllipsis(@ullable T text, @IntRange(from = 1) int size)2378 public static <T extends CharSequence> T trimToLengthWithEllipsis(@Nullable T text, 2379 @IntRange(from = 1) int size) { 2380 T trimmed = trimToSize(text, size); 2381 if (text != null && trimmed.length() < text.length()) { 2382 trimmed = (T) (trimmed.toString() + "..."); 2383 } 2384 return trimmed; 2385 } 2386 2387 /** @hide */ 2388 @android.ravenwood.annotation.RavenwoodKeep isNewline(int codePoint)2389 public static boolean isNewline(int codePoint) { 2390 int type = Character.getType(codePoint); 2391 return type == Character.PARAGRAPH_SEPARATOR || type == Character.LINE_SEPARATOR 2392 || codePoint == LINE_FEED_CODE_POINT; 2393 } 2394 2395 /** @hide */ 2396 @android.ravenwood.annotation.RavenwoodKeep isWhitespace(int codePoint)2397 public static boolean isWhitespace(int codePoint) { 2398 return Character.isWhitespace(codePoint) || codePoint == NBSP_CODE_POINT; 2399 } 2400 2401 /** @hide */ 2402 @android.ravenwood.annotation.RavenwoodKeep isWhitespaceExceptNewline(int codePoint)2403 public static boolean isWhitespaceExceptNewline(int codePoint) { 2404 return isWhitespace(codePoint) && !isNewline(codePoint); 2405 } 2406 2407 /** @hide */ 2408 @android.ravenwood.annotation.RavenwoodKeep isPunctuation(int codePoint)2409 public static boolean isPunctuation(int codePoint) { 2410 int type = Character.getType(codePoint); 2411 return type == Character.CONNECTOR_PUNCTUATION 2412 || type == Character.DASH_PUNCTUATION 2413 || type == Character.END_PUNCTUATION 2414 || type == Character.FINAL_QUOTE_PUNCTUATION 2415 || type == Character.INITIAL_QUOTE_PUNCTUATION 2416 || type == Character.OTHER_PUNCTUATION 2417 || type == Character.START_PUNCTUATION; 2418 } 2419 2420 /** @hide */ 2421 @Nullable withoutPrefix(@ullable String prefix, @Nullable String str)2422 public static String withoutPrefix(@Nullable String prefix, @Nullable String str) { 2423 if (prefix == null || str == null) return str; 2424 return str.startsWith(prefix) ? str.substring(prefix.length()) : str; 2425 } 2426 2427 /** 2428 * Remove html, remove bad characters, and truncate string. 2429 * 2430 * <p>This method is meant to remove common mistakes and nefarious formatting from strings that 2431 * were loaded from untrusted sources (such as other packages). 2432 * 2433 * <p>This method first {@link Html#fromHtml treats the string like HTML} and then ... 2434 * <ul> 2435 * <li>Removes new lines or truncates at first new line 2436 * <li>Trims the white-space off the end 2437 * <li>Truncates the string 2438 * </ul> 2439 * ... if specified. 2440 * 2441 * @param unclean The input string 2442 * @param maxCharactersToConsider The maximum number of characters of {@code unclean} to 2443 * consider from the input string. {@code 0} disables this 2444 * feature. 2445 * @param ellipsizeDip Assuming maximum length of the string (in dip), assuming font size 42. 2446 * This is roughly 50 characters for {@code ellipsizeDip == 1000}.<br /> 2447 * Usually ellipsizing should be left to the view showing the string. If a 2448 * string is used as an input to another string, it might be useful to 2449 * control the length of the input string though. {@code 0} disables this 2450 * feature. 2451 * @param flags Flags controlling cleaning behavior (Can be {@link #SAFE_STRING_FLAG_TRIM}, 2452 * {@link #SAFE_STRING_FLAG_SINGLE_LINE}, 2453 * and {@link #SAFE_STRING_FLAG_FIRST_LINE}) 2454 * 2455 * @return The cleaned string 2456 */ makeSafeForPresentation(@onNull String unclean, @IntRange(from = 0) int maxCharactersToConsider, @FloatRange(from = 0) float ellipsizeDip, @SafeStringFlags int flags)2457 public static @NonNull CharSequence makeSafeForPresentation(@NonNull String unclean, 2458 @IntRange(from = 0) int maxCharactersToConsider, 2459 @FloatRange(from = 0) float ellipsizeDip, @SafeStringFlags int flags) { 2460 boolean onlyKeepFirstLine = ((flags & SAFE_STRING_FLAG_FIRST_LINE) != 0); 2461 boolean forceSingleLine = ((flags & SAFE_STRING_FLAG_SINGLE_LINE) != 0); 2462 boolean trim = ((flags & SAFE_STRING_FLAG_TRIM) != 0); 2463 2464 Preconditions.checkNotNull(unclean); 2465 Preconditions.checkArgumentNonnegative(maxCharactersToConsider); 2466 Preconditions.checkArgumentNonNegative(ellipsizeDip, "ellipsizeDip"); 2467 Preconditions.checkFlagsArgument(flags, SAFE_STRING_FLAG_TRIM 2468 | SAFE_STRING_FLAG_SINGLE_LINE | SAFE_STRING_FLAG_FIRST_LINE); 2469 Preconditions.checkArgument(!(onlyKeepFirstLine && forceSingleLine), 2470 "Cannot set SAFE_STRING_FLAG_SINGLE_LINE and SAFE_STRING_FLAG_FIRST_LINE at the" 2471 + "same time"); 2472 2473 String shortString; 2474 if (maxCharactersToConsider > 0) { 2475 shortString = unclean.substring(0, Math.min(unclean.length(), maxCharactersToConsider)); 2476 } else { 2477 shortString = unclean; 2478 } 2479 2480 // Treat string as HTML. This 2481 // - converts HTML symbols: e.g. ß -> ß 2482 // - applies some HTML tags: e.g. <br> -> \n 2483 // - removes invalid characters such as \b 2484 // - removes html styling, such as <b> 2485 // - applies html formatting: e.g. a<p>b</p>c -> a\n\nb\n\nc 2486 // - replaces some html tags by "object replacement" markers: <img> -> \ufffc 2487 // - Removes leading white space 2488 // - Removes all trailing white space beside a single space 2489 // - Collapses double white space 2490 StringWithRemovedChars gettingCleaned = new StringWithRemovedChars( 2491 Html.fromHtml(shortString).toString()); 2492 2493 int firstNonWhiteSpace = -1; 2494 int firstTrailingWhiteSpace = -1; 2495 2496 // Remove new lines (if requested) and control characters. 2497 int uncleanLength = gettingCleaned.length(); 2498 for (int offset = 0; offset < uncleanLength; ) { 2499 int codePoint = gettingCleaned.codePointAt(offset); 2500 int type = Character.getType(codePoint); 2501 int codePointLen = Character.charCount(codePoint); 2502 boolean isNewline = isNewline(codePoint); 2503 2504 if (onlyKeepFirstLine && isNewline) { 2505 gettingCleaned.removeAllCharAfter(offset); 2506 break; 2507 } else if (forceSingleLine && isNewline) { 2508 gettingCleaned.removeRange(offset, offset + codePointLen); 2509 } else if (type == Character.CONTROL && !isNewline) { 2510 gettingCleaned.removeRange(offset, offset + codePointLen); 2511 } else if (trim && !isWhitespace(codePoint)) { 2512 // This is only executed if the code point is not removed 2513 if (firstNonWhiteSpace == -1) { 2514 firstNonWhiteSpace = offset; 2515 } 2516 firstTrailingWhiteSpace = offset + codePointLen; 2517 } 2518 2519 offset += codePointLen; 2520 } 2521 2522 if (trim) { 2523 // Remove leading and trailing white space 2524 if (firstNonWhiteSpace == -1) { 2525 // No non whitespace found, remove all 2526 gettingCleaned.removeAllCharAfter(0); 2527 } else { 2528 if (firstNonWhiteSpace > 0) { 2529 gettingCleaned.removeAllCharBefore(firstNonWhiteSpace); 2530 } 2531 if (firstTrailingWhiteSpace < uncleanLength) { 2532 gettingCleaned.removeAllCharAfter(firstTrailingWhiteSpace); 2533 } 2534 } 2535 } 2536 2537 if (ellipsizeDip == 0) { 2538 return gettingCleaned.toString(); 2539 } else { 2540 final float assumedFontSizePx = 42; 2541 if (Typeface.getSystemFontMap().isEmpty()) { 2542 // In the system server, the font files may not be loaded, so unable to perform 2543 // ellipsize, so use the estimated char count for the ellipsize. 2544 2545 // The median of glyph widths of the Roboto is 0.57em, so use it as a reference 2546 // of the glyph width. 2547 final float assumedCharWidthInEm = 0.57f; 2548 final float assumedCharWidthInPx = assumedFontSizePx * assumedCharWidthInEm; 2549 2550 // Even if the argument name is `ellipsizeDip`, the unit of this argument is pixels. 2551 final int charCount = (int) ((ellipsizeDip + 0.5f) / assumedCharWidthInPx); 2552 2553 final String text = gettingCleaned.toString(); 2554 if (TextUtils.isEmpty(text) || text.length() <= charCount) { 2555 return text; 2556 } else { 2557 return TextUtils.trimToSize(text, charCount) 2558 + getEllipsisString(TruncateAt.END); 2559 } 2560 } else { 2561 // Truncate 2562 final TextPaint paint = new TextPaint(); 2563 paint.setTextSize(assumedFontSizePx); 2564 2565 return TextUtils.ellipsize(gettingCleaned.toString(), paint, ellipsizeDip, 2566 TextUtils.TruncateAt.END); 2567 } 2568 } 2569 } 2570 2571 /** 2572 * A special string manipulation class. Just records removals and executes the when onString() 2573 * is called. 2574 */ 2575 private static class StringWithRemovedChars { 2576 /** The original string */ 2577 private final String mOriginal; 2578 2579 /** 2580 * One bit per char in string. If bit is set, character needs to be removed. If whole 2581 * bit field is not initialized nothing needs to be removed. 2582 */ 2583 private BitSet mRemovedChars; 2584 StringWithRemovedChars(@onNull String original)2585 StringWithRemovedChars(@NonNull String original) { 2586 mOriginal = original; 2587 } 2588 2589 /** 2590 * Mark all chars in a range {@code [firstRemoved - firstNonRemoved[} (not including 2591 * firstNonRemoved) as removed. 2592 */ removeRange(int firstRemoved, int firstNonRemoved)2593 void removeRange(int firstRemoved, int firstNonRemoved) { 2594 if (mRemovedChars == null) { 2595 mRemovedChars = new BitSet(mOriginal.length()); 2596 } 2597 2598 mRemovedChars.set(firstRemoved, firstNonRemoved); 2599 } 2600 2601 /** 2602 * Remove all characters before {@code firstNonRemoved}. 2603 */ removeAllCharBefore(int firstNonRemoved)2604 void removeAllCharBefore(int firstNonRemoved) { 2605 if (mRemovedChars == null) { 2606 mRemovedChars = new BitSet(mOriginal.length()); 2607 } 2608 2609 mRemovedChars.set(0, firstNonRemoved); 2610 } 2611 2612 /** 2613 * Remove all characters after and including {@code firstRemoved}. 2614 */ removeAllCharAfter(int firstRemoved)2615 void removeAllCharAfter(int firstRemoved) { 2616 if (mRemovedChars == null) { 2617 mRemovedChars = new BitSet(mOriginal.length()); 2618 } 2619 2620 mRemovedChars.set(firstRemoved, mOriginal.length()); 2621 } 2622 2623 @Override toString()2624 public String toString() { 2625 // Common case, no chars removed 2626 if (mRemovedChars == null) { 2627 return mOriginal; 2628 } 2629 2630 StringBuilder sb = new StringBuilder(mOriginal.length()); 2631 for (int i = 0; i < mOriginal.length(); i++) { 2632 if (!mRemovedChars.get(i)) { 2633 sb.append(mOriginal.charAt(i)); 2634 } 2635 } 2636 2637 return sb.toString(); 2638 } 2639 2640 /** 2641 * Return length or the original string 2642 */ length()2643 int length() { 2644 return mOriginal.length(); 2645 } 2646 2647 /** 2648 * Return codePoint of original string at a certain {@code offset} 2649 */ codePointAt(int offset)2650 int codePointAt(int offset) { 2651 return mOriginal.codePointAt(offset); 2652 } 2653 } 2654 2655 private static Object sLock = new Object(); 2656 2657 private static char[] sTemp = null; 2658 } 2659