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