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