diff --git a/AGENTS.md b/AGENTS.md
index ae128ca37f..69027f403e 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -32,13 +32,13 @@ This file serves as a comprehensive guide for AI agents and developers working o
- **Material 3:** The app uses Material 3. Look for ways to use **Material 3 Expressive** components where appropriate.
- **Strings:**
- Do **not** use `app/src/main/res/values/strings.xml` for UI strings.
- - Use the **Compose Multiplatform Resource** library in `core/strings`.
- - **Definition:** Add strings to `core/strings/src/commonMain/composeResources/values/strings.xml`.
+ - Use the **Compose Multiplatform Resource** library in `core:resources`.
+ - **Definition:** Add strings to `core/resources/src/commonMain/composeResources/values/strings.xml`.
- **Usage:**
```kotlin
import org.jetbrains.compose.resources.stringResource
- import org.meshtastic.core.strings.Res
- import org.meshtastic.core.strings.your_string_key
+ import org.meshtastic.core.resources.Res
+ import org.meshtastic.core.resources.your_string_key
Text(text = stringResource(Res.string.your_string_key))
```
@@ -102,7 +102,7 @@ This file serves as a comprehensive guide for AI agents and developers working o
1. **Explore First:** Before making changes, read `gradle/libs.versions.toml` and the relevant `build.gradle.kts` to understand the environment.
2. **Plan:** Identify which modules (`core` or `feature`) need modification.
3. **Implement:**
- - If adding a string, modify `core/strings`.
+ - If adding a string, modify `core:resources`.
- If adding a dependency, modify `libs.versions.toml` first.
4. **Verify:**
- Run `./gradlew spotlessApply` (Essential!).
@@ -118,8 +118,14 @@ This file serves as a comprehensive guide for AI agents and developers working o
## 7. Troubleshooting
-- **Missing Strings:** If `Res.string.xyz` is unresolved, ensure you have imported `org.meshtastic.core.strings.Res` and the specific string property, and that you have run a build to generate the resources.
+- **Missing Strings:** If `Res.string.xyz` is unresolved, ensure you have imported `org.meshtastic.core.resources.Res` and the specific string property, and that you have run a build to generate the resources.
- **Build Errors:** Check `gradle/libs.versions.toml` for version conflicts. Use `build-logic` conventions to ensure plugins are applied correctly.
---
*Refer to `CONTRIBUTING.md` for human-centric processes like Code of Conduct and Pull Request etiquette.*
+
+### E. Resources and Assets
+- **Centralization:** All global app resources (Strings, Drawables, Fonts, raw files) should be placed in `:core:resources`.
+- **Module Path:** `core/resources/src/commonMain/composeResources/`
+- **Decentralization:** Feature-specific strings and assets can (and should) be housed in their respective feature module's `composeResources` directory to maintain modular boundaries and clean architectural dependency graphs. Crowdin localization handles globbing `/**/composeResources/values/strings.xml` perfectly.
+- **Drawables:** Use `painterResource(Res.drawable.your_icon)` to access cross-platform drawables. Name them consistently (`ic_` for icons, `img_` for artwork). Avoid putting standard Drawables or Vectors in legacy Android `res/drawable` folders unless strictly required by a legacy library (like `OsmDroid` map markers) or the OS layer (like `app_icon.xml`).
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index b47041cebb..d64fe9976d 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -19,14 +19,14 @@ Thank you for your interest in contributing to Meshtastic-Android! We welcome co
- Write clear, descriptive variable and function names.
- Add comments where necessary, especially for complex logic.
- Keep methods and classes focused and concise.
-- **Strings:** Use localised strings via the **Compose Multiplatform Resource** library in `:core:strings`.
+- **Strings:** Use localised strings via the **Compose Multiplatform Resource** library in `:core:resources`.
- Do **not** use the legacy `app/src/main/res/values/strings.xml`.
- - **Definition:** Add strings to `core/strings/src/commonMain/composeResources/values/strings.xml`.
+ - **Definition:** Add strings to `core/resources/src/commonMain/composeResources/values/strings.xml`.
- **Usage:**
```kotlin
import org.jetbrains.compose.resources.stringResource
- import org.meshtastic.core.strings.Res
- import org.meshtastic.core.strings.your_string_key
+ import org.meshtastic.core.resources.Res
+ import org.meshtastic.core.resources.your_string_key
Text(text = stringResource(Res.string.your_string_key))
```
diff --git a/app/README.md b/app/README.md
index 6dd8c1ca7b..d61f3a4183 100644
--- a/app/README.md
+++ b/app/README.md
@@ -39,7 +39,7 @@ graph TB
:app -.-> :core:prefs
:app -.-> :core:proto
:app -.-> :core:service
- :app -.-> :core:strings
+ :app -.-> :core:resources
:app -.-> :core:ui
:app -.-> :core:barcode
:app -.-> :feature:intro
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 7503e1f7b0..1743e37bce 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -188,11 +188,9 @@ secrets {
androidComponents {
onVariants(selector().withBuildType("debug")) { variant ->
- variant.flavorName?.let { flavor ->
- variant.applicationId = "com.geeksville.mesh.$flavor.debug"
- }
+ variant.flavorName?.let { flavor -> variant.applicationId = "com.geeksville.mesh.$flavor.debug" }
}
-
+
onVariants(selector().withBuildType("release")) { variant ->
if (variant.flavorName == "google") {
val variantNameCapped = variant.name.replaceFirstChar { it.uppercase() }
@@ -226,7 +224,7 @@ dependencies {
implementation(projects.core.prefs)
implementation(projects.core.proto)
implementation(projects.core.service)
- implementation(projects.core.strings)
+ implementation(projects.core.resources)
implementation(projects.core.ui)
implementation(projects.core.barcode)
implementation(projects.feature.intro)
diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt
index ef82258384..3b5dffc1e3 100644
--- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt
+++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt
@@ -50,8 +50,8 @@ import no.nordicsemi.kotlin.ble.core.android.AndroidEnvironment
import no.nordicsemi.kotlin.ble.environment.android.compose.LocalEnvironmentOwner
import org.meshtastic.core.model.util.dispatchMeshtasticUri
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
-import org.meshtastic.core.strings.Res
-import org.meshtastic.core.strings.channel_invalid
+import org.meshtastic.core.resources.Res
+import org.meshtastic.core.resources.channel_invalid
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.theme.MODE_DYNAMIC
import org.meshtastic.core.ui.util.showToast
diff --git a/app/src/main/java/com/geeksville/mesh/domain/usecase/GetDiscoveredDevicesUseCase.kt b/app/src/main/java/com/geeksville/mesh/domain/usecase/GetDiscoveredDevicesUseCase.kt
new file mode 100644
index 0000000000..a6759dae6c
--- /dev/null
+++ b/app/src/main/java/com/geeksville/mesh/domain/usecase/GetDiscoveredDevicesUseCase.kt
@@ -0,0 +1,202 @@
+/*
+ * Copyright (c) 2025-2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.geeksville.mesh.domain.usecase
+
+import android.hardware.usb.UsbManager
+import android.net.nsd.NsdServiceInfo
+import com.geeksville.mesh.model.DeviceListEntry
+import com.geeksville.mesh.model.getMeshtasticShortName
+import com.geeksville.mesh.repository.network.NetworkRepository
+import com.geeksville.mesh.repository.network.NetworkRepository.Companion.toAddressString
+import com.geeksville.mesh.repository.radio.RadioInterfaceService
+import com.geeksville.mesh.repository.usb.UsbRepository
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
+import org.jetbrains.compose.resources.getString
+import org.meshtastic.core.ble.BluetoothRepository
+import org.meshtastic.core.data.repository.NodeRepository
+import org.meshtastic.core.database.DatabaseManager
+import org.meshtastic.core.database.model.Node
+import org.meshtastic.core.datastore.RecentAddressesDataSource
+import org.meshtastic.core.datastore.model.RecentAddress
+import org.meshtastic.core.resources.Res
+import org.meshtastic.core.resources.meshtastic
+import java.util.Locale
+import javax.inject.Inject
+
+data class DiscoveredDevices(
+ val bleDevices: List,
+ val usbDevices: List,
+ val discoveredTcpDevices: List,
+ val recentTcpDevices: List,
+)
+
+@Suppress("LongParameterList")
+class GetDiscoveredDevicesUseCase
+@Inject
+constructor(
+ private val bluetoothRepository: BluetoothRepository,
+ private val networkRepository: NetworkRepository,
+ private val recentAddressesDataSource: RecentAddressesDataSource,
+ private val nodeRepository: NodeRepository,
+ private val databaseManager: DatabaseManager,
+ private val usbRepository: UsbRepository,
+ private val radioInterfaceService: RadioInterfaceService,
+ private val usbManagerLazy: dagger.Lazy,
+) {
+ private val suffixLength = 4
+
+ @Suppress("LongMethod", "CyclomaticComplexMethod")
+ fun invoke(showMock: Boolean): Flow {
+ val nodeDb = nodeRepository.nodeDBbyNum
+
+ val bondedBleFlow = bluetoothRepository.state.map { ble -> ble.bondedDevices.map { DeviceListEntry.Ble(it) } }
+
+ val processedTcpFlow =
+ combine(networkRepository.resolvedList, recentAddressesDataSource.recentAddresses) {
+ tcpServices,
+ recentList,
+ ->
+ val recentMap = recentList.associateBy({ it.address }) { it.name }
+ tcpServices
+ .map { service ->
+ val address = "t${service.toAddressString()}"
+ val txtRecords = service.attributes
+ val shortNameBytes = txtRecords["shortname"]
+ val idBytes = txtRecords["id"]
+
+ val shortName =
+ shortNameBytes?.let { String(it, Charsets.UTF_8) } ?: getString(Res.string.meshtastic)
+ val deviceId = idBytes?.let { String(it, Charsets.UTF_8) }?.replace("!", "")
+ var displayName = recentMap[address] ?: shortName
+ if (deviceId != null && (displayName.split("_").none { it == deviceId })) {
+ displayName += "_$deviceId"
+ }
+ DeviceListEntry.Tcp(displayName, address)
+ }
+ .sortedBy { it.name }
+ }
+
+ val usbDevicesFlow =
+ usbRepository.serialDevices.map { usb ->
+ usb.map { (_, d) -> DeviceListEntry.Usb(radioInterfaceService, usbManagerLazy.get(), d) }
+ }
+
+ return combine(
+ nodeDb,
+ bondedBleFlow,
+ processedTcpFlow,
+ usbDevicesFlow,
+ networkRepository.resolvedList,
+ recentAddressesDataSource.recentAddresses,
+ ) { args: Array ->
+ @Suppress("UNCHECKED_CAST", "MagicNumber")
+ val db = args[0] as Map
+
+ @Suppress("UNCHECKED_CAST", "MagicNumber")
+ val bondedBle = args[1] as List
+
+ @Suppress("UNCHECKED_CAST", "MagicNumber")
+ val processedTcp = args[2] as List
+
+ @Suppress("UNCHECKED_CAST", "MagicNumber")
+ val usbDevices = args[3] as List
+
+ @Suppress("UNCHECKED_CAST", "MagicNumber")
+ val resolved = args[4] as List
+
+ @Suppress("UNCHECKED_CAST", "MagicNumber")
+ val recentList = args[5] as List
+
+ val bleForUi =
+ bondedBle
+ .map { entry ->
+ val matchingNode =
+ if (databaseManager.hasDatabaseFor(entry.fullAddress)) {
+ db.values.find { node ->
+ val suffix = entry.peripheral.getMeshtasticShortName()?.lowercase(Locale.ROOT)
+ suffix != null && node.user.id.lowercase(Locale.ROOT).endsWith(suffix)
+ }
+ } else {
+ null
+ }
+ entry.copy(node = matchingNode)
+ }
+ .sortedBy { it.name }
+
+ val usbForUi =
+ (usbDevices + if (showMock) listOf(DeviceListEntry.Mock("Demo Mode")) else emptyList()).map { entry ->
+ val matchingNode =
+ if (databaseManager.hasDatabaseFor(entry.fullAddress)) {
+ db.values.find { node ->
+ val suffix = entry.name.split("_").lastOrNull()?.lowercase(Locale.ROOT)
+ suffix != null &&
+ suffix.length >= suffixLength &&
+ node.user.id.lowercase(Locale.ROOT).endsWith(suffix)
+ }
+ } else {
+ null
+ }
+ entry.copy(node = matchingNode)
+ }
+
+ val discoveredTcpForUi =
+ processedTcp.map { entry ->
+ val matchingNode =
+ if (databaseManager.hasDatabaseFor(entry.fullAddress)) {
+ val resolvedService = resolved.find { "t${it.toAddressString()}" == entry.fullAddress }
+ val deviceId = resolvedService?.attributes?.get("id")?.let { String(it, Charsets.UTF_8) }
+ db.values.find { node ->
+ node.user.id == deviceId || (deviceId != null && node.user.id == "!$deviceId")
+ }
+ } else {
+ null
+ }
+ entry.copy(node = matchingNode)
+ }
+
+ val discoveredTcpAddresses = processedTcp.map { it.fullAddress }.toSet()
+ val recentTcpForUi =
+ recentList
+ .filterNot { discoveredTcpAddresses.contains(it.address) }
+ .map { DeviceListEntry.Tcp(it.name, it.address) }
+ .map { entry ->
+ val matchingNode =
+ if (databaseManager.hasDatabaseFor(entry.fullAddress)) {
+ val suffix = entry.name.split("_").lastOrNull()?.lowercase(Locale.ROOT)
+ db.values.find { node ->
+ suffix != null &&
+ suffix.length >= suffixLength &&
+ node.user.id.lowercase(Locale.ROOT).endsWith(suffix)
+ }
+ } else {
+ null
+ }
+ entry.copy(node = matchingNode)
+ }
+ .sortedBy { it.name }
+
+ DiscoveredDevices(
+ bleDevices = bleForUi,
+ usbDevices = usbForUi,
+ discoveredTcpDevices = discoveredTcpForUi,
+ recentTcpDevices = recentTcpForUi,
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/com/geeksville/mesh/model/UIViewModel.kt b/app/src/main/java/com/geeksville/mesh/model/UIViewModel.kt
index 87e0519325..52ef78ce5e 100644
--- a/app/src/main/java/com/geeksville/mesh/model/UIViewModel.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/UIViewModel.kt
@@ -53,13 +53,13 @@ import org.meshtastic.core.datastore.UiPreferencesDataSource
import org.meshtastic.core.model.TracerouteMapAvailability
import org.meshtastic.core.model.evaluateTracerouteMapAvailability
import org.meshtastic.core.model.util.dispatchMeshtasticUri
+import org.meshtastic.core.resources.Res
+import org.meshtastic.core.resources.client_notification
+import org.meshtastic.core.resources.compromised_keys
import org.meshtastic.core.service.IMeshService
import org.meshtastic.core.service.MeshServiceNotifications
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.service.TracerouteResponse
-import org.meshtastic.core.strings.Res
-import org.meshtastic.core.strings.client_notification
-import org.meshtastic.core.strings.compromised_keys
import org.meshtastic.core.ui.component.ScrollToTopEvent
import org.meshtastic.core.ui.util.AlertManager
import org.meshtastic.core.ui.util.ComposableContent
diff --git a/app/src/main/java/com/geeksville/mesh/navigation/NodesNavigation.kt b/app/src/main/java/com/geeksville/mesh/navigation/NodesNavigation.kt
index 0a8e50f344..d9fded5b43 100644
--- a/app/src/main/java/com/geeksville/mesh/navigation/NodesNavigation.kt
+++ b/app/src/main/java/com/geeksville/mesh/navigation/NodesNavigation.kt
@@ -46,16 +46,16 @@ import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
import org.meshtastic.core.navigation.NodeDetailRoutes
import org.meshtastic.core.navigation.NodesRoutes
import org.meshtastic.core.navigation.Route
-import org.meshtastic.core.strings.Res
-import org.meshtastic.core.strings.device
-import org.meshtastic.core.strings.environment
-import org.meshtastic.core.strings.host
-import org.meshtastic.core.strings.neighbor_info
-import org.meshtastic.core.strings.pax
-import org.meshtastic.core.strings.position_log
-import org.meshtastic.core.strings.power
-import org.meshtastic.core.strings.signal
-import org.meshtastic.core.strings.traceroute
+import org.meshtastic.core.resources.Res
+import org.meshtastic.core.resources.device
+import org.meshtastic.core.resources.environment
+import org.meshtastic.core.resources.host
+import org.meshtastic.core.resources.neighbor_info
+import org.meshtastic.core.resources.pax
+import org.meshtastic.core.resources.position_log
+import org.meshtastic.core.resources.power
+import org.meshtastic.core.resources.signal
+import org.meshtastic.core.resources.traceroute
import org.meshtastic.core.ui.component.ScrollToTopEvent
import org.meshtastic.feature.map.node.NodeMapScreen
import org.meshtastic.feature.map.node.NodeMapViewModel
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt b/app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt
index 4512b7a7d2..f746684259 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt
@@ -36,14 +36,14 @@ import org.meshtastic.core.common.util.nowSeconds
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.data.repository.RadioConfigRepository
import org.meshtastic.core.prefs.ui.UiPrefs
+import org.meshtastic.core.resources.Res
+import org.meshtastic.core.resources.connected_count
+import org.meshtastic.core.resources.connecting
+import org.meshtastic.core.resources.device_sleeping
+import org.meshtastic.core.resources.disconnected
+import org.meshtastic.core.resources.getString
import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.service.MeshServiceNotifications
-import org.meshtastic.core.strings.Res
-import org.meshtastic.core.strings.connected_count
-import org.meshtastic.core.strings.connecting
-import org.meshtastic.core.strings.device_sleeping
-import org.meshtastic.core.strings.disconnected
-import org.meshtastic.core.strings.getString
import org.meshtastic.proto.AdminMessage
import org.meshtastic.proto.Config
import org.meshtastic.proto.Telemetry
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt b/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt
index bc880d4111..36338d4934 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt
@@ -43,15 +43,15 @@ import org.meshtastic.core.model.util.SfppHasher
import org.meshtastic.core.model.util.decodeOrNull
import org.meshtastic.core.model.util.toOneLiner
import org.meshtastic.core.prefs.mesh.MeshPrefs
+import org.meshtastic.core.resources.Res
+import org.meshtastic.core.resources.critical_alert
+import org.meshtastic.core.resources.error_duty_cycle
+import org.meshtastic.core.resources.getString
+import org.meshtastic.core.resources.unknown_username
+import org.meshtastic.core.resources.waypoint_received
import org.meshtastic.core.service.MeshServiceNotifications
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.service.filter.MessageFilterService
-import org.meshtastic.core.strings.Res
-import org.meshtastic.core.strings.critical_alert
-import org.meshtastic.core.strings.error_duty_cycle
-import org.meshtastic.core.strings.getString
-import org.meshtastic.core.strings.unknown_username
-import org.meshtastic.core.strings.waypoint_received
import org.meshtastic.proto.AdminMessage
import org.meshtastic.proto.MeshPacket
import org.meshtastic.proto.Paxcount
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshNeighborInfoHandler.kt b/app/src/main/java/com/geeksville/mesh/service/MeshNeighborInfoHandler.kt
index 37694ada0e..3574bf6e1c 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshNeighborInfoHandler.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshNeighborInfoHandler.kt
@@ -21,10 +21,10 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import org.meshtastic.core.common.util.nowMillis
+import org.meshtastic.core.resources.Res
+import org.meshtastic.core.resources.getString
+import org.meshtastic.core.resources.unknown_username
import org.meshtastic.core.service.ServiceRepository
-import org.meshtastic.core.strings.Res
-import org.meshtastic.core.strings.getString
-import org.meshtastic.core.strings.unknown_username
import org.meshtastic.proto.MeshPacket
import org.meshtastic.proto.NeighborInfo
import java.util.Locale
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
index 34e1adf4da..db1a6066ff 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
@@ -107,7 +107,19 @@ class MeshService : Service() {
}
override fun onCreate() {
- super.onCreate()
+ try {
+ super.onCreate()
+ } catch (e: IllegalStateException) {
+ // Hilt can throw IllegalStateException in tests if the component is not created.
+ // This can happen if the service is started by the system (e.g. after a crash or on boot)
+ // before the test rule has a chance to create the component.
+ if (e.message?.contains("HiltAndroidRule") == true) {
+ Logger.w(e) { "MeshService created before Hilt component was ready in test. Stopping service." }
+ stopSelf()
+ return
+ }
+ throw e
+ }
Logger.i { "Creating mesh service" }
serviceNotifications.initChannels()
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotificationsImpl.kt b/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotificationsImpl.kt
index 62fe766e11..0a37174ee6 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotificationsImpl.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotificationsImpl.kt
@@ -53,27 +53,27 @@ import org.meshtastic.core.database.model.Message
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.util.formatUptime
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
+import org.meshtastic.core.resources.Res
+import org.meshtastic.core.resources.client_notification
+import org.meshtastic.core.resources.getString
+import org.meshtastic.core.resources.low_battery_message
+import org.meshtastic.core.resources.low_battery_title
+import org.meshtastic.core.resources.mark_as_read
+import org.meshtastic.core.resources.meshtastic_alerts_notifications
+import org.meshtastic.core.resources.meshtastic_app_name
+import org.meshtastic.core.resources.meshtastic_broadcast_notifications
+import org.meshtastic.core.resources.meshtastic_low_battery_notifications
+import org.meshtastic.core.resources.meshtastic_low_battery_temporary_remote_notifications
+import org.meshtastic.core.resources.meshtastic_messages_notifications
+import org.meshtastic.core.resources.meshtastic_new_nodes_notifications
+import org.meshtastic.core.resources.meshtastic_service_notifications
+import org.meshtastic.core.resources.meshtastic_waypoints_notifications
+import org.meshtastic.core.resources.new_node_seen
+import org.meshtastic.core.resources.no_local_stats
+import org.meshtastic.core.resources.reply
+import org.meshtastic.core.resources.you
import org.meshtastic.core.service.MeshServiceNotifications
import org.meshtastic.core.service.SERVICE_NOTIFY_ID
-import org.meshtastic.core.strings.Res
-import org.meshtastic.core.strings.client_notification
-import org.meshtastic.core.strings.getString
-import org.meshtastic.core.strings.low_battery_message
-import org.meshtastic.core.strings.low_battery_title
-import org.meshtastic.core.strings.mark_as_read
-import org.meshtastic.core.strings.meshtastic_alerts_notifications
-import org.meshtastic.core.strings.meshtastic_app_name
-import org.meshtastic.core.strings.meshtastic_broadcast_notifications
-import org.meshtastic.core.strings.meshtastic_low_battery_notifications
-import org.meshtastic.core.strings.meshtastic_low_battery_temporary_remote_notifications
-import org.meshtastic.core.strings.meshtastic_messages_notifications
-import org.meshtastic.core.strings.meshtastic_new_nodes_notifications
-import org.meshtastic.core.strings.meshtastic_service_notifications
-import org.meshtastic.core.strings.meshtastic_waypoints_notifications
-import org.meshtastic.core.strings.new_node_seen
-import org.meshtastic.core.strings.no_local_stats
-import org.meshtastic.core.strings.reply
-import org.meshtastic.core.strings.you
import org.meshtastic.proto.ClientNotification
import org.meshtastic.proto.DeviceMetrics
import org.meshtastic.proto.LocalStats
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshTracerouteHandler.kt b/app/src/main/java/com/geeksville/mesh/service/MeshTracerouteHandler.kt
index d03c3042a0..0ca3e3947c 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshTracerouteHandler.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshTracerouteHandler.kt
@@ -26,14 +26,14 @@ import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.data.repository.TracerouteSnapshotRepository
import org.meshtastic.core.model.fullRouteDiscovery
import org.meshtastic.core.model.getFullTracerouteResponse
+import org.meshtastic.core.resources.Res
+import org.meshtastic.core.resources.getString
+import org.meshtastic.core.resources.traceroute_duration
+import org.meshtastic.core.resources.traceroute_route_back_to_us
+import org.meshtastic.core.resources.traceroute_route_towards_dest
+import org.meshtastic.core.resources.unknown_username
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.service.TracerouteResponse
-import org.meshtastic.core.strings.Res
-import org.meshtastic.core.strings.getString
-import org.meshtastic.core.strings.traceroute_duration
-import org.meshtastic.core.strings.traceroute_route_back_to_us
-import org.meshtastic.core.strings.traceroute_route_towards_dest
-import org.meshtastic.core.strings.unknown_username
import org.meshtastic.proto.MeshPacket
import java.util.Locale
import javax.inject.Inject
diff --git a/app/src/main/java/com/geeksville/mesh/ui/Main.kt b/app/src/main/java/com/geeksville/mesh/ui/Main.kt
index 170926b7cc..bc3b82a6d4 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/Main.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/Main.kt
@@ -106,26 +106,26 @@ import org.meshtastic.core.navigation.NodeDetailRoutes
import org.meshtastic.core.navigation.NodesRoutes
import org.meshtastic.core.navigation.Route
import org.meshtastic.core.navigation.SettingsRoutes
+import org.meshtastic.core.resources.Res
+import org.meshtastic.core.resources.app_too_old
+import org.meshtastic.core.resources.bottom_nav_settings
+import org.meshtastic.core.resources.connected
+import org.meshtastic.core.resources.connecting
+import org.meshtastic.core.resources.connections
+import org.meshtastic.core.resources.conversations
+import org.meshtastic.core.resources.device_sleeping
+import org.meshtastic.core.resources.disconnected
+import org.meshtastic.core.resources.firmware_old
+import org.meshtastic.core.resources.firmware_too_old
+import org.meshtastic.core.resources.map
+import org.meshtastic.core.resources.must_update
+import org.meshtastic.core.resources.nodes
+import org.meshtastic.core.resources.okay
+import org.meshtastic.core.resources.should_update
+import org.meshtastic.core.resources.should_update_firmware
+import org.meshtastic.core.resources.traceroute
+import org.meshtastic.core.resources.view_on_map
import org.meshtastic.core.service.ConnectionState
-import org.meshtastic.core.strings.Res
-import org.meshtastic.core.strings.app_too_old
-import org.meshtastic.core.strings.bottom_nav_settings
-import org.meshtastic.core.strings.connected
-import org.meshtastic.core.strings.connecting
-import org.meshtastic.core.strings.connections
-import org.meshtastic.core.strings.conversations
-import org.meshtastic.core.strings.device_sleeping
-import org.meshtastic.core.strings.disconnected
-import org.meshtastic.core.strings.firmware_old
-import org.meshtastic.core.strings.firmware_too_old
-import org.meshtastic.core.strings.map
-import org.meshtastic.core.strings.must_update
-import org.meshtastic.core.strings.nodes
-import org.meshtastic.core.strings.okay
-import org.meshtastic.core.strings.should_update
-import org.meshtastic.core.strings.should_update_firmware
-import org.meshtastic.core.strings.traceroute
-import org.meshtastic.core.strings.view_on_map
import org.meshtastic.core.ui.component.MeshtasticDialog
import org.meshtastic.core.ui.component.ScrollToTopEvent
import org.meshtastic.core.ui.icon.Conversations
diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt
index 6b2873d5b4..7f9c74d594 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt
@@ -64,17 +64,17 @@ import org.jetbrains.compose.resources.getString
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.navigation.Route
import org.meshtastic.core.navigation.SettingsRoutes
+import org.meshtastic.core.resources.Res
+import org.meshtastic.core.resources.connected
+import org.meshtastic.core.resources.connected_device
+import org.meshtastic.core.resources.connected_sleeping
+import org.meshtastic.core.resources.connecting
+import org.meshtastic.core.resources.connections
+import org.meshtastic.core.resources.must_set_region
+import org.meshtastic.core.resources.no_device_selected
+import org.meshtastic.core.resources.not_connected
+import org.meshtastic.core.resources.set_your_region
import org.meshtastic.core.service.ConnectionState
-import org.meshtastic.core.strings.Res
-import org.meshtastic.core.strings.connected
-import org.meshtastic.core.strings.connected_device
-import org.meshtastic.core.strings.connected_sleeping
-import org.meshtastic.core.strings.connecting
-import org.meshtastic.core.strings.connections
-import org.meshtastic.core.strings.must_set_region
-import org.meshtastic.core.strings.no_device_selected
-import org.meshtastic.core.strings.not_connected
-import org.meshtastic.core.strings.set_your_region
import org.meshtastic.core.ui.component.ListItem
import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.TitledCard
diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/ScannerViewModel.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/ScannerViewModel.kt
index f694a3bf88..131eb33e8f 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/connections/ScannerViewModel.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/connections/ScannerViewModel.kt
@@ -18,16 +18,13 @@ package com.geeksville.mesh.ui.connections
import android.app.Application
import android.content.Context
-import android.hardware.usb.UsbManager
import android.os.RemoteException
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
import co.touchlab.kermit.Severity
+import com.geeksville.mesh.domain.usecase.GetDiscoveredDevicesUseCase
import com.geeksville.mesh.model.DeviceListEntry
-import com.geeksville.mesh.model.getMeshtasticShortName
-import com.geeksville.mesh.repository.network.NetworkRepository
-import com.geeksville.mesh.repository.network.NetworkRepository.Companion.toAddressString
import com.geeksville.mesh.repository.radio.RadioInterfaceService
import com.geeksville.mesh.repository.usb.UsbRepository
import com.geeksville.mesh.service.MeshService
@@ -36,25 +33,18 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
-import org.jetbrains.compose.resources.getString
import org.meshtastic.core.ble.BluetoothRepository
-import org.meshtastic.core.data.repository.NodeRepository
-import org.meshtastic.core.database.DatabaseManager
-import org.meshtastic.core.database.model.Node
import org.meshtastic.core.datastore.RecentAddressesDataSource
import org.meshtastic.core.datastore.model.RecentAddress
import org.meshtastic.core.model.util.anonymize
import org.meshtastic.core.service.ServiceRepository
-import org.meshtastic.core.strings.Res
-import org.meshtastic.core.strings.meshtastic
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
-import java.util.Locale
import javax.inject.Inject
@HiltViewModel
@@ -66,12 +56,9 @@ constructor(
private val serviceRepository: ServiceRepository,
private val bluetoothRepository: BluetoothRepository,
private val usbRepository: UsbRepository,
- private val usbManagerLazy: dagger.Lazy,
- private val networkRepository: NetworkRepository,
private val radioInterfaceService: RadioInterfaceService,
private val recentAddressesDataSource: RecentAddressesDataSource,
- private val nodeRepository: NodeRepository,
- private val databaseManager: DatabaseManager,
+ private val getDiscoveredDevicesUseCase: GetDiscoveredDevicesUseCase,
) : ViewModel() {
private val context: Context
get() = application.applicationContext
@@ -81,142 +68,32 @@ constructor(
private val _errorText = MutableStateFlow(null)
val errorText: StateFlow = _errorText.asStateFlow()
- private val nodeDb: StateFlow