Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 17dfc61e authored by Sara Ting's avatar Sara Ting Committed by Android (Google) Code Review
Browse files

Merge "Allow coordinates in event location." into ics-ub-calendar-burgundy

parents dab989bf 29dc76a4
Loading
Loading
Loading
Loading
+11 −1
Original line number Diff line number Diff line
@@ -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);
+118 −65
Original line number Diff line number Diff line
@@ -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 {
@@ -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;
@@ -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
@@ -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;
        }

        /*
@@ -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.
@@ -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;
            }

@@ -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) {
@@ -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;
            }
        }
+3 −10
Original line number Diff line number Diff line
@@ -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];
+124 −1
Original line number Diff line number Diff line
@@ -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;
@@ -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,
@@ -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);
    }

@@ -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