From 2676a51647ab40bf92811e35566ad2bdd181df8f Mon Sep 17 00:00:00 2001
From: James Rich <2199651+jamesarich@users.noreply.github.com>
Date: Sun, 22 Feb 2026 21:39:50 -0600
Subject: [PATCH] refactor(ui): compose resources, domain layer (#4628)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
---
AGENTS.md | 18 +-
CONTRIBUTING.md | 8 +-
app/README.md | 2 +-
app/build.gradle.kts | 8 +-
.../java/com/geeksville/mesh/MainActivity.kt | 4 +-
.../usecase/GetDiscoveredDevicesUseCase.kt | 202 +++++++++
.../com/geeksville/mesh/model/UIViewModel.kt | 6 +-
.../mesh/navigation/NodesNavigation.kt | 20 +-
.../mesh/service/MeshConnectionManager.kt | 12 +-
.../mesh/service/MeshDataHandler.kt | 12 +-
.../mesh/service/MeshNeighborInfoHandler.kt | 6 +-
.../geeksville/mesh/service/MeshService.kt | 14 +-
.../service/MeshServiceNotificationsImpl.kt | 38 +-
.../mesh/service/MeshTracerouteHandler.kt | 12 +-
.../main/java/com/geeksville/mesh/ui/Main.kt | 38 +-
.../mesh/ui/connections/ConnectionsScreen.kt | 20 +-
.../mesh/ui/connections/ScannerViewModel.kt | 157 +------
.../ui/connections/components/BLEDevices.kt | 4 +-
.../components/ConnectingDeviceInfo.kt | 6 +-
.../components/ConnectionsSegmentedBar.kt | 11 +-
.../components/CurrentlyConnectedInfo.kt | 6 +-
.../connections/components/DeviceListItem.kt | 10 +-
.../connections/components/NetworkDevices.kt | 20 +-
.../ui/connections/components/UsbDevices.kt | 4 +-
.../mesh/ui/contact/AdaptiveContactsScreen.kt | 7 +-
.../geeksville/mesh/ui/contact/ContactItem.kt | 8 +-
.../geeksville/mesh/ui/contact/Contacts.kt | 42 +-
.../mesh/ui/contact/ContactsViewModel.kt | 4 +-
.../mesh/ui/node/AdaptiveNodeListScreen.kt | 4 +-
.../com/geeksville/mesh/ui/sharing/Channel.kt | 28 +-
.../com/geeksville/mesh/ui/sharing/Share.kt | 15 +-
app/src/main/res/values/strings.xml | 19 -
.../radio/NordicBleInterfaceDrainTest.kt | 161 -------
.../kotlin/org/meshtastic/buildlogic/Dokka.kt | 2 +-
.../kotlin/org/meshtastic/buildlogic/Kover.kt | 2 +-
core/barcode/README.md | 2 +-
core/barcode/build.gradle.kts | 2 +-
.../core/barcode/BarcodeScannerProvider.kt | 4 +-
.../core/barcode/BarcodeScannerProvider.kt | 4 +-
.../core/ble/BluetoothRepository.kt | 3 +-
.../repository/FirmwareReleaseRepository.kt | 8 +-
core/database/README.md | 2 +-
core/database/build.gradle.kts | 2 +-
.../meshtastic/core/database/model/Message.kt | 54 +--
.../core/database/model/NodeSortOption.kt | 19 +-
core/{strings => resources}/README.md | 10 +-
core/{strings => resources}/build.gradle.kts | 2 +-
.../meshtastic/core/resources}/ContextExt.kt | 2 +-
.../composeResources/drawable/ic_antenna.xml} | 0
.../drawable/ic_battery_alert.xml | 0
.../drawable/ic_battery_high.xml | 0
.../drawable/ic_battery_low.xml | 0
.../drawable/ic_battery_medium.xml | 0
.../drawable/ic_battery_outline.xml | 0
.../drawable/ic_battery_unknown.xml | 0
.../drawable/ic_counter_0.xml} | 0
.../drawable/ic_counter_1.xml} | 0
.../drawable/ic_counter_2.xml} | 0
.../drawable/ic_counter_3.xml} | 0
.../drawable/ic_counter_4.xml} | 0
.../drawable/ic_counter_5.xml} | 0
.../drawable/ic_counter_6.xml} | 0
.../drawable/ic_counter_7.xml} | 0
.../drawable/ic_counter_8.xml} | 0
.../drawable/ic_dew_point.xml | 0
.../drawable/ic_location_on.xml} | 0
.../drawable/ic_lock_open_right.xml} | 0
.../drawable/ic_map_location_dot.xml} | 0
.../drawable/ic_map_navigation.xml} | 0
.../drawable/ic_meshtastic.xml | 0
.../drawable/ic_mountain_flag.xml} | 0
.../drawable/ic_power_plug.xml} | 0
.../drawable/ic_radioactive.xml | 0
.../drawable/ic_soil_moisture.xml | 0
.../drawable/ic_soil_temperature.xml | 0
.../drawable/ic_unverified.xml | 0
.../composeResources/drawable/img_chirpy.xml} | 0
.../drawable/img_hw_unknown.xml | 0
.../composeResources/drawable/img_qrcode.png} | Bin
.../composeResources/values-ar/strings.xml | 0
.../composeResources/values-be/strings.xml | 0
.../composeResources/values-bg/strings.xml | 0
.../composeResources/values-ca/strings.xml | 0
.../composeResources/values-cs/strings.xml | 0
.../composeResources/values-de/strings.xml | 0
.../composeResources/values-el/strings.xml | 0
.../composeResources/values-es/strings.xml | 0
.../composeResources/values-et/strings.xml | 0
.../composeResources/values-fi/strings.xml | 0
.../composeResources/values-fr/strings.xml | 0
.../composeResources/values-ga/strings.xml | 0
.../composeResources/values-gl/strings.xml | 0
.../composeResources/values-he/strings.xml | 0
.../composeResources/values-hr/strings.xml | 0
.../composeResources/values-ht/strings.xml | 0
.../composeResources/values-hu/strings.xml | 0
.../composeResources/values-is/strings.xml | 0
.../composeResources/values-it/strings.xml | 0
.../composeResources/values-ja/strings.xml | 0
.../composeResources/values-ko/strings.xml | 0
.../composeResources/values-lt/strings.xml | 0
.../composeResources/values-nl/strings.xml | 0
.../composeResources/values-no/strings.xml | 0
.../composeResources/values-pl/strings.xml | 0
.../values-pt-rBR/strings.xml | 0
.../composeResources/values-pt/strings.xml | 0
.../composeResources/values-ro/strings.xml | 0
.../composeResources/values-ru/strings.xml | 0
.../composeResources/values-sk/strings.xml | 0
.../composeResources/values-sl/strings.xml | 0
.../composeResources/values-sq/strings.xml | 0
.../composeResources/values-sr/strings.xml | 0
.../composeResources/values-srp/strings.xml | 0
.../composeResources/values-sv/strings.xml | 0
.../composeResources/values-tr/strings.xml | 0
.../composeResources/values-uk/strings.xml | 0
.../values-zh-rCN/strings.xml | 0
.../values-zh-rTW/strings.xml | 0
.../composeResources/values/strings.xml | 0
.../org/meshtastic/core/resources/UiText.kt | 84 ++++
core/ui/README.md | 6 +-
core/ui/build.gradle.kts | 2 +-
.../core/ui/component/AlertDialogs.kt | 6 +-
.../core/ui/component/BitwisePreference.kt | 6 +-
.../core/ui/component/ChannelInfo.kt | 4 +-
.../core/ui/component/ContactSharing.kt | 4 +-
.../core/ui/component/CopyIconButton.kt | 7 +-
.../core/ui/component/DistanceInfo.kt | 4 +-
.../core/ui/component/EditBase64Preference.kt | 6 +-
.../core/ui/component/EditListPreference.kt | 14 +-
.../ui/component/EditPasswordPreference.kt | 9 +-
.../core/ui/component/EditTextPreference.kt | 7 +-
.../core/ui/component/ElevationInfo.kt | 6 +-
.../meshtastic/core/ui/component/HopsInfo.kt | 4 +-
.../meshtastic/core/ui/component/ImportFab.kt | 32 +-
.../core/ui/component/IndoorAirQuality.kt | 8 +-
.../core/ui/component/LastHeardInfo.kt | 11 +-
.../core/ui/component/LoraSignalIndicator.kt | 18 +-
.../core/ui/component/MainAppBar.kt | 19 +-
.../core/ui/component/MaterialBatteryInfo.kt | 4 +-
.../component/MaterialBluetoothSignalInfo.kt | 7 +-
.../core/ui/component/NodeKeyStatusIcon.kt | 26 +-
.../component/PositionPrecisionPreference.kt | 9 +-
.../meshtastic/core/ui/component/QrDialog.kt | 10 +-
.../core/ui/component/SatelliteCountInfo.kt | 4 +-
.../core/ui/component/SecurityIcon.kt | 32 +-
.../core/ui/component/SignalInfo.kt | 4 +-
.../core/ui/component/TelemetryInfo.kt | 26 +-
.../core/ui/component/TransportIcon.kt | 10 +-
.../org/meshtastic/core/ui/icon/Counter.kt | 38 +-
.../org/meshtastic/core/ui/icon/Device.kt | 7 +-
.../core/ui/qr/ScannedQrCodeDialog.kt | 16 +-
.../core/ui/share/SharedContactDialog.kt | 12 +-
.../org/meshtastic/core/ui/util/FormatAgo.kt | 8 +-
.../core/ui/util/ModelExtensions.kt | 26 +-
.../core/ui/util/ProtoExtensions.kt | 4 +-
crowdin.yml | 2 +-
feature/firmware/README.md | 2 +-
feature/firmware/build.gradle.kts | 2 +-
.../feature/firmware/FirmwareDfuService.kt | 6 +-
.../feature/firmware/FirmwareUpdateScreen.kt | 93 +++--
.../firmware/FirmwareUpdateViewModel.kt | 44 +-
.../feature/firmware/NordicDfuHandler.kt | 10 +-
.../feature/firmware/UsbUpdateHandler.kt | 10 +-
.../firmware/ota/Esp32OtaUpdateHandler.kt | 22 +-
feature/intro/README.md | 2 +-
feature/intro/build.gradle.kts | 2 +-
.../feature/intro/AnalyticsIntro.kt | 14 +-
.../feature/intro/AppIntroductionScreen.kt | 6 +-
.../feature/intro/BluetoothScreen.kt | 20 +-
.../feature/intro/CriticalAlertsScreen.kt | 13 +-
.../feature/intro/LocationScreen.kt | 31 +-
.../feature/intro/NotificationsScreen.kt | 24 +-
.../feature/intro/PermissionScreenLayout.kt | 7 +-
.../meshtastic/feature/intro/WelcomeScreen.kt | 23 +-
feature/map/README.md | 2 +-
feature/map/build.gradle.kts | 2 +-
.../org/meshtastic/feature/map/MapView.kt | 80 ++--
.../feature/map/MapViewExtensions.kt | 3 +-
.../feature/map/component/CacheLayout.kt | 13 +-
.../feature/map/component/DownloadButton.kt | 4 +-
.../map/component/EditWaypointDialog.kt | 24 +-
.../feature/map/component/MapButton.kt | 7 +-
.../fdroid/res/drawable/ic_location_on.xml | 14 +
.../res/drawable/ic_map_location_dot.xml | 11 +
.../fdroid/res/drawable/ic_map_navigation.xml | 11 +
.../org/meshtastic/feature/map/MapView.kt | 20 +-
.../map/component/ClusterItemsListDialog.kt | 9 +-
.../map/component/CustomMapLayersSheet.kt | 19 +-
.../CustomTileProviderManagerSheet.kt | 30 +-
.../map/component/EditWaypointDialog.kt | 24 +-
.../map/component/MapControlsOverlay.kt | 12 +-
.../map/component/MapFilterDropdown.kt | 13 +-
.../feature/map/component/MapTypeDropdown.kt | 17 +-
.../feature/map/component/WaypointMarkers.kt | 4 +-
.../feature/map/BaseMapViewModel.kt | 12 +-
.../org/meshtastic/feature/map/MapScreen.kt | 7 +-
feature/messaging/README.md | 2 +-
feature/messaging/build.gradle.kts | 4 +-
.../feature/messaging/DeliveryInfoDialog.kt | 8 +-
.../meshtastic/feature/messaging/Message.kt | 58 ++-
.../feature/messaging/MessageListPaged.kt | 117 ++++--
.../feature/messaging/MessageViewModel.kt | 73 +---
.../meshtastic/feature/messaging/QuickChat.kt | 24 +-
.../messaging/component/MessageActions.kt | 8 +-
.../component/MessageActionsBottomSheet.kt | 12 +-
.../messaging/component/MessageItem.kt | 10 +-
.../feature/messaging/component/Reaction.kt | 16 +-
.../domain/usecase/SendMessageUseCase.kt | 103 +++++
.../domain/usecase/SendMessageUseCaseTest.kt | 146 +++++++
feature/node/README.md | 2 +-
feature/node/build.gradle.kts | 2 +-
feature/node/component/DeviceActions.kt | 18 +-
.../node/component/AdministrationSection.kt | 18 +-
.../node/component/CompassBottomSheet.kt | 28 +-
.../feature/node/component/DeviceActions.kt | 16 +-
.../node/component/DeviceDetailsSection.kt | 26 +-
.../feature/node/component/DistanceInfo.kt | 4 +-
.../feature/node/component/ElevationInfo.kt | 6 +-
.../node/component/EnvironmentMetrics.kt | 50 ++-
.../component/FirmwareReleaseSheetContent.kt | 8 +-
.../feature/node/component/HopsInfo.kt | 4 +-
.../feature/node/component/InfoCard.kt | 14 +-
.../feature/node/component/LastHeardInfo.kt | 10 +-
.../node/component/LinkedCoordinatesItem.kt | 8 +-
.../node/component/NodeDetailComponents.kt | 4 +-
.../node/component/NodeDetailsSection.kt | 40 +-
.../node/component/NodeFilterTextField.kt | 24 +-
.../feature/node/component/NodeItem.kt | 16 +-
.../feature/node/component/NodeStatusIcons.kt | 18 +-
.../feature/node/component/NotesSection.kt | 8 +-
.../feature/node/component/PositionSection.kt | 8 +-
.../feature/node/component/PowerMetrics.kt | 8 +-
.../node/component/SatelliteCountInfo.kt | 4 +-
.../component/TelemetricActionsSection.kt | 12 +-
.../feature/node/component/TelemetryInfo.kt | 22 +-
.../feature/node/detail/NodeDetailScreen.kt | 20 +-
.../node/detail/NodeDetailViewModel.kt | 192 +--------
.../node/detail/NodeManagementActions.kt | 26 +-
.../feature/node/detail/NodeRequestActions.kt | 49 ++-
.../domain/usecase/GetFilteredNodesUseCase.kt | 60 +++
.../domain/usecase/GetNodeDetailsUseCase.kt | 237 +++++++++++
.../feature/node/list/NodeListScreen.kt | 22 +-
.../feature/node/list/NodeListViewModel.kt | 35 +-
.../feature/node/metrics/BaseMetricChart.kt | 6 +-
.../feature/node/metrics/CommonCharts.kt | 12 +-
.../feature/node/metrics/DeviceMetrics.kt | 21 +-
.../feature/node/metrics/EnvironmentCharts.kt | 18 +-
.../node/metrics/EnvironmentMetrics.kt | 31 +-
.../feature/node/metrics/HostMetricsLog.kt | 15 +-
.../feature/node/metrics/MetricsViewModel.kt | 393 +++++-------------
.../feature/node/metrics/NeighborInfoLog.kt | 15 +-
.../feature/node/metrics/PaxMetrics.kt | 17 +-
.../feature/node/metrics/PositionLog.kt | 23 +-
.../feature/node/metrics/PowerMetrics.kt | 17 +-
.../feature/node/metrics/SignalMetrics.kt | 15 +-
.../feature/node/metrics/TracerouteLog.kt | 35 +-
.../node/metrics/TracerouteMapScreen.kt | 10 +-
.../meshtastic/feature/node/model/LogsType.kt | 22 +-
.../feature/node/model/MetricInfo.kt | 7 +-
.../feature/node/model/TimeFrame.kt | 14 +-
.../usecase/GetFilteredNodesUseCaseTest.kt | 119 ++++++
.../node/metrics/BaseMetricScreenTest.kt | 4 +-
feature/settings/README.md | 2 +-
feature/settings/build.gradle.kts | 2 +-
.../settings/debugging/DebugFiltersTest.kt | 8 +-
.../settings/debugging/DebugSearchTest.kt | 10 +-
.../component/EditDeviceProfileDialogTest.kt | 8 +-
.../component/MapReportingPreferenceTest.kt | 10 +-
.../feature/settings/AboutScreen.kt | 4 +-
.../feature/settings/SettingsScreen.kt | 54 +--
.../feature/settings/debugging/Debug.kt | 32 +-
.../settings/debugging/DebugFilters.kt | 20 +-
.../feature/settings/debugging/DebugSearch.kt | 12 +-
.../settings/debugging/DebugViewModel.kt | 6 +-
.../settings/filter/FilterSettingsScreen.kt | 24 +-
.../settings/navigation/ConfigRoute.kt | 22 +-
.../settings/navigation/ModuleRoute.kt | 30 +-
.../settings/radio/CleanNodeDatabaseScreen.kt | 14 +-
.../radio/CleanNodeDatabaseViewModel.kt | 8 +-
.../feature/settings/radio/RadioConfig.kt | 36 +-
.../settings/radio/RadioConfigViewModel.kt | 8 +-
.../feature/settings/radio/ResponseState.kt | 2 +-
.../radio/channel/ChannelConfigScreen.kt | 14 +-
.../radio/channel/component/ChannelCard.kt | 4 +-
.../channel/component/ChannelConfigHeader.kt | 11 +-
.../radio/channel/component/ChannelLegend.kt | 34 +-
.../channel/component/EditChannelDialog.kt | 14 +-
.../AmbientLightingConfigItemList.kt | 16 +-
.../radio/component/AudioConfigItemList.kt | 20 +-
.../component/BluetoothConfigItemList.kt | 12 +-
.../component/CannedMessageConfigItemList.kt | 30 +-
.../DetectionSensorConfigItemList.kt | 22 +-
.../radio/component/DeviceConfigItemList.kt | 88 ++--
.../radio/component/DisplayConfigItemList.kt | 50 +--
.../component/EditDeviceProfileDialog.kt | 18 +-
.../ExternalNotificationConfigItemList.kt | 48 +--
.../radio/component/LoRaConfigItemList.kt | 48 +--
.../radio/component/MQTTConfigItemList.kt | 28 +-
.../radio/component/MapReportingPreference.kt | 19 +-
.../component/NeighborInfoConfigItemList.kt | 14 +-
.../radio/component/NetworkConfigItemList.kt | 58 +--
.../component/PacketResponseStateDialog.kt | 8 +-
.../component/PaxcounterConfigItemList.kt | 14 +-
.../radio/component/PositionConfigItemList.kt | 50 +--
.../radio/component/PowerConfigItemList.kt | 24 +-
.../radio/component/RadioConfigScreenList.kt | 6 +-
.../component/RangeTestConfigItemList.kt | 12 +-
.../component/RemoteHardwareConfigItemList.kt | 12 +-
.../radio/component/SecurityConfigItemList.kt | 46 +-
.../radio/component/SerialConfigItemList.kt | 18 +-
.../component/ShutdownConfirmationDialog.kt | 10 +-
.../component/StatusMessageConfigItemList.kt | 10 +-
.../component/StoreForwardConfigItemList.kt | 18 +-
.../component/TelemetryConfigItemList.kt | 30 +-
.../radio/component/UserConfigItemList.kt | 22 +-
.../settings/radio/component/WarningDialog.kt | 6 +-
.../settings/util/FixedUpdateIntervals.kt | 12 +-
.../feature/settings/util/LanguageUtils.kt | 12 +-
.../feature/settings/util/UiText.kt | 34 --
.../feature/settings/HomoglyphSettingTest.kt | 6 +-
settings.gradle.kts | 2 +-
322 files changed, 3031 insertions(+), 2790 deletions(-)
create mode 100644 app/src/main/java/com/geeksville/mesh/domain/usecase/GetDiscoveredDevicesUseCase.kt
delete mode 100644 app/src/main/res/values/strings.xml
delete mode 100644 app/src/test/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceDrainTest.kt
rename core/{strings => resources}/README.md (78%)
rename core/{strings => resources}/build.gradle.kts (94%)
rename core/{strings/src/androidMain/kotlin/org/meshtastic/core/strings => resources/src/androidMain/kotlin/org/meshtastic/core/resources}/ContextExt.kt (97%)
rename core/{ui/src/main/res/drawable/ic_antenna_24.xml => resources/src/commonMain/composeResources/drawable/ic_antenna.xml} (100%)
rename core/{ui/src/main/res => resources/src/commonMain/composeResources}/drawable/ic_battery_alert.xml (100%)
rename core/{ui/src/main/res => resources/src/commonMain/composeResources}/drawable/ic_battery_high.xml (100%)
rename core/{ui/src/main/res => resources/src/commonMain/composeResources}/drawable/ic_battery_low.xml (100%)
rename core/{ui/src/main/res => resources/src/commonMain/composeResources}/drawable/ic_battery_medium.xml (100%)
rename core/{ui/src/main/res => resources/src/commonMain/composeResources}/drawable/ic_battery_outline.xml (100%)
rename core/{ui/src/main/res => resources/src/commonMain/composeResources}/drawable/ic_battery_unknown.xml (100%)
rename core/{ui/src/main/res/drawable/counter_0_24px.xml => resources/src/commonMain/composeResources/drawable/ic_counter_0.xml} (100%)
rename core/{ui/src/main/res/drawable/counter_1_24px.xml => resources/src/commonMain/composeResources/drawable/ic_counter_1.xml} (100%)
rename core/{ui/src/main/res/drawable/counter_2_24px.xml => resources/src/commonMain/composeResources/drawable/ic_counter_2.xml} (100%)
rename core/{ui/src/main/res/drawable/counter_3_24px.xml => resources/src/commonMain/composeResources/drawable/ic_counter_3.xml} (100%)
rename core/{ui/src/main/res/drawable/counter_4_24px.xml => resources/src/commonMain/composeResources/drawable/ic_counter_4.xml} (100%)
rename core/{ui/src/main/res/drawable/counter_5_24px.xml => resources/src/commonMain/composeResources/drawable/ic_counter_5.xml} (100%)
rename core/{ui/src/main/res/drawable/counter_6_24px.xml => resources/src/commonMain/composeResources/drawable/ic_counter_6.xml} (100%)
rename core/{ui/src/main/res/drawable/counter_7_24px.xml => resources/src/commonMain/composeResources/drawable/ic_counter_7.xml} (100%)
rename core/{ui/src/main/res/drawable/counter_8_24px.xml => resources/src/commonMain/composeResources/drawable/ic_counter_8.xml} (100%)
rename feature/node/src/main/res/drawable/ic_outlined_dew_point_24.xml => core/resources/src/commonMain/composeResources/drawable/ic_dew_point.xml (100%)
rename core/{ui/src/main/res/drawable/ic_baseline_location_on_24.xml => resources/src/commonMain/composeResources/drawable/ic_location_on.xml} (100%)
rename core/{ui/src/main/res/drawable/ic_lock_open_right_24.xml => resources/src/commonMain/composeResources/drawable/ic_lock_open_right.xml} (100%)
rename core/{ui/src/main/res/drawable/ic_map_location_dot_24.xml => resources/src/commonMain/composeResources/drawable/ic_map_location_dot.xml} (100%)
rename core/{ui/src/main/res/drawable/ic_map_navigation_24.xml => resources/src/commonMain/composeResources/drawable/ic_map_navigation.xml} (100%)
rename core/{ui/src/main/res => resources/src/commonMain/composeResources}/drawable/ic_meshtastic.xml (100%)
rename core/{ui/src/main/res/drawable/mountain_flag_24px.xml => resources/src/commonMain/composeResources/drawable/ic_mountain_flag.xml} (100%)
rename core/{ui/src/main/res/drawable/ic_power_plug_24.xml => resources/src/commonMain/composeResources/drawable/ic_power_plug.xml} (100%)
rename feature/node/src/main/res/drawable/ic_filled_radioactive_24.xml => core/resources/src/commonMain/composeResources/drawable/ic_radioactive.xml (100%)
rename feature/node/src/main/res/drawable/soil_moisture.xml => core/resources/src/commonMain/composeResources/drawable/ic_soil_moisture.xml (100%)
rename feature/node/src/main/res/drawable/soil_temperature.xml => core/resources/src/commonMain/composeResources/drawable/ic_soil_temperature.xml (100%)
rename feature/node/src/main/res/drawable/unverified.xml => core/resources/src/commonMain/composeResources/drawable/ic_unverified.xml (100%)
rename core/{ui/src/main/res/drawable/chirpy.xml => resources/src/commonMain/composeResources/drawable/img_chirpy.xml} (100%)
rename feature/node/src/main/res/drawable/hw_unknown.xml => core/resources/src/commonMain/composeResources/drawable/img_hw_unknown.xml (100%)
rename core/{ui/src/main/res/drawable-nodpi/qrcode.png => resources/src/commonMain/composeResources/drawable/img_qrcode.png} (100%)
rename core/{strings => resources}/src/commonMain/composeResources/values-ar/strings.xml (100%)
rename core/{strings => resources}/src/commonMain/composeResources/values-be/strings.xml (100%)
rename core/{strings => resources}/src/commonMain/composeResources/values-bg/strings.xml (100%)
rename core/{strings => resources}/src/commonMain/composeResources/values-ca/strings.xml (100%)
rename core/{strings => resources}/src/commonMain/composeResources/values-cs/strings.xml (100%)
rename core/{strings => resources}/src/commonMain/composeResources/values-de/strings.xml (100%)
rename core/{strings => resources}/src/commonMain/composeResources/values-el/strings.xml (100%)
rename core/{strings => resources}/src/commonMain/composeResources/values-es/strings.xml (100%)
rename core/{strings => resources}/src/commonMain/composeResources/values-et/strings.xml (100%)
rename core/{strings => resources}/src/commonMain/composeResources/values-fi/strings.xml (100%)
rename core/{strings => resources}/src/commonMain/composeResources/values-fr/strings.xml (100%)
rename core/{strings => resources}/src/commonMain/composeResources/values-ga/strings.xml (100%)
rename core/{strings => resources}/src/commonMain/composeResources/values-gl/strings.xml (100%)
rename core/{strings => resources}/src/commonMain/composeResources/values-he/strings.xml (100%)
rename core/{strings => resources}/src/commonMain/composeResources/values-hr/strings.xml (100%)
rename core/{strings => resources}/src/commonMain/composeResources/values-ht/strings.xml (100%)
rename core/{strings => resources}/src/commonMain/composeResources/values-hu/strings.xml (100%)
rename core/{strings => resources}/src/commonMain/composeResources/values-is/strings.xml (100%)
rename core/{strings => resources}/src/commonMain/composeResources/values-it/strings.xml (100%)
rename core/{strings => resources}/src/commonMain/composeResources/values-ja/strings.xml (100%)
rename core/{strings => resources}/src/commonMain/composeResources/values-ko/strings.xml (100%)
rename core/{strings => resources}/src/commonMain/composeResources/values-lt/strings.xml (100%)
rename core/{strings => resources}/src/commonMain/composeResources/values-nl/strings.xml (100%)
rename core/{strings => resources}/src/commonMain/composeResources/values-no/strings.xml (100%)
rename core/{strings => resources}/src/commonMain/composeResources/values-pl/strings.xml (100%)
rename core/{strings => resources}/src/commonMain/composeResources/values-pt-rBR/strings.xml (100%)
rename core/{strings => resources}/src/commonMain/composeResources/values-pt/strings.xml (100%)
rename core/{strings => resources}/src/commonMain/composeResources/values-ro/strings.xml (100%)
rename core/{strings => resources}/src/commonMain/composeResources/values-ru/strings.xml (100%)
rename core/{strings => resources}/src/commonMain/composeResources/values-sk/strings.xml (100%)
rename core/{strings => resources}/src/commonMain/composeResources/values-sl/strings.xml (100%)
rename core/{strings => resources}/src/commonMain/composeResources/values-sq/strings.xml (100%)
rename core/{strings => resources}/src/commonMain/composeResources/values-sr/strings.xml (100%)
rename core/{strings => resources}/src/commonMain/composeResources/values-srp/strings.xml (100%)
rename core/{strings => resources}/src/commonMain/composeResources/values-sv/strings.xml (100%)
rename core/{strings => resources}/src/commonMain/composeResources/values-tr/strings.xml (100%)
rename core/{strings => resources}/src/commonMain/composeResources/values-uk/strings.xml (100%)
rename core/{strings => resources}/src/commonMain/composeResources/values-zh-rCN/strings.xml (100%)
rename core/{strings => resources}/src/commonMain/composeResources/values-zh-rTW/strings.xml (100%)
rename core/{strings => resources}/src/commonMain/composeResources/values/strings.xml (100%)
create mode 100644 core/resources/src/commonMain/kotlin/org/meshtastic/core/resources/UiText.kt
create mode 100644 feature/map/src/fdroid/res/drawable/ic_location_on.xml
create mode 100644 feature/map/src/fdroid/res/drawable/ic_map_location_dot.xml
create mode 100644 feature/map/src/fdroid/res/drawable/ic_map_navigation.xml
create mode 100644 feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/domain/usecase/SendMessageUseCase.kt
create mode 100644 feature/messaging/src/test/kotlin/org/meshtastic/feature/messaging/domain/usecase/SendMessageUseCaseTest.kt
create mode 100644 feature/node/src/main/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCase.kt
create mode 100644 feature/node/src/main/kotlin/org/meshtastic/feature/node/domain/usecase/GetNodeDetailsUseCase.kt
create mode 100644 feature/node/src/test/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCaseTest.kt
delete mode 100644 feature/settings/src/main/kotlin/org/meshtastic/feature/settings/util/UiText.kt
diff --git a/AGENTS.md b/AGENTS.md
index ae128ca37..69027f403 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 b47041ceb..d64fe9976 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 6dd8c1ca7..d61f3a418 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 7503e1f7b..1743e37bc 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 ef8225838..3b5dffc1e 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 000000000..a6759dae6
--- /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 87e051932..52ef78ce5 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 0a8e50f34..d9fded5b4 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 4512b7a7d..f74668425 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 bc880d411..36338d493 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 37694ada0..3574bf6e1 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 34e1adf4d..db1a6066f 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 62fe766e1..0a37174ee 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 d03c3042a..0ca3e3947 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 170926b7c..bc3b82a6d 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 6b2873d5b..7f9c74d59 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 f694a3bf8..131eb33e8 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