blob: 409e51438d2017255be63958f546b07c1b8f689d [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;
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
Roozbeh Pournader9ea756f2017-07-25 11:20:29 -070080 // Zero-width character used to fill ellipsized strings when codepoint lenght must be preserved.
81 /* package */ static final char ELLIPSIS_FILLER = '\uFEFF'; // ZERO WIDTH NO-BREAK SPACE
Neil Fullerd29bdb22015-02-06 10:03:08 +000082
Roozbeh Pournader9ea756f2017-07-25 11:20:29 -070083 // TODO: Based on CLDR data, these need to be localized for Dzongkha (dz) and perhaps
84 // Hong Kong Traditional Chinese (zh-Hant-HK), but that may need to depend on the actual word
85 // being ellipsized and not the locale.
86 private static final String ELLIPSIS_NORMAL = "\u2026"; // HORIZONTAL ELLIPSIS (…)
87 private static final String ELLIPSIS_TWO_DOTS = "\u2025"; // TWO DOT LEADER (‥)
88
89 /** {@hide} */
90 @NonNull
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -070091 public static String getEllipsisString(@NonNull TruncateAt method) {
92 return (method == TruncateAt.END_SMALL) ? ELLIPSIS_TWO_DOTS : ELLIPSIS_NORMAL;
Roozbeh Pournader9ea756f2017-07-25 11:20:29 -070093 }
94
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080095
Fabrice Di Megliocb332642011-09-23 19:08:04 -070096 private TextUtils() { /* cannot be instantiated */ }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080097
98 public static void getChars(CharSequence s, int start, int end,
99 char[] dest, int destoff) {
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800100 Class<? extends CharSequence> c = s.getClass();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800101
102 if (c == String.class)
103 ((String) s).getChars(start, end, dest, destoff);
104 else if (c == StringBuffer.class)
105 ((StringBuffer) s).getChars(start, end, dest, destoff);
106 else if (c == StringBuilder.class)
107 ((StringBuilder) s).getChars(start, end, dest, destoff);
108 else if (s instanceof GetChars)
109 ((GetChars) s).getChars(start, end, dest, destoff);
110 else {
111 for (int i = start; i < end; i++)
112 dest[destoff++] = s.charAt(i);
113 }
114 }
115
116 public static int indexOf(CharSequence s, char ch) {
117 return indexOf(s, ch, 0);
118 }
119
120 public static int indexOf(CharSequence s, char ch, int start) {
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 (c == String.class)
124 return ((String) s).indexOf(ch, start);
125
126 return indexOf(s, ch, start, s.length());
127 }
128
129 public static int indexOf(CharSequence s, char ch, int start, int end) {
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800130 Class<? extends CharSequence> c = s.getClass();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800131
132 if (s instanceof GetChars || c == StringBuffer.class ||
133 c == StringBuilder.class || c == String.class) {
134 final int INDEX_INCREMENT = 500;
135 char[] temp = obtain(INDEX_INCREMENT);
136
137 while (start < end) {
138 int segend = start + INDEX_INCREMENT;
139 if (segend > end)
140 segend = end;
141
142 getChars(s, start, segend, temp, 0);
143
144 int count = segend - start;
145 for (int i = 0; i < count; i++) {
146 if (temp[i] == ch) {
147 recycle(temp);
148 return i + start;
149 }
150 }
151
152 start = segend;
153 }
154
155 recycle(temp);
156 return -1;
157 }
158
159 for (int i = start; i < end; i++)
160 if (s.charAt(i) == ch)
161 return i;
162
163 return -1;
164 }
165
166 public static int lastIndexOf(CharSequence s, char ch) {
167 return lastIndexOf(s, ch, s.length() - 1);
168 }
169
170 public static int lastIndexOf(CharSequence s, char ch, int last) {
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800171 Class<? extends CharSequence> c = s.getClass();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800172
173 if (c == String.class)
174 return ((String) s).lastIndexOf(ch, last);
175
176 return lastIndexOf(s, ch, 0, last);
177 }
178
179 public static int lastIndexOf(CharSequence s, char ch,
180 int start, int last) {
181 if (last < 0)
182 return -1;
183 if (last >= s.length())
184 last = s.length() - 1;
185
186 int end = last + 1;
187
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800188 Class<? extends CharSequence> c = s.getClass();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800189
190 if (s instanceof GetChars || c == StringBuffer.class ||
191 c == StringBuilder.class || c == String.class) {
192 final int INDEX_INCREMENT = 500;
193 char[] temp = obtain(INDEX_INCREMENT);
194
195 while (start < end) {
196 int segstart = end - INDEX_INCREMENT;
197 if (segstart < start)
198 segstart = start;
199
200 getChars(s, segstart, end, temp, 0);
201
202 int count = end - segstart;
203 for (int i = count - 1; i >= 0; i--) {
204 if (temp[i] == ch) {
205 recycle(temp);
206 return i + segstart;
207 }
208 }
209
210 end = segstart;
211 }
212
213 recycle(temp);
214 return -1;
215 }
216
217 for (int i = end - 1; i >= start; i--)
218 if (s.charAt(i) == ch)
219 return i;
220
221 return -1;
222 }
223
224 public static int indexOf(CharSequence s, CharSequence needle) {
225 return indexOf(s, needle, 0, s.length());
226 }
227
228 public static int indexOf(CharSequence s, CharSequence needle, int start) {
229 return indexOf(s, needle, start, s.length());
230 }
231
232 public static int indexOf(CharSequence s, CharSequence needle,
233 int start, int end) {
234 int nlen = needle.length();
235 if (nlen == 0)
236 return start;
237
238 char c = needle.charAt(0);
239
240 for (;;) {
241 start = indexOf(s, c, start);
242 if (start > end - nlen) {
243 break;
244 }
245
246 if (start < 0) {
247 return -1;
248 }
249
250 if (regionMatches(s, start, needle, 0, nlen)) {
251 return start;
252 }
253
254 start++;
255 }
256 return -1;
257 }
258
259 public static boolean regionMatches(CharSequence one, int toffset,
260 CharSequence two, int ooffset,
261 int len) {
Raph Levien8d2aa192014-05-14 15:46:47 -0700262 int tempLen = 2 * len;
263 if (tempLen < len) {
264 // Integer overflow; len is unreasonably large
265 throw new IndexOutOfBoundsException();
266 }
267 char[] temp = obtain(tempLen);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800268
269 getChars(one, toffset, toffset + len, temp, 0);
270 getChars(two, ooffset, ooffset + len, temp, len);
271
272 boolean match = true;
273 for (int i = 0; i < len; i++) {
274 if (temp[i] != temp[i + len]) {
275 match = false;
276 break;
277 }
278 }
279
280 recycle(temp);
281 return match;
282 }
283
284 /**
285 * Create a new String object containing the given range of characters
286 * from the source string. This is different than simply calling
287 * {@link CharSequence#subSequence(int, int) CharSequence.subSequence}
288 * in that it does not preserve any style runs in the source sequence,
289 * allowing a more efficient implementation.
290 */
291 public static String substring(CharSequence source, int start, int end) {
292 if (source instanceof String)
293 return ((String) source).substring(start, end);
294 if (source instanceof StringBuilder)
295 return ((StringBuilder) source).substring(start, end);
296 if (source instanceof StringBuffer)
297 return ((StringBuffer) source).substring(start, end);
298
299 char[] temp = obtain(end - start);
300 getChars(source, start, end, temp, 0);
301 String ret = new String(temp, 0, end - start);
302 recycle(temp);
303
304 return ret;
305 }
306
307 /**
308 * Returns a string containing the tokens joined by delimiters.
Roozbeh Pournader42673c32017-07-20 15:23:33 -0700309 *
310 * @param delimiter a CharSequence that will be inserted between the tokens. If null, the string
311 * "null" will be used as the delimiter.
312 * @param tokens an array objects to be joined. Strings will be formed from the objects by
313 * calling object.toString(). If tokens is null, a NullPointerException will be thrown. If
314 * tokens is an empty array, an empty string will be returned.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800315 */
Roozbeh Pournader42673c32017-07-20 15:23:33 -0700316 public static String join(@NonNull CharSequence delimiter, @NonNull Object[] tokens) {
317 final int length = tokens.length;
318 if (length == 0) {
319 return "";
320 }
321 final StringBuilder sb = new StringBuilder();
322 sb.append(tokens[0]);
323 for (int i = 1; i < length; i++) {
324 sb.append(delimiter);
325 sb.append(tokens[i]);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800326 }
327 return sb.toString();
328 }
329
330 /**
331 * Returns a string containing the tokens joined by delimiters.
Roozbeh Pournader42673c32017-07-20 15:23:33 -0700332 *
333 * @param delimiter a CharSequence that will be inserted between the tokens. If null, the string
334 * "null" will be used as the delimiter.
335 * @param tokens an array objects to be joined. Strings will be formed from the objects by
336 * calling object.toString(). If tokens is null, a NullPointerException will be thrown. If
337 * tokens is empty, an empty string will be returned.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800338 */
Roozbeh Pournader42673c32017-07-20 15:23:33 -0700339 public static String join(@NonNull CharSequence delimiter, @NonNull Iterable tokens) {
340 final Iterator<?> it = tokens.iterator();
341 if (!it.hasNext()) {
342 return "";
343 }
344 final StringBuilder sb = new StringBuilder();
345 sb.append(it.next());
346 while (it.hasNext()) {
347 sb.append(delimiter);
Andreas Gampea8a58ff2016-05-18 11:58:39 -0700348 sb.append(it.next());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800349 }
350 return sb.toString();
351 }
352
353 /**
354 * String.split() returns [''] when the string to be split is empty. This returns []. This does
355 * not remove any empty strings from the result. For example split("a,", "," ) returns {"a", ""}.
356 *
357 * @param text the string to split
358 * @param expression the regular expression to match
359 * @return an array of strings. The array will be empty if text is empty
360 *
361 * @throws NullPointerException if expression or text is null
362 */
363 public static String[] split(String text, String expression) {
364 if (text.length() == 0) {
365 return EMPTY_STRING_ARRAY;
366 } else {
367 return text.split(expression, -1);
368 }
369 }
370
371 /**
372 * Splits a string on a pattern. String.split() returns [''] when the string to be
373 * split is empty. This returns []. This does not remove any empty strings from the result.
374 * @param text the string to split
375 * @param pattern the regular expression to match
376 * @return an array of strings. The array will be empty if text is empty
377 *
378 * @throws NullPointerException if expression or text is null
379 */
380 public static String[] split(String text, Pattern pattern) {
381 if (text.length() == 0) {
382 return EMPTY_STRING_ARRAY;
383 } else {
384 return pattern.split(text, -1);
385 }
386 }
387
388 /**
389 * An interface for splitting strings according to rules that are opaque to the user of this
390 * interface. This also has less overhead than split, which uses regular expressions and
391 * allocates an array to hold the results.
392 *
393 * <p>The most efficient way to use this class is:
394 *
395 * <pre>
396 * // Once
397 * TextUtils.StringSplitter splitter = new TextUtils.SimpleStringSplitter(delimiter);
398 *
399 * // Once per string to split
400 * splitter.setString(string);
401 * for (String s : splitter) {
402 * ...
403 * }
404 * </pre>
405 */
406 public interface StringSplitter extends Iterable<String> {
407 public void setString(String string);
408 }
409
410 /**
411 * A simple string splitter.
412 *
413 * <p>If the final character in the string to split is the delimiter then no empty string will
414 * be returned for the empty string after that delimeter. That is, splitting <tt>"a,b,"</tt> on
415 * comma will return <tt>"a", "b"</tt>, not <tt>"a", "b", ""</tt>.
416 */
417 public static class SimpleStringSplitter implements StringSplitter, Iterator<String> {
418 private String mString;
419 private char mDelimiter;
420 private int mPosition;
421 private int mLength;
422
423 /**
424 * Initializes the splitter. setString may be called later.
425 * @param delimiter the delimeter on which to split
426 */
427 public SimpleStringSplitter(char delimiter) {
428 mDelimiter = delimiter;
429 }
430
431 /**
432 * Sets the string to split
433 * @param string the string to split
434 */
435 public void setString(String string) {
436 mString = string;
437 mPosition = 0;
438 mLength = mString.length();
439 }
440
441 public Iterator<String> iterator() {
442 return this;
443 }
444
445 public boolean hasNext() {
446 return mPosition < mLength;
447 }
448
449 public String next() {
450 int end = mString.indexOf(mDelimiter, mPosition);
451 if (end == -1) {
452 end = mLength;
453 }
454 String nextString = mString.substring(mPosition, end);
455 mPosition = end + 1; // Skip the delimiter.
456 return nextString;
457 }
458
459 public void remove() {
460 throw new UnsupportedOperationException();
461 }
462 }
463
464 public static CharSequence stringOrSpannedString(CharSequence source) {
465 if (source == null)
466 return null;
467 if (source instanceof SpannedString)
468 return source;
469 if (source instanceof Spanned)
470 return new SpannedString(source);
471
472 return source.toString();
473 }
474
475 /**
476 * Returns true if the string is null or 0-length.
477 * @param str the string to be examined
478 * @return true if str is null or zero length
479 */
Scott Kennedy6cd132f2015-02-19 10:36:12 -0800480 public static boolean isEmpty(@Nullable CharSequence str) {
Amin Shaikhd4196c92017-02-06 17:04:47 -0800481 return str == null || str.length() == 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800482 }
483
Jeff Sharkey5cc0df22015-06-17 19:44:05 -0700484 /** {@hide} */
485 public static String nullIfEmpty(@Nullable String str) {
486 return isEmpty(str) ? null : str;
487 }
488
Eugene Susla6ed45d82017-01-22 13:52:51 -0800489 /** {@hide} */
490 public static String emptyIfNull(@Nullable String str) {
491 return str == null ? "" : str;
492 }
493
494 /** {@hide} */
495 public static String firstNotEmpty(@Nullable String a, @NonNull String b) {
496 return !isEmpty(a) ? a : Preconditions.checkStringNotEmpty(b);
497 }
498
Eugene Susla36e866b2017-02-23 18:24:39 -0800499 /** {@hide} */
500 public static int length(@Nullable String s) {
501 return isEmpty(s) ? 0 : s.length();
502 }
503
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800504 /**
Makoto Onuki812d188a2017-08-07 09:58:23 -0700505 * @return interned string if it's null.
506 * @hide
507 */
508 public static String safeIntern(String s) {
509 return (s != null) ? s.intern() : null;
510 }
511
512 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800513 * Returns the length that the specified CharSequence would have if
Roozbeh Pournader3efda952015-08-11 09:55:57 -0700514 * spaces and ASCII control characters were trimmed from the start and end,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800515 * as by {@link String#trim}.
516 */
517 public static int getTrimmedLength(CharSequence s) {
518 int len = s.length();
519
520 int start = 0;
521 while (start < len && s.charAt(start) <= ' ') {
522 start++;
523 }
524
525 int end = len;
526 while (end > start && s.charAt(end - 1) <= ' ') {
527 end--;
528 }
529
530 return end - start;
531 }
532
533 /**
534 * Returns true if a and b are equal, including if they are both null.
535 * <p><i>Note: In platform versions 1.1 and earlier, this method only worked well if
536 * both the arguments were instances of String.</i></p>
537 * @param a first CharSequence to check
538 * @param b second CharSequence to check
539 * @return true if a and b are equal
540 */
541 public static boolean equals(CharSequence a, CharSequence b) {
542 if (a == b) return true;
543 int length;
544 if (a != null && b != null && (length = a.length()) == b.length()) {
545 if (a instanceof String && b instanceof String) {
546 return a.equals(b);
547 } else {
548 for (int i = 0; i < length; i++) {
549 if (a.charAt(i) != b.charAt(i)) return false;
550 }
551 return true;
552 }
553 }
554 return false;
555 }
556
Clara Bayarrid608a0a2016-04-27 11:53:22 +0100557 /**
558 * This function only reverses individual {@code char}s and not their associated
559 * spans. It doesn't support surrogate pairs (that correspond to non-BMP code points), combining
560 * sequences or conjuncts either.
561 * @deprecated Do not use.
Roozbeh Pournader3efda952015-08-11 09:55:57 -0700562 */
563 @Deprecated
Clara Bayarrid608a0a2016-04-27 11:53:22 +0100564 public static CharSequence getReverse(CharSequence source, int start, int end) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800565 return new Reverser(source, start, end);
566 }
567
568 private static class Reverser
569 implements CharSequence, GetChars
570 {
571 public Reverser(CharSequence source, int start, int end) {
572 mSource = source;
573 mStart = start;
574 mEnd = end;
575 }
576
577 public int length() {
578 return mEnd - mStart;
579 }
580
581 public CharSequence subSequence(int start, int end) {
582 char[] buf = new char[end - start];
583
584 getChars(start, end, buf, 0);
585 return new String(buf);
586 }
587
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800588 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800589 public String toString() {
590 return subSequence(0, length()).toString();
591 }
592
593 public char charAt(int off) {
Roozbeh Pournader9559c202016-12-13 10:59:50 -0800594 return (char) UCharacter.getMirror(mSource.charAt(mEnd - 1 - off));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800595 }
596
Roozbeh Pournader9559c202016-12-13 10:59:50 -0800597 @SuppressWarnings("deprecation")
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800598 public void getChars(int start, int end, char[] dest, int destoff) {
599 TextUtils.getChars(mSource, start + mStart, end + mStart,
600 dest, destoff);
601 AndroidCharacter.mirror(dest, 0, end - start);
602
603 int len = end - start;
604 int n = (end - start) / 2;
605 for (int i = 0; i < n; i++) {
606 char tmp = dest[destoff + i];
607
608 dest[destoff + i] = dest[destoff + len - i - 1];
609 dest[destoff + len - i - 1] = tmp;
610 }
611 }
612
613 private CharSequence mSource;
614 private int mStart;
615 private int mEnd;
616 }
617
618 /** @hide */
619 public static final int ALIGNMENT_SPAN = 1;
620 /** @hide */
Victoria Lease577ba532013-04-19 13:12:15 -0700621 public static final int FIRST_SPAN = ALIGNMENT_SPAN;
622 /** @hide */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800623 public static final int FOREGROUND_COLOR_SPAN = 2;
624 /** @hide */
625 public static final int RELATIVE_SIZE_SPAN = 3;
626 /** @hide */
627 public static final int SCALE_X_SPAN = 4;
628 /** @hide */
629 public static final int STRIKETHROUGH_SPAN = 5;
630 /** @hide */
631 public static final int UNDERLINE_SPAN = 6;
632 /** @hide */
633 public static final int STYLE_SPAN = 7;
634 /** @hide */
635 public static final int BULLET_SPAN = 8;
636 /** @hide */
637 public static final int QUOTE_SPAN = 9;
638 /** @hide */
639 public static final int LEADING_MARGIN_SPAN = 10;
640 /** @hide */
641 public static final int URL_SPAN = 11;
642 /** @hide */
643 public static final int BACKGROUND_COLOR_SPAN = 12;
644 /** @hide */
645 public static final int TYPEFACE_SPAN = 13;
646 /** @hide */
647 public static final int SUPERSCRIPT_SPAN = 14;
648 /** @hide */
649 public static final int SUBSCRIPT_SPAN = 15;
650 /** @hide */
651 public static final int ABSOLUTE_SIZE_SPAN = 16;
652 /** @hide */
653 public static final int TEXT_APPEARANCE_SPAN = 17;
654 /** @hide */
655 public static final int ANNOTATION = 18;
satokadb43582011-03-09 10:08:47 +0900656 /** @hide */
Gilles Debunnea00972a2011-04-13 16:07:31 -0700657 public static final int SUGGESTION_SPAN = 19;
Gilles Debunne28294cc2011-08-24 12:02:05 -0700658 /** @hide */
659 public static final int SPELL_CHECK_SPAN = 20;
660 /** @hide */
661 public static final int SUGGESTION_RANGE_SPAN = 21;
Luca Zanoline6d36822011-08-30 18:04:34 +0100662 /** @hide */
663 public static final int EASY_EDIT_SPAN = 22;
Victoria Leasedf8ef4b2012-08-17 15:34:01 -0700664 /** @hide */
665 public static final int LOCALE_SPAN = 23;
Victoria Lease577ba532013-04-19 13:12:15 -0700666 /** @hide */
Niels Egberts4f4ead42014-06-23 12:01:14 +0100667 public static final int TTS_SPAN = 24;
668 /** @hide */
Phil Weaver193520e2016-12-13 09:39:06 -0800669 public static final int ACCESSIBILITY_CLICKABLE_SPAN = 25;
670 /** @hide */
671 public static final int ACCESSIBILITY_URL_SPAN = 26;
672 /** @hide */
673 public static final int LAST_SPAN = ACCESSIBILITY_URL_SPAN;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800674
675 /**
676 * Flatten a CharSequence and whatever styles can be copied across processes
677 * into the parcel.
678 */
Alan Viverettea70d4a92015-06-02 16:11:00 -0700679 public static void writeToParcel(CharSequence cs, Parcel p, int parcelableFlags) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800680 if (cs instanceof Spanned) {
681 p.writeInt(0);
682 p.writeString(cs.toString());
683
684 Spanned sp = (Spanned) cs;
685 Object[] os = sp.getSpans(0, cs.length(), Object.class);
686
687 // note to people adding to this: check more specific types
688 // before more generic types. also notice that it uses
689 // "if" instead of "else if" where there are interfaces
690 // so one object can be several.
691
692 for (int i = 0; i < os.length; i++) {
693 Object o = os[i];
694 Object prop = os[i];
695
696 if (prop instanceof CharacterStyle) {
697 prop = ((CharacterStyle) prop).getUnderlying();
698 }
699
700 if (prop instanceof ParcelableSpan) {
Alan Viverettea70d4a92015-06-02 16:11:00 -0700701 final ParcelableSpan ps = (ParcelableSpan) prop;
702 final int spanTypeId = ps.getSpanTypeIdInternal();
Victoria Lease577ba532013-04-19 13:12:15 -0700703 if (spanTypeId < FIRST_SPAN || spanTypeId > LAST_SPAN) {
Alan Viverettea70d4a92015-06-02 16:11:00 -0700704 Log.e(TAG, "External class \"" + ps.getClass().getSimpleName()
Victoria Lease577ba532013-04-19 13:12:15 -0700705 + "\" is attempting to use the frameworks-only ParcelableSpan"
706 + " interface");
707 } else {
708 p.writeInt(spanTypeId);
Alan Viverettea70d4a92015-06-02 16:11:00 -0700709 ps.writeToParcelInternal(p, parcelableFlags);
Victoria Lease577ba532013-04-19 13:12:15 -0700710 writeWhere(p, sp, o);
711 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800712 }
713 }
714
715 p.writeInt(0);
716 } else {
717 p.writeInt(1);
718 if (cs != null) {
719 p.writeString(cs.toString());
720 } else {
721 p.writeString(null);
722 }
723 }
724 }
725
726 private static void writeWhere(Parcel p, Spanned sp, Object o) {
727 p.writeInt(sp.getSpanStart(o));
728 p.writeInt(sp.getSpanEnd(o));
729 p.writeInt(sp.getSpanFlags(o));
730 }
731
732 public static final Parcelable.Creator<CharSequence> CHAR_SEQUENCE_CREATOR
733 = new Parcelable.Creator<CharSequence>() {
734 /**
735 * Read and return a new CharSequence, possibly with styles,
736 * from the parcel.
737 */
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800738 public CharSequence createFromParcel(Parcel p) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800739 int kind = p.readInt();
740
Martin Wallgrencee20512011-04-07 14:45:43 +0200741 String string = p.readString();
742 if (string == null) {
743 return null;
744 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800745
Martin Wallgrencee20512011-04-07 14:45:43 +0200746 if (kind == 1) {
747 return string;
748 }
749
750 SpannableString sp = new SpannableString(string);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800751
752 while (true) {
753 kind = p.readInt();
754
755 if (kind == 0)
756 break;
757
758 switch (kind) {
759 case ALIGNMENT_SPAN:
760 readSpan(p, sp, new AlignmentSpan.Standard(p));
761 break;
762
763 case FOREGROUND_COLOR_SPAN:
764 readSpan(p, sp, new ForegroundColorSpan(p));
765 break;
766
767 case RELATIVE_SIZE_SPAN:
768 readSpan(p, sp, new RelativeSizeSpan(p));
769 break;
770
771 case SCALE_X_SPAN:
772 readSpan(p, sp, new ScaleXSpan(p));
773 break;
774
775 case STRIKETHROUGH_SPAN:
776 readSpan(p, sp, new StrikethroughSpan(p));
777 break;
778
779 case UNDERLINE_SPAN:
780 readSpan(p, sp, new UnderlineSpan(p));
781 break;
782
783 case STYLE_SPAN:
784 readSpan(p, sp, new StyleSpan(p));
785 break;
786
787 case BULLET_SPAN:
788 readSpan(p, sp, new BulletSpan(p));
789 break;
790
791 case QUOTE_SPAN:
792 readSpan(p, sp, new QuoteSpan(p));
793 break;
794
795 case LEADING_MARGIN_SPAN:
796 readSpan(p, sp, new LeadingMarginSpan.Standard(p));
797 break;
798
799 case URL_SPAN:
800 readSpan(p, sp, new URLSpan(p));
801 break;
802
803 case BACKGROUND_COLOR_SPAN:
804 readSpan(p, sp, new BackgroundColorSpan(p));
805 break;
806
807 case TYPEFACE_SPAN:
808 readSpan(p, sp, new TypefaceSpan(p));
809 break;
810
811 case SUPERSCRIPT_SPAN:
812 readSpan(p, sp, new SuperscriptSpan(p));
813 break;
814
815 case SUBSCRIPT_SPAN:
816 readSpan(p, sp, new SubscriptSpan(p));
817 break;
818
819 case ABSOLUTE_SIZE_SPAN:
820 readSpan(p, sp, new AbsoluteSizeSpan(p));
821 break;
822
823 case TEXT_APPEARANCE_SPAN:
824 readSpan(p, sp, new TextAppearanceSpan(p));
825 break;
826
827 case ANNOTATION:
828 readSpan(p, sp, new Annotation(p));
829 break;
830
Gilles Debunnea00972a2011-04-13 16:07:31 -0700831 case SUGGESTION_SPAN:
832 readSpan(p, sp, new SuggestionSpan(p));
833 break;
834
Gilles Debunne28294cc2011-08-24 12:02:05 -0700835 case SPELL_CHECK_SPAN:
836 readSpan(p, sp, new SpellCheckSpan(p));
837 break;
838
839 case SUGGESTION_RANGE_SPAN:
Gilles Debunne0eea6682011-08-29 13:30:31 -0700840 readSpan(p, sp, new SuggestionRangeSpan(p));
Gilles Debunne28294cc2011-08-24 12:02:05 -0700841 break;
Gilles Debunnee90bed12011-08-30 14:28:27 -0700842
Luca Zanoline6d36822011-08-30 18:04:34 +0100843 case EASY_EDIT_SPAN:
Luca Zanolin1b15ba52013-02-20 14:31:37 +0000844 readSpan(p, sp, new EasyEditSpan(p));
Luca Zanoline6d36822011-08-30 18:04:34 +0100845 break;
846
Victoria Leasedf8ef4b2012-08-17 15:34:01 -0700847 case LOCALE_SPAN:
848 readSpan(p, sp, new LocaleSpan(p));
849 break;
850
Niels Egberts4f4ead42014-06-23 12:01:14 +0100851 case TTS_SPAN:
852 readSpan(p, sp, new TtsSpan(p));
853 break;
854
Phil Weaver193520e2016-12-13 09:39:06 -0800855 case ACCESSIBILITY_CLICKABLE_SPAN:
856 readSpan(p, sp, new AccessibilityClickableSpan(p));
857 break;
858
859 case ACCESSIBILITY_URL_SPAN:
860 readSpan(p, sp, new AccessibilityURLSpan(p));
861 break;
862
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800863 default:
864 throw new RuntimeException("bogus span encoding " + kind);
865 }
866 }
867
868 return sp;
869 }
870
871 public CharSequence[] newArray(int size)
872 {
873 return new CharSequence[size];
874 }
875 };
876
877 /**
878 * Debugging tool to print the spans in a CharSequence. The output will
879 * be printed one span per line. If the CharSequence is not a Spanned,
880 * then the entire string will be printed on a single line.
881 */
882 public static void dumpSpans(CharSequence cs, Printer printer, String prefix) {
883 if (cs instanceof Spanned) {
884 Spanned sp = (Spanned) cs;
885 Object[] os = sp.getSpans(0, cs.length(), Object.class);
886
887 for (int i = 0; i < os.length; i++) {
888 Object o = os[i];
889 printer.println(prefix + cs.subSequence(sp.getSpanStart(o),
890 sp.getSpanEnd(o)) + ": "
891 + Integer.toHexString(System.identityHashCode(o))
892 + " " + o.getClass().getCanonicalName()
893 + " (" + sp.getSpanStart(o) + "-" + sp.getSpanEnd(o)
894 + ") fl=#" + sp.getSpanFlags(o));
895 }
896 } else {
897 printer.println(prefix + cs + ": (no spans)");
898 }
899 }
900
901 /**
902 * Return a new CharSequence in which each of the source strings is
903 * replaced by the corresponding element of the destinations.
904 */
905 public static CharSequence replace(CharSequence template,
906 String[] sources,
907 CharSequence[] destinations) {
908 SpannableStringBuilder tb = new SpannableStringBuilder(template);
909
910 for (int i = 0; i < sources.length; i++) {
911 int where = indexOf(tb, sources[i]);
912
913 if (where >= 0)
914 tb.setSpan(sources[i], where, where + sources[i].length(),
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800915 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800916 }
917
918 for (int i = 0; i < sources.length; i++) {
919 int start = tb.getSpanStart(sources[i]);
920 int end = tb.getSpanEnd(sources[i]);
921
922 if (start >= 0) {
923 tb.replace(start, end, destinations[i]);
924 }
925 }
926
927 return tb;
928 }
929
930 /**
931 * Replace instances of "^1", "^2", etc. in the
932 * <code>template</code> CharSequence with the corresponding
933 * <code>values</code>. "^^" is used to produce a single caret in
934 * the output. Only up to 9 replacement values are supported,
935 * "^10" will be produce the first replacement value followed by a
936 * '0'.
937 *
938 * @param template the input text containing "^1"-style
939 * placeholder values. This object is not modified; a copy is
940 * returned.
941 *
942 * @param values CharSequences substituted into the template. The
943 * first is substituted for "^1", the second for "^2", and so on.
944 *
945 * @return the new CharSequence produced by doing the replacement
946 *
947 * @throws IllegalArgumentException if the template requests a
948 * value that was not provided, or if more than 9 values are
949 * provided.
950 */
951 public static CharSequence expandTemplate(CharSequence template,
952 CharSequence... values) {
953 if (values.length > 9) {
954 throw new IllegalArgumentException("max of 9 values are supported");
955 }
956
957 SpannableStringBuilder ssb = new SpannableStringBuilder(template);
958
959 try {
960 int i = 0;
961 while (i < ssb.length()) {
962 if (ssb.charAt(i) == '^') {
963 char next = ssb.charAt(i+1);
964 if (next == '^') {
965 ssb.delete(i+1, i+2);
966 ++i;
967 continue;
968 } else if (Character.isDigit(next)) {
969 int which = Character.getNumericValue(next) - 1;
970 if (which < 0) {
971 throw new IllegalArgumentException(
972 "template requests value ^" + (which+1));
973 }
974 if (which >= values.length) {
975 throw new IllegalArgumentException(
976 "template requests value ^" + (which+1) +
977 "; only " + values.length + " provided");
978 }
979 ssb.replace(i, i+2, values[which]);
980 i += values[which].length();
981 continue;
982 }
983 }
984 ++i;
985 }
986 } catch (IndexOutOfBoundsException ignore) {
987 // happens when ^ is the last character in the string.
988 }
989 return ssb;
990 }
991
992 public static int getOffsetBefore(CharSequence text, int offset) {
993 if (offset == 0)
994 return 0;
995 if (offset == 1)
996 return 0;
997
998 char c = text.charAt(offset - 1);
999
1000 if (c >= '\uDC00' && c <= '\uDFFF') {
1001 char c1 = text.charAt(offset - 2);
1002
1003 if (c1 >= '\uD800' && c1 <= '\uDBFF')
1004 offset -= 2;
1005 else
1006 offset -= 1;
1007 } else {
1008 offset -= 1;
1009 }
1010
1011 if (text instanceof Spanned) {
1012 ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
1013 ReplacementSpan.class);
1014
1015 for (int i = 0; i < spans.length; i++) {
1016 int start = ((Spanned) text).getSpanStart(spans[i]);
1017 int end = ((Spanned) text).getSpanEnd(spans[i]);
1018
1019 if (start < offset && end > offset)
1020 offset = start;
1021 }
1022 }
1023
1024 return offset;
1025 }
1026
1027 public static int getOffsetAfter(CharSequence text, int offset) {
1028 int len = text.length();
1029
1030 if (offset == len)
1031 return len;
1032 if (offset == len - 1)
1033 return len;
1034
1035 char c = text.charAt(offset);
1036
1037 if (c >= '\uD800' && c <= '\uDBFF') {
1038 char c1 = text.charAt(offset + 1);
1039
1040 if (c1 >= '\uDC00' && c1 <= '\uDFFF')
1041 offset += 2;
1042 else
1043 offset += 1;
1044 } else {
1045 offset += 1;
1046 }
1047
1048 if (text instanceof Spanned) {
1049 ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
1050 ReplacementSpan.class);
1051
1052 for (int i = 0; i < spans.length; i++) {
1053 int start = ((Spanned) text).getSpanStart(spans[i]);
1054 int end = ((Spanned) text).getSpanEnd(spans[i]);
1055
1056 if (start < offset && end > offset)
1057 offset = end;
1058 }
1059 }
1060
1061 return offset;
1062 }
1063
1064 private static void readSpan(Parcel p, Spannable sp, Object o) {
1065 sp.setSpan(o, p.readInt(), p.readInt(), p.readInt());
1066 }
1067
Daisuke Miyakawac1d27482009-05-25 17:37:41 +09001068 /**
1069 * Copies the spans from the region <code>start...end</code> in
1070 * <code>source</code> to the region
1071 * <code>destoff...destoff+end-start</code> in <code>dest</code>.
1072 * Spans in <code>source</code> that begin before <code>start</code>
1073 * or end after <code>end</code> but overlap this range are trimmed
1074 * as if they began at <code>start</code> or ended at <code>end</code>.
1075 *
1076 * @throws IndexOutOfBoundsException if any of the copied spans
1077 * are out of range in <code>dest</code>.
1078 */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001079 public static void copySpansFrom(Spanned source, int start, int end,
1080 Class kind,
1081 Spannable dest, int destoff) {
1082 if (kind == null) {
1083 kind = Object.class;
1084 }
1085
1086 Object[] spans = source.getSpans(start, end, kind);
1087
1088 for (int i = 0; i < spans.length; i++) {
1089 int st = source.getSpanStart(spans[i]);
1090 int en = source.getSpanEnd(spans[i]);
1091 int fl = source.getSpanFlags(spans[i]);
1092
1093 if (st < start)
1094 st = start;
1095 if (en > end)
1096 en = end;
1097
1098 dest.setSpan(spans[i], st - start + destoff, en - start + destoff,
1099 fl);
1100 }
1101 }
1102
Roozbeh Pournader205a9932017-06-08 00:23:42 -07001103 /**
1104 * Transforms a CharSequences to uppercase, copying the sources spans and keeping them spans as
1105 * much as possible close to their relative original places. In the case the the uppercase
1106 * string is identical to the sources, the source itself is returned instead of being copied.
1107 *
1108 * If copySpans is set, source must be an instance of Spanned.
1109 *
1110 * {@hide}
1111 */
1112 @NonNull
1113 public static CharSequence toUpperCase(@Nullable Locale locale, @NonNull CharSequence source,
1114 boolean copySpans) {
1115 final Edits edits = new Edits();
1116 if (!copySpans) { // No spans. Just uppercase the characters.
1117 final StringBuilder result = CaseMap.toUpper().apply(
1118 locale, source, new StringBuilder(), edits);
1119 return edits.hasChanges() ? result : source;
1120 }
1121
1122 final SpannableStringBuilder result = CaseMap.toUpper().apply(
1123 locale, source, new SpannableStringBuilder(), edits);
1124 if (!edits.hasChanges()) {
1125 // No changes happened while capitalizing. We can return the source as it was.
1126 return source;
1127 }
1128
1129 final Edits.Iterator iterator = edits.getFineIterator();
1130 final int sourceLength = source.length();
1131 final Spanned spanned = (Spanned) source;
1132 final Object[] spans = spanned.getSpans(0, sourceLength, Object.class);
1133 for (Object span : spans) {
1134 final int sourceStart = spanned.getSpanStart(span);
1135 final int sourceEnd = spanned.getSpanEnd(span);
1136 final int flags = spanned.getSpanFlags(span);
1137 // Make sure the indices are not at the end of the string, since in that case
1138 // iterator.findSourceIndex() would fail.
1139 final int destStart = sourceStart == sourceLength ? result.length() :
1140 toUpperMapToDest(iterator, sourceStart);
1141 final int destEnd = sourceEnd == sourceLength ? result.length() :
1142 toUpperMapToDest(iterator, sourceEnd);
1143 result.setSpan(span, destStart, destEnd, flags);
1144 }
1145 return result;
1146 }
1147
1148 // helper method for toUpperCase()
1149 private static int toUpperMapToDest(Edits.Iterator iterator, int sourceIndex) {
1150 // Guaranteed to succeed if sourceIndex < source.length().
1151 iterator.findSourceIndex(sourceIndex);
1152 if (sourceIndex == iterator.sourceIndex()) {
1153 return iterator.destinationIndex();
1154 }
1155 // We handle the situation differently depending on if we are in the changed slice or an
1156 // unchanged one: In an unchanged slice, we can find the exact location the span
1157 // boundary was before and map there.
1158 //
1159 // But in a changed slice, we need to treat the whole destination slice as an atomic unit.
1160 // We adjust the span boundary to the end of that slice to reduce of the chance of adjacent
1161 // spans in the source overlapping in the result. (The choice for the end vs the beginning
1162 // is somewhat arbitrary, but was taken because we except to see slightly more spans only
1163 // affecting a base character compared to spans only affecting a combining character.)
1164 if (iterator.hasChange()) {
1165 return iterator.destinationIndex() + iterator.newLength();
1166 } else {
1167 // Move the index 1:1 along with this unchanged piece of text.
1168 return iterator.destinationIndex() + (sourceIndex - iterator.sourceIndex());
1169 }
1170 }
1171
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001172 public enum TruncateAt {
1173 START,
1174 MIDDLE,
1175 END,
1176 MARQUEE,
Fabrice Di Megliocb332642011-09-23 19:08:04 -07001177 /**
1178 * @hide
1179 */
1180 END_SMALL
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001181 }
1182
1183 public interface EllipsizeCallback {
1184 /**
1185 * This method is called to report that the specified region of
1186 * text was ellipsized away by a call to {@link #ellipsize}.
1187 */
1188 public void ellipsized(int start, int end);
1189 }
1190
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001191 /**
1192 * Returns the original text if it fits in the specified width
1193 * given the properties of the specified Paint,
1194 * or, if it does not fit, a truncated
1195 * copy with ellipsis character added at the specified edge or center.
1196 */
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001197 @NonNull
1198 public static CharSequence ellipsize(@NonNull CharSequence text,
1199 @NonNull TextPaint p,
1200 @FloatRange(from = 0.0) float avail,
1201 @NonNull TruncateAt where) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001202 return ellipsize(text, p, avail, where, false, null);
1203 }
1204
1205 /**
1206 * Returns the original text if it fits in the specified width
1207 * given the properties of the specified Paint,
Doug Felte8e45f22010-03-29 14:58:40 -07001208 * or, if it does not fit, a copy with ellipsis character added
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001209 * at the specified edge or center.
1210 * If <code>preserveLength</code> is specified, the returned copy
1211 * will be padded with zero-width spaces to preserve the original
1212 * length and offsets instead of truncating.
1213 * If <code>callback</code> is non-null, it will be called to
Doug Feltcb3791202011-07-07 11:57:48 -07001214 * report the start and end of the ellipsized range. TextDirection
1215 * is determined by the first strong directional character.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001216 */
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001217 @NonNull
1218 public static CharSequence ellipsize(@NonNull CharSequence text,
1219 @NonNull TextPaint paint,
1220 @FloatRange(from = 0.0) float avail,
1221 @NonNull TruncateAt where,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001222 boolean preserveLength,
Roozbeh Pournader9ea756f2017-07-25 11:20:29 -07001223 @Nullable EllipsizeCallback callback) {
Doug Feltcb3791202011-07-07 11:57:48 -07001224 return ellipsize(text, paint, avail, where, preserveLength, callback,
Fabrice Di Megliocb332642011-09-23 19:08:04 -07001225 TextDirectionHeuristics.FIRSTSTRONG_LTR,
Roozbeh Pournader9ea756f2017-07-25 11:20:29 -07001226 getEllipsisString(where));
Doug Feltcb3791202011-07-07 11:57:48 -07001227 }
1228
1229 /**
1230 * Returns the original text if it fits in the specified width
1231 * given the properties of the specified Paint,
1232 * or, if it does not fit, a copy with ellipsis character added
1233 * at the specified edge or center.
1234 * If <code>preserveLength</code> is specified, the returned copy
1235 * will be padded with zero-width spaces to preserve the original
1236 * length and offsets instead of truncating.
1237 * If <code>callback</code> is non-null, it will be called to
1238 * report the start and end of the ellipsized range.
1239 *
1240 * @hide
1241 */
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001242 @NonNull
1243 public static CharSequence ellipsize(@NonNull CharSequence text,
1244 @NonNull TextPaint paint,
1245 @FloatRange(from = 0.0) float avail,
1246 @NonNull TruncateAt where,
Doug Feltcb3791202011-07-07 11:57:48 -07001247 boolean preserveLength,
Roozbeh Pournader9ea756f2017-07-25 11:20:29 -07001248 @Nullable EllipsizeCallback callback,
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001249 @NonNull TextDirectionHeuristic textDir,
1250 @NonNull String ellipsis) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001251
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001252 final int len = text.length();
Seigo Nonaka9d3bd082018-01-11 10:02:12 -08001253 MeasuredParagraph mt = null;
1254 MeasuredParagraph resultMt = null;
Doug Felte8e45f22010-03-29 14:58:40 -07001255 try {
Seigo Nonaka9d3bd082018-01-11 10:02:12 -08001256 mt = MeasuredParagraph.buildForMeasurement(paint, text, 0, text.length(), textDir, mt);
Seigo Nonakaf1644f72017-11-27 22:09:49 -08001257 float width = mt.getWholeWidth();
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 // First estimate of effective width of ellipsis.
1267 float ellipsisWidth = paint.measureText(ellipsis);
1268 int numberOfTries = 0;
1269 boolean textFits = false;
1270 int start, end;
1271 CharSequence result;
1272 do {
1273 if (avail < ellipsisWidth) {
1274 // Even the ellipsis can't fit. So it all goes.
1275 start = 0;
1276 end = len;
1277 } else {
1278 final float remainingWidth = avail - ellipsisWidth;
1279 if (where == TruncateAt.START) {
1280 start = 0;
1281 end = len - mt.breakText(len, false /* backwards */, remainingWidth);
1282 } else if (where == TruncateAt.END || where == TruncateAt.END_SMALL) {
1283 start = mt.breakText(len, true /* forwards */, remainingWidth);
1284 end = len;
1285 } else {
1286 end = len - mt.breakText(len, false /* backwards */, remainingWidth / 2);
1287 start = mt.breakText(end, true /* forwards */,
1288 remainingWidth - mt.measure(end, len));
1289 }
1290 }
Roozbeh Pournader287c8d62017-07-25 13:52:57 -07001291
Seigo Nonakaf1644f72017-11-27 22:09:49 -08001292 final char[] buf = mt.getChars();
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001293 final Spanned sp = text instanceof Spanned ? (Spanned) text : null;
Roozbeh Pournader7f0ebc92017-08-02 22:47:14 +00001294
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001295 final int removed = end - start;
1296 final int remaining = len - removed;
1297 if (preserveLength) {
1298 int pos = start;
1299 if (remaining > 0 && removed >= ellipsis.length()) {
1300 ellipsis.getChars(0, ellipsis.length(), buf, start);
1301 pos += ellipsis.length();
1302 } // else eliminate the ellipsis
1303 while (pos < end) {
1304 buf[pos++] = ELLIPSIS_FILLER;
1305 }
1306 final String s = new String(buf, 0, len);
1307 if (sp == null) {
1308 result = s;
1309 } else {
1310 final SpannableString ss = new SpannableString(s);
1311 copySpansFrom(sp, 0, len, Object.class, ss, 0);
1312 result = ss;
1313 }
1314 } else {
1315 if (remaining == 0) {
1316 result = "";
1317 } else if (sp == null) {
1318 final StringBuilder sb = new StringBuilder(remaining + ellipsis.length());
1319 sb.append(buf, 0, start);
1320 sb.append(ellipsis);
1321 sb.append(buf, end, len - end);
1322 result = sb.toString();
1323 } else {
1324 final SpannableStringBuilder ssb = new SpannableStringBuilder();
1325 ssb.append(text, 0, start);
1326 ssb.append(ellipsis);
1327 ssb.append(text, end, len);
1328 result = ssb;
1329 }
1330 }
1331
1332 if (remaining == 0) { // All text is gone.
1333 textFits = true;
1334 } else {
Seigo Nonaka9d3bd082018-01-11 10:02:12 -08001335 resultMt = MeasuredParagraph.buildForMeasurement(
Seigo Nonakaf1644f72017-11-27 22:09:49 -08001336 paint, result, 0, result.length(), textDir, resultMt);
1337 width = resultMt.getWholeWidth();
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001338 if (width <= avail) {
1339 textFits = true;
1340 } else {
1341 numberOfTries++;
1342 if (numberOfTries > 10) {
1343 // If the text still doesn't fit after ten tries, assume it will never
1344 // fit and ellipsize it all. We do this by setting the width of the
1345 // ellipsis to be positive infinity, so we get to empty text in the next
1346 // round.
1347 ellipsisWidth = Float.POSITIVE_INFINITY;
1348 } else {
1349 // Adjust the width of the ellipsis by adding the amount 'width' is
1350 // still over.
1351 ellipsisWidth += width - avail;
1352 }
1353 }
1354 }
1355 } while (!textFits);
Roozbeh Pournader7f0ebc92017-08-02 22:47:14 +00001356 if (callback != null) {
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001357 callback.ellipsized(start, end);
Roozbeh Pournader7f0ebc92017-08-02 22:47:14 +00001358 }
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001359 return result;
Doug Felte8e45f22010-03-29 14:58:40 -07001360 } finally {
Seigo Nonakaf1644f72017-11-27 22:09:49 -08001361 if (mt != null) {
1362 mt.recycle();
1363 }
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001364 if (resultMt != null) {
Seigo Nonakaf1644f72017-11-27 22:09:49 -08001365 resultMt.recycle();
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001366 }
Doug Felte8e45f22010-03-29 14:58:40 -07001367 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001368 }
1369
1370 /**
Roozbeh Pournader3bfce332016-06-17 15:03:56 -07001371 * Formats a list of CharSequences by repeatedly inserting the separator between them,
1372 * but stopping when the resulting sequence is too wide for the specified width.
1373 *
1374 * This method actually tries to fit the maximum number of elements. So if {@code "A, 11 more"
1375 * fits}, {@code "A, B, 10 more"} doesn't fit, but {@code "A, B, C, 9 more"} fits again (due to
1376 * the glyphs for the digits being very wide, for example), it returns
1377 * {@code "A, B, C, 9 more"}. Because of this, this method may be inefficient for very long
1378 * lists.
1379 *
1380 * Note that the elements of the returned value, as well as the string for {@code moreId}, will
1381 * be bidi-wrapped using {@link BidiFormatter#unicodeWrap} based on the locale of the input
1382 * Context. If the input {@code Context} is null, the default BidiFormatter from
1383 * {@link BidiFormatter#getInstance()} will be used.
1384 *
1385 * @param context the {@code Context} to get the {@code moreId} resource from. If {@code null},
1386 * an ellipsis (U+2026) would be used for {@code moreId}.
1387 * @param elements the list to format
1388 * @param separator a separator, such as {@code ", "}
1389 * @param paint the Paint with which to measure the text
1390 * @param avail the horizontal width available for the text (in pixels)
1391 * @param moreId the resource ID for the pluralized string to insert at the end of sequence when
1392 * some of the elements don't fit.
1393 *
1394 * @return the formatted CharSequence. If even the shortest sequence (e.g. {@code "A, 11 more"})
1395 * doesn't fit, it will return an empty string.
1396 */
Roozbeh Pournader3bfce332016-06-17 15:03:56 -07001397 public static CharSequence listEllipsize(@Nullable Context context,
1398 @Nullable List<CharSequence> elements, @NonNull String separator,
1399 @NonNull TextPaint paint, @FloatRange(from=0.0,fromInclusive=false) float avail,
1400 @PluralsRes int moreId) {
1401 if (elements == null) {
1402 return "";
1403 }
1404 final int totalLen = elements.size();
1405 if (totalLen == 0) {
1406 return "";
1407 }
1408
1409 final Resources res;
1410 final BidiFormatter bidiFormatter;
1411 if (context == null) {
1412 res = null;
1413 bidiFormatter = BidiFormatter.getInstance();
1414 } else {
1415 res = context.getResources();
1416 bidiFormatter = BidiFormatter.getInstance(res.getConfiguration().getLocales().get(0));
1417 }
1418
1419 final SpannableStringBuilder output = new SpannableStringBuilder();
1420 final int[] endIndexes = new int[totalLen];
1421 for (int i = 0; i < totalLen; i++) {
1422 output.append(bidiFormatter.unicodeWrap(elements.get(i)));
1423 if (i != totalLen - 1) { // Insert a separator, except at the very end.
1424 output.append(separator);
1425 }
1426 endIndexes[i] = output.length();
1427 }
1428
1429 for (int i = totalLen - 1; i >= 0; i--) {
1430 // Delete the tail of the string, cutting back to one less element.
1431 output.delete(endIndexes[i], output.length());
1432
1433 final int remainingElements = totalLen - i - 1;
1434 if (remainingElements > 0) {
1435 CharSequence morePiece = (res == null) ?
Roozbeh Pournader9ea756f2017-07-25 11:20:29 -07001436 ELLIPSIS_NORMAL :
Roozbeh Pournader3bfce332016-06-17 15:03:56 -07001437 res.getQuantityString(moreId, remainingElements, remainingElements);
1438 morePiece = bidiFormatter.unicodeWrap(morePiece);
1439 output.append(morePiece);
1440 }
1441
1442 final float width = paint.measureText(output, 0, output.length());
1443 if (width <= avail) { // The string fits.
1444 return output;
1445 }
1446 }
1447 return ""; // Nothing fits.
1448 }
1449
1450 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001451 * Converts a CharSequence of the comma-separated form "Andy, Bob,
1452 * Charles, David" that is too wide to fit into the specified width
1453 * into one like "Andy, Bob, 2 more".
1454 *
1455 * @param text the text to truncate
1456 * @param p the Paint with which to measure the text
Roozbeh Pournader3bfce332016-06-17 15:03:56 -07001457 * @param avail the horizontal width available for the text (in pixels)
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001458 * @param oneMore the string for "1 more" in the current locale
1459 * @param more the string for "%d more" in the current locale
Roozbeh Pournader3bfce332016-06-17 15:03:56 -07001460 *
1461 * @deprecated Do not use. This is not internationalized, and has known issues
1462 * with right-to-left text, languages that have more than one plural form, languages
1463 * that use a different character as a comma-like separator, etc.
1464 * Use {@link #listEllipsize} instead.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001465 */
Roozbeh Pournader3bfce332016-06-17 15:03:56 -07001466 @Deprecated
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001467 public static CharSequence commaEllipsize(CharSequence text,
1468 TextPaint p, float avail,
1469 String oneMore,
1470 String more) {
Doug Feltcb3791202011-07-07 11:57:48 -07001471 return commaEllipsize(text, p, avail, oneMore, more,
1472 TextDirectionHeuristics.FIRSTSTRONG_LTR);
1473 }
1474
1475 /**
1476 * @hide
1477 */
Roozbeh Pournader3bfce332016-06-17 15:03:56 -07001478 @Deprecated
Doug Feltcb3791202011-07-07 11:57:48 -07001479 public static CharSequence commaEllipsize(CharSequence text, TextPaint p,
1480 float avail, String oneMore, String more, TextDirectionHeuristic textDir) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001481
Seigo Nonaka9d3bd082018-01-11 10:02:12 -08001482 MeasuredParagraph mt = null;
1483 MeasuredParagraph tempMt = null;
Doug Felte8e45f22010-03-29 14:58:40 -07001484 try {
1485 int len = text.length();
Seigo Nonaka9d3bd082018-01-11 10:02:12 -08001486 mt = MeasuredParagraph.buildForMeasurement(p, text, 0, len, textDir, mt);
Seigo Nonakaf1644f72017-11-27 22:09:49 -08001487 final float width = mt.getWholeWidth();
Doug Felte8e45f22010-03-29 14:58:40 -07001488 if (width <= avail) {
1489 return text;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001490 }
1491
Seigo Nonakaf1644f72017-11-27 22:09:49 -08001492 char[] buf = mt.getChars();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001493
Doug Felte8e45f22010-03-29 14:58:40 -07001494 int commaCount = 0;
1495 for (int i = 0; i < len; i++) {
1496 if (buf[i] == ',') {
1497 commaCount++;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001498 }
1499 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001500
Doug Felte8e45f22010-03-29 14:58:40 -07001501 int remaining = commaCount + 1;
1502
1503 int ok = 0;
Doug Felte8e45f22010-03-29 14:58:40 -07001504 String okFormat = "";
1505
1506 int w = 0;
1507 int count = 0;
Seigo Nonakaf1644f72017-11-27 22:09:49 -08001508 float[] widths = mt.getWidths().getRawArray();
Doug Felte8e45f22010-03-29 14:58:40 -07001509
Doug Felte8e45f22010-03-29 14:58:40 -07001510 for (int i = 0; i < len; i++) {
1511 w += widths[i];
1512
1513 if (buf[i] == ',') {
1514 count++;
1515
1516 String format;
1517 // XXX should not insert spaces, should be part of string
1518 // XXX should use plural rules and not assume English plurals
1519 if (--remaining == 1) {
1520 format = " " + oneMore;
1521 } else {
1522 format = " " + String.format(more, remaining);
1523 }
1524
1525 // XXX this is probably ok, but need to look at it more
Seigo Nonaka9d3bd082018-01-11 10:02:12 -08001526 tempMt = MeasuredParagraph.buildForMeasurement(
Seigo Nonakaf1644f72017-11-27 22:09:49 -08001527 p, format, 0, format.length(), textDir, tempMt);
1528 float moreWid = tempMt.getWholeWidth();
Doug Felte8e45f22010-03-29 14:58:40 -07001529
1530 if (w + moreWid <= avail) {
1531 ok = i + 1;
Doug Felte8e45f22010-03-29 14:58:40 -07001532 okFormat = format;
1533 }
1534 }
1535 }
Doug Felte8e45f22010-03-29 14:58:40 -07001536
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001537 SpannableStringBuilder out = new SpannableStringBuilder(okFormat);
1538 out.insert(0, text, 0, ok);
1539 return out;
Doug Felte8e45f22010-03-29 14:58:40 -07001540 } finally {
Seigo Nonakaf1644f72017-11-27 22:09:49 -08001541 if (mt != null) {
1542 mt.recycle();
1543 }
1544 if (tempMt != null) {
1545 tempMt.recycle();
Doug Felte8e45f22010-03-29 14:58:40 -07001546 }
1547 }
Doug Felte8e45f22010-03-29 14:58:40 -07001548 }
1549
Roozbeh Pournader8823c852016-06-09 18:36:47 -07001550 // Returns true if the character's presence could affect RTL layout.
1551 //
1552 // In order to be fast, the code is intentionally rough and quite conservative in its
1553 // considering inclusion of any non-BMP or surrogate characters or anything in the bidi
1554 // blocks or any bidi formatting characters with a potential to affect RTL layout.
Doug Felte8e45f22010-03-29 14:58:40 -07001555 /* package */
Roozbeh Pournader8823c852016-06-09 18:36:47 -07001556 static boolean couldAffectRtl(char c) {
1557 return (0x0590 <= c && c <= 0x08FF) || // RTL scripts
1558 c == 0x200E || // Bidi format character
1559 c == 0x200F || // Bidi format character
1560 (0x202A <= c && c <= 0x202E) || // Bidi format characters
1561 (0x2066 <= c && c <= 0x2069) || // Bidi format characters
1562 (0xD800 <= c && c <= 0xDFFF) || // Surrogate pairs
1563 (0xFB1D <= c && c <= 0xFDFF) || // Hebrew and Arabic presentation forms
1564 (0xFE70 <= c && c <= 0xFEFE); // Arabic presentation forms
Doug Felte8e45f22010-03-29 14:58:40 -07001565 }
1566
Roozbeh Pournader8823c852016-06-09 18:36:47 -07001567 // Returns true if there is no character present that may potentially affect RTL layout.
1568 // Since this calls couldAffectRtl() above, it's also quite conservative, in the way that
1569 // it may return 'false' (needs bidi) although careful consideration may tell us it should
1570 // return 'true' (does not need bidi).
Doug Felte8e45f22010-03-29 14:58:40 -07001571 /* package */
1572 static boolean doesNotNeedBidi(char[] text, int start, int len) {
Roozbeh Pournader8823c852016-06-09 18:36:47 -07001573 final int end = start + len;
1574 for (int i = start; i < end; i++) {
1575 if (couldAffectRtl(text[i])) {
Doug Felte8e45f22010-03-29 14:58:40 -07001576 return false;
1577 }
1578 }
1579 return true;
1580 }
1581
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001582 /* package */ static char[] obtain(int len) {
1583 char[] buf;
1584
1585 synchronized (sLock) {
1586 buf = sTemp;
1587 sTemp = null;
1588 }
1589
1590 if (buf == null || buf.length < len)
Adam Lesinski776abc22014-03-07 11:30:59 -05001591 buf = ArrayUtils.newUnpaddedCharArray(len);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001592
1593 return buf;
1594 }
1595
1596 /* package */ static void recycle(char[] temp) {
1597 if (temp.length > 1000)
1598 return;
1599
1600 synchronized (sLock) {
1601 sTemp = temp;
1602 }
1603 }
1604
1605 /**
1606 * Html-encode the string.
1607 * @param s the string to be encoded
1608 * @return the encoded string
1609 */
1610 public static String htmlEncode(String s) {
1611 StringBuilder sb = new StringBuilder();
1612 char c;
1613 for (int i = 0; i < s.length(); i++) {
1614 c = s.charAt(i);
1615 switch (c) {
1616 case '<':
1617 sb.append("&lt;"); //$NON-NLS-1$
1618 break;
1619 case '>':
1620 sb.append("&gt;"); //$NON-NLS-1$
1621 break;
1622 case '&':
1623 sb.append("&amp;"); //$NON-NLS-1$
1624 break;
1625 case '\'':
Marc Blankf4832da2012-02-13 10:11:50 -08001626 //http://www.w3.org/TR/xhtml1
1627 // The named character reference &apos; (the apostrophe, U+0027) was introduced in
1628 // XML 1.0 but does not appear in HTML. Authors should therefore use &#39; instead
1629 // of &apos; to work as expected in HTML 4 user agents.
1630 sb.append("&#39;"); //$NON-NLS-1$
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001631 break;
1632 case '"':
1633 sb.append("&quot;"); //$NON-NLS-1$
1634 break;
1635 default:
1636 sb.append(c);
1637 }
1638 }
1639 return sb.toString();
1640 }
1641
1642 /**
1643 * Returns a CharSequence concatenating the specified CharSequences,
1644 * retaining their spans if any.
Roozbeh Pournadere57886e2017-05-02 18:10:10 -07001645 *
1646 * If there are no parameters, an empty string will be returned.
1647 *
1648 * If the number of parameters is exactly one, that parameter is returned as output, even if it
1649 * is null.
1650 *
1651 * If the number of parameters is at least two, any null CharSequence among the parameters is
1652 * treated as if it was the string <code>"null"</code>.
1653 *
1654 * If there are paragraph spans in the source CharSequences that satisfy paragraph boundary
1655 * requirements in the sources but would no longer satisfy them in the concatenated
1656 * CharSequence, they may get extended in the resulting CharSequence or not retained.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001657 */
1658 public static CharSequence concat(CharSequence... text) {
1659 if (text.length == 0) {
1660 return "";
1661 }
1662
1663 if (text.length == 1) {
1664 return text[0];
1665 }
1666
1667 boolean spanned = false;
Roozbeh Pournadere57886e2017-05-02 18:10:10 -07001668 for (CharSequence piece : text) {
1669 if (piece instanceof Spanned) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001670 spanned = true;
1671 break;
1672 }
1673 }
1674
Roozbeh Pournadere57886e2017-05-02 18:10:10 -07001675 if (spanned) {
1676 final SpannableStringBuilder ssb = new SpannableStringBuilder();
1677 for (CharSequence piece : text) {
1678 // If a piece is null, we append the string "null" for compatibility with the
1679 // behavior of StringBuilder and the behavior of the concat() method in earlier
1680 // versions of Android.
1681 ssb.append(piece == null ? "null" : piece);
1682 }
1683 return new SpannedString(ssb);
1684 } else {
1685 final StringBuilder sb = new StringBuilder();
1686 for (CharSequence piece : text) {
1687 sb.append(piece);
1688 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001689 return sb.toString();
1690 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001691 }
1692
1693 /**
1694 * Returns whether the given CharSequence contains any printable characters.
1695 */
1696 public static boolean isGraphic(CharSequence str) {
1697 final int len = str.length();
Roozbeh Pournadera93880e2015-08-10 17:23:05 -07001698 for (int cp, i=0; i<len; i+=Character.charCount(cp)) {
Roozbeh Pournader1cc2acf2015-08-11 10:37:07 -07001699 cp = Character.codePointAt(str, i);
Roozbeh Pournadera93880e2015-08-10 17:23:05 -07001700 int gc = Character.getType(cp);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001701 if (gc != Character.CONTROL
1702 && gc != Character.FORMAT
1703 && gc != Character.SURROGATE
1704 && gc != Character.UNASSIGNED
1705 && gc != Character.LINE_SEPARATOR
1706 && gc != Character.PARAGRAPH_SEPARATOR
1707 && gc != Character.SPACE_SEPARATOR) {
1708 return true;
1709 }
1710 }
1711 return false;
1712 }
1713
1714 /**
1715 * Returns whether this character is a printable character.
Roozbeh Pournadera93880e2015-08-10 17:23:05 -07001716 *
1717 * This does not support non-BMP characters and should not be used.
1718 *
1719 * @deprecated Use {@link #isGraphic(CharSequence)} instead.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001720 */
Roozbeh Pournadera93880e2015-08-10 17:23:05 -07001721 @Deprecated
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001722 public static boolean isGraphic(char c) {
1723 int gc = Character.getType(c);
1724 return gc != Character.CONTROL
1725 && gc != Character.FORMAT
1726 && gc != Character.SURROGATE
1727 && gc != Character.UNASSIGNED
1728 && gc != Character.LINE_SEPARATOR
1729 && gc != Character.PARAGRAPH_SEPARATOR
1730 && gc != Character.SPACE_SEPARATOR;
1731 }
1732
1733 /**
1734 * Returns whether the given CharSequence contains only digits.
1735 */
1736 public static boolean isDigitsOnly(CharSequence str) {
1737 final int len = str.length();
Roozbeh Pournader3efda952015-08-11 09:55:57 -07001738 for (int cp, i = 0; i < len; i += Character.charCount(cp)) {
1739 cp = Character.codePointAt(str, i);
1740 if (!Character.isDigit(cp)) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001741 return false;
1742 }
1743 }
1744 return true;
1745 }
1746
1747 /**
Daisuke Miyakawa973afa92009-12-03 10:43:45 +09001748 * @hide
1749 */
1750 public static boolean isPrintableAscii(final char c) {
1751 final int asciiFirst = 0x20;
1752 final int asciiLast = 0x7E; // included
1753 return (asciiFirst <= c && c <= asciiLast) || c == '\r' || c == '\n';
1754 }
1755
1756 /**
1757 * @hide
1758 */
1759 public static boolean isPrintableAsciiOnly(final CharSequence str) {
1760 final int len = str.length();
1761 for (int i = 0; i < len; i++) {
1762 if (!isPrintableAscii(str.charAt(i))) {
1763 return false;
1764 }
1765 }
1766 return true;
1767 }
1768
1769 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001770 * Capitalization mode for {@link #getCapsMode}: capitalize all
1771 * characters. This value is explicitly defined to be the same as
1772 * {@link InputType#TYPE_TEXT_FLAG_CAP_CHARACTERS}.
1773 */
1774 public static final int CAP_MODE_CHARACTERS
1775 = InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS;
Doug Felte8e45f22010-03-29 14:58:40 -07001776
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001777 /**
1778 * Capitalization mode for {@link #getCapsMode}: capitalize the first
1779 * character of all words. This value is explicitly defined to be the same as
1780 * {@link InputType#TYPE_TEXT_FLAG_CAP_WORDS}.
1781 */
1782 public static final int CAP_MODE_WORDS
1783 = InputType.TYPE_TEXT_FLAG_CAP_WORDS;
Doug Felte8e45f22010-03-29 14:58:40 -07001784
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001785 /**
1786 * Capitalization mode for {@link #getCapsMode}: capitalize the first
1787 * character of each sentence. This value is explicitly defined to be the same as
1788 * {@link InputType#TYPE_TEXT_FLAG_CAP_SENTENCES}.
1789 */
1790 public static final int CAP_MODE_SENTENCES
1791 = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
Doug Felte8e45f22010-03-29 14:58:40 -07001792
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001793 /**
1794 * Determine what caps mode should be in effect at the current offset in
1795 * the text. Only the mode bits set in <var>reqModes</var> will be
1796 * checked. Note that the caps mode flags here are explicitly defined
1797 * to match those in {@link InputType}.
Doug Felte8e45f22010-03-29 14:58:40 -07001798 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001799 * @param cs The text that should be checked for caps modes.
1800 * @param off Location in the text at which to check.
1801 * @param reqModes The modes to be checked: may be any combination of
1802 * {@link #CAP_MODE_CHARACTERS}, {@link #CAP_MODE_WORDS}, and
1803 * {@link #CAP_MODE_SENTENCES}.
Mark Wagner60919952010-03-01 09:24:59 -08001804 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001805 * @return Returns the actual capitalization modes that can be in effect
1806 * at the current position, which is any combination of
1807 * {@link #CAP_MODE_CHARACTERS}, {@link #CAP_MODE_WORDS}, and
1808 * {@link #CAP_MODE_SENTENCES}.
1809 */
1810 public static int getCapsMode(CharSequence cs, int off, int reqModes) {
Mark Wagner60919952010-03-01 09:24:59 -08001811 if (off < 0) {
1812 return 0;
1813 }
1814
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001815 int i;
1816 char c;
1817 int mode = 0;
1818
1819 if ((reqModes&CAP_MODE_CHARACTERS) != 0) {
1820 mode |= CAP_MODE_CHARACTERS;
1821 }
1822 if ((reqModes&(CAP_MODE_WORDS|CAP_MODE_SENTENCES)) == 0) {
1823 return mode;
1824 }
1825
1826 // Back over allowed opening punctuation.
1827
1828 for (i = off; i > 0; i--) {
1829 c = cs.charAt(i - 1);
1830
1831 if (c != '"' && c != '\'' &&
1832 Character.getType(c) != Character.START_PUNCTUATION) {
1833 break;
1834 }
1835 }
1836
1837 // Start of paragraph, with optional whitespace.
1838
1839 int j = i;
1840 while (j > 0 && ((c = cs.charAt(j - 1)) == ' ' || c == '\t')) {
1841 j--;
1842 }
1843 if (j == 0 || cs.charAt(j - 1) == '\n') {
1844 return mode | CAP_MODE_WORDS;
1845 }
1846
1847 // Or start of word if we are that style.
1848
1849 if ((reqModes&CAP_MODE_SENTENCES) == 0) {
1850 if (i != j) mode |= CAP_MODE_WORDS;
1851 return mode;
1852 }
1853
1854 // There must be a space if not the start of paragraph.
1855
1856 if (i == j) {
1857 return mode;
1858 }
1859
1860 // Back over allowed closing punctuation.
1861
1862 for (; j > 0; j--) {
1863 c = cs.charAt(j - 1);
1864
1865 if (c != '"' && c != '\'' &&
1866 Character.getType(c) != Character.END_PUNCTUATION) {
1867 break;
1868 }
1869 }
1870
1871 if (j > 0) {
1872 c = cs.charAt(j - 1);
1873
1874 if (c == '.' || c == '?' || c == '!') {
1875 // Do not capitalize if the word ends with a period but
1876 // also contains a period, in which case it is an abbreviation.
1877
1878 if (c == '.') {
1879 for (int k = j - 2; k >= 0; k--) {
1880 c = cs.charAt(k);
1881
1882 if (c == '.') {
1883 return mode;
1884 }
1885
1886 if (!Character.isLetter(c)) {
1887 break;
1888 }
1889 }
1890 }
1891
1892 return mode | CAP_MODE_SENTENCES;
1893 }
1894 }
1895
1896 return mode;
1897 }
Doug Felte8e45f22010-03-29 14:58:40 -07001898
Brad Fitzpatrick11fe1812010-09-10 16:07:52 -07001899 /**
1900 * Does a comma-delimited list 'delimitedString' contain a certain item?
1901 * (without allocating memory)
1902 *
1903 * @hide
1904 */
1905 public static boolean delimitedStringContains(
1906 String delimitedString, char delimiter, String item) {
1907 if (isEmpty(delimitedString) || isEmpty(item)) {
1908 return false;
1909 }
1910 int pos = -1;
1911 int length = delimitedString.length();
1912 while ((pos = delimitedString.indexOf(item, pos + 1)) != -1) {
1913 if (pos > 0 && delimitedString.charAt(pos - 1) != delimiter) {
1914 continue;
1915 }
1916 int expectedDelimiterPos = pos + item.length();
1917 if (expectedDelimiterPos == length) {
1918 // Match at end of string.
1919 return true;
1920 }
1921 if (delimitedString.charAt(expectedDelimiterPos) == delimiter) {
1922 return true;
1923 }
1924 }
1925 return false;
1926 }
1927
Gilles Debunne1e3ac182011-03-08 14:22:34 -08001928 /**
1929 * Removes empty spans from the <code>spans</code> array.
1930 *
1931 * When parsing a Spanned using {@link Spanned#nextSpanTransition(int, int, Class)}, empty spans
1932 * will (correctly) create span transitions, and calling getSpans on a slice of text bounded by
1933 * one of these transitions will (correctly) include the empty overlapping span.
1934 *
1935 * However, these empty spans should not be taken into account when layouting or rendering the
1936 * string and this method provides a way to filter getSpans' results accordingly.
1937 *
1938 * @param spans A list of spans retrieved using {@link Spanned#getSpans(int, int, Class)} from
1939 * the <code>spanned</code>
1940 * @param spanned The Spanned from which spans were extracted
1941 * @return A subset of spans where empty spans ({@link Spanned#getSpanStart(Object)} ==
1942 * {@link Spanned#getSpanEnd(Object)} have been removed. The initial order is preserved
1943 * @hide
1944 */
1945 @SuppressWarnings("unchecked")
1946 public static <T> T[] removeEmptySpans(T[] spans, Spanned spanned, Class<T> klass) {
1947 T[] copy = null;
1948 int count = 0;
1949
1950 for (int i = 0; i < spans.length; i++) {
1951 final T span = spans[i];
1952 final int start = spanned.getSpanStart(span);
1953 final int end = spanned.getSpanEnd(span);
1954
1955 if (start == end) {
1956 if (copy == null) {
1957 copy = (T[]) Array.newInstance(klass, spans.length - 1);
1958 System.arraycopy(spans, 0, copy, 0, i);
1959 count = i;
1960 }
1961 } else {
1962 if (copy != null) {
1963 copy[count] = span;
1964 count++;
1965 }
1966 }
1967 }
1968
1969 if (copy != null) {
1970 T[] result = (T[]) Array.newInstance(klass, count);
1971 System.arraycopy(copy, 0, result, 0, count);
1972 return result;
1973 } else {
1974 return spans;
1975 }
1976 }
1977
Gilles Debunne6c488de2012-03-01 16:20:35 -08001978 /**
1979 * Pack 2 int values into a long, useful as a return value for a range
1980 * @see #unpackRangeStartFromLong(long)
1981 * @see #unpackRangeEndFromLong(long)
1982 * @hide
1983 */
1984 public static long packRangeInLong(int start, int end) {
1985 return (((long) start) << 32) | end;
1986 }
1987
1988 /**
1989 * Get the start value from a range packed in a long by {@link #packRangeInLong(int, int)}
1990 * @see #unpackRangeEndFromLong(long)
1991 * @see #packRangeInLong(int, int)
1992 * @hide
1993 */
1994 public static int unpackRangeStartFromLong(long range) {
1995 return (int) (range >>> 32);
1996 }
1997
1998 /**
1999 * Get the end value from a range packed in a long by {@link #packRangeInLong(int, int)}
2000 * @see #unpackRangeStartFromLong(long)
2001 * @see #packRangeInLong(int, int)
2002 * @hide
2003 */
2004 public static int unpackRangeEndFromLong(long range) {
2005 return (int) (range & 0x00000000FFFFFFFFL);
2006 }
2007
Fabrice Di Megliod3d9f3f2012-09-18 12:55:32 -07002008 /**
2009 * Return the layout direction for a given Locale
2010 *
2011 * @param locale the Locale for which we want the layout direction. Can be null.
2012 * @return the layout direction. This may be one of:
2013 * {@link android.view.View#LAYOUT_DIRECTION_LTR} or
2014 * {@link android.view.View#LAYOUT_DIRECTION_RTL}.
2015 *
2016 * Be careful: this code will need to be updated when vertical scripts will be supported
2017 */
2018 public static int getLayoutDirectionFromLocale(Locale locale) {
Roozbeh Pournader463b4822015-08-06 16:04:45 -07002019 return ((locale != null && !locale.equals(Locale.ROOT)
2020 && ULocale.forLocale(locale).isRightToLeft())
2021 // If forcing into RTL layout mode, return RTL as default
2022 || SystemProperties.getBoolean(Settings.Global.DEVELOPMENT_FORCE_RTL, false))
2023 ? View.LAYOUT_DIRECTION_RTL
2024 : View.LAYOUT_DIRECTION_LTR;
Fabrice Di Megliod3d9f3f2012-09-18 12:55:32 -07002025 }
2026
Jeff Sharkeyf491c722015-06-11 09:16:19 -07002027 /**
2028 * Return localized string representing the given number of selected items.
2029 *
2030 * @hide
2031 */
2032 public static CharSequence formatSelectedCount(int count) {
2033 return Resources.getSystem().getQuantityString(R.plurals.selected_count, count, count);
2034 }
2035
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +01002036 /**
2037 * Returns whether or not the specified spanned text has a style span.
2038 * @hide
2039 */
2040 public static boolean hasStyleSpan(@NonNull Spanned spanned) {
2041 Preconditions.checkArgument(spanned != null);
2042 final Class<?>[] styleClasses = {
2043 CharacterStyle.class, ParagraphStyle.class, UpdateAppearance.class};
2044 for (Class<?> clazz : styleClasses) {
2045 if (spanned.nextSpanTransition(-1, spanned.length(), clazz) < spanned.length()) {
2046 return true;
2047 }
2048 }
2049 return false;
2050 }
2051
Felipe Lemea8fce3b2017-04-04 14:22:12 -07002052 /**
2053 * If the {@code charSequence} is instance of {@link Spanned}, creates a new copy and
2054 * {@link NoCopySpan}'s are removed from the copy. Otherwise the given {@code charSequence} is
2055 * returned as it is.
2056 *
2057 * @hide
2058 */
2059 @Nullable
2060 public static CharSequence trimNoCopySpans(@Nullable CharSequence charSequence) {
2061 if (charSequence != null && charSequence instanceof Spanned) {
2062 // SpannableStringBuilder copy constructor trims NoCopySpans.
2063 return new SpannableStringBuilder(charSequence);
2064 }
2065 return charSequence;
2066 }
2067
Eugene Susla4a34f9c2017-05-16 14:16:38 -07002068 /**
2069 * Prepends {@code start} and appends {@code end} to a given {@link StringBuilder}
2070 *
2071 * @hide
2072 */
2073 public static void wrap(StringBuilder builder, String start, String end) {
2074 builder.insert(0, start);
2075 builder.append(end);
2076 }
2077
Siyamed Sinirce3b05a2017-07-18 18:54:31 -07002078 /**
2079 * Intent size limitations prevent sending over a megabyte of data. Limit
2080 * text length to 100K characters - 200KB.
2081 */
2082 private static final int PARCEL_SAFE_TEXT_LENGTH = 100000;
2083
2084 /**
2085 * Trims the text to {@link #PARCEL_SAFE_TEXT_LENGTH} length. Returns the string as it is if
2086 * the length() is smaller than {@link #PARCEL_SAFE_TEXT_LENGTH}. Used for text that is parceled
2087 * into a {@link Parcelable}.
2088 *
2089 * @hide
2090 */
2091 @Nullable
2092 public static <T extends CharSequence> T trimToParcelableSize(@Nullable T text) {
2093 return trimToSize(text, PARCEL_SAFE_TEXT_LENGTH);
2094 }
2095
2096 /**
2097 * Trims the text to {@code size} length. Returns the string as it is if the length() is
2098 * smaller than {@code size}. If chars at {@code size-1} and {@code size} is a surrogate
2099 * pair, returns a CharSequence of length {@code size-1}.
2100 *
2101 * @param size length of the result, should be greater than 0
2102 *
2103 * @hide
2104 */
2105 @Nullable
2106 public static <T extends CharSequence> T trimToSize(@Nullable T text,
2107 @IntRange(from = 1) int size) {
2108 Preconditions.checkArgument(size > 0);
2109 if (TextUtils.isEmpty(text) || text.length() <= size) return text;
2110 if (Character.isHighSurrogate(text.charAt(size - 1))
2111 && Character.isLowSurrogate(text.charAt(size))) {
2112 size = size - 1;
2113 }
2114 return (T) text.subSequence(0, size);
2115 }
2116
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002117 private static Object sLock = new Object();
Fabrice Di Megliod3d9f3f2012-09-18 12:55:32 -07002118
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002119 private static char[] sTemp = null;
Fabrice Di Megliocb332642011-09-23 19:08:04 -07002120
2121 private static String[] EMPTY_STRING_ARRAY = new String[]{};
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002122}