Home | History | Annotate | Download | only in text
      1 /*
      2  * Copyright (C) 2006 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.text;
     18 
     19 import android.annotation.FloatRange;
     20 import android.annotation.IntRange;
     21 import android.annotation.NonNull;
     22 import android.annotation.Nullable;
     23 import android.annotation.PluralsRes;
     24 import android.content.Context;
     25 import android.content.res.Resources;
     26 import android.icu.lang.UCharacter;
     27 import android.icu.text.CaseMap;
     28 import android.icu.text.Edits;
     29 import android.icu.util.ULocale;
     30 import android.os.Parcel;
     31 import android.os.Parcelable;
     32 import android.os.SystemProperties;
     33 import android.provider.Settings;
     34 import android.text.style.AbsoluteSizeSpan;
     35 import android.text.style.AccessibilityClickableSpan;
     36 import android.text.style.AccessibilityURLSpan;
     37 import android.text.style.AlignmentSpan;
     38 import android.text.style.BackgroundColorSpan;
     39 import android.text.style.BulletSpan;
     40 import android.text.style.CharacterStyle;
     41 import android.text.style.EasyEditSpan;
     42 import android.text.style.ForegroundColorSpan;
     43 import android.text.style.LeadingMarginSpan;
     44 import android.text.style.LocaleSpan;
     45 import android.text.style.ParagraphStyle;
     46 import android.text.style.QuoteSpan;
     47 import android.text.style.RelativeSizeSpan;
     48 import android.text.style.ReplacementSpan;
     49 import android.text.style.ScaleXSpan;
     50 import android.text.style.SpellCheckSpan;
     51 import android.text.style.StrikethroughSpan;
     52 import android.text.style.StyleSpan;
     53 import android.text.style.SubscriptSpan;
     54 import android.text.style.SuggestionRangeSpan;
     55 import android.text.style.SuggestionSpan;
     56 import android.text.style.SuperscriptSpan;
     57 import android.text.style.TextAppearanceSpan;
     58 import android.text.style.TtsSpan;
     59 import android.text.style.TypefaceSpan;
     60 import android.text.style.URLSpan;
     61 import android.text.style.UnderlineSpan;
     62 import android.text.style.UpdateAppearance;
     63 import android.util.Log;
     64 import android.util.Printer;
     65 import android.view.View;
     66 
     67 import com.android.internal.R;
     68 import com.android.internal.util.ArrayUtils;
     69 import com.android.internal.util.Preconditions;
     70 
     71 import java.lang.reflect.Array;
     72 import java.util.Iterator;
     73 import java.util.List;
     74 import java.util.Locale;
     75 import java.util.regex.Pattern;
     76 
     77 public class TextUtils {
     78     private static final String TAG = "TextUtils";
     79 
     80     // Zero-width character used to fill ellipsized strings when codepoint lenght must be preserved.
     81     /* package */ static final char ELLIPSIS_FILLER = '\uFEFF'; // ZERO WIDTH NO-BREAK SPACE
     82 
     83     // TODO: Based on CLDR data, these need to be localized for Dzongkha (dz) and perhaps
     84     // Hong Kong Traditional Chinese (zh-Hant-HK), but that may need to depend on the actual word
     85     // being ellipsized and not the locale.
     86     private static final String ELLIPSIS_NORMAL = "\u2026"; // HORIZONTAL ELLIPSIS ()
     87     private static final String ELLIPSIS_TWO_DOTS = "\u2025"; // TWO DOT LEADER ()
     88 
     89     /** {@hide} */
     90     @NonNull
     91     public static String getEllipsisString(@NonNull TextUtils.TruncateAt method) {
     92         return (method == TextUtils.TruncateAt.END_SMALL) ? ELLIPSIS_TWO_DOTS : ELLIPSIS_NORMAL;
     93     }
     94 
     95 
     96     private TextUtils() { /* cannot be instantiated */ }
     97 
     98     public static void getChars(CharSequence s, int start, int end,
     99                                 char[] dest, int destoff) {
    100         Class<? extends CharSequence> c = s.getClass();
    101 
    102         if (c == String.class)
    103             ((String) s).getChars(start, end, dest, destoff);
    104         else if (c == StringBuffer.class)
    105             ((StringBuffer) s).getChars(start, end, dest, destoff);
    106         else if (c == StringBuilder.class)
    107             ((StringBuilder) s).getChars(start, end, dest, destoff);
    108         else if (s instanceof GetChars)
    109             ((GetChars) s).getChars(start, end, dest, destoff);
    110         else {
    111             for (int i = start; i < end; i++)
    112                 dest[destoff++] = s.charAt(i);
    113         }
    114     }
    115 
    116     public static int indexOf(CharSequence s, char ch) {
    117         return indexOf(s, ch, 0);
    118     }
    119 
    120     public static int indexOf(CharSequence s, char ch, int start) {
    121         Class<? extends CharSequence> c = s.getClass();
    122 
    123         if (c == String.class)
    124             return ((String) s).indexOf(ch, start);
    125 
    126         return indexOf(s, ch, start, s.length());
    127     }
    128 
    129     public static int indexOf(CharSequence s, char ch, int start, int end) {
    130         Class<? extends CharSequence> c = s.getClass();
    131 
    132         if (s instanceof GetChars || c == StringBuffer.class ||
    133             c == StringBuilder.class || c == String.class) {
    134             final int INDEX_INCREMENT = 500;
    135             char[] temp = obtain(INDEX_INCREMENT);
    136 
    137             while (start < end) {
    138                 int segend = start + INDEX_INCREMENT;
    139                 if (segend > end)
    140                     segend = end;
    141 
    142                 getChars(s, start, segend, temp, 0);
    143 
    144                 int count = segend - start;
    145                 for (int i = 0; i < count; i++) {
    146                     if (temp[i] == ch) {
    147                         recycle(temp);
    148                         return i + start;
    149                     }
    150                 }
    151 
    152                 start = segend;
    153             }
    154 
    155             recycle(temp);
    156             return -1;
    157         }
    158 
    159         for (int i = start; i < end; i++)
    160             if (s.charAt(i) == ch)
    161                 return i;
    162 
    163         return -1;
    164     }
    165 
    166     public static int lastIndexOf(CharSequence s, char ch) {
    167         return lastIndexOf(s, ch, s.length() - 1);
    168     }
    169 
    170     public static int lastIndexOf(CharSequence s, char ch, int last) {
    171         Class<? extends CharSequence> c = s.getClass();
    172 
    173         if (c == String.class)
    174             return ((String) s).lastIndexOf(ch, last);
    175 
    176         return lastIndexOf(s, ch, 0, last);
    177     }
    178 
    179     public static int lastIndexOf(CharSequence s, char ch,
    180                                   int start, int last) {
    181         if (last < 0)
    182             return -1;
    183         if (last >= s.length())
    184             last = s.length() - 1;
    185 
    186         int end = last + 1;
    187 
    188         Class<? extends CharSequence> c = s.getClass();
    189 
    190         if (s instanceof GetChars || c == StringBuffer.class ||
    191             c == StringBuilder.class || c == String.class) {
    192             final int INDEX_INCREMENT = 500;
    193             char[] temp = obtain(INDEX_INCREMENT);
    194 
    195             while (start < end) {
    196                 int segstart = end - INDEX_INCREMENT;
    197                 if (segstart < start)
    198                     segstart = start;
    199 
    200                 getChars(s, segstart, end, temp, 0);
    201 
    202                 int count = end - segstart;
    203                 for (int i = count - 1; i >= 0; i--) {
    204                     if (temp[i] == ch) {
    205                         recycle(temp);
    206                         return i + segstart;
    207                     }
    208                 }
    209 
    210                 end = segstart;
    211             }
    212 
    213             recycle(temp);
    214             return -1;
    215         }
    216 
    217         for (int i = end - 1; i >= start; i--)
    218             if (s.charAt(i) == ch)
    219                 return i;
    220 
    221         return -1;
    222     }
    223 
    224     public static int indexOf(CharSequence s, CharSequence needle) {
    225         return indexOf(s, needle, 0, s.length());
    226     }
    227 
    228     public static int indexOf(CharSequence s, CharSequence needle, int start) {
    229         return indexOf(s, needle, start, s.length());
    230     }
    231 
    232     public static int indexOf(CharSequence s, CharSequence needle,
    233                               int start, int end) {
    234         int nlen = needle.length();
    235         if (nlen == 0)
    236             return start;
    237 
    238         char c = needle.charAt(0);
    239 
    240         for (;;) {
    241             start = indexOf(s, c, start);
    242             if (start > end - nlen) {
    243                 break;
    244             }
    245 
    246             if (start < 0) {
    247                 return -1;
    248             }
    249 
    250             if (regionMatches(s, start, needle, 0, nlen)) {
    251                 return start;
    252             }
    253 
    254             start++;
    255         }
    256         return -1;
    257     }
    258 
    259     public static boolean regionMatches(CharSequence one, int toffset,
    260                                         CharSequence two, int ooffset,
    261                                         int len) {
    262         int tempLen = 2 * len;
    263         if (tempLen < len) {
    264             // Integer overflow; len is unreasonably large
    265             throw new IndexOutOfBoundsException();
    266         }
    267         char[] temp = obtain(tempLen);
    268 
    269         getChars(one, toffset, toffset + len, temp, 0);
    270         getChars(two, ooffset, ooffset + len, temp, len);
    271 
    272         boolean match = true;
    273         for (int i = 0; i < len; i++) {
    274             if (temp[i] != temp[i + len]) {
    275                 match = false;
    276                 break;
    277             }
    278         }
    279 
    280         recycle(temp);
    281         return match;
    282     }
    283 
    284     /**
    285      * Create a new String object containing the given range of characters
    286      * from the source string.  This is different than simply calling
    287      * {@link CharSequence#subSequence(int, int) CharSequence.subSequence}
    288      * in that it does not preserve any style runs in the source sequence,
    289      * allowing a more efficient implementation.
    290      */
    291     public static String substring(CharSequence source, int start, int end) {
    292         if (source instanceof String)
    293             return ((String) source).substring(start, end);
    294         if (source instanceof StringBuilder)
    295             return ((StringBuilder) source).substring(start, end);
    296         if (source instanceof StringBuffer)
    297             return ((StringBuffer) source).substring(start, end);
    298 
    299         char[] temp = obtain(end - start);
    300         getChars(source, start, end, temp, 0);
    301         String ret = new String(temp, 0, end - start);
    302         recycle(temp);
    303 
    304         return ret;
    305     }
    306 
    307     /**
    308      * Returns a string containing the tokens joined by delimiters.
    309      *
    310      * @param delimiter a CharSequence that will be inserted between the tokens. If null, the string
    311      *     "null" will be used as the delimiter.
    312      * @param tokens an array objects to be joined. Strings will be formed from the objects by
    313      *     calling object.toString(). If tokens is null, a NullPointerException will be thrown. If
    314      *     tokens is an empty array, an empty string will be returned.
    315      */
    316     public static String join(@NonNull CharSequence delimiter, @NonNull Object[] tokens) {
    317         final int length = tokens.length;
    318         if (length == 0) {
    319             return "";
    320         }
    321         final StringBuilder sb = new StringBuilder();
    322         sb.append(tokens[0]);
    323         for (int i = 1; i < length; i++) {
    324             sb.append(delimiter);
    325             sb.append(tokens[i]);
    326         }
    327         return sb.toString();
    328     }
    329 
    330     /**
    331      * Returns a string containing the tokens joined by delimiters.
    332      *
    333      * @param delimiter a CharSequence that will be inserted between the tokens. If null, the string
    334      *     "null" will be used as the delimiter.
    335      * @param tokens an array objects to be joined. Strings will be formed from the objects by
    336      *     calling object.toString(). If tokens is null, a NullPointerException will be thrown. If
    337      *     tokens is empty, an empty string will be returned.
    338      */
    339     public static String join(@NonNull CharSequence delimiter, @NonNull Iterable tokens) {
    340         final Iterator<?> it = tokens.iterator();
    341         if (!it.hasNext()) {
    342             return "";
    343         }
    344         final StringBuilder sb = new StringBuilder();
    345         sb.append(it.next());
    346         while (it.hasNext()) {
    347             sb.append(delimiter);
    348             sb.append(it.next());
    349         }
    350         return sb.toString();
    351     }
    352 
    353     /**
    354      * String.split() returns [''] when the string to be split is empty. This returns []. This does
    355      * not remove any empty strings from the result. For example split("a,", ","  ) returns {"a", ""}.
    356      *
    357      * @param text the string to split
    358      * @param expression the regular expression to match
    359      * @return an array of strings. The array will be empty if text is empty
    360      *
    361      * @throws NullPointerException if expression or text is null
    362      */
    363     public static String[] split(String text, String expression) {
    364         if (text.length() == 0) {
    365             return EMPTY_STRING_ARRAY;
    366         } else {
    367             return text.split(expression, -1);
    368         }
    369     }
    370 
    371     /**
    372      * Splits a string on a pattern. String.split() returns [''] when the string to be
    373      * split is empty. This returns []. This does not remove any empty strings from the result.
    374      * @param text the string to split
    375      * @param pattern the regular expression to match
    376      * @return an array of strings. The array will be empty if text is empty
    377      *
    378      * @throws NullPointerException if expression or text is null
    379      */
    380     public static String[] split(String text, Pattern pattern) {
    381         if (text.length() == 0) {
    382             return EMPTY_STRING_ARRAY;
    383         } else {
    384             return pattern.split(text, -1);
    385         }
    386     }
    387 
    388     /**
    389      * An interface for splitting strings according to rules that are opaque to the user of this
    390      * interface. This also has less overhead than split, which uses regular expressions and
    391      * allocates an array to hold the results.
    392      *
    393      * <p>The most efficient way to use this class is:
    394      *
    395      * <pre>
    396      * // Once
    397      * TextUtils.StringSplitter splitter = new TextUtils.SimpleStringSplitter(delimiter);
    398      *
    399      * // Once per string to split
    400      * splitter.setString(string);
    401      * for (String s : splitter) {
    402      *     ...
    403      * }
    404      * </pre>
    405      */
    406     public interface StringSplitter extends Iterable<String> {
    407         public void setString(String string);
    408     }
    409 
    410     /**
    411      * A simple string splitter.
    412      *
    413      * <p>If the final character in the string to split is the delimiter then no empty string will
    414      * be returned for the empty string after that delimeter. That is, splitting <tt>"a,b,"</tt> on
    415      * comma will return <tt>"a", "b"</tt>, not <tt>"a", "b", ""</tt>.
    416      */
    417     public static class SimpleStringSplitter implements StringSplitter, Iterator<String> {
    418         private String mString;
    419         private char mDelimiter;
    420         private int mPosition;
    421         private int mLength;
    422 
    423         /**
    424          * Initializes the splitter. setString may be called later.
    425          * @param delimiter the delimeter on which to split
    426          */
    427         public SimpleStringSplitter(char delimiter) {
    428             mDelimiter = delimiter;
    429         }
    430 
    431         /**
    432          * Sets the string to split
    433          * @param string the string to split
    434          */
    435         public void setString(String string) {
    436             mString = string;
    437             mPosition = 0;
    438             mLength = mString.length();
    439         }
    440 
    441         public Iterator<String> iterator() {
    442             return this;
    443         }
    444 
    445         public boolean hasNext() {
    446             return mPosition < mLength;
    447         }
    448 
    449         public String next() {
    450             int end = mString.indexOf(mDelimiter, mPosition);
    451             if (end == -1) {
    452                 end = mLength;
    453             }
    454             String nextString = mString.substring(mPosition, end);
    455             mPosition = end + 1; // Skip the delimiter.
    456             return nextString;
    457         }
    458 
    459         public void remove() {
    460             throw new UnsupportedOperationException();
    461         }
    462     }
    463 
    464     public static CharSequence stringOrSpannedString(CharSequence source) {
    465         if (source == null)
    466             return null;
    467         if (source instanceof SpannedString)
    468             return source;
    469         if (source instanceof Spanned)
    470             return new SpannedString(source);
    471 
    472         return source.toString();
    473     }
    474 
    475     /**
    476      * Returns true if the string is null or 0-length.
    477      * @param str the string to be examined
    478      * @return true if str is null or zero length
    479      */
    480     public static boolean isEmpty(@Nullable CharSequence str) {
    481         return str == null || str.length() == 0;
    482     }
    483 
    484     /** {@hide} */
    485     public static String nullIfEmpty(@Nullable String str) {
    486         return isEmpty(str) ? null : str;
    487     }
    488 
    489     /** {@hide} */
    490     public static String emptyIfNull(@Nullable String str) {
    491         return str == null ? "" : str;
    492     }
    493 
    494     /** {@hide} */
    495     public static String firstNotEmpty(@Nullable String a, @NonNull String b) {
    496         return !isEmpty(a) ? a : Preconditions.checkStringNotEmpty(b);
    497     }
    498 
    499     /** {@hide} */
    500     public static int length(@Nullable String s) {
    501         return isEmpty(s) ? 0 : s.length();
    502     }
    503 
    504     /**
    505      * @return interned string if it's null.
    506      * @hide
    507      */
    508     public static String safeIntern(String s) {
    509         return (s != null) ? s.intern() : null;
    510     }
    511 
    512     /**
    513      * Returns the length that the specified CharSequence would have if
    514      * spaces and ASCII control characters were trimmed from the start and end,
    515      * as by {@link String#trim}.
    516      */
    517     public static int getTrimmedLength(CharSequence s) {
    518         int len = s.length();
    519 
    520         int start = 0;
    521         while (start < len && s.charAt(start) <= ' ') {
    522             start++;
    523         }
    524 
    525         int end = len;
    526         while (end > start && s.charAt(end - 1) <= ' ') {
    527             end--;
    528         }
    529 
    530         return end - start;
    531     }
    532 
    533     /**
    534      * Returns true if a and b are equal, including if they are both null.
    535      * <p><i>Note: In platform versions 1.1 and earlier, this method only worked well if
    536      * both the arguments were instances of String.</i></p>
    537      * @param a first CharSequence to check
    538      * @param b second CharSequence to check
    539      * @return true if a and b are equal
    540      */
    541     public static boolean equals(CharSequence a, CharSequence b) {
    542         if (a == b) return true;
    543         int length;
    544         if (a != null && b != null && (length = a.length()) == b.length()) {
    545             if (a instanceof String && b instanceof String) {
    546                 return a.equals(b);
    547             } else {
    548                 for (int i = 0; i < length; i++) {
    549                     if (a.charAt(i) != b.charAt(i)) return false;
    550                 }
    551                 return true;
    552             }
    553         }
    554         return false;
    555     }
    556 
    557     /**
    558      * This function only reverses individual {@code char}s and not their associated
    559      * spans. It doesn't support surrogate pairs (that correspond to non-BMP code points), combining
    560      * sequences or conjuncts either.
    561      * @deprecated Do not use.
    562      */
    563     @Deprecated
    564     public static CharSequence getReverse(CharSequence source, int start, int end) {
    565         return new Reverser(source, start, end);
    566     }
    567 
    568     private static class Reverser
    569     implements CharSequence, GetChars
    570     {
    571         public Reverser(CharSequence source, int start, int end) {
    572             mSource = source;
    573             mStart = start;
    574             mEnd = end;
    575         }
    576 
    577         public int length() {
    578             return mEnd - mStart;
    579         }
    580 
    581         public CharSequence subSequence(int start, int end) {
    582             char[] buf = new char[end - start];
    583 
    584             getChars(start, end, buf, 0);
    585             return new String(buf);
    586         }
    587 
    588         @Override
    589         public String toString() {
    590             return subSequence(0, length()).toString();
    591         }
    592 
    593         public char charAt(int off) {
    594             return (char) UCharacter.getMirror(mSource.charAt(mEnd - 1 - off));
    595         }
    596 
    597         @SuppressWarnings("deprecation")
    598         public void getChars(int start, int end, char[] dest, int destoff) {
    599             TextUtils.getChars(mSource, start + mStart, end + mStart,
    600                                dest, destoff);
    601             AndroidCharacter.mirror(dest, 0, end - start);
    602 
    603             int len = end - start;
    604             int n = (end - start) / 2;
    605             for (int i = 0; i < n; i++) {
    606                 char tmp = dest[destoff + i];
    607 
    608                 dest[destoff + i] = dest[destoff + len - i - 1];
    609                 dest[destoff + len - i - 1] = tmp;
    610             }
    611         }
    612 
    613         private CharSequence mSource;
    614         private int mStart;
    615         private int mEnd;
    616     }
    617 
    618     /** @hide */
    619     public static final int ALIGNMENT_SPAN = 1;
    620     /** @hide */
    621     public static final int FIRST_SPAN = ALIGNMENT_SPAN;
    622     /** @hide */
    623     public static final int FOREGROUND_COLOR_SPAN = 2;
    624     /** @hide */
    625     public static final int RELATIVE_SIZE_SPAN = 3;
    626     /** @hide */
    627     public static final int SCALE_X_SPAN = 4;
    628     /** @hide */
    629     public static final int STRIKETHROUGH_SPAN = 5;
    630     /** @hide */
    631     public static final int UNDERLINE_SPAN = 6;
    632     /** @hide */
    633     public static final int STYLE_SPAN = 7;
    634     /** @hide */
    635     public static final int BULLET_SPAN = 8;
    636     /** @hide */
    637     public static final int QUOTE_SPAN = 9;
    638     /** @hide */
    639     public static final int LEADING_MARGIN_SPAN = 10;
    640     /** @hide */
    641     public static final int URL_SPAN = 11;
    642     /** @hide */
    643     public static final int BACKGROUND_COLOR_SPAN = 12;
    644     /** @hide */
    645     public static final int TYPEFACE_SPAN = 13;
    646     /** @hide */
    647     public static final int SUPERSCRIPT_SPAN = 14;
    648     /** @hide */
    649     public static final int SUBSCRIPT_SPAN = 15;
    650     /** @hide */
    651     public static final int ABSOLUTE_SIZE_SPAN = 16;
    652     /** @hide */
    653     public static final int TEXT_APPEARANCE_SPAN = 17;
    654     /** @hide */
    655     public static final int ANNOTATION = 18;
    656     /** @hide */
    657     public static final int SUGGESTION_SPAN = 19;
    658     /** @hide */
    659     public static final int SPELL_CHECK_SPAN = 20;
    660     /** @hide */
    661     public static final int SUGGESTION_RANGE_SPAN = 21;
    662     /** @hide */
    663     public static final int EASY_EDIT_SPAN = 22;
    664     /** @hide */
    665     public static final int LOCALE_SPAN = 23;
    666     /** @hide */
    667     public static final int TTS_SPAN = 24;
    668     /** @hide */
    669     public static final int ACCESSIBILITY_CLICKABLE_SPAN = 25;
    670     /** @hide */
    671     public static final int ACCESSIBILITY_URL_SPAN = 26;
    672     /** @hide */
    673     public static final int LAST_SPAN = ACCESSIBILITY_URL_SPAN;
    674 
    675     /**
    676      * Flatten a CharSequence and whatever styles can be copied across processes
    677      * into the parcel.
    678      */
    679     public static void writeToParcel(CharSequence cs, Parcel p, int parcelableFlags) {
    680         if (cs instanceof Spanned) {
    681             p.writeInt(0);
    682             p.writeString(cs.toString());
    683 
    684             Spanned sp = (Spanned) cs;
    685             Object[] os = sp.getSpans(0, cs.length(), Object.class);
    686 
    687             // note to people adding to this: check more specific types
    688             // before more generic types.  also notice that it uses
    689             // "if" instead of "else if" where there are interfaces
    690             // so one object can be several.
    691 
    692             for (int i = 0; i < os.length; i++) {
    693                 Object o = os[i];
    694                 Object prop = os[i];
    695 
    696                 if (prop instanceof CharacterStyle) {
    697                     prop = ((CharacterStyle) prop).getUnderlying();
    698                 }
    699 
    700                 if (prop instanceof ParcelableSpan) {
    701                     final ParcelableSpan ps = (ParcelableSpan) prop;
    702                     final int spanTypeId = ps.getSpanTypeIdInternal();
    703                     if (spanTypeId < FIRST_SPAN || spanTypeId > LAST_SPAN) {
    704                         Log.e(TAG, "External class \"" + ps.getClass().getSimpleName()
    705                                 + "\" is attempting to use the frameworks-only ParcelableSpan"
    706                                 + " interface");
    707                     } else {
    708                         p.writeInt(spanTypeId);
    709                         ps.writeToParcelInternal(p, parcelableFlags);
    710                         writeWhere(p, sp, o);
    711                     }
    712                 }
    713             }
    714 
    715             p.writeInt(0);
    716         } else {
    717             p.writeInt(1);
    718             if (cs != null) {
    719                 p.writeString(cs.toString());
    720             } else {
    721                 p.writeString(null);
    722             }
    723         }
    724     }
    725 
    726     private static void writeWhere(Parcel p, Spanned sp, Object o) {
    727         p.writeInt(sp.getSpanStart(o));
    728         p.writeInt(sp.getSpanEnd(o));
    729         p.writeInt(sp.getSpanFlags(o));
    730     }
    731 
    732     public static final Parcelable.Creator<CharSequence> CHAR_SEQUENCE_CREATOR
    733             = new Parcelable.Creator<CharSequence>() {
    734         /**
    735          * Read and return a new CharSequence, possibly with styles,
    736          * from the parcel.
    737          */
    738         public CharSequence createFromParcel(Parcel p) {
    739             int kind = p.readInt();
    740 
    741             String string = p.readString();
    742             if (string == null) {
    743                 return null;
    744             }
    745 
    746             if (kind == 1) {
    747                 return string;
    748             }
    749 
    750             SpannableString sp = new SpannableString(string);
    751 
    752             while (true) {
    753                 kind = p.readInt();
    754 
    755                 if (kind == 0)
    756                     break;
    757 
    758                 switch (kind) {
    759                 case ALIGNMENT_SPAN:
    760                     readSpan(p, sp, new AlignmentSpan.Standard(p));
    761                     break;
    762 
    763                 case FOREGROUND_COLOR_SPAN:
    764                     readSpan(p, sp, new ForegroundColorSpan(p));
    765                     break;
    766 
    767                 case RELATIVE_SIZE_SPAN:
    768                     readSpan(p, sp, new RelativeSizeSpan(p));
    769                     break;
    770 
    771                 case SCALE_X_SPAN:
    772                     readSpan(p, sp, new ScaleXSpan(p));
    773                     break;
    774 
    775                 case STRIKETHROUGH_SPAN:
    776                     readSpan(p, sp, new StrikethroughSpan(p));
    777                     break;
    778 
    779                 case UNDERLINE_SPAN:
    780                     readSpan(p, sp, new UnderlineSpan(p));
    781                     break;
    782 
    783                 case STYLE_SPAN:
    784                     readSpan(p, sp, new StyleSpan(p));
    785                     break;
    786 
    787                 case BULLET_SPAN:
    788                     readSpan(p, sp, new BulletSpan(p));
    789                     break;
    790 
    791                 case QUOTE_SPAN:
    792                     readSpan(p, sp, new QuoteSpan(p));
    793                     break;
    794 
    795                 case LEADING_MARGIN_SPAN:
    796                     readSpan(p, sp, new LeadingMarginSpan.Standard(p));
    797                 break;
    798 
    799                 case URL_SPAN:
    800                     readSpan(p, sp, new URLSpan(p));
    801                     break;
    802 
    803                 case BACKGROUND_COLOR_SPAN:
    804                     readSpan(p, sp, new BackgroundColorSpan(p));
    805                     break;
    806 
    807                 case TYPEFACE_SPAN:
    808                     readSpan(p, sp, new TypefaceSpan(p));
    809                     break;
    810 
    811                 case SUPERSCRIPT_SPAN:
    812                     readSpan(p, sp, new SuperscriptSpan(p));
    813                     break;
    814 
    815                 case SUBSCRIPT_SPAN:
    816                     readSpan(p, sp, new SubscriptSpan(p));
    817                     break;
    818 
    819                 case ABSOLUTE_SIZE_SPAN:
    820                     readSpan(p, sp, new AbsoluteSizeSpan(p));
    821                     break;
    822 
    823                 case TEXT_APPEARANCE_SPAN:
    824                     readSpan(p, sp, new TextAppearanceSpan(p));
    825                     break;
    826 
    827                 case ANNOTATION:
    828                     readSpan(p, sp, new Annotation(p));
    829                     break;
    830 
    831                 case SUGGESTION_SPAN:
    832                     readSpan(p, sp, new SuggestionSpan(p));
    833                     break;
    834 
    835                 case SPELL_CHECK_SPAN:
    836                     readSpan(p, sp, new SpellCheckSpan(p));
    837                     break;
    838 
    839                 case SUGGESTION_RANGE_SPAN:
    840                     readSpan(p, sp, new SuggestionRangeSpan(p));
    841                     break;
    842 
    843                 case EASY_EDIT_SPAN:
    844                     readSpan(p, sp, new EasyEditSpan(p));
    845                     break;
    846 
    847                 case LOCALE_SPAN:
    848                     readSpan(p, sp, new LocaleSpan(p));
    849                     break;
    850 
    851                 case TTS_SPAN:
    852                     readSpan(p, sp, new TtsSpan(p));
    853                     break;
    854 
    855                 case ACCESSIBILITY_CLICKABLE_SPAN:
    856                     readSpan(p, sp, new AccessibilityClickableSpan(p));
    857                     break;
    858 
    859                 case ACCESSIBILITY_URL_SPAN:
    860                     readSpan(p, sp, new AccessibilityURLSpan(p));
    861                     break;
    862 
    863                 default:
    864                     throw new RuntimeException("bogus span encoding " + kind);
    865                 }
    866             }
    867 
    868             return sp;
    869         }
    870 
    871         public CharSequence[] newArray(int size)
    872         {
    873             return new CharSequence[size];
    874         }
    875     };
    876 
    877     /**
    878      * Debugging tool to print the spans in a CharSequence.  The output will
    879      * be printed one span per line.  If the CharSequence is not a Spanned,
    880      * then the entire string will be printed on a single line.
    881      */
    882     public static void dumpSpans(CharSequence cs, Printer printer, String prefix) {
    883         if (cs instanceof Spanned) {
    884             Spanned sp = (Spanned) cs;
    885             Object[] os = sp.getSpans(0, cs.length(), Object.class);
    886 
    887             for (int i = 0; i < os.length; i++) {
    888                 Object o = os[i];
    889                 printer.println(prefix + cs.subSequence(sp.getSpanStart(o),
    890                         sp.getSpanEnd(o)) + ": "
    891                         + Integer.toHexString(System.identityHashCode(o))
    892                         + " " + o.getClass().getCanonicalName()
    893                          + " (" + sp.getSpanStart(o) + "-" + sp.getSpanEnd(o)
    894                          + ") fl=#" + sp.getSpanFlags(o));
    895             }
    896         } else {
    897             printer.println(prefix + cs + ": (no spans)");
    898         }
    899     }
    900 
    901     /**
    902      * Return a new CharSequence in which each of the source strings is
    903      * replaced by the corresponding element of the destinations.
    904      */
    905     public static CharSequence replace(CharSequence template,
    906                                        String[] sources,
    907                                        CharSequence[] destinations) {
    908         SpannableStringBuilder tb = new SpannableStringBuilder(template);
    909 
    910         for (int i = 0; i < sources.length; i++) {
    911             int where = indexOf(tb, sources[i]);
    912 
    913             if (where >= 0)
    914                 tb.setSpan(sources[i], where, where + sources[i].length(),
    915                            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    916         }
    917 
    918         for (int i = 0; i < sources.length; i++) {
    919             int start = tb.getSpanStart(sources[i]);
    920             int end = tb.getSpanEnd(sources[i]);
    921 
    922             if (start >= 0) {
    923                 tb.replace(start, end, destinations[i]);
    924             }
    925         }
    926 
    927         return tb;
    928     }
    929 
    930     /**
    931      * Replace instances of "^1", "^2", etc. in the
    932      * <code>template</code> CharSequence with the corresponding
    933      * <code>values</code>.  "^^" is used to produce a single caret in
    934      * the output.  Only up to 9 replacement values are supported,
    935      * "^10" will be produce the first replacement value followed by a
    936      * '0'.
    937      *
    938      * @param template the input text containing "^1"-style
    939      * placeholder values.  This object is not modified; a copy is
    940      * returned.
    941      *
    942      * @param values CharSequences substituted into the template.  The
    943      * first is substituted for "^1", the second for "^2", and so on.
    944      *
    945      * @return the new CharSequence produced by doing the replacement
    946      *
    947      * @throws IllegalArgumentException if the template requests a
    948      * value that was not provided, or if more than 9 values are
    949      * provided.
    950      */
    951     public static CharSequence expandTemplate(CharSequence template,
    952                                               CharSequence... values) {
    953         if (values.length > 9) {
    954             throw new IllegalArgumentException("max of 9 values are supported");
    955         }
    956 
    957         SpannableStringBuilder ssb = new SpannableStringBuilder(template);
    958 
    959         try {
    960             int i = 0;
    961             while (i < ssb.length()) {
    962                 if (ssb.charAt(i) == '^') {
    963                     char next = ssb.charAt(i+1);
    964                     if (next == '^') {
    965                         ssb.delete(i+1, i+2);
    966                         ++i;
    967                         continue;
    968                     } else if (Character.isDigit(next)) {
    969                         int which = Character.getNumericValue(next) - 1;
    970                         if (which < 0) {
    971                             throw new IllegalArgumentException(
    972                                 "template requests value ^" + (which+1));
    973                         }
    974                         if (which >= values.length) {
    975                             throw new IllegalArgumentException(
    976                                 "template requests value ^" + (which+1) +
    977                                 "; only " + values.length + " provided");
    978                         }
    979                         ssb.replace(i, i+2, values[which]);
    980                         i += values[which].length();
    981                         continue;
    982                     }
    983                 }
    984                 ++i;
    985             }
    986         } catch (IndexOutOfBoundsException ignore) {
    987             // happens when ^ is the last character in the string.
    988         }
    989         return ssb;
    990     }
    991 
    992     public static int getOffsetBefore(CharSequence text, int offset) {
    993         if (offset == 0)
    994             return 0;
    995         if (offset == 1)
    996             return 0;
    997 
    998         char c = text.charAt(offset - 1);
    999 
   1000         if (c >= '\uDC00' && c <= '\uDFFF') {
   1001             char c1 = text.charAt(offset - 2);
   1002 
   1003             if (c1 >= '\uD800' && c1 <= '\uDBFF')
   1004                 offset -= 2;
   1005             else
   1006                 offset -= 1;
   1007         } else {
   1008             offset -= 1;
   1009         }
   1010 
   1011         if (text instanceof Spanned) {
   1012             ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
   1013                                                        ReplacementSpan.class);
   1014 
   1015             for (int i = 0; i < spans.length; i++) {
   1016                 int start = ((Spanned) text).getSpanStart(spans[i]);
   1017                 int end = ((Spanned) text).getSpanEnd(spans[i]);
   1018 
   1019                 if (start < offset && end > offset)
   1020                     offset = start;
   1021             }
   1022         }
   1023 
   1024         return offset;
   1025     }
   1026 
   1027     public static int getOffsetAfter(CharSequence text, int offset) {
   1028         int len = text.length();
   1029 
   1030         if (offset == len)
   1031             return len;
   1032         if (offset == len - 1)
   1033             return len;
   1034 
   1035         char c = text.charAt(offset);
   1036 
   1037         if (c >= '\uD800' && c <= '\uDBFF') {
   1038             char c1 = text.charAt(offset + 1);
   1039 
   1040             if (c1 >= '\uDC00' && c1 <= '\uDFFF')
   1041                 offset += 2;
   1042             else
   1043                 offset += 1;
   1044         } else {
   1045             offset += 1;
   1046         }
   1047 
   1048         if (text instanceof Spanned) {
   1049             ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
   1050                                                        ReplacementSpan.class);
   1051 
   1052             for (int i = 0; i < spans.length; i++) {
   1053                 int start = ((Spanned) text).getSpanStart(spans[i]);
   1054                 int end = ((Spanned) text).getSpanEnd(spans[i]);
   1055 
   1056                 if (start < offset && end > offset)
   1057                     offset = end;
   1058             }
   1059         }
   1060 
   1061         return offset;
   1062     }
   1063 
   1064     private static void readSpan(Parcel p, Spannable sp, Object o) {
   1065         sp.setSpan(o, p.readInt(), p.readInt(), p.readInt());
   1066     }
   1067 
   1068     /**
   1069      * Copies the spans from the region <code>start...end</code> in
   1070      * <code>source</code> to the region
   1071      * <code>destoff...destoff+end-start</code> in <code>dest</code>.
   1072      * Spans in <code>source</code> that begin before <code>start</code>
   1073      * or end after <code>end</code> but overlap this range are trimmed
   1074      * as if they began at <code>start</code> or ended at <code>end</code>.
   1075      *
   1076      * @throws IndexOutOfBoundsException if any of the copied spans
   1077      * are out of range in <code>dest</code>.
   1078      */
   1079     public static void copySpansFrom(Spanned source, int start, int end,
   1080                                      Class kind,
   1081                                      Spannable dest, int destoff) {
   1082         if (kind == null) {
   1083             kind = Object.class;
   1084         }
   1085 
   1086         Object[] spans = source.getSpans(start, end, kind);
   1087 
   1088         for (int i = 0; i < spans.length; i++) {
   1089             int st = source.getSpanStart(spans[i]);
   1090             int en = source.getSpanEnd(spans[i]);
   1091             int fl = source.getSpanFlags(spans[i]);
   1092 
   1093             if (st < start)
   1094                 st = start;
   1095             if (en > end)
   1096                 en = end;
   1097 
   1098             dest.setSpan(spans[i], st - start + destoff, en - start + destoff,
   1099                          fl);
   1100         }
   1101     }
   1102 
   1103     /**
   1104      * Transforms a CharSequences to uppercase, copying the sources spans and keeping them spans as
   1105      * much as possible close to their relative original places. In the case the the uppercase
   1106      * string is identical to the sources, the source itself is returned instead of being copied.
   1107      *
   1108      * If copySpans is set, source must be an instance of Spanned.
   1109      *
   1110      * {@hide}
   1111      */
   1112     @NonNull
   1113     public static CharSequence toUpperCase(@Nullable Locale locale, @NonNull CharSequence source,
   1114             boolean copySpans) {
   1115         final Edits edits = new Edits();
   1116         if (!copySpans) { // No spans. Just uppercase the characters.
   1117             final StringBuilder result = CaseMap.toUpper().apply(
   1118                     locale, source, new StringBuilder(), edits);
   1119             return edits.hasChanges() ? result : source;
   1120         }
   1121 
   1122         final SpannableStringBuilder result = CaseMap.toUpper().apply(
   1123                 locale, source, new SpannableStringBuilder(), edits);
   1124         if (!edits.hasChanges()) {
   1125             // No changes happened while capitalizing. We can return the source as it was.
   1126             return source;
   1127         }
   1128 
   1129         final Edits.Iterator iterator = edits.getFineIterator();
   1130         final int sourceLength = source.length();
   1131         final Spanned spanned = (Spanned) source;
   1132         final Object[] spans = spanned.getSpans(0, sourceLength, Object.class);
   1133         for (Object span : spans) {
   1134             final int sourceStart = spanned.getSpanStart(span);
   1135             final int sourceEnd = spanned.getSpanEnd(span);
   1136             final int flags = spanned.getSpanFlags(span);
   1137             // Make sure the indices are not at the end of the string, since in that case
   1138             // iterator.findSourceIndex() would fail.
   1139             final int destStart = sourceStart == sourceLength ? result.length() :
   1140                     toUpperMapToDest(iterator, sourceStart);
   1141             final int destEnd = sourceEnd == sourceLength ? result.length() :
   1142                     toUpperMapToDest(iterator, sourceEnd);
   1143             result.setSpan(span, destStart, destEnd, flags);
   1144         }
   1145         return result;
   1146     }
   1147 
   1148     // helper method for toUpperCase()
   1149     private static int toUpperMapToDest(Edits.Iterator iterator, int sourceIndex) {
   1150         // Guaranteed to succeed if sourceIndex < source.length().
   1151         iterator.findSourceIndex(sourceIndex);
   1152         if (sourceIndex == iterator.sourceIndex()) {
   1153             return iterator.destinationIndex();
   1154         }
   1155         // We handle the situation differently depending on if we are in the changed slice or an
   1156         // unchanged one: In an unchanged slice, we can find the exact location the span
   1157         // boundary was before and map there.
   1158         //
   1159         // But in a changed slice, we need to treat the whole destination slice as an atomic unit.
   1160         // We adjust the span boundary to the end of that slice to reduce of the chance of adjacent
   1161         // spans in the source overlapping in the result. (The choice for the end vs the beginning
   1162         // is somewhat arbitrary, but was taken because we except to see slightly more spans only
   1163         // affecting a base character compared to spans only affecting a combining character.)
   1164         if (iterator.hasChange()) {
   1165             return iterator.destinationIndex() + iterator.newLength();
   1166         } else {
   1167             // Move the index 1:1 along with this unchanged piece of text.
   1168             return iterator.destinationIndex() + (sourceIndex - iterator.sourceIndex());
   1169         }
   1170     }
   1171 
   1172     public enum TruncateAt {
   1173         START,
   1174         MIDDLE,
   1175         END,
   1176         MARQUEE,
   1177         /**
   1178          * @hide
   1179          */
   1180         END_SMALL
   1181     }
   1182 
   1183     public interface EllipsizeCallback {
   1184         /**
   1185          * This method is called to report that the specified region of
   1186          * text was ellipsized away by a call to {@link #ellipsize}.
   1187          */
   1188         public void ellipsized(int start, int end);
   1189     }
   1190 
   1191     /**
   1192      * Returns the original text if it fits in the specified width
   1193      * given the properties of the specified Paint,
   1194      * or, if it does not fit, a truncated
   1195      * copy with ellipsis character added at the specified edge or center.
   1196      */
   1197     public static CharSequence ellipsize(CharSequence text,
   1198                                          TextPaint p,
   1199                                          float avail, TruncateAt where) {
   1200         return ellipsize(text, p, avail, where, false, null);
   1201     }
   1202 
   1203     /**
   1204      * Returns the original text if it fits in the specified width
   1205      * given the properties of the specified Paint,
   1206      * or, if it does not fit, a copy with ellipsis character added
   1207      * at the specified edge or center.
   1208      * If <code>preserveLength</code> is specified, the returned copy
   1209      * will be padded with zero-width spaces to preserve the original
   1210      * length and offsets instead of truncating.
   1211      * If <code>callback</code> is non-null, it will be called to
   1212      * report the start and end of the ellipsized range.  TextDirection
   1213      * is determined by the first strong directional character.
   1214      */
   1215     public static CharSequence ellipsize(CharSequence text,
   1216                                          TextPaint paint,
   1217                                          float avail, TruncateAt where,
   1218                                          boolean preserveLength,
   1219                                          @Nullable EllipsizeCallback callback) {
   1220         return ellipsize(text, paint, avail, where, preserveLength, callback,
   1221                 TextDirectionHeuristics.FIRSTSTRONG_LTR,
   1222                 getEllipsisString(where));
   1223     }
   1224 
   1225     /**
   1226      * Returns the original text if it fits in the specified width
   1227      * given the properties of the specified Paint,
   1228      * or, if it does not fit, a copy with ellipsis character added
   1229      * at the specified edge or center.
   1230      * If <code>preserveLength</code> is specified, the returned copy
   1231      * will be padded with zero-width spaces to preserve the original
   1232      * length and offsets instead of truncating.
   1233      * If <code>callback</code> is non-null, it will be called to
   1234      * report the start and end of the ellipsized range.
   1235      *
   1236      * @hide
   1237      */
   1238     public static CharSequence ellipsize(CharSequence text,
   1239             TextPaint paint,
   1240             float avail, TruncateAt where,
   1241             boolean preserveLength,
   1242             @Nullable EllipsizeCallback callback,
   1243             TextDirectionHeuristic textDir, String ellipsis) {
   1244 
   1245         int len = text.length();
   1246 
   1247         MeasuredParagraph mt = null;
   1248         try {
   1249             mt = MeasuredParagraph.buildForMeasurement(paint, text, 0, text.length(), textDir, mt);
   1250             float width = mt.getWholeWidth();
   1251 
   1252             if (width <= avail) {
   1253                 if (callback != null) {
   1254                     callback.ellipsized(0, 0);
   1255                 }
   1256 
   1257                 return text;
   1258             }
   1259 
   1260             // XXX assumes ellipsis string does not require shaping and
   1261             // is unaffected by style
   1262             float ellipsiswid = paint.measureText(ellipsis);
   1263             avail -= ellipsiswid;
   1264 
   1265             int left = 0;
   1266             int right = len;
   1267             if (avail < 0) {
   1268                 // it all goes
   1269             } else if (where == TruncateAt.START) {
   1270                 right = len - mt.breakText(len, false, avail);
   1271             } else if (where == TruncateAt.END || where == TruncateAt.END_SMALL) {
   1272                 left = mt.breakText(len, true, avail);
   1273             } else {
   1274                 right = len - mt.breakText(len, false, avail / 2);
   1275                 avail -= mt.measure(right, len);
   1276                 left = mt.breakText(right, true, avail);
   1277             }
   1278 
   1279             if (callback != null) {
   1280                 callback.ellipsized(left, right);
   1281             }
   1282 
   1283             final char[] buf = mt.getChars();
   1284             Spanned sp = text instanceof Spanned ? (Spanned) text : null;
   1285 
   1286             final int removed = right - left;
   1287             final int remaining = len - removed;
   1288             if (preserveLength) {
   1289                 if (remaining > 0 && removed >= ellipsis.length()) {
   1290                     ellipsis.getChars(0, ellipsis.length(), buf, left);
   1291                     left += ellipsis.length();
   1292                 } // else skip the ellipsis
   1293                 for (int i = left; i < right; i++) {
   1294                     buf[i] = ELLIPSIS_FILLER;
   1295                 }
   1296                 String s = new String(buf, 0, len);
   1297                 if (sp == null) {
   1298                     return s;
   1299                 }
   1300                 SpannableString ss = new SpannableString(s);
   1301                 copySpansFrom(sp, 0, len, Object.class, ss, 0);
   1302                 return ss;
   1303             }
   1304 
   1305             if (remaining == 0) {
   1306                 return "";
   1307             }
   1308 
   1309             if (sp == null) {
   1310                 StringBuilder sb = new StringBuilder(remaining + ellipsis.length());
   1311                 sb.append(buf, 0, left);
   1312                 sb.append(ellipsis);
   1313                 sb.append(buf, right, len - right);
   1314                 return sb.toString();
   1315             }
   1316 
   1317             SpannableStringBuilder ssb = new SpannableStringBuilder();
   1318             ssb.append(text, 0, left);
   1319             ssb.append(ellipsis);
   1320             ssb.append(text, right, len);
   1321             return ssb;
   1322         } finally {
   1323             if (mt != null) {
   1324                 mt.recycle();
   1325             }
   1326         }
   1327     }
   1328 
   1329     /**
   1330      * Formats a list of CharSequences by repeatedly inserting the separator between them,
   1331      * but stopping when the resulting sequence is too wide for the specified width.
   1332      *
   1333      * This method actually tries to fit the maximum number of elements. So if {@code "A, 11 more"
   1334      * fits}, {@code "A, B, 10 more"} doesn't fit, but {@code "A, B, C, 9 more"} fits again (due to
   1335      * the glyphs for the digits being very wide, for example), it returns
   1336      * {@code "A, B, C, 9 more"}. Because of this, this method may be inefficient for very long
   1337      * lists.
   1338      *
   1339      * Note that the elements of the returned value, as well as the string for {@code moreId}, will
   1340      * be bidi-wrapped using {@link BidiFormatter#unicodeWrap} based on the locale of the input
   1341      * Context. If the input {@code Context} is null, the default BidiFormatter from
   1342      * {@link BidiFormatter#getInstance()} will be used.
   1343      *
   1344      * @param context the {@code Context} to get the {@code moreId} resource from. If {@code null},
   1345      *     an ellipsis (U+2026) would be used for {@code moreId}.
   1346      * @param elements the list to format
   1347      * @param separator a separator, such as {@code ", "}
   1348      * @param paint the Paint with which to measure the text
   1349      * @param avail the horizontal width available for the text (in pixels)
   1350      * @param moreId the resource ID for the pluralized string to insert at the end of sequence when
   1351      *     some of the elements don't fit.
   1352      *
   1353      * @return the formatted CharSequence. If even the shortest sequence (e.g. {@code "A, 11 more"})
   1354      *     doesn't fit, it will return an empty string.
   1355      */
   1356 
   1357     public static CharSequence listEllipsize(@Nullable Context context,
   1358             @Nullable List<CharSequence> elements, @NonNull String separator,
   1359             @NonNull TextPaint paint, @FloatRange(from=0.0,fromInclusive=false) float avail,
   1360             @PluralsRes int moreId) {
   1361         if (elements == null) {
   1362             return "";
   1363         }
   1364         final int totalLen = elements.size();
   1365         if (totalLen == 0) {
   1366             return "";
   1367         }
   1368 
   1369         final Resources res;
   1370         final BidiFormatter bidiFormatter;
   1371         if (context == null) {
   1372             res = null;
   1373             bidiFormatter = BidiFormatter.getInstance();
   1374         } else {
   1375             res = context.getResources();
   1376             bidiFormatter = BidiFormatter.getInstance(res.getConfiguration().getLocales().get(0));
   1377         }
   1378 
   1379         final SpannableStringBuilder output = new SpannableStringBuilder();
   1380         final int[] endIndexes = new int[totalLen];
   1381         for (int i = 0; i < totalLen; i++) {
   1382             output.append(bidiFormatter.unicodeWrap(elements.get(i)));
   1383             if (i != totalLen - 1) {  // Insert a separator, except at the very end.
   1384                 output.append(separator);
   1385             }
   1386             endIndexes[i] = output.length();
   1387         }
   1388 
   1389         for (int i = totalLen - 1; i >= 0; i--) {
   1390             // Delete the tail of the string, cutting back to one less element.
   1391             output.delete(endIndexes[i], output.length());
   1392 
   1393             final int remainingElements = totalLen - i - 1;
   1394             if (remainingElements > 0) {
   1395                 CharSequence morePiece = (res == null) ?
   1396                         ELLIPSIS_NORMAL :
   1397                         res.getQuantityString(moreId, remainingElements, remainingElements);
   1398                 morePiece = bidiFormatter.unicodeWrap(morePiece);
   1399                 output.append(morePiece);
   1400             }
   1401 
   1402             final float width = paint.measureText(output, 0, output.length());
   1403             if (width <= avail) {  // The string fits.
   1404                 return output;
   1405             }
   1406         }
   1407         return "";  // Nothing fits.
   1408     }
   1409 
   1410     /**
   1411      * Converts a CharSequence of the comma-separated form "Andy, Bob,
   1412      * Charles, David" that is too wide to fit into the specified width
   1413      * into one like "Andy, Bob, 2 more".
   1414      *
   1415      * @param text the text to truncate
   1416      * @param p the Paint with which to measure the text
   1417      * @param avail the horizontal width available for the text (in pixels)
   1418      * @param oneMore the string for "1 more" in the current locale
   1419      * @param more the string for "%d more" in the current locale
   1420      *
   1421      * @deprecated Do not use. This is not internationalized, and has known issues
   1422      * with right-to-left text, languages that have more than one plural form, languages
   1423      * that use a different character as a comma-like separator, etc.
   1424      * Use {@link #listEllipsize} instead.
   1425      */
   1426     @Deprecated
   1427     public static CharSequence commaEllipsize(CharSequence text,
   1428                                               TextPaint p, float avail,
   1429                                               String oneMore,
   1430                                               String more) {
   1431         return commaEllipsize(text, p, avail, oneMore, more,
   1432                 TextDirectionHeuristics.FIRSTSTRONG_LTR);
   1433     }
   1434 
   1435     /**
   1436      * @hide
   1437      */
   1438     @Deprecated
   1439     public static CharSequence commaEllipsize(CharSequence text, TextPaint p,
   1440          float avail, String oneMore, String more, TextDirectionHeuristic textDir) {
   1441 
   1442         MeasuredParagraph mt = null;
   1443         MeasuredParagraph tempMt = null;
   1444         try {
   1445             int len = text.length();
   1446             mt = MeasuredParagraph.buildForMeasurement(p, text, 0, len, textDir, mt);
   1447             final float width = mt.getWholeWidth();
   1448             if (width <= avail) {
   1449                 return text;
   1450             }
   1451 
   1452             char[] buf = mt.getChars();
   1453 
   1454             int commaCount = 0;
   1455             for (int i = 0; i < len; i++) {
   1456                 if (buf[i] == ',') {
   1457                     commaCount++;
   1458                 }
   1459             }
   1460 
   1461             int remaining = commaCount + 1;
   1462 
   1463             int ok = 0;
   1464             String okFormat = "";
   1465 
   1466             int w = 0;
   1467             int count = 0;
   1468             float[] widths = mt.getWidths().getRawArray();
   1469 
   1470             for (int i = 0; i < len; i++) {
   1471                 w += widths[i];
   1472 
   1473                 if (buf[i] == ',') {
   1474                     count++;
   1475 
   1476                     String format;
   1477                     // XXX should not insert spaces, should be part of string
   1478                     // XXX should use plural rules and not assume English plurals
   1479                     if (--remaining == 1) {
   1480                         format = " " + oneMore;
   1481                     } else {
   1482                         format = " " + String.format(more, remaining);
   1483                     }
   1484 
   1485                     // XXX this is probably ok, but need to look at it more
   1486                     tempMt = MeasuredParagraph.buildForMeasurement(
   1487                             p, format, 0, format.length(), textDir, tempMt);
   1488                     float moreWid = tempMt.getWholeWidth();
   1489 
   1490                     if (w + moreWid <= avail) {
   1491                         ok = i + 1;
   1492                         okFormat = format;
   1493                     }
   1494                 }
   1495             }
   1496 
   1497             SpannableStringBuilder out = new SpannableStringBuilder(okFormat);
   1498             out.insert(0, text, 0, ok);
   1499             return out;
   1500         } finally {
   1501             if (mt != null) {
   1502                 mt.recycle();
   1503             }
   1504             if (tempMt != null) {
   1505                 tempMt.recycle();
   1506             }
   1507         }
   1508     }
   1509 
   1510     // Returns true if the character's presence could affect RTL layout.
   1511     //
   1512     // In order to be fast, the code is intentionally rough and quite conservative in its
   1513     // considering inclusion of any non-BMP or surrogate characters or anything in the bidi
   1514     // blocks or any bidi formatting characters with a potential to affect RTL layout.
   1515     /* package */
   1516     static boolean couldAffectRtl(char c) {
   1517         return (0x0590 <= c && c <= 0x08FF) ||  // RTL scripts
   1518                 c == 0x200E ||  // Bidi format character
   1519                 c == 0x200F ||  // Bidi format character
   1520                 (0x202A <= c && c <= 0x202E) ||  // Bidi format characters
   1521                 (0x2066 <= c && c <= 0x2069) ||  // Bidi format characters
   1522                 (0xD800 <= c && c <= 0xDFFF) ||  // Surrogate pairs
   1523                 (0xFB1D <= c && c <= 0xFDFF) ||  // Hebrew and Arabic presentation forms
   1524                 (0xFE70 <= c && c <= 0xFEFE);  // Arabic presentation forms
   1525     }
   1526 
   1527     // Returns true if there is no character present that may potentially affect RTL layout.
   1528     // Since this calls couldAffectRtl() above, it's also quite conservative, in the way that
   1529     // it may return 'false' (needs bidi) although careful consideration may tell us it should
   1530     // return 'true' (does not need bidi).
   1531     /* package */
   1532     static boolean doesNotNeedBidi(char[] text, int start, int len) {
   1533         final int end = start + len;
   1534         for (int i = start; i < end; i++) {
   1535             if (couldAffectRtl(text[i])) {
   1536                 return false;
   1537             }
   1538         }
   1539         return true;
   1540     }
   1541 
   1542     /* package */ static char[] obtain(int len) {
   1543         char[] buf;
   1544 
   1545         synchronized (sLock) {
   1546             buf = sTemp;
   1547             sTemp = null;
   1548         }
   1549 
   1550         if (buf == null || buf.length < len)
   1551             buf = ArrayUtils.newUnpaddedCharArray(len);
   1552 
   1553         return buf;
   1554     }
   1555 
   1556     /* package */ static void recycle(char[] temp) {
   1557         if (temp.length > 1000)
   1558             return;
   1559 
   1560         synchronized (sLock) {
   1561             sTemp = temp;
   1562         }
   1563     }
   1564 
   1565     /**
   1566      * Html-encode the string.
   1567      * @param s the string to be encoded
   1568      * @return the encoded string
   1569      */
   1570     public static String htmlEncode(String s) {
   1571         StringBuilder sb = new StringBuilder();
   1572         char c;
   1573         for (int i = 0; i < s.length(); i++) {
   1574             c = s.charAt(i);
   1575             switch (c) {
   1576             case '<':
   1577                 sb.append("&lt;"); //$NON-NLS-1$
   1578                 break;
   1579             case '>':
   1580                 sb.append("&gt;"); //$NON-NLS-1$
   1581                 break;
   1582             case '&':
   1583                 sb.append("&amp;"); //$NON-NLS-1$
   1584                 break;
   1585             case '\'':
   1586                 //http://www.w3.org/TR/xhtml1
   1587                 // The named character reference &apos; (the apostrophe, U+0027) was introduced in
   1588                 // XML 1.0 but does not appear in HTML. Authors should therefore use &#39; instead
   1589                 // of &apos; to work as expected in HTML 4 user agents.
   1590                 sb.append("&#39;"); //$NON-NLS-1$
   1591                 break;
   1592             case '"':
   1593                 sb.append("&quot;"); //$NON-NLS-1$
   1594                 break;
   1595             default:
   1596                 sb.append(c);
   1597             }
   1598         }
   1599         return sb.toString();
   1600     }
   1601 
   1602     /**
   1603      * Returns a CharSequence concatenating the specified CharSequences,
   1604      * retaining their spans if any.
   1605      *
   1606      * If there are no parameters, an empty string will be returned.
   1607      *
   1608      * If the number of parameters is exactly one, that parameter is returned as output, even if it
   1609      * is null.
   1610      *
   1611      * If the number of parameters is at least two, any null CharSequence among the parameters is
   1612      * treated as if it was the string <code>"null"</code>.
   1613      *
   1614      * If there are paragraph spans in the source CharSequences that satisfy paragraph boundary
   1615      * requirements in the sources but would no longer satisfy them in the concatenated
   1616      * CharSequence, they may get extended in the resulting CharSequence or not retained.
   1617      */
   1618     public static CharSequence concat(CharSequence... text) {
   1619         if (text.length == 0) {
   1620             return "";
   1621         }
   1622 
   1623         if (text.length == 1) {
   1624             return text[0];
   1625         }
   1626 
   1627         boolean spanned = false;
   1628         for (CharSequence piece : text) {
   1629             if (piece instanceof Spanned) {
   1630                 spanned = true;
   1631                 break;
   1632             }
   1633         }
   1634 
   1635         if (spanned) {
   1636             final SpannableStringBuilder ssb = new SpannableStringBuilder();
   1637             for (CharSequence piece : text) {
   1638                 // If a piece is null, we append the string "null" for compatibility with the
   1639                 // behavior of StringBuilder and the behavior of the concat() method in earlier
   1640                 // versions of Android.
   1641                 ssb.append(piece == null ? "null" : piece);
   1642             }
   1643             return new SpannedString(ssb);
   1644         } else {
   1645             final StringBuilder sb = new StringBuilder();
   1646             for (CharSequence piece : text) {
   1647                 sb.append(piece);
   1648             }
   1649             return sb.toString();
   1650         }
   1651     }
   1652 
   1653     /**
   1654      * Returns whether the given CharSequence contains any printable characters.
   1655      */
   1656     public static boolean isGraphic(CharSequence str) {
   1657         final int len = str.length();
   1658         for (int cp, i=0; i<len; i+=Character.charCount(cp)) {
   1659             cp = Character.codePointAt(str, i);
   1660             int gc = Character.getType(cp);
   1661             if (gc != Character.CONTROL
   1662                     && gc != Character.FORMAT
   1663                     && gc != Character.SURROGATE
   1664                     && gc != Character.UNASSIGNED
   1665                     && gc != Character.LINE_SEPARATOR
   1666                     && gc != Character.PARAGRAPH_SEPARATOR
   1667                     && gc != Character.SPACE_SEPARATOR) {
   1668                 return true;
   1669             }
   1670         }
   1671         return false;
   1672     }
   1673 
   1674     /**
   1675      * Returns whether this character is a printable character.
   1676      *
   1677      * This does not support non-BMP characters and should not be used.
   1678      *
   1679      * @deprecated Use {@link #isGraphic(CharSequence)} instead.
   1680      */
   1681     @Deprecated
   1682     public static boolean isGraphic(char c) {
   1683         int gc = Character.getType(c);
   1684         return     gc != Character.CONTROL
   1685                 && gc != Character.FORMAT
   1686                 && gc != Character.SURROGATE
   1687                 && gc != Character.UNASSIGNED
   1688                 && gc != Character.LINE_SEPARATOR
   1689                 && gc != Character.PARAGRAPH_SEPARATOR
   1690                 && gc != Character.SPACE_SEPARATOR;
   1691     }
   1692 
   1693     /**
   1694      * Returns whether the given CharSequence contains only digits.
   1695      */
   1696     public static boolean isDigitsOnly(CharSequence str) {
   1697         final int len = str.length();
   1698         for (int cp, i = 0; i < len; i += Character.charCount(cp)) {
   1699             cp = Character.codePointAt(str, i);
   1700             if (!Character.isDigit(cp)) {
   1701                 return false;
   1702             }
   1703         }
   1704         return true;
   1705     }
   1706 
   1707     /**
   1708      * @hide
   1709      */
   1710     public static boolean isPrintableAscii(final char c) {
   1711         final int asciiFirst = 0x20;
   1712         final int asciiLast = 0x7E;  // included
   1713         return (asciiFirst <= c && c <= asciiLast) || c == '\r' || c == '\n';
   1714     }
   1715 
   1716     /**
   1717      * @hide
   1718      */
   1719     public static boolean isPrintableAsciiOnly(final CharSequence str) {
   1720         final int len = str.length();
   1721         for (int i = 0; i < len; i++) {
   1722             if (!isPrintableAscii(str.charAt(i))) {
   1723                 return false;
   1724             }
   1725         }
   1726         return true;
   1727     }
   1728 
   1729     /**
   1730      * Capitalization mode for {@link #getCapsMode}: capitalize all
   1731      * characters.  This value is explicitly defined to be the same as
   1732      * {@link InputType#TYPE_TEXT_FLAG_CAP_CHARACTERS}.
   1733      */
   1734     public static final int CAP_MODE_CHARACTERS
   1735             = InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS;
   1736 
   1737     /**
   1738      * Capitalization mode for {@link #getCapsMode}: capitalize the first
   1739      * character of all words.  This value is explicitly defined to be the same as
   1740      * {@link InputType#TYPE_TEXT_FLAG_CAP_WORDS}.
   1741      */
   1742     public static final int CAP_MODE_WORDS
   1743             = InputType.TYPE_TEXT_FLAG_CAP_WORDS;
   1744 
   1745     /**
   1746      * Capitalization mode for {@link #getCapsMode}: capitalize the first
   1747      * character of each sentence.  This value is explicitly defined to be the same as
   1748      * {@link InputType#TYPE_TEXT_FLAG_CAP_SENTENCES}.
   1749      */
   1750     public static final int CAP_MODE_SENTENCES
   1751             = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
   1752 
   1753     /**
   1754      * Determine what caps mode should be in effect at the current offset in
   1755      * the text.  Only the mode bits set in <var>reqModes</var> will be
   1756      * checked.  Note that the caps mode flags here are explicitly defined
   1757      * to match those in {@link InputType}.
   1758      *
   1759      * @param cs The text that should be checked for caps modes.
   1760      * @param off Location in the text at which to check.
   1761      * @param reqModes The modes to be checked: may be any combination of
   1762      * {@link #CAP_MODE_CHARACTERS}, {@link #CAP_MODE_WORDS}, and
   1763      * {@link #CAP_MODE_SENTENCES}.
   1764      *
   1765      * @return Returns the actual capitalization modes that can be in effect
   1766      * at the current position, which is any combination of
   1767      * {@link #CAP_MODE_CHARACTERS}, {@link #CAP_MODE_WORDS}, and
   1768      * {@link #CAP_MODE_SENTENCES}.
   1769      */
   1770     public static int getCapsMode(CharSequence cs, int off, int reqModes) {
   1771         if (off < 0) {
   1772             return 0;
   1773         }
   1774 
   1775         int i;
   1776         char c;
   1777         int mode = 0;
   1778 
   1779         if ((reqModes&CAP_MODE_CHARACTERS) != 0) {
   1780             mode |= CAP_MODE_CHARACTERS;
   1781         }
   1782         if ((reqModes&(CAP_MODE_WORDS|CAP_MODE_SENTENCES)) == 0) {
   1783             return mode;
   1784         }
   1785 
   1786         // Back over allowed opening punctuation.
   1787 
   1788         for (i = off; i > 0; i--) {
   1789             c = cs.charAt(i - 1);
   1790 
   1791             if (c != '"' && c != '\'' &&
   1792                 Character.getType(c) != Character.START_PUNCTUATION) {
   1793                 break;
   1794             }
   1795         }
   1796 
   1797         // Start of paragraph, with optional whitespace.
   1798 
   1799         int j = i;
   1800         while (j > 0 && ((c = cs.charAt(j - 1)) == ' ' || c == '\t')) {
   1801             j--;
   1802         }
   1803         if (j == 0 || cs.charAt(j - 1) == '\n') {
   1804             return mode | CAP_MODE_WORDS;
   1805         }
   1806 
   1807         // Or start of word if we are that style.
   1808 
   1809         if ((reqModes&CAP_MODE_SENTENCES) == 0) {
   1810             if (i != j) mode |= CAP_MODE_WORDS;
   1811             return mode;
   1812         }
   1813 
   1814         // There must be a space if not the start of paragraph.
   1815 
   1816         if (i == j) {
   1817             return mode;
   1818         }
   1819 
   1820         // Back over allowed closing punctuation.
   1821 
   1822         for (; j > 0; j--) {
   1823             c = cs.charAt(j - 1);
   1824 
   1825             if (c != '"' && c != '\'' &&
   1826                 Character.getType(c) != Character.END_PUNCTUATION) {
   1827                 break;
   1828             }
   1829         }
   1830 
   1831         if (j > 0) {
   1832             c = cs.charAt(j - 1);
   1833 
   1834             if (c == '.' || c == '?' || c == '!') {
   1835                 // Do not capitalize if the word ends with a period but
   1836                 // also contains a period, in which case it is an abbreviation.
   1837 
   1838                 if (c == '.') {
   1839                     for (int k = j - 2; k >= 0; k--) {
   1840                         c = cs.charAt(k);
   1841 
   1842                         if (c == '.') {
   1843                             return mode;
   1844                         }
   1845 
   1846                         if (!Character.isLetter(c)) {
   1847                             break;
   1848                         }
   1849                     }
   1850                 }
   1851 
   1852                 return mode | CAP_MODE_SENTENCES;
   1853             }
   1854         }
   1855 
   1856         return mode;
   1857     }
   1858 
   1859     /**
   1860      * Does a comma-delimited list 'delimitedString' contain a certain item?
   1861      * (without allocating memory)
   1862      *
   1863      * @hide
   1864      */
   1865     public static boolean delimitedStringContains(
   1866             String delimitedString, char delimiter, String item) {
   1867         if (isEmpty(delimitedString) || isEmpty(item)) {
   1868             return false;
   1869         }
   1870         int pos = -1;
   1871         int length = delimitedString.length();
   1872         while ((pos = delimitedString.indexOf(item, pos + 1)) != -1) {
   1873             if (pos > 0 && delimitedString.charAt(pos - 1) != delimiter) {
   1874                 continue;
   1875             }
   1876             int expectedDelimiterPos = pos + item.length();
   1877             if (expectedDelimiterPos == length) {
   1878                 // Match at end of string.
   1879                 return true;
   1880             }
   1881             if (delimitedString.charAt(expectedDelimiterPos) == delimiter) {
   1882                 return true;
   1883             }
   1884         }
   1885         return false;
   1886     }
   1887 
   1888     /**
   1889      * Removes empty spans from the <code>spans</code> array.
   1890      *
   1891      * When parsing a Spanned using {@link Spanned#nextSpanTransition(int, int, Class)}, empty spans
   1892      * will (correctly) create span transitions, and calling getSpans on a slice of text bounded by
   1893      * one of these transitions will (correctly) include the empty overlapping span.
   1894      *
   1895      * However, these empty spans should not be taken into account when layouting or rendering the
   1896      * string and this method provides a way to filter getSpans' results accordingly.
   1897      *
   1898      * @param spans A list of spans retrieved using {@link Spanned#getSpans(int, int, Class)} from
   1899      * the <code>spanned</code>
   1900      * @param spanned The Spanned from which spans were extracted
   1901      * @return A subset of spans where empty spans ({@link Spanned#getSpanStart(Object)}  ==
   1902      * {@link Spanned#getSpanEnd(Object)} have been removed. The initial order is preserved
   1903      * @hide
   1904      */
   1905     @SuppressWarnings("unchecked")
   1906     public static <T> T[] removeEmptySpans(T[] spans, Spanned spanned, Class<T> klass) {
   1907         T[] copy = null;
   1908         int count = 0;
   1909 
   1910         for (int i = 0; i < spans.length; i++) {
   1911             final T span = spans[i];
   1912             final int start = spanned.getSpanStart(span);
   1913             final int end = spanned.getSpanEnd(span);
   1914 
   1915             if (start == end) {
   1916                 if (copy == null) {
   1917                     copy = (T[]) Array.newInstance(klass, spans.length - 1);
   1918                     System.arraycopy(spans, 0, copy, 0, i);
   1919                     count = i;
   1920                 }
   1921             } else {
   1922                 if (copy != null) {
   1923                     copy[count] = span;
   1924                     count++;
   1925                 }
   1926             }
   1927         }
   1928 
   1929         if (copy != null) {
   1930             T[] result = (T[]) Array.newInstance(klass, count);
   1931             System.arraycopy(copy, 0, result, 0, count);
   1932             return result;
   1933         } else {
   1934             return spans;
   1935         }
   1936     }
   1937 
   1938     /**
   1939      * Pack 2 int values into a long, useful as a return value for a range
   1940      * @see #unpackRangeStartFromLong(long)
   1941      * @see #unpackRangeEndFromLong(long)
   1942      * @hide
   1943      */
   1944     public static long packRangeInLong(int start, int end) {
   1945         return (((long) start) << 32) | end;
   1946     }
   1947 
   1948     /**
   1949      * Get the start value from a range packed in a long by {@link #packRangeInLong(int, int)}
   1950      * @see #unpackRangeEndFromLong(long)
   1951      * @see #packRangeInLong(int, int)
   1952      * @hide
   1953      */
   1954     public static int unpackRangeStartFromLong(long range) {
   1955         return (int) (range >>> 32);
   1956     }
   1957 
   1958     /**
   1959      * Get the end value from a range packed in a long by {@link #packRangeInLong(int, int)}
   1960      * @see #unpackRangeStartFromLong(long)
   1961      * @see #packRangeInLong(int, int)
   1962      * @hide
   1963      */
   1964     public static int unpackRangeEndFromLong(long range) {
   1965         return (int) (range & 0x00000000FFFFFFFFL);
   1966     }
   1967 
   1968     /**
   1969      * Return the layout direction for a given Locale
   1970      *
   1971      * @param locale the Locale for which we want the layout direction. Can be null.
   1972      * @return the layout direction. This may be one of:
   1973      * {@link android.view.View#LAYOUT_DIRECTION_LTR} or
   1974      * {@link android.view.View#LAYOUT_DIRECTION_RTL}.
   1975      *
   1976      * Be careful: this code will need to be updated when vertical scripts will be supported
   1977      */
   1978     public static int getLayoutDirectionFromLocale(Locale locale) {
   1979         return ((locale != null && !locale.equals(Locale.ROOT)
   1980                         && ULocale.forLocale(locale).isRightToLeft())
   1981                 // If forcing into RTL layout mode, return RTL as default
   1982                 || SystemProperties.getBoolean(Settings.Global.DEVELOPMENT_FORCE_RTL, false))
   1983             ? View.LAYOUT_DIRECTION_RTL
   1984             : View.LAYOUT_DIRECTION_LTR;
   1985     }
   1986 
   1987     /**
   1988      * Return localized string representing the given number of selected items.
   1989      *
   1990      * @hide
   1991      */
   1992     public static CharSequence formatSelectedCount(int count) {
   1993         return Resources.getSystem().getQuantityString(R.plurals.selected_count, count, count);
   1994     }
   1995 
   1996     /**
   1997      * Returns whether or not the specified spanned text has a style span.
   1998      * @hide
   1999      */
   2000     public static boolean hasStyleSpan(@NonNull Spanned spanned) {
   2001         Preconditions.checkArgument(spanned != null);
   2002         final Class<?>[] styleClasses = {
   2003                 CharacterStyle.class, ParagraphStyle.class, UpdateAppearance.class};
   2004         for (Class<?> clazz : styleClasses) {
   2005             if (spanned.nextSpanTransition(-1, spanned.length(), clazz) < spanned.length()) {
   2006                 return true;
   2007             }
   2008         }
   2009         return false;
   2010     }
   2011 
   2012     /**
   2013      * If the {@code charSequence} is instance of {@link Spanned}, creates a new copy and
   2014      * {@link NoCopySpan}'s are removed from the copy. Otherwise the given {@code charSequence} is
   2015      * returned as it is.
   2016      *
   2017      * @hide
   2018      */
   2019     @Nullable
   2020     public static CharSequence trimNoCopySpans(@Nullable CharSequence charSequence) {
   2021         if (charSequence != null && charSequence instanceof Spanned) {
   2022             // SpannableStringBuilder copy constructor trims NoCopySpans.
   2023             return new SpannableStringBuilder(charSequence);
   2024         }
   2025         return charSequence;
   2026     }
   2027 
   2028     /**
   2029      * Prepends {@code start} and appends {@code end} to a given {@link StringBuilder}
   2030      *
   2031      * @hide
   2032      */
   2033     public static void wrap(StringBuilder builder, String start, String end) {
   2034         builder.insert(0, start);
   2035         builder.append(end);
   2036     }
   2037 
   2038     /**
   2039      * Intent size limitations prevent sending over a megabyte of data. Limit
   2040      * text length to 100K characters - 200KB.
   2041      */
   2042     private static final int PARCEL_SAFE_TEXT_LENGTH = 100000;
   2043 
   2044     /**
   2045      * Trims the text to {@link #PARCEL_SAFE_TEXT_LENGTH} length. Returns the string as it is if
   2046      * the length() is smaller than {@link #PARCEL_SAFE_TEXT_LENGTH}. Used for text that is parceled
   2047      * into a {@link Parcelable}.
   2048      *
   2049      * @hide
   2050      */
   2051     @Nullable
   2052     public static <T extends CharSequence> T trimToParcelableSize(@Nullable T text) {
   2053         return trimToSize(text, PARCEL_SAFE_TEXT_LENGTH);
   2054     }
   2055 
   2056     /**
   2057      * Trims the text to {@code size} length. Returns the string as it is if the length() is
   2058      * smaller than {@code size}. If chars at {@code size-1} and {@code size} is a surrogate
   2059      * pair, returns a CharSequence of length {@code size-1}.
   2060      *
   2061      * @param size length of the result, should be greater than 0
   2062      *
   2063      * @hide
   2064      */
   2065     @Nullable
   2066     public static <T extends CharSequence> T trimToSize(@Nullable T text,
   2067             @IntRange(from = 1) int size) {
   2068         Preconditions.checkArgument(size > 0);
   2069         if (TextUtils.isEmpty(text) || text.length() <= size) return text;
   2070         if (Character.isHighSurrogate(text.charAt(size - 1))
   2071                 && Character.isLowSurrogate(text.charAt(size))) {
   2072             size = size - 1;
   2073         }
   2074         return (T) text.subSequence(0, size);
   2075     }
   2076 
   2077     private static Object sLock = new Object();
   2078 
   2079     private static char[] sTemp = null;
   2080 
   2081     private static String[] EMPTY_STRING_ARRAY = new String[]{};
   2082 }
   2083