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