• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
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 
17 package android.text.util;
18 
19 import android.telephony.PhoneNumberUtils;
20 import android.text.method.LinkMovementMethod;
21 import android.text.method.MovementMethod;
22 import android.text.style.URLSpan;
23 import android.text.Spannable;
24 import android.text.SpannableString;
25 import android.text.Spanned;
26 import android.util.Patterns;
27 import android.webkit.WebView;
28 import android.widget.TextView;
29 
30 
31 import java.io.UnsupportedEncodingException;
32 import java.net.URLEncoder;
33 import java.util.ArrayList;
34 import java.util.Collections;
35 import java.util.Comparator;
36 import java.util.Locale;
37 import java.util.regex.Matcher;
38 import java.util.regex.Pattern;
39 
40 import com.android.i18n.phonenumbers.PhoneNumberMatch;
41 import com.android.i18n.phonenumbers.PhoneNumberUtil;
42 import com.android.i18n.phonenumbers.PhoneNumberUtil.Leniency;
43 
44 /**
45  *  Linkify take a piece of text and a regular expression and turns all of the
46  *  regex matches in the text into clickable links.  This is particularly
47  *  useful for matching things like email addresses, web urls, etc. and making
48  *  them actionable.
49  *
50  *  Alone with the pattern that is to be matched, a url scheme prefix is also
51  *  required.  Any pattern match that does not begin with the supplied scheme
52  *  will have the scheme prepended to the matched text when the clickable url
53  *  is created.  For instance, if you are matching web urls you would supply
54  *  the scheme <code>http://</code>.  If the pattern matches example.com, which
55  *  does not have a url scheme prefix, the supplied scheme will be prepended to
56  *  create <code>http://example.com</code> when the clickable url link is
57  *  created.
58  */
59 
60 public class Linkify {
61     /**
62      *  Bit field indicating that web URLs should be matched in methods that
63      *  take an options mask
64      */
65     public static final int WEB_URLS = 0x01;
66 
67     /**
68      *  Bit field indicating that email addresses should be matched in methods
69      *  that take an options mask
70      */
71     public static final int EMAIL_ADDRESSES = 0x02;
72 
73     /**
74      *  Bit field indicating that phone numbers should be matched in methods that
75      *  take an options mask
76      */
77     public static final int PHONE_NUMBERS = 0x04;
78 
79     /**
80      *  Bit field indicating that street addresses should be matched in methods that
81      *  take an options mask
82      */
83     public static final int MAP_ADDRESSES = 0x08;
84 
85     /**
86      *  Bit mask indicating that all available patterns should be matched in
87      *  methods that take an options mask
88      */
89     public static final int ALL = WEB_URLS | EMAIL_ADDRESSES | PHONE_NUMBERS | MAP_ADDRESSES;
90 
91     /**
92      * Don't treat anything with fewer than this many digits as a
93      * phone number.
94      */
95     private static final int PHONE_NUMBER_MINIMUM_DIGITS = 5;
96 
97     /**
98      *  Filters out web URL matches that occur after an at-sign (@).  This is
99      *  to prevent turning the domain name in an email address into a web link.
100      */
101     public static final MatchFilter sUrlMatchFilter = new MatchFilter() {
102         public final boolean acceptMatch(CharSequence s, int start, int end) {
103             if (start == 0) {
104                 return true;
105             }
106 
107             if (s.charAt(start - 1) == '@') {
108                 return false;
109             }
110 
111             return true;
112         }
113     };
114 
115     /**
116      *  Filters out URL matches that don't have enough digits to be a
117      *  phone number.
118      */
119     public static final MatchFilter sPhoneNumberMatchFilter = new MatchFilter() {
120         public final boolean acceptMatch(CharSequence s, int start, int end) {
121             int digitCount = 0;
122 
123             for (int i = start; i < end; i++) {
124                 if (Character.isDigit(s.charAt(i))) {
125                     digitCount++;
126                     if (digitCount >= PHONE_NUMBER_MINIMUM_DIGITS) {
127                         return true;
128                     }
129                 }
130             }
131             return false;
132         }
133     };
134 
135     /**
136      *  Transforms matched phone number text into something suitable
137      *  to be used in a tel: URL.  It does this by removing everything
138      *  but the digits and plus signs.  For instance:
139      *  &apos;+1 (919) 555-1212&apos;
140      *  becomes &apos;+19195551212&apos;
141      */
142     public static final TransformFilter sPhoneNumberTransformFilter = new TransformFilter() {
143         public final String transformUrl(final Matcher match, String url) {
144             return Patterns.digitsAndPlusOnly(match);
145         }
146     };
147 
148     /**
149      *  MatchFilter enables client code to have more control over
150      *  what is allowed to match and become a link, and what is not.
151      *
152      *  For example:  when matching web urls you would like things like
153      *  http://www.example.com to match, as well as just example.com itelf.
154      *  However, you would not want to match against the domain in
155      *  support@example.com.  So, when matching against a web url pattern you
156      *  might also include a MatchFilter that disallows the match if it is
157      *  immediately preceded by an at-sign (@).
158      */
159     public interface MatchFilter {
160         /**
161          *  Examines the character span matched by the pattern and determines
162          *  if the match should be turned into an actionable link.
163          *
164          *  @param s        The body of text against which the pattern
165          *                  was matched
166          *  @param start    The index of the first character in s that was
167          *                  matched by the pattern - inclusive
168          *  @param end      The index of the last character in s that was
169          *                  matched - exclusive
170          *
171          *  @return         Whether this match should be turned into a link
172          */
acceptMatch(CharSequence s, int start, int end)173         boolean acceptMatch(CharSequence s, int start, int end);
174     }
175 
176     /**
177      *  TransformFilter enables client code to have more control over
178      *  how matched patterns are represented as URLs.
179      *
180      *  For example:  when converting a phone number such as (919)  555-1212
181      *  into a tel: URL the parentheses, white space, and hyphen need to be
182      *  removed to produce tel:9195551212.
183      */
184     public interface TransformFilter {
185         /**
186          *  Examines the matched text and either passes it through or uses the
187          *  data in the Matcher state to produce a replacement.
188          *
189          *  @param match    The regex matcher state that found this URL text
190          *  @param url      The text that was matched
191          *
192          *  @return         The transformed form of the URL
193          */
transformUrl(final Matcher match, String url)194         String transformUrl(final Matcher match, String url);
195     }
196 
197     /**
198      *  Scans the text of the provided Spannable and turns all occurrences
199      *  of the link types indicated in the mask into clickable links.
200      *  If the mask is nonzero, it also removes any existing URLSpans
201      *  attached to the Spannable, to avoid problems if you call it
202      *  repeatedly on the same text.
203      */
addLinks(Spannable text, int mask)204     public static final boolean addLinks(Spannable text, int mask) {
205         if (mask == 0) {
206             return false;
207         }
208 
209         URLSpan[] old = text.getSpans(0, text.length(), URLSpan.class);
210 
211         for (int i = old.length - 1; i >= 0; i--) {
212             text.removeSpan(old[i]);
213         }
214 
215         ArrayList<LinkSpec> links = new ArrayList<LinkSpec>();
216 
217         if ((mask & WEB_URLS) != 0) {
218             gatherLinks(links, text, Patterns.WEB_URL,
219                 new String[] { "http://", "https://", "rtsp://" },
220                 sUrlMatchFilter, null);
221         }
222 
223         if ((mask & EMAIL_ADDRESSES) != 0) {
224             gatherLinks(links, text, Patterns.EMAIL_ADDRESS,
225                 new String[] { "mailto:" },
226                 null, null);
227         }
228 
229         if ((mask & PHONE_NUMBERS) != 0) {
230             gatherTelLinks(links, text);
231         }
232 
233         if ((mask & MAP_ADDRESSES) != 0) {
234             gatherMapLinks(links, text);
235         }
236 
237         pruneOverlaps(links);
238 
239         if (links.size() == 0) {
240             return false;
241         }
242 
243         for (LinkSpec link: links) {
244             applyLink(link.url, link.start, link.end, text);
245         }
246 
247         return true;
248     }
249 
250     /**
251      *  Scans the text of the provided TextView and turns all occurrences of
252      *  the link types indicated in the mask into clickable links.  If matches
253      *  are found the movement method for the TextView is set to
254      *  LinkMovementMethod.
255      */
addLinks(TextView text, int mask)256     public static final boolean addLinks(TextView text, int mask) {
257         if (mask == 0) {
258             return false;
259         }
260 
261         CharSequence t = text.getText();
262 
263         if (t instanceof Spannable) {
264             if (addLinks((Spannable) t, mask)) {
265                 addLinkMovementMethod(text);
266                 return true;
267             }
268 
269             return false;
270         } else {
271             SpannableString s = SpannableString.valueOf(t);
272 
273             if (addLinks(s, mask)) {
274                 addLinkMovementMethod(text);
275                 text.setText(s);
276 
277                 return true;
278             }
279 
280             return false;
281         }
282     }
283 
addLinkMovementMethod(TextView t)284     private static final void addLinkMovementMethod(TextView t) {
285         MovementMethod m = t.getMovementMethod();
286 
287         if ((m == null) || !(m instanceof LinkMovementMethod)) {
288             if (t.getLinksClickable()) {
289                 t.setMovementMethod(LinkMovementMethod.getInstance());
290             }
291         }
292     }
293 
294     /**
295      *  Applies a regex to the text of a TextView turning the matches into
296      *  links.  If links are found then UrlSpans are applied to the link
297      *  text match areas, and the movement method for the text is changed
298      *  to LinkMovementMethod.
299      *
300      *  @param text         TextView whose text is to be marked-up with links
301      *  @param pattern      Regex pattern to be used for finding links
302      *  @param scheme       Url scheme string (eg <code>http://</code> to be
303      *                      prepended to the url of links that do not have
304      *                      a scheme specified in the link text
305      */
addLinks(TextView text, Pattern pattern, String scheme)306     public static final void addLinks(TextView text, Pattern pattern, String scheme) {
307         addLinks(text, pattern, scheme, null, null);
308     }
309 
310     /**
311      *  Applies a regex to the text of a TextView turning the matches into
312      *  links.  If links are found then UrlSpans are applied to the link
313      *  text match areas, and the movement method for the text is changed
314      *  to LinkMovementMethod.
315      *
316      *  @param text         TextView whose text is to be marked-up with links
317      *  @param p            Regex pattern to be used for finding links
318      *  @param scheme       Url scheme string (eg <code>http://</code> to be
319      *                      prepended to the url of links that do not have
320      *                      a scheme specified in the link text
321      *  @param matchFilter  The filter that is used to allow the client code
322      *                      additional control over which pattern matches are
323      *                      to be converted into links.
324      */
addLinks(TextView text, Pattern p, String scheme, MatchFilter matchFilter, TransformFilter transformFilter)325     public static final void addLinks(TextView text, Pattern p, String scheme,
326             MatchFilter matchFilter, TransformFilter transformFilter) {
327         SpannableString s = SpannableString.valueOf(text.getText());
328 
329         if (addLinks(s, p, scheme, matchFilter, transformFilter)) {
330             text.setText(s);
331             addLinkMovementMethod(text);
332         }
333     }
334 
335     /**
336      *  Applies a regex to a Spannable turning the matches into
337      *  links.
338      *
339      *  @param text         Spannable whose text is to be marked-up with
340      *                      links
341      *  @param pattern      Regex pattern to be used for finding links
342      *  @param scheme       Url scheme string (eg <code>http://</code> to be
343      *                      prepended to the url of links that do not have
344      *                      a scheme specified in the link text
345      */
addLinks(Spannable text, Pattern pattern, String scheme)346     public static final boolean addLinks(Spannable text, Pattern pattern, String scheme) {
347         return addLinks(text, pattern, scheme, null, null);
348     }
349 
350     /**
351      *  Applies a regex to a Spannable turning the matches into
352      *  links.
353      *
354      *  @param s            Spannable whose text is to be marked-up with
355      *                      links
356      *  @param p            Regex pattern to be used for finding links
357      *  @param scheme       Url scheme string (eg <code>http://</code> to be
358      *                      prepended to the url of links that do not have
359      *                      a scheme specified in the link text
360      *  @param matchFilter  The filter that is used to allow the client code
361      *                      additional control over which pattern matches are
362      *                      to be converted into links.
363      */
addLinks(Spannable s, Pattern p, String scheme, MatchFilter matchFilter, TransformFilter transformFilter)364     public static final boolean addLinks(Spannable s, Pattern p,
365             String scheme, MatchFilter matchFilter,
366             TransformFilter transformFilter) {
367         boolean hasMatches = false;
368         String prefix = (scheme == null) ? "" : scheme.toLowerCase(Locale.ROOT);
369         Matcher m = p.matcher(s);
370 
371         while (m.find()) {
372             int start = m.start();
373             int end = m.end();
374             boolean allowed = true;
375 
376             if (matchFilter != null) {
377                 allowed = matchFilter.acceptMatch(s, start, end);
378             }
379 
380             if (allowed) {
381                 String url = makeUrl(m.group(0), new String[] { prefix },
382                                      m, transformFilter);
383 
384                 applyLink(url, start, end, s);
385                 hasMatches = true;
386             }
387         }
388 
389         return hasMatches;
390     }
391 
applyLink(String url, int start, int end, Spannable text)392     private static final void applyLink(String url, int start, int end, Spannable text) {
393         URLSpan span = new URLSpan(url);
394 
395         text.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
396     }
397 
makeUrl(String url, String[] prefixes, Matcher m, TransformFilter filter)398     private static final String makeUrl(String url, String[] prefixes,
399             Matcher m, TransformFilter filter) {
400         if (filter != null) {
401             url = filter.transformUrl(m, url);
402         }
403 
404         boolean hasPrefix = false;
405 
406         for (int i = 0; i < prefixes.length; i++) {
407             if (url.regionMatches(true, 0, prefixes[i], 0,
408                                   prefixes[i].length())) {
409                 hasPrefix = true;
410 
411                 // Fix capitalization if necessary
412                 if (!url.regionMatches(false, 0, prefixes[i], 0,
413                                        prefixes[i].length())) {
414                     url = prefixes[i] + url.substring(prefixes[i].length());
415                 }
416 
417                 break;
418             }
419         }
420 
421         if (!hasPrefix) {
422             url = prefixes[0] + url;
423         }
424 
425         return url;
426     }
427 
gatherLinks(ArrayList<LinkSpec> links, Spannable s, Pattern pattern, String[] schemes, MatchFilter matchFilter, TransformFilter transformFilter)428     private static final void gatherLinks(ArrayList<LinkSpec> links,
429             Spannable s, Pattern pattern, String[] schemes,
430             MatchFilter matchFilter, TransformFilter transformFilter) {
431         Matcher m = pattern.matcher(s);
432 
433         while (m.find()) {
434             int start = m.start();
435             int end = m.end();
436 
437             if (matchFilter == null || matchFilter.acceptMatch(s, start, end)) {
438                 LinkSpec spec = new LinkSpec();
439                 String url = makeUrl(m.group(0), schemes, m, transformFilter);
440 
441                 spec.url = url;
442                 spec.start = start;
443                 spec.end = end;
444 
445                 links.add(spec);
446             }
447         }
448     }
449 
gatherTelLinks(ArrayList<LinkSpec> links, Spannable s)450     private static final void gatherTelLinks(ArrayList<LinkSpec> links, Spannable s) {
451         PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();
452         Iterable<PhoneNumberMatch> matches = phoneUtil.findNumbers(s.toString(),
453                 Locale.getDefault().getCountry(), Leniency.POSSIBLE, Long.MAX_VALUE);
454         for (PhoneNumberMatch match : matches) {
455             LinkSpec spec = new LinkSpec();
456             spec.url = "tel:" + PhoneNumberUtils.normalizeNumber(match.rawString());
457             spec.start = match.start();
458             spec.end = match.end();
459             links.add(spec);
460         }
461     }
462 
gatherMapLinks(ArrayList<LinkSpec> links, Spannable s)463     private static final void gatherMapLinks(ArrayList<LinkSpec> links, Spannable s) {
464         String string = s.toString();
465         String address;
466         int base = 0;
467 
468         while ((address = WebView.findAddress(string)) != null) {
469             int start = string.indexOf(address);
470 
471             if (start < 0) {
472                 break;
473             }
474 
475             LinkSpec spec = new LinkSpec();
476             int length = address.length();
477             int end = start + length;
478 
479             spec.start = base + start;
480             spec.end = base + end;
481             string = string.substring(end);
482             base += end;
483 
484             String encodedAddress = null;
485 
486             try {
487                 encodedAddress = URLEncoder.encode(address,"UTF-8");
488             } catch (UnsupportedEncodingException e) {
489                 continue;
490             }
491 
492             spec.url = "geo:0,0?q=" + encodedAddress;
493             links.add(spec);
494         }
495     }
496 
pruneOverlaps(ArrayList<LinkSpec> links)497     private static final void pruneOverlaps(ArrayList<LinkSpec> links) {
498         Comparator<LinkSpec>  c = new Comparator<LinkSpec>() {
499             public final int compare(LinkSpec a, LinkSpec b) {
500                 if (a.start < b.start) {
501                     return -1;
502                 }
503 
504                 if (a.start > b.start) {
505                     return 1;
506                 }
507 
508                 if (a.end < b.end) {
509                     return 1;
510                 }
511 
512                 if (a.end > b.end) {
513                     return -1;
514                 }
515 
516                 return 0;
517             }
518 
519             public final boolean equals(Object o) {
520                 return false;
521             }
522         };
523 
524         Collections.sort(links, c);
525 
526         int len = links.size();
527         int i = 0;
528 
529         while (i < len - 1) {
530             LinkSpec a = links.get(i);
531             LinkSpec b = links.get(i + 1);
532             int remove = -1;
533 
534             if ((a.start <= b.start) && (a.end > b.start)) {
535                 if (b.end <= a.end) {
536                     remove = i + 1;
537                 } else if ((a.end - a.start) > (b.end - b.start)) {
538                     remove = i + 1;
539                 } else if ((a.end - a.start) < (b.end - b.start)) {
540                     remove = i;
541                 }
542 
543                 if (remove != -1) {
544                     links.remove(remove);
545                     len--;
546                     continue;
547                 }
548 
549             }
550 
551             i++;
552         }
553     }
554 }
555 
556 class LinkSpec {
557     String url;
558     int start;
559     int end;
560 }
561