OSDN Git Service

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