2 * Copyright (C) 2007 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package android.text.util;
19 import android.annotation.IntDef;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.content.Context;
23 import android.telephony.PhoneNumberUtils;
24 import android.telephony.TelephonyManager;
25 import android.text.Spannable;
26 import android.text.SpannableString;
27 import android.text.Spanned;
28 import android.text.method.LinkMovementMethod;
29 import android.text.method.MovementMethod;
30 import android.text.style.URLSpan;
31 import android.util.Patterns;
32 import android.webkit.WebView;
33 import android.widget.TextView;
35 import com.android.i18n.phonenumbers.PhoneNumberMatch;
36 import com.android.i18n.phonenumbers.PhoneNumberUtil;
37 import com.android.i18n.phonenumbers.PhoneNumberUtil.Leniency;
39 import libcore.util.EmptyArray;
41 import java.io.UnsupportedEncodingException;
42 import java.lang.annotation.Retention;
43 import java.lang.annotation.RetentionPolicy;
44 import java.net.URLEncoder;
45 import java.util.ArrayList;
46 import java.util.Collections;
47 import java.util.Comparator;
48 import java.util.Locale;
49 import java.util.regex.Matcher;
50 import java.util.regex.Pattern;
53 * Linkify take a piece of text and a regular expression and turns all of the
54 * regex matches in the text into clickable links. This is particularly
55 * useful for matching things like email addresses, web URLs, etc. and making
58 * Alone with the pattern that is to be matched, a URL scheme prefix is also
59 * required. Any pattern match that does not begin with the supplied scheme
60 * will have the scheme prepended to the matched text when the clickable URL
61 * is created. For instance, if you are matching web URLs you would supply
62 * the scheme <code>http://</code>. If the pattern matches example.com, which
63 * does not have a URL scheme prefix, the supplied scheme will be prepended to
64 * create <code>http://example.com</code> when the clickable URL link is
68 public class Linkify {
70 * Bit field indicating that web URLs should be matched in methods that
71 * take an options mask
73 public static final int WEB_URLS = 0x01;
76 * Bit field indicating that email addresses should be matched in methods
77 * that take an options mask
79 public static final int EMAIL_ADDRESSES = 0x02;
82 * Bit field indicating that phone numbers should be matched in methods that
83 * take an options mask
85 public static final int PHONE_NUMBERS = 0x04;
88 * Bit field indicating that street addresses should be matched in methods that
89 * take an options mask. Note that this uses the
90 * {@link android.webkit.WebView#findAddress(String) findAddress()} method in
91 * {@link android.webkit.WebView} for finding addresses, which has various
94 public static final int MAP_ADDRESSES = 0x08;
97 * Bit mask indicating that all available patterns should be matched in
98 * methods that take an options mask
100 public static final int ALL = WEB_URLS | EMAIL_ADDRESSES | PHONE_NUMBERS | MAP_ADDRESSES;
103 * Don't treat anything with fewer than this many digits as a
106 private static final int PHONE_NUMBER_MINIMUM_DIGITS = 5;
109 @IntDef(flag = true, value = { WEB_URLS, EMAIL_ADDRESSES, PHONE_NUMBERS, MAP_ADDRESSES, ALL })
110 @Retention(RetentionPolicy.SOURCE)
111 public @interface LinkifyMask {}
114 * Filters out web URL matches that occur after an at-sign (@). This is
115 * to prevent turning the domain name in an email address into a web link.
117 public static final MatchFilter sUrlMatchFilter = new MatchFilter() {
118 public final boolean acceptMatch(CharSequence s, int start, int end) {
123 if (s.charAt(start - 1) == '@') {
132 * Filters out URL matches that don't have enough digits to be a
135 public static final MatchFilter sPhoneNumberMatchFilter = new MatchFilter() {
136 public final boolean acceptMatch(CharSequence s, int start, int end) {
139 for (int i = start; i < end; i++) {
140 if (Character.isDigit(s.charAt(i))) {
142 if (digitCount >= PHONE_NUMBER_MINIMUM_DIGITS) {
152 * Transforms matched phone number text into something suitable
153 * to be used in a tel: URL. It does this by removing everything
154 * but the digits and plus signs. For instance:
155 * '+1 (919) 555-1212'
156 * becomes '+19195551212'
158 public static final TransformFilter sPhoneNumberTransformFilter = new TransformFilter() {
159 public final String transformUrl(final Matcher match, String url) {
160 return Patterns.digitsAndPlusOnly(match);
165 * MatchFilter enables client code to have more control over
166 * what is allowed to match and become a link, and what is not.
168 * For example: when matching web URLs you would like things like
169 * http://www.example.com to match, as well as just example.com itelf.
170 * However, you would not want to match against the domain in
171 * support@example.com. So, when matching against a web URL pattern you
172 * might also include a MatchFilter that disallows the match if it is
173 * immediately preceded by an at-sign (@).
175 public interface MatchFilter {
177 * Examines the character span matched by the pattern and determines
178 * if the match should be turned into an actionable link.
180 * @param s The body of text against which the pattern
182 * @param start The index of the first character in s that was
183 * matched by the pattern - inclusive
184 * @param end The index of the last character in s that was
185 * matched - exclusive
187 * @return Whether this match should be turned into a link
189 boolean acceptMatch(CharSequence s, int start, int end);
193 * TransformFilter enables client code to have more control over
194 * how matched patterns are represented as URLs.
196 * For example: when converting a phone number such as (919) 555-1212
197 * into a tel: URL the parentheses, white space, and hyphen need to be
198 * removed to produce tel:9195551212.
200 public interface TransformFilter {
202 * Examines the matched text and either passes it through or uses the
203 * data in the Matcher state to produce a replacement.
205 * @param match The regex matcher state that found this URL text
206 * @param url The text that was matched
208 * @return The transformed form of the URL
210 String transformUrl(final Matcher match, String url);
214 * Scans the text of the provided Spannable and turns all occurrences
215 * of the link types indicated in the mask into clickable links.
216 * If the mask is nonzero, it also removes any existing URLSpans
217 * attached to the Spannable, to avoid problems if you call it
218 * repeatedly on the same text.
220 * @param text Spannable whose text is to be marked-up with links
221 * @param mask Mask to define which kinds of links will be searched.
223 * @return True if at least one link is found and applied.
225 public static final boolean addLinks(@NonNull Spannable text, @LinkifyMask int mask) {
226 return addLinks(text, mask, null);
229 private static boolean addLinks(@NonNull Spannable text, @LinkifyMask int mask,
230 @Nullable Context context) {
235 URLSpan[] old = text.getSpans(0, text.length(), URLSpan.class);
237 for (int i = old.length - 1; i >= 0; i--) {
238 text.removeSpan(old[i]);
241 ArrayList<LinkSpec> links = new ArrayList<LinkSpec>();
243 if ((mask & WEB_URLS) != 0) {
244 gatherLinks(links, text, Patterns.AUTOLINK_WEB_URL,
245 new String[] { "http://", "https://", "rtsp://" },
246 sUrlMatchFilter, null);
249 if ((mask & EMAIL_ADDRESSES) != 0) {
250 gatherLinks(links, text, Patterns.AUTOLINK_EMAIL_ADDRESS,
251 new String[] { "mailto:" },
255 if ((mask & PHONE_NUMBERS) != 0) {
256 gatherTelLinks(links, text, context);
259 if ((mask & MAP_ADDRESSES) != 0) {
260 gatherMapLinks(links, text);
263 pruneOverlaps(links);
265 if (links.size() == 0) {
269 for (LinkSpec link: links) {
270 applyLink(link.url, link.start, link.end, text);
277 * Scans the text of the provided TextView and turns all occurrences of
278 * the link types indicated in the mask into clickable links. If matches
279 * are found the movement method for the TextView is set to
280 * LinkMovementMethod.
282 * @param text TextView whose text is to be marked-up with links
283 * @param mask Mask to define which kinds of links will be searched.
285 * @return True if at least one link is found and applied.
287 public static final boolean addLinks(@NonNull TextView text, @LinkifyMask int mask) {
292 final Context context = text.getContext();
293 final CharSequence t = text.getText();
294 if (t instanceof Spannable) {
295 if (addLinks((Spannable) t, mask, context)) {
296 addLinkMovementMethod(text);
302 SpannableString s = SpannableString.valueOf(t);
304 if (addLinks(s, mask, context)) {
305 addLinkMovementMethod(text);
315 private static final void addLinkMovementMethod(@NonNull TextView t) {
316 MovementMethod m = t.getMovementMethod();
318 if ((m == null) || !(m instanceof LinkMovementMethod)) {
319 if (t.getLinksClickable()) {
320 t.setMovementMethod(LinkMovementMethod.getInstance());
326 * Applies a regex to the text of a TextView turning the matches into
327 * links. If links are found then UrlSpans are applied to the link
328 * text match areas, and the movement method for the text is changed
329 * to LinkMovementMethod.
331 * @param text TextView whose text is to be marked-up with links
332 * @param pattern Regex pattern to be used for finding links
333 * @param scheme URL scheme string (eg <code>http://</code>) to be
334 * prepended to the links that do not start with this scheme.
336 public static final void addLinks(@NonNull TextView text, @NonNull Pattern pattern,
337 @Nullable String scheme) {
338 addLinks(text, pattern, scheme, null, null, null);
342 * Applies a regex to the text of a TextView turning the matches into
343 * links. If links are found then UrlSpans are applied to the link
344 * text match areas, and the movement method for the text is changed
345 * to LinkMovementMethod.
347 * @param text TextView whose text is to be marked-up with links
348 * @param pattern Regex pattern to be used for finding links
349 * @param scheme URL scheme string (eg <code>http://</code>) to be
350 * prepended to the links that do not start with this scheme.
351 * @param matchFilter The filter that is used to allow the client code
352 * additional control over which pattern matches are
353 * to be converted into links.
355 public static final void addLinks(@NonNull TextView text, @NonNull Pattern pattern,
356 @Nullable String scheme, @Nullable MatchFilter matchFilter,
357 @Nullable TransformFilter transformFilter) {
358 addLinks(text, pattern, scheme, null, matchFilter, transformFilter);
362 * Applies a regex to the text of a TextView turning the matches into
363 * links. If links are found then UrlSpans are applied to the link
364 * text match areas, and the movement method for the text is changed
365 * to LinkMovementMethod.
367 * @param text TextView whose text is to be marked-up with links.
368 * @param pattern Regex pattern to be used for finding links.
369 * @param defaultScheme The default scheme to be prepended to links if the link does not
370 * start with one of the <code>schemes</code> given.
371 * @param schemes Array of schemes (eg <code>http://</code>) to check if the link found
372 * contains a scheme. Passing a null or empty value means prepend defaultScheme
374 * @param matchFilter The filter that is used to allow the client code additional control
375 * over which pattern matches are to be converted into links.
376 * @param transformFilter Filter to allow the client code to update the link found.
378 public static final void addLinks(@NonNull TextView text, @NonNull Pattern pattern,
379 @Nullable String defaultScheme, @Nullable String[] schemes,
380 @Nullable MatchFilter matchFilter, @Nullable TransformFilter transformFilter) {
381 SpannableString spannable = SpannableString.valueOf(text.getText());
383 boolean linksAdded = addLinks(spannable, pattern, defaultScheme, schemes, matchFilter,
386 text.setText(spannable);
387 addLinkMovementMethod(text);
392 * Applies a regex to a Spannable turning the matches into
395 * @param text Spannable whose text is to be marked-up with links
396 * @param pattern Regex pattern to be used for finding links
397 * @param scheme URL scheme string (eg <code>http://</code>) to be
398 * prepended to the links that do not start with this scheme.
400 public static final boolean addLinks(@NonNull Spannable text, @NonNull Pattern pattern,
401 @Nullable String scheme) {
402 return addLinks(text, pattern, scheme, null, null, null);
406 * Applies a regex to a Spannable turning the matches into
409 * @param spannable Spannable whose text is to be marked-up with links
410 * @param pattern Regex pattern to be used for finding links
411 * @param scheme URL scheme string (eg <code>http://</code>) to be
412 * prepended to the links that do not start with this scheme.
413 * @param matchFilter The filter that is used to allow the client code
414 * additional control over which pattern matches are
415 * to be converted into links.
416 * @param transformFilter Filter to allow the client code to update the link found.
418 * @return True if at least one link is found and applied.
420 public static final boolean addLinks(@NonNull Spannable spannable, @NonNull Pattern pattern,
421 @Nullable String scheme, @Nullable MatchFilter matchFilter,
422 @Nullable TransformFilter transformFilter) {
423 return addLinks(spannable, pattern, scheme, null, matchFilter,
428 * Applies a regex to a Spannable turning the matches into links.
430 * @param spannable Spannable whose text is to be marked-up with links.
431 * @param pattern Regex pattern to be used for finding links.
432 * @param defaultScheme The default scheme to be prepended to links if the link does not
433 * start with one of the <code>schemes</code> given.
434 * @param schemes Array of schemes (eg <code>http://</code>) to check if the link found
435 * contains a scheme. Passing a null or empty value means prepend defaultScheme
437 * @param matchFilter The filter that is used to allow the client code additional control
438 * over which pattern matches are to be converted into links.
439 * @param transformFilter Filter to allow the client code to update the link found.
441 * @return True if at least one link is found and applied.
443 public static final boolean addLinks(@NonNull Spannable spannable, @NonNull Pattern pattern,
444 @Nullable String defaultScheme, @Nullable String[] schemes,
445 @Nullable MatchFilter matchFilter, @Nullable TransformFilter transformFilter) {
446 final String[] schemesCopy;
447 if (defaultScheme == null) defaultScheme = "";
448 if (schemes == null || schemes.length < 1) {
449 schemes = EmptyArray.STRING;
452 schemesCopy = new String[schemes.length + 1];
453 schemesCopy[0] = defaultScheme.toLowerCase(Locale.ROOT);
454 for (int index = 0; index < schemes.length; index++) {
455 String scheme = schemes[index];
456 schemesCopy[index + 1] = (scheme == null) ? "" : scheme.toLowerCase(Locale.ROOT);
459 boolean hasMatches = false;
460 Matcher m = pattern.matcher(spannable);
463 int start = m.start();
465 boolean allowed = true;
467 if (matchFilter != null) {
468 allowed = matchFilter.acceptMatch(spannable, start, end);
472 String url = makeUrl(m.group(0), schemesCopy, m, transformFilter);
474 applyLink(url, start, end, spannable);
482 private static final void applyLink(String url, int start, int end, Spannable text) {
483 URLSpan span = new URLSpan(url);
485 text.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
488 private static final String makeUrl(@NonNull String url, @NonNull String[] prefixes,
489 Matcher matcher, @Nullable TransformFilter filter) {
490 if (filter != null) {
491 url = filter.transformUrl(matcher, url);
494 boolean hasPrefix = false;
496 for (int i = 0; i < prefixes.length; i++) {
497 if (url.regionMatches(true, 0, prefixes[i], 0, prefixes[i].length())) {
500 // Fix capitalization if necessary
501 if (!url.regionMatches(false, 0, prefixes[i], 0, prefixes[i].length())) {
502 url = prefixes[i] + url.substring(prefixes[i].length());
509 if (!hasPrefix && prefixes.length > 0) {
510 url = prefixes[0] + url;
516 private static final void gatherLinks(ArrayList<LinkSpec> links,
517 Spannable s, Pattern pattern, String[] schemes,
518 MatchFilter matchFilter, TransformFilter transformFilter) {
519 Matcher m = pattern.matcher(s);
522 int start = m.start();
525 if (matchFilter == null || matchFilter.acceptMatch(s, start, end)) {
526 LinkSpec spec = new LinkSpec();
527 String url = makeUrl(m.group(0), schemes, m, transformFilter);
538 private static void gatherTelLinks(ArrayList<LinkSpec> links, Spannable s,
539 @Nullable Context context) {
540 PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();
541 final TelephonyManager tm = (context == null)
542 ? TelephonyManager.getDefault()
543 : TelephonyManager.from(context);
544 Iterable<PhoneNumberMatch> matches = phoneUtil.findNumbers(s.toString(),
545 tm.getSimCountryIso().toUpperCase(Locale.US),
546 Leniency.POSSIBLE, Long.MAX_VALUE);
547 for (PhoneNumberMatch match : matches) {
548 LinkSpec spec = new LinkSpec();
549 spec.url = "tel:" + PhoneNumberUtils.normalizeNumber(match.rawString());
550 spec.start = match.start();
551 spec.end = match.end();
556 private static final void gatherMapLinks(ArrayList<LinkSpec> links, Spannable s) {
557 String string = s.toString();
562 while ((address = WebView.findAddress(string)) != null) {
563 int start = string.indexOf(address);
569 LinkSpec spec = new LinkSpec();
570 int length = address.length();
571 int end = start + length;
573 spec.start = base + start;
574 spec.end = base + end;
575 string = string.substring(end);
578 String encodedAddress = null;
581 encodedAddress = URLEncoder.encode(address,"UTF-8");
582 } catch (UnsupportedEncodingException e) {
586 spec.url = "geo:0,0?q=" + encodedAddress;
589 } catch (UnsupportedOperationException e) {
590 // findAddress may fail with an unsupported exception on platforms without a WebView.
591 // In this case, we will not append anything to the links variable: it would have died
592 // in WebView.findAddress.
597 private static final void pruneOverlaps(ArrayList<LinkSpec> links) {
598 Comparator<LinkSpec> c = new Comparator<LinkSpec>() {
599 public final int compare(LinkSpec a, LinkSpec b) {
600 if (a.start < b.start) {
604 if (a.start > b.start) {
620 Collections.sort(links, c);
622 int len = links.size();
625 while (i < len - 1) {
626 LinkSpec a = links.get(i);
627 LinkSpec b = links.get(i + 1);
630 if ((a.start <= b.start) && (a.end > b.start)) {
631 if (b.end <= a.end) {
633 } else if ((a.end - a.start) > (b.end - b.start)) {
635 } else if ((a.end - a.start) < (b.end - b.start)) {
640 links.remove(remove);