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