/* * 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. *
* The TextClassifier may be used to understand the meaning of text, as well as generating predicted * next actions based on the text. * *
NOTE: 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. *
* 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. * *
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) { 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. * *
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); } /** * 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) { 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. * *
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); } /** * 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) { 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. * *
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; } /** * Detects the language of the text in the given request. * *
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 {@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); } /** * NOTE: Use {@link #onTextClassifierEvent(TextClassifierEvent)} instead. *
* 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) { // TODO: Consider rerouting to onTextClassifierEvent() } /** * Reports a text classifier event. *
* NOTE: Call on a worker thread. * * @throws IllegalStateException if this TextClassifier has been destroyed. * @see #isDestroyed() */ default void onTextClassifierEvent(@NonNull TextClassifierEvent 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;
}
/** @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 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
*
*
* 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} (inclusive) to index {@code end} <(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