1 /* 2 * Copyright 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.FloatRange; 20 import android.annotation.IntDef; 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.annotation.UserIdInt; 24 import android.content.Context; 25 import android.os.Bundle; 26 import android.os.LocaleList; 27 import android.os.Parcel; 28 import android.os.Parcelable; 29 import android.os.UserHandle; 30 import android.text.Spannable; 31 import android.text.method.MovementMethod; 32 import android.text.style.ClickableSpan; 33 import android.text.style.URLSpan; 34 import android.view.View; 35 import android.view.textclassifier.TextClassifier.EntityConfig; 36 import android.view.textclassifier.TextClassifier.EntityType; 37 import android.widget.TextView; 38 39 import com.android.internal.annotations.VisibleForTesting; 40 import com.android.internal.annotations.VisibleForTesting.Visibility; 41 import com.android.internal.util.Preconditions; 42 43 import java.lang.annotation.Retention; 44 import java.lang.annotation.RetentionPolicy; 45 import java.util.ArrayList; 46 import java.util.Collection; 47 import java.util.Collections; 48 import java.util.List; 49 import java.util.Locale; 50 import java.util.Map; 51 import java.util.function.Function; 52 53 /** 54 * A collection of links, representing subsequences of text and the entity types (phone number, 55 * address, url, etc) they may be. 56 */ 57 public final class TextLinks implements Parcelable { 58 59 /** 60 * Return status of an attempt to apply TextLinks to text. 61 * @hide 62 */ 63 @Retention(RetentionPolicy.SOURCE) 64 @IntDef({STATUS_LINKS_APPLIED, STATUS_NO_LINKS_FOUND, STATUS_NO_LINKS_APPLIED, 65 STATUS_DIFFERENT_TEXT, STATUS_UNSUPPORTED_CHARACTER}) 66 public @interface Status {} 67 68 /** Links were successfully applied to the text. */ 69 public static final int STATUS_LINKS_APPLIED = 0; 70 71 /** No links exist to apply to text. Links count is zero. */ 72 public static final int STATUS_NO_LINKS_FOUND = 1; 73 74 /** No links applied to text. The links were filtered out. */ 75 public static final int STATUS_NO_LINKS_APPLIED = 2; 76 77 /** The specified text does not match the text used to generate the links. */ 78 public static final int STATUS_DIFFERENT_TEXT = 3; 79 80 /** The specified text contains unsupported characters. */ 81 public static final int STATUS_UNSUPPORTED_CHARACTER = 4; 82 83 /** @hide */ 84 @Retention(RetentionPolicy.SOURCE) 85 @IntDef({APPLY_STRATEGY_IGNORE, APPLY_STRATEGY_REPLACE}) 86 public @interface ApplyStrategy {} 87 88 /** 89 * Do not replace {@link ClickableSpan}s that exist where the {@link TextLinkSpan} needs to 90 * be applied to. Do not apply the TextLinkSpan. 91 */ 92 public static final int APPLY_STRATEGY_IGNORE = 0; 93 94 /** 95 * Replace any {@link ClickableSpan}s that exist where the {@link TextLinkSpan} needs to be 96 * applied to. 97 */ 98 public static final int APPLY_STRATEGY_REPLACE = 1; 99 100 private final String mFullText; 101 private final List<TextLink> mLinks; 102 private final Bundle mExtras; 103 TextLinks(String fullText, ArrayList<TextLink> links, Bundle extras)104 private TextLinks(String fullText, ArrayList<TextLink> links, Bundle extras) { 105 mFullText = fullText; 106 mLinks = Collections.unmodifiableList(links); 107 mExtras = extras; 108 } 109 110 /** 111 * Returns the text that was used to generate these links. 112 * @hide 113 */ 114 @NonNull getText()115 public String getText() { 116 return mFullText; 117 } 118 119 /** 120 * Returns an unmodifiable Collection of the links. 121 */ 122 @NonNull getLinks()123 public Collection<TextLink> getLinks() { 124 return mLinks; 125 } 126 127 /** 128 * Returns the extended data. 129 * 130 * <p><b>NOTE: </b>Do not modify this bundle. 131 */ 132 @NonNull getExtras()133 public Bundle getExtras() { 134 return mExtras; 135 } 136 137 /** 138 * Annotates the given text with the generated links. It will fail if the provided text doesn't 139 * match the original text used to create the TextLinks. 140 * 141 * <p><strong>NOTE: </strong>It may be necessary to set a LinkMovementMethod on the TextView 142 * widget to properly handle links. See {@link TextView#setMovementMethod(MovementMethod)} 143 * 144 * @param text the text to apply the links to. Must match the original text 145 * @param applyStrategy the apply strategy used to determine how to apply links to text. 146 * e.g {@link TextLinks#APPLY_STRATEGY_IGNORE} 147 * @param spanFactory a custom span factory for converting TextLinks to TextLinkSpans. 148 * Set to {@code null} to use the default span factory. 149 * 150 * @return a status code indicating whether or not the links were successfully applied 151 * e.g. {@link #STATUS_LINKS_APPLIED} 152 */ 153 @Status apply( @onNull Spannable text, @ApplyStrategy int applyStrategy, @Nullable Function<TextLink, TextLinkSpan> spanFactory)154 public int apply( 155 @NonNull Spannable text, 156 @ApplyStrategy int applyStrategy, 157 @Nullable Function<TextLink, TextLinkSpan> spanFactory) { 158 Preconditions.checkNotNull(text); 159 return new TextLinksParams.Builder() 160 .setApplyStrategy(applyStrategy) 161 .setSpanFactory(spanFactory) 162 .build() 163 .apply(text, this); 164 } 165 166 @Override toString()167 public String toString() { 168 return String.format(Locale.US, "TextLinks{fullText=%s, links=%s}", mFullText, mLinks); 169 } 170 171 @Override describeContents()172 public int describeContents() { 173 return 0; 174 } 175 176 @Override writeToParcel(Parcel dest, int flags)177 public void writeToParcel(Parcel dest, int flags) { 178 dest.writeString(mFullText); 179 dest.writeTypedList(mLinks); 180 dest.writeBundle(mExtras); 181 } 182 183 public static final @android.annotation.NonNull Parcelable.Creator<TextLinks> CREATOR = 184 new Parcelable.Creator<TextLinks>() { 185 @Override 186 public TextLinks createFromParcel(Parcel in) { 187 return new TextLinks(in); 188 } 189 190 @Override 191 public TextLinks[] newArray(int size) { 192 return new TextLinks[size]; 193 } 194 }; 195 TextLinks(Parcel in)196 private TextLinks(Parcel in) { 197 mFullText = in.readString(); 198 mLinks = in.createTypedArrayList(TextLink.CREATOR); 199 mExtras = in.readBundle(); 200 } 201 202 /** 203 * A link, identifying a substring of text and possible entity types for it. 204 */ 205 public static final class TextLink implements Parcelable { 206 private final EntityConfidence mEntityScores; 207 private final int mStart; 208 private final int mEnd; 209 private final Bundle mExtras; 210 @Nullable private final URLSpan mUrlSpan; 211 212 /** 213 * Create a new TextLink. 214 * 215 * @param start The start index of the identified subsequence 216 * @param end The end index of the identified subsequence 217 * @param entityConfidence A mapping of entity type to confidence score 218 * @param extras A bundle containing custom data related to this TextLink 219 * @param urlSpan An optional URLSpan to delegate to. NOTE: Not parcelled 220 * 221 * @throws IllegalArgumentException if {@code entityConfidence} is null or empty 222 * @throws IllegalArgumentException if {@code start} is greater than {@code end} 223 */ TextLink(int start, int end, @NonNull EntityConfidence entityConfidence, @NonNull Bundle extras, @Nullable URLSpan urlSpan)224 private TextLink(int start, int end, @NonNull EntityConfidence entityConfidence, 225 @NonNull Bundle extras, @Nullable URLSpan urlSpan) { 226 Preconditions.checkNotNull(entityConfidence); 227 Preconditions.checkArgument(!entityConfidence.getEntities().isEmpty()); 228 Preconditions.checkArgument(start <= end); 229 Preconditions.checkNotNull(extras); 230 mStart = start; 231 mEnd = end; 232 mEntityScores = entityConfidence; 233 mUrlSpan = urlSpan; 234 mExtras = extras; 235 } 236 237 /** 238 * Returns the start index of this link in the original text. 239 * 240 * @return the start index 241 */ getStart()242 public int getStart() { 243 return mStart; 244 } 245 246 /** 247 * Returns the end index of this link in the original text. 248 * 249 * @return the end index 250 */ getEnd()251 public int getEnd() { 252 return mEnd; 253 } 254 255 /** 256 * Returns the number of entity types that have confidence scores. 257 * 258 * @return the entity count 259 */ getEntityCount()260 public int getEntityCount() { 261 return mEntityScores.getEntities().size(); 262 } 263 264 /** 265 * Returns the entity type at a given index. Entity types are sorted by confidence. 266 * 267 * @return the entity type at the provided index 268 */ getEntity(int index)269 @NonNull public @EntityType String getEntity(int index) { 270 return mEntityScores.getEntities().get(index); 271 } 272 273 /** 274 * Returns the confidence score for a particular entity type. 275 * 276 * @param entityType the entity type 277 */ getConfidenceScore( @ntityType String entityType)278 public @FloatRange(from = 0.0, to = 1.0) float getConfidenceScore( 279 @EntityType String entityType) { 280 return mEntityScores.getConfidenceScore(entityType); 281 } 282 283 /** 284 * Returns a bundle containing custom data related to this TextLink. 285 */ 286 @NonNull getExtras()287 public Bundle getExtras() { 288 return mExtras; 289 } 290 291 @Override toString()292 public String toString() { 293 return String.format(Locale.US, 294 "TextLink{start=%s, end=%s, entityScores=%s, urlSpan=%s}", 295 mStart, mEnd, mEntityScores, mUrlSpan); 296 } 297 298 @Override describeContents()299 public int describeContents() { 300 return 0; 301 } 302 303 @Override writeToParcel(Parcel dest, int flags)304 public void writeToParcel(Parcel dest, int flags) { 305 mEntityScores.writeToParcel(dest, flags); 306 dest.writeInt(mStart); 307 dest.writeInt(mEnd); 308 dest.writeBundle(mExtras); 309 } 310 readFromParcel(Parcel in)311 private static TextLink readFromParcel(Parcel in) { 312 final EntityConfidence entityConfidence = EntityConfidence.CREATOR.createFromParcel(in); 313 final int start = in.readInt(); 314 final int end = in.readInt(); 315 final Bundle extras = in.readBundle(); 316 return new TextLink(start, end, entityConfidence, extras, null /* urlSpan */); 317 } 318 319 public static final @android.annotation.NonNull Parcelable.Creator<TextLink> CREATOR = 320 new Parcelable.Creator<TextLink>() { 321 @Override 322 public TextLink createFromParcel(Parcel in) { 323 return readFromParcel(in); 324 } 325 326 @Override 327 public TextLink[] newArray(int size) { 328 return new TextLink[size]; 329 } 330 }; 331 } 332 333 /** 334 * A request object for generating TextLinks. 335 */ 336 public static final class Request implements Parcelable { 337 338 private final CharSequence mText; 339 @Nullable private final LocaleList mDefaultLocales; 340 @Nullable private final EntityConfig mEntityConfig; 341 private final boolean mLegacyFallback; 342 @Nullable private String mCallingPackageName; 343 private final Bundle mExtras; 344 @UserIdInt 345 private int mUserId = UserHandle.USER_NULL; 346 Request( CharSequence text, LocaleList defaultLocales, EntityConfig entityConfig, boolean legacyFallback, Bundle extras)347 private Request( 348 CharSequence text, 349 LocaleList defaultLocales, 350 EntityConfig entityConfig, 351 boolean legacyFallback, 352 Bundle extras) { 353 mText = text; 354 mDefaultLocales = defaultLocales; 355 mEntityConfig = entityConfig; 356 mLegacyFallback = legacyFallback; 357 mExtras = extras; 358 } 359 360 /** 361 * Returns the text to generate links for. 362 */ 363 @NonNull getText()364 public CharSequence getText() { 365 return mText; 366 } 367 368 /** 369 * @return ordered list of locale preferences that can be used to disambiguate 370 * the provided text 371 */ 372 @Nullable getDefaultLocales()373 public LocaleList getDefaultLocales() { 374 return mDefaultLocales; 375 } 376 377 /** 378 * @return The config representing the set of entities to look for 379 * @see Builder#setEntityConfig(EntityConfig) 380 */ 381 @Nullable getEntityConfig()382 public EntityConfig getEntityConfig() { 383 return mEntityConfig; 384 } 385 386 /** 387 * Returns whether the TextClassifier can fallback to legacy links if smart linkify is 388 * disabled. 389 * <strong>Note: </strong>This is not parcelled. 390 * @hide 391 */ isLegacyFallback()392 public boolean isLegacyFallback() { 393 return mLegacyFallback; 394 } 395 396 /** 397 * Sets the name of the package that is sending this request. 398 * <p> 399 * Package-private for SystemTextClassifier's use. 400 * @hide 401 */ 402 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) setCallingPackageName(@ullable String callingPackageName)403 public void setCallingPackageName(@Nullable String callingPackageName) { 404 mCallingPackageName = callingPackageName; 405 } 406 407 /** 408 * Returns the name of the package that sent this request. 409 * This returns {@code null} if no calling package name is set. 410 */ 411 @Nullable getCallingPackageName()412 public String getCallingPackageName() { 413 return mCallingPackageName; 414 } 415 416 /** 417 * Sets the id of the user that sent this request. 418 * <p> 419 * Package-private for SystemTextClassifier's use. 420 */ setUserId(@serIdInt int userId)421 void setUserId(@UserIdInt int userId) { 422 mUserId = userId; 423 } 424 425 /** 426 * Returns the id of the user that sent this request. 427 * @hide 428 */ 429 @UserIdInt getUserId()430 public int getUserId() { 431 return mUserId; 432 } 433 434 /** 435 * Returns the extended data. 436 * 437 * <p><b>NOTE: </b>Do not modify this bundle. 438 */ 439 @NonNull getExtras()440 public Bundle getExtras() { 441 return mExtras; 442 } 443 444 /** 445 * A builder for building TextLinks requests. 446 */ 447 public static final class Builder { 448 449 private final CharSequence mText; 450 451 @Nullable private LocaleList mDefaultLocales; 452 @Nullable private EntityConfig mEntityConfig; 453 private boolean mLegacyFallback = true; // Use legacy fall back by default. 454 @Nullable private Bundle mExtras; 455 Builder(@onNull CharSequence text)456 public Builder(@NonNull CharSequence text) { 457 mText = Preconditions.checkNotNull(text); 458 } 459 460 /** 461 * @param defaultLocales ordered list of locale preferences that may be used to 462 * disambiguate the provided text. If no locale preferences exist, 463 * set this to null or an empty locale list. 464 * @return this builder 465 */ 466 @NonNull setDefaultLocales(@ullable LocaleList defaultLocales)467 public Builder setDefaultLocales(@Nullable LocaleList defaultLocales) { 468 mDefaultLocales = defaultLocales; 469 return this; 470 } 471 472 /** 473 * Sets the entity configuration to use. This determines what types of entities the 474 * TextClassifier will look for. 475 * Set to {@code null} for the default entity config and teh TextClassifier will 476 * automatically determine what links to generate. 477 * 478 * @return this builder 479 */ 480 @NonNull setEntityConfig(@ullable EntityConfig entityConfig)481 public Builder setEntityConfig(@Nullable EntityConfig entityConfig) { 482 mEntityConfig = entityConfig; 483 return this; 484 } 485 486 /** 487 * Sets whether the TextClassifier can fallback to legacy links if smart linkify is 488 * disabled. 489 * 490 * <p><strong>Note: </strong>This is not parcelled. 491 * 492 * @return this builder 493 * @hide 494 */ 495 @NonNull setLegacyFallback(boolean legacyFallback)496 public Builder setLegacyFallback(boolean legacyFallback) { 497 mLegacyFallback = legacyFallback; 498 return this; 499 } 500 501 /** 502 * Sets the extended data. 503 * 504 * @return this builder 505 */ setExtras(@ullable Bundle extras)506 public Builder setExtras(@Nullable Bundle extras) { 507 mExtras = extras; 508 return this; 509 } 510 511 /** 512 * Builds and returns the request object. 513 */ 514 @NonNull build()515 public Request build() { 516 return new Request( 517 mText, mDefaultLocales, mEntityConfig, 518 mLegacyFallback, 519 mExtras == null ? Bundle.EMPTY : mExtras); 520 } 521 } 522 523 @Override describeContents()524 public int describeContents() { 525 return 0; 526 } 527 528 @Override writeToParcel(Parcel dest, int flags)529 public void writeToParcel(Parcel dest, int flags) { 530 dest.writeString(mText.toString()); 531 dest.writeParcelable(mDefaultLocales, flags); 532 dest.writeParcelable(mEntityConfig, flags); 533 dest.writeString(mCallingPackageName); 534 dest.writeInt(mUserId); 535 dest.writeBundle(mExtras); 536 } 537 readFromParcel(Parcel in)538 private static Request readFromParcel(Parcel in) { 539 final String text = in.readString(); 540 final LocaleList defaultLocales = in.readParcelable(null); 541 final EntityConfig entityConfig = in.readParcelable(null); 542 final String callingPackageName = in.readString(); 543 final int userId = in.readInt(); 544 final Bundle extras = in.readBundle(); 545 546 final Request request = new Request(text, defaultLocales, entityConfig, 547 /* legacyFallback= */ true, extras); 548 request.setCallingPackageName(callingPackageName); 549 request.setUserId(userId); 550 return request; 551 } 552 553 public static final @android.annotation.NonNull Parcelable.Creator<Request> CREATOR = 554 new Parcelable.Creator<Request>() { 555 @Override 556 public Request createFromParcel(Parcel in) { 557 return readFromParcel(in); 558 } 559 560 @Override 561 public Request[] newArray(int size) { 562 return new Request[size]; 563 } 564 }; 565 } 566 567 /** 568 * A ClickableSpan for a TextLink. 569 * 570 * <p>Applies only to TextViews. 571 */ 572 public static class TextLinkSpan extends ClickableSpan { 573 574 /** 575 * How the clickspan is triggered. 576 * @hide 577 */ 578 @Retention(RetentionPolicy.SOURCE) 579 @IntDef({INVOCATION_METHOD_UNSPECIFIED, INVOCATION_METHOD_TOUCH, 580 INVOCATION_METHOD_KEYBOARD}) 581 public @interface InvocationMethod {} 582 583 /** @hide */ 584 public static final int INVOCATION_METHOD_UNSPECIFIED = -1; 585 /** @hide */ 586 public static final int INVOCATION_METHOD_TOUCH = 0; 587 /** @hide */ 588 public static final int INVOCATION_METHOD_KEYBOARD = 1; 589 590 private final TextLink mTextLink; 591 TextLinkSpan(@onNull TextLink textLink)592 public TextLinkSpan(@NonNull TextLink textLink) { 593 mTextLink = textLink; 594 } 595 596 @Override onClick(View widget)597 public void onClick(View widget) { 598 onClick(widget, INVOCATION_METHOD_UNSPECIFIED); 599 } 600 601 /** @hide */ onClick(View widget, @InvocationMethod int invocationMethod)602 public final void onClick(View widget, @InvocationMethod int invocationMethod) { 603 if (widget instanceof TextView) { 604 final TextView textView = (TextView) widget; 605 final Context context = textView.getContext(); 606 if (TextClassificationManager.getSettings(context).isSmartLinkifyEnabled()) { 607 switch (invocationMethod) { 608 case INVOCATION_METHOD_TOUCH: 609 textView.requestActionMode(this); 610 break; 611 case INVOCATION_METHOD_KEYBOARD:// fall though 612 case INVOCATION_METHOD_UNSPECIFIED: // fall through 613 default: 614 textView.handleClick(this); 615 break; 616 } 617 } else { 618 if (mTextLink.mUrlSpan != null) { 619 mTextLink.mUrlSpan.onClick(textView); 620 } else { 621 textView.handleClick(this); 622 } 623 } 624 } 625 } 626 getTextLink()627 public final TextLink getTextLink() { 628 return mTextLink; 629 } 630 631 /** @hide */ 632 @VisibleForTesting(visibility = Visibility.PRIVATE) 633 @Nullable getUrl()634 public final String getUrl() { 635 if (mTextLink.mUrlSpan != null) { 636 return mTextLink.mUrlSpan.getURL(); 637 } 638 return null; 639 } 640 } 641 642 /** 643 * A builder to construct a TextLinks instance. 644 */ 645 public static final class Builder { 646 private final String mFullText; 647 private final ArrayList<TextLink> mLinks; 648 private Bundle mExtras; 649 650 /** 651 * Create a new TextLinks.Builder. 652 * 653 * @param fullText The full text to annotate with links 654 */ Builder(@onNull String fullText)655 public Builder(@NonNull String fullText) { 656 mFullText = Preconditions.checkNotNull(fullText); 657 mLinks = new ArrayList<>(); 658 } 659 660 /** 661 * Adds a TextLink. 662 * 663 * @param start The start index of the identified subsequence 664 * @param end The end index of the identified subsequence 665 * @param entityScores A mapping of entity type to confidence score 666 * 667 * @throws IllegalArgumentException if entityScores is null or empty. 668 */ 669 @NonNull addLink(int start, int end, @NonNull Map<String, Float> entityScores)670 public Builder addLink(int start, int end, @NonNull Map<String, Float> entityScores) { 671 return addLink(start, end, entityScores, Bundle.EMPTY, null); 672 } 673 674 /** 675 * Adds a TextLink. 676 * 677 * @see #addLink(int, int, Map) 678 * @param extras An optional bundle containing custom data related to this TextLink 679 */ 680 @NonNull addLink(int start, int end, @NonNull Map<String, Float> entityScores, @NonNull Bundle extras)681 public Builder addLink(int start, int end, @NonNull Map<String, Float> entityScores, 682 @NonNull Bundle extras) { 683 return addLink(start, end, entityScores, extras, null); 684 } 685 686 /** 687 * @see #addLink(int, int, Map) 688 * @param urlSpan An optional URLSpan to delegate to. NOTE: Not parcelled. 689 */ 690 @NonNull addLink(int start, int end, @NonNull Map<String, Float> entityScores, @Nullable URLSpan urlSpan)691 Builder addLink(int start, int end, @NonNull Map<String, Float> entityScores, 692 @Nullable URLSpan urlSpan) { 693 return addLink(start, end, entityScores, Bundle.EMPTY, urlSpan); 694 } 695 addLink(int start, int end, @NonNull Map<String, Float> entityScores, @NonNull Bundle extras, @Nullable URLSpan urlSpan)696 private Builder addLink(int start, int end, @NonNull Map<String, Float> entityScores, 697 @NonNull Bundle extras, @Nullable URLSpan urlSpan) { 698 mLinks.add(new TextLink( 699 start, end, new EntityConfidence(entityScores), extras, urlSpan)); 700 return this; 701 } 702 703 /** 704 * Removes all {@link TextLink}s. 705 */ 706 @NonNull clearTextLinks()707 public Builder clearTextLinks() { 708 mLinks.clear(); 709 return this; 710 } 711 712 /** 713 * Sets the extended data. 714 * 715 * @return this builder 716 */ 717 @NonNull setExtras(@ullable Bundle extras)718 public Builder setExtras(@Nullable Bundle extras) { 719 mExtras = extras; 720 return this; 721 } 722 723 /** 724 * Constructs a TextLinks instance. 725 * 726 * @return the constructed TextLinks 727 */ 728 @NonNull build()729 public TextLinks build() { 730 return new TextLinks(mFullText, mLinks, 731 mExtras == null ? Bundle.EMPTY : mExtras); 732 } 733 } 734 } 735