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