diff --git a/android/car-app/src/main/java/com/stadiamaps/ferrostar/car/app/intent/NavigationIntentParser.kt b/android/car-app/src/main/java/com/stadiamaps/ferrostar/car/app/intent/NavigationIntentParser.kt index 7590d17d5..93f25e8eb 100644 --- a/android/car-app/src/main/java/com/stadiamaps/ferrostar/car/app/intent/NavigationIntentParser.kt +++ b/android/car-app/src/main/java/com/stadiamaps/ferrostar/car/app/intent/NavigationIntentParser.kt @@ -27,16 +27,23 @@ open class NavigationIntentParser { } /** Parses a navigation [Uri] into a [NavigationDestination], or null if unrecognized. */ - open fun parseUri(uri: Uri): NavigationDestination? = - when (uri.scheme) { - "geo" -> - parseGeoSsp( - coordString = uri.schemeSpecificPart?.substringBefore('?').orEmpty(), - query = uri.getQueryParameter("q")) - "google.navigation" -> - uri.getQueryParameter("q")?.let { parseGoogleNavigationSsp(it) } - else -> null - } + open fun parseUri(uri: Uri): NavigationDestination? { + // Uri.getQueryParameter() throws UnsupportedOperationException on opaque URIs (i.e. URIs + // without an authority component, such as geo: and google.navigation:). Parse the + // scheme-specific part directly instead. + val ssp = uri.schemeSpecificPart ?: return null + return when (uri.scheme) { + "geo" -> + parseGeoSsp( + coordString = ssp.substringBefore('?'), + query = ssp.substringAfter("?q=", "").ifEmpty { null }?.let { decodeQueryValue(it) }) + "google.navigation" -> + ssp.substringAfter("q=", "").ifEmpty { null }?.let { + parseGoogleNavigationSsp(decodeQueryValue(it)) + } + else -> null + } + } companion object { /** @@ -72,6 +79,15 @@ open class NavigationIntentParser { } } + /** + * Decodes a query parameter value extracted from an opaque URI's scheme-specific part. + * + * Handles both percent-encoding and `+` as space, matching the behavior of + * [Uri.getQueryParameter] on hierarchical URIs. + */ + internal fun decodeQueryValue(encoded: String): String = + Uri.decode(encoded.replace("+", "%20")) + internal fun parseCoordinates(str: String): GeographicCoordinate? { // limit=3 so altitude (geo:lat,lng,alt per RFC 5870) is captured and ignored val parts = str.split(",", limit = 3) diff --git a/android/car-app/src/test/java/com/stadiamaps/ferrostar/car/app/NavigationIntentParserTest.kt b/android/car-app/src/test/java/com/stadiamaps/ferrostar/car/app/NavigationIntentParserTest.kt index 2bfbaf841..622e258fe 100644 --- a/android/car-app/src/test/java/com/stadiamaps/ferrostar/car/app/NavigationIntentParserTest.kt +++ b/android/car-app/src/test/java/com/stadiamaps/ferrostar/car/app/NavigationIntentParserTest.kt @@ -1,5 +1,6 @@ package com.stadiamaps.ferrostar.car.app +import android.net.Uri import com.stadiamaps.ferrostar.car.app.intent.NavigationDestination import com.stadiamaps.ferrostar.car.app.intent.NavigationIntentParser import org.junit.Assert.assertEquals @@ -9,6 +10,76 @@ import org.junit.Test class NavigationIntentParserTest { + // parseUri — exercises opaque URI parsing without Uri.getQueryParameter() + + @Test + fun `parseUri geo coordinates`() { + val result = NavigationIntentParser().parseUri(Uri.parse("geo:37.81,-122.42")) + assertNotNull(result) + assertEquals(37.81, result!!.latitude!!, 0.0001) + assertEquals(-122.42, result.longitude!!, 0.0001) + assertNull(result.query) + } + + @Test + fun `parseUri geo coordinates with altitude`() { + val result = NavigationIntentParser().parseUri(Uri.parse("geo:37.81,-122.42,100")) + assertNotNull(result) + assertEquals(37.81, result!!.latitude!!, 0.0001) + assertEquals(-122.42, result.longitude!!, 0.0001) + } + + @Test + fun `parseUri geo query only`() { + val result = NavigationIntentParser().parseUri(Uri.parse("geo:0,0?q=coffee+shops")) + assertNotNull(result) + assertNull(result!!.latitude) + assertNull(result.longitude) + assertEquals("coffee shops", result.query) + } + + @Test + fun `parseUri geo percent-encoded query`() { + val result = NavigationIntentParser().parseUri(Uri.parse("geo:0,0?q=Caf%C3%A9%20Roma")) + assertNotNull(result) + assertEquals("Café Roma", result!!.query) + } + + @Test + fun `parseUri google navigation coordinates`() { + val result = NavigationIntentParser().parseUri(Uri.parse("google.navigation:q=37.81,-122.42")) + assertNotNull(result) + assertEquals(37.81, result!!.latitude!!, 0.0001) + assertEquals(-122.42, result.longitude!!, 0.0001) + assertNull(result.query) + } + + @Test + fun `parseUri google navigation place name with plus encoding`() { + val result = + NavigationIntentParser().parseUri(Uri.parse("google.navigation:q=Golden+Gate+Bridge")) + assertNotNull(result) + assertNull(result!!.latitude) + assertEquals("Golden Gate Bridge", result.query) + } + + @Test + fun `parseUri unrecognized scheme returns null`() { + assertNull(NavigationIntentParser().parseUri(Uri.parse("https://example.com"))) + } + + // decodeQueryValue + + @Test + fun `decodeQueryValue converts plus to space`() { + assertEquals("Golden Gate Bridge", NavigationIntentParser.decodeQueryValue("Golden+Gate+Bridge")) + } + + @Test + fun `decodeQueryValue handles percent encoding`() { + assertEquals("Café", NavigationIntentParser.decodeQueryValue("Caf%C3%A9")) + } + // parseGeoSsp @Test