TextClassifier.java revision 88be5a6cee59868eaee6f7b52fd8b2e6f6f28429
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
17package android.view.textclassifier;
18
19import android.annotation.IntDef;
20import android.annotation.IntRange;
21import android.annotation.NonNull;
22import android.annotation.Nullable;
23import android.annotation.StringDef;
24import android.annotation.WorkerThread;
25import android.os.LocaleList;
26import android.os.Looper;
27import android.os.Parcel;
28import android.os.Parcelable;
29import android.text.Spannable;
30import android.text.SpannableString;
31import android.text.style.URLSpan;
32import android.text.util.Linkify;
33import android.text.util.Linkify.LinkifyMask;
34import android.util.ArrayMap;
35import android.util.ArraySet;
36import android.util.Slog;
37
38import com.android.internal.util.Preconditions;
39
40import java.lang.annotation.Retention;
41import java.lang.annotation.RetentionPolicy;
42import java.util.ArrayList;
43import java.util.Collection;
44import java.util.Collections;
45import java.util.List;
46import java.util.Map;
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 */
54public 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_WEBVIEW, WIDGET_TYPE_EDITTEXT,
118            WIDGET_TYPE_EDIT_WEBVIEW, WIDGET_TYPE_CUSTOM_TEXTVIEW, WIDGET_TYPE_CUSTOM_EDITTEXT,
119            WIDGET_TYPE_CUSTOM_UNSELECTABLE_TEXTVIEW, WIDGET_TYPE_UNKNOWN})
120    @interface WidgetType {}
121
122    /** The widget involved in the text classification session is a standard
123     * {@link android.widget.TextView}. */
124    String WIDGET_TYPE_TEXTVIEW = "textview";
125    /** The widget involved in the text classification session is a standard
126     * {@link android.widget.EditText}. */
127    String WIDGET_TYPE_EDITTEXT = "edittext";
128    /** The widget involved in the text classification session is a standard non-selectable
129     * {@link android.widget.TextView}. */
130    String WIDGET_TYPE_UNSELECTABLE_TEXTVIEW = "nosel-textview";
131    /** The widget involved in the text classification session is a standard
132     * {@link android.webkit.WebView}. */
133    String WIDGET_TYPE_WEBVIEW = "webview";
134    /** The widget involved in the text classification session is a standard editable
135     * {@link android.webkit.WebView}. */
136    String WIDGET_TYPE_EDIT_WEBVIEW = "edit-webview";
137    /** The widget involved in the text classification session is a custom text widget. */
138    String WIDGET_TYPE_CUSTOM_TEXTVIEW = "customview";
139    /** The widget involved in the text classification session is a custom editable text widget. */
140    String WIDGET_TYPE_CUSTOM_EDITTEXT = "customedit";
141    /** The widget involved in the text classification session is a custom non-selectable text
142     * widget. */
143    String WIDGET_TYPE_CUSTOM_UNSELECTABLE_TEXTVIEW = "nosel-customview";
144    /** The widget involved in the text classification session is of an unknown/unspecified type. */
145    String WIDGET_TYPE_UNKNOWN = "unknown";
146
147    /**
148     * No-op TextClassifier.
149     * This may be used to turn off TextClassifier features.
150     */
151    TextClassifier NO_OP = new TextClassifier() {};
152
153    /**
154     * Returns suggested text selection start and end indices, recognized entity types, and their
155     * associated confidence scores. The entity types are ordered from highest to lowest scoring.
156     *
157     * <p><strong>NOTE: </strong>Call on a worker thread.
158     *
159     * <strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
160     * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
161     *
162     * @param text text providing context for the selected text (which is specified
163     *      by the sub sequence starting at selectionStartIndex and ending at selectionEndIndex)
164     * @param selectionStartIndex start index of the selected part of text
165     * @param selectionEndIndex end index of the selected part of text
166     * @param options optional input parameters
167     *
168     * @throws IllegalArgumentException if text is null; selectionStartIndex is negative;
169     *      selectionEndIndex is greater than text.length() or not greater than selectionStartIndex
170     *
171     * @see #suggestSelection(CharSequence, int, int)
172     */
173    @WorkerThread
174    @NonNull
175    default TextSelection suggestSelection(
176            @NonNull CharSequence text,
177            @IntRange(from = 0) int selectionStartIndex,
178            @IntRange(from = 0) int selectionEndIndex,
179            @Nullable TextSelection.Options options) {
180        Utils.validate(text, selectionStartIndex, selectionEndIndex, false);
181        return new TextSelection.Builder(selectionStartIndex, selectionEndIndex).build();
182    }
183
184    /**
185     * Returns suggested text selection start and end indices, recognized entity types, and their
186     * associated confidence scores. The entity types are ordered from highest to lowest scoring.
187     *
188     * <p><b>NOTE:</b> Do not implement. The default implementation of this method calls
189     * {@link #suggestSelection(CharSequence, int, int, TextSelection.Options)}. If that method
190     * calls this method, a stack overflow error will happen.
191     *
192     * <p><strong>NOTE: </strong>Call on a worker thread.
193     *
194     * <strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
195     * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
196     *
197     * @param text text providing context for the selected text (which is specified
198     *      by the sub sequence starting at selectionStartIndex and ending at selectionEndIndex)
199     * @param selectionStartIndex start index of the selected part of text
200     * @param selectionEndIndex end index of the selected part of text
201     *
202     * @throws IllegalArgumentException if text is null; selectionStartIndex is negative;
203     *      selectionEndIndex is greater than text.length() or not greater than selectionStartIndex
204     *
205     * @see #suggestSelection(CharSequence, int, int, TextSelection.Options)
206     */
207    @WorkerThread
208    @NonNull
209    default TextSelection suggestSelection(
210            @NonNull CharSequence text,
211            @IntRange(from = 0) int selectionStartIndex,
212            @IntRange(from = 0) int selectionEndIndex) {
213        return suggestSelection(text, selectionStartIndex, selectionEndIndex,
214                (TextSelection.Options) null);
215    }
216
217    /**
218     * See {@link #suggestSelection(CharSequence, int, int)} or
219     * {@link #suggestSelection(CharSequence, int, int, TextSelection.Options)}.
220     *
221     * <p><strong>NOTE: </strong>Call on a worker thread.
222     *
223     * <p><b>NOTE:</b> Do not implement. The default implementation of this method calls
224     * {@link #suggestSelection(CharSequence, int, int, TextSelection.Options)}. If that method
225     * calls this method, a stack overflow error will happen.
226     *
227     * <strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
228     * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
229     */
230    @WorkerThread
231    @NonNull
232    default TextSelection suggestSelection(
233            @NonNull CharSequence text,
234            @IntRange(from = 0) int selectionStartIndex,
235            @IntRange(from = 0) int selectionEndIndex,
236            @Nullable LocaleList defaultLocales) {
237        final TextSelection.Options options = (defaultLocales != null)
238                ? new TextSelection.Options().setDefaultLocales(defaultLocales)
239                : null;
240        return suggestSelection(text, selectionStartIndex, selectionEndIndex, options);
241    }
242
243    /**
244     * Classifies the specified text and returns a {@link TextClassification} object that can be
245     * used to generate a widget for handling the classified text.
246     *
247     * <p><strong>NOTE: </strong>Call on a worker thread.
248     *
249     * <strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
250     * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
251     *
252     * @param text text providing context for the text to classify (which is specified
253     *      by the sub sequence starting at startIndex and ending at endIndex)
254     * @param startIndex start index of the text to classify
255     * @param endIndex end index of the text to classify
256     * @param options optional input parameters
257     *
258     * @throws IllegalArgumentException if text is null; startIndex is negative;
259     *      endIndex is greater than text.length() or not greater than startIndex
260     *
261     * @see #classifyText(CharSequence, int, int)
262     */
263    @WorkerThread
264    @NonNull
265    default TextClassification classifyText(
266            @NonNull CharSequence text,
267            @IntRange(from = 0) int startIndex,
268            @IntRange(from = 0) int endIndex,
269            @Nullable TextClassification.Options options) {
270        Utils.validate(text, startIndex, endIndex, false);
271        return TextClassification.EMPTY;
272    }
273
274    /**
275     * Classifies the specified text and returns a {@link TextClassification} object that can be
276     * used to generate a widget for handling the classified text.
277     *
278     * <p><strong>NOTE: </strong>Call on a worker thread.
279     *
280     * <p><b>NOTE:</b> Do not implement. The default implementation of this method calls
281     * {@link #classifyText(CharSequence, int, int, TextClassification.Options)}. If that method
282     * calls this method, a stack overflow error will happen.
283     *
284     * <strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
285     * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
286     *
287     * @param text text providing context for the text to classify (which is specified
288     *      by the sub sequence starting at startIndex and ending at endIndex)
289     * @param startIndex start index of the text to classify
290     * @param endIndex end index of the text to classify
291     *
292     * @throws IllegalArgumentException if text is null; startIndex is negative;
293     *      endIndex is greater than text.length() or not greater than startIndex
294     *
295     * @see #classifyText(CharSequence, int, int, TextClassification.Options)
296     */
297    @WorkerThread
298    @NonNull
299    default TextClassification classifyText(
300            @NonNull CharSequence text,
301            @IntRange(from = 0) int startIndex,
302            @IntRange(from = 0) int endIndex) {
303        return classifyText(text, startIndex, endIndex, (TextClassification.Options) null);
304    }
305
306    /**
307     * See {@link #classifyText(CharSequence, int, int, TextClassification.Options)} or
308     * {@link #classifyText(CharSequence, int, int)}.
309     *
310     * <p><strong>NOTE: </strong>Call on a worker thread.
311     *
312     * <p><b>NOTE:</b> Do not implement. The default implementation of this method calls
313     * {@link #classifyText(CharSequence, int, int, TextClassification.Options)}. If that method
314     * calls this method, a stack overflow error will happen.
315     *
316     * <strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
317     * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
318     */
319    @WorkerThread
320    @NonNull
321    default TextClassification classifyText(
322            @NonNull CharSequence text,
323            @IntRange(from = 0) int startIndex,
324            @IntRange(from = 0) int endIndex,
325            @Nullable LocaleList defaultLocales) {
326        final TextClassification.Options options = (defaultLocales != null)
327                ? new TextClassification.Options().setDefaultLocales(defaultLocales)
328                : null;
329        return classifyText(text, startIndex, endIndex, options);
330    }
331
332    /**
333     * Generates and returns a {@link TextLinks} that may be applied to the text to annotate it with
334     * links information.
335     *
336     * <p><strong>NOTE: </strong>Call on a worker thread.
337     *
338     * <strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
339     * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
340     *
341     * @param text the text to generate annotations for
342     * @param options configuration for link generation
343     *
344     * @throws IllegalArgumentException if text is null or the text is too long for the
345     *      TextClassifier implementation.
346     *
347     * @see #generateLinks(CharSequence)
348     * @see #getMaxGenerateLinksTextLength()
349     */
350    @WorkerThread
351    @NonNull
352    default TextLinks generateLinks(
353            @NonNull CharSequence text, @Nullable TextLinks.Options options) {
354        Utils.validate(text, false);
355        return new TextLinks.Builder(text.toString()).build();
356    }
357
358    /**
359     * Generates and returns a {@link TextLinks} that may be applied to the text to annotate it with
360     * links information.
361     *
362     * <p><strong>NOTE: </strong>Call on a worker thread.
363     *
364     * <p><b>NOTE:</b> Do not implement. The default implementation of this method calls
365     * {@link #generateLinks(CharSequence, TextLinks.Options)}. If that method calls this method,
366     * a stack overflow error will happen.
367     *
368     * <strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
369     * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
370     *
371     * @param text the text to generate annotations for
372     *
373     * @throws IllegalArgumentException if text is null or the text is too long for the
374     *      TextClassifier implementation.
375     *
376     * @see #generateLinks(CharSequence, TextLinks.Options)
377     * @see #getMaxGenerateLinksTextLength()
378     */
379    @WorkerThread
380    @NonNull
381    default TextLinks generateLinks(@NonNull CharSequence text) {
382        return generateLinks(text, null);
383    }
384
385    /**
386     * Returns the maximal length of text that can be processed by generateLinks.
387     *
388     * <strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
389     * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
390     *
391     * @see #generateLinks(CharSequence)
392     * @see #generateLinks(CharSequence, TextLinks.Options)
393     */
394    @WorkerThread
395    default int getMaxGenerateLinksTextLength() {
396        return Integer.MAX_VALUE;
397    }
398
399    /**
400     * Returns a helper for logging TextClassifier related events.
401     *
402     * @param config logger configuration
403     * @hide
404     */
405    @WorkerThread
406    default Logger getLogger(@NonNull Logger.Config config) {
407        Preconditions.checkNotNull(config);
408        return Logger.DISABLED;
409    }
410
411    /**
412     * Reports a selection event.
413     *
414     * <strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
415     * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
416     */
417    default void onSelectionEvent(@NonNull SelectionEvent event) {}
418
419    /**
420     * Destroys this TextClassifier.
421     *
422     * <strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to its methods should
423     * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
424     *
425     * <p>Subsequent calls to this method are no-ops.
426     */
427    default void destroy() {}
428
429    /**
430     * Returns whether or not this TextClassifier has been destroyed.
431     *
432     * <strong>NOTE: </strong>If a TextClassifier has been destroyed, caller should not interact
433     * with the classifier and an attempt to do so would throw an {@link IllegalStateException}.
434     * However, this method should never throw an {@link IllegalStateException}.
435     *
436     * @see #destroy()
437     */
438    default boolean isDestroyed() {
439        return false;
440    }
441
442    /**
443     * Configuration object for specifying what entities to identify.
444     *
445     * Configs are initially based on a predefined preset, and can be modified from there.
446     */
447    final class EntityConfig implements Parcelable {
448        private final Collection<String> mHints;
449        private final Collection<String> mExcludedEntityTypes;
450        private final Collection<String> mIncludedEntityTypes;
451        private final boolean mUseHints;
452
453        private EntityConfig(boolean useHints, Collection<String> hints,
454                Collection<String> includedEntityTypes, Collection<String> excludedEntityTypes) {
455            mHints = hints == null
456                    ? Collections.EMPTY_LIST
457                    : Collections.unmodifiableCollection(new ArraySet<>(hints));
458            mExcludedEntityTypes = excludedEntityTypes == null
459                    ? Collections.EMPTY_LIST : new ArraySet<>(excludedEntityTypes);
460            mIncludedEntityTypes = includedEntityTypes == null
461                    ? Collections.EMPTY_LIST : new ArraySet<>(includedEntityTypes);
462            mUseHints = useHints;
463        }
464
465        /**
466         * Creates an EntityConfig.
467         *
468         * @param hints Hints for the TextClassifier to determine what types of entities to find.
469         */
470        public static EntityConfig create(@Nullable Collection<String> hints) {
471            return new EntityConfig(/* useHints */ true, hints,
472                    /* includedEntityTypes */null, /* excludedEntityTypes */ null);
473        }
474
475        /**
476         * Creates an EntityConfig.
477         *
478         * @param hints Hints for the TextClassifier to determine what types of entities to find
479         * @param includedEntityTypes Entity types, e.g. {@link #TYPE_EMAIL}, to explicitly include
480         * @param excludedEntityTypes Entity types, e.g. {@link #TYPE_PHONE}, to explicitly exclude
481         *
482         *
483         * Note that if an entity has been excluded, the exclusion will take precedence.
484         */
485        public static EntityConfig create(@Nullable Collection<String> hints,
486                @Nullable Collection<String> includedEntityTypes,
487                @Nullable Collection<String> excludedEntityTypes) {
488            return new EntityConfig(/* useHints */ true, hints,
489                    includedEntityTypes, excludedEntityTypes);
490        }
491
492        /**
493         * Creates an EntityConfig with an explicit entity list.
494         *
495         * @param entityTypes Complete set of entities, e.g. {@link #TYPE_URL} to find.
496         *
497         */
498        public static EntityConfig createWithEntityList(@Nullable Collection<String> entityTypes) {
499            return new EntityConfig(/* useHints */ false, /* hints */ null,
500                    /* includedEntityTypes */ entityTypes, /* excludedEntityTypes */ null);
501        }
502
503        /**
504         * Returns a list of the final set of entities to find.
505         *
506         * @param entities Entities we think should be found before factoring in includes/excludes
507         *
508         * This method is intended for use by TextClassifier implementations.
509         */
510        public List<String> resolveEntityListModifications(@NonNull Collection<String> entities) {
511            final ArrayList<String> finalList = new ArrayList<>();
512            if (mUseHints) {
513                for (String entity : entities) {
514                    if (!mExcludedEntityTypes.contains(entity)) {
515                        finalList.add(entity);
516                    }
517                }
518            }
519            for (String entity : mIncludedEntityTypes) {
520                if (!mExcludedEntityTypes.contains(entity) && !finalList.contains(entity)) {
521                    finalList.add(entity);
522                }
523            }
524            return finalList;
525        }
526
527        /**
528         * Retrieves the list of hints.
529         *
530         * @return An unmodifiable collection of the hints.
531         */
532        public Collection<String> getHints() {
533            return mHints;
534        }
535
536        @Override
537        public int describeContents() {
538            return 0;
539        }
540
541        @Override
542        public void writeToParcel(Parcel dest, int flags) {
543            dest.writeStringList(new ArrayList<>(mHints));
544            dest.writeStringList(new ArrayList<>(mExcludedEntityTypes));
545            dest.writeStringList(new ArrayList<>(mIncludedEntityTypes));
546            dest.writeInt(mUseHints ? 1 : 0);
547        }
548
549        public static final Parcelable.Creator<EntityConfig> CREATOR =
550                new Parcelable.Creator<EntityConfig>() {
551                    @Override
552                    public EntityConfig createFromParcel(Parcel in) {
553                        return new EntityConfig(in);
554                    }
555
556                    @Override
557                    public EntityConfig[] newArray(int size) {
558                        return new EntityConfig[size];
559                    }
560                };
561
562        private EntityConfig(Parcel in) {
563            mHints = new ArraySet<>(in.createStringArrayList());
564            mExcludedEntityTypes = new ArraySet<>(in.createStringArrayList());
565            mIncludedEntityTypes = new ArraySet<>(in.createStringArrayList());
566            mUseHints = in.readInt() == 1;
567        }
568    }
569
570    /**
571     * Utility functions for TextClassifier methods.
572     *
573     * <ul>
574     *  <li>Provides validation of input parameters to TextClassifier methods
575     * </ul>
576     *
577     * Intended to be used only in this package.
578     * @hide
579     */
580    final class Utils {
581
582        /**
583         * @throws IllegalArgumentException if text is null; startIndex is negative;
584         *      endIndex is greater than text.length() or is not greater than startIndex;
585         *      options is null
586         */
587        public static void validate(
588                @NonNull CharSequence text, int startIndex, int endIndex,
589                boolean allowInMainThread) {
590            Preconditions.checkArgument(text != null);
591            Preconditions.checkArgument(startIndex >= 0);
592            Preconditions.checkArgument(endIndex <= text.length());
593            Preconditions.checkArgument(endIndex > startIndex);
594            checkMainThread(allowInMainThread);
595        }
596
597        /**
598         * @throws IllegalArgumentException if text is null or options is null
599         */
600        public static void validate(@NonNull CharSequence text, boolean allowInMainThread) {
601            Preconditions.checkArgument(text != null);
602            checkMainThread(allowInMainThread);
603        }
604
605        /**
606         * @throws IllegalArgumentException if text is null; the text is too long or options is null
607         */
608        public static void validate(@NonNull CharSequence text, int maxLength,
609                boolean allowInMainThread) {
610            validate(text, allowInMainThread);
611            Preconditions.checkArgumentInRange(text.length(), 0, maxLength, "text.length()");
612        }
613
614        /**
615         * Generates links using legacy {@link Linkify}.
616         */
617        public static TextLinks generateLegacyLinks(
618                @NonNull CharSequence text, @NonNull TextLinks.Options options) {
619            final String string = Preconditions.checkNotNull(text).toString();
620            final TextLinks.Builder links = new TextLinks.Builder(string);
621
622            final List<String> entities = Preconditions.checkNotNull(options).getEntityConfig()
623                    .resolveEntityListModifications(Collections.emptyList());
624            if (entities.contains(TextClassifier.TYPE_URL)) {
625                addLinks(links, string, TextClassifier.TYPE_URL);
626            }
627            if (entities.contains(TextClassifier.TYPE_PHONE)) {
628                addLinks(links, string, TextClassifier.TYPE_PHONE);
629            }
630            if (entities.contains(TextClassifier.TYPE_EMAIL)) {
631                addLinks(links, string, TextClassifier.TYPE_EMAIL);
632            }
633            // NOTE: Do not support MAP_ADDRESSES. Legacy version does not work well.
634            return links.build();
635        }
636
637        private static void addLinks(
638                TextLinks.Builder links, String string, @EntityType String entityType) {
639            final Spannable spannable = new SpannableString(string);
640            if (Linkify.addLinks(spannable, linkMask(entityType))) {
641                final URLSpan[] spans = spannable.getSpans(0, spannable.length(), URLSpan.class);
642                for (URLSpan urlSpan : spans) {
643                    links.addLink(
644                            spannable.getSpanStart(urlSpan),
645                            spannable.getSpanEnd(urlSpan),
646                            entityScores(entityType),
647                            urlSpan);
648                }
649            }
650        }
651
652        @LinkifyMask
653        private static int linkMask(@EntityType String entityType) {
654            switch (entityType) {
655                case TextClassifier.TYPE_URL:
656                    return Linkify.WEB_URLS;
657                case TextClassifier.TYPE_PHONE:
658                    return Linkify.PHONE_NUMBERS;
659                case TextClassifier.TYPE_EMAIL:
660                    return Linkify.EMAIL_ADDRESSES;
661                default:
662                    // NOTE: Do not support MAP_ADDRESSES. Legacy version does not work well.
663                    return 0;
664            }
665        }
666
667        private static Map<String, Float> entityScores(@EntityType String entityType) {
668            final Map<String, Float> scores = new ArrayMap<>();
669            scores.put(entityType, 1f);
670            return scores;
671        }
672
673        private static void checkMainThread(boolean allowInMainThread) {
674            if (!allowInMainThread && Looper.myLooper() == Looper.getMainLooper()) {
675                Slog.w(DEFAULT_LOG_TAG, "TextClassifier called on main thread");
676            }
677        }
678    }
679}
680