Home | History | Annotate | Download | only in textclassifier
      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 (at) 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
    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
    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 */
    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
    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
    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 */
    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
    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 */
    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
    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      */
    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      */
    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      */
    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 
    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          */
    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 */
    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          */
    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          */
    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 */
    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          */
    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          */
    490         public Collection<String> getHints() {
    491             return mHints;
    492         }
    493 
    494         @Override
    495         public int describeContents() {
    496             return 0;
    497         }
    498 
    499         @Override
    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 
    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          */
    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 
    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          */
    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 
    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
    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 
    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 
    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