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