1 /* 2 * Copyright 2018 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.NonNull; 20 import android.annotation.Nullable; 21 import android.text.Spannable; 22 import android.text.style.ClickableSpan; 23 import android.text.util.Linkify; 24 import android.text.util.Linkify.LinkifyMask; 25 import android.view.textclassifier.TextLinks.TextLink; 26 import android.view.textclassifier.TextLinks.TextLinkSpan; 27 28 import java.util.ArrayList; 29 import java.util.List; 30 import java.util.Objects; 31 import java.util.function.Function; 32 33 /** 34 * Parameters for generating and applying links. 35 * @hide 36 */ 37 public final class TextLinksParams { 38 39 /** 40 * A function to create spans from TextLinks. 41 */ 42 private static final Function<TextLink, TextLinkSpan> DEFAULT_SPAN_FACTORY = 43 textLink -> new TextLinkSpan(textLink); 44 45 @TextLinks.ApplyStrategy 46 private final int mApplyStrategy; 47 private final Function<TextLink, TextLinkSpan> mSpanFactory; 48 private final TextClassifier.EntityConfig mEntityConfig; 49 TextLinksParams( @extLinks.ApplyStrategy int applyStrategy, Function<TextLink, TextLinkSpan> spanFactory)50 private TextLinksParams( 51 @TextLinks.ApplyStrategy int applyStrategy, 52 Function<TextLink, TextLinkSpan> spanFactory) { 53 mApplyStrategy = applyStrategy; 54 mSpanFactory = spanFactory; 55 mEntityConfig = TextClassifier.EntityConfig.createWithHints(null); 56 } 57 58 /** 59 * Returns a new TextLinksParams object based on the specified link mask. 60 * 61 * @param mask the link mask 62 * e.g. {@link LinkifyMask#PHONE_NUMBERS} | {@link LinkifyMask#EMAIL_ADDRESSES} 63 * @hide 64 */ 65 @NonNull fromLinkMask(@inkifyMask int mask)66 public static TextLinksParams fromLinkMask(@LinkifyMask int mask) { 67 final List<String> entitiesToFind = new ArrayList<>(); 68 if ((mask & Linkify.WEB_URLS) != 0) { 69 entitiesToFind.add(TextClassifier.TYPE_URL); 70 } 71 if ((mask & Linkify.EMAIL_ADDRESSES) != 0) { 72 entitiesToFind.add(TextClassifier.TYPE_EMAIL); 73 } 74 if ((mask & Linkify.PHONE_NUMBERS) != 0) { 75 entitiesToFind.add(TextClassifier.TYPE_PHONE); 76 } 77 if ((mask & Linkify.MAP_ADDRESSES) != 0) { 78 entitiesToFind.add(TextClassifier.TYPE_ADDRESS); 79 } 80 return new TextLinksParams.Builder().setEntityConfig( 81 TextClassifier.EntityConfig.createWithExplicitEntityList(entitiesToFind)) 82 .build(); 83 } 84 85 /** 86 * Returns the entity config used to determine what entity types to generate. 87 */ 88 @NonNull getEntityConfig()89 public TextClassifier.EntityConfig getEntityConfig() { 90 return mEntityConfig; 91 } 92 93 /** 94 * Annotates the given text with the generated links. It will fail if the provided text doesn't 95 * match the original text used to crete the TextLinks. 96 * 97 * @param text the text to apply the links to. Must match the original text 98 * @param textLinks the links to apply to the text 99 * 100 * @return a status code indicating whether or not the links were successfully applied 101 * @hide 102 */ 103 @TextLinks.Status apply(@onNull Spannable text, @NonNull TextLinks textLinks)104 public int apply(@NonNull Spannable text, @NonNull TextLinks textLinks) { 105 Objects.requireNonNull(text); 106 Objects.requireNonNull(textLinks); 107 108 final String textString = text.toString(); 109 110 if (Linkify.containsUnsupportedCharacters(textString)) { 111 // Do not apply links to text containing unsupported characters. 112 android.util.EventLog.writeEvent(0x534e4554, "116321860", -1, ""); 113 return TextLinks.STATUS_UNSUPPORTED_CHARACTER; 114 } 115 116 if (!textString.startsWith(textLinks.getText().toString())) { 117 return TextLinks.STATUS_DIFFERENT_TEXT; 118 } 119 if (textLinks.getLinks().isEmpty()) { 120 return TextLinks.STATUS_NO_LINKS_FOUND; 121 } 122 123 int applyCount = 0; 124 for (TextLink link : textLinks.getLinks()) { 125 final TextLinkSpan span = mSpanFactory.apply(link); 126 if (span != null) { 127 final ClickableSpan[] existingSpans = text.getSpans( 128 link.getStart(), link.getEnd(), ClickableSpan.class); 129 if (existingSpans.length > 0) { 130 if (mApplyStrategy == TextLinks.APPLY_STRATEGY_REPLACE) { 131 for (ClickableSpan existingSpan : existingSpans) { 132 text.removeSpan(existingSpan); 133 } 134 text.setSpan(span, link.getStart(), link.getEnd(), 135 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 136 applyCount++; 137 } 138 } else { 139 text.setSpan(span, link.getStart(), link.getEnd(), 140 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 141 applyCount++; 142 } 143 } 144 } 145 if (applyCount == 0) { 146 return TextLinks.STATUS_NO_LINKS_APPLIED; 147 } 148 return TextLinks.STATUS_LINKS_APPLIED; 149 } 150 151 /** 152 * A builder for building TextLinksParams. 153 */ 154 public static final class Builder { 155 156 @TextLinks.ApplyStrategy 157 private int mApplyStrategy = TextLinks.APPLY_STRATEGY_IGNORE; 158 private Function<TextLink, TextLinkSpan> mSpanFactory = DEFAULT_SPAN_FACTORY; 159 160 /** 161 * Sets the apply strategy used to determine how to apply links to text. 162 * e.g {@link TextLinks#APPLY_STRATEGY_IGNORE} 163 * 164 * @return this builder 165 */ setApplyStrategy(@extLinks.ApplyStrategy int applyStrategy)166 public Builder setApplyStrategy(@TextLinks.ApplyStrategy int applyStrategy) { 167 mApplyStrategy = checkApplyStrategy(applyStrategy); 168 return this; 169 } 170 171 /** 172 * Sets a custom span factory for converting TextLinks to TextLinkSpans. 173 * Set to {@code null} to use the default span factory. 174 * 175 * @return this builder 176 */ setSpanFactory(@ullable Function<TextLink, TextLinkSpan> spanFactory)177 public Builder setSpanFactory(@Nullable Function<TextLink, TextLinkSpan> spanFactory) { 178 mSpanFactory = spanFactory == null ? DEFAULT_SPAN_FACTORY : spanFactory; 179 return this; 180 } 181 182 /** 183 * Sets the entity configuration used to determine what entity types to generate. 184 * Set to {@code null} for the default entity config which will automatically determine 185 * what links to generate. 186 * 187 * @return this builder 188 */ setEntityConfig(@ullable TextClassifier.EntityConfig entityConfig)189 public Builder setEntityConfig(@Nullable TextClassifier.EntityConfig entityConfig) { 190 return this; 191 } 192 193 /** 194 * Builds and returns a TextLinksParams object. 195 */ build()196 public TextLinksParams build() { 197 return new TextLinksParams(mApplyStrategy, mSpanFactory); 198 } 199 } 200 201 /** @throws IllegalArgumentException if the value is invalid */ 202 @TextLinks.ApplyStrategy checkApplyStrategy(int applyStrategy)203 private static int checkApplyStrategy(int applyStrategy) { 204 if (applyStrategy != TextLinks.APPLY_STRATEGY_IGNORE 205 && applyStrategy != TextLinks.APPLY_STRATEGY_REPLACE) { 206 throw new IllegalArgumentException( 207 "Invalid apply strategy. See TextLinksParams.ApplyStrategy for options."); 208 } 209 return applyStrategy; 210 } 211 } 212 213