| /* |
| * Copyright (C) 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.IntDef; |
| import android.annotation.IntRange; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.annotation.StringDef; |
| import android.annotation.WorkerThread; |
| import android.os.LocaleList; |
| import android.os.Looper; |
| import android.os.Parcel; |
| import android.os.Parcelable; |
| import android.text.Spannable; |
| import android.text.SpannableString; |
| import android.text.style.URLSpan; |
| import android.text.util.Linkify; |
| import android.text.util.Linkify.LinkifyMask; |
| import android.util.ArrayMap; |
| |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.internal.util.IndentingPrintWriter; |
| import com.android.internal.util.Preconditions; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.text.BreakIterator; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.Set; |
| |
| /** |
| * Interface for providing text classification related features. |
| * <p> |
| * The TextClassifier may be used to understand the meaning of text, as well as generating predicted |
| * next actions based on the text. |
| * |
| * <p><strong>NOTE: </strong>Unless otherwise stated, methods of this interface are blocking |
| * operations. Call on a worker thread. |
| */ |
| public interface TextClassifier { |
| |
| /** @hide */ |
| String LOG_TAG = "androidtc"; |
| |
| |
| /** @hide */ |
| @Retention(RetentionPolicy.SOURCE) |
| @IntDef(value = {LOCAL, SYSTEM, DEFAULT_SYSTEM}) |
| @interface TextClassifierType {} // TODO: Expose as system APIs. |
| /** Specifies a TextClassifier that runs locally in the app's process. @hide */ |
| int LOCAL = 0; |
| /** Specifies a TextClassifier that runs in the system process and serves all apps. @hide */ |
| int SYSTEM = 1; |
| /** Specifies the default TextClassifier that runs in the system process. @hide */ |
| int DEFAULT_SYSTEM = 2; |
| |
| /** @hide */ |
| static String typeToString(@TextClassifierType int type) { |
| switch (type) { |
| case LOCAL: |
| return "Local"; |
| case SYSTEM: |
| return "System"; |
| case DEFAULT_SYSTEM: |
| return "Default system"; |
| } |
| return "Unknown"; |
| } |
| |
| /** The TextClassifier failed to run. */ |
| String TYPE_UNKNOWN = ""; |
| /** The classifier ran, but didn't recognize a known entity. */ |
| String TYPE_OTHER = "other"; |
| /** E-mail address (e.g. "[email protected]"). */ |
| String TYPE_EMAIL = "email"; |
| /** Phone number (e.g. "555-123 456"). */ |
| String TYPE_PHONE = "phone"; |
| /** Physical address. */ |
| String TYPE_ADDRESS = "address"; |
| /** Web URL. */ |
| String TYPE_URL = "url"; |
| /** Time reference that is no more specific than a date. May be absolute such as "01/01/2000" or |
| * relative like "tomorrow". **/ |
| String TYPE_DATE = "date"; |
| /** Time reference that includes a specific time. May be absolute such as "01/01/2000 5:30pm" or |
| * relative like "tomorrow at 5:30pm". **/ |
| String TYPE_DATE_TIME = "datetime"; |
| /** Flight number in IATA format. */ |
| String TYPE_FLIGHT_NUMBER = "flight"; |
| /** |
| * Word that users may be interested to look up for meaning. |
| * @hide |
| */ |
| String TYPE_DICTIONARY = "dictionary"; |
| |
| /** @hide */ |
| @Retention(RetentionPolicy.SOURCE) |
| @StringDef(prefix = { "TYPE_" }, value = { |
| TYPE_UNKNOWN, |
| TYPE_OTHER, |
| TYPE_EMAIL, |
| TYPE_PHONE, |
| TYPE_ADDRESS, |
| TYPE_URL, |
| TYPE_DATE, |
| TYPE_DATE_TIME, |
| TYPE_FLIGHT_NUMBER, |
| TYPE_DICTIONARY |
| }) |
| @interface EntityType {} |
| |
| /** Designates that the text in question is editable. **/ |
| String HINT_TEXT_IS_EDITABLE = "android.text_is_editable"; |
| /** Designates that the text in question is not editable. **/ |
| String HINT_TEXT_IS_NOT_EDITABLE = "android.text_is_not_editable"; |
| |
| /** @hide */ |
| @Retention(RetentionPolicy.SOURCE) |
| @StringDef(prefix = { "HINT_" }, value = {HINT_TEXT_IS_EDITABLE, HINT_TEXT_IS_NOT_EDITABLE}) |
| @interface Hints {} |
| |
| /** @hide */ |
| @Retention(RetentionPolicy.SOURCE) |
| @StringDef({WIDGET_TYPE_TEXTVIEW, WIDGET_TYPE_EDITTEXT, WIDGET_TYPE_UNSELECTABLE_TEXTVIEW, |
| WIDGET_TYPE_WEBVIEW, WIDGET_TYPE_EDIT_WEBVIEW, WIDGET_TYPE_CUSTOM_TEXTVIEW, |
| WIDGET_TYPE_CUSTOM_EDITTEXT, WIDGET_TYPE_CUSTOM_UNSELECTABLE_TEXTVIEW, |
| WIDGET_TYPE_NOTIFICATION, WIDGET_TYPE_CLIPBOARD, WIDGET_TYPE_UNKNOWN }) |
| @interface WidgetType {} |
| |
| /** The widget involved in the text classification context is a standard |
| * {@link android.widget.TextView}. */ |
| String WIDGET_TYPE_TEXTVIEW = "textview"; |
| /** The widget involved in the text classification context is a standard |
| * {@link android.widget.EditText}. */ |
| String WIDGET_TYPE_EDITTEXT = "edittext"; |
| /** The widget involved in the text classification context is a standard non-selectable |
| * {@link android.widget.TextView}. */ |
| String WIDGET_TYPE_UNSELECTABLE_TEXTVIEW = "nosel-textview"; |
| /** The widget involved in the text classification context is a standard |
| * {@link android.webkit.WebView}. */ |
| String WIDGET_TYPE_WEBVIEW = "webview"; |
| /** The widget involved in the text classification context is a standard editable |
| * {@link android.webkit.WebView}. */ |
| String WIDGET_TYPE_EDIT_WEBVIEW = "edit-webview"; |
| /** The widget involved in the text classification context is a custom text widget. */ |
| String WIDGET_TYPE_CUSTOM_TEXTVIEW = "customview"; |
| /** The widget involved in the text classification context is a custom editable text widget. */ |
| String WIDGET_TYPE_CUSTOM_EDITTEXT = "customedit"; |
| /** The widget involved in the text classification context is a custom non-selectable text |
| * widget. */ |
| String WIDGET_TYPE_CUSTOM_UNSELECTABLE_TEXTVIEW = "nosel-customview"; |
| /** The widget involved in the text classification context is a notification */ |
| String WIDGET_TYPE_NOTIFICATION = "notification"; |
| /** The text classification context is for use with the system clipboard. */ |
| String WIDGET_TYPE_CLIPBOARD = "clipboard"; |
| /** The widget involved in the text classification context is of an unknown/unspecified type. */ |
| String WIDGET_TYPE_UNKNOWN = "unknown"; |
| |
| /** |
| * No-op TextClassifier. |
| * This may be used to turn off TextClassifier features. |
| */ |
| TextClassifier NO_OP = new TextClassifier() { |
| @Override |
| public String toString() { |
| return "TextClassifier.NO_OP"; |
| } |
| }; |
| |
| /** |
| * Extra that is included on activity intents coming from a TextClassifier when |
| * it suggests actions to its caller. |
| * <p> |
| * All {@link TextClassifier} implementations should make sure this extra exists in their |
| * generated intents. |
| */ |
| String EXTRA_FROM_TEXT_CLASSIFIER = "android.view.textclassifier.extra.FROM_TEXT_CLASSIFIER"; |
| |
| /** |
| * Returns suggested text selection start and end indices, recognized entity types, and their |
| * associated confidence scores. The entity types are ordered from highest to lowest scoring. |
| * |
| * <p><strong>NOTE: </strong>Call on a worker thread. |
| * |
| * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should |
| * throw an {@link IllegalStateException}. See {@link #isDestroyed()}. |
| * |
| * @param request the text selection request |
| */ |
| @WorkerThread |
| @NonNull |
| default TextSelection suggestSelection(@NonNull TextSelection.Request request) { |
| Objects.requireNonNull(request); |
| Utils.checkMainThread(); |
| return new TextSelection.Builder(request.getStartIndex(), request.getEndIndex()).build(); |
| } |
| |
| /** |
| * Returns suggested text selection start and end indices, recognized entity types, and their |
| * associated confidence scores. The entity types are ordered from highest to lowest scoring. |
| * |
| * <p><strong>NOTE: </strong>Call on a worker thread. |
| * |
| * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should |
| * throw an {@link IllegalStateException}. See {@link #isDestroyed()}. |
| * |
| * <p><b>NOTE:</b> Do not implement. The default implementation of this method calls |
| * {@link #suggestSelection(TextSelection.Request)}. If that method calls this method, |
| * a stack overflow error will happen. |
| * |
| * @param text text providing context for the selected text (which is specified |
| * by the sub sequence starting at selectionStartIndex and ending at selectionEndIndex) |
| * @param selectionStartIndex start index of the selected part of text |
| * @param selectionEndIndex end index of the selected part of 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. |
| * |
| * @throws IllegalArgumentException if text is null; selectionStartIndex is negative; |
| * selectionEndIndex is greater than text.length() or not greater than selectionStartIndex |
| * |
| * @see #suggestSelection(TextSelection.Request) |
| */ |
| @WorkerThread |
| @NonNull |
| default TextSelection suggestSelection( |
| @NonNull CharSequence text, |
| @IntRange(from = 0) int selectionStartIndex, |
| @IntRange(from = 0) int selectionEndIndex, |
| @Nullable LocaleList defaultLocales) { |
| final TextSelection.Request request = new TextSelection.Request.Builder( |
| text, selectionStartIndex, selectionEndIndex) |
| .setDefaultLocales(defaultLocales) |
| .build(); |
| return suggestSelection(request); |
| } |
| |
| /** |
| * Classifies the specified text and returns a {@link TextClassification} object that can be |
| * used to generate a widget for handling the classified text. |
| * |
| * <p><strong>NOTE: </strong>Call on a worker thread. |
| * |
| * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should |
| * throw an {@link IllegalStateException}. See {@link #isDestroyed()}. |
| * |
| * @param request the text classification request |
| */ |
| @WorkerThread |
| @NonNull |
| default TextClassification classifyText(@NonNull TextClassification.Request request) { |
| Objects.requireNonNull(request); |
| Utils.checkMainThread(); |
| return TextClassification.EMPTY; |
| } |
| |
| /** |
| * Classifies the specified text and returns a {@link TextClassification} object that can be |
| * used to generate a widget for handling the classified text. |
| * |
| * <p><strong>NOTE: </strong>Call on a worker thread. |
| * |
| * <p><b>NOTE:</b> Do not implement. The default implementation of this method calls |
| * {@link #classifyText(TextClassification.Request)}. If that method calls this method, |
| * a stack overflow error will happen. |
| * |
| * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should |
| * throw an {@link IllegalStateException}. See {@link #isDestroyed()}. |
| * |
| * @param text text providing context for the text to classify (which is specified |
| * by the sub sequence starting at startIndex and ending at endIndex) |
| * @param startIndex start index of the text to classify |
| * @param endIndex end index of the text to classify |
| * @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. |
| * |
| * @throws IllegalArgumentException if text is null; startIndex is negative; |
| * endIndex is greater than text.length() or not greater than startIndex |
| * |
| * @see #classifyText(TextClassification.Request) |
| */ |
| @WorkerThread |
| @NonNull |
| default TextClassification classifyText( |
| @NonNull CharSequence text, |
| @IntRange(from = 0) int startIndex, |
| @IntRange(from = 0) int endIndex, |
| @Nullable LocaleList defaultLocales) { |
| final TextClassification.Request request = new TextClassification.Request.Builder( |
| text, startIndex, endIndex) |
| .setDefaultLocales(defaultLocales) |
| .build(); |
| return classifyText(request); |
| } |
| |
| /** |
| * Generates and returns a {@link TextLinks} that may be applied to the text to annotate it with |
| * links information. |
| * |
| * <p><strong>NOTE: </strong>Call on a worker thread. |
| * |
| * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should |
| * throw an {@link IllegalStateException}. See {@link #isDestroyed()}. |
| * |
| * @param request the text links request |
| * |
| * @see #getMaxGenerateLinksTextLength() |
| */ |
| @WorkerThread |
| @NonNull |
| default TextLinks generateLinks(@NonNull TextLinks.Request request) { |
| Objects.requireNonNull(request); |
| Utils.checkMainThread(); |
| return new TextLinks.Builder(request.getText().toString()).build(); |
| } |
| |
| /** |
| * Returns the maximal length of text that can be processed by generateLinks. |
| * |
| * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should |
| * throw an {@link IllegalStateException}. See {@link #isDestroyed()}. |
| * |
| * @see #generateLinks(TextLinks.Request) |
| */ |
| @WorkerThread |
| default int getMaxGenerateLinksTextLength() { |
| return Integer.MAX_VALUE; |
| } |
| |
| /** |
| * Detects the language of the text in the given request. |
| * |
| * <p><strong>NOTE: </strong>Call on a worker thread. |
| * |
| * |
| * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should |
| * throw an {@link IllegalStateException}. See {@link #isDestroyed()}. |
| * |
| * @param request the {@link TextLanguage} request. |
| * @return the {@link TextLanguage} result. |
| */ |
| @WorkerThread |
| @NonNull |
| default TextLanguage detectLanguage(@NonNull TextLanguage.Request request) { |
| Objects.requireNonNull(request); |
| Utils.checkMainThread(); |
| return TextLanguage.EMPTY; |
| } |
| |
| /** |
| * Suggests and returns a list of actions according to the given conversation. |
| */ |
| @WorkerThread |
| @NonNull |
| default ConversationActions suggestConversationActions( |
| @NonNull ConversationActions.Request request) { |
| Objects.requireNonNull(request); |
| Utils.checkMainThread(); |
| return new ConversationActions(Collections.emptyList(), null); |
| } |
| |
| /** |
| * <strong>NOTE: </strong>Use {@link #onTextClassifierEvent(TextClassifierEvent)} instead. |
| * <p> |
| * Reports a selection event. |
| * |
| * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should |
| * throw an {@link IllegalStateException}. See {@link #isDestroyed()}. |
| */ |
| default void onSelectionEvent(@NonNull SelectionEvent event) { |
| // TODO: Consider rerouting to onTextClassifierEvent() |
| } |
| |
| /** |
| * Reports a text classifier event. |
| * <p> |
| * <strong>NOTE: </strong>Call on a worker thread. |
| * |
| * @throws IllegalStateException if this TextClassifier has been destroyed. |
| * @see #isDestroyed() |
| */ |
| default void onTextClassifierEvent(@NonNull TextClassifierEvent event) {} |
| |
| /** |
| * Destroys this TextClassifier. |
| * |
| * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to its methods should |
| * throw an {@link IllegalStateException}. See {@link #isDestroyed()}. |
| * |
| * <p>Subsequent calls to this method are no-ops. |
| */ |
| default void destroy() {} |
| |
| /** |
| * Returns whether or not this TextClassifier has been destroyed. |
| * |
| * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, caller should not interact |
| * with the classifier and an attempt to do so would throw an {@link IllegalStateException}. |
| * However, this method should never throw an {@link IllegalStateException}. |
| * |
| * @see #destroy() |
| */ |
| default boolean isDestroyed() { |
| return false; |
| } |
| |
| /** @hide **/ |
| default void dump(@NonNull IndentingPrintWriter printWriter) {} |
| |
| /** |
| * Configuration object for specifying what entity types to identify. |
| * |
| * Configs are initially based on a predefined preset, and can be modified from there. |
| */ |
| final class EntityConfig implements Parcelable { |
| private final List<String> mIncludedTypes; |
| private final List<String> mExcludedTypes; |
| private final List<String> mHints; |
| private final boolean mIncludeTypesFromTextClassifier; |
| |
| private EntityConfig( |
| List<String> includedEntityTypes, |
| List<String> excludedEntityTypes, |
| List<String> hints, |
| boolean includeTypesFromTextClassifier) { |
| mIncludedTypes = Objects.requireNonNull(includedEntityTypes); |
| mExcludedTypes = Objects.requireNonNull(excludedEntityTypes); |
| mHints = Objects.requireNonNull(hints); |
| mIncludeTypesFromTextClassifier = includeTypesFromTextClassifier; |
| } |
| |
| private EntityConfig(Parcel in) { |
| mIncludedTypes = new ArrayList<>(); |
| in.readStringList(mIncludedTypes); |
| mExcludedTypes = new ArrayList<>(); |
| in.readStringList(mExcludedTypes); |
| List<String> tmpHints = new ArrayList<>(); |
| in.readStringList(tmpHints); |
| mHints = Collections.unmodifiableList(tmpHints); |
| mIncludeTypesFromTextClassifier = in.readByte() != 0; |
| } |
| |
| @Override |
| public void writeToParcel(Parcel parcel, int flags) { |
| parcel.writeStringList(mIncludedTypes); |
| parcel.writeStringList(mExcludedTypes); |
| parcel.writeStringList(mHints); |
| parcel.writeByte((byte) (mIncludeTypesFromTextClassifier ? 1 : 0)); |
| } |
| |
| /** |
| * Creates an EntityConfig. |
| * |
| * @param hints Hints for the TextClassifier to determine what types of entities to find. |
| * |
| * @deprecated Use {@link Builder} instead. |
| */ |
| @Deprecated |
| public static EntityConfig createWithHints(@Nullable Collection<String> hints) { |
| return new EntityConfig.Builder() |
| .includeTypesFromTextClassifier(true) |
| .setHints(hints) |
| .build(); |
| } |
| |
| /** |
| * Creates an EntityConfig. |
| * |
| * @param hints Hints for the TextClassifier to determine what types of entities to find |
| * @param includedEntityTypes Entity types, e.g. {@link #TYPE_EMAIL}, to explicitly include |
| * @param excludedEntityTypes Entity types, e.g. {@link #TYPE_PHONE}, to explicitly exclude |
| * |
| * |
| * Note that if an entity has been excluded, the exclusion will take precedence. |
| * |
| * @deprecated Use {@link Builder} instead. |
| */ |
| @Deprecated |
| public static EntityConfig create(@Nullable Collection<String> hints, |
| @Nullable Collection<String> includedEntityTypes, |
| @Nullable Collection<String> excludedEntityTypes) { |
| return new EntityConfig.Builder() |
| .setIncludedTypes(includedEntityTypes) |
| .setExcludedTypes(excludedEntityTypes) |
| .setHints(hints) |
| .includeTypesFromTextClassifier(true) |
| .build(); |
| } |
| |
| /** |
| * Creates an EntityConfig with an explicit entity list. |
| * |
| * @param entityTypes Complete set of entities, e.g. {@link #TYPE_URL} to find. |
| * |
| * @deprecated Use {@link Builder} instead. |
| */ |
| @Deprecated |
| public static EntityConfig createWithExplicitEntityList( |
| @Nullable Collection<String> entityTypes) { |
| return new EntityConfig.Builder() |
| .setIncludedTypes(entityTypes) |
| .includeTypesFromTextClassifier(false) |
| .build(); |
| } |
| |
| /** |
| * Returns a final list of entity types to find. |
| * |
| * @param entityTypes Entity types we think should be found before factoring in |
| * includes/excludes |
| * |
| * This method is intended for use by TextClassifier implementations. |
| */ |
| public Collection<String> resolveEntityListModifications( |
| @NonNull Collection<String> entityTypes) { |
| final Set<String> finalSet = new HashSet<>(); |
| if (mIncludeTypesFromTextClassifier) { |
| finalSet.addAll(entityTypes); |
| } |
| finalSet.addAll(mIncludedTypes); |
| finalSet.removeAll(mExcludedTypes); |
| return finalSet; |
| } |
| |
| /** |
| * Retrieves the list of hints. |
| * |
| * @return An unmodifiable collection of the hints. |
| */ |
| public Collection<String> getHints() { |
| return mHints; |
| } |
| |
| /** |
| * Return whether the client allows the text classifier to include its own list of |
| * default types. If this function returns {@code true}, a default list of types suggested |
| * from a text classifier will be taking into account. |
| * |
| * <p>NOTE: This method is intended for use by a text classifier. |
| * |
| * @see #resolveEntityListModifications(Collection) |
| */ |
| public boolean shouldIncludeTypesFromTextClassifier() { |
| return mIncludeTypesFromTextClassifier; |
| } |
| |
| @Override |
| public int describeContents() { |
| return 0; |
| } |
| |
| public static final @android.annotation.NonNull Parcelable.Creator<EntityConfig> CREATOR = |
| new Parcelable.Creator<EntityConfig>() { |
| @Override |
| public EntityConfig createFromParcel(Parcel in) { |
| return new EntityConfig(in); |
| } |
| |
| @Override |
| public EntityConfig[] newArray(int size) { |
| return new EntityConfig[size]; |
| } |
| }; |
| |
| |
| |
| /** Builder class to construct the {@link EntityConfig} object. */ |
| public static final class Builder { |
| @Nullable |
| private Collection<String> mIncludedTypes; |
| @Nullable |
| private Collection<String> mExcludedTypes; |
| @Nullable |
| private Collection<String> mHints; |
| private boolean mIncludeTypesFromTextClassifier = true; |
| |
| /** |
| * Sets a collection of types that are explicitly included. |
| */ |
| @NonNull |
| public Builder setIncludedTypes(@Nullable Collection<String> includedTypes) { |
| mIncludedTypes = includedTypes; |
| return this; |
| } |
| |
| /** |
| * Sets a collection of types that are explicitly excluded. |
| */ |
| @NonNull |
| public Builder setExcludedTypes(@Nullable Collection<String> excludedTypes) { |
| mExcludedTypes = excludedTypes; |
| return this; |
| } |
| |
| /** |
| * Specifies whether or not to include the types suggested by the text classifier. By |
| * default, it is included. |
| */ |
| @NonNull |
| public Builder includeTypesFromTextClassifier(boolean includeTypesFromTextClassifier) { |
| mIncludeTypesFromTextClassifier = includeTypesFromTextClassifier; |
| return this; |
| } |
| |
| |
| /** |
| * Sets the hints for the TextClassifier to determine what types of entities to find. |
| * These hints will only be used if {@link #includeTypesFromTextClassifier} is |
| * set to be true. |
| */ |
| @NonNull |
| public Builder setHints(@Nullable Collection<String> hints) { |
| mHints = hints; |
| return this; |
| } |
| |
| /** |
| * Combines all of the options that have been set and returns a new {@link EntityConfig} |
| * object. |
| */ |
| @NonNull |
| public EntityConfig build() { |
| return new EntityConfig( |
| mIncludedTypes == null |
| ? Collections.emptyList() |
| : new ArrayList<>(mIncludedTypes), |
| mExcludedTypes == null |
| ? Collections.emptyList() |
| : new ArrayList<>(mExcludedTypes), |
| mHints == null |
| ? Collections.emptyList() |
| : Collections.unmodifiableList(new ArrayList<>(mHints)), |
| mIncludeTypesFromTextClassifier); |
| } |
| } |
| } |
| |
| /** |
| * Utility functions for TextClassifier methods. |
| * |
| * <ul> |
| * <li>Provides validation of input parameters to TextClassifier methods |
| * </ul> |
| * |
| * Intended to be used only for TextClassifier purposes. |
| * @hide |
| */ |
| final class Utils { |
| |
| @GuardedBy("WORD_ITERATOR") |
| private static final BreakIterator WORD_ITERATOR = BreakIterator.getWordInstance(); |
| |
| /** |
| * @throws IllegalArgumentException if text is null; startIndex is negative; |
| * endIndex is greater than text.length() or is not greater than startIndex; |
| * options is null |
| */ |
| static void checkArgument(@NonNull CharSequence text, int startIndex, int endIndex) { |
| Preconditions.checkArgument(text != null); |
| Preconditions.checkArgument(startIndex >= 0); |
| Preconditions.checkArgument(endIndex <= text.length()); |
| Preconditions.checkArgument(endIndex > startIndex); |
| } |
| |
| /** Returns if the length of the text is within the range. */ |
| static boolean checkTextLength(CharSequence text, int maxLength) { |
| int textLength = text.length(); |
| return textLength >= 0 && textLength <= maxLength; |
| } |
| |
| /** |
| * Returns the substring of {@code text} that contains at least text from index |
| * {@code start} <i>(inclusive)</i> to index {@code end} <i><(exclusive)/i> with the goal of |
| * returning text that is at least {@code minimumLength}. If {@code text} is not long |
| * enough, this will return {@code text}. This method returns text at word boundaries. |
| * |
| * @param text the source text |
| * @param start the start index of text that must be included |
| * @param end the end index of text that must be included |
| * @param minimumLength minimum length of text to return if {@code text} is long enough |
| */ |
| public static String getSubString( |
| String text, int start, int end, int minimumLength) { |
| Preconditions.checkArgument(start >= 0); |
| Preconditions.checkArgument(end <= text.length()); |
| Preconditions.checkArgument(start <= end); |
| |
| if (text.length() < minimumLength) { |
| return text; |
| } |
| |
| final int length = end - start; |
| if (length >= minimumLength) { |
| return text.substring(start, end); |
| } |
| |
| final int offset = (minimumLength - length) / 2; |
| int iterStart = Math.max(0, Math.min(start - offset, text.length() - minimumLength)); |
| int iterEnd = Math.min(text.length(), iterStart + minimumLength); |
| |
| synchronized (WORD_ITERATOR) { |
| WORD_ITERATOR.setText(text); |
| iterStart = WORD_ITERATOR.isBoundary(iterStart) |
| ? iterStart : Math.max(0, WORD_ITERATOR.preceding(iterStart)); |
| iterEnd = WORD_ITERATOR.isBoundary(iterEnd) |
| ? iterEnd : Math.max(iterEnd, WORD_ITERATOR.following(iterEnd)); |
| WORD_ITERATOR.setText(""); |
| return text.substring(iterStart, iterEnd); |
| } |
| } |
| |
| /** |
| * Generates links using legacy {@link Linkify}. |
| */ |
| public static TextLinks generateLegacyLinks(@NonNull TextLinks.Request request) { |
| final String string = request.getText().toString(); |
| final TextLinks.Builder links = new TextLinks.Builder(string); |
| |
| final Collection<String> entities = request.getEntityConfig() |
| .resolveEntityListModifications(Collections.emptyList()); |
| if (entities.contains(TextClassifier.TYPE_URL)) { |
| addLinks(links, string, TextClassifier.TYPE_URL); |
| } |
| if (entities.contains(TextClassifier.TYPE_PHONE)) { |
| addLinks(links, string, TextClassifier.TYPE_PHONE); |
| } |
| if (entities.contains(TextClassifier.TYPE_EMAIL)) { |
| addLinks(links, string, TextClassifier.TYPE_EMAIL); |
| } |
| // NOTE: Do not support MAP_ADDRESSES. Legacy version does not work well. |
| return links.build(); |
| } |
| |
| private static void addLinks( |
| TextLinks.Builder links, String string, @EntityType String entityType) { |
| final Spannable spannable = new SpannableString(string); |
| if (Linkify.addLinks(spannable, linkMask(entityType))) { |
| final URLSpan[] spans = spannable.getSpans(0, spannable.length(), URLSpan.class); |
| for (URLSpan urlSpan : spans) { |
| links.addLink( |
| spannable.getSpanStart(urlSpan), |
| spannable.getSpanEnd(urlSpan), |
| entityScores(entityType), |
| urlSpan); |
| } |
| } |
| } |
| |
| @LinkifyMask |
| private static int linkMask(@EntityType String entityType) { |
| switch (entityType) { |
| case TextClassifier.TYPE_URL: |
| return Linkify.WEB_URLS; |
| case TextClassifier.TYPE_PHONE: |
| return Linkify.PHONE_NUMBERS; |
| case TextClassifier.TYPE_EMAIL: |
| return Linkify.EMAIL_ADDRESSES; |
| default: |
| // NOTE: Do not support MAP_ADDRESSES. Legacy version does not work well. |
| return 0; |
| } |
| } |
| |
| private static Map<String, Float> entityScores(@EntityType String entityType) { |
| final Map<String, Float> scores = new ArrayMap<>(); |
| scores.put(entityType, 1f); |
| return scores; |
| } |
| |
| static void checkMainThread() { |
| if (Looper.myLooper() == Looper.getMainLooper()) { |
| Log.w(LOG_TAG, "TextClassifier called on main thread"); |
| } |
| } |
| } |
| } |