/*
 * Copyright 2017 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.view.textclassifier;

import android.annotation.FloatRange;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.os.LocaleList;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.Spannable;
import android.text.method.MovementMethod;
import android.text.style.ClickableSpan;
import android.text.style.URLSpan;
import android.text.util.Linkify;
import android.text.util.Linkify.LinkifyMask;
import android.view.View;
import android.view.textclassifier.TextClassifier.EntityType;
import android.widget.TextView;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.annotations.VisibleForTesting.Visibility;
import com.android.internal.util.Preconditions;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.function.Function;

A collection of links, representing subsequences of text and the entity types (phone number, address, url, etc) they may be.
/** * A collection of links, representing subsequences of text and the entity types (phone number, * address, url, etc) they may be. */
public final class TextLinks implements Parcelable {
Return status of an attempt to apply TextLinks to text.
@hide
/** * Return status of an attempt to apply TextLinks to text. * @hide */
@Retention(RetentionPolicy.SOURCE) @IntDef({STATUS_LINKS_APPLIED, STATUS_NO_LINKS_FOUND, STATUS_NO_LINKS_APPLIED, STATUS_DIFFERENT_TEXT}) public @interface Status {}
Links were successfully applied to the text.
/** Links were successfully applied to the text. */
public static final int STATUS_LINKS_APPLIED = 0;
No links exist to apply to text. Links count is zero.
/** No links exist to apply to text. Links count is zero. */
public static final int STATUS_NO_LINKS_FOUND = 1;
No links applied to text. The links were filtered out.
/** No links applied to text. The links were filtered out. */
public static final int STATUS_NO_LINKS_APPLIED = 2;
The specified text does not match the text used to generate the links.
/** The specified text does not match the text used to generate the links. */
public static final int STATUS_DIFFERENT_TEXT = 3;
@hide
/** @hide */
@Retention(RetentionPolicy.SOURCE) @IntDef({APPLY_STRATEGY_IGNORE, APPLY_STRATEGY_REPLACE}) public @interface ApplyStrategy {}
Do not replace ClickableSpans that exist where the TextLinkSpan needs to be applied to. Do not apply the TextLinkSpan.
/** * Do not replace {@link ClickableSpan}s that exist where the {@link TextLinkSpan} needs to * be applied to. Do not apply the TextLinkSpan. */
public static final int APPLY_STRATEGY_IGNORE = 0;
Replace any ClickableSpans that exist where the TextLinkSpan needs to be applied to.
/** * Replace any {@link ClickableSpan}s that exist where the {@link TextLinkSpan} needs to be * applied to. */
public static final int APPLY_STRATEGY_REPLACE = 1; private final String mFullText; private final List<TextLink> mLinks; private TextLinks(String fullText, ArrayList<TextLink> links) { mFullText = fullText; mLinks = Collections.unmodifiableList(links); }
Returns the text that was used to generate these links.
@hide
/** * Returns the text that was used to generate these links. * @hide */
@NonNull public String getText() { return mFullText; }
Returns an unmodifiable Collection of the links.
/** * Returns an unmodifiable Collection of the links. */
@NonNull public Collection<TextLink> getLinks() { return mLinks; }
Annotates the given text with the generated links. It will fail if the provided text doesn't match the original text used to create the TextLinks.

NOTE: It may be necessary to set a LinkMovementMethod on the TextView widget to properly handle links. See TextView.setMovementMethod(MovementMethod)

Params:
  • text – the text to apply the links to. Must match the original text
  • applyStrategy – the apply strategy used to determine how to apply links to text. e.g APPLY_STRATEGY_IGNORE
  • spanFactory – a custom span factory for converting TextLinks to TextLinkSpans. Set to null to use the default span factory.
Returns:a status code indicating whether or not the links were successfully applied e.g. STATUS_LINKS_APPLIED
/** * Annotates the given text with the generated links. It will fail if the provided text doesn't * match the original text used to create the TextLinks. * * <p><strong>NOTE: </strong>It may be necessary to set a LinkMovementMethod on the TextView * widget to properly handle links. See {@link TextView#setMovementMethod(MovementMethod)} * * @param text the text to apply the links to. Must match the original text * @param applyStrategy the apply strategy used to determine how to apply links to text. * e.g {@link TextLinks#APPLY_STRATEGY_IGNORE} * @param spanFactory a custom span factory for converting TextLinks to TextLinkSpans. * Set to {@code null} to use the default span factory. * * @return a status code indicating whether or not the links were successfully applied * e.g. {@link #STATUS_LINKS_APPLIED} */
@Status public int apply( @NonNull Spannable text, @ApplyStrategy int applyStrategy, @Nullable Function<TextLink, TextLinkSpan> spanFactory) { Preconditions.checkNotNull(text); return new TextLinksParams.Builder() .setApplyStrategy(applyStrategy) .setSpanFactory(spanFactory) .build() .apply(text, this); } @Override public String toString() { return String.format(Locale.US, "TextLinks{fullText=%s, links=%s}", mFullText, mLinks); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(mFullText); dest.writeTypedList(mLinks); } public static final Parcelable.Creator<TextLinks> CREATOR = new Parcelable.Creator<TextLinks>() { @Override public TextLinks createFromParcel(Parcel in) { return new TextLinks(in); } @Override public TextLinks[] newArray(int size) { return new TextLinks[size]; } }; private TextLinks(Parcel in) { mFullText = in.readString(); mLinks = in.createTypedArrayList(TextLink.CREATOR); }
A link, identifying a substring of text and possible entity types for it.
/** * A link, identifying a substring of text and possible entity types for it. */
public static final class TextLink implements Parcelable { private final EntityConfidence mEntityScores; private final int mStart; private final int mEnd; @Nullable final URLSpan mUrlSpan;
Create a new TextLink.
Params:
  • start – The start index of the identified subsequence
  • end – The end index of the identified subsequence
  • entityScores – A mapping of entity type to confidence score
  • urlSpan – An optional URLSpan to delegate to. NOTE: Not parcelled
Throws:
/** * Create a new TextLink. * * @param start The start index of the identified subsequence * @param end The end index of the identified subsequence * @param entityScores A mapping of entity type to confidence score * @param urlSpan An optional URLSpan to delegate to. NOTE: Not parcelled * * @throws IllegalArgumentException if entityScores is null or empty */
TextLink(int start, int end, Map<String, Float> entityScores, @Nullable URLSpan urlSpan) { Preconditions.checkNotNull(entityScores); Preconditions.checkArgument(!entityScores.isEmpty()); Preconditions.checkArgument(start <= end); mStart = start; mEnd = end; mEntityScores = new EntityConfidence(entityScores); mUrlSpan = urlSpan; }
Returns the start index of this link in the original text.
Returns:the start index
/** * Returns the start index of this link in the original text. * * @return the start index */
public int getStart() { return mStart; }
Returns the end index of this link in the original text.
Returns:the end index
/** * Returns the end index of this link in the original text. * * @return the end index */
public int getEnd() { return mEnd; }
Returns the number of entity types that have confidence scores.
Returns:the entity count
/** * Returns the number of entity types that have confidence scores. * * @return the entity count */
public int getEntityCount() { return mEntityScores.getEntities().size(); }
Returns the entity type at a given index. Entity types are sorted by confidence.
Returns:the entity type at the provided index
/** * Returns the entity type at a given index. Entity types are sorted by confidence. * * @return the entity type at the provided index */
@NonNull public @EntityType String getEntity(int index) { return mEntityScores.getEntities().get(index); }
Returns the confidence score for a particular entity type.
Params:
  • entityType – the entity type
/** * Returns the confidence score for a particular entity type. * * @param entityType the entity type */
public @FloatRange(from = 0.0, to = 1.0) float getConfidenceScore( @EntityType String entityType) { return mEntityScores.getConfidenceScore(entityType); } @Override public String toString() { return String.format(Locale.US, "TextLink{start=%s, end=%s, entityScores=%s, urlSpan=%s}", mStart, mEnd, mEntityScores, mUrlSpan); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { mEntityScores.writeToParcel(dest, flags); dest.writeInt(mStart); dest.writeInt(mEnd); } public static final Parcelable.Creator<TextLink> CREATOR = new Parcelable.Creator<TextLink>() { @Override public TextLink createFromParcel(Parcel in) { return new TextLink(in); } @Override public TextLink[] newArray(int size) { return new TextLink[size]; } }; private TextLink(Parcel in) { mEntityScores = EntityConfidence.CREATOR.createFromParcel(in); mStart = in.readInt(); mEnd = in.readInt(); mUrlSpan = null; } }
A request object for generating TextLinks.
/** * A request object for generating TextLinks. */
public static final class Request implements Parcelable { private final CharSequence mText; @Nullable private final LocaleList mDefaultLocales; @Nullable private final TextClassifier.EntityConfig mEntityConfig; private final boolean mLegacyFallback; private String mCallingPackageName; private Request( CharSequence text, LocaleList defaultLocales, TextClassifier.EntityConfig entityConfig, boolean legacyFallback, String callingPackageName) { mText = text; mDefaultLocales = defaultLocales; mEntityConfig = entityConfig; mLegacyFallback = legacyFallback; mCallingPackageName = callingPackageName; }
Returns the text to generate links for.
/** * Returns the text to generate links for. */
@NonNull public CharSequence getText() { return mText; }
Returns:ordered list of locale preferences that can be used to disambiguate the provided text
/** * @return ordered list of locale preferences that can be used to disambiguate * the provided text */
@Nullable public LocaleList getDefaultLocales() { return mDefaultLocales; }
See Also:
Returns:The config representing the set of entities to look for
/** * @return The config representing the set of entities to look for * @see Builder#setEntityConfig(TextClassifier.EntityConfig) */
@Nullable public TextClassifier.EntityConfig getEntityConfig() { return mEntityConfig; }
Returns whether the TextClassifier can fallback to legacy links if smart linkify is disabled. Note: This is not parcelled.
@hide
/** * Returns whether the TextClassifier can fallback to legacy links if smart linkify is * disabled. * <strong>Note: </strong>This is not parcelled. * @hide */
public boolean isLegacyFallback() { return mLegacyFallback; }
Sets the name of the package that requested the links to get generated.
/** * Sets the name of the package that requested the links to get generated. */
void setCallingPackageName(@Nullable String callingPackageName) { mCallingPackageName = callingPackageName; }
A builder for building TextLinks requests.
/** * A builder for building TextLinks requests. */
public static final class Builder { private final CharSequence mText; @Nullable private LocaleList mDefaultLocales; @Nullable private TextClassifier.EntityConfig mEntityConfig; private boolean mLegacyFallback = true; // Use legacy fall back by default. private String mCallingPackageName; public Builder(@NonNull CharSequence text) { mText = Preconditions.checkNotNull(text); }
Params:
  • defaultLocales – ordered list of locale preferences that may be used to disambiguate the provided text. If no locale preferences exist, set this to null or an empty locale list.
Returns:this builder
/** * @param defaultLocales ordered list of locale preferences that may be used to * disambiguate the provided text. If no locale preferences exist, * set this to null or an empty locale list. * @return this builder */
@NonNull public Builder setDefaultLocales(@Nullable LocaleList defaultLocales) { mDefaultLocales = defaultLocales; return this; }
Sets the entity configuration to use. This determines what types of entities the TextClassifier will look for. Set to null for the default entity config and teh TextClassifier will automatically determine what links to generate.
Returns:this builder
/** * Sets the entity configuration to use. This determines what types of entities the * TextClassifier will look for. * Set to {@code null} for the default entity config and teh TextClassifier will * automatically determine what links to generate. * * @return this builder */
@NonNull public Builder setEntityConfig(@Nullable TextClassifier.EntityConfig entityConfig) { mEntityConfig = entityConfig; return this; }
Sets whether the TextClassifier can fallback to legacy links if smart linkify is disabled.

Note: This is not parcelled.

Returns:this builder
@hide
/** * Sets whether the TextClassifier can fallback to legacy links if smart linkify is * disabled. * * <p><strong>Note: </strong>This is not parcelled. * * @return this builder * @hide */
@NonNull public Builder setLegacyFallback(boolean legacyFallback) { mLegacyFallback = legacyFallback; return this; }
Sets the name of the package that requested the links to get generated.
Returns:this builder
@hide
/** * Sets the name of the package that requested the links to get generated. * * @return this builder * @hide */
@NonNull public Builder setCallingPackageName(@Nullable String callingPackageName) { mCallingPackageName = callingPackageName; return this; }
Builds and returns the request object.
/** * Builds and returns the request object. */
@NonNull public Request build() { return new Request( mText, mDefaultLocales, mEntityConfig, mLegacyFallback, mCallingPackageName); } }
Returns:the name of the package that requested the links to get generated. TODO: make available as system API
@hide
/** * @return the name of the package that requested the links to get generated. * TODO: make available as system API * @hide */
@Nullable public String getCallingPackageName() { return mCallingPackageName; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(mText.toString()); dest.writeInt(mDefaultLocales != null ? 1 : 0); if (mDefaultLocales != null) { mDefaultLocales.writeToParcel(dest, flags); } dest.writeInt(mEntityConfig != null ? 1 : 0); if (mEntityConfig != null) { mEntityConfig.writeToParcel(dest, flags); } dest.writeString(mCallingPackageName); } public static final Parcelable.Creator<Request> CREATOR = new Parcelable.Creator<Request>() { @Override public Request createFromParcel(Parcel in) { return new Request(in); } @Override public Request[] newArray(int size) { return new Request[size]; } }; private Request(Parcel in) { mText = in.readString(); mDefaultLocales = in.readInt() == 0 ? null : LocaleList.CREATOR.createFromParcel(in); mEntityConfig = in.readInt() == 0 ? null : TextClassifier.EntityConfig.CREATOR.createFromParcel(in); mLegacyFallback = true; mCallingPackageName = in.readString(); } }
A ClickableSpan for a TextLink.

Applies only to TextViews.

/** * A ClickableSpan for a TextLink. * * <p>Applies only to TextViews. */
public static class TextLinkSpan extends ClickableSpan {
How the clickspan is triggered.
@hide
/** * How the clickspan is triggered. * @hide */
@Retention(RetentionPolicy.SOURCE) @IntDef({INVOCATION_METHOD_UNSPECIFIED, INVOCATION_METHOD_TOUCH, INVOCATION_METHOD_KEYBOARD}) public @interface InvocationMethod {}
@hide
/** @hide */
public static final int INVOCATION_METHOD_UNSPECIFIED = -1;
@hide
/** @hide */
public static final int INVOCATION_METHOD_TOUCH = 0;
@hide
/** @hide */
public static final int INVOCATION_METHOD_KEYBOARD = 1; private final TextLink mTextLink; public TextLinkSpan(@NonNull TextLink textLink) { mTextLink = textLink; } @Override public void onClick(View widget) { onClick(widget, INVOCATION_METHOD_UNSPECIFIED); }
@hide
/** @hide */
public final void onClick(View widget, @InvocationMethod int invocationMethod) { if (widget instanceof TextView) { final TextView textView = (TextView) widget; final Context context = textView.getContext(); if (TextClassificationManager.getSettings(context).isSmartLinkifyEnabled()) { switch (invocationMethod) { case INVOCATION_METHOD_TOUCH: textView.requestActionMode(this); break; case INVOCATION_METHOD_KEYBOARD:// fall though case INVOCATION_METHOD_UNSPECIFIED: // fall through default: textView.handleClick(this); break; } } else { if (mTextLink.mUrlSpan != null) { mTextLink.mUrlSpan.onClick(textView); } else { textView.handleClick(this); } } } } public final TextLink getTextLink() { return mTextLink; }
@hide
/** @hide */
@VisibleForTesting(visibility = Visibility.PRIVATE) @Nullable public final String getUrl() { if (mTextLink.mUrlSpan != null) { return mTextLink.mUrlSpan.getURL(); } return null; } }
A builder to construct a TextLinks instance.
/** * A builder to construct a TextLinks instance. */
public static final class Builder { private final String mFullText; private final ArrayList<TextLink> mLinks;
Create a new TextLinks.Builder.
Params:
  • fullText – The full text to annotate with links
/** * Create a new TextLinks.Builder. * * @param fullText The full text to annotate with links */
public Builder(@NonNull String fullText) { mFullText = Preconditions.checkNotNull(fullText); mLinks = new ArrayList<>(); }
Adds a TextLink.
Params:
  • start – The start index of the identified subsequence
  • end – The end index of the identified subsequence
  • entityScores – A mapping of entity type to confidence score
Throws:
/** * Adds a TextLink. * * @param start The start index of the identified subsequence * @param end The end index of the identified subsequence * @param entityScores A mapping of entity type to confidence score * * @throws IllegalArgumentException if entityScores is null or empty. */
@NonNull public Builder addLink(int start, int end, Map<String, Float> entityScores) { mLinks.add(new TextLink(start, end, entityScores, null)); return this; }
Params:
  • urlSpan – An optional URLSpan to delegate to. NOTE: Not parcelled.
See Also:
  • addLink(int, int, Map)
/** * @see #addLink(int, int, Map) * @param urlSpan An optional URLSpan to delegate to. NOTE: Not parcelled. */
@NonNull Builder addLink(int start, int end, Map<String, Float> entityScores, @Nullable URLSpan urlSpan) { mLinks.add(new TextLink(start, end, entityScores, urlSpan)); return this; }
Removes all TextLinks.
/** * Removes all {@link TextLink}s. */
@NonNull public Builder clearTextLinks() { mLinks.clear(); return this; }
Constructs a TextLinks instance.
Returns:the constructed TextLinks
/** * Constructs a TextLinks instance. * * @return the constructed TextLinks */
@NonNull public TextLinks build() { return new TextLinks(mFullText, mLinks); } } // TODO: Remove once apps can build against the latest sdk.
Optional input parameters for generating TextLinks.
@hide
/** * Optional input parameters for generating TextLinks. * @hide */
public static final class Options { @Nullable private final TextClassificationSessionId mSessionId; @Nullable private final Request mRequest; @Nullable private LocaleList mDefaultLocales; @Nullable private TextClassifier.EntityConfig mEntityConfig; private boolean mLegacyFallback; private @ApplyStrategy int mApplyStrategy; private Function<TextLink, TextLinkSpan> mSpanFactory; private String mCallingPackageName; public Options() { this(null, null); } private Options( @Nullable TextClassificationSessionId sessionId, @Nullable Request request) { mSessionId = sessionId; mRequest = request; }
Helper to create Options from a Request.
/** Helper to create Options from a Request. */
public static Options from(TextClassificationSessionId sessionId, Request request) { final Options options = new Options(sessionId, request); options.setDefaultLocales(request.getDefaultLocales()); options.setEntityConfig(request.getEntityConfig()); return options; }
Returns a new options object based on the specified link mask.
/** Returns a new options object based on the specified link mask. */
public static Options fromLinkMask(@LinkifyMask int mask) { final List<String> entitiesToFind = new ArrayList<>(); if ((mask & Linkify.WEB_URLS) != 0) { entitiesToFind.add(TextClassifier.TYPE_URL); } if ((mask & Linkify.EMAIL_ADDRESSES) != 0) { entitiesToFind.add(TextClassifier.TYPE_EMAIL); } if ((mask & Linkify.PHONE_NUMBERS) != 0) { entitiesToFind.add(TextClassifier.TYPE_PHONE); } if ((mask & Linkify.MAP_ADDRESSES) != 0) { entitiesToFind.add(TextClassifier.TYPE_ADDRESS); } return new Options().setEntityConfig( TextClassifier.EntityConfig.createWithEntityList(entitiesToFind)); }
Params:
  • defaultLocales – ordered list of locale preferences.
/** @param defaultLocales ordered list of locale preferences. */
public Options setDefaultLocales(@Nullable LocaleList defaultLocales) { mDefaultLocales = defaultLocales; return this; }
Params:
  • entityConfig – definition of which entity types to look for.
/** @param entityConfig definition of which entity types to look for. */
public Options setEntityConfig(@Nullable TextClassifier.EntityConfig entityConfig) { mEntityConfig = entityConfig; return this; }
Params:
  • applyStrategy – strategy to use when resolving conflicts.
/** @param applyStrategy strategy to use when resolving conflicts. */
public Options setApplyStrategy(@ApplyStrategy int applyStrategy) { checkValidApplyStrategy(applyStrategy); mApplyStrategy = applyStrategy; return this; }
Params:
  • spanFactory – factory for converting TextLink to TextLinkSpan.
/** @param spanFactory factory for converting TextLink to TextLinkSpan. */
public Options setSpanFactory(@Nullable Function<TextLink, TextLinkSpan> spanFactory) { mSpanFactory = spanFactory; return this; } @Nullable public LocaleList getDefaultLocales() { return mDefaultLocales; } @Nullable public TextClassifier.EntityConfig getEntityConfig() { return mEntityConfig; } @ApplyStrategy public int getApplyStrategy() { return mApplyStrategy; } @Nullable public Function<TextLink, TextLinkSpan> getSpanFactory() { return mSpanFactory; } @Nullable public Request getRequest() { return mRequest; } @Nullable public TextClassificationSessionId getSessionId() { return mSessionId; } private static void checkValidApplyStrategy(int applyStrategy) { if (applyStrategy != APPLY_STRATEGY_IGNORE && applyStrategy != APPLY_STRATEGY_REPLACE) { throw new IllegalArgumentException( "Invalid apply strategy. See TextLinks.ApplyStrategy for options."); } } } }