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