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