/* * 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 android.util.ArraySet; import com.android.internal.util.Preconditions; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Map; import java.util.Set; /** * Interface for providing text classification related features. * *
NOTE: Unless otherwise stated, methods of this interface are blocking * operations. Call on a worker thread. */ public interface TextClassifier { /** @hide */ String DEFAULT_LOG_TAG = "androidtc"; /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(value = {LOCAL, 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; /** 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"; /** @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, }) @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_UNKNOWN}) @interface WidgetType {} /** The widget involved in the text classification session is a standard * {@link android.widget.TextView}. */ String WIDGET_TYPE_TEXTVIEW = "textview"; /** The widget involved in the text classification session is a standard * {@link android.widget.EditText}. */ String WIDGET_TYPE_EDITTEXT = "edittext"; /** The widget involved in the text classification session is a standard non-selectable * {@link android.widget.TextView}. */ String WIDGET_TYPE_UNSELECTABLE_TEXTVIEW = "nosel-textview"; /** The widget involved in the text classification session is a standard * {@link android.webkit.WebView}. */ String WIDGET_TYPE_WEBVIEW = "webview"; /** The widget involved in the text classification session is a standard editable * {@link android.webkit.WebView}. */ String WIDGET_TYPE_EDIT_WEBVIEW = "edit-webview"; /** The widget involved in the text classification session is a custom text widget. */ String WIDGET_TYPE_CUSTOM_TEXTVIEW = "customview"; /** The widget involved in the text classification session is a custom editable text widget. */ String WIDGET_TYPE_CUSTOM_EDITTEXT = "customedit"; /** The widget involved in the text classification session is a custom non-selectable text * widget. */ String WIDGET_TYPE_CUSTOM_UNSELECTABLE_TEXTVIEW = "nosel-customview"; /** The widget involved in the text classification session 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() {}; /** * 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. * *
NOTE: Call on a worker thread. * *
NOTE: 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) { Preconditions.checkNotNull(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. * *
NOTE: Call on a worker thread. * *
NOTE: If a TextClassifier has been destroyed, calls to this method should * throw an {@link IllegalStateException}. See {@link #isDestroyed()}. * *
NOTE: 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); } // TODO: Remove once apps can build against the latest sdk. /** @hide */ default TextSelection suggestSelection( @NonNull CharSequence text, @IntRange(from = 0) int selectionStartIndex, @IntRange(from = 0) int selectionEndIndex, @Nullable TextSelection.Options options) { if (options == null) { return suggestSelection(new TextSelection.Request.Builder( text, selectionStartIndex, selectionEndIndex).build()); } else if (options.getRequest() != null) { return suggestSelection(options.getRequest()); } else { return suggestSelection( new TextSelection.Request.Builder(text, selectionStartIndex, selectionEndIndex) .setDefaultLocales(options.getDefaultLocales()) .build()); } } /** * Classifies the specified text and returns a {@link TextClassification} object that can be * used to generate a widget for handling the classified text. * *
NOTE: Call on a worker thread. * * NOTE: 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) { Preconditions.checkNotNull(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. * *
NOTE: Call on a worker thread. * *
NOTE: 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. * * NOTE: 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); } // TODO: Remove once apps can build against the latest sdk. /** @hide */ default TextClassification classifyText( @NonNull CharSequence text, @IntRange(from = 0) int startIndex, @IntRange(from = 0) int endIndex, @Nullable TextClassification.Options options) { if (options == null) { return classifyText( new TextClassification.Request.Builder(text, startIndex, endIndex).build()); } else if (options.getRequest() != null) { return classifyText(options.getRequest()); } else { return classifyText(new TextClassification.Request.Builder(text, startIndex, endIndex) .setDefaultLocales(options.getDefaultLocales()) .setReferenceTime(options.getReferenceTime()) .build()); } } /** * Generates and returns a {@link TextLinks} that may be applied to the text to annotate it with * links information. * *
NOTE: Call on a worker thread. * * NOTE: 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) { Preconditions.checkNotNull(request); Utils.checkMainThread(); return new TextLinks.Builder(request.getText().toString()).build(); } // TODO: Remove once apps can build against the latest sdk. /** @hide */ default TextLinks generateLinks( @NonNull CharSequence text, @Nullable TextLinks.Options options) { if (options == null) { return generateLinks(new TextLinks.Request.Builder(text).build()); } else if (options.getRequest() != null) { return generateLinks(options.getRequest()); } else { return generateLinks(new TextLinks.Request.Builder(text) .setDefaultLocales(options.getDefaultLocales()) .setEntityConfig(options.getEntityConfig()) .build()); } } /** * Returns the maximal length of text that can be processed by generateLinks. * * NOTE: 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; } /** * Reports a selection event. * * NOTE: If a TextClassifier has been destroyed, calls to this method should * throw an {@link IllegalStateException}. See {@link #isDestroyed()}. */ default void onSelectionEvent(@NonNull SelectionEvent event) {} /** * Destroys this TextClassifier. * * NOTE: If a TextClassifier has been destroyed, calls to its methods should * throw an {@link IllegalStateException}. See {@link #isDestroyed()}. * *
Subsequent calls to this method are no-ops.
*/
default void destroy() {}
/**
* Returns whether or not this TextClassifier has been destroyed.
*
* NOTE: 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;
}
/**
* Configuration object for specifying what entities to identify.
*
* Configs are initially based on a predefined preset, and can be modified from there.
*/
final class EntityConfig implements Parcelable {
private final Collection
*
*
* Intended to be used only in this package.
* @hide
*/
final class Utils {
/**
* @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);
}
static void checkTextLength(CharSequence text, int maxLength) {
Preconditions.checkArgumentInRange(text.length(), 0, maxLength, "text.length()");
}
/**
* 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