/*
* Copyright (C) 2018 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 androidx.textclassifier;
import android.os.Build;
import android.os.Bundle;
import android.os.Looper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.annotation.StringDef;
import androidx.annotation.WorkerThread;
import androidx.collection.ArraySet;
import androidx.core.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.Set;
/**
* Interface for providing text classification related features.
*
* TextClassifier acts as a proxy to either the system provided TextClassifier, or an
* equivalent implementation provided by an app. Each instance of the class therefore represents
* one connection to the classifier implementation.
*
* <p>Unless otherwise stated, methods of this interface are blocking operations.
* Avoid calling them on the UI thread.
*/
public abstract class TextClassifier {
// TODO: describe in the class documentation how a TC implementation in chosen/located.
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
public static final String DEFAULT_LOG_TAG = "androidx_tc";
/** Signifies that the TextClassifier did not identify an entity. */
public static final String TYPE_UNKNOWN = "";
/** Signifies that the classifier ran, but didn't recognize a know entity. */
public static final String TYPE_OTHER = "other";
/** Identifies an e-mail address. */
public static final String TYPE_EMAIL = "email";
/** Identifies a phone number. */
public static final String TYPE_PHONE = "phone";
/** Identifies a physical address. */
public static final String TYPE_ADDRESS = "address";
/** Identifies a URL. */
public static final 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".
**/
public static final 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".
**/
public static final String TYPE_DATE_TIME = "datetime";
/** Flight number in IATA format. */
public static final String TYPE_FLIGHT_NUMBER = "flight";
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@Retention(RetentionPolicy.SOURCE)
@StringDef(value = {
TYPE_UNKNOWN,
TYPE_OTHER,
TYPE_EMAIL,
TYPE_PHONE,
TYPE_ADDRESS,
TYPE_URL,
TYPE_DATE,
TYPE_DATE_TIME,
TYPE_FLIGHT_NUMBER,
})
@interface EntityType {}
/** Designates that the text in question is editable. **/
public static final String HINT_TEXT_IS_EDITABLE = "android.text_is_editable";
/** Designates that the text in question is not editable. **/
public static final String HINT_TEXT_IS_NOT_EDITABLE = "android.text_is_not_editable";
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
@Retention(RetentionPolicy.SOURCE)
@StringDef(value = {HINT_TEXT_IS_EDITABLE, HINT_TEXT_IS_NOT_EDITABLE})
@interface Hints {}
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
@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_UNKNOWN})
@interface WidgetType {}
/** The widget involved in the text classification session is a standard
* {@link android.widget.TextView}. */
public static final String WIDGET_TYPE_TEXTVIEW = "textview";
/** The widget involved in the text classification session is a standard
* {@link android.widget.EditText}. */
public static final String WIDGET_TYPE_EDITTEXT = "edittext";
/** The widget involved in the text classification session is a standard non-selectable
* {@link android.widget.TextView}. */
public static final String WIDGET_TYPE_UNSELECTABLE_TEXTVIEW = "nosel-textview";
/** The widget involved in the text classification session is a standard
* {@link android.webkit.WebView}. */
public static final String WIDGET_TYPE_WEBVIEW = "webview";
/** The widget involved in the text classification session is a standard editable
* {@link android.webkit.WebView}. */
public static final String WIDGET_TYPE_EDIT_WEBVIEW = "edit-webview";
/** The widget involved in the text classification session is a custom text widget. */
public static final String WIDGET_TYPE_CUSTOM_TEXTVIEW = "customview";
/** The widget involved in the text classification session is a custom editable text widget. */
public static final String WIDGET_TYPE_CUSTOM_EDITTEXT = "customedit";
/** The widget involved in the text classification session is a custom non-selectable text
* widget. */
public static final String WIDGET_TYPE_CUSTOM_UNSELECTABLE_TEXTVIEW = "nosel-customview";
/** The widget involved in the text classification session is of an unknown/unspecified type. */
public static final String WIDGET_TYPE_UNKNOWN = "unknown";
private static final int GENERATE_LINKS_MAX_TEXT_LENGTH_DEFAULT = 100 * 1000;
/**
* No-op TextClassifier.
* This may be used to turn off text classifier features.
*/
public static final TextClassifier NO_OP = new TextClassifier() {};
/**
* 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.
*
* @param request the text selection request
*/
@WorkerThread
@NonNull
public TextSelection suggestSelection(@NonNull TextSelection.Request request) {
Preconditions.checkNotNull(request);
ensureNotOnMainThread();
return new TextSelection.Builder(request.getStartIndex(), request.getEndIndex()).build();
}
/**
* 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.
*
* @param request the text classification request
*/
@WorkerThread
@NonNull
public TextClassification classifyText(@NonNull TextClassification.Request request) {
Preconditions.checkNotNull(request);
ensureNotOnMainThread();
return TextClassification.EMPTY;
}
/**
* 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.
*
* @param request the text links request
*
* @see #getMaxGenerateLinksTextLength()
*/
@WorkerThread
@NonNull
public TextLinks generateLinks(@NonNull TextLinks.Request request) {
Preconditions.checkNotNull(request);
ensureNotOnMainThread();
return new TextLinks.Builder(request.getText().toString()).build();
}
/**
* Suggests and returns a list of actions according to the given conversation.
*/
@WorkerThread
@NonNull
public ConversationActions suggestConversationActions(
@NonNull ConversationActions.Request request) {
Preconditions.checkNotNull(request);
ensureNotOnMainThread();
return new ConversationActions(Collections.<ConversationAction>emptyList(), null);
}
/**
* Returns the maximal length of text that can be processed by generateLinks.
*
* @see #generateLinks(TextLinks.Request)
*/
public int getMaxGenerateLinksTextLength() {
return GENERATE_LINKS_MAX_TEXT_LENGTH_DEFAULT;
}
/**
* Reports a selection event.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public final void reportSelectionEvent(@NonNull SelectionEvent event) {
}
/**
* Called when a selection event is reported.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@WorkerThread
public void onSelectionEvent(@NonNull SelectionEvent event) {
}
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
static void ensureNotOnMainThread() {
if (Looper.myLooper() == Looper.getMainLooper()) {
throw new IllegalStateException("Must not be on main thread");
}
}
/**
* Configuration object for specifying what entities to identify.
*
* Configs are initially based on a predefined preset, and can be modified from there.
*/
public static final class EntityConfig {
private static final String EXTRA_HINTS = "hints";
private static final String EXTRA_EXCLUDED_ENTITY_TYPES = "excluded";
private static final String EXTRA_INCLUDED_ENTITY_TYPES = "included";
private static final String EXTRA_INCLUDE_ENTITY_TYPES_FROM_TC =
"include_entity_types_from_tc";
private static final String EXTRA_PLATFORM_ENTITY_CONFIG = "platform_entity_config";
private final List<String> mHints;
private final List<String> mExcludedTypes;
private final List<String> mIncludedTypes;
private final boolean mIncludeTypesFromTextClassifier;
private final PlatformEntityConfigWrapper mPlatformEntityConfigWrapper;
EntityConfig(
Collection<String> includedTypes,
Collection<String> excludedTypes,
Collection<String> hints,
boolean includeTypesFromTextClassifier,
@Nullable PlatformEntityConfigWrapper platformEntityConfigWrapper) {
mIncludedTypes = includedTypes == null
? Collections.<String>emptyList() : new ArrayList<>(includedTypes);
mExcludedTypes = excludedTypes == null
? Collections.<String>emptyList() : new ArrayList<>(excludedTypes);
mHints = hints == null
? Collections.<String>emptyList()
: new ArrayList<>(hints);
mIncludeTypesFromTextClassifier = includeTypesFromTextClassifier;
mPlatformEntityConfigWrapper = platformEntityConfigWrapper;
}
/**
* Creates an androidX {@link EntityConfig} object by wrapping the platform
* {@link android.view.textclassifier.TextClassifier.EntityConfig} object.
*/
private EntityConfig(@NonNull PlatformEntityConfigWrapper platformEntityConfigWrapper) {
this(null, null, null, false, Preconditions.checkNotNull(platformEntityConfigWrapper));
}
/**
* Returns a final list of entity types that the text classifier should look for.
* <p>NOTE: This method is intended for use by text classifier.
*
* @param typesFromTextClassifier entity types the text classifier thinks should be included
* before factoring in the included/excluded entity types given
* by the client.
*/
public Collection<String> resolveTypes(
@Nullable Collection<String> typesFromTextClassifier) {
if (mPlatformEntityConfigWrapper != null && Build.VERSION.SDK_INT >= 28) {
return mPlatformEntityConfigWrapper.resolveEntityTypes(typesFromTextClassifier);
}
Set<String> types = new ArraySet<>();
if (mIncludeTypesFromTextClassifier && typesFromTextClassifier != null) {
types.addAll(typesFromTextClassifier);
}
types.addAll(mIncludedTypes);
types.removeAll(mExcludedTypes);
return Collections.unmodifiableCollection(types);
}
/**
* Retrieves the list of hints.
*
* @return An unmodifiable collection of the hints.
*/
@NonNull
public Collection<String> getHints() {
if (mPlatformEntityConfigWrapper != null && Build.VERSION.SDK_INT >= 28) {
return mPlatformEntityConfigWrapper.getHints();
}
return mHints;
}
/**
* Return whether the client allows the text classifier to include its own list of default
* entity types. If this functions returns {@code true}, text classifier can consider
* to specify its own list in {@link #resolveTypes(Collection)}.
*
* <p>NOTE: This method is intended for use by text classifier.
*
* @see #resolveTypes(Collection)
*/
public boolean shouldIncludeTypesFromTextClassifier() {
if (mPlatformEntityConfigWrapper != null && Build.VERSION.SDK_INT >= 28) {
return mPlatformEntityConfigWrapper.shouldIncludeDefaultEntityTypes();
}
return mIncludeTypesFromTextClassifier;
}
/**
* Adds this EntityConfig to a Bundle that can be read back with the same parameters
* to {@link #createFromBundle(Bundle)}.
*/
@NonNull
public Bundle toBundle() {
final Bundle bundle = new Bundle();
bundle.putStringArrayList(EXTRA_HINTS, new ArrayList<>(mHints));
bundle.putStringArrayList(EXTRA_INCLUDED_ENTITY_TYPES, new ArrayList<>(mIncludedTypes));
bundle.putStringArrayList(EXTRA_EXCLUDED_ENTITY_TYPES, new ArrayList<>(mExcludedTypes));
bundle.putBoolean(EXTRA_INCLUDE_ENTITY_TYPES_FROM_TC, mIncludeTypesFromTextClassifier);
if (mPlatformEntityConfigWrapper != null && Build.VERSION.SDK_INT >= 28) {
bundle.putParcelable(
EXTRA_PLATFORM_ENTITY_CONFIG, mPlatformEntityConfigWrapper.toBundle());
}
return bundle;
}
/**
* Extracts an EntityConfig from a bundle that was added using {@link #toBundle()}.
*/
@NonNull
public static EntityConfig createFromBundle(@NonNull Bundle bundle) {
PlatformEntityConfigWrapper platformEntityConfigWrapper = null;
if (Build.VERSION.SDK_INT >= 28) {
platformEntityConfigWrapper = PlatformEntityConfigWrapper.createFromBundle(
bundle.getBundle(EXTRA_PLATFORM_ENTITY_CONFIG));
}
return new EntityConfig(
bundle.getStringArrayList(EXTRA_INCLUDED_ENTITY_TYPES),
bundle.getStringArrayList(EXTRA_EXCLUDED_ENTITY_TYPES),
bundle.getStringArrayList(EXTRA_HINTS),
bundle.getBoolean(EXTRA_INCLUDE_ENTITY_TYPES_FROM_TC),
platformEntityConfigWrapper);
}
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
@RequiresApi(28)
@NonNull
public android.view.textclassifier.TextClassifier.EntityConfig toPlatform() {
if (Build.VERSION.SDK_INT >= 29) {
return toPlatformQ();
}
return toPlatformP();
}
@RequiresApi(28)
private android.view.textclassifier.TextClassifier.EntityConfig toPlatformP() {
if (mIncludeTypesFromTextClassifier) {
return android.view.textclassifier.TextClassifier.EntityConfig.create(
mHints,
mIncludedTypes,
mExcludedTypes
);
}
Set<String> entitiesSet = new ArraySet<>(mIncludedTypes);
entitiesSet.removeAll(mExcludedTypes);
return android.view.textclassifier.TextClassifier.EntityConfig
.createWithExplicitEntityList(new ArrayList<>(entitiesSet));
}
@RequiresApi(29)
private android.view.textclassifier.TextClassifier.EntityConfig toPlatformQ() {
return new android.view.textclassifier.TextClassifier.EntityConfig.Builder()
.setIncludedTypes(mIncludedTypes)
.setExcludedTypes(mExcludedTypes)
.setHints(mHints)
.includeTypesFromTextClassifier(mIncludeTypesFromTextClassifier)
.build();
}
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
@RequiresApi(28)
@Nullable
public static EntityConfig fromPlatform(
@Nullable android.view.textclassifier.TextClassifier.EntityConfig entityConfig) {
if (entityConfig == null) {
return null;
}
return new EntityConfig(new PlatformEntityConfigWrapper(entityConfig));
}
/**
* Builder class to construct the {@link EntityConfig} object.
*/
public static final class Builder {
@Nullable
private Collection<String> mHints;
@Nullable
private Collection<String> mExcludedTypes;
@Nullable
private Collection<String> mIncludedTypes;
private boolean mIncludeTypesFromTextClassifier = true;
/**
* Sets a collection of entity types that are explicitly included.
*/
public Builder setIncludedTypes(@Nullable Collection<String> includedTypes) {
mIncludedTypes = includedTypes;
return this;
}
/**
* Sets a collection of entity types that are explicitly excluded.
*/
public Builder setExcludedTypes(@Nullable Collection<String> excludedTypes) {
mExcludedTypes = excludedTypes;
return this;
}
/**
* Sets a collection of hints for the text classifier to determine what types of
* entities to find.
*
* @see #HINT_TEXT_IS_EDITABLE
* @see #HINT_TEXT_IS_NOT_EDITABLE
*/
public Builder setHints(@Nullable Collection<String> hints) {
mHints = hints;
return this;
}
/**
* Specifies to include the default entity types suggested by the text classifier. By
* default, it is included.
*/
public Builder includeTypesFromTextClassifier(boolean includeTypesFromTextClassifier) {
mIncludeTypesFromTextClassifier = includeTypesFromTextClassifier;
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,
mExcludedTypes,
mHints,
mIncludeTypesFromTextClassifier,
null);
}
}
}
}