/*
* 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;
}