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