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 * '+1 (919) 555-1212' 183 * becomes '+19195551212' 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