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