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