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