diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 3989bbf..4b6ed9a 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -24,12 +24,13 @@ val developerEmail by extra("androidteam@basecamp.com") android { namespace = "dev.hotwire.core" compileSdk = 34 + testOptions.unitTests.isIncludeAndroidResources = true testOptions.unitTests.isReturnDefaultValues = true + testOptions.targetSdk = 34 defaultConfig { minSdk = 28 - targetSdk = 34 } buildTypes { @@ -71,14 +72,14 @@ android { dependencies { // Kotlin - implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.22") + implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.23") // Material - implementation("com.google.android.material:material:1.11.0") + implementation("com.google.android.material:material:1.12.0") // AndroidX implementation("androidx.constraintlayout:constraintlayout:2.1.4") - implementation("androidx.lifecycle:lifecycle-common:2.7.0") + implementation("androidx.lifecycle:lifecycle-common:2.8.1") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") // JSON @@ -86,35 +87,28 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") // Networking/API - implementation("com.squareup.okhttp3:okhttp:4.11.0") - implementation("com.squareup.okhttp3:logging-interceptor:4.11.0") + implementation("com.squareup.okhttp3:okhttp:4.12.0") + implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") // Coroutines - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") - // Browser - implementation("androidx.browser:browser:1.7.0") - // Exported AndroidX dependencies - api("androidx.appcompat:appcompat:1.6.1") - api("androidx.core:core-ktx:1.12.0") - api("androidx.webkit:webkit:1.8.0") - api("androidx.activity:activity-ktx:1.8.1") - api("androidx.fragment:fragment-ktx:1.6.2") - api("androidx.navigation:navigation-fragment-ktx:2.7.5") - api("androidx.navigation:navigation-ui-ktx:2.7.5") + api("androidx.appcompat:appcompat:1.7.0") + api("androidx.core:core-ktx:1.13.1") + api("androidx.webkit:webkit:1.11.0") // Tests testImplementation("androidx.test:core:1.5.0") // Robolectric - testImplementation("androidx.navigation:navigation-testing:2.7.5") + testImplementation("androidx.navigation:navigation-testing:2.7.7") testImplementation("androidx.arch.core:core-testing:2.2.0") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") - testImplementation("org.assertj:assertj-core:3.24.2") - testImplementation("org.robolectric:robolectric:4.11.1") - testImplementation("org.mockito:mockito-core:5.2.0") + testImplementation("org.assertj:assertj-core:3.25.3") + testImplementation("org.robolectric:robolectric:4.12.1") + testImplementation("org.mockito:mockito-core:5.11.0") testImplementation("com.nhaarman:mockito-kotlin:1.6.0") - testImplementation("com.squareup.okhttp3:mockwebserver:4.11.0") + testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0") testImplementation("junit:junit:4.13.2") } diff --git a/core/src/main/kotlin/dev/hotwire/core/bridge/Bridge.kt b/core/src/main/kotlin/dev/hotwire/core/bridge/Bridge.kt index 7ed7ed3..cd1ee92 100644 --- a/core/src/main/kotlin/dev/hotwire/core/bridge/Bridge.kt +++ b/core/src/main/kotlin/dev/hotwire/core/bridge/Bridge.kt @@ -3,7 +3,7 @@ package dev.hotwire.core.bridge import android.webkit.JavascriptInterface import android.webkit.WebView import androidx.annotation.VisibleForTesting -import dev.hotwire.core.lib.logging.logEvent +import dev.hotwire.core.logging.logEvent import kotlinx.serialization.json.JsonElement import java.lang.ref.WeakReference @@ -18,7 +18,7 @@ class Bridge internal constructor(webView: WebView) { internal val webView: WebView? get() = webViewRef.get() internal var repository = Repository() - internal var delegate: BridgeDelegate? = null + internal var delegate: BridgeDelegate<*>? = null init { // Use a weak reference in case the WebView is no longer being @@ -120,7 +120,7 @@ class Bridge internal constructor(webView: WebView) { companion object { private val instances = mutableListOf() - internal fun initialize(webView: WebView) { + fun initialize(webView: WebView) { if (getBridgeFor(webView) == null) { initialize(Bridge(webView)) } diff --git a/core/src/main/kotlin/dev/hotwire/core/bridge/BridgeComponent.kt b/core/src/main/kotlin/dev/hotwire/core/bridge/BridgeComponent.kt index ae823c6..8eadf87 100644 --- a/core/src/main/kotlin/dev/hotwire/core/bridge/BridgeComponent.kt +++ b/core/src/main/kotlin/dev/hotwire/core/bridge/BridgeComponent.kt @@ -1,10 +1,10 @@ package dev.hotwire.core.bridge -import dev.hotwire.core.lib.logging.logWarning +import dev.hotwire.core.logging.logWarning -abstract class BridgeComponent( +abstract class BridgeComponent( val name: String, - private val delegate: BridgeDelegate + private val delegate: BridgeDelegate ) { private val receivedMessages = hashMapOf() diff --git a/core/src/main/kotlin/dev/hotwire/core/bridge/BridgeComponentFactory.kt b/core/src/main/kotlin/dev/hotwire/core/bridge/BridgeComponentFactory.kt index 5dfe394..bd6ac88 100644 --- a/core/src/main/kotlin/dev/hotwire/core/bridge/BridgeComponentFactory.kt +++ b/core/src/main/kotlin/dev/hotwire/core/bridge/BridgeComponentFactory.kt @@ -1,8 +1,8 @@ package dev.hotwire.core.bridge -class BridgeComponentFactory constructor( +class BridgeComponentFactory> constructor( val name: String, - private val creator: (name: String, delegate: BridgeDelegate) -> C + private val creator: (name: String, delegate: BridgeDelegate) -> C ) { - fun create(delegate: BridgeDelegate) = creator(name, delegate) + fun create(delegate: BridgeDelegate) = creator(name, delegate) } diff --git a/core/src/main/kotlin/dev/hotwire/core/bridge/BridgeDelegate.kt b/core/src/main/kotlin/dev/hotwire/core/bridge/BridgeDelegate.kt index 839a386..c9caacc 100644 --- a/core/src/main/kotlin/dev/hotwire/core/bridge/BridgeDelegate.kt +++ b/core/src/main/kotlin/dev/hotwire/core/bridge/BridgeDelegate.kt @@ -3,24 +3,22 @@ package dev.hotwire.core.bridge import android.webkit.WebView import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner -import dev.hotwire.core.config.Hotwire -import dev.hotwire.core.lib.logging.logEvent -import dev.hotwire.core.lib.logging.logWarning -import dev.hotwire.core.turbo.nav.HotwireNavDestination +import dev.hotwire.core.logging.logEvent +import dev.hotwire.core.logging.logWarning @Suppress("unused") -class BridgeDelegate( +class BridgeDelegate( val location: String, - val destination: HotwireNavDestination + val destination: D, + private val componentFactories: List>> ) : DefaultLifecycleObserver { internal var bridge: Bridge? = null private var destinationIsActive: Boolean = false - private val componentFactories = Hotwire.registeredBridgeComponentFactories - private val initializedComponents = hashMapOf() + private val initializedComponents = hashMapOf>() private val resolvedLocation: String get() = bridge?.webView?.url ?: location - val activeComponents: List + val activeComponents: List> get() = initializedComponents.map { it.value }.takeIf { destinationIsActive }.orEmpty() fun onColdBootPageCompleted() { @@ -75,7 +73,7 @@ class BridgeDelegate( } private fun shouldReloadBridge(): Boolean { - return destination.navigator.session.isReady && bridge?.isReady() == false + return destination.bridgeWebViewIsReady() && bridge?.isReady() == false } // Lifecycle events @@ -107,7 +105,7 @@ class BridgeDelegate( activeComponents.filterIsInstance().forEach { action(it) } } - private fun getOrCreateComponent(name: String): BridgeComponent? { + private fun getOrCreateComponent(name: String): BridgeComponent? { val factory = componentFactories.firstOrNull { it.name == name } ?: return null return initializedComponents.getOrPut(name) { factory.create(this) } } diff --git a/core/src/main/kotlin/dev/hotwire/core/bridge/BridgeDestination.kt b/core/src/main/kotlin/dev/hotwire/core/bridge/BridgeDestination.kt new file mode 100644 index 0000000..6fbdeac --- /dev/null +++ b/core/src/main/kotlin/dev/hotwire/core/bridge/BridgeDestination.kt @@ -0,0 +1,5 @@ +package dev.hotwire.core.bridge + +interface BridgeDestination { + fun bridgeWebViewIsReady(): Boolean +} diff --git a/core/src/main/kotlin/dev/hotwire/core/bridge/JsonExtensions.kt b/core/src/main/kotlin/dev/hotwire/core/bridge/JsonExtensions.kt index 54b2901..293e5b6 100644 --- a/core/src/main/kotlin/dev/hotwire/core/bridge/JsonExtensions.kt +++ b/core/src/main/kotlin/dev/hotwire/core/bridge/JsonExtensions.kt @@ -1,6 +1,6 @@ package dev.hotwire.core.bridge -import dev.hotwire.core.lib.logging.logError +import dev.hotwire.core.logging.logError import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement diff --git a/core/src/main/kotlin/dev/hotwire/core/bridge/StradaJsonConverter.kt b/core/src/main/kotlin/dev/hotwire/core/bridge/StradaJsonConverter.kt index 07c75ed..77cbec9 100644 --- a/core/src/main/kotlin/dev/hotwire/core/bridge/StradaJsonConverter.kt +++ b/core/src/main/kotlin/dev/hotwire/core/bridge/StradaJsonConverter.kt @@ -1,7 +1,7 @@ package dev.hotwire.core.bridge import dev.hotwire.core.config.Hotwire -import dev.hotwire.core.lib.logging.logError +import dev.hotwire.core.logging.logError import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json diff --git a/core/src/main/kotlin/dev/hotwire/core/config/Hotwire.kt b/core/src/main/kotlin/dev/hotwire/core/config/Hotwire.kt index 81ae0b2..69de2f1 100644 --- a/core/src/main/kotlin/dev/hotwire/core/config/Hotwire.kt +++ b/core/src/main/kotlin/dev/hotwire/core/config/Hotwire.kt @@ -1,77 +1,16 @@ package dev.hotwire.core.config import android.content.Context -import androidx.fragment.app.Fragment -import dev.hotwire.core.bridge.BridgeComponent -import dev.hotwire.core.bridge.BridgeComponentFactory -import dev.hotwire.core.navigation.fragments.HotwireWebBottomSheetFragment -import dev.hotwire.core.navigation.fragments.HotwireWebFragment -import dev.hotwire.core.navigation.routing.AppNavigationRouteDecisionHandler -import dev.hotwire.core.navigation.routing.BrowserRouteDecisionHandler -import dev.hotwire.core.navigation.routing.Router import dev.hotwire.core.turbo.config.PathConfiguration -import kotlin.reflect.KClass object Hotwire { - internal var registeredBridgeComponentFactories: - List> = emptyList() - private set - - internal var registeredFragmentDestinations: - List> = listOf( - HotwireWebFragment::class, - HotwireWebBottomSheetFragment::class - ) - private set - - internal var router = Router(listOf( - AppNavigationRouteDecisionHandler(), - BrowserRouteDecisionHandler() - )) - val config: HotwireConfig = HotwireConfig() - /** - * The path configuration that defines your navigation rules. - */ - val pathConfiguration = PathConfiguration() - /** * Loads the [PathConfiguration] JSON file(s) from the provided location to * configure navigation rules. */ fun loadPathConfiguration(context: Context, location: PathConfiguration.Location) { - pathConfiguration.load(context, location) - } - - /** - * Registers the [Router.RouteDecisionHandler] instances that determine whether to route location - * urls within in-app navigation or with alternative custom behaviors. - */ - fun registerRouteDecisionHandlers(decisionHandlers: List) { - router = Router(decisionHandlers) - } - - /** - * Register bridge components that the app supports. Every possible bridge - * component, wrapped in a [BridgeComponentFactory], must be provided here. - */ - fun registerBridgeComponents(factories: List>) { - registeredBridgeComponentFactories = factories - } - - /** - * The default fragment destination for web requests. If you have not - * loaded a path configuration with a matching rule and a `uri` available - * for all possible paths, this destination will be used as the default. - */ - var defaultFragmentDestination: KClass = HotwireWebFragment::class - - /** - * Register fragment destinations that can be navigated to. Every possible - * destination must be provided here. - */ - fun registerFragmentDestinations(destinations: List>) { - registeredFragmentDestinations = destinations + config.pathConfiguration.load(context, location) } } diff --git a/core/src/main/kotlin/dev/hotwire/core/config/HotwireConfig.kt b/core/src/main/kotlin/dev/hotwire/core/config/HotwireConfig.kt index 7bb7a24..3786676 100644 --- a/core/src/main/kotlin/dev/hotwire/core/config/HotwireConfig.kt +++ b/core/src/main/kotlin/dev/hotwire/core/config/HotwireConfig.kt @@ -2,11 +2,23 @@ package dev.hotwire.core.config import android.content.Context import android.webkit.WebView +import dev.hotwire.core.bridge.BridgeComponent +import dev.hotwire.core.bridge.BridgeComponentFactory import dev.hotwire.core.bridge.StradaJsonConverter +import dev.hotwire.core.turbo.config.PathConfiguration import dev.hotwire.core.turbo.http.TurboHttpClient +import dev.hotwire.core.turbo.http.TurboOfflineRequestHandler import dev.hotwire.core.turbo.views.TurboWebView class HotwireConfig internal constructor() { + /** + * The path configuration that defines your navigation rules. + */ + val pathConfiguration = PathConfiguration() + + var registeredBridgeComponentFactories: + List>> = emptyList() + /** * Set a custom JSON converter to easily decode Message.dataJson to a data * object in received messages and to encode a data object back to json to @@ -14,6 +26,11 @@ class HotwireConfig internal constructor() { */ var jsonConverter: StradaJsonConverter? = null + /** + * Experimental: API may be removed, not ready for production use. + */ + var offlineRequestHandler: TurboOfflineRequestHandler? = null + /** * Enables/disables debug logging. This should be disabled in production environments. * Disabled by default. @@ -55,7 +72,7 @@ class HotwireConfig internal constructor() { * calling this so the bridge component names are included in your user agent. */ fun userAgentSubstring(): String { - val components = Hotwire.registeredBridgeComponentFactories.joinToString(" ") { it.name } + val components = registeredBridgeComponentFactories.joinToString(" ") { it.name } return "Turbo Native Android; bridge-components: [$components];" } diff --git a/core/src/main/kotlin/dev/hotwire/core/lib/logging/HotwireLog.kt b/core/src/main/kotlin/dev/hotwire/core/logging/CoreLog.kt similarity index 72% rename from core/src/main/kotlin/dev/hotwire/core/lib/logging/HotwireLog.kt rename to core/src/main/kotlin/dev/hotwire/core/logging/CoreLog.kt index 266cce3..3533bfd 100644 --- a/core/src/main/kotlin/dev/hotwire/core/lib/logging/HotwireLog.kt +++ b/core/src/main/kotlin/dev/hotwire/core/logging/CoreLog.kt @@ -1,10 +1,10 @@ -package dev.hotwire.core.lib.logging +package dev.hotwire.core.logging import android.util.Log import dev.hotwire.core.config.Hotwire -internal object HotwireLog { - private const val DEFAULT_TAG = "Hotwire" +internal object CoreLog { + private const val DEFAULT_TAG = "Hotwire-Core" private val debugEnabled get() = Hotwire.config.debugLoggingEnabled @@ -26,20 +26,20 @@ internal object HotwireLog { private const val PAD_END_LENGTH = 35 internal fun logEvent(event: String, details: String = "") { - HotwireLog.d("$event ".padEnd(PAD_END_LENGTH, '.') + " [$details]") + CoreLog.d("$event ".padEnd(PAD_END_LENGTH, '.') + " [$details]") } internal fun logEvent(event: String, attributes: List>) { val description = attributes.joinToString(prefix = "[", postfix = "]", separator = ", ") { "${it.first}: ${it.second}" } - HotwireLog.d("$event ".padEnd(PAD_END_LENGTH, '.') + " $description") + CoreLog.d("$event ".padEnd(PAD_END_LENGTH, '.') + " $description") } internal fun logWarning(event: String, details: String) { - HotwireLog.w("$event ".padEnd(PAD_END_LENGTH, '.') + " [$details]") + CoreLog.w("$event ".padEnd(PAD_END_LENGTH, '.') + " [$details]") } internal fun logError(event: String, error: Exception) { - HotwireLog.e("$event: ${error.stackTraceToString()}") + CoreLog.e("$event: ${error.stackTraceToString()}") } diff --git a/core/src/main/kotlin/dev/hotwire/core/turbo/config/PathConfiguration.kt b/core/src/main/kotlin/dev/hotwire/core/turbo/config/PathConfiguration.kt index 65156fd..a55a6d8 100644 --- a/core/src/main/kotlin/dev/hotwire/core/turbo/config/PathConfiguration.kt +++ b/core/src/main/kotlin/dev/hotwire/core/turbo/config/PathConfiguration.kt @@ -5,8 +5,6 @@ import android.content.Context import android.net.Uri import androidx.core.net.toUri import com.google.gson.annotations.SerializedName -import dev.hotwire.core.config.Hotwire -import dev.hotwire.core.turbo.nav.HotwireDestination import dev.hotwire.core.turbo.nav.TurboNavPresentation import dev.hotwire.core.turbo.nav.TurboNavPresentationContext import dev.hotwire.core.turbo.nav.TurboNavQueryStringPresentation @@ -60,7 +58,7 @@ class PathConfiguration { * Loads and parses the specified configuration file(s) from their local * and/or remote locations. */ - internal fun load(context: Context, location: Location) { + fun load(context: Context, location: Location) { if (loader == null) { loader = PathConfigurationLoader(context.applicationContext) } @@ -133,9 +131,8 @@ val PathConfigurationProperties.context: TurboNavPresentationContext TurboNavPresentationContext.DEFAULT } -val PathConfigurationProperties.uri: Uri - get() = get("uri")?.toUri() ?: - HotwireDestination.from(Hotwire.defaultFragmentDestination).uri.toUri() +val PathConfigurationProperties.uri: Uri? + get() = get("uri")?.toUri() val PathConfigurationProperties.fallbackUri: Uri? get() = get("fallback_uri")?.toUri() diff --git a/core/src/main/kotlin/dev/hotwire/core/turbo/config/PathConfigurationLoader.kt b/core/src/main/kotlin/dev/hotwire/core/turbo/config/PathConfigurationLoader.kt index ad529b6..35eaacc 100644 --- a/core/src/main/kotlin/dev/hotwire/core/turbo/config/PathConfigurationLoader.kt +++ b/core/src/main/kotlin/dev/hotwire/core/turbo/config/PathConfigurationLoader.kt @@ -2,8 +2,8 @@ package dev.hotwire.core.turbo.config import android.content.Context import com.google.gson.reflect.TypeToken -import dev.hotwire.core.lib.logging.logError -import dev.hotwire.core.lib.logging.logEvent +import dev.hotwire.core.logging.logError +import dev.hotwire.core.logging.logEvent import dev.hotwire.core.turbo.util.dispatcherProvider import dev.hotwire.core.turbo.util.toObject import kotlinx.coroutines.CoroutineScope diff --git a/core/src/main/kotlin/dev/hotwire/core/turbo/config/PathConfigurationRepository.kt b/core/src/main/kotlin/dev/hotwire/core/turbo/config/PathConfigurationRepository.kt index 646fab1..e7284a3 100644 --- a/core/src/main/kotlin/dev/hotwire/core/turbo/config/PathConfigurationRepository.kt +++ b/core/src/main/kotlin/dev/hotwire/core/turbo/config/PathConfigurationRepository.kt @@ -3,7 +3,7 @@ package dev.hotwire.core.turbo.config import android.content.Context import android.content.SharedPreferences import androidx.core.content.edit -import dev.hotwire.core.lib.logging.logError +import dev.hotwire.core.logging.logError import dev.hotwire.core.turbo.http.TurboHttpClient import dev.hotwire.core.turbo.util.dispatcherProvider import dev.hotwire.core.turbo.util.toJson diff --git a/core/src/main/kotlin/dev/hotwire/core/turbo/config/PathConfigurationRule.kt b/core/src/main/kotlin/dev/hotwire/core/turbo/config/PathConfigurationRule.kt index 6df52f9..7b99392 100644 --- a/core/src/main/kotlin/dev/hotwire/core/turbo/config/PathConfigurationRule.kt +++ b/core/src/main/kotlin/dev/hotwire/core/turbo/config/PathConfigurationRule.kt @@ -2,7 +2,7 @@ package dev.hotwire.core.turbo.config import com.google.gson.annotations.SerializedName import dev.hotwire.core.BuildConfig -import dev.hotwire.core.lib.logging.logError +import dev.hotwire.core.logging.logError import java.util.regex.PatternSyntaxException internal data class PathConfigurationRule( diff --git a/core/src/main/kotlin/dev/hotwire/core/turbo/delegates/TurboCameraCaptureDelegate.kt b/core/src/main/kotlin/dev/hotwire/core/turbo/delegates/TurboCameraCaptureDelegate.kt index 0471c2b..a4a76ed 100644 --- a/core/src/main/kotlin/dev/hotwire/core/turbo/delegates/TurboCameraCaptureDelegate.kt +++ b/core/src/main/kotlin/dev/hotwire/core/turbo/delegates/TurboCameraCaptureDelegate.kt @@ -5,7 +5,7 @@ import android.content.Intent import android.net.Uri import android.provider.MediaStore import android.webkit.WebChromeClient.FileChooserParams -import dev.hotwire.core.lib.logging.logError +import dev.hotwire.core.logging.logError import dev.hotwire.core.turbo.util.TurboFileProvider import java.io.File import java.io.IOException diff --git a/core/src/main/kotlin/dev/hotwire/core/turbo/delegates/TurboFileChooserDelegate.kt b/core/src/main/kotlin/dev/hotwire/core/turbo/delegates/TurboFileChooserDelegate.kt index dc92f35..c9482cd 100644 --- a/core/src/main/kotlin/dev/hotwire/core/turbo/delegates/TurboFileChooserDelegate.kt +++ b/core/src/main/kotlin/dev/hotwire/core/turbo/delegates/TurboFileChooserDelegate.kt @@ -8,7 +8,7 @@ import android.webkit.ValueCallback import android.webkit.WebChromeClient.FileChooserParams import androidx.activity.result.ActivityResult import dev.hotwire.core.R -import dev.hotwire.core.lib.logging.logError +import dev.hotwire.core.logging.logError import dev.hotwire.core.turbo.session.Session import dev.hotwire.core.turbo.util.TURBO_REQUEST_CODE_FILES import dev.hotwire.core.turbo.util.TurboFileProvider @@ -18,7 +18,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlin.coroutines.CoroutineContext -internal class TurboFileChooserDelegate(val session: Session) : CoroutineScope { +class TurboFileChooserDelegate(val session: Session) : CoroutineScope { private val context: Context = session.context private var uploadCallback: ValueCallback>? = null private val browseFilesDelegate = TurboBrowseFilesDelegate(context) @@ -66,7 +66,7 @@ internal class TurboFileChooserDelegate(val session: Session) : CoroutineScope { } private fun startIntent(intent: Intent): Boolean { - val destination = session.currentVisitNavDestination ?: return false + val destination = session.currentVisit?.callback?.visitDestination() ?: return false return try { destination.activityResultLauncher(TURBO_REQUEST_CODE_FILES)?.launch(intent) diff --git a/core/src/main/kotlin/dev/hotwire/core/turbo/http/TurboHttpClient.kt b/core/src/main/kotlin/dev/hotwire/core/turbo/http/TurboHttpClient.kt index 9cc0d94..5e3066f 100644 --- a/core/src/main/kotlin/dev/hotwire/core/turbo/http/TurboHttpClient.kt +++ b/core/src/main/kotlin/dev/hotwire/core/turbo/http/TurboHttpClient.kt @@ -2,7 +2,7 @@ package dev.hotwire.core.turbo.http import android.content.Context import dev.hotwire.core.config.Hotwire -import dev.hotwire.core.lib.logging.logError +import dev.hotwire.core.logging.logError import okhttp3.Cache import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor diff --git a/core/src/main/kotlin/dev/hotwire/core/turbo/http/TurboHttpRepository.kt b/core/src/main/kotlin/dev/hotwire/core/turbo/http/TurboHttpRepository.kt index af269f4..8659119 100644 --- a/core/src/main/kotlin/dev/hotwire/core/turbo/http/TurboHttpRepository.kt +++ b/core/src/main/kotlin/dev/hotwire/core/turbo/http/TurboHttpRepository.kt @@ -3,7 +3,7 @@ package dev.hotwire.core.turbo.http import android.webkit.CookieManager import android.webkit.WebResourceRequest import android.webkit.WebResourceResponse -import dev.hotwire.core.lib.logging.logError +import dev.hotwire.core.logging.logError import dev.hotwire.core.turbo.util.dispatcherProvider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch diff --git a/core/src/main/kotlin/dev/hotwire/core/turbo/http/TurboWebViewRequestInterceptor.kt b/core/src/main/kotlin/dev/hotwire/core/turbo/http/TurboWebViewRequestInterceptor.kt index 2cc1c1b..dde5682 100644 --- a/core/src/main/kotlin/dev/hotwire/core/turbo/http/TurboWebViewRequestInterceptor.kt +++ b/core/src/main/kotlin/dev/hotwire/core/turbo/http/TurboWebViewRequestInterceptor.kt @@ -2,12 +2,13 @@ package dev.hotwire.core.turbo.http import android.webkit.WebResourceRequest import android.webkit.WebResourceResponse -import dev.hotwire.core.lib.logging.logEvent +import dev.hotwire.core.config.Hotwire +import dev.hotwire.core.logging.logEvent import dev.hotwire.core.turbo.session.Session import dev.hotwire.core.turbo.util.isHttpGetRequest internal class TurboWebViewRequestInterceptor(val session: Session) { - private val offlineRequestHandler get() = session.offlineRequestHandler + private val offlineRequestHandler get() = Hotwire.config.offlineRequestHandler private val httpRepository get() = session.httpRepository private val currentVisit get() = session.currentVisit diff --git a/core/src/main/kotlin/dev/hotwire/core/turbo/session/Session.kt b/core/src/main/kotlin/dev/hotwire/core/turbo/session/Session.kt index 6684ac4..ea7bfe6 100644 --- a/core/src/main/kotlin/dev/hotwire/core/turbo/session/Session.kt +++ b/core/src/main/kotlin/dev/hotwire/core/turbo/session/Session.kt @@ -11,69 +11,72 @@ import androidx.lifecycle.lifecycleScope import androidx.webkit.WebResourceErrorCompat import androidx.webkit.WebViewClientCompat import androidx.webkit.WebViewCompat -import androidx.webkit.WebViewFeature.* -import dev.hotwire.core.lib.logging.logEvent +import androidx.webkit.WebViewFeature.VISUAL_STATE_CALLBACK +import androidx.webkit.WebViewFeature.isFeatureSupported +import dev.hotwire.core.config.Hotwire +import dev.hotwire.core.logging.logEvent import dev.hotwire.core.turbo.delegates.TurboFileChooserDelegate import dev.hotwire.core.turbo.errors.HttpError import dev.hotwire.core.turbo.errors.LoadError import dev.hotwire.core.turbo.errors.WebError import dev.hotwire.core.turbo.errors.WebSslError import dev.hotwire.core.turbo.http.* -import dev.hotwire.core.turbo.nav.HotwireNavDestination -import dev.hotwire.core.turbo.util.* +import dev.hotwire.core.turbo.util.isHttpGetRequest +import dev.hotwire.core.turbo.util.runOnUiThread +import dev.hotwire.core.turbo.util.toJson import dev.hotwire.core.turbo.views.TurboWebView import dev.hotwire.core.turbo.visit.Visit import dev.hotwire.core.turbo.visit.VisitAction import dev.hotwire.core.turbo.visit.VisitOptions -import kotlinx.coroutines.* -import java.util.* +import java.util.Date /** * This class is primarily responsible for managing an instance of an Android WebView that will - * be shared between destinations. A a [Navigator] will create a session for you and it can be - * retrieved via [Navigator.session]. + * be shared between destinations. * * @property sessionName An arbitrary name to be used as an identifier for a given session. * @property activity The activity to which the session will be bound to. * @property webView An instance of a [TurboWebView] to be shared/managed. */ @Suppress("unused") -class Session internal constructor( +class Session( internal val sessionName: String, private val activity: AppCompatActivity, val webView: TurboWebView ) { - internal var currentVisit: Visit? = null internal var coldBootVisitIdentifier = "" internal var previousOverrideUrlTime = 0L internal var isColdBooting = false internal var visitPending = false - internal var isRenderProcessGone = false internal var restorationIdentifiers = SparseArray() internal val context: Context = activity.applicationContext internal val httpRepository = TurboHttpRepository(activity.lifecycleScope) internal val requestInterceptor = TurboWebViewRequestInterceptor(this) - internal val fileChooserDelegate = TurboFileChooserDelegate(this) // User accessible /** - * Experimental: API may change, not ready for production use. + * The current visit for the current destination. */ - var offlineRequestHandler: TurboOfflineRequestHandler? = null + var currentVisit: Visit? = null + internal set /** - * Gets the nav destination that corresponds to the current WebView visit. + * Provides the status of whether Turbo is initialized and ready for use. */ - val currentVisitNavDestination: HotwireNavDestination? - get() = currentVisit?.callback?.visitNavDestination() + var isReady = false + internal set /** - * Provides the status of whether Turbo is initialized and ready for use. + * Specifies whether the render process is gone for the WebView instance. If the + * render process is gone, this Session and its WebView cannot be reused and must + * be recreated. */ - var isReady = false + var isRenderProcessGone = false internal set + val fileChooserDelegate = TurboFileChooserDelegate(this) + init { initializeWebView() TurboHttpClient.enableCachingWith(context) @@ -89,7 +92,7 @@ class Session internal constructor( * @param location Location to cache. */ fun preCacheLocation(location: String) { - val requestHandler = checkNotNull(offlineRequestHandler) { + val requestHandler = checkNotNull(Hotwire.config.offlineRequestHandler) { "An offline request handler must be provided to pre-cache $location" } @@ -114,9 +117,7 @@ class Session internal constructor( isColdBooting = false } - // Internal - - internal fun visit(visit: Visit) { + fun visit(visit: Visit) { this.currentVisit = visit callback { it.visitLocationStarted(visit.location) } @@ -133,10 +134,10 @@ class Session internal constructor( /** * Synthetically restore the WebView's current visit without using a cached snapshot or a - * visit request. This is used when restoring a Fragment destination from the backstack, + * visit request. This is used when restoring a destination from the backstack, * but the WebView's current location hasn't changed from the destination's location. */ - internal fun restoreCurrentVisit(callback: SessionCallback): Boolean { + fun restoreCurrentVisit(callback: SessionCallback): Boolean { val visit = currentVisit ?: return false val restorationIdentifier = restorationIdentifiers[visit.destinationIdentifier] @@ -157,7 +158,7 @@ class Session internal constructor( return true } - internal fun removeCallback(callback: SessionCallback) { + fun removeCallback(callback: SessionCallback) { currentVisit?.let { visit -> if (visit.callback == callback) { visit.callback = null @@ -614,7 +615,7 @@ class Session internal constructor( private fun callback(action: (SessionCallback) -> Unit) { context.runOnUiThread { currentVisit?.callback?.let { callback -> - if (callback.visitNavDestination().isActive) { + if (callback.visitDestination().isActive()) { action(callback) } } diff --git a/core/src/main/kotlin/dev/hotwire/core/turbo/session/SessionCallback.kt b/core/src/main/kotlin/dev/hotwire/core/turbo/session/SessionCallback.kt index e4108c3..2fd56fa 100644 --- a/core/src/main/kotlin/dev/hotwire/core/turbo/session/SessionCallback.kt +++ b/core/src/main/kotlin/dev/hotwire/core/turbo/session/SessionCallback.kt @@ -2,10 +2,10 @@ package dev.hotwire.core.turbo.session import android.webkit.HttpAuthHandler import dev.hotwire.core.turbo.errors.VisitError -import dev.hotwire.core.turbo.nav.HotwireNavDestination +import dev.hotwire.core.turbo.visit.VisitDestination import dev.hotwire.core.turbo.visit.VisitOptions -internal interface SessionCallback { +interface SessionCallback { fun onPageStarted(location: String) fun onPageFinished(location: String) fun onReceivedError(error: VisitError) @@ -19,7 +19,7 @@ internal interface SessionCallback { fun visitCompleted(completedOffline: Boolean) fun visitLocationStarted(location: String) fun visitProposedToLocation(location: String, options: VisitOptions) - fun visitNavDestination(): HotwireNavDestination + fun visitDestination(): VisitDestination fun formSubmissionStarted(location: String) fun formSubmissionFinished(location: String) } diff --git a/core/src/main/kotlin/dev/hotwire/core/turbo/util/CoreConstants.kt b/core/src/main/kotlin/dev/hotwire/core/turbo/util/CoreConstants.kt new file mode 100644 index 0000000..fb62874 --- /dev/null +++ b/core/src/main/kotlin/dev/hotwire/core/turbo/util/CoreConstants.kt @@ -0,0 +1,3 @@ +package dev.hotwire.core.turbo.util + +const val TURBO_REQUEST_CODE_FILES = 37 diff --git a/core/src/main/kotlin/dev/hotwire/core/turbo/util/TurboExtensions.kt b/core/src/main/kotlin/dev/hotwire/core/turbo/util/CoreExtensions.kt similarity index 55% rename from core/src/main/kotlin/dev/hotwire/core/turbo/util/TurboExtensions.kt rename to core/src/main/kotlin/dev/hotwire/core/turbo/util/CoreExtensions.kt index d70a367..d7f4a24 100644 --- a/core/src/main/kotlin/dev/hotwire/core/turbo/util/TurboExtensions.kt +++ b/core/src/main/kotlin/dev/hotwire/core/turbo/util/CoreExtensions.kt @@ -1,32 +1,20 @@ package dev.hotwire.core.turbo.util -import android.animation.ArgbEvaluator -import android.animation.ValueAnimator import android.content.Context import android.os.Handler -import android.util.TypedValue import android.webkit.WebResourceRequest -import androidx.annotation.AttrRes -import androidx.appcompat.widget.Toolbar -import androidx.core.content.ContextCompat -import androidx.navigation.NavBackStackEntry import com.google.gson.Gson import com.google.gson.GsonBuilder import com.google.gson.reflect.TypeToken -import dev.hotwire.core.R import dev.hotwire.core.turbo.visit.VisitAction import dev.hotwire.core.turbo.visit.VisitActionAdapter import java.io.File -fun Toolbar.displayBackButton() { - navigationIcon = ContextCompat.getDrawable(context, R.drawable.ic_back) -} - -fun Toolbar.displayBackButtonAsCloseIcon() { - navigationIcon = ContextCompat.getDrawable(context, R.drawable.ic_close) +internal fun WebResourceRequest.isHttpGetRequest(): Boolean { + return method.equals("GET", ignoreCase = true) && + url.scheme?.startsWith("HTTP", ignoreCase = true) == true } - internal fun Context.runOnUiThread(func: () -> Unit) { when (mainLooper.isCurrentThread) { true -> func() @@ -40,19 +28,6 @@ internal fun Context.contentFromAsset(filePath: String): String { } } -internal fun Context.colorFromThemeAttr( - @AttrRes attrColor: Int, - typedValue: TypedValue = TypedValue(), - resolveRefs: Boolean = true -): Int { - theme.resolveAttribute(attrColor, typedValue, resolveRefs) - val attr = obtainStyledAttributes(typedValue.data, intArrayOf(attrColor)) - val attrValue = attr.getColor(0, -1) - attr.recycle() - - return attrValue -} - internal fun String.extract(patternRegex: String): String? { val regex = Regex(patternRegex, RegexOption.IGNORE_CASE) return regex.find(this)?.groups?.get(1)?.value @@ -80,14 +55,6 @@ internal fun File.deleteAllFilesInDirectory() { } } -internal val NavBackStackEntry?.location: String? - get() = this?.arguments?.getString("location") - -internal fun WebResourceRequest.isHttpGetRequest(): Boolean { - return method.equals("GET", ignoreCase = true) && - url.scheme?.startsWith("HTTP", ignoreCase = true) == true -} - internal fun Any.toJson(): String { return gson.toJson(this) } @@ -96,16 +63,6 @@ internal fun String.toObject(typeToken: TypeToken): T { return gson.fromJson(this, typeToken.type) } -internal fun Int.animateColorTo(toColor: Int, duration: Long = 150, onUpdate: (Int) -> Unit) { - ValueAnimator.ofObject(ArgbEvaluator(), this, toColor).apply { - this.duration = duration - this.addUpdateListener { - val color = it.animatedValue as Int? - color?.let { onUpdate(color) } - } - }.start() -} - private val gson: Gson = GsonBuilder() .registerTypeAdapter(VisitAction::class.java, VisitActionAdapter()) .create() diff --git a/core/src/main/kotlin/dev/hotwire/core/turbo/util/TurboConstants.kt b/core/src/main/kotlin/dev/hotwire/core/turbo/util/TurboConstants.kt deleted file mode 100644 index 418c469..0000000 --- a/core/src/main/kotlin/dev/hotwire/core/turbo/util/TurboConstants.kt +++ /dev/null @@ -1,3 +0,0 @@ -package dev.hotwire.core.turbo.util - -internal const val TURBO_REQUEST_CODE_FILES = 37 diff --git a/core/src/main/kotlin/dev/hotwire/core/turbo/util/TurboUriHelper.kt b/core/src/main/kotlin/dev/hotwire/core/turbo/util/TurboUriHelper.kt index c32f0d5..fe7f0cd 100644 --- a/core/src/main/kotlin/dev/hotwire/core/turbo/util/TurboUriHelper.kt +++ b/core/src/main/kotlin/dev/hotwire/core/turbo/util/TurboUriHelper.kt @@ -5,7 +5,7 @@ import android.database.Cursor import android.net.Uri import android.provider.OpenableColumns import android.webkit.MimeTypeMap -import dev.hotwire.core.lib.logging.logError +import dev.hotwire.core.logging.logError import kotlinx.coroutines.withContext import java.io.File import java.io.IOException diff --git a/core/src/main/kotlin/dev/hotwire/core/turbo/views/TurboWebView.kt b/core/src/main/kotlin/dev/hotwire/core/turbo/views/TurboWebView.kt index a31c6b2..d80cea3 100644 --- a/core/src/main/kotlin/dev/hotwire/core/turbo/views/TurboWebView.kt +++ b/core/src/main/kotlin/dev/hotwire/core/turbo/views/TurboWebView.kt @@ -26,10 +26,15 @@ import dev.hotwire.core.turbo.visit.VisitOptions * and available from the Turbo session. */ @SuppressLint("SetJavaScriptEnabled") -open class TurboWebView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : - WebView(context, attrs) { +open class TurboWebView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : WebView(context, attrs) { private val gson = GsonBuilder().disableHtmlEscaping().create() + var elementTouchIsScrollable = false + internal set + init { id = View.generateViewId() settings.javaScriptEnabled = true @@ -80,8 +85,6 @@ open class TurboWebView @JvmOverloads constructor(context: Context, attrs: Attri } } - internal var elementTouchIsScrollable = false - private fun WebView.runJavascript(javascript: String, onComplete: (String?) -> Unit = {}) { context.runOnUiThread { evaluateJavascript(javascript) { diff --git a/core/src/main/kotlin/dev/hotwire/core/turbo/visit/Visit.kt b/core/src/main/kotlin/dev/hotwire/core/turbo/visit/Visit.kt index f6d34a8..d5e63e3 100644 --- a/core/src/main/kotlin/dev/hotwire/core/turbo/visit/Visit.kt +++ b/core/src/main/kotlin/dev/hotwire/core/turbo/visit/Visit.kt @@ -2,7 +2,7 @@ package dev.hotwire.core.turbo.visit import dev.hotwire.core.turbo.session.SessionCallback -internal data class Visit( +data class Visit( val location: String, val destinationIdentifier: Int, val restoreWithCachedSnapshot: Boolean, diff --git a/core/src/main/kotlin/dev/hotwire/core/turbo/visit/VisitDestination.kt b/core/src/main/kotlin/dev/hotwire/core/turbo/visit/VisitDestination.kt new file mode 100644 index 0000000..ad17525 --- /dev/null +++ b/core/src/main/kotlin/dev/hotwire/core/turbo/visit/VisitDestination.kt @@ -0,0 +1,9 @@ +package dev.hotwire.core.turbo.visit + +import android.content.Intent +import androidx.activity.result.ActivityResultLauncher + +interface VisitDestination { + fun isActive(): Boolean + fun activityResultLauncher(requestCode: Int): ActivityResultLauncher? +} diff --git a/core/src/test/kotlin/dev/hotwire/core/bridge/BridgeComponentTest.kt b/core/src/test/kotlin/dev/hotwire/core/bridge/BridgeComponentTest.kt index 46513aa..a9d80f4 100644 --- a/core/src/test/kotlin/dev/hotwire/core/bridge/BridgeComponentTest.kt +++ b/core/src/test/kotlin/dev/hotwire/core/bridge/BridgeComponentTest.kt @@ -11,7 +11,7 @@ import org.junit.Test class BridgeComponentTest { private lateinit var component: TestData.OneBridgeComponent - private val delegate: BridgeDelegate = mock() + private val delegate: BridgeDelegate = mock() private val message = Message( id = "1", diff --git a/core/src/test/kotlin/dev/hotwire/core/bridge/BridgeDelegateTest.kt b/core/src/test/kotlin/dev/hotwire/core/bridge/BridgeDelegateTest.kt index 7615720..4ba0ff8 100644 --- a/core/src/test/kotlin/dev/hotwire/core/bridge/BridgeDelegateTest.kt +++ b/core/src/test/kotlin/dev/hotwire/core/bridge/BridgeDelegateTest.kt @@ -7,10 +7,6 @@ import com.nhaarman.mockito_kotlin.eq import com.nhaarman.mockito_kotlin.mock import com.nhaarman.mockito_kotlin.never import com.nhaarman.mockito_kotlin.whenever -import dev.hotwire.core.config.Hotwire -import dev.hotwire.core.turbo.nav.HotwireNavDestination -import dev.hotwire.core.navigation.navigator.Navigator -import dev.hotwire.core.turbo.session.Session import org.junit.Assert.* import org.junit.Before import org.junit.Rule @@ -18,13 +14,15 @@ import org.junit.Test import org.mockito.Mockito.verify class BridgeDelegateTest { - private lateinit var delegate: BridgeDelegate + private lateinit var delegate: BridgeDelegate private lateinit var lifecycleOwner: TestLifecycleOwner private val bridge: Bridge = mock() private val webView: WebView = mock() - private val navigator: Navigator = mock() - private val destination: HotwireNavDestination = mock() - private val session: Session = mock() + + private val factories = listOf( + BridgeComponentFactory("one", TestData::OneBridgeComponent), + BridgeComponentFactory("two", TestData::TwoBridgeComponent) + ) @Rule @JvmField @@ -33,14 +31,13 @@ class BridgeDelegateTest { @Before fun setup() { whenever(bridge.webView).thenReturn(webView) - whenever(destination.navigator).thenReturn(navigator) - whenever(destination.navigator.session).thenReturn(session) - whenever(session.isReady).thenReturn(true) - - Hotwire.registerBridgeComponents(TestData.componentFactories) Bridge.initialize(bridge) - delegate = BridgeDelegate(location = "https://37signals.com", destination = destination) + delegate = BridgeDelegate( + location = "https://37signals.com", + destination = TestData.AppBridgeDestination(), + componentFactories = factories + ) delegate.bridge = bridge lifecycleOwner = TestLifecycleOwner(Lifecycle.State.STARTED) diff --git a/core/src/test/kotlin/dev/hotwire/core/bridge/BridgeTest.kt b/core/src/test/kotlin/dev/hotwire/core/bridge/BridgeTest.kt index 9d2cdee..fe93c9b 100644 --- a/core/src/test/kotlin/dev/hotwire/core/bridge/BridgeTest.kt +++ b/core/src/test/kotlin/dev/hotwire/core/bridge/BridgeTest.kt @@ -16,7 +16,7 @@ class BridgeTest { private val webView: WebView = mock() private val context: Context = mock() private val repository: Repository = mock() - private val delegate: BridgeDelegate = mock() + private val delegate: BridgeDelegate = mock() @Before fun setup() { diff --git a/core/src/test/kotlin/dev/hotwire/core/bridge/TestData.kt b/core/src/test/kotlin/dev/hotwire/core/bridge/TestData.kt index 201ae66..688c8ba 100644 --- a/core/src/test/kotlin/dev/hotwire/core/bridge/TestData.kt +++ b/core/src/test/kotlin/dev/hotwire/core/bridge/TestData.kt @@ -1,11 +1,6 @@ package dev.hotwire.core.bridge -import dev.hotwire.core.turbo.nav.HotwireNavDestination -import org.mockito.Mockito.mock - object TestData { - private val mockNavDestination: HotwireNavDestination = mock() - val componentFactories = listOf( BridgeComponentFactory("one", TestData::OneBridgeComponent), BridgeComponentFactory("two", TestData::TwoBridgeComponent) @@ -13,17 +8,22 @@ object TestData { val bridgeDelegate = BridgeDelegate( location = "https://37signals.com", - destination = mockNavDestination + destination = AppBridgeDestination(), + componentFactories = componentFactories ) + class AppBridgeDestination : BridgeDestination { + override fun bridgeWebViewIsReady() = true + } + abstract class AppBridgeComponent( name: String, - delegate: BridgeDelegate - ) : BridgeComponent(name, delegate) + delegate: BridgeDelegate + ) : BridgeComponent(name, delegate) class OneBridgeComponent( name: String, - delegate: BridgeDelegate + delegate: BridgeDelegate ) : AppBridgeComponent(name, delegate) { var onStartCalled = false var onStopCalled = false @@ -45,7 +45,7 @@ object TestData { class TwoBridgeComponent( name: String, - delegate: BridgeDelegate + delegate: BridgeDelegate ) : AppBridgeComponent(name, delegate) { override fun onReceive(message: Message) {} } diff --git a/core/src/test/kotlin/dev/hotwire/core/bridge/UserAgentTest.kt b/core/src/test/kotlin/dev/hotwire/core/bridge/UserAgentTest.kt index 3ca8b1e..7d1b97c 100644 --- a/core/src/test/kotlin/dev/hotwire/core/bridge/UserAgentTest.kt +++ b/core/src/test/kotlin/dev/hotwire/core/bridge/UserAgentTest.kt @@ -7,7 +7,7 @@ import org.junit.Test class UserAgentTest { @Test fun userAgentSubstring() { - Hotwire.registerBridgeComponents(TestData.componentFactories) + Hotwire.config.registeredBridgeComponentFactories = TestData.componentFactories val userAgentSubstring = Hotwire.config.userAgentSubstring() assertEquals(userAgentSubstring, "Turbo Native Android; bridge-components: [one two];") @@ -15,7 +15,7 @@ class UserAgentTest { @Test fun userAgent() { - Hotwire.registerBridgeComponents(TestData.componentFactories) + Hotwire.config.registeredBridgeComponentFactories = TestData.componentFactories Hotwire.config.userAgent = "Test; ${Hotwire.config.userAgentSubstring()}" val userAgent = Hotwire.config.userAgent diff --git a/core/src/test/kotlin/dev/hotwire/core/turbo/session/SessionTest.kt b/core/src/test/kotlin/dev/hotwire/core/turbo/session/SessionTest.kt index ed5ab09..36e2936 100644 --- a/core/src/test/kotlin/dev/hotwire/core/turbo/session/SessionTest.kt +++ b/core/src/test/kotlin/dev/hotwire/core/turbo/session/SessionTest.kt @@ -5,13 +5,13 @@ import androidx.appcompat.app.AppCompatActivity import com.nhaarman.mockito_kotlin.never import com.nhaarman.mockito_kotlin.times import com.nhaarman.mockito_kotlin.whenever +import dev.hotwire.core.turbo.errors.HttpError.ServerError import dev.hotwire.core.turbo.errors.LoadError -import dev.hotwire.core.turbo.nav.HotwireNavDestination import dev.hotwire.core.turbo.util.toJson import dev.hotwire.core.turbo.views.TurboWebView import dev.hotwire.core.turbo.visit.Visit +import dev.hotwire.core.turbo.visit.VisitDestination import dev.hotwire.core.turbo.visit.VisitOptions -import dev.hotwire.core.turbo.errors.HttpError.ServerError import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Test @@ -31,7 +31,6 @@ class SessionTest { @Mock private lateinit var webView: TurboWebView @Mock - private lateinit var navDestination: HotwireNavDestination private lateinit var activity: AppCompatActivity private lateinit var session: Session private lateinit var visit: Visit @@ -52,8 +51,12 @@ class SessionTest { options = VisitOptions() ) - whenever(callback.visitNavDestination()).thenReturn(navDestination) - whenever(navDestination.isActive).thenReturn(true) + val visitDestination = object : VisitDestination { + override fun isActive() = true + override fun activityResultLauncher(requestCode: Int) = null + } + + whenever(callback.visitDestination()).thenReturn(visitDestination) } @Test diff --git a/demo/build.gradle.kts b/demo/build.gradle.kts index 27289ac..f42b89d 100644 --- a/demo/build.gradle.kts +++ b/demo/build.gradle.kts @@ -51,8 +51,11 @@ android { } dependencies { + implementation(project(":core")) + implementation(project(":navigation-fragments")) + // Material - implementation("com.google.android.material:material:1.11.0") + implementation("com.google.android.material:material:1.12.0") // AndroidX implementation("androidx.constraintlayout:constraintlayout:2.1.4") @@ -64,5 +67,4 @@ dependencies { // Images implementation("io.coil-kt:coil:2.6.0") - implementation(project(":core")) } \ No newline at end of file diff --git a/demo/src/main/kotlin/dev/hotwire/demo/DemoApplication.kt b/demo/src/main/kotlin/dev/hotwire/demo/DemoApplication.kt index 5875ee6..e9e3277 100644 --- a/demo/src/main/kotlin/dev/hotwire/demo/DemoApplication.kt +++ b/demo/src/main/kotlin/dev/hotwire/demo/DemoApplication.kt @@ -5,8 +5,6 @@ import dev.hotwire.core.BuildConfig import dev.hotwire.core.bridge.BridgeComponentFactory import dev.hotwire.core.bridge.KotlinXJsonConverter import dev.hotwire.core.config.Hotwire -import dev.hotwire.core.navigation.routing.AppNavigationRouteDecisionHandler -import dev.hotwire.core.navigation.routing.BrowserTabRouteDecisionHandler import dev.hotwire.core.turbo.config.PathConfiguration import dev.hotwire.demo.bridge.FormComponent import dev.hotwire.demo.bridge.MenuComponent @@ -18,6 +16,12 @@ import dev.hotwire.demo.features.web.WebBottomSheetFragment import dev.hotwire.demo.features.web.WebFragment import dev.hotwire.demo.features.web.WebHomeFragment import dev.hotwire.demo.features.web.WebModalFragment +import dev.hotwire.navigation.config.defaultFragmentDestination +import dev.hotwire.navigation.config.registerBridgeComponents +import dev.hotwire.navigation.config.registerFragmentDestinations +import dev.hotwire.navigation.config.registerRouteDecisionHandlers +import dev.hotwire.navigation.routing.AppNavigationRouteDecisionHandler +import dev.hotwire.navigation.routing.BrowserTabRouteDecisionHandler class DemoApplication : Application() { override fun onCreate() { @@ -26,10 +30,6 @@ class DemoApplication : Application() { } private fun configureApp() { - // Configure debugging - Hotwire.config.debugLoggingEnabled = BuildConfig.DEBUG - Hotwire.config.webViewDebuggingEnabled = BuildConfig.DEBUG - // Loads the path configuration Hotwire.loadPathConfiguration( context = this, @@ -66,7 +66,9 @@ class DemoApplication : Application() { )) // Set configuration options + Hotwire.config.debugLoggingEnabled = BuildConfig.DEBUG + Hotwire.config.webViewDebuggingEnabled = BuildConfig.DEBUG Hotwire.config.jsonConverter = KotlinXJsonConverter() Hotwire.config.userAgent = "Hotwire Demo; ${Hotwire.config.userAgentSubstring()}" } -} \ No newline at end of file +} diff --git a/demo/src/main/kotlin/dev/hotwire/demo/bridge/FormComponent.kt b/demo/src/main/kotlin/dev/hotwire/demo/bridge/FormComponent.kt index 3c9dec9..162f642 100644 --- a/demo/src/main/kotlin/dev/hotwire/demo/bridge/FormComponent.kt +++ b/demo/src/main/kotlin/dev/hotwire/demo/bridge/FormComponent.kt @@ -11,6 +11,7 @@ import dev.hotwire.core.bridge.BridgeDelegate import dev.hotwire.core.bridge.Message import dev.hotwire.demo.R import dev.hotwire.demo.databinding.FormComponentSubmitBinding +import dev.hotwire.navigation.destinations.HotwireNavDestination import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -20,8 +21,8 @@ import kotlinx.serialization.Serializable */ class FormComponent( name: String, - private val delegate: BridgeDelegate -) : BridgeComponent(name, delegate) { + private val delegate: BridgeDelegate +) : BridgeComponent(name, delegate) { private val submitButtonItemId = 37 private var submitMenuItem: MenuItem? = null diff --git a/demo/src/main/kotlin/dev/hotwire/demo/bridge/MenuComponent.kt b/demo/src/main/kotlin/dev/hotwire/demo/bridge/MenuComponent.kt index 6156c6e..e572d7b 100644 --- a/demo/src/main/kotlin/dev/hotwire/demo/bridge/MenuComponent.kt +++ b/demo/src/main/kotlin/dev/hotwire/demo/bridge/MenuComponent.kt @@ -9,6 +9,7 @@ import dev.hotwire.core.bridge.BridgeComponent import dev.hotwire.core.bridge.BridgeDelegate import dev.hotwire.core.bridge.Message import dev.hotwire.demo.databinding.MenuComponentBottomSheetBinding +import dev.hotwire.navigation.destinations.HotwireNavDestination import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -18,8 +19,8 @@ import kotlinx.serialization.Serializable */ class MenuComponent( name: String, - private val delegate: BridgeDelegate -) : BridgeComponent(name, delegate) { + private val delegate: BridgeDelegate +) : BridgeComponent(name, delegate) { private val fragment: Fragment get() = delegate.destination.fragment diff --git a/demo/src/main/kotlin/dev/hotwire/demo/bridge/OverflowMenuComponent.kt b/demo/src/main/kotlin/dev/hotwire/demo/bridge/OverflowMenuComponent.kt index 9b124ef..569ae83 100644 --- a/demo/src/main/kotlin/dev/hotwire/demo/bridge/OverflowMenuComponent.kt +++ b/demo/src/main/kotlin/dev/hotwire/demo/bridge/OverflowMenuComponent.kt @@ -7,6 +7,7 @@ import dev.hotwire.core.bridge.BridgeComponent import dev.hotwire.core.bridge.BridgeDelegate import dev.hotwire.core.bridge.Message import dev.hotwire.demo.R +import dev.hotwire.navigation.destinations.HotwireNavDestination import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -16,8 +17,8 @@ import kotlinx.serialization.Serializable */ class OverflowMenuComponent( name: String, - private val delegate: BridgeDelegate -) : BridgeComponent(name, delegate) { + private val delegate: BridgeDelegate +) : BridgeComponent(name, delegate) { private val fragment: Fragment get() = delegate.destination.fragment diff --git a/demo/src/main/kotlin/dev/hotwire/demo/features/imageviewer/ImageViewerFragment.kt b/demo/src/main/kotlin/dev/hotwire/demo/features/imageviewer/ImageViewerFragment.kt index 45a203b..9703e3c 100644 --- a/demo/src/main/kotlin/dev/hotwire/demo/features/imageviewer/ImageViewerFragment.kt +++ b/demo/src/main/kotlin/dev/hotwire/demo/features/imageviewer/ImageViewerFragment.kt @@ -6,10 +6,10 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageView import coil.load -import dev.hotwire.core.navigation.fragments.HotwireFragment -import dev.hotwire.core.turbo.nav.HotwireDestination -import dev.hotwire.core.turbo.util.displayBackButtonAsCloseIcon import dev.hotwire.demo.R +import dev.hotwire.navigation.destinations.HotwireDestination +import dev.hotwire.navigation.fragments.HotwireFragment +import dev.hotwire.navigation.util.displayBackButtonAsCloseIcon @HotwireDestination(uri = "turbo://fragment/image_viewer") class ImageViewerFragment : HotwireFragment() { diff --git a/demo/src/main/kotlin/dev/hotwire/demo/features/numbers/NumberBottomSheetFragment.kt b/demo/src/main/kotlin/dev/hotwire/demo/features/numbers/NumberBottomSheetFragment.kt index 432cd62..e3d06c6 100644 --- a/demo/src/main/kotlin/dev/hotwire/demo/features/numbers/NumberBottomSheetFragment.kt +++ b/demo/src/main/kotlin/dev/hotwire/demo/features/numbers/NumberBottomSheetFragment.kt @@ -6,10 +6,10 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import com.google.android.material.textview.MaterialTextView -import dev.hotwire.core.navigation.fragments.HotwireBottomSheetFragment import dev.hotwire.core.turbo.config.PathConfigurationProperties -import dev.hotwire.core.turbo.nav.HotwireDestination import dev.hotwire.demo.R +import dev.hotwire.navigation.destinations.HotwireDestination +import dev.hotwire.navigation.fragments.HotwireBottomSheetFragment @HotwireDestination(uri = "turbo://fragment/numbers/sheet") class NumberBottomSheetFragment : HotwireBottomSheetFragment() { diff --git a/demo/src/main/kotlin/dev/hotwire/demo/features/numbers/NumbersFragment.kt b/demo/src/main/kotlin/dev/hotwire/demo/features/numbers/NumbersFragment.kt index f4034f2..d6892c3 100644 --- a/demo/src/main/kotlin/dev/hotwire/demo/features/numbers/NumbersFragment.kt +++ b/demo/src/main/kotlin/dev/hotwire/demo/features/numbers/NumbersFragment.kt @@ -6,10 +6,10 @@ import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import dev.hotwire.core.navigation.fragments.HotwireFragment -import dev.hotwire.core.turbo.nav.HotwireDestination import dev.hotwire.demo.R import dev.hotwire.demo.Urls +import dev.hotwire.navigation.destinations.HotwireDestination +import dev.hotwire.navigation.fragments.HotwireFragment @HotwireDestination(uri = "turbo://fragment/numbers") class NumbersFragment : HotwireFragment(), NumbersFragmentCallback { diff --git a/demo/src/main/kotlin/dev/hotwire/demo/features/web/WebBottomSheetFragment.kt b/demo/src/main/kotlin/dev/hotwire/demo/features/web/WebBottomSheetFragment.kt index a092a92..c0bdda8 100644 --- a/demo/src/main/kotlin/dev/hotwire/demo/features/web/WebBottomSheetFragment.kt +++ b/demo/src/main/kotlin/dev/hotwire/demo/features/web/WebBottomSheetFragment.kt @@ -3,9 +3,9 @@ package dev.hotwire.demo.features.web import android.os.Bundle import android.view.MenuItem import android.view.View -import dev.hotwire.core.navigation.fragments.HotwireWebBottomSheetFragment -import dev.hotwire.core.turbo.nav.HotwireDestination import dev.hotwire.demo.R +import dev.hotwire.navigation.destinations.HotwireDestination +import dev.hotwire.navigation.fragments.HotwireWebBottomSheetFragment @HotwireDestination(uri = "turbo://fragment/web/modal/sheet") class WebBottomSheetFragment : HotwireWebBottomSheetFragment() { diff --git a/demo/src/main/kotlin/dev/hotwire/demo/features/web/WebFragment.kt b/demo/src/main/kotlin/dev/hotwire/demo/features/web/WebFragment.kt index a920ce4..aa11d10 100644 --- a/demo/src/main/kotlin/dev/hotwire/demo/features/web/WebFragment.kt +++ b/demo/src/main/kotlin/dev/hotwire/demo/features/web/WebFragment.kt @@ -3,14 +3,14 @@ package dev.hotwire.demo.features.web import android.os.Bundle import android.view.MenuItem import android.view.View -import dev.hotwire.core.navigation.fragments.HotwireWebFragment import dev.hotwire.core.turbo.errors.HttpError import dev.hotwire.core.turbo.errors.VisitError -import dev.hotwire.core.turbo.nav.HotwireDestination import dev.hotwire.core.turbo.visit.VisitAction.REPLACE import dev.hotwire.core.turbo.visit.VisitOptions import dev.hotwire.demo.R import dev.hotwire.demo.Urls +import dev.hotwire.navigation.destinations.HotwireDestination +import dev.hotwire.navigation.fragments.HotwireWebFragment @HotwireDestination(uri = "turbo://fragment/web") open class WebFragment : HotwireWebFragment() { diff --git a/demo/src/main/kotlin/dev/hotwire/demo/features/web/WebHomeFragment.kt b/demo/src/main/kotlin/dev/hotwire/demo/features/web/WebHomeFragment.kt index f1deb17..be3919d 100644 --- a/demo/src/main/kotlin/dev/hotwire/demo/features/web/WebHomeFragment.kt +++ b/demo/src/main/kotlin/dev/hotwire/demo/features/web/WebHomeFragment.kt @@ -6,7 +6,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import dev.hotwire.core.turbo.errors.VisitError -import dev.hotwire.core.turbo.nav.HotwireDestination +import dev.hotwire.navigation.destinations.HotwireDestination import dev.hotwire.demo.R @HotwireDestination(uri = "turbo://fragment/web/home") diff --git a/demo/src/main/kotlin/dev/hotwire/demo/features/web/WebModalFragment.kt b/demo/src/main/kotlin/dev/hotwire/demo/features/web/WebModalFragment.kt index ff0b011..85d98b3 100644 --- a/demo/src/main/kotlin/dev/hotwire/demo/features/web/WebModalFragment.kt +++ b/demo/src/main/kotlin/dev/hotwire/demo/features/web/WebModalFragment.kt @@ -1,6 +1,6 @@ package dev.hotwire.demo.features.web -import dev.hotwire.core.turbo.nav.HotwireDestination +import dev.hotwire.navigation.destinations.HotwireDestination @HotwireDestination(uri = "turbo://fragment/web/modal") class WebModalFragment : WebFragment() diff --git a/demo/src/main/kotlin/dev/hotwire/demo/main/MainActivity.kt b/demo/src/main/kotlin/dev/hotwire/demo/main/MainActivity.kt index 87902b9..5b6c2c3 100644 --- a/demo/src/main/kotlin/dev/hotwire/demo/main/MainActivity.kt +++ b/demo/src/main/kotlin/dev/hotwire/demo/main/MainActivity.kt @@ -1,10 +1,10 @@ package dev.hotwire.demo.main import android.os.Bundle -import dev.hotwire.core.navigation.activities.HotwireActivity -import dev.hotwire.core.navigation.navigator.NavigatorConfiguration import dev.hotwire.demo.R import dev.hotwire.demo.Urls +import dev.hotwire.navigation.activities.HotwireActivity +import dev.hotwire.navigation.navigator.NavigatorConfiguration class MainActivity : HotwireActivity() { override fun onCreate(savedInstanceState: Bundle?) { diff --git a/demo/src/main/res/layout/activity_main.xml b/demo/src/main/res/layout/activity_main.xml index d33ea25..2739ea3 100644 --- a/demo/src/main/res/layout/activity_main.xml +++ b/demo/src/main/res/layout/activity_main.xml @@ -9,7 +9,7 @@ diff --git a/navigation-fragments/.gitignore b/navigation-fragments/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/navigation-fragments/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/navigation-fragments/build.gradle.kts b/navigation-fragments/build.gradle.kts new file mode 100644 index 0000000..2aef559 --- /dev/null +++ b/navigation-fragments/build.gradle.kts @@ -0,0 +1,84 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "dev.hotwire.navigation" + compileSdk = 34 + + testOptions.unitTests.isIncludeAndroidResources = true + testOptions.unitTests.isReturnDefaultValues = true + testOptions.targetSdk = 34 + + defaultConfig { + minSdk = 28 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + consumerProguardFiles("proguard-consumer-rules.pro") + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + buildFeatures { + buildConfig = true + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } + + sourceSets { + named("main") { java { srcDirs("src/main/kotlin") } } + named("test") { java { srcDirs("src/test/kotlin") } } + named("debug") { java { srcDirs("src/debug/kotlin") } } + } +} + +dependencies { + implementation(project(":core")) + + // Kotlin + implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.23") + + // AndroidX + implementation("androidx.constraintlayout:constraintlayout:2.1.4") + implementation("androidx.lifecycle:lifecycle-common:2.8.0") + implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") + + // Material + implementation("com.google.android.material:material:1.12.0") + + // Browser + implementation("androidx.browser:browser:1.8.0") + + // Exported AndroidX dependencies + api("androidx.activity:activity-ktx:1.9.0") + api("androidx.fragment:fragment-ktx:1.7.1") + api("androidx.navigation:navigation-fragment-ktx:2.7.7") + api("androidx.navigation:navigation-ui-ktx:2.7.7") + + // Tests + testImplementation("androidx.test:core:1.5.0") // Robolectric + testImplementation("org.assertj:assertj-core:3.25.3") + testImplementation("androidx.navigation:navigation-testing:2.7.7") + testImplementation("org.robolectric:robolectric:4.12.1") + testImplementation("org.mockito:mockito-core:5.11.0") + testImplementation("com.nhaarman:mockito-kotlin:1.6.0") + testImplementation("junit:junit:4.13.2") +} + +// TODO add publishing support diff --git a/navigation-fragments/proguard-consumer-rules.pro b/navigation-fragments/proguard-consumer-rules.pro new file mode 100644 index 0000000..65151ca --- /dev/null +++ b/navigation-fragments/proguard-consumer-rules.pro @@ -0,0 +1,6 @@ +# The Android Gradle plugin allows to define ProGuard rules which get embedded in the AAR. +# These ProGuard rules are automatically applied when a consumer app sets minifyEnabled to true. +# The custom rule file must be defined using the 'consumerProguardFiles' property in your +# build.gradle.kts file. + +-keep class dev.hotwire.navigation.** { *; } diff --git a/navigation-fragments/proguard-rules.pro b/navigation-fragments/proguard-rules.pro new file mode 100644 index 0000000..567edf8 --- /dev/null +++ b/navigation-fragments/proguard-rules.pro @@ -0,0 +1,23 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +-keep class dev.hotwire.navigation.** { *; } + +# Uncomment this to preserve the line number information for +# debugging stack traces. +-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/core/src/main/kotlin/dev/hotwire/core/navigation/activities/HotwireActivity.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/activities/HotwireActivity.kt similarity index 81% rename from core/src/main/kotlin/dev/hotwire/core/navigation/activities/HotwireActivity.kt rename to navigation-fragments/src/main/java/dev/hotwire/navigation/activities/HotwireActivity.kt index 34dac08..3735c4e 100644 --- a/core/src/main/kotlin/dev/hotwire/core/navigation/activities/HotwireActivity.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/activities/HotwireActivity.kt @@ -1,8 +1,8 @@ -package dev.hotwire.core.navigation.activities +package dev.hotwire.navigation.activities import android.os.Bundle import androidx.appcompat.app.AppCompatActivity -import dev.hotwire.core.navigation.navigator.NavigatorConfiguration +import dev.hotwire.navigation.navigator.NavigatorConfiguration /** * Activity that should be implemented by any Activity using Hotwire. diff --git a/core/src/main/kotlin/dev/hotwire/core/navigation/activities/HotwireActivityDelegate.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/activities/HotwireActivityDelegate.kt similarity index 92% rename from core/src/main/kotlin/dev/hotwire/core/navigation/activities/HotwireActivityDelegate.kt rename to navigation-fragments/src/main/java/dev/hotwire/navigation/activities/HotwireActivityDelegate.kt index 781bb14..281178f 100644 --- a/core/src/main/kotlin/dev/hotwire/core/navigation/activities/HotwireActivityDelegate.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/activities/HotwireActivityDelegate.kt @@ -1,12 +1,12 @@ -package dev.hotwire.core.navigation.activities +package dev.hotwire.navigation.activities import androidx.activity.OnBackPressedCallback import androidx.annotation.IdRes import androidx.navigation.NavController -import dev.hotwire.core.navigation.navigator.NavigatorHost -import dev.hotwire.core.navigation.navigator.NavigatorConfiguration -import dev.hotwire.core.navigation.navigator.Navigator -import dev.hotwire.core.turbo.observers.HotwireActivityObserver +import dev.hotwire.navigation.navigator.Navigator +import dev.hotwire.navigation.navigator.NavigatorConfiguration +import dev.hotwire.navigation.navigator.NavigatorHost +import dev.hotwire.navigation.observers.HotwireActivityObserver /** * Initializes the Activity for Hotwire navigation and provides all the hooks for an diff --git a/navigation-fragments/src/main/java/dev/hotwire/navigation/config/HotwireNavigation.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/config/HotwireNavigation.kt new file mode 100644 index 0000000..4176505 --- /dev/null +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/config/HotwireNavigation.kt @@ -0,0 +1,65 @@ +package dev.hotwire.navigation.config + +import androidx.fragment.app.Fragment +import dev.hotwire.core.bridge.BridgeComponent +import dev.hotwire.core.bridge.BridgeComponentFactory +import dev.hotwire.core.config.Hotwire +import dev.hotwire.navigation.destinations.HotwireNavDestination +import dev.hotwire.navigation.fragments.HotwireWebBottomSheetFragment +import dev.hotwire.navigation.fragments.HotwireWebFragment +import dev.hotwire.navigation.routing.AppNavigationRouteDecisionHandler +import dev.hotwire.navigation.routing.BrowserRouteDecisionHandler +import dev.hotwire.navigation.routing.Router +import kotlin.reflect.KClass + +internal object HotwireNavigation { + var router = Router(listOf( + AppNavigationRouteDecisionHandler(), + BrowserRouteDecisionHandler() + )) + + var defaultFragmentDestination: KClass = HotwireWebFragment::class + + var registeredFragmentDestinations: List> = listOf( + HotwireWebFragment::class, + HotwireWebBottomSheetFragment::class + ) + + @Suppress("UNCHECKED_CAST") + var registeredBridgeComponentFactories: List>> + get() = Hotwire.config.registeredBridgeComponentFactories as List>> + set(value) { Hotwire.config.registeredBridgeComponentFactories = value } +} + +/** + * Registers the [Router.RouteDecisionHandler] instances that determine whether to route location + * urls within in-app navigation or with alternative custom behaviors. + */ +fun Hotwire.registerRouteDecisionHandlers(decisionHandlers: List) { + HotwireNavigation.router = Router(decisionHandlers) +} + +/** + * Register bridge components that the app supports. Every possible bridge + * component, wrapped in a [BridgeComponentFactory], must be provided here. + */ +fun Hotwire.registerBridgeComponents(factories: List>>) { + config.registeredBridgeComponentFactories = factories +} + +/** + * The default fragment destination for web requests. If you have not + * loaded a path configuration with a matching rule and a `uri` available + * for all possible paths, this destination will be used as the default. + */ +var Hotwire.defaultFragmentDestination: KClass + get() = HotwireNavigation.defaultFragmentDestination + set(value) { HotwireNavigation.defaultFragmentDestination = value } + +/** + * Register fragment destinations that can be navigated to. Every possible + * destination must be provided here. + */ +fun Hotwire.registerFragmentDestinations(destinations: List>) { + HotwireNavigation.registeredFragmentDestinations = destinations +} diff --git a/core/src/main/kotlin/dev/hotwire/core/turbo/nav/HotwireDestination.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/destinations/HotwireDestination.kt similarity index 94% rename from core/src/main/kotlin/dev/hotwire/core/turbo/nav/HotwireDestination.kt rename to navigation-fragments/src/main/java/dev/hotwire/navigation/destinations/HotwireDestination.kt index 21914b2..beee101 100644 --- a/core/src/main/kotlin/dev/hotwire/core/turbo/nav/HotwireDestination.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/destinations/HotwireDestination.kt @@ -1,4 +1,4 @@ -package dev.hotwire.core.turbo.nav +package dev.hotwire.navigation.destinations import kotlin.reflect.KClass import kotlin.reflect.full.findAnnotation diff --git a/core/src/main/kotlin/dev/hotwire/core/turbo/nav/HotwireNavDestination.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/destinations/HotwireNavDestination.kt similarity index 87% rename from core/src/main/kotlin/dev/hotwire/core/turbo/nav/HotwireNavDestination.kt rename to navigation-fragments/src/main/java/dev/hotwire/navigation/destinations/HotwireNavDestination.kt index 2f9357b..20a67c0 100644 --- a/core/src/main/kotlin/dev/hotwire/core/turbo/nav/HotwireNavDestination.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/destinations/HotwireNavDestination.kt @@ -1,4 +1,4 @@ -package dev.hotwire.core.turbo.nav +package dev.hotwire.navigation.destinations import android.content.Intent import android.os.Bundle @@ -7,24 +7,25 @@ import androidx.appcompat.widget.Toolbar import androidx.fragment.app.Fragment import androidx.navigation.NavOptions import androidx.navigation.navOptions -import dev.hotwire.core.R +import dev.hotwire.core.bridge.BridgeDestination import dev.hotwire.core.config.Hotwire -import dev.hotwire.core.config.Hotwire.pathConfiguration -import dev.hotwire.core.navigation.activities.HotwireActivity -import dev.hotwire.core.navigation.fragments.HotwireFragmentViewModel -import dev.hotwire.core.navigation.navigator.Navigator -import dev.hotwire.core.navigation.routing.Router import dev.hotwire.core.turbo.config.PathConfigurationProperties import dev.hotwire.core.turbo.config.context -import dev.hotwire.core.navigation.fragments.HotwireFragmentDelegate -import dev.hotwire.core.navigation.navigator.NestedNavigatorHostDelegate +import dev.hotwire.core.turbo.nav.TurboNavPresentationContext import dev.hotwire.core.turbo.visit.VisitAction +import dev.hotwire.navigation.R +import dev.hotwire.navigation.activities.HotwireActivity +import dev.hotwire.navigation.config.HotwireNavigation +import dev.hotwire.navigation.fragments.HotwireFragmentDelegate +import dev.hotwire.navigation.fragments.HotwireFragmentViewModel +import dev.hotwire.navigation.navigator.Navigator +import dev.hotwire.navigation.routing.Router /** * The primary interface that a navigable Fragment implements to provide the library with * the information it needs to properly navigate. */ -interface HotwireNavDestination { +interface HotwireNavDestination : BridgeDestination { /** * Gets the navigator instance associated with this destination. */ @@ -47,7 +48,7 @@ interface HotwireNavDestination { * destination. */ val pathProperties: PathConfigurationProperties - get() = pathConfiguration.properties(location) + get() = Hotwire.config.pathConfiguration.properties(location) /** * Gets the [HotwireFragmentViewModel] associated with this destination. @@ -116,7 +117,7 @@ interface HotwireNavDestination { * but it's recommend to use dedicated [Router.RouteDecisionHandler] instances for routing logic. */ fun decideRoute(newLocation: String): Router.Decision { - return Hotwire.router.decideRoute( + return HotwireNavigation.router.decideRoute( location = newLocation, configuration = navigator.configuration, activity = fragment.requireActivity() as HotwireActivity @@ -173,6 +174,10 @@ interface HotwireNavDestination { fun prepareNavigation(onReady: () -> Unit) + override fun bridgeWebViewIsReady(): Boolean { + return navigator.session.isReady + } + private val Bundle.location get() = getString("location") } diff --git a/core/src/main/kotlin/dev/hotwire/core/turbo/nav/HotwireNavDialogDestination.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/destinations/HotwireNavDialogDestination.kt similarity index 82% rename from core/src/main/kotlin/dev/hotwire/core/turbo/nav/HotwireNavDialogDestination.kt rename to navigation-fragments/src/main/java/dev/hotwire/navigation/destinations/HotwireNavDialogDestination.kt index 89911a6..e8ff79c 100644 --- a/core/src/main/kotlin/dev/hotwire/core/turbo/nav/HotwireNavDialogDestination.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/destinations/HotwireNavDialogDestination.kt @@ -1,4 +1,4 @@ -package dev.hotwire.core.turbo.nav +package dev.hotwire.navigation.destinations /** * The interface that a navigable DialogFragment implements to provide the library with diff --git a/core/src/main/kotlin/dev/hotwire/core/navigation/fragments/HotwireBottomSheetFragment.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/fragments/HotwireBottomSheetFragment.kt similarity index 92% rename from core/src/main/kotlin/dev/hotwire/core/navigation/fragments/HotwireBottomSheetFragment.kt rename to navigation-fragments/src/main/java/dev/hotwire/navigation/fragments/HotwireBottomSheetFragment.kt index da19d80..1f8b7f4 100644 --- a/core/src/main/kotlin/dev/hotwire/core/navigation/fragments/HotwireBottomSheetFragment.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/fragments/HotwireBottomSheetFragment.kt @@ -1,4 +1,4 @@ -package dev.hotwire.core.navigation.fragments +package dev.hotwire.navigation.fragments import android.content.DialogInterface import android.content.Intent @@ -6,12 +6,12 @@ import android.os.Bundle import android.view.View import androidx.appcompat.widget.Toolbar import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import dev.hotwire.core.R -import dev.hotwire.core.navigation.navigator.Navigator -import dev.hotwire.core.navigation.navigator.NavigatorHost import dev.hotwire.core.turbo.config.title -import dev.hotwire.core.turbo.nav.HotwireNavDestination -import dev.hotwire.core.turbo.nav.HotwireNavDialogDestination +import dev.hotwire.navigation.R +import dev.hotwire.navigation.destinations.HotwireNavDestination +import dev.hotwire.navigation.destinations.HotwireNavDialogDestination +import dev.hotwire.navigation.navigator.Navigator +import dev.hotwire.navigation.navigator.NavigatorHost /** * The base class from which all bottom sheet native fragments in a diff --git a/core/src/main/kotlin/dev/hotwire/core/navigation/fragments/HotwireFragment.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/fragments/HotwireFragment.kt similarity index 93% rename from core/src/main/kotlin/dev/hotwire/core/navigation/fragments/HotwireFragment.kt rename to navigation-fragments/src/main/java/dev/hotwire/navigation/fragments/HotwireFragment.kt index d9735b9..6333cae 100644 --- a/core/src/main/kotlin/dev/hotwire/core/navigation/fragments/HotwireFragment.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/fragments/HotwireFragment.kt @@ -1,19 +1,19 @@ -package dev.hotwire.core.navigation.fragments +package dev.hotwire.navigation.fragments import android.content.Intent import android.os.Bundle import android.view.View import androidx.appcompat.widget.Toolbar import androidx.fragment.app.Fragment -import dev.hotwire.core.R -import dev.hotwire.core.navigation.navigator.Navigator -import dev.hotwire.core.navigation.navigator.NavigatorHost import dev.hotwire.core.turbo.config.context import dev.hotwire.core.turbo.config.title -import dev.hotwire.core.turbo.nav.HotwireNavDestination import dev.hotwire.core.turbo.nav.TurboNavPresentationContext -import dev.hotwire.core.turbo.observers.HotwireWindowThemeObserver -import dev.hotwire.core.turbo.session.SessionModalResult +import dev.hotwire.navigation.R +import dev.hotwire.navigation.destinations.HotwireNavDestination +import dev.hotwire.navigation.navigator.Navigator +import dev.hotwire.navigation.navigator.NavigatorHost +import dev.hotwire.navigation.observers.HotwireWindowThemeObserver +import dev.hotwire.navigation.session.SessionModalResult /** * The base class from which all "standard" native Fragments (non-dialogs) in a diff --git a/core/src/main/kotlin/dev/hotwire/core/navigation/fragments/HotwireFragmentDelegate.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/fragments/HotwireFragmentDelegate.kt similarity index 83% rename from core/src/main/kotlin/dev/hotwire/core/navigation/fragments/HotwireFragmentDelegate.kt rename to navigation-fragments/src/main/java/dev/hotwire/navigation/fragments/HotwireFragmentDelegate.kt index 6d1c266..05c1083 100644 --- a/core/src/main/kotlin/dev/hotwire/core/navigation/fragments/HotwireFragmentDelegate.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/fragments/HotwireFragmentDelegate.kt @@ -1,11 +1,11 @@ -package dev.hotwire.core.navigation.fragments +package dev.hotwire.navigation.fragments -import dev.hotwire.core.lib.logging.logEvent -import dev.hotwire.core.turbo.nav.HotwireNavDestination -import dev.hotwire.core.turbo.session.SessionModalResult -import dev.hotwire.core.turbo.session.SessionViewModel -import dev.hotwire.core.turbo.util.displayBackButton -import dev.hotwire.core.turbo.util.displayBackButtonAsCloseIcon +import dev.hotwire.navigation.logging.logEvent +import dev.hotwire.navigation.destinations.HotwireNavDestination +import dev.hotwire.navigation.session.SessionModalResult +import dev.hotwire.navigation.session.SessionViewModel +import dev.hotwire.navigation.util.displayBackButton +import dev.hotwire.navigation.util.displayBackButtonAsCloseIcon /** * Provides all the hooks for a Fragment to delegate its lifecycle events @@ -17,8 +17,14 @@ class HotwireFragmentDelegate(private val navDestination: HotwireNavDestination) private val location = navDestination.location private val navigator = navDestination.navigator - internal val sessionViewModel = SessionViewModel.get(navigator.session.sessionName, fragment.requireActivity()) - internal val fragmentViewModel = HotwireFragmentViewModel.get(location, fragment) + internal val sessionViewModel = SessionViewModel.get( + sessionName = navigator.configuration.name, + activity = fragment.requireActivity() + ) + internal val fragmentViewModel = HotwireFragmentViewModel.get( + location = location, + fragment = fragment + ) fun prepareNavigation(onReady: () -> Unit) { onReady() @@ -108,7 +114,7 @@ class HotwireFragmentDelegate(private val navDestination: HotwireNavDestination) private fun logEvent(event: String, vararg params: Pair) { val attributes = params.toMutableList().apply { - add(0, "session" to navigator.session.sessionName) + add(0, "navigator" to navigator.configuration.name) add("fragment" to fragment.javaClass.simpleName) } logEvent(event, attributes) diff --git a/core/src/main/kotlin/dev/hotwire/core/navigation/fragments/HotwireFragmentViewModel.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/fragments/HotwireFragmentViewModel.kt similarity index 93% rename from core/src/main/kotlin/dev/hotwire/core/navigation/fragments/HotwireFragmentViewModel.kt rename to navigation-fragments/src/main/java/dev/hotwire/navigation/fragments/HotwireFragmentViewModel.kt index cdd1201..c228e04 100644 --- a/core/src/main/kotlin/dev/hotwire/core/navigation/fragments/HotwireFragmentViewModel.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/fragments/HotwireFragmentViewModel.kt @@ -1,4 +1,4 @@ -package dev.hotwire.core.navigation.fragments +package dev.hotwire.navigation.fragments import androidx.fragment.app.Fragment import androidx.lifecycle.MutableLiveData diff --git a/core/src/main/kotlin/dev/hotwire/core/navigation/fragments/HotwireWebBottomSheetFragment.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/fragments/HotwireWebBottomSheetFragment.kt similarity index 90% rename from core/src/main/kotlin/dev/hotwire/core/navigation/fragments/HotwireWebBottomSheetFragment.kt rename to navigation-fragments/src/main/java/dev/hotwire/navigation/fragments/HotwireWebBottomSheetFragment.kt index e3a8d6d..a5f60ba 100644 --- a/core/src/main/kotlin/dev/hotwire/core/navigation/fragments/HotwireWebBottomSheetFragment.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/fragments/HotwireWebBottomSheetFragment.kt @@ -1,4 +1,4 @@ -package dev.hotwire.core.navigation.fragments +package dev.hotwire.navigation.fragments import android.annotation.SuppressLint import android.content.DialogInterface @@ -8,14 +8,15 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.activity.result.ActivityResultLauncher -import dev.hotwire.core.R import dev.hotwire.core.bridge.BridgeDelegate import dev.hotwire.core.turbo.errors.VisitError -import dev.hotwire.core.turbo.nav.HotwireDestination import dev.hotwire.core.turbo.util.TURBO_REQUEST_CODE_FILES -import dev.hotwire.core.turbo.views.TurboView import dev.hotwire.core.turbo.views.TurboWebChromeClient import dev.hotwire.core.turbo.views.TurboWebView +import dev.hotwire.navigation.R +import dev.hotwire.navigation.config.HotwireNavigation +import dev.hotwire.navigation.destinations.HotwireDestination +import dev.hotwire.navigation.views.TurboView /** * The base class from which all bottom sheet web fragments in a @@ -28,7 +29,11 @@ open class HotwireWebBottomSheetFragment : HotwireBottomSheetFragment(), Hotwire private lateinit var webDelegate: HotwireWebFragmentDelegate private val bridgeDelegate by lazy { - BridgeDelegate(location = location, destination = this) + BridgeDelegate( + location = location, + destination = this, + componentFactories = HotwireNavigation.registeredBridgeComponentFactories + ) } override fun onCreate(savedInstanceState: Bundle?) { diff --git a/core/src/main/kotlin/dev/hotwire/core/navigation/fragments/HotwireWebFragment.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/fragments/HotwireWebFragment.kt similarity index 90% rename from core/src/main/kotlin/dev/hotwire/core/navigation/fragments/HotwireWebFragment.kt rename to navigation-fragments/src/main/java/dev/hotwire/navigation/fragments/HotwireWebFragment.kt index ec00647..1f69939 100644 --- a/core/src/main/kotlin/dev/hotwire/core/navigation/fragments/HotwireWebFragment.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/fragments/HotwireWebFragment.kt @@ -1,4 +1,4 @@ -package dev.hotwire.core.navigation.fragments +package dev.hotwire.navigation.fragments import android.annotation.SuppressLint import android.content.Intent @@ -7,15 +7,16 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.activity.result.ActivityResultLauncher -import dev.hotwire.core.R import dev.hotwire.core.bridge.BridgeDelegate import dev.hotwire.core.turbo.errors.VisitError -import dev.hotwire.core.turbo.nav.HotwireDestination -import dev.hotwire.core.turbo.session.SessionModalResult import dev.hotwire.core.turbo.util.TURBO_REQUEST_CODE_FILES -import dev.hotwire.core.turbo.views.TurboView import dev.hotwire.core.turbo.views.TurboWebChromeClient import dev.hotwire.core.turbo.views.TurboWebView +import dev.hotwire.navigation.R +import dev.hotwire.navigation.config.HotwireNavigation +import dev.hotwire.navigation.destinations.HotwireDestination +import dev.hotwire.navigation.session.SessionModalResult +import dev.hotwire.navigation.views.TurboView /** * The base class from which all web "standard" fragments (non-dialogs) in a @@ -28,7 +29,11 @@ open class HotwireWebFragment : HotwireFragment(), HotwireWebFragmentCallback { private lateinit var webDelegate: HotwireWebFragmentDelegate private val bridgeDelegate by lazy { - BridgeDelegate(location = location, destination = this) + BridgeDelegate( + location = location, + destination = this, + componentFactories = HotwireNavigation.registeredBridgeComponentFactories + ) } override fun onCreate(savedInstanceState: Bundle?) { diff --git a/core/src/main/kotlin/dev/hotwire/core/navigation/fragments/HotwireWebFragmentCallback.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/fragments/HotwireWebFragmentCallback.kt similarity index 96% rename from core/src/main/kotlin/dev/hotwire/core/navigation/fragments/HotwireWebFragmentCallback.kt rename to navigation-fragments/src/main/java/dev/hotwire/navigation/fragments/HotwireWebFragmentCallback.kt index 4e39bd6..a48720e 100644 --- a/core/src/main/kotlin/dev/hotwire/core/navigation/fragments/HotwireWebFragmentCallback.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/fragments/HotwireWebFragmentCallback.kt @@ -1,9 +1,9 @@ -package dev.hotwire.core.navigation.fragments +package dev.hotwire.navigation.fragments import android.view.View import android.webkit.HttpAuthHandler import dev.hotwire.core.turbo.errors.VisitError -import dev.hotwire.core.turbo.views.TurboView +import dev.hotwire.navigation.views.TurboView import dev.hotwire.core.turbo.views.TurboWebChromeClient import dev.hotwire.core.turbo.views.TurboWebView diff --git a/core/src/main/kotlin/dev/hotwire/core/navigation/fragments/HotwireWebFragmentDelegate.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/fragments/HotwireWebFragmentDelegate.kt similarity index 93% rename from core/src/main/kotlin/dev/hotwire/core/navigation/fragments/HotwireWebFragmentDelegate.kt rename to navigation-fragments/src/main/java/dev/hotwire/navigation/fragments/HotwireWebFragmentDelegate.kt index ebb0364..4f93ee5 100644 --- a/core/src/main/kotlin/dev/hotwire/core/navigation/fragments/HotwireWebFragmentDelegate.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/fragments/HotwireWebFragmentDelegate.kt @@ -1,4 +1,4 @@ -package dev.hotwire.core.navigation.fragments +package dev.hotwire.navigation.fragments import android.content.Intent import android.graphics.Bitmap @@ -9,17 +9,19 @@ import androidx.lifecycle.Lifecycle.State.STARTED import androidx.lifecycle.findViewTreeLifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.lifecycle.whenStateAtLeast +import dev.hotwire.core.config.Hotwire import dev.hotwire.core.turbo.config.pullToRefreshEnabled import dev.hotwire.core.turbo.errors.VisitError -import dev.hotwire.core.turbo.nav.HotwireNavDestination import dev.hotwire.core.turbo.session.SessionCallback -import dev.hotwire.core.turbo.session.SessionModalResult -import dev.hotwire.core.turbo.util.dispatcherProvider -import dev.hotwire.core.turbo.views.TurboView import dev.hotwire.core.turbo.views.TurboWebView import dev.hotwire.core.turbo.visit.Visit import dev.hotwire.core.turbo.visit.VisitAction +import dev.hotwire.core.turbo.visit.VisitDestination import dev.hotwire.core.turbo.visit.VisitOptions +import dev.hotwire.navigation.destinations.HotwireNavDestination +import dev.hotwire.navigation.session.SessionModalResult +import dev.hotwire.navigation.util.dispatcherProvider +import dev.hotwire.navigation.views.TurboView import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlin.random.Random @@ -32,7 +34,7 @@ internal class HotwireWebFragmentDelegate( private val delegate: HotwireFragmentDelegate, private val navDestination: HotwireNavDestination, private val callback: HotwireWebFragmentCallback -) : SessionCallback { +) : SessionCallback, VisitDestination { private val location = navDestination.location private val visitOptions = currentVisitOptions() @@ -150,6 +152,18 @@ internal class HotwireWebFragmentDelegate( turboView?.addErrorView(callback.createErrorView(error)) } + // ----------------------------------------------------------------------- + // VisitDestination interface + // ----------------------------------------------------------------------- + + override fun isActive(): Boolean { + return navDestination.isActive + } + + override fun activityResultLauncher(requestCode: Int): ActivityResultLauncher? { + return navDestination.activityResultLauncher(requestCode) + } + // ----------------------------------------------------------------------- // SessionCallback interface // ----------------------------------------------------------------------- @@ -220,8 +234,8 @@ internal class HotwireWebFragmentDelegate( navigator.route(location, options) } - override fun visitNavDestination(): HotwireNavDestination { - return navDestination + override fun visitDestination(): VisitDestination { + return this } override fun formSubmissionStarted(location: String) { @@ -361,7 +375,7 @@ internal class HotwireWebFragmentDelegate( private suspend fun fetchCachedSnapshot(): String? { return withContext(dispatcherProvider.io) { - val response = session.offlineRequestHandler?.getCachedSnapshot( + val response = Hotwire.config.offlineRequestHandler?.getCachedSnapshot( url = location ) diff --git a/navigation-fragments/src/main/java/dev/hotwire/navigation/logging/NavigationLog.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/logging/NavigationLog.kt new file mode 100644 index 0000000..13a4eb6 --- /dev/null +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/logging/NavigationLog.kt @@ -0,0 +1,45 @@ +package dev.hotwire.navigation.logging + +import android.util.Log +import dev.hotwire.core.config.Hotwire + +internal object NavigationLog { + private const val DEFAULT_TAG = "Hotwire-Navigation" + + private val debugEnabled get() = Hotwire.config.debugLoggingEnabled + + internal fun d(msg: String) = log(Log.DEBUG, msg) + + internal fun w(msg: String) = log(Log.WARN, msg) + + internal fun e(msg: String) = log(Log.ERROR, msg) + + private fun log(logLevel: Int, msg: String) { + when (logLevel) { + Log.DEBUG -> if (debugEnabled) Log.d(DEFAULT_TAG, msg) + Log.WARN -> Log.w(DEFAULT_TAG, msg) + Log.ERROR -> Log.e(DEFAULT_TAG, msg) + } + } +} + +private const val PAD_END_LENGTH = 35 + +internal fun logEvent(event: String, details: String = "") { + NavigationLog.d("$event ".padEnd(PAD_END_LENGTH, '.') + " [$details]") +} + +internal fun logEvent(event: String, attributes: List>) { + val description = attributes.joinToString(prefix = "[", postfix = "]", separator = ", ") { + "${it.first}: ${it.second}" + } + NavigationLog.d("$event ".padEnd(PAD_END_LENGTH, '.') + " $description") +} + +internal fun logWarning(event: String, details: String) { + NavigationLog.w("$event ".padEnd(PAD_END_LENGTH, '.') + " [$details]") +} + +internal fun logError(event: String, error: Exception) { + NavigationLog.e("$event: ${error.stackTraceToString()}") +} diff --git a/core/src/main/kotlin/dev/hotwire/core/navigation/navigator/Navigator.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/Navigator.kt similarity index 94% rename from core/src/main/kotlin/dev/hotwire/core/navigation/navigator/Navigator.kt rename to navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/Navigator.kt index ede1bb7..59c965a 100644 --- a/core/src/main/kotlin/dev/hotwire/core/navigation/navigator/Navigator.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/Navigator.kt @@ -1,4 +1,4 @@ -package dev.hotwire.core.navigation.navigator +package dev.hotwire.navigation.navigator import android.os.Bundle import androidx.annotation.IdRes @@ -9,14 +9,17 @@ import androidx.navigation.NavOptions import androidx.navigation.fragment.FragmentNavigator import dev.hotwire.core.bridge.Bridge import dev.hotwire.core.config.Hotwire -import dev.hotwire.core.config.Hotwire.pathConfiguration -import dev.hotwire.core.lib.logging.logEvent -import dev.hotwire.core.navigation.routing.Router -import dev.hotwire.core.turbo.nav.* +import dev.hotwire.core.turbo.nav.TurboNavPresentation +import dev.hotwire.core.turbo.nav.TurboNavPresentationContext import dev.hotwire.core.turbo.session.Session -import dev.hotwire.core.turbo.util.location import dev.hotwire.core.turbo.visit.VisitAction import dev.hotwire.core.turbo.visit.VisitOptions +import dev.hotwire.navigation.config.HotwireNavigation +import dev.hotwire.navigation.destinations.HotwireNavDestination +import dev.hotwire.navigation.destinations.HotwireNavDialogDestination +import dev.hotwire.navigation.logging.logEvent +import dev.hotwire.navigation.routing.Router +import dev.hotwire.navigation.util.location class Navigator( val host: NavigatorHost, @@ -56,7 +59,7 @@ class Navigator( webView = Hotwire.config.makeCustomWebView(host.requireContext()) ).also { // Initialize bridge with new WebView instance - if (Hotwire.registeredBridgeComponentFactories.isNotEmpty()) { + if (HotwireNavigation.registeredBridgeComponentFactories.isNotEmpty()) { Bridge.initialize(it.webView) } } @@ -105,7 +108,7 @@ class Navigator( bundle = bundle, navOptions = navOptions(location, options.action), extras = extras, - pathConfiguration = pathConfiguration, + pathConfiguration = Hotwire.config.pathConfiguration, controller = currentControllerForLocation(location) ) @@ -362,7 +365,7 @@ class Navigator( } private fun navOptions(location: String, action: VisitAction): NavOptions { - val properties = pathConfiguration.properties(location) + val properties = Hotwire.config.pathConfiguration.properties(location) return currentDestination.getNavigationOptions( newLocation = location, @@ -383,7 +386,7 @@ class Navigator( private fun logEvent(event: String, vararg params: Pair) { val attributes = params.toMutableList().apply { - add(0, "session" to session.sessionName) + add(0, "navigator" to configuration.name) add("currentFragment" to currentDestination.fragment.javaClass.simpleName) } logEvent(event, attributes) diff --git a/core/src/main/kotlin/dev/hotwire/core/navigation/navigator/NavigatorConfiguration.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/NavigatorConfiguration.kt similarity index 77% rename from core/src/main/kotlin/dev/hotwire/core/navigation/navigator/NavigatorConfiguration.kt rename to navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/NavigatorConfiguration.kt index 37d87a0..0b7be00 100644 --- a/core/src/main/kotlin/dev/hotwire/core/navigation/navigator/NavigatorConfiguration.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/NavigatorConfiguration.kt @@ -1,4 +1,4 @@ -package dev.hotwire.core.navigation.navigator +package dev.hotwire.navigation.navigator import androidx.annotation.IdRes diff --git a/core/src/main/kotlin/dev/hotwire/core/navigation/navigator/NavigatorException.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/NavigatorException.kt similarity index 58% rename from core/src/main/kotlin/dev/hotwire/core/navigation/navigator/NavigatorException.kt rename to navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/NavigatorException.kt index feb40f9..0218370 100644 --- a/core/src/main/kotlin/dev/hotwire/core/navigation/navigator/NavigatorException.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/NavigatorException.kt @@ -1,3 +1,3 @@ -package dev.hotwire.core.navigation.navigator +package dev.hotwire.navigation.navigator class NavigatorException(message: String) : Exception(message) diff --git a/core/src/main/kotlin/dev/hotwire/core/navigation/navigator/NavigatorGraphBuilder.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/NavigatorGraphBuilder.kt similarity index 90% rename from core/src/main/kotlin/dev/hotwire/core/navigation/navigator/NavigatorGraphBuilder.kt rename to navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/NavigatorGraphBuilder.kt index 64e25e0..e25d2a9 100644 --- a/core/src/main/kotlin/dev/hotwire/core/navigation/navigator/NavigatorGraphBuilder.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/NavigatorGraphBuilder.kt @@ -1,17 +1,22 @@ -package dev.hotwire.core.navigation.navigator +package dev.hotwire.navigation.navigator import android.net.Uri import androidx.core.net.toUri import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment -import androidx.navigation.* +import androidx.navigation.NavController +import androidx.navigation.NavGraph +import androidx.navigation.NavGraphBuilder +import androidx.navigation.createGraph import androidx.navigation.fragment.DialogFragmentNavigator import androidx.navigation.fragment.DialogFragmentNavigatorDestinationBuilder import androidx.navigation.fragment.FragmentNavigator import androidx.navigation.fragment.FragmentNavigatorDestinationBuilder +import androidx.navigation.get import dev.hotwire.core.turbo.config.PathConfiguration import dev.hotwire.core.turbo.config.uri -import dev.hotwire.core.turbo.nav.HotwireDestination +import dev.hotwire.navigation.config.HotwireNavigation +import dev.hotwire.navigation.destinations.HotwireDestination import java.util.UUID import kotlin.reflect.KClass import kotlin.reflect.full.isSubclassOf @@ -88,7 +93,9 @@ internal class NavigatorGraphBuilder( } private fun List.startDestination(): FragmentDestination { - val startDestinationUri = pathConfiguration.properties(startLocation).uri + val startDestinationUri = pathConfiguration.properties(startLocation).uri ?: + HotwireDestination.from(HotwireNavigation.defaultFragmentDestination).uri.toUri() + return requireNotNull(firstOrNull { it.uri == startDestinationUri }) { "A start Fragment destination was not found for uri: $startDestinationUri" } diff --git a/core/src/main/kotlin/dev/hotwire/core/navigation/navigator/NavigatorHost.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/NavigatorHost.kt similarity index 79% rename from core/src/main/kotlin/dev/hotwire/core/navigation/navigator/NavigatorHost.kt rename to navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/NavigatorHost.kt index ca69093..b5a2273 100644 --- a/core/src/main/kotlin/dev/hotwire/core/navigation/navigator/NavigatorHost.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/NavigatorHost.kt @@ -1,11 +1,11 @@ -package dev.hotwire.core.navigation.navigator +package dev.hotwire.navigation.navigator import android.os.Bundle import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.findNavController import dev.hotwire.core.config.Hotwire -import dev.hotwire.core.config.Hotwire.pathConfiguration -import dev.hotwire.core.navigation.activities.HotwireActivity +import dev.hotwire.navigation.activities.HotwireActivity +import dev.hotwire.navigation.config.HotwireNavigation open class NavigatorHost : NavHostFragment() { internal lateinit var activity: HotwireActivity @@ -31,10 +31,10 @@ open class NavigatorHost : NavHostFragment() { navController.apply { graph = NavigatorGraphBuilder( startLocation = configuration.startLocation, - pathConfiguration = pathConfiguration, + pathConfiguration = Hotwire.config.pathConfiguration, navController = findNavController() ).build( - registeredFragments = Hotwire.registeredFragmentDestinations + registeredFragments = HotwireNavigation.registeredFragmentDestinations ) } } diff --git a/core/src/main/kotlin/dev/hotwire/core/navigation/navigator/NavigatorMode.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/NavigatorMode.kt similarity index 67% rename from core/src/main/kotlin/dev/hotwire/core/navigation/navigator/NavigatorMode.kt rename to navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/NavigatorMode.kt index ce511ae..40259ec 100644 --- a/core/src/main/kotlin/dev/hotwire/core/navigation/navigator/NavigatorMode.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/NavigatorMode.kt @@ -1,4 +1,4 @@ -package dev.hotwire.core.navigation.navigator +package dev.hotwire.navigation.navigator internal enum class NavigatorMode { IN_CONTEXT, TO_MODAL, DISMISS_MODAL, REFRESH, NONE diff --git a/core/src/main/kotlin/dev/hotwire/core/navigation/navigator/NavigatorRule.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/NavigatorRule.kt similarity index 87% rename from core/src/main/kotlin/dev/hotwire/core/navigation/navigator/NavigatorRule.kt rename to navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/NavigatorRule.kt index 68c03e8..5c7c8d5 100644 --- a/core/src/main/kotlin/dev/hotwire/core/navigation/navigator/NavigatorRule.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/NavigatorRule.kt @@ -1,19 +1,29 @@ -package dev.hotwire.core.navigation.navigator +package dev.hotwire.navigation.navigator import android.net.Uri import android.os.Bundle +import androidx.core.net.toUri import androidx.core.os.bundleOf import androidx.navigation.NavController import androidx.navigation.NavDestination import androidx.navigation.NavOptions import androidx.navigation.fragment.FragmentNavigator import androidx.navigation.navOptions -import dev.hotwire.core.turbo.config.* -import dev.hotwire.core.turbo.nav.* -import dev.hotwire.core.turbo.session.SessionModalResult -import dev.hotwire.core.turbo.util.location +import dev.hotwire.core.turbo.config.PathConfiguration +import dev.hotwire.core.turbo.config.context +import dev.hotwire.core.turbo.config.fallbackUri +import dev.hotwire.core.turbo.config.presentation +import dev.hotwire.core.turbo.config.queryStringPresentation +import dev.hotwire.core.turbo.config.uri +import dev.hotwire.core.turbo.nav.TurboNavPresentation +import dev.hotwire.core.turbo.nav.TurboNavPresentationContext +import dev.hotwire.core.turbo.nav.TurboNavQueryStringPresentation import dev.hotwire.core.turbo.visit.VisitAction import dev.hotwire.core.turbo.visit.VisitOptions +import dev.hotwire.navigation.config.HotwireNavigation +import dev.hotwire.navigation.destinations.HotwireDestination +import dev.hotwire.navigation.session.SessionModalResult +import dev.hotwire.navigation.util.location @Suppress("MemberVisibilityCanBePrivate") internal class NavigatorRule( @@ -25,6 +35,8 @@ internal class NavigatorRule( pathConfiguration: PathConfiguration, val controller: NavController ) { + val defaultUri = HotwireDestination.from(HotwireNavigation.defaultFragmentDestination).uri.toUri() + // Current destination val previousLocation = controller.previousBackStackEntry.location val currentLocation = checkNotNull(controller.currentBackStackEntry.location) @@ -43,7 +55,7 @@ internal class NavigatorRule( val newPresentation = newPresentation() val newNavigationMode = newNavigationMode() val newModalResult = newModalResult() - val newDestinationUri = newProperties.uri + val newDestinationUri = newProperties.uri ?: defaultUri val newFallbackUri = newProperties.fallbackUri val newDestination = controller.destinationFor(newDestinationUri) val newFallbackDestination = controller.destinationFor(newFallbackUri) diff --git a/core/src/main/kotlin/dev/hotwire/core/navigation/navigator/NestedNavigatorHostDelegate.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/NestedNavigatorHostDelegate.kt similarity index 96% rename from core/src/main/kotlin/dev/hotwire/core/navigation/navigator/NestedNavigatorHostDelegate.kt rename to navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/NestedNavigatorHostDelegate.kt index ebf8f9f..7568af1 100644 --- a/core/src/main/kotlin/dev/hotwire/core/navigation/navigator/NestedNavigatorHostDelegate.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/NestedNavigatorHostDelegate.kt @@ -1,4 +1,4 @@ -package dev.hotwire.core.navigation.navigator +package dev.hotwire.navigation.navigator import androidx.annotation.IdRes import androidx.fragment.app.Fragment diff --git a/core/src/main/kotlin/dev/hotwire/core/turbo/observers/HotwireActivityObserver.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/observers/HotwireActivityObserver.kt similarity index 94% rename from core/src/main/kotlin/dev/hotwire/core/turbo/observers/HotwireActivityObserver.kt rename to navigation-fragments/src/main/java/dev/hotwire/navigation/observers/HotwireActivityObserver.kt index fb01ba7..0f62f4b 100644 --- a/core/src/main/kotlin/dev/hotwire/core/turbo/observers/HotwireActivityObserver.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/observers/HotwireActivityObserver.kt @@ -1,4 +1,4 @@ -package dev.hotwire.core.turbo.observers +package dev.hotwire.navigation.observers import android.webkit.CookieManager import androidx.lifecycle.DefaultLifecycleObserver diff --git a/core/src/main/kotlin/dev/hotwire/core/turbo/observers/HotwireWindowThemeObserver.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/observers/HotwireWindowThemeObserver.kt similarity index 96% rename from core/src/main/kotlin/dev/hotwire/core/turbo/observers/HotwireWindowThemeObserver.kt rename to navigation-fragments/src/main/java/dev/hotwire/navigation/observers/HotwireWindowThemeObserver.kt index 70e1ad6..4c6e91e 100644 --- a/core/src/main/kotlin/dev/hotwire/core/turbo/observers/HotwireWindowThemeObserver.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/observers/HotwireWindowThemeObserver.kt @@ -1,4 +1,4 @@ -package dev.hotwire.core.turbo.observers +package dev.hotwire.navigation.observers import android.content.res.Resources.Theme import android.os.Build @@ -9,8 +9,8 @@ import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS import androidx.annotation.RequiresApi import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner -import dev.hotwire.core.turbo.nav.HotwireNavDestination -import dev.hotwire.core.turbo.util.animateColorTo +import dev.hotwire.navigation.destinations.HotwireNavDestination +import dev.hotwire.navigation.util.animateColorTo internal class HotwireWindowThemeObserver(val destination: HotwireNavDestination) : DefaultLifecycleObserver { private val window: Window? diff --git a/core/src/main/kotlin/dev/hotwire/core/navigation/routing/AppNavigationRouteDecisionHandler.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/routing/AppNavigationRouteDecisionHandler.kt similarity index 76% rename from core/src/main/kotlin/dev/hotwire/core/navigation/routing/AppNavigationRouteDecisionHandler.kt rename to navigation-fragments/src/main/java/dev/hotwire/navigation/routing/AppNavigationRouteDecisionHandler.kt index c770f78..96aaaf7 100644 --- a/core/src/main/kotlin/dev/hotwire/core/navigation/routing/AppNavigationRouteDecisionHandler.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/routing/AppNavigationRouteDecisionHandler.kt @@ -1,8 +1,8 @@ -package dev.hotwire.core.navigation.routing +package dev.hotwire.navigation.routing import androidx.core.net.toUri -import dev.hotwire.core.navigation.activities.HotwireActivity -import dev.hotwire.core.navigation.navigator.NavigatorConfiguration +import dev.hotwire.navigation.activities.HotwireActivity +import dev.hotwire.navigation.navigator.NavigatorConfiguration class AppNavigationRouteDecisionHandler : Router.RouteDecisionHandler { override val name = "app-navigation" diff --git a/core/src/main/kotlin/dev/hotwire/core/navigation/routing/BrowserRouteDecisionHandler.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/routing/BrowserRouteDecisionHandler.kt similarity index 79% rename from core/src/main/kotlin/dev/hotwire/core/navigation/routing/BrowserRouteDecisionHandler.kt rename to navigation-fragments/src/main/java/dev/hotwire/navigation/routing/BrowserRouteDecisionHandler.kt index f59e04a..fa9c8ca 100644 --- a/core/src/main/kotlin/dev/hotwire/core/navigation/routing/BrowserRouteDecisionHandler.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/routing/BrowserRouteDecisionHandler.kt @@ -1,11 +1,11 @@ -package dev.hotwire.core.navigation.routing +package dev.hotwire.navigation.routing import android.content.ActivityNotFoundException import android.content.Intent import androidx.core.net.toUri -import dev.hotwire.core.lib.logging.logError -import dev.hotwire.core.navigation.activities.HotwireActivity -import dev.hotwire.core.navigation.navigator.NavigatorConfiguration +import dev.hotwire.navigation.activities.HotwireActivity +import dev.hotwire.navigation.logging.logError +import dev.hotwire.navigation.navigator.NavigatorConfiguration class BrowserRouteDecisionHandler : Router.RouteDecisionHandler { override val name = "browser" diff --git a/core/src/main/kotlin/dev/hotwire/core/navigation/routing/BrowserTabRouteDecisionHandler.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/routing/BrowserTabRouteDecisionHandler.kt similarity index 84% rename from core/src/main/kotlin/dev/hotwire/core/navigation/routing/BrowserTabRouteDecisionHandler.kt rename to navigation-fragments/src/main/java/dev/hotwire/navigation/routing/BrowserTabRouteDecisionHandler.kt index a1cdc62..00e8f79 100644 --- a/core/src/main/kotlin/dev/hotwire/core/navigation/routing/BrowserTabRouteDecisionHandler.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/routing/BrowserTabRouteDecisionHandler.kt @@ -1,12 +1,12 @@ -package dev.hotwire.core.navigation.routing +package dev.hotwire.navigation.routing 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.navigation.activities.HotwireActivity -import dev.hotwire.core.navigation.navigator.NavigatorConfiguration -import dev.hotwire.core.turbo.util.colorFromThemeAttr +import dev.hotwire.navigation.activities.HotwireActivity +import dev.hotwire.navigation.navigator.NavigatorConfiguration +import dev.hotwire.navigation.util.colorFromThemeAttr class BrowserTabRouteDecisionHandler : Router.RouteDecisionHandler { override val name = "browser-tab" diff --git a/core/src/main/kotlin/dev/hotwire/core/navigation/routing/Router.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/routing/Router.kt similarity index 88% rename from core/src/main/kotlin/dev/hotwire/core/navigation/routing/Router.kt rename to navigation-fragments/src/main/java/dev/hotwire/navigation/routing/Router.kt index 6e9c48d..ff983c9 100644 --- a/core/src/main/kotlin/dev/hotwire/core/navigation/routing/Router.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/routing/Router.kt @@ -1,9 +1,9 @@ -package dev.hotwire.core.navigation.routing +package dev.hotwire.navigation.routing -import dev.hotwire.core.lib.logging.logEvent -import dev.hotwire.core.navigation.activities.HotwireActivity -import dev.hotwire.core.navigation.navigator.NavigatorConfiguration -import dev.hotwire.core.navigation.routing.Router.RouteDecisionHandler +import dev.hotwire.navigation.activities.HotwireActivity +import dev.hotwire.navigation.logging.logEvent +import dev.hotwire.navigation.navigator.NavigatorConfiguration +import dev.hotwire.navigation.routing.Router.RouteDecisionHandler /** * Routes location urls within in-app navigation or with custom behaviors diff --git a/core/src/main/kotlin/dev/hotwire/core/turbo/session/SessionDialogResult.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/session/SessionDialogResult.kt similarity index 64% rename from core/src/main/kotlin/dev/hotwire/core/turbo/session/SessionDialogResult.kt rename to navigation-fragments/src/main/java/dev/hotwire/navigation/session/SessionDialogResult.kt index 362b513..63e0fe1 100644 --- a/core/src/main/kotlin/dev/hotwire/core/turbo/session/SessionDialogResult.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/session/SessionDialogResult.kt @@ -1,4 +1,4 @@ -package dev.hotwire.core.turbo.session +package dev.hotwire.navigation.session internal data class SessionDialogResult( val cancelled: Boolean diff --git a/core/src/main/kotlin/dev/hotwire/core/turbo/session/SessionEvent.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/session/SessionEvent.kt similarity index 94% rename from core/src/main/kotlin/dev/hotwire/core/turbo/session/SessionEvent.kt rename to navigation-fragments/src/main/java/dev/hotwire/navigation/session/SessionEvent.kt index 95e1695..7b4f34e 100644 --- a/core/src/main/kotlin/dev/hotwire/core/turbo/session/SessionEvent.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/session/SessionEvent.kt @@ -1,4 +1,4 @@ -package dev.hotwire.core.turbo.session +package dev.hotwire.navigation.session /** * Used as a wrapper for data that is exposed via a LiveData that represents an event. diff --git a/core/src/main/kotlin/dev/hotwire/core/turbo/session/SessionModalResult.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/session/SessionModalResult.kt similarity index 94% rename from core/src/main/kotlin/dev/hotwire/core/turbo/session/SessionModalResult.kt rename to navigation-fragments/src/main/java/dev/hotwire/navigation/session/SessionModalResult.kt index 3d1b05c..7eb093b 100644 --- a/core/src/main/kotlin/dev/hotwire/core/turbo/session/SessionModalResult.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/session/SessionModalResult.kt @@ -1,4 +1,4 @@ -package dev.hotwire.core.turbo.session +package dev.hotwire.navigation.session import android.os.Bundle import dev.hotwire.core.turbo.visit.VisitOptions diff --git a/core/src/main/kotlin/dev/hotwire/core/turbo/session/SessionViewModel.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/session/SessionViewModel.kt similarity index 98% rename from core/src/main/kotlin/dev/hotwire/core/turbo/session/SessionViewModel.kt rename to navigation-fragments/src/main/java/dev/hotwire/navigation/session/SessionViewModel.kt index 3555d36..61d0807 100644 --- a/core/src/main/kotlin/dev/hotwire/core/turbo/session/SessionViewModel.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/session/SessionViewModel.kt @@ -1,4 +1,4 @@ -package dev.hotwire.core.turbo.session +package dev.hotwire.navigation.session import androidx.fragment.app.FragmentActivity import androidx.lifecycle.MutableLiveData diff --git a/navigation-fragments/src/main/java/dev/hotwire/navigation/util/HotwireDispatcherProvider.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/util/HotwireDispatcherProvider.kt new file mode 100644 index 0000000..905beb5 --- /dev/null +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/util/HotwireDispatcherProvider.kt @@ -0,0 +1,14 @@ +package dev.hotwire.navigation.util + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers + +internal data class HotwireDispatcherProvider( + val main: CoroutineDispatcher, + var io: CoroutineDispatcher +) + +internal val dispatcherProvider = HotwireDispatcherProvider( + main = Dispatchers.Main, + io = Dispatchers.IO +) diff --git a/navigation-fragments/src/main/java/dev/hotwire/navigation/util/NavigationExtensions.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/util/NavigationExtensions.kt new file mode 100644 index 0000000..d81511e --- /dev/null +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/util/NavigationExtensions.kt @@ -0,0 +1,45 @@ +package dev.hotwire.navigation.util + +import android.animation.ArgbEvaluator +import android.animation.ValueAnimator +import android.content.Context +import android.util.TypedValue +import androidx.annotation.AttrRes +import androidx.appcompat.widget.Toolbar +import androidx.core.content.ContextCompat +import androidx.navigation.NavBackStackEntry +import dev.hotwire.navigation.R + +fun Toolbar.displayBackButton() { + navigationIcon = ContextCompat.getDrawable(context, R.drawable.ic_back) +} + +fun Toolbar.displayBackButtonAsCloseIcon() { + navigationIcon = ContextCompat.getDrawable(context, R.drawable.ic_close) +} + +internal val NavBackStackEntry?.location: String? + get() = this?.arguments?.getString("location") + +internal fun Context.colorFromThemeAttr( + @AttrRes attrColor: Int, + typedValue: TypedValue = TypedValue(), + resolveRefs: Boolean = true +): Int { + theme.resolveAttribute(attrColor, typedValue, resolveRefs) + val attr = obtainStyledAttributes(typedValue.data, intArrayOf(attrColor)) + val attrValue = attr.getColor(0, -1) + attr.recycle() + + return attrValue +} + +internal fun Int.animateColorTo(toColor: Int, duration: Long = 150, onUpdate: (Int) -> Unit) { + ValueAnimator.ofObject(ArgbEvaluator(), this, toColor).apply { + this.duration = duration + this.addUpdateListener { + val color = it.animatedValue as Int? + color?.let { onUpdate(color) } + } + }.start() +} diff --git a/core/src/main/kotlin/dev/hotwire/core/turbo/views/TurboSwipeRefreshLayout.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/views/TurboSwipeRefreshLayout.kt similarity index 92% rename from core/src/main/kotlin/dev/hotwire/core/turbo/views/TurboSwipeRefreshLayout.kt rename to navigation-fragments/src/main/java/dev/hotwire/navigation/views/TurboSwipeRefreshLayout.kt index 352faf1..174c3ef 100644 --- a/core/src/main/kotlin/dev/hotwire/core/turbo/views/TurboSwipeRefreshLayout.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/views/TurboSwipeRefreshLayout.kt @@ -1,9 +1,10 @@ -package dev.hotwire.core.turbo.views +package dev.hotwire.navigation.views import android.content.Context import android.util.AttributeSet import androidx.core.view.children import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import dev.hotwire.core.turbo.views.TurboWebView internal class TurboSwipeRefreshLayout @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : SwipeRefreshLayout(context, attrs) { diff --git a/core/src/main/kotlin/dev/hotwire/core/turbo/views/TurboView.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/views/TurboView.kt similarity index 98% rename from core/src/main/kotlin/dev/hotwire/core/turbo/views/TurboView.kt rename to navigation-fragments/src/main/java/dev/hotwire/navigation/views/TurboView.kt index 8145f07..6e7aaba 100644 --- a/core/src/main/kotlin/dev/hotwire/core/turbo/views/TurboView.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/views/TurboView.kt @@ -1,4 +1,4 @@ -package dev.hotwire.core.turbo.views +package dev.hotwire.navigation.views import android.content.Context import android.graphics.Bitmap @@ -11,7 +11,7 @@ import android.widget.FrameLayout import android.widget.ImageView import androidx.core.view.* import androidx.swiperefreshlayout.widget.SwipeRefreshLayout -import dev.hotwire.core.R +import dev.hotwire.navigation.R /** * Turbo view that hosts the shared WebView, a progress view, an error view, and allows diff --git a/core/src/main/res/anim/accelerate_interpolator.xml b/navigation-fragments/src/main/res/anim/accelerate_interpolator.xml similarity index 100% rename from core/src/main/res/anim/accelerate_interpolator.xml rename to navigation-fragments/src/main/res/anim/accelerate_interpolator.xml diff --git a/core/src/main/res/anim/decelerate_interpolator.xml b/navigation-fragments/src/main/res/anim/decelerate_interpolator.xml similarity index 100% rename from core/src/main/res/anim/decelerate_interpolator.xml rename to navigation-fragments/src/main/res/anim/decelerate_interpolator.xml diff --git a/core/src/main/res/anim/enter_slide_in_bottom.xml b/navigation-fragments/src/main/res/anim/enter_slide_in_bottom.xml similarity index 100% rename from core/src/main/res/anim/enter_slide_in_bottom.xml rename to navigation-fragments/src/main/res/anim/enter_slide_in_bottom.xml diff --git a/core/src/main/res/anim/enter_slide_in_left.xml b/navigation-fragments/src/main/res/anim/enter_slide_in_left.xml similarity index 100% rename from core/src/main/res/anim/enter_slide_in_left.xml rename to navigation-fragments/src/main/res/anim/enter_slide_in_left.xml diff --git a/core/src/main/res/anim/enter_slide_in_right.xml b/navigation-fragments/src/main/res/anim/enter_slide_in_right.xml similarity index 100% rename from core/src/main/res/anim/enter_slide_in_right.xml rename to navigation-fragments/src/main/res/anim/enter_slide_in_right.xml diff --git a/core/src/main/res/anim/exit_slide_out_bottom.xml b/navigation-fragments/src/main/res/anim/exit_slide_out_bottom.xml similarity index 100% rename from core/src/main/res/anim/exit_slide_out_bottom.xml rename to navigation-fragments/src/main/res/anim/exit_slide_out_bottom.xml diff --git a/core/src/main/res/anim/exit_slide_out_left.xml b/navigation-fragments/src/main/res/anim/exit_slide_out_left.xml similarity index 100% rename from core/src/main/res/anim/exit_slide_out_left.xml rename to navigation-fragments/src/main/res/anim/exit_slide_out_left.xml diff --git a/core/src/main/res/anim/exit_slide_out_right.xml b/navigation-fragments/src/main/res/anim/exit_slide_out_right.xml similarity index 100% rename from core/src/main/res/anim/exit_slide_out_right.xml rename to navigation-fragments/src/main/res/anim/exit_slide_out_right.xml diff --git a/core/src/main/res/drawable/ic_back.xml b/navigation-fragments/src/main/res/drawable/ic_back.xml similarity index 100% rename from core/src/main/res/drawable/ic_back.xml rename to navigation-fragments/src/main/res/drawable/ic_back.xml diff --git a/core/src/main/res/drawable/ic_close.xml b/navigation-fragments/src/main/res/drawable/ic_close.xml similarity index 100% rename from core/src/main/res/drawable/ic_close.xml rename to navigation-fragments/src/main/res/drawable/ic_close.xml diff --git a/core/src/main/res/layout/turbo_error.xml b/navigation-fragments/src/main/res/layout/turbo_error.xml similarity index 100% rename from core/src/main/res/layout/turbo_error.xml rename to navigation-fragments/src/main/res/layout/turbo_error.xml diff --git a/core/src/main/res/layout/turbo_fragment_web.xml b/navigation-fragments/src/main/res/layout/turbo_fragment_web.xml similarity index 100% rename from core/src/main/res/layout/turbo_fragment_web.xml rename to navigation-fragments/src/main/res/layout/turbo_fragment_web.xml diff --git a/core/src/main/res/layout/turbo_fragment_web_bottom_sheet.xml b/navigation-fragments/src/main/res/layout/turbo_fragment_web_bottom_sheet.xml similarity index 100% rename from core/src/main/res/layout/turbo_fragment_web_bottom_sheet.xml rename to navigation-fragments/src/main/res/layout/turbo_fragment_web_bottom_sheet.xml diff --git a/core/src/main/res/layout/turbo_progress.xml b/navigation-fragments/src/main/res/layout/turbo_progress.xml similarity index 100% rename from core/src/main/res/layout/turbo_progress.xml rename to navigation-fragments/src/main/res/layout/turbo_progress.xml diff --git a/core/src/main/res/layout/turbo_progress_bottom_sheet.xml b/navigation-fragments/src/main/res/layout/turbo_progress_bottom_sheet.xml similarity index 100% rename from core/src/main/res/layout/turbo_progress_bottom_sheet.xml rename to navigation-fragments/src/main/res/layout/turbo_progress_bottom_sheet.xml diff --git a/core/src/main/res/layout/turbo_view.xml b/navigation-fragments/src/main/res/layout/turbo_view.xml similarity index 87% rename from core/src/main/res/layout/turbo_view.xml rename to navigation-fragments/src/main/res/layout/turbo_view.xml index 61c7d3c..75b5cae 100644 --- a/core/src/main/res/layout/turbo_view.xml +++ b/navigation-fragments/src/main/res/layout/turbo_view.xml @@ -1,11 +1,12 @@ - - - + - + diff --git a/core/src/main/res/layout/turbo_view_bottom_sheet.xml b/navigation-fragments/src/main/res/layout/turbo_view_bottom_sheet.xml similarity index 95% rename from core/src/main/res/layout/turbo_view_bottom_sheet.xml rename to navigation-fragments/src/main/res/layout/turbo_view_bottom_sheet.xml index 404c9ff..a37a94b 100644 --- a/core/src/main/res/layout/turbo_view_bottom_sheet.xml +++ b/navigation-fragments/src/main/res/layout/turbo_view_bottom_sheet.xml @@ -1,5 +1,5 @@ - - + diff --git a/core/src/main/res/values/colors.xml b/navigation-fragments/src/main/res/values/colors.xml similarity index 100% rename from core/src/main/res/values/colors.xml rename to navigation-fragments/src/main/res/values/colors.xml diff --git a/core/src/test/kotlin/dev/hotwire/core/navigation/navigator/NavigatorRuleTest.kt b/navigation-fragments/src/test/kotlin/dev/hotwire/navigation/navigator/NavigatorRuleTest.kt similarity index 99% rename from core/src/test/kotlin/dev/hotwire/core/navigation/navigator/NavigatorRuleTest.kt rename to navigation-fragments/src/test/kotlin/dev/hotwire/navigation/navigator/NavigatorRuleTest.kt index 9521880..f04b7e1 100644 --- a/core/src/test/kotlin/dev/hotwire/core/navigation/navigator/NavigatorRuleTest.kt +++ b/navigation-fragments/src/test/kotlin/dev/hotwire/navigation/navigator/NavigatorRuleTest.kt @@ -1,4 +1,4 @@ -package dev.hotwire.core.navigation.navigator +package dev.hotwire.navigation.navigator import android.content.Context import android.net.Uri diff --git a/core/src/test/kotlin/dev/hotwire/core/navigation/routing/AppNavigationRouteDecisionHandlerTest.kt b/navigation-fragments/src/test/kotlin/dev/hotwire/navigation/routing/AppNavigationRouteDecisionHandlerTest.kt similarity index 90% rename from core/src/test/kotlin/dev/hotwire/core/navigation/routing/AppNavigationRouteDecisionHandlerTest.kt rename to navigation-fragments/src/test/kotlin/dev/hotwire/navigation/routing/AppNavigationRouteDecisionHandlerTest.kt index 1bdf276..f206802 100644 --- a/core/src/test/kotlin/dev/hotwire/core/navigation/routing/AppNavigationRouteDecisionHandlerTest.kt +++ b/navigation-fragments/src/test/kotlin/dev/hotwire/navigation/routing/AppNavigationRouteDecisionHandlerTest.kt @@ -1,6 +1,6 @@ -package dev.hotwire.core.navigation.routing +package dev.hotwire.navigation.routing -import dev.hotwire.core.navigation.navigator.NavigatorConfiguration +import dev.hotwire.navigation.navigator.NavigatorConfiguration import org.junit.Assert.* import org.junit.Test import org.junit.runner.RunWith diff --git a/core/src/test/kotlin/dev/hotwire/core/navigation/routing/BrowserRouteDecisionHandlerTest.kt b/navigation-fragments/src/test/kotlin/dev/hotwire/navigation/routing/BrowserRouteDecisionHandlerTest.kt similarity index 89% rename from core/src/test/kotlin/dev/hotwire/core/navigation/routing/BrowserRouteDecisionHandlerTest.kt rename to navigation-fragments/src/test/kotlin/dev/hotwire/navigation/routing/BrowserRouteDecisionHandlerTest.kt index 3cd450a..a94577f 100644 --- a/core/src/test/kotlin/dev/hotwire/core/navigation/routing/BrowserRouteDecisionHandlerTest.kt +++ b/navigation-fragments/src/test/kotlin/dev/hotwire/navigation/routing/BrowserRouteDecisionHandlerTest.kt @@ -1,6 +1,6 @@ -package dev.hotwire.core.navigation.routing +package dev.hotwire.navigation.routing -import dev.hotwire.core.navigation.navigator.NavigatorConfiguration +import dev.hotwire.navigation.navigator.NavigatorConfiguration import org.junit.Assert.* import org.junit.Test import org.junit.runner.RunWith diff --git a/core/src/test/kotlin/dev/hotwire/core/navigation/routing/BrowserTabRouteDecisionHandlerTest.kt b/navigation-fragments/src/test/kotlin/dev/hotwire/navigation/routing/BrowserTabRouteDecisionHandlerTest.kt similarity index 89% rename from core/src/test/kotlin/dev/hotwire/core/navigation/routing/BrowserTabRouteDecisionHandlerTest.kt rename to navigation-fragments/src/test/kotlin/dev/hotwire/navigation/routing/BrowserTabRouteDecisionHandlerTest.kt index 6a49227..8f4cce7 100644 --- a/core/src/test/kotlin/dev/hotwire/core/navigation/routing/BrowserTabRouteDecisionHandlerTest.kt +++ b/navigation-fragments/src/test/kotlin/dev/hotwire/navigation/routing/BrowserTabRouteDecisionHandlerTest.kt @@ -1,6 +1,6 @@ -package dev.hotwire.core.navigation.routing +package dev.hotwire.navigation.routing -import dev.hotwire.core.navigation.navigator.NavigatorConfiguration +import dev.hotwire.navigation.navigator.NavigatorConfiguration import org.junit.Assert.* import org.junit.Test import org.junit.runner.RunWith diff --git a/core/src/test/kotlin/dev/hotwire/core/turbo/views/TurboViewTest.kt b/navigation-fragments/src/test/kotlin/dev/hotwire/navigation/views/TurboViewTest.kt similarity index 95% rename from core/src/test/kotlin/dev/hotwire/core/turbo/views/TurboViewTest.kt rename to navigation-fragments/src/test/kotlin/dev/hotwire/navigation/views/TurboViewTest.kt index 19c0e6a..bf7405d 100644 --- a/core/src/test/kotlin/dev/hotwire/core/turbo/views/TurboViewTest.kt +++ b/navigation-fragments/src/test/kotlin/dev/hotwire/navigation/views/TurboViewTest.kt @@ -1,4 +1,4 @@ -package dev.hotwire.core.turbo.views +package dev.hotwire.navigation.views import android.content.Context import android.os.Build @@ -8,7 +8,7 @@ import android.webkit.WebView import android.widget.FrameLayout.LayoutParams import androidx.test.core.app.ApplicationProvider import com.nhaarman.mockito_kotlin.whenever -import dev.hotwire.core.R +import dev.hotwire.navigation.R import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Test diff --git a/settings.gradle.kts b/settings.gradle.kts index dc958d2..9eca507 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,3 +16,4 @@ dependencyResolutionManagement { rootProject.name = "hotwire-native-android" include(":core") include(":demo") +include(":navigation-fragments")