• 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 import android.util.ArraySet;
36 
37 import com.android.internal.util.Preconditions;
38 
39 import java.lang.annotation.Retention;
40 import java.lang.annotation.RetentionPolicy;
41 import java.util.ArrayList;
42 import java.util.Collection;
43 import java.util.Collections;
44 import java.util.HashSet;
45 import java.util.Map;
46 import java.util.Set;
47 
48 /**
49  * Interface for providing text classification related features.
50  *
51  * <p><strong>NOTE: </strong>Unless otherwise stated, methods of this interface are blocking
52  * operations. Call on a worker thread.
53  */
54 public interface TextClassifier {
55 
56     /** @hide */
57     String DEFAULT_LOG_TAG = "androidtc";
58 
59 
60     /** @hide */
61     @Retention(RetentionPolicy.SOURCE)
62     @IntDef(value = {LOCAL, SYSTEM})
63     @interface TextClassifierType {}  // TODO: Expose as system APIs.
64     /** Specifies a TextClassifier that runs locally in the app's process. @hide */
65     int LOCAL = 0;
66     /** Specifies a TextClassifier that runs in the system process and serves all apps. @hide */
67     int SYSTEM = 1;
68 
69     /** The TextClassifier failed to run. */
70     String TYPE_UNKNOWN = "";
71     /** The classifier ran, but didn't recognize a known entity. */
72     String TYPE_OTHER = "other";
73     /** E-mail address (e.g. "noreply@android.com"). */
74     String TYPE_EMAIL = "email";
75     /** Phone number (e.g. "555-123 456"). */
76     String TYPE_PHONE = "phone";
77     /** Physical address. */
78     String TYPE_ADDRESS = "address";
79     /** Web URL. */
80     String TYPE_URL = "url";
81     /** Time reference that is no more specific than a date. May be absolute such as "01/01/2000" or
82      * relative like "tomorrow". **/
83     String TYPE_DATE = "date";
84     /** Time reference that includes a specific time. May be absolute such as "01/01/2000 5:30pm" or
85      * relative like "tomorrow at 5:30pm". **/
86     String TYPE_DATE_TIME = "datetime";
87     /** Flight number in IATA format. */
88     String TYPE_FLIGHT_NUMBER = "flight";
89 
90     /** @hide */
91     @Retention(RetentionPolicy.SOURCE)
92     @StringDef(prefix = { "TYPE_" }, value = {
93             TYPE_UNKNOWN,
94             TYPE_OTHER,
95             TYPE_EMAIL,
96             TYPE_PHONE,
97             TYPE_ADDRESS,
98             TYPE_URL,
99             TYPE_DATE,
100             TYPE_DATE_TIME,
101             TYPE_FLIGHT_NUMBER,
102     })
103     @interface EntityType {}
104 
105     /** Designates that the text in question is editable. **/
106     String HINT_TEXT_IS_EDITABLE = "android.text_is_editable";
107     /** Designates that the text in question is not editable. **/
108     String HINT_TEXT_IS_NOT_EDITABLE = "android.text_is_not_editable";
109 
110     /** @hide */
111     @Retention(RetentionPolicy.SOURCE)
112     @StringDef(prefix = { "HINT_" }, value = {HINT_TEXT_IS_EDITABLE, HINT_TEXT_IS_NOT_EDITABLE})
113     @interface Hints {}
114 
115     /** @hide */
116     @Retention(RetentionPolicy.SOURCE)
117     @StringDef({WIDGET_TYPE_TEXTVIEW, WIDGET_TYPE_EDITTEXT, WIDGET_TYPE_UNSELECTABLE_TEXTVIEW,
118             WIDGET_TYPE_WEBVIEW, WIDGET_TYPE_EDIT_WEBVIEW, WIDGET_TYPE_CUSTOM_TEXTVIEW,
119             WIDGET_TYPE_CUSTOM_EDITTEXT, WIDGET_TYPE_CUSTOM_UNSELECTABLE_TEXTVIEW,
120             WIDGET_TYPE_UNKNOWN})
121     @interface WidgetType {}
122 
123     /** The widget involved in the text classification session is a standard
124      * {@link android.widget.TextView}. */
125     String WIDGET_TYPE_TEXTVIEW = "textview";
126     /** The widget involved in the text classification session is a standard
127      * {@link android.widget.EditText}. */
128     String WIDGET_TYPE_EDITTEXT = "edittext";
129     /** The widget involved in the text classification session is a standard non-selectable
130      * {@link android.widget.TextView}. */
131     String WIDGET_TYPE_UNSELECTABLE_TEXTVIEW = "nosel-textview";
132     /** The widget involved in the text classification session is a standard
133      * {@link android.webkit.WebView}. */
134     String WIDGET_TYPE_WEBVIEW = "webview";
135     /** The widget involved in the text classification session is a standard editable
136      * {@link android.webkit.WebView}. */
137     String WIDGET_TYPE_EDIT_WEBVIEW = "edit-webview";
138     /** The widget involved in the text classification session is a custom text widget. */
139     String WIDGET_TYPE_CUSTOM_TEXTVIEW = "customview";
140     /** The widget involved in the text classification session is a custom editable text widget. */
141     String WIDGET_TYPE_CUSTOM_EDITTEXT = "customedit";
142     /** The widget involved in the text classification session is a custom non-selectable text
143      * widget. */
144     String WIDGET_TYPE_CUSTOM_UNSELECTABLE_TEXTVIEW = "nosel-customview";
145     /** The widget involved in the text classification session is of an unknown/unspecified type. */
146     String WIDGET_TYPE_UNKNOWN = "unknown";
147 
148     /**
149      * No-op TextClassifier.
150      * This may be used to turn off TextClassifier features.
151      */
152     TextClassifier NO_OP = new TextClassifier() {};
153 
154     /**
155      * Returns suggested text selection start and end indices, recognized entity types, and their
156      * associated confidence scores. The entity types are ordered from highest to lowest scoring.
157      *
158      * <p><strong>NOTE: </strong>Call on a worker thread.
159      *
160      * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
161      * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
162      *
163      * @param request the text selection request
164      */
165     @WorkerThread
166     @NonNull
suggestSelection(@onNull TextSelection.Request request)167     default TextSelection suggestSelection(@NonNull TextSelection.Request request) {
168         Preconditions.checkNotNull(request);
169         Utils.checkMainThread();
170         return new TextSelection.Builder(request.getStartIndex(), request.getEndIndex()).build();
171     }
172 
173     /**
174      * Returns suggested text selection start and end indices, recognized entity types, and their
175      * associated confidence scores. The entity types are ordered from highest to lowest scoring.
176      *
177      * <p><strong>NOTE: </strong>Call on a worker thread.
178      *
179      * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
180      * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
181      *
182      * <p><b>NOTE:</b> Do not implement. The default implementation of this method calls
183      * {@link #suggestSelection(TextSelection.Request)}. If that method calls this method,
184      * a stack overflow error will happen.
185      *
186      * @param text text providing context for the selected text (which is specified
187      *      by the sub sequence starting at selectionStartIndex and ending at selectionEndIndex)
188      * @param selectionStartIndex start index of the selected part of text
189      * @param selectionEndIndex end index of the selected part of text
190      * @param defaultLocales ordered list of locale preferences that may be used to
191      *      disambiguate the provided text. If no locale preferences exist, set this to null
192      *      or an empty locale list.
193      *
194      * @throws IllegalArgumentException if text is null; selectionStartIndex is negative;
195      *      selectionEndIndex is greater than text.length() or not greater than selectionStartIndex
196      *
197      * @see #suggestSelection(TextSelection.Request)
198      */
199     @WorkerThread
200     @NonNull
suggestSelection( @onNull CharSequence text, @IntRange(from = 0) int selectionStartIndex, @IntRange(from = 0) int selectionEndIndex, @Nullable LocaleList defaultLocales)201     default TextSelection suggestSelection(
202             @NonNull CharSequence text,
203             @IntRange(from = 0) int selectionStartIndex,
204             @IntRange(from = 0) int selectionEndIndex,
205             @Nullable LocaleList defaultLocales) {
206         final TextSelection.Request request = new TextSelection.Request.Builder(
207                 text, selectionStartIndex, selectionEndIndex)
208                 .setDefaultLocales(defaultLocales)
209                 .build();
210         return suggestSelection(request);
211     }
212 
213     // TODO: Remove once apps can build against the latest sdk.
214     /** @hide */
suggestSelection( @onNull CharSequence text, @IntRange(from = 0) int selectionStartIndex, @IntRange(from = 0) int selectionEndIndex, @Nullable TextSelection.Options options)215     default TextSelection suggestSelection(
216             @NonNull CharSequence text,
217             @IntRange(from = 0) int selectionStartIndex,
218             @IntRange(from = 0) int selectionEndIndex,
219             @Nullable TextSelection.Options options) {
220         if (options == null) {
221             return suggestSelection(new TextSelection.Request.Builder(
222                     text, selectionStartIndex, selectionEndIndex).build());
223         } else if (options.getRequest() != null) {
224             return suggestSelection(options.getRequest());
225         } else {
226             return suggestSelection(
227                     new TextSelection.Request.Builder(text, selectionStartIndex, selectionEndIndex)
228                             .setDefaultLocales(options.getDefaultLocales())
229                             .build());
230         }
231     }
232 
233     /**
234      * Classifies the specified text and returns a {@link TextClassification} object that can be
235      * used to generate a widget for handling the classified text.
236      *
237      * <p><strong>NOTE: </strong>Call on a worker thread.
238      *
239      * <strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
240      * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
241      *
242      * @param request the text classification request
243      */
244     @WorkerThread
245     @NonNull
classifyText(@onNull TextClassification.Request request)246     default TextClassification classifyText(@NonNull TextClassification.Request request) {
247         Preconditions.checkNotNull(request);
248         Utils.checkMainThread();
249         return TextClassification.EMPTY;
250     }
251 
252     /**
253      * Classifies the specified text and returns a {@link TextClassification} object that can be
254      * used to generate a widget for handling the classified text.
255      *
256      * <p><strong>NOTE: </strong>Call on a worker thread.
257      *
258      * <p><b>NOTE:</b> Do not implement. The default implementation of this method calls
259      * {@link #classifyText(TextClassification.Request)}. If that method calls this method,
260      * a stack overflow error will happen.
261      *
262      * <strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
263      * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
264      *
265      * @param text text providing context for the text to classify (which is specified
266      *      by the sub sequence starting at startIndex and ending at endIndex)
267      * @param startIndex start index of the text to classify
268      * @param endIndex end index of the text to classify
269      * @param defaultLocales ordered list of locale preferences that may be used to
270      *      disambiguate the provided text. If no locale preferences exist, set this to null
271      *      or an empty locale list.
272      *
273      * @throws IllegalArgumentException if text is null; startIndex is negative;
274      *      endIndex is greater than text.length() or not greater than startIndex
275      *
276      * @see #classifyText(TextClassification.Request)
277      */
278     @WorkerThread
279     @NonNull
classifyText( @onNull CharSequence text, @IntRange(from = 0) int startIndex, @IntRange(from = 0) int endIndex, @Nullable LocaleList defaultLocales)280     default TextClassification classifyText(
281             @NonNull CharSequence text,
282             @IntRange(from = 0) int startIndex,
283             @IntRange(from = 0) int endIndex,
284             @Nullable LocaleList defaultLocales) {
285         final TextClassification.Request request = new TextClassification.Request.Builder(
286                 text, startIndex, endIndex)
287                 .setDefaultLocales(defaultLocales)
288                 .build();
289         return classifyText(request);
290     }
291 
292     // TODO: Remove once apps can build against the latest sdk.
293     /** @hide */
classifyText( @onNull CharSequence text, @IntRange(from = 0) int startIndex, @IntRange(from = 0) int endIndex, @Nullable TextClassification.Options options)294     default TextClassification classifyText(
295             @NonNull CharSequence text,
296             @IntRange(from = 0) int startIndex,
297             @IntRange(from = 0) int endIndex,
298             @Nullable TextClassification.Options options) {
299         if (options == null) {
300             return classifyText(
301                     new TextClassification.Request.Builder(text, startIndex, endIndex).build());
302         } else if (options.getRequest() != null) {
303             return classifyText(options.getRequest());
304         } else {
305             return classifyText(new TextClassification.Request.Builder(text, startIndex, endIndex)
306                     .setDefaultLocales(options.getDefaultLocales())
307                     .setReferenceTime(options.getReferenceTime())
308                     .build());
309         }
310     }
311 
312     /**
313      * Generates and returns a {@link TextLinks} that may be applied to the text to annotate it with
314      * links information.
315      *
316      * <p><strong>NOTE: </strong>Call on a worker thread.
317      *
318      * <strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
319      * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
320      *
321      * @param request the text links request
322      *
323      * @see #getMaxGenerateLinksTextLength()
324      */
325     @WorkerThread
326     @NonNull
generateLinks(@onNull TextLinks.Request request)327     default TextLinks generateLinks(@NonNull TextLinks.Request request) {
328         Preconditions.checkNotNull(request);
329         Utils.checkMainThread();
330         return new TextLinks.Builder(request.getText().toString()).build();
331     }
332 
333     // TODO: Remove once apps can build against the latest sdk.
334     /** @hide */
generateLinks( @onNull CharSequence text, @Nullable TextLinks.Options options)335     default TextLinks generateLinks(
336             @NonNull CharSequence text, @Nullable TextLinks.Options options) {
337         if (options == null) {
338             return generateLinks(new TextLinks.Request.Builder(text).build());
339         } else if (options.getRequest() != null) {
340             return generateLinks(options.getRequest());
341         } else {
342             return generateLinks(new TextLinks.Request.Builder(text)
343                     .setDefaultLocales(options.getDefaultLocales())
344                     .setEntityConfig(options.getEntityConfig())
345                     .build());
346         }
347     }
348 
349     /**
350      * Returns the maximal length of text that can be processed by generateLinks.
351      *
352      * <strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
353      * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
354      *
355      * @see #generateLinks(TextLinks.Request)
356      */
357     @WorkerThread
getMaxGenerateLinksTextLength()358     default int getMaxGenerateLinksTextLength() {
359         return Integer.MAX_VALUE;
360     }
361 
362     /**
363      * Reports a selection event.
364      *
365      * <strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
366      * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
367      */
onSelectionEvent(@onNull SelectionEvent event)368     default void onSelectionEvent(@NonNull SelectionEvent event) {}
369 
370     /**
371      * Destroys this TextClassifier.
372      *
373      * <strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to its methods should
374      * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
375      *
376      * <p>Subsequent calls to this method are no-ops.
377      */
destroy()378     default void destroy() {}
379 
380     /**
381      * Returns whether or not this TextClassifier has been destroyed.
382      *
383      * <strong>NOTE: </strong>If a TextClassifier has been destroyed, caller should not interact
384      * with the classifier and an attempt to do so would throw an {@link IllegalStateException}.
385      * However, this method should never throw an {@link IllegalStateException}.
386      *
387      * @see #destroy()
388      */
isDestroyed()389     default boolean isDestroyed() {
390         return false;
391     }
392 
393     /**
394      * Configuration object for specifying what entities to identify.
395      *
396      * Configs are initially based on a predefined preset, and can be modified from there.
397      */
398     final class EntityConfig implements Parcelable {
399         private final Collection<String> mHints;
400         private final Collection<String> mExcludedEntityTypes;
401         private final Collection<String> mIncludedEntityTypes;
402         private final boolean mUseHints;
403 
EntityConfig(boolean useHints, Collection<String> hints, Collection<String> includedEntityTypes, Collection<String> excludedEntityTypes)404         private EntityConfig(boolean useHints, Collection<String> hints,
405                 Collection<String> includedEntityTypes, Collection<String> excludedEntityTypes) {
406             mHints = hints == null
407                     ? Collections.EMPTY_LIST
408                     : Collections.unmodifiableCollection(new ArraySet<>(hints));
409             mExcludedEntityTypes = excludedEntityTypes == null
410                     ? Collections.EMPTY_LIST : new ArraySet<>(excludedEntityTypes);
411             mIncludedEntityTypes = includedEntityTypes == null
412                     ? Collections.EMPTY_LIST : new ArraySet<>(includedEntityTypes);
413             mUseHints = useHints;
414         }
415 
416         /**
417          * Creates an EntityConfig.
418          *
419          * @param hints Hints for the TextClassifier to determine what types of entities to find.
420          */
createWithHints(@ullable Collection<String> hints)421         public static EntityConfig createWithHints(@Nullable Collection<String> hints) {
422             return new EntityConfig(/* useHints */ true, hints,
423                     /* includedEntityTypes */null, /* excludedEntityTypes */ null);
424         }
425 
426         // TODO: Remove once apps can build against the latest sdk.
427         /** @hide */
create(@ullable Collection<String> hints)428         public static EntityConfig create(@Nullable Collection<String> hints) {
429             return createWithHints(hints);
430         }
431 
432         /**
433          * Creates an EntityConfig.
434          *
435          * @param hints Hints for the TextClassifier to determine what types of entities to find
436          * @param includedEntityTypes Entity types, e.g. {@link #TYPE_EMAIL}, to explicitly include
437          * @param excludedEntityTypes Entity types, e.g. {@link #TYPE_PHONE}, to explicitly exclude
438          *
439          *
440          * Note that if an entity has been excluded, the exclusion will take precedence.
441          */
create(@ullable Collection<String> hints, @Nullable Collection<String> includedEntityTypes, @Nullable Collection<String> excludedEntityTypes)442         public static EntityConfig create(@Nullable Collection<String> hints,
443                 @Nullable Collection<String> includedEntityTypes,
444                 @Nullable Collection<String> excludedEntityTypes) {
445             return new EntityConfig(/* useHints */ true, hints,
446                     includedEntityTypes, excludedEntityTypes);
447         }
448 
449         /**
450          * Creates an EntityConfig with an explicit entity list.
451          *
452          * @param entityTypes Complete set of entities, e.g. {@link #TYPE_URL} to find.
453          *
454          */
createWithExplicitEntityList( @ullable Collection<String> entityTypes)455         public static EntityConfig createWithExplicitEntityList(
456                 @Nullable Collection<String> entityTypes) {
457             return new EntityConfig(/* useHints */ false, /* hints */ null,
458                     /* includedEntityTypes */ entityTypes, /* excludedEntityTypes */ null);
459         }
460 
461         // TODO: Remove once apps can build against the latest sdk.
462         /** @hide */
createWithEntityList(@ullable Collection<String> entityTypes)463         public static EntityConfig createWithEntityList(@Nullable Collection<String> entityTypes) {
464             return createWithExplicitEntityList(entityTypes);
465         }
466 
467         /**
468          * Returns a list of the final set of entities to find.
469          *
470          * @param entities Entities we think should be found before factoring in includes/excludes
471          *
472          * This method is intended for use by TextClassifier implementations.
473          */
resolveEntityListModifications( @onNull Collection<String> entities)474         public Collection<String> resolveEntityListModifications(
475                 @NonNull Collection<String> entities) {
476             final Set<String> finalSet = new HashSet();
477             if (mUseHints) {
478                 finalSet.addAll(entities);
479             }
480             finalSet.addAll(mIncludedEntityTypes);
481             finalSet.removeAll(mExcludedEntityTypes);
482             return finalSet;
483         }
484 
485         /**
486          * Retrieves the list of hints.
487          *
488          * @return An unmodifiable collection of the hints.
489          */
getHints()490         public Collection<String> getHints() {
491             return mHints;
492         }
493 
494         @Override
describeContents()495         public int describeContents() {
496             return 0;
497         }
498 
499         @Override
writeToParcel(Parcel dest, int flags)500         public void writeToParcel(Parcel dest, int flags) {
501             dest.writeStringList(new ArrayList<>(mHints));
502             dest.writeStringList(new ArrayList<>(mExcludedEntityTypes));
503             dest.writeStringList(new ArrayList<>(mIncludedEntityTypes));
504             dest.writeInt(mUseHints ? 1 : 0);
505         }
506 
507         public static final Parcelable.Creator<EntityConfig> CREATOR =
508                 new Parcelable.Creator<EntityConfig>() {
509                     @Override
510                     public EntityConfig createFromParcel(Parcel in) {
511                         return new EntityConfig(in);
512                     }
513 
514                     @Override
515                     public EntityConfig[] newArray(int size) {
516                         return new EntityConfig[size];
517                     }
518                 };
519 
EntityConfig(Parcel in)520         private EntityConfig(Parcel in) {
521             mHints = new ArraySet<>(in.createStringArrayList());
522             mExcludedEntityTypes = new ArraySet<>(in.createStringArrayList());
523             mIncludedEntityTypes = new ArraySet<>(in.createStringArrayList());
524             mUseHints = in.readInt() == 1;
525         }
526     }
527 
528     /**
529      * Utility functions for TextClassifier methods.
530      *
531      * <ul>
532      *  <li>Provides validation of input parameters to TextClassifier methods
533      * </ul>
534      *
535      * Intended to be used only in this package.
536      * @hide
537      */
538     final class Utils {
539 
540         /**
541          * @throws IllegalArgumentException if text is null; startIndex is negative;
542          *      endIndex is greater than text.length() or is not greater than startIndex;
543          *      options is null
544          */
checkArgument(@onNull CharSequence text, int startIndex, int endIndex)545         static void checkArgument(@NonNull CharSequence text, int startIndex, int endIndex) {
546             Preconditions.checkArgument(text != null);
547             Preconditions.checkArgument(startIndex >= 0);
548             Preconditions.checkArgument(endIndex <= text.length());
549             Preconditions.checkArgument(endIndex > startIndex);
550         }
551 
checkTextLength(CharSequence text, int maxLength)552         static void checkTextLength(CharSequence text, int maxLength) {
553             Preconditions.checkArgumentInRange(text.length(), 0, maxLength, "text.length()");
554         }
555 
556         /**
557          * Generates links using legacy {@link Linkify}.
558          */
generateLegacyLinks(@onNull TextLinks.Request request)559         public static TextLinks generateLegacyLinks(@NonNull TextLinks.Request request) {
560             final String string = request.getText().toString();
561             final TextLinks.Builder links = new TextLinks.Builder(string);
562 
563             final Collection<String> entities = request.getEntityConfig()
564                     .resolveEntityListModifications(Collections.emptyList());
565             if (entities.contains(TextClassifier.TYPE_URL)) {
566                 addLinks(links, string, TextClassifier.TYPE_URL);
567             }
568             if (entities.contains(TextClassifier.TYPE_PHONE)) {
569                 addLinks(links, string, TextClassifier.TYPE_PHONE);
570             }
571             if (entities.contains(TextClassifier.TYPE_EMAIL)) {
572                 addLinks(links, string, TextClassifier.TYPE_EMAIL);
573             }
574             // NOTE: Do not support MAP_ADDRESSES. Legacy version does not work well.
575             return links.build();
576         }
577 
addLinks( TextLinks.Builder links, String string, @EntityType String entityType)578         private static void addLinks(
579                 TextLinks.Builder links, String string, @EntityType String entityType) {
580             final Spannable spannable = new SpannableString(string);
581             if (Linkify.addLinks(spannable, linkMask(entityType))) {
582                 final URLSpan[] spans = spannable.getSpans(0, spannable.length(), URLSpan.class);
583                 for (URLSpan urlSpan : spans) {
584                     links.addLink(
585                             spannable.getSpanStart(urlSpan),
586                             spannable.getSpanEnd(urlSpan),
587                             entityScores(entityType),
588                             urlSpan);
589                 }
590             }
591         }
592 
593         @LinkifyMask
linkMask(@ntityType String entityType)594         private static int linkMask(@EntityType String entityType) {
595             switch (entityType) {
596                 case TextClassifier.TYPE_URL:
597                     return Linkify.WEB_URLS;
598                 case TextClassifier.TYPE_PHONE:
599                     return Linkify.PHONE_NUMBERS;
600                 case TextClassifier.TYPE_EMAIL:
601                     return Linkify.EMAIL_ADDRESSES;
602                 default:
603                     // NOTE: Do not support MAP_ADDRESSES. Legacy version does not work well.
604                     return 0;
605             }
606         }
607 
entityScores(@ntityType String entityType)608         private static Map<String, Float> entityScores(@EntityType String entityType) {
609             final Map<String, Float> scores = new ArrayMap<>();
610             scores.put(entityType, 1f);
611             return scores;
612         }
613 
checkMainThread()614         static void checkMainThread() {
615             if (Looper.myLooper() == Looper.getMainLooper()) {
616                 Log.w(DEFAULT_LOG_TAG, "TextClassifier called on main thread");
617             }
618         }
619     }
620 }
621