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