blob: 68afeecfc996aa94b8cb9a8375b0fa3e3f7bef24 [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;
Siyamed Sinirce3b05a2017-07-18 18:54:31 -070020import android.annotation.IntRange;
Roozbeh Pournader3bfce332016-06-17 15:03:56 -070021import android.annotation.NonNull;
Scott Kennedy6cd132f2015-02-19 10:36:12 -080022import android.annotation.Nullable;
Roozbeh Pournader3bfce332016-06-17 15:03:56 -070023import android.annotation.PluralsRes;
24import android.content.Context;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080025import android.content.res.Resources;
Roozbeh Pournader9559c202016-12-13 10:59:50 -080026import android.icu.lang.UCharacter;
Roozbeh Pournader205a9932017-06-08 00:23:42 -070027import android.icu.text.CaseMap;
28import android.icu.text.Edits;
Roozbeh Pournader463b4822015-08-06 16:04:45 -070029import android.icu.util.ULocale;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080030import android.os.Parcel;
31import android.os.Parcelable;
Amith Yamasanid8415f42013-08-07 20:15:10 -070032import android.os.SystemProperties;
33import android.provider.Settings;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080034import android.text.style.AbsoluteSizeSpan;
Phil Weaver193520e2016-12-13 09:39:06 -080035import android.text.style.AccessibilityClickableSpan;
36import android.text.style.AccessibilityURLSpan;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080037import android.text.style.AlignmentSpan;
38import android.text.style.BackgroundColorSpan;
39import android.text.style.BulletSpan;
40import android.text.style.CharacterStyle;
Luca Zanoline6d36822011-08-30 18:04:34 +010041import android.text.style.EasyEditSpan;
Gilles Debunne0eea6682011-08-29 13:30:31 -070042import android.text.style.ForegroundColorSpan;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080043import android.text.style.LeadingMarginSpan;
Victoria Leasedf8ef4b2012-08-17 15:34:01 -070044import android.text.style.LocaleSpan;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080045import android.text.style.MetricAffectingSpan;
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +010046import android.text.style.ParagraphStyle;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080047import android.text.style.QuoteSpan;
48import android.text.style.RelativeSizeSpan;
49import android.text.style.ReplacementSpan;
50import android.text.style.ScaleXSpan;
Gilles Debunne28294cc2011-08-24 12:02:05 -070051import android.text.style.SpellCheckSpan;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080052import android.text.style.StrikethroughSpan;
53import android.text.style.StyleSpan;
54import android.text.style.SubscriptSpan;
Gilles Debunne28294cc2011-08-24 12:02:05 -070055import android.text.style.SuggestionRangeSpan;
Gilles Debunnea00972a2011-04-13 16:07:31 -070056import android.text.style.SuggestionSpan;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080057import android.text.style.SuperscriptSpan;
58import android.text.style.TextAppearanceSpan;
Niels Egberts4f4ead42014-06-23 12:01:14 +010059import android.text.style.TtsSpan;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080060import android.text.style.TypefaceSpan;
61import android.text.style.URLSpan;
62import android.text.style.UnderlineSpan;
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +010063import android.text.style.UpdateAppearance;
Victoria Lease577ba532013-04-19 13:12:15 -070064import android.util.Log;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080065import android.util.Printer;
Fabrice Di Megliod3d9f3f2012-09-18 12:55:32 -070066import android.view.View;
Raph Levien8d2aa192014-05-14 15:46:47 -070067
Doug Feltcb3791202011-07-07 11:57:48 -070068import com.android.internal.R;
69import com.android.internal.util.ArrayUtils;
Eugene Susla6ed45d82017-01-22 13:52:51 -080070import com.android.internal.util.Preconditions;
Raph Levien8d2aa192014-05-14 15:46:47 -070071
Gilles Debunne1e3ac182011-03-08 14:22:34 -080072import java.lang.reflect.Array;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080073import java.util.Iterator;
Roozbeh Pournader3bfce332016-06-17 15:03:56 -070074import java.util.List;
Fabrice Di Megliod3d9f3f2012-09-18 12:55:32 -070075import java.util.Locale;
Doug Felte8e45f22010-03-29 14:58:40 -070076import java.util.regex.Pattern;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080077
78public class TextUtils {
Victoria Lease577ba532013-04-19 13:12:15 -070079 private static final String TAG = "TextUtils";
80
Roozbeh Pournader9ea756f2017-07-25 11:20:29 -070081 // Zero-width character used to fill ellipsized strings when codepoint lenght must be preserved.
82 /* package */ static final char ELLIPSIS_FILLER = '\uFEFF'; // ZERO WIDTH NO-BREAK SPACE
Neil Fullerd29bdb22015-02-06 10:03:08 +000083
Roozbeh Pournader9ea756f2017-07-25 11:20:29 -070084 // TODO: Based on CLDR data, these need to be localized for Dzongkha (dz) and perhaps
85 // Hong Kong Traditional Chinese (zh-Hant-HK), but that may need to depend on the actual word
86 // being ellipsized and not the locale.
87 private static final String ELLIPSIS_NORMAL = "\u2026"; // HORIZONTAL ELLIPSIS (…)
88 private static final String ELLIPSIS_TWO_DOTS = "\u2025"; // TWO DOT LEADER (‥)
89
90 /** {@hide} */
91 @NonNull
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -070092 public static String getEllipsisString(@NonNull TruncateAt method) {
93 return (method == TruncateAt.END_SMALL) ? ELLIPSIS_TWO_DOTS : ELLIPSIS_NORMAL;
Roozbeh Pournader9ea756f2017-07-25 11:20:29 -070094 }
95
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080096
Fabrice Di Megliocb332642011-09-23 19:08:04 -070097 private TextUtils() { /* cannot be instantiated */ }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080098
99 public static void getChars(CharSequence s, int start, int end,
100 char[] dest, int destoff) {
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800101 Class<? extends CharSequence> c = s.getClass();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800102
103 if (c == String.class)
104 ((String) s).getChars(start, end, dest, destoff);
105 else if (c == StringBuffer.class)
106 ((StringBuffer) s).getChars(start, end, dest, destoff);
107 else if (c == StringBuilder.class)
108 ((StringBuilder) s).getChars(start, end, dest, destoff);
109 else if (s instanceof GetChars)
110 ((GetChars) s).getChars(start, end, dest, destoff);
111 else {
112 for (int i = start; i < end; i++)
113 dest[destoff++] = s.charAt(i);
114 }
115 }
116
117 public static int indexOf(CharSequence s, char ch) {
118 return indexOf(s, ch, 0);
119 }
120
121 public static int indexOf(CharSequence s, char ch, int start) {
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800122 Class<? extends CharSequence> c = s.getClass();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800123
124 if (c == String.class)
125 return ((String) s).indexOf(ch, start);
126
127 return indexOf(s, ch, start, s.length());
128 }
129
130 public static int indexOf(CharSequence s, char ch, int start, int end) {
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800131 Class<? extends CharSequence> c = s.getClass();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800132
133 if (s instanceof GetChars || c == StringBuffer.class ||
134 c == StringBuilder.class || c == String.class) {
135 final int INDEX_INCREMENT = 500;
136 char[] temp = obtain(INDEX_INCREMENT);
137
138 while (start < end) {
139 int segend = start + INDEX_INCREMENT;
140 if (segend > end)
141 segend = end;
142
143 getChars(s, start, segend, temp, 0);
144
145 int count = segend - start;
146 for (int i = 0; i < count; i++) {
147 if (temp[i] == ch) {
148 recycle(temp);
149 return i + start;
150 }
151 }
152
153 start = segend;
154 }
155
156 recycle(temp);
157 return -1;
158 }
159
160 for (int i = start; i < end; i++)
161 if (s.charAt(i) == ch)
162 return i;
163
164 return -1;
165 }
166
167 public static int lastIndexOf(CharSequence s, char ch) {
168 return lastIndexOf(s, ch, s.length() - 1);
169 }
170
171 public static int lastIndexOf(CharSequence s, char ch, int last) {
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800172 Class<? extends CharSequence> c = s.getClass();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800173
174 if (c == String.class)
175 return ((String) s).lastIndexOf(ch, last);
176
177 return lastIndexOf(s, ch, 0, last);
178 }
179
180 public static int lastIndexOf(CharSequence s, char ch,
181 int start, int last) {
182 if (last < 0)
183 return -1;
184 if (last >= s.length())
185 last = s.length() - 1;
186
187 int end = last + 1;
188
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800189 Class<? extends CharSequence> c = s.getClass();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800190
191 if (s instanceof GetChars || c == StringBuffer.class ||
192 c == StringBuilder.class || c == String.class) {
193 final int INDEX_INCREMENT = 500;
194 char[] temp = obtain(INDEX_INCREMENT);
195
196 while (start < end) {
197 int segstart = end - INDEX_INCREMENT;
198 if (segstart < start)
199 segstart = start;
200
201 getChars(s, segstart, end, temp, 0);
202
203 int count = end - segstart;
204 for (int i = count - 1; i >= 0; i--) {
205 if (temp[i] == ch) {
206 recycle(temp);
207 return i + segstart;
208 }
209 }
210
211 end = segstart;
212 }
213
214 recycle(temp);
215 return -1;
216 }
217
218 for (int i = end - 1; i >= start; i--)
219 if (s.charAt(i) == ch)
220 return i;
221
222 return -1;
223 }
224
225 public static int indexOf(CharSequence s, CharSequence needle) {
226 return indexOf(s, needle, 0, s.length());
227 }
228
229 public static int indexOf(CharSequence s, CharSequence needle, int start) {
230 return indexOf(s, needle, start, s.length());
231 }
232
233 public static int indexOf(CharSequence s, CharSequence needle,
234 int start, int end) {
235 int nlen = needle.length();
236 if (nlen == 0)
237 return start;
238
239 char c = needle.charAt(0);
240
241 for (;;) {
242 start = indexOf(s, c, start);
243 if (start > end - nlen) {
244 break;
245 }
246
247 if (start < 0) {
248 return -1;
249 }
250
251 if (regionMatches(s, start, needle, 0, nlen)) {
252 return start;
253 }
254
255 start++;
256 }
257 return -1;
258 }
259
260 public static boolean regionMatches(CharSequence one, int toffset,
261 CharSequence two, int ooffset,
262 int len) {
Raph Levien8d2aa192014-05-14 15:46:47 -0700263 int tempLen = 2 * len;
264 if (tempLen < len) {
265 // Integer overflow; len is unreasonably large
266 throw new IndexOutOfBoundsException();
267 }
268 char[] temp = obtain(tempLen);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800269
270 getChars(one, toffset, toffset + len, temp, 0);
271 getChars(two, ooffset, ooffset + len, temp, len);
272
273 boolean match = true;
274 for (int i = 0; i < len; i++) {
275 if (temp[i] != temp[i + len]) {
276 match = false;
277 break;
278 }
279 }
280
281 recycle(temp);
282 return match;
283 }
284
285 /**
286 * Create a new String object containing the given range of characters
287 * from the source string. This is different than simply calling
288 * {@link CharSequence#subSequence(int, int) CharSequence.subSequence}
289 * in that it does not preserve any style runs in the source sequence,
290 * allowing a more efficient implementation.
291 */
292 public static String substring(CharSequence source, int start, int end) {
293 if (source instanceof String)
294 return ((String) source).substring(start, end);
295 if (source instanceof StringBuilder)
296 return ((StringBuilder) source).substring(start, end);
297 if (source instanceof StringBuffer)
298 return ((StringBuffer) source).substring(start, end);
299
300 char[] temp = obtain(end - start);
301 getChars(source, start, end, temp, 0);
302 String ret = new String(temp, 0, end - start);
303 recycle(temp);
304
305 return ret;
306 }
307
308 /**
309 * Returns a string containing the tokens joined by delimiters.
Roozbeh Pournader42673c32017-07-20 15:23:33 -0700310 *
311 * @param delimiter a CharSequence that will be inserted between the tokens. If null, the string
312 * "null" will be used as the delimiter.
313 * @param tokens an array objects to be joined. Strings will be formed from the objects by
314 * calling object.toString(). If tokens is null, a NullPointerException will be thrown. If
315 * tokens is an empty array, an empty string will be returned.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800316 */
Roozbeh Pournader42673c32017-07-20 15:23:33 -0700317 public static String join(@NonNull CharSequence delimiter, @NonNull Object[] tokens) {
318 final int length = tokens.length;
319 if (length == 0) {
320 return "";
321 }
322 final StringBuilder sb = new StringBuilder();
323 sb.append(tokens[0]);
324 for (int i = 1; i < length; i++) {
325 sb.append(delimiter);
326 sb.append(tokens[i]);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800327 }
328 return sb.toString();
329 }
330
331 /**
332 * Returns a string containing the tokens joined by delimiters.
Roozbeh Pournader42673c32017-07-20 15:23:33 -0700333 *
334 * @param delimiter a CharSequence that will be inserted between the tokens. If null, the string
335 * "null" will be used as the delimiter.
336 * @param tokens an array objects to be joined. Strings will be formed from the objects by
337 * calling object.toString(). If tokens is null, a NullPointerException will be thrown. If
338 * tokens is empty, an empty string will be returned.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800339 */
Roozbeh Pournader42673c32017-07-20 15:23:33 -0700340 public static String join(@NonNull CharSequence delimiter, @NonNull Iterable tokens) {
341 final Iterator<?> it = tokens.iterator();
342 if (!it.hasNext()) {
343 return "";
344 }
345 final StringBuilder sb = new StringBuilder();
346 sb.append(it.next());
347 while (it.hasNext()) {
348 sb.append(delimiter);
Andreas Gampea8a58ff2016-05-18 11:58:39 -0700349 sb.append(it.next());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800350 }
351 return sb.toString();
352 }
353
354 /**
355 * String.split() returns [''] when the string to be split is empty. This returns []. This does
356 * not remove any empty strings from the result. For example split("a,", "," ) returns {"a", ""}.
357 *
358 * @param text the string to split
359 * @param expression the regular expression to match
360 * @return an array of strings. The array will be empty if text is empty
361 *
362 * @throws NullPointerException if expression or text is null
363 */
364 public static String[] split(String text, String expression) {
365 if (text.length() == 0) {
366 return EMPTY_STRING_ARRAY;
367 } else {
368 return text.split(expression, -1);
369 }
370 }
371
372 /**
373 * Splits a string on a pattern. String.split() returns [''] when the string to be
374 * split is empty. This returns []. This does not remove any empty strings from the result.
375 * @param text the string to split
376 * @param pattern the regular expression to match
377 * @return an array of strings. The array will be empty if text is empty
378 *
379 * @throws NullPointerException if expression or text is null
380 */
381 public static String[] split(String text, Pattern pattern) {
382 if (text.length() == 0) {
383 return EMPTY_STRING_ARRAY;
384 } else {
385 return pattern.split(text, -1);
386 }
387 }
388
389 /**
390 * An interface for splitting strings according to rules that are opaque to the user of this
391 * interface. This also has less overhead than split, which uses regular expressions and
392 * allocates an array to hold the results.
393 *
394 * <p>The most efficient way to use this class is:
395 *
396 * <pre>
397 * // Once
398 * TextUtils.StringSplitter splitter = new TextUtils.SimpleStringSplitter(delimiter);
399 *
400 * // Once per string to split
401 * splitter.setString(string);
402 * for (String s : splitter) {
403 * ...
404 * }
405 * </pre>
406 */
407 public interface StringSplitter extends Iterable<String> {
408 public void setString(String string);
409 }
410
411 /**
412 * A simple string splitter.
413 *
414 * <p>If the final character in the string to split is the delimiter then no empty string will
415 * be returned for the empty string after that delimeter. That is, splitting <tt>"a,b,"</tt> on
416 * comma will return <tt>"a", "b"</tt>, not <tt>"a", "b", ""</tt>.
417 */
418 public static class SimpleStringSplitter implements StringSplitter, Iterator<String> {
419 private String mString;
420 private char mDelimiter;
421 private int mPosition;
422 private int mLength;
423
424 /**
425 * Initializes the splitter. setString may be called later.
426 * @param delimiter the delimeter on which to split
427 */
428 public SimpleStringSplitter(char delimiter) {
429 mDelimiter = delimiter;
430 }
431
432 /**
433 * Sets the string to split
434 * @param string the string to split
435 */
436 public void setString(String string) {
437 mString = string;
438 mPosition = 0;
439 mLength = mString.length();
440 }
441
442 public Iterator<String> iterator() {
443 return this;
444 }
445
446 public boolean hasNext() {
447 return mPosition < mLength;
448 }
449
450 public String next() {
451 int end = mString.indexOf(mDelimiter, mPosition);
452 if (end == -1) {
453 end = mLength;
454 }
455 String nextString = mString.substring(mPosition, end);
456 mPosition = end + 1; // Skip the delimiter.
457 return nextString;
458 }
459
460 public void remove() {
461 throw new UnsupportedOperationException();
462 }
463 }
464
465 public static CharSequence stringOrSpannedString(CharSequence source) {
466 if (source == null)
467 return null;
468 if (source instanceof SpannedString)
469 return source;
470 if (source instanceof Spanned)
471 return new SpannedString(source);
472
473 return source.toString();
474 }
475
476 /**
477 * Returns true if the string is null or 0-length.
478 * @param str the string to be examined
479 * @return true if str is null or zero length
480 */
Scott Kennedy6cd132f2015-02-19 10:36:12 -0800481 public static boolean isEmpty(@Nullable CharSequence str) {
Amin Shaikhd4196c92017-02-06 17:04:47 -0800482 return str == null || str.length() == 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800483 }
484
Jeff Sharkey5cc0df22015-06-17 19:44:05 -0700485 /** {@hide} */
486 public static String nullIfEmpty(@Nullable String str) {
487 return isEmpty(str) ? null : str;
488 }
489
Eugene Susla6ed45d82017-01-22 13:52:51 -0800490 /** {@hide} */
491 public static String emptyIfNull(@Nullable String str) {
492 return str == null ? "" : str;
493 }
494
495 /** {@hide} */
496 public static String firstNotEmpty(@Nullable String a, @NonNull String b) {
497 return !isEmpty(a) ? a : Preconditions.checkStringNotEmpty(b);
498 }
499
Eugene Susla36e866b2017-02-23 18:24:39 -0800500 /** {@hide} */
501 public static int length(@Nullable String s) {
502 return isEmpty(s) ? 0 : s.length();
503 }
504
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800505 /**
Makoto Onuki812d188a2017-08-07 09:58:23 -0700506 * @return interned string if it's null.
507 * @hide
508 */
509 public static String safeIntern(String s) {
510 return (s != null) ? s.intern() : null;
511 }
512
513 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800514 * Returns the length that the specified CharSequence would have if
Roozbeh Pournader3efda952015-08-11 09:55:57 -0700515 * spaces and ASCII control characters were trimmed from the start and end,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800516 * as by {@link String#trim}.
517 */
518 public static int getTrimmedLength(CharSequence s) {
519 int len = s.length();
520
521 int start = 0;
522 while (start < len && s.charAt(start) <= ' ') {
523 start++;
524 }
525
526 int end = len;
527 while (end > start && s.charAt(end - 1) <= ' ') {
528 end--;
529 }
530
531 return end - start;
532 }
533
534 /**
535 * Returns true if a and b are equal, including if they are both null.
536 * <p><i>Note: In platform versions 1.1 and earlier, this method only worked well if
537 * both the arguments were instances of String.</i></p>
538 * @param a first CharSequence to check
539 * @param b second CharSequence to check
540 * @return true if a and b are equal
541 */
542 public static boolean equals(CharSequence a, CharSequence b) {
543 if (a == b) return true;
544 int length;
545 if (a != null && b != null && (length = a.length()) == b.length()) {
546 if (a instanceof String && b instanceof String) {
547 return a.equals(b);
548 } else {
549 for (int i = 0; i < length; i++) {
550 if (a.charAt(i) != b.charAt(i)) return false;
551 }
552 return true;
553 }
554 }
555 return false;
556 }
557
Clara Bayarrid608a0a2016-04-27 11:53:22 +0100558 /**
559 * This function only reverses individual {@code char}s and not their associated
560 * spans. It doesn't support surrogate pairs (that correspond to non-BMP code points), combining
561 * sequences or conjuncts either.
562 * @deprecated Do not use.
Roozbeh Pournader3efda952015-08-11 09:55:57 -0700563 */
564 @Deprecated
Clara Bayarrid608a0a2016-04-27 11:53:22 +0100565 public static CharSequence getReverse(CharSequence source, int start, int end) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800566 return new Reverser(source, start, end);
567 }
568
569 private static class Reverser
570 implements CharSequence, GetChars
571 {
572 public Reverser(CharSequence source, int start, int end) {
573 mSource = source;
574 mStart = start;
575 mEnd = end;
576 }
577
578 public int length() {
579 return mEnd - mStart;
580 }
581
582 public CharSequence subSequence(int start, int end) {
583 char[] buf = new char[end - start];
584
585 getChars(start, end, buf, 0);
586 return new String(buf);
587 }
588
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800589 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800590 public String toString() {
591 return subSequence(0, length()).toString();
592 }
593
594 public char charAt(int off) {
Roozbeh Pournader9559c202016-12-13 10:59:50 -0800595 return (char) UCharacter.getMirror(mSource.charAt(mEnd - 1 - off));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800596 }
597
Roozbeh Pournader9559c202016-12-13 10:59:50 -0800598 @SuppressWarnings("deprecation")
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800599 public void getChars(int start, int end, char[] dest, int destoff) {
600 TextUtils.getChars(mSource, start + mStart, end + mStart,
601 dest, destoff);
602 AndroidCharacter.mirror(dest, 0, end - start);
603
604 int len = end - start;
605 int n = (end - start) / 2;
606 for (int i = 0; i < n; i++) {
607 char tmp = dest[destoff + i];
608
609 dest[destoff + i] = dest[destoff + len - i - 1];
610 dest[destoff + len - i - 1] = tmp;
611 }
612 }
613
614 private CharSequence mSource;
615 private int mStart;
616 private int mEnd;
617 }
618
619 /** @hide */
620 public static final int ALIGNMENT_SPAN = 1;
621 /** @hide */
Victoria Lease577ba532013-04-19 13:12:15 -0700622 public static final int FIRST_SPAN = ALIGNMENT_SPAN;
623 /** @hide */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800624 public static final int FOREGROUND_COLOR_SPAN = 2;
625 /** @hide */
626 public static final int RELATIVE_SIZE_SPAN = 3;
627 /** @hide */
628 public static final int SCALE_X_SPAN = 4;
629 /** @hide */
630 public static final int STRIKETHROUGH_SPAN = 5;
631 /** @hide */
632 public static final int UNDERLINE_SPAN = 6;
633 /** @hide */
634 public static final int STYLE_SPAN = 7;
635 /** @hide */
636 public static final int BULLET_SPAN = 8;
637 /** @hide */
638 public static final int QUOTE_SPAN = 9;
639 /** @hide */
640 public static final int LEADING_MARGIN_SPAN = 10;
641 /** @hide */
642 public static final int URL_SPAN = 11;
643 /** @hide */
644 public static final int BACKGROUND_COLOR_SPAN = 12;
645 /** @hide */
646 public static final int TYPEFACE_SPAN = 13;
647 /** @hide */
648 public static final int SUPERSCRIPT_SPAN = 14;
649 /** @hide */
650 public static final int SUBSCRIPT_SPAN = 15;
651 /** @hide */
652 public static final int ABSOLUTE_SIZE_SPAN = 16;
653 /** @hide */
654 public static final int TEXT_APPEARANCE_SPAN = 17;
655 /** @hide */
656 public static final int ANNOTATION = 18;
satokadb43582011-03-09 10:08:47 +0900657 /** @hide */
Gilles Debunnea00972a2011-04-13 16:07:31 -0700658 public static final int SUGGESTION_SPAN = 19;
Gilles Debunne28294cc2011-08-24 12:02:05 -0700659 /** @hide */
660 public static final int SPELL_CHECK_SPAN = 20;
661 /** @hide */
662 public static final int SUGGESTION_RANGE_SPAN = 21;
Luca Zanoline6d36822011-08-30 18:04:34 +0100663 /** @hide */
664 public static final int EASY_EDIT_SPAN = 22;
Victoria Leasedf8ef4b2012-08-17 15:34:01 -0700665 /** @hide */
666 public static final int LOCALE_SPAN = 23;
Victoria Lease577ba532013-04-19 13:12:15 -0700667 /** @hide */
Niels Egberts4f4ead42014-06-23 12:01:14 +0100668 public static final int TTS_SPAN = 24;
669 /** @hide */
Phil Weaver193520e2016-12-13 09:39:06 -0800670 public static final int ACCESSIBILITY_CLICKABLE_SPAN = 25;
671 /** @hide */
672 public static final int ACCESSIBILITY_URL_SPAN = 26;
673 /** @hide */
674 public static final int LAST_SPAN = ACCESSIBILITY_URL_SPAN;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800675
676 /**
677 * Flatten a CharSequence and whatever styles can be copied across processes
678 * into the parcel.
679 */
Alan Viverettea70d4a92015-06-02 16:11:00 -0700680 public static void writeToParcel(CharSequence cs, Parcel p, int parcelableFlags) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800681 if (cs instanceof Spanned) {
682 p.writeInt(0);
683 p.writeString(cs.toString());
684
685 Spanned sp = (Spanned) cs;
686 Object[] os = sp.getSpans(0, cs.length(), Object.class);
687
688 // note to people adding to this: check more specific types
689 // before more generic types. also notice that it uses
690 // "if" instead of "else if" where there are interfaces
691 // so one object can be several.
692
693 for (int i = 0; i < os.length; i++) {
694 Object o = os[i];
695 Object prop = os[i];
696
697 if (prop instanceof CharacterStyle) {
698 prop = ((CharacterStyle) prop).getUnderlying();
699 }
700
701 if (prop instanceof ParcelableSpan) {
Alan Viverettea70d4a92015-06-02 16:11:00 -0700702 final ParcelableSpan ps = (ParcelableSpan) prop;
703 final int spanTypeId = ps.getSpanTypeIdInternal();
Victoria Lease577ba532013-04-19 13:12:15 -0700704 if (spanTypeId < FIRST_SPAN || spanTypeId > LAST_SPAN) {
Alan Viverettea70d4a92015-06-02 16:11:00 -0700705 Log.e(TAG, "External class \"" + ps.getClass().getSimpleName()
Victoria Lease577ba532013-04-19 13:12:15 -0700706 + "\" is attempting to use the frameworks-only ParcelableSpan"
707 + " interface");
708 } else {
709 p.writeInt(spanTypeId);
Alan Viverettea70d4a92015-06-02 16:11:00 -0700710 ps.writeToParcelInternal(p, parcelableFlags);
Victoria Lease577ba532013-04-19 13:12:15 -0700711 writeWhere(p, sp, o);
712 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800713 }
714 }
715
716 p.writeInt(0);
717 } else {
718 p.writeInt(1);
719 if (cs != null) {
720 p.writeString(cs.toString());
721 } else {
722 p.writeString(null);
723 }
724 }
725 }
726
727 private static void writeWhere(Parcel p, Spanned sp, Object o) {
728 p.writeInt(sp.getSpanStart(o));
729 p.writeInt(sp.getSpanEnd(o));
730 p.writeInt(sp.getSpanFlags(o));
731 }
732
733 public static final Parcelable.Creator<CharSequence> CHAR_SEQUENCE_CREATOR
734 = new Parcelable.Creator<CharSequence>() {
735 /**
736 * Read and return a new CharSequence, possibly with styles,
737 * from the parcel.
738 */
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800739 public CharSequence createFromParcel(Parcel p) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800740 int kind = p.readInt();
741
Martin Wallgrencee20512011-04-07 14:45:43 +0200742 String string = p.readString();
743 if (string == null) {
744 return null;
745 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800746
Martin Wallgrencee20512011-04-07 14:45:43 +0200747 if (kind == 1) {
748 return string;
749 }
750
751 SpannableString sp = new SpannableString(string);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800752
753 while (true) {
754 kind = p.readInt();
755
756 if (kind == 0)
757 break;
758
759 switch (kind) {
760 case ALIGNMENT_SPAN:
761 readSpan(p, sp, new AlignmentSpan.Standard(p));
762 break;
763
764 case FOREGROUND_COLOR_SPAN:
765 readSpan(p, sp, new ForegroundColorSpan(p));
766 break;
767
768 case RELATIVE_SIZE_SPAN:
769 readSpan(p, sp, new RelativeSizeSpan(p));
770 break;
771
772 case SCALE_X_SPAN:
773 readSpan(p, sp, new ScaleXSpan(p));
774 break;
775
776 case STRIKETHROUGH_SPAN:
777 readSpan(p, sp, new StrikethroughSpan(p));
778 break;
779
780 case UNDERLINE_SPAN:
781 readSpan(p, sp, new UnderlineSpan(p));
782 break;
783
784 case STYLE_SPAN:
785 readSpan(p, sp, new StyleSpan(p));
786 break;
787
788 case BULLET_SPAN:
789 readSpan(p, sp, new BulletSpan(p));
790 break;
791
792 case QUOTE_SPAN:
793 readSpan(p, sp, new QuoteSpan(p));
794 break;
795
796 case LEADING_MARGIN_SPAN:
797 readSpan(p, sp, new LeadingMarginSpan.Standard(p));
798 break;
799
800 case URL_SPAN:
801 readSpan(p, sp, new URLSpan(p));
802 break;
803
804 case BACKGROUND_COLOR_SPAN:
805 readSpan(p, sp, new BackgroundColorSpan(p));
806 break;
807
808 case TYPEFACE_SPAN:
809 readSpan(p, sp, new TypefaceSpan(p));
810 break;
811
812 case SUPERSCRIPT_SPAN:
813 readSpan(p, sp, new SuperscriptSpan(p));
814 break;
815
816 case SUBSCRIPT_SPAN:
817 readSpan(p, sp, new SubscriptSpan(p));
818 break;
819
820 case ABSOLUTE_SIZE_SPAN:
821 readSpan(p, sp, new AbsoluteSizeSpan(p));
822 break;
823
824 case TEXT_APPEARANCE_SPAN:
825 readSpan(p, sp, new TextAppearanceSpan(p));
826 break;
827
828 case ANNOTATION:
829 readSpan(p, sp, new Annotation(p));
830 break;
831
Gilles Debunnea00972a2011-04-13 16:07:31 -0700832 case SUGGESTION_SPAN:
833 readSpan(p, sp, new SuggestionSpan(p));
834 break;
835
Gilles Debunne28294cc2011-08-24 12:02:05 -0700836 case SPELL_CHECK_SPAN:
837 readSpan(p, sp, new SpellCheckSpan(p));
838 break;
839
840 case SUGGESTION_RANGE_SPAN:
Gilles Debunne0eea6682011-08-29 13:30:31 -0700841 readSpan(p, sp, new SuggestionRangeSpan(p));
Gilles Debunne28294cc2011-08-24 12:02:05 -0700842 break;
Gilles Debunnee90bed12011-08-30 14:28:27 -0700843
Luca Zanoline6d36822011-08-30 18:04:34 +0100844 case EASY_EDIT_SPAN:
Luca Zanolin1b15ba52013-02-20 14:31:37 +0000845 readSpan(p, sp, new EasyEditSpan(p));
Luca Zanoline6d36822011-08-30 18:04:34 +0100846 break;
847
Victoria Leasedf8ef4b2012-08-17 15:34:01 -0700848 case LOCALE_SPAN:
849 readSpan(p, sp, new LocaleSpan(p));
850 break;
851
Niels Egberts4f4ead42014-06-23 12:01:14 +0100852 case TTS_SPAN:
853 readSpan(p, sp, new TtsSpan(p));
854 break;
855
Phil Weaver193520e2016-12-13 09:39:06 -0800856 case ACCESSIBILITY_CLICKABLE_SPAN:
857 readSpan(p, sp, new AccessibilityClickableSpan(p));
858 break;
859
860 case ACCESSIBILITY_URL_SPAN:
861 readSpan(p, sp, new AccessibilityURLSpan(p));
862 break;
863
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800864 default:
865 throw new RuntimeException("bogus span encoding " + kind);
866 }
867 }
868
869 return sp;
870 }
871
872 public CharSequence[] newArray(int size)
873 {
874 return new CharSequence[size];
875 }
876 };
877
878 /**
879 * Debugging tool to print the spans in a CharSequence. The output will
880 * be printed one span per line. If the CharSequence is not a Spanned,
881 * then the entire string will be printed on a single line.
882 */
883 public static void dumpSpans(CharSequence cs, Printer printer, String prefix) {
884 if (cs instanceof Spanned) {
885 Spanned sp = (Spanned) cs;
886 Object[] os = sp.getSpans(0, cs.length(), Object.class);
887
888 for (int i = 0; i < os.length; i++) {
889 Object o = os[i];
890 printer.println(prefix + cs.subSequence(sp.getSpanStart(o),
891 sp.getSpanEnd(o)) + ": "
892 + Integer.toHexString(System.identityHashCode(o))
893 + " " + o.getClass().getCanonicalName()
894 + " (" + sp.getSpanStart(o) + "-" + sp.getSpanEnd(o)
895 + ") fl=#" + sp.getSpanFlags(o));
896 }
897 } else {
898 printer.println(prefix + cs + ": (no spans)");
899 }
900 }
901
902 /**
903 * Return a new CharSequence in which each of the source strings is
904 * replaced by the corresponding element of the destinations.
905 */
906 public static CharSequence replace(CharSequence template,
907 String[] sources,
908 CharSequence[] destinations) {
909 SpannableStringBuilder tb = new SpannableStringBuilder(template);
910
911 for (int i = 0; i < sources.length; i++) {
912 int where = indexOf(tb, sources[i]);
913
914 if (where >= 0)
915 tb.setSpan(sources[i], where, where + sources[i].length(),
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800916 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800917 }
918
919 for (int i = 0; i < sources.length; i++) {
920 int start = tb.getSpanStart(sources[i]);
921 int end = tb.getSpanEnd(sources[i]);
922
923 if (start >= 0) {
924 tb.replace(start, end, destinations[i]);
925 }
926 }
927
928 return tb;
929 }
930
931 /**
932 * Replace instances of "^1", "^2", etc. in the
933 * <code>template</code> CharSequence with the corresponding
934 * <code>values</code>. "^^" is used to produce a single caret in
935 * the output. Only up to 9 replacement values are supported,
936 * "^10" will be produce the first replacement value followed by a
937 * '0'.
938 *
939 * @param template the input text containing "^1"-style
940 * placeholder values. This object is not modified; a copy is
941 * returned.
942 *
943 * @param values CharSequences substituted into the template. The
944 * first is substituted for "^1", the second for "^2", and so on.
945 *
946 * @return the new CharSequence produced by doing the replacement
947 *
948 * @throws IllegalArgumentException if the template requests a
949 * value that was not provided, or if more than 9 values are
950 * provided.
951 */
952 public static CharSequence expandTemplate(CharSequence template,
953 CharSequence... values) {
954 if (values.length > 9) {
955 throw new IllegalArgumentException("max of 9 values are supported");
956 }
957
958 SpannableStringBuilder ssb = new SpannableStringBuilder(template);
959
960 try {
961 int i = 0;
962 while (i < ssb.length()) {
963 if (ssb.charAt(i) == '^') {
964 char next = ssb.charAt(i+1);
965 if (next == '^') {
966 ssb.delete(i+1, i+2);
967 ++i;
968 continue;
969 } else if (Character.isDigit(next)) {
970 int which = Character.getNumericValue(next) - 1;
971 if (which < 0) {
972 throw new IllegalArgumentException(
973 "template requests value ^" + (which+1));
974 }
975 if (which >= values.length) {
976 throw new IllegalArgumentException(
977 "template requests value ^" + (which+1) +
978 "; only " + values.length + " provided");
979 }
980 ssb.replace(i, i+2, values[which]);
981 i += values[which].length();
982 continue;
983 }
984 }
985 ++i;
986 }
987 } catch (IndexOutOfBoundsException ignore) {
988 // happens when ^ is the last character in the string.
989 }
990 return ssb;
991 }
992
993 public static int getOffsetBefore(CharSequence text, int offset) {
994 if (offset == 0)
995 return 0;
996 if (offset == 1)
997 return 0;
998
999 char c = text.charAt(offset - 1);
1000
1001 if (c >= '\uDC00' && c <= '\uDFFF') {
1002 char c1 = text.charAt(offset - 2);
1003
1004 if (c1 >= '\uD800' && c1 <= '\uDBFF')
1005 offset -= 2;
1006 else
1007 offset -= 1;
1008 } else {
1009 offset -= 1;
1010 }
1011
1012 if (text instanceof Spanned) {
1013 ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
1014 ReplacementSpan.class);
1015
1016 for (int i = 0; i < spans.length; i++) {
1017 int start = ((Spanned) text).getSpanStart(spans[i]);
1018 int end = ((Spanned) text).getSpanEnd(spans[i]);
1019
1020 if (start < offset && end > offset)
1021 offset = start;
1022 }
1023 }
1024
1025 return offset;
1026 }
1027
1028 public static int getOffsetAfter(CharSequence text, int offset) {
1029 int len = text.length();
1030
1031 if (offset == len)
1032 return len;
1033 if (offset == len - 1)
1034 return len;
1035
1036 char c = text.charAt(offset);
1037
1038 if (c >= '\uD800' && c <= '\uDBFF') {
1039 char c1 = text.charAt(offset + 1);
1040
1041 if (c1 >= '\uDC00' && c1 <= '\uDFFF')
1042 offset += 2;
1043 else
1044 offset += 1;
1045 } else {
1046 offset += 1;
1047 }
1048
1049 if (text instanceof Spanned) {
1050 ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
1051 ReplacementSpan.class);
1052
1053 for (int i = 0; i < spans.length; i++) {
1054 int start = ((Spanned) text).getSpanStart(spans[i]);
1055 int end = ((Spanned) text).getSpanEnd(spans[i]);
1056
1057 if (start < offset && end > offset)
1058 offset = end;
1059 }
1060 }
1061
1062 return offset;
1063 }
1064
1065 private static void readSpan(Parcel p, Spannable sp, Object o) {
1066 sp.setSpan(o, p.readInt(), p.readInt(), p.readInt());
1067 }
1068
Daisuke Miyakawac1d27482009-05-25 17:37:41 +09001069 /**
1070 * Copies the spans from the region <code>start...end</code> in
1071 * <code>source</code> to the region
1072 * <code>destoff...destoff+end-start</code> in <code>dest</code>.
1073 * Spans in <code>source</code> that begin before <code>start</code>
1074 * or end after <code>end</code> but overlap this range are trimmed
1075 * as if they began at <code>start</code> or ended at <code>end</code>.
1076 *
1077 * @throws IndexOutOfBoundsException if any of the copied spans
1078 * are out of range in <code>dest</code>.
1079 */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001080 public static void copySpansFrom(Spanned source, int start, int end,
1081 Class kind,
1082 Spannable dest, int destoff) {
1083 if (kind == null) {
1084 kind = Object.class;
1085 }
1086
1087 Object[] spans = source.getSpans(start, end, kind);
1088
1089 for (int i = 0; i < spans.length; i++) {
1090 int st = source.getSpanStart(spans[i]);
1091 int en = source.getSpanEnd(spans[i]);
1092 int fl = source.getSpanFlags(spans[i]);
1093
1094 if (st < start)
1095 st = start;
1096 if (en > end)
1097 en = end;
1098
1099 dest.setSpan(spans[i], st - start + destoff, en - start + destoff,
1100 fl);
1101 }
1102 }
1103
Roozbeh Pournader205a9932017-06-08 00:23:42 -07001104 /**
1105 * Transforms a CharSequences to uppercase, copying the sources spans and keeping them spans as
1106 * much as possible close to their relative original places. In the case the the uppercase
1107 * string is identical to the sources, the source itself is returned instead of being copied.
1108 *
1109 * If copySpans is set, source must be an instance of Spanned.
1110 *
1111 * {@hide}
1112 */
1113 @NonNull
1114 public static CharSequence toUpperCase(@Nullable Locale locale, @NonNull CharSequence source,
1115 boolean copySpans) {
1116 final Edits edits = new Edits();
1117 if (!copySpans) { // No spans. Just uppercase the characters.
1118 final StringBuilder result = CaseMap.toUpper().apply(
1119 locale, source, new StringBuilder(), edits);
1120 return edits.hasChanges() ? result : source;
1121 }
1122
1123 final SpannableStringBuilder result = CaseMap.toUpper().apply(
1124 locale, source, new SpannableStringBuilder(), edits);
1125 if (!edits.hasChanges()) {
1126 // No changes happened while capitalizing. We can return the source as it was.
1127 return source;
1128 }
1129
1130 final Edits.Iterator iterator = edits.getFineIterator();
1131 final int sourceLength = source.length();
1132 final Spanned spanned = (Spanned) source;
1133 final Object[] spans = spanned.getSpans(0, sourceLength, Object.class);
1134 for (Object span : spans) {
1135 final int sourceStart = spanned.getSpanStart(span);
1136 final int sourceEnd = spanned.getSpanEnd(span);
1137 final int flags = spanned.getSpanFlags(span);
1138 // Make sure the indices are not at the end of the string, since in that case
1139 // iterator.findSourceIndex() would fail.
1140 final int destStart = sourceStart == sourceLength ? result.length() :
1141 toUpperMapToDest(iterator, sourceStart);
1142 final int destEnd = sourceEnd == sourceLength ? result.length() :
1143 toUpperMapToDest(iterator, sourceEnd);
1144 result.setSpan(span, destStart, destEnd, flags);
1145 }
1146 return result;
1147 }
1148
1149 // helper method for toUpperCase()
1150 private static int toUpperMapToDest(Edits.Iterator iterator, int sourceIndex) {
1151 // Guaranteed to succeed if sourceIndex < source.length().
1152 iterator.findSourceIndex(sourceIndex);
1153 if (sourceIndex == iterator.sourceIndex()) {
1154 return iterator.destinationIndex();
1155 }
1156 // We handle the situation differently depending on if we are in the changed slice or an
1157 // unchanged one: In an unchanged slice, we can find the exact location the span
1158 // boundary was before and map there.
1159 //
1160 // But in a changed slice, we need to treat the whole destination slice as an atomic unit.
1161 // We adjust the span boundary to the end of that slice to reduce of the chance of adjacent
1162 // spans in the source overlapping in the result. (The choice for the end vs the beginning
1163 // is somewhat arbitrary, but was taken because we except to see slightly more spans only
1164 // affecting a base character compared to spans only affecting a combining character.)
1165 if (iterator.hasChange()) {
1166 return iterator.destinationIndex() + iterator.newLength();
1167 } else {
1168 // Move the index 1:1 along with this unchanged piece of text.
1169 return iterator.destinationIndex() + (sourceIndex - iterator.sourceIndex());
1170 }
1171 }
1172
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001173 public enum TruncateAt {
1174 START,
1175 MIDDLE,
1176 END,
1177 MARQUEE,
Fabrice Di Megliocb332642011-09-23 19:08:04 -07001178 /**
1179 * @hide
1180 */
1181 END_SMALL
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001182 }
1183
1184 public interface EllipsizeCallback {
1185 /**
1186 * This method is called to report that the specified region of
1187 * text was ellipsized away by a call to {@link #ellipsize}.
1188 */
1189 public void ellipsized(int start, int end);
1190 }
1191
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001192 /**
1193 * Returns the original text if it fits in the specified width
1194 * given the properties of the specified Paint,
1195 * or, if it does not fit, a truncated
1196 * copy with ellipsis character added at the specified edge or center.
1197 */
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001198 @NonNull
1199 public static CharSequence ellipsize(@NonNull CharSequence text,
1200 @NonNull TextPaint p,
1201 @FloatRange(from = 0.0) float avail,
1202 @NonNull TruncateAt where) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001203 return ellipsize(text, p, avail, where, false, null);
1204 }
1205
1206 /**
1207 * Returns the original text if it fits in the specified width
1208 * given the properties of the specified Paint,
Doug Felte8e45f22010-03-29 14:58:40 -07001209 * or, if it does not fit, a copy with ellipsis character added
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001210 * at the specified edge or center.
1211 * If <code>preserveLength</code> is specified, the returned copy
1212 * will be padded with zero-width spaces to preserve the original
1213 * length and offsets instead of truncating.
1214 * If <code>callback</code> is non-null, it will be called to
Doug Feltcb3791202011-07-07 11:57:48 -07001215 * report the start and end of the ellipsized range. TextDirection
1216 * is determined by the first strong directional character.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001217 */
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001218 @NonNull
1219 public static CharSequence ellipsize(@NonNull CharSequence text,
1220 @NonNull TextPaint paint,
1221 @FloatRange(from = 0.0) float avail,
1222 @NonNull TruncateAt where,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001223 boolean preserveLength,
Roozbeh Pournader9ea756f2017-07-25 11:20:29 -07001224 @Nullable EllipsizeCallback callback) {
Doug Feltcb3791202011-07-07 11:57:48 -07001225 return ellipsize(text, paint, avail, where, preserveLength, callback,
Fabrice Di Megliocb332642011-09-23 19:08:04 -07001226 TextDirectionHeuristics.FIRSTSTRONG_LTR,
Roozbeh Pournader9ea756f2017-07-25 11:20:29 -07001227 getEllipsisString(where));
Doug Feltcb3791202011-07-07 11:57:48 -07001228 }
1229
1230 /**
1231 * Returns the original text if it fits in the specified width
1232 * given the properties of the specified Paint,
1233 * or, if it does not fit, a copy with ellipsis character added
1234 * at the specified edge or center.
1235 * If <code>preserveLength</code> is specified, the returned copy
1236 * will be padded with zero-width spaces to preserve the original
1237 * length and offsets instead of truncating.
1238 * If <code>callback</code> is non-null, it will be called to
1239 * report the start and end of the ellipsized range.
1240 *
1241 * @hide
1242 */
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001243 @NonNull
1244 public static CharSequence ellipsize(@NonNull CharSequence text,
1245 @NonNull TextPaint paint,
1246 @FloatRange(from = 0.0) float avail,
1247 @NonNull TruncateAt where,
Doug Feltcb3791202011-07-07 11:57:48 -07001248 boolean preserveLength,
Roozbeh Pournader9ea756f2017-07-25 11:20:29 -07001249 @Nullable EllipsizeCallback callback,
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001250 @NonNull TextDirectionHeuristic textDir,
1251 @NonNull String ellipsis) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001252
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001253 final int len = text.length();
1254 final MeasuredText mt = MeasuredText.obtain();
1255 MeasuredText resultMt = null;
Doug Felte8e45f22010-03-29 14:58:40 -07001256 try {
Doug Feltcb3791202011-07-07 11:57:48 -07001257 float width = setPara(mt, paint, text, 0, text.length(), textDir);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001258
Doug Felte8e45f22010-03-29 14:58:40 -07001259 if (width <= avail) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001260 if (callback != null) {
1261 callback.ellipsized(0, 0);
1262 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001263 return text;
1264 }
1265
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001266 resultMt = MeasuredText.obtain();
1267 // First estimate of effective width of ellipsis.
1268 float ellipsisWidth = paint.measureText(ellipsis);
1269 int numberOfTries = 0;
1270 boolean textFits = false;
1271 int start, end;
1272 CharSequence result;
1273 do {
1274 if (avail < ellipsisWidth) {
1275 // Even the ellipsis can't fit. So it all goes.
1276 start = 0;
1277 end = len;
1278 } else {
1279 final float remainingWidth = avail - ellipsisWidth;
1280 if (where == TruncateAt.START) {
1281 start = 0;
1282 end = len - mt.breakText(len, false /* backwards */, remainingWidth);
1283 } else if (where == TruncateAt.END || where == TruncateAt.END_SMALL) {
1284 start = mt.breakText(len, true /* forwards */, remainingWidth);
1285 end = len;
1286 } else {
1287 end = len - mt.breakText(len, false /* backwards */, remainingWidth / 2);
1288 start = mt.breakText(end, true /* forwards */,
1289 remainingWidth - mt.measure(end, len));
1290 }
1291 }
Roozbeh Pournader287c8d62017-07-25 13:52:57 -07001292
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001293 final char[] buf = mt.mChars;
1294 final Spanned sp = text instanceof Spanned ? (Spanned) text : null;
Roozbeh Pournader7f0ebc92017-08-02 22:47:14 +00001295
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001296 final int removed = end - start;
1297 final int remaining = len - removed;
1298 if (preserveLength) {
1299 int pos = start;
1300 if (remaining > 0 && removed >= ellipsis.length()) {
1301 ellipsis.getChars(0, ellipsis.length(), buf, start);
1302 pos += ellipsis.length();
1303 } // else eliminate the ellipsis
1304 while (pos < end) {
1305 buf[pos++] = ELLIPSIS_FILLER;
1306 }
1307 final String s = new String(buf, 0, len);
1308 if (sp == null) {
1309 result = s;
1310 } else {
1311 final SpannableString ss = new SpannableString(s);
1312 copySpansFrom(sp, 0, len, Object.class, ss, 0);
1313 result = ss;
1314 }
1315 } else {
1316 if (remaining == 0) {
1317 result = "";
1318 } else if (sp == null) {
1319 final StringBuilder sb = new StringBuilder(remaining + ellipsis.length());
1320 sb.append(buf, 0, start);
1321 sb.append(ellipsis);
1322 sb.append(buf, end, len - end);
1323 result = sb.toString();
1324 } else {
1325 final SpannableStringBuilder ssb = new SpannableStringBuilder();
1326 ssb.append(text, 0, start);
1327 ssb.append(ellipsis);
1328 ssb.append(text, end, len);
1329 result = ssb;
1330 }
1331 }
1332
1333 if (remaining == 0) { // All text is gone.
1334 textFits = true;
1335 } else {
1336 width = setPara(resultMt, paint, result, 0, result.length(), textDir);
1337 if (width <= avail) {
1338 textFits = true;
1339 } else {
1340 numberOfTries++;
1341 if (numberOfTries > 10) {
1342 // If the text still doesn't fit after ten tries, assume it will never
1343 // fit and ellipsize it all. We do this by setting the width of the
1344 // ellipsis to be positive infinity, so we get to empty text in the next
1345 // round.
1346 ellipsisWidth = Float.POSITIVE_INFINITY;
1347 } else {
1348 // Adjust the width of the ellipsis by adding the amount 'width' is
1349 // still over.
1350 ellipsisWidth += width - avail;
1351 }
1352 }
1353 }
1354 } while (!textFits);
Roozbeh Pournader7f0ebc92017-08-02 22:47:14 +00001355 if (callback != null) {
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001356 callback.ellipsized(start, end);
Roozbeh Pournader7f0ebc92017-08-02 22:47:14 +00001357 }
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001358 return result;
Doug Felte8e45f22010-03-29 14:58:40 -07001359 } finally {
1360 MeasuredText.recycle(mt);
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001361 if (resultMt != null) {
1362 MeasuredText.recycle(resultMt);
1363 }
Doug Felte8e45f22010-03-29 14:58:40 -07001364 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001365 }
1366
1367 /**
Roozbeh Pournader3bfce332016-06-17 15:03:56 -07001368 * Formats a list of CharSequences by repeatedly inserting the separator between them,
1369 * but stopping when the resulting sequence is too wide for the specified width.
1370 *
1371 * This method actually tries to fit the maximum number of elements. So if {@code "A, 11 more"
1372 * fits}, {@code "A, B, 10 more"} doesn't fit, but {@code "A, B, C, 9 more"} fits again (due to
1373 * the glyphs for the digits being very wide, for example), it returns
1374 * {@code "A, B, C, 9 more"}. Because of this, this method may be inefficient for very long
1375 * lists.
1376 *
1377 * Note that the elements of the returned value, as well as the string for {@code moreId}, will
1378 * be bidi-wrapped using {@link BidiFormatter#unicodeWrap} based on the locale of the input
1379 * Context. If the input {@code Context} is null, the default BidiFormatter from
1380 * {@link BidiFormatter#getInstance()} will be used.
1381 *
1382 * @param context the {@code Context} to get the {@code moreId} resource from. If {@code null},
1383 * an ellipsis (U+2026) would be used for {@code moreId}.
1384 * @param elements the list to format
1385 * @param separator a separator, such as {@code ", "}
1386 * @param paint the Paint with which to measure the text
1387 * @param avail the horizontal width available for the text (in pixels)
1388 * @param moreId the resource ID for the pluralized string to insert at the end of sequence when
1389 * some of the elements don't fit.
1390 *
1391 * @return the formatted CharSequence. If even the shortest sequence (e.g. {@code "A, 11 more"})
1392 * doesn't fit, it will return an empty string.
1393 */
Roozbeh Pournader3bfce332016-06-17 15:03:56 -07001394 public static CharSequence listEllipsize(@Nullable Context context,
1395 @Nullable List<CharSequence> elements, @NonNull String separator,
1396 @NonNull TextPaint paint, @FloatRange(from=0.0,fromInclusive=false) float avail,
1397 @PluralsRes int moreId) {
1398 if (elements == null) {
1399 return "";
1400 }
1401 final int totalLen = elements.size();
1402 if (totalLen == 0) {
1403 return "";
1404 }
1405
1406 final Resources res;
1407 final BidiFormatter bidiFormatter;
1408 if (context == null) {
1409 res = null;
1410 bidiFormatter = BidiFormatter.getInstance();
1411 } else {
1412 res = context.getResources();
1413 bidiFormatter = BidiFormatter.getInstance(res.getConfiguration().getLocales().get(0));
1414 }
1415
1416 final SpannableStringBuilder output = new SpannableStringBuilder();
1417 final int[] endIndexes = new int[totalLen];
1418 for (int i = 0; i < totalLen; i++) {
1419 output.append(bidiFormatter.unicodeWrap(elements.get(i)));
1420 if (i != totalLen - 1) { // Insert a separator, except at the very end.
1421 output.append(separator);
1422 }
1423 endIndexes[i] = output.length();
1424 }
1425
1426 for (int i = totalLen - 1; i >= 0; i--) {
1427 // Delete the tail of the string, cutting back to one less element.
1428 output.delete(endIndexes[i], output.length());
1429
1430 final int remainingElements = totalLen - i - 1;
1431 if (remainingElements > 0) {
1432 CharSequence morePiece = (res == null) ?
Roozbeh Pournader9ea756f2017-07-25 11:20:29 -07001433 ELLIPSIS_NORMAL :
Roozbeh Pournader3bfce332016-06-17 15:03:56 -07001434 res.getQuantityString(moreId, remainingElements, remainingElements);
1435 morePiece = bidiFormatter.unicodeWrap(morePiece);
1436 output.append(morePiece);
1437 }
1438
1439 final float width = paint.measureText(output, 0, output.length());
1440 if (width <= avail) { // The string fits.
1441 return output;
1442 }
1443 }
1444 return ""; // Nothing fits.
1445 }
1446
1447 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001448 * Converts a CharSequence of the comma-separated form "Andy, Bob,
1449 * Charles, David" that is too wide to fit into the specified width
1450 * into one like "Andy, Bob, 2 more".
1451 *
1452 * @param text the text to truncate
1453 * @param p the Paint with which to measure the text
Roozbeh Pournader3bfce332016-06-17 15:03:56 -07001454 * @param avail the horizontal width available for the text (in pixels)
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001455 * @param oneMore the string for "1 more" in the current locale
1456 * @param more the string for "%d more" in the current locale
Roozbeh Pournader3bfce332016-06-17 15:03:56 -07001457 *
1458 * @deprecated Do not use. This is not internationalized, and has known issues
1459 * with right-to-left text, languages that have more than one plural form, languages
1460 * that use a different character as a comma-like separator, etc.
1461 * Use {@link #listEllipsize} instead.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001462 */
Roozbeh Pournader3bfce332016-06-17 15:03:56 -07001463 @Deprecated
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001464 public static CharSequence commaEllipsize(CharSequence text,
1465 TextPaint p, float avail,
1466 String oneMore,
1467 String more) {
Doug Feltcb3791202011-07-07 11:57:48 -07001468 return commaEllipsize(text, p, avail, oneMore, more,
1469 TextDirectionHeuristics.FIRSTSTRONG_LTR);
1470 }
1471
1472 /**
1473 * @hide
1474 */
Roozbeh Pournader3bfce332016-06-17 15:03:56 -07001475 @Deprecated
Doug Feltcb3791202011-07-07 11:57:48 -07001476 public static CharSequence commaEllipsize(CharSequence text, TextPaint p,
1477 float avail, String oneMore, String more, TextDirectionHeuristic textDir) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001478
Doug Felte8e45f22010-03-29 14:58:40 -07001479 MeasuredText mt = MeasuredText.obtain();
1480 try {
1481 int len = text.length();
Doug Feltcb3791202011-07-07 11:57:48 -07001482 float width = setPara(mt, p, text, 0, len, textDir);
Doug Felte8e45f22010-03-29 14:58:40 -07001483 if (width <= avail) {
1484 return text;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001485 }
1486
Doug Felte8e45f22010-03-29 14:58:40 -07001487 char[] buf = mt.mChars;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001488
Doug Felte8e45f22010-03-29 14:58:40 -07001489 int commaCount = 0;
1490 for (int i = 0; i < len; i++) {
1491 if (buf[i] == ',') {
1492 commaCount++;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001493 }
1494 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001495
Doug Felte8e45f22010-03-29 14:58:40 -07001496 int remaining = commaCount + 1;
1497
1498 int ok = 0;
Doug Felte8e45f22010-03-29 14:58:40 -07001499 String okFormat = "";
1500
1501 int w = 0;
1502 int count = 0;
1503 float[] widths = mt.mWidths;
1504
Doug Felte8e45f22010-03-29 14:58:40 -07001505 MeasuredText tempMt = MeasuredText.obtain();
1506 for (int i = 0; i < len; i++) {
1507 w += widths[i];
1508
1509 if (buf[i] == ',') {
1510 count++;
1511
1512 String format;
1513 // XXX should not insert spaces, should be part of string
1514 // XXX should use plural rules and not assume English plurals
1515 if (--remaining == 1) {
1516 format = " " + oneMore;
1517 } else {
1518 format = " " + String.format(more, remaining);
1519 }
1520
1521 // XXX this is probably ok, but need to look at it more
Raph Levien70616ec2015-03-04 10:41:30 -08001522 tempMt.setPara(format, 0, format.length(), textDir, null);
Brian Muramatsu4c8ad6e2011-01-27 18:13:39 -08001523 float moreWid = tempMt.addStyleRun(p, tempMt.mLen, null);
Doug Felte8e45f22010-03-29 14:58:40 -07001524
1525 if (w + moreWid <= avail) {
1526 ok = i + 1;
Doug Felte8e45f22010-03-29 14:58:40 -07001527 okFormat = format;
1528 }
1529 }
1530 }
1531 MeasuredText.recycle(tempMt);
1532
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001533 SpannableStringBuilder out = new SpannableStringBuilder(okFormat);
1534 out.insert(0, text, 0, ok);
1535 return out;
Doug Felte8e45f22010-03-29 14:58:40 -07001536 } finally {
1537 MeasuredText.recycle(mt);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001538 }
1539 }
1540
Doug Felte8e45f22010-03-29 14:58:40 -07001541 private static float setPara(MeasuredText mt, TextPaint paint,
Doug Feltcb3791202011-07-07 11:57:48 -07001542 CharSequence text, int start, int end, TextDirectionHeuristic textDir) {
Doug Felte8e45f22010-03-29 14:58:40 -07001543
Raph Levien70616ec2015-03-04 10:41:30 -08001544 mt.setPara(text, start, end, textDir, null);
Doug Felte8e45f22010-03-29 14:58:40 -07001545
1546 float width;
1547 Spanned sp = text instanceof Spanned ? (Spanned) text : null;
1548 int len = end - start;
1549 if (sp == null) {
1550 width = mt.addStyleRun(paint, len, null);
1551 } else {
1552 width = 0;
1553 int spanEnd;
1554 for (int spanStart = 0; spanStart < len; spanStart = spanEnd) {
1555 spanEnd = sp.nextSpanTransition(spanStart, len,
1556 MetricAffectingSpan.class);
1557 MetricAffectingSpan[] spans = sp.getSpans(
1558 spanStart, spanEnd, MetricAffectingSpan.class);
Gilles Debunne1e3ac182011-03-08 14:22:34 -08001559 spans = TextUtils.removeEmptySpans(spans, sp, MetricAffectingSpan.class);
Doug Felte8e45f22010-03-29 14:58:40 -07001560 width += mt.addStyleRun(paint, spans, spanEnd - spanStart, null);
1561 }
1562 }
1563
1564 return width;
1565 }
1566
Roozbeh Pournader8823c852016-06-09 18:36:47 -07001567 // Returns true if the character's presence could affect RTL layout.
1568 //
1569 // In order to be fast, the code is intentionally rough and quite conservative in its
1570 // considering inclusion of any non-BMP or surrogate characters or anything in the bidi
1571 // blocks or any bidi formatting characters with a potential to affect RTL layout.
Doug Felte8e45f22010-03-29 14:58:40 -07001572 /* package */
Roozbeh Pournader8823c852016-06-09 18:36:47 -07001573 static boolean couldAffectRtl(char c) {
1574 return (0x0590 <= c && c <= 0x08FF) || // RTL scripts
1575 c == 0x200E || // Bidi format character
1576 c == 0x200F || // Bidi format character
1577 (0x202A <= c && c <= 0x202E) || // Bidi format characters
1578 (0x2066 <= c && c <= 0x2069) || // Bidi format characters
1579 (0xD800 <= c && c <= 0xDFFF) || // Surrogate pairs
1580 (0xFB1D <= c && c <= 0xFDFF) || // Hebrew and Arabic presentation forms
1581 (0xFE70 <= c && c <= 0xFEFE); // Arabic presentation forms
Doug Felte8e45f22010-03-29 14:58:40 -07001582 }
1583
Roozbeh Pournader8823c852016-06-09 18:36:47 -07001584 // Returns true if there is no character present that may potentially affect RTL layout.
1585 // Since this calls couldAffectRtl() above, it's also quite conservative, in the way that
1586 // it may return 'false' (needs bidi) although careful consideration may tell us it should
1587 // return 'true' (does not need bidi).
Doug Felte8e45f22010-03-29 14:58:40 -07001588 /* package */
1589 static boolean doesNotNeedBidi(char[] text, int start, int len) {
Roozbeh Pournader8823c852016-06-09 18:36:47 -07001590 final int end = start + len;
1591 for (int i = start; i < end; i++) {
1592 if (couldAffectRtl(text[i])) {
Doug Felte8e45f22010-03-29 14:58:40 -07001593 return false;
1594 }
1595 }
1596 return true;
1597 }
1598
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001599 /* package */ static char[] obtain(int len) {
1600 char[] buf;
1601
1602 synchronized (sLock) {
1603 buf = sTemp;
1604 sTemp = null;
1605 }
1606
1607 if (buf == null || buf.length < len)
Adam Lesinski776abc22014-03-07 11:30:59 -05001608 buf = ArrayUtils.newUnpaddedCharArray(len);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001609
1610 return buf;
1611 }
1612
1613 /* package */ static void recycle(char[] temp) {
1614 if (temp.length > 1000)
1615 return;
1616
1617 synchronized (sLock) {
1618 sTemp = temp;
1619 }
1620 }
1621
1622 /**
1623 * Html-encode the string.
1624 * @param s the string to be encoded
1625 * @return the encoded string
1626 */
1627 public static String htmlEncode(String s) {
1628 StringBuilder sb = new StringBuilder();
1629 char c;
1630 for (int i = 0; i < s.length(); i++) {
1631 c = s.charAt(i);
1632 switch (c) {
1633 case '<':
1634 sb.append("&lt;"); //$NON-NLS-1$
1635 break;
1636 case '>':
1637 sb.append("&gt;"); //$NON-NLS-1$
1638 break;
1639 case '&':
1640 sb.append("&amp;"); //$NON-NLS-1$
1641 break;
1642 case '\'':
Marc Blankf4832da2012-02-13 10:11:50 -08001643 //http://www.w3.org/TR/xhtml1
1644 // The named character reference &apos; (the apostrophe, U+0027) was introduced in
1645 // XML 1.0 but does not appear in HTML. Authors should therefore use &#39; instead
1646 // of &apos; to work as expected in HTML 4 user agents.
1647 sb.append("&#39;"); //$NON-NLS-1$
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001648 break;
1649 case '"':
1650 sb.append("&quot;"); //$NON-NLS-1$
1651 break;
1652 default:
1653 sb.append(c);
1654 }
1655 }
1656 return sb.toString();
1657 }
1658
1659 /**
1660 * Returns a CharSequence concatenating the specified CharSequences,
1661 * retaining their spans if any.
Roozbeh Pournadere57886e2017-05-02 18:10:10 -07001662 *
1663 * If there are no parameters, an empty string will be returned.
1664 *
1665 * If the number of parameters is exactly one, that parameter is returned as output, even if it
1666 * is null.
1667 *
1668 * If the number of parameters is at least two, any null CharSequence among the parameters is
1669 * treated as if it was the string <code>"null"</code>.
1670 *
1671 * If there are paragraph spans in the source CharSequences that satisfy paragraph boundary
1672 * requirements in the sources but would no longer satisfy them in the concatenated
1673 * CharSequence, they may get extended in the resulting CharSequence or not retained.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001674 */
1675 public static CharSequence concat(CharSequence... text) {
1676 if (text.length == 0) {
1677 return "";
1678 }
1679
1680 if (text.length == 1) {
1681 return text[0];
1682 }
1683
1684 boolean spanned = false;
Roozbeh Pournadere57886e2017-05-02 18:10:10 -07001685 for (CharSequence piece : text) {
1686 if (piece instanceof Spanned) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001687 spanned = true;
1688 break;
1689 }
1690 }
1691
Roozbeh Pournadere57886e2017-05-02 18:10:10 -07001692 if (spanned) {
1693 final SpannableStringBuilder ssb = new SpannableStringBuilder();
1694 for (CharSequence piece : text) {
1695 // If a piece is null, we append the string "null" for compatibility with the
1696 // behavior of StringBuilder and the behavior of the concat() method in earlier
1697 // versions of Android.
1698 ssb.append(piece == null ? "null" : piece);
1699 }
1700 return new SpannedString(ssb);
1701 } else {
1702 final StringBuilder sb = new StringBuilder();
1703 for (CharSequence piece : text) {
1704 sb.append(piece);
1705 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001706 return sb.toString();
1707 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001708 }
1709
1710 /**
1711 * Returns whether the given CharSequence contains any printable characters.
1712 */
1713 public static boolean isGraphic(CharSequence str) {
1714 final int len = str.length();
Roozbeh Pournadera93880e2015-08-10 17:23:05 -07001715 for (int cp, i=0; i<len; i+=Character.charCount(cp)) {
Roozbeh Pournader1cc2acf2015-08-11 10:37:07 -07001716 cp = Character.codePointAt(str, i);
Roozbeh Pournadera93880e2015-08-10 17:23:05 -07001717 int gc = Character.getType(cp);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001718 if (gc != Character.CONTROL
1719 && gc != Character.FORMAT
1720 && gc != Character.SURROGATE
1721 && gc != Character.UNASSIGNED
1722 && gc != Character.LINE_SEPARATOR
1723 && gc != Character.PARAGRAPH_SEPARATOR
1724 && gc != Character.SPACE_SEPARATOR) {
1725 return true;
1726 }
1727 }
1728 return false;
1729 }
1730
1731 /**
1732 * Returns whether this character is a printable character.
Roozbeh Pournadera93880e2015-08-10 17:23:05 -07001733 *
1734 * This does not support non-BMP characters and should not be used.
1735 *
1736 * @deprecated Use {@link #isGraphic(CharSequence)} instead.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001737 */
Roozbeh Pournadera93880e2015-08-10 17:23:05 -07001738 @Deprecated
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001739 public static boolean isGraphic(char c) {
1740 int gc = Character.getType(c);
1741 return gc != Character.CONTROL
1742 && gc != Character.FORMAT
1743 && gc != Character.SURROGATE
1744 && gc != Character.UNASSIGNED
1745 && gc != Character.LINE_SEPARATOR
1746 && gc != Character.PARAGRAPH_SEPARATOR
1747 && gc != Character.SPACE_SEPARATOR;
1748 }
1749
1750 /**
1751 * Returns whether the given CharSequence contains only digits.
1752 */
1753 public static boolean isDigitsOnly(CharSequence str) {
1754 final int len = str.length();
Roozbeh Pournader3efda952015-08-11 09:55:57 -07001755 for (int cp, i = 0; i < len; i += Character.charCount(cp)) {
1756 cp = Character.codePointAt(str, i);
1757 if (!Character.isDigit(cp)) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001758 return false;
1759 }
1760 }
1761 return true;
1762 }
1763
1764 /**
Daisuke Miyakawa973afa92009-12-03 10:43:45 +09001765 * @hide
1766 */
1767 public static boolean isPrintableAscii(final char c) {
1768 final int asciiFirst = 0x20;
1769 final int asciiLast = 0x7E; // included
1770 return (asciiFirst <= c && c <= asciiLast) || c == '\r' || c == '\n';
1771 }
1772
1773 /**
1774 * @hide
1775 */
1776 public static boolean isPrintableAsciiOnly(final CharSequence str) {
1777 final int len = str.length();
1778 for (int i = 0; i < len; i++) {
1779 if (!isPrintableAscii(str.charAt(i))) {
1780 return false;
1781 }
1782 }
1783 return true;
1784 }
1785
1786 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001787 * Capitalization mode for {@link #getCapsMode}: capitalize all
1788 * characters. This value is explicitly defined to be the same as
1789 * {@link InputType#TYPE_TEXT_FLAG_CAP_CHARACTERS}.
1790 */
1791 public static final int CAP_MODE_CHARACTERS
1792 = InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS;
Doug Felte8e45f22010-03-29 14:58:40 -07001793
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001794 /**
1795 * Capitalization mode for {@link #getCapsMode}: capitalize the first
1796 * character of all words. This value is explicitly defined to be the same as
1797 * {@link InputType#TYPE_TEXT_FLAG_CAP_WORDS}.
1798 */
1799 public static final int CAP_MODE_WORDS
1800 = InputType.TYPE_TEXT_FLAG_CAP_WORDS;
Doug Felte8e45f22010-03-29 14:58:40 -07001801
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001802 /**
1803 * Capitalization mode for {@link #getCapsMode}: capitalize the first
1804 * character of each sentence. This value is explicitly defined to be the same as
1805 * {@link InputType#TYPE_TEXT_FLAG_CAP_SENTENCES}.
1806 */
1807 public static final int CAP_MODE_SENTENCES
1808 = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
Doug Felte8e45f22010-03-29 14:58:40 -07001809
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001810 /**
1811 * Determine what caps mode should be in effect at the current offset in
1812 * the text. Only the mode bits set in <var>reqModes</var> will be
1813 * checked. Note that the caps mode flags here are explicitly defined
1814 * to match those in {@link InputType}.
Doug Felte8e45f22010-03-29 14:58:40 -07001815 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001816 * @param cs The text that should be checked for caps modes.
1817 * @param off Location in the text at which to check.
1818 * @param reqModes The modes to be checked: may be any combination of
1819 * {@link #CAP_MODE_CHARACTERS}, {@link #CAP_MODE_WORDS}, and
1820 * {@link #CAP_MODE_SENTENCES}.
Mark Wagner60919952010-03-01 09:24:59 -08001821 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001822 * @return Returns the actual capitalization modes that can be in effect
1823 * at the current position, which is any combination of
1824 * {@link #CAP_MODE_CHARACTERS}, {@link #CAP_MODE_WORDS}, and
1825 * {@link #CAP_MODE_SENTENCES}.
1826 */
1827 public static int getCapsMode(CharSequence cs, int off, int reqModes) {
Mark Wagner60919952010-03-01 09:24:59 -08001828 if (off < 0) {
1829 return 0;
1830 }
1831
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001832 int i;
1833 char c;
1834 int mode = 0;
1835
1836 if ((reqModes&CAP_MODE_CHARACTERS) != 0) {
1837 mode |= CAP_MODE_CHARACTERS;
1838 }
1839 if ((reqModes&(CAP_MODE_WORDS|CAP_MODE_SENTENCES)) == 0) {
1840 return mode;
1841 }
1842
1843 // Back over allowed opening punctuation.
1844
1845 for (i = off; i > 0; i--) {
1846 c = cs.charAt(i - 1);
1847
1848 if (c != '"' && c != '\'' &&
1849 Character.getType(c) != Character.START_PUNCTUATION) {
1850 break;
1851 }
1852 }
1853
1854 // Start of paragraph, with optional whitespace.
1855
1856 int j = i;
1857 while (j > 0 && ((c = cs.charAt(j - 1)) == ' ' || c == '\t')) {
1858 j--;
1859 }
1860 if (j == 0 || cs.charAt(j - 1) == '\n') {
1861 return mode | CAP_MODE_WORDS;
1862 }
1863
1864 // Or start of word if we are that style.
1865
1866 if ((reqModes&CAP_MODE_SENTENCES) == 0) {
1867 if (i != j) mode |= CAP_MODE_WORDS;
1868 return mode;
1869 }
1870
1871 // There must be a space if not the start of paragraph.
1872
1873 if (i == j) {
1874 return mode;
1875 }
1876
1877 // Back over allowed closing punctuation.
1878
1879 for (; j > 0; j--) {
1880 c = cs.charAt(j - 1);
1881
1882 if (c != '"' && c != '\'' &&
1883 Character.getType(c) != Character.END_PUNCTUATION) {
1884 break;
1885 }
1886 }
1887
1888 if (j > 0) {
1889 c = cs.charAt(j - 1);
1890
1891 if (c == '.' || c == '?' || c == '!') {
1892 // Do not capitalize if the word ends with a period but
1893 // also contains a period, in which case it is an abbreviation.
1894
1895 if (c == '.') {
1896 for (int k = j - 2; k >= 0; k--) {
1897 c = cs.charAt(k);
1898
1899 if (c == '.') {
1900 return mode;
1901 }
1902
1903 if (!Character.isLetter(c)) {
1904 break;
1905 }
1906 }
1907 }
1908
1909 return mode | CAP_MODE_SENTENCES;
1910 }
1911 }
1912
1913 return mode;
1914 }
Doug Felte8e45f22010-03-29 14:58:40 -07001915
Brad Fitzpatrick11fe1812010-09-10 16:07:52 -07001916 /**
1917 * Does a comma-delimited list 'delimitedString' contain a certain item?
1918 * (without allocating memory)
1919 *
1920 * @hide
1921 */
1922 public static boolean delimitedStringContains(
1923 String delimitedString, char delimiter, String item) {
1924 if (isEmpty(delimitedString) || isEmpty(item)) {
1925 return false;
1926 }
1927 int pos = -1;
1928 int length = delimitedString.length();
1929 while ((pos = delimitedString.indexOf(item, pos + 1)) != -1) {
1930 if (pos > 0 && delimitedString.charAt(pos - 1) != delimiter) {
1931 continue;
1932 }
1933 int expectedDelimiterPos = pos + item.length();
1934 if (expectedDelimiterPos == length) {
1935 // Match at end of string.
1936 return true;
1937 }
1938 if (delimitedString.charAt(expectedDelimiterPos) == delimiter) {
1939 return true;
1940 }
1941 }
1942 return false;
1943 }
1944
Gilles Debunne1e3ac182011-03-08 14:22:34 -08001945 /**
1946 * Removes empty spans from the <code>spans</code> array.
1947 *
1948 * When parsing a Spanned using {@link Spanned#nextSpanTransition(int, int, Class)}, empty spans
1949 * will (correctly) create span transitions, and calling getSpans on a slice of text bounded by
1950 * one of these transitions will (correctly) include the empty overlapping span.
1951 *
1952 * However, these empty spans should not be taken into account when layouting or rendering the
1953 * string and this method provides a way to filter getSpans' results accordingly.
1954 *
1955 * @param spans A list of spans retrieved using {@link Spanned#getSpans(int, int, Class)} from
1956 * the <code>spanned</code>
1957 * @param spanned The Spanned from which spans were extracted
1958 * @return A subset of spans where empty spans ({@link Spanned#getSpanStart(Object)} ==
1959 * {@link Spanned#getSpanEnd(Object)} have been removed. The initial order is preserved
1960 * @hide
1961 */
1962 @SuppressWarnings("unchecked")
1963 public static <T> T[] removeEmptySpans(T[] spans, Spanned spanned, Class<T> klass) {
1964 T[] copy = null;
1965 int count = 0;
1966
1967 for (int i = 0; i < spans.length; i++) {
1968 final T span = spans[i];
1969 final int start = spanned.getSpanStart(span);
1970 final int end = spanned.getSpanEnd(span);
1971
1972 if (start == end) {
1973 if (copy == null) {
1974 copy = (T[]) Array.newInstance(klass, spans.length - 1);
1975 System.arraycopy(spans, 0, copy, 0, i);
1976 count = i;
1977 }
1978 } else {
1979 if (copy != null) {
1980 copy[count] = span;
1981 count++;
1982 }
1983 }
1984 }
1985
1986 if (copy != null) {
1987 T[] result = (T[]) Array.newInstance(klass, count);
1988 System.arraycopy(copy, 0, result, 0, count);
1989 return result;
1990 } else {
1991 return spans;
1992 }
1993 }
1994
Gilles Debunne6c488de2012-03-01 16:20:35 -08001995 /**
1996 * Pack 2 int values into a long, useful as a return value for a range
1997 * @see #unpackRangeStartFromLong(long)
1998 * @see #unpackRangeEndFromLong(long)
1999 * @hide
2000 */
2001 public static long packRangeInLong(int start, int end) {
2002 return (((long) start) << 32) | end;
2003 }
2004
2005 /**
2006 * Get the start value from a range packed in a long by {@link #packRangeInLong(int, int)}
2007 * @see #unpackRangeEndFromLong(long)
2008 * @see #packRangeInLong(int, int)
2009 * @hide
2010 */
2011 public static int unpackRangeStartFromLong(long range) {
2012 return (int) (range >>> 32);
2013 }
2014
2015 /**
2016 * Get the end value from a range packed in a long by {@link #packRangeInLong(int, int)}
2017 * @see #unpackRangeStartFromLong(long)
2018 * @see #packRangeInLong(int, int)
2019 * @hide
2020 */
2021 public static int unpackRangeEndFromLong(long range) {
2022 return (int) (range & 0x00000000FFFFFFFFL);
2023 }
2024
Fabrice Di Megliod3d9f3f2012-09-18 12:55:32 -07002025 /**
2026 * Return the layout direction for a given Locale
2027 *
2028 * @param locale the Locale for which we want the layout direction. Can be null.
2029 * @return the layout direction. This may be one of:
2030 * {@link android.view.View#LAYOUT_DIRECTION_LTR} or
2031 * {@link android.view.View#LAYOUT_DIRECTION_RTL}.
2032 *
2033 * Be careful: this code will need to be updated when vertical scripts will be supported
2034 */
2035 public static int getLayoutDirectionFromLocale(Locale locale) {
Roozbeh Pournader463b4822015-08-06 16:04:45 -07002036 return ((locale != null && !locale.equals(Locale.ROOT)
2037 && ULocale.forLocale(locale).isRightToLeft())
2038 // If forcing into RTL layout mode, return RTL as default
2039 || SystemProperties.getBoolean(Settings.Global.DEVELOPMENT_FORCE_RTL, false))
2040 ? View.LAYOUT_DIRECTION_RTL
2041 : View.LAYOUT_DIRECTION_LTR;
Fabrice Di Megliod3d9f3f2012-09-18 12:55:32 -07002042 }
2043
Jeff Sharkeyf491c722015-06-11 09:16:19 -07002044 /**
2045 * Return localized string representing the given number of selected items.
2046 *
2047 * @hide
2048 */
2049 public static CharSequence formatSelectedCount(int count) {
2050 return Resources.getSystem().getQuantityString(R.plurals.selected_count, count, count);
2051 }
2052
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +01002053 /**
2054 * Returns whether or not the specified spanned text has a style span.
2055 * @hide
2056 */
2057 public static boolean hasStyleSpan(@NonNull Spanned spanned) {
2058 Preconditions.checkArgument(spanned != null);
2059 final Class<?>[] styleClasses = {
2060 CharacterStyle.class, ParagraphStyle.class, UpdateAppearance.class};
2061 for (Class<?> clazz : styleClasses) {
2062 if (spanned.nextSpanTransition(-1, spanned.length(), clazz) < spanned.length()) {
2063 return true;
2064 }
2065 }
2066 return false;
2067 }
2068
Felipe Lemea8fce3b2017-04-04 14:22:12 -07002069 /**
2070 * If the {@code charSequence} is instance of {@link Spanned}, creates a new copy and
2071 * {@link NoCopySpan}'s are removed from the copy. Otherwise the given {@code charSequence} is
2072 * returned as it is.
2073 *
2074 * @hide
2075 */
2076 @Nullable
2077 public static CharSequence trimNoCopySpans(@Nullable CharSequence charSequence) {
2078 if (charSequence != null && charSequence instanceof Spanned) {
2079 // SpannableStringBuilder copy constructor trims NoCopySpans.
2080 return new SpannableStringBuilder(charSequence);
2081 }
2082 return charSequence;
2083 }
2084
Eugene Susla4a34f9c2017-05-16 14:16:38 -07002085 /**
2086 * Prepends {@code start} and appends {@code end} to a given {@link StringBuilder}
2087 *
2088 * @hide
2089 */
2090 public static void wrap(StringBuilder builder, String start, String end) {
2091 builder.insert(0, start);
2092 builder.append(end);
2093 }
2094
Siyamed Sinirce3b05a2017-07-18 18:54:31 -07002095 /**
2096 * Intent size limitations prevent sending over a megabyte of data. Limit
2097 * text length to 100K characters - 200KB.
2098 */
2099 private static final int PARCEL_SAFE_TEXT_LENGTH = 100000;
2100
2101 /**
2102 * Trims the text to {@link #PARCEL_SAFE_TEXT_LENGTH} length. Returns the string as it is if
2103 * the length() is smaller than {@link #PARCEL_SAFE_TEXT_LENGTH}. Used for text that is parceled
2104 * into a {@link Parcelable}.
2105 *
2106 * @hide
2107 */
2108 @Nullable
2109 public static <T extends CharSequence> T trimToParcelableSize(@Nullable T text) {
2110 return trimToSize(text, PARCEL_SAFE_TEXT_LENGTH);
2111 }
2112
2113 /**
2114 * Trims the text to {@code size} length. Returns the string as it is if the length() is
2115 * smaller than {@code size}. If chars at {@code size-1} and {@code size} is a surrogate
2116 * pair, returns a CharSequence of length {@code size-1}.
2117 *
2118 * @param size length of the result, should be greater than 0
2119 *
2120 * @hide
2121 */
2122 @Nullable
2123 public static <T extends CharSequence> T trimToSize(@Nullable T text,
2124 @IntRange(from = 1) int size) {
2125 Preconditions.checkArgument(size > 0);
2126 if (TextUtils.isEmpty(text) || text.length() <= size) return text;
2127 if (Character.isHighSurrogate(text.charAt(size - 1))
2128 && Character.isLowSurrogate(text.charAt(size))) {
2129 size = size - 1;
2130 }
2131 return (T) text.subSequence(0, size);
2132 }
2133
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002134 private static Object sLock = new Object();
Fabrice Di Megliod3d9f3f2012-09-18 12:55:32 -07002135
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002136 private static char[] sTemp = null;
Fabrice Di Megliocb332642011-09-23 19:08:04 -07002137
2138 private static String[] EMPTY_STRING_ARRAY = new String[]{};
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002139}