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