blob: 6d5077a99c62d1ae8fb1ea9c77ef529b9bfe1970 [file] [log] [blame]
/*
* 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_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 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");
}
}
}
}