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