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