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