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