/* * Copyright (c) 2016 Zhang Hai <Dreaming.in.Code.ZH@Gmail.com> * All Rights Reserved. */ package me.zhanghai.android.douya.util; import android.support.annotation.NonNull; import android.text.Spannable; import android.text.SpannableString; import android.text.Spanned; import android.text.util.Linkify; import android.util.Patterns; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import me.zhanghai.android.douya.ui.UriSpan; public class SpanUtils { /** * Modified from {@link Patterns#WEB_URL}, with ftp scheme added and an optional trailing slash. */ @SuppressWarnings("deprecation") public static final Pattern WEB_URL_PATTERN = Pattern.compile( "((?:(http|https|Http|Https|ftp|Ftp|rtsp|Rtsp):\\/\\/(?:(?:[a-zA-Z0-9\\$\\-\\_\\.\\+\\!\\*\\'\\(\\)" + "\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,64}(?:\\:(?:[a-zA-Z0-9\\$\\-\\_" + "\\.\\+\\!\\*\\'\\(\\)\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,25})?\\@)?)?" + "(?:" + Patterns.DOMAIN_NAME + ")" + "(?:\\:\\d{1,5})?)" + "(\\/(?:(?:[" + Patterns.GOOD_IRI_CHAR + "\\;\\/\\?\\:\\@\\&\\=\\#\\~" + "\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])|(?:\\%[a-fA-F0-9]{2}))*)?" + "(?=\\W|$)"); private static final String[] WEB_URL_SCHEMES = { "http://", "https://", "ftp://", "rtsp://" }; /** * 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. */ private static final MatchFilter WEB_URL_MATCH_FILTER = new MatchFilter() { public final boolean acceptMatch(CharSequence s, int start, int end) { if (start == 0) { return true; } if (s.charAt(start - 1) == '@') { return false; } return true; } }; /** * Modified from {@link Patterns#EMAIL_ADDRESS}, with optional mailto scheme added. */ public static final Pattern EMAIL_PATTERN = Pattern.compile( "(mailto:)?" + "[a-zA-Z0-9\\+\\.\\_\\%\\-\\+]{1,256}" + "\\@" + "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" + "(" + "\\." + "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" + ")+" ); private static final String[] EMAIL_SCHEMES = { "mailto:" }; /** * Modified from {@link Patterns#PHONE}, with required tel/sms/smsto scheme added. */ public static final Pattern PHONE_URI_PATTERN = Pattern.compile( "(tel|sms|smsto):" + "(\\+[0-9]+[\\- \\.]*)?" + "(\\([0-9]+\\)[\\- \\.]*)?" + "([0-9][0-9\\- \\.]+[0-9])"); /** * Modified from {@link Linkify#addLinks(Spannable, int)}. */ public static Spannable addLinks(Spannable spannable) { List<Link> links = new ArrayList<>(); gatherLinks(links, spannable, WEB_URL_PATTERN, WEB_URL_SCHEMES, WEB_URL_MATCH_FILTER); gatherLinks(links, spannable, EMAIL_PATTERN, EMAIL_SCHEMES, null); gatherLinks(links, spannable, PHONE_URI_PATTERN, null, null); pruneOverlaps(links); if (links.size() == 0) { return spannable; } for (Link link: links) { applyLink(link, spannable); } return spannable; } public static Spannable addLinks(CharSequence text) { return addLinks(new SpannableString(text)); } private static void gatherLinks(List<Link> links, CharSequence text, Pattern pattern, String[] schemes, MatchFilter matchFilter) { Matcher matcher = pattern.matcher(text); while (matcher.find()) { int start = matcher.start(); int end = matcher.end(); if (matchFilter == null || matchFilter.acceptMatch(text, start, end)) { links.add(new Link(start, end, makeUrl(matcher.group(0), schemes))); } } } private static String makeUrl(String url, String[] prefixes) { if (ArrayUtils.isEmpty(prefixes)) { return url; } boolean hasPrefix = false; for (String prefix : prefixes) { if (url.regionMatches(true, 0, prefix, 0, prefix.length())) { hasPrefix = true; // Fix capitalization if necessary if (!url.regionMatches(false, 0, prefix, 0, prefix.length())) { url = prefix + url.substring(prefix.length()); } break; } } if (!hasPrefix) { url = prefixes[0] + url; } return url; } private static void pruneOverlaps(List<Link> links) { Collections.sort(links); int i = 0; int last = links.size() - 1; while (i < last) { Link a = links.get(i); Link 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); --last; continue; } } ++i; } } private static void applyLink(Link link, Spannable spannable) { spannable.setSpan(new UriSpan(link.url), link.start, link.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } private interface MatchFilter { boolean acceptMatch(CharSequence s, int start, int end); } private static class Link implements Comparable<Link> { public int start; public int end; public String url; public Link(int start, int end, String url) { this.start = start; this.end = end; this.url = url; } @Override public int compareTo(@NonNull Link that) { if (start < that.start) { return -1; } if (start > that.start) { return 1; } if (end < that.end) { return 1; } if (end > that.end) { return -1; } return 0; } } }