Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit fe20cdd9 authored by Abodunrinwa Toki's avatar Abodunrinwa Toki
Browse files

Smart Linkify API

Uses the TextClassifier to generate links on a background thread.
The links are applied on the calling thread.

Test: see topic
Bug: 67629726
Change-Id: I0f1940a2ffbf19f4436c0a20b0c62e6bbc03cd7a
parent 7c691c60
Loading
Loading
Loading
Loading
+24 −3
Original line number Original line Diff line number Diff line
@@ -44343,6 +44343,11 @@ package android.text.util {
    method public static final boolean addLinks(android.text.Spannable, java.util.regex.Pattern, java.lang.String);
    method public static final boolean addLinks(android.text.Spannable, java.util.regex.Pattern, java.lang.String);
    method public static final boolean addLinks(android.text.Spannable, java.util.regex.Pattern, java.lang.String, android.text.util.Linkify.MatchFilter, android.text.util.Linkify.TransformFilter);
    method public static final boolean addLinks(android.text.Spannable, java.util.regex.Pattern, java.lang.String, android.text.util.Linkify.MatchFilter, android.text.util.Linkify.TransformFilter);
    method public static final boolean addLinks(android.text.Spannable, java.util.regex.Pattern, java.lang.String, java.lang.String[], android.text.util.Linkify.MatchFilter, android.text.util.Linkify.TransformFilter);
    method public static final boolean addLinks(android.text.Spannable, java.util.regex.Pattern, java.lang.String, java.lang.String[], android.text.util.Linkify.MatchFilter, android.text.util.Linkify.TransformFilter);
    method public static java.util.concurrent.Future<java.lang.Void> addLinksAsync(android.widget.TextView, android.view.textclassifier.TextLinks.Options);
    method public static java.util.concurrent.Future<java.lang.Void> addLinksAsync(android.widget.TextView, android.view.textclassifier.TextLinks.Options, java.util.concurrent.Executor, java.util.function.Consumer<java.lang.Integer>);
    method public static java.util.concurrent.Future<java.lang.Void> addLinksAsync(android.text.Spannable, android.view.textclassifier.TextClassifier, android.view.textclassifier.TextLinks.Options);
    method public static java.util.concurrent.Future<java.lang.Void> addLinksAsync(android.text.Spannable, android.view.textclassifier.TextClassifier, int);
    method public static java.util.concurrent.Future<java.lang.Void> addLinksAsync(android.text.Spannable, android.view.textclassifier.TextClassifier, android.view.textclassifier.TextLinks.Options, java.util.concurrent.Executor, java.util.function.Consumer<java.lang.Integer>);
    field public static final int ALL = 15; // 0xf
    field public static final int ALL = 15; // 0xf
    field public static final int EMAIL_ADDRESSES = 2; // 0x2
    field public static final int EMAIL_ADDRESSES = 2; // 0x2
    field public static final int MAP_ADDRESSES = 8; // 0x8
    field public static final int MAP_ADDRESSES = 8; // 0x8
@@ -50125,32 +50130,42 @@ package android.view.textclassifier {
  }
  }
  public final class TextLinks implements android.os.Parcelable {
  public final class TextLinks implements android.os.Parcelable {
    method public boolean apply(android.text.SpannableString, java.util.function.Function<android.view.textclassifier.TextLinks.TextLink, android.text.style.ClickableSpan>);
    method public int describeContents();
    method public int describeContents();
    method public java.util.Collection<android.view.textclassifier.TextLinks.TextLink> getLinks();
    method public java.util.Collection<android.view.textclassifier.TextLinks.TextLink> getLinks();
    method public void writeToParcel(android.os.Parcel, int);
    method public void writeToParcel(android.os.Parcel, int);
    field public static final int APPLY_STRATEGY_IGNORE = 0; // 0x0
    field public static final int APPLY_STRATEGY_REPLACE = 1; // 0x1
    field public static final android.os.Parcelable.Creator<android.view.textclassifier.TextLinks> CREATOR;
    field public static final android.os.Parcelable.Creator<android.view.textclassifier.TextLinks> CREATOR;
    field public static final int STATUS_DIFFERENT_TEXT = 3; // 0x3
    field public static final int STATUS_LINKS_APPLIED = 0; // 0x0
    field public static final int STATUS_NO_LINKS_APPLIED = 2; // 0x2
    field public static final int STATUS_NO_LINKS_FOUND = 1; // 0x1
  }
  }
  public static final class TextLinks.Builder {
  public static final class TextLinks.Builder {
    ctor public TextLinks.Builder(java.lang.String);
    ctor public TextLinks.Builder(java.lang.String);
    method public android.view.textclassifier.TextLinks.Builder addLink(android.view.textclassifier.TextLinks.TextLink);
    method public android.view.textclassifier.TextLinks.Builder addLink(int, int, java.util.Map<java.lang.String, java.lang.Float>);
    method public android.view.textclassifier.TextLinks build();
    method public android.view.textclassifier.TextLinks build();
    method public android.view.textclassifier.TextLinks.Builder clearTextLinks();
  }
  }
  public static final class TextLinks.Options implements android.os.Parcelable {
  public static final class TextLinks.Options implements android.os.Parcelable {
    ctor public TextLinks.Options();
    ctor public TextLinks.Options();
    method public int describeContents();
    method public int describeContents();
    method public static android.view.textclassifier.TextLinks.Options fromLinkMask(int);
    method public int getApplyStrategy();
    method public android.os.LocaleList getDefaultLocales();
    method public android.os.LocaleList getDefaultLocales();
    method public android.view.textclassifier.TextClassifier.EntityConfig getEntityConfig();
    method public android.view.textclassifier.TextClassifier.EntityConfig getEntityConfig();
    method public java.util.function.Function<android.view.textclassifier.TextLinks.TextLink, android.view.textclassifier.TextLinks.TextLinkSpan> getSpanFactory();
    method public android.view.textclassifier.TextLinks.Options setApplyStrategy(int);
    method public android.view.textclassifier.TextLinks.Options setDefaultLocales(android.os.LocaleList);
    method public android.view.textclassifier.TextLinks.Options setDefaultLocales(android.os.LocaleList);
    method public android.view.textclassifier.TextLinks.Options setEntityConfig(android.view.textclassifier.TextClassifier.EntityConfig);
    method public android.view.textclassifier.TextLinks.Options setEntityConfig(android.view.textclassifier.TextClassifier.EntityConfig);
    method public android.view.textclassifier.TextLinks.Options setSpanFactory(java.util.function.Function<android.view.textclassifier.TextLinks.TextLink, android.view.textclassifier.TextLinks.TextLinkSpan>);
    method public void writeToParcel(android.os.Parcel, int);
    method public void writeToParcel(android.os.Parcel, int);
    field public static final android.os.Parcelable.Creator<android.view.textclassifier.TextLinks.Options> CREATOR;
    field public static final android.os.Parcelable.Creator<android.view.textclassifier.TextLinks.Options> CREATOR;
  }
  }
  public static final class TextLinks.TextLink implements android.os.Parcelable {
  public static final class TextLinks.TextLink implements android.os.Parcelable {
    ctor public TextLinks.TextLink(java.lang.String, int, int, java.util.Map<java.lang.String, java.lang.Float>);
    method public int describeContents();
    method public int describeContents();
    method public float getConfidenceScore(java.lang.String);
    method public float getConfidenceScore(java.lang.String);
    method public int getEnd();
    method public int getEnd();
@@ -50161,6 +50176,12 @@ package android.view.textclassifier {
    field public static final android.os.Parcelable.Creator<android.view.textclassifier.TextLinks.TextLink> CREATOR;
    field public static final android.os.Parcelable.Creator<android.view.textclassifier.TextLinks.TextLink> CREATOR;
  }
  }
  public static class TextLinks.TextLinkSpan extends android.text.style.ClickableSpan {
    ctor public TextLinks.TextLinkSpan(android.view.textclassifier.TextLinks.TextLink);
    method public final android.view.textclassifier.TextLinks.TextLink getTextLink();
    method public void onClick(android.view.View);
  }
  public final class TextSelection implements android.os.Parcelable {
  public final class TextSelection implements android.os.Parcelable {
    method public int describeContents();
    method public int describeContents();
    method public float getConfidenceScore(java.lang.String);
    method public float getConfidenceScore(java.lang.String);
+200 −0
Original line number Original line Diff line number Diff line
@@ -19,6 +19,7 @@ package android.text.util;
import android.annotation.IntDef;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.Nullable;
import android.annotation.UiThread;
import android.content.Context;
import android.content.Context;
import android.telephony.PhoneNumberUtils;
import android.telephony.PhoneNumberUtils;
import android.telephony.TelephonyManager;
import android.telephony.TelephonyManager;
@@ -29,12 +30,16 @@ import android.text.method.LinkMovementMethod;
import android.text.method.MovementMethod;
import android.text.method.MovementMethod;
import android.text.style.URLSpan;
import android.text.style.URLSpan;
import android.util.Patterns;
import android.util.Patterns;
import android.view.textclassifier.TextClassifier;
import android.view.textclassifier.TextLinks;
import android.view.textclassifier.TextLinks.TextLinkSpan;
import android.webkit.WebView;
import android.webkit.WebView;
import android.widget.TextView;
import android.widget.TextView;


import com.android.i18n.phonenumbers.PhoneNumberMatch;
import com.android.i18n.phonenumbers.PhoneNumberMatch;
import com.android.i18n.phonenumbers.PhoneNumberUtil;
import com.android.i18n.phonenumbers.PhoneNumberUtil;
import com.android.i18n.phonenumbers.PhoneNumberUtil.Leniency;
import com.android.i18n.phonenumbers.PhoneNumberUtil.Leniency;
import com.android.internal.util.Preconditions;


import libcore.util.EmptyArray;
import libcore.util.EmptyArray;


@@ -46,6 +51,12 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Collections;
import java.util.Comparator;
import java.util.Comparator;
import java.util.Locale;
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.Function;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.Pattern;


@@ -479,6 +490,195 @@ public class Linkify {
        return hasMatches;
        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 options optional parameters to specify how to generate the links
     *
     * @return a future that may be used to interrupt or query the background task
     */
    @UiThread
    public static Future<Void> addLinksAsync(
            @NonNull TextView textView,
            @Nullable TextLinks.Options options) {
        return addLinksAsync(textView, options, 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 options 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
     */
    @UiThread
    public static Future<Void> addLinksAsync(
            @NonNull TextView textView,
            @Nullable TextLinks.Options options,
            @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(),
                options, 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 options optional parameters to specify how to generate the links
     *
     * @return a future that may be used to interrupt or query the background task
     */
    public static Future<Void> addLinksAsync(
            @NonNull Spannable text,
            @NonNull TextClassifier classifier,
            @Nullable TextLinks.Options options) {
        return addLinksAsync(text, classifier, options, 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
     */
    public static Future<Void> addLinksAsync(
            @NonNull Spannable text,
            @NonNull TextClassifier classifier,
            @LinkifyMask int mask) {
        return addLinksAsync(text, classifier, TextLinks.Options.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 options 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
     */
    public static Future<Void> addLinksAsync(
            @NonNull Spannable text,
            @NonNull TextClassifier classifier,
            @Nullable TextLinks.Options options,
            @Nullable Executor executor,
            @Nullable Consumer<Integer> callback) {
        return addLinksAsync(text, classifier, options, executor, callback,
                null /* modifyTextView */);
    }

    private static Future<Void> addLinksAsync(
            @NonNull Spannable text,
            @NonNull TextClassifier classifier,
            @Nullable TextLinks.Options options,
            @Nullable Executor executor,
            @Nullable Consumer<Integer> callback,
            @Nullable Runnable modifyTextView) {
        Preconditions.checkNotNull(text);
        Preconditions.checkNotNull(classifier);
        final Supplier<TextLinks> supplier = () -> classifier.generateLinks(text, options);
        final Consumer<TextLinks> consumer = links -> {
            if (links.getLinks().isEmpty()) {
                if (callback != null) {
                    callback.accept(TextLinks.STATUS_NO_LINKS_FOUND);
                }
                return;
            }

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

            final Function<TextLinks.TextLink, TextLinkSpan> spanFactory = (options == null)
                    ? null : options.getSpanFactory();
            final @TextLinks.ApplyStrategy int applyStrategy = (options == null)
                    ? TextLinks.APPLY_STRATEGY_IGNORE : options.getApplyStrategy();
            final @TextLinks.Status int result =  links.apply(text, applyStrategy, spanFactory);
            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) {
    private static final void applyLink(String url, int start, int end, Spannable text) {
        URLSpan span = new URLSpan(url);
        URLSpan span = new URLSpan(url);


+1 −2
Original line number Original line Diff line number Diff line
@@ -221,8 +221,7 @@ public final class TextClassifierImpl implements TextClassifier {
                for (int i = 0; i < results.length; i++) {
                for (int i = 0; i < results.length; i++) {
                    entityScores.put(results[i].mCollection, results[i].mScore);
                    entityScores.put(results[i].mCollection, results[i].mScore);
                }
                }
                builder.addLink(new TextLinks.TextLink(
                builder.addLink(span.getStartIndex(), span.getEndIndex(), entityScores);
                        textString, span.getStartIndex(), span.getEndIndex(), entityScores));
            }
            }
        } catch (Throwable t) {
        } catch (Throwable t) {
            // Avoid throwing from this method. Log the error.
            // Avoid throwing from this method. Log the error.
+218 −42

File changed.

Preview size limit exceeded, changes collapsed.

+2 −2
Original line number Original line Diff line number Diff line
@@ -68,8 +68,8 @@ public class TextLinksTest {
    public void testParcel() {
    public void testParcel() {
        final String fullText = "this is just a test";
        final String fullText = "this is just a test";
        final TextLinks reference = new TextLinks.Builder(fullText)
        final TextLinks reference = new TextLinks.Builder(fullText)
                .addLink(new TextLinks.TextLink(fullText, 0, 4, getEntityScores(0.f, 0.f, 1.f)))
                .addLink(0, 4, getEntityScores(0.f, 0.f, 1.f))
                .addLink(new TextLinks.TextLink(fullText, 5, 12, getEntityScores(.8f, .1f, .5f)))
                .addLink(5, 12, getEntityScores(.8f, .1f, .5f))
                .build();
                .build();


        // Parcel and unparcel.
        // Parcel and unparcel.
Loading