799 lines
31 KiB
Java
799 lines
31 KiB
Java
![]() |
/*
|
||
|
* 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. "noreply@android.com"). */
|
||
|
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");
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|