Skip to content

Commit

Permalink
Merge branch 'main' into location-access
Browse files Browse the repository at this point in the history
* main:
  Fix dialog fragment navigation issues. This fixes regressions when refactoring the Navigator to use a single instance across a navigation host. The topmost DialogFragment is not available from the NavigatorHost.childFragmentManager, since it is added directly to the Activity's window instead.
  Fix tests
  Fix lifecycle crash/timing issues when the Activity is recreated (such as during config changes)
  Add API doc
  Allow an app to check whether a navigator or its host are ready for navigation
  Provide the WebView system information in an easy to access way.
  • Loading branch information
jayohms committed Dec 2, 2024
2 parents dbfa30e + a103ddc commit c99972b
Show file tree
Hide file tree
Showing 18 changed files with 247 additions and 53 deletions.
11 changes: 10 additions & 1 deletion core/src/main/kotlin/dev/hotwire/core/config/Hotwire.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,24 @@ package dev.hotwire.core.config

import android.content.Context
import dev.hotwire.core.turbo.config.PathConfiguration
import dev.hotwire.core.turbo.webview.WebViewInfo

object Hotwire {
val config: HotwireConfig = HotwireConfig()

/**
* Provides useful version and type information about the system WebView component installed
* on the device. This can be used in your app to require a minimum system WebView version on
* the device and point users to the Play Store to update the corresponding app (Google Chrome
* or Android System WebView).
*/
fun webViewInfo(context: Context) = WebViewInfo(context.applicationContext)

/**
* Loads the [PathConfiguration] JSON file(s) from the provided location to
* configure navigation rules.
*/
fun loadPathConfiguration(context: Context, location: PathConfiguration.Location) {
config.pathConfiguration.load(context, location)
config.pathConfiguration.load(context.applicationContext, location)
}
}
67 changes: 67 additions & 0 deletions core/src/main/kotlin/dev/hotwire/core/turbo/webview/WebViewInfo.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package dev.hotwire.core.turbo.webview

import android.content.Context
import android.content.pm.PackageInfo
import android.net.Uri
import android.webkit.WebSettings
import androidx.webkit.WebViewCompat

private const val PACKAGE_SYSTEM_WEBVIEW = "com.google.android.webview"
private const val PACKAGE_CHROME_WEBVIEW = "com.android.chrome"
private const val PACKAGE_CHROME_PRE_RELEASE_WEBVIEW = "com.chrome"

