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