1 /*
2  * Copyright (C) 2007 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.util;
18 
19 import android.annotation.IntDef;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.app.ActivityThread;
23 import android.compat.annotation.UnsupportedAppUsage;
24 import android.content.Context;
25 import android.telephony.PhoneNumberUtils;
26 import android.telephony.TelephonyManager;
27 import android.text.Spannable;
28 import android.text.SpannableString;
29 import android.text.Spanned;
30 import android.text.method.LinkMovementMethod;
31 import android.text.method.MovementMethod;
32 import android.text.style.URLSpan;
33 import android.util.Log;
34 import android.util.Patterns;
35 import android.webkit.WebView;
36 import android.widget.TextView;
37 
38 import com.android.i18n.phonenumbers.PhoneNumberMatch;
39 import com.android.i18n.phonenumbers.PhoneNumberUtil;
40 import com.android.i18n.phonenumbers.PhoneNumberUtil.Leniency;
41 
42 import libcore.util.EmptyArray;
43 
44 import java.io.UnsupportedEncodingException;
45 import java.lang.annotation.Retention;
46 import java.lang.annotation.RetentionPolicy;
47 import java.net.URLEncoder;
48 import java.util.ArrayList;
49 import java.util.Collections;
50 import java.util.Comparator;
51 import java.util.Locale;
52 import java.util.function.Function;
53 import java.util.regex.Matcher;
54 import java.util.regex.Pattern;
55 
56 /**
57  *  Linkify take a piece of text and a regular expression and turns all of the
58  *  regex matches in the text into clickable links.  This is particularly
59  *  useful for matching things like email addresses, web URLs, etc. and making
60  *  them actionable.
61  *
62  *  Alone with the pattern that is to be matched, a URL scheme prefix is also
63  *  required.  Any pattern match that does not begin with the supplied scheme
64  *  will have the scheme prepended to the matched text when the clickable URL
65  *  is created.  For instance, if you are matching web URLs you would supply
66  *  the scheme <code>http://</code>. If the pattern matches example.com, which
67  *  does not have a URL scheme prefix, the supplied scheme will be prepended to
68  *  create <code>http://example.com</code> when the clickable URL link is
69  *  created.
70  *
71  *  <p class="note"><b>Note:</b> When using {@link #MAP_ADDRESSES} or {@link #ALL}
72  *  to match street addresses on API level {@link android.os.Build.VERSION_CODES#O_MR1}
73  *  and earlier, methods in this class may throw
74  *  {@link android.util.AndroidRuntimeException} or other exceptions if the
75  *  device's WebView implementation is currently being updated, because
76  *  {@link android.webkit.WebView#findAddress} is required to match street
77  *  addresses.
78  *
79  * @see MatchFilter
80  * @see TransformFilter
81  */
82 
83 public class Linkify {
84 
85     private static final String LOG_TAG = "Linkify";
86 
87     /**
88      *  Bit field indicating that web URLs should be matched in methods that
89      *  take an options mask
90      */
91     public static final int WEB_URLS = 0x01;
92 
93     /**
94      *  Bit field indicating that email addresses should be matched in methods
95      *  that take an options mask
96      */
97     public static final int EMAIL_ADDRESSES = 0x02;
98 
99     /**
100      *  Bit field indicating that phone numbers should be matched in methods that
101      *  take an options mask
102      */
103     public static final int PHONE_NUMBERS = 0x04;
104 
105     /**
106      *  Bit field indicating that street addresses should be matched in methods that
107      *  take an options mask. Note that this should be avoided, as it uses the
108      *  {@link android.webkit.WebView#findAddress(String)} method, which has various
109      *  limitations and has been deprecated: see the documentation for
110      *  {@link android.webkit.WebView#findAddress(String)} for more information.
111      *
112      *  @deprecated use {@link android.view.textclassifier.TextClassifier#generateLinks(
113      *  TextLinks.Request)} instead and avoid it even when targeting API levels where no alternative
114      *  is available.
115      */
116     @Deprecated
117     public static final int MAP_ADDRESSES = 0x08;
118 
119     /**
120      *  Bit mask indicating that all available patterns should be matched in
121      *  methods that take an options mask
122      *  <p><strong>Note:</strong></p> {@link #MAP_ADDRESSES} is deprecated.
123      *  Use {@link android.view.textclassifier.TextClassifier#generateLinks(TextLinks.Request)}
124      *  instead and avoid it even when targeting API levels where no alternative is available.
125      */
126     public static final int ALL = WEB_URLS | EMAIL_ADDRESSES | PHONE_NUMBERS | MAP_ADDRESSES;
127 
128     /**
129      * Don't treat anything with fewer than this many digits as a
130      * phone number.
131      */
132     private static final int PHONE_NUMBER_MINIMUM_DIGITS = 5;
133 
134     /** @hide */
135     @IntDef(flag = true, value = { WEB_URLS, EMAIL_ADDRESSES, PHONE_NUMBERS, MAP_ADDRESSES, ALL })
136     @Retention(RetentionPolicy.SOURCE)
137     public @interface LinkifyMask {}
138 
139     /**
140      *  Filters out web URL matches that occur after an at-sign (@).  This is
141      *  to prevent turning the domain name in an email address into a web link.
142      */
143     public static final MatchFilter sUrlMatchFilter = new MatchFilter() {
144         public final boolean acceptMatch(CharSequence s, int start, int end) {
145             if (start == 0) {
146                 return true;
147             }
148 
149             if (s.charAt(start - 1) == '@') {
150                 return false;
151             }
152 
153             return true;
154         }
155     };
156 
157     /**
158      *  Filters out URL matches that don't have enough digits to be a
159      *  phone number.
160      */
161     public static final MatchFilter sPhoneNumberMatchFilter = new MatchFilter() {
162         public final boolean acceptMatch(CharSequence s, int start, int end) {
163             int digitCount = 0;
164 
165             for (int i = start; i < end; i++) {
166                 if (Character.isDigit(s.charAt(i))) {
167                     digitCount++;
168                     if (digitCount >= PHONE_NUMBER_MINIMUM_DIGITS) {
169                         return true;
170                     }
171                 }
172             }
173             return false;
174         }
175     };
176 
177     /**
178      *  Transforms matched phone number text into something suitable
179      *  to be used in a tel: URL.  It does this by removing everything
180      *  but the digits and plus signs.  For instance:
181      *  &apos;+1 (919) 555-1212&apos;
182      *  becomes &apos;+19195551212&apos;
183      */
184     public static final TransformFilter sPhoneNumberTransformFilter = new TransformFilter() {
185         public final String transformUrl(final Matcher match, String url) {
186             return Patterns.digitsAndPlusOnly(match);
187         }
188     };
189 
190     /**
191      *  MatchFilter enables client code to have more control over
192      *  what is allowed to match and become a link, and what is not.
193      *
194      *  For example:  when matching web URLs you would like things like
195      *  http://www.example.com to match, as well as just example.com itelf.
196      *  However, you would not want to match against the domain in
197      *  support@example.com.  So, when matching against a web URL pattern you
198      *  might also include a MatchFilter that disallows the match if it is
199      *  immediately preceded by an at-sign (@).
200      */
201     public interface MatchFilter {
202         /**
203          *  Examines the character span matched by the pattern and determines
204          *  if the match should be turned into an actionable link.
205          *
206          *  @param s        The body of text against which the pattern
207          *                  was matched
208          *  @param start    The index of the first character in s that was
209          *                  matched by the pattern - inclusive
210          *  @param end      The index of the last character in s that was
211          *                  matched - exclusive
212          *
213          *  @return         Whether this match should be turned into a link
214          */
acceptMatch(CharSequence s, int start, int end)215         boolean acceptMatch(CharSequence s, int start, int end);
216     }
217 
218     /**
219      *  TransformFilter enables client code to have more control over
220      *  how matched patterns are represented as URLs.
221      *
222      *  For example:  when converting a phone number such as (919)  555-1212
223      *  into a tel: URL the parentheses, white space, and hyphen need to be
224      *  removed to produce tel:9195551212.
225      */
226     public interface TransformFilter {
227         /**
228          *  Examines the matched text and either passes it through or uses the
229          *  data in the Matcher state to produce a replacement.
230          *
231          *  @param match    The regex matcher state that found this URL text
232          *  @param url      The text that was matched
233          *
234          *  @return         The transformed form of the URL
235          */
transformUrl(final Matcher match, String url)236         String transformUrl(final Matcher match, String url);
237     }
238 
239     /**
240      *  Scans the text of the provided Spannable and turns all occurrences
241      *  of the link types indicated in the mask into clickable links.
242      *  If the mask is nonzero, it also removes any existing URLSpans
243      *  attached to the Spannable, to avoid problems if you call it
244      *  repeatedly on the same text.
245      *
246      *  @param text Spannable whose text is to be marked-up with links
247      *  @param mask Mask to define which kinds of links will be searched.
248      *
249      *  @return True if at least one link is found and applied.
250      *
251      * @see #addLinks(Spannable, int, Function)
252      */
addLinks(@onNull Spannable text, @LinkifyMask int mask)253     public static final boolean addLinks(@NonNull Spannable text, @LinkifyMask int mask) {
254         return addLinks(text, mask, null, null);
255     }
256 
257     /**
258      *  Scans the text of the provided Spannable and turns all occurrences
259      *  of the link types indicated in the mask into clickable links.
260      *  If the mask is nonzero, it also removes any existing URLSpans
261      *  attached to the Spannable, to avoid problems if you call it
262      *  repeatedly on the same text.
263      *
264      *  @param text Spannable whose text is to be marked-up with links
265      *  @param mask mask to define which kinds of links will be searched
266      *  @param urlSpanFactory function used to create {@link URLSpan}s
267      *  @return True if at least one link is found and applied.
268      */
addLinks(@onNull Spannable text, @LinkifyMask int mask, @Nullable Function<String, URLSpan> urlSpanFactory)269     public static final boolean addLinks(@NonNull Spannable text, @LinkifyMask int mask,
270             @Nullable Function<String, URLSpan> urlSpanFactory) {
271         return addLinks(text, mask, null, urlSpanFactory);
272     }
273 
274     /**
275      *  Scans the text of the provided Spannable and turns all occurrences of the link types
276      *  indicated in the mask into clickable links. If the mask is nonzero, it also removes any
277      *  existing URLSpans attached to the Spannable, to avoid problems if you call it repeatedly
278      *  on the same text.
279      *
280      * @param text Spannable whose text is to be marked-up with links
281      * @param mask mask to define which kinds of links will be searched
282      * @param context Context to be used while identifying phone numbers
283      * @param urlSpanFactory function used to create {@link URLSpan}s
284      * @return true if at least one link is found and applied.
285      */
addLinks(@onNull Spannable text, @LinkifyMask int mask, @Nullable Context context, @Nullable Function<String, URLSpan> urlSpanFactory)286     private static boolean addLinks(@NonNull Spannable text, @LinkifyMask int mask,
287             @Nullable Context context, @Nullable Function<String, URLSpan> urlSpanFactory) {
288         if (text != null && containsUnsupportedCharacters(text.toString())) {
289             android.util.EventLog.writeEvent(0x534e4554, "116321860", -1, "");
290             return false;
291         }
292 
293         if (mask == 0) {
294             return false;
295         }
296 
297         final URLSpan[] old = text.getSpans(0, text.length(), URLSpan.class);
298 
299         for (int i = old.length - 1; i >= 0; i--) {
300             text.removeSpan(old[i]);
301         }
302 
303         final ArrayList<LinkSpec> links = new ArrayList<LinkSpec>();
304 
305         if ((mask & WEB_URLS) != 0) {
306             gatherLinks(links, text, Patterns.AUTOLINK_WEB_URL,
307                 new String[] { "http://", "https://", "rtsp://" },
308                 sUrlMatchFilter, null);
309         }
310 
311         if ((mask & EMAIL_ADDRESSES) != 0) {
312             gatherLinks(links, text, Patterns.AUTOLINK_EMAIL_ADDRESS,
313                 new String[] { "mailto:" },
314                 null, null);
315         }
316 
317         if ((mask & PHONE_NUMBERS) != 0) {
318             gatherTelLinks(links, text, context);
319         }
320 
321         if ((mask & MAP_ADDRESSES) != 0) {
322             gatherMapLinks(links, text);
323         }
324 
325         pruneOverlaps(links);
326 
327         if (links.size() == 0) {
328             return false;
329         }
330 
331         for (LinkSpec link: links) {
332             applyLink(link.url, link.start, link.end, text, urlSpanFactory);
333         }
334 
335         return true;
336     }
337 
338     /**
339      * Returns true if the specified text contains at least one unsupported character for applying
340      * links. Also logs the error.
341      *
342      * @param text the text to apply links to
343      * @hide
344      */
containsUnsupportedCharacters(String text)345     public static boolean containsUnsupportedCharacters(String text) {
346         if (text.contains("\u202C")) {
347             Log.e(LOG_TAG, "Unsupported character for applying links: u202C");
348             return true;
349         }
350         if (text.contains("\u202D")) {
351             Log.e(LOG_TAG, "Unsupported character for applying links: u202D");
352             return true;
353         }
354         if (text.contains("\u202E")) {
355             Log.e(LOG_TAG, "Unsupported character for applying links: u202E");
356             return true;
357         }
358         return false;
359     }
360 
361     /**
362      *  Scans the text of the provided TextView and turns all occurrences of
363      *  the link types indicated in the mask into clickable links.  If matches
364      *  are found the movement method for the TextView is set to
365      *  LinkMovementMethod.
366      *
367      *  @param text TextView whose text is to be marked-up with links
368      *  @param mask Mask to define which kinds of links will be searched.
369      *
370      *  @return True if at least one link is found and applied.
371      *
372      *  @see #addLinks(Spannable, int, Function)
373      */
addLinks(@onNull TextView text, @LinkifyMask int mask)374     public static final boolean addLinks(@NonNull TextView text, @LinkifyMask int mask) {
375         if (mask == 0) {
376             return false;
377         }
378 
379         final Context context = text.getContext();
380         final CharSequence t = text.getText();
381         if (t instanceof Spannable) {
382             if (addLinks((Spannable) t, mask, context, null)) {
383                 addLinkMovementMethod(text);
384                 return true;
385             }
386 
387             return false;
388         } else {
389             SpannableString s = SpannableString.valueOf(t);
390 
391             if (addLinks(s, mask, context, null)) {
392                 addLinkMovementMethod(text);
393                 text.setText(s);
394 
395                 return true;
396             }
397 
398             return false;
399         }
400     }
401 
addLinkMovementMethod(@onNull TextView t)402     private static final void addLinkMovementMethod(@NonNull TextView t) {
403         MovementMethod m = t.getMovementMethod();
404 
405         if ((m == null) || !(m instanceof LinkMovementMethod)) {
406             if (t.getLinksClickable()) {
407                 t.setMovementMethod(LinkMovementMethod.getInstance());
408             }
409         }
410     }
411 
412     /**
413      *  Applies a regex to the text of a TextView turning the matches into
414      *  links.  If links are found then UrlSpans are applied to the link
415      *  text match areas, and the movement method for the text is changed
416      *  to LinkMovementMethod.
417      *
418      *  @param text         TextView whose text is to be marked-up with links
419      *  @param pattern      Regex pattern to be used for finding links
420      *  @param scheme       URL scheme string (eg <code>http://</code>) to be
421      *                      prepended to the links that do not start with this scheme.
422      */
addLinks(@onNull TextView text, @NonNull Pattern pattern, @Nullable String scheme)423     public static final void addLinks(@NonNull TextView text, @NonNull Pattern pattern,
424             @Nullable String scheme) {
425         addLinks(text, pattern, scheme, null, null, null);
426     }
427 
428     /**
429      *  Applies a regex to the text of a TextView turning the matches into
430      *  links.  If links are found then UrlSpans are applied to the link
431      *  text match areas, and the movement method for the text is changed
432      *  to LinkMovementMethod.
433      *
434      *  @param text         TextView whose text is to be marked-up with links
435      *  @param pattern      Regex pattern to be used for finding links
436      *  @param scheme       URL scheme string (eg <code>http://</code>) to be
437      *                      prepended to the links that do not start with this scheme.
438      *  @param matchFilter  The filter that is used to allow the client code
439      *                      additional control over which pattern matches are
440      *                      to be converted into links.
441      */
addLinks(@onNull TextView text, @NonNull Pattern pattern, @Nullable String scheme, @Nullable MatchFilter matchFilter, @Nullable TransformFilter transformFilter)442     public static final void addLinks(@NonNull TextView text, @NonNull Pattern pattern,
443             @Nullable String scheme, @Nullable MatchFilter matchFilter,
444             @Nullable TransformFilter transformFilter) {
445         addLinks(text, pattern, scheme, null, matchFilter, transformFilter);
446     }
447 
448     /**
449      *  Applies a regex to the text of a TextView turning the matches into
450      *  links.  If links are found then UrlSpans are applied to the link
451      *  text match areas, and the movement method for the text is changed
452      *  to LinkMovementMethod.
453      *
454      *  @param text TextView whose text is to be marked-up with links.
455      *  @param pattern Regex pattern to be used for finding links.
456      *  @param defaultScheme The default scheme to be prepended to links if the link does not
457      *                       start with one of the <code>schemes</code> given.
458      *  @param schemes Array of schemes (eg <code>http://</code>) to check if the link found
459      *                 contains a scheme. Passing a null or empty value means prepend defaultScheme
460      *                 to all links.
461      *  @param matchFilter  The filter that is used to allow the client code additional control
462      *                      over which pattern matches are to be converted into links.
463      *  @param transformFilter Filter to allow the client code to update the link found.
464      */
addLinks(@onNull TextView text, @NonNull Pattern pattern, @Nullable String defaultScheme, @Nullable String[] schemes, @Nullable MatchFilter matchFilter, @Nullable TransformFilter transformFilter)465     public static final void addLinks(@NonNull TextView text, @NonNull Pattern pattern,
466              @Nullable  String defaultScheme, @Nullable String[] schemes,
467              @Nullable MatchFilter matchFilter, @Nullable TransformFilter transformFilter) {
468         SpannableString spannable = SpannableString.valueOf(text.getText());
469 
470         boolean linksAdded = addLinks(spannable, pattern, defaultScheme, schemes, matchFilter,
471                 transformFilter);
472         if (linksAdded) {
473             text.setText(spannable);
474             addLinkMovementMethod(text);
475         }
476     }
477 
478     /**
479      *  Applies a regex to a Spannable turning the matches into
480      *  links.
481      *
482      *  @param text         Spannable whose text is to be marked-up with links
483      *  @param pattern      Regex pattern to be used for finding links
484      *  @param scheme       URL scheme string (eg <code>http://</code>) to be
485      *                      prepended to the links that do not start with this scheme.
486      * @see #addLinks(Spannable, Pattern, String, String[], MatchFilter, TransformFilter, Function)
487      */
addLinks(@onNull Spannable text, @NonNull Pattern pattern, @Nullable String scheme)488     public static final boolean addLinks(@NonNull Spannable text, @NonNull Pattern pattern,
489             @Nullable String scheme) {
490         return addLinks(text, pattern, scheme, null, null, null);
491     }
492 
493     /**
494      * Applies a regex to a Spannable turning the matches into
495      * links.
496      *
497      * @param spannable    Spannable whose text is to be marked-up with links
498      * @param pattern      Regex pattern to be used for finding links
499      * @param scheme       URL scheme string (eg <code>http://</code>) to be
500      *                     prepended to the links that do not start with this scheme.
501      * @param matchFilter  The filter that is used to allow the client code
502      *                     additional control over which pattern matches are
503      *                     to be converted into links.
504      * @param transformFilter Filter to allow the client code to update the link found.
505      *
506      * @return True if at least one link is found and applied.
507      * @see #addLinks(Spannable, Pattern, String, String[], MatchFilter, TransformFilter, Function)
508      */
addLinks(@onNull Spannable spannable, @NonNull Pattern pattern, @Nullable String scheme, @Nullable MatchFilter matchFilter, @Nullable TransformFilter transformFilter)509     public static final boolean addLinks(@NonNull Spannable spannable, @NonNull Pattern pattern,
510             @Nullable String scheme, @Nullable MatchFilter matchFilter,
511             @Nullable TransformFilter transformFilter) {
512         return addLinks(spannable, pattern, scheme, null, matchFilter,
513                 transformFilter);
514     }
515 
516     /**
517      * Applies a regex to a Spannable turning the matches into links.
518      *
519      * @param spannable Spannable whose text is to be marked-up with links.
520      * @param pattern Regex pattern to be used for finding links.
521      * @param defaultScheme The default scheme to be prepended to links if the link does not
522      *                      start with one of the <code>schemes</code> given.
523      * @param schemes Array of schemes (eg <code>http://</code>) to check if the link found
524      *                contains a scheme. Passing a null or empty value means prepend defaultScheme
525      *                to all links.
526      * @param matchFilter  The filter that is used to allow the client code additional control
527      *                     over which pattern matches are to be converted into links.
528      * @param transformFilter Filter to allow the client code to update the link found.
529      *
530      * @return True if at least one link is found and applied.
531      *
532      * @see #addLinks(Spannable, Pattern, String, String[], MatchFilter, TransformFilter, Function)
533      */
addLinks(@onNull Spannable spannable, @NonNull Pattern pattern, @Nullable String defaultScheme, @Nullable String[] schemes, @Nullable MatchFilter matchFilter, @Nullable TransformFilter transformFilter)534     public static final boolean addLinks(@NonNull Spannable spannable, @NonNull Pattern pattern,
535             @Nullable String defaultScheme, @Nullable String[] schemes,
536             @Nullable MatchFilter matchFilter, @Nullable TransformFilter transformFilter) {
537         return addLinks(spannable, pattern, defaultScheme, schemes, matchFilter, transformFilter,
538                 null);
539     }
540 
541     /**
542      * Applies a regex to a Spannable turning the matches into links.
543      *
544      * @param spannable       spannable whose text is to be marked-up with links.
545      * @param pattern         regex pattern to be used for finding links.
546      * @param defaultScheme   the default scheme to be prepended to links if the link does not
547      *                        start with one of the <code>schemes</code> given.
548      * @param schemes         array of schemes (eg <code>http://</code>) to check if the link found
549      *                        contains a scheme. Passing a null or empty value means prepend
550      *                        defaultScheme
551      *                        to all links.
552      * @param matchFilter     the filter that is used to allow the client code additional control
553      *                        over which pattern matches are to be converted into links.
554      * @param transformFilter filter to allow the client code to update the link found.
555      * @param urlSpanFactory  function used to create {@link URLSpan}s
556      *
557      * @return True if at least one link is found and applied.
558      */
addLinks(@onNull Spannable spannable, @NonNull Pattern pattern, @Nullable String defaultScheme, @Nullable String[] schemes, @Nullable MatchFilter matchFilter, @Nullable TransformFilter transformFilter, @Nullable Function<String, URLSpan> urlSpanFactory)559     public static final boolean addLinks(@NonNull Spannable spannable, @NonNull Pattern pattern,
560             @Nullable String defaultScheme, @Nullable String[] schemes,
561             @Nullable MatchFilter matchFilter, @Nullable TransformFilter transformFilter,
562             @Nullable Function<String, URLSpan> urlSpanFactory) {
563         if (spannable != null && containsUnsupportedCharacters(spannable.toString())) {
564             android.util.EventLog.writeEvent(0x534e4554, "116321860", -1, "");
565             return false;
566         }
567 
568         final String[] schemesCopy;
569         if (defaultScheme == null) defaultScheme = "";
570         if (schemes == null || schemes.length < 1) {
571             schemes = EmptyArray.STRING;
572         }
573 
574         schemesCopy = new String[schemes.length + 1];
575         schemesCopy[0] = defaultScheme.toLowerCase(Locale.ROOT);
576         for (int index = 0; index < schemes.length; index++) {
577             String scheme = schemes[index];
578             schemesCopy[index + 1] = (scheme == null) ? "" : scheme.toLowerCase(Locale.ROOT);
579         }
580 
581         boolean hasMatches = false;
582         Matcher m = pattern.matcher(spannable);
583 
584         while (m.find()) {
585             int start = m.start();
586             int end = m.end();
587             boolean allowed = true;
588 
589             if (matchFilter != null) {
590                 allowed = matchFilter.acceptMatch(spannable, start, end);
591             }
592 
593             if (allowed) {
594                 String url = makeUrl(m.group(0), schemesCopy, m, transformFilter);
595 
596                 applyLink(url, start, end, spannable, urlSpanFactory);
597                 hasMatches = true;
598             }
599         }
600 
601         return hasMatches;
602     }
603 
applyLink(String url, int start, int end, Spannable text, @Nullable Function<String, URLSpan> urlSpanFactory)604     private static void applyLink(String url, int start, int end, Spannable text,
605             @Nullable Function<String, URLSpan> urlSpanFactory) {
606         if (urlSpanFactory == null) {
607             urlSpanFactory = DEFAULT_SPAN_FACTORY;
608         }
609         final URLSpan span = urlSpanFactory.apply(url);
610         text.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
611     }
612 
makeUrl(@onNull String url, @NonNull String[] prefixes, Matcher matcher, @Nullable TransformFilter filter)613     private static final String makeUrl(@NonNull String url, @NonNull String[] prefixes,
614             Matcher matcher, @Nullable TransformFilter filter) {
615         if (filter != null) {
616             url = filter.transformUrl(matcher, url);
617         }
618 
619         boolean hasPrefix = false;
620 
621         for (int i = 0; i < prefixes.length; i++) {
622             if (url.regionMatches(true, 0, prefixes[i], 0, prefixes[i].length())) {
623                 hasPrefix = true;
624 
625                 // Fix capitalization if necessary
626                 if (!url.regionMatches(false, 0, prefixes[i], 0, prefixes[i].length())) {
627                     url = prefixes[i] + url.substring(prefixes[i].length());
628                 }
629 
630                 break;
631             }
632         }
633 
634         if (!hasPrefix && prefixes.length > 0) {
635             url = prefixes[0] + url;
636         }
637 
638         return url;
639     }
640 
gatherLinks(ArrayList<LinkSpec> links, Spannable s, Pattern pattern, String[] schemes, MatchFilter matchFilter, TransformFilter transformFilter)641     private static final void gatherLinks(ArrayList<LinkSpec> links,
642             Spannable s, Pattern pattern, String[] schemes,
643             MatchFilter matchFilter, TransformFilter transformFilter) {
644         Matcher m = pattern.matcher(s);
645 
646         while (m.find()) {
647             int start = m.start();
648             int end = m.end();
649 
650             if (matchFilter == null || matchFilter.acceptMatch(s, start, end)) {
651                 LinkSpec spec = new LinkSpec();
652                 String url = makeUrl(m.group(0), schemes, m, transformFilter);
653 
654                 spec.url = url;
655                 spec.start = start;
656                 spec.end = end;
657 
658                 links.add(spec);
659             }
660         }
661     }
662 
663     @UnsupportedAppUsage
gatherTelLinks(ArrayList<LinkSpec> links, Spannable s, @Nullable Context context)664     private static void gatherTelLinks(ArrayList<LinkSpec> links, Spannable s,
665             @Nullable Context context) {
666         PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();
667         final Context ctx = (context != null) ? context : ActivityThread.currentApplication();
668         final String regionCode = (ctx != null) ? ctx.getSystemService(TelephonyManager.class).
669                 getSimCountryIso().toUpperCase(Locale.US) : Locale.getDefault().getCountry();
670         Iterable<PhoneNumberMatch> matches = phoneUtil.findNumbers(s.toString(),
671                 regionCode, Leniency.POSSIBLE, Long.MAX_VALUE);
672         for (PhoneNumberMatch match : matches) {
673             LinkSpec spec = new LinkSpec();
674             spec.url = "tel:" + PhoneNumberUtils.normalizeNumber(match.rawString());
675             spec.start = match.start();
676             spec.end = match.end();
677             links.add(spec);
678         }
679     }
680 
gatherMapLinks(ArrayList<LinkSpec> links, Spannable s)681     private static final void gatherMapLinks(ArrayList<LinkSpec> links, Spannable s) {
682         String string = s.toString();
683         String address;
684         int base = 0;
685 
686         try {
687             while ((address = WebView.findAddress(string)) != null) {
688                 int start = string.indexOf(address);
689 
690                 if (start < 0) {
691                     break;
692                 }
693 
694                 LinkSpec spec = new LinkSpec();
695                 int length = address.length();
696                 int end = start + length;
697 
698                 spec.start = base + start;
699                 spec.end = base + end;
700                 string = string.substring(end);
701                 base += end;
702 
703                 String encodedAddress = null;
704 
705                 try {
706                     encodedAddress = URLEncoder.encode(address,"UTF-8");
707                 } catch (UnsupportedEncodingException e) {
708                     continue;
709                 }
710 
711                 spec.url = "geo:0,0?q=" + encodedAddress;
712                 links.add(spec);
713             }
714         } catch (UnsupportedOperationException e) {
715             // findAddress may fail with an unsupported exception on platforms without a WebView.
716             // In this case, we will not append anything to the links variable: it would have died
717             // in WebView.findAddress.
718             return;
719         }
720     }
721 
pruneOverlaps(ArrayList<LinkSpec> links)722     private static final void pruneOverlaps(ArrayList<LinkSpec> links) {
723         Comparator<LinkSpec>  c = new Comparator<LinkSpec>() {
724             public final int compare(LinkSpec a, LinkSpec b) {
725                 if (a.start < b.start) {
726                     return -1;
727                 }
728 
729                 if (a.start > b.start) {
730                     return 1;
731                 }
732 
733                 if (a.end < b.end) {
734                     return 1;
735                 }
736 
737                 if (a.end > b.end) {
738                     return -1;
739                 }
740 
741                 return 0;
742             }
743         };
744 
745         Collections.sort(links, c);
746 
747         int len = links.size();
748         int i = 0;
749 
750         while (i < len - 1) {
751             LinkSpec a = links.get(i);
752             LinkSpec b = links.get(i + 1);
753             int remove = -1;
754 
755             if ((a.start <= b.start) && (a.end > b.start)) {
756                 if (b.end <= a.end) {
757                     remove = i + 1;
758                 } else if ((a.end - a.start) > (b.end - b.start)) {
759                     remove = i + 1;
760                 } else if ((a.end - a.start) < (b.end - b.start)) {
761                     remove = i;
762                 }
763 
764                 if (remove != -1) {
765                     links.remove(remove);
766                     len--;
767                     continue;
768                 }
769 
770             }
771 
772             i++;
773         }
774     }
775 
776     /**
777      * Default factory function to create {@link URLSpan}s. While adding spans to a
778      * {@link Spannable}, {@link Linkify} will call this function to create a {@link URLSpan}.
779      */
780     private static final Function<String, URLSpan> DEFAULT_SPAN_FACTORY =
781             (String string) -> new URLSpan(string);
782 }
783 
784 class LinkSpec {
785     String url;
786     int start;
787     int end;
788 }
789