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