diff --git a/core/src/main/kotlin/dev/hotwire/core/turbo/visit/VisitProposal.kt b/core/src/main/kotlin/dev/hotwire/core/turbo/visit/VisitProposal.kt new file mode 100644 index 00000000..3b56019a --- /dev/null +++ b/core/src/main/kotlin/dev/hotwire/core/turbo/visit/VisitProposal.kt @@ -0,0 +1,11 @@ +package dev.hotwire.core.turbo.visit + +import android.os.Bundle +import dev.hotwire.core.turbo.config.PathConfigurationProperties + +data class VisitProposal( + val location: String, + val options: VisitOptions, + val properties: PathConfigurationProperties, + val bundle: Bundle? +) diff --git a/navigation-fragments/src/main/java/dev/hotwire/navigation/destinations/HotwireDestination.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/destinations/HotwireDestination.kt index 4b67dde4..d4d8f7b5 100644 --- a/navigation-fragments/src/main/java/dev/hotwire/navigation/destinations/HotwireDestination.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/destinations/HotwireDestination.kt @@ -11,6 +11,7 @@ import dev.hotwire.core.turbo.config.PathConfigurationProperties import dev.hotwire.core.turbo.config.context import dev.hotwire.core.turbo.nav.PresentationContext import dev.hotwire.core.turbo.visit.VisitAction +import dev.hotwire.core.turbo.visit.VisitProposal import dev.hotwire.navigation.fragments.HotwireFragmentDelegate import dev.hotwire.navigation.fragments.HotwireFragmentViewModel import dev.hotwire.navigation.navigator.Navigator @@ -115,7 +116,7 @@ interface HotwireDestination : BridgeDestination { * Return `null` to use the global [Router.RouteDecisionHandler] instances to determine * routing logic. */ - fun customRouteDecision(newLocation: String): Router.Decision? { + fun customRouteDecision(proposal: VisitProposal): Router.Decision? { return null } diff --git a/navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/Navigator.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/Navigator.kt index 55de0572..bc3790f3 100644 --- a/navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/Navigator.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/Navigator.kt @@ -13,6 +13,7 @@ import dev.hotwire.core.turbo.nav.PresentationContext import dev.hotwire.core.turbo.session.Session import dev.hotwire.core.turbo.visit.VisitAction import dev.hotwire.core.turbo.visit.VisitOptions +import dev.hotwire.core.turbo.visit.VisitProposal import dev.hotwire.navigation.activities.HotwireActivity import dev.hotwire.navigation.config.HotwireNavigation import dev.hotwire.navigation.destinations.HotwireDestination @@ -138,7 +139,7 @@ class Navigator( extras: FragmentNavigator.Extras? = null ) { - if (getRouteDecision(location) == Router.Decision.CANCEL) { + if (getRouteDecision(location, options, bundle) == Router.Decision.CANCEL) { return } @@ -423,11 +424,22 @@ class Navigator( return customNavigator?.navController ?: navController } - private fun getRouteDecision(location: String): Router.Decision { - val customDecision = currentDestination?.customRouteDecision(location) + private fun getRouteDecision( + location: String, + options: VisitOptions, + bundle: Bundle? + ): Router.Decision { + val proposal = VisitProposal( + location = location, + options = options, + properties = Hotwire.config.pathConfiguration.properties(location), + bundle = bundle + ) + + val customDecision = currentDestination?.customRouteDecision(proposal) val decision = customDecision ?: HotwireNavigation.router.decideRoute( - location = location, + proposal = proposal, configuration = configuration, activity = activity ) diff --git a/navigation-fragments/src/main/java/dev/hotwire/navigation/routing/AppNavigationRouteDecisionHandler.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/routing/AppNavigationRouteDecisionHandler.kt index 64eca1a1..5981438c 100644 --- a/navigation-fragments/src/main/java/dev/hotwire/navigation/routing/AppNavigationRouteDecisionHandler.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/routing/AppNavigationRouteDecisionHandler.kt @@ -1,6 +1,7 @@ package dev.hotwire.navigation.routing import androidx.core.net.toUri +import dev.hotwire.core.turbo.visit.VisitProposal import dev.hotwire.navigation.activities.HotwireActivity import dev.hotwire.navigation.navigator.NavigatorConfiguration @@ -11,14 +12,14 @@ class AppNavigationRouteDecisionHandler : Router.RouteDecisionHandler { override val name = "app-navigation" override fun matches( - location: String, + proposal: VisitProposal, configuration: NavigatorConfiguration ): Boolean { - return configuration.startLocation.toUri().host == location.toUri().host + return configuration.startLocation.toUri().host == proposal.location.toUri().host } override fun handle( - location: String, + proposal: VisitProposal, configuration: NavigatorConfiguration, activity: HotwireActivity ): Router.Decision { diff --git a/navigation-fragments/src/main/java/dev/hotwire/navigation/routing/BrowserTabRouteDecisionHandler.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/routing/BrowserTabRouteDecisionHandler.kt index eae3fd1a..00d8d4ef 100644 --- a/navigation-fragments/src/main/java/dev/hotwire/navigation/routing/BrowserTabRouteDecisionHandler.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/routing/BrowserTabRouteDecisionHandler.kt @@ -5,6 +5,7 @@ import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabsIntent import androidx.core.net.toUri import com.google.android.material.R +import dev.hotwire.core.turbo.visit.VisitProposal import dev.hotwire.navigation.activities.HotwireActivity import dev.hotwire.navigation.logging.logError import dev.hotwire.navigation.navigator.NavigatorConfiguration @@ -21,17 +22,17 @@ class BrowserTabRouteDecisionHandler : Router.RouteDecisionHandler { override val name = "browser-tab" override fun matches( - location: String, + proposal: VisitProposal, configuration: NavigatorConfiguration ): Boolean { - val locationUri = location.toUri() + val locationUri = proposal.location.toUri() return configuration.startLocation.toUri().host != locationUri.host && (locationUri.scheme?.lowercase() == "https" || locationUri.scheme?.lowercase() == "http") } override fun handle( - location: String, + proposal: VisitProposal, configuration: NavigatorConfiguration, activity: HotwireActivity ): Router.Decision { @@ -48,7 +49,7 @@ class BrowserTabRouteDecisionHandler : Router.RouteDecisionHandler { .setUrlBarHidingEnabled(false) .setDefaultColorSchemeParams(colorParams) .build() - .launchUrl(activity, location.toUri()) + .launchUrl(activity, proposal.location.toUri()) } catch (e: ActivityNotFoundException) { logError("BrowserTabRouteDecisionHandler", e) } diff --git a/navigation-fragments/src/main/java/dev/hotwire/navigation/routing/Router.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/routing/Router.kt index 6163668b..572659fa 100644 --- a/navigation-fragments/src/main/java/dev/hotwire/navigation/routing/Router.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/routing/Router.kt @@ -1,5 +1,6 @@ package dev.hotwire.navigation.routing +import dev.hotwire.core.turbo.visit.VisitProposal import dev.hotwire.navigation.activities.HotwireActivity import dev.hotwire.navigation.logging.logDebug import dev.hotwire.navigation.navigator.NavigatorConfiguration @@ -21,12 +22,12 @@ class Router(private val decisionHandlers: List) { val name: String /** - * Determines whether the location matches this decision handler. Use - * your own custom rules based on the location's domain, protocol, - * path, or any other factors. + * Determines whether the visit proposal matches this decision handler. Use + * your own custom rules based on the proposal's location domain, protocol, + * path, options, path configuration properties, or any other factors. */ fun matches( - location: String, + proposal: VisitProposal, configuration: NavigatorConfiguration ): Boolean @@ -36,7 +37,7 @@ class Router(private val decisionHandlers: List) { * [Decision.CANCEL]. */ fun handle( - location: String, + proposal: VisitProposal, configuration: NavigatorConfiguration, activity: HotwireActivity ): Decision @@ -55,22 +56,22 @@ class Router(private val decisionHandlers: List) { } internal fun decideRoute( - location: String, + proposal: VisitProposal, configuration: NavigatorConfiguration, activity: HotwireActivity ): Decision { decisionHandlers.forEach { handler -> - if (handler.matches(location, configuration)) { + if (handler.matches(proposal, configuration)) { logDebug("handlerMatch", listOf( "handler" to handler.name, - "location" to location + "proposal" to proposal )) - return handler.handle(location, configuration, activity) + return handler.handle(proposal, configuration, activity) } } - logDebug("noHandlerForLocation", location) + logDebug("noHandlerForProposal", proposal.toString()) return Decision.CANCEL } } diff --git a/navigation-fragments/src/main/java/dev/hotwire/navigation/routing/SystemNavigationRouteDecisionHandler.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/routing/SystemNavigationRouteDecisionHandler.kt index 603ac673..cd39fa01 100644 --- a/navigation-fragments/src/main/java/dev/hotwire/navigation/routing/SystemNavigationRouteDecisionHandler.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/routing/SystemNavigationRouteDecisionHandler.kt @@ -3,6 +3,7 @@ package dev.hotwire.navigation.routing import android.content.ActivityNotFoundException import android.content.Intent import androidx.core.net.toUri +import dev.hotwire.core.turbo.visit.VisitProposal import dev.hotwire.navigation.activities.HotwireActivity import dev.hotwire.navigation.logging.logError import dev.hotwire.navigation.navigator.NavigatorConfiguration @@ -14,18 +15,18 @@ class SystemNavigationRouteDecisionHandler : Router.RouteDecisionHandler { override val name = "system-navigation" override fun matches( - location: String, + proposal: VisitProposal, configuration: NavigatorConfiguration ): Boolean { - return configuration.startLocation.toUri().host != location.toUri().host + return configuration.startLocation.toUri().host != proposal.location.toUri().host } override fun handle( - location: String, + proposal: VisitProposal, configuration: NavigatorConfiguration, activity: HotwireActivity ): Router.Decision { - val intent = Intent(Intent.ACTION_VIEW, location.toUri()) + val intent = Intent(Intent.ACTION_VIEW, proposal.location.toUri()) try { activity.startActivity(intent) diff --git a/navigation-fragments/src/test/kotlin/dev/hotwire/navigation/routing/AppNavigationRouteDecisionHandlerTest.kt b/navigation-fragments/src/test/kotlin/dev/hotwire/navigation/routing/AppNavigationRouteDecisionHandlerTest.kt index 8b21501f..2eec1611 100644 --- a/navigation-fragments/src/test/kotlin/dev/hotwire/navigation/routing/AppNavigationRouteDecisionHandlerTest.kt +++ b/navigation-fragments/src/test/kotlin/dev/hotwire/navigation/routing/AppNavigationRouteDecisionHandlerTest.kt @@ -1,5 +1,8 @@ package dev.hotwire.navigation.routing +import dev.hotwire.core.turbo.config.PathConfigurationProperties +import dev.hotwire.core.turbo.visit.VisitOptions +import dev.hotwire.core.turbo.visit.VisitProposal import dev.hotwire.navigation.activities.HotwireActivity import dev.hotwire.navigation.navigator.NavigatorConfiguration import org.junit.Assert.assertEquals @@ -29,28 +32,35 @@ class AppNavigationRouteDecisionHandlerTest { @Test fun `matching result navigates`() { - val decision = route.handle(config.startLocation, config, activity) + val decision = route.handle(proposal(config.startLocation), config, activity) assertEquals(Router.Decision.NAVIGATE, decision) } @Test fun `url on app domain matches`() { val url = "https://my.app.com/page" - assertTrue(route.matches(url, config)) + assertTrue(route.matches(proposal(url), config)) } @Test fun `url without subdomain does not match`() { val url = "https://app.com/page" - assertFalse(route.matches(url, config)) + assertFalse(route.matches(proposal(url), config)) } @Test fun `masqueraded url does not match`() { val url = "https://app.my.com@fake.domain" - assertFalse(route.matches(url, config)) + assertFalse(route.matches(proposal(url), config)) } + private fun proposal(location: String) = VisitProposal( + location = location, + options = VisitOptions(), + properties = PathConfigurationProperties(), + bundle = null + ) + private class TestActivity : HotwireActivity() { override fun navigatorConfigurations() = emptyList() } diff --git a/navigation-fragments/src/test/kotlin/dev/hotwire/navigation/routing/BrowserTabRouteDecisionHandlerTest.kt b/navigation-fragments/src/test/kotlin/dev/hotwire/navigation/routing/BrowserTabRouteDecisionHandlerTest.kt index d5c56643..622da810 100644 --- a/navigation-fragments/src/test/kotlin/dev/hotwire/navigation/routing/BrowserTabRouteDecisionHandlerTest.kt +++ b/navigation-fragments/src/test/kotlin/dev/hotwire/navigation/routing/BrowserTabRouteDecisionHandlerTest.kt @@ -1,5 +1,8 @@ package dev.hotwire.navigation.routing +import dev.hotwire.core.turbo.config.PathConfigurationProperties +import dev.hotwire.core.turbo.visit.VisitOptions +import dev.hotwire.core.turbo.visit.VisitProposal import dev.hotwire.navigation.activities.HotwireActivity import dev.hotwire.navigation.navigator.NavigatorConfiguration import org.junit.Assert.assertEquals @@ -29,34 +32,41 @@ class BrowserTabRouteDecisionHandlerTest { @Test fun `matching result stops navigation`() { - val decision = route.handle("https://external.com/page", config, activity) + val decision = route.handle(proposal("https://external.com/page"), config, activity) assertEquals(Router.Decision.CANCEL, decision) } @Test fun `url on external domain matches`() { val url = "https://external.com/page" - assertTrue(route.matches(url, config)) + assertTrue(route.matches(proposal(url), config)) } @Test fun `url without subdomain matches`() { val url = "https://app.com/page" - assertTrue(route.matches(url, config)) + assertTrue(route.matches(proposal(url), config)) } @Test fun `url on app domain does not match`() { val url = "https://my.app.com/page" - assertFalse(route.matches(url, config)) + assertFalse(route.matches(proposal(url), config)) } @Test fun `non-http scheme does not match`() { val url = "sms:555-555-5555" - assertFalse(route.matches(url, config)) + assertFalse(route.matches(proposal(url), config)) } + private fun proposal(location: String) = VisitProposal( + location = location, + options = VisitOptions(), + properties = PathConfigurationProperties(), + bundle = null + ) + private class TestActivity : HotwireActivity() { override fun navigatorConfigurations() = emptyList() } diff --git a/navigation-fragments/src/test/kotlin/dev/hotwire/navigation/routing/SystemNavigationRouteDecisionHandlerTest.kt b/navigation-fragments/src/test/kotlin/dev/hotwire/navigation/routing/SystemNavigationRouteDecisionHandlerTest.kt index 064bc082..8cce68ea 100644 --- a/navigation-fragments/src/test/kotlin/dev/hotwire/navigation/routing/SystemNavigationRouteDecisionHandlerTest.kt +++ b/navigation-fragments/src/test/kotlin/dev/hotwire/navigation/routing/SystemNavigationRouteDecisionHandlerTest.kt @@ -1,5 +1,8 @@ package dev.hotwire.navigation.routing +import dev.hotwire.core.turbo.config.PathConfigurationProperties +import dev.hotwire.core.turbo.visit.VisitOptions +import dev.hotwire.core.turbo.visit.VisitProposal import dev.hotwire.navigation.activities.HotwireActivity import dev.hotwire.navigation.navigator.NavigatorConfiguration import org.junit.Assert.assertEquals @@ -29,34 +32,41 @@ class SystemNavigationRouteDecisionHandlerTest { @Test fun `matching result stops navigation`() { - val decision = route.handle("https://external.com/page", config, activity) + val decision = route.handle(proposal("https://external.com/page"), config, activity) assertEquals(Router.Decision.CANCEL, decision) } @Test fun `url on external domain matches`() { val url = "https://external.com/page" - assertTrue(route.matches(url, config)) + assertTrue(route.matches(proposal(url), config)) } @Test fun `url without subdomain matches`() { val url = "https://app.com/page" - assertTrue(route.matches(url, config)) + assertTrue(route.matches(proposal(url), config)) } @Test fun `url on app domain does not match`() { val url = "https://my.app.com/page" - assertFalse(route.matches(url, config)) + assertFalse(route.matches(proposal(url), config)) } @Test fun `non-http scheme matches`() { val url = "sms:555-555-5555" - assertTrue(route.matches(url, config)) + assertTrue(route.matches(proposal(url), config)) } + private fun proposal(location: String) = VisitProposal( + location = location, + options = VisitOptions(), + properties = PathConfigurationProperties(), + bundle = null + ) + private class TestActivity : HotwireActivity() { override fun navigatorConfigurations() = emptyList() }