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_CLIPBOARD, 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 text classification context is for use with the system clipboard. */ 176 String WIDGET_TYPE_CLIPBOARD = "clipboard"; 177 /** The widget involved in the text classification context is of an unknown/unspecified type. */ 178 String WIDGET_TYPE_UNKNOWN = "unknown"; 179 180 /** 181 * No-op TextClassifier. 182 * This may be used to turn off TextClassifier features. 183 */ 184 TextClassifier NO_OP = new TextClassifier() { 185 @Override 186 public String toString() { 187 return "TextClassifier.NO_OP"; 188 } 189 }; 190 191 /** 192 * Extra that is included on activity intents coming from a TextClassifier when 193 * it suggests actions to its caller. 194 * <p> 195 * All {@link TextClassifier} implementations should make sure this extra exists in their 196 * generated intents. 197 */ 198 String EXTRA_FROM_TEXT_CLASSIFIER = "android.view.textclassifier.extra.FROM_TEXT_CLASSIFIER"; 199 200 /** 201 * Returns suggested text selection start and end indices, recognized entity types, and their 202 * associated confidence scores. The entity types are ordered from highest to lowest scoring. 203 * 204 * <p><strong>NOTE: </strong>Call on a worker thread. 205 * 206 * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should 207 * throw an {@link IllegalStateException}. See {@link #isDestroyed()}. 208 * 209 * @param request the text selection request 210 */ 211 @WorkerThread 212 @NonNull suggestSelection(@onNull TextSelection.Request request)213 default TextSelection suggestSelection(@NonNull TextSelection.Request request) { 214 Objects.requireNonNull(request); 215 Utils.checkMainThread(); 216 return new TextSelection.Builder(request.getStartIndex(), request.getEndIndex()).build(); 217 } 218 219 /** 220 * Returns suggested text selection start and end indices, recognized entity types, and their 221 * associated confidence scores. The entity types are ordered from highest to lowest scoring. 222 * 223 * <p><strong>NOTE: </strong>Call on a worker thread. 224 * 225 * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should 226 * throw an {@link IllegalStateException}. See {@link #isDestroyed()}. 227 * 228 * <p><b>NOTE:</b> Do not implement. The default implementation of this method calls 229 * {@link #suggestSelection(TextSelection.Request)}. If that method calls this method, 230 * a stack overflow error will happen. 231 * 232 * @param text text providing context for the selected text (which is specified 233 * by the sub sequence starting at selectionStartIndex and ending at selectionEndIndex) 234 * @param selectionStartIndex start index of the selected part of text 235 * @param selectionEndIndex end index of the selected part of text 236 * @param defaultLocales ordered list of locale preferences that may be used to 237 * disambiguate the provided text. If no locale preferences exist, set this to null 238 * or an empty locale list. 239 * 240 * @throws IllegalArgumentException if text is null; selectionStartIndex is negative; 241 * selectionEndIndex is greater than text.length() or not greater than selectionStartIndex 242 * 243 * @see #suggestSelection(TextSelection.Request) 244 */ 245 @WorkerThread 246 @NonNull suggestSelection( @onNull CharSequence text, @IntRange(from = 0) int selectionStartIndex, @IntRange(from = 0) int selectionEndIndex, @Nullable LocaleList defaultLocales)247 default TextSelection suggestSelection( 248 @NonNull CharSequence text, 249 @IntRange(from = 0) int selectionStartIndex, 250 @IntRange(from = 0) int selectionEndIndex, 251 @Nullable LocaleList defaultLocales) { 252 final TextSelection.Request request = new TextSelection.Request.Builder( 253 text, selectionStartIndex, selectionEndIndex) 254 .setDefaultLocales(defaultLocales) 255 .build(); 256 return suggestSelection(request); 257 } 258 259 /** 260 * Classifies the specified text and returns a {@link TextClassification} object that can be 261 * used to generate a widget for handling the classified text. 262 * 263 * <p><strong>NOTE: </strong>Call on a worker thread. 264 * 265 * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should 266 * throw an {@link IllegalStateException}. See {@link #isDestroyed()}. 267 * 268 * @param request the text classification request 269 */ 270 @WorkerThread 271 @NonNull classifyText(@onNull TextClassification.Request request)272 default TextClassification classifyText(@NonNull TextClassification.Request request) { 273 Objects.requireNonNull(request); 274 Utils.checkMainThread(); 275 return TextClassification.EMPTY; 276 } 277 278 /** 279 * Classifies the specified text and returns a {@link TextClassification} object that can be 280 * used to generate a widget for handling the classified text. 281 * 282 * <p><strong>NOTE: </strong>Call on a worker thread. 283 * 284 * <p><b>NOTE:</b> Do not implement. The default implementation of this method calls 285 * {@link #classifyText(TextClassification.Request)}. If that method calls this method, 286 * a stack overflow error will happen. 287 * 288 * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should 289 * throw an {@link IllegalStateException}. See {@link #isDestroyed()}. 290 * 291 * @param text text providing context for the text to classify (which is specified 292 * by the sub sequence starting at startIndex and ending at endIndex) 293 * @param startIndex start index of the text to classify 294 * @param endIndex end index of the text to classify 295 * @param defaultLocales ordered list of locale preferences that may be used to 296 * disambiguate the provided text. If no locale preferences exist, set this to null 297 * or an empty locale list. 298 * 299 * @throws IllegalArgumentException if text is null; startIndex is negative; 300 * endIndex is greater than text.length() or not greater than startIndex 301 * 302 * @see #classifyText(TextClassification.Request) 303 */ 304 @WorkerThread 305 @NonNull classifyText( @onNull CharSequence text, @IntRange(from = 0) int startIndex, @IntRange(from = 0) int endIndex, @Nullable LocaleList defaultLocales)306 default TextClassification classifyText( 307 @NonNull CharSequence text, 308 @IntRange(from = 0) int startIndex, 309 @IntRange(from = 0) int endIndex, 310 @Nullable LocaleList defaultLocales) { 311 final TextClassification.Request request = new TextClassification.Request.Builder( 312 text, startIndex, endIndex) 313 .setDefaultLocales(defaultLocales) 314 .build(); 315 return classifyText(request); 316 } 317 318 /** 319 * Generates and returns a {@link TextLinks} that may be applied to the text to annotate it with 320 * links information. 321 * 322 * <p><strong>NOTE: </strong>Call on a worker thread. 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 * @param request the text links request 328 * 329 * @see #getMaxGenerateLinksTextLength() 330 */ 331 @WorkerThread 332 @NonNull generateLinks(@onNull TextLinks.Request request)333 default TextLinks generateLinks(@NonNull TextLinks.Request request) { 334 Objects.requireNonNull(request); 335 Utils.checkMainThread(); 336 return new TextLinks.Builder(request.getText().toString()).build(); 337 } 338 339 /** 340 * Returns the maximal length of text that can be processed by generateLinks. 341 * 342 * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should 343 * throw an {@link IllegalStateException}. See {@link #isDestroyed()}. 344 * 345 * @see #generateLinks(TextLinks.Request) 346 */ 347 @WorkerThread getMaxGenerateLinksTextLength()348 default int getMaxGenerateLinksTextLength() { 349 return Integer.MAX_VALUE; 350 } 351 352 /** 353 * Detects the language of the text in the given request. 354 * 355 * <p><strong>NOTE: </strong>Call on a worker thread. 356 * 357 * 358 * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should 359 * throw an {@link IllegalStateException}. See {@link #isDestroyed()}. 360 * 361 * @param request the {@link TextLanguage} request. 362 * @return the {@link TextLanguage} result. 363 */ 364 @WorkerThread 365 @NonNull detectLanguage(@onNull TextLanguage.Request request)366 default TextLanguage detectLanguage(@NonNull TextLanguage.Request request) { 367 Objects.requireNonNull(request); 368 Utils.checkMainThread(); 369 return TextLanguage.EMPTY; 370 } 371 372 /** 373 * Suggests and returns a list of actions according to the given conversation. 374 */ 375 @WorkerThread 376 @NonNull suggestConversationActions( @onNull ConversationActions.Request request)377 default ConversationActions suggestConversationActions( 378 @NonNull ConversationActions.Request request) { 379 Objects.requireNonNull(request); 380 Utils.checkMainThread(); 381 return new ConversationActions(Collections.emptyList(), null); 382 } 383 384 /** 385 * <strong>NOTE: </strong>Use {@link #onTextClassifierEvent(TextClassifierEvent)} instead. 386 * <p> 387 * Reports a selection event. 388 * 389 * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should 390 * throw an {@link IllegalStateException}. See {@link #isDestroyed()}. 391 */ onSelectionEvent(@onNull SelectionEvent event)392 default void onSelectionEvent(@NonNull SelectionEvent event) { 393 // TODO: Consider rerouting to onTextClassifierEvent() 394 } 395 396 /** 397 * Reports a text classifier event. 398 * <p> 399 * <strong>NOTE: </strong>Call on a worker thread. 400 * 401 * @throws IllegalStateException if this TextClassifier has been destroyed. 402 * @see #isDestroyed() 403 */ onTextClassifierEvent(@onNull TextClassifierEvent event)404 default void onTextClassifierEvent(@NonNull TextClassifierEvent event) {} 405 406 /** 407 * Destroys this TextClassifier. 408 * 409 * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to its methods should 410 * throw an {@link IllegalStateException}. See {@link #isDestroyed()}. 411 * 412 * <p>Subsequent calls to this method are no-ops. 413 */ destroy()414 default void destroy() {} 415 416 /** 417 * Returns whether or not this TextClassifier has been destroyed. 418 * 419 * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, caller should not interact 420 * with the classifier and an attempt to do so would throw an {@link IllegalStateException}. 421 * However, this method should never throw an {@link IllegalStateException}. 422 * 423 * @see #destroy() 424 */ isDestroyed()425 default boolean isDestroyed() { 426 return false; 427 } 428 429 /** @hide **/ dump(@onNull IndentingPrintWriter printWriter)430 default void dump(@NonNull IndentingPrintWriter printWriter) {} 431 432 /** 433 * Configuration object for specifying what entity types to identify. 434 * 435 * Configs are initially based on a predefined preset, and can be modified from there. 436 */ 437 final class EntityConfig implements Parcelable { 438 private final List<String> mIncludedTypes; 439 private final List<String> mExcludedTypes; 440 private final List<String> mHints; 441 private final boolean mIncludeTypesFromTextClassifier; 442 EntityConfig( List<String> includedEntityTypes, List<String> excludedEntityTypes, List<String> hints, boolean includeTypesFromTextClassifier)443 private EntityConfig( 444 List<String> includedEntityTypes, 445 List<String> excludedEntityTypes, 446 List<String> hints, 447 boolean includeTypesFromTextClassifier) { 448 mIncludedTypes = Objects.requireNonNull(includedEntityTypes); 449 mExcludedTypes = Objects.requireNonNull(excludedEntityTypes); 450 mHints = Objects.requireNonNull(hints); 451 mIncludeTypesFromTextClassifier = includeTypesFromTextClassifier; 452 } 453 EntityConfig(Parcel in)454 private EntityConfig(Parcel in) { 455 mIncludedTypes = new ArrayList<>(); 456 in.readStringList(mIncludedTypes); 457 mExcludedTypes = new ArrayList<>(); 458 in.readStringList(mExcludedTypes); 459 List<String> tmpHints = new ArrayList<>(); 460 in.readStringList(tmpHints); 461 mHints = Collections.unmodifiableList(tmpHints); 462 mIncludeTypesFromTextClassifier = in.readByte() != 0; 463 } 464 465 @Override writeToParcel(Parcel parcel, int flags)466 public void writeToParcel(Parcel parcel, int flags) { 467 parcel.writeStringList(mIncludedTypes); 468 parcel.writeStringList(mExcludedTypes); 469 parcel.writeStringList(mHints); 470 parcel.writeByte((byte) (mIncludeTypesFromTextClassifier ? 1 : 0)); 471 } 472 473 /** 474 * Creates an EntityConfig. 475 * 476 * @param hints Hints for the TextClassifier to determine what types of entities to find. 477 * 478 * @deprecated Use {@link Builder} instead. 479 */ 480 @Deprecated createWithHints(@ullable Collection<String> hints)481 public static EntityConfig createWithHints(@Nullable Collection<String> hints) { 482 return new EntityConfig.Builder() 483 .includeTypesFromTextClassifier(true) 484 .setHints(hints) 485 .build(); 486 } 487 488 /** 489 * Creates an EntityConfig. 490 * 491 * @param hints Hints for the TextClassifier to determine what types of entities to find 492 * @param includedEntityTypes Entity types, e.g. {@link #TYPE_EMAIL}, to explicitly include 493 * @param excludedEntityTypes Entity types, e.g. {@link #TYPE_PHONE}, to explicitly exclude 494 * 495 * 496 * Note that if an entity has been excluded, the exclusion will take precedence. 497 * 498 * @deprecated Use {@link Builder} instead. 499 */ 500 @Deprecated create(@ullable Collection<String> hints, @Nullable Collection<String> includedEntityTypes, @Nullable Collection<String> excludedEntityTypes)501 public static EntityConfig create(@Nullable Collection<String> hints, 502 @Nullable Collection<String> includedEntityTypes, 503 @Nullable Collection<String> excludedEntityTypes) { 504 return new EntityConfig.Builder() 505 .setIncludedTypes(includedEntityTypes) 506 .setExcludedTypes(excludedEntityTypes) 507 .setHints(hints) 508 .includeTypesFromTextClassifier(true) 509 .build(); 510 } 511 512 /** 513 * Creates an EntityConfig with an explicit entity list. 514 * 515 * @param entityTypes Complete set of entities, e.g. {@link #TYPE_URL} to find. 516 * 517 * @deprecated Use {@link Builder} instead. 518 */ 519 @Deprecated createWithExplicitEntityList( @ullable Collection<String> entityTypes)520 public static EntityConfig createWithExplicitEntityList( 521 @Nullable Collection<String> entityTypes) { 522 return new EntityConfig.Builder() 523 .setIncludedTypes(entityTypes) 524 .includeTypesFromTextClassifier(false) 525 .build(); 526 } 527 528 /** 529 * Returns a final list of entity types to find. 530 * 531 * @param entityTypes Entity types we think should be found before factoring in 532 * includes/excludes 533 * 534 * This method is intended for use by TextClassifier implementations. 535 */ resolveEntityListModifications( @onNull Collection<String> entityTypes)536 public Collection<String> resolveEntityListModifications( 537 @NonNull Collection<String> entityTypes) { 538 final Set<String> finalSet = new HashSet<>(); 539 if (mIncludeTypesFromTextClassifier) { 540 finalSet.addAll(entityTypes); 541 } 542 finalSet.addAll(mIncludedTypes); 543 finalSet.removeAll(mExcludedTypes); 544 return finalSet; 545 } 546 547 /** 548 * Retrieves the list of hints. 549 * 550 * @return An unmodifiable collection of the hints. 551 */ getHints()552 public Collection<String> getHints() { 553 return mHints; 554 } 555 556 /** 557 * Return whether the client allows the text classifier to include its own list of 558 * default types. If this function returns {@code true}, a default list of types suggested 559 * from a text classifier will be taking into account. 560 * 561 * <p>NOTE: This method is intended for use by a text classifier. 562 * 563 * @see #resolveEntityListModifications(Collection) 564 */ shouldIncludeTypesFromTextClassifier()565 public boolean shouldIncludeTypesFromTextClassifier() { 566 return mIncludeTypesFromTextClassifier; 567 } 568 569 @Override describeContents()570 public int describeContents() { 571 return 0; 572 } 573 574 public static final @android.annotation.NonNull Parcelable.Creator<EntityConfig> CREATOR = 575 new Parcelable.Creator<EntityConfig>() { 576 @Override 577 public EntityConfig createFromParcel(Parcel in) { 578 return new EntityConfig(in); 579 } 580 581 @Override 582 public EntityConfig[] newArray(int size) { 583 return new EntityConfig[size]; 584 } 585 }; 586 587 588 589 /** Builder class to construct the {@link EntityConfig} object. */ 590 public static final class Builder { 591 @Nullable 592 private Collection<String> mIncludedTypes; 593 @Nullable 594 private Collection<String> mExcludedTypes; 595 @Nullable 596 private Collection<String> mHints; 597 private boolean mIncludeTypesFromTextClassifier = true; 598 599 /** 600 * Sets a collection of types that are explicitly included. 601 */ 602 @NonNull setIncludedTypes(@ullable Collection<String> includedTypes)603 public Builder setIncludedTypes(@Nullable Collection<String> includedTypes) { 604 mIncludedTypes = includedTypes; 605 return this; 606 } 607 608 /** 609 * Sets a collection of types that are explicitly excluded. 610 */ 611 @NonNull setExcludedTypes(@ullable Collection<String> excludedTypes)612 public Builder setExcludedTypes(@Nullable Collection<String> excludedTypes) { 613 mExcludedTypes = excludedTypes; 614 return this; 615 } 616 617 /** 618 * Specifies whether or not to include the types suggested by the text classifier. By 619 * default, it is included. 620 */ 621 @NonNull includeTypesFromTextClassifier(boolean includeTypesFromTextClassifier)622 public Builder includeTypesFromTextClassifier(boolean includeTypesFromTextClassifier) { 623 mIncludeTypesFromTextClassifier = includeTypesFromTextClassifier; 624 return this; 625 } 626 627 628 /** 629 * Sets the hints for the TextClassifier to determine what types of entities to find. 630 * These hints will only be used if {@link #includeTypesFromTextClassifier} is 631 * set to be true. 632 */ 633 @NonNull setHints(@ullable Collection<String> hints)634 public Builder setHints(@Nullable Collection<String> hints) { 635 mHints = hints; 636 return this; 637 } 638 639 /** 640 * Combines all of the options that have been set and returns a new {@link EntityConfig} 641 * object. 642 */ 643 @NonNull build()644 public EntityConfig build() { 645 return new EntityConfig( 646 mIncludedTypes == null 647 ? Collections.emptyList() 648 : new ArrayList<>(mIncludedTypes), 649 mExcludedTypes == null 650 ? Collections.emptyList() 651 : new ArrayList<>(mExcludedTypes), 652 mHints == null 653 ? Collections.emptyList() 654 : Collections.unmodifiableList(new ArrayList<>(mHints)), 655 mIncludeTypesFromTextClassifier); 656 } 657 } 658 } 659 660 /** 661 * Utility functions for TextClassifier methods. 662 * 663 * <ul> 664 * <li>Provides validation of input parameters to TextClassifier methods 665 * </ul> 666 * 667 * Intended to be used only for TextClassifier purposes. 668 * @hide 669 */ 670 final class Utils { 671 672 @GuardedBy("WORD_ITERATOR") 673 private static final BreakIterator WORD_ITERATOR = BreakIterator.getWordInstance(); 674 675 /** 676 * @throws IllegalArgumentException if text is null; startIndex is negative; 677 * endIndex is greater than text.length() or is not greater than startIndex; 678 * options is null 679 */ checkArgument(@onNull CharSequence text, int startIndex, int endIndex)680 static void checkArgument(@NonNull CharSequence text, int startIndex, int endIndex) { 681 Preconditions.checkArgument(text != null); 682 Preconditions.checkArgument(startIndex >= 0); 683 Preconditions.checkArgument(endIndex <= text.length()); 684 Preconditions.checkArgument(endIndex > startIndex); 685 } 686 687 /** Returns if the length of the text is within the range. */ checkTextLength(CharSequence text, int maxLength)688 static boolean checkTextLength(CharSequence text, int maxLength) { 689 int textLength = text.length(); 690 return textLength >= 0 && textLength <= maxLength; 691 } 692 693 /** 694 * Returns the substring of {@code text} that contains at least text from index 695 * {@code start} <i>(inclusive)</i> to index {@code end} <i><(exclusive)/i> with the goal of 696 * returning text that is at least {@code minimumLength}. If {@code text} is not long 697 * enough, this will return {@code text}. This method returns text at word boundaries. 698 * 699 * @param text the source text 700 * @param start the start index of text that must be included 701 * @param end the end index of text that must be included 702 * @param minimumLength minimum length of text to return if {@code text} is long enough 703 */ getSubString( String text, int start, int end, int minimumLength)704 public static String getSubString( 705 String text, int start, int end, int minimumLength) { 706 Preconditions.checkArgument(start >= 0); 707 Preconditions.checkArgument(end <= text.length()); 708 Preconditions.checkArgument(start <= end); 709 710 if (text.length() < minimumLength) { 711 return text; 712 } 713 714 final int length = end - start; 715 if (length >= minimumLength) { 716 return text.substring(start, end); 717 } 718 719 final int offset = (minimumLength - length) / 2; 720 int iterStart = Math.max(0, Math.min(start - offset, text.length() - minimumLength)); 721 int iterEnd = Math.min(text.length(), iterStart + minimumLength); 722 723 synchronized (WORD_ITERATOR) { 724 WORD_ITERATOR.setText(text); 725 iterStart = WORD_ITERATOR.isBoundary(iterStart) 726 ? iterStart : Math.max(0, WORD_ITERATOR.preceding(iterStart)); 727 iterEnd = WORD_ITERATOR.isBoundary(iterEnd) 728 ? iterEnd : Math.max(iterEnd, WORD_ITERATOR.following(iterEnd)); 729 WORD_ITERATOR.setText(""); 730 return text.substring(iterStart, iterEnd); 731 } 732 } 733 734 /** 735 * Generates links using legacy {@link Linkify}. 736 */ generateLegacyLinks(@onNull TextLinks.Request request)737 public static TextLinks generateLegacyLinks(@NonNull TextLinks.Request request) { 738 final String string = request.getText().toString(); 739 final TextLinks.Builder links = new TextLinks.Builder(string); 740 741 final Collection<String> entities = request.getEntityConfig() 742 .resolveEntityListModifications(Collections.emptyList()); 743 if (entities.contains(TextClassifier.TYPE_URL)) { 744 addLinks(links, string, TextClassifier.TYPE_URL); 745 } 746 if (entities.contains(TextClassifier.TYPE_PHONE)) { 747 addLinks(links, string, TextClassifier.TYPE_PHONE); 748 } 749 if (entities.contains(TextClassifier.TYPE_EMAIL)) { 750 addLinks(links, string, TextClassifier.TYPE_EMAIL); 751 } 752 // NOTE: Do not support MAP_ADDRESSES. Legacy version does not work well. 753 return links.build(); 754 } 755 addLinks( TextLinks.Builder links, String string, @EntityType String entityType)756 private static void addLinks( 757 TextLinks.Builder links, String string, @EntityType String entityType) { 758 final Spannable spannable = new SpannableString(string); 759 if (Linkify.addLinks(spannable, linkMask(entityType))) { 760 final URLSpan[] spans = spannable.getSpans(0, spannable.length(), URLSpan.class); 761 for (URLSpan urlSpan : spans) { 762 links.addLink( 763 spannable.getSpanStart(urlSpan), 764 spannable.getSpanEnd(urlSpan), 765 entityScores(entityType), 766 urlSpan); 767 } 768 } 769 } 770 771 @LinkifyMask linkMask(@ntityType String entityType)772 private static int linkMask(@EntityType String entityType) { 773 switch (entityType) { 774 case TextClassifier.TYPE_URL: 775 return Linkify.WEB_URLS; 776 case TextClassifier.TYPE_PHONE: 777 return Linkify.PHONE_NUMBERS; 778 case TextClassifier.TYPE_EMAIL: 779 return Linkify.EMAIL_ADDRESSES; 780 default: 781 // NOTE: Do not support MAP_ADDRESSES. Legacy version does not work well. 782 return 0; 783 } 784 } 785 entityScores(@ntityType String entityType)786 private static Map<String, Float> entityScores(@EntityType String entityType) { 787 final Map<String, Float> scores = new ArrayMap<>(); 788 scores.put(entityType, 1f); 789 return scores; 790 } 791 checkMainThread()792 static void checkMainThread() { 793 if (Looper.myLooper() == Looper.getMainLooper()) { 794 Log.w(LOG_TAG, "TextClassifier called on main thread"); 795 } 796 } 797 } 798 } 799