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