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