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