/*
* 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 android.text.util;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityThread;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.os.Build;
import android.telephony.PhoneNumberUtils;
import android.telephony.TelephonyManager;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.text.method.MovementMethod;
import android.text.style.URLSpan;
import android.util.Log;
import android.util.Patterns;
import android.webkit.WebView;
import android.widget.TextView;
import com.android.i18n.phonenumbers.PhoneNumberMatch;
import com.android.i18n.phonenumbers.PhoneNumberUtil;
import com.android.i18n.phonenumbers.PhoneNumberUtil.Leniency;
import libcore.util.EmptyArray;
import java.io.UnsupportedEncodingException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Locale;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 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 http://
. If the pattern matches example.com, which
* does not have a URL scheme prefix, the supplied scheme will be prepended to
* create http://example.com
when the clickable URL link is
* created.
*
*
Note: When using {@link #MAP_ADDRESSES} or {@link #ALL} * to match street addresses on API level {@link android.os.Build.VERSION_CODES#O_MR1} * and earlier, methods in this class may throw * {@link android.util.AndroidRuntimeException} or other exceptions if the * device's WebView implementation is currently being updated, because * {@link android.webkit.WebView#findAddress} is required to match street * addresses. * * @see MatchFilter * @see TransformFilter */ public class Linkify { private static final String LOG_TAG = "Linkify"; /** * 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. Note that this should be avoided, as it uses the * {@link android.webkit.WebView#findAddress(String)} method, which has various * limitations and has been deprecated: see the documentation for * {@link android.webkit.WebView#findAddress(String)} for more information. * * @deprecated use {@link android.view.textclassifier.TextClassifier#generateLinks( * TextLinks.Request)} instead and avoid it even when targeting API levels where no alternative * is available. */ @Deprecated public static final int MAP_ADDRESSES = 0x08; /** * Bit mask indicating that all available patterns should be matched in * methods that take an options mask *
Note:
{@link #MAP_ADDRESSES} is deprecated. * Use {@link android.view.textclassifier.TextClassifier#generateLinks(TextLinks.Request)} * instead and avoid it even when targeting API levels where no alternative is available. */ public static final int ALL = WEB_URLS | EMAIL_ADDRESSES | PHONE_NUMBERS | 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; /** @hide */ @IntDef(flag = true, value = { WEB_URLS, EMAIL_ADDRESSES, PHONE_NUMBERS, MAP_ADDRESSES, ALL }) @Retention(RetentionPolicy.SOURCE) public @interface LinkifyMask {} /** * 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() { public final boolean acceptMatch(CharSequence s, int start, 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() { public final boolean acceptMatch(CharSequence s, int start, int end) { int digitCount = 0; for (int i = start; i < end; i++) { if (Character.isDigit(s.charAt(i))) { digitCount++; if (digitCount >= 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() { public final String transformUrl(final Matcher match, String url) { return Patterns.digitsAndPlusOnly(match); } }; /** * 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); } /** * 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. * * @param text Spannable whose text is to be marked-up with links * @param mask Mask to define which kinds of links will be searched. * * @return True if at least one link is found and applied. * * @see #addLinks(Spannable, int, Function) */ public static final boolean addLinks(@NonNull Spannable text, @LinkifyMask int mask) { return addLinks(text, mask, null, null); } /** * 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. * * @param text Spannable whose text is to be marked-up with links * @param mask mask to define which kinds of links will be searched * @param urlSpanFactory function used to create {@link URLSpan}s * @return True if at least one link is found and applied. */ public static final boolean addLinks(@NonNull Spannable text, @LinkifyMask int mask, @Nullable Functionhttp://
) to be
* prepended to the links that do not start with this scheme.
*/
public static final void addLinks(@NonNull TextView text, @NonNull Pattern pattern,
@Nullable String scheme) {
addLinks(text, pattern, scheme, null, 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 pattern Regex pattern to be used for finding links
* @param scheme URL scheme string (eg http://
) to be
* prepended to the links that do not start with this scheme.
* @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(@NonNull TextView text, @NonNull Pattern pattern,
@Nullable String scheme, @Nullable MatchFilter matchFilter,
@Nullable TransformFilter transformFilter) {
addLinks(text, pattern, scheme, null, matchFilter, transformFilter);
}
/**
* 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 defaultScheme The default scheme to be prepended to links if the link does not
* start with one of the schemes
given.
* @param schemes Array of schemes (eg http://
) to check if the link found
* contains a scheme. Passing a null or empty value means prepend defaultScheme
* to all links.
* @param matchFilter The filter that is used to allow the client code additional control
* over which pattern matches are to be converted into links.
* @param transformFilter Filter to allow the client code to update the link found.
*/
public static final void addLinks(@NonNull TextView text, @NonNull Pattern pattern,
@Nullable String defaultScheme, @Nullable String[] schemes,
@Nullable MatchFilter matchFilter, @Nullable TransformFilter transformFilter) {
SpannableString spannable = SpannableString.valueOf(text.getText());
boolean linksAdded = addLinks(spannable, pattern, defaultScheme, schemes, matchFilter,
transformFilter);
if (linksAdded) {
text.setText(spannable);
addLinkMovementMethod(text);
}
}
/**
* 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 http://
) to be
* prepended to the links that do not start with this scheme.
* @see #addLinks(Spannable, Pattern, String, String[], MatchFilter, TransformFilter, Function)
*/
public static final boolean addLinks(@NonNull Spannable text, @NonNull Pattern pattern,
@Nullable String scheme) {
return addLinks(text, pattern, scheme, null, null, null);
}
/**
* Applies a regex to a Spannable turning the matches into
* links.
*
* @param spannable 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 http://
) to be
* prepended to the links that do not start with this scheme.
* @param matchFilter The filter that is used to allow the client code
* additional control over which pattern matches are
* to be converted into links.
* @param transformFilter Filter to allow the client code to update the link found.
*
* @return True if at least one link is found and applied.
* @see #addLinks(Spannable, Pattern, String, String[], MatchFilter, TransformFilter, Function)
*/
public static final boolean addLinks(@NonNull Spannable spannable, @NonNull Pattern pattern,
@Nullable String scheme, @Nullable MatchFilter matchFilter,
@Nullable TransformFilter transformFilter) {
return addLinks(spannable, pattern, scheme, null, matchFilter,
transformFilter);
}
/**
* Applies a regex to a Spannable turning the matches into links.
*
* @param spannable Spannable whose text is to be marked-up with links.
* @param pattern Regex pattern to be used for finding links.
* @param defaultScheme The default scheme to be prepended to links if the link does not
* start with one of the schemes
given.
* @param schemes Array of schemes (eg http://
) to check if the link found
* contains a scheme. Passing a null or empty value means prepend defaultScheme
* to all links.
* @param matchFilter The filter that is used to allow the client code additional control
* over which pattern matches are to be converted into links.
* @param transformFilter Filter to allow the client code to update the link found.
*
* @return True if at least one link is found and applied.
*
* @see #addLinks(Spannable, Pattern, String, String[], MatchFilter, TransformFilter, Function)
*/
public static final boolean addLinks(@NonNull Spannable spannable, @NonNull Pattern pattern,
@Nullable String defaultScheme, @Nullable String[] schemes,
@Nullable MatchFilter matchFilter, @Nullable TransformFilter transformFilter) {
return addLinks(spannable, pattern, defaultScheme, schemes, matchFilter, transformFilter,
null);
}
/**
* Applies a regex to a Spannable turning the matches into links.
*
* @param spannable spannable whose text is to be marked-up with links.
* @param pattern regex pattern to be used for finding links.
* @param defaultScheme the default scheme to be prepended to links if the link does not
* start with one of the schemes
given.
* @param schemes array of schemes (eg http://
) to check if the link found
* contains a scheme. Passing a null or empty value means prepend
* defaultScheme
* to all links.
* @param matchFilter the filter that is used to allow the client code additional control
* over which pattern matches are to be converted into links.
* @param transformFilter filter to allow the client code to update the link found.
* @param urlSpanFactory function used to create {@link URLSpan}s
*
* @return True if at least one link is found and applied.
*/
public static final boolean addLinks(@NonNull Spannable spannable, @NonNull Pattern pattern,
@Nullable String defaultScheme, @Nullable String[] schemes,
@Nullable MatchFilter matchFilter, @Nullable TransformFilter transformFilter,
@Nullable Function