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