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