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