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