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