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