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