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