1 /*
2  * Copyright (C) 2008 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 
17 package android.text;
18 
19 import static android.text.TextUtils.formatSimple;
20 
21 import static org.junit.Assert.assertEquals;
22 import static org.junit.Assert.assertFalse;
23 import static org.junit.Assert.assertNotNull;
24 import static org.junit.Assert.assertNull;
25 import static org.junit.Assert.assertSame;
26 import static org.junit.Assert.assertTrue;
27 import static org.junit.Assert.fail;
28 
29 import android.os.Parcel;
30 import android.platform.test.annotations.Presubmit;
31 import android.test.MoreAsserts;
32 import android.text.style.StyleSpan;
33 import android.text.util.Rfc822Token;
34 import android.text.util.Rfc822Tokenizer;
35 import android.view.View;
36 
37 import androidx.test.filters.LargeTest;
38 import androidx.test.filters.SmallTest;
39 import androidx.test.runner.AndroidJUnit4;
40 
41 import com.google.android.collect.Lists;
42 
43 import org.junit.Test;
44 import org.junit.runner.RunWith;
45 
46 import java.nio.charset.StandardCharsets;
47 import java.util.ArrayList;
48 import java.util.List;
49 import java.util.Locale;
50 
51 /**
52  * TextUtilsTest tests {@link TextUtils}.
53  */
54 @Presubmit
55 @SmallTest
56 @RunWith(AndroidJUnit4.class)
57 public class TextUtilsTest {
58 
59     @Test
testBasic()60     public void testBasic() {
61         assertEquals("", TextUtils.concat());
62         assertEquals("foo", TextUtils.concat("foo"));
63         assertEquals("foobar", TextUtils.concat("foo", "bar"));
64         assertEquals("foobarbaz", TextUtils.concat("foo", "bar", "baz"));
65 
66         SpannableString foo = new SpannableString("foo");
67         foo.setSpan("foo", 1, 2, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
68 
69         SpannableString bar = new SpannableString("bar");
70         bar.setSpan("bar", 1, 2, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
71 
72         SpannableString baz = new SpannableString("baz");
73         baz.setSpan("baz", 1, 2, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
74 
75         assertEquals("foo", TextUtils.concat(foo).toString());
76         assertEquals("foobar", TextUtils.concat(foo, bar).toString());
77         assertEquals("foobarbaz", TextUtils.concat(foo, bar, baz).toString());
78 
79         assertEquals(1, ((Spanned) TextUtils.concat(foo)).getSpanStart("foo"));
80 
81         assertEquals(1, ((Spanned) TextUtils.concat(foo, bar)).getSpanStart("foo"));
82         assertEquals(4, ((Spanned) TextUtils.concat(foo, bar)).getSpanStart("bar"));
83 
84         assertEquals(1, ((Spanned) TextUtils.concat(foo, bar, baz)).getSpanStart("foo"));
85         assertEquals(4, ((Spanned) TextUtils.concat(foo, bar, baz)).getSpanStart("bar"));
86         assertEquals(7, ((Spanned) TextUtils.concat(foo, bar, baz)).getSpanStart("baz"));
87 
88         assertTrue(TextUtils.concat("foo", "bar") instanceof String);
89         assertTrue(TextUtils.concat(foo, bar) instanceof SpannedString);
90     }
91 
92     @Test
testTemplateString()93     public void testTemplateString() {
94         CharSequence result;
95 
96         result = TextUtils.expandTemplate("This is a ^1 of the ^2 broadcast ^3.",
97                                           "test", "emergency", "system");
98         assertEquals("This is a test of the emergency broadcast system.",
99                      result.toString());
100 
101         result = TextUtils.expandTemplate("^^^1^^^2^3^a^1^^b^^^c",
102                                           "one", "two", "three");
103         assertEquals("^one^twothree^aone^b^^c",
104                      result.toString());
105 
106         result = TextUtils.expandTemplate("^");
107         assertEquals("^", result.toString());
108 
109         result = TextUtils.expandTemplate("^^");
110         assertEquals("^", result.toString());
111 
112         result = TextUtils.expandTemplate("^^^");
113         assertEquals("^^", result.toString());
114 
115         result = TextUtils.expandTemplate("shorter ^1 values ^2.", "a", "");
116         assertEquals("shorter a values .", result.toString());
117 
118         try {
119             TextUtils.expandTemplate("Only ^1 value given, but ^2 used.", "foo");
120             fail();
121         } catch (IllegalArgumentException e) {
122         }
123 
124         try {
125             TextUtils.expandTemplate("^1 value given, and ^0 used.", "foo");
126             fail();
127         } catch (IllegalArgumentException e) {
128         }
129 
130         result = TextUtils.expandTemplate("^1 value given, and ^9 used.",
131                                           "one", "two", "three", "four", "five",
132                                           "six", "seven", "eight", "nine");
133         assertEquals("one value given, and nine used.", result.toString());
134 
135         try {
136             TextUtils.expandTemplate("^1 value given, and ^10 used.",
137                                      "one", "two", "three", "four", "five",
138                                      "six", "seven", "eight", "nine", "ten");
139             fail();
140         } catch (IllegalArgumentException e) {
141         }
142 
143         // putting carets in the values: expansion is not recursive.
144 
145         result = TextUtils.expandTemplate("^2", "foo", "^^");
146         assertEquals("^^", result.toString());
147 
148         result = TextUtils.expandTemplate("^^2", "foo", "1");
149         assertEquals("^2", result.toString());
150 
151         result = TextUtils.expandTemplate("^1", "value with ^2 in it", "foo");
152         assertEquals("value with ^2 in it", result.toString());
153     }
154 
155     /** Fail unless text+spans contains a span 'spanName' with the given start and end. */
checkContains(Spanned text, String[] spans, String spanName, int start, int end)156     private void checkContains(Spanned text, String[] spans, String spanName,
157                                int start, int end) {
158         for (String i: spans) {
159             if (i.equals(spanName)) {
160                 assertEquals(start, text.getSpanStart(i));
161                 assertEquals(end, text.getSpanEnd(i));
162                 return;
163             }
164         }
165         fail();
166     }
167 
168     @Test
testTemplateSpan()169     public void testTemplateSpan() {
170         SpannableString template;
171         Spanned result;
172         String[] spans;
173 
174         // ordinary replacement
175 
176         template = new SpannableString("a^1b");
177         template.setSpan("before", 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
178         template.setSpan("during", 1, 3, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
179         template.setSpan("after", 3, 4, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
180         template.setSpan("during+after", 1, 4, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
181 
182         result = (Spanned) TextUtils.expandTemplate(template, "foo");
183         assertEquals(5, result.length());
184         spans = result.getSpans(0, result.length(), String.class);
185 
186         // value is one character longer, so span endpoints should change.
187         assertEquals(4, spans.length);
188         checkContains(result, spans, "before", 0, 1);
189         checkContains(result, spans, "during", 1, 4);
190         checkContains(result, spans, "after", 4, 5);
191         checkContains(result, spans, "during+after", 1, 5);
192 
193 
194         // replacement with empty string
195 
196         result = (Spanned) TextUtils.expandTemplate(template, "");
197         assertEquals(2, result.length());
198         spans = result.getSpans(0, result.length(), String.class);
199 
200         // the "during" span should disappear.
201         assertEquals(3, spans.length);
202         checkContains(result, spans, "before", 0, 1);
203         checkContains(result, spans, "after", 1, 2);
204         checkContains(result, spans, "during+after", 1, 2);
205     }
206 
207     @Test
testStringSplitterSimple()208     public void testStringSplitterSimple() {
209         stringSplitterTestHelper("a,b,cde", new String[] {"a", "b", "cde"});
210     }
211 
212     @Test
testStringSplitterEmpty()213     public void testStringSplitterEmpty() {
214         stringSplitterTestHelper("", new String[] {});
215     }
216 
217     @Test
testStringSplitterWithLeadingEmptyString()218     public void testStringSplitterWithLeadingEmptyString() {
219         stringSplitterTestHelper(",a,b,cde", new String[] {"", "a", "b", "cde"});
220     }
221 
222     @Test
testStringSplitterWithInternalEmptyString()223     public void testStringSplitterWithInternalEmptyString() {
224         stringSplitterTestHelper("a,b,,cde", new String[] {"a", "b", "", "cde"});
225     }
226 
227     @Test
testStringSplitterWithTrailingEmptyString()228     public void testStringSplitterWithTrailingEmptyString() {
229         // A single trailing emtpy string should be ignored.
230         stringSplitterTestHelper("a,b,cde,", new String[] {"a", "b", "cde"});
231     }
232 
stringSplitterTestHelper(String string, String[] expectedStrings)233     private void stringSplitterTestHelper(String string, String[] expectedStrings) {
234         TextUtils.StringSplitter splitter = new TextUtils.SimpleStringSplitter(',');
235         splitter.setString(string);
236         List<String> strings = Lists.newArrayList();
237         for (String s : splitter) {
238             strings.add(s);
239         }
240         MoreAsserts.assertEquals(expectedStrings, strings.toArray(new String[]{}));
241     }
242 
243     @Test
testTrim()244     public void testTrim() {
245         String[] strings = { "abc", " abc", "  abc", "abc ", "abc  ",
246                              " abc ", "  abc  ", "\nabc\n", "\nabc", "abc\n" };
247 
248         for (String s : strings) {
249             assertEquals(s.trim().length(), TextUtils.getTrimmedLength(s));
250         }
251     }
252 
253     @Test
testRfc822TokenizerFullAddress()254     public void testRfc822TokenizerFullAddress() {
255         Rfc822Token[] tokens = Rfc822Tokenizer.tokenize("Foo Bar (something) <foo@google.com>");
256         assertNotNull(tokens);
257         assertEquals(1, tokens.length);
258         assertEquals("foo@google.com", tokens[0].getAddress());
259         assertEquals("Foo Bar", tokens[0].getName());
260         assertEquals("something",tokens[0].getComment());
261     }
262 
263     @Test
testRfc822TokenizeItemWithError()264     public void testRfc822TokenizeItemWithError() {
265         Rfc822Token[] tokens = Rfc822Tokenizer.tokenize("\"Foo Bar\\");
266         assertNotNull(tokens);
267         assertEquals(1, tokens.length);
268         assertEquals("Foo Bar", tokens[0].getAddress());
269     }
270 
271     @Test
testRfc822FindToken()272     public void testRfc822FindToken() {
273         Rfc822Tokenizer tokenizer = new Rfc822Tokenizer();
274         //                0           1         2           3         4
275         //                0 1234 56789012345678901234 5678 90123456789012345
276         String address = "\"Foo\" <foo@google.com>, \"Bar\" <bar@google.com>";
277         assertEquals(0, tokenizer.findTokenStart(address, 21));
278         assertEquals(22, tokenizer.findTokenEnd(address, 21));
279         assertEquals(24, tokenizer.findTokenStart(address, 25));
280         assertEquals(46, tokenizer.findTokenEnd(address, 25));
281     }
282 
283     @Test
testRfc822FindTokenWithError()284     public void testRfc822FindTokenWithError() {
285         assertEquals(9, new Rfc822Tokenizer().findTokenEnd("\"Foo Bar\\", 0));
286     }
287 
288     @LargeTest
289     @Test
testEllipsize()290     public void testEllipsize() {
291         CharSequence s1 = "The quick brown fox jumps over \u00FEhe lazy dog.";
292         CharSequence s2 = new Wrapper(s1);
293         Spannable s3 = new SpannableString(s1);
294         s3.setSpan(new StyleSpan(0), 5, 10, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
295         TextPaint p = new TextPaint();
296         p.setFlags(p.getFlags() & ~p.DEV_KERN_TEXT_FLAG);
297 
298         for (int i = 0; i < 100; i++) {
299             for (int j = 0; j < 3; j++) {
300                 TextUtils.TruncateAt kind = null;
301 
302                 switch (j) {
303                 case 0:
304                     kind = TextUtils.TruncateAt.START;
305                     break;
306 
307                 case 1:
308                     kind = TextUtils.TruncateAt.END;
309                     break;
310 
311                 case 2:
312                     kind = TextUtils.TruncateAt.MIDDLE;
313                     break;
314                 }
315 
316                 String out1 = TextUtils.ellipsize(s1, p, i, kind).toString();
317                 String out2 = TextUtils.ellipsize(s2, p, i, kind).toString();
318                 String out3 = TextUtils.ellipsize(s3, p, i, kind).toString();
319 
320                 String keep1 = TextUtils.ellipsize(s1, p, i, kind, true, null).toString();
321                 String keep2 = TextUtils.ellipsize(s2, p, i, kind, true, null).toString();
322                 String keep3 = TextUtils.ellipsize(s3, p, i, kind, true, null).toString();
323 
324                 String trim1 = keep1.replace("\uFEFF", "");
325 
326                 // Are all normal output strings identical?
327                 assertEquals("wid " + i + " pass " + j, out1, out2);
328                 assertEquals("wid " + i + " pass " + j, out2, out3);
329 
330                 // Are preserved output strings identical?
331                 assertEquals("wid " + i + " pass " + j, keep1, keep2);
332                 assertEquals("wid " + i + " pass " + j, keep2, keep3);
333 
334                 // Does trimming padding from preserved yield normal?
335                 assertEquals("wid " + i + " pass " + j, out1, trim1);
336 
337                 // Did preserved output strings preserve length?
338                 assertEquals("wid " + i + " pass " + j, keep1.length(), s1.length());
339 
340                 // Does the output string actually fit in the space?
341                 assertTrue("wid " + i + " pass " + j, p.measureText(out1) <= i);
342 
343                 // Is the padded output the same width as trimmed output?
344                 assertTrue("wid " + i + " pass " + j, p.measureText(keep1) == p.measureText(out1));
345             }
346         }
347     }
348 
349     @Test
testEllipsize_multiCodepoint()350     public void testEllipsize_multiCodepoint() {
351         final TextPaint paint = new TextPaint();
352         final float wordWidth = paint.measureText("MMMM");
353 
354         // Establish the ground rules first, for single-codepoint cases.
355         final String ellipsis = "."; // one full stop character
356         assertEquals(
357                 "MM.\uFEFF",
358                 TextUtils.ellipsize("MMMM", paint, 0.7f * wordWidth,
359                         TextUtils.TruncateAt.END, true /* preserve length */,
360                         null /* no callback */, TextDirectionHeuristics.LTR,
361                         ellipsis));
362         assertEquals(
363                 "MM.",
364                 TextUtils.ellipsize("MMMM", paint, 0.7f * wordWidth,
365                         TextUtils.TruncateAt.END, false /* preserve length */,
366                         null /* no callback */, TextDirectionHeuristics.LTR,
367                         ellipsis));
368         assertEquals(
369                 "M.",
370                 TextUtils.ellipsize("MM", paint, 0.45f * wordWidth,
371                         TextUtils.TruncateAt.END, true /* preserve length */,
372                         null /* no callback */, TextDirectionHeuristics.LTR,
373                         ellipsis));
374         assertEquals(
375                 "M.",
376                 TextUtils.ellipsize("MM", paint, 0.45f * wordWidth,
377                         TextUtils.TruncateAt.END, false /* preserve length */,
378                         null /* no callback */, TextDirectionHeuristics.LTR,
379                         ellipsis));
380 
381         // Now check the differences for multi-codepoint ellipsis.
382         final String longEllipsis = ".."; // two full stop characters
383         assertEquals(
384                 "MM..",
385                 TextUtils.ellipsize("MMMM", paint, 0.7f * wordWidth,
386                         TextUtils.TruncateAt.END, true /* preserve length */,
387                         null /* no callback */, TextDirectionHeuristics.LTR,
388                         longEllipsis));
389         assertEquals(
390                 "MM..",
391                 TextUtils.ellipsize("MMMM", paint, 0.7f * wordWidth,
392                         TextUtils.TruncateAt.END, false /* preserve length */,
393                         null /* no callback */, TextDirectionHeuristics.LTR,
394                         longEllipsis));
395         assertEquals(
396                 "M\uFEFF",
397                 TextUtils.ellipsize("MM", paint, 0.45f * wordWidth,
398                         TextUtils.TruncateAt.END, true /* preserve length */,
399                         null /* no callback */, TextDirectionHeuristics.LTR,
400                         longEllipsis));
401         assertEquals(
402                 "M..",
403                 TextUtils.ellipsize("MM", paint, 0.45f * wordWidth,
404                         TextUtils.TruncateAt.END, false /* preserve length */,
405                         null /* no callback */, TextDirectionHeuristics.LTR,
406                         longEllipsis));
407     }
408 
409     @Test
testDelimitedStringContains()410     public void testDelimitedStringContains() {
411         assertFalse(TextUtils.delimitedStringContains("", ',', null));
412         assertFalse(TextUtils.delimitedStringContains(null, ',', ""));
413         // Whole match
414         assertTrue(TextUtils.delimitedStringContains("gps", ',', "gps"));
415         // At beginning.
416         assertTrue(TextUtils.delimitedStringContains("gps,gpsx,network,mock", ',', "gps"));
417         assertTrue(TextUtils.delimitedStringContains("gps,network,mock", ',', "gps"));
418         // In middle, both without, before & after a false match.
419         assertTrue(TextUtils.delimitedStringContains("network,gps,mock", ',', "gps"));
420         assertTrue(TextUtils.delimitedStringContains("network,gps,gpsx,mock", ',', "gps"));
421         assertTrue(TextUtils.delimitedStringContains("network,gpsx,gps,mock", ',', "gps"));
422         // At the end.
423         assertTrue(TextUtils.delimitedStringContains("network,mock,gps", ',', "gps"));
424         assertTrue(TextUtils.delimitedStringContains("network,mock,gpsx,gps", ',', "gps"));
425         // Not present (but with a false match)
426         assertFalse(TextUtils.delimitedStringContains("network,mock,gpsx", ',', "gps"));
427     }
428 
429     @Test
testCharSequenceCreator()430     public void testCharSequenceCreator() {
431         Parcel p = Parcel.obtain();
432         CharSequence text;
433         try {
434             TextUtils.writeToParcel(null, p, 0);
435             text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p);
436             assertNull("null CharSequence should generate null from parcel", text);
437         } finally {
438             p.recycle();
439         }
440         p = Parcel.obtain();
441         try {
442             TextUtils.writeToParcel("test", p, 0);
443             p.setDataPosition(0);
444             text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p);
445             assertEquals("conversion to/from parcel failed", "test", text);
446         } finally {
447             p.recycle();
448         }
449     }
450 
451     @Test
testCharSequenceCreatorNull()452     public void testCharSequenceCreatorNull() {
453         Parcel p;
454         CharSequence text;
455         p = Parcel.obtain();
456         try {
457             TextUtils.writeToParcel(null, p, 0);
458             p.setDataPosition(0);
459             text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p);
460             assertNull("null CharSequence should generate null from parcel", text);
461         } finally {
462             p.recycle();
463         }
464     }
465 
466     @Test
testCharSequenceCreatorSpannable()467     public void testCharSequenceCreatorSpannable() {
468         Parcel p;
469         CharSequence text;
470         p = Parcel.obtain();
471         try {
472             TextUtils.writeToParcel(new SpannableString("test"), p, 0);
473             p.setDataPosition(0);
474             text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p);
475             assertEquals("conversion to/from parcel failed", "test", text.toString());
476         } finally {
477             p.recycle();
478         }
479     }
480 
481     @Test
testCharSequenceCreatorString()482     public void testCharSequenceCreatorString() {
483         Parcel p;
484         CharSequence text;
485         p = Parcel.obtain();
486         try {
487             TextUtils.writeToParcel("test", p, 0);
488             p.setDataPosition(0);
489             text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p);
490             assertEquals("conversion to/from parcel failed", "test", text.toString());
491         } finally {
492             p.recycle();
493         }
494     }
495 
496     /**
497      * CharSequence wrapper for testing the cases where text is copied into
498      * a char array instead of working from a String or a Spanned.
499      */
500     private static class Wrapper implements CharSequence {
501         private CharSequence mString;
502 
Wrapper(CharSequence s)503         public Wrapper(CharSequence s) {
504             mString = s;
505         }
506 
507         @Override
length()508         public int length() {
509             return mString.length();
510         }
511 
512         @Override
charAt(int off)513         public char charAt(int off) {
514             return mString.charAt(off);
515         }
516 
517         @Override
toString()518         public String toString() {
519             return mString.toString();
520         }
521 
522         @Override
subSequence(int start, int end)523         public CharSequence subSequence(int start, int end) {
524             return new Wrapper(mString.subSequence(start, end));
525         }
526     }
527 
528     @Test
testRemoveEmptySpans()529     public void testRemoveEmptySpans() {
530         MockSpanned spanned = new MockSpanned();
531 
532         spanned.test();
533         spanned.addSpan().test();
534         spanned.addSpan().test();
535         spanned.addSpan().test();
536         spanned.addEmptySpan().test();
537         spanned.addSpan().test();
538         spanned.addEmptySpan().test();
539         spanned.addEmptySpan().test();
540         spanned.addSpan().test();
541 
542         spanned.clear();
543         spanned.addEmptySpan().test();
544         spanned.addEmptySpan().test();
545         spanned.addEmptySpan().test();
546         spanned.addSpan().test();
547         spanned.addEmptySpan().test();
548         spanned.addSpan().test();
549 
550         spanned.clear();
551         spanned.addSpan().test();
552         spanned.addEmptySpan().test();
553         spanned.addSpan().test();
554         spanned.addEmptySpan().test();
555         spanned.addSpan().test();
556         spanned.addSpan().test();
557     }
558 
559     protected static class MockSpanned implements Spanned {
560 
561         private List<Object> allSpans = new ArrayList<Object>();
562         private List<Object> nonEmptySpans = new ArrayList<Object>();
563 
clear()564         public void clear() {
565             allSpans.clear();
566             nonEmptySpans.clear();
567         }
568 
addSpan()569         public MockSpanned addSpan() {
570             Object o = new Object();
571             allSpans.add(o);
572             nonEmptySpans.add(o);
573             return this;
574         }
575 
addEmptySpan()576         public MockSpanned addEmptySpan() {
577             Object o = new Object();
578             allSpans.add(o);
579             return this;
580         }
581 
test()582         public void test() {
583             Object[] nonEmpty = TextUtils.removeEmptySpans(allSpans.toArray(), this, Object.class);
584             assertEquals("Mismatched array size", nonEmptySpans.size(), nonEmpty.length);
585             for (int i=0; i<nonEmpty.length; i++) {
586                 assertEquals("Span differ", nonEmptySpans.get(i), nonEmpty[i]);
587             }
588         }
589 
590         @Override
charAt(int arg0)591         public char charAt(int arg0) {
592             return 0;
593         }
594 
595         @Override
length()596         public int length() {
597             return 0;
598         }
599 
600         @Override
subSequence(int arg0, int arg1)601         public CharSequence subSequence(int arg0, int arg1) {
602             return null;
603         }
604 
605         @Override
getSpans(int start, int end, Class<T> type)606         public <T> T[] getSpans(int start, int end, Class<T> type) {
607             return null;
608         }
609 
610         @Override
getSpanStart(Object tag)611         public int getSpanStart(Object tag) {
612             return 0;
613         }
614 
615         @Override
getSpanEnd(Object tag)616         public int getSpanEnd(Object tag) {
617             return nonEmptySpans.contains(tag) ? 1 : 0;
618         }
619 
620         @Override
getSpanFlags(Object tag)621         public int getSpanFlags(Object tag) {
622             return 0;
623         }
624 
625         @Override
nextSpanTransition(int start, int limit, Class type)626         public int nextSpanTransition(int start, int limit, Class type) {
627             return 0;
628         }
629     }
630 
631     @Test
testGetLayoutDirectionFromLocale()632     public void testGetLayoutDirectionFromLocale() {
633         assertEquals(View.LAYOUT_DIRECTION_LTR, TextUtils.getLayoutDirectionFromLocale(null));
634         assertEquals(View.LAYOUT_DIRECTION_LTR,
635                 TextUtils.getLayoutDirectionFromLocale(Locale.ROOT));
636         assertEquals(View.LAYOUT_DIRECTION_LTR,
637                 TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("en")));
638         assertEquals(View.LAYOUT_DIRECTION_LTR,
639                 TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("en-US")));
640         assertEquals(View.LAYOUT_DIRECTION_LTR,
641                 TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("az")));
642         assertEquals(View.LAYOUT_DIRECTION_LTR,
643                 TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("az-AZ")));
644         assertEquals(View.LAYOUT_DIRECTION_LTR,
645                 TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("az-Latn")));
646         assertEquals(View.LAYOUT_DIRECTION_LTR,
647                 TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("en-EG")));
648         assertEquals(View.LAYOUT_DIRECTION_LTR,
649                 TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("ar-Latn")));
650 
651         assertEquals(View.LAYOUT_DIRECTION_RTL,
652                 TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("ar")));
653         assertEquals(View.LAYOUT_DIRECTION_RTL,
654                 TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("fa")));
655         assertEquals(View.LAYOUT_DIRECTION_RTL,
656                 TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("he")));
657         assertEquals(View.LAYOUT_DIRECTION_RTL,
658                 TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("iw")));
659         assertEquals(View.LAYOUT_DIRECTION_RTL,
660                 TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("ur")));
661         assertEquals(View.LAYOUT_DIRECTION_RTL,
662                 TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("dv")));
663         assertEquals(View.LAYOUT_DIRECTION_RTL,
664                 TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("az-Arab")));
665         assertEquals(View.LAYOUT_DIRECTION_RTL,
666                 TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("az-IR")));
667         assertEquals(View.LAYOUT_DIRECTION_RTL,
668                 TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("fa-US")));
669         assertEquals(View.LAYOUT_DIRECTION_RTL,
670                 TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("tr-Arab")));
671     }
672 
673     @Test
testToUpperCase()674     public void testToUpperCase() {
675         {
676             final CharSequence result = TextUtils.toUpperCase(null, "abc", false);
677             assertEquals(StringBuilder.class, result.getClass());
678             assertEquals("ABC", result.toString());
679         }
680         {
681             final SpannableString str = new SpannableString("abc");
682             Object span = new Object();
683             str.setSpan(span, 1, 2, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
684 
685             final CharSequence result = TextUtils.toUpperCase(null, str, true /* copySpans */);
686             assertEquals(SpannableStringBuilder.class, result.getClass());
687             assertEquals("ABC", result.toString());
688             final Spanned spanned = (Spanned) result;
689             final Object[] resultSpans = spanned.getSpans(0, result.length(), Object.class);
690             assertEquals(1, resultSpans.length);
691             assertSame(span, resultSpans[0]);
692             assertEquals(1, spanned.getSpanStart(span));
693             assertEquals(2, spanned.getSpanEnd(span));
694             assertEquals(Spanned.SPAN_INCLUSIVE_INCLUSIVE, spanned.getSpanFlags(span));
695         }
696         {
697             final Locale turkish = new Locale("tr", "TR");
698             final CharSequence result = TextUtils.toUpperCase(turkish, "i", false);
699             assertEquals(StringBuilder.class, result.getClass());
700             assertEquals("İ", result.toString());
701         }
702         {
703             final String str = "ABC";
704             assertSame(str, TextUtils.toUpperCase(null, str, false));
705         }
706         {
707             final SpannableString str = new SpannableString("ABC");
708             str.setSpan(new Object(), 1, 2, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
709             assertSame(str, TextUtils.toUpperCase(null, str, true /* copySpans */));
710         }
711     }
712 
713     // Copied from cts/tests/tests/widget/src/android/widget/cts/TextViewTest.java and modified
714     // for the TextUtils.toUpperCase method.
715     @Test
testToUpperCase_SpansArePreserved()716     public void testToUpperCase_SpansArePreserved() {
717         final Locale greek = new Locale("el", "GR");
718         final String lowerString = "ι\u0301ριδα";  // ίριδα with first letter decomposed
719         final String upperString = "ΙΡΙΔΑ";  // uppercased
720         // expected lowercase to uppercase index map
721         final int[] indexMap = {0, 1, 1, 2, 3, 4, 5};
722         final int flags = Spanned.SPAN_INCLUSIVE_INCLUSIVE;
723 
724         final Spannable source = new SpannableString(lowerString);
725         source.setSpan(new Object(), 0, 1, flags);
726         source.setSpan(new Object(), 1, 2, flags);
727         source.setSpan(new Object(), 2, 3, flags);
728         source.setSpan(new Object(), 3, 4, flags);
729         source.setSpan(new Object(), 4, 5, flags);
730         source.setSpan(new Object(), 5, 6, flags);
731         source.setSpan(new Object(), 0, 2, flags);
732         source.setSpan(new Object(), 1, 3, flags);
733         source.setSpan(new Object(), 2, 4, flags);
734         source.setSpan(new Object(), 0, 6, flags);
735         final Object[] sourceSpans = source.getSpans(0, source.length(), Object.class);
736 
737         final CharSequence uppercase = TextUtils.toUpperCase(greek, source, true /* copySpans */);
738         assertEquals(SpannableStringBuilder.class, uppercase.getClass());
739         final Spanned result = (Spanned) uppercase;
740 
741         assertEquals(upperString, result.toString());
742         final Object[] resultSpans = result.getSpans(0, result.length(), Object.class);
743         assertEquals(sourceSpans.length, resultSpans.length);
744         for (int i = 0; i < sourceSpans.length; i++) {
745             assertSame(sourceSpans[i], resultSpans[i]);
746             final Object span = sourceSpans[i];
747             assertEquals(indexMap[source.getSpanStart(span)], result.getSpanStart(span));
748             assertEquals(indexMap[source.getSpanEnd(span)], result.getSpanEnd(span));
749             assertEquals(source.getSpanFlags(span), result.getSpanFlags(span));
750         }
751     }
752 
753     @Test
testTrimToSize()754     public void testTrimToSize() {
755         final String testString = "a\uD800\uDC00a";
756         assertEquals("Should return text as it is if size is longer than length",
757                 testString, TextUtils.trimToSize(testString, 5));
758         assertEquals("Should return text as it is if size is equal to length",
759                 testString, TextUtils.trimToSize(testString, 4));
760         assertEquals("Should trim text",
761                 "a\uD800\uDC00", TextUtils.trimToSize(testString, 3));
762         assertEquals("Should trim surrogate pairs if size is in the middle of a pair",
763                 "a", TextUtils.trimToSize(testString, 2));
764         assertEquals("Should trim text",
765                 "a", TextUtils.trimToSize(testString, 1));
766         assertEquals("Should handle null",
767                 null, TextUtils.trimToSize(null, 1));
768 
769         assertEquals("Should trim high surrogate if invalid surrogate",
770                 "a\uD800", TextUtils.trimToSize("a\uD800\uD800", 2));
771         assertEquals("Should trim low surrogate if invalid surrogate",
772                 "a\uDC00", TextUtils.trimToSize("a\uDC00\uDC00", 2));
773     }
774 
775     @Test(expected = IllegalArgumentException.class)
testTrimToSizeThrowsExceptionForNegativeSize()776     public void testTrimToSizeThrowsExceptionForNegativeSize() {
777         TextUtils.trimToSize("", -1);
778     }
779 
780     @Test(expected = IllegalArgumentException.class)
testTrimToSizeThrowsExceptionForZeroSize()781     public void testTrimToSizeThrowsExceptionForZeroSize() {
782         TextUtils.trimToSize("abc", 0);
783     }
784 
785     @Test
truncateStringForUtf8Storage()786     public void truncateStringForUtf8Storage() {
787         assertEquals("", TextUtils.truncateStringForUtf8Storage("abc", 0));
788 
789         //================ long normal case ================
790         StringBuilder builder = new StringBuilder();
791 
792         int n = 50;
793         for (int i = 0; i < 2 * n; i++) {
794             builder.append("哈");
795         }
796         String initial = builder.toString();
797         String result = TextUtils.truncateStringForUtf8Storage(initial, n);
798 
799         // Result should be the beginning of initial
800         assertTrue(initial.startsWith(result));
801 
802         // Result should take less than n bytes in UTF-8
803         assertTrue(result.getBytes(StandardCharsets.UTF_8).length <= n);
804 
805         // result + the next codePoint should take strictly more than
806         // n bytes in UTF-8
807         assertTrue(initial.substring(0, initial.offsetByCodePoints(result.length(), 1))
808                 .getBytes(StandardCharsets.UTF_8).length > n);
809 
810         // =================== short normal case =====================
811         String s = "sf\u20ACgk\u00E9ls\u00E9fg";
812         result = TextUtils.truncateStringForUtf8Storage(s, 100);
813         assertEquals(s, result);
814     }
815 
816     @Test
testTruncateInMiddleOfSurrogate()817     public void testTruncateInMiddleOfSurrogate() {
818         StringBuilder builder = new StringBuilder();
819         String beginning = "a";
820         builder.append(beginning);
821         builder.append(Character.toChars(0x1D11E));
822 
823         String result = TextUtils.truncateStringForUtf8Storage(builder.toString(), 3);
824 
825         // \u1D11E is a surrogate and needs 4 bytes in UTF-8. beginning == "a" uses
826         // only 1 bytes in UTF8
827         // As we allow only 3 bytes for the whole string, so just 2 for this
828         // codePoint, there is not enough place and the string will be truncated
829         // just before it
830         assertEquals(beginning, result);
831     }
832 
833     @Test
testTruncateInMiddleOfChar()834     public void testTruncateInMiddleOfChar() {
835         StringBuilder builder = new StringBuilder();
836         String beginning = "a";
837         builder.append(beginning);
838         builder.append(Character.toChars(0x20AC));
839 
840         String result = TextUtils.truncateStringForUtf8Storage(builder.toString(), 3);
841 
842         // Like above, \u20AC uses 3 bytes in UTF-8, with "beginning", that makes
843         // 4 bytes so it is too big and should be truncated
844         assertEquals(beginning, result);
845     }
846 
847     @Test
testTruncateSubString()848     public void testTruncateSubString() {
849         String test = "sdgkl;hjsl;gjhdgkljdfhglkdj";
850         String sub = test.substring(10, 20);
851         String res = TextUtils.truncateStringForUtf8Storage(sub, 255);
852         assertEquals(sub, res);
853     }
854 
855     @Test(expected = IndexOutOfBoundsException.class)
truncateStringForUtf8StorageThrowsExceptionForNegativeSize()856     public void truncateStringForUtf8StorageThrowsExceptionForNegativeSize() {
857         TextUtils.truncateStringForUtf8Storage("abc", -1);
858     }
859 
860     @Test
length()861     public void length() {
862         assertEquals(0, TextUtils.length(null));
863         assertEquals(0, TextUtils.length(""));
864         assertEquals(2, TextUtils.length("  "));
865         assertEquals(6, TextUtils.length("Hello!"));
866     }
867 
868     @Test
testTrimToLengthWithEllipsis()869     public void testTrimToLengthWithEllipsis() {
870         assertEquals("ABC...", TextUtils.trimToLengthWithEllipsis("ABCDEF", 3));
871         assertEquals("ABC", TextUtils.trimToLengthWithEllipsis("ABC", 3));
872         assertEquals("", TextUtils.trimToLengthWithEllipsis("", 3));
873         assertNull(TextUtils.trimToLengthWithEllipsis(null, 3));
874     }
875 
876     @Test
testFormatSimple_Types()877     public void testFormatSimple_Types() {
878         assertEquals("true", formatSimple("%b", true));
879         assertEquals("false", formatSimple("%b", false));
880         assertEquals("true", formatSimple("%b", this));
881         assertEquals("false", formatSimple("%b", new Object[] { null }));
882 
883         assertEquals("!", formatSimple("%c", '!'));
884 
885         assertEquals("42", formatSimple("%d", 42));
886         assertEquals("281474976710656", formatSimple("%d", 281474976710656L));
887 
888         assertEquals("3.14159", formatSimple("%f", 3.14159));
889         assertEquals("NaN", formatSimple("%f", Float.NaN));
890 
891         assertEquals("example", formatSimple("%s", "example"));
892         assertEquals("null", formatSimple("%s", new Object[] { null }));
893 
894         assertEquals("2a", formatSimple("%x", 42));
895         assertEquals("1000000000000", formatSimple("%x", 281474976710656L));
896 
897         assertEquals("%", formatSimple("%%"));
898     }
899 
900     @Test
testFormatSimple_Width()901     public void testFormatSimple_Width() {
902         assertEquals("42", formatSimple("%1d", 42));
903         assertEquals("42", formatSimple("%2d", 42));
904         assertEquals(" 42", formatSimple("%3d", 42));
905         assertEquals("  42", formatSimple("%4d", 42));
906         assertEquals("  42  42", formatSimple("%4d%4d", 42, 42));
907         assertEquals(" -42", formatSimple("%4d", -42));
908         assertEquals("        42", formatSimple("%10d", 42));
909 
910         assertEquals("42", formatSimple("%01d", 42));
911         assertEquals("42", formatSimple("%02d", 42));
912         assertEquals("042", formatSimple("%03d", 42));
913         assertEquals("0042", formatSimple("%04d", 42));
914         assertEquals("00420042", formatSimple("%04d%04d", 42, 42));
915         assertEquals("-042", formatSimple("%04d", -42));
916         assertEquals("0000000042", formatSimple("%010d", 42));
917     }
918 
919     @Test
testFormatSimple_Empty()920     public void testFormatSimple_Empty() {
921         assertEquals("", formatSimple(""));
922     }
923 
924     @Test
testFormatSimple_Typical()925     public void testFormatSimple_Typical() {
926         assertEquals("String foobar and %% number -42 together",
927                 formatSimple("String %s%s and %%%% number %d%d together", "foo", "bar", -4, 2));
928     }
929 
930     @Test
testFormatSimple_Advanced()931     public void testFormatSimple_Advanced() {
932         assertEquals("000000000000002a.ext",
933                 formatSimple("%016x.%s", 42, "ext"));
934         assertEquals("crtcl=0x002a:intrsv=Y:grnk=0x0018:gsmry=A:example:rnk=0x0000",
935                 formatSimple("crtcl=0x%04x:intrsv=%c:grnk=0x%04x:gsmry=%c:%s:rnk=0x%04x",
936                         42, 'Y', 24, 'A', "example", 0));
937     }
938 
939     @Test
testFormatSimple_Mismatch()940     public void testFormatSimple_Mismatch() {
941         try {
942             formatSimple("%s");
943             fail();
944         } catch (IllegalArgumentException expected) {
945         }
946         try {
947             formatSimple("%s", "foo", "bar");
948             fail();
949         } catch (IllegalArgumentException expected) {
950         }
951     }
952 }
953