blob: 9c8148de1e61d0acf1538898cb9e0cc39775b578 [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
2 * Copyright (C) 2007 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.text.util;
18
Siyamed Sinir6eccafd2016-03-02 16:02:38 -080019import android.annotation.IntDef;
20import android.annotation.NonNull;
21import android.annotation.Nullable;
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +000022import android.annotation.UiThread;
Mathew Inwood9cf99fd2018-08-14 15:31:03 +010023import android.annotation.UnsupportedAppUsage;
Roozbeh Pournaderdc8f3ef2017-06-09 16:01:27 -070024import android.content.Context;
Victoria Lease430fc972013-04-01 10:22:00 -070025import android.telephony.PhoneNumberUtils;
Roozbeh Pournaderdc8f3ef2017-06-09 16:01:27 -070026import android.telephony.TelephonyManager;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080027import android.text.Spannable;
28import android.text.SpannableString;
29import android.text.Spanned;
Aurimas Liutikas4037d512016-10-11 17:20:06 -070030import android.text.method.LinkMovementMethod;
31import android.text.method.MovementMethod;
32import android.text.style.URLSpan;
Dianne Hackborn2269d1572010-02-24 19:54:22 -080033import android.util.Patterns;
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +000034import android.view.textclassifier.TextClassifier;
35import android.view.textclassifier.TextLinks;
36import android.view.textclassifier.TextLinks.TextLinkSpan;
Abodunrinwa Toki080c8542018-03-27 00:04:06 +010037import android.view.textclassifier.TextLinksParams;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080038import android.webkit.WebView;
39import android.widget.TextView;
40
Aurimas Liutikas4037d512016-10-11 17:20:06 -070041import com.android.i18n.phonenumbers.PhoneNumberMatch;
42import com.android.i18n.phonenumbers.PhoneNumberUtil;
43import com.android.i18n.phonenumbers.PhoneNumberUtil.Leniency;
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +000044import com.android.internal.util.Preconditions;
Aurimas Liutikas4037d512016-10-11 17:20:06 -070045
46import libcore.util.EmptyArray;
Dan Egnorded0e642009-11-18 11:23:45 -080047
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080048import java.io.UnsupportedEncodingException;
Siyamed Sinir6eccafd2016-03-02 16:02:38 -080049import java.lang.annotation.Retention;
50import java.lang.annotation.RetentionPolicy;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080051import java.net.URLEncoder;
52import java.util.ArrayList;
53import java.util.Collections;
54import java.util.Comparator;
Victoria Lease430fc972013-04-01 10:22:00 -070055import java.util.Locale;
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +000056import java.util.concurrent.CompletableFuture;
57import java.util.concurrent.Executor;
58import java.util.concurrent.Future;
59import java.util.function.Consumer;
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +000060import java.util.function.Supplier;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080061import java.util.regex.Matcher;
62import java.util.regex.Pattern;
63
64/**
65 * Linkify take a piece of text and a regular expression and turns all of the
66 * regex matches in the text into clickable links. This is particularly
Siyamed Sinir6eccafd2016-03-02 16:02:38 -080067 * useful for matching things like email addresses, web URLs, etc. and making
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080068 * them actionable.
69 *
Siyamed Sinir6eccafd2016-03-02 16:02:38 -080070 * Alone with the pattern that is to be matched, a URL scheme prefix is also
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080071 * required. Any pattern match that does not begin with the supplied scheme
Siyamed Sinir6eccafd2016-03-02 16:02:38 -080072 * will have the scheme prepended to the matched text when the clickable URL
73 * is created. For instance, if you are matching web URLs you would supply
74 * the scheme <code>http://</code>. If the pattern matches example.com, which
75 * does not have a URL scheme prefix, the supplied scheme will be prepended to
76 * create <code>http://example.com</code> when the clickable URL link is
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080077 * created.
78 */
79
80public class Linkify {
81 /**
82 * Bit field indicating that web URLs should be matched in methods that
83 * take an options mask
84 */
85 public static final int WEB_URLS = 0x01;
86
87 /**
88 * Bit field indicating that email addresses should be matched in methods
89 * that take an options mask
90 */
91 public static final int EMAIL_ADDRESSES = 0x02;
92
93 /**
94 * Bit field indicating that phone numbers should be matched in methods that
95 * take an options mask
96 */
97 public static final int PHONE_NUMBERS = 0x04;
98
99 /**
100 * Bit field indicating that street addresses should be matched in methods that
Roozbeh Pournader1ff71962015-02-13 17:08:27 -0800101 * take an options mask. Note that this uses the
102 * {@link android.webkit.WebView#findAddress(String) findAddress()} method in
103 * {@link android.webkit.WebView} for finding addresses, which has various
Jan Althaus003889a2018-04-04 15:29:10 +0200104 * limitations and has been deprecated.
105 * @deprecated use {@link android.view.textclassifier.TextClassifier#generateLinks(
106 * TextLinks.Request)} instead and avoid it even when targeting API levels where no alternative
107 * is available.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800108 */
Jan Althaus003889a2018-04-04 15:29:10 +0200109 @Deprecated
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800110 public static final int MAP_ADDRESSES = 0x08;
111
112 /**
113 * Bit mask indicating that all available patterns should be matched in
114 * methods that take an options mask
Jan Althaus003889a2018-04-04 15:29:10 +0200115 * <p><strong>Note:</strong></p> {@link #MAP_ADDRESSES} is deprecated.
116 * Use {@link android.view.textclassifier.TextClassifier#generateLinks(TextLinks.Request)}
117 * instead and avoid it even when targeting API levels where no alternative is available.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800118 */
119 public static final int ALL = WEB_URLS | EMAIL_ADDRESSES | PHONE_NUMBERS | MAP_ADDRESSES;
120
121 /**
122 * Don't treat anything with fewer than this many digits as a
123 * phone number.
124 */
125 private static final int PHONE_NUMBER_MINIMUM_DIGITS = 5;
126
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800127 /** @hide */
128 @IntDef(flag = true, value = { WEB_URLS, EMAIL_ADDRESSES, PHONE_NUMBERS, MAP_ADDRESSES, ALL })
129 @Retention(RetentionPolicy.SOURCE)
130 public @interface LinkifyMask {}
131
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800132 /**
133 * Filters out web URL matches that occur after an at-sign (@). This is
134 * to prevent turning the domain name in an email address into a web link.
135 */
136 public static final MatchFilter sUrlMatchFilter = new MatchFilter() {
137 public final boolean acceptMatch(CharSequence s, int start, int end) {
138 if (start == 0) {
139 return true;
140 }
141
142 if (s.charAt(start - 1) == '@') {
143 return false;
144 }
145
146 return true;
147 }
148 };
149
150 /**
151 * Filters out URL matches that don't have enough digits to be a
152 * phone number.
153 */
154 public static final MatchFilter sPhoneNumberMatchFilter = new MatchFilter() {
155 public final boolean acceptMatch(CharSequence s, int start, int end) {
156 int digitCount = 0;
157
158 for (int i = start; i < end; i++) {
159 if (Character.isDigit(s.charAt(i))) {
160 digitCount++;
161 if (digitCount >= PHONE_NUMBER_MINIMUM_DIGITS) {
162 return true;
163 }
164 }
165 }
166 return false;
167 }
168 };
169
170 /**
171 * Transforms matched phone number text into something suitable
172 * to be used in a tel: URL. It does this by removing everything
173 * but the digits and plus signs. For instance:
174 * &apos;+1 (919) 555-1212&apos;
175 * becomes &apos;+19195551212&apos;
176 */
177 public static final TransformFilter sPhoneNumberTransformFilter = new TransformFilter() {
178 public final String transformUrl(final Matcher match, String url) {
Dan Egnorded0e642009-11-18 11:23:45 -0800179 return Patterns.digitsAndPlusOnly(match);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800180 }
181 };
182
183 /**
184 * MatchFilter enables client code to have more control over
185 * what is allowed to match and become a link, and what is not.
186 *
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800187 * For example: when matching web URLs you would like things like
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800188 * http://www.example.com to match, as well as just example.com itelf.
189 * However, you would not want to match against the domain in
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800190 * support@example.com. So, when matching against a web URL pattern you
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800191 * might also include a MatchFilter that disallows the match if it is
192 * immediately preceded by an at-sign (@).
193 */
194 public interface MatchFilter {
195 /**
196 * Examines the character span matched by the pattern and determines
197 * if the match should be turned into an actionable link.
198 *
199 * @param s The body of text against which the pattern
200 * was matched
201 * @param start The index of the first character in s that was
202 * matched by the pattern - inclusive
203 * @param end The index of the last character in s that was
204 * matched - exclusive
205 *
206 * @return Whether this match should be turned into a link
207 */
208 boolean acceptMatch(CharSequence s, int start, int end);
209 }
210
211 /**
212 * TransformFilter enables client code to have more control over
213 * how matched patterns are represented as URLs.
214 *
215 * For example: when converting a phone number such as (919) 555-1212
216 * into a tel: URL the parentheses, white space, and hyphen need to be
217 * removed to produce tel:9195551212.
218 */
219 public interface TransformFilter {
220 /**
221 * Examines the matched text and either passes it through or uses the
222 * data in the Matcher state to produce a replacement.
223 *
224 * @param match The regex matcher state that found this URL text
225 * @param url The text that was matched
226 *
227 * @return The transformed form of the URL
228 */
229 String transformUrl(final Matcher match, String url);
230 }
231
232 /**
233 * Scans the text of the provided Spannable and turns all occurrences
234 * of the link types indicated in the mask into clickable links.
235 * If the mask is nonzero, it also removes any existing URLSpans
236 * attached to the Spannable, to avoid problems if you call it
237 * repeatedly on the same text.
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800238 *
239 * @param text Spannable whose text is to be marked-up with links
240 * @param mask Mask to define which kinds of links will be searched.
241 *
242 * @return True if at least one link is found and applied.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800243 */
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800244 public static final boolean addLinks(@NonNull Spannable text, @LinkifyMask int mask) {
Roozbeh Pournaderdc8f3ef2017-06-09 16:01:27 -0700245 return addLinks(text, mask, null);
246 }
247
248 private static boolean addLinks(@NonNull Spannable text, @LinkifyMask int mask,
249 @Nullable Context context) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800250 if (mask == 0) {
251 return false;
252 }
253
254 URLSpan[] old = text.getSpans(0, text.length(), URLSpan.class);
255
256 for (int i = old.length - 1; i >= 0; i--) {
257 text.removeSpan(old[i]);
258 }
259
260 ArrayList<LinkSpec> links = new ArrayList<LinkSpec>();
261
262 if ((mask & WEB_URLS) != 0) {
Siyamed Sinir840385c2015-11-18 15:02:57 -0800263 gatherLinks(links, text, Patterns.AUTOLINK_WEB_URL,
Eric Fischer7a4be892009-09-14 10:24:24 -0700264 new String[] { "http://", "https://", "rtsp://" },
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800265 sUrlMatchFilter, null);
266 }
267
268 if ((mask & EMAIL_ADDRESSES) != 0) {
Siyamed Sinir0716d5f2016-02-25 15:54:35 -0800269 gatherLinks(links, text, Patterns.AUTOLINK_EMAIL_ADDRESS,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800270 new String[] { "mailto:" },
271 null, null);
272 }
273
274 if ((mask & PHONE_NUMBERS) != 0) {
Roozbeh Pournaderdc8f3ef2017-06-09 16:01:27 -0700275 gatherTelLinks(links, text, context);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800276 }
277
278 if ((mask & MAP_ADDRESSES) != 0) {
279 gatherMapLinks(links, text);
280 }
281
282 pruneOverlaps(links);
283
284 if (links.size() == 0) {
285 return false;
286 }
287
288 for (LinkSpec link: links) {
289 applyLink(link.url, link.start, link.end, text);
290 }
291
292 return true;
293 }
294
295 /**
296 * Scans the text of the provided TextView and turns all occurrences of
297 * the link types indicated in the mask into clickable links. If matches
298 * are found the movement method for the TextView is set to
299 * LinkMovementMethod.
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800300 *
301 * @param text TextView whose text is to be marked-up with links
302 * @param mask Mask to define which kinds of links will be searched.
303 *
304 * @return True if at least one link is found and applied.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800305 */
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800306 public static final boolean addLinks(@NonNull TextView text, @LinkifyMask int mask) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800307 if (mask == 0) {
308 return false;
309 }
310
Roozbeh Pournaderdc8f3ef2017-06-09 16:01:27 -0700311 final Context context = text.getContext();
312 final CharSequence t = text.getText();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800313 if (t instanceof Spannable) {
Roozbeh Pournaderdc8f3ef2017-06-09 16:01:27 -0700314 if (addLinks((Spannable) t, mask, context)) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800315 addLinkMovementMethod(text);
316 return true;
317 }
318
319 return false;
320 } else {
321 SpannableString s = SpannableString.valueOf(t);
322
Roozbeh Pournaderdc8f3ef2017-06-09 16:01:27 -0700323 if (addLinks(s, mask, context)) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800324 addLinkMovementMethod(text);
325 text.setText(s);
326
327 return true;
328 }
329
330 return false;
331 }
332 }
333
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800334 private static final void addLinkMovementMethod(@NonNull TextView t) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800335 MovementMethod m = t.getMovementMethod();
336
337 if ((m == null) || !(m instanceof LinkMovementMethod)) {
338 if (t.getLinksClickable()) {
339 t.setMovementMethod(LinkMovementMethod.getInstance());
340 }
341 }
342 }
343
344 /**
345 * Applies a regex to the text of a TextView turning the matches into
346 * links. If links are found then UrlSpans are applied to the link
347 * text match areas, and the movement method for the text is changed
348 * to LinkMovementMethod.
349 *
350 * @param text TextView whose text is to be marked-up with links
351 * @param pattern Regex pattern to be used for finding links
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800352 * @param scheme URL scheme string (eg <code>http://</code>) to be
353 * prepended to the links that do not start with this scheme.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800354 */
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800355 public static final void addLinks(@NonNull TextView text, @NonNull Pattern pattern,
356 @Nullable String scheme) {
357 addLinks(text, pattern, scheme, null, null, null);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800358 }
359
360 /**
361 * Applies a regex to the text of a TextView turning the matches into
362 * links. If links are found then UrlSpans are applied to the link
363 * text match areas, and the movement method for the text is changed
364 * to LinkMovementMethod.
365 *
366 * @param text TextView whose text is to be marked-up with links
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800367 * @param pattern Regex pattern to be used for finding links
368 * @param scheme URL scheme string (eg <code>http://</code>) to be
369 * prepended to the links that do not start with this scheme.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800370 * @param matchFilter The filter that is used to allow the client code
371 * additional control over which pattern matches are
372 * to be converted into links.
373 */
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800374 public static final void addLinks(@NonNull TextView text, @NonNull Pattern pattern,
375 @Nullable String scheme, @Nullable MatchFilter matchFilter,
376 @Nullable TransformFilter transformFilter) {
377 addLinks(text, pattern, scheme, null, matchFilter, transformFilter);
378 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800379
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800380 /**
381 * Applies a regex to the text of a TextView turning the matches into
382 * links. If links are found then UrlSpans are applied to the link
383 * text match areas, and the movement method for the text is changed
384 * to LinkMovementMethod.
385 *
386 * @param text TextView whose text is to be marked-up with links.
387 * @param pattern Regex pattern to be used for finding links.
388 * @param defaultScheme The default scheme to be prepended to links if the link does not
389 * start with one of the <code>schemes</code> given.
390 * @param schemes Array of schemes (eg <code>http://</code>) to check if the link found
391 * contains a scheme. Passing a null or empty value means prepend defaultScheme
392 * to all links.
393 * @param matchFilter The filter that is used to allow the client code additional control
394 * over which pattern matches are to be converted into links.
395 * @param transformFilter Filter to allow the client code to update the link found.
396 */
397 public static final void addLinks(@NonNull TextView text, @NonNull Pattern pattern,
398 @Nullable String defaultScheme, @Nullable String[] schemes,
399 @Nullable MatchFilter matchFilter, @Nullable TransformFilter transformFilter) {
400 SpannableString spannable = SpannableString.valueOf(text.getText());
401
402 boolean linksAdded = addLinks(spannable, pattern, defaultScheme, schemes, matchFilter,
403 transformFilter);
404 if (linksAdded) {
405 text.setText(spannable);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800406 addLinkMovementMethod(text);
407 }
408 }
409
410 /**
411 * Applies a regex to a Spannable turning the matches into
412 * links.
413 *
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800414 * @param text Spannable whose text is to be marked-up with links
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800415 * @param pattern Regex pattern to be used for finding links
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800416 * @param scheme URL scheme string (eg <code>http://</code>) to be
417 * prepended to the links that do not start with this scheme.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800418 */
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800419 public static final boolean addLinks(@NonNull Spannable text, @NonNull Pattern pattern,
420 @Nullable String scheme) {
421 return addLinks(text, pattern, scheme, null, null, null);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800422 }
423
424 /**
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800425 * Applies a regex to a Spannable turning the matches into
426 * links.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800427 *
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800428 * @param spannable Spannable whose text is to be marked-up with links
429 * @param pattern Regex pattern to be used for finding links
430 * @param scheme URL scheme string (eg <code>http://</code>) to be
431 * prepended to the links that do not start with this scheme.
432 * @param matchFilter The filter that is used to allow the client code
433 * additional control over which pattern matches are
434 * to be converted into links.
435 * @param transformFilter Filter to allow the client code to update the link found.
436 *
437 * @return True if at least one link is found and applied.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800438 */
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800439 public static final boolean addLinks(@NonNull Spannable spannable, @NonNull Pattern pattern,
440 @Nullable String scheme, @Nullable MatchFilter matchFilter,
441 @Nullable TransformFilter transformFilter) {
442 return addLinks(spannable, pattern, scheme, null, matchFilter,
443 transformFilter);
444 }
445
446 /**
447 * Applies a regex to a Spannable turning the matches into links.
448 *
449 * @param spannable Spannable whose text is to be marked-up with links.
450 * @param pattern Regex pattern to be used for finding links.
451 * @param defaultScheme The default scheme to be prepended to links if the link does not
452 * start with one of the <code>schemes</code> given.
453 * @param schemes Array of schemes (eg <code>http://</code>) to check if the link found
454 * contains a scheme. Passing a null or empty value means prepend defaultScheme
455 * to all links.
456 * @param matchFilter The filter that is used to allow the client code additional control
457 * over which pattern matches are to be converted into links.
458 * @param transformFilter Filter to allow the client code to update the link found.
459 *
460 * @return True if at least one link is found and applied.
461 */
462 public static final boolean addLinks(@NonNull Spannable spannable, @NonNull Pattern pattern,
463 @Nullable String defaultScheme, @Nullable String[] schemes,
464 @Nullable MatchFilter matchFilter, @Nullable TransformFilter transformFilter) {
465 final String[] schemesCopy;
466 if (defaultScheme == null) defaultScheme = "";
467 if (schemes == null || schemes.length < 1) {
468 schemes = EmptyArray.STRING;
469 }
470
471 schemesCopy = new String[schemes.length + 1];
472 schemesCopy[0] = defaultScheme.toLowerCase(Locale.ROOT);
473 for (int index = 0; index < schemes.length; index++) {
474 String scheme = schemes[index];
475 schemesCopy[index + 1] = (scheme == null) ? "" : scheme.toLowerCase(Locale.ROOT);
476 }
477
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800478 boolean hasMatches = false;
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800479 Matcher m = pattern.matcher(spannable);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800480
481 while (m.find()) {
482 int start = m.start();
483 int end = m.end();
484 boolean allowed = true;
485
486 if (matchFilter != null) {
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800487 allowed = matchFilter.acceptMatch(spannable, start, end);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800488 }
489
490 if (allowed) {
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800491 String url = makeUrl(m.group(0), schemesCopy, m, transformFilter);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800492
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800493 applyLink(url, start, end, spannable);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800494 hasMatches = true;
495 }
496 }
497
498 return hasMatches;
499 }
500
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000501 /**
502 * Scans the text of the provided TextView and turns all occurrences of the entity types
503 * specified by {@code options} into clickable links. If links are found, this method
504 * removes any pre-existing {@link TextLinkSpan} attached to the text (to avoid
505 * problems if you call it repeatedly on the same text) and sets the movement method for the
506 * TextView to LinkMovementMethod.
507 *
508 * <p><strong>Note:</strong> This method returns immediately but generates the links with
509 * the specified classifier on a background thread. The generated links are applied on the
510 * calling thread.
511 *
512 * @param textView TextView whose text is to be marked-up with links
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100513 * @param params optional parameters to specify how to generate the links
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000514 *
515 * @return a future that may be used to interrupt or query the background task
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100516 * @hide
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000517 */
518 @UiThread
519 public static Future<Void> addLinksAsync(
520 @NonNull TextView textView,
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100521 @Nullable TextLinksParams params) {
522 return addLinksAsync(textView, params, null /* executor */, null /* callback */);
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000523 }
524
525 /**
526 * Scans the text of the provided TextView and turns all occurrences of the entity types
527 * specified by {@code options} into clickable links. If links are found, this method
528 * removes any pre-existing {@link TextLinkSpan} attached to the text (to avoid
529 * problems if you call it repeatedly on the same text) and sets the movement method for the
530 * TextView to LinkMovementMethod.
531 *
532 * <p><strong>Note:</strong> This method returns immediately but generates the links with
533 * the specified classifier on a background thread. The generated links are applied on the
534 * calling thread.
535 *
536 * @param textView TextView whose text is to be marked-up with links
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100537 * @param mask mask to define which kinds of links will be generated
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000538 *
539 * @return a future that may be used to interrupt or query the background task
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100540 * @hide
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000541 */
542 @UiThread
543 public static Future<Void> addLinksAsync(
544 @NonNull TextView textView,
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100545 @LinkifyMask int mask) {
546 return addLinksAsync(textView, TextLinksParams.fromLinkMask(mask),
547 null /* executor */, null /* callback */);
548 }
549
550 /**
551 * Scans the text of the provided TextView and turns all occurrences of the entity types
552 * specified by {@code options} into clickable links. If links are found, this method
553 * removes any pre-existing {@link TextLinkSpan} attached to the text (to avoid
554 * problems if you call it repeatedly on the same text) and sets the movement method for the
555 * TextView to LinkMovementMethod.
556 *
557 * <p><strong>Note:</strong> This method returns immediately but generates the links with
558 * the specified classifier on a background thread. The generated links are applied on the
559 * calling thread.
560 *
561 * @param textView TextView whose text is to be marked-up with links
562 * @param params optional parameters to specify how to generate the links
563 * @param executor Executor that runs the background task
564 * @param callback Callback that receives the final status of the background task execution
565 *
566 * @return a future that may be used to interrupt or query the background task
567 * @hide
568 */
569 @UiThread
570 public static Future<Void> addLinksAsync(
571 @NonNull TextView textView,
572 @Nullable TextLinksParams params,
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000573 @Nullable Executor executor,
574 @Nullable Consumer<Integer> callback) {
575 Preconditions.checkNotNull(textView);
576 final CharSequence text = textView.getText();
577 final Spannable spannable = (text instanceof Spannable)
578 ? (Spannable) text : SpannableString.valueOf(text);
579 final Runnable modifyTextView = () -> {
580 addLinkMovementMethod(textView);
581 if (spannable != text) {
582 textView.setText(spannable);
583 }
584 };
585 return addLinksAsync(spannable, textView.getTextClassifier(),
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100586 params, executor, callback, modifyTextView);
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000587 }
588
589 /**
590 * Scans the text of the provided TextView and turns all occurrences of the entity types
591 * specified by {@code options} into clickable links. If links are found, this method
592 * removes any pre-existing {@link TextLinkSpan} attached to the text to avoid
593 * problems if you call it repeatedly on the same text.
594 *
595 * <p><strong>Note:</strong> This method returns immediately but generates the links with
596 * the specified classifier on a background thread. The generated links are applied on the
597 * calling thread.
598 *
599 * <p><strong>Note:</strong> If the text is currently attached to a TextView, this method
600 * should be called on the UI thread.
601 *
602 * @param text Spannable whose text is to be marked-up with links
603 * @param classifier the TextClassifier to use to generate the links
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100604 * @param params optional parameters to specify how to generate the links
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000605 *
606 * @return a future that may be used to interrupt or query the background task
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100607 * @hide
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000608 */
609 public static Future<Void> addLinksAsync(
610 @NonNull Spannable text,
611 @NonNull TextClassifier classifier,
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100612 @Nullable TextLinksParams params) {
613 return addLinksAsync(text, classifier, params, null /* executor */, null /* callback */);
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000614 }
615
616 /**
617 * Scans the text of the provided TextView and turns all occurrences of the entity types
618 * specified by the link {@code mask} into clickable links. If links are found, this method
619 * removes any pre-existing {@link TextLinkSpan} attached to the text to avoid
620 * problems if you call it repeatedly on the same text.
621 *
622 * <p><strong>Note:</strong> This method returns immediately but generates the links with
623 * the specified classifier on a background thread. The generated links are applied on the
624 * calling thread.
625 *
626 * <p><strong>Note:</strong> If the text is currently attached to a TextView, this method
627 * should be called on the UI thread.
628 *
629 * @param text Spannable whose text is to be marked-up with links
630 * @param classifier the TextClassifier to use to generate the links
631 * @param mask mask to define which kinds of links will be generated
632 *
633 * @return a future that may be used to interrupt or query the background task
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100634 * @hide
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000635 */
636 public static Future<Void> addLinksAsync(
637 @NonNull Spannable text,
638 @NonNull TextClassifier classifier,
639 @LinkifyMask int mask) {
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100640 return addLinksAsync(text, classifier, TextLinksParams.fromLinkMask(mask),
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000641 null /* executor */, null /* callback */);
642 }
643
644 /**
645 * Scans the text of the provided TextView and turns all occurrences of the entity types
646 * specified by {@code options} into clickable links. If links are found, this method
647 * removes any pre-existing {@link TextLinkSpan} attached to the text to avoid
648 * problems if you call it repeatedly on the same text.
649 *
650 * <p><strong>Note:</strong> This method returns immediately but generates the links with
651 * the specified classifier on a background thread. The generated links are applied on the
652 * calling thread.
653 *
654 * <p><strong>Note:</strong> If the text is currently attached to a TextView, this method
655 * should be called on the UI thread.
656 *
657 * @param text Spannable whose text is to be marked-up with links
658 * @param classifier the TextClassifier to use to generate the links
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100659 * @param params optional parameters to specify how to generate the links
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000660 * @param executor Executor that runs the background task
661 * @param callback Callback that receives the final status of the background task execution
662 *
663 * @return a future that may be used to interrupt or query the background task
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100664 * @hide
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000665 */
666 public static Future<Void> addLinksAsync(
667 @NonNull Spannable text,
668 @NonNull TextClassifier classifier,
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100669 @Nullable TextLinksParams params,
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000670 @Nullable Executor executor,
671 @Nullable Consumer<Integer> callback) {
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100672 return addLinksAsync(text, classifier, params, executor, callback,
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000673 null /* modifyTextView */);
674 }
675
676 private static Future<Void> addLinksAsync(
677 @NonNull Spannable text,
678 @NonNull TextClassifier classifier,
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100679 @Nullable TextLinksParams params,
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000680 @Nullable Executor executor,
681 @Nullable Consumer<Integer> callback,
682 @Nullable Runnable modifyTextView) {
683 Preconditions.checkNotNull(text);
684 Preconditions.checkNotNull(classifier);
Jan Althaus108aad32018-01-30 15:26:55 +0100685
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100686 // TODO: This is a bug. We shouldnot call getMaxGenerateLinksTextLength() on the UI thread.
Jan Althaus108aad32018-01-30 15:26:55 +0100687 // The input text may exceed the maximum length the text classifier can handle. In such
688 // cases, we process the text up to the maximum length.
689 final CharSequence truncatedText = text.subSequence(
690 0, Math.min(text.length(), classifier.getMaxGenerateLinksTextLength()));
691
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100692 final TextClassifier.EntityConfig entityConfig = (params == null)
693 ? null : params.getEntityConfig();
694 final TextLinks.Request request = new TextLinks.Request.Builder(truncatedText)
695 .setLegacyFallback(true)
696 .setEntityConfig(entityConfig)
697 .build();
698 final Supplier<TextLinks> supplier = () -> classifier.generateLinks(request);
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000699 final Consumer<TextLinks> consumer = links -> {
700 if (links.getLinks().isEmpty()) {
701 if (callback != null) {
702 callback.accept(TextLinks.STATUS_NO_LINKS_FOUND);
703 }
704 return;
705 }
706
Jan Althaus108aad32018-01-30 15:26:55 +0100707 // Remove spans only for the part of the text we generated links for.
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100708 final TextLinkSpan[] old =
709 text.getSpans(0, truncatedText.length(), TextLinkSpan.class);
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000710 for (int i = old.length - 1; i >= 0; i--) {
711 text.removeSpan(old[i]);
712 }
713
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100714 final @TextLinks.Status int result = params.apply(text, links);
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000715 if (result == TextLinks.STATUS_LINKS_APPLIED) {
716 if (modifyTextView != null) {
717 modifyTextView.run();
718 }
719 }
720 if (callback != null) {
721 callback.accept(result);
722 }
723 };
724 if (executor == null) {
725 return CompletableFuture.supplyAsync(supplier).thenAccept(consumer);
726 } else {
727 return CompletableFuture.supplyAsync(supplier, executor).thenAccept(consumer);
728 }
729 }
730
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800731 private static final void applyLink(String url, int start, int end, Spannable text) {
732 URLSpan span = new URLSpan(url);
733
734 text.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
735 }
736
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800737 private static final String makeUrl(@NonNull String url, @NonNull String[] prefixes,
738 Matcher matcher, @Nullable TransformFilter filter) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800739 if (filter != null) {
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800740 url = filter.transformUrl(matcher, url);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800741 }
742
743 boolean hasPrefix = false;
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800744
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800745 for (int i = 0; i < prefixes.length; i++) {
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800746 if (url.regionMatches(true, 0, prefixes[i], 0, prefixes[i].length())) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800747 hasPrefix = true;
748
749 // Fix capitalization if necessary
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800750 if (!url.regionMatches(false, 0, prefixes[i], 0, prefixes[i].length())) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800751 url = prefixes[i] + url.substring(prefixes[i].length());
752 }
753
754 break;
755 }
756 }
757
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800758 if (!hasPrefix && prefixes.length > 0) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800759 url = prefixes[0] + url;
760 }
761
762 return url;
763 }
764
765 private static final void gatherLinks(ArrayList<LinkSpec> links,
766 Spannable s, Pattern pattern, String[] schemes,
767 MatchFilter matchFilter, TransformFilter transformFilter) {
768 Matcher m = pattern.matcher(s);
769
770 while (m.find()) {
771 int start = m.start();
772 int end = m.end();
773
774 if (matchFilter == null || matchFilter.acceptMatch(s, start, end)) {
775 LinkSpec spec = new LinkSpec();
776 String url = makeUrl(m.group(0), schemes, m, transformFilter);
777
778 spec.url = url;
779 spec.start = start;
780 spec.end = end;
781
782 links.add(spec);
783 }
784 }
785 }
786
Mathew Inwood9cf99fd2018-08-14 15:31:03 +0100787 @UnsupportedAppUsage
Roozbeh Pournaderdc8f3ef2017-06-09 16:01:27 -0700788 private static void gatherTelLinks(ArrayList<LinkSpec> links, Spannable s,
789 @Nullable Context context) {
Victoria Lease430fc972013-04-01 10:22:00 -0700790 PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();
Roozbeh Pournaderdc8f3ef2017-06-09 16:01:27 -0700791 final TelephonyManager tm = (context == null)
792 ? TelephonyManager.getDefault()
793 : TelephonyManager.from(context);
Victoria Lease430fc972013-04-01 10:22:00 -0700794 Iterable<PhoneNumberMatch> matches = phoneUtil.findNumbers(s.toString(),
Roozbeh Pournaderdc8f3ef2017-06-09 16:01:27 -0700795 tm.getSimCountryIso().toUpperCase(Locale.US),
796 Leniency.POSSIBLE, Long.MAX_VALUE);
Victoria Lease430fc972013-04-01 10:22:00 -0700797 for (PhoneNumberMatch match : matches) {
798 LinkSpec spec = new LinkSpec();
799 spec.url = "tel:" + PhoneNumberUtils.normalizeNumber(match.rawString());
800 spec.start = match.start();
801 spec.end = match.end();
802 links.add(spec);
803 }
804 }
805
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800806 private static final void gatherMapLinks(ArrayList<LinkSpec> links, Spannable s) {
807 String string = s.toString();
808 String address;
809 int base = 0;
810
Aaron Whyte83955cb2014-06-06 16:41:38 -0700811 try {
812 while ((address = WebView.findAddress(string)) != null) {
813 int start = string.indexOf(address);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800814
Aaron Whyte83955cb2014-06-06 16:41:38 -0700815 if (start < 0) {
816 break;
817 }
818
819 LinkSpec spec = new LinkSpec();
820 int length = address.length();
821 int end = start + length;
822
823 spec.start = base + start;
824 spec.end = base + end;
825 string = string.substring(end);
826 base += end;
827
828 String encodedAddress = null;
829
830 try {
831 encodedAddress = URLEncoder.encode(address,"UTF-8");
832 } catch (UnsupportedEncodingException e) {
833 continue;
834 }
835
836 spec.url = "geo:0,0?q=" + encodedAddress;
837 links.add(spec);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800838 }
Aaron Whyte83955cb2014-06-06 16:41:38 -0700839 } catch (UnsupportedOperationException e) {
840 // findAddress may fail with an unsupported exception on platforms without a WebView.
841 // In this case, we will not append anything to the links variable: it would have died
842 // in WebView.findAddress.
843 return;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800844 }
845 }
846
847 private static final void pruneOverlaps(ArrayList<LinkSpec> links) {
848 Comparator<LinkSpec> c = new Comparator<LinkSpec>() {
849 public final int compare(LinkSpec a, LinkSpec b) {
850 if (a.start < b.start) {
851 return -1;
852 }
853
854 if (a.start > b.start) {
855 return 1;
856 }
857
858 if (a.end < b.end) {
859 return 1;
860 }
861
862 if (a.end > b.end) {
863 return -1;
864 }
865
866 return 0;
867 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800868 };
869
870 Collections.sort(links, c);
871
872 int len = links.size();
873 int i = 0;
874
875 while (i < len - 1) {
876 LinkSpec a = links.get(i);
877 LinkSpec b = links.get(i + 1);
878 int remove = -1;
879
880 if ((a.start <= b.start) && (a.end > b.start)) {
881 if (b.end <= a.end) {
882 remove = i + 1;
883 } else if ((a.end - a.start) > (b.end - b.start)) {
884 remove = i + 1;
885 } else if ((a.end - a.start) < (b.end - b.start)) {
886 remove = i;
887 }
888
889 if (remove != -1) {
890 links.remove(remove);
891 len--;
892 continue;
893 }
894
895 }
896
897 i++;
898 }
899 }
900}
901
902class LinkSpec {
903 String url;
904 int start;
905 int end;
906}