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 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 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 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 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 if (!textString.startsWith(textLinks.getText())) { 111 return TextLinks.STATUS_DIFFERENT_TEXT; 112 } 113 if (textLinks.getLinks().isEmpty()) { 114 return TextLinks.STATUS_NO_LINKS_FOUND; 115 } 116 117 int applyCount = 0; 118 for (TextLink link : textLinks.getLinks()) { 119 final TextLinkSpan span = mSpanFactory.apply(link); 120 if (span != null) { 121 final ClickableSpan[] existingSpans = text.getSpans( 122 link.getStart(), link.getEnd(), ClickableSpan.class); 123 if (existingSpans.length > 0) { 124 if (mApplyStrategy == TextLinks.APPLY_STRATEGY_REPLACE) { 125 for (ClickableSpan existingSpan : existingSpans) { 126 text.removeSpan(existingSpan); 127 } 128 text.setSpan(span, link.getStart(), link.getEnd(), 129 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 130 applyCount++; 131 } 132 } else { 133 text.setSpan(span, link.getStart(), link.getEnd(), 134 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 135 applyCount++; 136 } 137 } 138 } 139 if (applyCount == 0) { 140 return TextLinks.STATUS_NO_LINKS_APPLIED; 141 } 142 return TextLinks.STATUS_LINKS_APPLIED; 143 } 144 145 /** 146 * A builder for building TextLinksParams. 147 */ 148 public static final class Builder { 149 150 @TextLinks.ApplyStrategy 151 private int mApplyStrategy = TextLinks.APPLY_STRATEGY_IGNORE; 152 private Function<TextLink, TextLinkSpan> mSpanFactory = DEFAULT_SPAN_FACTORY; 153 154 /** 155 * Sets the apply strategy used to determine how to apply links to text. 156 * e.g {@link TextLinks#APPLY_STRATEGY_IGNORE} 157 * 158 * @return this builder 159 */ 160 public Builder setApplyStrategy(@TextLinks.ApplyStrategy int applyStrategy) { 161 mApplyStrategy = checkApplyStrategy(applyStrategy); 162 return this; 163 } 164 165 /** 166 * Sets a custom span factory for converting TextLinks to TextLinkSpans. 167 * Set to {@code null} to use the default span factory. 168 * 169 * @return this builder 170 */ 171 public Builder setSpanFactory(@Nullable Function<TextLink, TextLinkSpan> spanFactory) { 172 mSpanFactory = spanFactory == null ? DEFAULT_SPAN_FACTORY : spanFactory; 173 return this; 174 } 175 176 /** 177 * Sets the entity configuration used to determine what entity types to generate. 178 * Set to {@code null} for the default entity config which will automatically determine 179 * what links to generate. 180 * 181 * @return this builder 182 */ 183 public Builder setEntityConfig(@Nullable TextClassifier.EntityConfig entityConfig) { 184 return this; 185 } 186 187 /** 188 * Builds and returns a TextLinksParams object. 189 */ 190 public TextLinksParams build() { 191 return new TextLinksParams(mApplyStrategy, mSpanFactory); 192 } 193 } 194 195 /** @throws IllegalArgumentException if the value is invalid */ 196 @TextLinks.ApplyStrategy 197 private static int checkApplyStrategy(int applyStrategy) { 198 if (applyStrategy != TextLinks.APPLY_STRATEGY_IGNORE 199 && applyStrategy != TextLinks.APPLY_STRATEGY_REPLACE) { 200 throw new IllegalArgumentException( 201 "Invalid apply strategy. See TextLinksParams.ApplyStrategy for options."); 202 } 203 return applyStrategy; 204 } 205 } 206 207