blob: 6a91191967442516c2ef849fc40a40ccdae94326 [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;
Abodunrinwa Tokia69950c2018-11-29 13:51:56 +000033import android.util.Log;
Dianne Hackborn2269d1572010-02-24 19:54:22 -080034import android.util.Patterns;
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +000035import android.view.textclassifier.TextClassifier;
36import android.view.textclassifier.TextLinks;
37import android.view.textclassifier.TextLinks.TextLinkSpan;
Abodunrinwa Toki080c8542018-03-27 00:04:06 +010038import android.view.textclassifier.TextLinksParams;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080039import android.webkit.WebView;
40import android.widget.TextView;
41
Aurimas Liutikas4037d512016-10-11 17:20:06 -070042import com.android.i18n.phonenumbers.PhoneNumberMatch;
43import com.android.i18n.phonenumbers.PhoneNumberUtil;
44import com.android.i18n.phonenumbers.PhoneNumberUtil.Leniency;
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +000045import com.android.internal.util.Preconditions;
Aurimas Liutikas4037d512016-10-11 17:20:06 -070046
47import libcore.util.EmptyArray;
Dan Egnorded0e642009-11-18 11:23:45 -080048
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080049import java.io.UnsupportedEncodingException;
Siyamed Sinir6eccafd2016-03-02 16:02:38 -080050import java.lang.annotation.Retention;
51import java.lang.annotation.RetentionPolicy;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080052import java.net.URLEncoder;
53import java.util.ArrayList;
54import java.util.Collections;
55import java.util.Comparator;
Victoria Lease430fc972013-04-01 10:22:00 -070056import java.util.Locale;
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +000057import java.util.concurrent.CompletableFuture;
58import java.util.concurrent.Executor;
59import java.util.concurrent.Future;
60import java.util.function.Consumer;
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +000061import java.util.function.Supplier;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080062import java.util.regex.Matcher;
63import java.util.regex.Pattern;
64
65/**
66 * Linkify take a piece of text and a regular expression and turns all of the
67 * regex matches in the text into clickable links. This is particularly
Siyamed Sinir6eccafd2016-03-02 16:02:38 -080068 * useful for matching things like email addresses, web URLs, etc. and making
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080069 * them actionable.
70 *
Siyamed Sinir6eccafd2016-03-02 16:02:38 -080071 * 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 -080072 * required. Any pattern match that does not begin with the supplied scheme
Siyamed Sinir6eccafd2016-03-02 16:02:38 -080073 * will have the scheme prepended to the matched text when the clickable URL
74 * is created. For instance, if you are matching web URLs you would supply
75 * the scheme <code>http://</code>. If the pattern matches example.com, which
76 * does not have a URL scheme prefix, the supplied scheme will be prepended to
77 * create <code>http://example.com</code> when the clickable URL link is
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080078 * created.
79 */
80
81public class Linkify {
Abodunrinwa Tokia69950c2018-11-29 13:51:56 +000082
83 private static final String LOG_TAG = "Linkify";
84
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080085 /**
86 * Bit field indicating that web URLs should be matched in methods that
87 * take an options mask
88 */
89 public static final int WEB_URLS = 0x01;
90
91 /**
92 * Bit field indicating that email addresses should be matched in methods
93 * that take an options mask
94 */
95 public static final int EMAIL_ADDRESSES = 0x02;
96
97 /**
98 * Bit field indicating that phone numbers should be matched in methods that
99 * take an options mask
100 */
101 public static final int PHONE_NUMBERS = 0x04;
102
103 /**
104 * Bit field indicating that street addresses should be matched in methods that
Roozbeh Pournader1ff71962015-02-13 17:08:27 -0800105 * take an options mask. Note that this uses the
106 * {@link android.webkit.WebView#findAddress(String) findAddress()} method in
107 * {@link android.webkit.WebView} for finding addresses, which has various
Jan Althaus003889a2018-04-04 15:29:10 +0200108 * limitations and has been deprecated.
109 * @deprecated use {@link android.view.textclassifier.TextClassifier#generateLinks(
110 * TextLinks.Request)} instead and avoid it even when targeting API levels where no alternative
111 * is available.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800112 */
Jan Althaus003889a2018-04-04 15:29:10 +0200113 @Deprecated
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800114 public static final int MAP_ADDRESSES = 0x08;
115
116 /**
117 * Bit mask indicating that all available patterns should be matched in
118 * methods that take an options mask
Jan Althaus003889a2018-04-04 15:29:10 +0200119 * <p><strong>Note:</strong></p> {@link #MAP_ADDRESSES} is deprecated.
120 * Use {@link android.view.textclassifier.TextClassifier#generateLinks(TextLinks.Request)}
121 * instead and avoid it even when targeting API levels where no alternative is available.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800122 */
123 public static final int ALL = WEB_URLS | EMAIL_ADDRESSES | PHONE_NUMBERS | MAP_ADDRESSES;
124
125 /**
126 * Don't treat anything with fewer than this many digits as a
127 * phone number.
128 */
129 private static final int PHONE_NUMBER_MINIMUM_DIGITS = 5;
130
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800131 /** @hide */
132 @IntDef(flag = true, value = { WEB_URLS, EMAIL_ADDRESSES, PHONE_NUMBERS, MAP_ADDRESSES, ALL })
133 @Retention(RetentionPolicy.SOURCE)
134 public @interface LinkifyMask {}
135
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800136 /**
137 * Filters out web URL matches that occur after an at-sign (@). This is
138 * to prevent turning the domain name in an email address into a web link.
139 */
140 public static final MatchFilter sUrlMatchFilter = new MatchFilter() {
141 public final boolean acceptMatch(CharSequence s, int start, int end) {
142 if (start == 0) {
143 return true;
144 }
145
146 if (s.charAt(start - 1) == '@') {
147 return false;
148 }
149
150 return true;
151 }
152 };
153
154 /**
155 * Filters out URL matches that don't have enough digits to be a
156 * phone number.
157 */
158 public static final MatchFilter sPhoneNumberMatchFilter = new MatchFilter() {
159 public final boolean acceptMatch(CharSequence s, int start, int end) {
160 int digitCount = 0;
161
162 for (int i = start; i < end; i++) {
163 if (Character.isDigit(s.charAt(i))) {
164 digitCount++;
165 if (digitCount >= PHONE_NUMBER_MINIMUM_DIGITS) {
166 return true;
167 }
168 }
169 }
170 return false;
171 }
172 };
173
174 /**
175 * Transforms matched phone number text into something suitable
176 * to be used in a tel: URL. It does this by removing everything
177 * but the digits and plus signs. For instance:
178 * &apos;+1 (919) 555-1212&apos;
179 * becomes &apos;+19195551212&apos;
180 */
181 public static final TransformFilter sPhoneNumberTransformFilter = new TransformFilter() {
182 public final String transformUrl(final Matcher match, String url) {
Dan Egnorded0e642009-11-18 11:23:45 -0800183 return Patterns.digitsAndPlusOnly(match);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800184 }
185 };
186
187 /**
188 * MatchFilter enables client code to have more control over
189 * what is allowed to match and become a link, and what is not.
190 *
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800191 * For example: when matching web URLs you would like things like
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800192 * http://www.example.com to match, as well as just example.com itelf.
193 * However, you would not want to match against the domain in
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800194 * support@example.com. So, when matching against a web URL pattern you
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800195 * might also include a MatchFilter that disallows the match if it is
196 * immediately preceded by an at-sign (@).
197 */
198 public interface MatchFilter {
199 /**
200 * Examines the character span matched by the pattern and determines
201 * if the match should be turned into an actionable link.
202 *
203 * @param s The body of text against which the pattern
204 * was matched
205 * @param start The index of the first character in s that was
206 * matched by the pattern - inclusive
207 * @param end The index of the last character in s that was
208 * matched - exclusive
209 *
210 * @return Whether this match should be turned into a link
211 */
212 boolean acceptMatch(CharSequence s, int start, int end);
213 }
214
215 /**
216 * TransformFilter enables client code to have more control over
217 * how matched patterns are represented as URLs.
218 *
219 * For example: when converting a phone number such as (919) 555-1212
220 * into a tel: URL the parentheses, white space, and hyphen need to be
221 * removed to produce tel:9195551212.
222 */
223 public interface TransformFilter {
224 /**
225 * Examines the matched text and either passes it through or uses the
226 * data in the Matcher state to produce a replacement.
227 *
228 * @param match The regex matcher state that found this URL text
229 * @param url The text that was matched
230 *
231 * @return The transformed form of the URL
232 */
233 String transformUrl(final Matcher match, String url);
234 }
235
236 /**
237 * Scans the text of the provided Spannable and turns all occurrences
238 * of the link types indicated in the mask into clickable links.
239 * If the mask is nonzero, it also removes any existing URLSpans
240 * attached to the Spannable, to avoid problems if you call it
241 * repeatedly on the same text.
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800242 *
243 * @param text Spannable whose text is to be marked-up with links
244 * @param mask Mask to define which kinds of links will be searched.
245 *
246 * @return True if at least one link is found and applied.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800247 */
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800248 public static final boolean addLinks(@NonNull Spannable text, @LinkifyMask int mask) {
Roozbeh Pournaderdc8f3ef2017-06-09 16:01:27 -0700249 return addLinks(text, mask, null);
250 }
251
252 private static boolean addLinks(@NonNull Spannable text, @LinkifyMask int mask,
253 @Nullable Context context) {
Abodunrinwa Tokia69950c2018-11-29 13:51:56 +0000254 if (text != null && containsUnsupportedCharacters(text.toString())) {
255 android.util.EventLog.writeEvent(0x534e4554, "116321860", -1, "");
256 return false;
257 }
258
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800259 if (mask == 0) {
260 return false;
261 }
262
263 URLSpan[] old = text.getSpans(0, text.length(), URLSpan.class);
264
265 for (int i = old.length - 1; i >= 0; i--) {
266 text.removeSpan(old[i]);
267 }
268
269 ArrayList<LinkSpec> links = new ArrayList<LinkSpec>();
270
271 if ((mask & WEB_URLS) != 0) {
Siyamed Sinir840385c2015-11-18 15:02:57 -0800272 gatherLinks(links, text, Patterns.AUTOLINK_WEB_URL,
Eric Fischer7a4be892009-09-14 10:24:24 -0700273 new String[] { "http://", "https://", "rtsp://" },
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800274 sUrlMatchFilter, null);
275 }
276
277 if ((mask & EMAIL_ADDRESSES) != 0) {
Siyamed Sinir0716d5f2016-02-25 15:54:35 -0800278 gatherLinks(links, text, Patterns.AUTOLINK_EMAIL_ADDRESS,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800279 new String[] { "mailto:" },
280 null, null);
281 }
282
283 if ((mask & PHONE_NUMBERS) != 0) {
Roozbeh Pournaderdc8f3ef2017-06-09 16:01:27 -0700284 gatherTelLinks(links, text, context);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800285 }
286
287 if ((mask & MAP_ADDRESSES) != 0) {
288 gatherMapLinks(links, text);
289 }
290
291 pruneOverlaps(links);
292
293 if (links.size() == 0) {
294 return false;
295 }
296
297 for (LinkSpec link: links) {
298 applyLink(link.url, link.start, link.end, text);
299 }
300
301 return true;
302 }
303
304 /**
Abodunrinwa Tokia69950c2018-11-29 13:51:56 +0000305 * Returns true if the specified text contains at least one unsupported character for applying
306 * links. Also logs the error.
307 *
308 * @param text the text to apply links to
309 * @hide
310 */
311 public static boolean containsUnsupportedCharacters(String text) {
312 if (text.contains("\u202C")) {
313 Log.e(LOG_TAG, "Unsupported character for applying links: u202C");
314 return true;
315 }
316 if (text.contains("\u202D")) {
317 Log.e(LOG_TAG, "Unsupported character for applying links: u202D");
318 return true;
319 }
320 if (text.contains("\u202E")) {
321 Log.e(LOG_TAG, "Unsupported character for applying links: u202E");
322 return true;
323 }
324 return false;
325 }
326
327 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800328 * Scans the text of the provided TextView and turns all occurrences of
329 * the link types indicated in the mask into clickable links. If matches
330 * are found the movement method for the TextView is set to
331 * LinkMovementMethod.
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800332 *
333 * @param text TextView whose text is to be marked-up with links
334 * @param mask Mask to define which kinds of links will be searched.
335 *
336 * @return True if at least one link is found and applied.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800337 */
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800338 public static final boolean addLinks(@NonNull TextView text, @LinkifyMask int mask) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800339 if (mask == 0) {
340 return false;
341 }
342
Roozbeh Pournaderdc8f3ef2017-06-09 16:01:27 -0700343 final Context context = text.getContext();
344 final CharSequence t = text.getText();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800345 if (t instanceof Spannable) {
Roozbeh Pournaderdc8f3ef2017-06-09 16:01:27 -0700346 if (addLinks((Spannable) t, mask, context)) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800347 addLinkMovementMethod(text);
348 return true;
349 }
350
351 return false;
352 } else {
353 SpannableString s = SpannableString.valueOf(t);
354
Roozbeh Pournaderdc8f3ef2017-06-09 16:01:27 -0700355 if (addLinks(s, mask, context)) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800356 addLinkMovementMethod(text);
357 text.setText(s);
358
359 return true;
360 }
361
362 return false;
363 }
364 }
365
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800366 private static final void addLinkMovementMethod(@NonNull TextView t) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800367 MovementMethod m = t.getMovementMethod();
368
369 if ((m == null) || !(m instanceof LinkMovementMethod)) {
370 if (t.getLinksClickable()) {
371 t.setMovementMethod(LinkMovementMethod.getInstance());
372 }
373 }
374 }
375
376 /**
377 * Applies a regex to the text of a TextView turning the matches into
378 * links. If links are found then UrlSpans are applied to the link
379 * text match areas, and the movement method for the text is changed
380 * to LinkMovementMethod.
381 *
382 * @param text TextView whose text is to be marked-up with links
383 * @param pattern Regex pattern to be used for finding links
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800384 * @param scheme URL scheme string (eg <code>http://</code>) to be
385 * prepended to the links that do not start with this scheme.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800386 */
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800387 public static final void addLinks(@NonNull TextView text, @NonNull Pattern pattern,
388 @Nullable String scheme) {
389 addLinks(text, pattern, scheme, null, null, null);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800390 }
391
392 /**
393 * Applies a regex to the text of a TextView turning the matches into
394 * links. If links are found then UrlSpans are applied to the link
395 * text match areas, and the movement method for the text is changed
396 * to LinkMovementMethod.
397 *
398 * @param text TextView whose text is to be marked-up with links
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800399 * @param pattern Regex pattern to be used for finding links
400 * @param scheme URL scheme string (eg <code>http://</code>) to be
401 * prepended to the links that do not start with this scheme.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800402 * @param matchFilter The filter that is used to allow the client code
403 * additional control over which pattern matches are
404 * to be converted into links.
405 */
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800406 public static final void addLinks(@NonNull TextView text, @NonNull Pattern pattern,
407 @Nullable String scheme, @Nullable MatchFilter matchFilter,
408 @Nullable TransformFilter transformFilter) {
409 addLinks(text, pattern, scheme, null, matchFilter, transformFilter);
410 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800411
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800412 /**
413 * Applies a regex to the text of a TextView turning the matches into
414 * links. If links are found then UrlSpans are applied to the link
415 * text match areas, and the movement method for the text is changed
416 * to LinkMovementMethod.
417 *
418 * @param text TextView whose text is to be marked-up with links.
419 * @param pattern Regex pattern to be used for finding links.
420 * @param defaultScheme The default scheme to be prepended to links if the link does not
421 * start with one of the <code>schemes</code> given.
422 * @param schemes Array of schemes (eg <code>http://</code>) to check if the link found
423 * contains a scheme. Passing a null or empty value means prepend defaultScheme
424 * to all links.
425 * @param matchFilter The filter that is used to allow the client code additional control
426 * over which pattern matches are to be converted into links.
427 * @param transformFilter Filter to allow the client code to update the link found.
428 */
429 public static final void addLinks(@NonNull TextView text, @NonNull Pattern pattern,
430 @Nullable String defaultScheme, @Nullable String[] schemes,
431 @Nullable MatchFilter matchFilter, @Nullable TransformFilter transformFilter) {
432 SpannableString spannable = SpannableString.valueOf(text.getText());
433
434 boolean linksAdded = addLinks(spannable, pattern, defaultScheme, schemes, matchFilter,
435 transformFilter);
436 if (linksAdded) {
437 text.setText(spannable);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800438 addLinkMovementMethod(text);
439 }
440 }
441
442 /**
443 * Applies a regex to a Spannable turning the matches into
444 * links.
445 *
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800446 * @param text Spannable whose text is to be marked-up with links
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800447 * @param pattern Regex pattern to be used for finding links
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800448 * @param scheme URL scheme string (eg <code>http://</code>) to be
449 * prepended to the links that do not start with this scheme.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800450 */
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800451 public static final boolean addLinks(@NonNull Spannable text, @NonNull Pattern pattern,
452 @Nullable String scheme) {
453 return addLinks(text, pattern, scheme, null, null, null);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800454 }
455
456 /**
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800457 * Applies a regex to a Spannable turning the matches into
458 * links.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800459 *
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800460 * @param spannable Spannable whose text is to be marked-up with links
461 * @param pattern Regex pattern to be used for finding links
462 * @param scheme URL scheme string (eg <code>http://</code>) to be
463 * prepended to the links that do not start with this scheme.
464 * @param matchFilter The filter that is used to allow the client code
465 * additional control over which pattern matches are
466 * to be converted into links.
467 * @param transformFilter Filter to allow the client code to update the link found.
468 *
469 * @return True if at least one link is found and applied.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800470 */
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800471 public static final boolean addLinks(@NonNull Spannable spannable, @NonNull Pattern pattern,
472 @Nullable String scheme, @Nullable MatchFilter matchFilter,
473 @Nullable TransformFilter transformFilter) {
474 return addLinks(spannable, pattern, scheme, null, matchFilter,
475 transformFilter);
476 }
477
478 /**
479 * Applies a regex to a Spannable turning the matches into links.
480 *
481 * @param spannable Spannable whose text is to be marked-up with links.
482 * @param pattern Regex pattern to be used for finding links.
483 * @param defaultScheme The default scheme to be prepended to links if the link does not
484 * start with one of the <code>schemes</code> given.
485 * @param schemes Array of schemes (eg <code>http://</code>) to check if the link found
486 * contains a scheme. Passing a null or empty value means prepend defaultScheme
487 * to all links.
488 * @param matchFilter The filter that is used to allow the client code additional control
489 * over which pattern matches are to be converted into links.
490 * @param transformFilter Filter to allow the client code to update the link found.
491 *
492 * @return True if at least one link is found and applied.
493 */
494 public static final boolean addLinks(@NonNull Spannable spannable, @NonNull Pattern pattern,
495 @Nullable String defaultScheme, @Nullable String[] schemes,
496 @Nullable MatchFilter matchFilter, @Nullable TransformFilter transformFilter) {
Abodunrinwa Tokia69950c2018-11-29 13:51:56 +0000497 if (spannable != null && containsUnsupportedCharacters(spannable.toString())) {
498 android.util.EventLog.writeEvent(0x534e4554, "116321860", -1, "");
499 return false;
500 }
501
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800502 final String[] schemesCopy;
503 if (defaultScheme == null) defaultScheme = "";
504 if (schemes == null || schemes.length < 1) {
505 schemes = EmptyArray.STRING;
506 }
507
508 schemesCopy = new String[schemes.length + 1];
509 schemesCopy[0] = defaultScheme.toLowerCase(Locale.ROOT);
510 for (int index = 0; index < schemes.length; index++) {
511 String scheme = schemes[index];
512 schemesCopy[index + 1] = (scheme == null) ? "" : scheme.toLowerCase(Locale.ROOT);
513 }
514
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800515 boolean hasMatches = false;
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800516 Matcher m = pattern.matcher(spannable);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800517
518 while (m.find()) {
519 int start = m.start();
520 int end = m.end();
521 boolean allowed = true;
522
523 if (matchFilter != null) {
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800524 allowed = matchFilter.acceptMatch(spannable, start, end);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800525 }
526
527 if (allowed) {
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800528 String url = makeUrl(m.group(0), schemesCopy, m, transformFilter);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800529
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800530 applyLink(url, start, end, spannable);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800531 hasMatches = true;
532 }
533 }
534
535 return hasMatches;
536 }
537
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000538 /**
539 * Scans the text of the provided TextView and turns all occurrences of the entity types
540 * specified by {@code options} into clickable links. If links are found, this method
541 * removes any pre-existing {@link TextLinkSpan} attached to the text (to avoid
542 * problems if you call it repeatedly on the same text) and sets the movement method for the
543 * TextView to LinkMovementMethod.
544 *
545 * <p><strong>Note:</strong> This method returns immediately but generates the links with
546 * the specified classifier on a background thread. The generated links are applied on the
547 * calling thread.
548 *
549 * @param textView TextView whose text is to be marked-up with links
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100550 * @param params optional parameters to specify how to generate the links
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000551 *
552 * @return a future that may be used to interrupt or query the background task
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100553 * @hide
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000554 */
555 @UiThread
556 public static Future<Void> addLinksAsync(
557 @NonNull TextView textView,
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100558 @Nullable TextLinksParams params) {
559 return addLinksAsync(textView, params, null /* executor */, null /* callback */);
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000560 }
561
562 /**
563 * Scans the text of the provided TextView and turns all occurrences of the entity types
564 * specified by {@code options} into clickable links. If links are found, this method
565 * removes any pre-existing {@link TextLinkSpan} attached to the text (to avoid
566 * problems if you call it repeatedly on the same text) and sets the movement method for the
567 * TextView to LinkMovementMethod.
568 *
569 * <p><strong>Note:</strong> This method returns immediately but generates the links with
570 * the specified classifier on a background thread. The generated links are applied on the
571 * calling thread.
572 *
573 * @param textView TextView whose text is to be marked-up with links
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100574 * @param mask mask to define which kinds of links will be generated
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000575 *
576 * @return a future that may be used to interrupt or query the background task
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100577 * @hide
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000578 */
579 @UiThread
580 public static Future<Void> addLinksAsync(
581 @NonNull TextView textView,
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100582 @LinkifyMask int mask) {
583 return addLinksAsync(textView, TextLinksParams.fromLinkMask(mask),
584 null /* executor */, null /* callback */);
585 }
586
587 /**
588 * Scans the text of the provided TextView and turns all occurrences of the entity types
589 * specified by {@code options} into clickable links. If links are found, this method
590 * removes any pre-existing {@link TextLinkSpan} attached to the text (to avoid
591 * problems if you call it repeatedly on the same text) and sets the movement method for the
592 * TextView to LinkMovementMethod.
593 *
594 * <p><strong>Note:</strong> This method returns immediately but generates the links with
595 * the specified classifier on a background thread. The generated links are applied on the
596 * calling thread.
597 *
598 * @param textView TextView whose text is to be marked-up with links
599 * @param params optional parameters to specify how to generate the links
600 * @param executor Executor that runs the background task
601 * @param callback Callback that receives the final status of the background task execution
602 *
603 * @return a future that may be used to interrupt or query the background task
604 * @hide
605 */
606 @UiThread
607 public static Future<Void> addLinksAsync(
608 @NonNull TextView textView,
609 @Nullable TextLinksParams params,
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000610 @Nullable Executor executor,
611 @Nullable Consumer<Integer> callback) {
612 Preconditions.checkNotNull(textView);
613 final CharSequence text = textView.getText();
614 final Spannable spannable = (text instanceof Spannable)
615 ? (Spannable) text : SpannableString.valueOf(text);
616 final Runnable modifyTextView = () -> {
617 addLinkMovementMethod(textView);
618 if (spannable != text) {
619 textView.setText(spannable);
620 }
621 };
622 return addLinksAsync(spannable, textView.getTextClassifier(),
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100623 params, executor, callback, modifyTextView);
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000624 }
625
626 /**
627 * Scans the text of the provided TextView and turns all occurrences of the entity types
628 * specified by {@code options} into clickable links. If links are found, this method
629 * removes any pre-existing {@link TextLinkSpan} attached to the text to avoid
630 * problems if you call it repeatedly on the same text.
631 *
632 * <p><strong>Note:</strong> This method returns immediately but generates the links with
633 * the specified classifier on a background thread. The generated links are applied on the
634 * calling thread.
635 *
636 * <p><strong>Note:</strong> If the text is currently attached to a TextView, this method
637 * should be called on the UI thread.
638 *
639 * @param text Spannable whose text is to be marked-up with links
640 * @param classifier the TextClassifier to use to generate the links
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100641 * @param params optional parameters to specify how to generate the links
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000642 *
643 * @return a future that may be used to interrupt or query the background task
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100644 * @hide
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000645 */
646 public static Future<Void> addLinksAsync(
647 @NonNull Spannable text,
648 @NonNull TextClassifier classifier,
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100649 @Nullable TextLinksParams params) {
650 return addLinksAsync(text, classifier, params, null /* executor */, null /* callback */);
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000651 }
652
653 /**
654 * Scans the text of the provided TextView and turns all occurrences of the entity types
655 * specified by the link {@code mask} into clickable links. If links are found, this method
656 * removes any pre-existing {@link TextLinkSpan} attached to the text to avoid
657 * problems if you call it repeatedly on the same text.
658 *
659 * <p><strong>Note:</strong> This method returns immediately but generates the links with
660 * the specified classifier on a background thread. The generated links are applied on the
661 * calling thread.
662 *
663 * <p><strong>Note:</strong> If the text is currently attached to a TextView, this method
664 * should be called on the UI thread.
665 *
666 * @param text Spannable whose text is to be marked-up with links
667 * @param classifier the TextClassifier to use to generate the links
668 * @param mask mask to define which kinds of links will be generated
669 *
670 * @return a future that may be used to interrupt or query the background task
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100671 * @hide
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000672 */
673 public static Future<Void> addLinksAsync(
674 @NonNull Spannable text,
675 @NonNull TextClassifier classifier,
676 @LinkifyMask int mask) {
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100677 return addLinksAsync(text, classifier, TextLinksParams.fromLinkMask(mask),
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000678 null /* executor */, null /* callback */);
679 }
680
681 /**
682 * Scans the text of the provided TextView and turns all occurrences of the entity types
683 * specified by {@code options} into clickable links. If links are found, this method
684 * removes any pre-existing {@link TextLinkSpan} attached to the text to avoid
685 * problems if you call it repeatedly on the same text.
686 *
687 * <p><strong>Note:</strong> This method returns immediately but generates the links with
688 * the specified classifier on a background thread. The generated links are applied on the
689 * calling thread.
690 *
691 * <p><strong>Note:</strong> If the text is currently attached to a TextView, this method
692 * should be called on the UI thread.
693 *
694 * @param text Spannable whose text is to be marked-up with links
695 * @param classifier the TextClassifier to use to generate the links
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100696 * @param params optional parameters to specify how to generate the links
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000697 * @param executor Executor that runs the background task
698 * @param callback Callback that receives the final status of the background task execution
699 *
700 * @return a future that may be used to interrupt or query the background task
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100701 * @hide
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000702 */
703 public static Future<Void> addLinksAsync(
704 @NonNull Spannable text,
705 @NonNull TextClassifier classifier,
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100706 @Nullable TextLinksParams params,
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000707 @Nullable Executor executor,
708 @Nullable Consumer<Integer> callback) {
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100709 return addLinksAsync(text, classifier, params, executor, callback,
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000710 null /* modifyTextView */);
711 }
712
713 private static Future<Void> addLinksAsync(
714 @NonNull Spannable text,
715 @NonNull TextClassifier classifier,
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100716 @Nullable TextLinksParams params,
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000717 @Nullable Executor executor,
718 @Nullable Consumer<Integer> callback,
719 @Nullable Runnable modifyTextView) {
720 Preconditions.checkNotNull(text);
721 Preconditions.checkNotNull(classifier);
Jan Althaus108aad32018-01-30 15:26:55 +0100722
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100723 // TODO: This is a bug. We shouldnot call getMaxGenerateLinksTextLength() on the UI thread.
Jan Althaus108aad32018-01-30 15:26:55 +0100724 // The input text may exceed the maximum length the text classifier can handle. In such
725 // cases, we process the text up to the maximum length.
726 final CharSequence truncatedText = text.subSequence(
727 0, Math.min(text.length(), classifier.getMaxGenerateLinksTextLength()));
728
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100729 final TextClassifier.EntityConfig entityConfig = (params == null)
730 ? null : params.getEntityConfig();
731 final TextLinks.Request request = new TextLinks.Request.Builder(truncatedText)
732 .setLegacyFallback(true)
733 .setEntityConfig(entityConfig)
734 .build();
735 final Supplier<TextLinks> supplier = () -> classifier.generateLinks(request);
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000736 final Consumer<TextLinks> consumer = links -> {
737 if (links.getLinks().isEmpty()) {
738 if (callback != null) {
739 callback.accept(TextLinks.STATUS_NO_LINKS_FOUND);
740 }
741 return;
742 }
743
Jan Althaus108aad32018-01-30 15:26:55 +0100744 // Remove spans only for the part of the text we generated links for.
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100745 final TextLinkSpan[] old =
746 text.getSpans(0, truncatedText.length(), TextLinkSpan.class);
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000747 for (int i = old.length - 1; i >= 0; i--) {
748 text.removeSpan(old[i]);
749 }
750
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100751 final @TextLinks.Status int result = params.apply(text, links);
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000752 if (result == TextLinks.STATUS_LINKS_APPLIED) {
753 if (modifyTextView != null) {
754 modifyTextView.run();
755 }
756 }
757 if (callback != null) {
758 callback.accept(result);
759 }
760 };
761 if (executor == null) {
762 return CompletableFuture.supplyAsync(supplier).thenAccept(consumer);
763 } else {
764 return CompletableFuture.supplyAsync(supplier, executor).thenAccept(consumer);
765 }
766 }
767
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800768 private static final void applyLink(String url, int start, int end, Spannable text) {
769 URLSpan span = new URLSpan(url);
770
771 text.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
772 }
773
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800774 private static final String makeUrl(@NonNull String url, @NonNull String[] prefixes,
775 Matcher matcher, @Nullable TransformFilter filter) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800776 if (filter != null) {
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800777 url = filter.transformUrl(matcher, url);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800778 }
779
780 boolean hasPrefix = false;
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800781
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800782 for (int i = 0; i < prefixes.length; i++) {
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800783 if (url.regionMatches(true, 0, prefixes[i], 0, prefixes[i].length())) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800784 hasPrefix = true;
785
786 // Fix capitalization if necessary
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800787 if (!url.regionMatches(false, 0, prefixes[i], 0, prefixes[i].length())) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800788 url = prefixes[i] + url.substring(prefixes[i].length());
789 }
790
791 break;
792 }
793 }
794
Siyamed Sinir6eccafd2016-03-02 16:02:38 -0800795 if (!hasPrefix && prefixes.length > 0) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800796 url = prefixes[0] + url;
797 }
798
799 return url;
800 }
801
802 private static final void gatherLinks(ArrayList<LinkSpec> links,
803 Spannable s, Pattern pattern, String[] schemes,
804 MatchFilter matchFilter, TransformFilter transformFilter) {
805 Matcher m = pattern.matcher(s);
806
807 while (m.find()) {
808 int start = m.start();
809 int end = m.end();
810
811 if (matchFilter == null || matchFilter.acceptMatch(s, start, end)) {
812 LinkSpec spec = new LinkSpec();
813 String url = makeUrl(m.group(0), schemes, m, transformFilter);
814
815 spec.url = url;
816 spec.start = start;
817 spec.end = end;
818
819 links.add(spec);
820 }
821 }
822 }
823
Mathew Inwood9cf99fd2018-08-14 15:31:03 +0100824 @UnsupportedAppUsage
Roozbeh Pournaderdc8f3ef2017-06-09 16:01:27 -0700825 private static void gatherTelLinks(ArrayList<LinkSpec> links, Spannable s,
826 @Nullable Context context) {
Victoria Lease430fc972013-04-01 10:22:00 -0700827 PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();
Roozbeh Pournaderdc8f3ef2017-06-09 16:01:27 -0700828 final TelephonyManager tm = (context == null)
829 ? TelephonyManager.getDefault()
830 : TelephonyManager.from(context);
Victoria Lease430fc972013-04-01 10:22:00 -0700831 Iterable<PhoneNumberMatch> matches = phoneUtil.findNumbers(s.toString(),
Roozbeh Pournaderdc8f3ef2017-06-09 16:01:27 -0700832 tm.getSimCountryIso().toUpperCase(Locale.US),
833 Leniency.POSSIBLE, Long.MAX_VALUE);
Victoria Lease430fc972013-04-01 10:22:00 -0700834 for (PhoneNumberMatch match : matches) {
835 LinkSpec spec = new LinkSpec();
836 spec.url = "tel:" + PhoneNumberUtils.normalizeNumber(match.rawString());
837 spec.start = match.start();
838 spec.end = match.end();
839 links.add(spec);
840 }
841 }
842
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800843 private static final void gatherMapLinks(ArrayList<LinkSpec> links, Spannable s) {
844 String string = s.toString();
845 String address;
846 int base = 0;
847
Aaron Whyte83955cb2014-06-06 16:41:38 -0700848 try {
849 while ((address = WebView.findAddress(string)) != null) {
850 int start = string.indexOf(address);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800851
Aaron Whyte83955cb2014-06-06 16:41:38 -0700852 if (start < 0) {
853 break;
854 }
855
856 LinkSpec spec = new LinkSpec();
857 int length = address.length();
858 int end = start + length;
859
860 spec.start = base + start;
861 spec.end = base + end;
862 string = string.substring(end);
863 base += end;
864
865 String encodedAddress = null;
866
867 try {
868 encodedAddress = URLEncoder.encode(address,"UTF-8");
869 } catch (UnsupportedEncodingException e) {
870 continue;
871 }
872
873 spec.url = "geo:0,0?q=" + encodedAddress;
874 links.add(spec);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800875 }
Aaron Whyte83955cb2014-06-06 16:41:38 -0700876 } catch (UnsupportedOperationException e) {
877 // findAddress may fail with an unsupported exception on platforms without a WebView.
878 // In this case, we will not append anything to the links variable: it would have died
879 // in WebView.findAddress.
880 return;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800881 }
882 }
883
884 private static final void pruneOverlaps(ArrayList<LinkSpec> links) {
885 Comparator<LinkSpec> c = new Comparator<LinkSpec>() {
886 public final int compare(LinkSpec a, LinkSpec b) {
887 if (a.start < b.start) {
888 return -1;
889 }
890
891 if (a.start > b.start) {
892 return 1;
893 }
894
895 if (a.end < b.end) {
896 return 1;
897 }
898
899 if (a.end > b.end) {
900 return -1;
901 }
902
903 return 0;
904 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800905 };
906
907 Collections.sort(links, c);
908
909 int len = links.size();
910 int i = 0;
911
912 while (i < len - 1) {
913 LinkSpec a = links.get(i);
914 LinkSpec b = links.get(i + 1);
915 int remove = -1;
916
917 if ((a.start <= b.start) && (a.end > b.start)) {
918 if (b.end <= a.end) {
919 remove = i + 1;
920 } else if ((a.end - a.start) > (b.end - b.start)) {
921 remove = i + 1;
922 } else if ((a.end - a.start) < (b.end - b.start)) {
923 remove = i;
924 }
925
926 if (remove != -1) {
927 links.remove(remove);
928 len--;
929 continue;
930 }
931
932 }
933
934 i++;
935 }
936 }
937}
938
939class LinkSpec {
940 String url;
941 int start;
942 int end;
943}