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