/* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fanfou.app.opensource.util; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.regex.Matcher; import java.util.regex.Pattern; import android.text.Spannable; import android.text.SpannableString; import android.text.Spanned; import android.text.TextPaint; import android.text.method.LinkMovementMethod; import android.text.method.MovementMethod; import android.text.style.URLSpan; import android.webkit.WebView; import android.widget.TextView; /** * Linkify take a piece of text and a regular expression and turns all of the * regex matches in the text into clickable links. This is particularly * useful for matching things like email addresses, web urls, etc. and making * them actionable. * * Alone with the pattern that is to be matched, a url scheme prefix is also * required. Any pattern match that does not begin with the supplied scheme * will have the scheme prepended to the matched text when the clickable url * is created. For instance, if you are matching web urls you would supply * the scheme <code>http://</code>. If the pattern matches example.com, which * does not have a url scheme prefix, the supplied scheme will be prepended to * create <code>http://example.com</code> when the clickable url link is * created. */ /** * custom at 2011.11.17 * * @author mcxiaoke * @version 1.0 2011.11.17 * */ public class LinkifyCompat { /** * MatchFilter enables client code to have more control over what is allowed * to match and become a link, and what is not. * * For example: when matching web urls you would like things like * http://www.example.com to match, as well as just example.com itelf. * However, you would not want to match against the domain in * support@example.com. So, when matching against a web url pattern you * might also include a MatchFilter that disallows the match if it is * immediately preceded by an at-sign (@). */ public interface MatchFilter { /** * Examines the character span matched by the pattern and determines if * the match should be turned into an actionable link. * * @param s * The body of text against which the pattern was matched * @param start * The index of the first character in s that was matched by * the pattern - inclusive * @param end * The index of the last character in s that was matched - * exclusive * * @return Whether this match should be turned into a link */ boolean acceptMatch(CharSequence s, int start, int end); } /** * TransformFilter enables client code to have more control over how matched * patterns are represented as URLs. * * For example: when converting a phone number such as (919) 555-1212 into a * tel: URL the parentheses, white space, and hyphen need to be removed to * produce tel:9195551212. */ public interface TransformFilter { /** * Examines the matched text and either passes it through or uses the * data in the Matcher state to produce a replacement. * * @param match * The regex matcher state that found this URL text * @param url * The text that was matched * * @return The transformed form of the URL */ String transformUrl(final Matcher match, String url); } public static class URLSpanNoUnderline extends URLSpan { public URLSpanNoUnderline(final String url) { super(url); } @Override public void updateDrawState(final TextPaint tp) { super.updateDrawState(tp); tp.setUnderlineText(false); } } /** * Bit field indicating that web URLs should be matched in methods that take * an options mask */ public static final int WEB_URLS = 0x01; /** * Bit field indicating that email addresses should be matched in methods * that take an options mask */ public static final int EMAIL_ADDRESSES = 0x02; /** * Bit field indicating that phone numbers should be matched in methods that * take an options mask */ public static final int PHONE_NUMBERS = 0x04; /** * Bit field indicating that street addresses should be matched in methods * that take an options mask */ public static final int MAP_ADDRESSES = 0x08; /** * Bit mask indicating that all available patterns should be matched in * methods that take an options mask */ public static final int ALL = LinkifyCompat.WEB_URLS | LinkifyCompat.EMAIL_ADDRESSES | LinkifyCompat.PHONE_NUMBERS | LinkifyCompat.MAP_ADDRESSES; /** * Don't treat anything with fewer than this many digits as a phone number. */ private static final int PHONE_NUMBER_MINIMUM_DIGITS = 5; /** * Filters out web URL matches that occur after an at-sign (@). This is to * prevent turning the domain name in an email address into a web link. */ public static final MatchFilter sUrlMatchFilter = new MatchFilter() { @Override public final boolean acceptMatch(final CharSequence s, final int start, final int end) { if (start == 0) { return true; } if (s.charAt(start - 1) == '@') { return false; } return true; } }; /** * Filters out URL matches that don't have enough digits to be a phone * number. */ public static final MatchFilter sPhoneNumberMatchFilter = new MatchFilter() { @Override public final boolean acceptMatch(final CharSequence s, final int start, final int end) { int digitCount = 0; for (int i = start; i < end; i++) { if (Character.isDigit(s.charAt(i))) { digitCount++; if (digitCount >= LinkifyCompat.PHONE_NUMBER_MINIMUM_DIGITS) { return true; } } } return false; } }; /** * Transforms matched phone number text into something suitable to be used * in a tel: URL. It does this by removing everything but the digits and * plus signs. For instance: '+1 (919) 555-1212' becomes * '+19195551212' */ public static final TransformFilter sPhoneNumberTransformFilter = new TransformFilter() { @Override public final String transformUrl(final Matcher match, final String url) { return PatternsCompat.digitsAndPlusOnly(match); } }; private static final void addLinkMovementMethod(final TextView t) { final MovementMethod m = t.getMovementMethod(); if ((m == null) || !(m instanceof LinkMovementMethod)) { if (t.getLinksClickable()) { t.setMovementMethod(LinkMovementMethod.getInstance()); } } } /** * Scans the text of the provided Spannable and turns all occurrences of the * link types indicated in the mask into clickable links. If the mask is * nonzero, it also removes any existing URLSpans attached to the Spannable, * to avoid problems if you call it repeatedly on the same text. */ public static final boolean addLinks(final Spannable text, final int mask) { if (mask == 0) { return false; } final URLSpan[] old = text.getSpans(0, text.length(), URLSpan.class); for (int i = old.length - 1; i >= 0; i--) { text.removeSpan(old[i]); } final ArrayList<LinkSpec> links = new ArrayList<LinkSpec>(); if ((mask & LinkifyCompat.WEB_URLS) != 0) { LinkifyCompat.gatherLinks(links, text, PatternsCompat.WEB_URL, new String[] { "http://", "https://", "rtsp://" }, LinkifyCompat.sUrlMatchFilter, null); } if ((mask & LinkifyCompat.EMAIL_ADDRESSES) != 0) { LinkifyCompat.gatherLinks(links, text, PatternsCompat.EMAIL_ADDRESS, new String[] { "mailto:" }, null, null); } if ((mask & LinkifyCompat.PHONE_NUMBERS) != 0) { LinkifyCompat.gatherLinks(links, text, PatternsCompat.PHONE, new String[] { "tel:" }, LinkifyCompat.sPhoneNumberMatchFilter, LinkifyCompat.sPhoneNumberTransformFilter); } if ((mask & LinkifyCompat.MAP_ADDRESSES) != 0) { LinkifyCompat.gatherMapLinks(links, text); } LinkifyCompat.pruneOverlaps(links); if (links.size() == 0) { return false; } for (final LinkSpec link : links) { LinkifyCompat.applyLink(link.url, link.start, link.end, text); } return true; } /** * Applies a regex to a Spannable turning the matches into links. * * @param text * Spannable whose text is to be marked-up with links * @param pattern * Regex pattern to be used for finding links * @param scheme * Url scheme string (eg <code>http://</code> to be prepended to * the url of links that do not have a scheme specified in the * link text */ public static final boolean addLinks(final Spannable text, final Pattern pattern, final String scheme) { return LinkifyCompat.addLinks(text, pattern, scheme, null, null); } /** * Applies a regex to a Spannable turning the matches into links. * * @param s * Spannable whose text is to be marked-up with links * @param p * Regex pattern to be used for finding links * @param scheme * Url scheme string (eg <code>http://</code> to be prepended to * the url of links that do not have a scheme specified in the * link text * @param matchFilter * The filter that is used to allow the client code additional * control over which pattern matches are to be converted into * links. */ public static final boolean addLinks(final Spannable s, final Pattern p, final String scheme, final MatchFilter matchFilter, final TransformFilter transformFilter) { boolean hasMatches = false; final String prefix = (scheme == null) ? "" : scheme.toLowerCase(); final Matcher m = p.matcher(s); while (m.find()) { final int start = m.start(); final int end = m.end(); boolean allowed = true; if (matchFilter != null) { allowed = matchFilter.acceptMatch(s, start, end); } if (allowed) { final String url = LinkifyCompat.makeUrl(m.group(0), new String[] { prefix }, m, transformFilter); LinkifyCompat.applyLink(url, start, end, s); hasMatches = true; } } return hasMatches; } /** * Scans the text of the provided TextView and turns all occurrences of the * link types indicated in the mask into clickable links. If matches are * found the movement method for the TextView is set to LinkMovementMethod. */ public static final boolean addLinks(final TextView text, final int mask) { if (mask == 0) { return false; } final CharSequence t = text.getText(); if (t instanceof Spannable) { if (LinkifyCompat.addLinks((Spannable) t, mask)) { LinkifyCompat.addLinkMovementMethod(text); return true; } return false; } else { final SpannableString s = SpannableString.valueOf(t); if (LinkifyCompat.addLinks(s, mask)) { LinkifyCompat.addLinkMovementMethod(text); text.setText(s); return true; } return false; } } /** * Applies a regex to the text of a TextView turning the matches into links. * If links are found then UrlSpans are applied to the link text match * areas, and the movement method for the text is changed to * LinkMovementMethod. * * @param text * TextView whose text is to be marked-up with links * @param pattern * Regex pattern to be used for finding links * @param scheme * Url scheme string (eg <code>http://</code> to be prepended to * the url of links that do not have a scheme specified in the * link text */ public static final void addLinks(final TextView text, final Pattern pattern, final String scheme) { LinkifyCompat.addLinks(text, pattern, scheme, null, null); } /** * Applies a regex to the text of a TextView turning the matches into links. * If links are found then UrlSpans are applied to the link text match * areas, and the movement method for the text is changed to * LinkMovementMethod. * * @param text * TextView whose text is to be marked-up with links * @param p * Regex pattern to be used for finding links * @param scheme * Url scheme string (eg <code>http://</code> to be prepended to * the url of links that do not have a scheme specified in the * link text * @param matchFilter * The filter that is used to allow the client code additional * control over which pattern matches are to be converted into * links. */ public static final void addLinks(final TextView text, final Pattern p, final String scheme, final MatchFilter matchFilter, final TransformFilter transformFilter) { final SpannableString s = SpannableString.valueOf(text.getText()); if (LinkifyCompat.addLinks(s, p, scheme, matchFilter, transformFilter)) { text.setText(s); LinkifyCompat.addLinkMovementMethod(text); } } private static final void applyLink(final String url, final int start, final int end, final Spannable text) { final URLSpanNoUnderline span = new URLSpanNoUnderline(url); text.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } private static final void gatherLinks(final ArrayList<LinkSpec> links, final Spannable s, final Pattern pattern, final String[] schemes, final MatchFilter matchFilter, final TransformFilter transformFilter) { final Matcher m = pattern.matcher(s); while (m.find()) { final int start = m.start(); final int end = m.end(); if ((matchFilter == null) || matchFilter.acceptMatch(s, start, end)) { final LinkSpec spec = new LinkSpec(); final String url = LinkifyCompat.makeUrl(m.group(0), schemes, m, transformFilter); spec.url = url; spec.start = start; spec.end = end; links.add(spec); } } } private static final void gatherMapLinks(final ArrayList<LinkSpec> links, final Spannable s) { String string = s.toString(); String address; int base = 0; while ((address = WebView.findAddress(string)) != null) { final int start = string.indexOf(address); if (start < 0) { break; } final LinkSpec spec = new LinkSpec(); final int length = address.length(); final int end = start + length; spec.start = base + start; spec.end = base + end; string = string.substring(end); base += end; String encodedAddress = null; try { encodedAddress = URLEncoder.encode(address, "UTF-8"); } catch (final UnsupportedEncodingException e) { continue; } spec.url = "geo:0,0?q=" + encodedAddress; links.add(spec); } } private static final String makeUrl(String url, final String[] prefixes, final Matcher m, final TransformFilter filter) { if (filter != null) { url = filter.transformUrl(m, url); } boolean hasPrefix = false; for (int i = 0; i < prefixes.length; i++) { if (url.regionMatches(true, 0, prefixes[i], 0, prefixes[i].length())) { hasPrefix = true; // Fix capitalization if necessary if (!url.regionMatches(false, 0, prefixes[i], 0, prefixes[i].length())) { url = prefixes[i] + url.substring(prefixes[i].length()); } break; } } if (!hasPrefix) { url = prefixes[0] + url; } return url; } private static final void pruneOverlaps(final ArrayList<LinkSpec> links) { final Comparator<LinkSpec> c = new Comparator<LinkSpec>() { @Override public final int compare(final LinkSpec a, final LinkSpec b) { if (a.start < b.start) { return -1; } if (a.start > b.start) { return 1; } if (a.end < b.end) { return 1; } if (a.end > b.end) { return -1; } return 0; } @Override public final boolean equals(final Object o) { return false; } }; Collections.sort(links, c); int len = links.size(); int i = 0; while (i < (len - 1)) { final LinkSpec a = links.get(i); final LinkSpec b = links.get(i + 1); int remove = -1; if ((a.start <= b.start) && (a.end > b.start)) { if (b.end <= a.end) { remove = i + 1; } else if ((a.end - a.start) > (b.end - b.start)) { remove = i + 1; } else if ((a.end - a.start) < (b.end - b.start)) { remove = i; } if (remove != -1) { links.remove(remove); len--; continue; } } i++; } } } class LinkSpec { String url; int start; int end; }