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