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