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_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 widget involved in the text classification context is of an unknown/unspecified type. */
176     String WIDGET_TYPE_UNKNOWN = "unknown";
177 
178     /**
179      * No-op TextClassifier.
180      * This may be used to turn off TextClassifier features.
181      */
182     TextClassifier NO_OP = new TextClassifier() {
183         @Override
184         public String toString() {
185             return "TextClassifier.NO_OP";
186         }
187     };
188 
189     /**
190      * Extra that is included on activity intents coming from a TextClassifier when
191      * it suggests actions to its caller.
192      * <p>
193      * All {@link TextClassifier} implementations should make sure this extra exists in their
194      * generated intents.
195      */
196     String EXTRA_FROM_TEXT_CLASSIFIER = "android.view.textclassifier.extra.FROM_TEXT_CLASSIFIER";
197 
198     /**
199      * Returns suggested text selection start and end indices, recognized entity types, and their
200      * associated confidence scores. The entity types are ordered from highest to lowest scoring.
201      *
202      * <p><strong>NOTE: </strong>Call on a worker thread.
203      *
204      * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
205      * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
206      *
207      * @param request the text selection request
208      */
209     @WorkerThread
210     @NonNull
suggestSelection(@onNull TextSelection.Request request)211     default TextSelection suggestSelection(@NonNull TextSelection.Request request) {
212         Objects.requireNonNull(request);
213         Utils.checkMainThread();
214         return new TextSelection.Builder(request.getStartIndex(), request.getEndIndex()).build();
215     }
216 
217     /**
218      * Returns suggested text selection start and end indices, recognized entity types, and their
219      * associated confidence scores. The entity types are ordered from highest to lowest scoring.
220      *
221      * <p><strong>NOTE: </strong>Call on a worker thread.
222      *
223      * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
224      * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
225      *
226      * <p><b>NOTE:</b> Do not implement. The default implementation of this method calls
227      * {@link #suggestSelection(TextSelection.Request)}. If that method calls this method,
228      * a stack overflow error will happen.
229      *
230      * @param text text providing context for the selected text (which is specified
231      *      by the sub sequence starting at selectionStartIndex and ending at selectionEndIndex)
232      * @param selectionStartIndex start index of the selected part of text
233      * @param selectionEndIndex end index of the selected part of text
234      * @param defaultLocales ordered list of locale preferences that may be used to
235      *      disambiguate the provided text. If no locale preferences exist, set this to null
236      *      or an empty locale list.
237      *
238      * @throws IllegalArgumentException if text is null; selectionStartIndex is negative;
239      *      selectionEndIndex is greater than text.length() or not greater than selectionStartIndex
240      *
241      * @see #suggestSelection(TextSelection.Request)
242      */
243     @WorkerThread
244     @NonNull
suggestSelection( @onNull CharSequence text, @IntRange(from = 0) int selectionStartIndex, @IntRange(from = 0) int selectionEndIndex, @Nullable LocaleList defaultLocales)245     default TextSelection suggestSelection(
246             @NonNull CharSequence text,
247             @IntRange(from = 0) int selectionStartIndex,
248             @IntRange(from = 0) int selectionEndIndex,
249             @Nullable LocaleList defaultLocales) {
250         final TextSelection.Request request = new TextSelection.Request.Builder(
251                 text, selectionStartIndex, selectionEndIndex)
252                 .setDefaultLocales(defaultLocales)
253                 .build();
254         return suggestSelection(request);
255     }
256 
257     /**
258      * Classifies the specified text and returns a {@link TextClassification} object that can be
259      * used to generate a widget for handling the classified text.
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      * @param request the text classification request
267      */
268     @WorkerThread
269     @NonNull
classifyText(@onNull TextClassification.Request request)270     default TextClassification classifyText(@NonNull TextClassification.Request request) {
271         Objects.requireNonNull(request);
272         Utils.checkMainThread();
273         return TextClassification.EMPTY;
274     }
275 
276     /**
277      * Classifies the specified text and returns a {@link TextClassification} object that can be
278      * used to generate a widget for handling the classified text.
279      *
280      * <p><strong>NOTE: </strong>Call on a worker thread.
281      *
282      * <p><b>NOTE:</b> Do not implement. The default implementation of this method calls
283      * {@link #classifyText(TextClassification.Request)}. If that method calls this method,
284      * a stack overflow error will happen.
285      *
286      * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
287      * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
288      *
289      * @param text text providing context for the text to classify (which is specified
290      *      by the sub sequence starting at startIndex and ending at endIndex)
291      * @param startIndex start index of the text to classify
292      * @param endIndex end index of the text to classify
293      * @param defaultLocales ordered list of locale preferences that may be used to
294      *      disambiguate the provided text. If no locale preferences exist, set this to null
295      *      or an empty locale list.
296      *
297      * @throws IllegalArgumentException if text is null; startIndex is negative;
298      *      endIndex is greater than text.length() or not greater than startIndex
299      *
300      * @see #classifyText(TextClassification.Request)
301      */
302     @WorkerThread
303     @NonNull
classifyText( @onNull CharSequence text, @IntRange(from = 0) int startIndex, @IntRange(from = 0) int endIndex, @Nullable LocaleList defaultLocales)304     default TextClassification classifyText(
305             @NonNull CharSequence text,
306             @IntRange(from = 0) int startIndex,
307             @IntRange(from = 0) int endIndex,
308             @Nullable LocaleList defaultLocales) {
309         final TextClassification.Request request = new TextClassification.Request.Builder(
310                 text, startIndex, endIndex)
311                 .setDefaultLocales(defaultLocales)
312                 .build();
313         return classifyText(request);
314     }
315 
316     /**
317      * Generates and returns a {@link TextLinks} that may be applied to the text to annotate it with
318      * links information.
319      *
320      * <p><strong>NOTE: </strong>Call on a worker thread.
321      *
322      * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
323      * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
324      *
325      * @param request the text links request
326      *
327      * @see #getMaxGenerateLinksTextLength()
328      */
329     @WorkerThread
330     @NonNull
generateLinks(@onNull TextLinks.Request request)331     default TextLinks generateLinks(@NonNull TextLinks.Request request) {
332         Objects.requireNonNull(request);
333         Utils.checkMainThread();
334         return new TextLinks.Builder(request.getText().toString()).build();
335     }
336 
337     /**
338      * Returns the maximal length of text that can be processed by generateLinks.
339      *
340      * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
341      * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
342      *
343      * @see #generateLinks(TextLinks.Request)
344      */
345     @WorkerThread
getMaxGenerateLinksTextLength()346     default int getMaxGenerateLinksTextLength() {
347         return Integer.MAX_VALUE;
348     }
349 
350     /**
351      * Detects the language of the text in the given request.
352      *
353      * <p><strong>NOTE: </strong>Call on a worker thread.
354      *
355      *
356      * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
357      * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
358      *
359      * @param request the {@link TextLanguage} request.
360      * @return the {@link TextLanguage} result.
361      */
362     @WorkerThread
363     @NonNull
detectLanguage(@onNull TextLanguage.Request request)364     default TextLanguage detectLanguage(@NonNull TextLanguage.Request request) {
365         Objects.requireNonNull(request);
366         Utils.checkMainThread();
367         return TextLanguage.EMPTY;
368     }
369 
370     /**
371      * Suggests and returns a list of actions according to the given conversation.
372      */
373     @WorkerThread
374     @NonNull
suggestConversationActions( @onNull ConversationActions.Request request)375     default ConversationActions suggestConversationActions(
376             @NonNull ConversationActions.Request request) {
377         Objects.requireNonNull(request);
378         Utils.checkMainThread();
379         return new ConversationActions(Collections.emptyList(), null);
380     }
381 
382     /**
383      * <strong>NOTE: </strong>Use {@link #onTextClassifierEvent(TextClassifierEvent)} instead.
384      * <p>
385      * Reports a selection event.
386      *
387      * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
388      * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
389      */
onSelectionEvent(@onNull SelectionEvent event)390     default void onSelectionEvent(@NonNull SelectionEvent event) {
391         // TODO: Consider rerouting to onTextClassifierEvent()
392     }
393 
394     /**
395      * Reports a text classifier event.
396      * <p>
397      * <strong>NOTE: </strong>Call on a worker thread.
398      *
399      * @throws IllegalStateException if this TextClassifier has been destroyed.
400      * @see #isDestroyed()
401      */
onTextClassifierEvent(@onNull TextClassifierEvent event)402     default void onTextClassifierEvent(@NonNull TextClassifierEvent event) {}
403 
404     /**
405      * Destroys this TextClassifier.
406      *
407      * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to its methods should
408      * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
409      *
410      * <p>Subsequent calls to this method are no-ops.
411      */
destroy()412     default void destroy() {}
413 
414     /**
415      * Returns whether or not this TextClassifier has been destroyed.
416      *
417      * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, caller should not interact
418      * with the classifier and an attempt to do so would throw an {@link IllegalStateException}.
419      * However, this method should never throw an {@link IllegalStateException}.
420      *
421      * @see #destroy()
422      */
isDestroyed()423     default boolean isDestroyed() {
424         return false;
425     }
426 
427     /** @hide **/
dump(@onNull IndentingPrintWriter printWriter)428     default void dump(@NonNull IndentingPrintWriter printWriter) {}
429 
430     /**
431      * Configuration object for specifying what entity types to identify.
432      *
433      * Configs are initially based on a predefined preset, and can be modified from there.
434      */
435     final class EntityConfig implements Parcelable {
436         private final List<String> mIncludedTypes;
437         private final List<String> mExcludedTypes;
438         private final List<String> mHints;
439         private final boolean mIncludeTypesFromTextClassifier;
440 
EntityConfig( List<String> includedEntityTypes, List<String> excludedEntityTypes, List<String> hints, boolean includeTypesFromTextClassifier)441         private EntityConfig(
442                 List<String> includedEntityTypes,
443                 List<String> excludedEntityTypes,
444                 List<String> hints,
445                 boolean includeTypesFromTextClassifier) {
446             mIncludedTypes = Objects.requireNonNull(includedEntityTypes);
447             mExcludedTypes = Objects.requireNonNull(excludedEntityTypes);
448             mHints = Objects.requireNonNull(hints);
449             mIncludeTypesFromTextClassifier = includeTypesFromTextClassifier;
450         }
451 
EntityConfig(Parcel in)452         private EntityConfig(Parcel in) {
453             mIncludedTypes = new ArrayList<>();
454             in.readStringList(mIncludedTypes);
455             mExcludedTypes = new ArrayList<>();
456             in.readStringList(mExcludedTypes);
457             List<String> tmpHints = new ArrayList<>();
458             in.readStringList(tmpHints);
459             mHints = Collections.unmodifiableList(tmpHints);
460             mIncludeTypesFromTextClassifier = in.readByte() != 0;
461         }
462 
463         @Override
writeToParcel(Parcel parcel, int flags)464         public void writeToParcel(Parcel parcel, int flags) {
465             parcel.writeStringList(mIncludedTypes);
466             parcel.writeStringList(mExcludedTypes);
467             parcel.writeStringList(mHints);
468             parcel.writeByte((byte) (mIncludeTypesFromTextClassifier ? 1 : 0));
469         }
470 
471         /**
472          * Creates an EntityConfig.
473          *
474          * @param hints Hints for the TextClassifier to determine what types of entities to find.
475          *
476          * @deprecated Use {@link Builder} instead.
477          */
478         @Deprecated
createWithHints(@ullable Collection<String> hints)479         public static EntityConfig createWithHints(@Nullable Collection<String> hints) {
480             return new EntityConfig.Builder()
481                     .includeTypesFromTextClassifier(true)
482                     .setHints(hints)
483                     .build();
484         }
485 
486         /**
487          * Creates an EntityConfig.
488          *
489          * @param hints Hints for the TextClassifier to determine what types of entities to find
490          * @param includedEntityTypes Entity types, e.g. {@link #TYPE_EMAIL}, to explicitly include
491          * @param excludedEntityTypes Entity types, e.g. {@link #TYPE_PHONE}, to explicitly exclude
492          *
493          *
494          * Note that if an entity has been excluded, the exclusion will take precedence.
495          *
496          * @deprecated Use {@link Builder} instead.
497          */
498         @Deprecated
create(@ullable Collection<String> hints, @Nullable Collection<String> includedEntityTypes, @Nullable Collection<String> excludedEntityTypes)499         public static EntityConfig create(@Nullable Collection<String> hints,
500                 @Nullable Collection<String> includedEntityTypes,
501                 @Nullable Collection<String> excludedEntityTypes) {
502             return new EntityConfig.Builder()
503                     .setIncludedTypes(includedEntityTypes)
504                     .setExcludedTypes(excludedEntityTypes)
505                     .setHints(hints)
506                     .includeTypesFromTextClassifier(true)
507                     .build();
508         }
509 
510         /**
511          * Creates an EntityConfig with an explicit entity list.
512          *
513          * @param entityTypes Complete set of entities, e.g. {@link #TYPE_URL} to find.
514          *
515          * @deprecated Use {@link Builder} instead.
516          */
517         @Deprecated
createWithExplicitEntityList( @ullable Collection<String> entityTypes)518         public static EntityConfig createWithExplicitEntityList(
519                 @Nullable Collection<String> entityTypes) {
520             return new EntityConfig.Builder()
521                     .setIncludedTypes(entityTypes)
522                     .includeTypesFromTextClassifier(false)
523                     .build();
524         }
525 
526         /**
527          * Returns a final list of entity types to find.
528          *
529          * @param entityTypes Entity types we think should be found before factoring in
530          *                    includes/excludes
531          *
532          * This method is intended for use by TextClassifier implementations.
533          */
resolveEntityListModifications( @onNull Collection<String> entityTypes)534         public Collection<String> resolveEntityListModifications(
535                 @NonNull Collection<String> entityTypes) {
536             final Set<String> finalSet = new HashSet<>();
537             if (mIncludeTypesFromTextClassifier) {
538                 finalSet.addAll(entityTypes);
539             }
540             finalSet.addAll(mIncludedTypes);
541             finalSet.removeAll(mExcludedTypes);
542             return finalSet;
543         }
544 
545         /**
546          * Retrieves the list of hints.
547          *
548          * @return An unmodifiable collection of the hints.
549          */
getHints()550         public Collection<String> getHints() {
551             return mHints;
552         }
553 
554         /**
555          * Return whether the client allows the text classifier to include its own list of
556          * default types. If this function returns {@code true}, a default list of types suggested
557          * from a text classifier will be taking into account.
558          *
559          * <p>NOTE: This method is intended for use by a text classifier.
560          *
561          * @see #resolveEntityListModifications(Collection)
562          */
shouldIncludeTypesFromTextClassifier()563         public boolean shouldIncludeTypesFromTextClassifier() {
564             return mIncludeTypesFromTextClassifier;
565         }
566 
567         @Override
describeContents()568         public int describeContents() {
569             return 0;
570         }
571 
572         public static final @android.annotation.NonNull Parcelable.Creator<EntityConfig> CREATOR =
573                 new Parcelable.Creator<EntityConfig>() {
574                     @Override
575                     public EntityConfig createFromParcel(Parcel in) {
576                         return new EntityConfig(in);
577                     }
578 
579                     @Override
580                     public EntityConfig[] newArray(int size) {
581                         return new EntityConfig[size];
582                     }
583                 };
584 
585 
586 
587         /** Builder class to construct the {@link EntityConfig} object. */
588         public static final class Builder {
589             @Nullable
590             private Collection<String> mIncludedTypes;
591             @Nullable
592             private Collection<String> mExcludedTypes;
593             @Nullable
594             private Collection<String> mHints;
595             private boolean mIncludeTypesFromTextClassifier = true;
596 
597             /**
598              * Sets a collection of types that are explicitly included.
599              */
600             @NonNull
setIncludedTypes(@ullable Collection<String> includedTypes)601             public Builder setIncludedTypes(@Nullable Collection<String> includedTypes) {
602                 mIncludedTypes = includedTypes;
603                 return this;
604             }
605 
606             /**
607              * Sets a collection of types that are explicitly excluded.
608              */
609             @NonNull
setExcludedTypes(@ullable Collection<String> excludedTypes)610             public Builder setExcludedTypes(@Nullable Collection<String> excludedTypes) {
611                 mExcludedTypes = excludedTypes;
612                 return this;
613             }
614 
615             /**
616              * Specifies whether or not to include the types suggested by the text classifier. By
617              * default, it is included.
618              */
619             @NonNull
includeTypesFromTextClassifier(boolean includeTypesFromTextClassifier)620             public Builder includeTypesFromTextClassifier(boolean includeTypesFromTextClassifier) {
621                 mIncludeTypesFromTextClassifier = includeTypesFromTextClassifier;
622                 return this;
623             }
624 
625 
626             /**
627              * Sets the hints for the TextClassifier to determine what types of entities to find.
628              * These hints will only be used if {@link #includeTypesFromTextClassifier} is
629              * set to be true.
630              */
631             @NonNull
setHints(@ullable Collection<String> hints)632             public Builder setHints(@Nullable Collection<String> hints) {
633                 mHints = hints;
634                 return this;
635             }
636 
637             /**
638              * Combines all of the options that have been set and returns a new {@link EntityConfig}
639              * object.
640              */
641             @NonNull
build()642             public EntityConfig build() {
643                 return new EntityConfig(
644                         mIncludedTypes == null
645                                 ? Collections.emptyList()
646                                 : new ArrayList<>(mIncludedTypes),
647                         mExcludedTypes == null
648                                 ? Collections.emptyList()
649                                 : new ArrayList<>(mExcludedTypes),
650                         mHints == null
651                                 ? Collections.emptyList()
652                                 : Collections.unmodifiableList(new ArrayList<>(mHints)),
653                         mIncludeTypesFromTextClassifier);
654             }
655         }
656     }
657 
658     /**
659      * Utility functions for TextClassifier methods.
660      *
661      * <ul>
662      *  <li>Provides validation of input parameters to TextClassifier methods
663      * </ul>
664      *
665      * Intended to be used only for TextClassifier purposes.
666      * @hide
667      */
668     final class Utils {
669 
670         @GuardedBy("WORD_ITERATOR")
671         private static final BreakIterator WORD_ITERATOR = BreakIterator.getWordInstance();
672 
673         /**
674          * @throws IllegalArgumentException if text is null; startIndex is negative;
675          *      endIndex is greater than text.length() or is not greater than startIndex;
676          *      options is null
677          */
checkArgument(@onNull CharSequence text, int startIndex, int endIndex)678         static void checkArgument(@NonNull CharSequence text, int startIndex, int endIndex) {
679             Preconditions.checkArgument(text != null);
680             Preconditions.checkArgument(startIndex >= 0);
681             Preconditions.checkArgument(endIndex <= text.length());
682             Preconditions.checkArgument(endIndex > startIndex);
683         }
684 
685         /** Returns if the length of the text is within the range. */
checkTextLength(CharSequence text, int maxLength)686         static boolean checkTextLength(CharSequence text, int maxLength) {
687             int textLength = text.length();
688             return textLength >= 0 && textLength <= maxLength;
689         }
690 
691         /**
692          * Returns the substring of {@code text} that contains at least text from index
693          * {@code start} <i>(inclusive)</i> to index {@code end} <i><(exclusive)/i> with the goal of
694          * returning text that is at least {@code minimumLength}. If {@code text} is not long
695          * enough, this will return {@code text}. This method returns text at word boundaries.
696          *
697          * @param text the source text
698          * @param start the start index of text that must be included
699          * @param end the end index of text that must be included
700          * @param minimumLength minimum length of text to return if {@code text} is long enough
701          */
getSubString( String text, int start, int end, int minimumLength)702         public static String getSubString(
703                 String text, int start, int end, int minimumLength) {
704             Preconditions.checkArgument(start >= 0);
705             Preconditions.checkArgument(end <= text.length());
706             Preconditions.checkArgument(start <= end);
707 
708             if (text.length() < minimumLength) {
709                 return text;
710             }
711 
712             final int length = end - start;
713             if (length >= minimumLength) {
714                 return text.substring(start, end);
715             }
716 
717             final int offset = (minimumLength - length) / 2;
718             int iterStart = Math.max(0, Math.min(start - offset, text.length() - minimumLength));
719             int iterEnd = Math.min(text.length(), iterStart + minimumLength);
720 
721             synchronized (WORD_ITERATOR) {
722                 WORD_ITERATOR.setText(text);
723                 iterStart = WORD_ITERATOR.isBoundary(iterStart)
724                         ? iterStart : Math.max(0, WORD_ITERATOR.preceding(iterStart));
725                 iterEnd = WORD_ITERATOR.isBoundary(iterEnd)
726                         ? iterEnd : Math.max(iterEnd, WORD_ITERATOR.following(iterEnd));
727                 WORD_ITERATOR.setText("");
728                 return text.substring(iterStart, iterEnd);
729             }
730         }
731 
732         /**
733          * Generates links using legacy {@link Linkify}.
734          */
generateLegacyLinks(@onNull TextLinks.Request request)735         public static TextLinks generateLegacyLinks(@NonNull TextLinks.Request request) {
736             final String string = request.getText().toString();
737             final TextLinks.Builder links = new TextLinks.Builder(string);
738 
739             final Collection<String> entities = request.getEntityConfig()
740                     .resolveEntityListModifications(Collections.emptyList());
741             if (entities.contains(TextClassifier.TYPE_URL)) {
742                 addLinks(links, string, TextClassifier.TYPE_URL);
743             }
744             if (entities.contains(TextClassifier.TYPE_PHONE)) {
745                 addLinks(links, string, TextClassifier.TYPE_PHONE);
746             }
747             if (entities.contains(TextClassifier.TYPE_EMAIL)) {
748                 addLinks(links, string, TextClassifier.TYPE_EMAIL);
749             }
750             // NOTE: Do not support MAP_ADDRESSES. Legacy version does not work well.
751             return links.build();
752         }
753 
addLinks( TextLinks.Builder links, String string, @EntityType String entityType)754         private static void addLinks(
755                 TextLinks.Builder links, String string, @EntityType String entityType) {
756             final Spannable spannable = new SpannableString(string);
757             if (Linkify.addLinks(spannable, linkMask(entityType))) {
758                 final URLSpan[] spans = spannable.getSpans(0, spannable.length(), URLSpan.class);
759                 for (URLSpan urlSpan : spans) {
760                     links.addLink(
761                             spannable.getSpanStart(urlSpan),
762                             spannable.getSpanEnd(urlSpan),
763                             entityScores(entityType),
764                             urlSpan);
765                 }
766             }
767         }
768 
769         @LinkifyMask
linkMask(@ntityType String entityType)770         private static int linkMask(@EntityType String entityType) {
771             switch (entityType) {
772                 case TextClassifier.TYPE_URL:
773                     return Linkify.WEB_URLS;
774                 case TextClassifier.TYPE_PHONE:
775                     return Linkify.PHONE_NUMBERS;
776                 case TextClassifier.TYPE_EMAIL:
777                     return Linkify.EMAIL_ADDRESSES;
778                 default:
779                     // NOTE: Do not support MAP_ADDRESSES. Legacy version does not work well.
780                     return 0;
781             }
782         }
783 
entityScores(@ntityType String entityType)784         private static Map<String, Float> entityScores(@EntityType String entityType) {
785             final Map<String, Float> scores = new ArrayMap<>();
786             scores.put(entityType, 1f);
787             return scores;
788         }
789 
checkMainThread()790         static void checkMainThread() {
791             if (Looper.myLooper() == Looper.getMainLooper()) {
792                 Log.w(LOG_TAG, "TextClassifier called on main thread");
793             }
794         }
795     }
796 }
797