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.text.method.LinkMovementMethod;
20 import android.text.method.MovementMethod;
21 import android.text.style.URLSpan;
22 import android.text.Spannable;
23 import android.text.SpannableString;
24 import android.text.Spanned;
25 import android.util.Patterns;
26 import android.webkit.WebView;
27 import android.widget.TextView;
30 import java.io.UnsupportedEncodingException;
31 import java.net.URLEncoder;
32 import java.util.ArrayList;
33 import java.util.Collections;
34 import java.util.Comparator;
35 import java.util.regex.Matcher;
36 import java.util.regex.Pattern;
39 * Linkify take a piece of text and a regular expression and turns all of the
40 * regex matches in the text into clickable links. This is particularly
41 * useful for matching things like email addresses, web urls, etc. and making
44 * Alone with the pattern that is to be matched, a url scheme prefix is also
45 * required. Any pattern match that does not begin with the supplied scheme
46 * will have the scheme prepended to the matched text when the clickable url
47 * is created. For instance, if you are matching web urls you would supply
48 * the scheme <code>http://</code>. If the pattern matches example.com, which
49 * does not have a url scheme prefix, the supplied scheme will be prepended to
50 * create <code>http://example.com</code> when the clickable url link is
54 public class Linkify {
56 * Bit field indicating that web URLs should be matched in methods that
57 * take an options mask
59 public static final int WEB_URLS = 0x01;
62 * Bit field indicating that email addresses should be matched in methods
63 * that take an options mask
65 public static final int EMAIL_ADDRESSES = 0x02;
68 * Bit field indicating that phone numbers should be matched in methods that
69 * take an options mask
71 public static final int PHONE_NUMBERS = 0x04;
74 * Bit field indicating that street addresses should be matched in methods that
75 * take an options mask
77 public static final int MAP_ADDRESSES = 0x08;
80 * Bit mask indicating that all available patterns should be matched in
81 * methods that take an options mask
83 public static final int ALL = WEB_URLS | EMAIL_ADDRESSES | PHONE_NUMBERS | MAP_ADDRESSES;
86 * Don't treat anything with fewer than this many digits as a
89 private static final int PHONE_NUMBER_MINIMUM_DIGITS = 5;
92 * Filters out web URL matches that occur after an at-sign (@). This is
93 * to prevent turning the domain name in an email address into a web link.
95 public static final MatchFilter sUrlMatchFilter = new MatchFilter() {
96 public final boolean acceptMatch(CharSequence s, int start, int end) {
101 if (s.charAt(start - 1) == '@') {
110 * Filters out URL matches that don't have enough digits to be a
113 public static final MatchFilter sPhoneNumberMatchFilter = new MatchFilter() {
114 public final boolean acceptMatch(CharSequence s, int start, int end) {
117 for (int i = start; i < end; i++) {
118 if (Character.isDigit(s.charAt(i))) {
120 if (digitCount >= PHONE_NUMBER_MINIMUM_DIGITS) {
130 * Transforms matched phone number text into something suitable
131 * to be used in a tel: URL. It does this by removing everything
132 * but the digits and plus signs. For instance:
133 * '+1 (919) 555-1212'
134 * becomes '+19195551212'
136 public static final TransformFilter sPhoneNumberTransformFilter = new TransformFilter() {
137 public final String transformUrl(final Matcher match, String url) {
138 return Patterns.digitsAndPlusOnly(match);
143 * MatchFilter enables client code to have more control over
144 * what is allowed to match and become a link, and what is not.
146 * For example: when matching web urls you would like things like
147 * http://www.example.com to match, as well as just example.com itelf.
148 * However, you would not want to match against the domain in
149 * support@example.com. So, when matching against a web url pattern you
150 * might also include a MatchFilter that disallows the match if it is
151 * immediately preceded by an at-sign (@).
153 public interface MatchFilter {
155 * Examines the character span matched by the pattern and determines
156 * if the match should be turned into an actionable link.
158 * @param s The body of text against which the pattern
160 * @param start The index of the first character in s that was
161 * matched by the pattern - inclusive
162 * @param end The index of the last character in s that was
163 * matched - exclusive
165 * @return Whether this match should be turned into a link
167 boolean acceptMatch(CharSequence s, int start, int end);
171 * TransformFilter enables client code to have more control over
172 * how matched patterns are represented as URLs.
174 * For example: when converting a phone number such as (919) 555-1212
175 * into a tel: URL the parentheses, white space, and hyphen need to be
176 * removed to produce tel:9195551212.
178 public interface TransformFilter {
180 * Examines the matched text and either passes it through or uses the
181 * data in the Matcher state to produce a replacement.
183 * @param match The regex matcher state that found this URL text
184 * @param url The text that was matched
186 * @return The transformed form of the URL
188 String transformUrl(final Matcher match, String url);
192 * Scans the text of the provided Spannable and turns all occurrences
193 * of the link types indicated in the mask into clickable links.
194 * If the mask is nonzero, it also removes any existing URLSpans
195 * attached to the Spannable, to avoid problems if you call it
196 * repeatedly on the same text.
198 public static final boolean addLinks(Spannable text, int mask) {
203 URLSpan[] old = text.getSpans(0, text.length(), URLSpan.class);
205 for (int i = old.length - 1; i >= 0; i--) {
206 text.removeSpan(old[i]);
209 ArrayList<LinkSpec> links = new ArrayList<LinkSpec>();
211 if ((mask & WEB_URLS) != 0) {
212 gatherLinks(links, text, Patterns.WEB_URL,
213 new String[] { "http://", "https://", "rtsp://" },
214 sUrlMatchFilter, null);
217 if ((mask & EMAIL_ADDRESSES) != 0) {
218 gatherLinks(links, text, Patterns.EMAIL_ADDRESS,
219 new String[] { "mailto:" },
223 if ((mask & PHONE_NUMBERS) != 0) {
224 gatherLinks(links, text, Patterns.PHONE,
225 new String[] { "tel:" },
226 sPhoneNumberMatchFilter, sPhoneNumberTransformFilter);
229 if ((mask & MAP_ADDRESSES) != 0) {
230 gatherMapLinks(links, text);
233 pruneOverlaps(links);
235 if (links.size() == 0) {
239 for (LinkSpec link: links) {
240 applyLink(link.url, link.start, link.end, text);
247 * Scans the text of the provided TextView and turns all occurrences of
248 * the link types indicated in the mask into clickable links. If matches
249 * are found the movement method for the TextView is set to
250 * LinkMovementMethod.
252 public static final boolean addLinks(TextView text, int mask) {
257 CharSequence t = text.getText();
259 if (t instanceof Spannable) {
260 if (addLinks((Spannable) t, mask)) {
261 addLinkMovementMethod(text);
267 SpannableString s = SpannableString.valueOf(t);
269 if (addLinks(s, mask)) {
270 addLinkMovementMethod(text);
280 private static final void addLinkMovementMethod(TextView t) {
281 MovementMethod m = t.getMovementMethod();
283 if ((m == null) || !(m instanceof LinkMovementMethod)) {
284 if (t.getLinksClickable()) {
285 t.setMovementMethod(LinkMovementMethod.getInstance());
291 * Applies a regex to the text of a TextView turning the matches into
292 * links. If links are found then UrlSpans are applied to the link
293 * text match areas, and the movement method for the text is changed
294 * to LinkMovementMethod.
296 * @param text TextView whose text is to be marked-up with links
297 * @param pattern Regex pattern to be used for finding links
298 * @param scheme Url scheme string (eg <code>http://</code> to be
299 * prepended to the url of links that do not have
300 * a scheme specified in the link text
302 public static final void addLinks(TextView text, Pattern pattern, String scheme) {
303 addLinks(text, pattern, scheme, null, null);
307 * Applies a regex to the text of a TextView turning the matches into
308 * links. If links are found then UrlSpans are applied to the link
309 * text match areas, and the movement method for the text is changed
310 * to LinkMovementMethod.
312 * @param text TextView whose text is to be marked-up with links
313 * @param p Regex pattern to be used for finding links
314 * @param scheme Url scheme string (eg <code>http://</code> to be
315 * prepended to the url of links that do not have
316 * a scheme specified in the link text
317 * @param matchFilter The filter that is used to allow the client code
318 * additional control over which pattern matches are
319 * to be converted into links.
321 public static final void addLinks(TextView text, Pattern p, String scheme,
322 MatchFilter matchFilter, TransformFilter transformFilter) {
323 SpannableString s = SpannableString.valueOf(text.getText());
325 if (addLinks(s, p, scheme, matchFilter, transformFilter)) {
327 addLinkMovementMethod(text);
332 * Applies a regex to a Spannable turning the matches into
335 * @param text Spannable whose text is to be marked-up with
337 * @param pattern Regex pattern to be used for finding links
338 * @param scheme Url scheme string (eg <code>http://</code> to be
339 * prepended to the url of links that do not have
340 * a scheme specified in the link text
342 public static final boolean addLinks(Spannable text, Pattern pattern, String scheme) {
343 return addLinks(text, pattern, scheme, null, null);
347 * Applies a regex to a Spannable turning the matches into
350 * @param s Spannable whose text is to be marked-up with
352 * @param p Regex pattern to be used for finding links
353 * @param scheme Url scheme string (eg <code>http://</code> to be
354 * prepended to the url of links that do not have
355 * a scheme specified in the link text
356 * @param matchFilter The filter that is used to allow the client code
357 * additional control over which pattern matches are
358 * to be converted into links.
360 public static final boolean addLinks(Spannable s, Pattern p,
361 String scheme, MatchFilter matchFilter,
362 TransformFilter transformFilter) {
363 boolean hasMatches = false;
364 String prefix = (scheme == null) ? "" : scheme.toLowerCase();
365 Matcher m = p.matcher(s);
368 int start = m.start();
370 boolean allowed = true;
372 if (matchFilter != null) {
373 allowed = matchFilter.acceptMatch(s, start, end);
377 String url = makeUrl(m.group(0), new String[] { prefix },
380 applyLink(url, start, end, s);
388 private static final void applyLink(String url, int start, int end, Spannable text) {
389 URLSpan span = new URLSpan(url);
391 text.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
394 private static final String makeUrl(String url, String[] prefixes,
395 Matcher m, TransformFilter filter) {
396 if (filter != null) {
397 url = filter.transformUrl(m, url);
400 boolean hasPrefix = false;
402 for (int i = 0; i < prefixes.length; i++) {
403 if (url.regionMatches(true, 0, prefixes[i], 0,
404 prefixes[i].length())) {
407 // Fix capitalization if necessary
408 if (!url.regionMatches(false, 0, prefixes[i], 0,
409 prefixes[i].length())) {
410 url = prefixes[i] + url.substring(prefixes[i].length());
418 url = prefixes[0] + url;
424 private static final void gatherLinks(ArrayList<LinkSpec> links,
425 Spannable s, Pattern pattern, String[] schemes,
426 MatchFilter matchFilter, TransformFilter transformFilter) {
427 Matcher m = pattern.matcher(s);
430 int start = m.start();
433 if (matchFilter == null || matchFilter.acceptMatch(s, start, end)) {
434 LinkSpec spec = new LinkSpec();
435 String url = makeUrl(m.group(0), schemes, m, transformFilter);
446 private static final void gatherMapLinks(ArrayList<LinkSpec> links, Spannable s) {
447 String string = s.toString();
451 while ((address = WebView.findAddress(string)) != null) {
452 int start = string.indexOf(address);
458 LinkSpec spec = new LinkSpec();
459 int length = address.length();
460 int end = start + length;
462 spec.start = base + start;
463 spec.end = base + end;
464 string = string.substring(end);
467 String encodedAddress = null;
470 encodedAddress = URLEncoder.encode(address,"UTF-8");
471 } catch (UnsupportedEncodingException e) {
475 spec.url = "geo:0,0?q=" + encodedAddress;
480 private static final void pruneOverlaps(ArrayList<LinkSpec> links) {
481 Comparator<LinkSpec> c = new Comparator<LinkSpec>() {
482 public final int compare(LinkSpec a, LinkSpec b) {
483 if (a.start < b.start) {
487 if (a.start > b.start) {
502 public final boolean equals(Object o) {
507 Collections.sort(links, c);
509 int len = links.size();
512 while (i < len - 1) {
513 LinkSpec a = links.get(i);
514 LinkSpec b = links.get(i + 1);
517 if ((a.start <= b.start) && (a.end > b.start)) {
518 if (b.end <= a.end) {
520 } else if ((a.end - a.start) > (b.end - b.start)) {
522 } else if ((a.end - a.start) < (b.end - b.start)) {
527 links.remove(remove);