2 * Copyright (C) 2006 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
19 import android.annotation.FloatRange;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.annotation.PluralsRes;
23 import android.content.Context;
24 import android.content.res.Resources;
25 import android.icu.lang.UCharacter;
26 import android.icu.util.ULocale;
27 import android.os.Parcel;
28 import android.os.Parcelable;
29 import android.os.SystemProperties;
30 import android.provider.Settings;
31 import android.text.style.AbsoluteSizeSpan;
32 import android.text.style.AccessibilityClickableSpan;
33 import android.text.style.AccessibilityURLSpan;
34 import android.text.style.AlignmentSpan;
35 import android.text.style.BackgroundColorSpan;
36 import android.text.style.BulletSpan;
37 import android.text.style.CharacterStyle;
38 import android.text.style.EasyEditSpan;
39 import android.text.style.ForegroundColorSpan;
40 import android.text.style.LeadingMarginSpan;
41 import android.text.style.LocaleSpan;
42 import android.text.style.MetricAffectingSpan;
43 import android.text.style.ParagraphStyle;
44 import android.text.style.QuoteSpan;
45 import android.text.style.RelativeSizeSpan;
46 import android.text.style.ReplacementSpan;
47 import android.text.style.ScaleXSpan;
48 import android.text.style.SpellCheckSpan;
49 import android.text.style.StrikethroughSpan;
50 import android.text.style.StyleSpan;
51 import android.text.style.SubscriptSpan;
52 import android.text.style.SuggestionRangeSpan;
53 import android.text.style.SuggestionSpan;
54 import android.text.style.SuperscriptSpan;
55 import android.text.style.TextAppearanceSpan;
56 import android.text.style.TtsSpan;
57 import android.text.style.TypefaceSpan;
58 import android.text.style.URLSpan;
59 import android.text.style.UnderlineSpan;
60 import android.text.style.UpdateAppearance;
61 import android.util.Log;
62 import android.util.Printer;
63 import android.view.View;
65 import com.android.internal.R;
66 import com.android.internal.util.ArrayUtils;
67 import com.android.internal.util.Preconditions;
69 import java.lang.reflect.Array;
70 import java.util.Iterator;
71 import java.util.List;
72 import java.util.Locale;
73 import java.util.regex.Pattern;
75 public class TextUtils {
76 private static final String TAG = "TextUtils";
78 /* package */ static final char[] ELLIPSIS_NORMAL = { '\u2026' }; // this is "..."
80 public static final String ELLIPSIS_STRING = new String(ELLIPSIS_NORMAL);
82 /* package */ static final char[] ELLIPSIS_TWO_DOTS = { '\u2025' }; // this is ".."
83 private static final String ELLIPSIS_TWO_DOTS_STRING = new String(ELLIPSIS_TWO_DOTS);
85 private TextUtils() { /* cannot be instantiated */ }
87 public static void getChars(CharSequence s, int start, int end,
88 char[] dest, int destoff) {
89 Class<? extends CharSequence> c = s.getClass();
91 if (c == String.class)
92 ((String) s).getChars(start, end, dest, destoff);
93 else if (c == StringBuffer.class)
94 ((StringBuffer) s).getChars(start, end, dest, destoff);
95 else if (c == StringBuilder.class)
96 ((StringBuilder) s).getChars(start, end, dest, destoff);
97 else if (s instanceof GetChars)
98 ((GetChars) s).getChars(start, end, dest, destoff);
100 for (int i = start; i < end; i++)
101 dest[destoff++] = s.charAt(i);
105 public static int indexOf(CharSequence s, char ch) {
106 return indexOf(s, ch, 0);
109 public static int indexOf(CharSequence s, char ch, int start) {
110 Class<? extends CharSequence> c = s.getClass();
112 if (c == String.class)
113 return ((String) s).indexOf(ch, start);
115 return indexOf(s, ch, start, s.length());
118 public static int indexOf(CharSequence s, char ch, int start, int end) {
119 Class<? extends CharSequence> c = s.getClass();
121 if (s instanceof GetChars || c == StringBuffer.class ||
122 c == StringBuilder.class || c == String.class) {
123 final int INDEX_INCREMENT = 500;
124 char[] temp = obtain(INDEX_INCREMENT);
126 while (start < end) {
127 int segend = start + INDEX_INCREMENT;
131 getChars(s, start, segend, temp, 0);
133 int count = segend - start;
134 for (int i = 0; i < count; i++) {
148 for (int i = start; i < end; i++)
149 if (s.charAt(i) == ch)
155 public static int lastIndexOf(CharSequence s, char ch) {
156 return lastIndexOf(s, ch, s.length() - 1);
159 public static int lastIndexOf(CharSequence s, char ch, int last) {
160 Class<? extends CharSequence> c = s.getClass();
162 if (c == String.class)
163 return ((String) s).lastIndexOf(ch, last);
165 return lastIndexOf(s, ch, 0, last);
168 public static int lastIndexOf(CharSequence s, char ch,
169 int start, int last) {
172 if (last >= s.length())
173 last = s.length() - 1;
177 Class<? extends CharSequence> c = s.getClass();
179 if (s instanceof GetChars || c == StringBuffer.class ||
180 c == StringBuilder.class || c == String.class) {
181 final int INDEX_INCREMENT = 500;
182 char[] temp = obtain(INDEX_INCREMENT);
184 while (start < end) {
185 int segstart = end - INDEX_INCREMENT;
186 if (segstart < start)
189 getChars(s, segstart, end, temp, 0);
191 int count = end - segstart;
192 for (int i = count - 1; i >= 0; i--) {
206 for (int i = end - 1; i >= start; i--)
207 if (s.charAt(i) == ch)
213 public static int indexOf(CharSequence s, CharSequence needle) {
214 return indexOf(s, needle, 0, s.length());
217 public static int indexOf(CharSequence s, CharSequence needle, int start) {
218 return indexOf(s, needle, start, s.length());
221 public static int indexOf(CharSequence s, CharSequence needle,
222 int start, int end) {
223 int nlen = needle.length();
227 char c = needle.charAt(0);
230 start = indexOf(s, c, start);
231 if (start > end - nlen) {
239 if (regionMatches(s, start, needle, 0, nlen)) {
248 public static boolean regionMatches(CharSequence one, int toffset,
249 CharSequence two, int ooffset,
251 int tempLen = 2 * len;
253 // Integer overflow; len is unreasonably large
254 throw new IndexOutOfBoundsException();
256 char[] temp = obtain(tempLen);
258 getChars(one, toffset, toffset + len, temp, 0);
259 getChars(two, ooffset, ooffset + len, temp, len);
261 boolean match = true;
262 for (int i = 0; i < len; i++) {
263 if (temp[i] != temp[i + len]) {
274 * Create a new String object containing the given range of characters
275 * from the source string. This is different than simply calling
276 * {@link CharSequence#subSequence(int, int) CharSequence.subSequence}
277 * in that it does not preserve any style runs in the source sequence,
278 * allowing a more efficient implementation.
280 public static String substring(CharSequence source, int start, int end) {
281 if (source instanceof String)
282 return ((String) source).substring(start, end);
283 if (source instanceof StringBuilder)
284 return ((StringBuilder) source).substring(start, end);
285 if (source instanceof StringBuffer)
286 return ((StringBuffer) source).substring(start, end);
288 char[] temp = obtain(end - start);
289 getChars(source, start, end, temp, 0);
290 String ret = new String(temp, 0, end - start);
297 * Returns a string containing the tokens joined by delimiters.
298 * @param tokens an array objects to be joined. Strings will be formed from
299 * the objects by calling object.toString().
301 public static String join(CharSequence delimiter, Object[] tokens) {
302 StringBuilder sb = new StringBuilder();
303 boolean firstTime = true;
304 for (Object token: tokens) {
308 sb.append(delimiter);
312 return sb.toString();
316 * Returns a string containing the tokens joined by delimiters.
317 * @param tokens an array objects to be joined. Strings will be formed from
318 * the objects by calling object.toString().
320 public static String join(CharSequence delimiter, Iterable tokens) {
321 StringBuilder sb = new StringBuilder();
322 Iterator<?> it = tokens.iterator();
324 sb.append(it.next());
325 while (it.hasNext()) {
326 sb.append(delimiter);
327 sb.append(it.next());
330 return sb.toString();
334 * String.split() returns [''] when the string to be split is empty. This returns []. This does
335 * not remove any empty strings from the result. For example split("a,", "," ) returns {"a", ""}.
337 * @param text the string to split
338 * @param expression the regular expression to match
339 * @return an array of strings. The array will be empty if text is empty
341 * @throws NullPointerException if expression or text is null
343 public static String[] split(String text, String expression) {
344 if (text.length() == 0) {
345 return EMPTY_STRING_ARRAY;
347 return text.split(expression, -1);
352 * Splits a string on a pattern. String.split() returns [''] when the string to be
353 * split is empty. This returns []. This does not remove any empty strings from the result.
354 * @param text the string to split
355 * @param pattern the regular expression to match
356 * @return an array of strings. The array will be empty if text is empty
358 * @throws NullPointerException if expression or text is null
360 public static String[] split(String text, Pattern pattern) {
361 if (text.length() == 0) {
362 return EMPTY_STRING_ARRAY;
364 return pattern.split(text, -1);
369 * An interface for splitting strings according to rules that are opaque to the user of this
370 * interface. This also has less overhead than split, which uses regular expressions and
371 * allocates an array to hold the results.
373 * <p>The most efficient way to use this class is:
377 * TextUtils.StringSplitter splitter = new TextUtils.SimpleStringSplitter(delimiter);
379 * // Once per string to split
380 * splitter.setString(string);
381 * for (String s : splitter) {
386 public interface StringSplitter extends Iterable<String> {
387 public void setString(String string);
391 * A simple string splitter.
393 * <p>If the final character in the string to split is the delimiter then no empty string will
394 * be returned for the empty string after that delimeter. That is, splitting <tt>"a,b,"</tt> on
395 * comma will return <tt>"a", "b"</tt>, not <tt>"a", "b", ""</tt>.
397 public static class SimpleStringSplitter implements StringSplitter, Iterator<String> {
398 private String mString;
399 private char mDelimiter;
400 private int mPosition;
404 * Initializes the splitter. setString may be called later.
405 * @param delimiter the delimeter on which to split
407 public SimpleStringSplitter(char delimiter) {
408 mDelimiter = delimiter;
412 * Sets the string to split
413 * @param string the string to split
415 public void setString(String string) {
418 mLength = mString.length();
421 public Iterator<String> iterator() {
425 public boolean hasNext() {
426 return mPosition < mLength;
429 public String next() {
430 int end = mString.indexOf(mDelimiter, mPosition);
434 String nextString = mString.substring(mPosition, end);
435 mPosition = end + 1; // Skip the delimiter.
439 public void remove() {
440 throw new UnsupportedOperationException();
444 public static CharSequence stringOrSpannedString(CharSequence source) {
447 if (source instanceof SpannedString)
449 if (source instanceof Spanned)
450 return new SpannedString(source);
452 return source.toString();
456 * Returns true if the string is null or 0-length.
457 * @param str the string to be examined
458 * @return true if str is null or zero length
460 public static boolean isEmpty(@Nullable CharSequence str) {
461 return str == null || str.length() == 0;
465 public static String nullIfEmpty(@Nullable String str) {
466 return isEmpty(str) ? null : str;
470 public static String emptyIfNull(@Nullable String str) {
471 return str == null ? "" : str;
475 public static String firstNotEmpty(@Nullable String a, @NonNull String b) {
476 return !isEmpty(a) ? a : Preconditions.checkStringNotEmpty(b);
480 public static int length(@Nullable String s) {
481 return isEmpty(s) ? 0 : s.length();
485 * Returns the length that the specified CharSequence would have if
486 * spaces and ASCII control characters were trimmed from the start and end,
487 * as by {@link String#trim}.
489 public static int getTrimmedLength(CharSequence s) {
490 int len = s.length();
493 while (start < len && s.charAt(start) <= ' ') {
498 while (end > start && s.charAt(end - 1) <= ' ') {
506 * Returns true if a and b are equal, including if they are both null.
507 * <p><i>Note: In platform versions 1.1 and earlier, this method only worked well if
508 * both the arguments were instances of String.</i></p>
509 * @param a first CharSequence to check
510 * @param b second CharSequence to check
511 * @return true if a and b are equal
513 public static boolean equals(CharSequence a, CharSequence b) {
514 if (a == b) return true;
516 if (a != null && b != null && (length = a.length()) == b.length()) {
517 if (a instanceof String && b instanceof String) {
520 for (int i = 0; i < length; i++) {
521 if (a.charAt(i) != b.charAt(i)) return false;
530 * This function only reverses individual {@code char}s and not their associated
531 * spans. It doesn't support surrogate pairs (that correspond to non-BMP code points), combining
532 * sequences or conjuncts either.
533 * @deprecated Do not use.
536 public static CharSequence getReverse(CharSequence source, int start, int end) {
537 return new Reverser(source, start, end);
540 private static class Reverser
541 implements CharSequence, GetChars
543 public Reverser(CharSequence source, int start, int end) {
549 public int length() {
550 return mEnd - mStart;
553 public CharSequence subSequence(int start, int end) {
554 char[] buf = new char[end - start];
556 getChars(start, end, buf, 0);
557 return new String(buf);
561 public String toString() {
562 return subSequence(0, length()).toString();
565 public char charAt(int off) {
566 return (char) UCharacter.getMirror(mSource.charAt(mEnd - 1 - off));
569 @SuppressWarnings("deprecation")
570 public void getChars(int start, int end, char[] dest, int destoff) {
571 TextUtils.getChars(mSource, start + mStart, end + mStart,
573 AndroidCharacter.mirror(dest, 0, end - start);
575 int len = end - start;
576 int n = (end - start) / 2;
577 for (int i = 0; i < n; i++) {
578 char tmp = dest[destoff + i];
580 dest[destoff + i] = dest[destoff + len - i - 1];
581 dest[destoff + len - i - 1] = tmp;
585 private CharSequence mSource;
591 public static final int ALIGNMENT_SPAN = 1;
593 public static final int FIRST_SPAN = ALIGNMENT_SPAN;
595 public static final int FOREGROUND_COLOR_SPAN = 2;
597 public static final int RELATIVE_SIZE_SPAN = 3;
599 public static final int SCALE_X_SPAN = 4;
601 public static final int STRIKETHROUGH_SPAN = 5;
603 public static final int UNDERLINE_SPAN = 6;
605 public static final int STYLE_SPAN = 7;
607 public static final int BULLET_SPAN = 8;
609 public static final int QUOTE_SPAN = 9;
611 public static final int LEADING_MARGIN_SPAN = 10;
613 public static final int URL_SPAN = 11;
615 public static final int BACKGROUND_COLOR_SPAN = 12;
617 public static final int TYPEFACE_SPAN = 13;
619 public static final int SUPERSCRIPT_SPAN = 14;
621 public static final int SUBSCRIPT_SPAN = 15;
623 public static final int ABSOLUTE_SIZE_SPAN = 16;
625 public static final int TEXT_APPEARANCE_SPAN = 17;
627 public static final int ANNOTATION = 18;
629 public static final int SUGGESTION_SPAN = 19;
631 public static final int SPELL_CHECK_SPAN = 20;
633 public static final int SUGGESTION_RANGE_SPAN = 21;
635 public static final int EASY_EDIT_SPAN = 22;
637 public static final int LOCALE_SPAN = 23;
639 public static final int TTS_SPAN = 24;
641 public static final int ACCESSIBILITY_CLICKABLE_SPAN = 25;
643 public static final int ACCESSIBILITY_URL_SPAN = 26;
645 public static final int LAST_SPAN = ACCESSIBILITY_URL_SPAN;
648 * Flatten a CharSequence and whatever styles can be copied across processes
651 public static void writeToParcel(CharSequence cs, Parcel p, int parcelableFlags) {
652 if (cs instanceof Spanned) {
654 p.writeString(cs.toString());
656 Spanned sp = (Spanned) cs;
657 Object[] os = sp.getSpans(0, cs.length(), Object.class);
659 // note to people adding to this: check more specific types
660 // before more generic types. also notice that it uses
661 // "if" instead of "else if" where there are interfaces
662 // so one object can be several.
664 for (int i = 0; i < os.length; i++) {
668 if (prop instanceof CharacterStyle) {
669 prop = ((CharacterStyle) prop).getUnderlying();
672 if (prop instanceof ParcelableSpan) {
673 final ParcelableSpan ps = (ParcelableSpan) prop;
674 final int spanTypeId = ps.getSpanTypeIdInternal();
675 if (spanTypeId < FIRST_SPAN || spanTypeId > LAST_SPAN) {
676 Log.e(TAG, "External class \"" + ps.getClass().getSimpleName()
677 + "\" is attempting to use the frameworks-only ParcelableSpan"
680 p.writeInt(spanTypeId);
681 ps.writeToParcelInternal(p, parcelableFlags);
682 writeWhere(p, sp, o);
691 p.writeString(cs.toString());
698 private static void writeWhere(Parcel p, Spanned sp, Object o) {
699 p.writeInt(sp.getSpanStart(o));
700 p.writeInt(sp.getSpanEnd(o));
701 p.writeInt(sp.getSpanFlags(o));
704 public static final Parcelable.Creator<CharSequence> CHAR_SEQUENCE_CREATOR
705 = new Parcelable.Creator<CharSequence>() {
707 * Read and return a new CharSequence, possibly with styles,
710 public CharSequence createFromParcel(Parcel p) {
711 int kind = p.readInt();
713 String string = p.readString();
714 if (string == null) {
722 SpannableString sp = new SpannableString(string);
732 readSpan(p, sp, new AlignmentSpan.Standard(p));
735 case FOREGROUND_COLOR_SPAN:
736 readSpan(p, sp, new ForegroundColorSpan(p));
739 case RELATIVE_SIZE_SPAN:
740 readSpan(p, sp, new RelativeSizeSpan(p));
744 readSpan(p, sp, new ScaleXSpan(p));
747 case STRIKETHROUGH_SPAN:
748 readSpan(p, sp, new StrikethroughSpan(p));
752 readSpan(p, sp, new UnderlineSpan(p));
756 readSpan(p, sp, new StyleSpan(p));
760 readSpan(p, sp, new BulletSpan(p));
764 readSpan(p, sp, new QuoteSpan(p));
767 case LEADING_MARGIN_SPAN:
768 readSpan(p, sp, new LeadingMarginSpan.Standard(p));
772 readSpan(p, sp, new URLSpan(p));
775 case BACKGROUND_COLOR_SPAN:
776 readSpan(p, sp, new BackgroundColorSpan(p));
780 readSpan(p, sp, new TypefaceSpan(p));
783 case SUPERSCRIPT_SPAN:
784 readSpan(p, sp, new SuperscriptSpan(p));
788 readSpan(p, sp, new SubscriptSpan(p));
791 case ABSOLUTE_SIZE_SPAN:
792 readSpan(p, sp, new AbsoluteSizeSpan(p));
795 case TEXT_APPEARANCE_SPAN:
796 readSpan(p, sp, new TextAppearanceSpan(p));
800 readSpan(p, sp, new Annotation(p));
803 case SUGGESTION_SPAN:
804 readSpan(p, sp, new SuggestionSpan(p));
807 case SPELL_CHECK_SPAN:
808 readSpan(p, sp, new SpellCheckSpan(p));
811 case SUGGESTION_RANGE_SPAN:
812 readSpan(p, sp, new SuggestionRangeSpan(p));
816 readSpan(p, sp, new EasyEditSpan(p));
820 readSpan(p, sp, new LocaleSpan(p));
824 readSpan(p, sp, new TtsSpan(p));
827 case ACCESSIBILITY_CLICKABLE_SPAN:
828 readSpan(p, sp, new AccessibilityClickableSpan(p));
831 case ACCESSIBILITY_URL_SPAN:
832 readSpan(p, sp, new AccessibilityURLSpan(p));
836 throw new RuntimeException("bogus span encoding " + kind);
843 public CharSequence[] newArray(int size)
845 return new CharSequence[size];
850 * Debugging tool to print the spans in a CharSequence. The output will
851 * be printed one span per line. If the CharSequence is not a Spanned,
852 * then the entire string will be printed on a single line.
854 public static void dumpSpans(CharSequence cs, Printer printer, String prefix) {
855 if (cs instanceof Spanned) {
856 Spanned sp = (Spanned) cs;
857 Object[] os = sp.getSpans(0, cs.length(), Object.class);
859 for (int i = 0; i < os.length; i++) {
861 printer.println(prefix + cs.subSequence(sp.getSpanStart(o),
862 sp.getSpanEnd(o)) + ": "
863 + Integer.toHexString(System.identityHashCode(o))
864 + " " + o.getClass().getCanonicalName()
865 + " (" + sp.getSpanStart(o) + "-" + sp.getSpanEnd(o)
866 + ") fl=#" + sp.getSpanFlags(o));
869 printer.println(prefix + cs + ": (no spans)");
874 * Return a new CharSequence in which each of the source strings is
875 * replaced by the corresponding element of the destinations.
877 public static CharSequence replace(CharSequence template,
879 CharSequence[] destinations) {
880 SpannableStringBuilder tb = new SpannableStringBuilder(template);
882 for (int i = 0; i < sources.length; i++) {
883 int where = indexOf(tb, sources[i]);
886 tb.setSpan(sources[i], where, where + sources[i].length(),
887 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
890 for (int i = 0; i < sources.length; i++) {
891 int start = tb.getSpanStart(sources[i]);
892 int end = tb.getSpanEnd(sources[i]);
895 tb.replace(start, end, destinations[i]);
903 * Replace instances of "^1", "^2", etc. in the
904 * <code>template</code> CharSequence with the corresponding
905 * <code>values</code>. "^^" is used to produce a single caret in
906 * the output. Only up to 9 replacement values are supported,
907 * "^10" will be produce the first replacement value followed by a
910 * @param template the input text containing "^1"-style
911 * placeholder values. This object is not modified; a copy is
914 * @param values CharSequences substituted into the template. The
915 * first is substituted for "^1", the second for "^2", and so on.
917 * @return the new CharSequence produced by doing the replacement
919 * @throws IllegalArgumentException if the template requests a
920 * value that was not provided, or if more than 9 values are
923 public static CharSequence expandTemplate(CharSequence template,
924 CharSequence... values) {
925 if (values.length > 9) {
926 throw new IllegalArgumentException("max of 9 values are supported");
929 SpannableStringBuilder ssb = new SpannableStringBuilder(template);
933 while (i < ssb.length()) {
934 if (ssb.charAt(i) == '^') {
935 char next = ssb.charAt(i+1);
937 ssb.delete(i+1, i+2);
940 } else if (Character.isDigit(next)) {
941 int which = Character.getNumericValue(next) - 1;
943 throw new IllegalArgumentException(
944 "template requests value ^" + (which+1));
946 if (which >= values.length) {
947 throw new IllegalArgumentException(
948 "template requests value ^" + (which+1) +
949 "; only " + values.length + " provided");
951 ssb.replace(i, i+2, values[which]);
952 i += values[which].length();
958 } catch (IndexOutOfBoundsException ignore) {
959 // happens when ^ is the last character in the string.
964 public static int getOffsetBefore(CharSequence text, int offset) {
970 char c = text.charAt(offset - 1);
972 if (c >= '\uDC00' && c <= '\uDFFF') {
973 char c1 = text.charAt(offset - 2);
975 if (c1 >= '\uD800' && c1 <= '\uDBFF')
983 if (text instanceof Spanned) {
984 ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
985 ReplacementSpan.class);
987 for (int i = 0; i < spans.length; i++) {
988 int start = ((Spanned) text).getSpanStart(spans[i]);
989 int end = ((Spanned) text).getSpanEnd(spans[i]);
991 if (start < offset && end > offset)
999 public static int getOffsetAfter(CharSequence text, int offset) {
1000 int len = text.length();
1004 if (offset == len - 1)
1007 char c = text.charAt(offset);
1009 if (c >= '\uD800' && c <= '\uDBFF') {
1010 char c1 = text.charAt(offset + 1);
1012 if (c1 >= '\uDC00' && c1 <= '\uDFFF')
1020 if (text instanceof Spanned) {
1021 ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
1022 ReplacementSpan.class);
1024 for (int i = 0; i < spans.length; i++) {
1025 int start = ((Spanned) text).getSpanStart(spans[i]);
1026 int end = ((Spanned) text).getSpanEnd(spans[i]);
1028 if (start < offset && end > offset)
1036 private static void readSpan(Parcel p, Spannable sp, Object o) {
1037 sp.setSpan(o, p.readInt(), p.readInt(), p.readInt());
1041 * Copies the spans from the region <code>start...end</code> in
1042 * <code>source</code> to the region
1043 * <code>destoff...destoff+end-start</code> in <code>dest</code>.
1044 * Spans in <code>source</code> that begin before <code>start</code>
1045 * or end after <code>end</code> but overlap this range are trimmed
1046 * as if they began at <code>start</code> or ended at <code>end</code>.
1048 * @throws IndexOutOfBoundsException if any of the copied spans
1049 * are out of range in <code>dest</code>.
1051 public static void copySpansFrom(Spanned source, int start, int end,
1053 Spannable dest, int destoff) {
1055 kind = Object.class;
1058 Object[] spans = source.getSpans(start, end, kind);
1060 for (int i = 0; i < spans.length; i++) {
1061 int st = source.getSpanStart(spans[i]);
1062 int en = source.getSpanEnd(spans[i]);
1063 int fl = source.getSpanFlags(spans[i]);
1070 dest.setSpan(spans[i], st - start + destoff, en - start + destoff,
1075 public enum TruncateAt {
1086 public interface EllipsizeCallback {
1088 * This method is called to report that the specified region of
1089 * text was ellipsized away by a call to {@link #ellipsize}.
1091 public void ellipsized(int start, int end);
1095 * Returns the original text if it fits in the specified width
1096 * given the properties of the specified Paint,
1097 * or, if it does not fit, a truncated
1098 * copy with ellipsis character added at the specified edge or center.
1100 public static CharSequence ellipsize(CharSequence text,
1102 float avail, TruncateAt where) {
1103 return ellipsize(text, p, avail, where, false, null);
1107 * Returns the original text if it fits in the specified width
1108 * given the properties of the specified Paint,
1109 * or, if it does not fit, a copy with ellipsis character added
1110 * at the specified edge or center.
1111 * If <code>preserveLength</code> is specified, the returned copy
1112 * will be padded with zero-width spaces to preserve the original
1113 * length and offsets instead of truncating.
1114 * If <code>callback</code> is non-null, it will be called to
1115 * report the start and end of the ellipsized range. TextDirection
1116 * is determined by the first strong directional character.
1118 public static CharSequence ellipsize(CharSequence text,
1120 float avail, TruncateAt where,
1121 boolean preserveLength,
1122 EllipsizeCallback callback) {
1123 return ellipsize(text, paint, avail, where, preserveLength, callback,
1124 TextDirectionHeuristics.FIRSTSTRONG_LTR,
1125 (where == TruncateAt.END_SMALL) ? ELLIPSIS_TWO_DOTS_STRING : ELLIPSIS_STRING);
1129 * Returns the original text if it fits in the specified width
1130 * given the properties of the specified Paint,
1131 * or, if it does not fit, a copy with ellipsis character added
1132 * at the specified edge or center.
1133 * If <code>preserveLength</code> is specified, the returned copy
1134 * will be padded with zero-width spaces to preserve the original
1135 * length and offsets instead of truncating.
1136 * If <code>callback</code> is non-null, it will be called to
1137 * report the start and end of the ellipsized range.
1141 public static CharSequence ellipsize(CharSequence text,
1143 float avail, TruncateAt where,
1144 boolean preserveLength,
1145 EllipsizeCallback callback,
1146 TextDirectionHeuristic textDir, String ellipsis) {
1148 int len = text.length();
1150 MeasuredText mt = MeasuredText.obtain();
1152 float width = setPara(mt, paint, text, 0, text.length(), textDir);
1154 if (width <= avail) {
1155 if (callback != null) {
1156 callback.ellipsized(0, 0);
1162 // XXX assumes ellipsis string does not require shaping and
1163 // is unaffected by style
1164 float ellipsiswid = paint.measureText(ellipsis);
1165 avail -= ellipsiswid;
1171 } else if (where == TruncateAt.START) {
1172 right = len - mt.breakText(len, false, avail);
1173 } else if (where == TruncateAt.END || where == TruncateAt.END_SMALL) {
1174 left = mt.breakText(len, true, avail);
1176 right = len - mt.breakText(len, false, avail / 2);
1177 avail -= mt.measure(right, len);
1178 left = mt.breakText(right, true, avail);
1181 if (callback != null) {
1182 callback.ellipsized(left, right);
1185 char[] buf = mt.mChars;
1186 Spanned sp = text instanceof Spanned ? (Spanned) text : null;
1188 int remaining = len - (right - left);
1189 if (preserveLength) {
1190 if (remaining > 0) { // else eliminate the ellipsis too
1191 buf[left++] = ellipsis.charAt(0);
1193 for (int i = left; i < right; i++) {
1194 buf[i] = ZWNBS_CHAR;
1196 String s = new String(buf, 0, len);
1200 SpannableString ss = new SpannableString(s);
1201 copySpansFrom(sp, 0, len, Object.class, ss, 0);
1205 if (remaining == 0) {
1210 StringBuilder sb = new StringBuilder(remaining + ellipsis.length());
1211 sb.append(buf, 0, left);
1212 sb.append(ellipsis);
1213 sb.append(buf, right, len - right);
1214 return sb.toString();
1217 SpannableStringBuilder ssb = new SpannableStringBuilder();
1218 ssb.append(text, 0, left);
1219 ssb.append(ellipsis);
1220 ssb.append(text, right, len);
1223 MeasuredText.recycle(mt);
1228 * Formats a list of CharSequences by repeatedly inserting the separator between them,
1229 * but stopping when the resulting sequence is too wide for the specified width.
1231 * This method actually tries to fit the maximum number of elements. So if {@code "A, 11 more"
1232 * fits}, {@code "A, B, 10 more"} doesn't fit, but {@code "A, B, C, 9 more"} fits again (due to
1233 * the glyphs for the digits being very wide, for example), it returns
1234 * {@code "A, B, C, 9 more"}. Because of this, this method may be inefficient for very long
1237 * Note that the elements of the returned value, as well as the string for {@code moreId}, will
1238 * be bidi-wrapped using {@link BidiFormatter#unicodeWrap} based on the locale of the input
1239 * Context. If the input {@code Context} is null, the default BidiFormatter from
1240 * {@link BidiFormatter#getInstance()} will be used.
1242 * @param context the {@code Context} to get the {@code moreId} resource from. If {@code null},
1243 * an ellipsis (U+2026) would be used for {@code moreId}.
1244 * @param elements the list to format
1245 * @param separator a separator, such as {@code ", "}
1246 * @param paint the Paint with which to measure the text
1247 * @param avail the horizontal width available for the text (in pixels)
1248 * @param moreId the resource ID for the pluralized string to insert at the end of sequence when
1249 * some of the elements don't fit.
1251 * @return the formatted CharSequence. If even the shortest sequence (e.g. {@code "A, 11 more"})
1252 * doesn't fit, it will return an empty string.
1255 public static CharSequence listEllipsize(@Nullable Context context,
1256 @Nullable List<CharSequence> elements, @NonNull String separator,
1257 @NonNull TextPaint paint, @FloatRange(from=0.0,fromInclusive=false) float avail,
1258 @PluralsRes int moreId) {
1259 if (elements == null) {
1262 final int totalLen = elements.size();
1263 if (totalLen == 0) {
1267 final Resources res;
1268 final BidiFormatter bidiFormatter;
1269 if (context == null) {
1271 bidiFormatter = BidiFormatter.getInstance();
1273 res = context.getResources();
1274 bidiFormatter = BidiFormatter.getInstance(res.getConfiguration().getLocales().get(0));
1277 final SpannableStringBuilder output = new SpannableStringBuilder();
1278 final int[] endIndexes = new int[totalLen];
1279 for (int i = 0; i < totalLen; i++) {
1280 output.append(bidiFormatter.unicodeWrap(elements.get(i)));
1281 if (i != totalLen - 1) { // Insert a separator, except at the very end.
1282 output.append(separator);
1284 endIndexes[i] = output.length();
1287 for (int i = totalLen - 1; i >= 0; i--) {
1288 // Delete the tail of the string, cutting back to one less element.
1289 output.delete(endIndexes[i], output.length());
1291 final int remainingElements = totalLen - i - 1;
1292 if (remainingElements > 0) {
1293 CharSequence morePiece = (res == null) ?
1295 res.getQuantityString(moreId, remainingElements, remainingElements);
1296 morePiece = bidiFormatter.unicodeWrap(morePiece);
1297 output.append(morePiece);
1300 final float width = paint.measureText(output, 0, output.length());
1301 if (width <= avail) { // The string fits.
1305 return ""; // Nothing fits.
1309 * Converts a CharSequence of the comma-separated form "Andy, Bob,
1310 * Charles, David" that is too wide to fit into the specified width
1311 * into one like "Andy, Bob, 2 more".
1313 * @param text the text to truncate
1314 * @param p the Paint with which to measure the text
1315 * @param avail the horizontal width available for the text (in pixels)
1316 * @param oneMore the string for "1 more" in the current locale
1317 * @param more the string for "%d more" in the current locale
1319 * @deprecated Do not use. This is not internationalized, and has known issues
1320 * with right-to-left text, languages that have more than one plural form, languages
1321 * that use a different character as a comma-like separator, etc.
1322 * Use {@link #listEllipsize} instead.
1325 public static CharSequence commaEllipsize(CharSequence text,
1326 TextPaint p, float avail,
1329 return commaEllipsize(text, p, avail, oneMore, more,
1330 TextDirectionHeuristics.FIRSTSTRONG_LTR);
1337 public static CharSequence commaEllipsize(CharSequence text, TextPaint p,
1338 float avail, String oneMore, String more, TextDirectionHeuristic textDir) {
1340 MeasuredText mt = MeasuredText.obtain();
1342 int len = text.length();
1343 float width = setPara(mt, p, text, 0, len, textDir);
1344 if (width <= avail) {
1348 char[] buf = mt.mChars;
1351 for (int i = 0; i < len; i++) {
1352 if (buf[i] == ',') {
1357 int remaining = commaCount + 1;
1360 String okFormat = "";
1364 float[] widths = mt.mWidths;
1366 MeasuredText tempMt = MeasuredText.obtain();
1367 for (int i = 0; i < len; i++) {
1370 if (buf[i] == ',') {
1374 // XXX should not insert spaces, should be part of string
1375 // XXX should use plural rules and not assume English plurals
1376 if (--remaining == 1) {
1377 format = " " + oneMore;
1379 format = " " + String.format(more, remaining);
1382 // XXX this is probably ok, but need to look at it more
1383 tempMt.setPara(format, 0, format.length(), textDir, null);
1384 float moreWid = tempMt.addStyleRun(p, tempMt.mLen, null);
1386 if (w + moreWid <= avail) {
1392 MeasuredText.recycle(tempMt);
1394 SpannableStringBuilder out = new SpannableStringBuilder(okFormat);
1395 out.insert(0, text, 0, ok);
1398 MeasuredText.recycle(mt);
1402 private static float setPara(MeasuredText mt, TextPaint paint,
1403 CharSequence text, int start, int end, TextDirectionHeuristic textDir) {
1405 mt.setPara(text, start, end, textDir, null);
1408 Spanned sp = text instanceof Spanned ? (Spanned) text : null;
1409 int len = end - start;
1411 width = mt.addStyleRun(paint, len, null);
1415 for (int spanStart = 0; spanStart < len; spanStart = spanEnd) {
1416 spanEnd = sp.nextSpanTransition(spanStart, len,
1417 MetricAffectingSpan.class);
1418 MetricAffectingSpan[] spans = sp.getSpans(
1419 spanStart, spanEnd, MetricAffectingSpan.class);
1420 spans = TextUtils.removeEmptySpans(spans, sp, MetricAffectingSpan.class);
1421 width += mt.addStyleRun(paint, spans, spanEnd - spanStart, null);
1428 // Returns true if the character's presence could affect RTL layout.
1430 // In order to be fast, the code is intentionally rough and quite conservative in its
1431 // considering inclusion of any non-BMP or surrogate characters or anything in the bidi
1432 // blocks or any bidi formatting characters with a potential to affect RTL layout.
1434 static boolean couldAffectRtl(char c) {
1435 return (0x0590 <= c && c <= 0x08FF) || // RTL scripts
1436 c == 0x200E || // Bidi format character
1437 c == 0x200F || // Bidi format character
1438 (0x202A <= c && c <= 0x202E) || // Bidi format characters
1439 (0x2066 <= c && c <= 0x2069) || // Bidi format characters
1440 (0xD800 <= c && c <= 0xDFFF) || // Surrogate pairs
1441 (0xFB1D <= c && c <= 0xFDFF) || // Hebrew and Arabic presentation forms
1442 (0xFE70 <= c && c <= 0xFEFE); // Arabic presentation forms
1445 // Returns true if there is no character present that may potentially affect RTL layout.
1446 // Since this calls couldAffectRtl() above, it's also quite conservative, in the way that
1447 // it may return 'false' (needs bidi) although careful consideration may tell us it should
1448 // return 'true' (does not need bidi).
1450 static boolean doesNotNeedBidi(char[] text, int start, int len) {
1451 final int end = start + len;
1452 for (int i = start; i < end; i++) {
1453 if (couldAffectRtl(text[i])) {
1460 /* package */ static char[] obtain(int len) {
1463 synchronized (sLock) {
1468 if (buf == null || buf.length < len)
1469 buf = ArrayUtils.newUnpaddedCharArray(len);
1474 /* package */ static void recycle(char[] temp) {
1475 if (temp.length > 1000)
1478 synchronized (sLock) {
1484 * Html-encode the string.
1485 * @param s the string to be encoded
1486 * @return the encoded string
1488 public static String htmlEncode(String s) {
1489 StringBuilder sb = new StringBuilder();
1491 for (int i = 0; i < s.length(); i++) {
1495 sb.append("<"); //$NON-NLS-1$
1498 sb.append(">"); //$NON-NLS-1$
1501 sb.append("&"); //$NON-NLS-1$
1504 //http://www.w3.org/TR/xhtml1
1505 // The named character reference ' (the apostrophe, U+0027) was introduced in
1506 // XML 1.0 but does not appear in HTML. Authors should therefore use ' instead
1507 // of ' to work as expected in HTML 4 user agents.
1508 sb.append("'"); //$NON-NLS-1$
1511 sb.append("""); //$NON-NLS-1$
1517 return sb.toString();
1521 * Returns a CharSequence concatenating the specified CharSequences,
1522 * retaining their spans if any.
1524 * If there are no parameters, an empty string will be returned.
1526 * If the number of parameters is exactly one, that parameter is returned as output, even if it
1529 * If the number of parameters is at least two, any null CharSequence among the parameters is
1530 * treated as if it was the string <code>"null"</code>.
1532 * If there are paragraph spans in the source CharSequences that satisfy paragraph boundary
1533 * requirements in the sources but would no longer satisfy them in the concatenated
1534 * CharSequence, they may get extended in the resulting CharSequence or not retained.
1536 public static CharSequence concat(CharSequence... text) {
1537 if (text.length == 0) {
1541 if (text.length == 1) {
1545 boolean spanned = false;
1546 for (CharSequence piece : text) {
1547 if (piece instanceof Spanned) {
1554 final SpannableStringBuilder ssb = new SpannableStringBuilder();
1555 for (CharSequence piece : text) {
1556 // If a piece is null, we append the string "null" for compatibility with the
1557 // behavior of StringBuilder and the behavior of the concat() method in earlier
1558 // versions of Android.
1559 ssb.append(piece == null ? "null" : piece);
1561 return new SpannedString(ssb);
1563 final StringBuilder sb = new StringBuilder();
1564 for (CharSequence piece : text) {
1567 return sb.toString();
1572 * Returns whether the given CharSequence contains any printable characters.
1574 public static boolean isGraphic(CharSequence str) {
1575 final int len = str.length();
1576 for (int cp, i=0; i<len; i+=Character.charCount(cp)) {
1577 cp = Character.codePointAt(str, i);
1578 int gc = Character.getType(cp);
1579 if (gc != Character.CONTROL
1580 && gc != Character.FORMAT
1581 && gc != Character.SURROGATE
1582 && gc != Character.UNASSIGNED
1583 && gc != Character.LINE_SEPARATOR
1584 && gc != Character.PARAGRAPH_SEPARATOR
1585 && gc != Character.SPACE_SEPARATOR) {
1593 * Returns whether this character is a printable character.
1595 * This does not support non-BMP characters and should not be used.
1597 * @deprecated Use {@link #isGraphic(CharSequence)} instead.
1600 public static boolean isGraphic(char c) {
1601 int gc = Character.getType(c);
1602 return gc != Character.CONTROL
1603 && gc != Character.FORMAT
1604 && gc != Character.SURROGATE
1605 && gc != Character.UNASSIGNED
1606 && gc != Character.LINE_SEPARATOR
1607 && gc != Character.PARAGRAPH_SEPARATOR
1608 && gc != Character.SPACE_SEPARATOR;
1612 * Returns whether the given CharSequence contains only digits.
1614 public static boolean isDigitsOnly(CharSequence str) {
1615 final int len = str.length();
1616 for (int cp, i = 0; i < len; i += Character.charCount(cp)) {
1617 cp = Character.codePointAt(str, i);
1618 if (!Character.isDigit(cp)) {
1628 public static boolean isPrintableAscii(final char c) {
1629 final int asciiFirst = 0x20;
1630 final int asciiLast = 0x7E; // included
1631 return (asciiFirst <= c && c <= asciiLast) || c == '\r' || c == '\n';
1637 public static boolean isPrintableAsciiOnly(final CharSequence str) {
1638 final int len = str.length();
1639 for (int i = 0; i < len; i++) {
1640 if (!isPrintableAscii(str.charAt(i))) {
1648 * Capitalization mode for {@link #getCapsMode}: capitalize all
1649 * characters. This value is explicitly defined to be the same as
1650 * {@link InputType#TYPE_TEXT_FLAG_CAP_CHARACTERS}.
1652 public static final int CAP_MODE_CHARACTERS
1653 = InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS;
1656 * Capitalization mode for {@link #getCapsMode}: capitalize the first
1657 * character of all words. This value is explicitly defined to be the same as
1658 * {@link InputType#TYPE_TEXT_FLAG_CAP_WORDS}.
1660 public static final int CAP_MODE_WORDS
1661 = InputType.TYPE_TEXT_FLAG_CAP_WORDS;
1664 * Capitalization mode for {@link #getCapsMode}: capitalize the first
1665 * character of each sentence. This value is explicitly defined to be the same as
1666 * {@link InputType#TYPE_TEXT_FLAG_CAP_SENTENCES}.
1668 public static final int CAP_MODE_SENTENCES
1669 = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
1672 * Determine what caps mode should be in effect at the current offset in
1673 * the text. Only the mode bits set in <var>reqModes</var> will be
1674 * checked. Note that the caps mode flags here are explicitly defined
1675 * to match those in {@link InputType}.
1677 * @param cs The text that should be checked for caps modes.
1678 * @param off Location in the text at which to check.
1679 * @param reqModes The modes to be checked: may be any combination of
1680 * {@link #CAP_MODE_CHARACTERS}, {@link #CAP_MODE_WORDS}, and
1681 * {@link #CAP_MODE_SENTENCES}.
1683 * @return Returns the actual capitalization modes that can be in effect
1684 * at the current position, which is any combination of
1685 * {@link #CAP_MODE_CHARACTERS}, {@link #CAP_MODE_WORDS}, and
1686 * {@link #CAP_MODE_SENTENCES}.
1688 public static int getCapsMode(CharSequence cs, int off, int reqModes) {
1697 if ((reqModes&CAP_MODE_CHARACTERS) != 0) {
1698 mode |= CAP_MODE_CHARACTERS;
1700 if ((reqModes&(CAP_MODE_WORDS|CAP_MODE_SENTENCES)) == 0) {
1704 // Back over allowed opening punctuation.
1706 for (i = off; i > 0; i--) {
1707 c = cs.charAt(i - 1);
1709 if (c != '"' && c != '\'' &&
1710 Character.getType(c) != Character.START_PUNCTUATION) {
1715 // Start of paragraph, with optional whitespace.
1718 while (j > 0 && ((c = cs.charAt(j - 1)) == ' ' || c == '\t')) {
1721 if (j == 0 || cs.charAt(j - 1) == '\n') {
1722 return mode | CAP_MODE_WORDS;
1725 // Or start of word if we are that style.
1727 if ((reqModes&CAP_MODE_SENTENCES) == 0) {
1728 if (i != j) mode |= CAP_MODE_WORDS;
1732 // There must be a space if not the start of paragraph.
1738 // Back over allowed closing punctuation.
1740 for (; j > 0; j--) {
1741 c = cs.charAt(j - 1);
1743 if (c != '"' && c != '\'' &&
1744 Character.getType(c) != Character.END_PUNCTUATION) {
1750 c = cs.charAt(j - 1);
1752 if (c == '.' || c == '?' || c == '!') {
1753 // Do not capitalize if the word ends with a period but
1754 // also contains a period, in which case it is an abbreviation.
1757 for (int k = j - 2; k >= 0; k--) {
1764 if (!Character.isLetter(c)) {
1770 return mode | CAP_MODE_SENTENCES;
1778 * Does a comma-delimited list 'delimitedString' contain a certain item?
1779 * (without allocating memory)
1783 public static boolean delimitedStringContains(
1784 String delimitedString, char delimiter, String item) {
1785 if (isEmpty(delimitedString) || isEmpty(item)) {
1789 int length = delimitedString.length();
1790 while ((pos = delimitedString.indexOf(item, pos + 1)) != -1) {
1791 if (pos > 0 && delimitedString.charAt(pos - 1) != delimiter) {
1794 int expectedDelimiterPos = pos + item.length();
1795 if (expectedDelimiterPos == length) {
1796 // Match at end of string.
1799 if (delimitedString.charAt(expectedDelimiterPos) == delimiter) {
1807 * Removes empty spans from the <code>spans</code> array.
1809 * When parsing a Spanned using {@link Spanned#nextSpanTransition(int, int, Class)}, empty spans
1810 * will (correctly) create span transitions, and calling getSpans on a slice of text bounded by
1811 * one of these transitions will (correctly) include the empty overlapping span.
1813 * However, these empty spans should not be taken into account when layouting or rendering the
1814 * string and this method provides a way to filter getSpans' results accordingly.
1816 * @param spans A list of spans retrieved using {@link Spanned#getSpans(int, int, Class)} from
1817 * the <code>spanned</code>
1818 * @param spanned The Spanned from which spans were extracted
1819 * @return A subset of spans where empty spans ({@link Spanned#getSpanStart(Object)} ==
1820 * {@link Spanned#getSpanEnd(Object)} have been removed. The initial order is preserved
1823 @SuppressWarnings("unchecked")
1824 public static <T> T[] removeEmptySpans(T[] spans, Spanned spanned, Class<T> klass) {
1828 for (int i = 0; i < spans.length; i++) {
1829 final T span = spans[i];
1830 final int start = spanned.getSpanStart(span);
1831 final int end = spanned.getSpanEnd(span);
1835 copy = (T[]) Array.newInstance(klass, spans.length - 1);
1836 System.arraycopy(spans, 0, copy, 0, i);
1848 T[] result = (T[]) Array.newInstance(klass, count);
1849 System.arraycopy(copy, 0, result, 0, count);
1857 * Pack 2 int values into a long, useful as a return value for a range
1858 * @see #unpackRangeStartFromLong(long)
1859 * @see #unpackRangeEndFromLong(long)
1862 public static long packRangeInLong(int start, int end) {
1863 return (((long) start) << 32) | end;
1867 * Get the start value from a range packed in a long by {@link #packRangeInLong(int, int)}
1868 * @see #unpackRangeEndFromLong(long)
1869 * @see #packRangeInLong(int, int)
1872 public static int unpackRangeStartFromLong(long range) {
1873 return (int) (range >>> 32);
1877 * Get the end value from a range packed in a long by {@link #packRangeInLong(int, int)}
1878 * @see #unpackRangeStartFromLong(long)
1879 * @see #packRangeInLong(int, int)
1882 public static int unpackRangeEndFromLong(long range) {
1883 return (int) (range & 0x00000000FFFFFFFFL);
1887 * Return the layout direction for a given Locale
1889 * @param locale the Locale for which we want the layout direction. Can be null.
1890 * @return the layout direction. This may be one of:
1891 * {@link android.view.View#LAYOUT_DIRECTION_LTR} or
1892 * {@link android.view.View#LAYOUT_DIRECTION_RTL}.
1894 * Be careful: this code will need to be updated when vertical scripts will be supported
1896 public static int getLayoutDirectionFromLocale(Locale locale) {
1897 return ((locale != null && !locale.equals(Locale.ROOT)
1898 && ULocale.forLocale(locale).isRightToLeft())
1899 // If forcing into RTL layout mode, return RTL as default
1900 || SystemProperties.getBoolean(Settings.Global.DEVELOPMENT_FORCE_RTL, false))
1901 ? View.LAYOUT_DIRECTION_RTL
1902 : View.LAYOUT_DIRECTION_LTR;
1906 * Return localized string representing the given number of selected items.
1910 public static CharSequence formatSelectedCount(int count) {
1911 return Resources.getSystem().getQuantityString(R.plurals.selected_count, count, count);
1915 * Returns whether or not the specified spanned text has a style span.
1918 public static boolean hasStyleSpan(@NonNull Spanned spanned) {
1919 Preconditions.checkArgument(spanned != null);
1920 final Class<?>[] styleClasses = {
1921 CharacterStyle.class, ParagraphStyle.class, UpdateAppearance.class};
1922 for (Class<?> clazz : styleClasses) {
1923 if (spanned.nextSpanTransition(-1, spanned.length(), clazz) < spanned.length()) {
1931 * If the {@code charSequence} is instance of {@link Spanned}, creates a new copy and
1932 * {@link NoCopySpan}'s are removed from the copy. Otherwise the given {@code charSequence} is
1933 * returned as it is.
1938 public static CharSequence trimNoCopySpans(@Nullable CharSequence charSequence) {
1939 if (charSequence != null && charSequence instanceof Spanned) {
1940 // SpannableStringBuilder copy constructor trims NoCopySpans.
1941 return new SpannableStringBuilder(charSequence);
1943 return charSequence;
1947 * Prepends {@code start} and appends {@code end} to a given {@link StringBuilder}
1951 public static void wrap(StringBuilder builder, String start, String end) {
1952 builder.insert(0, start);
1953 builder.append(end);
1956 private static Object sLock = new Object();
1958 private static char[] sTemp = null;
1960 private static String[] EMPTY_STRING_ARRAY = new String[]{};
1962 private static final char ZWNBS_CHAR = '\uFEFF';