Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
Loading