diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 61fcd9d50..7ca4e26eb 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -162,11 +162,7 @@
-
-
-
-
-
+
diff --git a/app/src/main/java/com/geeksville/mesh/navigation/ChannelsRoutes.kt b/app/src/main/java/com/geeksville/mesh/navigation/ChannelsRoutes.kt
index cbec3dff0..da929f2ad 100644
--- a/app/src/main/java/com/geeksville/mesh/navigation/ChannelsRoutes.kt
+++ b/app/src/main/java/com/geeksville/mesh/navigation/ChannelsRoutes.kt
@@ -22,6 +22,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
+import androidx.navigation.navDeepLink
import androidx.navigation.navigation
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.ui.radioconfig.components.ChannelConfigScreen
@@ -31,48 +32,36 @@ import kotlinx.serialization.Serializable
@Serializable
sealed class ChannelsRoutes {
- @Serializable
- data object ChannelsGraph : Graph
+ @Serializable data object ChannelsGraph : Graph
- @Serializable
- data object Channels : Route
+ @Serializable data object Channels : Route
}
-/**
- * Navigation graph for for the top level ChannelScreen - [ChannelsRoutes.Channels].
- */
+/** Navigation graph for for the top level ChannelScreen - [ChannelsRoutes.Channels]. */
fun NavGraphBuilder.channelsGraph(navController: NavHostController, uiViewModel: UIViewModel) {
- navigation(
- startDestination = ChannelsRoutes.Channels,
- ) {
- composable { backStackEntry ->
- val parentEntry = remember(backStackEntry) {
- val parentRoute = backStackEntry.destination.parent!!.route!!
- navController.getBackStackEntry(parentRoute)
- }
+ navigation(startDestination = ChannelsRoutes.Channels) {
+ composable(
+ deepLinks = listOf(navDeepLink(basePath = "$DEEP_LINK_BASE_URI/channels")),
+ ) { backStackEntry ->
+ val parentEntry = remember(backStackEntry) { navController.getBackStackEntry(ChannelsRoutes.ChannelsGraph) }
ChannelScreen(
viewModel = uiViewModel,
radioConfigViewModel = hiltViewModel(parentEntry),
- onNavigate = { route -> navController.navigate(route) }
+ onNavigate = { route -> navController.navigate(route) },
)
}
configRoutes(navController)
}
}
-private fun NavGraphBuilder.configRoutes(
- navController: NavHostController,
-) {
+private fun NavGraphBuilder.configRoutes(navController: NavHostController) {
ConfigRoute.entries.forEach { configRoute ->
composable(configRoute.route::class) { backStackEntry ->
- val parentEntry = remember(backStackEntry) {
- val parentRoute = backStackEntry.destination.parent!!.route!!
- navController.getBackStackEntry(parentRoute)
- }
+ val parentEntry = remember(backStackEntry) { navController.getBackStackEntry(ChannelsRoutes.ChannelsGraph) }
when (configRoute) {
ConfigRoute.CHANNELS -> ChannelConfigScreen(hiltViewModel(parentEntry))
ConfigRoute.LORA -> LoRaConfigScreen(hiltViewModel(parentEntry))
- else -> Unit
+ else -> Unit // Should not happen if ConfigRoute enum is exhaustive for this context
}
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/navigation/ConnectionsRoutes.kt b/app/src/main/java/com/geeksville/mesh/navigation/ConnectionsRoutes.kt
index ba8a1d3e4..d9f929bde 100644
--- a/app/src/main/java/com/geeksville/mesh/navigation/ConnectionsRoutes.kt
+++ b/app/src/main/java/com/geeksville/mesh/navigation/ConnectionsRoutes.kt
@@ -32,57 +32,43 @@ import kotlinx.serialization.Serializable
@Serializable
sealed class ConnectionsRoutes {
- @Serializable
- data object ConnectionsGraph : Graph
+ @Serializable data object ConnectionsGraph : Graph
- @Serializable
- data object Connections : Route
+ @Serializable data object Connections : Route
}
-/**
- * Navigation graph for for the top level ConnectionsScreen - [ConnectionsRoutes.Connections].
- */
+/** Navigation graph for for the top level ConnectionsScreen - [ConnectionsRoutes.Connections]. */
fun NavGraphBuilder.connectionsGraph(
navController: NavHostController,
uiViewModel: UIViewModel,
- bluetoothViewModel: BluetoothViewModel
+ bluetoothViewModel: BluetoothViewModel,
) {
- navigation(
- startDestination = ConnectionsRoutes.Connections,
- ) {
+ @Suppress("ktlint:standard:max-line-length")
+ navigation(startDestination = ConnectionsRoutes.Connections) {
composable(
deepLinks = listOf(
- navDeepLink {
- uriPattern = "$DEEP_LINK_BASE_URI/connections"
- action = "android.intent.action.VIEW"
- }
- )
+ navDeepLink(basePath = "$DEEP_LINK_BASE_URI/connections"),
+ ),
) { backStackEntry ->
- val parentEntry = remember(backStackEntry) {
- val parentRoute = backStackEntry.destination.parent!!.route!!
- navController.getBackStackEntry(parentRoute)
- }
+ val parentEntry =
+ remember(backStackEntry) { navController.getBackStackEntry(ConnectionsRoutes.ConnectionsGraph) }
ConnectionsScreen(
uiViewModel = uiViewModel,
bluetoothViewModel = bluetoothViewModel,
radioConfigViewModel = hiltViewModel(parentEntry),
onNavigateToRadioConfig = { navController.navigate(RadioConfigRoutes.RadioConfig()) },
onNavigateToNodeDetails = { navController.navigate(NodesRoutes.NodeDetailGraph(it)) },
- onConfigNavigate = { route -> navController.navigate(route) }
+ onConfigNavigate = { route -> navController.navigate(route) },
)
}
configRoutes(navController)
}
}
-private fun NavGraphBuilder.configRoutes(
- navController: NavHostController,
-) {
+private fun NavGraphBuilder.configRoutes(navController: NavHostController) {
composable { backStackEntry ->
- val parentEntry = remember(backStackEntry) {
- val parentRoute = backStackEntry.destination.parent!!.route!!
- navController.getBackStackEntry(parentRoute)
- }
+ val parentEntry =
+ remember(backStackEntry) { navController.getBackStackEntry(ConnectionsRoutes.ConnectionsGraph) }
LoRaConfigScreen(hiltViewModel(parentEntry))
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/navigation/ContactsRoutes.kt b/app/src/main/java/com/geeksville/mesh/navigation/ContactsRoutes.kt
index 1e4d206ba..980d84c4d 100644
--- a/app/src/main/java/com/geeksville/mesh/navigation/ContactsRoutes.kt
+++ b/app/src/main/java/com/geeksville/mesh/navigation/ContactsRoutes.kt
@@ -42,9 +42,12 @@ sealed class ContactsRoutes {
@Serializable data object ContactsGraph : Graph
}
+@Suppress("LongMethod")
fun NavGraphBuilder.contactsGraph(navController: NavHostController, uiViewModel: UIViewModel) {
navigation(startDestination = ContactsRoutes.Contacts) {
- composable {
+ composable(
+ deepLinks = listOf(navDeepLink(basePath = "$DEEP_LINK_BASE_URI/contacts")),
+ ) {
ContactsScreen(
uiViewModel,
onNavigateToMessages = { navController.navigate(ContactsRoutes.Messages(it)) },
@@ -55,10 +58,10 @@ fun NavGraphBuilder.contactsGraph(navController: NavHostController, uiViewModel:
composable(
deepLinks =
listOf(
- navDeepLink {
- uriPattern = "$DEEP_LINK_BASE_URI/messages/{contactKey}?message={message}"
- action = "android.intent.action.VIEW"
- },
+ navDeepLink(
+ basePath =
+ "$DEEP_LINK_BASE_URI/messages", // {contactKey} and ?message={message} are auto-appended
+ ),
),
) { backStackEntry ->
val args = backStackEntry.toRoute()
@@ -76,10 +79,9 @@ fun NavGraphBuilder.contactsGraph(navController: NavHostController, uiViewModel:
composable(
deepLinks =
listOf(
- navDeepLink {
- uriPattern = "$DEEP_LINK_BASE_URI/share?message={message}"
- action = "android.intent.action.VIEW"
- },
+ navDeepLink(
+ basePath = "$DEEP_LINK_BASE_URI/share", // ?message={message} is auto-appended
+ ),
),
) { backStackEntry ->
val message = backStackEntry.toRoute().message
@@ -89,5 +91,9 @@ fun NavGraphBuilder.contactsGraph(navController: NavHostController, uiViewModel:
}
}
}
- composable { QuickChatScreen() }
+ composable(
+ deepLinks = listOf(navDeepLink(basePath = "$DEEP_LINK_BASE_URI/quick_chat")),
+ ) {
+ QuickChatScreen()
+ }
}
diff --git a/app/src/main/java/com/geeksville/mesh/navigation/MapRoutes.kt b/app/src/main/java/com/geeksville/mesh/navigation/MapRoutes.kt
index 48a2044aa..9c0947264 100644
--- a/app/src/main/java/com/geeksville/mesh/navigation/MapRoutes.kt
+++ b/app/src/main/java/com/geeksville/mesh/navigation/MapRoutes.kt
@@ -20,6 +20,7 @@ package com.geeksville.mesh.navigation
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
+import androidx.navigation.navDeepLink
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.ui.map.MapView
import com.geeksville.mesh.ui.map.MapViewModel
@@ -30,7 +31,7 @@ sealed class MapRoutes {
}
fun NavGraphBuilder.mapGraph(navController: NavHostController, uiViewModel: UIViewModel, mapViewModel: MapViewModel) {
- composable {
+ composable(deepLinks = listOf(navDeepLink(basePath = "$DEEP_LINK_BASE_URI/map"))) {
MapView(
uiViewModel = uiViewModel,
mapViewModel = mapViewModel,
diff --git a/app/src/main/java/com/geeksville/mesh/navigation/NavGraph.kt b/app/src/main/java/com/geeksville/mesh/navigation/NavGraph.kt
index 9d6364af5..74b83a74f 100644
--- a/app/src/main/java/com/geeksville/mesh/navigation/NavGraph.kt
+++ b/app/src/main/java/com/geeksville/mesh/navigation/NavGraph.kt
@@ -27,6 +27,7 @@ import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
+import androidx.navigation.navDeepLink
import com.geeksville.mesh.R
import com.geeksville.mesh.model.BluetoothViewModel
import com.geeksville.mesh.model.UIViewModel
@@ -79,7 +80,11 @@ fun NavGraph(
mapGraph(navController, uIViewModel, mapViewModel)
channelsGraph(navController, uIViewModel)
connectionsGraph(navController, uIViewModel, bluetoothViewModel)
- composable { DebugScreen() }
+ composable(
+ deepLinks = listOf(navDeepLink(basePath = "$DEEP_LINK_BASE_URI/debug_panel")),
+ ) {
+ DebugScreen()
+ }
radioConfigGraph(navController, uIViewModel)
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/navigation/NodesRoutes.kt b/app/src/main/java/com/geeksville/mesh/navigation/NodesRoutes.kt
index 61dc747d8..bba6fe66d 100644
--- a/app/src/main/java/com/geeksville/mesh/navigation/NodesRoutes.kt
+++ b/app/src/main/java/com/geeksville/mesh/navigation/NodesRoutes.kt
@@ -27,14 +27,18 @@ import androidx.compose.material.icons.filled.People
import androidx.compose.material.icons.filled.PermScanWifi
import androidx.compose.material.icons.filled.Power
import androidx.compose.material.icons.filled.Router
+import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import androidx.navigation.compose.navigation
+import androidx.navigation.navDeepLink
import com.geeksville.mesh.R
+import com.geeksville.mesh.model.MetricsViewModel
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.ui.metrics.DeviceMetricsScreen
import com.geeksville.mesh.ui.metrics.EnvironmentMetricsScreen
@@ -60,7 +64,6 @@ sealed class NodesRoutes {
}
sealed class NodeDetailRoutes {
-
@Serializable data object DeviceMetrics : Route
@Serializable data object NodeMap : Route
@@ -82,7 +85,9 @@ sealed class NodeDetailRoutes {
fun NavGraphBuilder.nodesGraph(navController: NavHostController, uiViewModel: UIViewModel) {
navigation(startDestination = NodesRoutes.Nodes) {
- composable {
+ composable(
+ deepLinks = listOf(navDeepLink(basePath = "$DEEP_LINK_BASE_URI/nodes")),
+ ) {
NodeScreen(
model = uiViewModel,
navigateToMessages = { navController.navigate(ContactsRoutes.Messages(it)) },
@@ -93,14 +98,19 @@ fun NavGraphBuilder.nodesGraph(navController: NavHostController, uiViewModel: UI
}
}
+@Suppress("LongMethod")
fun NavGraphBuilder.nodeDetailGraph(navController: NavHostController, uiViewModel: UIViewModel) {
navigation(startDestination = NodesRoutes.NodeDetail()) {
- composable { backStackEntry ->
+ composable(
+ deepLinks =
+ listOf(
+ navDeepLink( // Handles both /node and /node/{destNum} due to destNum: Int?
+ basePath = "$DEEP_LINK_BASE_URI/node",
+ ),
+ ),
+ ) { backStackEntry ->
val parentEntry =
- remember(backStackEntry) {
- val parentRoute = backStackEntry.destination.parent!!.route!!
- navController.getBackStackEntry(parentRoute)
- }
+ remember(backStackEntry) { navController.getBackStackEntry(NodesRoutes.NodeDetailGraph::class) }
NodeDetailScreen(
uiViewModel = uiViewModel,
navigateToMessages = { navController.navigate(ContactsRoutes.Messages(it)) },
@@ -109,39 +119,171 @@ fun NavGraphBuilder.nodeDetailGraph(navController: NavHostController, uiViewMode
viewModel = hiltViewModel(parentEntry),
)
}
- NodeDetailRoute.entries.forEach { nodeDetailRoute ->
- composable(nodeDetailRoute.route::class) { backStackEntry ->
- val parentEntry =
- remember(backStackEntry) {
- val parentRoute = backStackEntry.destination.parent!!.route!!
- navController.getBackStackEntry(parentRoute)
- }
- when (nodeDetailRoute) {
- NodeDetailRoute.DEVICE -> DeviceMetricsScreen(hiltViewModel(parentEntry))
- NodeDetailRoute.NODE_MAP -> NodeMapScreen(uiViewModel, hiltViewModel(parentEntry))
- NodeDetailRoute.POSITION_LOG -> PositionLogScreen(hiltViewModel(parentEntry))
- NodeDetailRoute.ENVIRONMENT -> EnvironmentMetricsScreen(hiltViewModel(parentEntry))
- NodeDetailRoute.SIGNAL -> SignalMetricsScreen(hiltViewModel(parentEntry))
- NodeDetailRoute.TRACEROUTE -> TracerouteLogScreen(viewModel = hiltViewModel(parentEntry))
-
- NodeDetailRoute.POWER -> PowerMetricsScreen(hiltViewModel(parentEntry))
- NodeDetailRoute.HOST -> HostMetricsLogScreen(hiltViewModel(parentEntry))
- NodeDetailRoute.PAX -> PaxMetricsScreen(hiltViewModel(parentEntry))
- }
+ NodeDetailRoute.entries.forEach { entry ->
+ when (entry.route) {
+ is NodeDetailRoutes.DeviceMetrics ->
+ addNodeDetailScreenComposable(
+ navController,
+ uiViewModel,
+ entry,
+ entry.screenComposable,
+ )
+ is NodeDetailRoutes.NodeMap ->
+ addNodeDetailScreenComposable(
+ navController,
+ uiViewModel,
+ entry,
+ entry.screenComposable,
+ )
+ is NodeDetailRoutes.PositionLog ->
+ addNodeDetailScreenComposable(
+ navController,
+ uiViewModel,
+ entry,
+ entry.screenComposable,
+ )
+ is NodeDetailRoutes.EnvironmentMetrics ->
+ addNodeDetailScreenComposable(
+ navController,
+ uiViewModel,
+ entry,
+ entry.screenComposable,
+ )
+ is NodeDetailRoutes.SignalMetrics ->
+ addNodeDetailScreenComposable(
+ navController,
+ uiViewModel,
+ entry,
+ entry.screenComposable,
+ )
+ is NodeDetailRoutes.PowerMetrics ->
+ addNodeDetailScreenComposable(
+ navController,
+ uiViewModel,
+ entry,
+ entry.screenComposable,
+ )
+ is NodeDetailRoutes.TracerouteLog ->
+ addNodeDetailScreenComposable(
+ navController,
+ uiViewModel,
+ entry,
+ entry.screenComposable,
+ )
+ is NodeDetailRoutes.HostMetricsLog ->
+ addNodeDetailScreenComposable(
+ navController,
+ uiViewModel,
+ entry,
+ entry.screenComposable,
+ )
+ is NodeDetailRoutes.PaxMetrics ->
+ addNodeDetailScreenComposable(
+ navController,
+ uiViewModel,
+ entry,
+ entry.screenComposable,
+ )
+ else -> Unit
}
}
}
}
-enum class NodeDetailRoute(@StringRes val title: Int, val route: Route, val icon: ImageVector?) {
- DEVICE(R.string.device, NodeDetailRoutes.DeviceMetrics, Icons.Default.Router),
- NODE_MAP(R.string.node_map, NodeDetailRoutes.NodeMap, Icons.Default.LocationOn),
- POSITION_LOG(R.string.position_log, NodeDetailRoutes.PositionLog, Icons.Default.LocationOn),
- ENVIRONMENT(R.string.environment, NodeDetailRoutes.EnvironmentMetrics, Icons.Default.LightMode),
- SIGNAL(R.string.signal, NodeDetailRoutes.SignalMetrics, Icons.Default.CellTower),
- TRACEROUTE(R.string.traceroute, NodeDetailRoutes.TracerouteLog, Icons.Default.PermScanWifi),
- POWER(R.string.power, NodeDetailRoutes.PowerMetrics, Icons.Default.Power),
- HOST(R.string.host, NodeDetailRoutes.HostMetricsLog, Icons.Default.Memory),
- PAX(R.string.pax, NodeDetailRoutes.PaxMetrics, Icons.Default.People),
+/**
+ * Helper to define a composable route for a screen within the node detail graph.
+ *
+ * This function simplifies adding screens by handling common tasks like:
+ * - Setting up deep links based on the [NodeDetailRoute] definition.
+ * - Retrieving the parent [NavBackStackEntry] for the [NodesRoutes.NodeDetailGraph].
+ * - Providing the [MetricsViewModel] scoped to the parent graph.
+ *
+ * @param R The type of the [Route] object, must be serializable.
+ * @param navController The [NavHostController] for navigation.
+ * @param uiViewModel The shared [UIViewModel], passed to the [screenContent].
+ * @param routeInfo The [NodeDetailRoute] enum entry that defines the path and metadata for this route.
+ * @param screenContent A lambda that defines the composable content for the screen. It receives the shared
+ * [MetricsViewModel] and the [UIViewModel].
+ */
+private inline fun NavGraphBuilder.addNodeDetailScreenComposable(
+ navController: NavHostController,
+ uiViewModel: UIViewModel,
+ routeInfo: NodeDetailRoute,
+ crossinline screenContent: @Composable (metricsViewModel: MetricsViewModel, passedUiViewModel: UIViewModel) -> Unit,
+) {
+ composable(
+ deepLinks =
+ listOf(
+ navDeepLink(basePath = "$DEEP_LINK_BASE_URI/node/{destNum}/${routeInfo.name.lowercase()}"),
+ navDeepLink(basePath = "$DEEP_LINK_BASE_URI/node/${routeInfo.name.lowercase()}"),
+ ),
+ ) { backStackEntry ->
+ val parentGraphBackStackEntry =
+ remember(backStackEntry) { navController.getBackStackEntry(NodesRoutes.NodeDetailGraph::class) }
+ val metricsViewModel = hiltViewModel(parentGraphBackStackEntry)
+ screenContent(metricsViewModel, uiViewModel)
+ }
+}
+
+enum class NodeDetailRoute(
+ @StringRes val title: Int,
+ val route: Route,
+ val icon: ImageVector?,
+ val screenComposable: @Composable (metricsViewModel: MetricsViewModel, uiViewModel: UIViewModel) -> Unit,
+) {
+ DEVICE(
+ R.string.device,
+ NodeDetailRoutes.DeviceMetrics,
+ Icons.Default.Router,
+ { metricsVM, _ -> DeviceMetricsScreen(metricsVM) },
+ ),
+ NODE_MAP(
+ R.string.node_map,
+ NodeDetailRoutes.NodeMap,
+ Icons.Default.LocationOn,
+ { metricsVM, uiVM -> NodeMapScreen(uiVM, metricsVM) },
+ ),
+ POSITION_LOG(
+ R.string.position_log,
+ NodeDetailRoutes.PositionLog,
+ Icons.Default.LocationOn,
+ { metricsVM, _ -> PositionLogScreen(metricsVM) },
+ ),
+ ENVIRONMENT(
+ R.string.environment,
+ NodeDetailRoutes.EnvironmentMetrics,
+ Icons.Default.LightMode,
+ { metricsVM, _ -> EnvironmentMetricsScreen(metricsVM) },
+ ),
+ SIGNAL(
+ R.string.signal,
+ NodeDetailRoutes.SignalMetrics,
+ Icons.Default.CellTower,
+ { metricsVM, _ -> SignalMetricsScreen(metricsVM) },
+ ),
+ TRACEROUTE(
+ R.string.traceroute,
+ NodeDetailRoutes.TracerouteLog,
+ Icons.Default.PermScanWifi,
+ { metricsVM, _ -> TracerouteLogScreen(viewModel = metricsVM) },
+ ),
+ POWER(
+ R.string.power,
+ NodeDetailRoutes.PowerMetrics,
+ Icons.Default.Power,
+ { metricsVM, _ -> PowerMetricsScreen(metricsVM) },
+ ),
+ HOST(
+ R.string.host,
+ NodeDetailRoutes.HostMetricsLog,
+ Icons.Default.Memory,
+ { metricsVM, _ -> HostMetricsLogScreen(metricsVM) },
+ ),
+ PAX(
+ R.string.pax,
+ NodeDetailRoutes.PaxMetrics,
+ Icons.Default.People,
+ { metricsVM, _ -> PaxMetricsScreen(metricsVM) },
+ ),
}
diff --git a/app/src/main/java/com/geeksville/mesh/navigation/RadioConfigRoutes.kt b/app/src/main/java/com/geeksville/mesh/navigation/RadioConfigRoutes.kt
index 5676376ac..fc0746d6d 100644
--- a/app/src/main/java/com/geeksville/mesh/navigation/RadioConfigRoutes.kt
+++ b/app/src/main/java/com/geeksville/mesh/navigation/RadioConfigRoutes.kt
@@ -42,18 +42,23 @@ import androidx.compose.material.icons.filled.SettingsRemote
import androidx.compose.material.icons.filled.Speed
import androidx.compose.material.icons.filled.Usb
import androidx.compose.material.icons.filled.Wifi
+import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
+import androidx.navigation.navDeepLink
import androidx.navigation.navigation
+import com.geeksville.mesh.AdminProtos
import com.geeksville.mesh.MeshProtos.DeviceMetadata
import com.geeksville.mesh.R
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.ui.radioconfig.CleanNodeDatabaseScreen
import com.geeksville.mesh.ui.radioconfig.RadioConfigScreen
+import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
import com.geeksville.mesh.ui.radioconfig.components.AmbientLightingConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.AudioConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.BluetoothConfigScreen
@@ -136,100 +141,301 @@ sealed class RadioConfigRoutes {
fun getNavRouteFrom(routeName: String): Route? =
ConfigRoute.entries.find { it.name == routeName }?.route ?: ModuleRoute.entries.find { it.name == routeName }?.route
+@Suppress("LongMethod")
fun NavGraphBuilder.radioConfigGraph(navController: NavHostController, uiViewModel: UIViewModel) {
navigation(startDestination = RadioConfigRoutes.RadioConfig()) {
- composable { backStackEntry ->
+ composable(
+ deepLinks =
+ listOf(navDeepLink(basePath = "$DEEP_LINK_BASE_URI/radio_config")),
+ ) { backStackEntry ->
val parentEntry =
- remember(backStackEntry) {
- val parentRoute = backStackEntry.destination.parent!!.route!!
- navController.getBackStackEntry(parentRoute)
- }
+ remember(backStackEntry) { navController.getBackStackEntry(RadioConfigRoutes.RadioConfigGraph::class) }
RadioConfigScreen(uiViewModel = uiViewModel, viewModel = hiltViewModel(parentEntry)) {
navController.navigate(it) { popUpTo(RadioConfigRoutes.RadioConfig()) { inclusive = false } }
}
}
- composable { CleanNodeDatabaseScreen() }
- configRoutes(navController)
- moduleRoutes(navController)
+ composable(
+ deepLinks =
+ listOf(
+ navDeepLink(
+ basePath = "$DEEP_LINK_BASE_URI/radio_config/clean_node_db",
+ ),
+ ),
+ ) {
+ CleanNodeDatabaseScreen()
+ }
+ configRoutesScreens(navController)
+ moduleRoutesScreens(navController)
}
}
-private fun NavGraphBuilder.configRoutes(navController: NavHostController) {
- ConfigRoute.entries.forEach { configRoute ->
- composable(configRoute.route::class) { backStackEntry ->
- val parentEntry =
- remember(backStackEntry) {
- val parentRoute = backStackEntry.destination.parent!!.route!!
- navController.getBackStackEntry(parentRoute)
- }
- when (configRoute) {
- ConfigRoute.USER -> UserConfigScreen(hiltViewModel(parentEntry))
- ConfigRoute.CHANNELS -> ChannelConfigScreen(hiltViewModel(parentEntry))
- ConfigRoute.DEVICE -> DeviceConfigScreen(hiltViewModel(parentEntry))
- ConfigRoute.POSITION -> PositionConfigScreen(hiltViewModel(parentEntry))
- ConfigRoute.POWER -> PowerConfigScreen(hiltViewModel(parentEntry))
- ConfigRoute.NETWORK -> NetworkConfigScreen(hiltViewModel(parentEntry))
- ConfigRoute.DISPLAY -> DisplayConfigScreen(hiltViewModel(parentEntry))
- ConfigRoute.LORA -> LoRaConfigScreen(hiltViewModel(parentEntry))
- ConfigRoute.BLUETOOTH -> BluetoothConfigScreen(hiltViewModel(parentEntry))
- ConfigRoute.SECURITY -> SecurityConfigScreen(hiltViewModel(parentEntry))
- }
+/**
+ * Helper to define a composable route for a radio configuration screen within the radio config graph.
+ *
+ * This function simplifies adding screens by handling common tasks like:
+ * - Setting up deep links based on the route's name.
+ * - Retrieving the parent [NavBackStackEntry] for the [RadioConfigRoutes.RadioConfigGraph].
+ * - Providing the [RadioConfigViewModel] scoped to the parent graph, which the [screenContent] will use.
+ *
+ * @param R The type of the [Route] object, must be serializable.
+ * @param navController The [NavHostController] for navigation.
+ * @param routeNameString The string name of the route (from the enum entry's name) used for deep link paths.
+ * @param screenContent A lambda that defines the composable content for the screen. It receives the parent-scoped
+ * [RadioConfigViewModel].
+ */
+private inline fun NavGraphBuilder.addRadioConfigScreenComposable(
+ navController: NavHostController,
+ routeNameString: String,
+ crossinline screenContent: @Composable (viewModel: RadioConfigViewModel) -> Unit,
+) {
+ composable(
+ deepLinks =
+ listOf(
+ navDeepLink(basePath = "$DEEP_LINK_BASE_URI/radio_config/{destNum}/${routeNameString.lowercase()}"),
+ navDeepLink(basePath = "$DEEP_LINK_BASE_URI/radio_config/${routeNameString.lowercase()}"),
+ ),
+ ) { backStackEntry ->
+ val parentEntry =
+ remember(backStackEntry) { navController.getBackStackEntry(RadioConfigRoutes.RadioConfigGraph::class) }
+ val viewModel = hiltViewModel(parentEntry)
+ screenContent(viewModel)
+ }
+}
+
+@Suppress("LongMethod")
+private fun NavGraphBuilder.configRoutesScreens(navController: NavHostController) {
+ ConfigRoute.entries.forEach { entry ->
+ when (entry.route) {
+ is RadioConfigRoutes.User ->
+ addRadioConfigScreenComposable(
+ navController,
+ entry.name,
+ entry.screenComposable,
+ )
+ is RadioConfigRoutes.ChannelConfig ->
+ addRadioConfigScreenComposable(
+ navController,
+ entry.name,
+ entry.screenComposable,
+ )
+ is RadioConfigRoutes.Device ->
+ addRadioConfigScreenComposable(
+ navController,
+ entry.name,
+ entry.screenComposable,
+ )
+ is RadioConfigRoutes.Position ->
+ addRadioConfigScreenComposable(
+ navController,
+ entry.name,
+ entry.screenComposable,
+ )
+ is RadioConfigRoutes.Power ->
+ addRadioConfigScreenComposable(
+ navController,
+ entry.name,
+ entry.screenComposable,
+ )
+ is RadioConfigRoutes.Network ->
+ addRadioConfigScreenComposable(
+ navController,
+ entry.name,
+ entry.screenComposable,
+ )
+ is RadioConfigRoutes.Display ->
+ addRadioConfigScreenComposable(
+ navController,
+ entry.name,
+ entry.screenComposable,
+ )
+ is RadioConfigRoutes.LoRa ->
+ addRadioConfigScreenComposable(
+ navController,
+ entry.name,
+ entry.screenComposable,
+ )
+ is RadioConfigRoutes.Bluetooth ->
+ addRadioConfigScreenComposable(
+ navController,
+ entry.name,
+ entry.screenComposable,
+ )
+ is RadioConfigRoutes.Security ->
+ addRadioConfigScreenComposable(
+ navController,
+ entry.name,
+ entry.screenComposable,
+ )
+ else -> Unit // Should not happen if ConfigRoute enum is exhaustive for this context
}
}
}
-@Suppress("CyclomaticComplexMethod")
-private fun NavGraphBuilder.moduleRoutes(navController: NavHostController) {
- ModuleRoute.entries.forEach { moduleRoute ->
- composable(moduleRoute.route::class) { backStackEntry ->
- val parentEntry =
- remember(backStackEntry) {
- val parentRoute = backStackEntry.destination.parent!!.route!!
- navController.getBackStackEntry(parentRoute)
- }
- when (moduleRoute) {
- ModuleRoute.MQTT -> MQTTConfigScreen(hiltViewModel(parentEntry))
- ModuleRoute.SERIAL -> SerialConfigScreen(hiltViewModel(parentEntry))
- ModuleRoute.EXT_NOTIFICATION -> ExternalNotificationConfigScreen(hiltViewModel(parentEntry))
-
- ModuleRoute.STORE_FORWARD -> StoreForwardConfigScreen(hiltViewModel(parentEntry))
- ModuleRoute.RANGE_TEST -> RangeTestConfigScreen(hiltViewModel(parentEntry))
- ModuleRoute.TELEMETRY -> TelemetryConfigScreen(hiltViewModel(parentEntry))
- ModuleRoute.CANNED_MESSAGE -> CannedMessageConfigScreen(hiltViewModel(parentEntry))
-
- ModuleRoute.AUDIO -> AudioConfigScreen(hiltViewModel(parentEntry))
- ModuleRoute.REMOTE_HARDWARE -> RemoteHardwareConfigScreen(hiltViewModel(parentEntry))
-
- ModuleRoute.NEIGHBOR_INFO -> NeighborInfoConfigScreen(hiltViewModel(parentEntry))
- ModuleRoute.AMBIENT_LIGHTING -> AmbientLightingConfigScreen(hiltViewModel(parentEntry))
-
- ModuleRoute.DETECTION_SENSOR -> DetectionSensorConfigScreen(hiltViewModel(parentEntry))
-
- ModuleRoute.PAXCOUNTER -> PaxcounterConfigScreen(hiltViewModel(parentEntry))
- }
+@Suppress("LongMethod", "CyclomaticComplexMethod")
+private fun NavGraphBuilder.moduleRoutesScreens(navController: NavHostController) {
+ ModuleRoute.entries.forEach { entry ->
+ when (entry.route) {
+ is RadioConfigRoutes.MQTT ->
+ addRadioConfigScreenComposable(
+ navController,
+ entry.name,
+ entry.screenComposable,
+ )
+ is RadioConfigRoutes.Serial ->
+ addRadioConfigScreenComposable(
+ navController,
+ entry.name,
+ entry.screenComposable,
+ )
+ is RadioConfigRoutes.ExtNotification ->
+ addRadioConfigScreenComposable(
+ navController,
+ entry.name,
+ entry.screenComposable,
+ )
+ is RadioConfigRoutes.StoreForward ->
+ addRadioConfigScreenComposable(
+ navController,
+ entry.name,
+ entry.screenComposable,
+ )
+ is RadioConfigRoutes.RangeTest ->
+ addRadioConfigScreenComposable(
+ navController,
+ entry.name,
+ entry.screenComposable,
+ )
+ is RadioConfigRoutes.Telemetry ->
+ addRadioConfigScreenComposable(
+ navController,
+ entry.name,
+ entry.screenComposable,
+ )
+ is RadioConfigRoutes.CannedMessage ->
+ addRadioConfigScreenComposable(
+ navController,
+ entry.name,
+ entry.screenComposable,
+ )
+ is RadioConfigRoutes.Audio ->
+ addRadioConfigScreenComposable(
+ navController,
+ entry.name,
+ entry.screenComposable,
+ )
+ is RadioConfigRoutes.RemoteHardware ->
+ addRadioConfigScreenComposable(
+ navController,
+ entry.name,
+ entry.screenComposable,
+ )
+ is RadioConfigRoutes.NeighborInfo ->
+ addRadioConfigScreenComposable(
+ navController,
+ entry.name,
+ entry.screenComposable,
+ )
+ is RadioConfigRoutes.AmbientLighting ->
+ addRadioConfigScreenComposable(
+ navController,
+ entry.name,
+ entry.screenComposable,
+ )
+ is RadioConfigRoutes.DetectionSensor ->
+ addRadioConfigScreenComposable(
+ navController,
+ entry.name,
+ entry.screenComposable,
+ )
+ is RadioConfigRoutes.Paxcounter ->
+ addRadioConfigScreenComposable(
+ navController,
+ entry.name,
+ entry.screenComposable,
+ )
+ else -> Unit // Should not happen if ModuleRoute enum is exhaustive for this context
}
}
}
-// Config (type = AdminProtos.AdminMessage.ConfigType)
@Suppress("MagicNumber")
-enum class ConfigRoute(@StringRes val title: Int, val route: Route, val icon: ImageVector?, val type: Int = 0) {
- USER(R.string.user, RadioConfigRoutes.User, Icons.Default.Person, 0),
- CHANNELS(R.string.channels, RadioConfigRoutes.ChannelConfig, Icons.AutoMirrored.Default.List, 0),
- DEVICE(R.string.device, RadioConfigRoutes.Device, Icons.Default.Router, 0),
- POSITION(R.string.position, RadioConfigRoutes.Position, Icons.Default.LocationOn, 1),
- POWER(R.string.power, RadioConfigRoutes.Power, Icons.Default.Power, 2),
- NETWORK(R.string.network, RadioConfigRoutes.Network, Icons.Default.Wifi, 3),
- DISPLAY(R.string.display, RadioConfigRoutes.Display, Icons.Default.DisplaySettings, 4),
- LORA(R.string.lora, RadioConfigRoutes.LoRa, Icons.Default.CellTower, 5),
- BLUETOOTH(R.string.bluetooth, RadioConfigRoutes.Bluetooth, Icons.Default.Bluetooth, 6),
- SECURITY(R.string.security, RadioConfigRoutes.Security, Icons.Default.Security, 7),
+enum class ConfigRoute(
+ @StringRes val title: Int,
+ val route: Route,
+ val icon: ImageVector?,
+ val type: Int = 0,
+ val screenComposable: @Composable (viewModel: RadioConfigViewModel) -> Unit,
+) {
+ USER(R.string.user, RadioConfigRoutes.User, Icons.Default.Person, 0, { vm -> UserConfigScreen(vm) }),
+ CHANNELS(
+ R.string.channels,
+ RadioConfigRoutes.ChannelConfig,
+ Icons.AutoMirrored.Default.List,
+ 0,
+ { vm -> ChannelConfigScreen(vm) },
+ ),
+ DEVICE(
+ R.string.device,
+ RadioConfigRoutes.Device,
+ Icons.Default.Router,
+ AdminProtos.AdminMessage.ConfigType.DEVICE_CONFIG_VALUE,
+ { vm -> DeviceConfigScreen(vm) },
+ ),
+ POSITION(
+ R.string.position,
+ RadioConfigRoutes.Position,
+ Icons.Default.LocationOn,
+ AdminProtos.AdminMessage.ConfigType.POSITION_CONFIG_VALUE,
+ { vm -> PositionConfigScreen(vm) },
+ ),
+ POWER(
+ R.string.power,
+ RadioConfigRoutes.Power,
+ Icons.Default.Power,
+ AdminProtos.AdminMessage.ConfigType.POWER_CONFIG_VALUE,
+ { vm -> PowerConfigScreen(vm) },
+ ),
+ NETWORK(
+ R.string.network,
+ RadioConfigRoutes.Network,
+ Icons.Default.Wifi,
+ AdminProtos.AdminMessage.ConfigType.NETWORK_CONFIG_VALUE,
+ { vm -> NetworkConfigScreen(vm) },
+ ),
+ DISPLAY(
+ R.string.display,
+ RadioConfigRoutes.Display,
+ Icons.Default.DisplaySettings,
+ AdminProtos.AdminMessage.ConfigType.DISPLAY_CONFIG_VALUE,
+ { vm -> DisplayConfigScreen(vm) },
+ ),
+ LORA(
+ R.string.lora,
+ RadioConfigRoutes.LoRa,
+ Icons.Default.CellTower,
+ AdminProtos.AdminMessage.ConfigType.LORA_CONFIG_VALUE,
+ { vm -> LoRaConfigScreen(vm) },
+ ),
+ BLUETOOTH(
+ R.string.bluetooth,
+ RadioConfigRoutes.Bluetooth,
+ Icons.Default.Bluetooth,
+ AdminProtos.AdminMessage.ConfigType.BLUETOOTH_CONFIG_VALUE,
+ { vm -> BluetoothConfigScreen(vm) },
+ ),
+ SECURITY(
+ R.string.security,
+ RadioConfigRoutes.Security,
+ Icons.Default.Security,
+ AdminProtos.AdminMessage.ConfigType.SECURITY_CONFIG_VALUE,
+ { vm -> SecurityConfigScreen(vm) },
+ ),
;
companion object {
fun filterExcludedFrom(metadata: DeviceMetadata?): List = entries.filter {
when {
- metadata == null -> true
+ metadata == null -> true // Include all routes if metadata is null
it == BLUETOOTH -> metadata.hasBluetooth
it == NETWORK -> metadata.hasWifi || metadata.hasEthernet
else -> true // Include all other routes by default
@@ -238,22 +444,105 @@ enum class ConfigRoute(@StringRes val title: Int, val route: Route, val icon: Im
}
}
-// ModuleConfig (type = AdminProtos.AdminMessage.ModuleConfigType)
@Suppress("MagicNumber")
-enum class ModuleRoute(@StringRes val title: Int, val route: Route, val icon: ImageVector?, val type: Int = 0) {
- MQTT(R.string.mqtt, RadioConfigRoutes.MQTT, Icons.Default.Cloud, 0),
- SERIAL(R.string.serial, RadioConfigRoutes.Serial, Icons.Default.Usb, 1),
- EXT_NOTIFICATION(R.string.external_notification, RadioConfigRoutes.ExtNotification, Icons.Default.Notifications, 2),
- STORE_FORWARD(R.string.store_forward, RadioConfigRoutes.StoreForward, Icons.AutoMirrored.Default.Forward, 3),
- RANGE_TEST(R.string.range_test, RadioConfigRoutes.RangeTest, Icons.Default.Speed, 4),
- TELEMETRY(R.string.telemetry, RadioConfigRoutes.Telemetry, Icons.Default.DataUsage, 5),
- CANNED_MESSAGE(R.string.canned_message, RadioConfigRoutes.CannedMessage, Icons.AutoMirrored.Default.Message, 6),
- AUDIO(R.string.audio, RadioConfigRoutes.Audio, Icons.AutoMirrored.Default.VolumeUp, 7),
- REMOTE_HARDWARE(R.string.remote_hardware, RadioConfigRoutes.RemoteHardware, Icons.Default.SettingsRemote, 8),
- NEIGHBOR_INFO(R.string.neighbor_info, RadioConfigRoutes.NeighborInfo, Icons.Default.People, 9),
- AMBIENT_LIGHTING(R.string.ambient_lighting, RadioConfigRoutes.AmbientLighting, Icons.Default.LightMode, 10),
- DETECTION_SENSOR(R.string.detection_sensor, RadioConfigRoutes.DetectionSensor, Icons.Default.Sensors, 11),
- PAXCOUNTER(R.string.paxcounter, RadioConfigRoutes.Paxcounter, Icons.Default.PermScanWifi, 12),
+enum class ModuleRoute(
+ @StringRes val title: Int,
+ val route: Route,
+ val icon: ImageVector?,
+ val type: Int = 0,
+ val screenComposable: @Composable (viewModel: RadioConfigViewModel) -> Unit,
+) {
+ MQTT(
+ R.string.mqtt,
+ RadioConfigRoutes.MQTT,
+ Icons.Default.Cloud,
+ AdminProtos.AdminMessage.ModuleConfigType.MQTT_CONFIG_VALUE,
+ { vm -> MQTTConfigScreen(vm) },
+ ),
+ SERIAL(
+ R.string.serial,
+ RadioConfigRoutes.Serial,
+ Icons.Default.Usb,
+ AdminProtos.AdminMessage.ModuleConfigType.SERIAL_CONFIG_VALUE,
+ { vm -> SerialConfigScreen(vm) },
+ ),
+ EXT_NOTIFICATION(
+ R.string.external_notification,
+ RadioConfigRoutes.ExtNotification,
+ Icons.Default.Notifications,
+ AdminProtos.AdminMessage.ModuleConfigType.EXTNOTIF_CONFIG_VALUE,
+ { vm -> ExternalNotificationConfigScreen(vm) },
+ ),
+ STORE_FORWARD(
+ R.string.store_forward,
+ RadioConfigRoutes.StoreForward,
+ Icons.AutoMirrored.Default.Forward,
+ AdminProtos.AdminMessage.ModuleConfigType.STOREFORWARD_CONFIG_VALUE,
+ { vm -> StoreForwardConfigScreen(vm) },
+ ),
+ RANGE_TEST(
+ R.string.range_test,
+ RadioConfigRoutes.RangeTest,
+ Icons.Default.Speed,
+ AdminProtos.AdminMessage.ModuleConfigType.RANGETEST_CONFIG_VALUE,
+ { vm -> RangeTestConfigScreen(vm) },
+ ),
+ TELEMETRY(
+ R.string.telemetry,
+ RadioConfigRoutes.Telemetry,
+ Icons.Default.DataUsage,
+ AdminProtos.AdminMessage.ModuleConfigType.TELEMETRY_CONFIG_VALUE,
+ { vm -> TelemetryConfigScreen(vm) },
+ ),
+ CANNED_MESSAGE(
+ R.string.canned_message,
+ RadioConfigRoutes.CannedMessage,
+ Icons.AutoMirrored.Default.Message,
+ AdminProtos.AdminMessage.ModuleConfigType.CANNEDMSG_CONFIG_VALUE,
+ { vm -> CannedMessageConfigScreen(vm) },
+ ),
+ AUDIO(
+ R.string.audio,
+ RadioConfigRoutes.Audio,
+ Icons.AutoMirrored.Default.VolumeUp,
+ AdminProtos.AdminMessage.ModuleConfigType.AUDIO_CONFIG_VALUE,
+ { vm -> AudioConfigScreen(vm) },
+ ),
+ REMOTE_HARDWARE(
+ R.string.remote_hardware,
+ RadioConfigRoutes.RemoteHardware,
+ Icons.Default.SettingsRemote,
+ AdminProtos.AdminMessage.ModuleConfigType.REMOTEHARDWARE_CONFIG_VALUE,
+ { vm -> RemoteHardwareConfigScreen(vm) },
+ ),
+ NEIGHBOR_INFO(
+ R.string.neighbor_info,
+ RadioConfigRoutes.NeighborInfo,
+ Icons.Default.People,
+ AdminProtos.AdminMessage.ModuleConfigType.NEIGHBORINFO_CONFIG_VALUE,
+ { vm -> NeighborInfoConfigScreen(vm) },
+ ),
+ AMBIENT_LIGHTING(
+ R.string.ambient_lighting,
+ RadioConfigRoutes.AmbientLighting,
+ Icons.Default.LightMode,
+ AdminProtos.AdminMessage.ModuleConfigType.AMBIENTLIGHTING_CONFIG_VALUE,
+ { vm -> AmbientLightingConfigScreen(vm) },
+ ),
+ DETECTION_SENSOR(
+ R.string.detection_sensor,
+ RadioConfigRoutes.DetectionSensor,
+ Icons.Default.Sensors,
+ AdminProtos.AdminMessage.ModuleConfigType.DETECTIONSENSOR_CONFIG_VALUE,
+ { vm -> DetectionSensorConfigScreen(vm) },
+ ),
+ PAXCOUNTER(
+ R.string.paxcounter,
+ RadioConfigRoutes.Paxcounter,
+ Icons.Default.PermScanWifi,
+ AdminProtos.AdminMessage.ModuleConfigType.PAXCOUNTER_CONFIG_VALUE,
+ { vm -> PaxcounterConfigScreen(vm) },
+ ),
;
val bitfield: Int
@@ -262,7 +551,7 @@ enum class ModuleRoute(@StringRes val title: Int, val route: Route, val icon: Im
companion object {
fun filterExcludedFrom(metadata: DeviceMetadata?): List = entries.filter {
when (metadata) {
- null -> true
+ null -> true // Include all routes if metadata is null
else -> metadata.excludedModules and it.bitfield == 0
}
}