blob: f981e740dcfbbe3590c1881d84c4fc41fb01c4c9 [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
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
17package android.text;
18
Roozbeh Pournader3bfce332016-06-17 15:03:56 -070019import android.annotation.FloatRange;
20import android.annotation.NonNull;
Scott Kennedy6cd132f2015-02-19 10:36:12 -080021import android.annotation.Nullable;
Roozbeh Pournader3bfce332016-06-17 15:03:56 -070022import android.annotation.PluralsRes;
23import android.content.Context;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080024import android.content.res.Resources;
Roozbeh Pournader9559c202016-12-13 10:59:50 -080025import android.icu.lang.UCharacter;
Roozbeh Pournader205a9932017-06-08 00:23:42 -070026import android.icu.text.CaseMap;
27import android.icu.text.Edits;
Roozbeh Pournader463b4822015-08-06 16:04:45 -070028import android.icu.util.ULocale;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080029import android.os.Parcel;
30import android.os.Parcelable;
Amith Yamasanid8415f42013-08-07 20:15:10 -070031import android.os.SystemProperties;
32import android.provider.Settings;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080033import android.text.style.AbsoluteSizeSpan;
Phil Weaver193520e2016-12-13 09:39:06 -080034import android.text.style.AccessibilityClickableSpan;
35import android.text.style.AccessibilityURLSpan;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080036import android.text.style.AlignmentSpan;
37import android.text.style.BackgroundColorSpan;
38import android.text.style.BulletSpan;
39import android.text.style.CharacterStyle;
Luca Zanoline6d36822011-08-30 18:04:34 +010040import android.text.style.EasyEditSpan;
Gilles Debunne0eea6682011-08-29 13:30:31 -070041import android.text.style.ForegroundColorSpan;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080042import android.text.style.LeadingMarginSpan;
Victoria Leasedf8ef4b2012-08-17 15:34:01 -070043import android.text.style.LocaleSpan;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080044import android.text.style.MetricAffectingSpan;
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +010045import android.text.style.ParagraphStyle;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080046import android.text.style.QuoteSpan;
47import android.text.style.RelativeSizeSpan;
48import android.text.style.ReplacementSpan;
49import android.text.style.ScaleXSpan;
Gilles Debunne28294cc2011-08-24 12:02:05 -070050import android.text.style.SpellCheckSpan;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080051import android.text.style.StrikethroughSpan;
52import android.text.style.StyleSpan;
53import android.text.style.SubscriptSpan;
Gilles Debunne28294cc2011-08-24 12:02:05 -070054import android.text.style.SuggestionRangeSpan;
Gilles Debunnea00972a2011-04-13 16:07:31 -070055import android.text.style.SuggestionSpan;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080056import android.text.style.SuperscriptSpan;
57import android.text.style.TextAppearanceSpan;
Niels Egberts4f4ead42014-06-23 12:01:14 +010058import android.text.style.TtsSpan;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080059import android.text.style.TypefaceSpan;
60import android.text.style.URLSpan;
61import android.text.style.UnderlineSpan;
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +010062import android.text.style.UpdateAppearance;
Victoria Lease577ba532013-04-19 13:12:15 -070063import android.util.Log;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080064import android.util.Printer;
Fabrice Di Megliod3d9f3f2012-09-18 12:55:32 -070065import android.view.View;
Raph Levien8d2aa192014-05-14 15:46:47 -070066
Doug Feltcb3791202011-07-07 11:57:48 -070067import com.android.internal.R;
68import com.android.internal.util.ArrayUtils;
Eugene Susla6ed45d82017-01-22 13:52:51 -080069import com.android.internal.util.Preconditions;
Raph Levien8d2aa192014-05-14 15:46:47 -070070
Gilles Debunne1e3ac182011-03-08 14:22:34 -080071import java.lang.reflect.Array;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080072import java.util.Iterator;
Roozbeh Pournader3bfce332016-06-17 15:03:56 -070073import java.util.List;
Fabrice Di Megliod3d9f3f2012-09-18 12:55:32 -070074import java.util.Locale;
Doug Felte8e45f22010-03-29 14:58:40 -070075import java.util.regex.Pattern;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080076
77public class TextUtils {
Victoria Lease577ba532013-04-19 13:12:15 -070078 private static final String TAG = "TextUtils";
79
Neil Fullerd29bdb22015-02-06 10:03:08 +000080 /* package */ static final char[] ELLIPSIS_NORMAL = { '\u2026' }; // this is "..."
Roozbeh Pournader0ad4dcc2016-05-26 18:51:59 -070081 /** {@hide} */
82 public static final String ELLIPSIS_STRING = new String(ELLIPSIS_NORMAL);
Neil Fullerd29bdb22015-02-06 10:03:08 +000083
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);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080086
Fabrice Di Megliocb332642011-09-23 19:08:04 -070087 private TextUtils() { /* cannot be instantiated */ }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080088
89 public static void getChars(CharSequence s, int start, int end,
90 char[] dest, int destoff) {
Gilles Debunne1e3ac182011-03-08 14:22:34 -080091 Class<? extends CharSequence> c = s.getClass();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080092
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
107 public static int indexOf(CharSequence s, char ch) {
108 return indexOf(s, ch, 0);
109 }
110
111 public static int indexOf(CharSequence s, char ch, int start) {
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800112 Class<? extends CharSequence> c = s.getClass();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800113
114 if (c == String.class)
115 return ((String) s).indexOf(ch, start);
116
117 return indexOf(s, ch, start, s.length());
118 }
119
120 public static int indexOf(CharSequence s, char ch, int start, int end) {
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800121 Class<? extends CharSequence> c = s.getClass();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800122
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
157 public static int lastIndexOf(CharSequence s, char ch) {
158 return lastIndexOf(s, ch, s.length() - 1);
159 }
160
161 public static int lastIndexOf(CharSequence s, char ch, int last) {
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800162 Class<? extends CharSequence> c = s.getClass();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800163
164 if (c == String.class)
165 return ((String) s).lastIndexOf(ch, last);
166
167 return lastIndexOf(s, ch, 0, last);
168 }
169
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
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800179 Class<? extends CharSequence> c = s.getClass();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800180
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
215 public static int indexOf(CharSequence s, CharSequence needle) {
216 return indexOf(s, needle, 0, s.length());
217 }
218
219 public static int indexOf(CharSequence s, CharSequence needle, int start) {
220 return indexOf(s, needle, start, s.length());
221 }
222
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
250 public static boolean regionMatches(CharSequence one, int toffset,
251 CharSequence two, int ooffset,
252 int len) {
Raph Levien8d2aa192014-05-14 15:46:47 -0700253 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);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800259
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 */
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.
Roozbeh Pournader42673c32017-07-20 15:23:33 -0700300 *
301 * @param delimiter a CharSequence that will be inserted between the tokens. If null, the string
302 * "null" will be used as the delimiter.
303 * @param tokens an array objects to be joined. Strings will be formed from the objects by
304 * calling object.toString(). If tokens is null, a NullPointerException will be thrown. If
305 * tokens is an empty array, an empty string will be returned.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800306 */
Roozbeh Pournader42673c32017-07-20 15:23:33 -0700307 public static String join(@NonNull CharSequence delimiter, @NonNull Object[] tokens) {
308 final int length = tokens.length;
309 if (length == 0) {
310 return "";
311 }
312 final StringBuilder sb = new StringBuilder();
313 sb.append(tokens[0]);
314 for (int i = 1; i < length; i++) {
315 sb.append(delimiter);
316 sb.append(tokens[i]);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800317 }
318 return sb.toString();
319 }
320
321 /**
322 * Returns a string containing the tokens joined by delimiters.
Roozbeh Pournader42673c32017-07-20 15:23:33 -0700323 *
324 * @param delimiter a CharSequence that will be inserted between the tokens. If null, the string
325 * "null" will be used as the delimiter.
326 * @param tokens an array objects to be joined. Strings will be formed from the objects by
327 * calling object.toString(). If tokens is null, a NullPointerException will be thrown. If
328 * tokens is empty, an empty string will be returned.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800329 */
Roozbeh Pournader42673c32017-07-20 15:23:33 -0700330 public static String join(@NonNull CharSequence delimiter, @NonNull Iterable tokens) {
331 final Iterator<?> it = tokens.iterator();
332 if (!it.hasNext()) {
333 return "";
334 }
335 final StringBuilder sb = new StringBuilder();
336 sb.append(it.next());
337 while (it.hasNext()) {
338 sb.append(delimiter);
Andreas Gampea8a58ff2016-05-18 11:58:39 -0700339 sb.append(it.next());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800340 }
341 return sb.toString();
342 }
343
344 /**
345 * String.split() returns [''] when the string to be split is empty. This returns []. This does
346 * not remove any empty strings from the result. For example split("a,", "," ) returns {"a", ""}.
347 *
348 * @param text the string to split
349 * @param expression the regular expression to match
350 * @return an array of strings. The array will be empty if text is empty
351 *
352 * @throws NullPointerException if expression or text is null
353 */
354 public static String[] split(String text, String expression) {
355 if (text.length() == 0) {
356 return EMPTY_STRING_ARRAY;
357 } else {
358 return text.split(expression, -1);
359 }
360 }
361
362 /**
363 * Splits a string on a pattern. String.split() returns [''] when the string to be
364 * split is empty. This returns []. This does not remove any empty strings from the result.
365 * @param text the string to split
366 * @param pattern the regular expression to match
367 * @return an array of strings. The array will be empty if text is empty
368 *
369 * @throws NullPointerException if expression or text is null
370 */
371 public static String[] split(String text, Pattern pattern) {
372 if (text.length() == 0) {
373 return EMPTY_STRING_ARRAY;
374 } else {
375 return pattern.split(text, -1);
376 }
377 }
378
379 /**
380 * An interface for splitting strings according to rules that are opaque to the user of this
381 * interface. This also has less overhead than split, which uses regular expressions and
382 * allocates an array to hold the results.
383 *
384 * <p>The most efficient way to use this class is:
385 *
386 * <pre>
387 * // Once
388 * TextUtils.StringSplitter splitter = new TextUtils.SimpleStringSplitter(delimiter);
389 *
390 * // Once per string to split
391 * splitter.setString(string);
392 * for (String s : splitter) {
393 * ...
394 * }
395 * </pre>
396 */
397 public interface StringSplitter extends Iterable<String> {
398 public void setString(String string);
399 }
400
401 /**
402 * A simple string splitter.
403 *
404 * <p>If the final character in the string to split is the delimiter then no empty string will
405 * be returned for the empty string after that delimeter. That is, splitting <tt>"a,b,"</tt> on
406 * comma will return <tt>"a", "b"</tt>, not <tt>"a", "b", ""</tt>.
407 */
408 public static class SimpleStringSplitter implements StringSplitter, Iterator<String> {
409 private String mString;
410 private char mDelimiter;
411 private int mPosition;
412 private int mLength;
413
414 /**
415 * Initializes the splitter. setString may be called later.
416 * @param delimiter the delimeter on which to split
417 */
418 public SimpleStringSplitter(char delimiter) {
419 mDelimiter = delimiter;
420 }
421
422 /**
423 * Sets the string to split
424 * @param string the string to split
425 */
426 public void setString(String string) {
427 mString = string;
428 mPosition = 0;
429 mLength = mString.length();
430 }
431
432 public Iterator<String> iterator() {
433 return this;
434 }
435
436 public boolean hasNext() {
437 return mPosition < mLength;
438 }
439
440 public String next() {
441 int end = mString.indexOf(mDelimiter, mPosition);
442 if (end == -1) {
443 end = mLength;
444 }
445 String nextString = mString.substring(mPosition, end);
446 mPosition = end + 1; // Skip the delimiter.
447 return nextString;
448 }
449
450 public void remove() {
451 throw new UnsupportedOperationException();
452 }
453 }
454
455 public static CharSequence stringOrSpannedString(CharSequence source) {
456 if (source == null)
457 return null;
458 if (source instanceof SpannedString)
459 return source;
460 if (source instanceof Spanned)
461 return new SpannedString(source);
462
463 return source.toString();
464 }
465
466 /**
467 * Returns true if the string is null or 0-length.
468 * @param str the string to be examined
469 * @return true if str is null or zero length
470 */
Scott Kennedy6cd132f2015-02-19 10:36:12 -0800471 public static boolean isEmpty(@Nullable CharSequence str) {
Amin Shaikhd4196c92017-02-06 17:04:47 -0800472 return str == null || str.length() == 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800473 }
474
Jeff Sharkey5cc0df22015-06-17 19:44:05 -0700475 /** {@hide} */
476 public static String nullIfEmpty(@Nullable String str) {
477 return isEmpty(str) ? null : str;
478 }
479
Eugene Susla6ed45d82017-01-22 13:52:51 -0800480 /** {@hide} */
481 public static String emptyIfNull(@Nullable String str) {
482 return str == null ? "" : str;
483 }
484
485 /** {@hide} */
486 public static String firstNotEmpty(@Nullable String a, @NonNull String b) {
487 return !isEmpty(a) ? a : Preconditions.checkStringNotEmpty(b);
488 }
489
Eugene Susla36e866b2017-02-23 18:24:39 -0800490 /** {@hide} */
491 public static int length(@Nullable String s) {
492 return isEmpty(s) ? 0 : s.length();
493 }
494
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800495 /**
496 * Returns the length that the specified CharSequence would have if
Roozbeh Pournader3efda952015-08-11 09:55:57 -0700497 * spaces and ASCII control characters were trimmed from the start and end,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800498 * as by {@link String#trim}.
499 */
500 public static int getTrimmedLength(CharSequence s) {
501 int len = s.length();
502
503 int start = 0;
504 while (start < len && s.charAt(start) <= ' ') {
505 start++;
506 }
507
508 int end = len;
509 while (end > start && s.charAt(end - 1) <= ' ') {
510 end--;
511 }
512
513 return end - start;
514 }
515
516 /**
517 * Returns true if a and b are equal, including if they are both null.
518 * <p><i>Note: In platform versions 1.1 and earlier, this method only worked well if
519 * both the arguments were instances of String.</i></p>
520 * @param a first CharSequence to check
521 * @param b second CharSequence to check
522 * @return true if a and b are equal
523 */
524 public static boolean equals(CharSequence a, CharSequence b) {
525 if (a == b) return true;
526 int length;
527 if (a != null && b != null && (length = a.length()) == b.length()) {
528 if (a instanceof String && b instanceof String) {
529 return a.equals(b);
530 } else {
531 for (int i = 0; i < length; i++) {
532 if (a.charAt(i) != b.charAt(i)) return false;
533 }
534 return true;
535 }
536 }
537 return false;
538 }
539
Clara Bayarrid608a0a2016-04-27 11:53:22 +0100540 /**
541 * This function only reverses individual {@code char}s and not their associated
542 * spans. It doesn't support surrogate pairs (that correspond to non-BMP code points), combining
543 * sequences or conjuncts either.
544 * @deprecated Do not use.
Roozbeh Pournader3efda952015-08-11 09:55:57 -0700545 */
546 @Deprecated
Clara Bayarrid608a0a2016-04-27 11:53:22 +0100547 public static CharSequence getReverse(CharSequence source, int start, int end) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800548 return new Reverser(source, start, end);
549 }
550
551 private static class Reverser
552 implements CharSequence, GetChars
553 {
554 public Reverser(CharSequence source, int start, int end) {
555 mSource = source;
556 mStart = start;
557 mEnd = end;
558 }
559
560 public int length() {
561 return mEnd - mStart;
562 }
563
564 public CharSequence subSequence(int start, int end) {
565 char[] buf = new char[end - start];
566
567 getChars(start, end, buf, 0);
568 return new String(buf);
569 }
570
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800571 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800572 public String toString() {
573 return subSequence(0, length()).toString();
574 }
575
576 public char charAt(int off) {
Roozbeh Pournader9559c202016-12-13 10:59:50 -0800577 return (char) UCharacter.getMirror(mSource.charAt(mEnd - 1 - off));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800578 }
579
Roozbeh Pournader9559c202016-12-13 10:59:50 -0800580 @SuppressWarnings("deprecation")
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800581 public void getChars(int start, int end, char[] dest, int destoff) {
582 TextUtils.getChars(mSource, start + mStart, end + mStart,
583 dest, destoff);
584 AndroidCharacter.mirror(dest, 0, end - start);
585
586 int len = end - start;
587 int n = (end - start) / 2;
588 for (int i = 0; i < n; i++) {
589 char tmp = dest[destoff + i];
590
591 dest[destoff + i] = dest[destoff + len - i - 1];
592 dest[destoff + len - i - 1] = tmp;
593 }
594 }
595
596 private CharSequence mSource;
597 private int mStart;
598 private int mEnd;
599 }
600
601 /** @hide */
602 public static final int ALIGNMENT_SPAN = 1;
603 /** @hide */
Victoria Lease577ba532013-04-19 13:12:15 -0700604 public static final int FIRST_SPAN = ALIGNMENT_SPAN;
605 /** @hide */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800606 public static final int FOREGROUND_COLOR_SPAN = 2;
607 /** @hide */
608 public static final int RELATIVE_SIZE_SPAN = 3;
609 /** @hide */
610 public static final int SCALE_X_SPAN = 4;
611 /** @hide */
612 public static final int STRIKETHROUGH_SPAN = 5;
613 /** @hide */
614 public static final int UNDERLINE_SPAN = 6;
615 /** @hide */
616 public static final int STYLE_SPAN = 7;
617 /** @hide */
618 public static final int BULLET_SPAN = 8;
619 /** @hide */
620 public static final int QUOTE_SPAN = 9;
621 /** @hide */
622 public static final int LEADING_MARGIN_SPAN = 10;
623 /** @hide */
624 public static final int URL_SPAN = 11;
625 /** @hide */
626 public static final int BACKGROUND_COLOR_SPAN = 12;
627 /** @hide */
628 public static final int TYPEFACE_SPAN = 13;
629 /** @hide */
630 public static final int SUPERSCRIPT_SPAN = 14;
631 /** @hide */
632 public static final int SUBSCRIPT_SPAN = 15;
633 /** @hide */
634 public static final int ABSOLUTE_SIZE_SPAN = 16;
635 /** @hide */
636 public static final int TEXT_APPEARANCE_SPAN = 17;
637 /** @hide */
638 public static final int ANNOTATION = 18;
satokadb43582011-03-09 10:08:47 +0900639 /** @hide */
Gilles Debunnea00972a2011-04-13 16:07:31 -0700640 public static final int SUGGESTION_SPAN = 19;
Gilles Debunne28294cc2011-08-24 12:02:05 -0700641 /** @hide */
642 public static final int SPELL_CHECK_SPAN = 20;
643 /** @hide */
644 public static final int SUGGESTION_RANGE_SPAN = 21;
Luca Zanoline6d36822011-08-30 18:04:34 +0100645 /** @hide */
646 public static final int EASY_EDIT_SPAN = 22;
Victoria Leasedf8ef4b2012-08-17 15:34:01 -0700647 /** @hide */
648 public static final int LOCALE_SPAN = 23;
Victoria Lease577ba532013-04-19 13:12:15 -0700649 /** @hide */
Niels Egberts4f4ead42014-06-23 12:01:14 +0100650 public static final int TTS_SPAN = 24;
651 /** @hide */
Phil Weaver193520e2016-12-13 09:39:06 -0800652 public static final int ACCESSIBILITY_CLICKABLE_SPAN = 25;
653 /** @hide */
654 public static final int ACCESSIBILITY_URL_SPAN = 26;
655 /** @hide */
656 public static final int LAST_SPAN = ACCESSIBILITY_URL_SPAN;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800657
658 /**
659 * Flatten a CharSequence and whatever styles can be copied across processes
660 * into the parcel.
661 */
Alan Viverettea70d4a92015-06-02 16:11:00 -0700662 public static void writeToParcel(CharSequence cs, Parcel p, int parcelableFlags) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800663 if (cs instanceof Spanned) {
664 p.writeInt(0);
665 p.writeString(cs.toString());
666
667 Spanned sp = (Spanned) cs;
668 Object[] os = sp.getSpans(0, cs.length(), Object.class);
669
670 // note to people adding to this: check more specific types
671 // before more generic types. also notice that it uses
672 // "if" instead of "else if" where there are interfaces
673 // so one object can be several.
674
675 for (int i = 0; i < os.length; i++) {
676 Object o = os[i];
677 Object prop = os[i];
678
679 if (prop instanceof CharacterStyle) {
680 prop = ((CharacterStyle) prop).getUnderlying();
681 }
682
683 if (prop instanceof ParcelableSpan) {
Alan Viverettea70d4a92015-06-02 16:11:00 -0700684 final ParcelableSpan ps = (ParcelableSpan) prop;
685 final int spanTypeId = ps.getSpanTypeIdInternal();
Victoria Lease577ba532013-04-19 13:12:15 -0700686 if (spanTypeId < FIRST_SPAN || spanTypeId > LAST_SPAN) {
Alan Viverettea70d4a92015-06-02 16:11:00 -0700687 Log.e(TAG, "External class \"" + ps.getClass().getSimpleName()
Victoria Lease577ba532013-04-19 13:12:15 -0700688 + "\" is attempting to use the frameworks-only ParcelableSpan"
689 + " interface");
690 } else {
691 p.writeInt(spanTypeId);
Alan Viverettea70d4a92015-06-02 16:11:00 -0700692 ps.writeToParcelInternal(p, parcelableFlags);
Victoria Lease577ba532013-04-19 13:12:15 -0700693 writeWhere(p, sp, o);
694 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800695 }
696 }
697
698 p.writeInt(0);
699 } else {
700 p.writeInt(1);
701 if (cs != null) {
702 p.writeString(cs.toString());
703 } else {
704 p.writeString(null);
705 }
706 }
707 }
708
709 private static void writeWhere(Parcel p, Spanned sp, Object o) {
710 p.writeInt(sp.getSpanStart(o));
711 p.writeInt(sp.getSpanEnd(o));
712 p.writeInt(sp.getSpanFlags(o));
713 }
714
715 public static final Parcelable.Creator<CharSequence> CHAR_SEQUENCE_CREATOR
716 = new Parcelable.Creator<CharSequence>() {
717 /**
718 * Read and return a new CharSequence, possibly with styles,
719 * from the parcel.
720 */
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800721 public CharSequence createFromParcel(Parcel p) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800722 int kind = p.readInt();
723
Martin Wallgrencee20512011-04-07 14:45:43 +0200724 String string = p.readString();
725 if (string == null) {
726 return null;
727 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800728
Martin Wallgrencee20512011-04-07 14:45:43 +0200729 if (kind == 1) {
730 return string;
731 }
732
733 SpannableString sp = new SpannableString(string);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800734
735 while (true) {
736 kind = p.readInt();
737
738 if (kind == 0)
739 break;
740
741 switch (kind) {
742 case ALIGNMENT_SPAN:
743 readSpan(p, sp, new AlignmentSpan.Standard(p));
744 break;
745
746 case FOREGROUND_COLOR_SPAN:
747 readSpan(p, sp, new ForegroundColorSpan(p));
748 break;
749
750 case RELATIVE_SIZE_SPAN:
751 readSpan(p, sp, new RelativeSizeSpan(p));
752 break;
753
754 case SCALE_X_SPAN:
755 readSpan(p, sp, new ScaleXSpan(p));
756 break;
757
758 case STRIKETHROUGH_SPAN:
759 readSpan(p, sp, new StrikethroughSpan(p));
760 break;
761
762 case UNDERLINE_SPAN:
763 readSpan(p, sp, new UnderlineSpan(p));
764 break;
765
766 case STYLE_SPAN:
767 readSpan(p, sp, new StyleSpan(p));
768 break;
769
770 case BULLET_SPAN:
771 readSpan(p, sp, new BulletSpan(p));
772 break;
773
774 case QUOTE_SPAN:
775 readSpan(p, sp, new QuoteSpan(p));
776 break;
777
778 case LEADING_MARGIN_SPAN:
779 readSpan(p, sp, new LeadingMarginSpan.Standard(p));
780 break;
781
782 case URL_SPAN:
783 readSpan(p, sp, new URLSpan(p));
784 break;
785
786 case BACKGROUND_COLOR_SPAN:
787 readSpan(p, sp, new BackgroundColorSpan(p));
788 break;
789
790 case TYPEFACE_SPAN:
791 readSpan(p, sp, new TypefaceSpan(p));
792 break;
793
794 case SUPERSCRIPT_SPAN:
795 readSpan(p, sp, new SuperscriptSpan(p));
796 break;
797
798 case SUBSCRIPT_SPAN:
799 readSpan(p, sp, new SubscriptSpan(p));
800 break;
801
802 case ABSOLUTE_SIZE_SPAN:
803 readSpan(p, sp, new AbsoluteSizeSpan(p));
804 break;
805
806 case TEXT_APPEARANCE_SPAN:
807 readSpan(p, sp, new TextAppearanceSpan(p));
808 break;
809
810 case ANNOTATION:
811 readSpan(p, sp, new Annotation(p));
812 break;
813
Gilles Debunnea00972a2011-04-13 16:07:31 -0700814 case SUGGESTION_SPAN:
815 readSpan(p, sp, new SuggestionSpan(p));
816 break;
817
Gilles Debunne28294cc2011-08-24 12:02:05 -0700818 case SPELL_CHECK_SPAN:
819 readSpan(p, sp, new SpellCheckSpan(p));
820 break;
821
822 case SUGGESTION_RANGE_SPAN:
Gilles Debunne0eea6682011-08-29 13:30:31 -0700823 readSpan(p, sp, new SuggestionRangeSpan(p));
Gilles Debunne28294cc2011-08-24 12:02:05 -0700824 break;
Gilles Debunnee90bed12011-08-30 14:28:27 -0700825
Luca Zanoline6d36822011-08-30 18:04:34 +0100826 case EASY_EDIT_SPAN:
Luca Zanolin1b15ba52013-02-20 14:31:37 +0000827 readSpan(p, sp, new EasyEditSpan(p));
Luca Zanoline6d36822011-08-30 18:04:34 +0100828 break;
829
Victoria Leasedf8ef4b2012-08-17 15:34:01 -0700830 case LOCALE_SPAN:
831 readSpan(p, sp, new LocaleSpan(p));
832 break;
833
Niels Egberts4f4ead42014-06-23 12:01:14 +0100834 case TTS_SPAN:
835 readSpan(p, sp, new TtsSpan(p));
836 break;
837
Phil Weaver193520e2016-12-13 09:39:06 -0800838 case ACCESSIBILITY_CLICKABLE_SPAN:
839 readSpan(p, sp, new AccessibilityClickableSpan(p));
840 break;
841
842 case ACCESSIBILITY_URL_SPAN:
843 readSpan(p, sp, new AccessibilityURLSpan(p));
844 break;
845
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800846 default:
847 throw new RuntimeException("bogus span encoding " + kind);
848 }
849 }
850
851 return sp;
852 }
853
854 public CharSequence[] newArray(int size)
855 {
856 return new CharSequence[size];
857 }
858 };
859
860 /**
861 * Debugging tool to print the spans in a CharSequence. The output will
862 * be printed one span per line. If the CharSequence is not a Spanned,
863 * then the entire string will be printed on a single line.
864 */
865 public static void dumpSpans(CharSequence cs, Printer printer, String prefix) {
866 if (cs instanceof Spanned) {
867 Spanned sp = (Spanned) cs;
868 Object[] os = sp.getSpans(0, cs.length(), Object.class);
869
870 for (int i = 0; i < os.length; i++) {
871 Object o = os[i];
872 printer.println(prefix + cs.subSequence(sp.getSpanStart(o),
873 sp.getSpanEnd(o)) + ": "
874 + Integer.toHexString(System.identityHashCode(o))
875 + " " + o.getClass().getCanonicalName()
876 + " (" + sp.getSpanStart(o) + "-" + sp.getSpanEnd(o)
877 + ") fl=#" + sp.getSpanFlags(o));
878 }
879 } else {
880 printer.println(prefix + cs + ": (no spans)");
881 }
882 }
883
884 /**
885 * Return a new CharSequence in which each of the source strings is
886 * replaced by the corresponding element of the destinations.
887 */
888 public static CharSequence replace(CharSequence template,
889 String[] sources,
890 CharSequence[] destinations) {
891 SpannableStringBuilder tb = new SpannableStringBuilder(template);
892
893 for (int i = 0; i < sources.length; i++) {
894 int where = indexOf(tb, sources[i]);
895
896 if (where >= 0)
897 tb.setSpan(sources[i], where, where + sources[i].length(),
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800898 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800899 }
900
901 for (int i = 0; i < sources.length; i++) {
902 int start = tb.getSpanStart(sources[i]);
903 int end = tb.getSpanEnd(sources[i]);
904
905 if (start >= 0) {
906 tb.replace(start, end, destinations[i]);
907 }
908 }
909
910 return tb;
911 }
912
913 /**
914 * Replace instances of "^1", "^2", etc. in the
915 * <code>template</code> CharSequence with the corresponding
916 * <code>values</code>. "^^" is used to produce a single caret in
917 * the output. Only up to 9 replacement values are supported,
918 * "^10" will be produce the first replacement value followed by a
919 * '0'.
920 *
921 * @param template the input text containing "^1"-style
922 * placeholder values. This object is not modified; a copy is
923 * returned.
924 *
925 * @param values CharSequences substituted into the template. The
926 * first is substituted for "^1", the second for "^2", and so on.
927 *
928 * @return the new CharSequence produced by doing the replacement
929 *
930 * @throws IllegalArgumentException if the template requests a
931 * value that was not provided, or if more than 9 values are
932 * provided.
933 */
934 public static CharSequence expandTemplate(CharSequence template,
935 CharSequence... values) {
936 if (values.length > 9) {
937 throw new IllegalArgumentException("max of 9 values are supported");
938 }
939
940 SpannableStringBuilder ssb = new SpannableStringBuilder(template);
941
942 try {
943 int i = 0;
944 while (i < ssb.length()) {
945 if (ssb.charAt(i) == '^') {
946 char next = ssb.charAt(i+1);
947 if (next == '^') {
948 ssb.delete(i+1, i+2);
949 ++i;
950 continue;
951 } else if (Character.isDigit(next)) {
952 int which = Character.getNumericValue(next) - 1;
953 if (which < 0) {
954 throw new IllegalArgumentException(
955 "template requests value ^" + (which+1));
956 }
957 if (which >= values.length) {
958 throw new IllegalArgumentException(
959 "template requests value ^" + (which+1) +
960 "; only " + values.length + " provided");
961 }
962 ssb.replace(i, i+2, values[which]);
963 i += values[which].length();
964 continue;
965 }
966 }
967 ++i;
968 }
969 } catch (IndexOutOfBoundsException ignore) {
970 // happens when ^ is the last character in the string.
971 }
972 return ssb;
973 }
974
975 public static int getOffsetBefore(CharSequence text, int offset) {
976 if (offset == 0)
977 return 0;
978 if (offset == 1)
979 return 0;
980
981 char c = text.charAt(offset - 1);
982
983 if (c >= '\uDC00' && c <= '\uDFFF') {
984 char c1 = text.charAt(offset - 2);
985
986 if (c1 >= '\uD800' && c1 <= '\uDBFF')
987 offset -= 2;
988 else
989 offset -= 1;
990 } else {
991 offset -= 1;
992 }
993
994 if (text instanceof Spanned) {
995 ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
996 ReplacementSpan.class);
997
998 for (int i = 0; i < spans.length; i++) {
999 int start = ((Spanned) text).getSpanStart(spans[i]);
1000 int end = ((Spanned) text).getSpanEnd(spans[i]);
1001
1002 if (start < offset && end > offset)
1003 offset = start;
1004 }
1005 }
1006
1007 return offset;
1008 }
1009
1010 public static int getOffsetAfter(CharSequence text, int offset) {
1011 int len = text.length();
1012
1013 if (offset == len)
1014 return len;
1015 if (offset == len - 1)
1016 return len;
1017
1018 char c = text.charAt(offset);
1019
1020 if (c >= '\uD800' && c <= '\uDBFF') {
1021 char c1 = text.charAt(offset + 1);
1022
1023 if (c1 >= '\uDC00' && c1 <= '\uDFFF')
1024 offset += 2;
1025 else
1026 offset += 1;
1027 } else {
1028 offset += 1;
1029 }
1030
1031 if (text instanceof Spanned) {
1032 ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
1033 ReplacementSpan.class);
1034
1035 for (int i = 0; i < spans.length; i++) {
1036 int start = ((Spanned) text).getSpanStart(spans[i]);
1037 int end = ((Spanned) text).getSpanEnd(spans[i]);
1038
1039 if (start < offset && end > offset)
1040 offset = end;
1041 }
1042 }
1043
1044 return offset;
1045 }
1046
1047 private static void readSpan(Parcel p, Spannable sp, Object o) {
1048 sp.setSpan(o, p.readInt(), p.readInt(), p.readInt());
1049 }
1050
Daisuke Miyakawac1d27482009-05-25 17:37:41 +09001051 /**
1052 * Copies the spans from the region <code>start...end</code> in
1053 * <code>source</code> to the region
1054 * <code>destoff...destoff+end-start</code> in <code>dest</code>.
1055 * Spans in <code>source</code> that begin before <code>start</code>
1056 * or end after <code>end</code> but overlap this range are trimmed
1057 * as if they began at <code>start</code> or ended at <code>end</code>.
1058 *
1059 * @throws IndexOutOfBoundsException if any of the copied spans
1060 * are out of range in <code>dest</code>.
1061 */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001062 public static void copySpansFrom(Spanned source, int start, int end,
1063 Class kind,
1064 Spannable dest, int destoff) {
1065 if (kind == null) {
1066 kind = Object.class;
1067 }
1068
1069 Object[] spans = source.getSpans(start, end, kind);
1070
1071 for (int i = 0; i < spans.length; i++) {
1072 int st = source.getSpanStart(spans[i]);
1073 int en = source.getSpanEnd(spans[i]);
1074 int fl = source.getSpanFlags(spans[i]);
1075
1076 if (st < start)
1077 st = start;
1078 if (en > end)
1079 en = end;
1080
1081 dest.setSpan(spans[i], st - start + destoff, en - start + destoff,
1082 fl);
1083 }
1084 }
1085
Roozbeh Pournader205a9932017-06-08 00:23:42 -07001086 /**
1087 * Transforms a CharSequences to uppercase, copying the sources spans and keeping them spans as
1088 * much as possible close to their relative original places. In the case the the uppercase
1089 * string is identical to the sources, the source itself is returned instead of being copied.
1090 *
1091 * If copySpans is set, source must be an instance of Spanned.
1092 *
1093 * {@hide}
1094 */
1095 @NonNull
1096 public static CharSequence toUpperCase(@Nullable Locale locale, @NonNull CharSequence source,
1097 boolean copySpans) {
1098 final Edits edits = new Edits();
1099 if (!copySpans) { // No spans. Just uppercase the characters.
1100 final StringBuilder result = CaseMap.toUpper().apply(
1101 locale, source, new StringBuilder(), edits);
1102 return edits.hasChanges() ? result : source;
1103 }
1104
1105 final SpannableStringBuilder result = CaseMap.toUpper().apply(
1106 locale, source, new SpannableStringBuilder(), edits);
1107 if (!edits.hasChanges()) {
1108 // No changes happened while capitalizing. We can return the source as it was.
1109 return source;
1110 }
1111
1112 final Edits.Iterator iterator = edits.getFineIterator();
1113 final int sourceLength = source.length();
1114 final Spanned spanned = (Spanned) source;
1115 final Object[] spans = spanned.getSpans(0, sourceLength, Object.class);
1116 for (Object span : spans) {
1117 final int sourceStart = spanned.getSpanStart(span);
1118 final int sourceEnd = spanned.getSpanEnd(span);
1119 final int flags = spanned.getSpanFlags(span);
1120 // Make sure the indices are not at the end of the string, since in that case
1121 // iterator.findSourceIndex() would fail.
1122 final int destStart = sourceStart == sourceLength ? result.length() :
1123 toUpperMapToDest(iterator, sourceStart);
1124 final int destEnd = sourceEnd == sourceLength ? result.length() :
1125 toUpperMapToDest(iterator, sourceEnd);
1126 result.setSpan(span, destStart, destEnd, flags);
1127 }
1128 return result;
1129 }
1130
1131 // helper method for toUpperCase()
1132 private static int toUpperMapToDest(Edits.Iterator iterator, int sourceIndex) {
1133 // Guaranteed to succeed if sourceIndex < source.length().
1134 iterator.findSourceIndex(sourceIndex);
1135 if (sourceIndex == iterator.sourceIndex()) {
1136 return iterator.destinationIndex();
1137 }
1138 // We handle the situation differently depending on if we are in the changed slice or an
1139 // unchanged one: In an unchanged slice, we can find the exact location the span
1140 // boundary was before and map there.
1141 //
1142 // But in a changed slice, we need to treat the whole destination slice as an atomic unit.
1143 // We adjust the span boundary to the end of that slice to reduce of the chance of adjacent
1144 // spans in the source overlapping in the result. (The choice for the end vs the beginning
1145 // is somewhat arbitrary, but was taken because we except to see slightly more spans only
1146 // affecting a base character compared to spans only affecting a combining character.)
1147 if (iterator.hasChange()) {
1148 return iterator.destinationIndex() + iterator.newLength();
1149 } else {
1150 // Move the index 1:1 along with this unchanged piece of text.
1151 return iterator.destinationIndex() + (sourceIndex - iterator.sourceIndex());
1152 }
1153 }
1154
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001155 public enum TruncateAt {
1156 START,
1157 MIDDLE,
1158 END,
1159 MARQUEE,
Fabrice Di Megliocb332642011-09-23 19:08:04 -07001160 /**
1161 * @hide
1162 */
1163 END_SMALL
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001164 }
1165
1166 public interface EllipsizeCallback {
1167 /**
1168 * This method is called to report that the specified region of
1169 * text was ellipsized away by a call to {@link #ellipsize}.
1170 */
1171 public void ellipsized(int start, int end);
1172 }
1173
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001174 /**
1175 * Returns the original text if it fits in the specified width
1176 * given the properties of the specified Paint,
1177 * or, if it does not fit, a truncated
1178 * copy with ellipsis character added at the specified edge or center.
1179 */
1180 public static CharSequence ellipsize(CharSequence text,
1181 TextPaint p,
1182 float avail, TruncateAt where) {
1183 return ellipsize(text, p, avail, where, false, null);
1184 }
1185
1186 /**
1187 * Returns the original text if it fits in the specified width
1188 * given the properties of the specified Paint,
Doug Felte8e45f22010-03-29 14:58:40 -07001189 * or, if it does not fit, a copy with ellipsis character added
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001190 * at the specified edge or center.
1191 * If <code>preserveLength</code> is specified, the returned copy
1192 * will be padded with zero-width spaces to preserve the original
1193 * length and offsets instead of truncating.
1194 * If <code>callback</code> is non-null, it will be called to
Doug Feltcb3791202011-07-07 11:57:48 -07001195 * report the start and end of the ellipsized range. TextDirection
1196 * is determined by the first strong directional character.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001197 */
1198 public static CharSequence ellipsize(CharSequence text,
Doug Felte8e45f22010-03-29 14:58:40 -07001199 TextPaint paint,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001200 float avail, TruncateAt where,
1201 boolean preserveLength,
1202 EllipsizeCallback callback) {
Doug Feltcb3791202011-07-07 11:57:48 -07001203 return ellipsize(text, paint, avail, where, preserveLength, callback,
Fabrice Di Megliocb332642011-09-23 19:08:04 -07001204 TextDirectionHeuristics.FIRSTSTRONG_LTR,
Neil Fullerd29bdb22015-02-06 10:03:08 +00001205 (where == TruncateAt.END_SMALL) ? ELLIPSIS_TWO_DOTS_STRING : ELLIPSIS_STRING);
Doug Feltcb3791202011-07-07 11:57:48 -07001206 }
1207
1208 /**
1209 * Returns the original text if it fits in the specified width
1210 * given the properties of the specified Paint,
1211 * or, if it does not fit, a copy with ellipsis character added
1212 * at the specified edge or center.
1213 * If <code>preserveLength</code> is specified, the returned copy
1214 * will be padded with zero-width spaces to preserve the original
1215 * length and offsets instead of truncating.
1216 * If <code>callback</code> is non-null, it will be called to
1217 * report the start and end of the ellipsized range.
1218 *
1219 * @hide
1220 */
1221 public static CharSequence ellipsize(CharSequence text,
1222 TextPaint paint,
1223 float avail, TruncateAt where,
1224 boolean preserveLength,
1225 EllipsizeCallback callback,
Fabrice Di Megliocb332642011-09-23 19:08:04 -07001226 TextDirectionHeuristic textDir, String ellipsis) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001227
1228 int len = text.length();
1229
Doug Felte8e45f22010-03-29 14:58:40 -07001230 MeasuredText mt = MeasuredText.obtain();
1231 try {
Doug Feltcb3791202011-07-07 11:57:48 -07001232 float width = setPara(mt, paint, text, 0, text.length(), textDir);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001233
Doug Felte8e45f22010-03-29 14:58:40 -07001234 if (width <= avail) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001235 if (callback != null) {
1236 callback.ellipsized(0, 0);
1237 }
1238
1239 return text;
1240 }
1241
Doug Felte8e45f22010-03-29 14:58:40 -07001242 // XXX assumes ellipsis string does not require shaping and
1243 // is unaffected by style
Fabrice Di Megliocb332642011-09-23 19:08:04 -07001244 float ellipsiswid = paint.measureText(ellipsis);
Doug Felte8e45f22010-03-29 14:58:40 -07001245 avail -= ellipsiswid;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001246
Doug Felte8e45f22010-03-29 14:58:40 -07001247 int left = 0;
1248 int right = len;
1249 if (avail < 0) {
1250 // it all goes
1251 } else if (where == TruncateAt.START) {
Gilles Debunnec70e7a02012-02-23 18:05:55 -08001252 right = len - mt.breakText(len, false, avail);
Fabrice Di Megliocb332642011-09-23 19:08:04 -07001253 } else if (where == TruncateAt.END || where == TruncateAt.END_SMALL) {
Gilles Debunnec70e7a02012-02-23 18:05:55 -08001254 left = mt.breakText(len, true, avail);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001255 } else {
Gilles Debunnec70e7a02012-02-23 18:05:55 -08001256 right = len - mt.breakText(len, false, avail / 2);
Doug Felte8e45f22010-03-29 14:58:40 -07001257 avail -= mt.measure(right, len);
Gilles Debunnec70e7a02012-02-23 18:05:55 -08001258 left = mt.breakText(right, true, avail);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001259 }
1260
1261 if (callback != null) {
1262 callback.ellipsized(left, right);
1263 }
1264
Doug Felte8e45f22010-03-29 14:58:40 -07001265 char[] buf = mt.mChars;
1266 Spanned sp = text instanceof Spanned ? (Spanned) text : null;
1267
1268 int remaining = len - (right - left);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001269 if (preserveLength) {
Doug Felte8e45f22010-03-29 14:58:40 -07001270 if (remaining > 0) { // else eliminate the ellipsis too
Fabrice Di Megliocb332642011-09-23 19:08:04 -07001271 buf[left++] = ellipsis.charAt(0);
Doug Felte8e45f22010-03-29 14:58:40 -07001272 }
1273 for (int i = left; i < right; i++) {
Fabrice Di Megliocb332642011-09-23 19:08:04 -07001274 buf[i] = ZWNBS_CHAR;
Doug Felte8e45f22010-03-29 14:58:40 -07001275 }
1276 String s = new String(buf, 0, len);
1277 if (sp == null) {
1278 return s;
1279 }
1280 SpannableString ss = new SpannableString(s);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001281 copySpansFrom(sp, 0, len, Object.class, ss, 0);
1282 return ss;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001283 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001284
Doug Felte8e45f22010-03-29 14:58:40 -07001285 if (remaining == 0) {
1286 return "";
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001287 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001288
Doug Felte8e45f22010-03-29 14:58:40 -07001289 if (sp == null) {
Fabrice Di Megliocb332642011-09-23 19:08:04 -07001290 StringBuilder sb = new StringBuilder(remaining + ellipsis.length());
Doug Felte8e45f22010-03-29 14:58:40 -07001291 sb.append(buf, 0, left);
Fabrice Di Megliocb332642011-09-23 19:08:04 -07001292 sb.append(ellipsis);
Doug Felte8e45f22010-03-29 14:58:40 -07001293 sb.append(buf, right, len - right);
1294 return sb.toString();
1295 }
1296
1297 SpannableStringBuilder ssb = new SpannableStringBuilder();
1298 ssb.append(text, 0, left);
Fabrice Di Megliocb332642011-09-23 19:08:04 -07001299 ssb.append(ellipsis);
Doug Felte8e45f22010-03-29 14:58:40 -07001300 ssb.append(text, right, len);
1301 return ssb;
1302 } finally {
1303 MeasuredText.recycle(mt);
1304 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001305 }
1306
1307 /**
Roozbeh Pournader3bfce332016-06-17 15:03:56 -07001308 * Formats a list of CharSequences by repeatedly inserting the separator between them,
1309 * but stopping when the resulting sequence is too wide for the specified width.
1310 *
1311 * This method actually tries to fit the maximum number of elements. So if {@code "A, 11 more"
1312 * fits}, {@code "A, B, 10 more"} doesn't fit, but {@code "A, B, C, 9 more"} fits again (due to
1313 * the glyphs for the digits being very wide, for example), it returns
1314 * {@code "A, B, C, 9 more"}. Because of this, this method may be inefficient for very long
1315 * lists.
1316 *
1317 * Note that the elements of the returned value, as well as the string for {@code moreId}, will
1318 * be bidi-wrapped using {@link BidiFormatter#unicodeWrap} based on the locale of the input
1319 * Context. If the input {@code Context} is null, the default BidiFormatter from
1320 * {@link BidiFormatter#getInstance()} will be used.
1321 *
1322 * @param context the {@code Context} to get the {@code moreId} resource from. If {@code null},
1323 * an ellipsis (U+2026) would be used for {@code moreId}.
1324 * @param elements the list to format
1325 * @param separator a separator, such as {@code ", "}
1326 * @param paint the Paint with which to measure the text
1327 * @param avail the horizontal width available for the text (in pixels)
1328 * @param moreId the resource ID for the pluralized string to insert at the end of sequence when
1329 * some of the elements don't fit.
1330 *
1331 * @return the formatted CharSequence. If even the shortest sequence (e.g. {@code "A, 11 more"})
1332 * doesn't fit, it will return an empty string.
1333 */
1334
1335 public static CharSequence listEllipsize(@Nullable Context context,
1336 @Nullable List<CharSequence> elements, @NonNull String separator,
1337 @NonNull TextPaint paint, @FloatRange(from=0.0,fromInclusive=false) float avail,
1338 @PluralsRes int moreId) {
1339 if (elements == null) {
1340 return "";
1341 }
1342 final int totalLen = elements.size();
1343 if (totalLen == 0) {
1344 return "";
1345 }
1346
1347 final Resources res;
1348 final BidiFormatter bidiFormatter;
1349 if (context == null) {
1350 res = null;
1351 bidiFormatter = BidiFormatter.getInstance();
1352 } else {
1353 res = context.getResources();
1354 bidiFormatter = BidiFormatter.getInstance(res.getConfiguration().getLocales().get(0));
1355 }
1356
1357 final SpannableStringBuilder output = new SpannableStringBuilder();
1358 final int[] endIndexes = new int[totalLen];
1359 for (int i = 0; i < totalLen; i++) {
1360 output.append(bidiFormatter.unicodeWrap(elements.get(i)));
1361 if (i != totalLen - 1) { // Insert a separator, except at the very end.
1362 output.append(separator);
1363 }
1364 endIndexes[i] = output.length();
1365 }
1366
1367 for (int i = totalLen - 1; i >= 0; i--) {
1368 // Delete the tail of the string, cutting back to one less element.
1369 output.delete(endIndexes[i], output.length());
1370
1371 final int remainingElements = totalLen - i - 1;
1372 if (remainingElements > 0) {
1373 CharSequence morePiece = (res == null) ?
1374 ELLIPSIS_STRING :
1375 res.getQuantityString(moreId, remainingElements, remainingElements);
1376 morePiece = bidiFormatter.unicodeWrap(morePiece);
1377 output.append(morePiece);
1378 }
1379
1380 final float width = paint.measureText(output, 0, output.length());
1381 if (width <= avail) { // The string fits.
1382 return output;
1383 }
1384 }
1385 return ""; // Nothing fits.
1386 }
1387
1388 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001389 * Converts a CharSequence of the comma-separated form "Andy, Bob,
1390 * Charles, David" that is too wide to fit into the specified width
1391 * into one like "Andy, Bob, 2 more".
1392 *
1393 * @param text the text to truncate
1394 * @param p the Paint with which to measure the text
Roozbeh Pournader3bfce332016-06-17 15:03:56 -07001395 * @param avail the horizontal width available for the text (in pixels)
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001396 * @param oneMore the string for "1 more" in the current locale
1397 * @param more the string for "%d more" in the current locale
Roozbeh Pournader3bfce332016-06-17 15:03:56 -07001398 *
1399 * @deprecated Do not use. This is not internationalized, and has known issues
1400 * with right-to-left text, languages that have more than one plural form, languages
1401 * that use a different character as a comma-like separator, etc.
1402 * Use {@link #listEllipsize} instead.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001403 */
Roozbeh Pournader3bfce332016-06-17 15:03:56 -07001404 @Deprecated
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001405 public static CharSequence commaEllipsize(CharSequence text,
1406 TextPaint p, float avail,
1407 String oneMore,
1408 String more) {
Doug Feltcb3791202011-07-07 11:57:48 -07001409 return commaEllipsize(text, p, avail, oneMore, more,
1410 TextDirectionHeuristics.FIRSTSTRONG_LTR);
1411 }
1412
1413 /**
1414 * @hide
1415 */
Roozbeh Pournader3bfce332016-06-17 15:03:56 -07001416 @Deprecated
Doug Feltcb3791202011-07-07 11:57:48 -07001417 public static CharSequence commaEllipsize(CharSequence text, TextPaint p,
1418 float avail, String oneMore, String more, TextDirectionHeuristic textDir) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001419
Doug Felte8e45f22010-03-29 14:58:40 -07001420 MeasuredText mt = MeasuredText.obtain();
1421 try {
1422 int len = text.length();
Doug Feltcb3791202011-07-07 11:57:48 -07001423 float width = setPara(mt, p, text, 0, len, textDir);
Doug Felte8e45f22010-03-29 14:58:40 -07001424 if (width <= avail) {
1425 return text;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001426 }
1427
Doug Felte8e45f22010-03-29 14:58:40 -07001428 char[] buf = mt.mChars;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001429
Doug Felte8e45f22010-03-29 14:58:40 -07001430 int commaCount = 0;
1431 for (int i = 0; i < len; i++) {
1432 if (buf[i] == ',') {
1433 commaCount++;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001434 }
1435 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001436
Doug Felte8e45f22010-03-29 14:58:40 -07001437 int remaining = commaCount + 1;
1438
1439 int ok = 0;
Doug Felte8e45f22010-03-29 14:58:40 -07001440 String okFormat = "";
1441
1442 int w = 0;
1443 int count = 0;
1444 float[] widths = mt.mWidths;
1445
Doug Felte8e45f22010-03-29 14:58:40 -07001446 MeasuredText tempMt = MeasuredText.obtain();
1447 for (int i = 0; i < len; i++) {
1448 w += widths[i];
1449
1450 if (buf[i] == ',') {
1451 count++;
1452
1453 String format;
1454 // XXX should not insert spaces, should be part of string
1455 // XXX should use plural rules and not assume English plurals
1456 if (--remaining == 1) {
1457 format = " " + oneMore;
1458 } else {
1459 format = " " + String.format(more, remaining);
1460 }
1461
1462 // XXX this is probably ok, but need to look at it more
Raph Levien70616ec2015-03-04 10:41:30 -08001463 tempMt.setPara(format, 0, format.length(), textDir, null);
Brian Muramatsu4c8ad6e2011-01-27 18:13:39 -08001464 float moreWid = tempMt.addStyleRun(p, tempMt.mLen, null);
Doug Felte8e45f22010-03-29 14:58:40 -07001465
1466 if (w + moreWid <= avail) {
1467 ok = i + 1;
Doug Felte8e45f22010-03-29 14:58:40 -07001468 okFormat = format;
1469 }
1470 }
1471 }
1472 MeasuredText.recycle(tempMt);
1473
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001474 SpannableStringBuilder out = new SpannableStringBuilder(okFormat);
1475 out.insert(0, text, 0, ok);
1476 return out;
Doug Felte8e45f22010-03-29 14:58:40 -07001477 } finally {
1478 MeasuredText.recycle(mt);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001479 }
1480 }
1481
Doug Felte8e45f22010-03-29 14:58:40 -07001482 private static float setPara(MeasuredText mt, TextPaint paint,
Doug Feltcb3791202011-07-07 11:57:48 -07001483 CharSequence text, int start, int end, TextDirectionHeuristic textDir) {
Doug Felte8e45f22010-03-29 14:58:40 -07001484
Raph Levien70616ec2015-03-04 10:41:30 -08001485 mt.setPara(text, start, end, textDir, null);
Doug Felte8e45f22010-03-29 14:58:40 -07001486
1487 float width;
1488 Spanned sp = text instanceof Spanned ? (Spanned) text : null;
1489 int len = end - start;
1490 if (sp == null) {
1491 width = mt.addStyleRun(paint, len, null);
1492 } else {
1493 width = 0;
1494 int spanEnd;
1495 for (int spanStart = 0; spanStart < len; spanStart = spanEnd) {
1496 spanEnd = sp.nextSpanTransition(spanStart, len,
1497 MetricAffectingSpan.class);
1498 MetricAffectingSpan[] spans = sp.getSpans(
1499 spanStart, spanEnd, MetricAffectingSpan.class);
Gilles Debunne1e3ac182011-03-08 14:22:34 -08001500 spans = TextUtils.removeEmptySpans(spans, sp, MetricAffectingSpan.class);
Doug Felte8e45f22010-03-29 14:58:40 -07001501 width += mt.addStyleRun(paint, spans, spanEnd - spanStart, null);
1502 }
1503 }
1504
1505 return width;
1506 }
1507
Roozbeh Pournader8823c852016-06-09 18:36:47 -07001508 // Returns true if the character's presence could affect RTL layout.
1509 //
1510 // In order to be fast, the code is intentionally rough and quite conservative in its
1511 // considering inclusion of any non-BMP or surrogate characters or anything in the bidi
1512 // blocks or any bidi formatting characters with a potential to affect RTL layout.
Doug Felte8e45f22010-03-29 14:58:40 -07001513 /* package */
Roozbeh Pournader8823c852016-06-09 18:36:47 -07001514 static boolean couldAffectRtl(char c) {
1515 return (0x0590 <= c && c <= 0x08FF) || // RTL scripts
1516 c == 0x200E || // Bidi format character
1517 c == 0x200F || // Bidi format character
1518 (0x202A <= c && c <= 0x202E) || // Bidi format characters
1519 (0x2066 <= c && c <= 0x2069) || // Bidi format characters
1520 (0xD800 <= c && c <= 0xDFFF) || // Surrogate pairs
1521 (0xFB1D <= c && c <= 0xFDFF) || // Hebrew and Arabic presentation forms
1522 (0xFE70 <= c && c <= 0xFEFE); // Arabic presentation forms
Doug Felte8e45f22010-03-29 14:58:40 -07001523 }
1524
Roozbeh Pournader8823c852016-06-09 18:36:47 -07001525 // Returns true if there is no character present that may potentially affect RTL layout.
1526 // Since this calls couldAffectRtl() above, it's also quite conservative, in the way that
1527 // it may return 'false' (needs bidi) although careful consideration may tell us it should
1528 // return 'true' (does not need bidi).
Doug Felte8e45f22010-03-29 14:58:40 -07001529 /* package */
1530 static boolean doesNotNeedBidi(char[] text, int start, int len) {
Roozbeh Pournader8823c852016-06-09 18:36:47 -07001531 final int end = start + len;
1532 for (int i = start; i < end; i++) {
1533 if (couldAffectRtl(text[i])) {
Doug Felte8e45f22010-03-29 14:58:40 -07001534 return false;
1535 }
1536 }
1537 return true;
1538 }
1539
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001540 /* package */ static char[] obtain(int len) {
1541 char[] buf;
1542
1543 synchronized (sLock) {
1544 buf = sTemp;
1545 sTemp = null;
1546 }
1547
1548 if (buf == null || buf.length < len)
Adam Lesinski776abc22014-03-07 11:30:59 -05001549 buf = ArrayUtils.newUnpaddedCharArray(len);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001550
1551 return buf;
1552 }
1553
1554 /* package */ static void recycle(char[] temp) {
1555 if (temp.length > 1000)
1556 return;
1557
1558 synchronized (sLock) {
1559 sTemp = temp;
1560 }
1561 }
1562
1563 /**
1564 * Html-encode the string.
1565 * @param s the string to be encoded
1566 * @return the encoded string
1567 */
1568 public static String htmlEncode(String s) {
1569 StringBuilder sb = new StringBuilder();
1570 char c;
1571 for (int i = 0; i < s.length(); i++) {
1572 c = s.charAt(i);
1573 switch (c) {
1574 case '<':
1575 sb.append("&lt;"); //$NON-NLS-1$
1576 break;
1577 case '>':
1578 sb.append("&gt;"); //$NON-NLS-1$
1579 break;
1580 case '&':
1581 sb.append("&amp;"); //$NON-NLS-1$
1582 break;
1583 case '\'':
Marc Blankf4832da2012-02-13 10:11:50 -08001584 //http://www.w3.org/TR/xhtml1
1585 // The named character reference &apos; (the apostrophe, U+0027) was introduced in
1586 // XML 1.0 but does not appear in HTML. Authors should therefore use &#39; instead
1587 // of &apos; to work as expected in HTML 4 user agents.
1588 sb.append("&#39;"); //$NON-NLS-1$
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001589 break;
1590 case '"':
1591 sb.append("&quot;"); //$NON-NLS-1$
1592 break;
1593 default:
1594 sb.append(c);
1595 }
1596 }
1597 return sb.toString();
1598 }
1599
1600 /**
1601 * Returns a CharSequence concatenating the specified CharSequences,
1602 * retaining their spans if any.
Roozbeh Pournadere57886e2017-05-02 18:10:10 -07001603 *
1604 * If there are no parameters, an empty string will be returned.
1605 *
1606 * If the number of parameters is exactly one, that parameter is returned as output, even if it
1607 * is null.
1608 *
1609 * If the number of parameters is at least two, any null CharSequence among the parameters is
1610 * treated as if it was the string <code>"null"</code>.
1611 *
1612 * If there are paragraph spans in the source CharSequences that satisfy paragraph boundary
1613 * requirements in the sources but would no longer satisfy them in the concatenated
1614 * CharSequence, they may get extended in the resulting CharSequence or not retained.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001615 */
1616 public static CharSequence concat(CharSequence... text) {
1617 if (text.length == 0) {
1618 return "";
1619 }
1620
1621 if (text.length == 1) {
1622 return text[0];
1623 }
1624
1625 boolean spanned = false;
Roozbeh Pournadere57886e2017-05-02 18:10:10 -07001626 for (CharSequence piece : text) {
1627 if (piece instanceof Spanned) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001628 spanned = true;
1629 break;
1630 }
1631 }
1632
Roozbeh Pournadere57886e2017-05-02 18:10:10 -07001633 if (spanned) {
1634 final SpannableStringBuilder ssb = new SpannableStringBuilder();
1635 for (CharSequence piece : text) {
1636 // If a piece is null, we append the string "null" for compatibility with the
1637 // behavior of StringBuilder and the behavior of the concat() method in earlier
1638 // versions of Android.
1639 ssb.append(piece == null ? "null" : piece);
1640 }
1641 return new SpannedString(ssb);
1642 } else {
1643 final StringBuilder sb = new StringBuilder();
1644 for (CharSequence piece : text) {
1645 sb.append(piece);
1646 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001647 return sb.toString();
1648 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001649 }
1650
1651 /**
1652 * Returns whether the given CharSequence contains any printable characters.
1653 */
1654 public static boolean isGraphic(CharSequence str) {
1655 final int len = str.length();
Roozbeh Pournadera93880e2015-08-10 17:23:05 -07001656 for (int cp, i=0; i<len; i+=Character.charCount(cp)) {
Roozbeh Pournader1cc2acf2015-08-11 10:37:07 -07001657 cp = Character.codePointAt(str, i);
Roozbeh Pournadera93880e2015-08-10 17:23:05 -07001658 int gc = Character.getType(cp);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001659 if (gc != Character.CONTROL
1660 && gc != Character.FORMAT
1661 && gc != Character.SURROGATE
1662 && gc != Character.UNASSIGNED
1663 && gc != Character.LINE_SEPARATOR
1664 && gc != Character.PARAGRAPH_SEPARATOR
1665 && gc != Character.SPACE_SEPARATOR) {
1666 return true;
1667 }
1668 }
1669 return false;
1670 }
1671
1672 /**
1673 * Returns whether this character is a printable character.
Roozbeh Pournadera93880e2015-08-10 17:23:05 -07001674 *
1675 * This does not support non-BMP characters and should not be used.
1676 *
1677 * @deprecated Use {@link #isGraphic(CharSequence)} instead.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001678 */
Roozbeh Pournadera93880e2015-08-10 17:23:05 -07001679 @Deprecated
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001680 public static boolean isGraphic(char c) {
1681 int gc = Character.getType(c);
1682 return gc != Character.CONTROL
1683 && gc != Character.FORMAT
1684 && gc != Character.SURROGATE
1685 && gc != Character.UNASSIGNED
1686 && gc != Character.LINE_SEPARATOR
1687 && gc != Character.PARAGRAPH_SEPARATOR
1688 && gc != Character.SPACE_SEPARATOR;
1689 }
1690
1691 /**
1692 * Returns whether the given CharSequence contains only digits.
1693 */
1694 public static boolean isDigitsOnly(CharSequence str) {
1695 final int len = str.length();
Roozbeh Pournader3efda952015-08-11 09:55:57 -07001696 for (int cp, i = 0; i < len; i += Character.charCount(cp)) {
1697 cp = Character.codePointAt(str, i);
1698 if (!Character.isDigit(cp)) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001699 return false;
1700 }
1701 }
1702 return true;
1703 }
1704
1705 /**
Daisuke Miyakawa973afa92009-12-03 10:43:45 +09001706 * @hide
1707 */
1708 public static boolean isPrintableAscii(final char c) {
1709 final int asciiFirst = 0x20;
1710 final int asciiLast = 0x7E; // included
1711 return (asciiFirst <= c && c <= asciiLast) || c == '\r' || c == '\n';
1712 }
1713
1714 /**
1715 * @hide
1716 */
1717 public static boolean isPrintableAsciiOnly(final CharSequence str) {
1718 final int len = str.length();
1719 for (int i = 0; i < len; i++) {
1720 if (!isPrintableAscii(str.charAt(i))) {
1721 return false;
1722 }
1723 }
1724 return true;
1725 }
1726
1727 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001728 * Capitalization mode for {@link #getCapsMode}: capitalize all
1729 * characters. This value is explicitly defined to be the same as
1730 * {@link InputType#TYPE_TEXT_FLAG_CAP_CHARACTERS}.
1731 */
1732 public static final int CAP_MODE_CHARACTERS
1733 = InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS;
Doug Felte8e45f22010-03-29 14:58:40 -07001734
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001735 /**
1736 * Capitalization mode for {@link #getCapsMode}: capitalize the first
1737 * character of all words. This value is explicitly defined to be the same as
1738 * {@link InputType#TYPE_TEXT_FLAG_CAP_WORDS}.
1739 */
1740 public static final int CAP_MODE_WORDS
1741 = InputType.TYPE_TEXT_FLAG_CAP_WORDS;
Doug Felte8e45f22010-03-29 14:58:40 -07001742
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001743 /**
1744 * Capitalization mode for {@link #getCapsMode}: capitalize the first
1745 * character of each sentence. This value is explicitly defined to be the same as
1746 * {@link InputType#TYPE_TEXT_FLAG_CAP_SENTENCES}.
1747 */
1748 public static final int CAP_MODE_SENTENCES
1749 = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
Doug Felte8e45f22010-03-29 14:58:40 -07001750
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001751 /**
1752 * Determine what caps mode should be in effect at the current offset in
1753 * the text. Only the mode bits set in <var>reqModes</var> will be
1754 * checked. Note that the caps mode flags here are explicitly defined
1755 * to match those in {@link InputType}.
Doug Felte8e45f22010-03-29 14:58:40 -07001756 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001757 * @param cs The text that should be checked for caps modes.
1758 * @param off Location in the text at which to check.
1759 * @param reqModes The modes to be checked: may be any combination of
1760 * {@link #CAP_MODE_CHARACTERS}, {@link #CAP_MODE_WORDS}, and
1761 * {@link #CAP_MODE_SENTENCES}.
Mark Wagner60919952010-03-01 09:24:59 -08001762 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001763 * @return Returns the actual capitalization modes that can be in effect
1764 * at the current position, which is any combination of
1765 * {@link #CAP_MODE_CHARACTERS}, {@link #CAP_MODE_WORDS}, and
1766 * {@link #CAP_MODE_SENTENCES}.
1767 */
1768 public static int getCapsMode(CharSequence cs, int off, int reqModes) {
Mark Wagner60919952010-03-01 09:24:59 -08001769 if (off < 0) {
1770 return 0;
1771 }
1772
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001773 int i;
1774 char c;
1775 int mode = 0;
1776
1777 if ((reqModes&CAP_MODE_CHARACTERS) != 0) {
1778 mode |= CAP_MODE_CHARACTERS;
1779 }
1780 if ((reqModes&(CAP_MODE_WORDS|CAP_MODE_SENTENCES)) == 0) {
1781 return mode;
1782 }
1783
1784 // Back over allowed opening punctuation.
1785
1786 for (i = off; i > 0; i--) {
1787 c = cs.charAt(i - 1);
1788
1789 if (c != '"' && c != '\'' &&
1790 Character.getType(c) != Character.START_PUNCTUATION) {
1791 break;
1792 }
1793 }
1794
1795 // Start of paragraph, with optional whitespace.
1796
1797 int j = i;
1798 while (j > 0 && ((c = cs.charAt(j - 1)) == ' ' || c == '\t')) {
1799 j--;
1800 }
1801 if (j == 0 || cs.charAt(j - 1) == '\n') {
1802 return mode | CAP_MODE_WORDS;
1803 }
1804
1805 // Or start of word if we are that style.
1806
1807 if ((reqModes&CAP_MODE_SENTENCES) == 0) {
1808 if (i != j) mode |= CAP_MODE_WORDS;
1809 return mode;
1810 }
1811
1812 // There must be a space if not the start of paragraph.
1813
1814 if (i == j) {
1815 return mode;
1816 }
1817
1818 // Back over allowed closing punctuation.
1819
1820 for (; j > 0; j--) {
1821 c = cs.charAt(j - 1);
1822
1823 if (c != '"' && c != '\'' &&
1824 Character.getType(c) != Character.END_PUNCTUATION) {
1825 break;
1826 }
1827 }
1828
1829 if (j > 0) {
1830 c = cs.charAt(j - 1);
1831
1832 if (c == '.' || c == '?' || c == '!') {
1833 // Do not capitalize if the word ends with a period but
1834 // also contains a period, in which case it is an abbreviation.
1835
1836 if (c == '.') {
1837 for (int k = j - 2; k >= 0; k--) {
1838 c = cs.charAt(k);
1839
1840 if (c == '.') {
1841 return mode;
1842 }
1843
1844 if (!Character.isLetter(c)) {
1845 break;
1846 }
1847 }
1848 }
1849
1850 return mode | CAP_MODE_SENTENCES;
1851 }
1852 }
1853
1854 return mode;
1855 }
Doug Felte8e45f22010-03-29 14:58:40 -07001856
Brad Fitzpatrick11fe1812010-09-10 16:07:52 -07001857 /**
1858 * Does a comma-delimited list 'delimitedString' contain a certain item?
1859 * (without allocating memory)
1860 *
1861 * @hide
1862 */
1863 public static boolean delimitedStringContains(
1864 String delimitedString, char delimiter, String item) {
1865 if (isEmpty(delimitedString) || isEmpty(item)) {
1866 return false;
1867 }
1868 int pos = -1;
1869 int length = delimitedString.length();
1870 while ((pos = delimitedString.indexOf(item, pos + 1)) != -1) {
1871 if (pos > 0 && delimitedString.charAt(pos - 1) != delimiter) {
1872 continue;
1873 }
1874 int expectedDelimiterPos = pos + item.length();
1875 if (expectedDelimiterPos == length) {
1876 // Match at end of string.
1877 return true;
1878 }
1879 if (delimitedString.charAt(expectedDelimiterPos) == delimiter) {
1880 return true;
1881 }
1882 }
1883 return false;
1884 }
1885
Gilles Debunne1e3ac182011-03-08 14:22:34 -08001886 /**
1887 * Removes empty spans from the <code>spans</code> array.
1888 *
1889 * When parsing a Spanned using {@link Spanned#nextSpanTransition(int, int, Class)}, empty spans
1890 * will (correctly) create span transitions, and calling getSpans on a slice of text bounded by
1891 * one of these transitions will (correctly) include the empty overlapping span.
1892 *
1893 * However, these empty spans should not be taken into account when layouting or rendering the
1894 * string and this method provides a way to filter getSpans' results accordingly.
1895 *
1896 * @param spans A list of spans retrieved using {@link Spanned#getSpans(int, int, Class)} from
1897 * the <code>spanned</code>
1898 * @param spanned The Spanned from which spans were extracted
1899 * @return A subset of spans where empty spans ({@link Spanned#getSpanStart(Object)} ==
1900 * {@link Spanned#getSpanEnd(Object)} have been removed. The initial order is preserved
1901 * @hide
1902 */
1903 @SuppressWarnings("unchecked")
1904 public static <T> T[] removeEmptySpans(T[] spans, Spanned spanned, Class<T> klass) {
1905 T[] copy = null;
1906 int count = 0;
1907
1908 for (int i = 0; i < spans.length; i++) {
1909 final T span = spans[i];
1910 final int start = spanned.getSpanStart(span);
1911 final int end = spanned.getSpanEnd(span);
1912
1913 if (start == end) {
1914 if (copy == null) {
1915 copy = (T[]) Array.newInstance(klass, spans.length - 1);
1916 System.arraycopy(spans, 0, copy, 0, i);
1917 count = i;
1918 }
1919 } else {
1920 if (copy != null) {
1921 copy[count] = span;
1922 count++;
1923 }
1924 }
1925 }
1926
1927 if (copy != null) {
1928 T[] result = (T[]) Array.newInstance(klass, count);
1929 System.arraycopy(copy, 0, result, 0, count);
1930 return result;
1931 } else {
1932 return spans;
1933 }
1934 }
1935
Gilles Debunne6c488de2012-03-01 16:20:35 -08001936 /**
1937 * Pack 2 int values into a long, useful as a return value for a range
1938 * @see #unpackRangeStartFromLong(long)
1939 * @see #unpackRangeEndFromLong(long)
1940 * @hide
1941 */
1942 public static long packRangeInLong(int start, int end) {
1943 return (((long) start) << 32) | end;
1944 }
1945
1946 /**
1947 * Get the start value from a range packed in a long by {@link #packRangeInLong(int, int)}
1948 * @see #unpackRangeEndFromLong(long)
1949 * @see #packRangeInLong(int, int)
1950 * @hide
1951 */
1952 public static int unpackRangeStartFromLong(long range) {
1953 return (int) (range >>> 32);
1954 }
1955
1956 /**
1957 * Get the end value from a range packed in a long by {@link #packRangeInLong(int, int)}
1958 * @see #unpackRangeStartFromLong(long)
1959 * @see #packRangeInLong(int, int)
1960 * @hide
1961 */
1962 public static int unpackRangeEndFromLong(long range) {
1963 return (int) (range & 0x00000000FFFFFFFFL);
1964 }
1965
Fabrice Di Megliod3d9f3f2012-09-18 12:55:32 -07001966 /**
1967 * Return the layout direction for a given Locale
1968 *
1969 * @param locale the Locale for which we want the layout direction. Can be null.
1970 * @return the layout direction. This may be one of:
1971 * {@link android.view.View#LAYOUT_DIRECTION_LTR} or
1972 * {@link android.view.View#LAYOUT_DIRECTION_RTL}.
1973 *
1974 * Be careful: this code will need to be updated when vertical scripts will be supported
1975 */
1976 public static int getLayoutDirectionFromLocale(Locale locale) {
Roozbeh Pournader463b4822015-08-06 16:04:45 -07001977 return ((locale != null && !locale.equals(Locale.ROOT)
1978 && ULocale.forLocale(locale).isRightToLeft())
1979 // If forcing into RTL layout mode, return RTL as default
1980 || SystemProperties.getBoolean(Settings.Global.DEVELOPMENT_FORCE_RTL, false))
1981 ? View.LAYOUT_DIRECTION_RTL
1982 : View.LAYOUT_DIRECTION_LTR;
Fabrice Di Megliod3d9f3f2012-09-18 12:55:32 -07001983 }
1984
Jeff Sharkeyf491c722015-06-11 09:16:19 -07001985 /**
1986 * Return localized string representing the given number of selected items.
1987 *
1988 * @hide
1989 */
1990 public static CharSequence formatSelectedCount(int count) {
1991 return Resources.getSystem().getQuantityString(R.plurals.selected_count, count, count);
1992 }
1993
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +01001994 /**
1995 * Returns whether or not the specified spanned text has a style span.
1996 * @hide
1997 */
1998 public static boolean hasStyleSpan(@NonNull Spanned spanned) {
1999 Preconditions.checkArgument(spanned != null);
2000 final Class<?>[] styleClasses = {
2001 CharacterStyle.class, ParagraphStyle.class, UpdateAppearance.class};
2002 for (Class<?> clazz : styleClasses) {
2003 if (spanned.nextSpanTransition(-1, spanned.length(), clazz) < spanned.length()) {
2004 return true;
2005 }
2006 }
2007 return false;
2008 }
2009
Felipe Lemea8fce3b2017-04-04 14:22:12 -07002010 /**
2011 * If the {@code charSequence} is instance of {@link Spanned}, creates a new copy and
2012 * {@link NoCopySpan}'s are removed from the copy. Otherwise the given {@code charSequence} is
2013 * returned as it is.
2014 *
2015 * @hide
2016 */
2017 @Nullable
2018 public static CharSequence trimNoCopySpans(@Nullable CharSequence charSequence) {
2019 if (charSequence != null && charSequence instanceof Spanned) {
2020 // SpannableStringBuilder copy constructor trims NoCopySpans.
2021 return new SpannableStringBuilder(charSequence);
2022 }
2023 return charSequence;
2024 }
2025
Eugene Susla4a34f9c2017-05-16 14:16:38 -07002026 /**
2027 * Prepends {@code start} and appends {@code end} to a given {@link StringBuilder}
2028 *
2029 * @hide
2030 */
2031 public static void wrap(StringBuilder builder, String start, String end) {
2032 builder.insert(0, start);
2033 builder.append(end);
2034 }
2035
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002036 private static Object sLock = new Object();
Fabrice Di Megliod3d9f3f2012-09-18 12:55:32 -07002037
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002038 private static char[] sTemp = null;
Fabrice Di Megliocb332642011-09-23 19:08:04 -07002039
2040 private static String[] EMPTY_STRING_ARRAY = new String[]{};
2041
2042 private static final char ZWNBS_CHAR = '\uFEFF';
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002043}