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