Loading src/com/android/calendar/EventInfoFragment.java +11 −1 Original line number Diff line number Diff line Loading @@ -1227,7 +1227,17 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange textView.setAutoLinkMask(0); textView.setText(location.trim()); try { Utils.linkifyTextView(textView, true); textView.setText(Utils.extendedLinkify(textView.getText().toString(), true)); // Linkify.addLinks() sets the TextView movement method if it finds any links. // We must do the same here, in case linkify by itself did not find any. // (This is cloned from Linkify.addLinkMovementMethod().) MovementMethod mm = textView.getMovementMethod(); if ((mm == null) || !(mm instanceof LinkMovementMethod)) { if (textView.getLinksClickable()) { textView.setMovementMethod(LinkMovementMethod.getInstance()); } } } catch (Exception ex) { // unexpected Log.e(TAG, "Linkification failed", ex); Loading src/com/android/calendar/Utils.java +118 −65 Original line number Diff line number Diff line Loading @@ -70,6 +70,7 @@ import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; public class Utils { Loading Loading @@ -140,6 +141,59 @@ public class Utils { private static String sVersion = null; private static final Pattern mWildcardPattern = Pattern.compile("^.*$"); /** * A coordinate must be of the following form for Google Maps to correctly use it: * Latitude, Longitude * * This may be in decimal form: * Latitude: {-90 to 90} * Longitude: {-180 to 180} * * Or, in degrees, minutes, and seconds: * Latitude: {-90 to 90}° {0 to 59}' {0 to 59}" * Latitude: {-180 to 180}° {0 to 59}' {0 to 59}" * + or - degrees may also be represented with N or n, S or s for latitude, and with * E or e, W or w for longitude, where the direction may either precede or follow the value. * * Some examples of coordinates that will be accepted by the regex: * 37.422081°, -122.084576° * 37.422081,-122.084576 * +37°25'19.49", -122°5'4.47" * 37°25'19.49"N, 122°5'4.47"W * N 37° 25' 19.49", W 122° 5' 4.47" **/ private static final String COORD_DEGREES_LATITUDE = "([-+NnSs]" + "(\\s)*)?" + "[1-9]?[0-9](\u00B0)" + "(\\s)*" + "([1-5]?[0-9]\')?" + "(\\s)*" + "([1-5]?[0-9]" + "(\\.[0-9]+)?\")?" + "((\\s)*" + "[NnSs])?"; private static final String COORD_DEGREES_LONGITUDE = "([-+EeWw]" + "(\\s)*)?" + "(1)?[0-9]?[0-9](\u00B0)" + "(\\s)*" + "([1-5]?[0-9]\')?" + "(\\s)*" + "([1-5]?[0-9]" + "(\\.[0-9]+)?\")?" + "((\\s)*" + "[EeWw])?"; private static final String COORD_DEGREES_PATTERN = COORD_DEGREES_LATITUDE + "(\\s)*" + "," + "(\\s)*" + COORD_DEGREES_LONGITUDE; private static final String COORD_DECIMAL_LATITUDE = "[+-]?" + "[1-9]?[0-9]" + "(\\.[0-9]+)" + "(\u00B0)?"; private static final String COORD_DECIMAL_LONGITUDE = "[+-]?" + "(1)?[0-9]?[0-9]" + "(\\.[0-9]+)" + "(\u00B0)?"; private static final String COORD_DECIMAL_PATTERN = COORD_DECIMAL_LATITUDE + "(\\s)*" + "," + "(\\s)*" + COORD_DECIMAL_LONGITUDE; private static final Pattern COORD_PATTERN = Pattern.compile(COORD_DEGREES_PATTERN + "|" + COORD_DECIMAL_PATTERN); private static final String NANP_ALLOWED_SYMBOLS = "()+-*#."; private static final int NANP_MIN_DIGITS = 7; private static final int NANP_MAX_DIGITS = 11; Loading Loading @@ -1591,11 +1645,19 @@ public class Utils { /** * Replaces stretches of text that look like addresses and phone numbers with clickable * links. If lastDitchGeo is true, then if no links are found in the textview, the entire * string will be converted to a single geo link. * string will be converted to a single geo link. Any spans that may have previously been * in the text will be cleared out. * <p> * This is really just an enhanced version of Linkify.addLinks(). * * @param text - The string to search for links. * @param lastDitchGeo - If no links are found, turn the entire string into one geo link. * @return Spannable object containing the list of URL spans found. */ public static void linkifyTextView(TextView textView, boolean lastDitchGeo) { public static Spannable extendedLinkify(String text, boolean lastDitchGeo) { // We use a copy of the string argument so it's available for later if necessary. Spannable spanText = SpannableString.valueOf(text); /* * If the text includes a street address like "1600 Amphitheater Parkway, 94043", * the current Linkify code will identify "94043" as a phone number and invite Loading @@ -1604,33 +1666,26 @@ public class Utils { */ String defaultPhoneRegion = System.getProperty("user.region", "US"); if (!defaultPhoneRegion.equals("US")) { // We make a copy of the spannable so that we can replace it back // into the textview if the first linkify pass does not work. // This will still maintain any spans already present in the textView argument. CharSequence origText = Spannable.Factory.getInstance().newSpannable(textView.getText()); Linkify.addLinks(textView, Linkify.ALL); Linkify.addLinks(spanText, Linkify.ALL); // If Linkify links the entire text, use that result. if (textView.getText() instanceof Spannable) { Spannable spanText = (Spannable) textView.getText(); URLSpan[] spans = spanText.getSpans(0, spanText.length(), URLSpan.class); if (spans.length == 1) { int linkStart = spanText.getSpanStart(spans[0]); int linkEnd = spanText.getSpanEnd(spans[0]); if (linkStart <= indexFirstNonWhitespaceChar(origText) && linkEnd >= indexLastNonWhitespaceChar(origText) + 1) { return; } if (linkStart <= indexFirstNonWhitespaceChar(spanText) && linkEnd >= indexLastNonWhitespaceChar(spanText) + 1) { return spanText; } } // Otherwise default to geo. textView.setText(origText); if (lastDitchGeo) { Linkify.addLinks(textView, mWildcardPattern, "geo:0,0?q="); // Otherwise, to be cautious and to try to prevent false positives, reset the spannable. spanText = SpannableString.valueOf(text); // If lastDitchGeo is true, default the entire string to geo. if (lastDitchGeo && !text.isEmpty()) { Linkify.addLinks(spanText, mWildcardPattern, "geo:0,0?q="); } return; return spanText; } /* Loading @@ -1653,36 +1708,45 @@ public class Utils { * Ideally we'd use the external/libphonenumber routines, but those aren't available * to unbundled applications. */ boolean linkifyFoundLinks = Linkify.addLinks(textView, boolean linkifyFoundLinks = Linkify.addLinks(spanText, Linkify.ALL & ~(Linkify.PHONE_NUMBERS)); /* * Search for phone numbers. * * Some URIs contain strings of digits that look like phone numbers. If both the URI * scanner and the phone number scanner find them, we want the URI link to win. Since * the URI scanner runs first, we just need to avoid creating overlapping spans. * Get a list of any spans created by Linkify, for the coordinate overlapping span check. */ CharSequence text = textView.getText(); int[] phoneSequences = findNanpPhoneNumbers(text); URLSpan[] existingSpans = spanText.getSpans(0, spanText.length(), URLSpan.class); /* * If the contents of the TextView are already Spannable (which will be the case if * Linkify found stuff, but might not be otherwise), we can just add annotations * to what's there. If it's not, and we find phone numbers, we need to convert it to * a Spannable form. (This mimics the behavior of Linkable.addLinks().) * Check for coordinates. * This must be done before phone numbers because longitude may look like a phone number. */ Spannable spanText; if (text instanceof SpannableString) { spanText = (SpannableString) text; } else { spanText = SpannableString.valueOf(text); Matcher coordMatcher = COORD_PATTERN.matcher(spanText); int coordCount = 0; while (coordMatcher.find()) { int start = coordMatcher.start(); int end = coordMatcher.end(); if (spanWillOverlap(spanText, existingSpans, start, end)) { continue; } URLSpan span = new URLSpan("geo:0,0?q=" + coordMatcher.group()); spanText.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); coordCount++; } /* * Get a list of any spans created by Linkify, for the overlapping span check. * Update the list of existing spans, for the phone number overlapping span check. */ URLSpan[] existingSpans = spanText.getSpans(0, spanText.length(), URLSpan.class); existingSpans = spanText.getSpans(0, spanText.length(), URLSpan.class); /* * Search for phone numbers. * * Some URIs contain strings of digits that look like phone numbers. If both the URI * scanner and the phone number scanner find them, we want the URI link to win. Since * the URI scanner runs first, we just need to avoid creating overlapping spans. */ int[] phoneSequences = findNanpPhoneNumbers(text); /* * Insert spans for the numbers we found. We generate "tel:" URIs. Loading @@ -1693,10 +1757,6 @@ public class Utils { int end = phoneSequences[match*2 + 1]; if (spanWillOverlap(spanText, existingSpans, start, end)) { if (Log.isLoggable(TAG, Log.VERBOSE)) { CharSequence seq = text.subSequence(start, end); Log.v(TAG, "Not linkifying " + seq + " as phone number due to overlap"); } continue; } Loading @@ -1722,29 +1782,18 @@ public class Utils { phoneCount++; } if (phoneCount != 0) { // If we had to "upgrade" to Spannable, store the object into the TextView. if (spanText != text) { textView.setText(spanText); } // Linkify.addLinks() sets the TextView movement method if it finds any links. We // want to do the same here. (This is cloned from Linkify.addLinkMovementMethod().) MovementMethod mm = textView.getMovementMethod(); if ((mm == null) || !(mm instanceof LinkMovementMethod)) { if (textView.getLinksClickable()) { textView.setMovementMethod(LinkMovementMethod.getInstance()); } } } if (lastDitchGeo && !linkifyFoundLinks && phoneCount == 0) { /* * If lastDitchGeo, and no other links have been found, set the entire string as a geo link. */ if (lastDitchGeo && !text.isEmpty() && !linkifyFoundLinks && phoneCount == 0 && coordCount == 0) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "No linkification matches, using geo default"); } Linkify.addLinks(textView, mWildcardPattern, "geo:0,0?q="); Linkify.addLinks(spanText, mWildcardPattern, "geo:0,0?q="); } return spanText; } private static int indexFirstNonWhitespaceChar(CharSequence str) { Loading Loading @@ -1908,6 +1957,10 @@ public class Utils { int existingEnd = spanText.getSpanEnd(span); if ((start >= existingStart && start < existingEnd) || end > existingStart && end <= existingEnd) { if (Log.isLoggable(TAG, Log.VERBOSE)) { CharSequence seq = spanText.subSequence(start, end); Log.v(TAG, "Not linkifying " + seq + " as phone number due to overlap"); } return true; } } Loading src/com/android/calendar/alerts/AlertReceiver.java +3 −10 Original line number Diff line number Diff line Loading @@ -771,20 +771,13 @@ public class AlertReceiver extends BroadcastReceiver { return new URLSpan[0]; } TextView locationTV = new TextView(context); locationTV.setText(location); Utils.linkifyTextView(locationTV, false); CharSequence text = locationTV.getText(); Spannable text = Utils.extendedLinkify(location, false); // The linkify method should have found at least one link, at the very least. // If no smart links were found, it should have set the whole string as a geo link. if (text instanceof Spannable) { Spannable spanText = (SpannableString) locationTV.getText(); URLSpan[] urlSpans = spanText.getSpans(0, spanText.length(), URLSpan.class); URLSpan[] urlSpans = text.getSpans(0, text.length(), URLSpan.class); return urlSpans; } } // If no links were found or location was empty, return an empty list. return new URLSpan[0]; Loading tests/src/com/android/calendar/UtilsTests.java +124 −1 Original line number Diff line number Diff line Loading @@ -16,18 +16,34 @@ package com.android.calendar; import com.android.calendar.Utils; import com.android.calendar.CalendarUtils.TimeZoneUtils; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.Resources.NotFoundException; import android.database.MatrixCursor; import android.provider.CalendarContract.CalendarCache; import android.test.mock.MockResources; import android.test.suitebuilder.annotation.SmallTest; import android.test.suitebuilder.annotation.Smoke; import android.text.Spannable; import android.text.SpannableString; import android.text.format.Time; import android.text.style.URLSpan; import android.util.DisplayMetrics; import android.util.Log; import android.view.Display; import android.view.View; import android.view.WindowManager; import android.view.ViewGroup.LayoutParams; import android.widget.TextView; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.Locale; import junit.framework.TestCase; Loading Loading @@ -112,6 +128,13 @@ public class UtilsTests extends TestCase { config.locale = Locale.getDefault(); return config; } @Override public DisplayMetrics getDisplayMetrics(){ DisplayMetrics metrics = new DisplayMetrics(); metrics.density = 2.0f; return metrics; } } private static long createTimeInMillis(int second, int minute, int hour, int monthDay, Loading Loading @@ -152,6 +175,7 @@ public class UtilsTests extends TestCase { dbUtils.getContentResolver().addProvider("settings", dbUtils.getContentProvider()); dbUtils.getContentResolver().addProvider(CalendarCache.URI.getAuthority(), dbUtils.getContentProvider()); setTimezone(DEFAULT_TIMEZONE); } Loading Loading @@ -378,6 +402,105 @@ public class UtilsTests extends TestCase { } } /** * Tests the linkify section of event locations. */ @SmallTest public void testExtendedLinkify() { final URLSpan[] NO_LINKS = new URLSpan[] {}; URLSpan span_tel01 = new URLSpan("tel:6505551234"); URLSpan span_tel02 = new URLSpan("tel:5555678"); URLSpan span_tel03 = new URLSpan("tel:+16505551234"); URLSpan span_tel04 = new URLSpan("tel:16505551234"); URLSpan span_web = new URLSpan("http://www.google.com"); URLSpan span_geo01 = new URLSpan("geo:0,0?q=1600+Amphitheatre+Parkway%2C+Mountain+View+CA+94043"); URLSpan span_geo02 = new URLSpan("geo:0,0?q=37.422081°, -122.084576°"); URLSpan span_geo03 = new URLSpan("geo:0,0?q=37.422081,-122.084576"); URLSpan span_geo04 = new URLSpan("geo:0,0?q=+37°25'19.49\", -122°5'4.47\""); URLSpan span_geo05 = new URLSpan("geo:0,0?q=37°25'19.49\"N, 122°5'4.47\"W"); URLSpan span_geo06 = new URLSpan("geo:0,0?q=N 37° 25' 19.49\", W 122° 5' 4.47\""); URLSpan span_geo07 = new URLSpan("geo:0,0?q=non-specified address"); // First test without the last-ditch geo attempt. // Phone spans. findLinks("", NO_LINKS, false); findLinks("(650) 555-1234", new URLSpan[]{span_tel01}, false); findLinks("94043", NO_LINKS, false); findLinks("123456789012", NO_LINKS, false); findLinks("+1 (650) 555-1234", new URLSpan[]{span_tel03}, false); findLinks("(650) 555 1234", new URLSpan[]{span_tel01}, false); findLinks("1-650-555-1234", new URLSpan[]{span_tel04}, false); findLinks("*#650.555.1234#*!", new URLSpan[]{span_tel01}, false); findLinks("555.5678", new URLSpan[]{span_tel02}, false); // Web spans. findLinks("http://www.google.com", new URLSpan[]{span_web}, false); // Geo spans. findLinks("1600 Amphitheatre Parkway, Mountain View CA 94043", new URLSpan[]{span_geo01}, false); findLinks("37.422081°, -122.084576°", new URLSpan[]{span_geo02}, false); findLinks("37.422081,-122.084576", new URLSpan[]{span_geo03}, false); findLinks("+37°25'19.49\", -122°5'4.47\"", new URLSpan[]{span_geo04}, false); findLinks("37°25'19.49\"N, 122°5'4.47\"W", new URLSpan[]{span_geo05}, false); findLinks("N 37° 25' 19.49\", W 122° 5' 4.47\"", new URLSpan[]{span_geo06}, false); // Multiple spans. findLinks("(650) 555-1234 1600 Amphitheatre Parkway, Mountain View CA 94043", new URLSpan[]{span_tel01, span_geo01}, false); findLinks("(650) 555-1234, 555-5678", new URLSpan[]{span_tel01, span_tel02}, false); // Now test using the last-ditch geo attempt. findLinks("", NO_LINKS, true); findLinks("(650) 555-1234", new URLSpan[]{span_tel01}, true); findLinks("http://www.google.com", new URLSpan[]{span_web}, true); findLinks("1600 Amphitheatre Parkway, Mountain View CA 94043", new URLSpan[]{span_geo01}, true); findLinks("37.422081°, -122.084576°", new URLSpan[]{span_geo02}, true); findLinks("37.422081,-122.084576", new URLSpan[]{span_geo03}, true); findLinks("+37°25'19.49\", -122°5'4.47\"", new URLSpan[]{span_geo04}, true); findLinks("37°25'19.49\"N, 122°5'4.47\"W", new URLSpan[]{span_geo05}, true); findLinks("N 37° 25' 19.49\", W 122° 5' 4.47\"", new URLSpan[]{span_geo06}, true); findLinks("non-specified address", new URLSpan[]{span_geo07}, true); } private static void findLinks(String text, URLSpan[] matches, boolean lastDitchGeo) { Spannable spanText = Utils.extendedLinkify(text, lastDitchGeo); URLSpan[] spansFound = spanText.getSpans(0, spanText.length(), URLSpan.class); assertEquals(matches.length, spansFound.length); // Make sure the expected matches list of URLs is the same as that returned by linkify. ArrayList<URLSpan> matchesArrayList = new ArrayList<URLSpan>(Arrays.asList(matches)); for (URLSpan spanFound : spansFound) { Iterator<URLSpan> matchesIt = matchesArrayList.iterator(); boolean removed = false; while (matchesIt.hasNext()) { URLSpan match = matchesIt.next(); if (match.getURL().equals(spanFound.getURL())) { matchesIt.remove(); removed = true; break; } } if (!removed) { // If a match was not found for the current spanFound, the lists aren't equal. fail("No match found for span: "+spanFound.getURL()); } } // As a sanity check, ensure the matches list is empty, as each item should have been // removed by going through the spans returned by linkify. assertTrue(matchesArrayList.isEmpty()); } @SmallTest public void testGetDisplayedDatetime_differentYear() { // 4/12/2000 5pm - 4/12/2000 6pm Loading Loading
src/com/android/calendar/EventInfoFragment.java +11 −1 Original line number Diff line number Diff line Loading @@ -1227,7 +1227,17 @@ public class EventInfoFragment extends DialogFragment implements OnCheckedChange textView.setAutoLinkMask(0); textView.setText(location.trim()); try { Utils.linkifyTextView(textView, true); textView.setText(Utils.extendedLinkify(textView.getText().toString(), true)); // Linkify.addLinks() sets the TextView movement method if it finds any links. // We must do the same here, in case linkify by itself did not find any. // (This is cloned from Linkify.addLinkMovementMethod().) MovementMethod mm = textView.getMovementMethod(); if ((mm == null) || !(mm instanceof LinkMovementMethod)) { if (textView.getLinksClickable()) { textView.setMovementMethod(LinkMovementMethod.getInstance()); } } } catch (Exception ex) { // unexpected Log.e(TAG, "Linkification failed", ex); Loading
src/com/android/calendar/Utils.java +118 −65 Original line number Diff line number Diff line Loading @@ -70,6 +70,7 @@ import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; public class Utils { Loading Loading @@ -140,6 +141,59 @@ public class Utils { private static String sVersion = null; private static final Pattern mWildcardPattern = Pattern.compile("^.*$"); /** * A coordinate must be of the following form for Google Maps to correctly use it: * Latitude, Longitude * * This may be in decimal form: * Latitude: {-90 to 90} * Longitude: {-180 to 180} * * Or, in degrees, minutes, and seconds: * Latitude: {-90 to 90}° {0 to 59}' {0 to 59}" * Latitude: {-180 to 180}° {0 to 59}' {0 to 59}" * + or - degrees may also be represented with N or n, S or s for latitude, and with * E or e, W or w for longitude, where the direction may either precede or follow the value. * * Some examples of coordinates that will be accepted by the regex: * 37.422081°, -122.084576° * 37.422081,-122.084576 * +37°25'19.49", -122°5'4.47" * 37°25'19.49"N, 122°5'4.47"W * N 37° 25' 19.49", W 122° 5' 4.47" **/ private static final String COORD_DEGREES_LATITUDE = "([-+NnSs]" + "(\\s)*)?" + "[1-9]?[0-9](\u00B0)" + "(\\s)*" + "([1-5]?[0-9]\')?" + "(\\s)*" + "([1-5]?[0-9]" + "(\\.[0-9]+)?\")?" + "((\\s)*" + "[NnSs])?"; private static final String COORD_DEGREES_LONGITUDE = "([-+EeWw]" + "(\\s)*)?" + "(1)?[0-9]?[0-9](\u00B0)" + "(\\s)*" + "([1-5]?[0-9]\')?" + "(\\s)*" + "([1-5]?[0-9]" + "(\\.[0-9]+)?\")?" + "((\\s)*" + "[EeWw])?"; private static final String COORD_DEGREES_PATTERN = COORD_DEGREES_LATITUDE + "(\\s)*" + "," + "(\\s)*" + COORD_DEGREES_LONGITUDE; private static final String COORD_DECIMAL_LATITUDE = "[+-]?" + "[1-9]?[0-9]" + "(\\.[0-9]+)" + "(\u00B0)?"; private static final String COORD_DECIMAL_LONGITUDE = "[+-]?" + "(1)?[0-9]?[0-9]" + "(\\.[0-9]+)" + "(\u00B0)?"; private static final String COORD_DECIMAL_PATTERN = COORD_DECIMAL_LATITUDE + "(\\s)*" + "," + "(\\s)*" + COORD_DECIMAL_LONGITUDE; private static final Pattern COORD_PATTERN = Pattern.compile(COORD_DEGREES_PATTERN + "|" + COORD_DECIMAL_PATTERN); private static final String NANP_ALLOWED_SYMBOLS = "()+-*#."; private static final int NANP_MIN_DIGITS = 7; private static final int NANP_MAX_DIGITS = 11; Loading Loading @@ -1591,11 +1645,19 @@ public class Utils { /** * Replaces stretches of text that look like addresses and phone numbers with clickable * links. If lastDitchGeo is true, then if no links are found in the textview, the entire * string will be converted to a single geo link. * string will be converted to a single geo link. Any spans that may have previously been * in the text will be cleared out. * <p> * This is really just an enhanced version of Linkify.addLinks(). * * @param text - The string to search for links. * @param lastDitchGeo - If no links are found, turn the entire string into one geo link. * @return Spannable object containing the list of URL spans found. */ public static void linkifyTextView(TextView textView, boolean lastDitchGeo) { public static Spannable extendedLinkify(String text, boolean lastDitchGeo) { // We use a copy of the string argument so it's available for later if necessary. Spannable spanText = SpannableString.valueOf(text); /* * If the text includes a street address like "1600 Amphitheater Parkway, 94043", * the current Linkify code will identify "94043" as a phone number and invite Loading @@ -1604,33 +1666,26 @@ public class Utils { */ String defaultPhoneRegion = System.getProperty("user.region", "US"); if (!defaultPhoneRegion.equals("US")) { // We make a copy of the spannable so that we can replace it back // into the textview if the first linkify pass does not work. // This will still maintain any spans already present in the textView argument. CharSequence origText = Spannable.Factory.getInstance().newSpannable(textView.getText()); Linkify.addLinks(textView, Linkify.ALL); Linkify.addLinks(spanText, Linkify.ALL); // If Linkify links the entire text, use that result. if (textView.getText() instanceof Spannable) { Spannable spanText = (Spannable) textView.getText(); URLSpan[] spans = spanText.getSpans(0, spanText.length(), URLSpan.class); if (spans.length == 1) { int linkStart = spanText.getSpanStart(spans[0]); int linkEnd = spanText.getSpanEnd(spans[0]); if (linkStart <= indexFirstNonWhitespaceChar(origText) && linkEnd >= indexLastNonWhitespaceChar(origText) + 1) { return; } if (linkStart <= indexFirstNonWhitespaceChar(spanText) && linkEnd >= indexLastNonWhitespaceChar(spanText) + 1) { return spanText; } } // Otherwise default to geo. textView.setText(origText); if (lastDitchGeo) { Linkify.addLinks(textView, mWildcardPattern, "geo:0,0?q="); // Otherwise, to be cautious and to try to prevent false positives, reset the spannable. spanText = SpannableString.valueOf(text); // If lastDitchGeo is true, default the entire string to geo. if (lastDitchGeo && !text.isEmpty()) { Linkify.addLinks(spanText, mWildcardPattern, "geo:0,0?q="); } return; return spanText; } /* Loading @@ -1653,36 +1708,45 @@ public class Utils { * Ideally we'd use the external/libphonenumber routines, but those aren't available * to unbundled applications. */ boolean linkifyFoundLinks = Linkify.addLinks(textView, boolean linkifyFoundLinks = Linkify.addLinks(spanText, Linkify.ALL & ~(Linkify.PHONE_NUMBERS)); /* * Search for phone numbers. * * Some URIs contain strings of digits that look like phone numbers. If both the URI * scanner and the phone number scanner find them, we want the URI link to win. Since * the URI scanner runs first, we just need to avoid creating overlapping spans. * Get a list of any spans created by Linkify, for the coordinate overlapping span check. */ CharSequence text = textView.getText(); int[] phoneSequences = findNanpPhoneNumbers(text); URLSpan[] existingSpans = spanText.getSpans(0, spanText.length(), URLSpan.class); /* * If the contents of the TextView are already Spannable (which will be the case if * Linkify found stuff, but might not be otherwise), we can just add annotations * to what's there. If it's not, and we find phone numbers, we need to convert it to * a Spannable form. (This mimics the behavior of Linkable.addLinks().) * Check for coordinates. * This must be done before phone numbers because longitude may look like a phone number. */ Spannable spanText; if (text instanceof SpannableString) { spanText = (SpannableString) text; } else { spanText = SpannableString.valueOf(text); Matcher coordMatcher = COORD_PATTERN.matcher(spanText); int coordCount = 0; while (coordMatcher.find()) { int start = coordMatcher.start(); int end = coordMatcher.end(); if (spanWillOverlap(spanText, existingSpans, start, end)) { continue; } URLSpan span = new URLSpan("geo:0,0?q=" + coordMatcher.group()); spanText.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); coordCount++; } /* * Get a list of any spans created by Linkify, for the overlapping span check. * Update the list of existing spans, for the phone number overlapping span check. */ URLSpan[] existingSpans = spanText.getSpans(0, spanText.length(), URLSpan.class); existingSpans = spanText.getSpans(0, spanText.length(), URLSpan.class); /* * Search for phone numbers. * * Some URIs contain strings of digits that look like phone numbers. If both the URI * scanner and the phone number scanner find them, we want the URI link to win. Since * the URI scanner runs first, we just need to avoid creating overlapping spans. */ int[] phoneSequences = findNanpPhoneNumbers(text); /* * Insert spans for the numbers we found. We generate "tel:" URIs. Loading @@ -1693,10 +1757,6 @@ public class Utils { int end = phoneSequences[match*2 + 1]; if (spanWillOverlap(spanText, existingSpans, start, end)) { if (Log.isLoggable(TAG, Log.VERBOSE)) { CharSequence seq = text.subSequence(start, end); Log.v(TAG, "Not linkifying " + seq + " as phone number due to overlap"); } continue; } Loading @@ -1722,29 +1782,18 @@ public class Utils { phoneCount++; } if (phoneCount != 0) { // If we had to "upgrade" to Spannable, store the object into the TextView. if (spanText != text) { textView.setText(spanText); } // Linkify.addLinks() sets the TextView movement method if it finds any links. We // want to do the same here. (This is cloned from Linkify.addLinkMovementMethod().) MovementMethod mm = textView.getMovementMethod(); if ((mm == null) || !(mm instanceof LinkMovementMethod)) { if (textView.getLinksClickable()) { textView.setMovementMethod(LinkMovementMethod.getInstance()); } } } if (lastDitchGeo && !linkifyFoundLinks && phoneCount == 0) { /* * If lastDitchGeo, and no other links have been found, set the entire string as a geo link. */ if (lastDitchGeo && !text.isEmpty() && !linkifyFoundLinks && phoneCount == 0 && coordCount == 0) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "No linkification matches, using geo default"); } Linkify.addLinks(textView, mWildcardPattern, "geo:0,0?q="); Linkify.addLinks(spanText, mWildcardPattern, "geo:0,0?q="); } return spanText; } private static int indexFirstNonWhitespaceChar(CharSequence str) { Loading Loading @@ -1908,6 +1957,10 @@ public class Utils { int existingEnd = spanText.getSpanEnd(span); if ((start >= existingStart && start < existingEnd) || end > existingStart && end <= existingEnd) { if (Log.isLoggable(TAG, Log.VERBOSE)) { CharSequence seq = spanText.subSequence(start, end); Log.v(TAG, "Not linkifying " + seq + " as phone number due to overlap"); } return true; } } Loading
src/com/android/calendar/alerts/AlertReceiver.java +3 −10 Original line number Diff line number Diff line Loading @@ -771,20 +771,13 @@ public class AlertReceiver extends BroadcastReceiver { return new URLSpan[0]; } TextView locationTV = new TextView(context); locationTV.setText(location); Utils.linkifyTextView(locationTV, false); CharSequence text = locationTV.getText(); Spannable text = Utils.extendedLinkify(location, false); // The linkify method should have found at least one link, at the very least. // If no smart links were found, it should have set the whole string as a geo link. if (text instanceof Spannable) { Spannable spanText = (SpannableString) locationTV.getText(); URLSpan[] urlSpans = spanText.getSpans(0, spanText.length(), URLSpan.class); URLSpan[] urlSpans = text.getSpans(0, text.length(), URLSpan.class); return urlSpans; } } // If no links were found or location was empty, return an empty list. return new URLSpan[0]; Loading
tests/src/com/android/calendar/UtilsTests.java +124 −1 Original line number Diff line number Diff line Loading @@ -16,18 +16,34 @@ package com.android.calendar; import com.android.calendar.Utils; import com.android.calendar.CalendarUtils.TimeZoneUtils; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.Resources.NotFoundException; import android.database.MatrixCursor; import android.provider.CalendarContract.CalendarCache; import android.test.mock.MockResources; import android.test.suitebuilder.annotation.SmallTest; import android.test.suitebuilder.annotation.Smoke; import android.text.Spannable; import android.text.SpannableString; import android.text.format.Time; import android.text.style.URLSpan; import android.util.DisplayMetrics; import android.util.Log; import android.view.Display; import android.view.View; import android.view.WindowManager; import android.view.ViewGroup.LayoutParams; import android.widget.TextView; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.Locale; import junit.framework.TestCase; Loading Loading @@ -112,6 +128,13 @@ public class UtilsTests extends TestCase { config.locale = Locale.getDefault(); return config; } @Override public DisplayMetrics getDisplayMetrics(){ DisplayMetrics metrics = new DisplayMetrics(); metrics.density = 2.0f; return metrics; } } private static long createTimeInMillis(int second, int minute, int hour, int monthDay, Loading Loading @@ -152,6 +175,7 @@ public class UtilsTests extends TestCase { dbUtils.getContentResolver().addProvider("settings", dbUtils.getContentProvider()); dbUtils.getContentResolver().addProvider(CalendarCache.URI.getAuthority(), dbUtils.getContentProvider()); setTimezone(DEFAULT_TIMEZONE); } Loading Loading @@ -378,6 +402,105 @@ public class UtilsTests extends TestCase { } } /** * Tests the linkify section of event locations. */ @SmallTest public void testExtendedLinkify() { final URLSpan[] NO_LINKS = new URLSpan[] {}; URLSpan span_tel01 = new URLSpan("tel:6505551234"); URLSpan span_tel02 = new URLSpan("tel:5555678"); URLSpan span_tel03 = new URLSpan("tel:+16505551234"); URLSpan span_tel04 = new URLSpan("tel:16505551234"); URLSpan span_web = new URLSpan("http://www.google.com"); URLSpan span_geo01 = new URLSpan("geo:0,0?q=1600+Amphitheatre+Parkway%2C+Mountain+View+CA+94043"); URLSpan span_geo02 = new URLSpan("geo:0,0?q=37.422081°, -122.084576°"); URLSpan span_geo03 = new URLSpan("geo:0,0?q=37.422081,-122.084576"); URLSpan span_geo04 = new URLSpan("geo:0,0?q=+37°25'19.49\", -122°5'4.47\""); URLSpan span_geo05 = new URLSpan("geo:0,0?q=37°25'19.49\"N, 122°5'4.47\"W"); URLSpan span_geo06 = new URLSpan("geo:0,0?q=N 37° 25' 19.49\", W 122° 5' 4.47\""); URLSpan span_geo07 = new URLSpan("geo:0,0?q=non-specified address"); // First test without the last-ditch geo attempt. // Phone spans. findLinks("", NO_LINKS, false); findLinks("(650) 555-1234", new URLSpan[]{span_tel01}, false); findLinks("94043", NO_LINKS, false); findLinks("123456789012", NO_LINKS, false); findLinks("+1 (650) 555-1234", new URLSpan[]{span_tel03}, false); findLinks("(650) 555 1234", new URLSpan[]{span_tel01}, false); findLinks("1-650-555-1234", new URLSpan[]{span_tel04}, false); findLinks("*#650.555.1234#*!", new URLSpan[]{span_tel01}, false); findLinks("555.5678", new URLSpan[]{span_tel02}, false); // Web spans. findLinks("http://www.google.com", new URLSpan[]{span_web}, false); // Geo spans. findLinks("1600 Amphitheatre Parkway, Mountain View CA 94043", new URLSpan[]{span_geo01}, false); findLinks("37.422081°, -122.084576°", new URLSpan[]{span_geo02}, false); findLinks("37.422081,-122.084576", new URLSpan[]{span_geo03}, false); findLinks("+37°25'19.49\", -122°5'4.47\"", new URLSpan[]{span_geo04}, false); findLinks("37°25'19.49\"N, 122°5'4.47\"W", new URLSpan[]{span_geo05}, false); findLinks("N 37° 25' 19.49\", W 122° 5' 4.47\"", new URLSpan[]{span_geo06}, false); // Multiple spans. findLinks("(650) 555-1234 1600 Amphitheatre Parkway, Mountain View CA 94043", new URLSpan[]{span_tel01, span_geo01}, false); findLinks("(650) 555-1234, 555-5678", new URLSpan[]{span_tel01, span_tel02}, false); // Now test using the last-ditch geo attempt. findLinks("", NO_LINKS, true); findLinks("(650) 555-1234", new URLSpan[]{span_tel01}, true); findLinks("http://www.google.com", new URLSpan[]{span_web}, true); findLinks("1600 Amphitheatre Parkway, Mountain View CA 94043", new URLSpan[]{span_geo01}, true); findLinks("37.422081°, -122.084576°", new URLSpan[]{span_geo02}, true); findLinks("37.422081,-122.084576", new URLSpan[]{span_geo03}, true); findLinks("+37°25'19.49\", -122°5'4.47\"", new URLSpan[]{span_geo04}, true); findLinks("37°25'19.49\"N, 122°5'4.47\"W", new URLSpan[]{span_geo05}, true); findLinks("N 37° 25' 19.49\", W 122° 5' 4.47\"", new URLSpan[]{span_geo06}, true); findLinks("non-specified address", new URLSpan[]{span_geo07}, true); } private static void findLinks(String text, URLSpan[] matches, boolean lastDitchGeo) { Spannable spanText = Utils.extendedLinkify(text, lastDitchGeo); URLSpan[] spansFound = spanText.getSpans(0, spanText.length(), URLSpan.class); assertEquals(matches.length, spansFound.length); // Make sure the expected matches list of URLs is the same as that returned by linkify. ArrayList<URLSpan> matchesArrayList = new ArrayList<URLSpan>(Arrays.asList(matches)); for (URLSpan spanFound : spansFound) { Iterator<URLSpan> matchesIt = matchesArrayList.iterator(); boolean removed = false; while (matchesIt.hasNext()) { URLSpan match = matchesIt.next(); if (match.getURL().equals(spanFound.getURL())) { matchesIt.remove(); removed = true; break; } } if (!removed) { // If a match was not found for the current spanFound, the lists aren't equal. fail("No match found for span: "+spanFound.getURL()); } } // As a sanity check, ensure the matches list is empty, as each item should have been // removed by going through the spans returned by linkify. assertTrue(matchesArrayList.isEmpty()); } @SmallTest public void testGetDisplayedDatetime_differentYear() { // 4/12/2000 5pm - 4/12/2000 6pm Loading