android.text.util.Linkify.java Source code

Java tutorial

Introduction

Here is the source code for android.text.util.Linkify.java

Source

/*
 * 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.annotation.UiThread;
import android.annotation.UnsupportedAppUsage;
import android.content.Context;
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.view.textclassifier.TextClassifier;
import android.view.textclassifier.TextLinks;
import android.view.textclassifier.TextLinks.TextLinkSpan;
import android.view.textclassifier.TextLinksParams;
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 com.android.internal.util.Preconditions;

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.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
import java.util.function.Consumer;
import java.util.function.Supplier;
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 <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 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 uses the
     *  {@link android.webkit.WebView#findAddress(String) findAddress()} method in
     *  {@link android.webkit.WebView} for finding addresses, which has various
     *  limitations and has been deprecated.
     *  @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
     *  <p><strong>Note:</strong></p> {@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:
     *  &apos;+1 (919) 555-1212&apos;
     *  becomes &apos;+19195551212&apos;
     */
    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.
     */
    public static final boolean addLinks(@NonNull Spannable text, @LinkifyMask int mask) {
        return addLinks(text, mask, null);
    }

    private static boolean addLinks(@NonNull Spannable text, @LinkifyMask int mask, @Nullable Context context) {
        if (text != null && containsUnsupportedCharacters(text.toString())) {
            android.util.EventLog.writeEvent(0x534e4554, "116321860", -1, "");
            return false;
        }

        if (mask == 0) {
            return false;
        }

        URLSpan[] old = text.getSpans(0, text.length(), URLSpan.class);

        for (int i = old.length - 1; i >= 0; i--) {
            text.removeSpan(old[i]);
        }

        ArrayList<LinkSpec> links = new ArrayList<LinkSpec>();

        if ((mask & WEB_URLS) != 0) {
            gatherLinks(links, text, Patterns.AUTOLINK_WEB_URL, new String[] { "http://", "https://", "rtsp://" },
                    sUrlMatchFilter, null);
        }

        if ((mask & EMAIL_ADDRESSES) != 0) {
            gatherLinks(links, text, Patterns.AUTOLINK_EMAIL_ADDRESS, new String[] { "mailto:" }, null, null);
        }

        if ((mask & PHONE_NUMBERS) != 0) {
            gatherTelLinks(links, text, context);
        }

        if ((mask & MAP_ADDRESSES) != 0) {
            gatherMapLinks(links, text);
        }

        pruneOverlaps(links);

        if (links.size() == 0) {
            return false;
        }

        for (LinkSpec link : links) {
            applyLink(link.url, link.start, link.end, text);
        }

        return true;
    }

    /**
     * Returns true if the specified text contains at least one unsupported character for applying
     * links. Also logs the error.
     *
     * @param text the text to apply links to
     * @hide
     */
    public static boolean containsUnsupportedCharacters(String text) {
        if (text.contains("\u202C")) {
            Log.e(LOG_TAG, "Unsupported character for applying links: u202C");
            return true;
        }
        if (text.contains("\u202D")) {
            Log.e(LOG_TAG, "Unsupported character for applying links: u202D");
            return true;
        }
        if (text.contains("\u202E")) {
            Log.e(LOG_TAG, "Unsupported character for applying links: u202E");
            return true;
        }
        return false;
    }

    /**
     *  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.
     *
     *  @param text TextView 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.
     */
    public static final boolean addLinks(@NonNull TextView text, @LinkifyMask int mask) {
        if (mask == 0) {
            return false;
        }

        final Context context = text.getContext();
        final CharSequence t = text.getText();
        if (t instanceof Spannable) {
            if (addLinks((Spannable) t, mask, context)) {
                addLinkMovementMethod(text);
                return true;
            }

            return false;
        } else {
            SpannableString s = SpannableString.valueOf(t);

            if (addLinks(s, mask, context)) {
                addLinkMovementMethod(text);
                text.setText(s);

                return true;
            }

            return false;
        }
    }

    private static final void addLinkMovementMethod(@NonNull TextView t) {
        MovementMethod m = t.getMovementMethod();

        if ((m == null) || !(m instanceof LinkMovementMethod)) {
            if (t.getLinksClickable()) {
                t.setMovementMethod(LinkMovementMethod.getInstance());
            }
        }
    }

    /**
     *  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 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 <code>http://</code>) 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 <code>schemes</code> given.
     *  @param schemes Array of schemes (eg <code>http://</code>) 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 <code>http://</code>) to be
     *                      prepended to the links that do not start with this scheme.
     */
    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 <code>http://</code>) 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.
     */
    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 <code>schemes</code> given.
     * @param schemes Array of schemes (eg <code>http://</code>) 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.
     */
    public static final boolean addLinks(@NonNull Spannable spannable, @NonNull Pattern pattern,
            @Nullable String defaultScheme, @Nullable String[] schemes, @Nullable MatchFilter matchFilter,
            @Nullable TransformFilter transformFilter) {
        if (spannable != null && containsUnsupportedCharacters(spannable.toString())) {
            android.util.EventLog.writeEvent(0x534e4554, "116321860", -1, "");
            return false;
        }

        final String[] schemesCopy;
        if (defaultScheme == null)
            defaultScheme = "";
        if (schemes == null || schemes.length < 1) {
            schemes = EmptyArray.STRING;
        }

        schemesCopy = new String[schemes.length + 1];
        schemesCopy[0] = defaultScheme.toLowerCase(Locale.ROOT);
        for (int index = 0; index < schemes.length; index++) {
            String scheme = schemes[index];
            schemesCopy[index + 1] = (scheme == null) ? "" : scheme.toLowerCase(Locale.ROOT);
        }

        boolean hasMatches = false;
        Matcher m = pattern.matcher(spannable);

        while (m.find()) {
            int start = m.start();
            int end = m.end();
            boolean allowed = true;

            if (matchFilter != null) {
                allowed = matchFilter.acceptMatch(spannable, start, end);
            }

            if (allowed) {
                String url = makeUrl(m.group(0), schemesCopy, m, transformFilter);

                applyLink(url, start, end, spannable);
                hasMatches = true;
            }
        }

        return hasMatches;
    }

    /**
     * Scans the text of the provided TextView and turns all occurrences of the entity types
     * specified by {@code options} into clickable links. If links are found, this method
     * removes any pre-existing {@link TextLinkSpan} attached to the text (to avoid
     * problems if you call it repeatedly on the same text) and sets the movement method for the
     * TextView to LinkMovementMethod.
     *
     * <p><strong>Note:</strong> This method returns immediately but generates the links with
     * the specified classifier on a background thread. The generated links are applied on the
     * calling thread.
     *
     * @param textView TextView whose text is to be marked-up with links
     * @param params optional parameters to specify how to generate the links
     *
     * @return a future that may be used to interrupt or query the background task
     * @hide
     */
    @UiThread
    public static Future<Void> addLinksAsync(@NonNull TextView textView, @Nullable TextLinksParams params) {
        return addLinksAsync(textView, params, null /* executor */, null /* callback */);
    }

    /**
     * Scans the text of the provided TextView and turns all occurrences of the entity types
     * specified by {@code options} into clickable links. If links are found, this method
     * removes any pre-existing {@link TextLinkSpan} attached to the text (to avoid
     * problems if you call it repeatedly on the same text) and sets the movement method for the
     * TextView to LinkMovementMethod.
     *
     * <p><strong>Note:</strong> This method returns immediately but generates the links with
     * the specified classifier on a background thread. The generated links are applied on the
     * calling thread.
     *
     * @param textView TextView whose text is to be marked-up with links
     * @param mask mask to define which kinds of links will be generated
     *
     * @return a future that may be used to interrupt or query the background task
     * @hide
     */
    @UiThread
    public static Future<Void> addLinksAsync(@NonNull TextView textView, @LinkifyMask int mask) {
        return addLinksAsync(textView, TextLinksParams.fromLinkMask(mask), null /* executor */,
                null /* callback */);
    }

    /**
     * Scans the text of the provided TextView and turns all occurrences of the entity types
     * specified by {@code options} into clickable links. If links are found, this method
     * removes any pre-existing {@link TextLinkSpan} attached to the text (to avoid
     * problems if you call it repeatedly on the same text) and sets the movement method for the
     * TextView to LinkMovementMethod.
     *
     * <p><strong>Note:</strong> This method returns immediately but generates the links with
     * the specified classifier on a background thread. The generated links are applied on the
     * calling thread.
     *
     * @param textView TextView whose text is to be marked-up with links
     * @param params optional parameters to specify how to generate the links
     * @param executor Executor that runs the background task
     * @param callback Callback that receives the final status of the background task execution
     *
     * @return a future that may be used to interrupt or query the background task
     * @hide
     */
    @UiThread
    public static Future<Void> addLinksAsync(@NonNull TextView textView, @Nullable TextLinksParams params,
            @Nullable Executor executor, @Nullable Consumer<Integer> callback) {
        Preconditions.checkNotNull(textView);
        final CharSequence text = textView.getText();
        final Spannable spannable = (text instanceof Spannable) ? (Spannable) text : SpannableString.valueOf(text);
        final Runnable modifyTextView = () -> {
            addLinkMovementMethod(textView);
            if (spannable != text) {
                textView.setText(spannable);
            }
        };
        return addLinksAsync(spannable, textView.getTextClassifier(), params, executor, callback, modifyTextView);
    }

    /**
     * Scans the text of the provided TextView and turns all occurrences of the entity types
     * specified by {@code options} into clickable links. If links are found, this method
     * removes any pre-existing {@link TextLinkSpan} attached to the text to avoid
     * problems if you call it repeatedly on the same text.
     *
     * <p><strong>Note:</strong> This method returns immediately but generates the links with
     * the specified classifier on a background thread. The generated links are applied on the
     * calling thread.
     *
     * <p><strong>Note:</strong> If the text is currently attached to a TextView, this method
     * should be called on the UI thread.
     *
     * @param text Spannable whose text is to be marked-up with links
     * @param classifier the TextClassifier to use to generate the links
     * @param params optional parameters to specify how to generate the links
     *
     * @return a future that may be used to interrupt or query the background task
     * @hide
     */
    public static Future<Void> addLinksAsync(@NonNull Spannable text, @NonNull TextClassifier classifier,
            @Nullable TextLinksParams params) {
        return addLinksAsync(text, classifier, params, null /* executor */, null /* callback */);
    }

    /**
     * Scans the text of the provided TextView and turns all occurrences of the entity types
     * specified by the link {@code mask} into clickable links. If links are found, this method
     * removes any pre-existing {@link TextLinkSpan} attached to the text to avoid
     * problems if you call it repeatedly on the same text.
     *
     * <p><strong>Note:</strong> This method returns immediately but generates the links with
     * the specified classifier on a background thread. The generated links are applied on the
     * calling thread.
     *
     * <p><strong>Note:</strong> If the text is currently attached to a TextView, this method
     * should be called on the UI thread.
     *
     * @param text Spannable whose text is to be marked-up with links
     * @param classifier the TextClassifier to use to generate the links
     * @param mask mask to define which kinds of links will be generated
     *
     * @return a future that may be used to interrupt or query the background task
     * @hide
     */
    public static Future<Void> addLinksAsync(@NonNull Spannable text, @NonNull TextClassifier classifier,
            @LinkifyMask int mask) {
        return addLinksAsync(text, classifier, TextLinksParams.fromLinkMask(mask), null /* executor */,
                null /* callback */);
    }

    /**
     * Scans the text of the provided TextView and turns all occurrences of the entity types
     * specified by {@code options} into clickable links. If links are found, this method
     * removes any pre-existing {@link TextLinkSpan} attached to the text to avoid
     * problems if you call it repeatedly on the same text.
     *
     * <p><strong>Note:</strong> This method returns immediately but generates the links with
     * the specified classifier on a background thread. The generated links are applied on the
     * calling thread.
     *
     * <p><strong>Note:</strong> If the text is currently attached to a TextView, this method
     * should be called on the UI thread.
     *
     * @param text Spannable whose text is to be marked-up with links
     * @param classifier the TextClassifier to use to generate the links
     * @param params optional parameters to specify how to generate the links
     * @param executor Executor that runs the background task
     * @param callback Callback that receives the final status of the background task execution
     *
     * @return a future that may be used to interrupt or query the background task
     * @hide
     */
    public static Future<Void> addLinksAsync(@NonNull Spannable text, @NonNull TextClassifier classifier,
            @Nullable TextLinksParams params, @Nullable Executor executor, @Nullable Consumer<Integer> callback) {
        return addLinksAsync(text, classifier, params, executor, callback, null /* modifyTextView */);
    }

    private static Future<Void> addLinksAsync(@NonNull Spannable text, @NonNull TextClassifier classifier,
            @Nullable TextLinksParams params, @Nullable Executor executor, @Nullable Consumer<Integer> callback,
            @Nullable Runnable modifyTextView) {
        Preconditions.checkNotNull(text);
        Preconditions.checkNotNull(classifier);

        // TODO: This is a bug. We shouldnot call getMaxGenerateLinksTextLength() on the UI thread.
        // The input text may exceed the maximum length the text classifier can handle. In such
        // cases, we process the text up to the maximum length.
        final CharSequence truncatedText = text.subSequence(0,
                Math.min(text.length(), classifier.getMaxGenerateLinksTextLength()));

        final TextClassifier.EntityConfig entityConfig = (params == null) ? null : params.getEntityConfig();
        final TextLinks.Request request = new TextLinks.Request.Builder(truncatedText).setLegacyFallback(true)
                .setEntityConfig(entityConfig).build();
        final Supplier<TextLinks> supplier = () -> classifier.generateLinks(request);
        final Consumer<TextLinks> consumer = links -> {
            if (links.getLinks().isEmpty()) {
                if (callback != null) {
                    callback.accept(TextLinks.STATUS_NO_LINKS_FOUND);
                }
                return;
            }

            // Remove spans only for the part of the text we generated links for.
            final TextLinkSpan[] old = text.getSpans(0, truncatedText.length(), TextLinkSpan.class);
            for (int i = old.length - 1; i >= 0; i--) {
                text.removeSpan(old[i]);
            }

            final @TextLinks.Status int result = params.apply(text, links);
            if (result == TextLinks.STATUS_LINKS_APPLIED) {
                if (modifyTextView != null) {
                    modifyTextView.run();
                }
            }
            if (callback != null) {
                callback.accept(result);
            }
        };
        if (executor == null) {
            return CompletableFuture.supplyAsync(supplier).thenAccept(consumer);
        } else {
            return CompletableFuture.supplyAsync(supplier, executor).thenAccept(consumer);
        }
    }

    private static final void applyLink(String url, int start, int end, Spannable text) {
        URLSpan span = new URLSpan(url);

        text.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    }

    private static final String makeUrl(@NonNull String url, @NonNull String[] prefixes, Matcher matcher,
            @Nullable TransformFilter filter) {
        if (filter != null) {
            url = filter.transformUrl(matcher, 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 && prefixes.length > 0) {
            url = prefixes[0] + url;
        }

        return url;
    }

    private static final void gatherLinks(ArrayList<LinkSpec> links, Spannable s, Pattern pattern, String[] schemes,
            MatchFilter matchFilter, TransformFilter transformFilter) {
        Matcher m = pattern.matcher(s);

        while (m.find()) {
            int start = m.start();
            int end = m.end();

            if (matchFilter == null || matchFilter.acceptMatch(s, start, end)) {
                LinkSpec spec = new LinkSpec();
                String url = makeUrl(m.group(0), schemes, m, transformFilter);

                spec.url = url;
                spec.start = start;
                spec.end = end;

                links.add(spec);
            }
        }
    }

    @UnsupportedAppUsage
    private static void gatherTelLinks(ArrayList<LinkSpec> links, Spannable s, @Nullable Context context) {
        PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();
        final TelephonyManager tm = (context == null) ? TelephonyManager.getDefault()
                : TelephonyManager.from(context);
        Iterable<PhoneNumberMatch> matches = phoneUtil.findNumbers(s.toString(),
                tm.getSimCountryIso().toUpperCase(Locale.US), Leniency.POSSIBLE, Long.MAX_VALUE);
        for (PhoneNumberMatch match : matches) {
            LinkSpec spec = new LinkSpec();
            spec.url = "tel:" + PhoneNumberUtils.normalizeNumber(match.rawString());
            spec.start = match.start();
            spec.end = match.end();
            links.add(spec);
        }
    }

    private static final void gatherMapLinks(ArrayList<LinkSpec> links, Spannable s) {
        String string = s.toString();
        String address;
        int base = 0;

        try {
            while ((address = WebView.findAddress(string)) != null) {
                int start = string.indexOf(address);

                if (start < 0) {
                    break;
                }

                LinkSpec spec = new LinkSpec();
                int length = address.length();
                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 (UnsupportedEncodingException e) {
                    continue;
                }

                spec.url = "geo:0,0?q=" + encodedAddress;
                links.add(spec);
            }
        } catch (UnsupportedOperationException e) {
            // findAddress may fail with an unsupported exception on platforms without a WebView.
            // In this case, we will not append anything to the links variable: it would have died
            // in WebView.findAddress.
            return;
        }
    }

    private static final void pruneOverlaps(ArrayList<LinkSpec> links) {
        Comparator<LinkSpec> c = new Comparator<LinkSpec>() {
            public final int compare(LinkSpec a, 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;
            }
        };

        Collections.sort(links, c);

        int len = links.size();
        int i = 0;

        while (i < len - 1) {
            LinkSpec a = links.get(i);
            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;
}