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