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 85 private TextUtils() { /* cannot be instantiated */ } 86 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 105 public static int indexOf(CharSequence s, char ch) { 106 return indexOf(s, ch, 0); 107 } 108 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 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 155 public static int lastIndexOf(CharSequence s, char ch) { 156 return lastIndexOf(s, ch, s.length() - 1); 157 } 158 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 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 213 public static int indexOf(CharSequence s, CharSequence needle) { 214 return indexOf(s, needle, 0, s.length()); 215 } 216 217 public static int indexOf(CharSequence s, CharSequence needle, int start) { 218 return indexOf(s, needle, start, s.length()); 219 } 220 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 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 */ 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 */ 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 */ 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 */ 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 */ 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> { 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 */ 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 */ 415 public void setString(String string) { 416 mString = string; 417 mPosition = 0; 418 mLength = mString.length(); 419 } 420 421 public Iterator<String> iterator() { 422 return this; 423 } 424 425 public boolean hasNext() { 426 return mPosition < mLength; 427 } 428 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 439 public void remove() { 440 throw new UnsupportedOperationException(); 441 } 442 } 443 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 */ 460 public static boolean isEmpty(@Nullable CharSequence str) { 461 return str == null || str.length() == 0; 462 } 463 464 /** {@hide} */ 465 public static String nullIfEmpty(@Nullable String str) { 466 return isEmpty(str) ? null : str; 467 } 468 469 /** {@hide} */ 470 public static String emptyIfNull(@Nullable String str) { 471 return str == null ? "" : str; 472 } 473 474 /** {@hide} */ 475 public static String firstNotEmpty(@Nullable String a, @NonNull String b) { 476 return !isEmpty(a) ? a : Preconditions.checkStringNotEmpty(b); 477 } 478 479 /** {@hide} */ 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 */ 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 */ 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 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 { 543 public Reverser(CharSequence source, int start, int end) { 544 mSource = source; 545 mStart = start; 546 mEnd = end; 547 } 548 549 public int length() { 550 return mEnd - mStart; 551 } 552 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 561 public String toString() { 562 return subSequence(0, length()).toString(); 563 } 564 565 public char charAt(int off) { 566 return (char) UCharacter.getMirror(mSource.charAt(mEnd - 1 - off)); 567 } 568 569 @SuppressWarnings("deprecation") 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 */ 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 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 */ 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 */ 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 */ 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 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 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 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 */ 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 */ 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 */ 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 */ 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 */ 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 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 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 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 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 */ 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 */ 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 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 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 */ 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 */ 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 */ 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 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 */ 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 */ 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 */ 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 */ 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 */ 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") 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 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