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