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