1/*
2 * Copyright (C) 2016 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.support.v4.text.util;
18
19import static org.junit.Assert.assertEquals;
20import static org.junit.Assert.assertFalse;
21import static org.junit.Assert.assertNotEquals;
22import static org.junit.Assert.assertTrue;
23import static org.junit.Assert.fail;
24
25import android.support.test.runner.AndroidJUnit4;
26import android.support.v4.util.PatternsCompat;
27import android.test.suitebuilder.annotation.SmallTest;
28import android.text.Spannable;
29import android.text.SpannableString;
30import android.text.style.URLSpan;
31import android.text.util.Linkify;
32import android.text.util.Linkify.MatchFilter;
33import android.text.util.Linkify.TransformFilter;
34
35import org.junit.Test;
36import org.junit.runner.RunWith;
37
38import java.util.Locale;
39import java.util.regex.Matcher;
40import java.util.regex.Pattern;
41
42@RunWith(AndroidJUnit4.class)
43@SmallTest
44public class LinkifyCompatTest {
45    private static final Pattern LINKIFY_TEST_PATTERN = Pattern.compile(
46            "(test:)?[a-zA-Z0-9]+(\\.pattern)?");
47
48    private MatchFilter mMatchFilterStartWithDot = new MatchFilter() {
49        public final boolean acceptMatch(final CharSequence s, final int start, final int end) {
50            if (start == 0) {
51                return true;
52            }
53
54            if (s.charAt(start - 1) == '.') {
55                return false;
56            }
57
58            return true;
59        }
60    };
61
62    private TransformFilter mTransformFilterUpperChar = new TransformFilter() {
63        public final String transformUrl(final Matcher match, String url) {
64            StringBuilder buffer = new StringBuilder();
65            String matchingRegion = match.group();
66
67            for (int i = 0, size = matchingRegion.length(); i < size; i++) {
68                char character = matchingRegion.charAt(i);
69
70                if (character == '.' || Character.isLowerCase(character)
71                        || Character.isDigit(character)) {
72                    buffer.append(character);
73                }
74            }
75            return buffer.toString();
76        }
77    };
78
79    @Test
80    public void testAddLinks1() {
81        // Verify URLs including the ones that have new gTLDs, and the
82        // ones that look like gTLDs (and so are accepted by linkify)
83        // and the ones that should not be linkified due to non-compliant
84        // gTLDs
85        final String longGTLD =
86                "abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabc";
87        SpannableString spannable = new SpannableString("name@gmail.com, "
88                + "www.google.com, http://www.google.com/language_tools?hl=en, "
89                + "a.bd, "   // a URL with accepted TLD so should be linkified
90                + "d.e, f.1, g.12, "  // not valid, so should not be linkified
91                + "http://h." + longGTLD + " "  // valid, should be linkified
92                + "j." + longGTLD + "a"); // not a valid URL (gtld too long), no linkify
93
94        assertTrue(LinkifyCompat.addLinks(spannable, Linkify.WEB_URLS));
95        URLSpan[] spans = spannable.getSpans(0, spannable.length(), URLSpan.class);
96        assertEquals(4, spans.length);
97        assertEquals("http://www.google.com", spans[0].getURL());
98        assertEquals("http://www.google.com/language_tools?hl=en", spans[1].getURL());
99        assertEquals("http://a.bd", spans[2].getURL());
100        assertEquals("http://h." + longGTLD, spans[3].getURL());
101
102        assertTrue(LinkifyCompat.addLinks(spannable, Linkify.EMAIL_ADDRESSES));
103        spans = spannable.getSpans(0, spannable.length(), URLSpan.class);
104        assertEquals(1, spans.length);
105        assertEquals("mailto:name@gmail.com", spans[0].getURL());
106
107        try {
108            LinkifyCompat.addLinks((Spannable) null, Linkify.WEB_URLS);
109            fail("Should throw NullPointerException!");
110        } catch (NullPointerException e) {
111            // expect
112        }
113
114        assertFalse(LinkifyCompat.addLinks((Spannable) null, 0));
115    }
116
117    @Test
118    public void testAddLinks2() {
119        String text = "google.pattern, test:AZ0101.pattern";
120
121        SpannableString spannable = new SpannableString(text);
122        LinkifyCompat.addLinks(spannable, LINKIFY_TEST_PATTERN, "Test:");
123        URLSpan[] spans = (spannable.getSpans(0, spannable.length(), URLSpan.class));
124        assertEquals(2, spans.length);
125        assertEquals("test:google.pattern", spans[0].getURL());
126        assertEquals("test:AZ0101.pattern", spans[1].getURL());
127
128        try {
129            LinkifyCompat.addLinks((Spannable)null, LINKIFY_TEST_PATTERN, "Test:");
130            fail("Should throw NullPointerException!");
131        } catch (NullPointerException e) {
132        }
133
134        try {
135            LinkifyCompat.addLinks(spannable, null, "Test:");
136            fail("Should throw NullPointerException!");
137        } catch (NullPointerException e) {
138        }
139
140        spannable = new SpannableString(text);
141        LinkifyCompat.addLinks(spannable, LINKIFY_TEST_PATTERN, null);
142        spans = (spannable.getSpans(0, spannable.length(), URLSpan.class));
143        assertEquals(2, spans.length);
144        assertEquals("google.pattern", spans[0].getURL());
145        assertEquals("test:AZ0101.pattern", spans[1].getURL());
146    }
147
148    @Test
149    public void testAddLinks3() {
150        String text = "FilterUpperCase.pattern, 12.345.pattern";
151
152        SpannableString spannable = new SpannableString(text);
153        LinkifyCompat.addLinks(spannable, LINKIFY_TEST_PATTERN, "Test:",
154                mMatchFilterStartWithDot, mTransformFilterUpperChar);
155        URLSpan[] spans = (spannable.getSpans(0, spannable.length(), URLSpan.class));
156        assertEquals(2, spans.length);
157        assertEquals("test:ilterpperase.pattern", spans[0].getURL());
158        assertEquals("test:12", spans[1].getURL());
159
160        try {
161            LinkifyCompat.addLinks((Spannable)null, LINKIFY_TEST_PATTERN, "Test:",
162                    mMatchFilterStartWithDot, mTransformFilterUpperChar);
163            fail("Should throw NullPointerException!");
164        } catch (NullPointerException e) {
165            // expect
166        }
167
168        try {
169            LinkifyCompat.addLinks(spannable, null, "Test:", mMatchFilterStartWithDot,
170                    mTransformFilterUpperChar);
171            fail("Should throw NullPointerException!");
172        } catch (NullPointerException e) {
173            // expect
174        }
175
176        spannable = new SpannableString(text);
177        LinkifyCompat.addLinks(spannable, LINKIFY_TEST_PATTERN, null, mMatchFilterStartWithDot,
178                mTransformFilterUpperChar);
179        spans = (spannable.getSpans(0, spannable.length(), URLSpan.class));
180        assertEquals(2, spans.length);
181        assertEquals("ilterpperase.pattern", spans[0].getURL());
182        assertEquals("12", spans[1].getURL());
183
184        spannable = new SpannableString(text);
185        LinkifyCompat.addLinks(spannable, LINKIFY_TEST_PATTERN, "Test:", null, mTransformFilterUpperChar);
186        spans = (spannable.getSpans(0, spannable.length(), URLSpan.class));
187        assertEquals(3, spans.length);
188        assertEquals("test:ilterpperase.pattern", spans[0].getURL());
189        assertEquals("test:12", spans[1].getURL());
190        assertEquals("test:345.pattern", spans[2].getURL());
191
192        spannable = new SpannableString(text);
193        LinkifyCompat.addLinks(spannable, LINKIFY_TEST_PATTERN, "Test:", mMatchFilterStartWithDot, null);
194        spans = (spannable.getSpans(0, spannable.length(), URLSpan.class));
195        assertEquals(2, spans.length);
196        assertEquals("test:FilterUpperCase.pattern", spans[0].getURL());
197        assertEquals("test:12", spans[1].getURL());
198    }
199
200    @Test
201    public void testAddLinks4() {
202        String numbersInvalid = "123456789 not a phone number";
203        String numbersUKLocal = "tel:(0812)1234560 (0812)1234561";
204        String numbersUSLocal = "tel:(812)1234562 (812)123.4563 "
205                + " tel:(800)5551210 (800)555-1211 555-1212";
206        String numbersIntl = "tel:+4408121234564 +44-0812-123-4565"
207                + " tel:+18005551213 +1-800-555-1214";
208        SpannableString spannable = new SpannableString(
209                numbersInvalid
210                        + " " + numbersUKLocal
211                        + " " + numbersUSLocal
212                        + " " + numbersIntl);
213
214        // phonenumber linkify is locale-dependent
215        if (Locale.US.equals(Locale.getDefault())) {
216            assertTrue(LinkifyCompat.addLinks(spannable, Linkify.PHONE_NUMBERS));
217            URLSpan[] spans = spannable.getSpans(0, spannable.length(), URLSpan.class);
218            // We cannot assert the contents of the spans as support library falls back to the
219            // framework libphonenumber which behaves differently for different API levels.
220            assertNotEquals("There should be more than zero phone number spans.", 0, spans.length);
221        }
222
223        try {
224            LinkifyCompat.addLinks((Spannable) null, Linkify.WEB_URLS);
225            fail("Should throw NullPointerException!");
226        } catch (NullPointerException e) {
227            // expect
228        }
229
230        assertFalse(LinkifyCompat.addLinks((Spannable) null, 0));
231    }
232
233    @Test
234    public void testAddLinks_spanOverlapPruning() {
235        SpannableString spannable = new SpannableString("800-555-1211@gmail.com 800-555-1222.com"
236                + " phone800-555-1233");
237
238        // phonenumber linkify is locale-dependent
239        if (Locale.US.equals(Locale.getDefault())) {
240            assertTrue(LinkifyCompat.addLinks(spannable, Linkify.ALL));
241            URLSpan[] spans = spannable.getSpans(0, spannable.length(), URLSpan.class);
242            assertEquals(3, spans.length);
243            assertEquals("tel:8005551233", spans[0].getURL());
244            assertEquals("mailto:800-555-1211@gmail.com", spans[1].getURL());
245            assertEquals("http://800-555-1222.com", spans[2].getURL());
246        }
247    }
248
249    @Test
250    public void testAddLinks_addsLinksWhenDefaultSchemeIsNull() {
251        Spannable spannable = new SpannableString("any https://android.com any android.com any");
252        LinkifyCompat.addLinks(spannable, PatternsCompat.AUTOLINK_WEB_URL, null, null, null);
253
254        URLSpan[] spans = spannable.getSpans(0, spannable.length(), URLSpan.class);
255        assertEquals("android.com and https://android.com should be linkified", 2, spans.length);
256        assertEquals("https://android.com", spans[0].getURL());
257        assertEquals("android.com", spans[1].getURL());
258    }
259
260    @Test
261    public void testAddLinks_addsLinksWhenSchemesArrayIsNull() {
262        Spannable spannable = new SpannableString("any https://android.com any android.com any");
263        LinkifyCompat.addLinks(spannable, PatternsCompat.AUTOLINK_WEB_URL, "http://", null, null);
264
265        URLSpan[] spans = spannable.getSpans(0, spannable.length(), URLSpan.class);
266        assertEquals("android.com and https://android.com should be linkified", 2, spans.length);
267        // expected behavior, passing null schemes array means: prepend defaultScheme to all links.
268        assertEquals("http://https://android.com", spans[0].getURL());
269        assertEquals("http://android.com", spans[1].getURL());
270    }
271
272    @Test
273    public void testAddLinks_prependsDefaultSchemeToBeginingOfLink() {
274        Spannable spannable = new SpannableString("any android.com any");
275        LinkifyCompat.addLinks(spannable, PatternsCompat.AUTOLINK_WEB_URL, "http://",
276                new String[] { "http://", "https://"}, null, null);
277
278        URLSpan[] spans = spannable.getSpans(0, spannable.length(), URLSpan.class);
279        assertEquals("android.com should be linkified", 1, spans.length);
280        assertEquals("http://android.com", spans[0].getURL());
281    }
282
283    @Test
284    public void testAddLinks_doesNotPrependSchemeIfSchemeExists() {
285        Spannable spannable = new SpannableString("any https://android.com any");
286        LinkifyCompat.addLinks(spannable, PatternsCompat.AUTOLINK_WEB_URL, "http://",
287                new String[] { "http://", "https://"}, null, null);
288
289        URLSpan[] spans = spannable.getSpans(0, spannable.length(), URLSpan.class);
290        assertEquals("android.com should be linkified", 1, spans.length);
291        assertEquals("https://android.com", spans[0].getURL());
292    }
293
294    // WEB_URLS Related Tests
295
296    @Test
297    public void testAddLinks_doesNotAddLinksForUrlWithoutProtocolAndWithoutKnownTld()
298            throws Exception {
299        Spannable spannable = new SpannableString("hey man.its me");
300        boolean linksAdded = LinkifyCompat.addLinks(spannable, Linkify.ALL);
301        assertFalse("Should not add link with unknown TLD", linksAdded);
302    }
303
304    @Test
305    public void testAddLinks_shouldNotAddEmailAddressAsUrl() throws Exception {
306        String url = "name@gmail.com";
307        assertAddLinksWithWebUrlFails("Should not recognize email address as URL", url);
308    }
309
310    public void testAddLinks_acceptsUrlsWithCommasInRequestParameterValues() throws Exception {
311        String url = "https://android.com/path?ll=37.4221,-122.0836&z=17&pll=37.4221,-122.0836";
312        assertAddLinksWithWebUrlSucceeds("Should accept commas", url);
313    }
314
315    @Test
316    public void testAddLinks_addsLinksForUrlWithProtocolWithoutTld() throws Exception {
317        String url = "http://android/#notld///a/n/d/r/o/i/d&p1=1&p2=2";
318        assertAddLinksWithWebUrlSucceeds("Should accept URL starting with protocol but does not" +
319                " have TLD", url);
320    }
321
322    @Test
323    public void testAddLinks_matchesProtocolCaseInsensitive() throws Exception {
324        String url = "hTtP://android.com";
325        assertAddLinksWithWebUrlSucceeds("Protocol matching should be case insensitive", url);
326    }
327
328    @Test
329    public void testAddLinks_matchesValidUrlWithSchemeAndHostname() throws Exception {
330        String url = "http://www.android.com";
331        assertAddLinksWithWebUrlSucceeds("Should match valid URL with scheme and hostname", url);
332    }
333
334    @Test
335    public void testAddLinks_matchesValidUrlWithSchemeHostnameAndNewTld() throws Exception {
336        String url = "http://www.android.me";
337        assertAddLinksWithWebUrlSucceeds("Should match valid URL with scheme hostname and new TLD",
338                url);
339    }
340
341    @Test
342    public void testAddLinks_matchesValidUrlWithHostnameAndNewTld() throws Exception {
343        String url = "android.camera";
344        assertAddLinksWithWebUrlSucceeds("Should match valid URL with hostname and new TLD", url);
345    }
346
347    @Test
348    public void testAddLinks_matchesPunycodeUrl() throws Exception {
349        String url = "http://xn--fsqu00a.xn--unup4y";
350        assertAddLinksWithWebUrlSucceeds("Should match Punycode URL", url);
351    }
352
353    @Test
354    public void testAddLinks_matchesPunycodeUrlWithoutProtocol() throws Exception {
355        String url = "xn--fsqu00a.xn--unup4y";
356        assertAddLinksWithWebUrlSucceeds("Should match Punycode URL without protocol", url);
357    }
358
359    @Test
360    public void testAddLinks_doesNotMatchPunycodeTldThatStartsWithDash() throws Exception {
361        String url = "xn--fsqu00a.-xn--unup4y";
362        assertAddLinksWithWebUrlFails("Should not match Punycode TLD that starts with dash", url);
363    }
364
365    @Test
366    public void testAddLinks_partiallyMatchesPunycodeTldThatEndsWithDash() throws Exception {
367        String url = "http://xn--fsqu00a.xn--unup4y-";
368        assertAddLinksWithWebUrlPartiallyMatches("Should partially match Punycode TLD that ends " +
369                "with dash", "http://xn--fsqu00a.xn--unup4y", url);
370    }
371
372    @Test
373    public void testAddLinks_matchesUrlWithUnicodeDomainName() throws Exception {
374        String url = "http://\uD604\uAE08\uC601\uC218\uC99D.kr";
375        assertAddLinksWithWebUrlSucceeds("Should match URL with Unicode domain name", url);
376    }
377
378    @Test
379    public void testAddLinks_matchesUrlWithUnicodeDomainNameWithoutProtocol() throws Exception {
380        String url = "\uD604\uAE08\uC601\uC218\uC99D.kr";
381        assertAddLinksWithWebUrlSucceeds("Should match URL without protocol and with Unicode " +
382                "domain name", url);
383    }
384
385    @Test
386    public void testAddLinks_matchesUrlWithUnicodeDomainNameAndTld() throws Exception {
387        String url = "\uB3C4\uBA54\uC778.\uD55C\uAD6D";
388        assertAddLinksWithWebUrlSucceeds("Should match URL with Unicode domain name and TLD", url);
389    }
390
391    @Test
392    public void testAddLinks_matchesUrlWithUnicodePath() throws Exception {
393        String url = "http://android.com/\u2019/a";
394        assertAddLinksWithWebUrlSucceeds("Should match URL with Unicode path", url);
395    }
396
397    @Test
398    public void testAddLinks_matchesValidUrlWithPort() throws Exception {
399        String url = "http://www.example.com:8080";
400        assertAddLinksWithWebUrlSucceeds("Should match URL with port", url);
401    }
402
403    @Test
404    public void testAddLinks_matchesUrlWithPortAndQuery() throws Exception {
405        String url = "http://www.example.com:8080/?foo=bar";
406        assertAddLinksWithWebUrlSucceeds("Should match URL with port and query", url);
407    }
408
409    @Test
410    public void testAddLinks_matchesUrlWithTilde() throws Exception {
411        String url = "http://www.example.com:8080/~user/?foo=bar";
412        assertAddLinksWithWebUrlSucceeds("Should match URL with tilde", url);
413    }
414
415    @Test
416    public void testAddLinks_matchesUrlStartingWithHttpAndDoesNotHaveTld() throws Exception {
417        String url = "http://android/#notld///a/n/d/r/o/i/d&p1=1&p2=2";
418        assertAddLinksWithWebUrlSucceeds("Should match URL without a TLD and starting with http",
419                url);
420    }
421
422    @Test
423    public void testAddLinks_doesNotMatchUrlsWithoutProtocolAndWithUnknownTld() throws Exception {
424        String url = "thank.you";
425        assertAddLinksWithWebUrlFails("Should not match URL that does not start with a protocol " +
426                "and does not contain a known TLD", url);
427    }
428
429    @Test
430    public void testAddLinks_matchesValidUrlWithEmoji() throws Exception {
431        String url = "Thank\u263A.com";
432        assertAddLinksWithWebUrlSucceeds("Should match URL with emoji", url);
433    }
434
435    @Test
436    public void testAddLinks_doesNotMatchUrlsWithEmojiWithoutProtocolAndWithoutKnownTld()
437            throws Exception {
438        String url = "Thank\u263A.you";
439        assertAddLinksWithWebUrlFails("Should not match URLs containing emoji and with unknown " +
440                "TLD", url);
441    }
442
443    @Test
444    public void testAddLinks_matchesDomainNameWithSurrogatePairs() throws Exception {
445        String url = "android\uD83C\uDF38.com";
446        assertAddLinksWithWebUrlSucceeds("Should match domain name with Unicode surrogate pairs",
447                url);
448    }
449
450    @Test
451    public void testAddLinks_matchesTldWithSurrogatePairs() throws Exception {
452        String url = "http://android.\uD83C\uDF38com";
453        assertAddLinksWithWebUrlSucceeds("Should match TLD with Unicode surrogate pairs", url);
454    }
455
456    @Test
457    public void testAddLinks_doesNotMatchUrlWithExcludedSurrogate() throws Exception {
458        String url = "android\uD83F\uDFFE.com";
459        assertAddLinksWithWebUrlFails("Should not match URL with excluded Unicode surrogate" +
460                " pair",  url);
461    }
462
463    @Test
464    public void testAddLinks_matchesPathWithSurrogatePairs() throws Exception {
465        String url = "http://android.com/path-with-\uD83C\uDF38?v=\uD83C\uDF38f";
466        assertAddLinksWithWebUrlSucceeds("Should match path and query with Unicode surrogate pairs",
467                url);
468    }
469
470    @Test
471    public void testAddLinks__doesNotMatchUnicodeSpaces() throws Exception {
472        String part1 = "http://and";
473        String part2 = "roid.com";
474        String[] emptySpaces = new String[]{
475                "\u00A0", // no-break space
476                "\u2000", // en quad
477                "\u2001", // em quad
478                "\u2002", // en space
479                "\u2003", // em space
480                "\u2004", // three-per-em space
481                "\u2005", // four-per-em space
482                "\u2006", // six-per-em space
483                "\u2007", // figure space
484                "\u2008", // punctuation space
485                "\u2009", // thin space
486                "\u200A", // hair space
487                "\u2028", // line separator
488                "\u2029", // paragraph separator
489                "\u202F", // narrow no-break space
490                "\u3000"  // ideographic space
491        };
492
493        for (String emptySpace : emptySpaces) {
494            String url = part1 + emptySpace + part2;
495            assertAddLinksWithWebUrlPartiallyMatches("Should not include empty space with code: " +
496                    emptySpace.codePointAt(0), part1, url);
497        }
498    }
499
500    @Test
501    public void testAddLinks_matchesDomainNameWithDash() throws Exception {
502        String url = "http://a-nd.r-oid.com";
503        assertAddLinksWithWebUrlSucceeds("Should match domain name with '-'", url);
504
505        url = "a-nd.r-oid.com";
506        assertAddLinksWithWebUrlSucceeds("Should match domain name with '-'", url);
507    }
508
509    @Test
510    public void testAddLinks_matchesDomainNameWithUnderscore() throws Exception {
511        String url = "http://a_nd.r_oid.com";
512        assertAddLinksWithWebUrlSucceeds("Should match domain name with '_'", url);
513
514        url = "a_nd.r_oid.com";
515        assertAddLinksWithWebUrlSucceeds("Should match domain name with '_'", url);
516    }
517
518    @Test
519    public void testAddLinks_matchesPathAndQueryWithDollarSign() throws Exception {
520        String url = "http://android.com/path$?v=$val";
521        assertAddLinksWithWebUrlSucceeds("Should match path and query with '$'", url);
522
523        url = "android.com/path$?v=$val";
524        assertAddLinksWithWebUrlSucceeds("Should match path and query with '$'", url);
525    }
526
527    @Test
528    public void testAddLinks_matchesEmptyPathWithQueryParams() throws Exception {
529        String url = "http://android.com?q=v";
530        assertAddLinksWithWebUrlSucceeds("Should match empty path with query params", url);
531
532        url = "android.com?q=v";
533        assertAddLinksWithWebUrlSucceeds("Should match empty path with query params", url);
534
535        url = "http://android.com/?q=v";
536        assertAddLinksWithWebUrlSucceeds("Should match empty path with query params", url);
537
538        url = "android.com/?q=v";
539        assertAddLinksWithWebUrlSucceeds("Should match empty path with query params", url);
540    }
541
542    // EMAIL_ADDRESSES Related Tests
543
544    @Test
545    public void testAddLinks_email_matchesShortValidEmail() throws Exception {
546        String email = "a@a.co";
547        assertAddLinksWithEmailSucceeds("Should match email: " + email, email);
548    }
549
550    @Test
551    public void testAddLinks_email_matchesRegularEmail() throws Exception {
552        String email = "email@android.com";
553        assertAddLinksWithEmailSucceeds("Should match email: " + email, email);
554    }
555
556    @Test
557    public void testAddLinks_email_matchesEmailWithMultipleSubdomains() throws Exception {
558        String email = "email@e.somelongdomainnameforandroid.abc.uk";
559        assertAddLinksWithEmailSucceeds("Should match email: " + email, email);
560    }
561
562    @Test
563    public void testAddLinks_email_matchesLocalPartWithDot() throws Exception {
564        String email = "e.mail@android.com";
565        assertAddLinksWithEmailSucceeds("Should match email: " + email, email);
566    }
567
568    @Test
569    public void testAddLinks_email_matchesLocalPartWithPlus() throws Exception {
570        String email = "e+mail@android.com";
571        assertAddLinksWithEmailSucceeds("Should match email: " + email, email);
572    }
573
574    @Test
575    public void testAddLinks_email_matchesLocalPartWithUnderscore() throws Exception {
576        String email = "e_mail@android.com";
577        assertAddLinksWithEmailSucceeds("Should match email: " + email, email);
578    }
579
580    @Test
581    public void testAddLinks_email_matchesLocalPartWithDash() throws Exception {
582        String email = "e-mail@android.com";
583        assertAddLinksWithEmailSucceeds("Should match email: " + email, email);
584    }
585
586    @Test
587    public void testAddLinks_email_matchesLocalPartWithApostrophe() throws Exception {
588        String email = "e'mail@android.com";
589        assertAddLinksWithEmailSucceeds("Should match email: " + email, email);
590    }
591
592    @Test
593    public void testAddLinks_email_matchesLocalPartWithDigits() throws Exception {
594        String email = "123@android.com";
595        assertAddLinksWithEmailSucceeds("Should match email: " + email, email);
596    }
597
598    @Test
599    public void testAddLinks_email_matchesUnicodeLocalPart() throws Exception {
600        String email = "\uD604\uAE08\uC601\uC218\uC99D@android.kr";
601        assertAddLinksWithEmailSucceeds("Should match email: " + email, email);
602    }
603
604    @Test
605    public void testAddLinks_email_matchesLocalPartWithEmoji() throws Exception {
606        String email = "smiley\u263A@android.com";
607        assertAddLinksWithEmailSucceeds("Should match email: " + email, email);
608    }
609
610    @Test
611    public void testAddLinks_email_matchesLocalPartWithSurrogatePairs()
612            throws Exception {
613        String email = "a\uD83C\uDF38a@android.com";
614        assertAddLinksWithEmailSucceeds("Should match email: " + email, email);
615    }
616
617    @Test
618    public void testAddLinks_email_matchesDomainWithDash() throws Exception {
619        String email = "email@an-droid.com";
620        assertAddLinksWithEmailSucceeds("Should match email: " + email, email);
621    }
622
623    @Test
624    public void testAddLinks_email_matchesUnicodeDomain() throws Exception {
625        String email = "email@\uD604\uAE08\uC601\uC218\uC99D.kr";
626        assertAddLinksWithEmailSucceeds("Should match email: " + email, email);
627    }
628
629    @Test
630    public void testAddLinks_email_matchesUnicodeLocalPartAndDomain()
631            throws Exception {
632        String email = "\uD604\uAE08\uC601\uC218\uC99D@\uD604\uAE08\uC601\uC218\uC99D.kr";
633        assertAddLinksWithEmailSucceeds("Should match email: " + email, email);
634    }
635
636    @Test
637    public void testAddLinks_email_matchesDomainWithEmoji() throws Exception {
638        String email = "smiley@\u263Aandroid.com";
639        assertAddLinksWithEmailSucceeds("Should match email: " + email, email);
640    }
641
642    @Test
643    public void testAddLinks_email_matchesDomainWithSurrogatePairs()
644            throws Exception {
645        String email = "email@\uD83C\uDF38android.com";
646        assertAddLinksWithEmailSucceeds("Should match email: " + email, email);
647    }
648
649    @Test
650    public void testAddLinks_email_matchesLocalPartAndDomainWithSurrogatePairs()
651            throws Exception {
652        String email = "a\uD83C\uDF38a@\uD83C\uDF38android.com";
653        assertAddLinksWithEmailSucceeds("Should match email: " + email, email);
654    }
655
656    @Test
657    public void testAddLinks_partiallyMatchesEmailEndingWithDot() throws Exception {
658        String email = "email@android.co.uk.";
659        assertAddLinksWithEmailPartiallyMatches("Should partially match email ending with dot",
660                "mailto:email@android.co.uk", email);
661    }
662
663    @Test
664    public void testAddLinks_email_partiallyMatchesLocalPartStartingWithDot()
665            throws Exception {
666        String email = ".email@android.com";
667        assertAddLinksWithEmailPartiallyMatches("Should partially match email starting " +
668                "with dot", "mailto:email@android.com", email);
669    }
670
671    @Test
672    public void testAddLinks_email_doesNotMatchStringWithoutAtSign() throws Exception {
673        String email = "android.com";
674        assertAddLinksWithEmailFails("Should not match email: " + email, email);
675    }
676
677    @Test
678    public void testAddLinks_email_doesNotMatchPlainString() throws Exception {
679        String email = "email";
680        assertAddLinksWithEmailFails("Should not match email: " + email, email);
681    }
682
683    @Test
684    public void testAddLinks_email_doesNotMatchEmailWithoutTld() throws Exception {
685        String email = "email@android";
686        assertAddLinksWithEmailFails("Should not match email: " + email, email);
687    }
688
689    @Test
690    public void testAddLinks_email_doesNotMatchLocalPartEndingWithDot()
691            throws Exception {
692        String email = "email.@android.com";
693        assertAddLinksWithEmailFails("Should not match email: " + email, email);
694    }
695
696    @Test
697    public void testAddLinks_email_doesNotMatchDomainStartingWithDash()
698            throws Exception {
699        String email = "email@-android.com";
700        assertAddLinksWithEmailFails("Should not match email: " + email, email);
701    }
702
703    @Test
704    public void testAddLinks_email_doesNotMatchDomainWithConsecutiveDots()
705            throws Exception {
706        String email = "email@android..com";
707        assertAddLinksWithEmailFails("Should not match email: " + email, email);
708    }
709
710    @Test
711    public void testAddLinks_email_doesNotMatchEmailWithIp() throws Exception {
712        String email = "email@127.0.0.1";
713        assertAddLinksWithEmailFails("Should not match email: " + email, email);
714    }
715
716    @Test
717    public void testAddLinks_email_doesNotMatchEmailWithInvalidTld()
718            throws Exception {
719        String email = "email@android.c";
720        assertAddLinksWithEmailFails("Should not match email: " + email, email);
721    }
722
723    @Test
724    public void testAddLinks_email_matchesLocalPartUpTo64Chars() throws Exception {
725        String localPart = "";
726        for (int i = 0; i < 64; i++) {
727            localPart += "a";
728        }
729        String email = localPart + "@android.com";
730        assertAddLinksWithEmailSucceeds("Should match email local part of length: " +
731                localPart.length(), email);
732
733        email = localPart + "a@android.com";
734        assertAddLinksWithEmailFails("Should not match email local part of length:" +
735                localPart.length(), email);
736    }
737
738    @Test
739    public void testAddLinks_email_matchesSubdomainUpTo63Chars() throws Exception {
740        String subdomain = "";
741        for (int i = 0; i < 63; i++) {
742            subdomain += "a";
743        }
744        String email = "email@" + subdomain + ".com";
745
746        assertAddLinksWithEmailSucceeds("Should match email subdomain of length: " +
747                subdomain.length(), email);
748
749        subdomain += "a";
750        email = "email@" + subdomain + ".com";
751
752        assertAddLinksWithEmailFails("Should not match email subdomain of length:" +
753                subdomain.length(), email);
754    }
755
756    @Test
757    public void testAddLinks_email_matchesDomainUpTo255Chars() throws Exception {
758        String domain = "";
759        while (domain.length() <= 250) {
760            domain += "d.";
761        }
762        domain += "com";
763        assertEquals(255, domain.length());
764        String email = "a@" + domain;
765        assertAddLinksWithEmailSucceeds("Should match email domain of length: " +
766                domain.length(), email);
767
768        email = email + "m";
769        assertAddLinksWithEmailFails("Should not match email domain of length:" +
770                domain.length(), email);
771    }
772
773    // Utility functions
774    private static void assertAddLinksWithWebUrlSucceeds(String msg, String url) {
775        assertAddLinksSucceeds(msg, url, Linkify.WEB_URLS);
776    }
777
778    private static void assertAddLinksWithWebUrlFails(String msg, String url) {
779        assertAddLinksFails(msg, url, Linkify.WEB_URLS);
780    }
781
782    private static void assertAddLinksWithWebUrlPartiallyMatches(String msg, String expected,
783            String url) {
784        assertAddLinksPartiallyMatches(msg, expected, url, Linkify.WEB_URLS);
785    }
786
787    private static void assertAddLinksWithEmailSucceeds(String msg, String url) {
788        assertAddLinksSucceeds(msg, url, Linkify.EMAIL_ADDRESSES);
789    }
790
791    private static void assertAddLinksWithEmailFails(String msg, String url) {
792        assertAddLinksFails(msg, url, Linkify.EMAIL_ADDRESSES);
793    }
794
795    private static void assertAddLinksWithEmailPartiallyMatches(String msg, String expected,
796            String url) {
797        assertAddLinksPartiallyMatches(msg, expected, url, Linkify.EMAIL_ADDRESSES);
798    }
799
800    private static void assertAddLinksSucceeds(String msg, String string, int type) {
801        String str = "start " + string + " end";
802        Spannable spannable = new SpannableString(str);
803
804        boolean linksAdded = LinkifyCompat.addLinks(spannable, type);
805        URLSpan[] spans = spannable.getSpans(0, str.length(), URLSpan.class);
806
807        assertTrue(msg, linksAdded);
808        assertEquals("Span should start from the beginning of: " + string,
809                "start ".length(), spannable.getSpanStart(spans[0]));
810        assertEquals("Span should end at the end of: " + string,
811                str.length() - " end".length(), spannable.getSpanEnd(spans[0]));
812    }
813
814    private static void assertAddLinksFails(String msg, String string, int type) {
815        Spannable spannable = new SpannableString("start " + string + " end");
816        boolean linksAdded = LinkifyCompat.addLinks(spannable, type);
817        assertFalse(msg, linksAdded);
818    }
819
820    private static void assertAddLinksPartiallyMatches(String msg, String expected,
821            String string, int type) {
822        Spannable spannable = new SpannableString("start " + string + " end");
823        boolean linksAdded = LinkifyCompat.addLinks(spannable, type);
824        URLSpan[] spans = spannable.getSpans(0, spannable.length(), URLSpan.class);
825        assertTrue(msg, linksAdded);
826        assertEquals(msg, expected, spans[0].getURL().toString());
827    }
828}