blob: 44353b1609b71a4aae9f3aad6170f4107e4a76ac [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
Philip P. Moltmannc1fda742018-10-05 16:52:35 -070019import static java.lang.annotation.RetentionPolicy.SOURCE;
20
Roozbeh Pournader3bfce332016-06-17 15:03:56 -070021import android.annotation.FloatRange;
Philip P. Moltmannc1fda742018-10-05 16:52:35 -070022import android.annotation.IntDef;
Siyamed Sinirce3b05a2017-07-18 18:54:31 -070023import android.annotation.IntRange;
Roozbeh Pournader3bfce332016-06-17 15:03:56 -070024import android.annotation.NonNull;
Scott Kennedy6cd132f2015-02-19 10:36:12 -080025import android.annotation.Nullable;
Roozbeh Pournader3bfce332016-06-17 15:03:56 -070026import android.annotation.PluralsRes;
Mathew Inwoodefeab842018-08-14 15:21:30 +010027import android.annotation.UnsupportedAppUsage;
Roozbeh Pournader3bfce332016-06-17 15:03:56 -070028import android.content.Context;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080029import android.content.res.Resources;
Roozbeh Pournader9559c202016-12-13 10:59:50 -080030import android.icu.lang.UCharacter;
Roozbeh Pournader205a9932017-06-08 00:23:42 -070031import android.icu.text.CaseMap;
32import android.icu.text.Edits;
Roozbeh Pournader463b4822015-08-06 16:04:45 -070033import android.icu.util.ULocale;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080034import android.os.Parcel;
35import android.os.Parcelable;
Kiyoung Kim0fe161d2018-12-20 18:26:10 +090036import android.sysprop.DisplayProperties;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080037import android.text.style.AbsoluteSizeSpan;
Phil Weaver193520e2016-12-13 09:39:06 -080038import android.text.style.AccessibilityClickableSpan;
39import android.text.style.AccessibilityURLSpan;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080040import android.text.style.AlignmentSpan;
41import android.text.style.BackgroundColorSpan;
42import android.text.style.BulletSpan;
43import android.text.style.CharacterStyle;
Luca Zanoline6d36822011-08-30 18:04:34 +010044import android.text.style.EasyEditSpan;
Gilles Debunne0eea6682011-08-29 13:30:31 -070045import android.text.style.ForegroundColorSpan;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080046import android.text.style.LeadingMarginSpan;
Haoyu Zhang003f7372018-09-24 09:47:58 -070047import android.text.style.LineBackgroundSpan;
Haoyu Zhang63a5efb2018-11-26 15:36:29 -080048import android.text.style.LineHeightSpan;
Victoria Leasedf8ef4b2012-08-17 15:34:01 -070049import android.text.style.LocaleSpan;
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +010050import android.text.style.ParagraphStyle;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080051import android.text.style.QuoteSpan;
52import android.text.style.RelativeSizeSpan;
53import android.text.style.ReplacementSpan;
54import android.text.style.ScaleXSpan;
Gilles Debunne28294cc2011-08-24 12:02:05 -070055import android.text.style.SpellCheckSpan;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080056import android.text.style.StrikethroughSpan;
57import android.text.style.StyleSpan;
58import android.text.style.SubscriptSpan;
Gilles Debunne28294cc2011-08-24 12:02:05 -070059import android.text.style.SuggestionRangeSpan;
Gilles Debunnea00972a2011-04-13 16:07:31 -070060import android.text.style.SuggestionSpan;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080061import android.text.style.SuperscriptSpan;
62import android.text.style.TextAppearanceSpan;
Niels Egberts4f4ead42014-06-23 12:01:14 +010063import android.text.style.TtsSpan;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080064import android.text.style.TypefaceSpan;
65import android.text.style.URLSpan;
66import android.text.style.UnderlineSpan;
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +010067import android.text.style.UpdateAppearance;
Victoria Lease577ba532013-04-19 13:12:15 -070068import android.util.Log;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080069import android.util.Printer;
Fabrice Di Megliod3d9f3f2012-09-18 12:55:32 -070070import android.view.View;
Raph Levien8d2aa192014-05-14 15:46:47 -070071
Doug Feltcb3791202011-07-07 11:57:48 -070072import com.android.internal.R;
73import com.android.internal.util.ArrayUtils;
Eugene Susla6ed45d82017-01-22 13:52:51 -080074import com.android.internal.util.Preconditions;
Raph Levien8d2aa192014-05-14 15:46:47 -070075
Philip P. Moltmannc1fda742018-10-05 16:52:35 -070076import java.lang.annotation.Retention;
Gilles Debunne1e3ac182011-03-08 14:22:34 -080077import java.lang.reflect.Array;
Philip P. Moltmannc1fda742018-10-05 16:52:35 -070078import java.util.BitSet;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080079import java.util.Iterator;
Roozbeh Pournader3bfce332016-06-17 15:03:56 -070080import java.util.List;
Fabrice Di Megliod3d9f3f2012-09-18 12:55:32 -070081import java.util.Locale;
Doug Felte8e45f22010-03-29 14:58:40 -070082import java.util.regex.Pattern;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080083
84public class TextUtils {
Victoria Lease577ba532013-04-19 13:12:15 -070085 private static final String TAG = "TextUtils";
86
Branden Archerea3bcb72018-10-29 08:45:31 -070087 // Zero-width character used to fill ellipsized strings when codepoint length must be preserved.
Roozbeh Pournader9ea756f2017-07-25 11:20:29 -070088 /* package */ static final char ELLIPSIS_FILLER = '\uFEFF'; // ZERO WIDTH NO-BREAK SPACE
Neil Fullerd29bdb22015-02-06 10:03:08 +000089
Roozbeh Pournader9ea756f2017-07-25 11:20:29 -070090 // TODO: Based on CLDR data, these need to be localized for Dzongkha (dz) and perhaps
91 // Hong Kong Traditional Chinese (zh-Hant-HK), but that may need to depend on the actual word
92 // being ellipsized and not the locale.
93 private static final String ELLIPSIS_NORMAL = "\u2026"; // HORIZONTAL ELLIPSIS (…)
94 private static final String ELLIPSIS_TWO_DOTS = "\u2025"; // TWO DOT LEADER (‥)
95
Philip P. Moltmannc1fda742018-10-05 16:52:35 -070096 private static final int LINE_FEED_CODE_POINT = 10;
97 private static final int NBSP_CODE_POINT = 160;
98
99 /**
100 * Flags for {@link #makeSafeForPresentation(String, int, float, int)}
101 *
102 * @hide
103 */
104 @Retention(SOURCE)
105 @IntDef(flag = true, prefix = "CLEAN_STRING_FLAG_",
106 value = {SAFE_STRING_FLAG_TRIM, SAFE_STRING_FLAG_SINGLE_LINE,
107 SAFE_STRING_FLAG_FIRST_LINE})
108 public @interface SafeStringFlags {}
109
110 /**
111 * Remove {@link Character#isWhitespace(int) whitespace} and non-breaking spaces from the edges
112 * of the label.
113 *
114 * @see #makeSafeForPresentation(String, int, float, int)
115 */
116 public static final int SAFE_STRING_FLAG_TRIM = 0x1;
117
118 /**
119 * Force entire string into single line of text (no newlines). Cannot be set at the same time as
120 * {@link #SAFE_STRING_FLAG_FIRST_LINE}.
121 *
122 * @see #makeSafeForPresentation(String, int, float, int)
123 */
124 public static final int SAFE_STRING_FLAG_SINGLE_LINE = 0x2;
125
126 /**
127 * Return only first line of text (truncate at first newline). Cannot be set at the same time as
128 * {@link #SAFE_STRING_FLAG_SINGLE_LINE}.
129 *
130 * @see #makeSafeForPresentation(String, int, float, int)
131 */
132 public static final int SAFE_STRING_FLAG_FIRST_LINE = 0x4;
133
Roozbeh Pournader9ea756f2017-07-25 11:20:29 -0700134 /** {@hide} */
135 @NonNull
Siyamed Sinir1a5648a2018-01-23 16:40:06 -0800136 public static String getEllipsisString(@NonNull TextUtils.TruncateAt method) {
137 return (method == TextUtils.TruncateAt.END_SMALL) ? ELLIPSIS_TWO_DOTS : ELLIPSIS_NORMAL;
Roozbeh Pournader9ea756f2017-07-25 11:20:29 -0700138 }
139
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800140
Fabrice Di Megliocb332642011-09-23 19:08:04 -0700141 private TextUtils() { /* cannot be instantiated */ }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800142
143 public static void getChars(CharSequence s, int start, int end,
144 char[] dest, int destoff) {
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800145 Class<? extends CharSequence> c = s.getClass();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800146
147 if (c == String.class)
148 ((String) s).getChars(start, end, dest, destoff);
149 else if (c == StringBuffer.class)
150 ((StringBuffer) s).getChars(start, end, dest, destoff);
151 else if (c == StringBuilder.class)
152 ((StringBuilder) s).getChars(start, end, dest, destoff);
153 else if (s instanceof GetChars)
154 ((GetChars) s).getChars(start, end, dest, destoff);
155 else {
156 for (int i = start; i < end; i++)
157 dest[destoff++] = s.charAt(i);
158 }
159 }
160
161 public static int indexOf(CharSequence s, char ch) {
162 return indexOf(s, ch, 0);
163 }
164
165 public static int indexOf(CharSequence s, char ch, int start) {
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800166 Class<? extends CharSequence> c = s.getClass();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800167
168 if (c == String.class)
169 return ((String) s).indexOf(ch, start);
170
171 return indexOf(s, ch, start, s.length());
172 }
173
174 public static int indexOf(CharSequence s, char ch, int start, int end) {
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800175 Class<? extends CharSequence> c = s.getClass();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800176
177 if (s instanceof GetChars || c == StringBuffer.class ||
178 c == StringBuilder.class || c == String.class) {
179 final int INDEX_INCREMENT = 500;
180 char[] temp = obtain(INDEX_INCREMENT);
181
182 while (start < end) {
183 int segend = start + INDEX_INCREMENT;
184 if (segend > end)
185 segend = end;
186
187 getChars(s, start, segend, temp, 0);
188
189 int count = segend - start;
190 for (int i = 0; i < count; i++) {
191 if (temp[i] == ch) {
192 recycle(temp);
193 return i + start;
194 }
195 }
196
197 start = segend;
198 }
199
200 recycle(temp);
201 return -1;
202 }
203
204 for (int i = start; i < end; i++)
205 if (s.charAt(i) == ch)
206 return i;
207
208 return -1;
209 }
210
211 public static int lastIndexOf(CharSequence s, char ch) {
212 return lastIndexOf(s, ch, s.length() - 1);
213 }
214
215 public static int lastIndexOf(CharSequence s, char ch, int last) {
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800216 Class<? extends CharSequence> c = s.getClass();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800217
218 if (c == String.class)
219 return ((String) s).lastIndexOf(ch, last);
220
221 return lastIndexOf(s, ch, 0, last);
222 }
223
224 public static int lastIndexOf(CharSequence s, char ch,
225 int start, int last) {
226 if (last < 0)
227 return -1;
228 if (last >= s.length())
229 last = s.length() - 1;
230
231 int end = last + 1;
232
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800233 Class<? extends CharSequence> c = s.getClass();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800234
235 if (s instanceof GetChars || c == StringBuffer.class ||
236 c == StringBuilder.class || c == String.class) {
237 final int INDEX_INCREMENT = 500;
238 char[] temp = obtain(INDEX_INCREMENT);
239
240 while (start < end) {
241 int segstart = end - INDEX_INCREMENT;
242 if (segstart < start)
243 segstart = start;
244
245 getChars(s, segstart, end, temp, 0);
246
247 int count = end - segstart;
248 for (int i = count - 1; i >= 0; i--) {
249 if (temp[i] == ch) {
250 recycle(temp);
251 return i + segstart;
252 }
253 }
254
255 end = segstart;
256 }
257
258 recycle(temp);
259 return -1;
260 }
261
262 for (int i = end - 1; i >= start; i--)
263 if (s.charAt(i) == ch)
264 return i;
265
266 return -1;
267 }
268
269 public static int indexOf(CharSequence s, CharSequence needle) {
270 return indexOf(s, needle, 0, s.length());
271 }
272
273 public static int indexOf(CharSequence s, CharSequence needle, int start) {
274 return indexOf(s, needle, start, s.length());
275 }
276
277 public static int indexOf(CharSequence s, CharSequence needle,
278 int start, int end) {
279 int nlen = needle.length();
280 if (nlen == 0)
281 return start;
282
283 char c = needle.charAt(0);
284
285 for (;;) {
286 start = indexOf(s, c, start);
287 if (start > end - nlen) {
288 break;
289 }
290
291 if (start < 0) {
292 return -1;
293 }
294
295 if (regionMatches(s, start, needle, 0, nlen)) {
296 return start;
297 }
298
299 start++;
300 }
301 return -1;
302 }
303
304 public static boolean regionMatches(CharSequence one, int toffset,
305 CharSequence two, int ooffset,
306 int len) {
Raph Levien8d2aa192014-05-14 15:46:47 -0700307 int tempLen = 2 * len;
308 if (tempLen < len) {
309 // Integer overflow; len is unreasonably large
310 throw new IndexOutOfBoundsException();
311 }
312 char[] temp = obtain(tempLen);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800313
314 getChars(one, toffset, toffset + len, temp, 0);
315 getChars(two, ooffset, ooffset + len, temp, len);
316
317 boolean match = true;
318 for (int i = 0; i < len; i++) {
319 if (temp[i] != temp[i + len]) {
320 match = false;
321 break;
322 }
323 }
324
325 recycle(temp);
326 return match;
327 }
328
329 /**
330 * Create a new String object containing the given range of characters
331 * from the source string. This is different than simply calling
332 * {@link CharSequence#subSequence(int, int) CharSequence.subSequence}
333 * in that it does not preserve any style runs in the source sequence,
334 * allowing a more efficient implementation.
335 */
336 public static String substring(CharSequence source, int start, int end) {
337 if (source instanceof String)
338 return ((String) source).substring(start, end);
339 if (source instanceof StringBuilder)
340 return ((StringBuilder) source).substring(start, end);
341 if (source instanceof StringBuffer)
342 return ((StringBuffer) source).substring(start, end);
343
344 char[] temp = obtain(end - start);
345 getChars(source, start, end, temp, 0);
346 String ret = new String(temp, 0, end - start);
347 recycle(temp);
348
349 return ret;
350 }
351
352 /**
353 * Returns a string containing the tokens joined by delimiters.
Roozbeh Pournader42673c32017-07-20 15:23:33 -0700354 *
355 * @param delimiter a CharSequence that will be inserted between the tokens. If null, the string
356 * "null" will be used as the delimiter.
357 * @param tokens an array objects to be joined. Strings will be formed from the objects by
358 * calling object.toString(). If tokens is null, a NullPointerException will be thrown. If
359 * tokens is an empty array, an empty string will be returned.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800360 */
Roozbeh Pournader42673c32017-07-20 15:23:33 -0700361 public static String join(@NonNull CharSequence delimiter, @NonNull Object[] tokens) {
362 final int length = tokens.length;
363 if (length == 0) {
364 return "";
365 }
366 final StringBuilder sb = new StringBuilder();
367 sb.append(tokens[0]);
368 for (int i = 1; i < length; i++) {
369 sb.append(delimiter);
370 sb.append(tokens[i]);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800371 }
372 return sb.toString();
373 }
374
375 /**
376 * Returns a string containing the tokens joined by delimiters.
Roozbeh Pournader42673c32017-07-20 15:23:33 -0700377 *
378 * @param delimiter a CharSequence that will be inserted between the tokens. If null, the string
379 * "null" will be used as the delimiter.
380 * @param tokens an array objects to be joined. Strings will be formed from the objects by
381 * calling object.toString(). If tokens is null, a NullPointerException will be thrown. If
382 * tokens is empty, an empty string will be returned.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800383 */
Roozbeh Pournader42673c32017-07-20 15:23:33 -0700384 public static String join(@NonNull CharSequence delimiter, @NonNull Iterable tokens) {
385 final Iterator<?> it = tokens.iterator();
386 if (!it.hasNext()) {
387 return "";
388 }
389 final StringBuilder sb = new StringBuilder();
390 sb.append(it.next());
391 while (it.hasNext()) {
392 sb.append(delimiter);
Andreas Gampea8a58ff2016-05-18 11:58:39 -0700393 sb.append(it.next());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800394 }
395 return sb.toString();
396 }
397
398 /**
Tobias Thierer3e76c042018-06-21 10:31:39 +0100399 *
400 * This method yields the same result as {@code text.split(expression, -1)} except that if
401 * {@code text.isEmpty()} then this method returns an empty array whereas
402 * {@code "".split(expression, -1)} would have returned an array with a single {@code ""}.
403 *
404 * The {@code -1} means that trailing empty Strings are not removed from the result; for
405 * example split("a,", "," ) returns {"a", ""}. Note that whether a leading zero-width match
406 * can result in a leading {@code ""} depends on whether your app
407 * {@link android.content.pm.ApplicationInfo#targetSdkVersion targets an SDK version}
408 * {@code <= 28}; see {@link Pattern#split(CharSequence, int)}.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800409 *
410 * @param text the string to split
411 * @param expression the regular expression to match
412 * @return an array of strings. The array will be empty if text is empty
413 *
414 * @throws NullPointerException if expression or text is null
415 */
416 public static String[] split(String text, String expression) {
417 if (text.length() == 0) {
418 return EMPTY_STRING_ARRAY;
419 } else {
420 return text.split(expression, -1);
421 }
422 }
423
424 /**
Tobias Thierer3e76c042018-06-21 10:31:39 +0100425 * Splits a string on a pattern. This method yields the same result as
426 * {@code pattern.split(text, -1)} except that if {@code text.isEmpty()} then this method
427 * returns an empty array whereas {@code pattern.split("", -1)} would have returned an array
428 * with a single {@code ""}.
429 *
430 * The {@code -1} means that trailing empty Strings are not removed from the result;
431 * Note that whether a leading zero-width match can result in a leading {@code ""} depends
432 * on whether your app {@link android.content.pm.ApplicationInfo#targetSdkVersion targets
433 * an SDK version} {@code <= 28}; see {@link Pattern#split(CharSequence, int)}.
434 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800435 * @param text the string to split
436 * @param pattern the regular expression to match
437 * @return an array of strings. The array will be empty if text is empty
438 *
439 * @throws NullPointerException if expression or text is null
440 */
441 public static String[] split(String text, Pattern pattern) {
442 if (text.length() == 0) {
443 return EMPTY_STRING_ARRAY;
444 } else {
445 return pattern.split(text, -1);
446 }
447 }
448
449 /**
450 * An interface for splitting strings according to rules that are opaque to the user of this
451 * interface. This also has less overhead than split, which uses regular expressions and
452 * allocates an array to hold the results.
453 *
454 * <p>The most efficient way to use this class is:
455 *
456 * <pre>
457 * // Once
458 * TextUtils.StringSplitter splitter = new TextUtils.SimpleStringSplitter(delimiter);
459 *
460 * // Once per string to split
461 * splitter.setString(string);
462 * for (String s : splitter) {
463 * ...
464 * }
465 * </pre>
466 */
467 public interface StringSplitter extends Iterable<String> {
468 public void setString(String string);
469 }
470
471 /**
472 * A simple string splitter.
473 *
474 * <p>If the final character in the string to split is the delimiter then no empty string will
475 * be returned for the empty string after that delimeter. That is, splitting <tt>"a,b,"</tt> on
476 * comma will return <tt>"a", "b"</tt>, not <tt>"a", "b", ""</tt>.
477 */
478 public static class SimpleStringSplitter implements StringSplitter, Iterator<String> {
479 private String mString;
480 private char mDelimiter;
481 private int mPosition;
482 private int mLength;
483
484 /**
485 * Initializes the splitter. setString may be called later.
486 * @param delimiter the delimeter on which to split
487 */
488 public SimpleStringSplitter(char delimiter) {
489 mDelimiter = delimiter;
490 }
491
492 /**
493 * Sets the string to split
494 * @param string the string to split
495 */
496 public void setString(String string) {
497 mString = string;
498 mPosition = 0;
499 mLength = mString.length();
500 }
501
502 public Iterator<String> iterator() {
503 return this;
504 }
505
506 public boolean hasNext() {
507 return mPosition < mLength;
508 }
509
510 public String next() {
511 int end = mString.indexOf(mDelimiter, mPosition);
512 if (end == -1) {
513 end = mLength;
514 }
515 String nextString = mString.substring(mPosition, end);
516 mPosition = end + 1; // Skip the delimiter.
517 return nextString;
518 }
519
520 public void remove() {
521 throw new UnsupportedOperationException();
522 }
523 }
524
525 public static CharSequence stringOrSpannedString(CharSequence source) {
526 if (source == null)
527 return null;
528 if (source instanceof SpannedString)
529 return source;
530 if (source instanceof Spanned)
531 return new SpannedString(source);
532
533 return source.toString();
534 }
535
536 /**
537 * Returns true if the string is null or 0-length.
538 * @param str the string to be examined
539 * @return true if str is null or zero length
540 */
Scott Kennedy6cd132f2015-02-19 10:36:12 -0800541 public static boolean isEmpty(@Nullable CharSequence str) {
Amin Shaikhd4196c92017-02-06 17:04:47 -0800542 return str == null || str.length() == 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800543 }
544
Jeff Sharkey5cc0df22015-06-17 19:44:05 -0700545 /** {@hide} */
546 public static String nullIfEmpty(@Nullable String str) {
547 return isEmpty(str) ? null : str;
548 }
549
Eugene Susla6ed45d82017-01-22 13:52:51 -0800550 /** {@hide} */
551 public static String emptyIfNull(@Nullable String str) {
552 return str == null ? "" : str;
553 }
554
555 /** {@hide} */
556 public static String firstNotEmpty(@Nullable String a, @NonNull String b) {
557 return !isEmpty(a) ? a : Preconditions.checkStringNotEmpty(b);
558 }
559
Eugene Susla36e866b2017-02-23 18:24:39 -0800560 /** {@hide} */
561 public static int length(@Nullable String s) {
Jake Whartonfe16dfe2018-04-23 15:34:44 -0400562 return s != null ? s.length() : 0;
Eugene Susla36e866b2017-02-23 18:24:39 -0800563 }
564
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800565 /**
Makoto Onuki812d188a2017-08-07 09:58:23 -0700566 * @return interned string if it's null.
567 * @hide
568 */
569 public static String safeIntern(String s) {
570 return (s != null) ? s.intern() : null;
571 }
572
573 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800574 * Returns the length that the specified CharSequence would have if
Roozbeh Pournader3efda952015-08-11 09:55:57 -0700575 * spaces and ASCII control characters were trimmed from the start and end,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800576 * as by {@link String#trim}.
577 */
578 public static int getTrimmedLength(CharSequence s) {
579 int len = s.length();
580
581 int start = 0;
582 while (start < len && s.charAt(start) <= ' ') {
583 start++;
584 }
585
586 int end = len;
587 while (end > start && s.charAt(end - 1) <= ' ') {
588 end--;
589 }
590
591 return end - start;
592 }
593
594 /**
595 * Returns true if a and b are equal, including if they are both null.
596 * <p><i>Note: In platform versions 1.1 and earlier, this method only worked well if
597 * both the arguments were instances of String.</i></p>
598 * @param a first CharSequence to check
599 * @param b second CharSequence to check
600 * @return true if a and b are equal
601 */
602 public static boolean equals(CharSequence a, CharSequence b) {
603 if (a == b) return true;
604 int length;
605 if (a != null && b != null && (length = a.length()) == b.length()) {
606 if (a instanceof String && b instanceof String) {
607 return a.equals(b);
608 } else {
609 for (int i = 0; i < length; i++) {
610 if (a.charAt(i) != b.charAt(i)) return false;
611 }
612 return true;
613 }
614 }
615 return false;
616 }
617
Clara Bayarrid608a0a2016-04-27 11:53:22 +0100618 /**
619 * This function only reverses individual {@code char}s and not their associated
620 * spans. It doesn't support surrogate pairs (that correspond to non-BMP code points), combining
621 * sequences or conjuncts either.
622 * @deprecated Do not use.
Roozbeh Pournader3efda952015-08-11 09:55:57 -0700623 */
624 @Deprecated
Clara Bayarrid608a0a2016-04-27 11:53:22 +0100625 public static CharSequence getReverse(CharSequence source, int start, int end) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800626 return new Reverser(source, start, end);
627 }
628
629 private static class Reverser
630 implements CharSequence, GetChars
631 {
632 public Reverser(CharSequence source, int start, int end) {
633 mSource = source;
634 mStart = start;
635 mEnd = end;
636 }
637
638 public int length() {
639 return mEnd - mStart;
640 }
641
642 public CharSequence subSequence(int start, int end) {
643 char[] buf = new char[end - start];
644
645 getChars(start, end, buf, 0);
646 return new String(buf);
647 }
648
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800649 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800650 public String toString() {
651 return subSequence(0, length()).toString();
652 }
653
654 public char charAt(int off) {
Roozbeh Pournader9559c202016-12-13 10:59:50 -0800655 return (char) UCharacter.getMirror(mSource.charAt(mEnd - 1 - off));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800656 }
657
Roozbeh Pournader9559c202016-12-13 10:59:50 -0800658 @SuppressWarnings("deprecation")
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800659 public void getChars(int start, int end, char[] dest, int destoff) {
660 TextUtils.getChars(mSource, start + mStart, end + mStart,
661 dest, destoff);
662 AndroidCharacter.mirror(dest, 0, end - start);
663
664 int len = end - start;
665 int n = (end - start) / 2;
666 for (int i = 0; i < n; i++) {
667 char tmp = dest[destoff + i];
668
669 dest[destoff + i] = dest[destoff + len - i - 1];
670 dest[destoff + len - i - 1] = tmp;
671 }
672 }
673
674 private CharSequence mSource;
675 private int mStart;
676 private int mEnd;
677 }
678
679 /** @hide */
680 public static final int ALIGNMENT_SPAN = 1;
681 /** @hide */
Victoria Lease577ba532013-04-19 13:12:15 -0700682 public static final int FIRST_SPAN = ALIGNMENT_SPAN;
683 /** @hide */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800684 public static final int FOREGROUND_COLOR_SPAN = 2;
685 /** @hide */
686 public static final int RELATIVE_SIZE_SPAN = 3;
687 /** @hide */
688 public static final int SCALE_X_SPAN = 4;
689 /** @hide */
690 public static final int STRIKETHROUGH_SPAN = 5;
691 /** @hide */
692 public static final int UNDERLINE_SPAN = 6;
693 /** @hide */
694 public static final int STYLE_SPAN = 7;
695 /** @hide */
696 public static final int BULLET_SPAN = 8;
697 /** @hide */
698 public static final int QUOTE_SPAN = 9;
699 /** @hide */
700 public static final int LEADING_MARGIN_SPAN = 10;
701 /** @hide */
702 public static final int URL_SPAN = 11;
703 /** @hide */
704 public static final int BACKGROUND_COLOR_SPAN = 12;
705 /** @hide */
706 public static final int TYPEFACE_SPAN = 13;
707 /** @hide */
708 public static final int SUPERSCRIPT_SPAN = 14;
709 /** @hide */
710 public static final int SUBSCRIPT_SPAN = 15;
711 /** @hide */
712 public static final int ABSOLUTE_SIZE_SPAN = 16;
713 /** @hide */
714 public static final int TEXT_APPEARANCE_SPAN = 17;
715 /** @hide */
716 public static final int ANNOTATION = 18;
satokadb43582011-03-09 10:08:47 +0900717 /** @hide */
Gilles Debunnea00972a2011-04-13 16:07:31 -0700718 public static final int SUGGESTION_SPAN = 19;
Gilles Debunne28294cc2011-08-24 12:02:05 -0700719 /** @hide */
720 public static final int SPELL_CHECK_SPAN = 20;
721 /** @hide */
722 public static final int SUGGESTION_RANGE_SPAN = 21;
Luca Zanoline6d36822011-08-30 18:04:34 +0100723 /** @hide */
724 public static final int EASY_EDIT_SPAN = 22;
Victoria Leasedf8ef4b2012-08-17 15:34:01 -0700725 /** @hide */
726 public static final int LOCALE_SPAN = 23;
Victoria Lease577ba532013-04-19 13:12:15 -0700727 /** @hide */
Niels Egberts4f4ead42014-06-23 12:01:14 +0100728 public static final int TTS_SPAN = 24;
729 /** @hide */
Phil Weaver193520e2016-12-13 09:39:06 -0800730 public static final int ACCESSIBILITY_CLICKABLE_SPAN = 25;
731 /** @hide */
732 public static final int ACCESSIBILITY_URL_SPAN = 26;
733 /** @hide */
Haoyu Zhang003f7372018-09-24 09:47:58 -0700734 public static final int LINE_BACKGROUND_SPAN = 27;
735 /** @hide */
Haoyu Zhang63a5efb2018-11-26 15:36:29 -0800736 public static final int LINE_HEIGHT_SPAN = 28;
737 /** @hide */
738 public static final int LAST_SPAN = LINE_HEIGHT_SPAN;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800739
740 /**
741 * Flatten a CharSequence and whatever styles can be copied across processes
742 * into the parcel.
743 */
Jake Whartonb1f474c2018-05-30 15:11:13 -0400744 public static void writeToParcel(@Nullable CharSequence cs, @NonNull Parcel p,
745 int parcelableFlags) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800746 if (cs instanceof Spanned) {
747 p.writeInt(0);
748 p.writeString(cs.toString());
749
750 Spanned sp = (Spanned) cs;
751 Object[] os = sp.getSpans(0, cs.length(), Object.class);
752
753 // note to people adding to this: check more specific types
754 // before more generic types. also notice that it uses
755 // "if" instead of "else if" where there are interfaces
756 // so one object can be several.
757
758 for (int i = 0; i < os.length; i++) {
759 Object o = os[i];
760 Object prop = os[i];
761
762 if (prop instanceof CharacterStyle) {
763 prop = ((CharacterStyle) prop).getUnderlying();
764 }
765
766 if (prop instanceof ParcelableSpan) {
Alan Viverettea70d4a92015-06-02 16:11:00 -0700767 final ParcelableSpan ps = (ParcelableSpan) prop;
768 final int spanTypeId = ps.getSpanTypeIdInternal();
Victoria Lease577ba532013-04-19 13:12:15 -0700769 if (spanTypeId < FIRST_SPAN || spanTypeId > LAST_SPAN) {
Alan Viverettea70d4a92015-06-02 16:11:00 -0700770 Log.e(TAG, "External class \"" + ps.getClass().getSimpleName()
Victoria Lease577ba532013-04-19 13:12:15 -0700771 + "\" is attempting to use the frameworks-only ParcelableSpan"
772 + " interface");
773 } else {
774 p.writeInt(spanTypeId);
Alan Viverettea70d4a92015-06-02 16:11:00 -0700775 ps.writeToParcelInternal(p, parcelableFlags);
Victoria Lease577ba532013-04-19 13:12:15 -0700776 writeWhere(p, sp, o);
777 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800778 }
779 }
780
781 p.writeInt(0);
782 } else {
783 p.writeInt(1);
784 if (cs != null) {
785 p.writeString(cs.toString());
786 } else {
787 p.writeString(null);
788 }
789 }
790 }
791
792 private static void writeWhere(Parcel p, Spanned sp, Object o) {
793 p.writeInt(sp.getSpanStart(o));
794 p.writeInt(sp.getSpanEnd(o));
795 p.writeInt(sp.getSpanFlags(o));
796 }
797
798 public static final Parcelable.Creator<CharSequence> CHAR_SEQUENCE_CREATOR
799 = new Parcelable.Creator<CharSequence>() {
800 /**
801 * Read and return a new CharSequence, possibly with styles,
802 * from the parcel.
803 */
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800804 public CharSequence createFromParcel(Parcel p) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800805 int kind = p.readInt();
806
Martin Wallgrencee20512011-04-07 14:45:43 +0200807 String string = p.readString();
808 if (string == null) {
809 return null;
810 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800811
Martin Wallgrencee20512011-04-07 14:45:43 +0200812 if (kind == 1) {
813 return string;
814 }
815
816 SpannableString sp = new SpannableString(string);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800817
818 while (true) {
819 kind = p.readInt();
820
821 if (kind == 0)
822 break;
823
824 switch (kind) {
825 case ALIGNMENT_SPAN:
826 readSpan(p, sp, new AlignmentSpan.Standard(p));
827 break;
828
829 case FOREGROUND_COLOR_SPAN:
830 readSpan(p, sp, new ForegroundColorSpan(p));
831 break;
832
833 case RELATIVE_SIZE_SPAN:
834 readSpan(p, sp, new RelativeSizeSpan(p));
835 break;
836
837 case SCALE_X_SPAN:
838 readSpan(p, sp, new ScaleXSpan(p));
839 break;
840
841 case STRIKETHROUGH_SPAN:
842 readSpan(p, sp, new StrikethroughSpan(p));
843 break;
844
845 case UNDERLINE_SPAN:
846 readSpan(p, sp, new UnderlineSpan(p));
847 break;
848
849 case STYLE_SPAN:
850 readSpan(p, sp, new StyleSpan(p));
851 break;
852
853 case BULLET_SPAN:
854 readSpan(p, sp, new BulletSpan(p));
855 break;
856
857 case QUOTE_SPAN:
858 readSpan(p, sp, new QuoteSpan(p));
859 break;
860
861 case LEADING_MARGIN_SPAN:
862 readSpan(p, sp, new LeadingMarginSpan.Standard(p));
863 break;
864
865 case URL_SPAN:
866 readSpan(p, sp, new URLSpan(p));
867 break;
868
869 case BACKGROUND_COLOR_SPAN:
870 readSpan(p, sp, new BackgroundColorSpan(p));
871 break;
872
873 case TYPEFACE_SPAN:
874 readSpan(p, sp, new TypefaceSpan(p));
875 break;
876
877 case SUPERSCRIPT_SPAN:
878 readSpan(p, sp, new SuperscriptSpan(p));
879 break;
880
881 case SUBSCRIPT_SPAN:
882 readSpan(p, sp, new SubscriptSpan(p));
883 break;
884
885 case ABSOLUTE_SIZE_SPAN:
886 readSpan(p, sp, new AbsoluteSizeSpan(p));
887 break;
888
889 case TEXT_APPEARANCE_SPAN:
890 readSpan(p, sp, new TextAppearanceSpan(p));
891 break;
892
893 case ANNOTATION:
894 readSpan(p, sp, new Annotation(p));
895 break;
896
Gilles Debunnea00972a2011-04-13 16:07:31 -0700897 case SUGGESTION_SPAN:
898 readSpan(p, sp, new SuggestionSpan(p));
899 break;
900
Gilles Debunne28294cc2011-08-24 12:02:05 -0700901 case SPELL_CHECK_SPAN:
902 readSpan(p, sp, new SpellCheckSpan(p));
903 break;
904
905 case SUGGESTION_RANGE_SPAN:
Gilles Debunne0eea6682011-08-29 13:30:31 -0700906 readSpan(p, sp, new SuggestionRangeSpan(p));
Gilles Debunne28294cc2011-08-24 12:02:05 -0700907 break;
Gilles Debunnee90bed12011-08-30 14:28:27 -0700908
Luca Zanoline6d36822011-08-30 18:04:34 +0100909 case EASY_EDIT_SPAN:
Luca Zanolin1b15ba52013-02-20 14:31:37 +0000910 readSpan(p, sp, new EasyEditSpan(p));
Luca Zanoline6d36822011-08-30 18:04:34 +0100911 break;
912
Victoria Leasedf8ef4b2012-08-17 15:34:01 -0700913 case LOCALE_SPAN:
914 readSpan(p, sp, new LocaleSpan(p));
915 break;
916
Niels Egberts4f4ead42014-06-23 12:01:14 +0100917 case TTS_SPAN:
918 readSpan(p, sp, new TtsSpan(p));
919 break;
920
Phil Weaver193520e2016-12-13 09:39:06 -0800921 case ACCESSIBILITY_CLICKABLE_SPAN:
922 readSpan(p, sp, new AccessibilityClickableSpan(p));
923 break;
924
925 case ACCESSIBILITY_URL_SPAN:
926 readSpan(p, sp, new AccessibilityURLSpan(p));
927 break;
928
Haoyu Zhang003f7372018-09-24 09:47:58 -0700929 case LINE_BACKGROUND_SPAN:
930 readSpan(p, sp, new LineBackgroundSpan.Standard(p));
931 break;
932
Haoyu Zhang63a5efb2018-11-26 15:36:29 -0800933 case LINE_HEIGHT_SPAN:
934 readSpan(p, sp, new LineHeightSpan.Standard(p));
935 break;
936
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800937 default:
938 throw new RuntimeException("bogus span encoding " + kind);
939 }
940 }
941
942 return sp;
943 }
944
945 public CharSequence[] newArray(int size)
946 {
947 return new CharSequence[size];
948 }
949 };
950
951 /**
952 * Debugging tool to print the spans in a CharSequence. The output will
953 * be printed one span per line. If the CharSequence is not a Spanned,
954 * then the entire string will be printed on a single line.
955 */
956 public static void dumpSpans(CharSequence cs, Printer printer, String prefix) {
957 if (cs instanceof Spanned) {
958 Spanned sp = (Spanned) cs;
959 Object[] os = sp.getSpans(0, cs.length(), Object.class);
960
961 for (int i = 0; i < os.length; i++) {
962 Object o = os[i];
963 printer.println(prefix + cs.subSequence(sp.getSpanStart(o),
964 sp.getSpanEnd(o)) + ": "
965 + Integer.toHexString(System.identityHashCode(o))
966 + " " + o.getClass().getCanonicalName()
967 + " (" + sp.getSpanStart(o) + "-" + sp.getSpanEnd(o)
968 + ") fl=#" + sp.getSpanFlags(o));
969 }
970 } else {
971 printer.println(prefix + cs + ": (no spans)");
972 }
973 }
974
975 /**
976 * Return a new CharSequence in which each of the source strings is
977 * replaced by the corresponding element of the destinations.
978 */
979 public static CharSequence replace(CharSequence template,
980 String[] sources,
981 CharSequence[] destinations) {
982 SpannableStringBuilder tb = new SpannableStringBuilder(template);
983
984 for (int i = 0; i < sources.length; i++) {
985 int where = indexOf(tb, sources[i]);
986
987 if (where >= 0)
988 tb.setSpan(sources[i], where, where + sources[i].length(),
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800989 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800990 }
991
992 for (int i = 0; i < sources.length; i++) {
993 int start = tb.getSpanStart(sources[i]);
994 int end = tb.getSpanEnd(sources[i]);
995
996 if (start >= 0) {
997 tb.replace(start, end, destinations[i]);
998 }
999 }
1000
1001 return tb;
1002 }
1003
1004 /**
1005 * Replace instances of "^1", "^2", etc. in the
1006 * <code>template</code> CharSequence with the corresponding
1007 * <code>values</code>. "^^" is used to produce a single caret in
1008 * the output. Only up to 9 replacement values are supported,
1009 * "^10" will be produce the first replacement value followed by a
1010 * '0'.
1011 *
1012 * @param template the input text containing "^1"-style
1013 * placeholder values. This object is not modified; a copy is
1014 * returned.
1015 *
1016 * @param values CharSequences substituted into the template. The
1017 * first is substituted for "^1", the second for "^2", and so on.
1018 *
1019 * @return the new CharSequence produced by doing the replacement
1020 *
1021 * @throws IllegalArgumentException if the template requests a
1022 * value that was not provided, or if more than 9 values are
1023 * provided.
1024 */
1025 public static CharSequence expandTemplate(CharSequence template,
1026 CharSequence... values) {
1027 if (values.length > 9) {
1028 throw new IllegalArgumentException("max of 9 values are supported");
1029 }
1030
1031 SpannableStringBuilder ssb = new SpannableStringBuilder(template);
1032
1033 try {
1034 int i = 0;
1035 while (i < ssb.length()) {
1036 if (ssb.charAt(i) == '^') {
1037 char next = ssb.charAt(i+1);
1038 if (next == '^') {
1039 ssb.delete(i+1, i+2);
1040 ++i;
1041 continue;
1042 } else if (Character.isDigit(next)) {
1043 int which = Character.getNumericValue(next) - 1;
1044 if (which < 0) {
1045 throw new IllegalArgumentException(
1046 "template requests value ^" + (which+1));
1047 }
1048 if (which >= values.length) {
1049 throw new IllegalArgumentException(
1050 "template requests value ^" + (which+1) +
1051 "; only " + values.length + " provided");
1052 }
1053 ssb.replace(i, i+2, values[which]);
1054 i += values[which].length();
1055 continue;
1056 }
1057 }
1058 ++i;
1059 }
1060 } catch (IndexOutOfBoundsException ignore) {
1061 // happens when ^ is the last character in the string.
1062 }
1063 return ssb;
1064 }
1065
1066 public static int getOffsetBefore(CharSequence text, int offset) {
1067 if (offset == 0)
1068 return 0;
1069 if (offset == 1)
1070 return 0;
1071
1072 char c = text.charAt(offset - 1);
1073
1074 if (c >= '\uDC00' && c <= '\uDFFF') {
1075 char c1 = text.charAt(offset - 2);
1076
1077 if (c1 >= '\uD800' && c1 <= '\uDBFF')
1078 offset -= 2;
1079 else
1080 offset -= 1;
1081 } else {
1082 offset -= 1;
1083 }
1084
1085 if (text instanceof Spanned) {
1086 ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
1087 ReplacementSpan.class);
1088
1089 for (int i = 0; i < spans.length; i++) {
1090 int start = ((Spanned) text).getSpanStart(spans[i]);
1091 int end = ((Spanned) text).getSpanEnd(spans[i]);
1092
1093 if (start < offset && end > offset)
1094 offset = start;
1095 }
1096 }
1097
1098 return offset;
1099 }
1100
1101 public static int getOffsetAfter(CharSequence text, int offset) {
1102 int len = text.length();
1103
1104 if (offset == len)
1105 return len;
1106 if (offset == len - 1)
1107 return len;
1108
1109 char c = text.charAt(offset);
1110
1111 if (c >= '\uD800' && c <= '\uDBFF') {
1112 char c1 = text.charAt(offset + 1);
1113
1114 if (c1 >= '\uDC00' && c1 <= '\uDFFF')
1115 offset += 2;
1116 else
1117 offset += 1;
1118 } else {
1119 offset += 1;
1120 }
1121
1122 if (text instanceof Spanned) {
1123 ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
1124 ReplacementSpan.class);
1125
1126 for (int i = 0; i < spans.length; i++) {
1127 int start = ((Spanned) text).getSpanStart(spans[i]);
1128 int end = ((Spanned) text).getSpanEnd(spans[i]);
1129
1130 if (start < offset && end > offset)
1131 offset = end;
1132 }
1133 }
1134
1135 return offset;
1136 }
1137
1138 private static void readSpan(Parcel p, Spannable sp, Object o) {
1139 sp.setSpan(o, p.readInt(), p.readInt(), p.readInt());
1140 }
1141
Daisuke Miyakawac1d27482009-05-25 17:37:41 +09001142 /**
1143 * Copies the spans from the region <code>start...end</code> in
1144 * <code>source</code> to the region
1145 * <code>destoff...destoff+end-start</code> in <code>dest</code>.
1146 * Spans in <code>source</code> that begin before <code>start</code>
1147 * or end after <code>end</code> but overlap this range are trimmed
1148 * as if they began at <code>start</code> or ended at <code>end</code>.
1149 *
1150 * @throws IndexOutOfBoundsException if any of the copied spans
1151 * are out of range in <code>dest</code>.
1152 */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001153 public static void copySpansFrom(Spanned source, int start, int end,
1154 Class kind,
1155 Spannable dest, int destoff) {
1156 if (kind == null) {
1157 kind = Object.class;
1158 }
1159
1160 Object[] spans = source.getSpans(start, end, kind);
1161
1162 for (int i = 0; i < spans.length; i++) {
1163 int st = source.getSpanStart(spans[i]);
1164 int en = source.getSpanEnd(spans[i]);
1165 int fl = source.getSpanFlags(spans[i]);
1166
1167 if (st < start)
1168 st = start;
1169 if (en > end)
1170 en = end;
1171
1172 dest.setSpan(spans[i], st - start + destoff, en - start + destoff,
1173 fl);
1174 }
1175 }
1176
Roozbeh Pournader205a9932017-06-08 00:23:42 -07001177 /**
1178 * Transforms a CharSequences to uppercase, copying the sources spans and keeping them spans as
1179 * much as possible close to their relative original places. In the case the the uppercase
1180 * string is identical to the sources, the source itself is returned instead of being copied.
1181 *
1182 * If copySpans is set, source must be an instance of Spanned.
1183 *
1184 * {@hide}
1185 */
1186 @NonNull
1187 public static CharSequence toUpperCase(@Nullable Locale locale, @NonNull CharSequence source,
1188 boolean copySpans) {
1189 final Edits edits = new Edits();
1190 if (!copySpans) { // No spans. Just uppercase the characters.
1191 final StringBuilder result = CaseMap.toUpper().apply(
1192 locale, source, new StringBuilder(), edits);
1193 return edits.hasChanges() ? result : source;
1194 }
1195
1196 final SpannableStringBuilder result = CaseMap.toUpper().apply(
1197 locale, source, new SpannableStringBuilder(), edits);
1198 if (!edits.hasChanges()) {
1199 // No changes happened while capitalizing. We can return the source as it was.
1200 return source;
1201 }
1202
1203 final Edits.Iterator iterator = edits.getFineIterator();
1204 final int sourceLength = source.length();
1205 final Spanned spanned = (Spanned) source;
1206 final Object[] spans = spanned.getSpans(0, sourceLength, Object.class);
1207 for (Object span : spans) {
1208 final int sourceStart = spanned.getSpanStart(span);
1209 final int sourceEnd = spanned.getSpanEnd(span);
1210 final int flags = spanned.getSpanFlags(span);
1211 // Make sure the indices are not at the end of the string, since in that case
1212 // iterator.findSourceIndex() would fail.
1213 final int destStart = sourceStart == sourceLength ? result.length() :
1214 toUpperMapToDest(iterator, sourceStart);
1215 final int destEnd = sourceEnd == sourceLength ? result.length() :
1216 toUpperMapToDest(iterator, sourceEnd);
1217 result.setSpan(span, destStart, destEnd, flags);
1218 }
1219 return result;
1220 }
1221
1222 // helper method for toUpperCase()
1223 private static int toUpperMapToDest(Edits.Iterator iterator, int sourceIndex) {
1224 // Guaranteed to succeed if sourceIndex < source.length().
1225 iterator.findSourceIndex(sourceIndex);
1226 if (sourceIndex == iterator.sourceIndex()) {
1227 return iterator.destinationIndex();
1228 }
1229 // We handle the situation differently depending on if we are in the changed slice or an
1230 // unchanged one: In an unchanged slice, we can find the exact location the span
1231 // boundary was before and map there.
1232 //
1233 // But in a changed slice, we need to treat the whole destination slice as an atomic unit.
1234 // We adjust the span boundary to the end of that slice to reduce of the chance of adjacent
1235 // spans in the source overlapping in the result. (The choice for the end vs the beginning
1236 // is somewhat arbitrary, but was taken because we except to see slightly more spans only
1237 // affecting a base character compared to spans only affecting a combining character.)
1238 if (iterator.hasChange()) {
1239 return iterator.destinationIndex() + iterator.newLength();
1240 } else {
1241 // Move the index 1:1 along with this unchanged piece of text.
1242 return iterator.destinationIndex() + (sourceIndex - iterator.sourceIndex());
1243 }
1244 }
1245
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001246 public enum TruncateAt {
1247 START,
1248 MIDDLE,
1249 END,
1250 MARQUEE,
Fabrice Di Megliocb332642011-09-23 19:08:04 -07001251 /**
1252 * @hide
1253 */
Mathew Inwoodefeab842018-08-14 15:21:30 +01001254 @UnsupportedAppUsage
Fabrice Di Megliocb332642011-09-23 19:08:04 -07001255 END_SMALL
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001256 }
1257
1258 public interface EllipsizeCallback {
1259 /**
1260 * This method is called to report that the specified region of
1261 * text was ellipsized away by a call to {@link #ellipsize}.
1262 */
1263 public void ellipsized(int start, int end);
1264 }
1265
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001266 /**
1267 * Returns the original text if it fits in the specified width
1268 * given the properties of the specified Paint,
1269 * or, if it does not fit, a truncated
1270 * copy with ellipsis character added at the specified edge or center.
1271 */
Siyamed Sinir1a5648a2018-01-23 16:40:06 -08001272 public static CharSequence ellipsize(CharSequence text,
1273 TextPaint p,
1274 float avail, TruncateAt where) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001275 return ellipsize(text, p, avail, where, false, null);
1276 }
1277
1278 /**
1279 * Returns the original text if it fits in the specified width
1280 * given the properties of the specified Paint,
Doug Felte8e45f22010-03-29 14:58:40 -07001281 * or, if it does not fit, a copy with ellipsis character added
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001282 * at the specified edge or center.
1283 * If <code>preserveLength</code> is specified, the returned copy
1284 * will be padded with zero-width spaces to preserve the original
1285 * length and offsets instead of truncating.
1286 * If <code>callback</code> is non-null, it will be called to
Doug Feltcb3791202011-07-07 11:57:48 -07001287 * report the start and end of the ellipsized range. TextDirection
1288 * is determined by the first strong directional character.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001289 */
Siyamed Sinir1a5648a2018-01-23 16:40:06 -08001290 public static CharSequence ellipsize(CharSequence text,
1291 TextPaint paint,
1292 float avail, TruncateAt where,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001293 boolean preserveLength,
Roozbeh Pournader9ea756f2017-07-25 11:20:29 -07001294 @Nullable EllipsizeCallback callback) {
Doug Feltcb3791202011-07-07 11:57:48 -07001295 return ellipsize(text, paint, avail, where, preserveLength, callback,
Fabrice Di Megliocb332642011-09-23 19:08:04 -07001296 TextDirectionHeuristics.FIRSTSTRONG_LTR,
Roozbeh Pournader9ea756f2017-07-25 11:20:29 -07001297 getEllipsisString(where));
Doug Feltcb3791202011-07-07 11:57:48 -07001298 }
1299
1300 /**
1301 * Returns the original text if it fits in the specified width
1302 * given the properties of the specified Paint,
1303 * or, if it does not fit, a copy with ellipsis character added
1304 * at the specified edge or center.
1305 * If <code>preserveLength</code> is specified, the returned copy
1306 * will be padded with zero-width spaces to preserve the original
1307 * length and offsets instead of truncating.
1308 * If <code>callback</code> is non-null, it will be called to
1309 * report the start and end of the ellipsized range.
1310 *
1311 * @hide
1312 */
Siyamed Sinir1a5648a2018-01-23 16:40:06 -08001313 public static CharSequence ellipsize(CharSequence text,
1314 TextPaint paint,
1315 float avail, TruncateAt where,
Doug Feltcb3791202011-07-07 11:57:48 -07001316 boolean preserveLength,
Roozbeh Pournader9ea756f2017-07-25 11:20:29 -07001317 @Nullable EllipsizeCallback callback,
Siyamed Sinir1a5648a2018-01-23 16:40:06 -08001318 TextDirectionHeuristic textDir, String ellipsis) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001319
Siyamed Sinir1a5648a2018-01-23 16:40:06 -08001320 int len = text.length();
1321
Seigo Nonaka9d3bd082018-01-11 10:02:12 -08001322 MeasuredParagraph mt = null;
Doug Felte8e45f22010-03-29 14:58:40 -07001323 try {
Seigo Nonaka9d3bd082018-01-11 10:02:12 -08001324 mt = MeasuredParagraph.buildForMeasurement(paint, text, 0, text.length(), textDir, mt);
Seigo Nonakaf1644f72017-11-27 22:09:49 -08001325 float width = mt.getWholeWidth();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001326
Doug Felte8e45f22010-03-29 14:58:40 -07001327 if (width <= avail) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001328 if (callback != null) {
1329 callback.ellipsized(0, 0);
1330 }
Siyamed Sinir1a5648a2018-01-23 16:40:06 -08001331
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001332 return text;
1333 }
1334
Siyamed Sinir1a5648a2018-01-23 16:40:06 -08001335 // XXX assumes ellipsis string does not require shaping and
1336 // is unaffected by style
1337 float ellipsiswid = paint.measureText(ellipsis);
1338 avail -= ellipsiswid;
Roozbeh Pournader287c8d62017-07-25 13:52:57 -07001339
Siyamed Sinir1a5648a2018-01-23 16:40:06 -08001340 int left = 0;
1341 int right = len;
1342 if (avail < 0) {
1343 // it all goes
1344 } else if (where == TruncateAt.START) {
1345 right = len - mt.breakText(len, false, avail);
1346 } else if (where == TruncateAt.END || where == TruncateAt.END_SMALL) {
1347 left = mt.breakText(len, true, avail);
1348 } else {
1349 right = len - mt.breakText(len, false, avail / 2);
1350 avail -= mt.measure(right, len);
1351 left = mt.breakText(right, true, avail);
Roozbeh Pournader7f0ebc92017-08-02 22:47:14 +00001352 }
Siyamed Sinir1a5648a2018-01-23 16:40:06 -08001353
1354 if (callback != null) {
1355 callback.ellipsized(left, right);
1356 }
1357
1358 final char[] buf = mt.getChars();
1359 Spanned sp = text instanceof Spanned ? (Spanned) text : null;
1360
1361 final int removed = right - left;
1362 final int remaining = len - removed;
1363 if (preserveLength) {
1364 if (remaining > 0 && removed >= ellipsis.length()) {
1365 ellipsis.getChars(0, ellipsis.length(), buf, left);
1366 left += ellipsis.length();
1367 } // else skip the ellipsis
1368 for (int i = left; i < right; i++) {
1369 buf[i] = ELLIPSIS_FILLER;
1370 }
1371 String s = new String(buf, 0, len);
1372 if (sp == null) {
1373 return s;
1374 }
1375 SpannableString ss = new SpannableString(s);
1376 copySpansFrom(sp, 0, len, Object.class, ss, 0);
1377 return ss;
1378 }
1379
1380 if (remaining == 0) {
1381 return "";
1382 }
1383
1384 if (sp == null) {
1385 StringBuilder sb = new StringBuilder(remaining + ellipsis.length());
1386 sb.append(buf, 0, left);
1387 sb.append(ellipsis);
1388 sb.append(buf, right, len - right);
1389 return sb.toString();
1390 }
1391
1392 SpannableStringBuilder ssb = new SpannableStringBuilder();
1393 ssb.append(text, 0, left);
1394 ssb.append(ellipsis);
1395 ssb.append(text, right, len);
1396 return ssb;
Doug Felte8e45f22010-03-29 14:58:40 -07001397 } finally {
Seigo Nonakaf1644f72017-11-27 22:09:49 -08001398 if (mt != null) {
1399 mt.recycle();
1400 }
Doug Felte8e45f22010-03-29 14:58:40 -07001401 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001402 }
1403
1404 /**
Roozbeh Pournader3bfce332016-06-17 15:03:56 -07001405 * Formats a list of CharSequences by repeatedly inserting the separator between them,
1406 * but stopping when the resulting sequence is too wide for the specified width.
1407 *
1408 * This method actually tries to fit the maximum number of elements. So if {@code "A, 11 more"
1409 * fits}, {@code "A, B, 10 more"} doesn't fit, but {@code "A, B, C, 9 more"} fits again (due to
1410 * the glyphs for the digits being very wide, for example), it returns
1411 * {@code "A, B, C, 9 more"}. Because of this, this method may be inefficient for very long
1412 * lists.
1413 *
1414 * Note that the elements of the returned value, as well as the string for {@code moreId}, will
1415 * be bidi-wrapped using {@link BidiFormatter#unicodeWrap} based on the locale of the input
1416 * Context. If the input {@code Context} is null, the default BidiFormatter from
1417 * {@link BidiFormatter#getInstance()} will be used.
1418 *
1419 * @param context the {@code Context} to get the {@code moreId} resource from. If {@code null},
1420 * an ellipsis (U+2026) would be used for {@code moreId}.
1421 * @param elements the list to format
1422 * @param separator a separator, such as {@code ", "}
1423 * @param paint the Paint with which to measure the text
1424 * @param avail the horizontal width available for the text (in pixels)
1425 * @param moreId the resource ID for the pluralized string to insert at the end of sequence when
1426 * some of the elements don't fit.
1427 *
1428 * @return the formatted CharSequence. If even the shortest sequence (e.g. {@code "A, 11 more"})
1429 * doesn't fit, it will return an empty string.
1430 */
Siyamed Sinir1a5648a2018-01-23 16:40:06 -08001431
Roozbeh Pournader3bfce332016-06-17 15:03:56 -07001432 public static CharSequence listEllipsize(@Nullable Context context,
1433 @Nullable List<CharSequence> elements, @NonNull String separator,
1434 @NonNull TextPaint paint, @FloatRange(from=0.0,fromInclusive=false) float avail,
1435 @PluralsRes int moreId) {
1436 if (elements == null) {
1437 return "";
1438 }
1439 final int totalLen = elements.size();
1440 if (totalLen == 0) {
1441 return "";
1442 }
1443
1444 final Resources res;
1445 final BidiFormatter bidiFormatter;
1446 if (context == null) {
1447 res = null;
1448 bidiFormatter = BidiFormatter.getInstance();
1449 } else {
1450 res = context.getResources();
1451 bidiFormatter = BidiFormatter.getInstance(res.getConfiguration().getLocales().get(0));
1452 }
1453
1454 final SpannableStringBuilder output = new SpannableStringBuilder();
1455 final int[] endIndexes = new int[totalLen];
1456 for (int i = 0; i < totalLen; i++) {
1457 output.append(bidiFormatter.unicodeWrap(elements.get(i)));
1458 if (i != totalLen - 1) { // Insert a separator, except at the very end.
1459 output.append(separator);
1460 }
1461 endIndexes[i] = output.length();
1462 }
1463
1464 for (int i = totalLen - 1; i >= 0; i--) {
1465 // Delete the tail of the string, cutting back to one less element.
1466 output.delete(endIndexes[i], output.length());
1467
1468 final int remainingElements = totalLen - i - 1;
1469 if (remainingElements > 0) {
1470 CharSequence morePiece = (res == null) ?
Roozbeh Pournader9ea756f2017-07-25 11:20:29 -07001471 ELLIPSIS_NORMAL :
Roozbeh Pournader3bfce332016-06-17 15:03:56 -07001472 res.getQuantityString(moreId, remainingElements, remainingElements);
1473 morePiece = bidiFormatter.unicodeWrap(morePiece);
1474 output.append(morePiece);
1475 }
1476
1477 final float width = paint.measureText(output, 0, output.length());
1478 if (width <= avail) { // The string fits.
1479 return output;
1480 }
1481 }
1482 return ""; // Nothing fits.
1483 }
1484
1485 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001486 * Converts a CharSequence of the comma-separated form "Andy, Bob,
1487 * Charles, David" that is too wide to fit into the specified width
1488 * into one like "Andy, Bob, 2 more".
1489 *
1490 * @param text the text to truncate
1491 * @param p the Paint with which to measure the text
Roozbeh Pournader3bfce332016-06-17 15:03:56 -07001492 * @param avail the horizontal width available for the text (in pixels)
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001493 * @param oneMore the string for "1 more" in the current locale
1494 * @param more the string for "%d more" in the current locale
Roozbeh Pournader3bfce332016-06-17 15:03:56 -07001495 *
1496 * @deprecated Do not use. This is not internationalized, and has known issues
1497 * with right-to-left text, languages that have more than one plural form, languages
1498 * that use a different character as a comma-like separator, etc.
1499 * Use {@link #listEllipsize} instead.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001500 */
Roozbeh Pournader3bfce332016-06-17 15:03:56 -07001501 @Deprecated
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001502 public static CharSequence commaEllipsize(CharSequence text,
1503 TextPaint p, float avail,
1504 String oneMore,
1505 String more) {
Doug Feltcb3791202011-07-07 11:57:48 -07001506 return commaEllipsize(text, p, avail, oneMore, more,
1507 TextDirectionHeuristics.FIRSTSTRONG_LTR);
1508 }
1509
1510 /**
1511 * @hide
1512 */
Roozbeh Pournader3bfce332016-06-17 15:03:56 -07001513 @Deprecated
Doug Feltcb3791202011-07-07 11:57:48 -07001514 public static CharSequence commaEllipsize(CharSequence text, TextPaint p,
1515 float avail, String oneMore, String more, TextDirectionHeuristic textDir) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001516
Seigo Nonaka9d3bd082018-01-11 10:02:12 -08001517 MeasuredParagraph mt = null;
1518 MeasuredParagraph tempMt = null;
Doug Felte8e45f22010-03-29 14:58:40 -07001519 try {
1520 int len = text.length();
Seigo Nonaka9d3bd082018-01-11 10:02:12 -08001521 mt = MeasuredParagraph.buildForMeasurement(p, text, 0, len, textDir, mt);
Seigo Nonakaf1644f72017-11-27 22:09:49 -08001522 final float width = mt.getWholeWidth();
Doug Felte8e45f22010-03-29 14:58:40 -07001523 if (width <= avail) {
1524 return text;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001525 }
1526
Seigo Nonakaf1644f72017-11-27 22:09:49 -08001527 char[] buf = mt.getChars();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001528
Doug Felte8e45f22010-03-29 14:58:40 -07001529 int commaCount = 0;
1530 for (int i = 0; i < len; i++) {
1531 if (buf[i] == ',') {
1532 commaCount++;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001533 }
1534 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001535
Doug Felte8e45f22010-03-29 14:58:40 -07001536 int remaining = commaCount + 1;
1537
1538 int ok = 0;
Doug Felte8e45f22010-03-29 14:58:40 -07001539 String okFormat = "";
1540
1541 int w = 0;
1542 int count = 0;
Seigo Nonakaf1644f72017-11-27 22:09:49 -08001543 float[] widths = mt.getWidths().getRawArray();
Doug Felte8e45f22010-03-29 14:58:40 -07001544
Doug Felte8e45f22010-03-29 14:58:40 -07001545 for (int i = 0; i < len; i++) {
1546 w += widths[i];
1547
1548 if (buf[i] == ',') {
1549 count++;
1550
1551 String format;
1552 // XXX should not insert spaces, should be part of string
1553 // XXX should use plural rules and not assume English plurals
1554 if (--remaining == 1) {
1555 format = " " + oneMore;
1556 } else {
1557 format = " " + String.format(more, remaining);
1558 }
1559
1560 // XXX this is probably ok, but need to look at it more
Seigo Nonaka9d3bd082018-01-11 10:02:12 -08001561 tempMt = MeasuredParagraph.buildForMeasurement(
Seigo Nonakaf1644f72017-11-27 22:09:49 -08001562 p, format, 0, format.length(), textDir, tempMt);
1563 float moreWid = tempMt.getWholeWidth();
Doug Felte8e45f22010-03-29 14:58:40 -07001564
1565 if (w + moreWid <= avail) {
1566 ok = i + 1;
Doug Felte8e45f22010-03-29 14:58:40 -07001567 okFormat = format;
1568 }
1569 }
1570 }
Doug Felte8e45f22010-03-29 14:58:40 -07001571
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001572 SpannableStringBuilder out = new SpannableStringBuilder(okFormat);
1573 out.insert(0, text, 0, ok);
1574 return out;
Doug Felte8e45f22010-03-29 14:58:40 -07001575 } finally {
Seigo Nonakaf1644f72017-11-27 22:09:49 -08001576 if (mt != null) {
1577 mt.recycle();
1578 }
1579 if (tempMt != null) {
1580 tempMt.recycle();
Doug Felte8e45f22010-03-29 14:58:40 -07001581 }
1582 }
Doug Felte8e45f22010-03-29 14:58:40 -07001583 }
1584
Roozbeh Pournader8823c852016-06-09 18:36:47 -07001585 // Returns true if the character's presence could affect RTL layout.
1586 //
1587 // In order to be fast, the code is intentionally rough and quite conservative in its
1588 // considering inclusion of any non-BMP or surrogate characters or anything in the bidi
1589 // blocks or any bidi formatting characters with a potential to affect RTL layout.
Doug Felte8e45f22010-03-29 14:58:40 -07001590 /* package */
Roozbeh Pournader8823c852016-06-09 18:36:47 -07001591 static boolean couldAffectRtl(char c) {
1592 return (0x0590 <= c && c <= 0x08FF) || // RTL scripts
1593 c == 0x200E || // Bidi format character
1594 c == 0x200F || // Bidi format character
1595 (0x202A <= c && c <= 0x202E) || // Bidi format characters
1596 (0x2066 <= c && c <= 0x2069) || // Bidi format characters
1597 (0xD800 <= c && c <= 0xDFFF) || // Surrogate pairs
1598 (0xFB1D <= c && c <= 0xFDFF) || // Hebrew and Arabic presentation forms
1599 (0xFE70 <= c && c <= 0xFEFE); // Arabic presentation forms
Doug Felte8e45f22010-03-29 14:58:40 -07001600 }
1601
Roozbeh Pournader8823c852016-06-09 18:36:47 -07001602 // Returns true if there is no character present that may potentially affect RTL layout.
1603 // Since this calls couldAffectRtl() above, it's also quite conservative, in the way that
1604 // it may return 'false' (needs bidi) although careful consideration may tell us it should
1605 // return 'true' (does not need bidi).
Doug Felte8e45f22010-03-29 14:58:40 -07001606 /* package */
1607 static boolean doesNotNeedBidi(char[] text, int start, int len) {
Roozbeh Pournader8823c852016-06-09 18:36:47 -07001608 final int end = start + len;
1609 for (int i = start; i < end; i++) {
1610 if (couldAffectRtl(text[i])) {
Doug Felte8e45f22010-03-29 14:58:40 -07001611 return false;
1612 }
1613 }
1614 return true;
1615 }
1616
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001617 /* package */ static char[] obtain(int len) {
1618 char[] buf;
1619
1620 synchronized (sLock) {
1621 buf = sTemp;
1622 sTemp = null;
1623 }
1624
1625 if (buf == null || buf.length < len)
Adam Lesinski776abc22014-03-07 11:30:59 -05001626 buf = ArrayUtils.newUnpaddedCharArray(len);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001627
1628 return buf;
1629 }
1630
1631 /* package */ static void recycle(char[] temp) {
1632 if (temp.length > 1000)
1633 return;
1634
1635 synchronized (sLock) {
1636 sTemp = temp;
1637 }
1638 }
1639
1640 /**
1641 * Html-encode the string.
1642 * @param s the string to be encoded
1643 * @return the encoded string
1644 */
1645 public static String htmlEncode(String s) {
1646 StringBuilder sb = new StringBuilder();
1647 char c;
1648 for (int i = 0; i < s.length(); i++) {
1649 c = s.charAt(i);
1650 switch (c) {
1651 case '<':
1652 sb.append("&lt;"); //$NON-NLS-1$
1653 break;
1654 case '>':
1655 sb.append("&gt;"); //$NON-NLS-1$
1656 break;
1657 case '&':
1658 sb.append("&amp;"); //$NON-NLS-1$
1659 break;
1660 case '\'':
Marc Blankf4832da2012-02-13 10:11:50 -08001661 //http://www.w3.org/TR/xhtml1
1662 // The named character reference &apos; (the apostrophe, U+0027) was introduced in
1663 // XML 1.0 but does not appear in HTML. Authors should therefore use &#39; instead
1664 // of &apos; to work as expected in HTML 4 user agents.
1665 sb.append("&#39;"); //$NON-NLS-1$
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001666 break;
1667 case '"':
1668 sb.append("&quot;"); //$NON-NLS-1$
1669 break;
1670 default:
1671 sb.append(c);
1672 }
1673 }
1674 return sb.toString();
1675 }
1676
1677 /**
1678 * Returns a CharSequence concatenating the specified CharSequences,
1679 * retaining their spans if any.
Roozbeh Pournadere57886e2017-05-02 18:10:10 -07001680 *
1681 * If there are no parameters, an empty string will be returned.
1682 *
1683 * If the number of parameters is exactly one, that parameter is returned as output, even if it
1684 * is null.
1685 *
1686 * If the number of parameters is at least two, any null CharSequence among the parameters is
1687 * treated as if it was the string <code>"null"</code>.
1688 *
1689 * If there are paragraph spans in the source CharSequences that satisfy paragraph boundary
1690 * requirements in the sources but would no longer satisfy them in the concatenated
1691 * CharSequence, they may get extended in the resulting CharSequence or not retained.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001692 */
1693 public static CharSequence concat(CharSequence... text) {
1694 if (text.length == 0) {
1695 return "";
1696 }
1697
1698 if (text.length == 1) {
1699 return text[0];
1700 }
1701
1702 boolean spanned = false;
Roozbeh Pournadere57886e2017-05-02 18:10:10 -07001703 for (CharSequence piece : text) {
1704 if (piece instanceof Spanned) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001705 spanned = true;
1706 break;
1707 }
1708 }
1709
Roozbeh Pournadere57886e2017-05-02 18:10:10 -07001710 if (spanned) {
1711 final SpannableStringBuilder ssb = new SpannableStringBuilder();
1712 for (CharSequence piece : text) {
1713 // If a piece is null, we append the string "null" for compatibility with the
1714 // behavior of StringBuilder and the behavior of the concat() method in earlier
1715 // versions of Android.
1716 ssb.append(piece == null ? "null" : piece);
1717 }
1718 return new SpannedString(ssb);
1719 } else {
1720 final StringBuilder sb = new StringBuilder();
1721 for (CharSequence piece : text) {
1722 sb.append(piece);
1723 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001724 return sb.toString();
1725 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001726 }
1727
1728 /**
1729 * Returns whether the given CharSequence contains any printable characters.
1730 */
1731 public static boolean isGraphic(CharSequence str) {
1732 final int len = str.length();
Roozbeh Pournadera93880e2015-08-10 17:23:05 -07001733 for (int cp, i=0; i<len; i+=Character.charCount(cp)) {
Roozbeh Pournader1cc2acf2015-08-11 10:37:07 -07001734 cp = Character.codePointAt(str, i);
Roozbeh Pournadera93880e2015-08-10 17:23:05 -07001735 int gc = Character.getType(cp);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001736 if (gc != Character.CONTROL
1737 && gc != Character.FORMAT
1738 && gc != Character.SURROGATE
1739 && gc != Character.UNASSIGNED
1740 && gc != Character.LINE_SEPARATOR
1741 && gc != Character.PARAGRAPH_SEPARATOR
1742 && gc != Character.SPACE_SEPARATOR) {
1743 return true;
1744 }
1745 }
1746 return false;
1747 }
1748
1749 /**
1750 * Returns whether this character is a printable character.
Roozbeh Pournadera93880e2015-08-10 17:23:05 -07001751 *
1752 * This does not support non-BMP characters and should not be used.
1753 *
1754 * @deprecated Use {@link #isGraphic(CharSequence)} instead.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001755 */
Roozbeh Pournadera93880e2015-08-10 17:23:05 -07001756 @Deprecated
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001757 public static boolean isGraphic(char c) {
1758 int gc = Character.getType(c);
1759 return gc != Character.CONTROL
1760 && gc != Character.FORMAT
1761 && gc != Character.SURROGATE
1762 && gc != Character.UNASSIGNED
1763 && gc != Character.LINE_SEPARATOR
1764 && gc != Character.PARAGRAPH_SEPARATOR
1765 && gc != Character.SPACE_SEPARATOR;
1766 }
1767
1768 /**
1769 * Returns whether the given CharSequence contains only digits.
1770 */
1771 public static boolean isDigitsOnly(CharSequence str) {
1772 final int len = str.length();
Roozbeh Pournader3efda952015-08-11 09:55:57 -07001773 for (int cp, i = 0; i < len; i += Character.charCount(cp)) {
1774 cp = Character.codePointAt(str, i);
1775 if (!Character.isDigit(cp)) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001776 return false;
1777 }
1778 }
1779 return true;
1780 }
1781
1782 /**
Daisuke Miyakawa973afa92009-12-03 10:43:45 +09001783 * @hide
1784 */
1785 public static boolean isPrintableAscii(final char c) {
1786 final int asciiFirst = 0x20;
1787 final int asciiLast = 0x7E; // included
1788 return (asciiFirst <= c && c <= asciiLast) || c == '\r' || c == '\n';
1789 }
1790
1791 /**
1792 * @hide
1793 */
Mathew Inwoodefeab842018-08-14 15:21:30 +01001794 @UnsupportedAppUsage
Daisuke Miyakawa973afa92009-12-03 10:43:45 +09001795 public static boolean isPrintableAsciiOnly(final CharSequence str) {
1796 final int len = str.length();
1797 for (int i = 0; i < len; i++) {
1798 if (!isPrintableAscii(str.charAt(i))) {
1799 return false;
1800 }
1801 }
1802 return true;
1803 }
1804
1805 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001806 * Capitalization mode for {@link #getCapsMode}: capitalize all
1807 * characters. This value is explicitly defined to be the same as
1808 * {@link InputType#TYPE_TEXT_FLAG_CAP_CHARACTERS}.
1809 */
1810 public static final int CAP_MODE_CHARACTERS
1811 = InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS;
Doug Felte8e45f22010-03-29 14:58:40 -07001812
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001813 /**
1814 * Capitalization mode for {@link #getCapsMode}: capitalize the first
1815 * character of all words. This value is explicitly defined to be the same as
1816 * {@link InputType#TYPE_TEXT_FLAG_CAP_WORDS}.
1817 */
1818 public static final int CAP_MODE_WORDS
1819 = InputType.TYPE_TEXT_FLAG_CAP_WORDS;
Doug Felte8e45f22010-03-29 14:58:40 -07001820
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001821 /**
1822 * Capitalization mode for {@link #getCapsMode}: capitalize the first
1823 * character of each sentence. This value is explicitly defined to be the same as
1824 * {@link InputType#TYPE_TEXT_FLAG_CAP_SENTENCES}.
1825 */
1826 public static final int CAP_MODE_SENTENCES
1827 = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
Doug Felte8e45f22010-03-29 14:58:40 -07001828
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001829 /**
1830 * Determine what caps mode should be in effect at the current offset in
1831 * the text. Only the mode bits set in <var>reqModes</var> will be
1832 * checked. Note that the caps mode flags here are explicitly defined
1833 * to match those in {@link InputType}.
Doug Felte8e45f22010-03-29 14:58:40 -07001834 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001835 * @param cs The text that should be checked for caps modes.
1836 * @param off Location in the text at which to check.
1837 * @param reqModes The modes to be checked: may be any combination of
1838 * {@link #CAP_MODE_CHARACTERS}, {@link #CAP_MODE_WORDS}, and
1839 * {@link #CAP_MODE_SENTENCES}.
Mark Wagner60919952010-03-01 09:24:59 -08001840 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001841 * @return Returns the actual capitalization modes that can be in effect
1842 * at the current position, which is any combination of
1843 * {@link #CAP_MODE_CHARACTERS}, {@link #CAP_MODE_WORDS}, and
1844 * {@link #CAP_MODE_SENTENCES}.
1845 */
1846 public static int getCapsMode(CharSequence cs, int off, int reqModes) {
Mark Wagner60919952010-03-01 09:24:59 -08001847 if (off < 0) {
1848 return 0;
1849 }
1850
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001851 int i;
1852 char c;
1853 int mode = 0;
1854
1855 if ((reqModes&CAP_MODE_CHARACTERS) != 0) {
1856 mode |= CAP_MODE_CHARACTERS;
1857 }
1858 if ((reqModes&(CAP_MODE_WORDS|CAP_MODE_SENTENCES)) == 0) {
1859 return mode;
1860 }
1861
1862 // Back over allowed opening punctuation.
1863
1864 for (i = off; i > 0; i--) {
1865 c = cs.charAt(i - 1);
1866
1867 if (c != '"' && c != '\'' &&
1868 Character.getType(c) != Character.START_PUNCTUATION) {
1869 break;
1870 }
1871 }
1872
1873 // Start of paragraph, with optional whitespace.
1874
1875 int j = i;
1876 while (j > 0 && ((c = cs.charAt(j - 1)) == ' ' || c == '\t')) {
1877 j--;
1878 }
1879 if (j == 0 || cs.charAt(j - 1) == '\n') {
1880 return mode | CAP_MODE_WORDS;
1881 }
1882
1883 // Or start of word if we are that style.
1884
1885 if ((reqModes&CAP_MODE_SENTENCES) == 0) {
1886 if (i != j) mode |= CAP_MODE_WORDS;
1887 return mode;
1888 }
1889
1890 // There must be a space if not the start of paragraph.
1891
1892 if (i == j) {
1893 return mode;
1894 }
1895
1896 // Back over allowed closing punctuation.
1897
1898 for (; j > 0; j--) {
1899 c = cs.charAt(j - 1);
1900
1901 if (c != '"' && c != '\'' &&
1902 Character.getType(c) != Character.END_PUNCTUATION) {
1903 break;
1904 }
1905 }
1906
1907 if (j > 0) {
1908 c = cs.charAt(j - 1);
1909
1910 if (c == '.' || c == '?' || c == '!') {
1911 // Do not capitalize if the word ends with a period but
1912 // also contains a period, in which case it is an abbreviation.
1913
1914 if (c == '.') {
1915 for (int k = j - 2; k >= 0; k--) {
1916 c = cs.charAt(k);
1917
1918 if (c == '.') {
1919 return mode;
1920 }
1921
1922 if (!Character.isLetter(c)) {
1923 break;
1924 }
1925 }
1926 }
1927
1928 return mode | CAP_MODE_SENTENCES;
1929 }
1930 }
1931
1932 return mode;
1933 }
Doug Felte8e45f22010-03-29 14:58:40 -07001934
Brad Fitzpatrick11fe1812010-09-10 16:07:52 -07001935 /**
1936 * Does a comma-delimited list 'delimitedString' contain a certain item?
1937 * (without allocating memory)
1938 *
1939 * @hide
1940 */
1941 public static boolean delimitedStringContains(
1942 String delimitedString, char delimiter, String item) {
1943 if (isEmpty(delimitedString) || isEmpty(item)) {
1944 return false;
1945 }
1946 int pos = -1;
1947 int length = delimitedString.length();
1948 while ((pos = delimitedString.indexOf(item, pos + 1)) != -1) {
1949 if (pos > 0 && delimitedString.charAt(pos - 1) != delimiter) {
1950 continue;
1951 }
1952 int expectedDelimiterPos = pos + item.length();
1953 if (expectedDelimiterPos == length) {
1954 // Match at end of string.
1955 return true;
1956 }
1957 if (delimitedString.charAt(expectedDelimiterPos) == delimiter) {
1958 return true;
1959 }
1960 }
1961 return false;
1962 }
1963
Gilles Debunne1e3ac182011-03-08 14:22:34 -08001964 /**
1965 * Removes empty spans from the <code>spans</code> array.
1966 *
1967 * When parsing a Spanned using {@link Spanned#nextSpanTransition(int, int, Class)}, empty spans
1968 * will (correctly) create span transitions, and calling getSpans on a slice of text bounded by
1969 * one of these transitions will (correctly) include the empty overlapping span.
1970 *
1971 * However, these empty spans should not be taken into account when layouting or rendering the
1972 * string and this method provides a way to filter getSpans' results accordingly.
1973 *
1974 * @param spans A list of spans retrieved using {@link Spanned#getSpans(int, int, Class)} from
1975 * the <code>spanned</code>
1976 * @param spanned The Spanned from which spans were extracted
1977 * @return A subset of spans where empty spans ({@link Spanned#getSpanStart(Object)} ==
1978 * {@link Spanned#getSpanEnd(Object)} have been removed. The initial order is preserved
1979 * @hide
1980 */
1981 @SuppressWarnings("unchecked")
1982 public static <T> T[] removeEmptySpans(T[] spans, Spanned spanned, Class<T> klass) {
1983 T[] copy = null;
1984 int count = 0;
1985
1986 for (int i = 0; i < spans.length; i++) {
1987 final T span = spans[i];
1988 final int start = spanned.getSpanStart(span);
1989 final int end = spanned.getSpanEnd(span);
1990
1991 if (start == end) {
1992 if (copy == null) {
1993 copy = (T[]) Array.newInstance(klass, spans.length - 1);
1994 System.arraycopy(spans, 0, copy, 0, i);
1995 count = i;
1996 }
1997 } else {
1998 if (copy != null) {
1999 copy[count] = span;
2000 count++;
2001 }
2002 }
2003 }
2004
2005 if (copy != null) {
2006 T[] result = (T[]) Array.newInstance(klass, count);
2007 System.arraycopy(copy, 0, result, 0, count);
2008 return result;
2009 } else {
2010 return spans;
2011 }
2012 }
2013
Gilles Debunne6c488de2012-03-01 16:20:35 -08002014 /**
2015 * Pack 2 int values into a long, useful as a return value for a range
2016 * @see #unpackRangeStartFromLong(long)
2017 * @see #unpackRangeEndFromLong(long)
2018 * @hide
2019 */
Mathew Inwoodefeab842018-08-14 15:21:30 +01002020 @UnsupportedAppUsage
Gilles Debunne6c488de2012-03-01 16:20:35 -08002021 public static long packRangeInLong(int start, int end) {
2022 return (((long) start) << 32) | end;
2023 }
2024
2025 /**
2026 * Get the start value from a range packed in a long by {@link #packRangeInLong(int, int)}
2027 * @see #unpackRangeEndFromLong(long)
2028 * @see #packRangeInLong(int, int)
2029 * @hide
2030 */
Mathew Inwoodefeab842018-08-14 15:21:30 +01002031 @UnsupportedAppUsage
Gilles Debunne6c488de2012-03-01 16:20:35 -08002032 public static int unpackRangeStartFromLong(long range) {
2033 return (int) (range >>> 32);
2034 }
2035
2036 /**
2037 * Get the end value from a range packed in a long by {@link #packRangeInLong(int, int)}
2038 * @see #unpackRangeStartFromLong(long)
2039 * @see #packRangeInLong(int, int)
2040 * @hide
2041 */
Mathew Inwoodefeab842018-08-14 15:21:30 +01002042 @UnsupportedAppUsage
Gilles Debunne6c488de2012-03-01 16:20:35 -08002043 public static int unpackRangeEndFromLong(long range) {
2044 return (int) (range & 0x00000000FFFFFFFFL);
2045 }
2046
Fabrice Di Megliod3d9f3f2012-09-18 12:55:32 -07002047 /**
2048 * Return the layout direction for a given Locale
2049 *
2050 * @param locale the Locale for which we want the layout direction. Can be null.
2051 * @return the layout direction. This may be one of:
2052 * {@link android.view.View#LAYOUT_DIRECTION_LTR} or
2053 * {@link android.view.View#LAYOUT_DIRECTION_RTL}.
2054 *
2055 * Be careful: this code will need to be updated when vertical scripts will be supported
2056 */
2057 public static int getLayoutDirectionFromLocale(Locale locale) {
Roozbeh Pournader463b4822015-08-06 16:04:45 -07002058 return ((locale != null && !locale.equals(Locale.ROOT)
2059 && ULocale.forLocale(locale).isRightToLeft())
2060 // If forcing into RTL layout mode, return RTL as default
Kiyoung Kim0fe161d2018-12-20 18:26:10 +09002061 || DisplayProperties.debug_force_rtl().orElse(false))
Roozbeh Pournader463b4822015-08-06 16:04:45 -07002062 ? View.LAYOUT_DIRECTION_RTL
2063 : View.LAYOUT_DIRECTION_LTR;
Fabrice Di Megliod3d9f3f2012-09-18 12:55:32 -07002064 }
2065
Jeff Sharkeyf491c722015-06-11 09:16:19 -07002066 /**
2067 * Return localized string representing the given number of selected items.
2068 *
2069 * @hide
2070 */
2071 public static CharSequence formatSelectedCount(int count) {
2072 return Resources.getSystem().getQuantityString(R.plurals.selected_count, count, count);
2073 }
2074
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +01002075 /**
2076 * Returns whether or not the specified spanned text has a style span.
2077 * @hide
2078 */
2079 public static boolean hasStyleSpan(@NonNull Spanned spanned) {
2080 Preconditions.checkArgument(spanned != null);
2081 final Class<?>[] styleClasses = {
2082 CharacterStyle.class, ParagraphStyle.class, UpdateAppearance.class};
2083 for (Class<?> clazz : styleClasses) {
2084 if (spanned.nextSpanTransition(-1, spanned.length(), clazz) < spanned.length()) {
2085 return true;
2086 }
2087 }
2088 return false;
2089 }
2090
Felipe Lemea8fce3b2017-04-04 14:22:12 -07002091 /**
2092 * If the {@code charSequence} is instance of {@link Spanned}, creates a new copy and
2093 * {@link NoCopySpan}'s are removed from the copy. Otherwise the given {@code charSequence} is
2094 * returned as it is.
2095 *
2096 * @hide
2097 */
2098 @Nullable
2099 public static CharSequence trimNoCopySpans(@Nullable CharSequence charSequence) {
2100 if (charSequence != null && charSequence instanceof Spanned) {
2101 // SpannableStringBuilder copy constructor trims NoCopySpans.
2102 return new SpannableStringBuilder(charSequence);
2103 }
2104 return charSequence;
2105 }
2106
Eugene Susla4a34f9c2017-05-16 14:16:38 -07002107 /**
2108 * Prepends {@code start} and appends {@code end} to a given {@link StringBuilder}
2109 *
2110 * @hide
2111 */
2112 public static void wrap(StringBuilder builder, String start, String end) {
2113 builder.insert(0, start);
2114 builder.append(end);
2115 }
2116
Siyamed Sinirce3b05a2017-07-18 18:54:31 -07002117 /**
2118 * Intent size limitations prevent sending over a megabyte of data. Limit
2119 * text length to 100K characters - 200KB.
2120 */
2121 private static final int PARCEL_SAFE_TEXT_LENGTH = 100000;
2122
2123 /**
2124 * Trims the text to {@link #PARCEL_SAFE_TEXT_LENGTH} length. Returns the string as it is if
2125 * the length() is smaller than {@link #PARCEL_SAFE_TEXT_LENGTH}. Used for text that is parceled
2126 * into a {@link Parcelable}.
2127 *
2128 * @hide
2129 */
2130 @Nullable
2131 public static <T extends CharSequence> T trimToParcelableSize(@Nullable T text) {
2132 return trimToSize(text, PARCEL_SAFE_TEXT_LENGTH);
2133 }
2134
2135 /**
2136 * Trims the text to {@code size} length. Returns the string as it is if the length() is
2137 * smaller than {@code size}. If chars at {@code size-1} and {@code size} is a surrogate
2138 * pair, returns a CharSequence of length {@code size-1}.
2139 *
2140 * @param size length of the result, should be greater than 0
2141 *
2142 * @hide
2143 */
2144 @Nullable
2145 public static <T extends CharSequence> T trimToSize(@Nullable T text,
2146 @IntRange(from = 1) int size) {
2147 Preconditions.checkArgument(size > 0);
2148 if (TextUtils.isEmpty(text) || text.length() <= size) return text;
2149 if (Character.isHighSurrogate(text.charAt(size - 1))
2150 && Character.isLowSurrogate(text.charAt(size))) {
2151 size = size - 1;
2152 }
2153 return (T) text.subSequence(0, size);
2154 }
2155
Aaron Heuckroth51d32882018-07-03 15:14:04 -04002156 /**
2157 * Trims the {@code text} to the first {@code size} characters and adds an ellipsis if the
2158 * resulting string is shorter than the input. This will result in an output string which is
2159 * longer than {@code size} for most inputs.
2160 *
2161 * @param size length of the result, should be greater than 0
2162 *
2163 * @hide
2164 */
2165 @Nullable
2166 public static <T extends CharSequence> T trimToLengthWithEllipsis(@Nullable T text,
2167 @IntRange(from = 1) int size) {
2168 T trimmed = trimToSize(text, size);
2169 if (trimmed.length() < text.length()) {
2170 trimmed = (T) (trimmed.toString() + "...");
2171 }
2172 return trimmed;
2173 }
2174
Philip P. Moltmannc1fda742018-10-05 16:52:35 -07002175 private static boolean isNewline(int codePoint) {
2176 int type = Character.getType(codePoint);
2177 return type == Character.PARAGRAPH_SEPARATOR || type == Character.LINE_SEPARATOR
2178 || codePoint == LINE_FEED_CODE_POINT;
2179 }
2180
2181 private static boolean isWhiteSpace(int codePoint) {
2182 return Character.isWhitespace(codePoint) || codePoint == NBSP_CODE_POINT;
2183 }
2184
2185 /**
2186 * Remove html, remove bad characters, and truncate string.
2187 *
2188 * <p>This method is meant to remove common mistakes and nefarious formatting from strings that
2189 * were loaded from untrusted sources (such as other packages).
2190 *
2191 * <p>This method first {@link Html#fromHtml treats the string like HTML} and then ...
2192 * <ul>
2193 * <li>Removes new lines or truncates at first new line
2194 * <li>Trims the white-space off the end
2195 * <li>Truncates the string
2196 * </ul>
2197 * ... if specified.
2198 *
2199 * @param unclean The input string
2200 * @param maxCharactersToConsider The maximum number of characters of {@code unclean} to
2201 * consider from the input string. {@code 0} disables this
2202 * feature.
2203 * @param ellipsizeDip Assuming maximum length of the string (in dip), assuming font size 42.
2204 * This is roughly 50 characters for {@code ellipsizeDip == 1000}.<br />
2205 * Usually ellipsizing should be left to the view showing the string. If a
2206 * string is used as an input to another string, it might be useful to
2207 * control the length of the input string though. {@code 0} disables this
2208 * feature.
2209 * @param flags Flags controlling cleaning behavior (Can be {@link #SAFE_STRING_FLAG_TRIM},
2210 * {@link #SAFE_STRING_FLAG_SINGLE_LINE},
2211 * and {@link #SAFE_STRING_FLAG_FIRST_LINE})
2212 *
2213 * @return The cleaned string
2214 */
2215 public static @NonNull CharSequence makeSafeForPresentation(@NonNull String unclean,
2216 @IntRange(from = 0) int maxCharactersToConsider,
2217 @FloatRange(from = 0) float ellipsizeDip, @SafeStringFlags int flags) {
2218 boolean onlyKeepFirstLine = ((flags & SAFE_STRING_FLAG_FIRST_LINE) != 0);
2219 boolean forceSingleLine = ((flags & SAFE_STRING_FLAG_SINGLE_LINE) != 0);
2220 boolean trim = ((flags & SAFE_STRING_FLAG_TRIM) != 0);
2221
2222 Preconditions.checkNotNull(unclean);
2223 Preconditions.checkArgumentNonnegative(maxCharactersToConsider);
2224 Preconditions.checkArgumentNonNegative(ellipsizeDip, "ellipsizeDip");
2225 Preconditions.checkFlagsArgument(flags, SAFE_STRING_FLAG_TRIM
2226 | SAFE_STRING_FLAG_SINGLE_LINE | SAFE_STRING_FLAG_FIRST_LINE);
2227 Preconditions.checkArgument(!(onlyKeepFirstLine && forceSingleLine),
2228 "Cannot set SAFE_STRING_FLAG_SINGLE_LINE and SAFE_STRING_FLAG_FIRST_LINE at the"
2229 + "same time");
2230
2231 String shortString;
2232 if (maxCharactersToConsider > 0) {
2233 shortString = unclean.substring(0, Math.min(unclean.length(), maxCharactersToConsider));
2234 } else {
2235 shortString = unclean;
2236 }
2237
2238 // Treat string as HTML. This
2239 // - converts HTML symbols: e.g. &szlig; -> ß
2240 // - applies some HTML tags: e.g. <br> -> \n
2241 // - removes invalid characters such as \b
2242 // - removes html styling, such as <b>
2243 // - applies html formatting: e.g. a<p>b</p>c -> a\n\nb\n\nc
2244 // - replaces some html tags by "object replacement" markers: <img> -> \ufffc
2245 // - Removes leading white space
2246 // - Removes all trailing white space beside a single space
2247 // - Collapses double white space
2248 StringWithRemovedChars gettingCleaned = new StringWithRemovedChars(
2249 Html.fromHtml(shortString).toString());
2250
2251 int firstNonWhiteSpace = -1;
2252 int firstTrailingWhiteSpace = -1;
2253
2254 // Remove new lines (if requested) and control characters.
2255 int uncleanLength = gettingCleaned.length();
2256 for (int offset = 0; offset < uncleanLength; ) {
2257 int codePoint = gettingCleaned.codePointAt(offset);
2258 int type = Character.getType(codePoint);
2259 int codePointLen = Character.charCount(codePoint);
2260 boolean isNewline = isNewline(codePoint);
2261
2262 if (onlyKeepFirstLine && isNewline) {
2263 gettingCleaned.removeAllCharAfter(offset);
2264 break;
2265 } else if (forceSingleLine && isNewline) {
2266 gettingCleaned.removeRange(offset, offset + codePointLen);
2267 } else if (type == Character.CONTROL && !isNewline) {
2268 gettingCleaned.removeRange(offset, offset + codePointLen);
2269 } else if (trim && !isWhiteSpace(codePoint)) {
2270 // This is only executed if the code point is not removed
2271 if (firstNonWhiteSpace == -1) {
2272 firstNonWhiteSpace = offset;
2273 }
2274 firstTrailingWhiteSpace = offset + codePointLen;
2275 }
2276
2277 offset += codePointLen;
2278 }
2279
2280 if (trim) {
2281 // Remove leading and trailing white space
2282 if (firstNonWhiteSpace == -1) {
2283 // No non whitespace found, remove all
2284 gettingCleaned.removeAllCharAfter(0);
2285 } else {
2286 if (firstNonWhiteSpace > 0) {
2287 gettingCleaned.removeAllCharBefore(firstNonWhiteSpace);
2288 }
2289 if (firstTrailingWhiteSpace < uncleanLength) {
2290 gettingCleaned.removeAllCharAfter(firstTrailingWhiteSpace);
2291 }
2292 }
2293 }
2294
2295 if (ellipsizeDip == 0) {
2296 return gettingCleaned.toString();
2297 } else {
2298 // Truncate
2299 final TextPaint paint = new TextPaint();
2300 paint.setTextSize(42);
2301
2302 return TextUtils.ellipsize(gettingCleaned.toString(), paint, ellipsizeDip,
2303 TextUtils.TruncateAt.END);
2304 }
2305 }
2306
2307 /**
2308 * A special string manipulation class. Just records removals and executes the when onString()
2309 * is called.
2310 */
2311 private static class StringWithRemovedChars {
2312 /** The original string */
2313 private final String mOriginal;
2314
2315 /**
2316 * One bit per char in string. If bit is set, character needs to be removed. If whole
2317 * bit field is not initialized nothing needs to be removed.
2318 */
2319 private BitSet mRemovedChars;
2320
2321 StringWithRemovedChars(@NonNull String original) {
2322 mOriginal = original;
2323 }
2324
2325 /**
2326 * Mark all chars in a range {@code [firstRemoved - firstNonRemoved[} (not including
2327 * firstNonRemoved) as removed.
2328 */
2329 void removeRange(int firstRemoved, int firstNonRemoved) {
2330 if (mRemovedChars == null) {
2331 mRemovedChars = new BitSet(mOriginal.length());
2332 }
2333
2334 mRemovedChars.set(firstRemoved, firstNonRemoved);
2335 }
2336
2337 /**
2338 * Remove all characters before {@code firstNonRemoved}.
2339 */
2340 void removeAllCharBefore(int firstNonRemoved) {
2341 if (mRemovedChars == null) {
2342 mRemovedChars = new BitSet(mOriginal.length());
2343 }
2344
2345 mRemovedChars.set(0, firstNonRemoved);
2346 }
2347
2348 /**
2349 * Remove all characters after and including {@code firstRemoved}.
2350 */
2351 void removeAllCharAfter(int firstRemoved) {
2352 if (mRemovedChars == null) {
2353 mRemovedChars = new BitSet(mOriginal.length());
2354 }
2355
2356 mRemovedChars.set(firstRemoved, mOriginal.length());
2357 }
2358
2359 @Override
2360 public String toString() {
2361 // Common case, no chars removed
2362 if (mRemovedChars == null) {
2363 return mOriginal;
2364 }
2365
2366 StringBuilder sb = new StringBuilder(mOriginal.length());
2367 for (int i = 0; i < mOriginal.length(); i++) {
2368 if (!mRemovedChars.get(i)) {
2369 sb.append(mOriginal.charAt(i));
2370 }
2371 }
2372
2373 return sb.toString();
2374 }
2375
2376 /**
2377 * Return length or the original string
2378 */
2379 int length() {
2380 return mOriginal.length();
2381 }
2382
2383 /**
2384 * Return codePoint of original string at a certain {@code offset}
2385 */
2386 int codePointAt(int offset) {
2387 return mOriginal.codePointAt(offset);
2388 }
2389 }
2390
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002391 private static Object sLock = new Object();
Fabrice Di Megliod3d9f3f2012-09-18 12:55:32 -07002392
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002393 private static char[] sTemp = null;
Fabrice Di Megliocb332642011-09-23 19:08:04 -07002394
2395 private static String[] EMPTY_STRING_ARRAY = new String[]{};
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002396}