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
17package android.text;
18
19import static org.junit.Assert.assertEquals;
20import static org.junit.Assert.assertFalse;
21import static org.junit.Assert.assertNotNull;
22import static org.junit.Assert.assertNull;
23import static org.junit.Assert.assertTrue;
24import static org.junit.Assert.fail;
25
26import android.support.test.runner.AndroidJUnit4;
27import com.google.android.collect.Lists;
28
29import android.os.Parcel;
30import android.support.test.filters.LargeTest;
31import android.support.test.filters.SmallTest;
32import android.test.MoreAsserts;
33import android.text.style.StyleSpan;
34import android.text.util.Rfc822Token;
35import android.text.util.Rfc822Tokenizer;
36import android.view.View;
37
38import java.util.ArrayList;
39import java.util.List;
40import java.util.Locale;
41import org.junit.Test;
42import org.junit.runner.RunWith;
43
44/**
45 * TextUtilsTest tests {@link TextUtils}.
46 */
47@SmallTest
48@RunWith(AndroidJUnit4.class)
49public class TextUtilsTest {
50
51    @Test
52    public void testBasic() {
53        assertEquals("", TextUtils.concat());
54        assertEquals("foo", TextUtils.concat("foo"));
55        assertEquals("foobar", TextUtils.concat("foo", "bar"));
56        assertEquals("foobarbaz", TextUtils.concat("foo", "bar", "baz"));
57
58        SpannableString foo = new SpannableString("foo");
59        foo.setSpan("foo", 1, 2, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
60
61        SpannableString bar = new SpannableString("bar");
62        bar.setSpan("bar", 1, 2, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
63
64        SpannableString baz = new SpannableString("baz");
65        baz.setSpan("baz", 1, 2, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
66
67        assertEquals("foo", TextUtils.concat(foo).toString());
68        assertEquals("foobar", TextUtils.concat(foo, bar).toString());
69        assertEquals("foobarbaz", TextUtils.concat(foo, bar, baz).toString());
70
71        assertEquals(1, ((Spanned) TextUtils.concat(foo)).getSpanStart("foo"));
72
73        assertEquals(1, ((Spanned) TextUtils.concat(foo, bar)).getSpanStart("foo"));
74        assertEquals(4, ((Spanned) TextUtils.concat(foo, bar)).getSpanStart("bar"));
75
76        assertEquals(1, ((Spanned) TextUtils.concat(foo, bar, baz)).getSpanStart("foo"));
77        assertEquals(4, ((Spanned) TextUtils.concat(foo, bar, baz)).getSpanStart("bar"));
78        assertEquals(7, ((Spanned) TextUtils.concat(foo, bar, baz)).getSpanStart("baz"));
79
80        assertTrue(TextUtils.concat("foo", "bar") instanceof String);
81        assertTrue(TextUtils.concat(foo, bar) instanceof SpannedString);
82    }
83
84    @Test
85    public void testTemplateString() {
86        CharSequence result;
87
88        result = TextUtils.expandTemplate("This is a ^1 of the ^2 broadcast ^3.",
89                                          "test", "emergency", "system");
90        assertEquals("This is a test of the emergency broadcast system.",
91                     result.toString());
92
93        result = TextUtils.expandTemplate("^^^1^^^2^3^a^1^^b^^^c",
94                                          "one", "two", "three");
95        assertEquals("^one^twothree^aone^b^^c",
96                     result.toString());
97
98        result = TextUtils.expandTemplate("^");
99        assertEquals("^", result.toString());
100
101        result = TextUtils.expandTemplate("^^");
102        assertEquals("^", result.toString());
103
104        result = TextUtils.expandTemplate("^^^");
105        assertEquals("^^", result.toString());
106
107        result = TextUtils.expandTemplate("shorter ^1 values ^2.", "a", "");
108        assertEquals("shorter a values .", result.toString());
109
110        try {
111            TextUtils.expandTemplate("Only ^1 value given, but ^2 used.", "foo");
112            fail();
113        } catch (IllegalArgumentException e) {
114        }
115
116        try {
117            TextUtils.expandTemplate("^1 value given, and ^0 used.", "foo");
118            fail();
119        } catch (IllegalArgumentException e) {
120        }
121
122        result = TextUtils.expandTemplate("^1 value given, and ^9 used.",
123                                          "one", "two", "three", "four", "five",
124                                          "six", "seven", "eight", "nine");
125        assertEquals("one value given, and nine used.", result.toString());
126
127        try {
128            TextUtils.expandTemplate("^1 value given, and ^10 used.",
129                                     "one", "two", "three", "four", "five",
130                                     "six", "seven", "eight", "nine", "ten");
131            fail();
132        } catch (IllegalArgumentException e) {
133        }
134
135        // putting carets in the values: expansion is not recursive.
136
137        result = TextUtils.expandTemplate("^2", "foo", "^^");
138        assertEquals("^^", result.toString());
139
140        result = TextUtils.expandTemplate("^^2", "foo", "1");
141        assertEquals("^2", result.toString());
142
143        result = TextUtils.expandTemplate("^1", "value with ^2 in it", "foo");
144        assertEquals("value with ^2 in it", result.toString());
145    }
146
147    /** Fail unless text+spans contains a span 'spanName' with the given start and end. */
148    private void checkContains(Spanned text, String[] spans, String spanName,
149                               int start, int end) {
150        for (String i: spans) {
151            if (i.equals(spanName)) {
152                assertEquals(start, text.getSpanStart(i));
153                assertEquals(end, text.getSpanEnd(i));
154                return;
155            }
156        }
157        fail();
158    }
159
160    @Test
161    public void testTemplateSpan() {
162        SpannableString template;
163        Spanned result;
164        String[] spans;
165
166        // ordinary replacement
167
168        template = new SpannableString("a^1b");
169        template.setSpan("before", 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
170        template.setSpan("during", 1, 3, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
171        template.setSpan("after", 3, 4, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
172        template.setSpan("during+after", 1, 4, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
173
174        result = (Spanned) TextUtils.expandTemplate(template, "foo");
175        assertEquals(5, result.length());
176        spans = result.getSpans(0, result.length(), String.class);
177
178        // value is one character longer, so span endpoints should change.
179        assertEquals(4, spans.length);
180        checkContains(result, spans, "before", 0, 1);
181        checkContains(result, spans, "during", 1, 4);
182        checkContains(result, spans, "after", 4, 5);
183        checkContains(result, spans, "during+after", 1, 5);
184
185
186        // replacement with empty string
187
188        result = (Spanned) TextUtils.expandTemplate(template, "");
189        assertEquals(2, result.length());
190        spans = result.getSpans(0, result.length(), String.class);
191
192        // the "during" span should disappear.
193        assertEquals(3, spans.length);
194        checkContains(result, spans, "before", 0, 1);
195        checkContains(result, spans, "after", 1, 2);
196        checkContains(result, spans, "during+after", 1, 2);
197    }
198
199    @Test
200    public void testStringSplitterSimple() {
201        stringSplitterTestHelper("a,b,cde", new String[] {"a", "b", "cde"});
202    }
203
204    @Test
205    public void testStringSplitterEmpty() {
206        stringSplitterTestHelper("", new String[] {});
207    }
208
209    @Test
210    public void testStringSplitterWithLeadingEmptyString() {
211        stringSplitterTestHelper(",a,b,cde", new String[] {"", "a", "b", "cde"});
212    }
213
214    @Test
215    public void testStringSplitterWithInternalEmptyString() {
216        stringSplitterTestHelper("a,b,,cde", new String[] {"a", "b", "", "cde"});
217    }
218
219    @Test
220    public void testStringSplitterWithTrailingEmptyString() {
221        // A single trailing emtpy string should be ignored.
222        stringSplitterTestHelper("a,b,cde,", new String[] {"a", "b", "cde"});
223    }
224
225    private void stringSplitterTestHelper(String string, String[] expectedStrings) {
226        TextUtils.StringSplitter splitter = new TextUtils.SimpleStringSplitter(',');
227        splitter.setString(string);
228        List<String> strings = Lists.newArrayList();
229        for (String s : splitter) {
230            strings.add(s);
231        }
232        MoreAsserts.assertEquals(expectedStrings, strings.toArray(new String[]{}));
233    }
234
235    @Test
236    public void testTrim() {
237        String[] strings = { "abc", " abc", "  abc", "abc ", "abc  ",
238                             " abc ", "  abc  ", "\nabc\n", "\nabc", "abc\n" };
239
240        for (String s : strings) {
241            assertEquals(s.trim().length(), TextUtils.getTrimmedLength(s));
242        }
243    }
244
245    @Test
246    public void testRfc822TokenizerFullAddress() {
247        Rfc822Token[] tokens = Rfc822Tokenizer.tokenize("Foo Bar (something) <foo@google.com>");
248        assertNotNull(tokens);
249        assertEquals(1, tokens.length);
250        assertEquals("foo@google.com", tokens[0].getAddress());
251        assertEquals("Foo Bar", tokens[0].getName());
252        assertEquals("something",tokens[0].getComment());
253    }
254
255    @Test
256    public void testRfc822TokenizeItemWithError() {
257        Rfc822Token[] tokens = Rfc822Tokenizer.tokenize("\"Foo Bar\\");
258        assertNotNull(tokens);
259        assertEquals(1, tokens.length);
260        assertEquals("Foo Bar", tokens[0].getAddress());
261    }
262
263    @Test
264    public void testRfc822FindToken() {
265        Rfc822Tokenizer tokenizer = new Rfc822Tokenizer();
266        //                0           1         2           3         4
267        //                0 1234 56789012345678901234 5678 90123456789012345
268        String address = "\"Foo\" <foo@google.com>, \"Bar\" <bar@google.com>";
269        assertEquals(0, tokenizer.findTokenStart(address, 21));
270        assertEquals(22, tokenizer.findTokenEnd(address, 21));
271        assertEquals(24, tokenizer.findTokenStart(address, 25));
272        assertEquals(46, tokenizer.findTokenEnd(address, 25));
273    }
274
275    @Test
276    public void testRfc822FindTokenWithError() {
277        assertEquals(9, new Rfc822Tokenizer().findTokenEnd("\"Foo Bar\\", 0));
278    }
279
280    @LargeTest
281    @Test
282    public void testEllipsize() {
283        CharSequence s1 = "The quick brown fox jumps over \u00FEhe lazy dog.";
284        CharSequence s2 = new Wrapper(s1);
285        Spannable s3 = new SpannableString(s1);
286        s3.setSpan(new StyleSpan(0), 5, 10, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
287        TextPaint p = new TextPaint();
288        p.setFlags(p.getFlags() & ~p.DEV_KERN_TEXT_FLAG);
289
290        for (int i = 0; i < 100; i++) {
291            for (int j = 0; j < 3; j++) {
292                TextUtils.TruncateAt kind = null;
293
294                switch (j) {
295                case 0:
296                    kind = TextUtils.TruncateAt.START;
297                    break;
298
299                case 1:
300                    kind = TextUtils.TruncateAt.END;
301                    break;
302
303                case 2:
304                    kind = TextUtils.TruncateAt.MIDDLE;
305                    break;
306                }
307
308                String out1 = TextUtils.ellipsize(s1, p, i, kind).toString();
309                String out2 = TextUtils.ellipsize(s2, p, i, kind).toString();
310                String out3 = TextUtils.ellipsize(s3, p, i, kind).toString();
311
312                String keep1 = TextUtils.ellipsize(s1, p, i, kind, true, null).toString();
313                String keep2 = TextUtils.ellipsize(s2, p, i, kind, true, null).toString();
314                String keep3 = TextUtils.ellipsize(s3, p, i, kind, true, null).toString();
315
316                String trim1 = keep1.replace("\uFEFF", "");
317
318                // Are all normal output strings identical?
319                assertEquals("wid " + i + " pass " + j, out1, out2);
320                assertEquals("wid " + i + " pass " + j, out2, out3);
321
322                // Are preserved output strings identical?
323                assertEquals("wid " + i + " pass " + j, keep1, keep2);
324                assertEquals("wid " + i + " pass " + j, keep2, keep3);
325
326                // Does trimming padding from preserved yield normal?
327                assertEquals("wid " + i + " pass " + j, out1, trim1);
328
329                // Did preserved output strings preserve length?
330                assertEquals("wid " + i + " pass " + j, keep1.length(), s1.length());
331
332                // Does the output string actually fit in the space?
333                assertTrue("wid " + i + " pass " + j, p.measureText(out1) <= i);
334
335                // Is the padded output the same width as trimmed output?
336                assertTrue("wid " + i + " pass " + j, p.measureText(keep1) == p.measureText(out1));
337            }
338        }
339    }
340
341    @Test
342    public void testDelimitedStringContains() {
343        assertFalse(TextUtils.delimitedStringContains("", ',', null));
344        assertFalse(TextUtils.delimitedStringContains(null, ',', ""));
345        // Whole match
346        assertTrue(TextUtils.delimitedStringContains("gps", ',', "gps"));
347        // At beginning.
348        assertTrue(TextUtils.delimitedStringContains("gps,gpsx,network,mock", ',', "gps"));
349        assertTrue(TextUtils.delimitedStringContains("gps,network,mock", ',', "gps"));
350        // In middle, both without, before & after a false match.
351        assertTrue(TextUtils.delimitedStringContains("network,gps,mock", ',', "gps"));
352        assertTrue(TextUtils.delimitedStringContains("network,gps,gpsx,mock", ',', "gps"));
353        assertTrue(TextUtils.delimitedStringContains("network,gpsx,gps,mock", ',', "gps"));
354        // At the end.
355        assertTrue(TextUtils.delimitedStringContains("network,mock,gps", ',', "gps"));
356        assertTrue(TextUtils.delimitedStringContains("network,mock,gpsx,gps", ',', "gps"));
357        // Not present (but with a false match)
358        assertFalse(TextUtils.delimitedStringContains("network,mock,gpsx", ',', "gps"));
359    }
360
361    @Test
362    public void testCharSequenceCreator() {
363        Parcel p = Parcel.obtain();
364        TextUtils.writeToParcel(null, p, 0);
365        CharSequence text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p);
366        assertNull("null CharSequence should generate null from parcel", text);
367        p = Parcel.obtain();
368        TextUtils.writeToParcel("test", p, 0);
369        p.setDataPosition(0);
370        text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p);
371        assertEquals("conversion to/from parcel failed", "test", text);
372    }
373
374    @Test
375    public void testCharSequenceCreatorNull() {
376        Parcel p;
377        CharSequence text;
378        p = Parcel.obtain();
379        TextUtils.writeToParcel(null, p, 0);
380        p.setDataPosition(0);
381        text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p);
382        assertNull("null CharSequence should generate null from parcel", text);
383    }
384
385    @Test
386    public void testCharSequenceCreatorSpannable() {
387        Parcel p;
388        CharSequence text;
389        p = Parcel.obtain();
390        TextUtils.writeToParcel(new SpannableString("test"), p, 0);
391        p.setDataPosition(0);
392        text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p);
393        assertEquals("conversion to/from parcel failed", "test", text.toString());
394    }
395
396    @Test
397    public void testCharSequenceCreatorString() {
398        Parcel p;
399        CharSequence text;
400        p = Parcel.obtain();
401        TextUtils.writeToParcel("test", p, 0);
402        p.setDataPosition(0);
403        text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p);
404        assertEquals("conversion to/from parcel failed", "test", text.toString());
405    }
406
407    /**
408     * CharSequence wrapper for testing the cases where text is copied into
409     * a char array instead of working from a String or a Spanned.
410     */
411    private static class Wrapper implements CharSequence {
412        private CharSequence mString;
413
414        public Wrapper(CharSequence s) {
415            mString = s;
416        }
417
418        @Override
419        public int length() {
420            return mString.length();
421        }
422
423        @Override
424        public char charAt(int off) {
425            return mString.charAt(off);
426        }
427
428        @Override
429        public String toString() {
430            return mString.toString();
431        }
432
433        @Override
434        public CharSequence subSequence(int start, int end) {
435            return new Wrapper(mString.subSequence(start, end));
436        }
437    }
438
439    @Test
440    public void testRemoveEmptySpans() {
441        MockSpanned spanned = new MockSpanned();
442
443        spanned.test();
444        spanned.addSpan().test();
445        spanned.addSpan().test();
446        spanned.addSpan().test();
447        spanned.addEmptySpan().test();
448        spanned.addSpan().test();
449        spanned.addEmptySpan().test();
450        spanned.addEmptySpan().test();
451        spanned.addSpan().test();
452
453        spanned.clear();
454        spanned.addEmptySpan().test();
455        spanned.addEmptySpan().test();
456        spanned.addEmptySpan().test();
457        spanned.addSpan().test();
458        spanned.addEmptySpan().test();
459        spanned.addSpan().test();
460
461        spanned.clear();
462        spanned.addSpan().test();
463        spanned.addEmptySpan().test();
464        spanned.addSpan().test();
465        spanned.addEmptySpan().test();
466        spanned.addSpan().test();
467        spanned.addSpan().test();
468    }
469
470    protected static class MockSpanned implements Spanned {
471
472        private List<Object> allSpans = new ArrayList<Object>();
473        private List<Object> nonEmptySpans = new ArrayList<Object>();
474
475        public void clear() {
476            allSpans.clear();
477            nonEmptySpans.clear();
478        }
479
480        public MockSpanned addSpan() {
481            Object o = new Object();
482            allSpans.add(o);
483            nonEmptySpans.add(o);
484            return this;
485        }
486
487        public MockSpanned addEmptySpan() {
488            Object o = new Object();
489            allSpans.add(o);
490            return this;
491        }
492
493        public void test() {
494            Object[] nonEmpty = TextUtils.removeEmptySpans(allSpans.toArray(), this, Object.class);
495            assertEquals("Mismatched array size", nonEmptySpans.size(), nonEmpty.length);
496            for (int i=0; i<nonEmpty.length; i++) {
497                assertEquals("Span differ", nonEmptySpans.get(i), nonEmpty[i]);
498            }
499        }
500
501        @Override
502        public char charAt(int arg0) {
503            return 0;
504        }
505
506        @Override
507        public int length() {
508            return 0;
509        }
510
511        @Override
512        public CharSequence subSequence(int arg0, int arg1) {
513            return null;
514        }
515
516        @Override
517        public <T> T[] getSpans(int start, int end, Class<T> type) {
518            return null;
519        }
520
521        @Override
522        public int getSpanStart(Object tag) {
523            return 0;
524        }
525
526        @Override
527        public int getSpanEnd(Object tag) {
528            return nonEmptySpans.contains(tag) ? 1 : 0;
529        }
530
531        @Override
532        public int getSpanFlags(Object tag) {
533            return 0;
534        }
535
536        @Override
537        public int nextSpanTransition(int start, int limit, Class type) {
538            return 0;
539        }
540    }
541
542    @Test
543    public void testGetLayoutDirectionFromLocale() {
544        assertEquals(View.LAYOUT_DIRECTION_LTR, TextUtils.getLayoutDirectionFromLocale(null));
545        assertEquals(View.LAYOUT_DIRECTION_LTR,
546                TextUtils.getLayoutDirectionFromLocale(Locale.ROOT));
547        assertEquals(View.LAYOUT_DIRECTION_LTR,
548                TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("en")));
549        assertEquals(View.LAYOUT_DIRECTION_LTR,
550                TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("en-US")));
551        assertEquals(View.LAYOUT_DIRECTION_LTR,
552                TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("az")));
553        assertEquals(View.LAYOUT_DIRECTION_LTR,
554                TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("az-AZ")));
555        assertEquals(View.LAYOUT_DIRECTION_LTR,
556                TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("az-Latn")));
557        assertEquals(View.LAYOUT_DIRECTION_LTR,
558                TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("en-EG")));
559        assertEquals(View.LAYOUT_DIRECTION_LTR,
560                TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("ar-Latn")));
561
562        assertEquals(View.LAYOUT_DIRECTION_RTL,
563                TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("ar")));
564        assertEquals(View.LAYOUT_DIRECTION_RTL,
565                TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("fa")));
566        assertEquals(View.LAYOUT_DIRECTION_RTL,
567                TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("he")));
568        assertEquals(View.LAYOUT_DIRECTION_RTL,
569                TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("iw")));
570        assertEquals(View.LAYOUT_DIRECTION_RTL,
571                TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("ur")));
572        assertEquals(View.LAYOUT_DIRECTION_RTL,
573                TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("dv")));
574        assertEquals(View.LAYOUT_DIRECTION_RTL,
575                TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("az-Arab")));
576        assertEquals(View.LAYOUT_DIRECTION_RTL,
577                TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("az-IR")));
578        assertEquals(View.LAYOUT_DIRECTION_RTL,
579                TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("fa-US")));
580        assertEquals(View.LAYOUT_DIRECTION_RTL,
581                TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("tr-Arab")));
582    }
583}
584