/* * Tweetings - Twitter client for Android * * Copyright (C) 2012-2013 RBD Solutions Limited <apps@tweetings.net> * Copyright (C) 2012 Mariotaku Lee <mariotaku.lee@gmail.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.dwdesign.tweetings.util; import static com.dwdesign.tweetings.util.Utils.getAllAvailableImage; import static com.dwdesign.tweetings.util.Utils.matcherEnd; import static com.dwdesign.tweetings.util.Utils.matcherGroup; import static com.dwdesign.tweetings.util.Utils.matcherStart; import static com.dwdesign.tweetings.util.Utils.openUserProfile; import static com.dwdesign.tweetings.util.Utils.openTweetSearch; import static com.dwdesign.tweetings.util.HtmlEscapeHelper.unescape; import java.util.ArrayList; import java.util.regex.Matcher; import java.util.regex.Pattern; import twitter4j.Status; import com.dwdesign.tweetings.model.ImageSpec; import com.dwdesign.tweetings.model.ParcelableStatus; import com.dwdesign.tweetings.view.NoUnderlineClickableSpan; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.net.Uri; 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.util.Log; import android.view.View; import android.widget.TextView; import com.twitter.Regex; /** * 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. */ public class TwidereLinkify { public static final int LINK_TYPE_MENTION_LIST = 1; public static final int LINK_TYPE_HASHTAG = 2; public static final int LINK_TYPE_LINK_WITH_IMAGE_EXTENSION = 3; public static final int LINK_TYPE_LINK = 4; public static final int LINK_TYPE_ALL_AVAILABLE_IMAGE = 5; public static final int LINK_TYPE_LIST = 6; public static final int LINK_TYPE_CASHTAG = 7; public static final int LINK_TYPE_TWITLONGER = 8; public static final int[] ALL_LINK_TYPES = new int[] { LINK_TYPE_MENTION_LIST, LINK_TYPE_HASHTAG, LINK_TYPE_LINK_WITH_IMAGE_EXTENSION, LINK_TYPE_LINK, LINK_TYPE_ALL_AVAILABLE_IMAGE, LINK_TYPE_CASHTAG, LINK_TYPE_TWITLONGER }; public static final String SINA_WEIBO_IMAGES_AVAILABLE_SIZES = "(woriginal|large|thumbnail|bmiddle|mw[\\d]+)"; public static final String AVAILABLE_URL_SCHEME_PREFIX = "(https?:\\/\\/)?"; public static final String AVAILABLE_IMAGE_SHUFFIX = "(png|jpeg|jpg|gif|bmp)"; private static final String STRING_PATTERN_TWITTER_IMAGES_DOMAIN = "(p|pbs)\\.twimg\\.com"; private static final String STRING_PATTERN_SINA_WEIBO_IMAGES_DOMAIN = "[\\w\\d]+\\.sinaimg\\.cn|[\\w\\d]+\\.sina\\.cn"; private static final String STRING_PATTERN_LOCKERZ_AND_PLIXI_DOMAIN = "plixi\\.com\\/p|lockerz\\.com\\/s"; private static final String STRING_PATTERN_INSTAGRAM_DOMAIN = "instagr\\.am|instagram\\.com"; private static final String STRING_PATTERN_TWITPIC_DOMAIN = "twitpic\\.com"; private static final String STRING_PATTERN_IMGLY_DOMAIN = "img\\.ly"; private static final String STRING_PATTERN_YFROG_DOMAIN = "yfrog\\.com"; private static final String STRING_PATTERN_TWITGOO_DOMAIN = "twitgoo\\.com"; private static final String STRING_PATTERN_MOBYPICTURE_DOMAIN = "moby\\.to"; private static final String STRING_PATTERN_IMGUR_DOMAIN = "imgur\\.com|i\\.imgur\\.com"; private static final String STRING_PATTERN_IMAGES_NO_SCHEME = "[^:\\/\\/].+?\\." + AVAILABLE_IMAGE_SHUFFIX; private static final String STRING_PATTERN_TWITTER_IMAGES_NO_SCHEME = STRING_PATTERN_TWITTER_IMAGES_DOMAIN + "(\\/media)?\\/([\\d\\w\\-_]+)\\.(png|jpeg|jpg|gif|bmp)"; private static final String STRING_PATTERN_SINA_WEIBO_IMAGES_NO_SCHEME = "(" + STRING_PATTERN_SINA_WEIBO_IMAGES_DOMAIN + ")" + "\\/" + SINA_WEIBO_IMAGES_AVAILABLE_SIZES + "\\/(([\\d\\w]+)\\.(png|jpeg|jpg|gif|bmp))"; private static final String STRING_PATTERN_LOCKERZ_AND_PLIXI_NO_SCHEME = "(" + STRING_PATTERN_LOCKERZ_AND_PLIXI_DOMAIN + ")" + "\\/(\\w+)\\/?"; private static final String STRING_PATTERN_INSTAGRAM_NO_SCHEME = "(" + STRING_PATTERN_INSTAGRAM_DOMAIN + ")" + "\\/p\\/([_\\-\\d\\w]+)\\/?"; private static final String STRING_PATTERN_TWITPIC_NO_SCHEME = STRING_PATTERN_TWITPIC_DOMAIN + "\\/([\\d\\w]+)\\/?"; private static final String STRING_PATTERN_IMGLY_NO_SCHEME = STRING_PATTERN_IMGLY_DOMAIN + "\\/([\\w\\d]+)\\/?"; private static final String STRING_PATTERN_YFROG_NO_SCHEME = STRING_PATTERN_YFROG_DOMAIN + "\\/([\\w\\d]+)\\/?"; private static final String STRING_PATTERN_TWITGOO_NO_SCHEME = STRING_PATTERN_TWITGOO_DOMAIN + "\\/([\\d\\w]+)\\/?"; private static final String STRING_PATTERN_MOBYPICTURE_NO_SCHEME = STRING_PATTERN_MOBYPICTURE_DOMAIN + "\\/([\\d\\w]+)\\/?"; private static final String STRING_PATTERN_IMGUR_NO_SCHEME = "(" + STRING_PATTERN_IMGUR_DOMAIN + ")" + "\\/([\\d\\w]+)((?-i)s|(?-i)l)?(\\." + AVAILABLE_IMAGE_SHUFFIX + ")?"; private static final String STRING_PATTERN_TWITLONGER = AVAILABLE_URL_SCHEME_PREFIX + "((tl\\.gd)\\/([\\w\\d]+))"; private static final String STRING_PATTERN_IMAGES = AVAILABLE_URL_SCHEME_PREFIX + STRING_PATTERN_IMAGES_NO_SCHEME; private static final String STRING_PATTERN_TWITTER_IMAGES = AVAILABLE_URL_SCHEME_PREFIX + STRING_PATTERN_TWITTER_IMAGES_NO_SCHEME; private static final String STRING_PATTERN_SINA_WEIBO_IMAGES = AVAILABLE_URL_SCHEME_PREFIX + STRING_PATTERN_SINA_WEIBO_IMAGES_NO_SCHEME; private static final String STRING_PATTERN_LOCKERZ_AND_PLIXI = AVAILABLE_URL_SCHEME_PREFIX + STRING_PATTERN_LOCKERZ_AND_PLIXI_NO_SCHEME; private static final String STRING_PATTERN_INSTAGRAM = AVAILABLE_URL_SCHEME_PREFIX + STRING_PATTERN_INSTAGRAM_NO_SCHEME; private static final String STRING_PATTERN_TWITPIC = AVAILABLE_URL_SCHEME_PREFIX + STRING_PATTERN_TWITPIC_NO_SCHEME; private static final String STRING_PATTERN_IMGLY = AVAILABLE_URL_SCHEME_PREFIX + STRING_PATTERN_IMGLY_NO_SCHEME; private static final String STRING_PATTERN_YFROG = AVAILABLE_URL_SCHEME_PREFIX + STRING_PATTERN_YFROG_NO_SCHEME; private static final String STRING_PATTERN_TWITGOO = AVAILABLE_URL_SCHEME_PREFIX + STRING_PATTERN_TWITGOO_NO_SCHEME; private static final String STRING_PATTERN_MOBYPICTURE = AVAILABLE_URL_SCHEME_PREFIX + STRING_PATTERN_MOBYPICTURE_NO_SCHEME; private static final String STRING_PATTERN_IMGUR = AVAILABLE_URL_SCHEME_PREFIX + STRING_PATTERN_IMGUR_NO_SCHEME; public static final Pattern PATTERN_ALL_AVAILABLE_IMAGES = Pattern.compile("(" + STRING_PATTERN_IMAGES_NO_SCHEME + "|" + STRING_PATTERN_TWITTER_IMAGES_NO_SCHEME + "|" + STRING_PATTERN_SINA_WEIBO_IMAGES_NO_SCHEME + "|" + STRING_PATTERN_LOCKERZ_AND_PLIXI_NO_SCHEME + "|" + STRING_PATTERN_INSTAGRAM_NO_SCHEME + "|" + STRING_PATTERN_TWITPIC_NO_SCHEME + "|" + STRING_PATTERN_IMGLY_NO_SCHEME + "|" + STRING_PATTERN_YFROG_NO_SCHEME + "|" + STRING_PATTERN_TWITGOO_NO_SCHEME + "|" + STRING_PATTERN_MOBYPICTURE_NO_SCHEME + "|" + STRING_PATTERN_IMGUR_DOMAIN + ")", Pattern.CASE_INSENSITIVE); public static final Pattern PATTERN_INLINE_PREVIEW_AVAILABLE_IMAGES_MATCH_ONLY = Pattern.compile( AVAILABLE_URL_SCHEME_PREFIX + "(" + STRING_PATTERN_IMAGES_NO_SCHEME + "|" + STRING_PATTERN_TWITTER_IMAGES_DOMAIN + "|" + STRING_PATTERN_SINA_WEIBO_IMAGES_DOMAIN + "|" + STRING_PATTERN_LOCKERZ_AND_PLIXI_DOMAIN + "|" + STRING_PATTERN_INSTAGRAM_DOMAIN + "|" + STRING_PATTERN_TWITPIC_DOMAIN + "|" + STRING_PATTERN_IMGLY_DOMAIN + "|" + STRING_PATTERN_YFROG_DOMAIN + "|" + STRING_PATTERN_TWITGOO_DOMAIN + "|" + STRING_PATTERN_MOBYPICTURE_DOMAIN + "|" + STRING_PATTERN_IMGUR_DOMAIN + ")", Pattern.CASE_INSENSITIVE); public static final Pattern PATTERN_PREVIEW_AVAILABLE_IMAGES_IN_HTML = Pattern.compile("(" + STRING_PATTERN_IMAGES_NO_SCHEME + "|" + STRING_PATTERN_SINA_WEIBO_IMAGES_NO_SCHEME + "|" + STRING_PATTERN_LOCKERZ_AND_PLIXI_NO_SCHEME + "|" + STRING_PATTERN_INSTAGRAM_NO_SCHEME + "|" + STRING_PATTERN_TWITPIC_NO_SCHEME + "|" + STRING_PATTERN_IMGLY_NO_SCHEME + "|" + STRING_PATTERN_YFROG_NO_SCHEME + "|" + STRING_PATTERN_TWITGOO_NO_SCHEME + "|" + STRING_PATTERN_MOBYPICTURE_NO_SCHEME + "|" + STRING_PATTERN_IMGUR_DOMAIN + ")", Pattern.CASE_INSENSITIVE); public static final Pattern PATTERN_PREVIEW_AVAILABLE_IMAGES_IN_HTML_TWITTER = Pattern.compile("<a href=\"(" + AVAILABLE_URL_SCHEME_PREFIX + STRING_PATTERN_TWITTER_IMAGES_NO_SCHEME + ")\">", Pattern.CASE_INSENSITIVE); public static final int PREVIEW_AVAILABLE_IMAGES_IN_HTML_GROUP_LINK = 1; public static final Pattern PATTERN_IMAGES = Pattern.compile(STRING_PATTERN_IMAGES, Pattern.CASE_INSENSITIVE); public static final Pattern PATTERN_TWITLONGER = Pattern.compile(STRING_PATTERN_TWITLONGER, Pattern.CASE_INSENSITIVE); public static final Pattern PATTERN_TWITTER_IMAGES = Pattern.compile(STRING_PATTERN_TWITTER_IMAGES, Pattern.CASE_INSENSITIVE); public static final Pattern PATTERN_SINA_WEIBO_IMAGES = Pattern.compile(STRING_PATTERN_SINA_WEIBO_IMAGES, Pattern.CASE_INSENSITIVE); public static final Pattern PATTERN_LOCKERZ_AND_PLIXI = Pattern.compile(STRING_PATTERN_LOCKERZ_AND_PLIXI, Pattern.CASE_INSENSITIVE); public static final Pattern PATTERN_INSTAGRAM = Pattern.compile(STRING_PATTERN_INSTAGRAM, Pattern.CASE_INSENSITIVE); public static final int INSTAGRAM_GROUP_ID = 3; public static final Pattern PATTERN_TWITPIC = Pattern.compile(STRING_PATTERN_TWITPIC, Pattern.CASE_INSENSITIVE); public static final int TWITPIC_GROUP_ID = 2; public static final Pattern PATTERN_IMGLY = Pattern.compile(STRING_PATTERN_IMGLY, Pattern.CASE_INSENSITIVE); public static final int IMGLY_GROUP_ID = 2; public static final Pattern PATTERN_YFROG = Pattern.compile(STRING_PATTERN_YFROG, Pattern.CASE_INSENSITIVE); public static final int YFROG_GROUP_ID = 2; public static final Pattern PATTERN_TWITGOO = Pattern.compile(STRING_PATTERN_TWITGOO, Pattern.CASE_INSENSITIVE); public static final int TWITGOO_GROUP_ID = 2; public static final Pattern PATTERN_MOBYPICTURE = Pattern.compile(STRING_PATTERN_MOBYPICTURE, Pattern.CASE_INSENSITIVE); public static final int MOBYPICTURE_GROUP_ID = 2; public static final Pattern PATTERN_IMGUR = Pattern.compile(STRING_PATTERN_IMGUR, Pattern.CASE_INSENSITIVE); public static final int IMGUR_GROUP_ID = 3; public static final String TWITTER_PROFILE_IMAGES_AVAILABLE_SIZES = "(bigger|normal|mini)"; private static final String STRING_PATTERN_TWITTER_PROFILE_IMAGES_NO_SCHEME = "(twimg[\\d\\w\\-]+\\.akamaihd\\.net|[\\w\\d]+\\.twimg\\.com)\\/profile_images\\/([\\d\\w\\-_]+)\\/([\\d\\w\\-_]+)_" + TWITTER_PROFILE_IMAGES_AVAILABLE_SIZES + "(\\.?" + AVAILABLE_IMAGE_SHUFFIX + ")?"; private static final String STRING_PATTERN_TWITTER_PROFILE_IMAGES = AVAILABLE_URL_SCHEME_PREFIX + STRING_PATTERN_TWITTER_PROFILE_IMAGES_NO_SCHEME; public static final Pattern PATTERN_TWITTER_PROFILE_IMAGES = Pattern.compile(STRING_PATTERN_TWITTER_PROFILE_IMAGES, Pattern.CASE_INSENSITIVE); private final TextView view; private OnLinkClickListener mOnLinkClickListener; /** * 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; } }; public static Spannable twitterifyText(final long account_id, final Context context, ParcelableStatus status) { return twitterifyText(account_id, context, status.text_html, null); } public static Spannable twitterifyText(final long account_id, final Context context, String status) { return twitterifyText(account_id, context, status, null); } public static Spannable twitterifyText(final long account_id, final Context context, final String status, final String highlightQuery) { final String textPlain = unescape(status); Spannable rtSpan = new SpannableString(textPlain); String anchorRegex = "<a href=\"(.*?)\">(.*?)</a>"; Pattern p = Pattern.compile(anchorRegex); Matcher m = p.matcher(status); while (m.find()) { final String url = m.group(1); final String displayUrl = m.group(2); int start = textPlain.indexOf(displayUrl); int end = start + displayUrl.length(); rtSpan.setSpan(new NoUnderlineClickableSpan() { @Override public void onClick(View widget) { context.startActivity(new Intent(Intent.ACTION_VIEW) .setData(Uri.parse(url)) .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); } }, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } m = Regex.VALID_MENTION_OR_LIST.matcher(rtSpan); while (m.find()) { final int start = matcherStart(m, Regex.VALID_MENTION_OR_LIST_GROUP_AT); final int username_end = matcherEnd(m, Regex.VALID_MENTION_OR_LIST_GROUP_USERNAME); final String mention = matcherGroup(m, Regex.VALID_MENTION_OR_LIST_GROUP_USERNAME); rtSpan.setSpan(new NoUnderlineClickableSpan() { @Override public void onClick(View widget) { openUserProfile((Activity) context, account_id, 0, mention); } }, start, username_end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } m = Regex.VALID_HASHTAG.matcher(rtSpan); while (m.find()) { final int start = matcherStart(m, Regex.VALID_HASHTAG_GROUP_HASHTAG_FULL); final int end = matcherEnd(m, Regex.VALID_HASHTAG_GROUP_HASHTAG_FULL); final String url = matcherGroup(m, Regex.VALID_HASHTAG_GROUP_HASHTAG_FULL); rtSpan.setSpan(new NoUnderlineClickableSpan() { @Override public void onClick(View widget) { openTweetSearch((Activity) context, account_id, url); } }, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } return rtSpan; } public TwidereLinkify() { this.view = null; } public TwidereLinkify(final TextView view) { this.view = view; view.setMovementMethod(LinkMovementMethod.getInstance()); } public final void addAllLinks() { for (final int type : ALL_LINK_TYPES) { addLinks(type); } } /** * 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 description 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 final void addLinks(final int type) { final SpannableString string = SpannableString.valueOf(view.getText()); switch (type) { case LINK_TYPE_MENTION_LIST: { addMentionOrListLinks(string); break; } case LINK_TYPE_HASHTAG: { addHashtagLinks(string); break; } case LINK_TYPE_LINK_WITH_IMAGE_EXTENSION: { final URLSpan[] spans = string.getSpans(0, string.length(), URLSpan.class); for (final URLSpan span : spans) { final int start = string.getSpanStart(span); final int end = string.getSpanEnd(span); final String url = span.getURL(); if (PATTERN_IMAGES.matcher(url).matches()) { string.removeSpan(span); applyLink(url, start, end, string, LINK_TYPE_LINK_WITH_IMAGE_EXTENSION); } } break; } case LINK_TYPE_LINK: { final ArrayList<LinkSpec> links = new ArrayList<LinkSpec>(); gatherLinks(links, string, Patterns.WEB_URL, new String[] { "http://", "https://", "rtsp://" }, sUrlMatchFilter, null); for (final LinkSpec link : links) { final URLSpan[] spans = string.getSpans(link.start, link.end, URLSpan.class); if (spans != null && spans.length == 1) { for (final URLSpan span : spans) { string.removeSpan(span); applyLink(span.getURL(), link.start, link.end, string, LINK_TYPE_LINK); } } else { applyLink(link.url, link.start, link.end, string, LINK_TYPE_LINK); } } break; } case LINK_TYPE_TWITLONGER: { final URLSpan[] spans = string.getSpans(0, string.length(), URLSpan.class); for (final URLSpan span : spans) { final int start = string.getSpanStart(span); final int end = string.getSpanEnd(span); final String url = span.getURL(); if (PATTERN_TWITLONGER.matcher(url).matches()) { string.removeSpan(span); applyLink(url, start, end, string, LINK_TYPE_TWITLONGER); } } break; } case LINK_TYPE_ALL_AVAILABLE_IMAGE: { final URLSpan[] spans = string.getSpans(0, string.length(), URLSpan.class); for (final URLSpan span : spans) { final Matcher matcher = PATTERN_ALL_AVAILABLE_IMAGES.matcher(span.getURL()); if (matcher.matches()) { final ImageSpec spec = getAllAvailableImage(matcher.group()); final int start = string.getSpanStart(span); final int end = string.getSpanEnd(span); if (spec == null || start < 0 || end > string.length() || start > end) { continue; } final String url = spec.full_image_link; string.removeSpan(span); applyLink(url, start, end, string, LINK_TYPE_LINK_WITH_IMAGE_EXTENSION); } } break; } case LINK_TYPE_CASHTAG: { addCashtagLinks(string); break; } default: { return; } } view.setText(string); addLinkMovementMethod(view); } public OnLinkClickListener getmOnLinkClickListener() { return mOnLinkClickListener; } public void setOnLinkClickListener(final OnLinkClickListener listener) { mOnLinkClickListener = listener; } private final boolean addCashtagLinks(final Spannable spannable) { boolean hasMatches = false; final Matcher matcher = Regex.VALID_CASHTAG.matcher(spannable); while (matcher.find()) { final int start = matcherStart(matcher, Regex.VALID_CASHTAG_GROUP_CASHTAG_FULL); final int end = matcherEnd(matcher, Regex.VALID_CASHTAG_GROUP_CASHTAG_FULL); final String url = matcherGroup(matcher, Regex.VALID_CASHTAG_GROUP_TAG); applyLink(url, start, end, spannable, LINK_TYPE_HASHTAG); hasMatches = true; } return hasMatches; } private final boolean addHashtagLinks(final Spannable spannable) { boolean hasMatches = false; final Matcher matcher = Regex.VALID_HASHTAG.matcher(spannable); while (matcher.find()) { final int start = matcherStart(matcher, Regex.VALID_HASHTAG_GROUP_HASHTAG_FULL); final int end = matcherEnd(matcher, Regex.VALID_HASHTAG_GROUP_HASHTAG_FULL); final String url = matcherGroup(matcher, Regex.VALID_HASHTAG_GROUP_HASHTAG_FULL); applyLink(url, start, end, spannable, LINK_TYPE_HASHTAG); hasMatches = true; } return hasMatches; } private final boolean addMentionOrListLinks(final Spannable spannable) { boolean hasMatches = false; final Matcher matcher = Regex.VALID_MENTION_OR_LIST.matcher(spannable); while (matcher.find()) { final int start = matcherStart(matcher, Regex.VALID_MENTION_OR_LIST_GROUP_AT); final int username_end = matcherEnd(matcher, Regex.VALID_MENTION_OR_LIST_GROUP_USERNAME); final int list_start = matcherStart(matcher, Regex.VALID_MENTION_OR_LIST_GROUP_LIST); final int list_end = matcherEnd(matcher, Regex.VALID_MENTION_OR_LIST_GROUP_LIST); final String mention = matcherGroup(matcher, Regex.VALID_MENTION_OR_LIST_GROUP_USERNAME); final String list = matcherGroup(matcher, Regex.VALID_MENTION_OR_LIST_GROUP_LIST); applyLink(mention, start, username_end, spannable, LINK_TYPE_MENTION_LIST); if (list_start >= 0 && list_end >= 0) { applyLink(mention + "/" + list, list_start, list_end, spannable, LINK_TYPE_LIST); } hasMatches = true; } return hasMatches; } private final void applyLink(final String url, final int start, final int end, final Spannable text, final int type) { final LinkSpan span = new LinkSpan(url, type, mOnLinkClickListener); text.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } 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()); } } } 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 = makeUrl(m.group(0), schemes, m, transformFilter); spec.url = url; spec.start = start; spec.end = end; 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; final int length = prefixes.length; for (int i = 0; i < 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; } /** * 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); } public interface OnLinkClickListener { public void onLinkClick(String link, int type); } /** * 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); } static class LinkSpan extends URLSpan { private final int type; private final OnLinkClickListener listener; @Override public void updateDrawState(TextPaint ds) { ds.setColor(ds.linkColor); ds.setUnderlineText(false); } public LinkSpan(final String url, final int type, final OnLinkClickListener listener) { super(url); this.type = type; this.listener = listener; } @Override public void onClick(final View widget) { if (listener != null) { listener.onLinkClick(getURL(), type); } } } } class LinkSpec { String url; int start; int end; }