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     /* package */ static final char[] ELLIPSIS_NORMAL = { '\u2026' }; // this is "..."
     68     private static final String ELLIPSIS_STRING = new String(ELLIPSIS_NORMAL);
     69 
     70     /* package */ static final char[] ELLIPSIS_TWO_DOTS = { '\u2025' }; // this is ".."
     71     private static final String ELLIPSIS_TWO_DOTS_STRING = new String(ELLIPSIS_TWO_DOTS);
     72 
     73     private TextUtils() { /* cannot be instantiated */ }
     74 
     75     public static void getChars(CharSequence s, int start, int end,
     76                                 char[] dest, int destoff) {
     77         Class<? extends CharSequence> c = s.getClass();
     78 
     79         if (c == String.class)
     80             ((String) s).getChars(start, end, dest, destoff);
     81         else if (c == StringBuffer.class)
     82             ((StringBuffer) s).getChars(start, end, dest, destoff);
     83         else if (c == StringBuilder.class)
     84             ((StringBuilder) s).getChars(start, end, dest, destoff);
     85         else if (s instanceof GetChars)
     86             ((GetChars) s).getChars(start, end, dest, destoff);
     87         else {
     88             for (int i = start; i < end; i++)
     89                 dest[destoff++] = s.charAt(i);
     90         }
     91     }
     92 
     93     public static int indexOf(CharSequence s, char ch) {
     94         return indexOf(s, ch, 0);
     95     }
     96 
     97     public static int indexOf(CharSequence s, char ch, int start) {
     98         Class<? extends CharSequence> c = s.getClass();
     99 
    100         if (c == String.class)
    101             return ((String) s).indexOf(ch, start);
    102 
    103         return indexOf(s, ch, start, s.length());
    104     }
    105 
    106     public static int indexOf(CharSequence s, char ch, int start, int end) {
    107         Class<? extends CharSequence> c = s.getClass();
    108 
    109         if (s instanceof GetChars || c == StringBuffer.class ||
    110             c == StringBuilder.class || c == String.class) {
    111             final int INDEX_INCREMENT = 500;
    112             char[] temp = obtain(INDEX_INCREMENT);
    113 
    114             while (start < end) {
    115                 int segend = start + INDEX_INCREMENT;
    116                 if (segend > end)
    117                     segend = end;
    118 
    119                 getChars(s, start, segend, temp, 0);
    120 
    121                 int count = segend - start;
    122                 for (int i = 0; i < count; i++) {
    123                     if (temp[i] == ch) {
    124                         recycle(temp);
    125                         return i + start;
    126                     }
    127                 }
    128 
    129                 start = segend;
    130             }
    131 
    132             recycle(temp);
    133             return -1;
    134         }
    135 
    136         for (int i = start; i < end; i++)
    137             if (s.charAt(i) == ch)
    138                 return i;
    139 
    140         return -1;
    141     }
    142 
    143     public static int lastIndexOf(CharSequence s, char ch) {
    144         return lastIndexOf(s, ch, s.length() - 1);
    145     }
    146 
    147     public static int lastIndexOf(CharSequence s, char ch, int last) {
    148         Class<? extends CharSequence> c = s.getClass();
    149 
    150         if (c == String.class)
    151             return ((String) s).lastIndexOf(ch, last);
    152 
    153         return lastIndexOf(s, ch, 0, last);
    154     }
    155 
    156     public static int lastIndexOf(CharSequence s, char ch,
    157                                   int start, int last) {
    158         if (last < 0)
    159             return -1;
    160         if (last >= s.length())
    161             last = s.length() - 1;
    162 
    163         int end = last + 1;
    164 
    165         Class<? extends CharSequence> c = s.getClass();
    166 
    167         if (s instanceof GetChars || c == StringBuffer.class ||
    168             c == StringBuilder.class || c == String.class) {
    169             final int INDEX_INCREMENT = 500;
    170             char[] temp = obtain(INDEX_INCREMENT);
    171 
    172             while (start < end) {
    173                 int segstart = end - INDEX_INCREMENT;
    174                 if (segstart < start)
    175                     segstart = start;
    176 
    177                 getChars(s, segstart, end, temp, 0);
    178 
    179                 int count = end - segstart;
    180                 for (int i = count - 1; i >= 0; i--) {
    181                     if (temp[i] == ch) {
    182                         recycle(temp);
    183                         return i + segstart;
    184                     }
    185                 }
    186 
    187                 end = segstart;
    188             }
    189 
    190             recycle(temp);
    191             return -1;
    192         }
    193 
    194         for (int i = end - 1; i >= start; i--)
    195             if (s.charAt(i) == ch)
    196                 return i;
    197 
    198         return -1;
    199     }
    200 
    201     public static int indexOf(CharSequence s, CharSequence needle) {
    202         return indexOf(s, needle, 0, s.length());
    203     }
    204 
    205     public static int indexOf(CharSequence s, CharSequence needle, int start) {
    206         return indexOf(s, needle, start, s.length());
    207     }
    208 
    209     public static int indexOf(CharSequence s, CharSequence needle,
    210                               int start, int end) {
    211         int nlen = needle.length();
    212         if (nlen == 0)
    213             return start;
    214 
    215         char c = needle.charAt(0);
    216 
    217         for (;;) {
    218             start = indexOf(s, c, start);
    219             if (start > end - nlen) {
    220                 break;
    221             }
    222 
    223             if (start < 0) {
    224                 return -1;
    225             }
    226 
    227             if (regionMatches(s, start, needle, 0, nlen)) {
    228                 return start;
    229             }
    230 
    231             start++;
    232         }
    233         return -1;
    234     }
    235 
    236     public static boolean regionMatches(CharSequence one, int toffset,
    237                                         CharSequence two, int ooffset,
    238                                         int len) {
    239         int tempLen = 2 * len;
    240         if (tempLen < len) {
    241             // Integer overflow; len is unreasonably large
    242             throw new IndexOutOfBoundsException();
    243         }
    244         char[] temp = obtain(tempLen);
    245 
    246         getChars(one, toffset, toffset + len, temp, 0);
    247         getChars(two, ooffset, ooffset + len, temp, len);
    248 
    249         boolean match = true;
    250         for (int i = 0; i < len; i++) {
    251             if (temp[i] != temp[i + len]) {
    252                 match = false;
    253                 break;
    254             }
    255         }
    256 
    257         recycle(temp);
    258         return match;
    259     }
    260 
    261     /**
    262      * Create a new String object containing the given range of characters
    263      * from the source string.  This is different than simply calling
    264      * {@link CharSequence#subSequence(int, int) CharSequence.subSequence}
    265      * in that it does not preserve any style runs in the source sequence,
    266      * allowing a more efficient implementation.
    267      */
    268     public static String substring(CharSequence source, int start, int end) {
    269         if (source instanceof String)
    270             return ((String) source).substring(start, end);
    271         if (source instanceof StringBuilder)
    272             return ((StringBuilder) source).substring(start, end);
    273         if (source instanceof StringBuffer)
    274             return ((StringBuffer) source).substring(start, end);
    275 
    276         char[] temp = obtain(end - start);
    277         getChars(source, start, end, temp, 0);
    278         String ret = new String(temp, 0, end - start);
    279         recycle(temp);
    280 
    281         return ret;
    282     }
    283 
    284     /**
    285      * Returns list of multiple {@link CharSequence} joined into a single
    286      * {@link CharSequence} separated by localized delimiter such as ", ".
    287      *
    288      * @hide
    289      */
    290     public static CharSequence join(Iterable<CharSequence> list) {
    291         final CharSequence delimiter = Resources.getSystem().getText(R.string.list_delimeter);
    292         return join(delimiter, list);
    293     }
    294 
    295     /**
    296      * Returns a string containing the tokens joined by delimiters.
    297      * @param tokens an array objects to be joined. Strings will be formed from
    298      *     the objects by calling object.toString().
    299      */
    300     public static String join(CharSequence delimiter, Object[] tokens) {
    301         StringBuilder sb = new StringBuilder();
    302         boolean firstTime = true;
    303         for (Object token: tokens) {
    304             if (firstTime) {
    305                 firstTime = false;
    306             } else {
    307                 sb.append(delimiter);
    308             }
    309             sb.append(token);
    310         }
    311         return sb.toString();
    312     }
    313 
    314     /**
    315      * Returns a string containing the tokens joined by delimiters.
    316      * @param tokens an array objects to be joined. Strings will be formed from
    317      *     the objects by calling object.toString().
    318      */
    319     public static String join(CharSequence delimiter, Iterable tokens) {
    320         StringBuilder sb = new StringBuilder();
    321         boolean firstTime = true;
    322         for (Object token: tokens) {
    323             if (firstTime) {
    324                 firstTime = false;
    325             } else {
    326                 sb.append(delimiter);
    327             }
    328             sb.append(token);
    329         }
    330         return sb.toString();
    331     }
    332 
    333     /**
    334      * String.split() returns [''] when the string to be split is empty. This returns []. This does
    335      * not remove any empty strings from the result. For example split("a,", ","  ) returns {"a", ""}.
    336      *
    337      * @param text the string to split
    338      * @param expression the regular expression to match
    339      * @return an array of strings. The array will be empty if text is empty
    340      *
    341      * @throws NullPointerException if expression or text is null
    342      */
    343     public static String[] split(String text, String expression) {
    344         if (text.length() == 0) {
    345             return EMPTY_STRING_ARRAY;
    346         } else {
    347             return text.split(expression, -1);
    348         }
    349     }
    350 
    351     /**
    352      * Splits a string on a pattern. String.split() returns [''] when the string to be
    353      * split is empty. This returns []. This does not remove any empty strings from the result.
    354      * @param text the string to split
    355      * @param pattern the regular expression to match
    356      * @return an array of strings. The array will be empty if text is empty
    357      *
    358      * @throws NullPointerException if expression or text is null
    359      */
    360     public static String[] split(String text, Pattern pattern) {
    361         if (text.length() == 0) {
    362             return EMPTY_STRING_ARRAY;
    363         } else {
    364             return pattern.split(text, -1);
    365         }
    366     }
    367 
    368     /**
    369      * An interface for splitting strings according to rules that are opaque to the user of this
    370      * interface. This also has less overhead than split, which uses regular expressions and
    371      * allocates an array to hold the results.
    372      *
    373      * <p>The most efficient way to use this class is:
    374      *
    375      * <pre>
    376      * // Once
    377      * TextUtils.StringSplitter splitter = new TextUtils.SimpleStringSplitter(delimiter);
    378      *
    379      * // Once per string to split
    380      * splitter.setString(string);
    381      * for (String s : splitter) {
    382      *     ...
    383      * }
    384      * </pre>
    385      */
    386     public interface StringSplitter extends Iterable<String> {
    387         public void setString(String string);
    388     }
    389 
    390     /**
    391      * A simple string splitter.
    392      *
    393      * <p>If the final character in the string to split is the delimiter then no empty string will
    394      * be returned for the empty string after that delimeter. That is, splitting <tt>"a,b,"</tt> on
    395      * comma will return <tt>"a", "b"</tt>, not <tt>"a", "b", ""</tt>.
    396      */
    397     public static class SimpleStringSplitter implements StringSplitter, Iterator<String> {
    398         private String mString;
    399         private char mDelimiter;
    400         private int mPosition;
    401         private int mLength;
    402 
    403         /**
    404          * Initializes the splitter. setString may be called later.
    405          * @param delimiter the delimeter on which to split
    406          */
    407         public SimpleStringSplitter(char delimiter) {
    408             mDelimiter = delimiter;
    409         }
    410 
    411         /**
    412          * Sets the string to split
    413          * @param string the string to split
    414          */
    415         public void setString(String string) {
    416             mString = string;
    417             mPosition = 0;
    418             mLength = mString.length();
    419         }
    420 
    421         public Iterator<String> iterator() {
    422             return this;
    423         }
    424 
    425         public boolean hasNext() {
    426             return mPosition < mLength;
    427         }
    428 
    429         public String next() {
    430             int end = mString.indexOf(mDelimiter, mPosition);
    431             if (end == -1) {
    432                 end = mLength;
    433             }
    434             String nextString = mString.substring(mPosition, end);
    435             mPosition = end + 1; // Skip the delimiter.
    436             return nextString;
    437         }
    438 
    439         public void remove() {
    440             throw new UnsupportedOperationException();
    441         }
    442     }
    443 
    444     public static CharSequence stringOrSpannedString(CharSequence source) {
    445         if (source == null)
    446             return null;
    447         if (source instanceof SpannedString)
    448             return source;
    449         if (source instanceof Spanned)
    450             return new SpannedString(source);
    451 
    452         return source.toString();
    453     }
    454 
    455     /**
    456      * Returns true if the string is null or 0-length.
    457      * @param str the string to be examined
    458      * @return true if str is null or zero length
    459      */
    460     public static boolean isEmpty(CharSequence str) {
    461         if (str == null || str.length() == 0)
    462             return true;
    463         else
    464             return false;
    465     }
    466 
    467     /**
    468      * Returns the length that the specified CharSequence would have if
    469      * spaces and control characters were trimmed from the start and end,
    470      * as by {@link String#trim}.
    471      */
    472     public static int getTrimmedLength(CharSequence s) {
    473         int len = s.length();
    474 
    475         int start = 0;
    476         while (start < len && s.charAt(start) <= ' ') {
    477             start++;
    478         }
    479 
    480         int end = len;
    481         while (end > start && s.charAt(end - 1) <= ' ') {
    482             end--;
    483         }
    484 
    485         return end - start;
    486     }
    487 
    488     /**
    489      * Returns true if a and b are equal, including if they are both null.
    490      * <p><i>Note: In platform versions 1.1 and earlier, this method only worked well if
    491      * both the arguments were instances of String.</i></p>
    492      * @param a first CharSequence to check
    493      * @param b second CharSequence to check
    494      * @return true if a and b are equal
    495      */
    496     public static boolean equals(CharSequence a, CharSequence b) {
    497         if (a == b) return true;
    498         int length;
    499         if (a != null && b != null && (length = a.length()) == b.length()) {
    500             if (a instanceof String && b instanceof String) {
    501                 return a.equals(b);
    502             } else {
    503                 for (int i = 0; i < length; i++) {
    504                     if (a.charAt(i) != b.charAt(i)) return false;
    505                 }
    506                 return true;
    507             }
    508         }
    509         return false;
    510     }
    511 
    512     // XXX currently this only reverses chars, not spans
    513     public static CharSequence getReverse(CharSequence source,
    514                                           int start, int end) {
    515         return new Reverser(source, start, end);
    516     }
    517 
    518     private static class Reverser
    519     implements CharSequence, GetChars
    520     {
    521         public Reverser(CharSequence source, int start, int end) {
    522             mSource = source;
    523             mStart = start;
    524             mEnd = end;
    525         }
    526 
    527         public int length() {
    528             return mEnd - mStart;
    529         }
    530 
    531         public CharSequence subSequence(int start, int end) {
    532             char[] buf = new char[end - start];
    533 
    534             getChars(start, end, buf, 0);
    535             return new String(buf);
    536         }
    537 
    538         @Override
    539         public String toString() {
    540             return subSequence(0, length()).toString();
    541         }
    542 
    543         public char charAt(int off) {
    544             return AndroidCharacter.getMirror(mSource.charAt(mEnd - 1 - off));
    545         }
    546 
    547         public void getChars(int start, int end, char[] dest, int destoff) {
    548             TextUtils.getChars(mSource, start + mStart, end + mStart,
    549                                dest, destoff);
    550             AndroidCharacter.mirror(dest, 0, end - start);
    551 
    552             int len = end - start;
    553             int n = (end - start) / 2;
    554             for (int i = 0; i < n; i++) {
    555                 char tmp = dest[destoff + i];
    556 
    557                 dest[destoff + i] = dest[destoff + len - i - 1];
    558                 dest[destoff + len - i - 1] = tmp;
    559             }
    560         }
    561 
    562         private CharSequence mSource;
    563         private int mStart;
    564         private int mEnd;
    565     }
    566 
    567     /** @hide */
    568     public static final int ALIGNMENT_SPAN = 1;
    569     /** @hide */
    570     public static final int FIRST_SPAN = ALIGNMENT_SPAN;
    571     /** @hide */
    572     public static final int FOREGROUND_COLOR_SPAN = 2;
    573     /** @hide */
    574     public static final int RELATIVE_SIZE_SPAN = 3;
    575     /** @hide */
    576     public static final int SCALE_X_SPAN = 4;
    577     /** @hide */
    578     public static final int STRIKETHROUGH_SPAN = 5;
    579     /** @hide */
    580     public static final int UNDERLINE_SPAN = 6;
    581     /** @hide */
    582     public static final int STYLE_SPAN = 7;
    583     /** @hide */
    584     public static final int BULLET_SPAN = 8;
    585     /** @hide */
    586     public static final int QUOTE_SPAN = 9;
    587     /** @hide */
    588     public static final int LEADING_MARGIN_SPAN = 10;
    589     /** @hide */
    590     public static final int URL_SPAN = 11;
    591     /** @hide */
    592     public static final int BACKGROUND_COLOR_SPAN = 12;
    593     /** @hide */
    594     public static final int TYPEFACE_SPAN = 13;
    595     /** @hide */
    596     public static final int SUPERSCRIPT_SPAN = 14;
    597     /** @hide */
    598     public static final int SUBSCRIPT_SPAN = 15;
    599     /** @hide */
    600     public static final int ABSOLUTE_SIZE_SPAN = 16;
    601     /** @hide */
    602     public static final int TEXT_APPEARANCE_SPAN = 17;
    603     /** @hide */
    604     public static final int ANNOTATION = 18;
    605     /** @hide */
    606     public static final int SUGGESTION_SPAN = 19;
    607     /** @hide */
    608     public static final int SPELL_CHECK_SPAN = 20;
    609     /** @hide */
    610     public static final int SUGGESTION_RANGE_SPAN = 21;
    611     /** @hide */
    612     public static final int EASY_EDIT_SPAN = 22;
    613     /** @hide */
    614     public static final int LOCALE_SPAN = 23;
    615     /** @hide */
    616     public static final int TTS_SPAN = 24;
    617     /** @hide */
    618     public static final int LAST_SPAN = TTS_SPAN;
    619 
    620     /**
    621      * Flatten a CharSequence and whatever styles can be copied across processes
    622      * into the parcel.
    623      */
    624     public static void writeToParcel(CharSequence cs, Parcel p,
    625             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                     ParcelableSpan ps = (ParcelableSpan)prop;
    648                     int spanTypeId = ps.getSpanTypeId();
    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.writeToParcel(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);
   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