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