class WebViewInfo internal constructor(context: Context) {
enum class WebViewType {
ANDROID_SYSTEM,
CHROME,
UNKNOWN
}

/**
* The system WebView's package info (corresponds to Chrome or Android System WebView).
*/
val packageInfo: PackageInfo? = WebViewCompat.getCurrentWebViewPackage(context)

/**
* The system WebView's major version (corresponds to Chrome or Android System WebView).
* Returns null if the major version cannot be determined.
*/
val majorVersion: Int? = packageInfo?.versionName?.substringBefore(".")?.toIntOrNull()

/**
* The default User-Agent provided by the system WebView before a call is made
* to WebView.settings.setUserAgentString(String).
*/
val defaultUserAgent: String = WebSettings.getDefaultUserAgent(context)

/**
* The system WebView's origin type. Different OS versions have the WebView component
* backed by either Google Chrome or the Android System WebView component. These are updatable
* through the Play Store.
*/
val webViewType = when {
packageInfo?.packageName?.contains(PACKAGE_CHROME_WEBVIEW) == true -> WebViewType.CHROME
packageInfo?.packageName?.contains(PACKAGE_CHROME_PRE_RELEASE_WEBVIEW) == true -> WebViewType.CHROME
packageInfo?.packageName?.contains(PACKAGE_SYSTEM_WEBVIEW) == true -> WebViewType.ANDROID_SYSTEM
else -> WebViewType.UNKNOWN
}

/**
* The system WebView's origin type as a human readable string.
*/
val webViewTypeName = when (webViewType) {
WebViewType.ANDROID_SYSTEM -> "Android System WebView"
WebViewType.CHROME -> "Google Chrome"
WebViewType.UNKNOWN -> "Unknown"
}

/**
* The Play Store app Uri for the system WebView type. This is useful to point users to the
* Play Store if their WebView version is outdated.
*/
val playStoreWebViewAppUri = when (webViewType) {
WebViewType.ANDROID_SYSTEM -> Uri.parse("market://details?id=${PACKAGE_SYSTEM_WEBVIEW}")
WebViewType.CHROME -> Uri.parse("market://details?id=${PACKAGE_CHROME_WEBVIEW}")
else -> null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package dev.hotwire.navigation.activities

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import dev.hotwire.navigation.navigator.Navigator
import dev.hotwire.navigation.navigator.NavigatorConfiguration

/**
Expand All @@ -11,8 +12,18 @@ abstract class HotwireActivity : AppCompatActivity() {
lateinit var delegate: HotwireActivityDelegate
private set

/**
* Provide a list of navigator configurations for the Activity. Configurations
* for all navigator instances available throughout the app should be provided here.
*/
abstract fun navigatorConfigurations(): List<NavigatorConfiguration>

/**
* Called when a navigator has been initialized and is ready for navigation. The
* root destination for the navigator has already been created.
*/
open fun onNavigatorReady(navigator: Navigator) {}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
delegate = HotwireActivityDelegate(this)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package dev.hotwire.navigation.activities

import androidx.activity.OnBackPressedCallback
import androidx.annotation.IdRes
import androidx.navigation.NavController
import dev.hotwire.navigation.logging.logEvent
import dev.hotwire.navigation.navigator.Navigator
import dev.hotwire.navigation.navigator.NavigatorConfiguration
import dev.hotwire.navigation.navigator.NavigatorHost
Expand All @@ -25,10 +25,6 @@ class HotwireActivityDelegate(val activity: HotwireActivity) {
}

private var currentNavigatorHostId = activity.navigatorConfigurations().first().navigatorHostId
set(value) {
field = value
updateOnBackPressedCallback(currentNavigatorHost.navController)
}

/**
* Initializes the Activity with a BackPressedDispatcher that properly
Expand All @@ -44,36 +40,59 @@ class HotwireActivityDelegate(val activity: HotwireActivity) {

/**
* Get the Activity's currently active [Navigator].
*
* Returns null if the navigator is not ready for navigation.
*/
val currentNavigator: Navigator?
get() {
return if (currentNavigatorHost.isAdded && !currentNavigatorHost.isDetached) {
currentNavigatorHost.navigator
val navigator = navigatorHosts[currentNavigatorHostId]

return if (navigator?.isReady() == true) {
navigator.navigator
} else {
null
}
}


/**
* Sets the currently active navigator in your Activity. If you use multiple
* [NavigatorHost] instances in your app (such as for bottom tabs),
* you must update this whenever the current navigator changes.
*/
fun setCurrentNavigator(configuration: NavigatorConfiguration) {
logEvent("navigatorSetAsCurrent", listOf("navigator" to configuration.name))
currentNavigatorHostId = configuration.navigatorHostId

val navigatorHost = navigatorHosts[currentNavigatorHostId]
if (navigatorHost != null) {
updateOnBackPressedCallback(navigatorHost)
}
}

internal fun registerNavigatorHost(host: NavigatorHost) {
logEvent("navigatorRegistered", listOf("navigator" to host.navigator.configuration.name))

if (navigatorHosts[host.id] == null) {
navigatorHosts[host.id] = host
listenToDestinationChanges(host.navController)
listenToDestinationChanges(host)

if (currentNavigatorHostId == host.id) {
updateOnBackPressedCallback(host)
}
}
}

internal fun unregisterNavigatorHost(host: NavigatorHost) {
logEvent("navigatorUnregistered", listOf("navigator" to host.navigator.configuration.name))
navigatorHosts.remove(host.id)
}

internal fun onNavigatorHostReady(host: NavigatorHost) {
logEvent("navigatorReady", listOf("navigator" to host.navigator.configuration.name))
activity.onNavigatorReady(host.navigator)
}

/**
* Finds the navigator host associated with the provided resource ID.
*
Expand All @@ -100,18 +119,15 @@ class HotwireActivityDelegate(val activity: HotwireActivity) {
navigatorHosts.forEach { it.value.navigator.reset() }
}

private fun listenToDestinationChanges(navController: NavController) {
navController.addOnDestinationChangedListener { controller, _, _ ->
updateOnBackPressedCallback(controller)
private fun listenToDestinationChanges(host: NavigatorHost) {
host.navController.addOnDestinationChangedListener { controller, _, _ ->
updateOnBackPressedCallback(host)
}
}

private fun updateOnBackPressedCallback(navController: NavController) {
if (navController == currentNavigatorHost.navController) {
onBackPressedCallback.isEnabled = navController.previousBackStackEntry != null
private fun updateOnBackPressedCallback(host: NavigatorHost) {
if (host.id == currentNavigatorHostId) {
onBackPressedCallback.isEnabled = host.navController.previousBackStackEntry != null
}
}

private val currentNavigatorHost: NavigatorHost
get() = navigatorHost(currentNavigatorHostId)
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ 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.navigator.location
import dev.hotwire.navigation.routing.Router

/**
Expand Down Expand Up @@ -193,7 +194,4 @@ interface HotwireDestination : BridgeDestination {
override fun bridgeWebViewIsReady(): Boolean {
return navigator.session.isReady
}

private val Bundle.location
get() = getString("location")
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,19 @@ import dev.hotwire.navigation.navigator.NavigatorHost
*/
abstract class HotwireBottomSheetFragment : BottomSheetDialogFragment(),
HotwireDestination, HotwireDialogDestination {
override lateinit var navigator: Navigator
internal lateinit var delegate: HotwireFragmentDelegate

override val navigator: Navigator
get() = (parentFragment as NavigatorHost).navigator

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
navigator = (parentFragment as NavigatorHost).navigator
delegate = HotwireFragmentDelegate(this)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
navigator.currentDialogDestination = this
delegate.onViewCreated()

if (shouldObserveTitleChanges()) {
Expand All @@ -42,6 +44,11 @@ abstract class HotwireBottomSheetFragment : BottomSheetDialogFragment(),
}
}

override fun onDestroyView() {
navigator.currentDialogDestination = null
super.onDestroyView()
}

/**
* This is marked `final` to prevent further use, as it's now deprecated in
* AndroidX's Fragment implementation.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@ import dev.hotwire.navigation.session.SessionModalResult
* For web fragments, refer to [HotwireWebFragment].
*/
abstract class HotwireFragment : Fragment(), HotwireDestination {
override lateinit var navigator: Navigator
internal lateinit var delegate: HotwireFragmentDelegate

override val navigator: Navigator
get() = (parentFragment as NavigatorHost).navigator

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
navigator = (parentFragment as NavigatorHost).navigator
delegate = HotwireFragmentDelegate(this)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package dev.hotwire.navigation.fragments

import dev.hotwire.navigation.logging.logEvent
import dev.hotwire.navigation.destinations.HotwireDestination
import dev.hotwire.navigation.navigator.navigatorName
import dev.hotwire.navigation.session.SessionModalResult
import dev.hotwire.navigation.session.SessionViewModel
import dev.hotwire.navigation.util.displayBackButton
Expand All @@ -15,10 +16,11 @@ import dev.hotwire.navigation.util.displayBackButtonAsCloseIcon
class HotwireFragmentDelegate(private val navDestination: HotwireDestination) {
private val fragment = navDestination.fragment
private val location = navDestination.location
private val navigator = navDestination.navigator
private val navigatorName = requireNotNull(fragment.arguments?.navigatorName)
private val navigator get() = navDestination.navigator

internal val sessionViewModel = SessionViewModel.get(
sessionName = navigator.configuration.name,
sessionName = navigatorName,
activity = fragment.requireActivity()
)
internal val fragmentViewModel = HotwireFragmentViewModel.get(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ open class HotwireWebBottomSheetFragment : HotwireBottomSheetFragment(), Hotwire

/**
* Gets the HotwireView instance in the Fragment's view
* with resource ID R.id.turbo_view.
* with resource ID R.id.hotwire_view.
*/
final override val hotwireView: HotwireView?
get() = view?.findViewById(R.id.hotwire_view)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ open class HotwireWebFragment : HotwireFragment(), HotwireWebFragmentCallback {

/**
* Gets the HotwireView instance in the Fragment's view
* with resource ID R.id.turbo_view.
* with resource ID R.id.hotwire_view.
*/
final override val hotwireView: HotwireView?
get() = view?.findViewById(R.id.hotwire_view)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ internal class HotwireWebFragmentDelegate(
private var screenshotOrientation = 0
private var screenshotZoomed = false
private var currentlyZoomed = false
private val navigator = navDestination.navigator
private val navigator get() = navDestination.navigator
private val session get() = navigator.session
private val turboView get() = callback.hotwireView
private val viewTreeLifecycleOwner get() = turboView?.findViewTreeLifecycleOwner()
Expand Down
Loading

0 comments on commit c99972b

Please sign in to comment.