| /* |
| * 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.Bundle; |
| 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.view.View; |
| import android.view.textclassifier.TextClassifier.EntityConfig; |
| 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.time.ZonedDateTime; |
| 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.Objects; |
| 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. |
| */ |
| public final class TextLinks implements Parcelable { |
| |
| /** |
| * 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, STATUS_UNSUPPORTED_CHARACTER}) |
| public @interface Status {} |
| |
| /** 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. */ |
| public static final int STATUS_NO_LINKS_FOUND = 1; |
| |
| /** 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. */ |
| public static final int STATUS_DIFFERENT_TEXT = 3; |
| |
| /** The specified text contains unsupported characters. */ |
| public static final int STATUS_UNSUPPORTED_CHARACTER = 4; |
| |
| /** @hide */ |
| @Retention(RetentionPolicy.SOURCE) |
| @IntDef({APPLY_STRATEGY_IGNORE, APPLY_STRATEGY_REPLACE}) |
| public @interface ApplyStrategy {} |
| |
| /** |
| * 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 {@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 final Bundle mExtras; |
| |
| private TextLinks(String fullText, ArrayList<TextLink> links, Bundle extras) { |
| mFullText = fullText; |
| mLinks = Collections.unmodifiableList(links); |
| mExtras = extras; |
| } |
| |
| /** |
| * Returns the text that was used to generate these links. |
| */ |
| @NonNull |
| public CharSequence getText() { |
| return mFullText; |
| } |
| |
| /** |
| * Returns an unmodifiable Collection of the links. |
| */ |
| @NonNull |
| public Collection<TextLink> getLinks() { |
| return mLinks; |
| } |
| |
| /** |
| * Returns the extended data. |
| * |
| * <p><b>NOTE: </b>Do not modify this bundle. |
| */ |
| @NonNull |
| public Bundle getExtras() { |
| return mExtras; |
| } |
| |
| /** |
| * 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) { |
| Objects.requireNonNull(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); |
| dest.writeBundle(mExtras); |
| } |
| |
| public static final @android.annotation.NonNull 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); |
| mExtras = in.readBundle(); |
| } |
| |
| /** |
| * 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; |
| private final Bundle mExtras; |
| @Nullable private final URLSpan mUrlSpan; |
| |
| /** |
| * Create a new TextLink. |
| * |
| * @param start The start index of the identified subsequence |
| * @param end The end index of the identified subsequence |
| * @param entityConfidence A mapping of entity type to confidence score |
| * @param extras A bundle containing custom data related to this TextLink |
| * @param urlSpan An optional URLSpan to delegate to. NOTE: Not parcelled |
| * |
| * @throws IllegalArgumentException if {@code entityConfidence} is null or empty |
| * @throws IllegalArgumentException if {@code start} is greater than {@code end} |
| */ |
| private TextLink(int start, int end, @NonNull EntityConfidence entityConfidence, |
| @NonNull Bundle extras, @Nullable URLSpan urlSpan) { |
| Objects.requireNonNull(entityConfidence); |
| Preconditions.checkArgument(!entityConfidence.getEntities().isEmpty()); |
| Preconditions.checkArgument(start <= end); |
| Objects.requireNonNull(extras); |
| mStart = start; |
| mEnd = end; |
| mEntityScores = entityConfidence; |
| mUrlSpan = urlSpan; |
| mExtras = extras; |
| } |
| |
| /** |
| * 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. |
| * |
| * @return the end index |
| */ |
| public int getEnd() { |
| return mEnd; |
| } |
| |
| /** |
| * 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. |
| * |
| * @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. |
| * |
| * @param entityType the entity type |
| */ |
| public @FloatRange(from = 0.0, to = 1.0) float getConfidenceScore( |
| @EntityType String entityType) { |
| return mEntityScores.getConfidenceScore(entityType); |
| } |
| |
| /** |
| * Returns a bundle containing custom data related to this TextLink. |
| */ |
| @NonNull |
| public Bundle getExtras() { |
| return mExtras; |
| } |
| |
| @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); |
| dest.writeBundle(mExtras); |
| } |
| |
| private static TextLink readFromParcel(Parcel in) { |
| final EntityConfidence entityConfidence = EntityConfidence.CREATOR.createFromParcel(in); |
| final int start = in.readInt(); |
| final int end = in.readInt(); |
| final Bundle extras = in.readBundle(); |
| return new TextLink(start, end, entityConfidence, extras, null /* urlSpan */); |
| } |
| |
| public static final @android.annotation.NonNull Parcelable.Creator<TextLink> CREATOR = |
| new Parcelable.Creator<TextLink>() { |
| @Override |
| public TextLink createFromParcel(Parcel in) { |
| return readFromParcel(in); |
| } |
| |
| @Override |
| public TextLink[] newArray(int size) { |
| return new TextLink[size]; |
| } |
| }; |
| } |
| |
| /** |
| * 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 EntityConfig mEntityConfig; |
| private final boolean mLegacyFallback; |
| private final Bundle mExtras; |
| @Nullable private final ZonedDateTime mReferenceTime; |
| @Nullable private SystemTextClassifierMetadata mSystemTcMetadata; |
| |
| private Request( |
| CharSequence text, |
| LocaleList defaultLocales, |
| EntityConfig entityConfig, |
| boolean legacyFallback, |
| ZonedDateTime referenceTime, |
| Bundle extras) { |
| mText = text; |
| mDefaultLocales = defaultLocales; |
| mEntityConfig = entityConfig; |
| mLegacyFallback = legacyFallback; |
| mReferenceTime = referenceTime; |
| mExtras = extras; |
| } |
| |
| /** |
| * Returns the text to generate links for. |
| */ |
| @NonNull |
| public CharSequence getText() { |
| return mText; |
| } |
| |
| /** |
| * Returns an ordered list of locale preferences that can be used to disambiguate the |
| * provided text. |
| */ |
| @Nullable |
| public LocaleList getDefaultLocales() { |
| return mDefaultLocales; |
| } |
| |
| /** |
| * Returns the config representing the set of entities to look for |
| * |
| * @see Builder#setEntityConfig(EntityConfig) |
| */ |
| @Nullable |
| public EntityConfig getEntityConfig() { |
| return mEntityConfig; |
| } |
| |
| /** |
| * 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; |
| } |
| |
| /** |
| * Returns reference time based on which relative dates (e.g. "tomorrow") should be |
| * interpreted. |
| */ |
| @Nullable |
| public ZonedDateTime getReferenceTime() { |
| return mReferenceTime; |
| } |
| |
| /** |
| * Returns the name of the package that sent this request. |
| * This returns {@code null} if no calling package name is set. |
| */ |
| @Nullable |
| public String getCallingPackageName() { |
| return mSystemTcMetadata != null ? mSystemTcMetadata.getCallingPackageName() : null; |
| } |
| |
| /** |
| * Sets the information about the {@link SystemTextClassifier} that sent this request. |
| * |
| * @hide |
| */ |
| @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) |
| public void setSystemTextClassifierMetadata( |
| @Nullable SystemTextClassifierMetadata systemTcMetadata) { |
| mSystemTcMetadata = systemTcMetadata; |
| } |
| |
| /** |
| * Returns the information about the {@link SystemTextClassifier} that sent this request. |
| * |
| * @hide |
| */ |
| @Nullable |
| public SystemTextClassifierMetadata getSystemTextClassifierMetadata() { |
| return mSystemTcMetadata; |
| } |
| |
| /** |
| * Returns the extended data. |
| * |
| * <p><b>NOTE: </b>Do not modify this bundle. |
| */ |
| @NonNull |
| public Bundle getExtras() { |
| return mExtras; |
| } |
| |
| /** |
| * A builder for building TextLinks requests. |
| */ |
| public static final class Builder { |
| |
| private final CharSequence mText; |
| |
| @Nullable private LocaleList mDefaultLocales; |
| @Nullable private EntityConfig mEntityConfig; |
| private boolean mLegacyFallback = true; // Use legacy fall back by default. |
| @Nullable private Bundle mExtras; |
| @Nullable private ZonedDateTime mReferenceTime; |
| |
| public Builder(@NonNull CharSequence text) { |
| mText = Objects.requireNonNull(text); |
| } |
| |
| /** |
| * Sets ordered list of locale preferences that may be used to disambiguate the |
| * provided text. |
| * |
| * @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 {@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 EntityConfig entityConfig) { |
| mEntityConfig = entityConfig; |
| return this; |
| } |
| |
| /** |
| * 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 extended data. |
| * |
| * @return this builder |
| */ |
| public Builder setExtras(@Nullable Bundle extras) { |
| mExtras = extras; |
| return this; |
| } |
| |
| /** |
| * Sets the reference time based on which relative dates (e.g. |
| * "tomorrow") should be interpreted. |
| * |
| * @param referenceTime reference time based on which relative dates. This should |
| * usually be the time when the text was originally composed. |
| * |
| * @return this builder |
| */ |
| @NonNull |
| public Builder setReferenceTime(@Nullable ZonedDateTime referenceTime) { |
| mReferenceTime = referenceTime; |
| return this; |
| } |
| |
| /** |
| * Builds and returns the request object. |
| */ |
| @NonNull |
| public Request build() { |
| return new Request( |
| mText, mDefaultLocales, mEntityConfig, |
| mLegacyFallback, mReferenceTime, |
| mExtras == null ? Bundle.EMPTY : mExtras); |
| } |
| } |
| |
| @Override |
| public int describeContents() { |
| return 0; |
| } |
| |
| @Override |
| public void writeToParcel(Parcel dest, int flags) { |
| dest.writeString(mText.toString()); |
| dest.writeParcelable(mDefaultLocales, flags); |
| dest.writeParcelable(mEntityConfig, flags); |
| dest.writeBundle(mExtras); |
| dest.writeString(mReferenceTime == null ? null : mReferenceTime.toString()); |
| dest.writeParcelable(mSystemTcMetadata, flags); |
| } |
| |
| private static Request readFromParcel(Parcel in) { |
| final String text = in.readString(); |
| final LocaleList defaultLocales = in.readParcelable(null); |
| final EntityConfig entityConfig = in.readParcelable(null); |
| final Bundle extras = in.readBundle(); |
| final String referenceTimeString = in.readString(); |
| final ZonedDateTime referenceTime = referenceTimeString == null |
| ? null : ZonedDateTime.parse(referenceTimeString); |
| final SystemTextClassifierMetadata systemTcMetadata = in.readParcelable(null); |
| |
| final Request request = new Request(text, defaultLocales, entityConfig, |
| /* legacyFallback= */ true, referenceTime, extras); |
| request.setSystemTextClassifierMetadata(systemTcMetadata); |
| return request; |
| } |
| |
| public static final @android.annotation.NonNull Parcelable.Creator<Request> CREATOR = |
| new Parcelable.Creator<Request>() { |
| @Override |
| public Request createFromParcel(Parcel in) { |
| return readFromParcel(in); |
| } |
| |
| @Override |
| public Request[] newArray(int size) { |
| return new Request[size]; |
| } |
| }; |
| } |
| |
| /** |
| * A ClickableSpan for a TextLink. |
| * |
| * <p>Applies only to TextViews. |
| */ |
| public static class TextLinkSpan extends ClickableSpan { |
| |
| /** |
| * How the clickspan is triggered. |
| * @hide |
| */ |
| @Retention(RetentionPolicy.SOURCE) |
| @IntDef({INVOCATION_METHOD_UNSPECIFIED, INVOCATION_METHOD_TOUCH, |
| INVOCATION_METHOD_KEYBOARD}) |
| public @interface InvocationMethod {} |
| |
| /** @hide */ |
| public static final int INVOCATION_METHOD_UNSPECIFIED = -1; |
| /** @hide */ |
| public static final int INVOCATION_METHOD_TOUCH = 0; |
| /** @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 */ |
| 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 */ |
| @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. |
| */ |
| public static final class Builder { |
| private final String mFullText; |
| private final ArrayList<TextLink> mLinks; |
| private Bundle mExtras; |
| |
| /** |
| * Create a new TextLinks.Builder. |
| * |
| * @param fullText The full text to annotate with links |
| */ |
| public Builder(@NonNull String fullText) { |
| mFullText = Objects.requireNonNull(fullText); |
| mLinks = new ArrayList<>(); |
| } |
| |
| /** |
| * 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, @NonNull Map<String, Float> entityScores) { |
| return addLink(start, end, entityScores, Bundle.EMPTY, null); |
| } |
| |
| /** |
| * Adds a TextLink. |
| * |
| * @see #addLink(int, int, Map) |
| * @param extras An optional bundle containing custom data related to this TextLink |
| */ |
| @NonNull |
| public Builder addLink(int start, int end, @NonNull Map<String, Float> entityScores, |
| @NonNull Bundle extras) { |
| return addLink(start, end, entityScores, extras, null); |
| } |
| |
| /** |
| * Adds a TextLink. |
| * |
| * @see #addLink(int, int, Map) |
| * @param urlSpan An optional URLSpan to delegate to. NOTE: Not parcelled. |
| */ |
| @NonNull |
| Builder addLink(int start, int end, @NonNull Map<String, Float> entityScores, |
| @Nullable URLSpan urlSpan) { |
| return addLink(start, end, entityScores, Bundle.EMPTY, urlSpan); |
| } |
| |
| private Builder addLink(int start, int end, @NonNull Map<String, Float> entityScores, |
| @NonNull Bundle extras, @Nullable URLSpan urlSpan) { |
| mLinks.add(new TextLink( |
| start, end, new EntityConfidence(entityScores), extras, urlSpan)); |
| return this; |
| } |
| |
| /** |
| * Removes all {@link TextLink}s. |
| */ |
| @NonNull |
| public Builder clearTextLinks() { |
| mLinks.clear(); |
| return this; |
| } |
| |
| /** |
| * Sets the extended data. |
| * |
| * @return this builder |
| */ |
| @NonNull |
| public Builder setExtras(@Nullable Bundle extras) { |
| mExtras = extras; |
| return this; |
| } |
| |
| /** |
| * Constructs a TextLinks instance. |
| * |
| * @return the constructed TextLinks |
| */ |
| @NonNull |
| public TextLinks build() { |
| return new TextLinks(mFullText, mLinks, |
| mExtras == null ? Bundle.EMPTY : mExtras); |
| } |
| } |
| } |