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 /* package */ static final char[] ELLIPSIS_NORMAL = { '\u2026' }; // this is "..." 68 private static final String ELLIPSIS_STRING = new String(ELLIPSIS_NORMAL); 69 70 /* package */ static final char[] ELLIPSIS_TWO_DOTS = { '\u2025' }; // this is ".." 71 private static final String ELLIPSIS_TWO_DOTS_STRING = new String(ELLIPSIS_TWO_DOTS); 72 TextUtils()73 private TextUtils() { /* cannot be instantiated */ } 74 getChars(CharSequence s, int start, int end, char[] dest, int destoff)75 public static void getChars(CharSequence s, int start, int end, 76 char[] dest, int destoff) { 77 Class<? extends CharSequence> c = s.getClass(); 78 79 if (c == String.class) 80 ((String) s).getChars(start, end, dest, destoff); 81 else if (c == StringBuffer.class) 82 ((StringBuffer) s).getChars(start, end, dest, destoff); 83 else if (c == StringBuilder.class) 84 ((StringBuilder) s).getChars(start, end, dest, destoff); 85 else if (s instanceof GetChars) 86 ((GetChars) s).getChars(start, end, dest, destoff); 87 else { 88 for (int i = start; i < end; i++) 89 dest[destoff++] = s.charAt(i); 90 } 91 } 92 indexOf(CharSequence s, char ch)93 public static int indexOf(CharSequence s, char ch) { 94 return indexOf(s, ch, 0); 95 } 96 indexOf(CharSequence s, char ch, int start)97 public static int indexOf(CharSequence s, char ch, int start) { 98 Class<? extends CharSequence> c = s.getClass(); 99 100 if (c == String.class) 101 return ((String) s).indexOf(ch, start); 102 103 return indexOf(s, ch, start, s.length()); 104 } 105 indexOf(CharSequence s, char ch, int start, int end)106 public static int indexOf(CharSequence s, char ch, int start, int end) { 107 Class<? extends CharSequence> c = s.getClass(); 108 109 if (s instanceof GetChars || c == StringBuffer.class || 110 c == StringBuilder.class || c == String.class) { 111 final int INDEX_INCREMENT = 500; 112 char[] temp = obtain(INDEX_INCREMENT); 113 114 while (start < end) { 115 int segend = start + INDEX_INCREMENT; 116 if (segend > end) 117 segend = end; 118 119 getChars(s, start, segend, temp, 0); 120 121 int count = segend - start; 122 for (int i = 0; i < count; i++) { 123 if (temp[i] == ch) { 124 recycle(temp); 125 return i + start; 126 } 127 } 128 129 start = segend; 130 } 131 132 recycle(temp); 133 return -1; 134 } 135 136 for (int i = start; i < end; i++) 137 if (s.charAt(i) == ch) 138 return i; 139 140 return -1; 141 } 142 lastIndexOf(CharSequence s, char ch)143 public static int lastIndexOf(CharSequence s, char ch) { 144 return lastIndexOf(s, ch, s.length() - 1); 145 } 146 lastIndexOf(CharSequence s, char ch, int last)147 public static int lastIndexOf(CharSequence s, char ch, int last) { 148 Class<? extends CharSequence> c = s.getClass(); 149 150 if (c == String.class) 151 return ((String) s).lastIndexOf(ch, last); 152 153 return lastIndexOf(s, ch, 0, last); 154 } 155 lastIndexOf(CharSequence s, char ch, int start, int last)156 public static int lastIndexOf(CharSequence s, char ch, 157 int start, int last) { 158 if (last < 0) 159 return -1; 160 if (last >= s.length()) 161 last = s.length() - 1; 162 163 int end = last + 1; 164 165 Class<? extends CharSequence> c = s.getClass(); 166 167 if (s instanceof GetChars || c == StringBuffer.class || 168 c == StringBuilder.class || c == String.class) { 169 final int INDEX_INCREMENT = 500; 170 char[] temp = obtain(INDEX_INCREMENT); 171 172 while (start < end) { 173 int segstart = end - INDEX_INCREMENT; 174 if (segstart < start) 175 segstart = start; 176 177 getChars(s, segstart, end, temp, 0); 178 179 int count = end - segstart; 180 for (int i = count - 1; i >= 0; i--) { 181 if (temp[i] == ch) { 182 recycle(temp); 183 return i + segstart; 184 } 185 } 186 187 end = segstart; 188 } 189 190 recycle(temp); 191 return -1; 192 } 193 194 for (int i = end - 1; i >= start; i--) 195 if (s.charAt(i) == ch) 196 return i; 197 198 return -1; 199 } 200 indexOf(CharSequence s, CharSequence needle)201 public static int indexOf(CharSequence s, CharSequence needle) { 202 return indexOf(s, needle, 0, s.length()); 203 } 204 indexOf(CharSequence s, CharSequence needle, int start)205 public static int indexOf(CharSequence s, CharSequence needle, int start) { 206 return indexOf(s, needle, start, s.length()); 207 } 208 indexOf(CharSequence s, CharSequence needle, int start, int end)209 public static int indexOf(CharSequence s, CharSequence needle, 210 int start, int end) { 211 int nlen = needle.length(); 212 if (nlen == 0) 213 return start; 214 215 char c = needle.charAt(0); 216 217 for (;;) { 218 start = indexOf(s, c, start); 219 if (start > end - nlen) { 220 break; 221 } 222 223 if (start < 0) { 224 return -1; 225 } 226 227 if (regionMatches(s, start, needle, 0, nlen)) { 228 return start; 229 } 230 231 start++; 232 } 233 return -1; 234 } 235 regionMatches(CharSequence one, int toffset, CharSequence two, int ooffset, int len)236 public static boolean regionMatches(CharSequence one, int toffset, 237 CharSequence two, int ooffset, 238 int len) { 239 int tempLen = 2 * len; 240 if (tempLen < len) { 241 // Integer overflow; len is unreasonably large 242 throw new IndexOutOfBoundsException(); 243 } 244 char[] temp = obtain(tempLen); 245 246 getChars(one, toffset, toffset + len, temp, 0); 247 getChars(two, ooffset, ooffset + len, temp, len); 248 249 boolean match = true; 250 for (int i = 0; i < len; i++) { 251 if (temp[i] != temp[i + len]) { 252 match = false; 253 break; 254 } 255 } 256 257 recycle(temp); 258 return match; 259 } 260 261 /** 262 * Create a new String object containing the given range of characters 263 * from the source string. This is different than simply calling 264 * {@link CharSequence#subSequence(int, int) CharSequence.subSequence} 265 * in that it does not preserve any style runs in the source sequence, 266 * allowing a more efficient implementation. 267 */ substring(CharSequence source, int start, int end)268 public static String substring(CharSequence source, int start, int end) { 269 if (source instanceof String) 270 return ((String) source).substring(start, end); 271 if (source instanceof StringBuilder) 272 return ((StringBuilder) source).substring(start, end); 273 if (source instanceof StringBuffer) 274 return ((StringBuffer) source).substring(start, end); 275 276 char[] temp = obtain(end - start); 277 getChars(source, start, end, temp, 0); 278 String ret = new String(temp, 0, end - start); 279 recycle(temp); 280 281 return ret; 282 } 283 284 /** 285 * Returns list of multiple {@link CharSequence} joined into a single 286 * {@link CharSequence} separated by localized delimiter such as ", ". 287 * 288 * @hide 289 */ join(Iterable<CharSequence> list)290 public static CharSequence join(Iterable<CharSequence> list) { 291 final CharSequence delimiter = Resources.getSystem().getText(R.string.list_delimeter); 292 return join(delimiter, list); 293 } 294 295 /** 296 * Returns a string containing the tokens joined by delimiters. 297 * @param tokens an array objects to be joined. Strings will be formed from 298 * the objects by calling object.toString(). 299 */ join(CharSequence delimiter, Object[] tokens)300 public static String join(CharSequence delimiter, Object[] tokens) { 301 StringBuilder sb = new StringBuilder(); 302 boolean firstTime = true; 303 for (Object token: tokens) { 304 if (firstTime) { 305 firstTime = false; 306 } else { 307 sb.append(delimiter); 308 } 309 sb.append(token); 310 } 311 return sb.toString(); 312 } 313 314 /** 315 * Returns a string containing the tokens joined by delimiters. 316 * @param tokens an array objects to be joined. Strings will be formed from 317 * the objects by calling object.toString(). 318 */ join(CharSequence delimiter, Iterable tokens)319 public static String join(CharSequence delimiter, Iterable tokens) { 320 StringBuilder sb = new StringBuilder(); 321 boolean firstTime = true; 322 for (Object token: tokens) { 323 if (firstTime) { 324 firstTime = false; 325 } else { 326 sb.append(delimiter); 327 } 328 sb.append(token); 329 } 330 return sb.toString(); 331 } 332 333 /** 334 * String.split() returns [''] when the string to be split is empty. This returns []. This does 335 * not remove any empty strings from the result. For example split("a,", "," ) returns {"a", ""}. 336 * 337 * @param text the string to split 338 * @param expression 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, String expression)343 public static String[] split(String text, String expression) { 344 if (text.length() == 0) { 345 return EMPTY_STRING_ARRAY; 346 } else { 347 return text.split(expression, -1); 348 } 349 } 350 351 /** 352 * Splits a string on a pattern. String.split() returns [''] when the string to be 353 * split is empty. This returns []. This does not remove any empty strings from the result. 354 * @param text the string to split 355 * @param pattern the regular expression to match 356 * @return an array of strings. The array will be empty if text is empty 357 * 358 * @throws NullPointerException if expression or text is null 359 */ split(String text, Pattern pattern)360 public static String[] split(String text, Pattern pattern) { 361 if (text.length() == 0) { 362 return EMPTY_STRING_ARRAY; 363 } else { 364 return pattern.split(text, -1); 365 } 366 } 367 368 /** 369 * An interface for splitting strings according to rules that are opaque to the user of this 370 * interface. This also has less overhead than split, which uses regular expressions and 371 * allocates an array to hold the results. 372 * 373 * <p>The most efficient way to use this class is: 374 * 375 * <pre> 376 * // Once 377 * TextUtils.StringSplitter splitter = new TextUtils.SimpleStringSplitter(delimiter); 378 * 379 * // Once per string to split 380 * splitter.setString(string); 381 * for (String s : splitter) { 382 * ... 383 * } 384 * </pre> 385 */ 386 public interface StringSplitter extends Iterable<String> { setString(String string)387 public void setString(String string); 388 } 389 390 /** 391 * A simple string splitter. 392 * 393 * <p>If the final character in the string to split is the delimiter then no empty string will 394 * be returned for the empty string after that delimeter. That is, splitting <tt>"a,b,"</tt> on 395 * comma will return <tt>"a", "b"</tt>, not <tt>"a", "b", ""</tt>. 396 */ 397 public static class SimpleStringSplitter implements StringSplitter, Iterator<String> { 398 private String mString; 399 private char mDelimiter; 400 private int mPosition; 401 private int mLength; 402 403 /** 404 * Initializes the splitter. setString may be called later. 405 * @param delimiter the delimeter on which to split 406 */ SimpleStringSplitter(char delimiter)407 public SimpleStringSplitter(char delimiter) { 408 mDelimiter = delimiter; 409 } 410 411 /** 412 * Sets the string to split 413 * @param string the string to split 414 */ setString(String string)415 public void setString(String string) { 416 mString = string; 417 mPosition = 0; 418 mLength = mString.length(); 419 } 420 iterator()421 public Iterator<String> iterator() { 422 return this; 423 } 424 hasNext()425 public boolean hasNext() { 426 return mPosition < mLength; 427 } 428 next()429 public String next() { 430 int end = mString.indexOf(mDelimiter, mPosition); 431 if (end == -1) { 432 end = mLength; 433 } 434 String nextString = mString.substring(mPosition, end); 435 mPosition = end + 1; // Skip the delimiter. 436 return nextString; 437 } 438 remove()439 public void remove() { 440 throw new UnsupportedOperationException(); 441 } 442 } 443 stringOrSpannedString(CharSequence source)444 public static CharSequence stringOrSpannedString(CharSequence source) { 445 if (source == null) 446 return null; 447 if (source instanceof SpannedString) 448 return source; 449 if (source instanceof Spanned) 450 return new SpannedString(source); 451 452 return source.toString(); 453 } 454 455 /** 456 * Returns true if the string is null or 0-length. 457 * @param str the string to be examined 458 * @return true if str is null or zero length 459 */ isEmpty(CharSequence str)460 public static boolean isEmpty(CharSequence str) { 461 if (str == null || str.length() == 0) 462 return true; 463 else 464 return false; 465 } 466 467 /** 468 * Returns the length that the specified CharSequence would have if 469 * spaces and control characters were trimmed from the start and end, 470 * as by {@link String#trim}. 471 */ getTrimmedLength(CharSequence s)472 public static int getTrimmedLength(CharSequence s) { 473 int len = s.length(); 474 475 int start = 0; 476 while (start < len && s.charAt(start) <= ' ') { 477 start++; 478 } 479 480 int end = len; 481 while (end > start && s.charAt(end - 1) <= ' ') { 482 end--; 483 } 484 485 return end - start; 486 } 487 488 /** 489 * Returns true if a and b are equal, including if they are both null. 490 * <p><i>Note: In platform versions 1.1 and earlier, this method only worked well if 491 * both the arguments were instances of String.</i></p> 492 * @param a first CharSequence to check 493 * @param b second CharSequence to check 494 * @return true if a and b are equal 495 */ equals(CharSequence a, CharSequence b)496 public static boolean equals(CharSequence a, CharSequence b) { 497 if (a == b) return true; 498 int length; 499 if (a != null && b != null && (length = a.length()) == b.length()) { 500 if (a instanceof String && b instanceof String) { 501 return a.equals(b); 502 } else { 503 for (int i = 0; i < length; i++) { 504 if (a.charAt(i) != b.charAt(i)) return false; 505 } 506 return true; 507 } 508 } 509 return false; 510 } 511 512 // XXX currently this only reverses chars, not spans getReverse(CharSequence source, int start, int end)513 public static CharSequence getReverse(CharSequence source, 514 int start, int end) { 515 return new Reverser(source, start, end); 516 } 517 518 private static class Reverser 519 implements CharSequence, GetChars 520 { Reverser(CharSequence source, int start, int end)521 public Reverser(CharSequence source, int start, int end) { 522 mSource = source; 523 mStart = start; 524 mEnd = end; 525 } 526 length()527 public int length() { 528 return mEnd - mStart; 529 } 530 subSequence(int start, int end)531 public CharSequence subSequence(int start, int end) { 532 char[] buf = new char[end - start]; 533 534 getChars(start, end, buf, 0); 535 return new String(buf); 536 } 537 538 @Override toString()539 public String toString() { 540 return subSequence(0, length()).toString(); 541 } 542 charAt(int off)543 public char charAt(int off) { 544 return AndroidCharacter.getMirror(mSource.charAt(mEnd - 1 - off)); 545 } 546 getChars(int start, int end, char[] dest, int destoff)547 public void getChars(int start, int end, char[] dest, int destoff) { 548 TextUtils.getChars(mSource, start + mStart, end + mStart, 549 dest, destoff); 550 AndroidCharacter.mirror(dest, 0, end - start); 551 552 int len = end - start; 553 int n = (end - start) / 2; 554 for (int i = 0; i < n; i++) { 555 char tmp = dest[destoff + i]; 556 557 dest[destoff + i] = dest[destoff + len - i - 1]; 558 dest[destoff + len - i - 1] = tmp; 559 } 560 } 561 562 private CharSequence mSource; 563 private int mStart; 564 private int mEnd; 565 } 566 567 /** @hide */ 568 public static final int ALIGNMENT_SPAN = 1; 569 /** @hide */ 570 public static final int FIRST_SPAN = ALIGNMENT_SPAN; 571 /** @hide */ 572 public static final int FOREGROUND_COLOR_SPAN = 2; 573 /** @hide */ 574 public static final int RELATIVE_SIZE_SPAN = 3; 575 /** @hide */ 576 public static final int SCALE_X_SPAN = 4; 577 /** @hide */ 578 public static final int STRIKETHROUGH_SPAN = 5; 579 /** @hide */ 580 public static final int UNDERLINE_SPAN = 6; 581 /** @hide */ 582 public static final int STYLE_SPAN = 7; 583 /** @hide */ 584 public static final int BULLET_SPAN = 8; 585 /** @hide */ 586 public static final int QUOTE_SPAN = 9; 587 /** @hide */ 588 public static final int LEADING_MARGIN_SPAN = 10; 589 /** @hide */ 590 public static final int URL_SPAN = 11; 591 /** @hide */ 592 public static final int BACKGROUND_COLOR_SPAN = 12; 593 /** @hide */ 594 public static final int TYPEFACE_SPAN = 13; 595 /** @hide */ 596 public static final int SUPERSCRIPT_SPAN = 14; 597 /** @hide */ 598 public static final int SUBSCRIPT_SPAN = 15; 599 /** @hide */ 600 public static final int ABSOLUTE_SIZE_SPAN = 16; 601 /** @hide */ 602 public static final int TEXT_APPEARANCE_SPAN = 17; 603 /** @hide */ 604 public static final int ANNOTATION = 18; 605 /** @hide */ 606 public static final int SUGGESTION_SPAN = 19; 607 /** @hide */ 608 public static final int SPELL_CHECK_SPAN = 20; 609 /** @hide */ 610 public static final int SUGGESTION_RANGE_SPAN = 21; 611 /** @hide */ 612 public static final int EASY_EDIT_SPAN = 22; 613 /** @hide */ 614 public static final int LOCALE_SPAN = 23; 615 /** @hide */ 616 public static final int TTS_SPAN = 24; 617 /** @hide */ 618 public static final int LAST_SPAN = TTS_SPAN; 619 620 /** 621 * Flatten a CharSequence and whatever styles can be copied across processes 622 * into the parcel. 623 */ writeToParcel(CharSequence cs, Parcel p, int parcelableFlags)624 public static void writeToParcel(CharSequence cs, Parcel p, 625 int parcelableFlags) { 626 if (cs instanceof Spanned) { 627 p.writeInt(0); 628 p.writeString(cs.toString()); 629 630 Spanned sp = (Spanned) cs; 631 Object[] os = sp.getSpans(0, cs.length(), Object.class); 632 633 // note to people adding to this: check more specific types 634 // before more generic types. also notice that it uses 635 // "if" instead of "else if" where there are interfaces 636 // so one object can be several. 637 638 for (int i = 0; i < os.length; i++) { 639 Object o = os[i]; 640 Object prop = os[i]; 641 642 if (prop instanceof CharacterStyle) { 643 prop = ((CharacterStyle) prop).getUnderlying(); 644 } 645 646 if (prop instanceof ParcelableSpan) { 647 ParcelableSpan ps = (ParcelableSpan)prop; 648 int spanTypeId = ps.getSpanTypeId(); 649 if (spanTypeId < FIRST_SPAN || spanTypeId > LAST_SPAN) { 650 Log.e(TAG, "external class \"" + ps.getClass().getSimpleName() 651 + "\" is attempting to use the frameworks-only ParcelableSpan" 652 + " interface"); 653 } else { 654 p.writeInt(spanTypeId); 655 ps.writeToParcel(p, parcelableFlags); 656 writeWhere(p, sp, o); 657 } 658 } 659 } 660 661 p.writeInt(0); 662 } else { 663 p.writeInt(1); 664 if (cs != null) { 665 p.writeString(cs.toString()); 666 } else { 667 p.writeString(null); 668 } 669 } 670 } 671 writeWhere(Parcel p, Spanned sp, Object o)672 private static void writeWhere(Parcel p, Spanned sp, Object o) { 673 p.writeInt(sp.getSpanStart(o)); 674 p.writeInt(sp.getSpanEnd(o)); 675 p.writeInt(sp.getSpanFlags(o)); 676 } 677 678 public static final Parcelable.Creator<CharSequence> CHAR_SEQUENCE_CREATOR 679 = new Parcelable.Creator<CharSequence>() { 680 /** 681 * Read and return a new CharSequence, possibly with styles, 682 * from the parcel. 683 */ 684 public CharSequence createFromParcel(Parcel p) { 685 int kind = p.readInt(); 686 687 String string = p.readString(); 688 if (string == null) { 689 return null; 690 } 691 692 if (kind == 1) { 693 return string; 694 } 695 696 SpannableString sp = new SpannableString(string); 697 698 while (true) { 699 kind = p.readInt(); 700 701 if (kind == 0) 702 break; 703 704 switch (kind) { 705 case ALIGNMENT_SPAN: 706 readSpan(p, sp, new AlignmentSpan.Standard(p)); 707 break; 708 709 case FOREGROUND_COLOR_SPAN: 710 readSpan(p, sp, new ForegroundColorSpan(p)); 711 break; 712 713 case RELATIVE_SIZE_SPAN: 714 readSpan(p, sp, new RelativeSizeSpan(p)); 715 break; 716 717 case SCALE_X_SPAN: 718 readSpan(p, sp, new ScaleXSpan(p)); 719 break; 720 721 case STRIKETHROUGH_SPAN: 722 readSpan(p, sp, new StrikethroughSpan(p)); 723 break; 724 725 case UNDERLINE_SPAN: 726 readSpan(p, sp, new UnderlineSpan(p)); 727 break; 728 729 case STYLE_SPAN: 730 readSpan(p, sp, new StyleSpan(p)); 731 break; 732 733 case BULLET_SPAN: 734 readSpan(p, sp, new BulletSpan(p)); 735 break; 736 737 case QUOTE_SPAN: 738 readSpan(p, sp, new QuoteSpan(p)); 739 break; 740 741 case LEADING_MARGIN_SPAN: 742 readSpan(p, sp, new LeadingMarginSpan.Standard(p)); 743 break; 744 745 case URL_SPAN: 746 readSpan(p, sp, new URLSpan(p)); 747 break; 748 749 case BACKGROUND_COLOR_SPAN: 750 readSpan(p, sp, new BackgroundColorSpan(p)); 751 break; 752 753 case TYPEFACE_SPAN: 754 readSpan(p, sp, new TypefaceSpan(p)); 755 break; 756 757 case SUPERSCRIPT_SPAN: 758 readSpan(p, sp, new SuperscriptSpan(p)); 759 break; 760 761 case SUBSCRIPT_SPAN: 762 readSpan(p, sp, new SubscriptSpan(p)); 763 break; 764 765 case ABSOLUTE_SIZE_SPAN: 766 readSpan(p, sp, new AbsoluteSizeSpan(p)); 767 break; 768 769 case TEXT_APPEARANCE_SPAN: 770 readSpan(p, sp, new TextAppearanceSpan(p)); 771 break; 772 773 case ANNOTATION: 774 readSpan(p, sp, new Annotation(p)); 775 break; 776 777 case SUGGESTION_SPAN: 778 readSpan(p, sp, new SuggestionSpan(p)); 779 break; 780 781 case SPELL_CHECK_SPAN: 782 readSpan(p, sp, new SpellCheckSpan(p)); 783 break; 784 785 case SUGGESTION_RANGE_SPAN: 786 readSpan(p, sp, new SuggestionRangeSpan(p)); 787 break; 788 789 case EASY_EDIT_SPAN: 790 readSpan(p, sp, new EasyEditSpan(p)); 791 break; 792 793 case LOCALE_SPAN: 794 readSpan(p, sp, new LocaleSpan(p)); 795 break; 796 797 case TTS_SPAN: 798 readSpan(p, sp, new TtsSpan(p)); 799 break; 800 801 default: 802 throw new RuntimeException("bogus span encoding " + kind); 803 } 804 } 805 806 return sp; 807 } 808 809 public CharSequence[] newArray(int size) 810 { 811 return new CharSequence[size]; 812 } 813 }; 814 815 /** 816 * Debugging tool to print the spans in a CharSequence. The output will 817 * be printed one span per line. If the CharSequence is not a Spanned, 818 * then the entire string will be printed on a single line. 819 */ dumpSpans(CharSequence cs, Printer printer, String prefix)820 public static void dumpSpans(CharSequence cs, Printer printer, String prefix) { 821 if (cs instanceof Spanned) { 822 Spanned sp = (Spanned) cs; 823 Object[] os = sp.getSpans(0, cs.length(), Object.class); 824 825 for (int i = 0; i < os.length; i++) { 826 Object o = os[i]; 827 printer.println(prefix + cs.subSequence(sp.getSpanStart(o), 828 sp.getSpanEnd(o)) + ": " 829 + Integer.toHexString(System.identityHashCode(o)) 830 + " " + o.getClass().getCanonicalName() 831 + " (" + sp.getSpanStart(o) + "-" + sp.getSpanEnd(o) 832 + ") fl=#" + sp.getSpanFlags(o)); 833 } 834 } else { 835 printer.println(prefix + cs + ": (no spans)"); 836 } 837 } 838 839 /** 840 * Return a new CharSequence in which each of the source strings is 841 * replaced by the corresponding element of the destinations. 842 */ replace(CharSequence template, String[] sources, CharSequence[] destinations)843 public static CharSequence replace(CharSequence template, 844 String[] sources, 845 CharSequence[] destinations) { 846 SpannableStringBuilder tb = new SpannableStringBuilder(template); 847 848 for (int i = 0; i < sources.length; i++) { 849 int where = indexOf(tb, sources[i]); 850 851 if (where >= 0) 852 tb.setSpan(sources[i], where, where + sources[i].length(), 853 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 854 } 855 856 for (int i = 0; i < sources.length; i++) { 857 int start = tb.getSpanStart(sources[i]); 858 int end = tb.getSpanEnd(sources[i]); 859 860 if (start >= 0) { 861 tb.replace(start, end, destinations[i]); 862 } 863 } 864 865 return tb; 866 } 867 868 /** 869 * Replace instances of "^1", "^2", etc. in the 870 * <code>template</code> CharSequence with the corresponding 871 * <code>values</code>. "^^" is used to produce a single caret in 872 * the output. Only up to 9 replacement values are supported, 873 * "^10" will be produce the first replacement value followed by a 874 * '0'. 875 * 876 * @param template the input text containing "^1"-style 877 * placeholder values. This object is not modified; a copy is 878 * returned. 879 * 880 * @param values CharSequences substituted into the template. The 881 * first is substituted for "^1", the second for "^2", and so on. 882 * 883 * @return the new CharSequence produced by doing the replacement 884 * 885 * @throws IllegalArgumentException if the template requests a 886 * value that was not provided, or if more than 9 values are 887 * provided. 888 */ expandTemplate(CharSequence template, CharSequence... values)889 public static CharSequence expandTemplate(CharSequence template, 890 CharSequence... values) { 891 if (values.length > 9) { 892 throw new IllegalArgumentException("max of 9 values are supported"); 893 } 894 895 SpannableStringBuilder ssb = new SpannableStringBuilder(template); 896 897 try { 898 int i = 0; 899 while (i < ssb.length()) { 900 if (ssb.charAt(i) == '^') { 901 char next = ssb.charAt(i+1); 902 if (next == '^') { 903 ssb.delete(i+1, i+2); 904 ++i; 905 continue; 906 } else if (Character.isDigit(next)) { 907 int which = Character.getNumericValue(next) - 1; 908 if (which < 0) { 909 throw new IllegalArgumentException( 910 "template requests value ^" + (which+1)); 911 } 912 if (which >= values.length) { 913 throw new IllegalArgumentException( 914 "template requests value ^" + (which+1) + 915 "; only " + values.length + " provided"); 916 } 917 ssb.replace(i, i+2, values[which]); 918 i += values[which].length(); 919 continue; 920 } 921 } 922 ++i; 923 } 924 } catch (IndexOutOfBoundsException ignore) { 925 // happens when ^ is the last character in the string. 926 } 927 return ssb; 928 } 929 getOffsetBefore(CharSequence text, int offset)930 public static int getOffsetBefore(CharSequence text, int offset) { 931 if (offset == 0) 932 return 0; 933 if (offset == 1) 934 return 0; 935 936 char c = text.charAt(offset - 1); 937 938 if (c >= '\uDC00' && c <= '\uDFFF') { 939 char c1 = text.charAt(offset - 2); 940 941 if (c1 >= '\uD800' && c1 <= '\uDBFF') 942 offset -= 2; 943 else 944 offset -= 1; 945 } else { 946 offset -= 1; 947 } 948 949 if (text instanceof Spanned) { 950 ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset, 951 ReplacementSpan.class); 952 953 for (int i = 0; i < spans.length; i++) { 954 int start = ((Spanned) text).getSpanStart(spans[i]); 955 int end = ((Spanned) text).getSpanEnd(spans[i]); 956 957 if (start < offset && end > offset) 958 offset = start; 959 } 960 } 961 962 return offset; 963 } 964 getOffsetAfter(CharSequence text, int offset)965 public static int getOffsetAfter(CharSequence text, int offset) { 966 int len = text.length(); 967 968 if (offset == len) 969 return len; 970 if (offset == len - 1) 971 return len; 972 973 char c = text.charAt(offset); 974 975 if (c >= '\uD800' && c <= '\uDBFF') { 976 char c1 = text.charAt(offset + 1); 977 978 if (c1 >= '\uDC00' && c1 <= '\uDFFF') 979 offset += 2; 980 else 981 offset += 1; 982 } else { 983 offset += 1; 984 } 985 986 if (text instanceof Spanned) { 987 ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset, 988 ReplacementSpan.class); 989 990 for (int i = 0; i < spans.length; i++) { 991 int start = ((Spanned) text).getSpanStart(spans[i]); 992 int end = ((Spanned) text).getSpanEnd(spans[i]); 993 994 if (start < offset && end > offset) 995 offset = end; 996 } 997 } 998 999 return offset; 1000 } 1001 readSpan(Parcel p, Spannable sp, Object o)1002 private static void readSpan(Parcel p, Spannable sp, Object o) { 1003 sp.setSpan(o, p.readInt(), p.readInt(), p.readInt()); 1004 } 1005 1006 /** 1007 * Copies the spans from the region <code>start...end</code> in 1008 * <code>source</code> to the region 1009 * <code>destoff...destoff+end-start</code> in <code>dest</code>. 1010 * Spans in <code>source</code> that begin before <code>start</code> 1011 * or end after <code>end</code> but overlap this range are trimmed 1012 * as if they began at <code>start</code> or ended at <code>end</code>. 1013 * 1014 * @throws IndexOutOfBoundsException if any of the copied spans 1015 * are out of range in <code>dest</code>. 1016 */ copySpansFrom(Spanned source, int start, int end, Class kind, Spannable dest, int destoff)1017 public static void copySpansFrom(Spanned source, int start, int end, 1018 Class kind, 1019 Spannable dest, int destoff) { 1020 if (kind == null) { 1021 kind = Object.class; 1022 } 1023 1024 Object[] spans = source.getSpans(start, end, kind); 1025 1026 for (int i = 0; i < spans.length; i++) { 1027 int st = source.getSpanStart(spans[i]); 1028 int en = source.getSpanEnd(spans[i]); 1029 int fl = source.getSpanFlags(spans[i]); 1030 1031 if (st < start) 1032 st = start; 1033 if (en > end) 1034 en = end; 1035 1036 dest.setSpan(spans[i], st - start + destoff, en - start + destoff, 1037 fl); 1038 } 1039 } 1040 1041 public enum TruncateAt { 1042 START, 1043 MIDDLE, 1044 END, 1045 MARQUEE, 1046 /** 1047 * @hide 1048 */ 1049 END_SMALL 1050 } 1051 1052 public interface EllipsizeCallback { 1053 /** 1054 * This method is called to report that the specified region of 1055 * text was ellipsized away by a call to {@link #ellipsize}. 1056 */ ellipsized(int start, int end)1057 public void ellipsized(int start, int end); 1058 } 1059 1060 /** 1061 * Returns the original text if it fits in the specified width 1062 * given the properties of the specified Paint, 1063 * or, if it does not fit, a truncated 1064 * copy with ellipsis character added at the specified edge or center. 1065 */ ellipsize(CharSequence text, TextPaint p, float avail, TruncateAt where)1066 public static CharSequence ellipsize(CharSequence text, 1067 TextPaint p, 1068 float avail, TruncateAt where) { 1069 return ellipsize(text, p, avail, where, false, null); 1070 } 1071 1072 /** 1073 * Returns the original text if it fits in the specified width 1074 * given the properties of the specified Paint, 1075 * or, if it does not fit, a copy with ellipsis character added 1076 * at the specified edge or center. 1077 * If <code>preserveLength</code> is specified, the returned copy 1078 * will be padded with zero-width spaces to preserve the original 1079 * length and offsets instead of truncating. 1080 * If <code>callback</code> is non-null, it will be called to 1081 * report the start and end of the ellipsized range. TextDirection 1082 * is determined by the first strong directional character. 1083 */ ellipsize(CharSequence text, TextPaint paint, float avail, TruncateAt where, boolean preserveLength, EllipsizeCallback callback)1084 public static CharSequence ellipsize(CharSequence text, 1085 TextPaint paint, 1086 float avail, TruncateAt where, 1087 boolean preserveLength, 1088 EllipsizeCallback callback) { 1089 return ellipsize(text, paint, avail, where, preserveLength, callback, 1090 TextDirectionHeuristics.FIRSTSTRONG_LTR, 1091 (where == TruncateAt.END_SMALL) ? ELLIPSIS_TWO_DOTS_STRING : ELLIPSIS_STRING); 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