Linkify.java revision 047e816fe2e6c94ff425188c4b12f02921d3f5bf
15d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)/* 290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * Copyright (C) 2007 The Android Open Source Project 390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * 490dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * Licensed under the Apache License, Version 2.0 (the "License"); 55d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) * you may not use this file except in compliance with the License. 690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * You may obtain a copy of the License at 790dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * 8868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) * http://www.apache.org/licenses/LICENSE-2.0 9868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) * 1090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * Unless required by applicable law or agreed to in writing, software 1190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * distributed under the License is distributed on an "AS IS" BASIS, 12f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) * See the License for the specific language governing permissions and 14f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) * limitations under the License. 15f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) */ 1690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) 175d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)package android.text.util; 1890dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) 1990dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)import android.telephony.PhoneNumberUtils; 2090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)import android.text.method.LinkMovementMethod; 2190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)import android.text.method.MovementMethod; 2290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)import android.text.style.URLSpan; 2390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)import android.text.Spannable; 2490dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)import android.text.SpannableString; 2590dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)import android.text.Spanned; 2690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)import android.util.Patterns; 2790dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)import android.webkit.WebView; 2890dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)import android.widget.TextView; 2990dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) 3090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) 3190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)import java.io.UnsupportedEncodingException; 3290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)import java.net.URLEncoder; 3390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)import java.util.ArrayList; 3490dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)import java.util.Collections; 3590dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)import java.util.Comparator; 3690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)import java.util.Locale; 3790dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)import java.util.regex.Matcher; 385d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)import java.util.regex.Pattern; 395d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 4090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)import com.android.i18n.phonenumbers.PhoneNumberMatch; 4190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)import com.android.i18n.phonenumbers.PhoneNumberUtil; 4290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)import com.android.i18n.phonenumbers.PhoneNumberUtil.Leniency; 4390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) 4490dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)/** 4590dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * Linkify take a piece of text and a regular expression and turns all of the 4690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * regex matches in the text into clickable links. This is particularly 4790dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * useful for matching things like email addresses, web urls, etc. and making 4890dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * them actionable. 4990dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * 5090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * Alone with the pattern that is to be matched, a url scheme prefix is also 5190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * required. Any pattern match that does not begin with the supplied scheme 5290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * will have the scheme prepended to the matched text when the clickable url 5390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * is created. For instance, if you are matching web urls you would supply 5490dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * the scheme <code>http://</code>. If the pattern matches example.com, which 5590dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * does not have a url scheme prefix, the supplied scheme will be prepended to 5690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * create <code>http://example.com</code> when the clickable url link is 5790dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * created. 5890dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) */ 5990dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) 6090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)public class Linkify { 6190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) /** 6290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * Bit field indicating that web URLs should be matched in methods that 6390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * take an options mask 6490dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) */ 6590dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) public static final int WEB_URLS = 0x01; 6690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) 6790dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) /** 6890dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * Bit field indicating that email addresses should be matched in methods 6990dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * that take an options mask 7090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) */ 7190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) public static final int EMAIL_ADDRESSES = 0x02; 7290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) 7390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) /** 7490dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * Bit field indicating that phone numbers should be matched in methods that 7590dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * take an options mask 7690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) */ 7790dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) public static final int PHONE_NUMBERS = 0x04; 7890dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) 7990dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) /** 8090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * Bit field indicating that street addresses should be matched in methods that 8190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * take an options mask 8290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) */ 8390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) public static final int MAP_ADDRESSES = 0x08; 8490dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) 8590dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) /** 8690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * Bit mask indicating that all available patterns should be matched in 8790dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * methods that take an options mask 8890dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) */ 8990dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) public static final int ALL = WEB_URLS | EMAIL_ADDRESSES | PHONE_NUMBERS | MAP_ADDRESSES; 9090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) 9190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) /** 9290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * Don't treat anything with fewer than this many digits as a 9390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * phone number. 9490dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) */ 9590dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) private static final int PHONE_NUMBER_MINIMUM_DIGITS = 5; 9690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) 9790dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) /** 9890dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * Filters out web URL matches that occur after an at-sign (@). This is 9990dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * to prevent turning the domain name in an email address into a web link. 10090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) */ 10190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) public static final MatchFilter sUrlMatchFilter = new MatchFilter() { 10290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) public final boolean acceptMatch(CharSequence s, int start, int end) { 10390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) if (start == 0) { 10490dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) return true; 10590dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) } 10690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) 10790dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) if (s.charAt(start - 1) == '@') { 10890dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) return false; 10990dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) } 11090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) 11190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) return true; 11290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) } 11390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) }; 11490dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) 11590dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) /** 11690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * Filters out URL matches that don't have enough digits to be a 11790dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * phone number. 11890dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) */ 11990dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) public static final MatchFilter sPhoneNumberMatchFilter = new MatchFilter() { 12090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) public final boolean acceptMatch(CharSequence s, int start, int end) { 12190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) int digitCount = 0; 12290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) 12390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) for (int i = start; i < end; i++) { 12490dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) if (Character.isDigit(s.charAt(i))) { 12590dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) digitCount++; 12690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) if (digitCount >= PHONE_NUMBER_MINIMUM_DIGITS) { 12790dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) return true; 12890dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) } 12990dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) } 13090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) } 13190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) return false; 13290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) } 13390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) }; 13490dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) 13590dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) /** 13690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * Transforms matched phone number text into something suitable 13790dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * to be used in a tel: URL. It does this by removing everything 13890dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * but the digits and plus signs. For instance: 13990dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * '+1 (919) 555-1212' 14090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * becomes '+19195551212' 14190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) */ 14290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) public static final TransformFilter sPhoneNumberTransformFilter = new TransformFilter() { 14390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) public final String transformUrl(final Matcher match, String url) { 14490dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) return Patterns.digitsAndPlusOnly(match); 14590dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) } 14690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) }; 14790dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) 14890dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) /** 14990dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * MatchFilter enables client code to have more control over 15090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * what is allowed to match and become a link, and what is not. 15190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * 15290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * For example: when matching web urls you would like things like 15390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * http://www.example.com to match, as well as just example.com itelf. 15490dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * However, you would not want to match against the domain in 15590dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * support@example.com. So, when matching against a web url pattern you 15690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * might also include a MatchFilter that disallows the match if it is 15790dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * immediately preceded by an at-sign (@). 15890dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) */ 15990dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) public interface MatchFilter { 16090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) /** 16190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * Examines the character span matched by the pattern and determines 16290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * if the match should be turned into an actionable link. 16390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * 16490dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * @param s The body of text against which the pattern 16590dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * was matched 16690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * @param start The index of the first character in s that was 16790dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * matched by the pattern - inclusive 16890dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * @param end The index of the last character in s that was 169f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) * matched - exclusive 17090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * 17190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * @return Whether this match should be turned into a link 17290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) */ 17390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) boolean acceptMatch(CharSequence s, int start, int end); 17490dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) } 17590dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) 17690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) /** 17790dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * TransformFilter enables client code to have more control over 178f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) * how matched patterns are represented as URLs. 17990dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * 18090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * For example: when converting a phone number such as (919) 555-1212 181f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) * into a tel: URL the parentheses, white space, and hyphen need to be 18290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * removed to produce tel:9195551212. 18390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) */ 18490dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) public interface TransformFilter { 18590dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) /** 18690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * Examines the matched text and either passes it through or uses the 18790dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * data in the Matcher state to produce a replacement. 18890dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * 18990dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * @param match The regex matcher state that found this URL text 19090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * @param url The text that was matched 19190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * 19290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) * @return The transformed form of the URL 1935d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) */ 194 String transformUrl(final Matcher match, String url); 195 } 196 197 /** 198 * Scans the text of the provided Spannable and turns all occurrences 199 * of the link types indicated in the mask into clickable links. 200 * If the mask is nonzero, it also removes any existing URLSpans 201 * attached to the Spannable, to avoid problems if you call it 202 * repeatedly on the same text. 203 */ 204 public static final boolean addLinks(Spannable text, int mask) { 205 if (mask == 0) { 206 return false; 207 } 208 209 URLSpan[] old = text.getSpans(0, text.length(), URLSpan.class); 210 211 for (int i = old.length - 1; i >= 0; i--) { 212 text.removeSpan(old[i]); 213 } 214 215 ArrayList<LinkSpec> links = new ArrayList<LinkSpec>(); 216 217 if ((mask & WEB_URLS) != 0) { 218 gatherLinks(links, text, Patterns.WEB_URL, 219 new String[] { "http://", "https://", "rtsp://" }, 220 sUrlMatchFilter, null); 221 } 222 223 if ((mask & EMAIL_ADDRESSES) != 0) { 224 gatherLinks(links, text, Patterns.EMAIL_ADDRESS, 225 new String[] { "mailto:" }, 226 null, null); 227 } 228 229 if ((mask & PHONE_NUMBERS) != 0) { 230 gatherTelLinks(links, text); 231 } 232 233 if ((mask & MAP_ADDRESSES) != 0) { 234 gatherMapLinks(links, text); 235 } 236 237 pruneOverlaps(links); 238 239 if (links.size() == 0) { 240 return false; 241 } 242 243 for (LinkSpec link: links) { 244 applyLink(link.url, link.start, link.end, text); 245 } 246 247 return true; 248 } 249 250 /** 251 * Scans the text of the provided TextView and turns all occurrences of 252 * the link types indicated in the mask into clickable links. If matches 253 * are found the movement method for the TextView is set to 254 * LinkMovementMethod. 255 */ 256 public static final boolean addLinks(TextView text, int mask) { 257 if (mask == 0) { 258 return false; 259 } 260 261 CharSequence t = text.getText(); 262 263 if (t instanceof Spannable) { 264 if (addLinks((Spannable) t, mask)) { 265 addLinkMovementMethod(text); 266 return true; 267 } 268 269 return false; 270 } else { 271 SpannableString s = SpannableString.valueOf(t); 272 273 if (addLinks(s, mask)) { 274 addLinkMovementMethod(text); 275 text.setText(s); 276 277 return true; 278 } 279 280 return false; 281 } 282 } 283 284 private static final void addLinkMovementMethod(TextView t) { 285 MovementMethod m = t.getMovementMethod(); 286 287 if ((m == null) || !(m instanceof LinkMovementMethod)) { 288 if (t.getLinksClickable()) { 289 t.setMovementMethod(LinkMovementMethod.getInstance()); 290 } 291 } 292 } 293 294 /** 295 * Applies a regex to the text of a TextView turning the matches into 296 * links. If links are found then UrlSpans are applied to the link 297 * text match areas, and the movement method for the text is changed 298 * to LinkMovementMethod. 299 * 300 * @param text TextView whose text is to be marked-up with links 301 * @param pattern Regex pattern to be used for finding links 302 * @param scheme Url scheme string (eg <code>http://</code> to be 303 * prepended to the url of links that do not have 304 * a scheme specified in the link text 305 */ 306 public static final void addLinks(TextView text, Pattern pattern, String scheme) { 307 addLinks(text, pattern, scheme, null, null); 308 } 309 310 /** 311 * Applies a regex to the text of a TextView turning the matches into 312 * links. If links are found then UrlSpans are applied to the link 313 * text match areas, and the movement method for the text is changed 314 * to LinkMovementMethod. 315 * 316 * @param text TextView whose text is to be marked-up with links 317 * @param p Regex pattern to be used for finding links 318 * @param scheme Url scheme string (eg <code>http://</code> to be 319 * prepended to the url of links that do not have 320 * a scheme specified in the link text 321 * @param matchFilter The filter that is used to allow the client code 322 * additional control over which pattern matches are 323 * to be converted into links. 324 */ 325 public static final void addLinks(TextView text, Pattern p, String scheme, 326 MatchFilter matchFilter, TransformFilter transformFilter) { 327 SpannableString s = SpannableString.valueOf(text.getText()); 328 329 if (addLinks(s, p, scheme, matchFilter, transformFilter)) { 330 text.setText(s); 331 addLinkMovementMethod(text); 332 } 333 } 334 335 /** 336 * Applies a regex to a Spannable turning the matches into 337 * links. 338 * 339 * @param text Spannable whose text is to be marked-up with 340 * links 341 * @param pattern Regex pattern to be used for finding links 342 * @param scheme Url scheme string (eg <code>http://</code> to be 343 * prepended to the url of links that do not have 344 * a scheme specified in the link text 345 */ 346 public static final boolean addLinks(Spannable text, Pattern pattern, String scheme) { 347 return addLinks(text, pattern, scheme, null, null); 348 } 349 350 /** 351 * Applies a regex to a Spannable turning the matches into 352 * links. 353 * 354 * @param s Spannable whose text is to be marked-up with 355 * links 356 * @param p Regex pattern to be used for finding links 357 * @param scheme Url scheme string (eg <code>http://</code> to be 358 * prepended to the url of links that do not have 359 * a scheme specified in the link text 360 * @param matchFilter The filter that is used to allow the client code 361 * additional control over which pattern matches are 362 * to be converted into links. 363 */ 364 public static final boolean addLinks(Spannable s, Pattern p, 365 String scheme, MatchFilter matchFilter, 366 TransformFilter transformFilter) { 367 boolean hasMatches = false; 368 String prefix = (scheme == null) ? "" : scheme.toLowerCase(Locale.ROOT); 369 Matcher m = p.matcher(s); 370 371 while (m.find()) { 372 int start = m.start(); 373 int end = m.end(); 374 boolean allowed = true; 375 376 if (matchFilter != null) { 377 allowed = matchFilter.acceptMatch(s, start, end); 378 } 379 380 if (allowed) { 381 String url = makeUrl(m.group(0), new String[] { prefix }, 382 m, transformFilter); 383 384 applyLink(url, start, end, s); 385 hasMatches = true; 386 } 387 } 388 389 return hasMatches; 390 } 391 392 private static final void applyLink(String url, int start, int end, Spannable text) { 393 URLSpan span = new URLSpan(url); 394 395 text.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 396 } 397 398 private static final String makeUrl(String url, String[] prefixes, 399 Matcher m, TransformFilter filter) { 400 if (filter != null) { 401 url = filter.transformUrl(m, url); 402 } 403 404 boolean hasPrefix = false; 405 406 for (int i = 0; i < prefixes.length; i++) { 407 if (url.regionMatches(true, 0, prefixes[i], 0, 408 prefixes[i].length())) { 409 hasPrefix = true; 410 411 // Fix capitalization if necessary 412 if (!url.regionMatches(false, 0, prefixes[i], 0, 413 prefixes[i].length())) { 414 url = prefixes[i] + url.substring(prefixes[i].length()); 415 } 416 417 break; 418 } 419 } 420 421 if (!hasPrefix) { 422 url = prefixes[0] + url; 423 } 424 425 return url; 426 } 427 428 private static final void gatherLinks(ArrayList<LinkSpec> links, 429 Spannable s, Pattern pattern, String[] schemes, 430 MatchFilter matchFilter, TransformFilter transformFilter) { 431 Matcher m = pattern.matcher(s); 432 433 while (m.find()) { 434 int start = m.start(); 435 int end = m.end(); 436 437 if (matchFilter == null || matchFilter.acceptMatch(s, start, end)) { 438 LinkSpec spec = new LinkSpec(); 439 String url = makeUrl(m.group(0), schemes, m, transformFilter); 440 441 spec.url = url; 442 spec.start = start; 443 spec.end = end; 444 445 links.add(spec); 446 } 447 } 448 } 449 450 private static final void gatherTelLinks(ArrayList<LinkSpec> links, Spannable s) { 451 PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance(); 452 Iterable<PhoneNumberMatch> matches = phoneUtil.findNumbers(s.toString(), 453 Locale.getDefault().getCountry(), Leniency.POSSIBLE, Long.MAX_VALUE); 454 for (PhoneNumberMatch match : matches) { 455 LinkSpec spec = new LinkSpec(); 456 spec.url = "tel:" + PhoneNumberUtils.normalizeNumber(match.rawString()); 457 spec.start = match.start(); 458 spec.end = match.end(); 459 links.add(spec); 460 } 461 } 462 463 private static final void gatherMapLinks(ArrayList<LinkSpec> links, Spannable s) { 464 String string = s.toString(); 465 String address; 466 int base = 0; 467 468 try { 469 while ((address = WebView.findAddress(string)) != null) { 470 int start = string.indexOf(address); 471 472 if (start < 0) { 473 break; 474 } 475 476 LinkSpec spec = new LinkSpec(); 477 int length = address.length(); 478 int end = start + length; 479 480 spec.start = base + start; 481 spec.end = base + end; 482 string = string.substring(end); 483 base += end; 484 485 String encodedAddress = null; 486 487 try { 488 encodedAddress = URLEncoder.encode(address,"UTF-8"); 489 } catch (UnsupportedEncodingException e) { 490 continue; 491 } 492 493 spec.url = "geo:0,0?q=" + encodedAddress; 494 links.add(spec); 495 } 496 } catch (UnsupportedOperationException e) { 497 // findAddress may fail with an unsupported exception on platforms without a WebView. 498 // In this case, we will not append anything to the links variable: it would have died 499 // in WebView.findAddress. 500 return; 501 } 502 } 503 504 private static final void pruneOverlaps(ArrayList<LinkSpec> links) { 505 Comparator<LinkSpec> c = new Comparator<LinkSpec>() { 506 public final int compare(LinkSpec a, LinkSpec b) { 507 if (a.start < b.start) { 508 return -1; 509 } 510 511 if (a.start > b.start) { 512 return 1; 513 } 514 515 if (a.end < b.end) { 516 return 1; 517 } 518 519 if (a.end > b.end) { 520 return -1; 521 } 522 523 return 0; 524 } 525 526 public final boolean equals(Object o) { 527 return false; 528 } 529 }; 530 531 Collections.sort(links, c); 532 533 int len = links.size(); 534 int i = 0; 535 536 while (i < len - 1) { 537 LinkSpec a = links.get(i); 538 LinkSpec b = links.get(i + 1); 539 int remove = -1; 540 541 if ((a.start <= b.start) && (a.end > b.start)) { 542 if (b.end <= a.end) { 543 remove = i + 1; 544 } else if ((a.end - a.start) > (b.end - b.start)) { 545 remove = i + 1; 546 } else if ((a.end - a.start) < (b.end - b.start)) { 547 remove = i; 548 } 549 550 if (remove != -1) { 551 links.remove(remove); 552 len--; 553 continue; 554 } 555 556 } 557 558 i++; 559 } 560 } 561} 562 563class LinkSpec { 564 String url; 565 int start; 566 int end; 567} 568