• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.view.textclassifier;
18 
19 import android.annotation.FlaggedApi;
20 import android.annotation.IntDef;
21 import android.annotation.IntRange;
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.annotation.StringDef;
25 import android.annotation.SuppressLint;
26 import android.annotation.SystemApi;
27 import android.annotation.WorkerThread;
28 import android.os.LocaleList;
29 import android.os.Looper;
30 import android.os.Parcel;
31 import android.os.Parcelable;
32 import android.permission.flags.Flags;
33 import android.text.Spannable;
34 import android.text.SpannableString;
35 import android.text.style.URLSpan;
36 import android.text.util.Linkify;
37 import android.text.util.Linkify.LinkifyMask;
38 import android.util.ArrayMap;
39 
40 import com.android.internal.annotations.GuardedBy;
41 import com.android.internal.util.IndentingPrintWriter;
42 import com.android.internal.util.Preconditions;
43 
44 import java.lang.annotation.Retention;
45 import java.lang.annotation.RetentionPolicy;
46 import java.text.BreakIterator;
47 import java.util.ArrayList;
48 import java.util.Collection;
49 import java.util.Collections;
50 import java.util.HashSet;
51 import java.util.List;
52 import java.util.Map;
53 import java.util.Objects;
54 import java.util.Set;
55 
56 /**
57  * Interface for providing text classification related features.
58  * <p>
59  * The TextClassifier may be used to understand the meaning of text, as well as generating predicted
60  * next actions based on the text.
61  *
62  * <p><strong>NOTE: </strong>Unless otherwise stated, methods of this interface are blocking
63  * operations. Call on a worker thread.
64  */
65 public interface TextClassifier {
66 
67     /** @hide */
68     String LOG_TAG = "androidtc";
69 
70     /** Specifies a TextClassifier that runs locally in the app's process. @hide */
71     int LOCAL = 0;
72     /** Specifies a TextClassifier that runs in the system process and serves all apps. @hide */
73     int SYSTEM = 1;
74     /** Specifies the default TextClassifier that runs in the system process. @hide */
75     int DEFAULT_SYSTEM = 2;
76 
77     /** @hide */
78     @Retention(RetentionPolicy.SOURCE)
79     @IntDef(value = {CLASSIFIER_TYPE_SELF_PROVIDED, CLASSIFIER_TYPE_DEVICE_DEFAULT,
80             CLASSIFIER_TYPE_ANDROID_DEFAULT})
81     @interface TextClassifierType {
82     }
83     /** Specifies a TextClassifier that runs locally in the app's process. @hide */
84     @FlaggedApi(Flags.FLAG_TEXT_CLASSIFIER_CHOICE_API_ENABLED)
85     @SystemApi
86     int CLASSIFIER_TYPE_SELF_PROVIDED = LOCAL;
87     /**
88      * Specifies a TextClassifier that is set as the default on this particular device. This may be
89      * the same as CLASSIFIER_TYPE_DEVICE_DEFAULT, unless set otherwise by the device manufacturer.
90      * @hide
91      */
92     @FlaggedApi(Flags.FLAG_TEXT_CLASSIFIER_CHOICE_API_ENABLED)
93     @SystemApi
94     int CLASSIFIER_TYPE_DEVICE_DEFAULT = SYSTEM;
95     /** Specifies the TextClassifier that is provided by Android. @hide */
96     @FlaggedApi(Flags.FLAG_TEXT_CLASSIFIER_CHOICE_API_ENABLED)
97     @SystemApi
98     int CLASSIFIER_TYPE_ANDROID_DEFAULT = DEFAULT_SYSTEM;
99 
100     /** @hide */
101     @SuppressLint("SwitchIntDef")
typeToString(@extClassifierType int type)102     static String typeToString(@TextClassifierType int type) {
103         int unflaggedType = type;
104         switch (unflaggedType) {
105             case LOCAL:
106                 return "Local";
107             case SYSTEM:
108                 return "System";
109             case DEFAULT_SYSTEM:
110                 return "Default system";
111         }
112         return "Unknown";
113     }
114 
115     /** The TextClassifier failed to run. */
116     String TYPE_UNKNOWN = "";
117     /** The classifier ran, but didn't recognize a known entity. */
118     String TYPE_OTHER = "other";
119     /** E-mail address (e.g. "noreply@android.com"). */
120     String TYPE_EMAIL = "email";
121     /** Phone number (e.g. "555-123 456"). */
122     String TYPE_PHONE = "phone";
123     /** Physical address. */
124     String TYPE_ADDRESS = "address";
125     /** Web URL. */
126     String TYPE_URL = "url";
127     /** Time reference that is no more specific than a date. May be absolute such as "01/01/2000" or
128      * relative like "tomorrow". **/
129     String TYPE_DATE = "date";
130     /** Time reference that includes a specific time. May be absolute such as "01/01/2000 5:30pm" or
131      * relative like "tomorrow at 5:30pm". **/
132     String TYPE_DATE_TIME = "datetime";
133     /** Flight number in IATA format. */
134     String TYPE_FLIGHT_NUMBER = "flight";
135     /** Onetime password. */
136     @FlaggedApi(Flags.FLAG_TEXT_CLASSIFIER_CHOICE_API_ENABLED)
137     String TYPE_OTP = "otp";
138     /**
139      * Word that users may be interested to look up for meaning.
140      * @hide
141      */
142     String TYPE_DICTIONARY = "dictionary";
143 
144     /** @hide */
145     @Retention(RetentionPolicy.SOURCE)
146     @StringDef(prefix = { "TYPE_" }, value = {
147             TYPE_UNKNOWN,
148             TYPE_OTHER,
149             TYPE_EMAIL,
150             TYPE_PHONE,
151             TYPE_ADDRESS,
152             TYPE_URL,
153             TYPE_DATE,
154             TYPE_DATE_TIME,
155             TYPE_FLIGHT_NUMBER,
156             TYPE_DICTIONARY,
157             TYPE_OTP
158     })
159     @interface EntityType {}
160 
161     /** Designates that the text in question is editable. **/
162     String HINT_TEXT_IS_EDITABLE = "android.text_is_editable";
163     /** Designates that the text in question is not editable. **/
164     String HINT_TEXT_IS_NOT_EDITABLE = "android.text_is_not_editable";
165 
166     /** @hide */
167     @Retention(RetentionPolicy.SOURCE)
168     @StringDef(prefix = { "HINT_" }, value = {HINT_TEXT_IS_EDITABLE, HINT_TEXT_IS_NOT_EDITABLE})
169     @interface Hints {}
170 
171     /** @hide */
172     @Retention(RetentionPolicy.SOURCE)
173     @StringDef({WIDGET_TYPE_TEXTVIEW, WIDGET_TYPE_EDITTEXT, WIDGET_TYPE_UNSELECTABLE_TEXTVIEW,
174             WIDGET_TYPE_WEBVIEW, WIDGET_TYPE_EDIT_WEBVIEW, WIDGET_TYPE_CUSTOM_TEXTVIEW,
175             WIDGET_TYPE_CUSTOM_EDITTEXT, WIDGET_TYPE_CUSTOM_UNSELECTABLE_TEXTVIEW,
176             WIDGET_TYPE_NOTIFICATION, WIDGET_TYPE_CLIPBOARD, WIDGET_TYPE_UNKNOWN })
177     @interface WidgetType {}
178 
179     /** The widget involved in the text classification context is a standard
180      * {@link android.widget.TextView}. */
181     String WIDGET_TYPE_TEXTVIEW = "textview";
182     /** The widget involved in the text classification context is a standard
183      * {@link android.widget.EditText}. */
184     String WIDGET_TYPE_EDITTEXT = "edittext";
185     /** The widget involved in the text classification context is a standard non-selectable
186      * {@link android.widget.TextView}. */
187     String WIDGET_TYPE_UNSELECTABLE_TEXTVIEW = "nosel-textview";
188     /** The widget involved in the text classification context is a standard
189      * {@link android.webkit.WebView}. */
190     String WIDGET_TYPE_WEBVIEW = "webview";
191     /** The widget involved in the text classification context is a standard editable
192      * {@link android.webkit.WebView}. */
193     String WIDGET_TYPE_EDIT_WEBVIEW = "edit-webview";
194     /** The widget involved in the text classification context is a custom text widget. */
195     String WIDGET_TYPE_CUSTOM_TEXTVIEW = "customview";
196     /** The widget involved in the text classification context is a custom editable text widget. */
197     String WIDGET_TYPE_CUSTOM_EDITTEXT = "customedit";
198     /** The widget involved in the text classification context is a custom non-selectable text
199      * widget. */
200     String WIDGET_TYPE_CUSTOM_UNSELECTABLE_TEXTVIEW = "nosel-customview";
201     /** The widget involved in the text classification context is a notification */
202     String WIDGET_TYPE_NOTIFICATION = "notification";
203     /** The text classification context is for use with the system clipboard. */
204     String WIDGET_TYPE_CLIPBOARD = "clipboard";
205     /** The widget involved in the text classification context is of an unknown/unspecified type. */
206     String WIDGET_TYPE_UNKNOWN = "unknown";
207 
208     /**
209      * No-op TextClassifier.
210      * This may be used to turn off TextClassifier features.
211      */
212     TextClassifier NO_OP = new TextClassifier() {
213         @Override
214         public String toString() {
215             return "TextClassifier.NO_OP";
216         }
217     };
218 
219     /**
220      * Extra that is included on activity intents coming from a TextClassifier when
221      * it suggests actions to its caller.
222      * <p>
223      * All {@link TextClassifier} implementations should make sure this extra exists in their
224      * generated intents.
225      */
226     String EXTRA_FROM_TEXT_CLASSIFIER = "android.view.textclassifier.extra.FROM_TEXT_CLASSIFIER";
227 
228     /**
229      * Extra specifying the package name of the app from which the text to be classified originated.
230      *
231      * For example, a notification assistant might use TextClassifier, but the notification
232      * content could originate from a different app. This key allows you to provide
233      * the package name of that source app.
234      */
235     @FlaggedApi(Flags.FLAG_TEXT_CLASSIFIER_CHOICE_API_ENABLED)
236     String EXTRA_TEXT_ORIGIN_PACKAGE = "android.view.textclassifier.extra.TEXT_ORIGIN_PACKAGE";
237 
238     /**
239      * Returns suggested text selection start and end indices, recognized entity types, and their
240      * associated confidence scores. The entity types are ordered from highest to lowest scoring.
241      *
242      * <p><strong>NOTE: </strong>Call on a worker thread.
243      *
244      * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
245      * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
246      *
247      * @param request the text selection request
248      */
249     @WorkerThread
250     @NonNull
suggestSelection(@onNull TextSelection.Request request)251     default TextSelection suggestSelection(@NonNull TextSelection.Request request) {
252         Objects.requireNonNull(request);
253         Utils.checkMainThread();
254         return new TextSelection.Builder(request.getStartIndex(), request.getEndIndex()).build();
255     }
256 
257     /**
258      * Returns suggested text selection start and end indices, recognized entity types, and their
259      * associated confidence scores. The entity types are ordered from highest to lowest scoring.
260      *
261      * <p><strong>NOTE: </strong>Call on a worker thread.
262      *
263      * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
264      * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
265      *
266      * <p><b>NOTE:</b> Do not implement. The default implementation of this method calls
267      * {@link #suggestSelection(TextSelection.Request)}. If that method calls this method,
268      * a stack overflow error will happen.
269      *
270      * @param text text providing context for the selected text (which is specified
271      *      by the sub sequence starting at selectionStartIndex and ending at selectionEndIndex)
272      * @param selectionStartIndex start index of the selected part of text
273      * @param selectionEndIndex end index of the selected part of text
274      * @param defaultLocales ordered list of locale preferences that may be used to
275      *      disambiguate the provided text. If no locale preferences exist, set this to null
276      *      or an empty locale list.
277      *
278      * @throws IllegalArgumentException if text is null; selectionStartIndex is negative;
279      *      selectionEndIndex is greater than text.length() or not greater than selectionStartIndex
280      *
281      * @see #suggestSelection(TextSelection.Request)
282      */
283     @WorkerThread
284     @NonNull
suggestSelection( @onNull CharSequence text, @IntRange(from = 0) int selectionStartIndex, @IntRange(from = 0) int selectionEndIndex, @Nullable LocaleList defaultLocales)285     default TextSelection suggestSelection(
286             @NonNull CharSequence text,
287             @IntRange(from = 0) int selectionStartIndex,
288             @IntRange(from = 0) int selectionEndIndex,
289             @Nullable LocaleList defaultLocales) {
290         final TextSelection.Request request = new TextSelection.Request.Builder(
291                 text, selectionStartIndex, selectionEndIndex)
292                 .setDefaultLocales(defaultLocales)
293                 .build();
294         return suggestSelection(request);
295     }
296 
297     /**
298      * Classifies the specified text and returns a {@link TextClassification} object that can be
299      * used to generate a widget for handling the classified text.
300      *
301      * <p><strong>NOTE: </strong>Call on a worker thread.
302      *
303      * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
304      * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
305      *
306      * @param request the text classification request
307      */
308     @WorkerThread
309     @NonNull
classifyText(@onNull TextClassification.Request request)310     default TextClassification classifyText(@NonNull TextClassification.Request request) {
311         Objects.requireNonNull(request);
312         Utils.checkMainThread();
313         return TextClassification.EMPTY;
314     }
315 
316     /**
317      * Classifies the specified text and returns a {@link TextClassification} object that can be
318      * used to generate a widget for handling the classified text.
319      *
320      * <p><strong>NOTE: </strong>Call on a worker thread.
321      *
322      * <p><b>NOTE:</b> Do not implement. The default implementation of this method calls
323      * {@link #classifyText(TextClassification.Request)}. If that method calls this method,
324      * a stack overflow error will happen.
325      *
326      * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
327      * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
328      *
329      * @param text text providing context for the text to classify (which is specified
330      *      by the sub sequence starting at startIndex and ending at endIndex)
331      * @param startIndex start index of the text to classify
332      * @param endIndex end index of the text to classify
333      * @param defaultLocales ordered list of locale preferences that may be used to
334      *      disambiguate the provided text. If no locale preferences exist, set this to null
335      *      or an empty locale list.
336      *
337      * @throws IllegalArgumentException if text is null; startIndex is negative;
338      *      endIndex is greater than text.length() or not greater than startIndex
339      *
340      * @see #classifyText(TextClassification.Request)
341      */
342     @WorkerThread
343     @NonNull
classifyText( @onNull CharSequence text, @IntRange(from = 0) int startIndex, @IntRange(from = 0) int endIndex, @Nullable LocaleList defaultLocales)344     default TextClassification classifyText(
345             @NonNull CharSequence text,
346             @IntRange(from = 0) int startIndex,
347             @IntRange(from = 0) int endIndex,
348             @Nullable LocaleList defaultLocales) {
349         final TextClassification.Request request = new TextClassification.Request.Builder(
350                 text, startIndex, endIndex)
351                 .setDefaultLocales(defaultLocales)
352                 .build();
353         return classifyText(request);
354     }
355 
356     /**
357      * Generates and returns a {@link TextLinks} that may be applied to the text to annotate it with
358      * links information.
359      *
360      * <p><strong>NOTE: </strong>Call on a worker thread.
361      *
362      * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
363      * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
364      *
365      * @param request the text links request
366      *
367      * @see #getMaxGenerateLinksTextLength()
368      */
369     @WorkerThread
370     @NonNull
generateLinks(@onNull TextLinks.Request request)371     default TextLinks generateLinks(@NonNull TextLinks.Request request) {
372         Objects.requireNonNull(request);
373         Utils.checkMainThread();
374         return new TextLinks.Builder(request.getText().toString()).build();
375     }
376 
377     /**
378      * Returns the maximal length of text that can be processed by generateLinks.
379      *
380      * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
381      * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
382      *
383      * @see #generateLinks(TextLinks.Request)
384      */
385     @WorkerThread
getMaxGenerateLinksTextLength()386     default int getMaxGenerateLinksTextLength() {
387         return Integer.MAX_VALUE;
388     }
389 
390     /**
391      * Detects the language of the text in the given request.
392      *
393      * <p><strong>NOTE: </strong>Call on a worker thread.
394      *
395      *
396      * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
397      * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
398      *
399      * @param request the {@link TextLanguage} request.
400      * @return the {@link TextLanguage} result.
401      */
402     @WorkerThread
403     @NonNull
detectLanguage(@onNull TextLanguage.Request request)404     default TextLanguage detectLanguage(@NonNull TextLanguage.Request request) {
405         Objects.requireNonNull(request);
406         Utils.checkMainThread();
407         return TextLanguage.EMPTY;
408     }
409 
410     /**
411      * Suggests and returns a list of actions according to the given conversation.
412      */
413     @WorkerThread
414     @NonNull
suggestConversationActions( @onNull ConversationActions.Request request)415     default ConversationActions suggestConversationActions(
416             @NonNull ConversationActions.Request request) {
417         Objects.requireNonNull(request);
418         Utils.checkMainThread();
419         return new ConversationActions(Collections.emptyList(), null);
420     }
421 
422     /**
423      * <strong>NOTE: </strong>Use {@link #onTextClassifierEvent(TextClassifierEvent)} instead.
424      * <p>
425      * Reports a selection event.
426      *
427      * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
428      * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
429      */
onSelectionEvent(@onNull SelectionEvent event)430     default void onSelectionEvent(@NonNull SelectionEvent event) {
431         // TODO: Consider rerouting to onTextClassifierEvent()
432     }
433 
434     /**
435      * Reports a text classifier event.
436      * <p>
437      * <strong>NOTE: </strong>Call on a worker thread.
438      *
439      * @throws IllegalStateException if this TextClassifier has been destroyed.
440      * @see #isDestroyed()
441      */
onTextClassifierEvent(@onNull TextClassifierEvent event)442     default void onTextClassifierEvent(@NonNull TextClassifierEvent event) {}
443 
444     /**
445      * Destroys this TextClassifier.
446      *
447      * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to its methods should
448      * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
449      *
450      * <p>Subsequent calls to this method are no-ops.
451      */
destroy()452     default void destroy() {}
453 
454     /**
455      * Returns whether or not this TextClassifier has been destroyed.
456      *
457      * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, caller should not interact
458      * with the classifier and an attempt to do so would throw an {@link IllegalStateException}.
459      * However, this method should never throw an {@link IllegalStateException}.
460      *
461      * @see #destroy()
462      */
isDestroyed()463     default boolean isDestroyed() {
464         return false;
465     }
466 
467     /** @hide **/
dump(@onNull IndentingPrintWriter printWriter)468     default void dump(@NonNull IndentingPrintWriter printWriter) {}
469 
470     /**
471      * Configuration object for specifying what entity types to identify.
472      *
473      * Configs are initially based on a predefined preset, and can be modified from there.
474      */
475     final class EntityConfig implements Parcelable {
476         private final List<String> mIncludedTypes;
477         private final List<String> mExcludedTypes;
478         private final List<String> mHints;
479         private final boolean mIncludeTypesFromTextClassifier;
480 
EntityConfig( List<String> includedEntityTypes, List<String> excludedEntityTypes, List<String> hints, boolean includeTypesFromTextClassifier)481         private EntityConfig(
482                 List<String> includedEntityTypes,
483                 List<String> excludedEntityTypes,
484                 List<String> hints,
485                 boolean includeTypesFromTextClassifier) {
486             mIncludedTypes = Objects.requireNonNull(includedEntityTypes);
487             mExcludedTypes = Objects.requireNonNull(excludedEntityTypes);
488             mHints = Objects.requireNonNull(hints);
489             mIncludeTypesFromTextClassifier = includeTypesFromTextClassifier;
490         }
491 
EntityConfig(Parcel in)492         private EntityConfig(Parcel in) {
493             mIncludedTypes = new ArrayList<>();
494             in.readStringList(mIncludedTypes);
495             mExcludedTypes = new ArrayList<>();
496             in.readStringList(mExcludedTypes);
497             List<String> tmpHints = new ArrayList<>();
498             in.readStringList(tmpHints);
499             mHints = Collections.unmodifiableList(tmpHints);
500             mIncludeTypesFromTextClassifier = in.readByte() != 0;
501         }
502 
503         @Override
writeToParcel(Parcel parcel, int flags)504         public void writeToParcel(Parcel parcel, int flags) {
505             parcel.writeStringList(mIncludedTypes);
506             parcel.writeStringList(mExcludedTypes);
507             parcel.writeStringList(mHints);
508             parcel.writeByte((byte) (mIncludeTypesFromTextClassifier ? 1 : 0));
509         }
510 
511         /**
512          * Creates an EntityConfig.
513          *
514          * @param hints Hints for the TextClassifier to determine what types of entities to find.
515          *
516          * @deprecated Use {@link Builder} instead.
517          */
518         @Deprecated
createWithHints(@ullable Collection<String> hints)519         public static EntityConfig createWithHints(@Nullable Collection<String> hints) {
520             return new EntityConfig.Builder()
521                     .includeTypesFromTextClassifier(true)
522                     .setHints(hints)
523                     .build();
524         }
525 
526         /**
527          * Creates an EntityConfig.
528          *
529          * @param hints Hints for the TextClassifier to determine what types of entities to find
530          * @param includedEntityTypes Entity types, e.g. {@link #TYPE_EMAIL}, to explicitly include
531          * @param excludedEntityTypes Entity types, e.g. {@link #TYPE_PHONE}, to explicitly exclude
532          *
533          *
534          * Note that if an entity has been excluded, the exclusion will take precedence.
535          *
536          * @deprecated Use {@link Builder} instead.
537          */
538         @Deprecated
create(@ullable Collection<String> hints, @Nullable Collection<String> includedEntityTypes, @Nullable Collection<String> excludedEntityTypes)539         public static EntityConfig create(@Nullable Collection<String> hints,
540                 @Nullable Collection<String> includedEntityTypes,
541                 @Nullable Collection<String> excludedEntityTypes) {
542             return new EntityConfig.Builder()
543                     .setIncludedTypes(includedEntityTypes)
544                     .setExcludedTypes(excludedEntityTypes)
545                     .setHints(hints)
546                     .includeTypesFromTextClassifier(true)
547                     .build();
548         }
549 
550         /**
551          * Creates an EntityConfig with an explicit entity list.
552          *
553          * @param entityTypes Complete set of entities, e.g. {@link #TYPE_URL} to find.
554          *
555          * @deprecated Use {@link Builder} instead.
556          */
557         @Deprecated
createWithExplicitEntityList( @ullable Collection<String> entityTypes)558         public static EntityConfig createWithExplicitEntityList(
559                 @Nullable Collection<String> entityTypes) {
560             return new EntityConfig.Builder()
561                     .setIncludedTypes(entityTypes)
562                     .includeTypesFromTextClassifier(false)
563                     .build();
564         }
565 
566         /**
567          * Returns a final list of entity types to find.
568          *
569          * @param entityTypes Entity types we think should be found before factoring in
570          *                    includes/excludes
571          *
572          * This method is intended for use by TextClassifier implementations.
573          */
resolveEntityListModifications( @onNull Collection<String> entityTypes)574         public Collection<String> resolveEntityListModifications(
575                 @NonNull Collection<String> entityTypes) {
576             final Set<String> finalSet = new HashSet<>();
577             if (mIncludeTypesFromTextClassifier) {
578                 finalSet.addAll(entityTypes);
579             }
580             finalSet.addAll(mIncludedTypes);
581             finalSet.removeAll(mExcludedTypes);
582             return finalSet;
583         }
584 
585         /**
586          * Retrieves the list of hints.
587          *
588          * @return An unmodifiable collection of the hints.
589          */
getHints()590         public Collection<String> getHints() {
591             return mHints;
592         }
593 
594         /**
595          * Return whether the client allows the text classifier to include its own list of
596          * default types. If this function returns {@code true}, a default list of types suggested
597          * from a text classifier will be taking into account.
598          *
599          * <p>NOTE: This method is intended for use by a text classifier.
600          *
601          * @see #resolveEntityListModifications(Collection)
602          */
shouldIncludeTypesFromTextClassifier()603         public boolean shouldIncludeTypesFromTextClassifier() {
604             return mIncludeTypesFromTextClassifier;
605         }
606 
607         @Override
describeContents()608         public int describeContents() {
609             return 0;
610         }
611 
612         public static final @android.annotation.NonNull Parcelable.Creator<EntityConfig> CREATOR =
613                 new Parcelable.Creator<EntityConfig>() {
614                     @Override
615                     public EntityConfig createFromParcel(Parcel in) {
616                         return new EntityConfig(in);
617                     }
618 
619                     @Override
620                     public EntityConfig[] newArray(int size) {
621                         return new EntityConfig[size];
622                     }
623                 };
624 
625 
626 
627         /** Builder class to construct the {@link EntityConfig} object. */
628         public static final class Builder {
629             @Nullable
630             private Collection<String> mIncludedTypes;
631             @Nullable
632             private Collection<String> mExcludedTypes;
633             @Nullable
634             private Collection<String> mHints;
635             private boolean mIncludeTypesFromTextClassifier = true;
636 
637             /**
638              * Sets a collection of types that are explicitly included.
639              */
640             @NonNull
setIncludedTypes(@ullable Collection<String> includedTypes)641             public Builder setIncludedTypes(@Nullable Collection<String> includedTypes) {
642                 mIncludedTypes = includedTypes;
643                 return this;
644             }
645 
646             /**
647              * Sets a collection of types that are explicitly excluded.
648              */
649             @NonNull
setExcludedTypes(@ullable Collection<String> excludedTypes)650             public Builder setExcludedTypes(@Nullable Collection<String> excludedTypes) {
651                 mExcludedTypes = excludedTypes;
652                 return this;
653             }
654 
655             /**
656              * Specifies whether or not to include the types suggested by the text classifier. By
657              * default, it is included.
658              */
659             @NonNull
includeTypesFromTextClassifier(boolean includeTypesFromTextClassifier)660             public Builder includeTypesFromTextClassifier(boolean includeTypesFromTextClassifier) {
661                 mIncludeTypesFromTextClassifier = includeTypesFromTextClassifier;
662                 return this;
663             }
664 
665 
666             /**
667              * Sets the hints for the TextClassifier to determine what types of entities to find.
668              * These hints will only be used if {@link #includeTypesFromTextClassifier} is
669              * set to be true.
670              */
671             @NonNull
setHints(@ullable Collection<String> hints)672             public Builder setHints(@Nullable Collection<String> hints) {
673                 mHints = hints;
674                 return this;
675             }
676 
677             /**
678              * Combines all of the options that have been set and returns a new {@link EntityConfig}
679              * object.
680              */
681             @NonNull
build()682             public EntityConfig build() {
683                 return new EntityConfig(
684                         mIncludedTypes == null
685                                 ? Collections.emptyList()
686                                 : new ArrayList<>(mIncludedTypes),
687                         mExcludedTypes == null
688                                 ? Collections.emptyList()
689                                 : new ArrayList<>(mExcludedTypes),
690                         mHints == null
691                                 ? Collections.emptyList()
692                                 : Collections.unmodifiableList(new ArrayList<>(mHints)),
693                         mIncludeTypesFromTextClassifier);
694             }
695         }
696     }
697 
698     /**
699      * Utility functions for TextClassifier methods.
700      *
701      * <ul>
702      *  <li>Provides validation of input parameters to TextClassifier methods
703      * </ul>
704      *
705      * Intended to be used only for TextClassifier purposes.
706      * @hide
707      */
708     final class Utils {
709 
710         @GuardedBy("WORD_ITERATOR")
711         private static final BreakIterator WORD_ITERATOR = BreakIterator.getWordInstance();
712 
713         /**
714          * @throws IllegalArgumentException if text is null; startIndex is negative;
715          *      endIndex is greater than text.length() or is not greater than startIndex;
716          *      options is null
717          */
checkArgument(@onNull CharSequence text, int startIndex, int endIndex)718         static void checkArgument(@NonNull CharSequence text, int startIndex, int endIndex) {
719             Preconditions.checkArgument(text != null);
720             Preconditions.checkArgument(startIndex >= 0);
721             Preconditions.checkArgument(endIndex <= text.length());
722             Preconditions.checkArgument(endIndex > startIndex);
723         }
724 
725         /** Returns if the length of the text is within the range. */
checkTextLength(CharSequence text, int maxLength)726         static boolean checkTextLength(CharSequence text, int maxLength) {
727             int textLength = text.length();
728             return textLength >= 0 && textLength <= maxLength;
729         }
730 
731         /**
732          * Returns the substring of {@code text} that contains at least text from index
733          * {@code start} <i>(inclusive)</i> to index {@code end} <i><(exclusive)/i> with the goal of
734          * returning text that is at least {@code minimumLength}. If {@code text} is not long
735          * enough, this will return {@code text}. This method returns text at word boundaries.
736          *
737          * @param text the source text
738          * @param start the start index of text that must be included
739          * @param end the end index of text that must be included
740          * @param minimumLength minimum length of text to return if {@code text} is long enough
741          */
getSubString( String text, int start, int end, int minimumLength)742         public static String getSubString(
743                 String text, int start, int end, int minimumLength) {
744             Preconditions.checkArgument(start >= 0);
745             Preconditions.checkArgument(end <= text.length());
746             Preconditions.checkArgument(start <= end);
747 
748             if (text.length() < minimumLength) {
749                 return text;
750             }
751 
752             final int length = end - start;
753             if (length >= minimumLength) {
754                 return text.substring(start, end);
755             }
756 
757             final int offset = (minimumLength - length) / 2;
758             int iterStart = Math.max(0, Math.min(start - offset, text.length() - minimumLength));
759             int iterEnd = Math.min(text.length(), iterStart + minimumLength);
760 
761             synchronized (WORD_ITERATOR) {
762                 WORD_ITERATOR.setText(text);
763                 iterStart = WORD_ITERATOR.isBoundary(iterStart)
764                         ? iterStart : Math.max(0, WORD_ITERATOR.preceding(iterStart));
765                 iterEnd = WORD_ITERATOR.isBoundary(iterEnd)
766                         ? iterEnd : Math.max(iterEnd, WORD_ITERATOR.following(iterEnd));
767                 WORD_ITERATOR.setText("");
768                 return text.substring(iterStart, iterEnd);
769             }
770         }
771 
772         /**
773          * Generates links using legacy {@link Linkify}.
774          */
generateLegacyLinks(@onNull TextLinks.Request request)775         public static TextLinks generateLegacyLinks(@NonNull TextLinks.Request request) {
776             final String string = request.getText().toString();
777             final TextLinks.Builder links = new TextLinks.Builder(string);
778 
779             final Collection<String> entities = request.getEntityConfig()
780                     .resolveEntityListModifications(Collections.emptyList());
781             if (entities.contains(TextClassifier.TYPE_URL)) {
782                 addLinks(links, string, TextClassifier.TYPE_URL);
783             }
784             if (entities.contains(TextClassifier.TYPE_PHONE)) {
785                 addLinks(links, string, TextClassifier.TYPE_PHONE);
786             }
787             if (entities.contains(TextClassifier.TYPE_EMAIL)) {
788                 addLinks(links, string, TextClassifier.TYPE_EMAIL);
789             }
790             // NOTE: Do not support MAP_ADDRESSES. Legacy version does not work well.
791             return links.build();
792         }
793 
addLinks( TextLinks.Builder links, String string, @EntityType String entityType)794         private static void addLinks(
795                 TextLinks.Builder links, String string, @EntityType String entityType) {
796             final Spannable spannable = new SpannableString(string);
797             if (Linkify.addLinks(spannable, linkMask(entityType))) {
798                 final URLSpan[] spans = spannable.getSpans(0, spannable.length(), URLSpan.class);
799                 for (URLSpan urlSpan : spans) {
800                     links.addLink(
801                             spannable.getSpanStart(urlSpan),
802                             spannable.getSpanEnd(urlSpan),
803                             entityScores(entityType),
804                             urlSpan);
805                 }
806             }
807         }
808 
809         @LinkifyMask
linkMask(@ntityType String entityType)810         private static int linkMask(@EntityType String entityType) {
811             switch (entityType) {
812                 case TextClassifier.TYPE_URL:
813                     return Linkify.WEB_URLS;
814                 case TextClassifier.TYPE_PHONE:
815                     return Linkify.PHONE_NUMBERS;
816                 case TextClassifier.TYPE_EMAIL:
817                     return Linkify.EMAIL_ADDRESSES;
818                 default:
819                     // NOTE: Do not support MAP_ADDRESSES. Legacy version does not work well.
820                     return 0;
821             }
822         }
823 
entityScores(@ntityType String entityType)824         private static Map<String, Float> entityScores(@EntityType String entityType) {
825             final Map<String, Float> scores = new ArrayMap<>();
826             scores.put(entityType, 1f);
827             return scores;
828         }
829 
checkMainThread()830         static void checkMainThread() {
831             if (Looper.myLooper() == Looper.getMainLooper()) {
832                 Log.w(LOG_TAG, "TextClassifier called on main thread");
833             }
834         }
835     }
836 }
837