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