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