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