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