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