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