From 875cf1cff2456584f856b4efd8cdff8d19956c2b Mon Sep 17 00:00:00 2001
From: James Rich <2199651+jamesarich@users.noreply.github.com>
Date: Mon, 9 Mar 2026 20:19:46 -0500
Subject: [PATCH] refactor: migrate from Hilt to Koin and expand KMP common
modules (#4746)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
---
AGENTS.md | 8 +-
GEMINI.md | 75 +++++
README.md | 2 +-
app/README.md | 4 +-
app/build.gradle.kts | 13 +-
app/detekt-baseline.xml | 10 +-
.../filter/MessageFilterIntegrationTest.kt | 21 +-
.../app/analytics/FdroidPlatformAnalytics.kt | 5 +-
.../meshtastic/app/di/FDroidNetworkModule.kt | 14 +-
.../org/meshtastic/app/di/FlavorModule.kt | 22 ++
.../app/map/FdroidMapViewProvider.kt | 6 +-
.../kotlin/org/meshtastic/app/map/MapView.kt | 4 +-
.../org/meshtastic/app/map/MapViewModel.kt | 9 +-
.../app/map/model/NOAAWmsTileSource.kt | 16 --
.../app}/node/component/InlineMap.kt | 4 +-
.../metrics/TracerouteMapOverlayInsets.kt | 16 +-
.../app/analytics/GooglePlatformAnalytics.kt | 12 +-
.../org/meshtastic/app/di/FlavorModule.kt | 23 ++
.../meshtastic/app/di/GoogleNetworkModule.kt | 30 +-
.../app/di/GooglePlatformAnalyticsModule.kt | 34 ---
.../app/map/GoogleMapViewProvider.kt | 6 +-
.../kotlin/org/meshtastic/app/map/MapView.kt | 4 +-
.../org/meshtastic/app/map/MapViewModel.kt | 9 +-
.../app/map/prefs/di/GoogleMapsKoinModule.kt | 44 +++
.../app/map/prefs/di/GoogleMapsModule.kt | 68 -----
.../app/map/prefs/map/GoogleMapsPrefs.kt | 13 +-
.../CustomTileProviderRepository.kt | 9 +-
.../app}/node/component/InlineMap.kt | 4 +-
.../metrics/TracerouteMapOverlayInsets.kt | 28 ++
.../org/meshtastic/app/ApplicationModule.kt | 78 -----
.../kotlin/org/meshtastic/app/MainActivity.kt | 26 +-
.../org/meshtastic/app/MainKoinModule.kt | 11 +-
.../org/meshtastic/app/MeshServiceClient.kt | 12 +-
.../org/meshtastic/app/MeshUtilApplication.kt | 61 ++--
.../org/meshtastic/app/di/AppKoinModule.kt | 111 ++++++++
.../kotlin/org/meshtastic/app/di/BleModule.kt | 75 -----
.../org/meshtastic/app/di/DataSourceModule.kt | 47 ---
.../org/meshtastic/app/di/NetworkModule.kt | 93 +++---
.../meshtastic/app/di/NodeDataSourceModule.kt | 37 ---
.../org/meshtastic/app/di/PrefsModule.kt | 269 ------------------
.../org/meshtastic/app/di/RepositoryModule.kt | 163 -----------
.../org/meshtastic/app/di/ServiceModule.kt | 38 ---
.../usecase/GetDiscoveredDevicesUseCase.kt | 11 +-
.../AndroidFirmwareUpdateViewModel.kt | 53 ++++
.../app/intro/AndroidIntroViewModel.kt | 7 +-
.../app/map/AndroidSharedMapViewModel.kt | 9 +-
.../app/map/node/NodeMapViewModel.kt | 9 +-
.../app/messaging/AndroidContactsViewModel.kt | 9 +-
.../app/messaging/AndroidMessageViewModel.kt | 9 +-
.../messaging/AndroidQuickChatViewModel.kt | 7 +-
.../domain/worker/SendMessageWorker.kt | 14 +-
.../domain/worker/WorkManagerMessageQueue.kt | 7 +-
.../org/meshtastic/app/model/UIViewModel.kt | 9 +-
.../app/navigation/ChannelsNavigation.kt | 5 +-
.../app/navigation/ConnectionsNavigation.kt | 5 +-
.../app/navigation/ContactsNavigation.kt | 18 +-
.../app/navigation/FirmwareNavigation.kt | 9 +-
.../app/navigation/MapNavigation.kt | 4 +-
.../app/navigation/NodesNavigation.kt | 13 +-
.../app/navigation/SettingsNavigation.kt | 41 ++-
.../AndroidCompassViewModel.kt} | 32 +--
.../app/node/AndroidMetricsViewModel.kt | 115 ++++++++
.../app/node/AndroidNodeDetailViewModel.kt | 40 +++
.../app/node/AndroidNodeListViewModel.kt | 49 ++++
.../repository/network/NetworkRepository.kt | 21 +-
.../network/NetworkRepositoryModule.kt | 40 ---
.../radio/AndroidRadioInterfaceService.kt | 59 ++--
.../app/repository/radio/InterfaceFactory.kt | 30 +-
.../app/repository/radio/InterfaceSpec.kt | 4 +-
.../app/repository/radio/MockInterface.kt | 9 +-
.../repository/radio/MockInterfaceFactory.kt | 9 +-
.../app/repository/radio/MockInterfaceSpec.kt | 9 +-
.../app/repository/radio/NopInterface.kt | 5 +-
.../repository/radio/NopInterfaceFactory.kt | 8 +-
.../app/repository/radio/NopInterfaceSpec.kt | 8 +-
.../repository/radio/NordicBleInterface.kt | 8 +-
.../radio/NordicBleInterfaceFactory.kt | 23 +-
.../radio/NordicBleInterfaceSpec.kt | 11 +-
.../repository/radio/RadioRepositoryModule.kt | 48 ----
.../app/repository/radio/SerialInterface.kt | 17 +-
.../radio/SerialInterfaceFactory.kt | 11 +-
.../repository/radio/SerialInterfaceSpec.kt | 17 +-
.../app/repository/radio/TCPInterface.kt | 8 +-
.../repository/radio/TCPInterfaceFactory.kt | 10 +-
.../app/repository/radio/TCPInterfaceSpec.kt | 9 +-
.../app/repository/usb/ProbeTableProvider.kt | 10 +-
.../repository/usb/SerialConnectionImpl.kt | 4 +-
.../repository/usb/UsbBroadcastReceiver.kt | 5 +-
.../app/repository/usb/UsbRepository.kt | 27 +-
.../app/repository/usb/UsbRepositoryModule.kt | 41 ---
.../app/service/AndroidAppWidgetUpdater.kt | 8 +-
.../app/service/AndroidMeshLocationManager.kt | 13 +-
.../app/service/AndroidMeshWorkerManager.kt | 7 +-
.../app/service/MarkAsReadReceiver.kt | 13 +-
.../org/meshtastic/app/service/MeshService.kt | 29 +-
.../service/MeshServiceNotificationsImpl.kt | 30 +-
.../app/service/ReactionReceiver.kt | 11 +-
.../meshtastic/app/service/ReplyReceiver.kt | 14 +-
.../app/service/ServiceBroadcasts.kt | 14 +-
.../AndroidCleanNodeDatabaseViewModel.kt | 28 ++
.../app/settings/AndroidDebugViewModel.kt | 38 +++
.../AndroidFilterSettingsViewModel.kt | 26 ++
.../settings/AndroidRadioConfigViewModel.kt | 164 +++++++++++
.../app/settings/AndroidSettingsViewModel.kt | 103 +++++++
.../main/kotlin/org/meshtastic/app/ui/Main.kt | 9 +-
.../app/ui/connections/ConnectionsScreen.kt | 8 +-
.../ui/connections/ConnectionsViewModel.kt | 9 +-
.../app/ui/connections/ScannerViewModel.kt | 9 +-
.../ui/connections/components/BLEDevices.kt | 40 +--
.../connections/components/DeviceListItem.kt | 23 +-
.../app/ui/node/AdaptiveNodeListScreen.kt | 24 ++
.../org/meshtastic/app/ui/sharing/Channel.kt | 6 +-
.../app/ui/sharing/ChannelViewModel.kt | 9 +-
.../meshtastic/app/widget/LocalStatsWidget.kt | 23 +-
.../app/widget/LocalStatsWidgetReceiver.kt | 2 -
.../app/widget/LocalStatsWidgetState.kt | 9 +-
.../app/widget/RefreshLocalStatsAction.kt | 24 +-
.../app/worker/MeshLogCleanupWorker.kt | 40 +--
.../app/worker/ServiceKeepAliveWorker.kt | 14 +-
.../src/main/res/xml/locales_config.xml | 0
.../org/meshtastic/app/MeshTestApplication.kt | 52 ----
.../meshtastic/app/di/KoinVerificationTest.kt | 56 ++++
build-logic/convention/build.gradle.kts | 8 +-
...ntionPlugin.kt => KoinConventionPlugin.kt} | 34 ++-
.../kotlin/org/meshtastic/buildlogic/Dokka.kt | 2 +-
.../kotlin/org/meshtastic/buildlogic/Kover.kt | 4 +-
build.gradle.kts | 2 +-
core/ble/README.md | 2 +-
core/ble/build.gradle.kts | 6 +-
.../core/ble/AndroidBleConnectionFactory.kt | 8 +-
.../meshtastic/core/ble/AndroidBleScanner.kt | 5 +-
.../core/ble/AndroidBluetoothRepository.kt | 13 +-
.../core/ble/di/CoreBleAndroidModule.kt | 49 ++++
.../meshtastic/core/ble/di/CoreBleModule.kt | 24 ++
core/common/build.gradle.kts | 1 +
.../core/common/di/CoreCommonModule.kt | 24 ++
.../core/common/util/SequentialJob.kt | 5 +-
core/data/build.gradle.kts | 6 +-
core/data/detekt-baseline.xml | 4 +-
.../BootloaderOtaQuirksJsonDataSourceImpl.kt | 6 +-
.../DeviceHardwareJsonDataSourceImpl.kt | 6 +-
.../FirmwareReleaseJsonDataSourceImpl.kt | 6 +-
.../core/data/di/CoreDataAndroidModule.kt | 24 ++
.../data/repository/LocationRepositoryImpl.kt | 13 +-
.../DeviceHardwareLocalDataSource.kt | 7 +-
.../FirmwareReleaseLocalDataSource.kt | 7 +-
.../SwitchingNodeInfoReadDataSource.kt | 8 +-
.../SwitchingNodeInfoWriteDataSource.kt | 9 +-
.../meshtastic/core/data/di/CoreDataModule.kt | 29 ++
.../core/data/manager/CommandSenderImpl.kt | 9 +-
.../manager/FromRadioPacketHandlerImpl.kt | 26 +-
.../core/data/manager/HistoryManagerImpl.kt | 12 +-
.../data/manager/MeshActionHandlerImpl.kt | 18 +-
.../data/manager/MeshConfigFlowManagerImpl.kt | 18 +-
.../data/manager/MeshConfigHandlerImpl.kt | 9 +-
.../data/manager/MeshConnectionManagerImpl.kt | 9 +-
.../core/data/manager/MeshDataHandlerImpl.kt | 72 +++--
.../data/manager/MeshMessageProcessorImpl.kt | 14 +-
.../core/data/manager/MeshRouterImpl.kt | 24 +-
.../core/data/manager/MessageFilterImpl.kt | 7 +-
.../core/data/manager/MqttManagerImpl.kt | 9 +-
.../data/manager/NeighborInfoHandlerImpl.kt | 9 +-
.../core/data/manager/NodeManagerImpl.kt | 10 +-
.../core/data/manager/PacketHandlerImpl.kt | 16 +-
.../data/manager/TracerouteHandlerImpl.kt | 9 +-
.../DeviceHardwareRepositoryImpl.kt | 9 +-
.../repository/FirmwareReleaseRepository.kt | 9 +-
.../data/repository/MeshLogRepositoryImpl.kt | 9 +-
.../data/repository/NodeRepositoryImpl.kt | 13 +-
.../data/repository/PacketRepositoryImpl.kt | 11 +-
.../repository/QuickChatActionRepository.kt | 10 +-
.../repository/RadioConfigRepositoryImpl.kt | 7 +-
.../TracerouteSnapshotRepository.kt | 7 +-
.../manager/FromRadioPacketHandlerImplTest.kt | 8 +-
.../core/data/manager/MeshDataHandlerTest.kt | 9 +-
.../data/manager/PacketHandlerImplTest.kt | 4 +-
core/database/build.gradle.kts | 1 +
.../core/database/DatabaseManager.kt | 13 +-
.../database/di/CoreDatabaseAndroidModule.kt | 24 ++
.../core/database/di/CoreDatabaseModule.kt | 24 ++
core/datastore/build.gradle.kts | 6 +-
.../di/CoreDatastoreAndroidModule.kt | 107 +++----
.../datastore/BootloaderWarningDataSource.kt | 8 +-
.../core/datastore/ChannelSetDataSource.kt | 8 +-
.../core/datastore/LocalConfigDataSource.kt | 8 +-
.../core/datastore/LocalStatsDataSource.kt | 8 +-
.../core/datastore/ModuleConfigDataSource.kt | 10 +-
.../datastore/RecentAddressesDataSource.kt | 8 +-
.../core/datastore/UiPreferencesDataSource.kt | 8 +-
.../core/datastore/di/CoreDatastoreModule.kt | 33 +++
core/di/README.md | 4 +-
core/di/build.gradle.kts | 5 +-
.../org/meshtastic/core/di/di/CoreDiModule.kt | 24 +-
core/domain/build.gradle.kts | 3 +-
.../core/domain/di/CoreDomainModule.kt | 24 ++
.../usecase/settings/AdminActionsUseCase.kt | 4 +-
.../settings/CleanNodeDatabaseUseCase.kt | 4 +-
.../usecase/settings/ExportDataUseCase.kt | 4 +-
.../usecase/settings/ExportProfileUseCase.kt | 5 +-
.../settings/ExportSecurityConfigUseCase.kt | 5 +-
.../usecase/settings/ImportProfileUseCase.kt | 5 +-
.../usecase/settings/InstallProfileUseCase.kt | 5 +-
.../usecase/settings/IsOtaCapableUseCase.kt | 4 +-
.../usecase/settings/MeshLocationUseCase.kt | 5 +-
.../settings/ProcessRadioResponseUseCase.kt | 5 +-
.../usecase/settings/RadioConfigUseCase.kt | 5 +-
.../settings/SetAppIntroCompletedUseCase.kt | 9 +-
.../settings/SetDatabaseCacheLimitUseCase.kt | 5 +-
.../settings/SetMeshLogSettingsUseCase.kt | 4 +-
.../settings/SetProvideLocationUseCase.kt | 5 +-
.../usecase/settings/SetThemeUseCase.kt | 5 +-
.../settings/ToggleAnalyticsUseCase.kt | 5 +-
.../ToggleHomoglyphEncodingUseCase.kt | 5 +-
core/navigation/build.gradle.kts | 26 +-
.../org/meshtastic/core/navigation/Routes.kt | 0
core/network/build.gradle.kts | 7 +-
.../network/di/CoreNetworkAndroidModule.kt | 30 +-
.../network/repository/MQTTRepositoryImpl.kt | 6 +-
.../network/DeviceHardwareRemoteDataSource.kt | 7 +-
.../FirmwareReleaseRemoteDataSource.kt | 7 +-
.../core/network/di/CoreNetworkModule.kt | 23 +-
.../core/network/service/ApiService.kt | 5 +-
core/prefs/build.gradle.kts | 5 +-
.../core/prefs/di/CorePrefsAndroidModule.kt | 132 +++++++++
.../prefs/analytics/AnalyticsPrefsImpl.kt | 16 +-
.../core/prefs/di/CorePrefsModule.kt | 24 ++
.../core/prefs/emoji/CustomEmojiPrefsImpl.kt | 13 +-
.../core/prefs/filter/FilterPrefsImpl.kt | 13 +-
.../prefs/homoglyph/HomoglyphPrefsImpl.kt | 13 +-
.../core/prefs/map/MapConsentPrefsImpl.kt | 13 +-
.../meshtastic/core/prefs/map/MapPrefsImpl.kt | 13 +-
.../prefs/map/MapTileProviderPrefsImpl.kt | 13 +-
.../core/prefs/mesh/MeshPrefsImpl.kt | 13 +-
.../core/prefs/meshlog/MeshLogPrefsImpl.kt | 13 +-
.../core/prefs/radio/RadioPrefsImpl.kt | 13 +-
.../meshtastic/core/prefs/ui/UiPrefsImpl.kt | 13 +-
core/repository/build.gradle.kts | 5 +-
.../repository/di/CoreRepositoryModule.kt | 20 +-
core/service/build.gradle.kts | 11 +-
.../service/AndroidRadioControllerImpl.kt | 12 +-
.../core/service/AndroidServiceRepository.kt | 7 +-
.../service/di/CoreServiceAndroidModule.kt | 24 ++
.../core/service/di/CoreServiceModule.kt | 24 ++
core/ui/build.gradle.kts | 3 +-
core/ui/detekt-baseline.xml | 1 +
.../org/meshtastic/core/ui/di/CoreUiModule.kt | 24 ++
.../meshtastic/core/ui/emoji/EmojiPicker.kt | 4 +-
.../core/ui/emoji/EmojiPickerViewModel.kt | 7 +-
.../core/ui/qr/ScannedQrCodeDialog.kt | 4 +-
.../core/ui/qr/ScannedQrCodeViewModel.kt | 9 +-
.../core/ui/share/SharedContactDialog.kt | 4 +-
.../core/ui/share/SharedContactViewModel.kt | 13 +-
.../meshtastic/core/ui/util/AlertManager.kt | 7 +-
.../core/ui/util/LocalInlineMapProvider.kt | 24 ++
...LocalTracerouteMapOverlayInsetsProvider.kt | 18 +-
feature/firmware/build.gradle.kts | 107 ++++---
.../{main => androidMain}/AndroidManifest.xml | 0
.../firmware/AndroidFirmwareFileHandler.kt} | 98 ++++---
.../firmware/AndroidFirmwareUpdateManager.kt} | 22 +-
.../firmware/AndroidFirmwareUsbManager.kt} | 10 +-
.../feature/firmware/FirmwareDfuService.kt | 10 +-
.../feature/firmware/FirmwareRetriever.kt | 16 +-
.../feature/firmware/FirmwareUpdateScreen.kt | 37 +--
.../feature/firmware/NordicDfuHandler.kt | 57 +---
.../feature/firmware/UsbUpdateHandler.kt | 16 +-
.../feature/firmware/ota/BleOtaTransport.kt | 0
.../firmware/ota/Esp32OtaUpdateHandler.kt | 99 ++++---
.../feature/firmware/ota/FirmwareHashUtil.kt | 0
.../firmware/ota/UnifiedOtaProtocol.kt | 0
.../feature/firmware/ota/WifiOtaTransport.kt | 0
.../feature/firmware/FirmwareRetrieverTest.kt | 0
.../firmware/ota/BleOtaTransportErrorTest.kt | 0
.../firmware/ota/BleOtaTransportMtuTest.kt | 0
.../ota/BleOtaTransportNordicMockTest.kt | 0
.../BleOtaTransportServiceDiscoveryTest.kt | 0
.../firmware/ota/BleOtaTransportTest.kt | 0
.../firmware/ota/Esp32OtaUpdateHandlerTest.kt | 0
.../firmware/ota/UnifiedOtaProtocolTest.kt | 0
.../feature/firmware/DfuInternalState.kt | 50 ++++
.../feature/firmware/FirmwareFileHandler.kt | 50 ++++
.../feature/firmware/FirmwareUpdateActions.kt | 0
.../feature/firmware/FirmwareUpdateHandler.kt | 9 +-
.../feature/firmware/FirmwareUpdateManager.kt | 33 +++
.../feature/firmware/FirmwareUpdateState.kt | 5 +-
.../firmware/FirmwareUpdateViewModel.kt | 32 +--
.../feature/firmware/FirmwareUsbManager.kt | 23 ++
.../firmware/di/FeatureFirmwareModule.kt | 24 ++
feature/intro/build.gradle.kts | 11 +-
.../feature/intro/di/FeatureIntroModule.kt | 24 ++
feature/map/build.gradle.kts | 11 +-
.../feature/map/SharedMapViewModel.kt | 7 +-
.../feature/map/di/FeatureMapModule.kt | 24 ++
feature/messaging/build.gradle.kts | 12 +-
.../messaging/di/FeatureMessagingModule.kt | 24 ++
feature/node/build.gradle.kts | 122 ++++----
feature/node/detekt-baseline.xml | 6 +-
.../compass/AndroidCompassHeadingProvider.kt} | 23 +-
.../compass/AndroidMagneticFieldProvider.kt | 18 +-
.../compass/AndroidPhoneLocationProvider.kt} | 31 +-
.../node/component/AdministrationSection.kt | 0
.../feature/node/component/ChannelInfo.kt | 0
.../node/component/CompassBottomSheet.kt | 0
.../component/CooldownOutlinedIconButton.kt | 0
.../feature/node/component/DeviceActions.kt | 0
.../node/component/DeviceDetailsSection.kt | 0
.../feature/node/component/DistanceInfo.kt | 0
.../feature/node/component/ElevationInfo.kt | 0
.../node/component/EnvironmentMetrics.kt | 0
.../component/FirmwareReleaseSheetContent.kt | 0
.../feature/node/component/HopsInfo.kt | 0
.../feature/node/component/IconInfo.kt | 0
.../feature/node/component/InfoCard.kt | 0
.../feature/node/component/InfoCardPreview.kt | 3 +-
.../feature/node/component/LastHeardInfo.kt | 1 -
.../node/component/LinkedCoordinatesItem.kt | 0
.../node/component/NodeDetailComponents.kt | 0
.../node/component/NodeDetailsSection.kt | 0
.../node/component/NodeFilterTextField.kt | 0
.../feature/node/component/NodeItem.kt | 0
.../feature/node/component/NodeStatusIcons.kt | 0
.../feature/node/component/NotesSection.kt | 0
.../feature/node/component/PositionSection.kt | 6 +-
.../feature/node/component/PowerMetrics.kt | 0
.../node/component/SatelliteCountInfo.kt | 0
.../component/TelemetricActionsSection.kt | 0
.../feature/node/component/TelemetryInfo.kt | 0
.../feature/node/detail/NodeDetailActions.kt | 6 +-
.../feature/node/detail/NodeDetailScreen.kt | 27 +-
.../feature/node/list/NodeListScreen.kt | 7 +-
.../feature/node/metrics/BaseMetricChart.kt | 0
.../feature/node/metrics/ChartStyling.kt | 0
.../feature/node/metrics/CommonCharts.kt | 0
.../feature/node/metrics/DeviceMetrics.kt | 3 +-
.../feature/node/metrics/EnvironmentCharts.kt | 0
.../node/metrics/EnvironmentMetrics.kt | 3 +-
.../node/metrics/HardwareModelExtensions.kt | 0
.../feature/node/metrics/HostMetricsLog.kt | 3 +-
.../feature/node/metrics/NeighborInfoLog.kt | 7 +-
.../feature/node/metrics/PaxMetrics.kt | 3 +-
.../feature/node/metrics/PositionLog.kt | 3 +-
.../feature/node/metrics/PowerMetrics.kt | 3 +-
.../feature/node/metrics/SignalMetrics.kt | 3 +-
.../feature/node/metrics/TimeFrameSelector.kt | 0
.../feature/node/metrics/TracerouteLog.kt | 3 +-
.../node/metrics/TracerouteMapScreen.kt | 11 +-
.../feature/node/model/MetricInfo.kt | 0
.../feature/node/model/NodeDetailAction.kt | 0
.../node/compass/CompassHeadingProvider.kt | 19 +-
.../feature/node/compass/CompassUiState.kt | 0
.../feature/node/compass/CompassViewModel.kt | 18 +-
.../node/compass/MagneticFieldProvider.kt | 19 +-
.../node/compass/PhoneLocationProvider.kt | 34 +++
.../feature/node/component/NodeMenuAction.kt | 0
.../node/detail/NodeDetailViewModel.kt | 22 +-
.../node/detail/NodeManagementActions.kt | 12 +-
.../feature/node/detail/NodeRequestActions.kt | 7 +-
.../feature/node/di/FeatureNodeModule.kt | 24 ++
.../domain/usecase/GetFilteredNodesUseCase.kt | 5 +-
.../domain/usecase/GetNodeDetailsUseCase.kt | 4 +-
.../node/list/NodeFilterPreferences.kt | 5 +-
.../feature/node/list/NodeListViewModel.kt | 12 +-
.../node/metrics/EnvironmentMetricsState.kt | 0
.../feature/node/metrics/MetricsViewModel.kt | 86 ++----
.../node/model/IsEffectivelyUnmessageable.kt | 0
.../meshtastic/feature/node/model/LogsType.kt | 0
.../feature/node/model/MetricsState.kt | 0
.../feature/node/model/TimeFrame.kt | 0
feature/settings/build.gradle.kts | 121 +++++---
feature/settings/detekt-baseline.xml | 34 +--
.../feature/settings/AboutScreen.kt | 0
.../feature/settings/AdministrationScreen.kt | 3 +-
.../settings/DeviceConfigurationScreen.kt | 7 +-
.../settings/ModuleConfigurationScreen.kt | 5 +-
.../feature/settings/SettingsScreen.kt | 0
.../settings/component/AppInfoSection.kt | 0
.../settings/component/AppearanceSection.kt | 0
.../settings/component/HomoglyphSetting.kt | 0
.../settings/component/PersistenceSection.kt | 0
.../settings/component/PrivacySection.kt | 0
.../feature/settings/debugging/Debug.kt | 6 +-
.../feature/settings/debugging/DebugSearch.kt | 5 +-
.../settings/filter/FilterSettingsScreen.kt | 3 +-
.../settings/navigation/SettingsNavUtils.kt | 3 +-
.../settings/radio/CleanNodeDatabaseScreen.kt | 3 +-
.../radio/channel/ChannelConfigScreen.kt | 0
.../radio/channel/component/ChannelCard.kt | 0
.../channel/component/ChannelConfigHeader.kt | 0
.../radio/channel/component/ChannelLegend.kt | 0
.../channel/component/EditChannelDialog.kt | 0
.../AmbientLightingConfigItemList.kt | 3 +-
.../radio/component/AudioConfigItemList.kt | 3 +-
.../component/BluetoothConfigItemList.kt | 3 +-
.../component/CannedMessageConfigItemList.kt | 3 +-
.../settings/radio/component/ConfigState.kt | 0
.../DetectionSensorConfigItemList.kt | 3 +-
.../radio/component/DeviceConfigItemList.kt | 3 +-
.../radio/component/DisplayConfigItemList.kt | 3 +-
.../component/EditDeviceProfileDialog.kt | 0
.../ExternalNotificationConfigItemList.kt | 3 +-
.../radio/component/LoRaConfigItemList.kt | 0
.../radio/component/LoadingOverlay.kt | 0
.../radio/component/MQTTConfigItemList.kt | 3 +-
.../radio/component/MapReportingPreference.kt | 0
.../component/NeighborInfoConfigItemList.kt | 3 +-
.../radio/component/NetworkConfigItemList.kt | 3 +-
.../radio/component/NodeActionButton.kt | 3 +-
.../component/PacketResponseStateDialog.kt | 0
.../component/PaxcounterConfigItemList.kt | 3 +-
.../radio/component/PositionConfigItemList.kt | 7 +-
.../radio/component/PowerConfigItemList.kt | 3 +-
.../radio/component/RadioConfigScreenList.kt | 0
.../component/RangeTestConfigItemList.kt | 3 +-
.../component/RemoteHardwareConfigItemList.kt | 3 +-
.../radio/component/SecurityConfigItemList.kt | 3 +-
.../radio/component/SerialConfigItemList.kt | 3 +-
.../component/ShutdownConfirmationDialog.kt | 0
.../component/StatusMessageConfigItemList.kt | 3 +-
.../component/StoreForwardConfigItemList.kt | 3 +-
.../radio/component/TAKConfigItemList.kt | 3 +-
.../component/TelemetryConfigItemList.kt | 3 +-
.../TrafficManagementConfigItemList.kt | 3 +-
.../radio/component/UserConfigItemList.kt | 3 +-
.../settings/radio/component/WarningDialog.kt | 0
.../settings/util/FixedUpdateIntervals.kt | 0
.../feature/settings/util/Formatting.kt | 3 +-
.../feature/settings/util/LanguageUtils.kt | 73 +++--
.../settings/util/SettingsIntervals.kt | 3 +-
.../feature/settings/SettingsViewModel.kt | 43 +--
.../settings/component/ExpressiveSection.kt | 0
.../settings/debugging/DebugFilters.kt | 5 -
.../settings/debugging/DebugViewModel.kt | 108 ++++---
.../settings/di/FeatureSettingsModule.kt | 24 ++
.../filter/FilterSettingsViewModel.kt | 11 +-
.../settings/navigation/ConfigRoute.kt | 0
.../settings/navigation/ModuleRoute.kt | 0
.../radio/CleanNodeDatabaseViewModel.kt | 7 +-
.../feature/settings/radio/RadioConfig.kt | 0
.../settings/radio/RadioConfigViewModel.kt | 256 ++++++-----------
.../feature/settings/radio/ResponseState.kt | 0
gradle/libs.versions.toml | 26 +-
440 files changed, 3738 insertions(+), 3508 deletions(-)
create mode 100644 GEMINI.md
create mode 100644 app/src/fdroid/kotlin/org/meshtastic/app/di/FlavorModule.kt
rename {feature/node/src/fdroid/kotlin/org/meshtastic/feature => app/src/fdroid/kotlin/org/meshtastic/app}/node/component/InlineMap.kt (88%)
rename {feature/node/src/fdroid/kotlin/org/meshtastic/feature => app/src/fdroid/kotlin/org/meshtastic/app}/node/metrics/TracerouteMapOverlayInsets.kt (66%)
create mode 100644 app/src/google/kotlin/org/meshtastic/app/di/FlavorModule.kt
delete mode 100644 app/src/google/kotlin/org/meshtastic/app/di/GooglePlatformAnalyticsModule.kt
create mode 100644 app/src/google/kotlin/org/meshtastic/app/map/prefs/di/GoogleMapsKoinModule.kt
delete mode 100644 app/src/google/kotlin/org/meshtastic/app/map/prefs/di/GoogleMapsModule.kt
rename {feature/node/src/google/kotlin/org/meshtastic/feature => app/src/google/kotlin/org/meshtastic/app}/node/component/InlineMap.kt (96%)
create mode 100644 app/src/google/kotlin/org/meshtastic/app/node/metrics/TracerouteMapOverlayInsets.kt
delete mode 100644 app/src/main/kotlin/org/meshtastic/app/ApplicationModule.kt
rename core/di/src/commonMain/kotlin/org/meshtastic/core/di/ProcessLifecycle.kt => app/src/main/kotlin/org/meshtastic/app/MainKoinModule.kt (79%)
create mode 100644 app/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt
delete mode 100644 app/src/main/kotlin/org/meshtastic/app/di/BleModule.kt
delete mode 100644 app/src/main/kotlin/org/meshtastic/app/di/DataSourceModule.kt
delete mode 100644 app/src/main/kotlin/org/meshtastic/app/di/NodeDataSourceModule.kt
delete mode 100644 app/src/main/kotlin/org/meshtastic/app/di/PrefsModule.kt
delete mode 100644 app/src/main/kotlin/org/meshtastic/app/di/RepositoryModule.kt
delete mode 100644 app/src/main/kotlin/org/meshtastic/app/di/ServiceModule.kt
create mode 100644 app/src/main/kotlin/org/meshtastic/app/firmware/AndroidFirmwareUpdateViewModel.kt
rename app/src/main/kotlin/org/meshtastic/app/{di/DataModule.kt => node/AndroidCompassViewModel.kt} (50%)
create mode 100644 app/src/main/kotlin/org/meshtastic/app/node/AndroidMetricsViewModel.kt
create mode 100644 app/src/main/kotlin/org/meshtastic/app/node/AndroidNodeDetailViewModel.kt
create mode 100644 app/src/main/kotlin/org/meshtastic/app/node/AndroidNodeListViewModel.kt
delete mode 100644 app/src/main/kotlin/org/meshtastic/app/repository/network/NetworkRepositoryModule.kt
delete mode 100644 app/src/main/kotlin/org/meshtastic/app/repository/radio/RadioRepositoryModule.kt
delete mode 100644 app/src/main/kotlin/org/meshtastic/app/repository/usb/UsbRepositoryModule.kt
create mode 100644 app/src/main/kotlin/org/meshtastic/app/settings/AndroidCleanNodeDatabaseViewModel.kt
create mode 100644 app/src/main/kotlin/org/meshtastic/app/settings/AndroidDebugViewModel.kt
create mode 100644 app/src/main/kotlin/org/meshtastic/app/settings/AndroidFilterSettingsViewModel.kt
create mode 100644 app/src/main/kotlin/org/meshtastic/app/settings/AndroidRadioConfigViewModel.kt
create mode 100644 app/src/main/kotlin/org/meshtastic/app/settings/AndroidSettingsViewModel.kt
rename {feature/settings => app}/src/main/res/xml/locales_config.xml (100%)
delete mode 100644 app/src/test/kotlin/org/meshtastic/app/MeshTestApplication.kt
create mode 100644 app/src/test/kotlin/org/meshtastic/app/di/KoinVerificationTest.kt
rename build-logic/convention/src/main/kotlin/{HiltConventionPlugin.kt => KoinConventionPlugin.kt} (52%)
create mode 100644 core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/di/CoreBleAndroidModule.kt
create mode 100644 core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/di/CoreBleModule.kt
create mode 100644 core/common/src/commonMain/kotlin/org/meshtastic/core/common/di/CoreCommonModule.kt
create mode 100644 core/data/src/androidMain/kotlin/org/meshtastic/core/data/di/CoreDataAndroidModule.kt
create mode 100644 core/data/src/commonMain/kotlin/org/meshtastic/core/data/di/CoreDataModule.kt
create mode 100644 core/database/src/androidMain/kotlin/org/meshtastic/core/database/di/CoreDatabaseAndroidModule.kt
create mode 100644 core/database/src/commonMain/kotlin/org/meshtastic/core/database/di/CoreDatabaseModule.kt
rename app/src/main/kotlin/org/meshtastic/app/di/DataStoreModule.kt => core/datastore/src/androidMain/kotlin/org/meshtastic/core/datastore/di/CoreDatastoreAndroidModule.kt (69%)
create mode 100644 core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/di/CoreDatastoreModule.kt
rename app/src/main/kotlin/org/meshtastic/app/di/AppModule.kt => core/di/src/commonMain/kotlin/org/meshtastic/core/di/di/CoreDiModule.kt (62%)
create mode 100644 core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/di/CoreDomainModule.kt
rename core/navigation/src/{main => commonMain}/kotlin/org/meshtastic/core/navigation/Routes.kt (100%)
rename app/src/fdroid/kotlin/org/meshtastic/app/di/FdroidPlatformAnalyticsModule.kt => core/network/src/androidMain/kotlin/org/meshtastic/core/network/di/CoreNetworkAndroidModule.kt (51%)
rename app/src/main/kotlin/org/meshtastic/app/messaging/di/MessagingModule.kt => core/network/src/commonMain/kotlin/org/meshtastic/core/network/di/CoreNetworkModule.kt (61%)
create mode 100644 core/prefs/src/androidMain/kotlin/org/meshtastic/core/prefs/di/CorePrefsAndroidModule.kt
create mode 100644 core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/di/CorePrefsModule.kt
rename app/src/main/kotlin/org/meshtastic/app/di/UseCaseModule.kt => core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/di/CoreRepositoryModule.kt (81%)
create mode 100644 core/service/src/androidMain/kotlin/org/meshtastic/core/service/di/CoreServiceAndroidModule.kt
create mode 100644 core/service/src/commonMain/kotlin/org/meshtastic/core/service/di/CoreServiceModule.kt
create mode 100644 core/ui/src/main/kotlin/org/meshtastic/core/ui/di/CoreUiModule.kt
create mode 100644 core/ui/src/main/kotlin/org/meshtastic/core/ui/util/LocalInlineMapProvider.kt
rename feature/node/src/google/kotlin/org/meshtastic/feature/node/metrics/TracerouteMapOverlayInsets.kt => core/ui/src/main/kotlin/org/meshtastic/core/ui/util/LocalTracerouteMapOverlayInsetsProvider.kt (71%)
rename feature/firmware/src/{main => androidMain}/AndroidManifest.xml (100%)
rename feature/firmware/src/{main/kotlin/org/meshtastic/feature/firmware/FirmwareFileHandler.kt => androidMain/kotlin/org/meshtastic/feature/firmware/AndroidFirmwareFileHandler.kt} (72%)
rename feature/firmware/src/{main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateManager.kt => androidMain/kotlin/org/meshtastic/feature/firmware/AndroidFirmwareUpdateManager.kt} (91%)
rename feature/firmware/src/{main/kotlin/org/meshtastic/feature/firmware/UsbManager.kt => androidMain/kotlin/org/meshtastic/feature/firmware/AndroidFirmwareUsbManager.kt} (88%)
rename feature/firmware/src/{main => androidMain}/kotlin/org/meshtastic/feature/firmware/FirmwareDfuService.kt (86%)
rename feature/firmware/src/{main => androidMain}/kotlin/org/meshtastic/feature/firmware/FirmwareRetriever.kt (92%)
rename feature/firmware/src/{main => androidMain}/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.kt (97%)
rename feature/firmware/src/{main => androidMain}/kotlin/org/meshtastic/feature/firmware/NordicDfuHandler.kt (85%)
rename feature/firmware/src/{main => androidMain}/kotlin/org/meshtastic/feature/firmware/UsbUpdateHandler.kt (95%)
rename feature/firmware/src/{main => androidMain}/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransport.kt (100%)
rename feature/firmware/src/{main => androidMain}/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandler.kt (82%)
rename feature/firmware/src/{main => androidMain}/kotlin/org/meshtastic/feature/firmware/ota/FirmwareHashUtil.kt (100%)
rename feature/firmware/src/{main => androidMain}/kotlin/org/meshtastic/feature/firmware/ota/UnifiedOtaProtocol.kt (100%)
rename feature/firmware/src/{main => androidMain}/kotlin/org/meshtastic/feature/firmware/ota/WifiOtaTransport.kt (100%)
rename feature/firmware/src/{test => androidUnitTest}/kotlin/org/meshtastic/feature/firmware/FirmwareRetrieverTest.kt (100%)
rename feature/firmware/src/{test => androidUnitTest}/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportErrorTest.kt (100%)
rename feature/firmware/src/{test => androidUnitTest}/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportMtuTest.kt (100%)
rename feature/firmware/src/{test => androidUnitTest}/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportNordicMockTest.kt (100%)
rename feature/firmware/src/{test => androidUnitTest}/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportServiceDiscoveryTest.kt (100%)
rename feature/firmware/src/{test => androidUnitTest}/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportTest.kt (100%)
rename feature/firmware/src/{test => androidUnitTest}/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandlerTest.kt (100%)
rename feature/firmware/src/{test => androidUnitTest}/kotlin/org/meshtastic/feature/firmware/ota/UnifiedOtaProtocolTest.kt (100%)
create mode 100644 feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/DfuInternalState.kt
create mode 100644 feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareFileHandler.kt
rename feature/firmware/src/{main => commonMain}/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateActions.kt (100%)
rename feature/firmware/src/{main => commonMain}/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateHandler.kt (87%)
create mode 100644 feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateManager.kt
rename feature/firmware/src/{main => commonMain}/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateState.kt (93%)
rename feature/firmware/src/{main => commonMain}/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt (96%)
create mode 100644 feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUsbManager.kt
create mode 100644 feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/di/FeatureFirmwareModule.kt
create mode 100644 feature/intro/src/commonMain/kotlin/org/meshtastic/feature/intro/di/FeatureIntroModule.kt
create mode 100644 feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/di/FeatureMapModule.kt
create mode 100644 feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/di/FeatureMessagingModule.kt
rename feature/node/src/{main/kotlin/org/meshtastic/feature/node/compass/CompassHeadingProvider.kt => androidMain/kotlin/org/meshtastic/feature/node/compass/AndroidCompassHeadingProvider.kt} (86%)
rename app/src/androidTest/kotlin/org/meshtastic/app/TestRunner.kt => feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/compass/AndroidMagneticFieldProvider.kt (59%)
rename feature/node/src/{main/kotlin/org/meshtastic/feature/node/compass/PhoneLocationProvider.kt => androidMain/kotlin/org/meshtastic/feature/node/compass/AndroidPhoneLocationProvider.kt} (84%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt (100%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/component/ChannelInfo.kt (100%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/component/CompassBottomSheet.kt (100%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/component/CooldownOutlinedIconButton.kt (100%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/component/DeviceActions.kt (100%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/component/DeviceDetailsSection.kt (100%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/component/DistanceInfo.kt (100%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/component/ElevationInfo.kt (100%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/component/EnvironmentMetrics.kt (100%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/component/FirmwareReleaseSheetContent.kt (100%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/component/HopsInfo.kt (100%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/component/IconInfo.kt (100%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/component/InfoCard.kt (100%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/component/InfoCardPreview.kt (98%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/component/LastHeardInfo.kt (97%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/component/LinkedCoordinatesItem.kt (100%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/component/NodeDetailComponents.kt (100%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt (100%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/component/NodeFilterTextField.kt (100%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/component/NodeItem.kt (100%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/component/NodeStatusIcons.kt (100%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/component/NotesSection.kt (100%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/component/PositionSection.kt (97%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/component/PowerMetrics.kt (100%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/component/SatelliteCountInfo.kt (100%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/component/TelemetricActionsSection.kt (100%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/component/TelemetryInfo.kt (100%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/detail/NodeDetailActions.kt (97%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/detail/NodeDetailScreen.kt (93%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt (98%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/metrics/BaseMetricChart.kt (100%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/metrics/ChartStyling.kt (100%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/metrics/CommonCharts.kt (100%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/metrics/DeviceMetrics.kt (99%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/metrics/EnvironmentCharts.kt (100%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt (99%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/metrics/HardwareModelExtensions.kt (100%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/metrics/HostMetricsLog.kt (98%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/metrics/NeighborInfoLog.kt (97%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/metrics/PaxMetrics.kt (98%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/metrics/PositionLog.kt (98%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/metrics/PowerMetrics.kt (99%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/metrics/SignalMetrics.kt (98%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/metrics/TimeFrameSelector.kt (100%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.kt (99%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/metrics/TracerouteMapScreen.kt (94%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/model/MetricInfo.kt (100%)
rename feature/node/src/{main => androidMain}/kotlin/org/meshtastic/feature/node/model/NodeDetailAction.kt (100%)
rename app/src/main/kotlin/org/meshtastic/app/repository/radio/InterfaceMapKey.kt => feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/CompassHeadingProvider.kt (63%)
rename feature/node/src/{main => commonMain}/kotlin/org/meshtastic/feature/node/compass/CompassUiState.kt (100%)
rename feature/node/src/{main => commonMain}/kotlin/org/meshtastic/feature/node/compass/CompassViewModel.kt (95%)
rename app/src/main/kotlin/org/meshtastic/app/di/DatabaseModule.kt => feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/MagneticFieldProvider.kt (62%)
create mode 100644 feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/PhoneLocationProvider.kt
rename feature/node/src/{main => commonMain}/kotlin/org/meshtastic/feature/node/component/NodeMenuAction.kt (100%)
rename feature/node/src/{main => commonMain}/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt (88%)
rename feature/node/src/{main => commonMain}/kotlin/org/meshtastic/feature/node/detail/NodeManagementActions.kt (93%)
rename feature/node/src/{main => commonMain}/kotlin/org/meshtastic/feature/node/detail/NodeRequestActions.kt (97%)
create mode 100644 feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/di/FeatureNodeModule.kt
rename feature/node/src/{main => commonMain}/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCase.kt (94%)
rename feature/node/src/{main => commonMain}/kotlin/org/meshtastic/feature/node/domain/usecase/GetNodeDetailsUseCase.kt (99%)
rename feature/node/src/{main => commonMain}/kotlin/org/meshtastic/feature/node/list/NodeFilterPreferences.kt (93%)
rename feature/node/src/{main => commonMain}/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt (97%)
rename feature/node/src/{main => commonMain}/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetricsState.kt (100%)
rename feature/node/src/{main => commonMain}/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt (81%)
rename feature/node/src/{main => commonMain}/kotlin/org/meshtastic/feature/node/model/IsEffectivelyUnmessageable.kt (100%)
rename feature/node/src/{main => commonMain}/kotlin/org/meshtastic/feature/node/model/LogsType.kt (100%)
rename feature/node/src/{main => commonMain}/kotlin/org/meshtastic/feature/node/model/MetricsState.kt (100%)
rename feature/node/src/{main => commonMain}/kotlin/org/meshtastic/feature/node/model/TimeFrame.kt (100%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/AboutScreen.kt (100%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/AdministrationScreen.kt (97%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/DeviceConfigurationScreen.kt (94%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/ModuleConfigurationScreen.kt (95%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/SettingsScreen.kt (100%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/component/AppInfoSection.kt (100%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/component/AppearanceSection.kt (100%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/component/HomoglyphSetting.kt (100%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/component/PersistenceSection.kt (100%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/component/PrivacySection.kt (100%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/debugging/Debug.kt (99%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/debugging/DebugSearch.kt (98%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsScreen.kt (98%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavUtils.kt (95%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseScreen.kt (98%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelConfigScreen.kt (100%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/radio/channel/component/ChannelCard.kt (100%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/radio/channel/component/ChannelConfigHeader.kt (100%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/radio/channel/component/ChannelLegend.kt (100%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/radio/channel/component/EditChannelDialog.kt (100%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/radio/component/AmbientLightingConfigItemList.kt (96%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/radio/component/AudioConfigItemList.kt (97%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/radio/component/BluetoothConfigItemList.kt (96%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/radio/component/CannedMessageConfigItemList.kt (98%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/radio/component/ConfigState.kt (100%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/radio/component/DetectionSensorConfigItemList.kt (97%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigItemList.kt (99%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/radio/component/DisplayConfigItemList.kt (98%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/radio/component/EditDeviceProfileDialog.kt (100%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/radio/component/ExternalNotificationConfigItemList.kt (99%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/radio/component/LoRaConfigItemList.kt (100%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/radio/component/LoadingOverlay.kt (100%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/radio/component/MQTTConfigItemList.kt (98%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/radio/component/MapReportingPreference.kt (100%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/radio/component/NeighborInfoConfigItemList.kt (96%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/radio/component/NetworkConfigItemList.kt (99%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/radio/component/NodeActionButton.kt (98%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/radio/component/PacketResponseStateDialog.kt (100%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/radio/component/PaxcounterConfigItemList.kt (96%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/radio/component/PositionConfigItemList.kt (98%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/radio/component/PowerConfigItemList.kt (98%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/radio/component/RadioConfigScreenList.kt (100%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/radio/component/RangeTestConfigItemList.kt (96%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/radio/component/RemoteHardwareConfigItemList.kt (96%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/radio/component/SecurityConfigItemList.kt (98%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/radio/component/SerialConfigItemList.kt (97%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/radio/component/ShutdownConfirmationDialog.kt (100%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/radio/component/StatusMessageConfigItemList.kt (96%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/radio/component/StoreForwardConfigItemList.kt (97%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/radio/component/TAKConfigItemList.kt (95%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/radio/component/TelemetryConfigItemList.kt (98%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/radio/component/TrafficManagementConfigItemList.kt (99%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/radio/component/UserConfigItemList.kt (97%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/radio/component/WarningDialog.kt (100%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/util/FixedUpdateIntervals.kt (100%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/util/Formatting.kt (96%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/util/LanguageUtils.kt (66%)
rename feature/settings/src/{main => androidMain}/kotlin/org/meshtastic/feature/settings/util/SettingsIntervals.kt (95%)
rename feature/settings/src/{main => commonMain}/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt (79%)
rename feature/settings/src/{main => commonMain}/kotlin/org/meshtastic/feature/settings/component/ExpressiveSection.kt (100%)
rename feature/settings/src/{main => commonMain}/kotlin/org/meshtastic/feature/settings/debugging/DebugFilters.kt (99%)
rename feature/settings/src/{main => commonMain}/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt (86%)
create mode 100644 feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/di/FeatureSettingsModule.kt
rename feature/settings/src/{main => commonMain}/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModel.kt (89%)
rename feature/settings/src/{main => commonMain}/kotlin/org/meshtastic/feature/settings/navigation/ConfigRoute.kt (100%)
rename feature/settings/src/{main => commonMain}/kotlin/org/meshtastic/feature/settings/navigation/ModuleRoute.kt (100%)
rename feature/settings/src/{main => commonMain}/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseViewModel.kt (96%)
rename feature/settings/src/{main => commonMain}/kotlin/org/meshtastic/feature/settings/radio/RadioConfig.kt (100%)
rename feature/settings/src/{main => commonMain}/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt (78%)
rename feature/settings/src/{main => commonMain}/kotlin/org/meshtastic/feature/settings/radio/ResponseState.kt (100%)
diff --git a/AGENTS.md b/AGENTS.md
index a7ea32e79..d16cc31ab 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -9,7 +9,7 @@ We are incrementally migrating Meshtastic-Android to a **Kotlin Multiplatform (K
| Directory | Description |
| :--- | :--- |
-| `app/` | Main application module. Contains `MainActivity`, Hilt DI modules, and app-level logic. Uses package `org.meshtastic.app`. |
+| `app/` | Main application module. Contains `MainActivity`, Koin DI modules, and app-level logic. Uses package `org.meshtastic.app`. |
| `core/model` | Domain models and common data structures. |
| `core:proto` | Protobuf definitions (Git submodule). |
| `core:common` | Low-level utilities, I/O abstractions (Okio), and common types. |
@@ -39,8 +39,8 @@ We are incrementally migrating Meshtastic-Android to a **Kotlin Multiplatform (K
- **Concurrency:** Use Kotlin Coroutines and Flow.
- **Thread-Safety:** Use `atomicfu` and `kotlinx.collections.immutable` for shared state in `commonMain`. Avoid `synchronized` or JVM-specific atomics.
- **Dependency Injection:**
- - Use **Hilt**.
- - **Restriction:** Move Hilt modules to the `app` module if the library module is KMP with multiple flavors, as KSP/Hilt generation often fails in these complex scenarios.
+ - Use **Koin**.
+ - **Restriction:** Move Koin modules to the `app` module if the library module is KMP with multiple flavors, as KSP/Koin generation often fails in these complex scenarios.
### C. Namespacing
- **Standard:** Use the `org.meshtastic.*` namespace for all code.
@@ -58,4 +58,4 @@ Use `expect`/`actual` sparingly for platform-specific types (e.g., `Location`, `
## 5. Troubleshooting
- **Build Failures:** Always check `gradle/libs.versions.toml` for dependency conflicts.
-- **Hilt Generation:** If `@Inject` fails in a KMP module, ensure the corresponding module is bound in the `app` layer's DI package.
+- **Koin Generation:** If a component fails to inject in a KMP module, ensure the corresponding module is bound in the `app` layer's DI package.
diff --git a/GEMINI.md b/GEMINI.md
new file mode 100644
index 000000000..87b88d43d
--- /dev/null
+++ b/GEMINI.md
@@ -0,0 +1,75 @@
+# Meshtastic-Android: AI Agent Instructions (GEMINI.md)
+
+**CRITICAL AGENT DIRECTIVE:** This file contains validated, comprehensive instructions for interacting with the Meshtastic-Android repository. You MUST adhere strictly to these rules, build commands, and architectural constraints. Only deviate or explore alternatives if the documented commands fail with unexpected errors.
+
+## 1. Project Overview & Architecture
+Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, decentralized mesh networks.
+
+- **Language:** Kotlin (primary), AIDL.
+- **Build System:** Gradle (Kotlin DSL). JDK 17 is REQUIRED.
+- **Target SDK:** API 36. Min SDK: API 26 (Android 8.0).
+- **Flavors:**
+ - `fdroid`: Open source only, no tracking/analytics.
+ - `google`: Includes Google Play Services (Maps) and DataDog analytics.
+- **Core Architecture:** Modern Android Development (MAD) with KMP core.
+ - **KMP Modules:** `core:model`, `core:proto`, `core:common`, `core:resources`, `core:database`, `core:datastore`, `core:repository`, `core:domain`, `core:prefs`, `core:network`, `core:di`, and `core:data`.
+ - **UI:** Jetpack Compose (Material 3).
+ - **DI:** Koin (centralized in `app` module for KMP modules).
+ - **Navigation:** Type-Safe Jetpack Navigation.
+ - **Room KMP:** Always use `factory = { MeshtasticDatabaseConstructor.initialize() }` in `Room.databaseBuilder` and `inMemoryDatabaseBuilder`. DAOs and Entities reside in `commonMain`.
+
+## 2. Environment Setup (Mandatory First Steps)
+Before attempting any builds or tests, ensure the environment is configured:
+
+1. **JDK 17 MUST be used** to prevent Gradle sync/build failures.
+2. **Secrets:** You must copy `secrets.defaults.properties` to `local.properties` to satisfy build requirements, even for dummy builds:
+ ```properties
+ # local.properties example
+ MAPS_API_KEY=dummy_key
+ datadogApplicationId=dummy_id
+ datadogClientToken=dummy_token
+ ```
+
+## 3. Strict Execution Commands
+Always run commands in the following order to ensure reliability. Do not attempt to bypass `clean` if you are facing build issues.
+
+**Formatting & Linting (Run BEFORE committing):**
+```bash
+./gradlew spotlessApply # Always run to auto-fix formatting
+./gradlew detekt # Run static analysis
+```
+
+**Building:**
+```bash
+./gradlew clean # Always start here if facing issues
+./gradlew assembleDebug # Full build (fdroid and google)
+```
+
+**Testing:**
+```bash
+./gradlew testAndroid # Run Android unit tests (Robolectric)
+./gradlew testCommonMain # Run KMP common tests (if applicable)
+./gradlew connectedAndroidTest # Run instrumented tests
+```
+*Note: If testing Compose UI on the JVM (Robolectric) with Java 17, pin your tests to `@Config(sdk = [34])` to avoid SDK 35 compatibility crashes.*
+
+## 4. Coding Standards & Mandates
+
+- **UI Components:** Always utilize `:core:ui` for shared Jetpack Compose components (e.g., `MeshtasticResourceDialog`, `TransportIcon`). Do not reinvent standard dialogs or preference screens.
+- **Strings/Localization:** **NEVER** use hardcoded strings or the legacy `app/src/main/res/values/strings.xml`.
+ - **Rule:** You MUST use the Compose Multiplatform Resource library.
+ - **Location:** `core/resources/src/commonMain/composeResources/values/strings.xml`.
+ - **Usage:** `stringResource(Res.string.your_key)`
+- **Bluetooth/BLE:** Do not use legacy Android Bluetooth callbacks. All BLE communication MUST route through `:core:ble`, utilizing Nordic Semiconductor's Android Common Libraries and Kotlin Coroutines/Flows.
+- **Dependencies:** Never assume a library is available. Check `gradle/libs.versions.toml` first. If adding a new dependency, it MUST be added to the version catalog, not directly to a `build.gradle.kts` file.
+- **Namespacing:** Prefer the `org.meshtastic` namespace for all new code. The legacy `com.geeksville.mesh` ApplicationId is maintained for compatibility.
+
+## 5. Module Map
+When locating code to modify, use this map:
+- **`app/`**: Main application wiring and Koin modules. Package: `org.meshtastic.app`.
+- **`:core:data`**: Core business logic and managers. Package: `org.meshtastic.core.data`.
+- **`:core:repository`**: Domain interfaces and common models. Package: `org.meshtastic.core.repository`.
+- **`:core:ble`**: Coroutine-based Bluetooth logic.
+- **`:core:api`**: AIDL service interface (`IMeshService.aidl`) for third-party integrations (like ATAK).
+- **`:core:ui`**: Shared Compose UI elements and theming.
+- **`:feature:*`**: Isolated feature screens (e.g., `:feature:messaging` for chat, `:feature:map` for mapping).
diff --git a/README.md b/README.md
index cab5bb9b0..c05a4f17e 100644
--- a/README.md
+++ b/README.md
@@ -63,7 +63,7 @@ The app follows modern Android development practices, built on top of a shared K
- **KMP Modules:** Business logic (`core:domain`), data sources (`core:data`, `core:database`, `core:datastore`), and communications (`core:network`, `core:ble`) are entirely platform-agnostic, enabling future support for Desktop and Web.
- **UI:** Jetpack Compose (Material 3) using Compose Multiplatform resources.
- **State Management:** Unidirectional Data Flow (UDF) with ViewModels, Coroutines, and Flow.
-- **Dependency Injection:** Hilt (mapped to KMP `javax.inject` interfaces).
+- **Dependency Injection:** Koin with Koin Annotations (Compiler Plugin).
- **Navigation:** Type-Safe Navigation (Jetpack Navigation).
- **Data Layer:** Repository pattern with Room KMP (local DB), DataStore (prefs), and Protobuf (device comms).
diff --git a/app/README.md b/app/README.md
index 1967019af..b386a45ce 100644
--- a/app/README.md
+++ b/app/README.md
@@ -11,8 +11,8 @@ The single Activity of the application. It hosts the `NavHost` and manages the r
### 2. `MeshService`
The core background service that manages long-running communication with the mesh radio. It runs as a **Foreground Service** to ensure reliable communication even when the app is in the background.
-### 3. Hilt Application
-`MeshUtilApplication` is the Hilt entry point, providing the global dependency injection container.
+### 3. Koin Application
+`MeshUtilApplication` is the Koin entry point, providing the global dependency injection container.
## Architecture
The module primarily serves as a "glue" layer, connecting:
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 0f427214e..8327d293f 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -29,7 +29,7 @@ plugins {
alias(libs.plugins.meshtastic.android.application)
alias(libs.plugins.meshtastic.android.application.flavors)
alias(libs.plugins.meshtastic.android.application.compose)
- alias(libs.plugins.meshtastic.hilt)
+ id("meshtastic.koin")
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.devtools.ksp)
alias(libs.plugins.secrets)
@@ -216,6 +216,7 @@ dependencies {
implementation(projects.core.database)
implementation(projects.core.datastore)
implementation(projects.core.di)
+ implementation(projects.core.domain)
implementation(projects.core.model)
implementation(projects.core.navigation)
implementation(projects.core.network)
@@ -261,9 +262,11 @@ dependencies {
implementation(libs.org.eclipse.paho.client.mqttv3)
implementation(libs.usb.serial.android)
implementation(libs.androidx.work.runtime.ktx)
- implementation(libs.androidx.hilt.work)
- implementation(libs.androidx.hilt.lifecycle.viewmodel.compose)
- ksp(libs.androidx.hilt.compiler)
+ implementation(libs.koin.android)
+ implementation(libs.koin.androidx.compose)
+ implementation(libs.koin.compose.viewmodel)
+ implementation(libs.koin.androidx.workmanager)
+ implementation(libs.koin.annotations)
implementation(libs.accompanist.permissions)
implementation(libs.kermit)
implementation(libs.kotlinx.datetime)
@@ -300,13 +303,13 @@ dependencies {
androidTestImplementation(libs.androidx.test.runner)
androidTestImplementation(libs.androidx.test.ext.junit)
- androidTestImplementation(libs.hilt.android.testing)
androidTestImplementation(libs.kotlinx.coroutines.test)
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
androidTestImplementation(libs.nordic.client.android.mock)
androidTestImplementation(libs.nordic.core.mock)
testImplementation(libs.androidx.work.testing)
+ testImplementation(libs.koin.test)
testImplementation(libs.junit)
testImplementation(libs.mockk)
testImplementation(libs.kotlinx.coroutines.test)
diff --git a/app/detekt-baseline.xml b/app/detekt-baseline.xml
index 0e08e976a..3ff014be2 100644
--- a/app/detekt-baseline.xml
+++ b/app/detekt-baseline.xml
@@ -4,6 +4,13 @@
CyclomaticComplexMethod:SettingsNavigation.kt$@Suppress("LongMethod") fun NavGraphBuilder.settingsGraph(navController: NavHostController)
LongMethod:TCPInterface.kt$TCPInterface$private suspend fun startConnect()
+ LongParameterList:AndroidMetricsViewModel.kt$AndroidMetricsViewModel$( savedStateHandle: SavedStateHandle, private val app: Application, dispatchers: CoroutineDispatchers, meshLogRepository: MeshLogRepository, serviceRepository: ServiceRepository, nodeRepository: NodeRepository, tracerouteSnapshotRepository: TracerouteSnapshotRepository, nodeRequestActions: NodeRequestActions, alertManager: AlertManager, getNodeDetailsUseCase: GetNodeDetailsUseCase, )
+ LongParameterList:AndroidNodeListViewModel.kt$AndroidNodeListViewModel$( savedStateHandle: SavedStateHandle, nodeRepository: NodeRepository, radioConfigRepository: RadioConfigRepository, serviceRepository: ServiceRepository, radioController: RadioController, nodeManagementActions: NodeManagementActions, getFilteredNodesUseCase: GetFilteredNodesUseCase, nodeFilterPreferences: NodeFilterPreferences, )
+ LongParameterList:AndroidRadioConfigViewModel.kt$AndroidRadioConfigViewModel$( savedStateHandle: SavedStateHandle, private val app: Application, radioConfigRepository: RadioConfigRepository, packetRepository: PacketRepository, serviceRepository: ServiceRepository, nodeRepository: NodeRepository, private val locationRepository: LocationRepository, mapConsentPrefs: MapConsentPrefs, analyticsPrefs: AnalyticsPrefs, homoglyphEncodingPrefs: HomoglyphPrefs, toggleAnalyticsUseCase: ToggleAnalyticsUseCase, toggleHomoglyphEncodingUseCase: ToggleHomoglyphEncodingUseCase, importProfileUseCase: ImportProfileUseCase, exportProfileUseCase: ExportProfileUseCase, exportSecurityConfigUseCase: ExportSecurityConfigUseCase, installProfileUseCase: InstallProfileUseCase, radioConfigUseCase: RadioConfigUseCase, adminActionsUseCase: AdminActionsUseCase, processRadioResponseUseCase: ProcessRadioResponseUseCase, )
+ LongParameterList:AndroidSettingsViewModel.kt$AndroidSettingsViewModel$( private val app: Application, radioConfigRepository: RadioConfigRepository, radioController: RadioController, nodeRepository: NodeRepository, uiPrefs: UiPrefs, buildConfigProvider: BuildConfigProvider, databaseManager: DatabaseManager, meshLogPrefs: MeshLogPrefs, setThemeUseCase: SetThemeUseCase, setAppIntroCompletedUseCase: SetAppIntroCompletedUseCase, setProvideLocationUseCase: SetProvideLocationUseCase, setDatabaseCacheLimitUseCase: SetDatabaseCacheLimitUseCase, setMeshLogSettingsUseCase: SetMeshLogSettingsUseCase, meshLocationUseCase: MeshLocationUseCase, exportDataUseCase: ExportDataUseCase, isOtaCapableUseCase: IsOtaCapableUseCase, )
+ MagicNumber:AndroidMetricsViewModel.kt$AndroidMetricsViewModel$1000L
+ MagicNumber:AndroidMetricsViewModel.kt$AndroidMetricsViewModel$1e-5
+ MagicNumber:AndroidMetricsViewModel.kt$AndroidMetricsViewModel$1e-7
MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$21972
MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$32809
MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$6790
@@ -15,10 +22,9 @@
MagicNumber:StreamInterface.kt$StreamInterface$4
MagicNumber:StreamInterface.kt$StreamInterface$8
MagicNumber:TCPInterface.kt$TCPInterface$1000
- MaxLineLength:DataSourceModule.kt$DataSourceModule$fun
- ParameterListWrapping:DataSourceModule.kt$DataSourceModule$(impl: BootloaderOtaQuirksJsonDataSourceImpl)
SwallowedException:NsdManager.kt$ex: IllegalArgumentException
SwallowedException:TCPInterface.kt$TCPInterface$ex: SocketTimeoutException
+ TooGenericExceptionCaught:AndroidRadioConfigViewModel.kt$AndroidRadioConfigViewModel$ex: Exception
TooGenericExceptionCaught:NordicBleInterface.kt$NordicBleInterface$e: Exception
TooGenericExceptionCaught:TCPInterface.kt$TCPInterface$ex: Throwable
TooManyFunctions:NordicBleInterface.kt$NordicBleInterface : IRadioInterface
diff --git a/app/src/androidTest/kotlin/org/meshtastic/app/filter/MessageFilterIntegrationTest.kt b/app/src/androidTest/kotlin/org/meshtastic/app/filter/MessageFilterIntegrationTest.kt
index a4c44e964..f2e806e29 100644
--- a/app/src/androidTest/kotlin/org/meshtastic/app/filter/MessageFilterIntegrationTest.kt
+++ b/app/src/androidTest/kotlin/org/meshtastic/app/filter/MessageFilterIntegrationTest.kt
@@ -17,32 +17,21 @@
package org.meshtastic.app.filter
import androidx.test.ext.junit.runners.AndroidJUnit4
-import dagger.hilt.android.testing.HiltAndroidRule
-import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertTrue
-import org.junit.Before
-import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import org.koin.test.KoinTest
+import org.koin.test.inject
import org.meshtastic.core.repository.FilterPrefs
import org.meshtastic.core.repository.MessageFilter
-import javax.inject.Inject
-@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
-class MessageFilterIntegrationTest {
+class MessageFilterIntegrationTest : KoinTest {
- @get:Rule var hiltRule = HiltAndroidRule(this)
+ private val filterPrefs: FilterPrefs by inject()
- @Inject lateinit var filterPrefs: FilterPrefs
-
- @Inject lateinit var filterService: MessageFilter
-
- @Before
- fun setup() {
- hiltRule.inject()
- }
+ private val filterService: MessageFilter by inject()
@Test
fun filterPrefsIntegration() = runTest {
diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/analytics/FdroidPlatformAnalytics.kt b/app/src/fdroid/kotlin/org/meshtastic/app/analytics/FdroidPlatformAnalytics.kt
index 69d9648d9..7d0daab08 100644
--- a/app/src/fdroid/kotlin/org/meshtastic/app/analytics/FdroidPlatformAnalytics.kt
+++ b/app/src/fdroid/kotlin/org/meshtastic/app/analytics/FdroidPlatformAnalytics.kt
@@ -18,16 +18,17 @@ package org.meshtastic.app.analytics
import co.touchlab.kermit.Logger
import co.touchlab.kermit.Severity
+import org.koin.core.annotation.Single
import org.meshtastic.app.BuildConfig
import org.meshtastic.core.repository.DataPair
import org.meshtastic.core.repository.PlatformAnalytics
-import javax.inject.Inject
/**
* F-Droid specific implementation of [PlatformAnalytics]. This provides no-op implementations for analytics and other
* platform services.
*/
-class FdroidPlatformAnalytics @Inject constructor() : PlatformAnalytics {
+@Single
+class FdroidPlatformAnalytics : PlatformAnalytics {
init {
// For F-Droid builds we don't initialize external analytics services.
// In debug builds we attach a DebugTree for convenient local logging, but
diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/di/FDroidNetworkModule.kt b/app/src/fdroid/kotlin/org/meshtastic/app/di/FDroidNetworkModule.kt
index a2716d1e0..42f1f9a88 100644
--- a/app/src/fdroid/kotlin/org/meshtastic/app/di/FDroidNetworkModule.kt
+++ b/app/src/fdroid/kotlin/org/meshtastic/app/di/FDroidNetworkModule.kt
@@ -16,24 +16,19 @@
*/
package org.meshtastic.app.di
-import dagger.Module
-import dagger.Provides
-import dagger.hilt.InstallIn
-import dagger.hilt.components.SingletonComponent
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
+import org.koin.core.annotation.Module
+import org.koin.core.annotation.Single
import org.meshtastic.core.common.BuildConfigProvider
import org.meshtastic.core.model.NetworkDeviceHardware
import org.meshtastic.core.model.NetworkFirmwareReleases
import org.meshtastic.core.network.service.ApiService
-import javax.inject.Singleton
-@InstallIn(SingletonComponent::class)
@Module
class FDroidNetworkModule {
- @Provides
- @Singleton
+ @Single
fun provideOkHttpClient(buildConfigProvider: BuildConfigProvider): OkHttpClient = OkHttpClient.Builder()
.addInterceptor(
interceptor =
@@ -45,8 +40,7 @@ class FDroidNetworkModule {
)
.build()
- @Provides
- @Singleton
+ @Single
fun provideApiService(): ApiService = object : ApiService {
override suspend fun getDeviceHardware(): List =
throw NotImplementedError("API calls to getDeviceHardware are not supported on Fdroid builds.")
diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/di/FlavorModule.kt b/app/src/fdroid/kotlin/org/meshtastic/app/di/FlavorModule.kt
new file mode 100644
index 000000000..5a192d437
--- /dev/null
+++ b/app/src/fdroid/kotlin/org/meshtastic/app/di/FlavorModule.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 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 org.meshtastic.app.di
+
+import org.koin.core.annotation.Module
+
+@Module(includes = [FDroidNetworkModule::class])
+class FlavorModule
diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/FdroidMapViewProvider.kt b/app/src/fdroid/kotlin/org/meshtastic/app/map/FdroidMapViewProvider.kt
index ba3300a99..290ea8667 100644
--- a/app/src/fdroid/kotlin/org/meshtastic/app/map/FdroidMapViewProvider.kt
+++ b/app/src/fdroid/kotlin/org/meshtastic/app/map/FdroidMapViewProvider.kt
@@ -18,9 +18,11 @@ package org.meshtastic.app.map
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
+import org.koin.compose.viewmodel.koinViewModel
+import org.koin.core.annotation.Single
import org.meshtastic.core.ui.util.MapViewProvider
+@Single
class FdroidMapViewProvider : MapViewProvider {
@Composable
override fun MapView(
@@ -33,7 +35,7 @@ class FdroidMapViewProvider : MapViewProvider {
tracerouteNodePositions: Map,
onTracerouteMappableCountChanged: (Int, Int) -> Unit,
) {
- val mapViewModel: MapViewModel = hiltViewModel()
+ val mapViewModel: MapViewModel = koinViewModel()
org.meshtastic.app.map.MapView(
modifier = modifier,
mapViewModel = mapViewModel,
diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt b/app/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt
index 8fa664f80..1ba1e02f7 100644
--- a/app/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt
+++ b/app/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt
@@ -74,7 +74,6 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.touchlab.kermit.Logger
import com.google.accompanist.permissions.ExperimentalPermissionsApi
@@ -83,6 +82,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.stringResource
+import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.app.R
import org.meshtastic.app.map.cluster.RadiusMarkerClusterer
import org.meshtastic.app.map.component.CacheLayout
@@ -235,7 +235,7 @@ private fun cacheManagerCallback(onTaskComplete: () -> Unit, onTaskFailed: (Int)
@Composable
fun MapView(
modifier: Modifier = Modifier,
- mapViewModel: MapViewModel = hiltViewModel(),
+ mapViewModel: MapViewModel = koinViewModel(),
navigateToNodeDetails: (Int) -> Unit,
focusedNodeNum: Int? = null,
nodeTracks: List? = null,
diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/MapViewModel.kt b/app/src/fdroid/kotlin/org/meshtastic/app/map/MapViewModel.kt
index 36b575d6a..83e253e59 100644
--- a/app/src/fdroid/kotlin/org/meshtastic/app/map/MapViewModel.kt
+++ b/app/src/fdroid/kotlin/org/meshtastic/app/map/MapViewModel.kt
@@ -18,10 +18,10 @@ package org.meshtastic.app.map
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.toRoute
-import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
+import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.common.BuildConfigProvider
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.RadioController
@@ -33,13 +33,10 @@ import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.feature.map.BaseMapViewModel
import org.meshtastic.proto.LocalConfig
-import javax.inject.Inject
@Suppress("LongParameterList")
-@HiltViewModel
-class MapViewModel
-@Inject
-constructor(
+@KoinViewModel
+class MapViewModel(
mapPrefs: MapPrefs,
packetRepository: PacketRepository,
override val nodeRepository: NodeRepository,
diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/model/NOAAWmsTileSource.kt b/app/src/fdroid/kotlin/org/meshtastic/app/map/model/NOAAWmsTileSource.kt
index bab1171d8..ac438397a 100644
--- a/app/src/fdroid/kotlin/org/meshtastic/app/map/model/NOAAWmsTileSource.kt
+++ b/app/src/fdroid/kotlin/org/meshtastic/app/map/model/NOAAWmsTileSource.kt
@@ -86,22 +86,6 @@ open class NOAAWmsTileSource(
if (time != null) this.time = time
}
- // fun createFrom(endpoint: WMSEndpoint, layer: WMSLayer): WMSTileSource? {
- // var srs: String? = "EPSG:900913"
- // if (layer.srs.isNotEmpty()) {
- // srs = layer.srs[0]
- // }
- // return if (layer.styles.isEmpty()) {
- // WMSTileSource(
- // layer.name, arrayOf(endpoint.baseurl), layer.name,
- // endpoint.wmsVersion, srs, null, layer.pixelSize
- // )
- // } else WMSTileSource(
- // layer.name, arrayOf(endpoint.baseurl), layer.name,
- // endpoint.wmsVersion, srs, layer.styles[0], layer.pixelSize
- // )
- // }
-
private fun tile2lon(x: Int, z: Int): Double = x / 2.0.pow(z.toDouble()) * 360.0 - 180
private fun tile2lat(y: Int, z: Int): Double {
diff --git a/feature/node/src/fdroid/kotlin/org/meshtastic/feature/node/component/InlineMap.kt b/app/src/fdroid/kotlin/org/meshtastic/app/node/component/InlineMap.kt
similarity index 88%
rename from feature/node/src/fdroid/kotlin/org/meshtastic/feature/node/component/InlineMap.kt
rename to app/src/fdroid/kotlin/org/meshtastic/app/node/component/InlineMap.kt
index e9b3c5054..638dcead9 100644
--- a/feature/node/src/fdroid/kotlin/org/meshtastic/feature/node/component/InlineMap.kt
+++ b/app/src/fdroid/kotlin/org/meshtastic/app/node/component/InlineMap.kt
@@ -14,13 +14,13 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package org.meshtastic.feature.node.component
+package org.meshtastic.app.node.component
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import org.meshtastic.core.model.Node
@Composable
-internal fun InlineMap(node: Node, modifier: Modifier = Modifier) {
+fun InlineMap(node: Node, modifier: Modifier = Modifier) {
// No-op for F-Droid builds
}
diff --git a/feature/node/src/fdroid/kotlin/org/meshtastic/feature/node/metrics/TracerouteMapOverlayInsets.kt b/app/src/fdroid/kotlin/org/meshtastic/app/node/metrics/TracerouteMapOverlayInsets.kt
similarity index 66%
rename from feature/node/src/fdroid/kotlin/org/meshtastic/feature/node/metrics/TracerouteMapOverlayInsets.kt
rename to app/src/fdroid/kotlin/org/meshtastic/app/node/metrics/TracerouteMapOverlayInsets.kt
index 2a35798f3..d6515eeb7 100644
--- a/feature/node/src/fdroid/kotlin/org/meshtastic/feature/node/metrics/TracerouteMapOverlayInsets.kt
+++ b/app/src/fdroid/kotlin/org/meshtastic/app/node/metrics/TracerouteMapOverlayInsets.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025 Meshtastic LLC
+ * 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
@@ -14,15 +14,15 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-
-package org.meshtastic.feature.node.metrics
+package org.meshtastic.app.node.metrics
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.dp
+import org.meshtastic.core.ui.util.TracerouteMapOverlayInsets
-internal object TracerouteMapOverlayInsets {
- val overlayAlignment: Alignment = Alignment.BottomEnd
- val overlayPadding: PaddingValues = PaddingValues(end = 16.dp, bottom = 16.dp)
- val contentHorizontalAlignment: Alignment.Horizontal = Alignment.End
-}
+fun getTracerouteMapOverlayInsets(): TracerouteMapOverlayInsets = TracerouteMapOverlayInsets(
+ overlayAlignment = Alignment.BottomEnd,
+ overlayPadding = PaddingValues(end = 16.dp, bottom = 16.dp),
+ contentHorizontalAlignment = Alignment.End,
+)
diff --git a/app/src/google/kotlin/org/meshtastic/app/analytics/GooglePlatformAnalytics.kt b/app/src/google/kotlin/org/meshtastic/app/analytics/GooglePlatformAnalytics.kt
index 30fa55730..a41eae2d3 100644
--- a/app/src/google/kotlin/org/meshtastic/app/analytics/GooglePlatformAnalytics.kt
+++ b/app/src/google/kotlin/org/meshtastic/app/analytics/GooglePlatformAnalytics.kt
@@ -46,16 +46,15 @@ import com.google.firebase.analytics.analytics
import com.google.firebase.crashlytics.crashlytics
import com.google.firebase.crashlytics.setCustomKeys
import com.google.firebase.initialize
-import dagger.hilt.android.qualifiers.ApplicationContext
import io.opentelemetry.api.GlobalOpenTelemetry
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
+import org.koin.core.annotation.Single
import org.meshtastic.app.BuildConfig
import org.meshtastic.core.repository.AnalyticsPrefs
import org.meshtastic.core.repository.DataPair
import org.meshtastic.core.repository.PlatformAnalytics
-import javax.inject.Inject
import co.touchlab.kermit.Logger as KermitLogger
/**
@@ -65,12 +64,9 @@ import co.touchlab.kermit.Logger as KermitLogger
* This implementation delays initialization of SDKs until user consent is granted to reduce tracking "noise" and
* respect privacy-focused environments.
*/
-class GooglePlatformAnalytics
-@Inject
-constructor(
- @ApplicationContext private val context: Context,
- private val analyticsPrefs: AnalyticsPrefs,
-) : PlatformAnalytics {
+@Single
+class GooglePlatformAnalytics(private val context: Context, private val analyticsPrefs: AnalyticsPrefs) :
+ PlatformAnalytics {
private val sampleRate = 100f.takeIf { BuildConfig.DEBUG } ?: 10f // For Datadog remote sample rate
diff --git a/app/src/google/kotlin/org/meshtastic/app/di/FlavorModule.kt b/app/src/google/kotlin/org/meshtastic/app/di/FlavorModule.kt
new file mode 100644
index 000000000..802f3b150
--- /dev/null
+++ b/app/src/google/kotlin/org/meshtastic/app/di/FlavorModule.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 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 org.meshtastic.app.di
+
+import org.koin.core.annotation.Module
+import org.meshtastic.app.map.prefs.di.GoogleMapsKoinModule
+
+@Module(includes = [GoogleNetworkModule::class, GoogleMapsKoinModule::class])
+class FlavorModule
diff --git a/app/src/google/kotlin/org/meshtastic/app/di/GoogleNetworkModule.kt b/app/src/google/kotlin/org/meshtastic/app/di/GoogleNetworkModule.kt
index 2a0894c45..0e88cb0fe 100644
--- a/app/src/google/kotlin/org/meshtastic/app/di/GoogleNetworkModule.kt
+++ b/app/src/google/kotlin/org/meshtastic/app/di/GoogleNetworkModule.kt
@@ -19,35 +19,24 @@ package org.meshtastic.app.di
import android.content.Context
import com.datadog.android.okhttp.DatadogEventListener
import com.datadog.android.okhttp.DatadogInterceptor
-import dagger.Binds
-import dagger.Module
-import dagger.Provides
-import dagger.hilt.InstallIn
-import dagger.hilt.android.qualifiers.ApplicationContext
-import dagger.hilt.components.SingletonComponent
import okhttp3.Cache
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
+import org.koin.core.annotation.Module
+import org.koin.core.annotation.Single
import org.meshtastic.core.common.BuildConfigProvider
import org.meshtastic.core.network.service.ApiService
import org.meshtastic.core.network.service.ApiServiceImpl
import java.io.File
-import javax.inject.Singleton
-@InstallIn(SingletonComponent::class)
@Module
-interface GoogleNetworkModule {
+class GoogleNetworkModule {
- @Binds @Singleton
- fun bindApiService(apiServiceImpl: ApiServiceImpl): ApiService
+ @Single fun bindApiService(apiServiceImpl: ApiServiceImpl): ApiService = apiServiceImpl
- companion object {
- @Provides
- @Singleton
- fun provideOkHttpClient(
- @ApplicationContext context: Context,
- buildConfigProvider: BuildConfigProvider,
- ): OkHttpClient = OkHttpClient.Builder()
+ @Single
+ fun provideOkHttpClient(context: Context, buildConfigProvider: BuildConfigProvider): OkHttpClient =
+ OkHttpClient.Builder()
.cache(
cache =
Cache(
@@ -63,10 +52,7 @@ interface GoogleNetworkModule {
}
},
)
- .addInterceptor(
- interceptor = DatadogInterceptor.Builder(tracedHosts = listOf("meshtastic.org")).build(),
- )
+ .addInterceptor(interceptor = DatadogInterceptor.Builder(tracedHosts = listOf("meshtastic.org")).build())
.eventListenerFactory(eventListenerFactory = DatadogEventListener.Factory())
.build()
- }
}
diff --git a/app/src/google/kotlin/org/meshtastic/app/di/GooglePlatformAnalyticsModule.kt b/app/src/google/kotlin/org/meshtastic/app/di/GooglePlatformAnalyticsModule.kt
deleted file mode 100644
index af63aab83..000000000
--- a/app/src/google/kotlin/org/meshtastic/app/di/GooglePlatformAnalyticsModule.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * 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 org.meshtastic.app.di
-
-import dagger.Binds
-import dagger.Module
-import dagger.hilt.InstallIn
-import dagger.hilt.components.SingletonComponent
-import org.meshtastic.app.analytics.GooglePlatformAnalytics
-import org.meshtastic.core.repository.PlatformAnalytics
-import javax.inject.Singleton
-
-/** Hilt module to provide the [GooglePlatformAnalytics] for the google flavor. */
-@Module
-@InstallIn(SingletonComponent::class)
-abstract class GooglePlatformAnalyticsModule {
-
- @Binds @Singleton
- abstract fun bindPlatformHelper(googlePlatformHelper: GooglePlatformAnalytics): PlatformAnalytics
-}
diff --git a/app/src/google/kotlin/org/meshtastic/app/map/GoogleMapViewProvider.kt b/app/src/google/kotlin/org/meshtastic/app/map/GoogleMapViewProvider.kt
index 63a7cd8a3..96680ce88 100644
--- a/app/src/google/kotlin/org/meshtastic/app/map/GoogleMapViewProvider.kt
+++ b/app/src/google/kotlin/org/meshtastic/app/map/GoogleMapViewProvider.kt
@@ -18,9 +18,11 @@ package org.meshtastic.app.map
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
+import org.koin.compose.viewmodel.koinViewModel
+import org.koin.core.annotation.Single
import org.meshtastic.core.ui.util.MapViewProvider
+@Single
class GoogleMapViewProvider : MapViewProvider {
@Composable
override fun MapView(
@@ -33,7 +35,7 @@ class GoogleMapViewProvider : MapViewProvider {
tracerouteNodePositions: Map,
onTracerouteMappableCountChanged: (Int, Int) -> Unit,
) {
- val mapViewModel: MapViewModel = hiltViewModel()
+ val mapViewModel: MapViewModel = koinViewModel()
org.meshtastic.app.map.MapView(
modifier = modifier,
mapViewModel = mapViewModel,
diff --git a/app/src/google/kotlin/org/meshtastic/app/map/MapView.kt b/app/src/google/kotlin/org/meshtastic/app/map/MapView.kt
index d9f12aac0..a67087399 100644
--- a/app/src/google/kotlin/org/meshtastic/app/map/MapView.kt
+++ b/app/src/google/kotlin/org/meshtastic/app/map/MapView.kt
@@ -59,7 +59,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.graphics.createBitmap
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.touchlab.kermit.Logger
import com.google.accompanist.permissions.ExperimentalPermissionsApi
@@ -95,6 +94,7 @@ import com.google.maps.android.data.kml.KmlLayer
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
import org.json.JSONObject
+import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.app.map.component.ClusterItemsListDialog
import org.meshtastic.app.map.component.CustomMapLayersSheet
import org.meshtastic.app.map.component.CustomTileProviderManagerSheet
@@ -149,7 +149,7 @@ private const val TRACEROUTE_BOUNDS_PADDING_PX = 120
@Composable
fun MapView(
modifier: Modifier = Modifier,
- mapViewModel: MapViewModel = hiltViewModel(),
+ mapViewModel: MapViewModel = koinViewModel(),
navigateToNodeDetails: (Int) -> Unit,
focusedNodeNum: Int? = null,
nodeTracks: List? = null,
diff --git a/app/src/google/kotlin/org/meshtastic/app/map/MapViewModel.kt b/app/src/google/kotlin/org/meshtastic/app/map/MapViewModel.kt
index 9a501b96c..cb3e00257 100644
--- a/app/src/google/kotlin/org/meshtastic/app/map/MapViewModel.kt
+++ b/app/src/google/kotlin/org/meshtastic/app/map/MapViewModel.kt
@@ -29,7 +29,6 @@ import com.google.android.gms.maps.model.TileProvider
import com.google.android.gms.maps.model.UrlTileProvider
import com.google.maps.android.compose.CameraPositionState
import com.google.maps.android.compose.MapType
-import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -43,6 +42,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
+import org.koin.core.annotation.KoinViewModel
import org.meshtastic.app.map.model.CustomTileProviderConfig
import org.meshtastic.app.map.prefs.map.GoogleMapsPrefs
import org.meshtastic.app.map.repository.CustomTileProviderRepository
@@ -62,7 +62,6 @@ import java.io.IOException
import java.io.InputStream
import java.net.MalformedURLException
import java.net.URL
-import javax.inject.Inject
import kotlin.uuid.Uuid
private const val TILE_SIZE = 256
@@ -77,10 +76,8 @@ data class MapCameraPosition(
)
@Suppress("TooManyFunctions", "LongParameterList")
-@HiltViewModel
-class MapViewModel
-@Inject
-constructor(
+@KoinViewModel
+class MapViewModel(
private val application: Application,
mapPrefs: MapPrefs,
private val googleMapsPrefs: GoogleMapsPrefs,
diff --git a/app/src/google/kotlin/org/meshtastic/app/map/prefs/di/GoogleMapsKoinModule.kt b/app/src/google/kotlin/org/meshtastic/app/map/prefs/di/GoogleMapsKoinModule.kt
new file mode 100644
index 000000000..e33fb1f8c
--- /dev/null
+++ b/app/src/google/kotlin/org/meshtastic/app/map/prefs/di/GoogleMapsKoinModule.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 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 org.meshtastic.app.map.prefs.di
+
+import android.content.Context
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.SharedPreferencesMigration
+import androidx.datastore.preferences.core.PreferenceDataStoreFactory
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.preferencesDataStoreFile
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import org.koin.core.annotation.ComponentScan
+import org.koin.core.annotation.Module
+import org.koin.core.annotation.Named
+import org.koin.core.annotation.Single
+
+@Module
+@ComponentScan("org.meshtastic.app.map")
+class GoogleMapsKoinModule {
+
+ @Single
+ @Named("GoogleMapsDataStore")
+ fun provideGoogleMapsDataStore(context: Context): DataStore = PreferenceDataStoreFactory.create(
+ migrations = listOf(SharedPreferencesMigration(context, "google_maps_prefs")),
+ scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
+ produceFile = { context.preferencesDataStoreFile("google_maps_ds") },
+ )
+}
diff --git a/app/src/google/kotlin/org/meshtastic/app/map/prefs/di/GoogleMapsModule.kt b/app/src/google/kotlin/org/meshtastic/app/map/prefs/di/GoogleMapsModule.kt
deleted file mode 100644
index a8d0a1192..000000000
--- a/app/src/google/kotlin/org/meshtastic/app/map/prefs/di/GoogleMapsModule.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * 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 org.meshtastic.app.map.prefs.di
-
-import android.content.Context
-import androidx.datastore.core.DataStore
-import androidx.datastore.preferences.SharedPreferencesMigration
-import androidx.datastore.preferences.core.PreferenceDataStoreFactory
-import androidx.datastore.preferences.core.Preferences
-import androidx.datastore.preferences.preferencesDataStoreFile
-import dagger.Binds
-import dagger.Module
-import dagger.Provides
-import dagger.hilt.InstallIn
-import dagger.hilt.android.qualifiers.ApplicationContext
-import dagger.hilt.components.SingletonComponent
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.SupervisorJob
-import org.meshtastic.app.map.prefs.map.GoogleMapsPrefs
-import org.meshtastic.app.map.prefs.map.GoogleMapsPrefsImpl
-import org.meshtastic.app.map.repository.CustomTileProviderRepository
-import org.meshtastic.app.map.repository.CustomTileProviderRepositoryImpl
-import javax.inject.Qualifier
-import javax.inject.Singleton
-
-@Qualifier
-@Retention(AnnotationRetention.BINARY)
-annotation class GoogleMapsDataStore
-
-@InstallIn(SingletonComponent::class)
-@Module
-interface GoogleMapsModule {
-
- @Binds fun bindGoogleMapsPrefs(googleMapsPrefsImpl: GoogleMapsPrefsImpl): GoogleMapsPrefs
-
- @Binds
- @Singleton
- fun bindCustomTileProviderRepository(impl: CustomTileProviderRepositoryImpl): CustomTileProviderRepository
-
- companion object {
- private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
-
- @Provides
- @Singleton
- @GoogleMapsDataStore
- fun provideGoogleMapsDataStore(@ApplicationContext context: Context): DataStore =
- PreferenceDataStoreFactory.create(
- migrations = listOf(SharedPreferencesMigration(context, "google_maps_prefs")),
- scope = scope,
- produceFile = { context.preferencesDataStoreFile("google_maps_ds") },
- )
- }
-}
diff --git a/app/src/google/kotlin/org/meshtastic/app/map/prefs/map/GoogleMapsPrefs.kt b/app/src/google/kotlin/org/meshtastic/app/map/prefs/map/GoogleMapsPrefs.kt
index 72760694a..0beba5e92 100644
--- a/app/src/google/kotlin/org/meshtastic/app/map/prefs/map/GoogleMapsPrefs.kt
+++ b/app/src/google/kotlin/org/meshtastic/app/map/prefs/map/GoogleMapsPrefs.kt
@@ -31,10 +31,9 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
-import org.meshtastic.app.map.prefs.di.GoogleMapsDataStore
+import org.koin.core.annotation.Named
+import org.koin.core.annotation.Single
import org.meshtastic.core.di.CoroutineDispatchers
-import javax.inject.Inject
-import javax.inject.Singleton
/** Interface for prefs specific to Google Maps. For general map prefs, see MapPrefs. */
interface GoogleMapsPrefs {
@@ -75,11 +74,9 @@ interface GoogleMapsPrefs {
fun setNetworkMapLayers(value: Set)
}
-@Singleton
-class GoogleMapsPrefsImpl
-@Inject
-constructor(
- @GoogleMapsDataStore private val dataStore: DataStore,
+@Single
+class GoogleMapsPrefsImpl(
+ @Named("GoogleMapsDataStore") private val dataStore: DataStore,
dispatchers: CoroutineDispatchers,
) : GoogleMapsPrefs {
private val scope = CoroutineScope(SupervisorJob() + dispatchers.default)
diff --git a/app/src/google/kotlin/org/meshtastic/app/map/repository/CustomTileProviderRepository.kt b/app/src/google/kotlin/org/meshtastic/app/map/repository/CustomTileProviderRepository.kt
index 8d8a1d6cf..6840cb17d 100644
--- a/app/src/google/kotlin/org/meshtastic/app/map/repository/CustomTileProviderRepository.kt
+++ b/app/src/google/kotlin/org/meshtastic/app/map/repository/CustomTileProviderRepository.kt
@@ -23,11 +23,10 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.withContext
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
+import org.koin.core.annotation.Single
import org.meshtastic.app.map.model.CustomTileProviderConfig
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.repository.MapTileProviderPrefs
-import javax.inject.Inject
-import javax.inject.Singleton
interface CustomTileProviderRepository {
fun getCustomTileProviders(): Flow>
@@ -41,10 +40,8 @@ interface CustomTileProviderRepository {
suspend fun getCustomTileProviderById(configId: String): CustomTileProviderConfig?
}
-@Singleton
-class CustomTileProviderRepositoryImpl
-@Inject
-constructor(
+@Single
+class CustomTileProviderRepositoryImpl(
private val json: Json,
private val dispatchers: CoroutineDispatchers,
private val mapTileProviderPrefs: MapTileProviderPrefs,
diff --git a/feature/node/src/google/kotlin/org/meshtastic/feature/node/component/InlineMap.kt b/app/src/google/kotlin/org/meshtastic/app/node/component/InlineMap.kt
similarity index 96%
rename from feature/node/src/google/kotlin/org/meshtastic/feature/node/component/InlineMap.kt
rename to app/src/google/kotlin/org/meshtastic/app/node/component/InlineMap.kt
index cb94e313f..c86e7a78c 100644
--- a/feature/node/src/google/kotlin/org/meshtastic/feature/node/component/InlineMap.kt
+++ b/app/src/google/kotlin/org/meshtastic/app/node/component/InlineMap.kt
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package org.meshtastic.feature.node.component
+package org.meshtastic.app.node.component
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
@@ -39,7 +39,7 @@ private const val DEFAULT_ZOOM = 15f
@OptIn(MapsComposeExperimentalApi::class)
@Composable
-internal fun InlineMap(node: Node, modifier: Modifier = Modifier) {
+fun InlineMap(node: Node, modifier: Modifier = Modifier) {
val dark = isSystemInDarkTheme()
val mapColorScheme =
when (dark) {
diff --git a/app/src/google/kotlin/org/meshtastic/app/node/metrics/TracerouteMapOverlayInsets.kt b/app/src/google/kotlin/org/meshtastic/app/node/metrics/TracerouteMapOverlayInsets.kt
new file mode 100644
index 000000000..992edf588
--- /dev/null
+++ b/app/src/google/kotlin/org/meshtastic/app/node/metrics/TracerouteMapOverlayInsets.kt
@@ -0,0 +1,28 @@
+/*
+ * 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 org.meshtastic.app.node.metrics
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.unit.dp
+import org.meshtastic.core.ui.util.TracerouteMapOverlayInsets
+
+fun getTracerouteMapOverlayInsets(): TracerouteMapOverlayInsets = TracerouteMapOverlayInsets(
+ overlayAlignment = Alignment.BottomCenter,
+ overlayPadding = PaddingValues(bottom = 16.dp),
+ contentHorizontalAlignment = Alignment.CenterHorizontally,
+)
diff --git a/app/src/main/kotlin/org/meshtastic/app/ApplicationModule.kt b/app/src/main/kotlin/org/meshtastic/app/ApplicationModule.kt
deleted file mode 100644
index d609d38dd..000000000
--- a/app/src/main/kotlin/org/meshtastic/app/ApplicationModule.kt
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * 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 org.meshtastic.app
-
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.LifecycleOwner
-import androidx.lifecycle.ProcessLifecycleOwner
-import dagger.Binds
-import dagger.Module
-import dagger.Provides
-import dagger.hilt.InstallIn
-import dagger.hilt.components.SingletonComponent
-import org.meshtastic.app.repository.radio.AndroidRadioInterfaceService
-import org.meshtastic.app.service.AndroidAppWidgetUpdater
-import org.meshtastic.app.service.AndroidMeshLocationManager
-import org.meshtastic.app.service.AndroidMeshWorkerManager
-import org.meshtastic.app.service.MeshServiceNotificationsImpl
-import org.meshtastic.app.service.ServiceBroadcasts
-import org.meshtastic.core.common.BuildConfigProvider
-import org.meshtastic.core.di.ProcessLifecycle
-import org.meshtastic.core.repository.MeshServiceNotifications
-import javax.inject.Singleton
-
-@InstallIn(SingletonComponent::class)
-@Module
-interface ApplicationModule {
-
- @Binds fun bindMeshServiceNotifications(impl: MeshServiceNotificationsImpl): MeshServiceNotifications
-
- @Binds
- fun bindMeshLocationManager(impl: AndroidMeshLocationManager): org.meshtastic.core.repository.MeshLocationManager
-
- @Binds fun bindMeshWorkerManager(impl: AndroidMeshWorkerManager): org.meshtastic.core.repository.MeshWorkerManager
-
- @Binds fun bindAppWidgetUpdater(impl: AndroidAppWidgetUpdater): org.meshtastic.core.repository.AppWidgetUpdater
-
- @Binds
- fun bindRadioInterfaceService(
- impl: AndroidRadioInterfaceService,
- ): org.meshtastic.core.repository.RadioInterfaceService
-
- @Binds fun bindServiceBroadcasts(impl: ServiceBroadcasts): org.meshtastic.core.repository.ServiceBroadcasts
-
- companion object {
- @Provides @ProcessLifecycle
- fun provideProcessLifecycleOwner(): LifecycleOwner = ProcessLifecycleOwner.get()
-
- @Provides
- @ProcessLifecycle
- fun provideProcessLifecycle(@ProcessLifecycle processLifecycleOwner: LifecycleOwner): Lifecycle =
- processLifecycleOwner.lifecycle
-
- @Singleton
- @Provides
- fun provideBuildConfigProvider(): BuildConfigProvider = object : BuildConfigProvider {
- override val isDebug: Boolean = BuildConfig.DEBUG
- override val applicationId: String = BuildConfig.APPLICATION_ID
- override val versionCode: Int = BuildConfig.VERSION_CODE
- override val versionName: String = BuildConfig.VERSION_NAME
- override val absoluteMinFwVersion: String = BuildConfig.ABS_MIN_FW_VERSION
- override val minFwVersion: String = BuildConfig.MIN_FW_VERSION
- }
- }
-}
diff --git a/app/src/main/kotlin/org/meshtastic/app/MainActivity.kt b/app/src/main/kotlin/org/meshtastic/app/MainActivity.kt
index d34038548..8ed01e5d8 100644
--- a/app/src/main/kotlin/org/meshtastic/app/MainActivity.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/MainActivity.kt
@@ -32,7 +32,6 @@ import androidx.activity.SystemBarStyle
import androidx.activity.compose.ReportDrawnWhen
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
-import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.CompositionLocalProvider
@@ -40,18 +39,22 @@ import androidx.compose.runtime.getValue
import androidx.core.content.IntentCompat
import androidx.core.net.toUri
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import co.touchlab.kermit.Logger
-import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import no.nordicsemi.kotlin.ble.core.android.AndroidEnvironment
import no.nordicsemi.kotlin.ble.environment.android.compose.LocalEnvironmentOwner
+import org.koin.android.ext.android.inject
+import org.koin.androidx.compose.koinViewModel
+import org.koin.androidx.viewmodel.ext.android.viewModel
+import org.koin.core.parameter.parametersOf
import org.meshtastic.app.intro.AnalyticsIntro
import org.meshtastic.app.intro.AndroidIntroViewModel
import org.meshtastic.app.map.getMapViewProvider
import org.meshtastic.app.model.UIViewModel
+import org.meshtastic.app.node.component.InlineMap
+import org.meshtastic.app.node.metrics.getTracerouteMapOverlayInsets
import org.meshtastic.app.ui.MainScreen
import org.meshtastic.core.barcode.rememberBarcodeScanner
import org.meshtastic.core.model.util.dispatchMeshtasticUri
@@ -63,27 +66,30 @@ import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.theme.MODE_DYNAMIC
import org.meshtastic.core.ui.util.LocalAnalyticsIntroProvider
import org.meshtastic.core.ui.util.LocalBarcodeScannerProvider
+import org.meshtastic.core.ui.util.LocalInlineMapProvider
import org.meshtastic.core.ui.util.LocalMapViewProvider
import org.meshtastic.core.ui.util.LocalNfcScannerProvider
+import org.meshtastic.core.ui.util.LocalTracerouteMapOverlayInsetsProvider
import org.meshtastic.core.ui.util.showToast
import org.meshtastic.feature.intro.AppIntroductionScreen
-import javax.inject.Inject
-@AndroidEntryPoint
class MainActivity : ComponentActivity() {
- private val model: UIViewModel by viewModels()
+ private val model: UIViewModel by viewModel()
/**
* Activity-lifecycle-aware client that binds to the mesh service. Note: This is used implicitly as it registers
* itself as a LifecycleObserver in its init block.
*/
- @Inject internal lateinit var meshServiceClient: MeshServiceClient
+ internal val meshServiceClient: MeshServiceClient by inject { parametersOf(this) }
- @Inject internal lateinit var androidEnvironment: AndroidEnvironment
+ internal val androidEnvironment: AndroidEnvironment by inject()
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
+ // Eagerly evaluate lazy Koin dependency so it registers its LifecycleObserver
+ meshServiceClient.hashCode()
+
super.onCreate(savedInstanceState)
enableEdgeToEdge()
@@ -124,6 +130,8 @@ class MainActivity : ComponentActivity() {
LocalNfcScannerProvider provides { onResult, onDisabled -> NfcScannerEffect(onResult, onDisabled) },
LocalAnalyticsIntroProvider provides { AnalyticsIntro() },
LocalMapViewProvider provides getMapViewProvider(),
+ LocalInlineMapProvider provides { node, modifier -> InlineMap(node, modifier) },
+ LocalTracerouteMapOverlayInsetsProvider provides getTracerouteMapOverlayInsets(),
) {
AppTheme(dynamicColor = dynamic, darkTheme = dark) {
val appIntroCompleted by model.appIntroCompleted.collectAsStateWithLifecycle()
@@ -135,7 +143,7 @@ class MainActivity : ComponentActivity() {
if (appIntroCompleted) {
MainScreen(uIViewModel = model)
} else {
- val introViewModel = hiltViewModel()
+ val introViewModel = koinViewModel()
AppIntroductionScreen(onDone = { model.onAppIntroCompleted() }, viewModel = introViewModel)
}
}
diff --git a/core/di/src/commonMain/kotlin/org/meshtastic/core/di/ProcessLifecycle.kt b/app/src/main/kotlin/org/meshtastic/app/MainKoinModule.kt
similarity index 79%
rename from core/di/src/commonMain/kotlin/org/meshtastic/core/di/ProcessLifecycle.kt
rename to app/src/main/kotlin/org/meshtastic/app/MainKoinModule.kt
index 5eb0b500c..80cc15dde 100644
--- a/core/di/src/commonMain/kotlin/org/meshtastic/core/di/ProcessLifecycle.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/MainKoinModule.kt
@@ -14,10 +14,11 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package org.meshtastic.core.di
+package org.meshtastic.app
-import javax.inject.Qualifier
+import org.koin.core.annotation.ComponentScan
+import org.koin.core.annotation.Module
-@Qualifier
-@Retention(AnnotationRetention.BINARY)
-annotation class ProcessLifecycle
+@Module
+@ComponentScan("org.meshtastic.app")
+class MainKoinModule
diff --git a/app/src/main/kotlin/org/meshtastic/app/MeshServiceClient.kt b/app/src/main/kotlin/org/meshtastic/app/MeshServiceClient.kt
index b683fd380..eacb76cc8 100644
--- a/app/src/main/kotlin/org/meshtastic/app/MeshServiceClient.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/MeshServiceClient.kt
@@ -23,9 +23,8 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import co.touchlab.kermit.Logger
-import dagger.hilt.android.qualifiers.ActivityContext
-import dagger.hilt.android.scopes.ActivityScoped
import kotlinx.coroutines.launch
+import org.koin.core.annotation.Factory
import org.meshtastic.app.service.MeshService
import org.meshtastic.app.service.startService
import org.meshtastic.core.common.util.SequentialJob
@@ -33,14 +32,11 @@ import org.meshtastic.core.service.AndroidServiceRepository
import org.meshtastic.core.service.BindFailedException
import org.meshtastic.core.service.IMeshService
import org.meshtastic.core.service.ServiceClient
-import javax.inject.Inject
/** A Activity-lifecycle-aware [ServiceClient] that binds [MeshService] once the Activity is started. */
-@ActivityScoped
-class MeshServiceClient
-@Inject
-constructor(
- @ActivityContext private val context: Context,
+@Factory
+class MeshServiceClient(
+ private val context: Context,
private val serviceRepository: AndroidServiceRepository,
private val serviceSetupJob: SequentialJob,
) : ServiceClient(IMeshService.Stub::asInterface),
diff --git a/app/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt b/app/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt
index daae4a159..6d96616fb 100644
--- a/app/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt
@@ -21,17 +21,11 @@ import android.appwidget.AppWidgetProviderInfo
import android.os.Build
import androidx.collection.intSetOf
import androidx.glance.appwidget.GlanceAppWidgetManager
-import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import co.touchlab.kermit.Logger
-import dagger.hilt.EntryPoint
-import dagger.hilt.InstallIn
-import dagger.hilt.android.EntryPointAccessors
-import dagger.hilt.android.HiltAndroidApp
-import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.TimeoutCancellationException
@@ -40,13 +34,17 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
import no.nordicsemi.kotlin.ble.core.android.AndroidEnvironment
+import org.koin.android.ext.android.get
+import org.koin.android.ext.koin.androidContext
+import org.koin.androidx.workmanager.koin.workManagerFactory
+import org.koin.core.context.startKoin
+import org.meshtastic.app.di.AppKoinModule
+import org.meshtastic.app.di.module
import org.meshtastic.app.widget.LocalStatsWidgetReceiver
import org.meshtastic.app.worker.MeshLogCleanupWorker
import org.meshtastic.core.common.ContextServices
import org.meshtastic.core.database.DatabaseManager
-import org.meshtastic.core.repository.MeshLogPrefs
import org.meshtastic.core.repository.MeshPrefs
-import javax.inject.Inject
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.seconds
import kotlin.time.toJavaDuration
@@ -54,15 +52,11 @@ import kotlin.time.toJavaDuration
/**
* The main application class for Meshtastic.
*
- * This class is annotated with [HiltAndroidApp] to enable Hilt for dependency injection. It initializes core
- * application components, including analytics and platform-specific helpers, and manages analytics consent based on
- * user preferences.
+ * This class initializes core application components using Koin for dependency injection.
*/
-@HiltAndroidApp
open class MeshUtilApplication :
Application(),
Configuration.Provider {
- @Inject lateinit var workerFactory: HiltWorkerFactory
private val applicationScope = CoroutineScope(Dispatchers.Default)
@@ -70,6 +64,12 @@ open class MeshUtilApplication :
super.onCreate()
ContextServices.app = this
+ startKoin {
+ androidContext(this@MeshUtilApplication)
+ workManagerFactory()
+ modules(AppKoinModule().module())
+ }
+
// Schedule periodic MeshLog cleanup
scheduleMeshLogCleanup()
@@ -93,15 +93,11 @@ open class MeshUtilApplication :
pushPreview()
- val entryPoint =
- EntryPointAccessors.fromApplication(
- this@MeshUtilApplication,
- org.meshtastic.app.widget.LocalStatsWidget.LocalStatsWidgetEntryPoint::class.java,
- )
+ val widgetStateProvider: org.meshtastic.app.widget.LocalStatsWidgetStateProvider = get()
try {
// Wait for real data for up to 30 seconds before pushing an updated preview
withTimeout(30.seconds) {
- entryPoint.widgetStateProvider().state.first { it.showContent && it.nodeShortName != null }
+ widgetStateProvider.state.first { it.showContent && it.nodeShortName != null }
}
Logger.i { "Real node data acquired. Pushing updated widget preview." }
@@ -113,17 +109,20 @@ open class MeshUtilApplication :
}
// Initialize DatabaseManager asynchronously with current device address so DAO consumers have an active DB
- val entryPoint = EntryPointAccessors.fromApplication(this, AppEntryPoint::class.java)
- applicationScope.launch { entryPoint.databaseManager().init(entryPoint.meshPrefs().deviceAddress.value) }
+ applicationScope.launch {
+ val dbManager: DatabaseManager = get()
+ val meshPrefs: MeshPrefs = get()
+ dbManager.init(meshPrefs.deviceAddress.value)
+ }
}
override fun onTerminate() {
// Shutdown managers (useful for Robolectric tests)
- val entryPoint = EntryPointAccessors.fromApplication(this, AppEntryPoint::class.java)
- entryPoint.databaseManager().close()
- entryPoint.androidEnvironment().close()
+ get().close()
+ get().close()
applicationScope.cancel()
super.onTerminate()
+ org.koin.core.context.stopKoin()
}
private fun scheduleMeshLogCleanup() {
@@ -139,19 +138,7 @@ open class MeshUtilApplication :
}
override val workManagerConfiguration: Configuration
- get() = Configuration.Builder().setWorkerFactory(workerFactory).build()
-}
-
-@EntryPoint
-@InstallIn(SingletonComponent::class)
-interface AppEntryPoint {
- fun databaseManager(): DatabaseManager
-
- fun meshPrefs(): MeshPrefs
-
- fun meshLogPrefs(): MeshLogPrefs
-
- fun androidEnvironment(): AndroidEnvironment
+ get() = Configuration.Builder().setWorkerFactory(get()).build()
}
fun logAssert(executeReliableWrite: Boolean) {
diff --git a/app/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt b/app/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt
new file mode 100644
index 000000000..becacee54
--- /dev/null
+++ b/app/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (c) 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 org.meshtastic.app.di
+
+import android.app.Application
+import android.content.Context
+import android.hardware.usb.UsbManager
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.ProcessLifecycleOwner
+import androidx.work.WorkManager
+import com.hoho.android.usbserial.driver.ProbeTable
+import com.hoho.android.usbserial.driver.UsbSerialProber
+import org.koin.core.annotation.Module
+import org.koin.core.annotation.Named
+import org.koin.core.annotation.Single
+import org.meshtastic.app.repository.usb.ProbeTableProvider
+import org.meshtastic.core.ble.di.CoreBleAndroidModule
+import org.meshtastic.core.ble.di.CoreBleModule
+import org.meshtastic.core.common.BuildConfigProvider
+import org.meshtastic.core.common.di.CoreCommonModule
+import org.meshtastic.core.data.di.CoreDataAndroidModule
+import org.meshtastic.core.data.di.CoreDataModule
+import org.meshtastic.core.database.di.CoreDatabaseAndroidModule
+import org.meshtastic.core.database.di.CoreDatabaseModule
+import org.meshtastic.core.datastore.di.CoreDatastoreAndroidModule
+import org.meshtastic.core.datastore.di.CoreDatastoreModule
+import org.meshtastic.core.di.di.CoreDiModule
+import org.meshtastic.core.network.di.CoreNetworkModule
+import org.meshtastic.core.prefs.di.CorePrefsAndroidModule
+import org.meshtastic.core.prefs.di.CorePrefsModule
+import org.meshtastic.core.service.di.CoreServiceAndroidModule
+import org.meshtastic.core.service.di.CoreServiceModule
+import org.meshtastic.core.ui.di.CoreUiModule
+import org.meshtastic.feature.firmware.di.FeatureFirmwareModule
+import org.meshtastic.feature.intro.di.FeatureIntroModule
+import org.meshtastic.feature.map.di.FeatureMapModule
+import org.meshtastic.feature.messaging.di.FeatureMessagingModule
+import org.meshtastic.feature.node.di.FeatureNodeModule
+import org.meshtastic.feature.settings.di.FeatureSettingsModule
+
+@Module(
+ includes =
+ [
+ org.meshtastic.app.MainKoinModule::class,
+ CoreDiModule::class,
+ CoreCommonModule::class,
+ CoreBleModule::class,
+ CoreBleAndroidModule::class,
+ CoreDataModule::class,
+ CoreDataAndroidModule::class,
+ org.meshtastic.core.domain.di.CoreDomainModule::class,
+ CoreDatabaseModule::class,
+ CoreDatabaseAndroidModule::class,
+ org.meshtastic.core.repository.di.CoreRepositoryModule::class,
+ CoreDatastoreModule::class,
+ CoreDatastoreAndroidModule::class,
+ CorePrefsModule::class,
+ CorePrefsAndroidModule::class,
+ CoreServiceModule::class,
+ CoreServiceAndroidModule::class,
+ CoreNetworkModule::class,
+ CoreUiModule::class,
+ FeatureNodeModule::class,
+ FeatureMessagingModule::class,
+ FeatureMapModule::class,
+ FeatureSettingsModule::class,
+ FeatureFirmwareModule::class,
+ FeatureIntroModule::class,
+ NetworkModule::class,
+ FlavorModule::class,
+ ],
+)
+class AppKoinModule {
+ @Single
+ @Named("ProcessLifecycle")
+ fun provideProcessLifecycle(): Lifecycle = ProcessLifecycleOwner.get().lifecycle
+
+ @Single
+ fun provideBuildConfigProvider(): BuildConfigProvider = object : BuildConfigProvider {
+ override val isDebug: Boolean = org.meshtastic.app.BuildConfig.DEBUG
+ override val applicationId: String = org.meshtastic.app.BuildConfig.APPLICATION_ID
+ override val versionCode: Int = org.meshtastic.app.BuildConfig.VERSION_CODE
+ override val versionName: String = org.meshtastic.app.BuildConfig.VERSION_NAME
+ override val absoluteMinFwVersion: String = org.meshtastic.app.BuildConfig.ABS_MIN_FW_VERSION
+ override val minFwVersion: String = org.meshtastic.app.BuildConfig.MIN_FW_VERSION
+ }
+
+ @Single fun provideWorkManager(context: Application): WorkManager = WorkManager.getInstance(context)
+
+ @Single
+ fun provideUsbManager(application: Application): UsbManager? =
+ application.getSystemService(Context.USB_SERVICE) as UsbManager?
+
+ @Single fun provideProbeTable(provider: ProbeTableProvider): ProbeTable = provider.get()
+
+ @Single fun provideUsbSerialProber(probeTable: ProbeTable): UsbSerialProber = UsbSerialProber(probeTable)
+}
diff --git a/app/src/main/kotlin/org/meshtastic/app/di/BleModule.kt b/app/src/main/kotlin/org/meshtastic/app/di/BleModule.kt
deleted file mode 100644
index 8e9a434fd..000000000
--- a/app/src/main/kotlin/org/meshtastic/app/di/BleModule.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * 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 org.meshtastic.app.di
-
-import android.content.Context
-import dagger.Binds
-import dagger.Module
-import dagger.Provides
-import dagger.hilt.InstallIn
-import dagger.hilt.android.qualifiers.ApplicationContext
-import dagger.hilt.components.SingletonComponent
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.SupervisorJob
-import no.nordicsemi.kotlin.ble.client.android.CentralManager
-import no.nordicsemi.kotlin.ble.client.android.native
-import no.nordicsemi.kotlin.ble.core.android.AndroidEnvironment
-import no.nordicsemi.kotlin.ble.environment.android.NativeAndroidEnvironment
-import org.meshtastic.core.ble.AndroidBleConnectionFactory
-import org.meshtastic.core.ble.AndroidBleScanner
-import org.meshtastic.core.ble.AndroidBluetoothRepository
-import org.meshtastic.core.ble.BleConnection
-import org.meshtastic.core.ble.BleConnectionFactory
-import org.meshtastic.core.ble.BleScanner
-import org.meshtastic.core.ble.BluetoothRepository
-import org.meshtastic.core.di.CoroutineDispatchers
-import javax.inject.Singleton
-
-@Module
-@InstallIn(SingletonComponent::class)
-abstract class BleModule {
-
- @Binds @Singleton
- abstract fun bindBleScanner(impl: AndroidBleScanner): BleScanner
-
- @Binds @Singleton
- abstract fun bindBluetoothRepository(impl: AndroidBluetoothRepository): BluetoothRepository
-
- @Binds @Singleton
- abstract fun bindBleConnectionFactory(impl: AndroidBleConnectionFactory): BleConnectionFactory
-
- companion object {
- @Provides
- @Singleton
- fun provideAndroidEnvironment(@ApplicationContext context: Context): AndroidEnvironment =
- NativeAndroidEnvironment.getInstance(context, isNeverForLocationFlagSet = true)
-
- @Provides
- @Singleton
- fun provideBleSingletonCoroutineScope(dispatchers: CoroutineDispatchers): CoroutineScope =
- CoroutineScope(SupervisorJob() + dispatchers.default)
-
- @Provides
- @Singleton
- fun provideCentralManager(environment: AndroidEnvironment, coroutineScope: CoroutineScope): CentralManager =
- CentralManager.native(environment as NativeAndroidEnvironment, coroutineScope)
-
- @Provides
- fun provideBleConnection(factory: BleConnectionFactory, coroutineScope: CoroutineScope): BleConnection =
- factory.create(coroutineScope, "BLE")
- }
-}
diff --git a/app/src/main/kotlin/org/meshtastic/app/di/DataSourceModule.kt b/app/src/main/kotlin/org/meshtastic/app/di/DataSourceModule.kt
deleted file mode 100644
index 55a42e183..000000000
--- a/app/src/main/kotlin/org/meshtastic/app/di/DataSourceModule.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * 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 org.meshtastic.app.di
-
-import dagger.Binds
-import dagger.Module
-import dagger.hilt.InstallIn
-import dagger.hilt.components.SingletonComponent
-import org.meshtastic.core.data.datasource.BootloaderOtaQuirksJsonDataSource
-import org.meshtastic.core.data.datasource.BootloaderOtaQuirksJsonDataSourceImpl
-import org.meshtastic.core.data.datasource.DeviceHardwareJsonDataSource
-import org.meshtastic.core.data.datasource.DeviceHardwareJsonDataSourceImpl
-import org.meshtastic.core.data.datasource.FirmwareReleaseJsonDataSource
-import org.meshtastic.core.data.datasource.FirmwareReleaseJsonDataSourceImpl
-import javax.inject.Singleton
-
-@Module
-@InstallIn(SingletonComponent::class)
-interface DataSourceModule {
- @Binds
- @Singleton
- fun bindDeviceHardwareJsonDataSource(impl: DeviceHardwareJsonDataSourceImpl): DeviceHardwareJsonDataSource
-
- @Binds
- @Singleton
- fun bindFirmwareReleaseJsonDataSource(impl: FirmwareReleaseJsonDataSourceImpl): FirmwareReleaseJsonDataSource
-
- @Binds
- @Singleton
- fun bindBootloaderOtaQuirksJsonDataSource(
- impl: BootloaderOtaQuirksJsonDataSourceImpl,
- ): BootloaderOtaQuirksJsonDataSource
-}
diff --git a/app/src/main/kotlin/org/meshtastic/app/di/NetworkModule.kt b/app/src/main/kotlin/org/meshtastic/app/di/NetworkModule.kt
index f3dabfe13..58416a139 100644
--- a/app/src/main/kotlin/org/meshtastic/app/di/NetworkModule.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/di/NetworkModule.kt
@@ -16,7 +16,10 @@
*/
package org.meshtastic.app.di
+import android.app.Application
import android.content.Context
+import android.net.ConnectivityManager
+import android.net.nsd.NsdManager
import coil3.ImageLoader
import coil3.disk.DiskCache
import coil3.memory.MemoryCache
@@ -25,72 +28,66 @@ import coil3.request.crossfade
import coil3.svg.SvgDecoder
import coil3.util.DebugLogger
import coil3.util.Logger
-import dagger.Binds
-import dagger.Module
-import dagger.Provides
-import dagger.hilt.InstallIn
-import dagger.hilt.android.qualifiers.ApplicationContext
-import dagger.hilt.components.SingletonComponent
import io.ktor.client.HttpClient
import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient
+import org.koin.core.annotation.Module
+import org.koin.core.annotation.Single
import org.meshtastic.core.common.BuildConfigProvider
-import javax.inject.Singleton
private const val DISK_CACHE_PERCENT = 0.02
private const val MEMORY_CACHE_PERCENT = 0.25
-@InstallIn(SingletonComponent::class)
@Module
-interface NetworkModule {
+class NetworkModule {
- @Binds
- @Singleton
+ @Single
+ fun provideConnectivityManager(application: Application): ConnectivityManager =
+ application.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+
+ @Single
+ fun provideNsdManager(application: Application): NsdManager =
+ application.getSystemService(Context.NSD_SERVICE) as NsdManager
+
+ @Single
fun bindMqttRepository(
impl: org.meshtastic.core.network.repository.MQTTRepositoryImpl,
- ): org.meshtastic.core.network.repository.MQTTRepository
+ ): org.meshtastic.core.network.repository.MQTTRepository = impl
- companion object {
- @Provides
- @Singleton
- fun provideImageLoader(
- okHttpClient: OkHttpClient,
- @ApplicationContext application: Context,
- buildConfigProvider: BuildConfigProvider,
- ): ImageLoader {
- val sharedOkHttp = okHttpClient.newBuilder().build()
- return ImageLoader.Builder(context = application)
- .components {
- add(OkHttpNetworkFetcherFactory(callFactory = { sharedOkHttp }))
- add(SvgDecoder.Factory(scaleToDensity = true))
- }
- .memoryCache {
- MemoryCache.Builder().maxSizePercent(context = application, percent = MEMORY_CACHE_PERCENT).build()
- }
- .diskCache { DiskCache.Builder().maxSizePercent(percent = DISK_CACHE_PERCENT).build() }
- .logger(
- logger = if (buildConfigProvider.isDebug) DebugLogger(minLevel = Logger.Level.Verbose) else null,
- )
- .crossfade(enable = true)
- .build()
- }
+ @Single
+ fun provideImageLoader(
+ okHttpClient: OkHttpClient,
+ application: Context,
+ buildConfigProvider: BuildConfigProvider,
+ ): ImageLoader {
+ val sharedOkHttp = okHttpClient.newBuilder().build()
+ return ImageLoader.Builder(context = application)
+ .components {
+ add(OkHttpNetworkFetcherFactory(callFactory = { sharedOkHttp }))
+ add(SvgDecoder.Factory(scaleToDensity = true))
+ }
+ .memoryCache {
+ MemoryCache.Builder().maxSizePercent(context = application, percent = MEMORY_CACHE_PERCENT).build()
+ }
+ .diskCache { DiskCache.Builder().maxSizePercent(percent = DISK_CACHE_PERCENT).build() }
+ .logger(logger = if (buildConfigProvider.isDebug) DebugLogger(minLevel = Logger.Level.Verbose) else null)
+ .crossfade(enable = true)
+ .build()
+ }
- @Provides
- @Singleton
- fun provideJson(): Json = Json {
- isLenient = true
- ignoreUnknownKeys = true
- }
+ @Single
+ fun provideJson(): Json = Json {
+ isLenient = true
+ ignoreUnknownKeys = true
+ }
- @Provides
- @Singleton
- fun provideHttpClient(okHttpClient: OkHttpClient, json: Json): HttpClient = HttpClient(engineFactory = OkHttp) {
- engine { preconfigured = okHttpClient }
+ @Single
+ fun provideHttpClient(okHttpClient: OkHttpClient, json: Json): HttpClient = HttpClient(engineFactory = OkHttp) {
+ engine { preconfigured = okHttpClient }
- install(plugin = ContentNegotiation) { json(json) }
- }
+ install(plugin = ContentNegotiation) { json(json) }
}
}
diff --git a/app/src/main/kotlin/org/meshtastic/app/di/NodeDataSourceModule.kt b/app/src/main/kotlin/org/meshtastic/app/di/NodeDataSourceModule.kt
deleted file mode 100644
index 54a91068d..000000000
--- a/app/src/main/kotlin/org/meshtastic/app/di/NodeDataSourceModule.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * 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 org.meshtastic.app.di
-
-import dagger.Binds
-import dagger.Module
-import dagger.hilt.InstallIn
-import dagger.hilt.components.SingletonComponent
-import org.meshtastic.core.data.datasource.NodeInfoReadDataSource
-import org.meshtastic.core.data.datasource.NodeInfoWriteDataSource
-import org.meshtastic.core.data.datasource.SwitchingNodeInfoReadDataSource
-import org.meshtastic.core.data.datasource.SwitchingNodeInfoWriteDataSource
-import javax.inject.Singleton
-
-@Module
-@InstallIn(SingletonComponent::class)
-interface NodeDataSourceModule {
- @Binds @Singleton
- fun bindNodeInfoReadDataSource(impl: SwitchingNodeInfoReadDataSource): NodeInfoReadDataSource
-
- @Binds @Singleton
- fun bindNodeInfoWriteDataSource(impl: SwitchingNodeInfoWriteDataSource): NodeInfoWriteDataSource
-}
diff --git a/app/src/main/kotlin/org/meshtastic/app/di/PrefsModule.kt b/app/src/main/kotlin/org/meshtastic/app/di/PrefsModule.kt
deleted file mode 100644
index 1d555b5b0..000000000
--- a/app/src/main/kotlin/org/meshtastic/app/di/PrefsModule.kt
+++ /dev/null
@@ -1,269 +0,0 @@
-/*
- * 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 org.meshtastic.app.di
-
-import android.content.Context
-import androidx.datastore.core.DataStore
-import androidx.datastore.preferences.SharedPreferencesMigration
-import androidx.datastore.preferences.core.PreferenceDataStoreFactory
-import androidx.datastore.preferences.core.Preferences
-import androidx.datastore.preferences.preferencesDataStoreFile
-import dagger.Binds
-import dagger.Module
-import dagger.Provides
-import dagger.hilt.InstallIn
-import dagger.hilt.android.qualifiers.ApplicationContext
-import dagger.hilt.components.SingletonComponent
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.SupervisorJob
-import org.meshtastic.core.prefs.analytics.AnalyticsPrefsImpl
-import org.meshtastic.core.prefs.di.AnalyticsDataStore
-import org.meshtastic.core.prefs.di.AppDataStore
-import org.meshtastic.core.prefs.di.CustomEmojiDataStore
-import org.meshtastic.core.prefs.di.FilterDataStore
-import org.meshtastic.core.prefs.di.HomoglyphEncodingDataStore
-import org.meshtastic.core.prefs.di.MapConsentDataStore
-import org.meshtastic.core.prefs.di.MapDataStore
-import org.meshtastic.core.prefs.di.MapTileProviderDataStore
-import org.meshtastic.core.prefs.di.MeshDataStore
-import org.meshtastic.core.prefs.di.MeshLogDataStore
-import org.meshtastic.core.prefs.di.RadioDataStore
-import org.meshtastic.core.prefs.di.UiDataStore
-import org.meshtastic.core.prefs.emoji.CustomEmojiPrefsImpl
-import org.meshtastic.core.prefs.filter.FilterPrefsImpl
-import org.meshtastic.core.prefs.homoglyph.HomoglyphPrefsImpl
-import org.meshtastic.core.prefs.map.MapConsentPrefsImpl
-import org.meshtastic.core.prefs.map.MapPrefsImpl
-import org.meshtastic.core.prefs.map.MapTileProviderPrefsImpl
-import org.meshtastic.core.prefs.mesh.MeshPrefsImpl
-import org.meshtastic.core.prefs.meshlog.MeshLogPrefsImpl
-import org.meshtastic.core.prefs.radio.RadioPrefsImpl
-import org.meshtastic.core.prefs.ui.UiPrefsImpl
-import org.meshtastic.core.repository.AnalyticsPrefs
-import org.meshtastic.core.repository.CustomEmojiPrefs
-import org.meshtastic.core.repository.FilterPrefs
-import org.meshtastic.core.repository.HomoglyphPrefs
-import org.meshtastic.core.repository.MapConsentPrefs
-import org.meshtastic.core.repository.MapPrefs
-import org.meshtastic.core.repository.MapTileProviderPrefs
-import org.meshtastic.core.repository.MeshLogPrefs
-import org.meshtastic.core.repository.MeshPrefs
-import org.meshtastic.core.repository.RadioPrefs
-import org.meshtastic.core.repository.UiPrefs
-import javax.inject.Qualifier
-import javax.inject.Singleton
-
-@Qualifier
-@Retention(AnnotationRetention.BINARY)
-internal annotation class AnalyticsDataStore
-
-@Qualifier
-@Retention(AnnotationRetention.BINARY)
-internal annotation class HomoglyphEncodingDataStore
-
-@Qualifier
-@Retention(AnnotationRetention.BINARY)
-internal annotation class AppDataStore
-
-@Qualifier
-@Retention(AnnotationRetention.BINARY)
-internal annotation class CustomEmojiDataStore
-
-@Qualifier
-@Retention(AnnotationRetention.BINARY)
-internal annotation class MapDataStore
-
-@Qualifier
-@Retention(AnnotationRetention.BINARY)
-internal annotation class MapConsentDataStore
-
-@Qualifier
-@Retention(AnnotationRetention.BINARY)
-internal annotation class MapTileProviderDataStore
-
-@Qualifier
-@Retention(AnnotationRetention.BINARY)
-internal annotation class MeshDataStore
-
-@Qualifier
-@Retention(AnnotationRetention.BINARY)
-internal annotation class RadioDataStore
-
-@Qualifier
-@Retention(AnnotationRetention.BINARY)
-internal annotation class UiDataStore
-
-@Qualifier
-@Retention(AnnotationRetention.BINARY)
-internal annotation class MeshLogDataStore
-
-@Qualifier
-@Retention(AnnotationRetention.BINARY)
-internal annotation class FilterDataStore
-
-@Suppress("TooManyFunctions")
-@InstallIn(SingletonComponent::class)
-@Module
-interface PrefsModule {
-
- @Binds fun bindAnalyticsPrefs(analyticsPrefsImpl: AnalyticsPrefsImpl): AnalyticsPrefs
-
- @Binds fun bindHomoglyphEncodingPrefs(homoglyphEncodingPrefsImpl: HomoglyphPrefsImpl): HomoglyphPrefs
-
- @Binds fun bindCustomEmojiPrefs(customEmojiPrefsImpl: CustomEmojiPrefsImpl): CustomEmojiPrefs
-
- @Binds fun bindMapConsentPrefs(mapConsentPrefsImpl: MapConsentPrefsImpl): MapConsentPrefs
-
- @Binds fun bindMapPrefs(mapPrefsImpl: MapPrefsImpl): MapPrefs
-
- @Binds fun bindMapTileProviderPrefs(mapTileProviderPrefsImpl: MapTileProviderPrefsImpl): MapTileProviderPrefs
-
- @Binds fun bindMeshPrefs(meshPrefsImpl: MeshPrefsImpl): MeshPrefs
-
- @Binds fun bindMeshLogPrefs(meshLogPrefsImpl: MeshLogPrefsImpl): MeshLogPrefs
-
- @Binds fun bindRadioPrefs(radioPrefsImpl: RadioPrefsImpl): RadioPrefs
-
- @Binds fun bindUiPrefs(uiPrefsImpl: UiPrefsImpl): UiPrefs
-
- @Binds fun bindFilterPrefs(filterPrefsImpl: FilterPrefsImpl): FilterPrefs
-
- companion object {
- private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
-
- @Provides
- @Singleton
- @AnalyticsDataStore
- fun provideAnalyticsDataStore(@ApplicationContext context: Context): DataStore =
- PreferenceDataStoreFactory.create(
- migrations = listOf(SharedPreferencesMigration(context, "analytics-prefs")),
- scope = scope,
- produceFile = { context.preferencesDataStoreFile("analytics_ds") },
- )
-
- @Provides
- @Singleton
- @HomoglyphEncodingDataStore
- fun provideHomoglyphEncodingDataStore(@ApplicationContext context: Context): DataStore =
- PreferenceDataStoreFactory.create(
- migrations = listOf(SharedPreferencesMigration(context, "homoglyph-encoding-prefs")),
- scope = scope,
- produceFile = { context.preferencesDataStoreFile("homoglyph_encoding_ds") },
- )
-
- @Provides
- @Singleton
- @AppDataStore
- fun provideAppDataStore(@ApplicationContext context: Context): DataStore =
- PreferenceDataStoreFactory.create(
- migrations = listOf(SharedPreferencesMigration(context, "prefs")),
- scope = scope,
- produceFile = { context.preferencesDataStoreFile("app_ds") },
- )
-
- @Provides
- @Singleton
- @CustomEmojiDataStore
- fun provideCustomEmojiDataStore(@ApplicationContext context: Context): DataStore =
- PreferenceDataStoreFactory.create(
- migrations = listOf(SharedPreferencesMigration(context, "org.geeksville.emoji.prefs")),
- scope = scope,
- produceFile = { context.preferencesDataStoreFile("custom_emoji_ds") },
- )
-
- @Provides
- @Singleton
- @MapDataStore
- fun provideMapDataStore(@ApplicationContext context: Context): DataStore =
- PreferenceDataStoreFactory.create(
- migrations = listOf(SharedPreferencesMigration(context, "map_prefs")),
- scope = scope,
- produceFile = { context.preferencesDataStoreFile("map_ds") },
- )
-
- @Provides
- @Singleton
- @MapConsentDataStore
- fun provideMapConsentDataStore(@ApplicationContext context: Context): DataStore =
- PreferenceDataStoreFactory.create(
- migrations = listOf(SharedPreferencesMigration(context, "map_consent_preferences")),
- scope = scope,
- produceFile = { context.preferencesDataStoreFile("map_consent_ds") },
- )
-
- @Provides
- @Singleton
- @MapTileProviderDataStore
- fun provideMapTileProviderDataStore(@ApplicationContext context: Context): DataStore =
- PreferenceDataStoreFactory.create(
- migrations = listOf(SharedPreferencesMigration(context, "map_tile_provider_prefs")),
- scope = scope,
- produceFile = { context.preferencesDataStoreFile("map_tile_provider_ds") },
- )
-
- @Provides
- @Singleton
- @MeshDataStore
- fun provideMeshDataStore(@ApplicationContext context: Context): DataStore =
- PreferenceDataStoreFactory.create(
- migrations = listOf(SharedPreferencesMigration(context, "mesh-prefs")),
- scope = scope,
- produceFile = { context.preferencesDataStoreFile("mesh_ds") },
- )
-
- @Provides
- @Singleton
- @RadioDataStore
- fun provideRadioDataStore(@ApplicationContext context: Context): DataStore =
- PreferenceDataStoreFactory.create(
- migrations = listOf(SharedPreferencesMigration(context, "radio-prefs")),
- scope = scope,
- produceFile = { context.preferencesDataStoreFile("radio_ds") },
- )
-
- @Provides
- @Singleton
- @UiDataStore
- fun provideUiDataStore(@ApplicationContext context: Context): DataStore =
- PreferenceDataStoreFactory.create(
- migrations = listOf(SharedPreferencesMigration(context, "ui-prefs")),
- scope = scope,
- produceFile = { context.preferencesDataStoreFile("ui_ds") },
- )
-
- @Provides
- @Singleton
- @MeshLogDataStore
- fun provideMeshLogDataStore(@ApplicationContext context: Context): DataStore =
- PreferenceDataStoreFactory.create(
- migrations = listOf(SharedPreferencesMigration(context, "meshlog-prefs")),
- scope = scope,
- produceFile = { context.preferencesDataStoreFile("meshlog_ds") },
- )
-
- @Provides
- @Singleton
- @FilterDataStore
- fun provideFilterDataStore(@ApplicationContext context: Context): DataStore =
- PreferenceDataStoreFactory.create(
- migrations = listOf(SharedPreferencesMigration(context, "filter-prefs")),
- scope = scope,
- produceFile = { context.preferencesDataStoreFile("filter_ds") },
- )
- }
-}
diff --git a/app/src/main/kotlin/org/meshtastic/app/di/RepositoryModule.kt b/app/src/main/kotlin/org/meshtastic/app/di/RepositoryModule.kt
deleted file mode 100644
index 98c19f5bc..000000000
--- a/app/src/main/kotlin/org/meshtastic/app/di/RepositoryModule.kt
+++ /dev/null
@@ -1,163 +0,0 @@
-/*
- * 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 org.meshtastic.app.di
-
-import dagger.Binds
-import dagger.Module
-import dagger.Provides
-import dagger.hilt.InstallIn
-import dagger.hilt.components.SingletonComponent
-import org.meshtastic.core.data.manager.CommandSenderImpl
-import org.meshtastic.core.data.manager.FromRadioPacketHandlerImpl
-import org.meshtastic.core.data.manager.HistoryManagerImpl
-import org.meshtastic.core.data.manager.MeshActionHandlerImpl
-import org.meshtastic.core.data.manager.MeshConfigFlowManagerImpl
-import org.meshtastic.core.data.manager.MeshConfigHandlerImpl
-import org.meshtastic.core.data.manager.MeshConnectionManagerImpl
-import org.meshtastic.core.data.manager.MeshDataHandlerImpl
-import org.meshtastic.core.data.manager.MeshMessageProcessorImpl
-import org.meshtastic.core.data.manager.MeshRouterImpl
-import org.meshtastic.core.data.manager.MessageFilterImpl
-import org.meshtastic.core.data.manager.MqttManagerImpl
-import org.meshtastic.core.data.manager.NeighborInfoHandlerImpl
-import org.meshtastic.core.data.manager.NodeManagerImpl
-import org.meshtastic.core.data.manager.PacketHandlerImpl
-import org.meshtastic.core.data.manager.TracerouteHandlerImpl
-import org.meshtastic.core.data.repository.DeviceHardwareRepositoryImpl
-import org.meshtastic.core.data.repository.LocationRepositoryImpl
-import org.meshtastic.core.data.repository.MeshLogRepositoryImpl
-import org.meshtastic.core.data.repository.NodeRepositoryImpl
-import org.meshtastic.core.data.repository.PacketRepositoryImpl
-import org.meshtastic.core.data.repository.RadioConfigRepositoryImpl
-import org.meshtastic.core.model.util.MeshDataMapper
-import org.meshtastic.core.repository.CommandSender
-import org.meshtastic.core.repository.DeviceHardwareRepository
-import org.meshtastic.core.repository.FromRadioPacketHandler
-import org.meshtastic.core.repository.HistoryManager
-import org.meshtastic.core.repository.LocationRepository
-import org.meshtastic.core.repository.MeshActionHandler
-import org.meshtastic.core.repository.MeshConfigFlowManager
-import org.meshtastic.core.repository.MeshConfigHandler
-import org.meshtastic.core.repository.MeshConnectionManager
-import org.meshtastic.core.repository.MeshDataHandler
-import org.meshtastic.core.repository.MeshLogRepository
-import org.meshtastic.core.repository.MeshMessageProcessor
-import org.meshtastic.core.repository.MeshRouter
-import org.meshtastic.core.repository.MessageFilter
-import org.meshtastic.core.repository.MqttManager
-import org.meshtastic.core.repository.NeighborInfoHandler
-import org.meshtastic.core.repository.NodeManager
-import org.meshtastic.core.repository.NodeRepository
-import org.meshtastic.core.repository.PacketHandler
-import org.meshtastic.core.repository.PacketRepository
-import org.meshtastic.core.repository.RadioConfigRepository
-import org.meshtastic.core.repository.TracerouteHandler
-import javax.inject.Singleton
-
-@Suppress("TooManyFunctions")
-@Module
-@InstallIn(SingletonComponent::class)
-abstract class RepositoryModule {
-
- @Binds @Singleton
- abstract fun bindNodeRepository(nodeRepositoryImpl: NodeRepositoryImpl): NodeRepository
-
- @Binds
- @Singleton
- abstract fun bindRadioConfigRepository(radioConfigRepositoryImpl: RadioConfigRepositoryImpl): RadioConfigRepository
-
- @Binds
- @Singleton
- abstract fun bindLocationRepository(locationRepositoryImpl: LocationRepositoryImpl): LocationRepository
-
- @Binds
- @Singleton
- abstract fun bindDeviceHardwareRepository(
- deviceHardwareRepositoryImpl: DeviceHardwareRepositoryImpl,
- ): DeviceHardwareRepository
-
- @Binds @Singleton
- abstract fun bindPacketRepository(packetRepositoryImpl: PacketRepositoryImpl): PacketRepository
-
- @Binds
- @Singleton
- abstract fun bindMeshLogRepository(meshLogRepositoryImpl: MeshLogRepositoryImpl): MeshLogRepository
-
- @Binds @Singleton
- abstract fun bindNodeManager(nodeManagerImpl: NodeManagerImpl): NodeManager
-
- @Binds @Singleton
- abstract fun bindCommandSender(commandSenderImpl: CommandSenderImpl): CommandSender
-
- @Binds @Singleton
- abstract fun bindHistoryManager(historyManagerImpl: HistoryManagerImpl): HistoryManager
-
- @Binds
- @Singleton
- abstract fun bindTracerouteHandler(tracerouteHandlerImpl: TracerouteHandlerImpl): TracerouteHandler
-
- @Binds
- @Singleton
- abstract fun bindNeighborInfoHandler(neighborInfoHandlerImpl: NeighborInfoHandlerImpl): NeighborInfoHandler
-
- @Binds @Singleton
- abstract fun bindMqttManager(mqttManagerImpl: MqttManagerImpl): MqttManager
-
- @Binds @Singleton
- abstract fun bindPacketHandler(packetHandlerImpl: PacketHandlerImpl): PacketHandler
-
- @Binds
- @Singleton
- abstract fun bindMeshConnectionManager(meshConnectionManagerImpl: MeshConnectionManagerImpl): MeshConnectionManager
-
- @Binds @Singleton
- abstract fun bindMeshDataHandler(meshDataHandlerImpl: MeshDataHandlerImpl): MeshDataHandler
-
- @Binds
- @Singleton
- abstract fun bindMeshActionHandler(meshActionHandlerImpl: MeshActionHandlerImpl): MeshActionHandler
-
- @Binds
- @Singleton
- abstract fun bindMeshMessageProcessor(meshMessageProcessorImpl: MeshMessageProcessorImpl): MeshMessageProcessor
-
- @Binds @Singleton
- abstract fun bindMeshRouter(meshRouterImpl: MeshRouterImpl): MeshRouter
-
- @Binds
- @Singleton
- abstract fun bindFromRadioPacketHandler(
- fromRadioPacketHandlerImpl: FromRadioPacketHandlerImpl,
- ): FromRadioPacketHandler
-
- @Binds
- @Singleton
- abstract fun bindMeshConfigHandler(meshConfigHandlerImpl: MeshConfigHandlerImpl): MeshConfigHandler
-
- @Binds
- @Singleton
- abstract fun bindMeshConfigFlowManager(meshConfigFlowManagerImpl: MeshConfigFlowManagerImpl): MeshConfigFlowManager
-
- @Binds @Singleton
- abstract fun bindMessageFilter(messageFilterImpl: MessageFilterImpl): MessageFilter
-
- companion object {
- @Provides
- @Singleton
- fun provideMeshDataMapper(nodeManager: NodeManager): MeshDataMapper = MeshDataMapper(nodeManager)
- }
-}
diff --git a/app/src/main/kotlin/org/meshtastic/app/di/ServiceModule.kt b/app/src/main/kotlin/org/meshtastic/app/di/ServiceModule.kt
deleted file mode 100644
index 918da974d..000000000
--- a/app/src/main/kotlin/org/meshtastic/app/di/ServiceModule.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright (c) 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 org.meshtastic.app.di
-
-import dagger.Binds
-import dagger.Module
-import dagger.hilt.InstallIn
-import dagger.hilt.components.SingletonComponent
-import org.meshtastic.core.model.RadioController
-import org.meshtastic.core.repository.ServiceRepository
-import org.meshtastic.core.service.AndroidRadioControllerImpl
-import org.meshtastic.core.service.AndroidServiceRepository
-import javax.inject.Singleton
-
-@Module
-@InstallIn(SingletonComponent::class)
-abstract class ServiceModule {
-
- @Binds @Singleton
- abstract fun bindRadioController(impl: AndroidRadioControllerImpl): RadioController
-
- @Binds @Singleton
- abstract fun bindServiceRepository(impl: AndroidServiceRepository): ServiceRepository
-}
diff --git a/app/src/main/kotlin/org/meshtastic/app/domain/usecase/GetDiscoveredDevicesUseCase.kt b/app/src/main/kotlin/org/meshtastic/app/domain/usecase/GetDiscoveredDevicesUseCase.kt
index 4d009e862..badfda791 100644
--- a/app/src/main/kotlin/org/meshtastic/app/domain/usecase/GetDiscoveredDevicesUseCase.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/domain/usecase/GetDiscoveredDevicesUseCase.kt
@@ -22,6 +22,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import org.jetbrains.compose.resources.getString
+import org.koin.core.annotation.Single
import org.meshtastic.app.model.DeviceListEntry
import org.meshtastic.app.model.getMeshtasticShortName
import org.meshtastic.app.repository.network.NetworkRepository
@@ -37,7 +38,6 @@ import org.meshtastic.core.repository.RadioInterfaceService
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,
@@ -47,9 +47,8 @@ data class DiscoveredDevices(
)
@Suppress("LongParameterList")
-class GetDiscoveredDevicesUseCase
-@Inject
-constructor(
+@Single
+class GetDiscoveredDevicesUseCase(
private val bluetoothRepository: BluetoothRepository,
private val networkRepository: NetworkRepository,
private val recentAddressesDataSource: RecentAddressesDataSource,
@@ -57,7 +56,7 @@ constructor(
private val databaseManager: DatabaseManager,
private val usbRepository: UsbRepository,
private val radioInterfaceService: RadioInterfaceService,
- private val usbManagerLazy: dagger.Lazy,
+ private val usbManagerLazy: Lazy,
) {
private val suffixLength = 4
@@ -94,7 +93,7 @@ constructor(
val usbDevicesFlow =
usbRepository.serialDevices.map { usb ->
- usb.map { (_, d) -> DeviceListEntry.Usb(radioInterfaceService, usbManagerLazy.get(), d) }
+ usb.map { (_, d) -> DeviceListEntry.Usb(radioInterfaceService, usbManagerLazy.value, d) }
}
return combine(
diff --git a/app/src/main/kotlin/org/meshtastic/app/firmware/AndroidFirmwareUpdateViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/firmware/AndroidFirmwareUpdateViewModel.kt
new file mode 100644
index 000000000..182863c9d
--- /dev/null
+++ b/app/src/main/kotlin/org/meshtastic/app/firmware/AndroidFirmwareUpdateViewModel.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 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 org.meshtastic.app.firmware
+
+import org.koin.core.annotation.KoinViewModel
+import org.meshtastic.core.data.repository.FirmwareReleaseRepository
+import org.meshtastic.core.datastore.BootloaderWarningDataSource
+import org.meshtastic.core.model.RadioController
+import org.meshtastic.core.repository.DeviceHardwareRepository
+import org.meshtastic.core.repository.NodeRepository
+import org.meshtastic.core.repository.RadioPrefs
+import org.meshtastic.feature.firmware.FirmwareFileHandler
+import org.meshtastic.feature.firmware.FirmwareUpdateManager
+import org.meshtastic.feature.firmware.FirmwareUpdateViewModel
+import org.meshtastic.feature.firmware.FirmwareUsbManager
+
+@Suppress("LongParameterList")
+@KoinViewModel
+class AndroidFirmwareUpdateViewModel(
+ firmwareReleaseRepository: FirmwareReleaseRepository,
+ deviceHardwareRepository: DeviceHardwareRepository,
+ nodeRepository: NodeRepository,
+ radioController: RadioController,
+ radioPrefs: RadioPrefs,
+ bootloaderWarningDataSource: BootloaderWarningDataSource,
+ firmwareUpdateManager: FirmwareUpdateManager,
+ usbManager: FirmwareUsbManager,
+ fileHandler: FirmwareFileHandler,
+) : FirmwareUpdateViewModel(
+ firmwareReleaseRepository,
+ deviceHardwareRepository,
+ nodeRepository,
+ radioController,
+ radioPrefs,
+ bootloaderWarningDataSource,
+ firmwareUpdateManager,
+ usbManager,
+ fileHandler,
+)
diff --git a/app/src/main/kotlin/org/meshtastic/app/intro/AndroidIntroViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/intro/AndroidIntroViewModel.kt
index 0414e37bf..c387f2e20 100644
--- a/app/src/main/kotlin/org/meshtastic/app/intro/AndroidIntroViewModel.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/intro/AndroidIntroViewModel.kt
@@ -16,9 +16,8 @@
*/
package org.meshtastic.app.intro
-import dagger.hilt.android.lifecycle.HiltViewModel
+import org.koin.core.annotation.KoinViewModel
import org.meshtastic.feature.intro.IntroViewModel
-import javax.inject.Inject
-/** Android-specific Hilt wrapper for IntroViewModel. */
-@HiltViewModel class AndroidIntroViewModel @Inject constructor() : IntroViewModel()
+/** Android-specific Koin wrapper for IntroViewModel. */
+@KoinViewModel class AndroidIntroViewModel : IntroViewModel()
diff --git a/app/src/main/kotlin/org/meshtastic/app/map/AndroidSharedMapViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/map/AndroidSharedMapViewModel.kt
index 24ebe7995..38a2e0746 100644
--- a/app/src/main/kotlin/org/meshtastic/app/map/AndroidSharedMapViewModel.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/map/AndroidSharedMapViewModel.kt
@@ -16,18 +16,15 @@
*/
package org.meshtastic.app.map
-import dagger.hilt.android.lifecycle.HiltViewModel
+import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.repository.MapPrefs
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.PacketRepository
import org.meshtastic.feature.map.SharedMapViewModel
-import javax.inject.Inject
-@HiltViewModel
-class AndroidSharedMapViewModel
-@Inject
-constructor(
+@KoinViewModel
+class AndroidSharedMapViewModel(
mapPrefs: MapPrefs,
nodeRepository: NodeRepository,
packetRepository: PacketRepository,
diff --git a/app/src/main/kotlin/org/meshtastic/app/map/node/NodeMapViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/map/node/NodeMapViewModel.kt
index a8780be59..63737002a 100644
--- a/app/src/main/kotlin/org/meshtastic/app/map/node/NodeMapViewModel.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/map/node/NodeMapViewModel.kt
@@ -19,7 +19,6 @@ package org.meshtastic.app.map.node
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.navigation.toRoute
-import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -27,6 +26,7 @@ import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.toList
+import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.common.BuildConfigProvider
import org.meshtastic.core.database.entity.MeshLog
import org.meshtastic.core.navigation.NodesRoutes
@@ -37,12 +37,9 @@ import org.meshtastic.core.ui.util.toPosition
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.proto.PortNum
import org.meshtastic.proto.Position
-import javax.inject.Inject
-@HiltViewModel
-class NodeMapViewModel
-@Inject
-constructor(
+@KoinViewModel
+class NodeMapViewModel(
savedStateHandle: SavedStateHandle,
nodeRepository: NodeRepository,
meshLogRepository: MeshLogRepository,
diff --git a/app/src/main/kotlin/org/meshtastic/app/messaging/AndroidContactsViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/messaging/AndroidContactsViewModel.kt
index e8a23a17a..8c56a2b62 100644
--- a/app/src/main/kotlin/org/meshtastic/app/messaging/AndroidContactsViewModel.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/messaging/AndroidContactsViewModel.kt
@@ -16,18 +16,15 @@
*/
package org.meshtastic.app.messaging
-import dagger.hilt.android.lifecycle.HiltViewModel
+import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.PacketRepository
import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.feature.messaging.ui.contact.ContactsViewModel
-import javax.inject.Inject
-@HiltViewModel
-class AndroidContactsViewModel
-@Inject
-constructor(
+@KoinViewModel
+class AndroidContactsViewModel(
nodeRepository: NodeRepository,
packetRepository: PacketRepository,
radioConfigRepository: RadioConfigRepository,
diff --git a/app/src/main/kotlin/org/meshtastic/app/messaging/AndroidMessageViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/messaging/AndroidMessageViewModel.kt
index ee7f4e7bb..a352b1804 100644
--- a/app/src/main/kotlin/org/meshtastic/app/messaging/AndroidMessageViewModel.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/messaging/AndroidMessageViewModel.kt
@@ -17,7 +17,7 @@
package org.meshtastic.app.messaging
import androidx.lifecycle.SavedStateHandle
-import dagger.hilt.android.lifecycle.HiltViewModel
+import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.data.repository.QuickChatActionRepository
import org.meshtastic.core.repository.CustomEmojiPrefs
import org.meshtastic.core.repository.HomoglyphPrefs
@@ -29,13 +29,10 @@ import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.repository.UiPrefs
import org.meshtastic.core.repository.usecase.SendMessageUseCase
import org.meshtastic.feature.messaging.MessageViewModel
-import javax.inject.Inject
@Suppress("LongParameterList")
-@HiltViewModel
-class AndroidMessageViewModel
-@Inject
-constructor(
+@KoinViewModel
+class AndroidMessageViewModel(
savedStateHandle: SavedStateHandle,
nodeRepository: NodeRepository,
radioConfigRepository: RadioConfigRepository,
diff --git a/app/src/main/kotlin/org/meshtastic/app/messaging/AndroidQuickChatViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/messaging/AndroidQuickChatViewModel.kt
index b64e5de24..1346b8b54 100644
--- a/app/src/main/kotlin/org/meshtastic/app/messaging/AndroidQuickChatViewModel.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/messaging/AndroidQuickChatViewModel.kt
@@ -16,11 +16,10 @@
*/
package org.meshtastic.app.messaging
-import dagger.hilt.android.lifecycle.HiltViewModel
+import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.data.repository.QuickChatActionRepository
import org.meshtastic.feature.messaging.QuickChatViewModel
-import javax.inject.Inject
-@HiltViewModel
-class AndroidQuickChatViewModel @Inject constructor(quickChatActionRepository: QuickChatActionRepository) :
+@KoinViewModel
+class AndroidQuickChatViewModel(quickChatActionRepository: QuickChatActionRepository) :
QuickChatViewModel(quickChatActionRepository)
diff --git a/app/src/main/kotlin/org/meshtastic/app/messaging/domain/worker/SendMessageWorker.kt b/app/src/main/kotlin/org/meshtastic/app/messaging/domain/worker/SendMessageWorker.kt
index 3b4b8f4d8..19fb3324e 100644
--- a/app/src/main/kotlin/org/meshtastic/app/messaging/domain/worker/SendMessageWorker.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/messaging/domain/worker/SendMessageWorker.kt
@@ -17,22 +17,18 @@
package org.meshtastic.app.messaging.domain.worker
import android.content.Context
-import androidx.hilt.work.HiltWorker
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
+import org.koin.android.annotation.KoinWorker
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.MessageStatus
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.repository.PacketRepository
-@HiltWorker
-class SendMessageWorker
-@AssistedInject
-constructor(
- @Assisted context: Context,
- @Assisted params: WorkerParameters,
+@KoinWorker
+class SendMessageWorker(
+ context: Context,
+ params: WorkerParameters,
private val packetRepository: PacketRepository,
private val radioController: RadioController,
) : CoroutineWorker(context, params) {
diff --git a/app/src/main/kotlin/org/meshtastic/app/messaging/domain/worker/WorkManagerMessageQueue.kt b/app/src/main/kotlin/org/meshtastic/app/messaging/domain/worker/WorkManagerMessageQueue.kt
index ea26e2c6c..cabc51caa 100644
--- a/app/src/main/kotlin/org/meshtastic/app/messaging/domain/worker/WorkManagerMessageQueue.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/messaging/domain/worker/WorkManagerMessageQueue.kt
@@ -20,13 +20,12 @@ import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.workDataOf
+import org.koin.core.annotation.Single
import org.meshtastic.core.repository.MessageQueue
-import javax.inject.Inject
-import javax.inject.Singleton
/** Android implementation of [MessageQueue] that uses [WorkManager] for reliable background transmission. */
-@Singleton
-class WorkManagerMessageQueue @Inject constructor(private val workManager: WorkManager) : MessageQueue {
+@Single
+class WorkManagerMessageQueue(private val workManager: WorkManager) : MessageQueue {
override suspend fun enqueue(packetId: Int) {
val workRequest =
diff --git a/app/src/main/kotlin/org/meshtastic/app/model/UIViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/model/UIViewModel.kt
index 54b2f6f2a..d82619961 100644
--- a/app/src/main/kotlin/org/meshtastic/app/model/UIViewModel.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/model/UIViewModel.kt
@@ -20,7 +20,6 @@ import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
-import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -35,6 +34,7 @@ import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.onEach
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.getString
+import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.data.repository.FirmwareReleaseRepository
import org.meshtastic.core.database.entity.asDeviceVersion
import org.meshtastic.core.datastore.UiPreferencesDataSource
@@ -62,13 +62,10 @@ import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.proto.ChannelSet
import org.meshtastic.proto.ClientNotification
import org.meshtastic.proto.SharedContact
-import javax.inject.Inject
-@HiltViewModel
+@KoinViewModel
@Suppress("LongParameterList", "TooManyFunctions")
-class UIViewModel
-@Inject
-constructor(
+class UIViewModel(
private val nodeDB: NodeRepository,
private val serviceRepository: AndroidServiceRepository,
private val radioController: RadioController,
diff --git a/app/src/main/kotlin/org/meshtastic/app/navigation/ChannelsNavigation.kt b/app/src/main/kotlin/org/meshtastic/app/navigation/ChannelsNavigation.kt
index 819d72e13..bcc47ddc1 100644
--- a/app/src/main/kotlin/org/meshtastic/app/navigation/ChannelsNavigation.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/navigation/ChannelsNavigation.kt
@@ -17,12 +17,13 @@
package org.meshtastic.app.navigation
import androidx.compose.runtime.remember
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import androidx.navigation.navDeepLink
import androidx.navigation.navigation
+import org.koin.compose.viewmodel.koinViewModel
+import org.meshtastic.app.settings.AndroidRadioConfigViewModel
import org.meshtastic.app.ui.sharing.ChannelScreen
import org.meshtastic.core.navigation.ChannelsRoutes
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
@@ -38,7 +39,7 @@ fun NavGraphBuilder.channelsGraph(navController: NavHostController) {
) { backStackEntry ->
val parentEntry = remember(backStackEntry) { navController.getBackStackEntry(ChannelsRoutes.ChannelsGraph) }
ChannelScreen(
- radioConfigViewModel = hiltViewModel(parentEntry),
+ radioConfigViewModel = koinViewModel(viewModelStoreOwner = parentEntry),
onNavigate = { route -> navController.navigate(route) },
onNavigateUp = { navController.navigateUp() },
)
diff --git a/app/src/main/kotlin/org/meshtastic/app/navigation/ConnectionsNavigation.kt b/app/src/main/kotlin/org/meshtastic/app/navigation/ConnectionsNavigation.kt
index 4ece8d6a5..02173ab7a 100644
--- a/app/src/main/kotlin/org/meshtastic/app/navigation/ConnectionsNavigation.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/navigation/ConnectionsNavigation.kt
@@ -17,12 +17,13 @@
package org.meshtastic.app.navigation
import androidx.compose.runtime.remember
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import androidx.navigation.navDeepLink
import androidx.navigation.navigation
+import org.koin.compose.viewmodel.koinViewModel
+import org.meshtastic.app.settings.AndroidRadioConfigViewModel
import org.meshtastic.app.ui.connections.ConnectionsScreen
import org.meshtastic.core.navigation.ConnectionsRoutes
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
@@ -42,7 +43,7 @@ fun NavGraphBuilder.connectionsGraph(navController: NavHostController) {
val parentEntry =
remember(backStackEntry) { navController.getBackStackEntry(ConnectionsRoutes.ConnectionsGraph) }
ConnectionsScreen(
- radioConfigViewModel = hiltViewModel(parentEntry),
+ radioConfigViewModel = koinViewModel(viewModelStoreOwner = parentEntry),
onClickNodeChip = {
navController.navigate(NodesRoutes.NodeDetailGraph(it)) {
launchSingleTop = true
diff --git a/app/src/main/kotlin/org/meshtastic/app/navigation/ContactsNavigation.kt b/app/src/main/kotlin/org/meshtastic/app/navigation/ContactsNavigation.kt
index 130196bc1..7f4a86e63 100644
--- a/app/src/main/kotlin/org/meshtastic/app/navigation/ContactsNavigation.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/navigation/ContactsNavigation.kt
@@ -17,7 +17,6 @@
package org.meshtastic.app.navigation
import androidx.compose.runtime.getValue
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
@@ -26,6 +25,7 @@ import androidx.navigation.navDeepLink
import androidx.navigation.navigation
import androidx.navigation.toRoute
import kotlinx.coroutines.flow.Flow
+import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.app.messaging.AndroidContactsViewModel
import org.meshtastic.app.messaging.AndroidMessageViewModel
import org.meshtastic.app.messaging.AndroidQuickChatViewModel
@@ -43,11 +43,11 @@ fun NavGraphBuilder.contactsGraph(navController: NavHostController, scrollToTopE
composable(
deepLinks = listOf(navDeepLink(basePath = "$DEEP_LINK_BASE_URI/contacts")),
) {
- val uiViewModel: UIViewModel = hiltViewModel()
+ val uiViewModel: UIViewModel = koinViewModel()
val sharedContactRequested by uiViewModel.sharedContactRequested.collectAsStateWithLifecycle()
val requestChannelSet by uiViewModel.requestChannelSet.collectAsStateWithLifecycle()
- val contactsViewModel = hiltViewModel()
- val messageViewModel = hiltViewModel()
+ val contactsViewModel = koinViewModel()
+ val messageViewModel = koinViewModel()
AdaptiveContactsScreen(
navController = navController,
@@ -71,11 +71,11 @@ fun NavGraphBuilder.contactsGraph(navController: NavHostController, scrollToTopE
),
) { backStackEntry ->
val args = backStackEntry.toRoute()
- val uiViewModel: UIViewModel = hiltViewModel()
+ val uiViewModel: UIViewModel = koinViewModel()
val sharedContactRequested by uiViewModel.sharedContactRequested.collectAsStateWithLifecycle()
val requestChannelSet by uiViewModel.requestChannelSet.collectAsStateWithLifecycle()
- val contactsViewModel = hiltViewModel()
- val messageViewModel = hiltViewModel()
+ val contactsViewModel = koinViewModel()
+ val messageViewModel = koinViewModel()
AdaptiveContactsScreen(
navController = navController,
@@ -101,7 +101,7 @@ fun NavGraphBuilder.contactsGraph(navController: NavHostController, scrollToTopE
),
) { backStackEntry ->
val message = backStackEntry.toRoute().message
- val viewModel = hiltViewModel()
+ val viewModel = koinViewModel()
ShareScreen(
viewModel = viewModel,
onConfirm = {
@@ -115,7 +115,7 @@ fun NavGraphBuilder.contactsGraph(navController: NavHostController, scrollToTopE
composable(
deepLinks = listOf(navDeepLink(basePath = "$DEEP_LINK_BASE_URI/quick_chat")),
) {
- val viewModel = hiltViewModel()
+ val viewModel = koinViewModel()
QuickChatScreen(viewModel = viewModel, onNavigateUp = navController::navigateUp)
}
}
diff --git a/app/src/main/kotlin/org/meshtastic/app/navigation/FirmwareNavigation.kt b/app/src/main/kotlin/org/meshtastic/app/navigation/FirmwareNavigation.kt
index 88439d6c8..5ab3efcdd 100644
--- a/app/src/main/kotlin/org/meshtastic/app/navigation/FirmwareNavigation.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/navigation/FirmwareNavigation.kt
@@ -19,12 +19,17 @@ package org.meshtastic.app.navigation
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
-import androidx.navigation.navigation
+import androidx.navigation.compose.navigation
+import org.koin.compose.viewmodel.koinViewModel
+import org.meshtastic.app.firmware.AndroidFirmwareUpdateViewModel
import org.meshtastic.core.navigation.FirmwareRoutes
import org.meshtastic.feature.firmware.FirmwareUpdateScreen
fun NavGraphBuilder.firmwareGraph(navController: NavController) {
navigation(startDestination = FirmwareRoutes.FirmwareUpdate) {
- composable { FirmwareUpdateScreen(navController) }
+ composable {
+ val viewModel = koinViewModel()
+ FirmwareUpdateScreen(onNavigateUp = { navController.navigateUp() }, viewModel = viewModel)
+ }
}
}
diff --git a/app/src/main/kotlin/org/meshtastic/app/navigation/MapNavigation.kt b/app/src/main/kotlin/org/meshtastic/app/navigation/MapNavigation.kt
index 71adb01cc..28f2ea3e8 100644
--- a/app/src/main/kotlin/org/meshtastic/app/navigation/MapNavigation.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/navigation/MapNavigation.kt
@@ -16,11 +16,11 @@
*/
package org.meshtastic.app.navigation
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import androidx.navigation.navDeepLink
+import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.app.map.AndroidSharedMapViewModel
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
import org.meshtastic.core.navigation.MapRoutes
@@ -29,7 +29,7 @@ import org.meshtastic.feature.map.MapScreen
fun NavGraphBuilder.mapGraph(navController: NavHostController) {
composable(deepLinks = listOf(navDeepLink(basePath = "$DEEP_LINK_BASE_URI/map"))) {
- val viewModel = hiltViewModel()
+ val viewModel = koinViewModel()
MapScreen(
viewModel = viewModel,
onClickNodeChip = {
diff --git a/app/src/main/kotlin/org/meshtastic/app/navigation/NodesNavigation.kt b/app/src/main/kotlin/org/meshtastic/app/navigation/NodesNavigation.kt
index 56d44b6f4..a8dc4c131 100644
--- a/app/src/main/kotlin/org/meshtastic/app/navigation/NodesNavigation.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/navigation/NodesNavigation.kt
@@ -29,7 +29,6 @@ import androidx.compose.material.icons.rounded.Router
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.NavGraphBuilder
@@ -40,8 +39,10 @@ import androidx.navigation.navDeepLink
import androidx.navigation.toRoute
import kotlinx.coroutines.flow.Flow
import org.jetbrains.compose.resources.StringResource
+import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.app.map.node.NodeMapScreen
import org.meshtastic.app.map.node.NodeMapViewModel
+import org.meshtastic.app.node.AndroidMetricsViewModel
import org.meshtastic.app.ui.node.AdaptiveNodeListScreen
import org.meshtastic.core.navigation.ContactsRoutes
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
@@ -120,7 +121,7 @@ fun NavGraphBuilder.nodeDetailGraph(navController: NavHostController, scrollToTo
) { backStackEntry ->
val parentGraphBackStackEntry =
remember(backStackEntry) { navController.getBackStackEntry(NodesRoutes.NodeDetailGraph::class) }
- val vm = hiltViewModel(parentGraphBackStackEntry)
+ val vm = koinViewModel(viewModelStoreOwner = parentGraphBackStackEntry)
NodeMapScreen(vm, onNavigateUp = navController::navigateUp)
}
@@ -135,7 +136,8 @@ fun NavGraphBuilder.nodeDetailGraph(navController: NavHostController, scrollToTo
) { backStackEntry ->
val parentGraphBackStackEntry =
remember(backStackEntry) { navController.getBackStackEntry(NodesRoutes.NodeDetailGraph::class) }
- val metricsViewModel = hiltViewModel(parentGraphBackStackEntry)
+ val metricsViewModel =
+ koinViewModel(viewModelStoreOwner = parentGraphBackStackEntry)
val args = backStackEntry.toRoute()
metricsViewModel.setNodeId(args.destNum)
@@ -166,7 +168,8 @@ fun NavGraphBuilder.nodeDetailGraph(navController: NavHostController, scrollToTo
) { backStackEntry ->
val parentGraphBackStackEntry =
remember(backStackEntry) { navController.getBackStackEntry(NodesRoutes.NodeDetailGraph::class) }
- val metricsViewModel = hiltViewModel(parentGraphBackStackEntry)
+ val metricsViewModel =
+ koinViewModel(viewModelStoreOwner = parentGraphBackStackEntry)
val args = backStackEntry.toRoute()
metricsViewModel.setNodeId(args.destNum)
@@ -277,7 +280,7 @@ private inline fun NavGraphBuilder.addNodeDetailScreenCompos
) { backStackEntry ->
val parentGraphBackStackEntry =
remember(backStackEntry) { navController.getBackStackEntry(NodesRoutes.NodeDetailGraph::class) }
- val metricsViewModel = hiltViewModel(parentGraphBackStackEntry)
+ val metricsViewModel = koinViewModel(viewModelStoreOwner = parentGraphBackStackEntry)
val args = backStackEntry.toRoute()
val destNum = getDestNum(args)
diff --git a/app/src/main/kotlin/org/meshtastic/app/navigation/SettingsNavigation.kt b/app/src/main/kotlin/org/meshtastic/app/navigation/SettingsNavigation.kt
index eebe1db28..f440fdfc3 100644
--- a/app/src/main/kotlin/org/meshtastic/app/navigation/SettingsNavigation.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/navigation/SettingsNavigation.kt
@@ -22,13 +22,18 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import androidx.navigation.navDeepLink
import androidx.navigation.navigation
+import org.koin.compose.viewmodel.koinViewModel
+import org.meshtastic.app.settings.AndroidCleanNodeDatabaseViewModel
+import org.meshtastic.app.settings.AndroidDebugViewModel
+import org.meshtastic.app.settings.AndroidFilterSettingsViewModel
+import org.meshtastic.app.settings.AndroidRadioConfigViewModel
+import org.meshtastic.app.settings.AndroidSettingsViewModel
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
import org.meshtastic.core.navigation.Graph
import org.meshtastic.core.navigation.NodesRoutes
@@ -39,13 +44,11 @@ import org.meshtastic.feature.settings.AdministrationScreen
import org.meshtastic.feature.settings.DeviceConfigurationScreen
import org.meshtastic.feature.settings.ModuleConfigurationScreen
import org.meshtastic.feature.settings.SettingsScreen
-import org.meshtastic.feature.settings.SettingsViewModel
import org.meshtastic.feature.settings.debugging.DebugScreen
import org.meshtastic.feature.settings.filter.FilterSettingsScreen
import org.meshtastic.feature.settings.navigation.ConfigRoute
import org.meshtastic.feature.settings.navigation.ModuleRoute
import org.meshtastic.feature.settings.radio.CleanNodeDatabaseScreen
-import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.feature.settings.radio.channel.ChannelConfigScreen
import org.meshtastic.feature.settings.radio.component.AmbientLightingConfigScreen
import org.meshtastic.feature.settings.radio.component.AudioConfigScreen
@@ -83,8 +86,8 @@ fun NavGraphBuilder.settingsGraph(navController: NavHostController) {
val parentEntry =
remember(backStackEntry) { navController.getBackStackEntry(SettingsRoutes.SettingsGraph::class) }
SettingsScreen(
- settingsViewModel = hiltViewModel(parentEntry),
- viewModel = hiltViewModel(parentEntry),
+ settingsViewModel = koinViewModel(viewModelStoreOwner = parentEntry),
+ viewModel = koinViewModel(viewModelStoreOwner = parentEntry),
onClickNodeChip = {
navController.navigate(NodesRoutes.NodeDetailGraph(it)) {
launchSingleTop = true
@@ -100,7 +103,7 @@ fun NavGraphBuilder.settingsGraph(navController: NavHostController) {
val parentEntry =
remember(backStackEntry) { navController.getBackStackEntry(SettingsRoutes.SettingsGraph::class) }
DeviceConfigurationScreen(
- viewModel = hiltViewModel(parentEntry),
+ viewModel = koinViewModel(viewModelStoreOwner = parentEntry),
onBack = navController::popBackStack,
onNavigate = { route -> navController.navigate(route) },
)
@@ -109,10 +112,10 @@ fun NavGraphBuilder.settingsGraph(navController: NavHostController) {
composable { backStackEntry ->
val parentEntry =
remember(backStackEntry) { navController.getBackStackEntry(SettingsRoutes.SettingsGraph::class) }
- val settingsViewModel: SettingsViewModel = hiltViewModel(parentEntry)
+ val settingsViewModel: AndroidSettingsViewModel = koinViewModel(viewModelStoreOwner = parentEntry)
val excludedModulesUnlocked by settingsViewModel.excludedModulesUnlocked.collectAsStateWithLifecycle()
ModuleConfigurationScreen(
- viewModel = hiltViewModel(parentEntry),
+ viewModel = koinViewModel(viewModelStoreOwner = parentEntry),
excludedModulesUnlocked = excludedModulesUnlocked,
onBack = navController::popBackStack,
onNavigate = { route -> navController.navigate(route) },
@@ -122,7 +125,10 @@ fun NavGraphBuilder.settingsGraph(navController: NavHostController) {
composable { backStackEntry ->
val parentEntry =
remember(backStackEntry) { navController.getBackStackEntry(SettingsRoutes.SettingsGraph::class) }
- AdministrationScreen(viewModel = hiltViewModel(parentEntry), onBack = navController::popBackStack)
+ AdministrationScreen(
+ viewModel = koinViewModel(viewModelStoreOwner = parentEntry),
+ onBack = navController::popBackStack,
+ )
}
composable(
@@ -133,7 +139,8 @@ fun NavGraphBuilder.settingsGraph(navController: NavHostController) {
),
),
) {
- CleanNodeDatabaseScreen()
+ val viewModel: AndroidCleanNodeDatabaseViewModel = koinViewModel()
+ CleanNodeDatabaseScreen(viewModel = viewModel)
}
ConfigRoute.entries.forEach { entry ->
@@ -221,18 +228,22 @@ fun NavGraphBuilder.settingsGraph(navController: NavHostController) {
deepLinks =
listOf(navDeepLink(basePath = "$DEEP_LINK_BASE_URI/settings/debug_panel")),
) {
- DebugScreen(onNavigateUp = navController::navigateUp)
+ val viewModel: AndroidDebugViewModel = koinViewModel()
+ DebugScreen(viewModel = viewModel, onNavigateUp = navController::navigateUp)
}
composable { AboutScreen(onNavigateUp = navController::navigateUp) }
- composable { FilterSettingsScreen(onBack = navController::navigateUp) }
+ composable {
+ val viewModel: AndroidFilterSettingsViewModel = koinViewModel()
+ FilterSettingsScreen(viewModel = viewModel, onBack = navController::navigateUp)
+ }
}
}
context(_: NavGraphBuilder)
inline fun NavHostController.configComposable(
- noinline content: @Composable (RadioConfigViewModel) -> Unit,
+ noinline content: @Composable (AndroidRadioConfigViewModel) -> Unit,
) {
configComposable(route = R::class, parentGraphRoute = G::class, content = content)
}
@@ -241,10 +252,10 @@ context(navGraphBuilder: NavGraphBuilder)
fun NavHostController.configComposable(
route: KClass,
parentGraphRoute: KClass,
- content: @Composable (RadioConfigViewModel) -> Unit,
+ content: @Composable (AndroidRadioConfigViewModel) -> Unit,
) {
navGraphBuilder.composable(route = route) { backStackEntry ->
val parentEntry = remember(backStackEntry) { getBackStackEntry(parentGraphRoute) }
- content(hiltViewModel(parentEntry))
+ content(koinViewModel(viewModelStoreOwner = parentEntry))
}
}
diff --git a/app/src/main/kotlin/org/meshtastic/app/di/DataModule.kt b/app/src/main/kotlin/org/meshtastic/app/node/AndroidCompassViewModel.kt
similarity index 50%
rename from app/src/main/kotlin/org/meshtastic/app/di/DataModule.kt
rename to app/src/main/kotlin/org/meshtastic/app/node/AndroidCompassViewModel.kt
index e20f08582..7feda7282 100644
--- a/app/src/main/kotlin/org/meshtastic/app/di/DataModule.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/node/AndroidCompassViewModel.kt
@@ -14,23 +14,19 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package org.meshtastic.app.di
+package org.meshtastic.app.node
-import android.content.Context
-import android.location.LocationManager
-import dagger.Module
-import dagger.Provides
-import dagger.hilt.InstallIn
-import dagger.hilt.android.qualifiers.ApplicationContext
-import dagger.hilt.components.SingletonComponent
-import javax.inject.Singleton
+import org.koin.core.annotation.KoinViewModel
+import org.meshtastic.core.di.CoroutineDispatchers
+import org.meshtastic.feature.node.compass.CompassHeadingProvider
+import org.meshtastic.feature.node.compass.CompassViewModel
+import org.meshtastic.feature.node.compass.MagneticFieldProvider
+import org.meshtastic.feature.node.compass.PhoneLocationProvider
-@Module
-@InstallIn(SingletonComponent::class)
-object DataModule {
-
- @Provides
- @Singleton
- fun provideLocationManager(@ApplicationContext context: Context): LocationManager =
- context.applicationContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
-}
+@KoinViewModel
+class AndroidCompassViewModel(
+ headingProvider: CompassHeadingProvider,
+ locationProvider: PhoneLocationProvider,
+ magneticFieldProvider: MagneticFieldProvider,
+ dispatchers: CoroutineDispatchers,
+) : CompassViewModel(headingProvider, locationProvider, magneticFieldProvider, dispatchers)
diff --git a/app/src/main/kotlin/org/meshtastic/app/node/AndroidMetricsViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/node/AndroidMetricsViewModel.kt
new file mode 100644
index 000000000..f7333c8af
--- /dev/null
+++ b/app/src/main/kotlin/org/meshtastic/app/node/AndroidMetricsViewModel.kt
@@ -0,0 +1,115 @@
+/*
+ * 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 org.meshtastic.app.node
+
+import android.app.Application
+import android.net.Uri
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.viewModelScope
+import androidx.navigation.toRoute
+import co.touchlab.kermit.Logger
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.koin.core.annotation.KoinViewModel
+import org.meshtastic.core.common.util.toDate
+import org.meshtastic.core.common.util.toInstant
+import org.meshtastic.core.data.repository.TracerouteSnapshotRepository
+import org.meshtastic.core.di.CoroutineDispatchers
+import org.meshtastic.core.navigation.NodesRoutes
+import org.meshtastic.core.repository.MeshLogRepository
+import org.meshtastic.core.repository.NodeRepository
+import org.meshtastic.core.repository.ServiceRepository
+import org.meshtastic.core.ui.util.AlertManager
+import org.meshtastic.feature.node.detail.NodeRequestActions
+import org.meshtastic.feature.node.domain.usecase.GetNodeDetailsUseCase
+import org.meshtastic.feature.node.metrics.MetricsViewModel
+import java.io.BufferedWriter
+import java.io.FileNotFoundException
+import java.io.FileWriter
+import java.text.SimpleDateFormat
+import java.util.Locale
+
+@KoinViewModel
+class AndroidMetricsViewModel(
+ savedStateHandle: SavedStateHandle,
+ private val app: Application,
+ dispatchers: CoroutineDispatchers,
+ meshLogRepository: MeshLogRepository,
+ serviceRepository: ServiceRepository,
+ nodeRepository: NodeRepository,
+ tracerouteSnapshotRepository: TracerouteSnapshotRepository,
+ nodeRequestActions: NodeRequestActions,
+ alertManager: AlertManager,
+ getNodeDetailsUseCase: GetNodeDetailsUseCase,
+) : MetricsViewModel(
+ savedStateHandle.toRoute().destNum ?: 0,
+ dispatchers,
+ meshLogRepository,
+ serviceRepository,
+ nodeRepository,
+ tracerouteSnapshotRepository,
+ nodeRequestActions,
+ alertManager,
+ getNodeDetailsUseCase,
+) {
+ override fun savePositionCSV(uri: Any) {
+ if (uri is Uri) {
+ savePositionCSVAndroid(uri)
+ }
+ }
+
+ private fun savePositionCSVAndroid(uri: Uri) = viewModelScope.launch(dispatchers.main) {
+ val positions = state.value.positionLogs
+ writeToUri(uri) { writer ->
+ writer.appendLine(
+ "\"date\",\"time\",\"latitude\",\"longitude\",\"altitude\",\"satsInView\",\"speed\",\"heading\"",
+ )
+
+ val dateFormat = SimpleDateFormat("\"yyyy-MM-dd\",\"HH:mm:ss\"", Locale.getDefault())
+
+ positions.forEach { position ->
+ val rxDateTime = dateFormat.format((position.time.toLong() * 1000L).toInstant().toDate())
+ val latitude = (position.latitude_i ?: 0) * 1e-7
+ val longitude = (position.longitude_i ?: 0) * 1e-7
+ val altitude = position.altitude
+ val satsInView = position.sats_in_view
+ val speed = position.ground_speed
+ val heading = "%.2f".format((position.ground_track ?: 0) * 1e-5)
+
+ writer.appendLine(
+ "$rxDateTime,\"$latitude\",\"$longitude\",\"$altitude\",\"$satsInView\",\"$speed\",\"$heading\"",
+ )
+ }
+ }
+ }
+
+ private suspend inline fun writeToUri(uri: Uri, crossinline block: suspend (BufferedWriter) -> Unit) =
+ withContext(dispatchers.io) {
+ try {
+ app.contentResolver.openFileDescriptor(uri, "wt")?.use { parcelFileDescriptor ->
+ FileWriter(parcelFileDescriptor.fileDescriptor).use { fileWriter ->
+ BufferedWriter(fileWriter).use { writer -> block.invoke(writer) }
+ }
+ }
+ } catch (ex: FileNotFoundException) {
+ Logger.e(ex) { "Can't write file error" }
+ }
+ }
+
+ override fun decodeBase64(base64: String): ByteArray =
+ android.util.Base64.decode(base64, android.util.Base64.DEFAULT)
+}
diff --git a/app/src/main/kotlin/org/meshtastic/app/node/AndroidNodeDetailViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/node/AndroidNodeDetailViewModel.kt
new file mode 100644
index 000000000..74ac78e09
--- /dev/null
+++ b/app/src/main/kotlin/org/meshtastic/app/node/AndroidNodeDetailViewModel.kt
@@ -0,0 +1,40 @@
+/*
+ * 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 org.meshtastic.app.node
+
+import androidx.lifecycle.SavedStateHandle
+import org.koin.core.annotation.KoinViewModel
+import org.meshtastic.core.repository.ServiceRepository
+import org.meshtastic.feature.node.detail.NodeDetailViewModel
+import org.meshtastic.feature.node.detail.NodeManagementActions
+import org.meshtastic.feature.node.detail.NodeRequestActions
+import org.meshtastic.feature.node.domain.usecase.GetNodeDetailsUseCase
+
+@KoinViewModel
+class AndroidNodeDetailViewModel(
+ savedStateHandle: SavedStateHandle,
+ nodeManagementActions: NodeManagementActions,
+ nodeRequestActions: NodeRequestActions,
+ serviceRepository: ServiceRepository,
+ getNodeDetailsUseCase: GetNodeDetailsUseCase,
+) : NodeDetailViewModel(
+ savedStateHandle,
+ nodeManagementActions,
+ nodeRequestActions,
+ serviceRepository,
+ getNodeDetailsUseCase,
+)
diff --git a/app/src/main/kotlin/org/meshtastic/app/node/AndroidNodeListViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/node/AndroidNodeListViewModel.kt
new file mode 100644
index 000000000..584c626ee
--- /dev/null
+++ b/app/src/main/kotlin/org/meshtastic/app/node/AndroidNodeListViewModel.kt
@@ -0,0 +1,49 @@
+/*
+ * 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 org.meshtastic.app.node
+
+import androidx.lifecycle.SavedStateHandle
+import org.koin.core.annotation.KoinViewModel
+import org.meshtastic.core.model.RadioController
+import org.meshtastic.core.repository.NodeRepository
+import org.meshtastic.core.repository.RadioConfigRepository
+import org.meshtastic.core.repository.ServiceRepository
+import org.meshtastic.feature.node.detail.NodeManagementActions
+import org.meshtastic.feature.node.domain.usecase.GetFilteredNodesUseCase
+import org.meshtastic.feature.node.list.NodeFilterPreferences
+import org.meshtastic.feature.node.list.NodeListViewModel
+
+@KoinViewModel
+class AndroidNodeListViewModel(
+ savedStateHandle: SavedStateHandle,
+ nodeRepository: NodeRepository,
+ radioConfigRepository: RadioConfigRepository,
+ serviceRepository: ServiceRepository,
+ radioController: RadioController,
+ nodeManagementActions: NodeManagementActions,
+ getFilteredNodesUseCase: GetFilteredNodesUseCase,
+ nodeFilterPreferences: NodeFilterPreferences,
+) : NodeListViewModel(
+ savedStateHandle,
+ nodeRepository,
+ radioConfigRepository,
+ serviceRepository,
+ radioController,
+ nodeManagementActions,
+ getFilteredNodesUseCase,
+ nodeFilterPreferences,
+)
diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/network/NetworkRepository.kt b/app/src/main/kotlin/org/meshtastic/app/repository/network/NetworkRepository.kt
index eeda06b17..76d3879a2 100644
--- a/app/src/main/kotlin/org/meshtastic/app/repository/network/NetworkRepository.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/repository/network/NetworkRepository.kt
@@ -27,24 +27,20 @@ import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.shareIn
+import org.koin.core.annotation.Named
+import org.koin.core.annotation.Single
import org.meshtastic.core.di.CoroutineDispatchers
-import org.meshtastic.core.di.ProcessLifecycle
-import javax.inject.Inject
-import javax.inject.Singleton
-@Singleton
-class NetworkRepository
-@Inject
-constructor(
- private val nsdManagerLazy: dagger.Lazy,
- private val connectivityManager: dagger.Lazy,
+@Single
+class NetworkRepository(
+ private val nsdManager: NsdManager,
+ private val connectivityManager: ConnectivityManager,
private val dispatchers: CoroutineDispatchers,
- @ProcessLifecycle private val processLifecycle: Lifecycle,
+ @Named("ProcessLifecycle") private val processLifecycle: Lifecycle,
) {
val networkAvailable: Flow by lazy {
connectivityManager
- .get()
.networkAvailable()
.flowOn(dispatchers.io)
.conflate()
@@ -57,8 +53,7 @@ constructor(
}
val resolvedList: Flow> by lazy {
- nsdManagerLazy
- .get()
+ nsdManager
.serviceList(SERVICE_TYPE)
.flowOn(dispatchers.io)
.conflate()
diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/network/NetworkRepositoryModule.kt b/app/src/main/kotlin/org/meshtastic/app/repository/network/NetworkRepositoryModule.kt
deleted file mode 100644
index 573ae4d9b..000000000
--- a/app/src/main/kotlin/org/meshtastic/app/repository/network/NetworkRepositoryModule.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * 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 org.meshtastic.app.repository.network
-
-import android.app.Application
-import android.content.Context
-import android.net.ConnectivityManager
-import android.net.nsd.NsdManager
-import dagger.Module
-import dagger.Provides
-import dagger.hilt.InstallIn
-import dagger.hilt.components.SingletonComponent
-
-@Module
-@InstallIn(SingletonComponent::class)
-class NetworkRepositoryModule {
- companion object {
- @Provides
- fun provideConnectivityManager(application: Application): ConnectivityManager =
- application.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
-
- @Provides
- fun provideNsdManager(application: Application): NsdManager =
- application.getSystemService(Context.NSD_SERVICE) as NsdManager
- }
-}
diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/AndroidRadioInterfaceService.kt b/app/src/main/kotlin/org/meshtastic/app/repository/radio/AndroidRadioInterfaceService.kt
index 4c2547a75..4a4105675 100644
--- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/AndroidRadioInterfaceService.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/repository/radio/AndroidRadioInterfaceService.kt
@@ -35,6 +35,8 @@ import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
+import org.koin.core.annotation.Named
+import org.koin.core.annotation.Single
import org.meshtastic.app.BuildConfig
import org.meshtastic.app.repository.network.NetworkRepository
import org.meshtastic.core.ble.BluetoothRepository
@@ -44,7 +46,6 @@ import org.meshtastic.core.common.util.ignoreException
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.common.util.toRemoteExceptions
import org.meshtastic.core.di.CoroutineDispatchers
-import org.meshtastic.core.di.ProcessLifecycle
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.InterfaceId
import org.meshtastic.core.model.MeshActivity
@@ -54,8 +55,6 @@ import org.meshtastic.core.repository.RadioInterfaceService
import org.meshtastic.core.repository.RadioPrefs
import org.meshtastic.proto.Heartbeat
import org.meshtastic.proto.ToRadio
-import javax.inject.Inject
-import javax.inject.Singleton
/**
* Handles the bluetooth link with a mesh radio device. Does not cache any device state, just does bluetooth comms
@@ -67,17 +66,15 @@ import javax.inject.Singleton
* can be stubbed out with a simulated version as needed.
*/
@Suppress("LongParameterList", "TooManyFunctions")
-@Singleton
-class AndroidRadioInterfaceService
-@Inject
-constructor(
+@Single
+class AndroidRadioInterfaceService(
private val context: Application,
private val dispatchers: CoroutineDispatchers,
private val bluetoothRepository: BluetoothRepository,
private val networkRepository: NetworkRepository,
- @ProcessLifecycle private val processLifecycle: Lifecycle,
+ @Named("ProcessLifecycle") private val processLifecycle: Lifecycle,
private val radioPrefs: RadioPrefs,
- private val interfaceFactory: InterfaceFactory,
+ private val interfaceFactory: Lazy,
private val analytics: PlatformAnalytics,
) : RadioInterfaceService {
@@ -179,7 +176,7 @@ constructor(
/** Constructs a full radio address for the specific interface type. */
override fun toInterfaceAddress(interfaceId: InterfaceId, rest: String): String =
- interfaceFactory.toInterfaceAddress(interfaceId, rest)
+ interfaceFactory.value.toInterfaceAddress(interfaceId, rest)
override fun isMockInterface(): Boolean =
BuildConfig.DEBUG || Settings.System.getString(context.contentResolver, "firebase.test.lab") == "true"
@@ -200,7 +197,7 @@ constructor(
fun getBondedDeviceAddress(): String? {
// If the user has unpaired our device, treat things as if we don't have one
val address = getDeviceAddress()
- return if (interfaceFactory.addressValid(address)) {
+ return if (interfaceFactory.value.addressValid(address)) {
address
} else {
null
@@ -259,24 +256,32 @@ constructor(
if (radioIf !is NopInterface) {
// Already running
return
+ }
+
+ val isTestLab = Settings.System.getString(context.contentResolver, "firebase.test.lab") == "true"
+ val address =
+ getBondedDeviceAddress()
+ ?: if (isTestLab) {
+ mockInterfaceAddress
+ } else {
+ null
+ }
+
+ if (address == null) {
+ Logger.w { "No bonded mesh radio, can't start interface" }
} else {
- val address = getBondedDeviceAddress()
- if (address == null) {
- Logger.w { "No bonded mesh radio, can't start interface" }
- } else {
- Logger.i { "Starting radio ${address.anonymize}" }
- isStarted = true
+ Logger.i { "Starting radio ${address.anonymize}" }
+ isStarted = true
- if (logSends) {
- sentPacketsLog = BinaryLogFile(context, "sent_log.pb")
- }
- if (logReceives) {
- receivedPacketsLog = BinaryLogFile(context, "receive_log.pb")
- }
-
- radioIf = interfaceFactory.createInterface(address)
- startHeartbeat()
+ if (logSends) {
+ sentPacketsLog = BinaryLogFile(context, "sent_log.pb")
}
+ if (logReceives) {
+ receivedPacketsLog = BinaryLogFile(context, "receive_log.pb")
+ }
+
+ radioIf = interfaceFactory.value.createInterface(address, this)
+ startHeartbeat()
}
}
@@ -297,7 +302,7 @@ constructor(
val r = radioIf
Logger.i { "stopping interface $r" }
isStarted = false
- radioIf = interfaceFactory.nopInterface
+ radioIf = interfaceFactory.value.nopInterface
r.close()
// cancel any old jobs and get ready for the new ones
diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/InterfaceFactory.kt b/app/src/main/kotlin/org/meshtastic/app/repository/radio/InterfaceFactory.kt
index dc6c1204d..548fb37b9 100644
--- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/InterfaceFactory.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/repository/radio/InterfaceFactory.kt
@@ -16,9 +16,9 @@
*/
package org.meshtastic.app.repository.radio
+import org.koin.core.annotation.Single
import org.meshtastic.core.model.InterfaceId
-import javax.inject.Inject
-import javax.inject.Provider
+import org.meshtastic.core.repository.RadioInterfaceService
/**
* Entry point for create radio backend instances given a specific address.
@@ -26,19 +26,31 @@ import javax.inject.Provider
* This class is responsible for building and dissecting radio addresses based upon their interface type and the "rest"
* of the address (which varies per implementation).
*/
-class InterfaceFactory
-@Inject
-constructor(
+@Single
+class InterfaceFactory(
private val nopInterfaceFactory: NopInterfaceFactory,
- private val specMap: Map>>,
+ private val bluetoothSpec: Lazy,
+ private val mockSpec: Lazy,
+ private val serialSpec: Lazy,
+ private val tcpSpec: Lazy,
) {
internal val nopInterface by lazy { nopInterfaceFactory.create("") }
+ private val specMap: Map>
+ get() =
+ mapOf(
+ InterfaceId.BLUETOOTH to bluetoothSpec.value,
+ InterfaceId.MOCK to mockSpec.value,
+ InterfaceId.NOP to NopInterfaceSpec(nopInterfaceFactory),
+ InterfaceId.SERIAL to serialSpec.value,
+ InterfaceId.TCP to tcpSpec.value,
+ )
+
fun toInterfaceAddress(interfaceId: InterfaceId, rest: String): String = "${interfaceId.id}$rest"
- fun createInterface(address: String): IRadioInterface {
+ fun createInterface(address: String, service: RadioInterfaceService): IRadioInterface {
val (spec, rest) = splitAddress(address)
- return spec?.createInterface(rest) ?: nopInterface
+ return spec?.createInterface(rest, service) ?: nopInterface
}
fun addressValid(address: String?): Boolean = address?.let {
@@ -47,7 +59,7 @@ constructor(
} ?: false
private fun splitAddress(address: String): Pair?, String> {
- val c = address[0].let { InterfaceId.forIdChar(it) }?.let { specMap[it]?.get() }
+ val c = address[0].let { InterfaceId.forIdChar(it) }?.let { specMap[it] }
val rest = address.substring(1)
return Pair(c, rest)
}
diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/InterfaceSpec.kt b/app/src/main/kotlin/org/meshtastic/app/repository/radio/InterfaceSpec.kt
index 5bfede5cd..ece828cc9 100644
--- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/InterfaceSpec.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/repository/radio/InterfaceSpec.kt
@@ -16,9 +16,11 @@
*/
package org.meshtastic.app.repository.radio
+import org.meshtastic.core.repository.RadioInterfaceService
+
/** This interface defines the contract that all radio backend implementations must adhere to. */
interface InterfaceSpec {
- fun createInterface(rest: String): T
+ fun createInterface(rest: String, service: RadioInterfaceService): T
/** Return true if this address is still acceptable. For BLE that means, still bonded */
fun addressValid(rest: String): Boolean = true
diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/MockInterface.kt b/app/src/main/kotlin/org/meshtastic/app/repository/radio/MockInterface.kt
index 4059b4e33..c2ff1f0e5 100644
--- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/MockInterface.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/repository/radio/MockInterface.kt
@@ -17,8 +17,6 @@
package org.meshtastic.app.repository.radio
import co.touchlab.kermit.Logger
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
import kotlinx.coroutines.delay
import okio.ByteString.Companion.encodeUtf8
import okio.ByteString.Companion.toByteString
@@ -58,12 +56,7 @@ private val defaultChannel = ProtoChannel(settings = Channel.default.settings, r
/** A simulated interface that is used for testing in the simulator */
@Suppress("detekt:TooManyFunctions", "detekt:MagicNumber")
-class MockInterface
-@AssistedInject
-constructor(
- private val service: RadioInterfaceService,
- @Assisted val address: String,
-) : IRadioInterface {
+class MockInterface(private val service: RadioInterfaceService, val address: String) : IRadioInterface {
companion object {
private const val MY_NODE = 0x42424242
diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/MockInterfaceFactory.kt b/app/src/main/kotlin/org/meshtastic/app/repository/radio/MockInterfaceFactory.kt
index f25aa828f..5f8328d3a 100644
--- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/MockInterfaceFactory.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/repository/radio/MockInterfaceFactory.kt
@@ -16,10 +16,11 @@
*/
package org.meshtastic.app.repository.radio
-import dagger.assisted.AssistedFactory
+import org.koin.core.annotation.Single
+import org.meshtastic.core.repository.RadioInterfaceService
/** Factory for creating `MockInterface` instances. */
-@AssistedFactory
-interface MockInterfaceFactory {
- fun create(rest: String): MockInterface
+@Single
+class MockInterfaceFactory {
+ fun create(rest: String, service: RadioInterfaceService): MockInterface = MockInterface(service, rest)
}
diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/MockInterfaceSpec.kt b/app/src/main/kotlin/org/meshtastic/app/repository/radio/MockInterfaceSpec.kt
index 4a6a1862f..13dcadd50 100644
--- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/MockInterfaceSpec.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/repository/radio/MockInterfaceSpec.kt
@@ -16,11 +16,14 @@
*/
package org.meshtastic.app.repository.radio
-import javax.inject.Inject
+import org.koin.core.annotation.Single
+import org.meshtastic.core.repository.RadioInterfaceService
/** Mock interface backend implementation. */
-class MockInterfaceSpec @Inject constructor(private val factory: MockInterfaceFactory) : InterfaceSpec {
- override fun createInterface(rest: String): MockInterface = factory.create(rest)
+@Single
+class MockInterfaceSpec(private val factory: MockInterfaceFactory) : InterfaceSpec {
+ override fun createInterface(rest: String, service: RadioInterfaceService): MockInterface =
+ factory.create(rest, service)
/** Return true if this address is still acceptable. For BLE that means, still bonded */
override fun addressValid(rest: String): Boolean = true
diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/NopInterface.kt b/app/src/main/kotlin/org/meshtastic/app/repository/radio/NopInterface.kt
index 60f30c743..2197bd748 100644
--- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/NopInterface.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/repository/radio/NopInterface.kt
@@ -16,10 +16,7 @@
*/
package org.meshtastic.app.repository.radio
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
-
-class NopInterface @AssistedInject constructor(@Assisted val address: String) : IRadioInterface {
+class NopInterface(val address: String) : IRadioInterface {
override fun handleSendToRadio(p: ByteArray) {
// No-op
}
diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/NopInterfaceFactory.kt b/app/src/main/kotlin/org/meshtastic/app/repository/radio/NopInterfaceFactory.kt
index e7b29e93d..56d58b846 100644
--- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/NopInterfaceFactory.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/repository/radio/NopInterfaceFactory.kt
@@ -16,10 +16,10 @@
*/
package org.meshtastic.app.repository.radio
-import dagger.assisted.AssistedFactory
+import org.koin.core.annotation.Single
/** Factory for creating `NopInterface` instances. */
-@AssistedFactory
-interface NopInterfaceFactory {
- fun create(rest: String): NopInterface
+@Single
+class NopInterfaceFactory {
+ fun create(rest: String): NopInterface = NopInterface(rest)
}
diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/NopInterfaceSpec.kt b/app/src/main/kotlin/org/meshtastic/app/repository/radio/NopInterfaceSpec.kt
index 791209c1b..149a2469a 100644
--- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/NopInterfaceSpec.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/repository/radio/NopInterfaceSpec.kt
@@ -16,9 +16,11 @@
*/
package org.meshtastic.app.repository.radio
-import javax.inject.Inject
+import org.koin.core.annotation.Single
+import org.meshtastic.core.repository.RadioInterfaceService
/** No-op interface backend implementation. */
-class NopInterfaceSpec @Inject constructor(private val factory: NopInterfaceFactory) : InterfaceSpec {
- override fun createInterface(rest: String): NopInterface = factory.create(rest)
+@Single
+class NopInterfaceSpec(private val factory: NopInterfaceFactory) : InterfaceSpec {
+ override fun createInterface(rest: String, service: RadioInterfaceService): NopInterface = factory.create(rest)
}
diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/NordicBleInterface.kt b/app/src/main/kotlin/org/meshtastic/app/repository/radio/NordicBleInterface.kt
index fd0371af8..3823c6161 100644
--- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/NordicBleInterface.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/repository/radio/NordicBleInterface.kt
@@ -18,8 +18,6 @@ package org.meshtastic.app.repository.radio
import android.annotation.SuppressLint
import co.touchlab.kermit.Logger
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
@@ -72,15 +70,13 @@ private val SCAN_TIMEOUT = 5.seconds
* @param address The BLE address of the device to connect to.
*/
@SuppressLint("MissingPermission")
-class NordicBleInterface
-@AssistedInject
-constructor(
+class NordicBleInterface(
private val serviceScope: CoroutineScope,
private val scanner: BleScanner,
private val bluetoothRepository: BluetoothRepository,
private val connectionFactory: BleConnectionFactory,
private val service: RadioInterfaceService,
- @Assisted val address: String,
+ val address: String,
) : IRadioInterface {
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/NordicBleInterfaceFactory.kt b/app/src/main/kotlin/org/meshtastic/app/repository/radio/NordicBleInterfaceFactory.kt
index 76835ffaf..8ea076ce2 100644
--- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/NordicBleInterfaceFactory.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/repository/radio/NordicBleInterfaceFactory.kt
@@ -16,10 +16,25 @@
*/
package org.meshtastic.app.repository.radio
-import dagger.assisted.AssistedFactory
+import org.koin.core.annotation.Single
+import org.meshtastic.core.ble.BleConnectionFactory
+import org.meshtastic.core.ble.BleScanner
+import org.meshtastic.core.ble.BluetoothRepository
+import org.meshtastic.core.repository.RadioInterfaceService
/** Factory for creating `NordicBleInterface` instances. */
-@AssistedFactory
-interface NordicBleInterfaceFactory {
- fun create(rest: String): NordicBleInterface
+@Single
+class NordicBleInterfaceFactory(
+ private val scanner: BleScanner,
+ private val bluetoothRepository: BluetoothRepository,
+ private val connectionFactory: BleConnectionFactory,
+) {
+ fun create(rest: String, service: RadioInterfaceService): NordicBleInterface = NordicBleInterface(
+ serviceScope = service.serviceScope,
+ scanner = scanner,
+ bluetoothRepository = bluetoothRepository,
+ connectionFactory = connectionFactory,
+ service = service,
+ address = rest,
+ )
}
diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/NordicBleInterfaceSpec.kt b/app/src/main/kotlin/org/meshtastic/app/repository/radio/NordicBleInterfaceSpec.kt
index d7b03d1a2..ce93bfb71 100644
--- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/NordicBleInterfaceSpec.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/repository/radio/NordicBleInterfaceSpec.kt
@@ -17,18 +17,19 @@
package org.meshtastic.app.repository.radio
import co.touchlab.kermit.Logger
+import org.koin.core.annotation.Single
import org.meshtastic.core.ble.BluetoothRepository
import org.meshtastic.core.model.util.anonymize
-import javax.inject.Inject
+import org.meshtastic.core.repository.RadioInterfaceService
/** Bluetooth backend implementation. */
-class NordicBleInterfaceSpec
-@Inject
-constructor(
+@Single
+class NordicBleInterfaceSpec(
private val factory: NordicBleInterfaceFactory,
private val bluetoothRepository: BluetoothRepository,
) : InterfaceSpec {
- override fun createInterface(rest: String): NordicBleInterface = factory.create(rest)
+ override fun createInterface(rest: String, service: RadioInterfaceService): NordicBleInterface =
+ factory.create(rest, service)
/** Return true if this address is still acceptable. For BLE that means, still bonded */
override fun addressValid(rest: String): Boolean = if (!bluetoothRepository.isBonded(rest)) {
diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/RadioRepositoryModule.kt b/app/src/main/kotlin/org/meshtastic/app/repository/radio/RadioRepositoryModule.kt
deleted file mode 100644
index 01a715312..000000000
--- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/RadioRepositoryModule.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * 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 org.meshtastic.app.repository.radio
-
-import dagger.Binds
-import dagger.Module
-import dagger.hilt.InstallIn
-import dagger.hilt.components.SingletonComponent
-import dagger.multibindings.IntoMap
-import dagger.multibindings.Multibinds
-import org.meshtastic.core.model.InterfaceId
-
-@Suppress("unused") // Used by hilt
-@Module
-@InstallIn(SingletonComponent::class)
-abstract class RadioRepositoryModule {
-
- @Multibinds abstract fun interfaceMap(): Map>
-
- @[Binds IntoMap InterfaceMapKey(InterfaceId.BLUETOOTH)]
- abstract fun bindBluetoothInterfaceSpec(spec: NordicBleInterfaceSpec): @JvmSuppressWildcards InterfaceSpec<*>
-
- @[Binds IntoMap InterfaceMapKey(InterfaceId.MOCK)]
- abstract fun bindMockInterfaceSpec(spec: MockInterfaceSpec): @JvmSuppressWildcards InterfaceSpec<*>
-
- @[Binds IntoMap InterfaceMapKey(InterfaceId.NOP)]
- abstract fun bindNopInterfaceSpec(spec: NopInterfaceSpec): @JvmSuppressWildcards InterfaceSpec<*>
-
- @[Binds IntoMap InterfaceMapKey(InterfaceId.SERIAL)]
- abstract fun bindSerialInterfaceSpec(spec: SerialInterfaceSpec): @JvmSuppressWildcards InterfaceSpec<*>
-
- @[Binds IntoMap InterfaceMapKey(InterfaceId.TCP)]
- abstract fun bindTCPInterfaceSpec(spec: TCPInterfaceSpec): @JvmSuppressWildcards InterfaceSpec<*>
-}
diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/SerialInterface.kt b/app/src/main/kotlin/org/meshtastic/app/repository/radio/SerialInterface.kt
index 39992f67b..718edf83b 100644
--- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/SerialInterface.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/repository/radio/SerialInterface.kt
@@ -17,8 +17,6 @@
package org.meshtastic.app.repository.radio
import co.touchlab.kermit.Logger
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
import org.meshtastic.app.repository.usb.SerialConnection
import org.meshtastic.app.repository.usb.SerialConnectionListener
import org.meshtastic.app.repository.usb.UsbRepository
@@ -27,13 +25,10 @@ import org.meshtastic.core.repository.RadioInterfaceService
import java.util.concurrent.atomic.AtomicReference
/** An interface that assumes we are talking to a meshtastic device via USB serial */
-class SerialInterface
-@AssistedInject
-constructor(
+class SerialInterface(
service: RadioInterfaceService,
- private val serialInterfaceSpec: SerialInterfaceSpec,
private val usbRepository: UsbRepository,
- @Assisted private val address: String,
+ private val address: String,
) : StreamInterface(service) {
private var connRef = AtomicReference()
@@ -47,7 +42,13 @@ constructor(
}
override fun connect() {
- val device = serialInterfaceSpec.findSerial(address)
+ val deviceMap = usbRepository.serialDevices.value
+ val device =
+ if (deviceMap.containsKey(address)) {
+ deviceMap[address]!!
+ } else {
+ deviceMap.map { (_, driver) -> driver }.firstOrNull()
+ }
if (device == null) {
Logger.e { "[$address] Serial device not found at address" }
} else {
diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/SerialInterfaceFactory.kt b/app/src/main/kotlin/org/meshtastic/app/repository/radio/SerialInterfaceFactory.kt
index ef518d324..56f76fd80 100644
--- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/SerialInterfaceFactory.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/repository/radio/SerialInterfaceFactory.kt
@@ -16,10 +16,13 @@
*/
package org.meshtastic.app.repository.radio
-import dagger.assisted.AssistedFactory
+import org.koin.core.annotation.Single
+import org.meshtastic.app.repository.usb.UsbRepository
+import org.meshtastic.core.repository.RadioInterfaceService
/** Factory for creating `SerialInterface` instances. */
-@AssistedFactory
-interface SerialInterfaceFactory {
- fun create(rest: String): SerialInterface
+@Single
+class SerialInterfaceFactory(private val usbRepository: UsbRepository) {
+ fun create(rest: String, service: RadioInterfaceService): SerialInterface =
+ SerialInterface(service, usbRepository, rest)
}
diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/SerialInterfaceSpec.kt b/app/src/main/kotlin/org/meshtastic/app/repository/radio/SerialInterfaceSpec.kt
index 874210352..75ab3e006 100644
--- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/SerialInterfaceSpec.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/repository/radio/SerialInterfaceSpec.kt
@@ -18,23 +18,24 @@ package org.meshtastic.app.repository.radio
import android.hardware.usb.UsbManager
import com.hoho.android.usbserial.driver.UsbSerialDriver
+import org.koin.core.annotation.Single
import org.meshtastic.app.repository.usb.UsbRepository
-import javax.inject.Inject
+import org.meshtastic.core.repository.RadioInterfaceService
/** Serial/USB interface backend implementation. */
-class SerialInterfaceSpec
-@Inject
-constructor(
+@Single
+class SerialInterfaceSpec(
private val factory: SerialInterfaceFactory,
- private val usbManager: dagger.Lazy,
+ private val usbManager: UsbManager,
private val usbRepository: UsbRepository,
) : InterfaceSpec {
- override fun createInterface(rest: String): SerialInterface = factory.create(rest)
+ override fun createInterface(rest: String, service: RadioInterfaceService): SerialInterface =
+ factory.create(rest, service)
override fun addressValid(rest: String): Boolean {
- usbRepository.serialDevices.value.filterValues { usbManager.get().hasPermission(it.device) }
+ usbRepository.serialDevices.value.filterValues { usbManager.hasPermission(it.device) }
findSerial(rest)?.let { d ->
- return usbManager.get().hasPermission(d.device)
+ return usbManager.hasPermission(d.device)
}
return false
}
diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/TCPInterface.kt b/app/src/main/kotlin/org/meshtastic/app/repository/radio/TCPInterface.kt
index 4ba551f2e..7f6fb4442 100644
--- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/TCPInterface.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/repository/radio/TCPInterface.kt
@@ -17,8 +17,6 @@
package org.meshtastic.app.repository.radio
import co.touchlab.kermit.Logger
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import org.meshtastic.app.repository.network.NetworkRepository
@@ -37,12 +35,10 @@ import java.net.InetAddress
import java.net.Socket
import java.net.SocketTimeoutException
-open class TCPInterface
-@AssistedInject
-constructor(
+open class TCPInterface(
service: RadioInterfaceService,
private val dispatchers: CoroutineDispatchers,
- @Assisted private val address: String,
+ private val address: String,
) : StreamInterface(service) {
companion object {
diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/TCPInterfaceFactory.kt b/app/src/main/kotlin/org/meshtastic/app/repository/radio/TCPInterfaceFactory.kt
index 1a96d9537..b11916940 100644
--- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/TCPInterfaceFactory.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/repository/radio/TCPInterfaceFactory.kt
@@ -16,10 +16,12 @@
*/
package org.meshtastic.app.repository.radio
-import dagger.assisted.AssistedFactory
+import org.koin.core.annotation.Single
+import org.meshtastic.core.di.CoroutineDispatchers
+import org.meshtastic.core.repository.RadioInterfaceService
/** Factory for creating `TCPInterface` instances. */
-@AssistedFactory
-interface TCPInterfaceFactory {
- fun create(rest: String): TCPInterface
+@Single
+class TCPInterfaceFactory(private val dispatchers: CoroutineDispatchers) {
+ fun create(rest: String, service: RadioInterfaceService): TCPInterface = TCPInterface(service, dispatchers, rest)
}
diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/TCPInterfaceSpec.kt b/app/src/main/kotlin/org/meshtastic/app/repository/radio/TCPInterfaceSpec.kt
index b5a9e1ed1..b48ee826c 100644
--- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/TCPInterfaceSpec.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/repository/radio/TCPInterfaceSpec.kt
@@ -16,9 +16,12 @@
*/
package org.meshtastic.app.repository.radio
-import javax.inject.Inject
+import org.koin.core.annotation.Single
+import org.meshtastic.core.repository.RadioInterfaceService
/** TCP interface backend implementation. */
-class TCPInterfaceSpec @Inject constructor(private val factory: TCPInterfaceFactory) : InterfaceSpec {
- override fun createInterface(rest: String): TCPInterface = factory.create(rest)
+@Single
+class TCPInterfaceSpec(private val factory: TCPInterfaceFactory) : InterfaceSpec {
+ override fun createInterface(rest: String, service: RadioInterfaceService): TCPInterface =
+ factory.create(rest, service)
}
diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/usb/ProbeTableProvider.kt b/app/src/main/kotlin/org/meshtastic/app/repository/usb/ProbeTableProvider.kt
index 9d8a21bae..3ae444175 100644
--- a/app/src/main/kotlin/org/meshtastic/app/repository/usb/ProbeTableProvider.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/repository/usb/ProbeTableProvider.kt
@@ -19,17 +19,15 @@ package org.meshtastic.app.repository.usb
import com.hoho.android.usbserial.driver.CdcAcmSerialDriver
import com.hoho.android.usbserial.driver.ProbeTable
import com.hoho.android.usbserial.driver.UsbSerialProber
-import dagger.Reusable
-import javax.inject.Inject
-import javax.inject.Provider
+import org.koin.core.annotation.Single
/**
* Creates a probe table for the USB driver. This augments the default device-to-driver mappings with additional known
* working configurations. See this package's README for more info.
*/
-@Reusable
-class ProbeTableProvider @Inject constructor() : Provider {
- override fun get(): ProbeTable = UsbSerialProber.getDefaultProbeTable().apply {
+@Single
+class ProbeTableProvider {
+ fun get(): ProbeTable = UsbSerialProber.getDefaultProbeTable().apply {
// RAK 4631:
addProduct(9114, 32809, CdcAcmSerialDriver::class.java)
// LilyGo TBeam v1.1:
diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/usb/SerialConnectionImpl.kt b/app/src/main/kotlin/org/meshtastic/app/repository/usb/SerialConnectionImpl.kt
index bfd959ef2..568010eea 100644
--- a/app/src/main/kotlin/org/meshtastic/app/repository/usb/SerialConnectionImpl.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/repository/usb/SerialConnectionImpl.kt
@@ -29,7 +29,7 @@ import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
internal class SerialConnectionImpl(
- private val usbManagerLazy: dagger.Lazy,
+ private val usbManagerLazy: Lazy,
private val device: UsbSerialDriver,
private val listener: SerialConnectionListener,
) : SerialConnection {
@@ -74,7 +74,7 @@ internal class SerialConnectionImpl(
override fun connect() {
// We shouldn't be able to get this far without a USB subsystem so explode if that isn't true
- val usbManager = usbManagerLazy.get()!!
+ val usbManager = usbManagerLazy.value!!
val usbDeviceConnection = usbManager.openDevice(device.device)
if (usbDeviceConnection == null) {
diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/usb/UsbBroadcastReceiver.kt b/app/src/main/kotlin/org/meshtastic/app/repository/usb/UsbBroadcastReceiver.kt
index 6be9c82c4..9a2904adf 100644
--- a/app/src/main/kotlin/org/meshtastic/app/repository/usb/UsbBroadcastReceiver.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/repository/usb/UsbBroadcastReceiver.kt
@@ -23,12 +23,13 @@ import android.content.IntentFilter
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager
import co.touchlab.kermit.Logger
+import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.exceptionReporter
import org.meshtastic.core.common.util.getParcelableExtraCompat
-import javax.inject.Inject
/** A helper class to call onChanged when bluetooth is enabled or disabled or when permissions are changed. */
-class UsbBroadcastReceiver @Inject constructor(private val usbRepository: UsbRepository) : BroadcastReceiver() {
+@Single
+class UsbBroadcastReceiver(private val usbRepository: UsbRepository) : BroadcastReceiver() {
// Can be used for registering
internal val intentFilter
get() =
diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/usb/UsbRepository.kt b/app/src/main/kotlin/org/meshtastic/app/repository/usb/UsbRepository.kt
index 3f9aad9ba..397b9ecd3 100644
--- a/app/src/main/kotlin/org/meshtastic/app/repository/usb/UsbRepository.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/repository/usb/UsbRepository.kt
@@ -32,31 +32,28 @@ import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import org.koin.core.annotation.Named
+import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.registerReceiverCompat
import org.meshtastic.core.di.CoroutineDispatchers
-import org.meshtastic.core.di.ProcessLifecycle
-import javax.inject.Inject
-import javax.inject.Singleton
/** Repository responsible for maintaining and updating the state of USB connectivity. */
@OptIn(ExperimentalCoroutinesApi::class)
-@Singleton
-class UsbRepository
-@Inject
-constructor(
+@Single
+class UsbRepository(
private val application: Application,
private val dispatchers: CoroutineDispatchers,
- @ProcessLifecycle private val processLifecycle: Lifecycle,
- private val usbBroadcastReceiverLazy: dagger.Lazy,
- private val usbManagerLazy: dagger.Lazy,
- private val usbSerialProberLazy: dagger.Lazy,
+ @Named("ProcessLifecycle") private val processLifecycle: Lifecycle,
+ private val usbBroadcastReceiverLazy: Lazy,
+ private val usbManagerLazy: Lazy,
+ private val usbSerialProberLazy: Lazy,
) {
private val _serialDevices = MutableStateFlow(emptyMap())
val serialDevices =
_serialDevices
.mapLatest { serialDevices ->
- val serialProber = usbSerialProberLazy.get()
+ val serialProber = usbSerialProberLazy.value
buildMap {
serialDevices.forEach { (k, v) -> serialProber.probeDevice(v)?.let { driver -> put(k, driver) } }
}
@@ -66,7 +63,7 @@ constructor(
init {
processLifecycle.coroutineScope.launch(dispatchers.default) {
refreshStateInternal()
- usbBroadcastReceiverLazy.get().let { receiver ->
+ usbBroadcastReceiverLazy.value.let { receiver ->
application.registerReceiverCompat(receiver, receiver.intentFilter)
}
}
@@ -80,12 +77,12 @@ constructor(
SerialConnectionImpl(usbManagerLazy, device, listener)
fun requestPermission(device: UsbDevice): Flow =
- usbManagerLazy.get()?.requestPermission(application, device) ?: emptyFlow()
+ usbManagerLazy.value?.requestPermission(application, device) ?: emptyFlow()
fun refreshState() {
processLifecycle.coroutineScope.launch(dispatchers.default) { refreshStateInternal() }
}
private suspend fun refreshStateInternal() =
- withContext(dispatchers.default) { _serialDevices.emit(usbManagerLazy.get()?.deviceList ?: emptyMap()) }
+ withContext(dispatchers.default) { _serialDevices.emit(usbManagerLazy.value?.deviceList ?: emptyMap()) }
}
diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/usb/UsbRepositoryModule.kt b/app/src/main/kotlin/org/meshtastic/app/repository/usb/UsbRepositoryModule.kt
deleted file mode 100644
index 7396619fa..000000000
--- a/app/src/main/kotlin/org/meshtastic/app/repository/usb/UsbRepositoryModule.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * 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 org.meshtastic.app.repository.usb
-
-import android.app.Application
-import android.content.Context
-import android.hardware.usb.UsbManager
-import com.hoho.android.usbserial.driver.ProbeTable
-import com.hoho.android.usbserial.driver.UsbSerialProber
-import dagger.Module
-import dagger.Provides
-import dagger.hilt.InstallIn
-import dagger.hilt.components.SingletonComponent
-
-@Module
-@InstallIn(SingletonComponent::class)
-interface UsbRepositoryModule {
- companion object {
- @Provides
- fun provideUsbManager(application: Application): UsbManager? =
- application.getSystemService(Context.USB_SERVICE) as UsbManager?
-
- @Provides fun provideProbeTable(provider: ProbeTableProvider): ProbeTable = provider.get()
-
- @Provides fun provideUsbSerialProber(probeTable: ProbeTable): UsbSerialProber = UsbSerialProber(probeTable)
- }
-}
diff --git a/app/src/main/kotlin/org/meshtastic/app/service/AndroidAppWidgetUpdater.kt b/app/src/main/kotlin/org/meshtastic/app/service/AndroidAppWidgetUpdater.kt
index f43935611..5749a9e7d 100644
--- a/app/src/main/kotlin/org/meshtastic/app/service/AndroidAppWidgetUpdater.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/service/AndroidAppWidgetUpdater.kt
@@ -18,14 +18,12 @@ package org.meshtastic.app.service
import android.content.Context
import androidx.glance.appwidget.updateAll
-import dagger.hilt.android.qualifiers.ApplicationContext
+import org.koin.core.annotation.Single
import org.meshtastic.app.widget.LocalStatsWidget
import org.meshtastic.core.repository.AppWidgetUpdater
-import javax.inject.Inject
-import javax.inject.Singleton
-@Singleton
-class AndroidAppWidgetUpdater @Inject constructor(@ApplicationContext private val context: Context) : AppWidgetUpdater {
+@Single
+class AndroidAppWidgetUpdater(private val context: Context) : AppWidgetUpdater {
override suspend fun updateAll() {
// Kickstart the widget composition.
// The widget internally uses collectAsState() and its own sampled StateFlow
diff --git a/app/src/main/kotlin/org/meshtastic/app/service/AndroidMeshLocationManager.kt b/app/src/main/kotlin/org/meshtastic/app/service/AndroidMeshLocationManager.kt
index c3d9d58f3..e820c3639 100644
--- a/app/src/main/kotlin/org/meshtastic/app/service/AndroidMeshLocationManager.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/service/AndroidMeshLocationManager.kt
@@ -26,22 +26,17 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
+import org.koin.core.annotation.Single
import org.meshtastic.core.common.hasLocationPermission
import org.meshtastic.core.model.Position
import org.meshtastic.core.repository.LocationRepository
import org.meshtastic.core.repository.MeshLocationManager
-import javax.inject.Inject
-import javax.inject.Singleton
import kotlin.time.Duration.Companion.milliseconds
import org.meshtastic.proto.Position as ProtoPosition
-@Singleton
-class AndroidMeshLocationManager
-@Inject
-constructor(
- private val context: Application,
- private val locationRepository: LocationRepository,
-) : MeshLocationManager {
+@Single
+class AndroidMeshLocationManager(private val context: Application, private val locationRepository: LocationRepository) :
+ MeshLocationManager {
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private var locationFlow: Job? = null
diff --git a/app/src/main/kotlin/org/meshtastic/app/service/AndroidMeshWorkerManager.kt b/app/src/main/kotlin/org/meshtastic/app/service/AndroidMeshWorkerManager.kt
index 570996691..25e88a9ff 100644
--- a/app/src/main/kotlin/org/meshtastic/app/service/AndroidMeshWorkerManager.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/service/AndroidMeshWorkerManager.kt
@@ -20,13 +20,12 @@ import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.workDataOf
+import org.koin.core.annotation.Single
import org.meshtastic.app.messaging.domain.worker.SendMessageWorker
import org.meshtastic.core.repository.MeshWorkerManager
-import javax.inject.Inject
-import javax.inject.Singleton
-@Singleton
-class AndroidMeshWorkerManager @Inject constructor(private val workManager: WorkManager) : MeshWorkerManager {
+@Single
+class AndroidMeshWorkerManager(private val workManager: WorkManager) : MeshWorkerManager {
override fun enqueueSendMessage(packetId: Int) {
val workRequest =
OneTimeWorkRequestBuilder()
diff --git a/app/src/main/kotlin/org/meshtastic/app/service/MarkAsReadReceiver.kt b/app/src/main/kotlin/org/meshtastic/app/service/MarkAsReadReceiver.kt
index 76b66bdbf..ebe68c74d 100644
--- a/app/src/main/kotlin/org/meshtastic/app/service/MarkAsReadReceiver.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/service/MarkAsReadReceiver.kt
@@ -19,23 +19,24 @@ package org.meshtastic.app.service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
-import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.repository.MeshServiceNotifications
import org.meshtastic.core.repository.PacketRepository
-import javax.inject.Inject
/** A [BroadcastReceiver] that handles "Mark as read" actions from notifications. */
-@AndroidEntryPoint
-class MarkAsReadReceiver : BroadcastReceiver() {
+class MarkAsReadReceiver :
+ BroadcastReceiver(),
+ KoinComponent {
- @Inject lateinit var packetRepository: PacketRepository
+ private val packetRepository: PacketRepository by inject()
- @Inject lateinit var serviceNotifications: MeshServiceNotifications
+ private val serviceNotifications: MeshServiceNotifications by inject()
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
diff --git a/app/src/main/kotlin/org/meshtastic/app/service/MeshService.kt b/app/src/main/kotlin/org/meshtastic/app/service/MeshService.kt
index 83e2a996f..72efaf81f 100644
--- a/app/src/main/kotlin/org/meshtastic/app/service/MeshService.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/service/MeshService.kt
@@ -24,12 +24,12 @@ import android.os.Build
import android.os.IBinder
import androidx.core.app.ServiceCompat
import co.touchlab.kermit.Logger
-import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
+import org.koin.android.ext.android.inject
import org.meshtastic.app.BuildConfig
import org.meshtastic.app.ui.connections.NO_DEVICE_SELECTED
import org.meshtastic.core.common.hasLocationPermission
@@ -50,42 +50,37 @@ import org.meshtastic.core.repository.MeshRouter
import org.meshtastic.core.repository.MeshServiceNotifications
import org.meshtastic.core.repository.NodeManager
import org.meshtastic.core.repository.PacketHandler
-import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.repository.RadioInterfaceService
import org.meshtastic.core.repository.SERVICE_NOTIFY_ID
import org.meshtastic.core.repository.ServiceBroadcasts
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.service.IMeshService
import org.meshtastic.proto.PortNum
-import javax.inject.Inject
-@AndroidEntryPoint
@Suppress("TooManyFunctions", "LargeClass")
class MeshService : Service() {
- @Inject lateinit var radioInterfaceService: RadioInterfaceService
+ private val radioInterfaceService: RadioInterfaceService by inject()
- @Inject lateinit var serviceRepository: ServiceRepository
+ private val serviceRepository: ServiceRepository by inject()
- @Inject lateinit var packetHandler: PacketHandler
+ private val packetHandler: PacketHandler by inject()
- @Inject lateinit var serviceBroadcasts: ServiceBroadcasts
+ private val serviceBroadcasts: ServiceBroadcasts by inject()
- @Inject lateinit var nodeManager: NodeManager
+ private val nodeManager: NodeManager by inject()
- @Inject lateinit var messageProcessor: MeshMessageProcessor
+ private val messageProcessor: MeshMessageProcessor by inject()
- @Inject lateinit var commandSender: CommandSender
+ private val commandSender: CommandSender by inject()
- @Inject lateinit var locationManager: MeshLocationManager
+ private val locationManager: MeshLocationManager by inject()
- @Inject lateinit var connectionManager: MeshConnectionManager
+ private val connectionManager: MeshConnectionManager by inject()
- @Inject lateinit var serviceNotifications: MeshServiceNotifications
+ private val serviceNotifications: MeshServiceNotifications by inject()
- @Inject lateinit var radioConfigRepository: RadioConfigRepository
-
- @Inject lateinit var router: MeshRouter
+ private val router: MeshRouter by inject()
private val serviceJob = Job()
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
diff --git a/app/src/main/kotlin/org/meshtastic/app/service/MeshServiceNotificationsImpl.kt b/app/src/main/kotlin/org/meshtastic/app/service/MeshServiceNotificationsImpl.kt
index a7680c117..e790d8d0d 100644
--- a/app/src/main/kotlin/org/meshtastic/app/service/MeshServiceNotificationsImpl.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/service/MeshServiceNotificationsImpl.kt
@@ -36,11 +36,10 @@ import androidx.core.content.getSystemService
import androidx.core.graphics.createBitmap
import androidx.core.graphics.drawable.IconCompat
import androidx.core.net.toUri
-import dagger.Lazy
-import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.jetbrains.compose.resources.StringResource
+import org.koin.core.annotation.Single
import org.meshtastic.app.MainActivity
import org.meshtastic.app.R.raw
import org.meshtastic.app.service.MarkAsReadReceiver.Companion.MARK_AS_READ_ACTION
@@ -92,8 +91,6 @@ import org.meshtastic.proto.ClientNotification
import org.meshtastic.proto.DeviceMetrics
import org.meshtastic.proto.LocalStats
import org.meshtastic.proto.Telemetry
-import javax.inject.Inject
-import javax.inject.Singleton
import kotlin.time.Duration.Companion.minutes
/**
@@ -103,11 +100,9 @@ import kotlin.time.Duration.Companion.minutes
* notifications for various events like new messages, alerts, and service status changes.
*/
@Suppress("TooManyFunctions", "LongParameterList")
-@Singleton
-class MeshServiceNotificationsImpl
-@Inject
-constructor(
- @ApplicationContext private val context: Context,
+@Single
+class MeshServiceNotificationsImpl(
+ private val context: Context,
private val packetRepository: Lazy,
private val nodeRepository: Lazy,
) : MeshServiceNotifications {
@@ -304,7 +299,7 @@ constructor(
// Seeding from database if caches are still null (e.g. on restart or reconnection)
if (cachedLocalStats == null || cachedDeviceMetrics == null) {
- val repo = nodeRepository.get()
+ val repo = nodeRepository.value
val myNodeNum = repo.myNodeInfo.value?.myNodeNum
if (myNodeNum != null) {
// We use runBlocking here because this is called from MeshConnectionManager's synchronous methods,
@@ -389,15 +384,14 @@ constructor(
channelName: String?,
isSilent: Boolean = false,
) {
- val ourNode = nodeRepository.get().ourNodeInfo.value
+ val ourNode = nodeRepository.value.ourNodeInfo.value
val history =
- packetRepository
- .get()
+ packetRepository.value
.getMessagesFrom(contactKey, includeFiltered = false) { nodeId ->
if (nodeId == DataPacket.ID_LOCAL) {
- ourNode ?: nodeRepository.get().getNode(nodeId)
+ ourNode ?: nodeRepository.value.getNode(nodeId)
} else {
- nodeRepository.get().getNode(nodeId ?: "")
+ nodeRepository.value.getNode(nodeId ?: "")
}
}
.first()
@@ -430,7 +424,7 @@ constructor(
it.id != SUMMARY_ID && it.notification.group == GROUP_KEY_MESSAGES
}
- val ourNode = nodeRepository.get().ourNodeInfo.value
+ val ourNode = nodeRepository.value.ourNodeInfo.value
val meName = ourNode?.user?.long_name ?: getString(Res.string.you)
val me =
Person.Builder()
@@ -542,7 +536,7 @@ constructor(
builder.setSilent(true)
}
- val ourNode = nodeRepository.get().ourNodeInfo.value
+ val ourNode = nodeRepository.value.ourNodeInfo.value
val meName = ourNode?.user?.long_name ?: getString(Res.string.you)
val me =
Person.Builder()
@@ -574,7 +568,7 @@ constructor(
// Add reactions as separate "messages" in history if they exist
msg.emojis.forEach { reaction ->
- val reactorNode = nodeRepository.get().getNode(reaction.user.id)
+ val reactorNode = nodeRepository.value.getNode(reaction.user.id)
val reactor =
Person.Builder()
.setName(reaction.user.long_name)
diff --git a/app/src/main/kotlin/org/meshtastic/app/service/ReactionReceiver.kt b/app/src/main/kotlin/org/meshtastic/app/service/ReactionReceiver.kt
index cd3f32c5b..fec13effb 100644
--- a/app/src/main/kotlin/org/meshtastic/app/service/ReactionReceiver.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/service/ReactionReceiver.kt
@@ -20,19 +20,20 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import co.touchlab.kermit.Logger
-import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
import org.meshtastic.core.model.service.ServiceAction
import org.meshtastic.core.repository.ServiceRepository
-import javax.inject.Inject
-@AndroidEntryPoint
-class ReactionReceiver : BroadcastReceiver() {
+class ReactionReceiver :
+ BroadcastReceiver(),
+ KoinComponent {
- @Inject lateinit var serviceRepository: ServiceRepository
+ private val serviceRepository: ServiceRepository by inject()
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
diff --git a/app/src/main/kotlin/org/meshtastic/app/service/ReplyReceiver.kt b/app/src/main/kotlin/org/meshtastic/app/service/ReplyReceiver.kt
index 190915b3f..e09f6c656 100644
--- a/app/src/main/kotlin/org/meshtastic/app/service/ReplyReceiver.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/service/ReplyReceiver.kt
@@ -20,16 +20,15 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import androidx.core.app.RemoteInput
-import dagger.hilt.android.AndroidEntryPoint
-import jakarta.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.repository.MeshServiceNotifications
-import org.meshtastic.core.repository.ServiceRepository
/**
* A [BroadcastReceiver] that handles inline replies from notifications.
@@ -38,11 +37,12 @@ import org.meshtastic.core.repository.ServiceRepository
* and the contact key from the intent, sends the message using the [ServiceRepository], and then cancels the original
* notification.
*/
-@AndroidEntryPoint
-class ReplyReceiver : BroadcastReceiver() {
- @Inject lateinit var radioController: RadioController
+class ReplyReceiver :
+ BroadcastReceiver(),
+ KoinComponent {
+ private val radioController: RadioController by inject()
- @Inject lateinit var meshServiceNotifications: MeshServiceNotifications
+ private val meshServiceNotifications: MeshServiceNotifications by inject()
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
diff --git a/app/src/main/kotlin/org/meshtastic/app/service/ServiceBroadcasts.kt b/app/src/main/kotlin/org/meshtastic/app/service/ServiceBroadcasts.kt
index 86845e25b..8b4ffc1a2 100644
--- a/app/src/main/kotlin/org/meshtastic/app/service/ServiceBroadcasts.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/service/ServiceBroadcasts.kt
@@ -20,7 +20,7 @@ import android.content.Context
import android.content.Intent
import android.os.Parcelable
import co.touchlab.kermit.Logger
-import dagger.hilt.android.qualifiers.ApplicationContext
+import org.koin.core.annotation.Single
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.MessageStatus
@@ -29,17 +29,11 @@ import org.meshtastic.core.model.NodeInfo
import org.meshtastic.core.model.util.toPIIString
import org.meshtastic.core.repository.ServiceRepository
import java.util.Locale
-import javax.inject.Inject
-import javax.inject.Singleton
import org.meshtastic.core.repository.ServiceBroadcasts as SharedServiceBroadcasts
-@Singleton
-class ServiceBroadcasts
-@Inject
-constructor(
- @ApplicationContext private val context: Context,
- private val serviceRepository: ServiceRepository,
-) : SharedServiceBroadcasts {
+@Single
+class ServiceBroadcasts(private val context: Context, private val serviceRepository: ServiceRepository) :
+ SharedServiceBroadcasts {
// A mapping of receiver class name to package name - used for explicit broadcasts
private val clientPackages = mutableMapOf()
diff --git a/app/src/main/kotlin/org/meshtastic/app/settings/AndroidCleanNodeDatabaseViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/settings/AndroidCleanNodeDatabaseViewModel.kt
new file mode 100644
index 000000000..08f308822
--- /dev/null
+++ b/app/src/main/kotlin/org/meshtastic/app/settings/AndroidCleanNodeDatabaseViewModel.kt
@@ -0,0 +1,28 @@
+/*
+ * 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 org.meshtastic.app.settings
+
+import org.koin.core.annotation.KoinViewModel
+import org.meshtastic.core.domain.usecase.settings.CleanNodeDatabaseUseCase
+import org.meshtastic.core.ui.util.AlertManager
+import org.meshtastic.feature.settings.radio.CleanNodeDatabaseViewModel
+
+@KoinViewModel
+class AndroidCleanNodeDatabaseViewModel(
+ cleanNodeDatabaseUseCase: CleanNodeDatabaseUseCase,
+ alertManager: AlertManager,
+) : CleanNodeDatabaseViewModel(cleanNodeDatabaseUseCase, alertManager)
diff --git a/app/src/main/kotlin/org/meshtastic/app/settings/AndroidDebugViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/settings/AndroidDebugViewModel.kt
new file mode 100644
index 000000000..1fb85df8a
--- /dev/null
+++ b/app/src/main/kotlin/org/meshtastic/app/settings/AndroidDebugViewModel.kt
@@ -0,0 +1,38 @@
+/*
+ * 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 org.meshtastic.app.settings
+
+import org.koin.core.annotation.KoinViewModel
+import org.meshtastic.core.repository.MeshLogPrefs
+import org.meshtastic.core.repository.MeshLogRepository
+import org.meshtastic.core.repository.NodeRepository
+import org.meshtastic.core.ui.util.AlertManager
+import org.meshtastic.feature.settings.debugging.DebugViewModel
+import java.util.Locale
+
+@KoinViewModel
+class AndroidDebugViewModel(
+ meshLogRepository: MeshLogRepository,
+ nodeRepository: NodeRepository,
+ meshLogPrefs: MeshLogPrefs,
+ alertManager: AlertManager,
+) : DebugViewModel(meshLogRepository, nodeRepository, meshLogPrefs, alertManager) {
+
+ override fun Int.toHex(length: Int): String = "!%0${length}x".format(Locale.getDefault(), this)
+
+ override fun Byte.toHex(): String = "%02x".format(Locale.getDefault(), this)
+}
diff --git a/app/src/main/kotlin/org/meshtastic/app/settings/AndroidFilterSettingsViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/settings/AndroidFilterSettingsViewModel.kt
new file mode 100644
index 000000000..03e9ded94
--- /dev/null
+++ b/app/src/main/kotlin/org/meshtastic/app/settings/AndroidFilterSettingsViewModel.kt
@@ -0,0 +1,26 @@
+/*
+ * 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 org.meshtastic.app.settings
+
+import org.koin.core.annotation.KoinViewModel
+import org.meshtastic.core.repository.FilterPrefs
+import org.meshtastic.core.repository.MessageFilter
+import org.meshtastic.feature.settings.filter.FilterSettingsViewModel
+
+@KoinViewModel
+class AndroidFilterSettingsViewModel(filterPrefs: FilterPrefs, messageFilter: MessageFilter) :
+ FilterSettingsViewModel(filterPrefs, messageFilter)
diff --git a/app/src/main/kotlin/org/meshtastic/app/settings/AndroidRadioConfigViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/settings/AndroidRadioConfigViewModel.kt
new file mode 100644
index 000000000..ab57c13b8
--- /dev/null
+++ b/app/src/main/kotlin/org/meshtastic/app/settings/AndroidRadioConfigViewModel.kt
@@ -0,0 +1,164 @@
+/*
+ * 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 org.meshtastic.app.settings
+
+import android.Manifest
+import android.app.Application
+import android.content.pm.PackageManager
+import android.location.Location
+import android.net.Uri
+import androidx.annotation.RequiresPermission
+import androidx.core.content.ContextCompat
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.viewModelScope
+import co.touchlab.kermit.Logger
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import okio.buffer
+import okio.sink
+import okio.source
+import org.koin.core.annotation.KoinViewModel
+import org.meshtastic.core.domain.usecase.settings.AdminActionsUseCase
+import org.meshtastic.core.domain.usecase.settings.ExportProfileUseCase
+import org.meshtastic.core.domain.usecase.settings.ExportSecurityConfigUseCase
+import org.meshtastic.core.domain.usecase.settings.ImportProfileUseCase
+import org.meshtastic.core.domain.usecase.settings.InstallProfileUseCase
+import org.meshtastic.core.domain.usecase.settings.ProcessRadioResponseUseCase
+import org.meshtastic.core.domain.usecase.settings.RadioConfigUseCase
+import org.meshtastic.core.domain.usecase.settings.ToggleAnalyticsUseCase
+import org.meshtastic.core.domain.usecase.settings.ToggleHomoglyphEncodingUseCase
+import org.meshtastic.core.repository.AnalyticsPrefs
+import org.meshtastic.core.repository.HomoglyphPrefs
+import org.meshtastic.core.repository.LocationRepository
+import org.meshtastic.core.repository.MapConsentPrefs
+import org.meshtastic.core.repository.NodeRepository
+import org.meshtastic.core.repository.PacketRepository
+import org.meshtastic.core.repository.RadioConfigRepository
+import org.meshtastic.core.repository.ServiceRepository
+import org.meshtastic.feature.settings.radio.RadioConfigViewModel
+import org.meshtastic.proto.Config
+import org.meshtastic.proto.DeviceProfile
+import java.io.FileOutputStream
+
+@KoinViewModel
+class AndroidRadioConfigViewModel(
+ savedStateHandle: SavedStateHandle,
+ private val app: Application,
+ radioConfigRepository: RadioConfigRepository,
+ packetRepository: PacketRepository,
+ serviceRepository: ServiceRepository,
+ nodeRepository: NodeRepository,
+ private val locationRepository: LocationRepository,
+ mapConsentPrefs: MapConsentPrefs,
+ analyticsPrefs: AnalyticsPrefs,
+ homoglyphEncodingPrefs: HomoglyphPrefs,
+ toggleAnalyticsUseCase: ToggleAnalyticsUseCase,
+ toggleHomoglyphEncodingUseCase: ToggleHomoglyphEncodingUseCase,
+ importProfileUseCase: ImportProfileUseCase,
+ exportProfileUseCase: ExportProfileUseCase,
+ exportSecurityConfigUseCase: ExportSecurityConfigUseCase,
+ installProfileUseCase: InstallProfileUseCase,
+ radioConfigUseCase: RadioConfigUseCase,
+ adminActionsUseCase: AdminActionsUseCase,
+ processRadioResponseUseCase: ProcessRadioResponseUseCase,
+) : RadioConfigViewModel(
+ savedStateHandle,
+ radioConfigRepository,
+ packetRepository,
+ serviceRepository,
+ nodeRepository,
+ locationRepository,
+ mapConsentPrefs,
+ analyticsPrefs,
+ homoglyphEncodingPrefs,
+ toggleAnalyticsUseCase,
+ toggleHomoglyphEncodingUseCase,
+ importProfileUseCase,
+ exportProfileUseCase,
+ exportSecurityConfigUseCase,
+ installProfileUseCase,
+ radioConfigUseCase,
+ adminActionsUseCase,
+ processRadioResponseUseCase,
+) {
+ @RequiresPermission(Manifest.permission.ACCESS_FINE_LOCATION)
+ override suspend fun getCurrentLocation(): Location? = if (
+ ContextCompat.checkSelfPermission(app, Manifest.permission.ACCESS_FINE_LOCATION) ==
+ PackageManager.PERMISSION_GRANTED
+ ) {
+ locationRepository.getLocations().firstOrNull()
+ } else {
+ null
+ }
+
+ override fun importProfile(uri: Any, onResult: (DeviceProfile) -> Unit) {
+ if (uri is Uri) {
+ viewModelScope.launch(Dispatchers.IO) {
+ try {
+ app.contentResolver.openInputStream(uri)?.source()?.buffer()?.use { inputStream ->
+ importProfileUseCase(inputStream).onSuccess(onResult).onFailure { throw it }
+ }
+ } catch (ex: Exception) {
+ Logger.e { "Import DeviceProfile error: ${ex.message}" }
+ // Error handling simplified for this example
+ }
+ }
+ }
+ }
+
+ override fun exportProfile(uri: Any, profile: DeviceProfile) {
+ if (uri is Uri) {
+ viewModelScope.launch {
+ withContext(Dispatchers.IO) {
+ try {
+ app.contentResolver.openFileDescriptor(uri, "wt")?.use { parcelFileDescriptor ->
+ FileOutputStream(parcelFileDescriptor.fileDescriptor).sink().buffer().use { outputStream ->
+ exportProfileUseCase(outputStream, profile)
+ .onSuccess { /* Success */ }
+ .onFailure { throw it }
+ }
+ }
+ } catch (ex: Exception) {
+ Logger.e { "Can't write file error: ${ex.message}" }
+ }
+ }
+ }
+ }
+ }
+
+ override fun exportSecurityConfig(uri: Any, securityConfig: Config.SecurityConfig) {
+ if (uri is Uri) {
+ viewModelScope.launch {
+ withContext(Dispatchers.IO) {
+ try {
+ app.contentResolver.openFileDescriptor(uri, "wt")?.use { parcelFileDescriptor ->
+ FileOutputStream(parcelFileDescriptor.fileDescriptor).sink().buffer().use { outputStream ->
+ exportSecurityConfigUseCase(outputStream, securityConfig)
+ .onSuccess { /* Success */ }
+ .onFailure { throw it }
+ }
+ }
+ } catch (ex: Exception) {
+ Logger.e { "Can't write security keys JSON error: ${ex.message}" }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/kotlin/org/meshtastic/app/settings/AndroidSettingsViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/settings/AndroidSettingsViewModel.kt
new file mode 100644
index 000000000..769036c40
--- /dev/null
+++ b/app/src/main/kotlin/org/meshtastic/app/settings/AndroidSettingsViewModel.kt
@@ -0,0 +1,103 @@
+/*
+ * 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 org.meshtastic.app.settings
+
+import android.app.Application
+import android.net.Uri
+import androidx.lifecycle.viewModelScope
+import co.touchlab.kermit.Logger
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import okio.BufferedSink
+import okio.buffer
+import okio.sink
+import org.koin.core.annotation.KoinViewModel
+import org.meshtastic.core.common.BuildConfigProvider
+import org.meshtastic.core.common.database.DatabaseManager
+import org.meshtastic.core.domain.usecase.settings.ExportDataUseCase
+import org.meshtastic.core.domain.usecase.settings.IsOtaCapableUseCase
+import org.meshtastic.core.domain.usecase.settings.MeshLocationUseCase
+import org.meshtastic.core.domain.usecase.settings.SetAppIntroCompletedUseCase
+import org.meshtastic.core.domain.usecase.settings.SetDatabaseCacheLimitUseCase
+import org.meshtastic.core.domain.usecase.settings.SetMeshLogSettingsUseCase
+import org.meshtastic.core.domain.usecase.settings.SetProvideLocationUseCase
+import org.meshtastic.core.domain.usecase.settings.SetThemeUseCase
+import org.meshtastic.core.model.RadioController
+import org.meshtastic.core.repository.MeshLogPrefs
+import org.meshtastic.core.repository.NodeRepository
+import org.meshtastic.core.repository.RadioConfigRepository
+import org.meshtastic.core.repository.UiPrefs
+import org.meshtastic.feature.settings.SettingsViewModel
+import java.io.FileNotFoundException
+import java.io.FileOutputStream
+
+@KoinViewModel
+class AndroidSettingsViewModel(
+ private val app: Application,
+ radioConfigRepository: RadioConfigRepository,
+ radioController: RadioController,
+ nodeRepository: NodeRepository,
+ uiPrefs: UiPrefs,
+ buildConfigProvider: BuildConfigProvider,
+ databaseManager: DatabaseManager,
+ meshLogPrefs: MeshLogPrefs,
+ setThemeUseCase: SetThemeUseCase,
+ setAppIntroCompletedUseCase: SetAppIntroCompletedUseCase,
+ setProvideLocationUseCase: SetProvideLocationUseCase,
+ setDatabaseCacheLimitUseCase: SetDatabaseCacheLimitUseCase,
+ setMeshLogSettingsUseCase: SetMeshLogSettingsUseCase,
+ meshLocationUseCase: MeshLocationUseCase,
+ exportDataUseCase: ExportDataUseCase,
+ isOtaCapableUseCase: IsOtaCapableUseCase,
+) : SettingsViewModel(
+ radioConfigRepository,
+ radioController,
+ nodeRepository,
+ uiPrefs,
+ buildConfigProvider,
+ databaseManager,
+ meshLogPrefs,
+ setThemeUseCase,
+ setAppIntroCompletedUseCase,
+ setProvideLocationUseCase,
+ setDatabaseCacheLimitUseCase,
+ setMeshLogSettingsUseCase,
+ meshLocationUseCase,
+ exportDataUseCase,
+ isOtaCapableUseCase,
+) {
+ override fun saveDataCsv(uri: Any, filterPortnum: Int?) {
+ if (uri is Uri) {
+ viewModelScope.launch { writeToUri(uri) { writer -> performDataExport(writer, filterPortnum) } }
+ }
+ }
+
+ private suspend inline fun writeToUri(uri: Uri, crossinline block: suspend (BufferedSink) -> Unit) {
+ withContext(Dispatchers.IO) {
+ try {
+ app.contentResolver.openFileDescriptor(uri, "wt")?.use { parcelFileDescriptor ->
+ FileOutputStream(parcelFileDescriptor.fileDescriptor).sink().buffer().use { writer ->
+ block.invoke(writer)
+ }
+ }
+ } catch (ex: FileNotFoundException) {
+ Logger.e { "Can't write file error: ${ex.message}" }
+ }
+ }
+ }
+}
diff --git a/app/src/main/kotlin/org/meshtastic/app/ui/Main.kt b/app/src/main/kotlin/org/meshtastic/app/ui/Main.kt
index adcab19c5..fcaf62df7 100644
--- a/app/src/main/kotlin/org/meshtastic/app/ui/Main.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/ui/Main.kt
@@ -67,7 +67,6 @@ import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hasRoute
@@ -79,10 +78,10 @@ import androidx.navigation.compose.rememberNavController
import co.touchlab.kermit.Logger
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
-import no.nordicsemi.android.common.permissions.notification.RequestNotificationPermission
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.getString
import org.jetbrains.compose.resources.stringResource
+import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.app.BuildConfig
import org.meshtastic.app.model.UIViewModel
import org.meshtastic.app.navigation.channelsGraph
@@ -159,7 +158,7 @@ enum class TopLevelDestination(val label: StringResource, val icon: ImageVector,
@OptIn(ExperimentalMaterial3Api::class)
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
-fun MainScreen(uIViewModel: UIViewModel = hiltViewModel(), scanModel: ScannerViewModel = hiltViewModel()) {
+fun MainScreen(uIViewModel: UIViewModel = koinViewModel(), scanModel: ScannerViewModel = koinViewModel()) {
val navController = rememberNavController()
LaunchedEffect(uIViewModel) { uIViewModel.navigationDeepLink.collectLatest { uri -> navController.navigate(uri) } }
val connectionState by uIViewModel.connectionState.collectAsStateWithLifecycle()
@@ -168,10 +167,6 @@ fun MainScreen(uIViewModel: UIViewModel = hiltViewModel(), scanModel: ScannerVie
val unreadMessageCount by uIViewModel.unreadMessageCount.collectAsStateWithLifecycle()
if (connectionState == ConnectionState.Connected) {
- RequestNotificationPermission {
- // Nordic handled the trigger for POST_NOTIFICATIONS when connected
- }
-
sharedContactRequested?.let {
SharedContactDialog(sharedContact = it, onDismiss = { uIViewModel.clearSharedContactRequested() })
}
diff --git a/app/src/main/kotlin/org/meshtastic/app/ui/connections/ConnectionsScreen.kt b/app/src/main/kotlin/org/meshtastic/app/ui/connections/ConnectionsScreen.kt
index 5f4e34e29..ba8d454ab 100644
--- a/app/src/main/kotlin/org/meshtastic/app/ui/connections/ConnectionsScreen.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/ui/connections/ConnectionsScreen.kt
@@ -46,11 +46,11 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import org.jetbrains.compose.resources.getString
import org.jetbrains.compose.resources.stringResource
+import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.app.model.DeviceListEntry
import org.meshtastic.app.ui.connections.components.BLEDevices
import org.meshtastic.app.ui.connections.components.ConnectingDeviceInfo
@@ -92,9 +92,9 @@ import kotlin.uuid.ExperimentalUuidApi
@Suppress("CyclomaticComplexMethod", "LongMethod", "MagicNumber", "ModifierMissing", "ComposableParamOrder")
@Composable
fun ConnectionsScreen(
- connectionsViewModel: ConnectionsViewModel = hiltViewModel(),
- scanModel: ScannerViewModel = hiltViewModel(),
- radioConfigViewModel: RadioConfigViewModel = hiltViewModel(),
+ connectionsViewModel: ConnectionsViewModel = koinViewModel(),
+ scanModel: ScannerViewModel = koinViewModel(),
+ radioConfigViewModel: RadioConfigViewModel = koinViewModel(),
onClickNodeChip: (Int) -> Unit,
onNavigateToNodeDetails: (Int) -> Unit,
onConfigNavigate: (Route) -> Unit,
diff --git a/app/src/main/kotlin/org/meshtastic/app/ui/connections/ConnectionsViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/ui/connections/ConnectionsViewModel.kt
index 8205ff0c0..372202c46 100644
--- a/app/src/main/kotlin/org/meshtastic/app/ui/connections/ConnectionsViewModel.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/ui/connections/ConnectionsViewModel.kt
@@ -17,10 +17,10 @@
package org.meshtastic.app.ui.connections
import androidx.lifecycle.ViewModel
-import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
+import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.model.MyNodeInfo
import org.meshtastic.core.model.Node
import org.meshtastic.core.repository.NodeRepository
@@ -29,12 +29,9 @@ import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.repository.UiPrefs
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.proto.LocalConfig
-import javax.inject.Inject
-@HiltViewModel
-class ConnectionsViewModel
-@Inject
-constructor(
+@KoinViewModel
+class ConnectionsViewModel(
radioConfigRepository: RadioConfigRepository,
serviceRepository: ServiceRepository,
nodeRepository: NodeRepository,
diff --git a/app/src/main/kotlin/org/meshtastic/app/ui/connections/ScannerViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/ui/connections/ScannerViewModel.kt
index cb03f8446..93005bec1 100644
--- a/app/src/main/kotlin/org/meshtastic/app/ui/connections/ScannerViewModel.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/ui/connections/ScannerViewModel.kt
@@ -20,7 +20,6 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
import co.touchlab.kermit.Severity
-import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@@ -31,6 +30,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
+import org.koin.core.annotation.KoinViewModel
import org.meshtastic.app.domain.usecase.GetDiscoveredDevicesUseCase
import org.meshtastic.app.model.DeviceListEntry
import org.meshtastic.app.repository.usb.UsbRepository
@@ -42,13 +42,10 @@ import org.meshtastic.core.model.util.anonymize
import org.meshtastic.core.repository.RadioInterfaceService
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
-import javax.inject.Inject
-@HiltViewModel
+@KoinViewModel
@Suppress("LongParameterList", "TooManyFunctions")
-class ScannerViewModel
-@Inject
-constructor(
+class ScannerViewModel(
private val serviceRepository: ServiceRepository,
private val radioController: RadioController,
private val bluetoothRepository: BluetoothRepository,
diff --git a/app/src/main/kotlin/org/meshtastic/app/ui/connections/components/BLEDevices.kt b/app/src/main/kotlin/org/meshtastic/app/ui/connections/components/BLEDevices.kt
index 959c4ff3f..45fcc2fbc 100644
--- a/app/src/main/kotlin/org/meshtastic/app/ui/connections/components/BLEDevices.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/ui/connections/components/BLEDevices.kt
@@ -19,6 +19,8 @@ package org.meshtastic.app.ui.connections.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
@@ -26,25 +28,17 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import no.nordicsemi.android.common.scanner.rememberFilterState
-import no.nordicsemi.android.common.scanner.view.ScannerView
import org.jetbrains.compose.resources.stringResource
-import org.meshtastic.app.model.DeviceListEntry
import org.meshtastic.app.ui.connections.ScannerViewModel
-import org.meshtastic.core.ble.AndroidBleDevice
-import org.meshtastic.core.ble.MeshtasticBleConstants.BLE_NAME_PATTERN
-import org.meshtastic.core.ble.MeshtasticBleConstants.SERVICE_UUID
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.bluetooth_available_devices
/**
- * Composable that displays a list of Bluetooth Low Energy (BLE) devices and allows scanning. It handles Bluetooth
- * permissions and hardware state using Nordic Common Libraries' ScannerView.
+ * Composable that displays a list of Bluetooth Low Energy (BLE) devices and allows scanning.
*
* @param connectionState The current connection state of the MeshService.
* @param selectedDevice The full address of the currently selected device.
@@ -53,15 +47,6 @@ import org.meshtastic.core.resources.bluetooth_available_devices
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun BLEDevices(connectionState: ConnectionState, selectedDevice: String, scanModel: ScannerViewModel) {
- val filterState =
- rememberFilterState(
- filter = {
- Any {
- ServiceUuid(SERVICE_UUID)
- Name(Regex(BLE_NAME_PATTERN))
- }
- },
- )
val bleDevices by scanModel.bleDevicesForUi.collectAsStateWithLifecycle()
Column {
@@ -72,17 +57,8 @@ fun BLEDevices(connectionState: ConnectionState, selectedDevice: String, scanMod
color = MaterialTheme.colorScheme.primary,
)
- ScannerView(
- state = filterState,
- onScanResultSelected = { result ->
- scanModel.onSelected(DeviceListEntry.Ble(AndroidBleDevice(result.peripheral)))
- },
- deviceItem = { result ->
- val device =
- remember(result.peripheral.address, bleDevices) {
- bleDevices.find { it.fullAddress == "x${result.peripheral.address}" }
- ?: DeviceListEntry.Ble(AndroidBleDevice(result.peripheral))
- }
+ LazyColumn(modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp)) {
+ items(bleDevices, key = { it.fullAddress }) { device ->
Card(
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
shape = MaterialTheme.shapes.large,
@@ -94,10 +70,10 @@ fun BLEDevices(connectionState: ConnectionState, selectedDevice: String, scanMod
?: ConnectionState.Disconnected,
device = device,
onSelect = { scanModel.onSelected(device) },
- rssi = result.rssi,
+ rssi = null,
)
}
- },
- )
+ }
+ }
}
}
diff --git a/app/src/main/kotlin/org/meshtastic/app/ui/connections/components/DeviceListItem.kt b/app/src/main/kotlin/org/meshtastic/app/ui/connections/components/DeviceListItem.kt
index 8fe790763..e25587d41 100644
--- a/app/src/main/kotlin/org/meshtastic/app/ui/connections/components/DeviceListItem.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/ui/connections/components/DeviceListItem.kt
@@ -16,11 +16,9 @@
*/
package org.meshtastic.app.ui.connections.components
-import androidx.compose.foundation.Indication
-import androidx.compose.foundation.LocalIndication
-import androidx.compose.foundation.gestures.detectTapGestures
-import androidx.compose.foundation.indication
-import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
@@ -50,7 +48,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import no.nordicsemi.android.common.ui.view.RssiIcon
@@ -66,7 +63,7 @@ import org.meshtastic.core.ui.component.NodeChip
private const val RSSI_UPDATE_RATE_MS = 2000L
-@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalFoundationApi::class)
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
fun DeviceListItem(
@@ -115,17 +112,11 @@ fun DeviceListItem(
is DeviceListEntry.Mock -> stringResource(Res.string.add)
}
- val useSelectable = modifier == Modifier
- val interactionSource = remember { MutableInteractionSource() }
- val indication: Indication = LocalIndication.current
-
val clickableModifier =
- if (useSelectable) {
- Modifier.indication(interactionSource, indication).pointerInput(device.fullAddress, onDelete) {
- detectTapGestures(onTap = { onSelect() }, onLongPress = onDelete?.let { { it() } })
- }
+ if (onDelete != null) {
+ Modifier.combinedClickable(onClick = onSelect, onLongClick = onDelete)
} else {
- Modifier
+ Modifier.clickable(onClick = onSelect)
}
ListItem(
diff --git a/app/src/main/kotlin/org/meshtastic/app/ui/node/AdaptiveNodeListScreen.kt b/app/src/main/kotlin/org/meshtastic/app/ui/node/AdaptiveNodeListScreen.kt
index f50acc4e7..b637b5080 100644
--- a/app/src/main/kotlin/org/meshtastic/app/ui/node/AdaptiveNodeListScreen.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/ui/node/AdaptiveNodeListScreen.kt
@@ -20,9 +20,11 @@ import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@@ -46,6 +48,10 @@ import androidx.navigation.NavHostController
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
+import org.koin.compose.viewmodel.koinViewModel
+import org.meshtastic.app.node.AndroidCompassViewModel
+import org.meshtastic.app.node.AndroidNodeDetailViewModel
+import org.meshtastic.app.node.AndroidNodeListViewModel
import org.meshtastic.core.navigation.ChannelsRoutes
import org.meshtastic.core.navigation.NodesRoutes
import org.meshtastic.core.resources.Res
@@ -65,6 +71,7 @@ fun AdaptiveNodeListScreen(
initialNodeId: Int? = null,
onNavigateToMessages: (String) -> Unit = {},
) {
+ val nodeListViewModel: AndroidNodeListViewModel = koinViewModel()
val navigator = rememberListDetailPaneScaffoldNavigator()
val scope = rememberCoroutineScope()
val backNavigationBehavior = BackNavigationBehavior.PopUntilScaffoldValueChange
@@ -118,6 +125,7 @@ fun AdaptiveNodeListScreen(
// Prevent TextFields from auto-focusing when pane animates in
LaunchedEffect(Unit) { focusManager.clearFocus() }
NodeListScreen(
+ viewModel = nodeListViewModel,
navigateToNodeDetails = { nodeId ->
scope.launch { navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, nodeId) }
},
@@ -134,8 +142,12 @@ fun AdaptiveNodeListScreen(
navigator.currentDestination?.contentKey?.let { nodeId ->
key(nodeId) {
LaunchedEffect(nodeId) { focusManager.clearFocus() }
+ val nodeDetailViewModel: AndroidNodeDetailViewModel = koinViewModel()
+ val compassViewModel: AndroidCompassViewModel = koinViewModel()
NodeDetailScreen(
nodeId = nodeId,
+ viewModel = nodeDetailViewModel,
+ compassViewModel = compassViewModel,
navigateToMessages = onNavigateToMessages,
onNavigate = { route -> navController.navigate(route) },
onNavigateUp = handleBack,
@@ -147,6 +159,18 @@ fun AdaptiveNodeListScreen(
)
}
+@Composable
+fun NodeTabTitle() {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Icon(imageVector = MeshtasticIcons.Nodes, contentDescription = null, modifier = Modifier.padding(end = 8.dp))
+ Text(
+ text = stringResource(Res.string.nodes),
+ style = MaterialTheme.typography.titleLarge,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ }
+}
+
@Composable
private fun PlaceholderScreen() {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
diff --git a/app/src/main/kotlin/org/meshtastic/app/ui/sharing/Channel.kt b/app/src/main/kotlin/org/meshtastic/app/ui/sharing/Channel.kt
index 627822b9a..eae4214c4 100644
--- a/app/src/main/kotlin/org/meshtastic/app/ui/sharing/Channel.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/ui/sharing/Channel.kt
@@ -64,11 +64,11 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewScreenSizes
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.touchlab.kermit.Logger
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
+import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.core.common.util.toPlatformUri
import org.meshtastic.core.model.Channel
import org.meshtastic.core.model.ConnectionState
@@ -112,8 +112,8 @@ import org.meshtastic.proto.Config
@Composable
@Suppress("LongMethod")
fun ChannelScreen(
- viewModel: ChannelViewModel = hiltViewModel(),
- radioConfigViewModel: RadioConfigViewModel = hiltViewModel(),
+ viewModel: ChannelViewModel = koinViewModel(),
+ radioConfigViewModel: RadioConfigViewModel = koinViewModel(),
onNavigate: (Route) -> Unit,
onNavigateUp: () -> Unit,
) {
diff --git a/app/src/main/kotlin/org/meshtastic/app/ui/sharing/ChannelViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/ui/sharing/ChannelViewModel.kt
index 0fad35a09..a6810c3af 100644
--- a/app/src/main/kotlin/org/meshtastic/app/ui/sharing/ChannelViewModel.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/ui/sharing/ChannelViewModel.kt
@@ -20,10 +20,10 @@ import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
-import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
+import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.model.util.toChannelSet
import org.meshtastic.core.repository.DataPair
@@ -35,12 +35,9 @@ import org.meshtastic.proto.Channel
import org.meshtastic.proto.ChannelSet
import org.meshtastic.proto.Config
import org.meshtastic.proto.LocalConfig
-import javax.inject.Inject
-@HiltViewModel
-class ChannelViewModel
-@Inject
-constructor(
+@KoinViewModel
+class ChannelViewModel(
private val radioController: RadioController,
private val radioConfigRepository: RadioConfigRepository,
private val analytics: PlatformAnalytics,
diff --git a/app/src/main/kotlin/org/meshtastic/app/widget/LocalStatsWidget.kt b/app/src/main/kotlin/org/meshtastic/app/widget/LocalStatsWidget.kt
index 5753f8040..c73a0e76a 100644
--- a/app/src/main/kotlin/org/meshtastic/app/widget/LocalStatsWidget.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/widget/LocalStatsWidget.kt
@@ -63,11 +63,9 @@ import androidx.glance.text.FontWeight
import androidx.glance.text.Text
import androidx.glance.text.TextStyle
import androidx.glance.unit.ColorProvider
-import dagger.hilt.EntryPoint
-import dagger.hilt.InstallIn
-import dagger.hilt.android.EntryPointAccessors
-import dagger.hilt.components.SingletonComponent
import org.jetbrains.compose.resources.stringResource
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
import org.meshtastic.core.common.util.DateFormatter
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.util.formatUptime
@@ -94,22 +92,16 @@ import org.meshtastic.core.resources.refresh
import org.meshtastic.core.resources.updated
import org.meshtastic.core.resources.uptime
-class LocalStatsWidget : GlanceAppWidget() {
+class LocalStatsWidget :
+ GlanceAppWidget(),
+ KoinComponent {
override val sizeMode: SizeMode = SizeMode.Responsive(RESPONSIVE_SIZES)
override val previewSizeMode: androidx.glance.appwidget.PreviewSizeMode = SizeMode.Responsive(RESPONSIVE_SIZES)
- @EntryPoint
- @InstallIn(SingletonComponent::class)
- interface LocalStatsWidgetEntryPoint {
- fun widgetStateProvider(): LocalStatsWidgetStateProvider
- }
+ private val stateProvider: LocalStatsWidgetStateProvider by inject()
override suspend fun provideGlance(context: Context, id: GlanceId) {
- val entryPoint =
- EntryPointAccessors.fromApplication(context.applicationContext, LocalStatsWidgetEntryPoint::class.java)
- val stateProvider = entryPoint.widgetStateProvider()
-
provideContent {
val state by stateProvider.state.collectAsState()
WidgetContent(state)
@@ -117,9 +109,6 @@ class LocalStatsWidget : GlanceAppWidget() {
}
override suspend fun providePreview(context: Context, widgetCategory: Int) {
- val entryPoint =
- EntryPointAccessors.fromApplication(context.applicationContext, LocalStatsWidgetEntryPoint::class.java)
- val stateProvider = entryPoint.widgetStateProvider()
val currentState = stateProvider.state.value
val stateToRender =
diff --git a/app/src/main/kotlin/org/meshtastic/app/widget/LocalStatsWidgetReceiver.kt b/app/src/main/kotlin/org/meshtastic/app/widget/LocalStatsWidgetReceiver.kt
index 2b162b9b8..28409d0f5 100644
--- a/app/src/main/kotlin/org/meshtastic/app/widget/LocalStatsWidgetReceiver.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/widget/LocalStatsWidgetReceiver.kt
@@ -18,9 +18,7 @@ package org.meshtastic.app.widget
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
-import dagger.hilt.android.AndroidEntryPoint
-@AndroidEntryPoint
class LocalStatsWidgetReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget = LocalStatsWidget()
}
diff --git a/app/src/main/kotlin/org/meshtastic/app/widget/LocalStatsWidgetState.kt b/app/src/main/kotlin/org/meshtastic/app/widget/LocalStatsWidgetState.kt
index b4d643d43..873ff90e8 100644
--- a/app/src/main/kotlin/org/meshtastic/app/widget/LocalStatsWidgetState.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/widget/LocalStatsWidgetState.kt
@@ -28,6 +28,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
+import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.Node
@@ -36,8 +37,6 @@ import org.meshtastic.core.repository.AppWidgetUpdater
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.proto.LocalStats
-import javax.inject.Inject
-import javax.inject.Singleton
data class LocalStatsWidgetUiState(
val connectionState: ConnectionState = ConnectionState.Disconnected,
@@ -79,10 +78,8 @@ data class LocalStatsWidgetUiState(
val updateTimeMillis: Long = 0,
)
-@Singleton
-class LocalStatsWidgetStateProvider
-@Inject
-constructor(
+@Single
+class LocalStatsWidgetStateProvider(
nodeRepository: NodeRepository,
serviceRepository: ServiceRepository,
appWidgetUpdater: AppWidgetUpdater,
diff --git a/app/src/main/kotlin/org/meshtastic/app/widget/RefreshLocalStatsAction.kt b/app/src/main/kotlin/org/meshtastic/app/widget/RefreshLocalStatsAction.kt
index e8a060681..291fc395e 100644
--- a/app/src/main/kotlin/org/meshtastic/app/widget/RefreshLocalStatsAction.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/widget/RefreshLocalStatsAction.kt
@@ -20,30 +20,20 @@ import android.content.Context
import androidx.glance.GlanceId
import androidx.glance.action.ActionParameters
import androidx.glance.appwidget.action.ActionCallback
-import dagger.hilt.EntryPoint
-import dagger.hilt.InstallIn
-import dagger.hilt.android.EntryPointAccessors
-import dagger.hilt.components.SingletonComponent
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
import org.meshtastic.core.model.TelemetryType
import org.meshtastic.core.repository.CommandSender
import org.meshtastic.core.repository.NodeManager
-class RefreshLocalStatsAction : ActionCallback {
+class RefreshLocalStatsAction :
+ ActionCallback,
+ KoinComponent {
- @EntryPoint
- @InstallIn(SingletonComponent::class)
- interface RefreshLocalStatsEntryPoint {
- fun commandSender(): CommandSender
-
- fun nodeManager(): NodeManager
- }
+ private val commandSender: CommandSender by inject()
+ private val nodeManager: NodeManager by inject()
override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
- val entryPoint =
- EntryPointAccessors.fromApplication(context.applicationContext, RefreshLocalStatsEntryPoint::class.java)
- val commandSender = entryPoint.commandSender()
- val nodeManager = entryPoint.nodeManager()
-
val myNodeNum = nodeManager.myNodeNum ?: return
commandSender.requestTelemetry(commandSender.generatePacketId(), myNodeNum, TelemetryType.LOCAL_STATS.ordinal)
diff --git a/app/src/main/kotlin/org/meshtastic/app/worker/MeshLogCleanupWorker.kt b/app/src/main/kotlin/org/meshtastic/app/worker/MeshLogCleanupWorker.kt
index e4e34a99d..11495b645 100644
--- a/app/src/main/kotlin/org/meshtastic/app/worker/MeshLogCleanupWorker.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/worker/MeshLogCleanupWorker.kt
@@ -17,40 +17,21 @@
package org.meshtastic.app.worker
import android.content.Context
-import androidx.hilt.work.HiltWorker
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import co.touchlab.kermit.Logger
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
-import dagger.hilt.EntryPoint
-import dagger.hilt.InstallIn
-import dagger.hilt.android.EntryPointAccessors
-import dagger.hilt.components.SingletonComponent
+import org.koin.android.annotation.KoinWorker
import org.meshtastic.core.repository.MeshLogPrefs
import org.meshtastic.core.repository.MeshLogRepository
-@HiltWorker
-class MeshLogCleanupWorker
-@AssistedInject
-constructor(
- @Assisted appContext: Context,
- @Assisted workerParams: WorkerParameters,
+@KoinWorker
+class MeshLogCleanupWorker(
+ appContext: Context,
+ workerParams: WorkerParameters,
private val meshLogRepository: MeshLogRepository,
private val meshLogPrefs: MeshLogPrefs,
) : CoroutineWorker(appContext, workerParams) {
- // Fallback constructor for cases where HiltWorkerFactory is not used (e.g., some WorkManager initializations)
- constructor(
- appContext: Context,
- workerParams: WorkerParameters,
- ) : this(
- appContext,
- workerParams,
- entryPoint(appContext).meshLogRepository(),
- entryPoint(appContext).meshLogPrefs(),
- )
-
@Suppress("TooGenericExceptionCaught")
override suspend fun doWork(): Result = try {
val retentionDays = meshLogPrefs.retentionDays.value
@@ -77,18 +58,7 @@ constructor(
companion object {
const val WORK_NAME = "meshlog_cleanup_worker"
-
- private fun entryPoint(context: Context): WorkerEntryPoint =
- EntryPointAccessors.fromApplication(context.applicationContext, WorkerEntryPoint::class.java)
}
private val logger = Logger.withTag(WORK_NAME)
}
-
-@EntryPoint
-@InstallIn(SingletonComponent::class)
-interface WorkerEntryPoint {
- fun meshLogRepository(): MeshLogRepository
-
- fun meshLogPrefs(): MeshLogPrefs
-}
diff --git a/app/src/main/kotlin/org/meshtastic/app/worker/ServiceKeepAliveWorker.kt b/app/src/main/kotlin/org/meshtastic/app/worker/ServiceKeepAliveWorker.kt
index ec443d408..b83fc9aff 100644
--- a/app/src/main/kotlin/org/meshtastic/app/worker/ServiceKeepAliveWorker.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/worker/ServiceKeepAliveWorker.kt
@@ -21,13 +21,11 @@ import android.content.Context
import android.content.pm.ServiceInfo
import android.os.Build
import androidx.core.app.NotificationCompat
-import androidx.hilt.work.HiltWorker
import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters
import co.touchlab.kermit.Logger
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
+import org.koin.android.annotation.KoinWorker
import org.meshtastic.app.R
import org.meshtastic.app.service.MeshService
import org.meshtastic.app.service.startService
@@ -39,12 +37,10 @@ import org.meshtastic.core.repository.SERVICE_NOTIFY_ID
* `startForegroundService` is blocked by Android 14+ restrictions. It runs as an Expedited worker to gain temporary
* foreground start privileges.
*/
-@HiltWorker
-class ServiceKeepAliveWorker
-@AssistedInject
-constructor(
- @Assisted appContext: Context,
- @Assisted workerParams: WorkerParameters,
+@KoinWorker
+class ServiceKeepAliveWorker(
+ appContext: Context,
+ workerParams: WorkerParameters,
private val serviceNotifications: MeshServiceNotifications,
) : CoroutineWorker(appContext, workerParams) {
diff --git a/feature/settings/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml
similarity index 100%
rename from feature/settings/src/main/res/xml/locales_config.xml
rename to app/src/main/res/xml/locales_config.xml
diff --git a/app/src/test/kotlin/org/meshtastic/app/MeshTestApplication.kt b/app/src/test/kotlin/org/meshtastic/app/MeshTestApplication.kt
deleted file mode 100644
index 45381aa98..000000000
--- a/app/src/test/kotlin/org/meshtastic/app/MeshTestApplication.kt
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * 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 org.meshtastic.app
-
-import androidx.work.Configuration
-import dagger.hilt.android.EntryPointAccessors
-
-/**
- * A lightweight application class for Robolectric tests.
- *
- * It prevents heavy background initialization (WorkManager, DatabaseManager) by default to avoid resource leaks and
- * flaky native SQLite issues on the JVM.
- */
-class MeshTestApplication : MeshUtilApplication() {
-
- override fun onCreate() {
- // Only run real onCreate logic if a test explicitly asks for it
- if (shouldInitialize) {
- super.onCreate()
- }
- }
-
- override fun onTerminate() {
- if (shouldInitialize) {
- val entryPoint = EntryPointAccessors.fromApplication(this, AppEntryPoint::class.java)
- entryPoint.databaseManager().close()
- }
- super.onTerminate()
- }
-
- override val workManagerConfiguration: Configuration
- get() = Configuration.Builder().setMinimumLoggingLevel(android.util.Log.DEBUG).build()
-
- companion object {
- /** Set to true in a test @Before block if you need real DB/WorkManager init. */
- var shouldInitialize = false
- }
-}
diff --git a/app/src/test/kotlin/org/meshtastic/app/di/KoinVerificationTest.kt b/app/src/test/kotlin/org/meshtastic/app/di/KoinVerificationTest.kt
new file mode 100644
index 000000000..dce13a652
--- /dev/null
+++ b/app/src/test/kotlin/org/meshtastic/app/di/KoinVerificationTest.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 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 org.meshtastic.app.di
+
+import android.app.Application
+import android.content.Context
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.SavedStateHandle
+import androidx.work.WorkManager
+import androidx.work.WorkerParameters
+import io.ktor.client.HttpClient
+import io.ktor.client.engine.HttpClientEngine
+import kotlinx.coroutines.CoroutineDispatcher
+import okhttp3.OkHttpClient
+import org.junit.Test
+import org.koin.test.verify.verify
+import org.meshtastic.core.model.util.NodeIdLookup
+
+class KoinVerificationTest {
+
+ @Test
+ fun verifyKoinConfiguration() {
+ AppKoinModule()
+ .module()
+ .verify(
+ extraTypes =
+ listOf(
+ Application::class,
+ Context::class,
+ Lifecycle::class,
+ SavedStateHandle::class,
+ WorkerParameters::class,
+ WorkManager::class,
+ CoroutineDispatcher::class,
+ NodeIdLookup::class,
+ HttpClient::class,
+ HttpClientEngine::class,
+ OkHttpClient::class,
+ ),
+ )
+ }
+}
diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts
index 1208de17f..041693fbb 100644
--- a/build-logic/convention/build.gradle.kts
+++ b/build-logic/convention/build.gradle.kts
@@ -52,7 +52,7 @@ dependencies {
compileOnly(libs.dokka.gradlePlugin)
compileOnly(libs.firebase.crashlytics.gradlePlugin)
compileOnly(libs.google.services.gradlePlugin)
- compileOnly(libs.hilt.gradlePlugin)
+ compileOnly(libs.koin.gradlePlugin)
implementation(libs.kover.gradlePlugin)
compileOnly(libs.kotlin.gradlePlugin)
compileOnly(libs.ksp.gradlePlugin)
@@ -144,9 +144,9 @@ gradlePlugin {
id = "meshtastic.analytics"
implementationClass = "AnalyticsConventionPlugin"
}
- register("meshtasticHilt") {
- id = "meshtastic.hilt"
- implementationClass = "HiltConventionPlugin"
+ register("meshtasticKoin") {
+ id = "meshtastic.koin"
+ implementationClass = "KoinConventionPlugin"
}
register("meshtasticDetekt") {
id = "meshtastic.detekt"
diff --git a/build-logic/convention/src/main/kotlin/HiltConventionPlugin.kt b/build-logic/convention/src/main/kotlin/KoinConventionPlugin.kt
similarity index 52%
rename from build-logic/convention/src/main/kotlin/HiltConventionPlugin.kt
rename to build-logic/convention/src/main/kotlin/KoinConventionPlugin.kt
index f570e721e..9539f439d 100644
--- a/build-logic/convention/src/main/kotlin/HiltConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/KoinConventionPlugin.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025 Meshtastic LLC
+ * Copyright (c) 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
@@ -19,33 +19,31 @@ import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.dependencies
-import org.meshtastic.buildlogic.library
import org.meshtastic.buildlogic.libs
+import org.meshtastic.buildlogic.plugin
-class HiltConventionPlugin : Plugin {
+class KoinConventionPlugin : Plugin {
override fun apply(target: Project) {
with(target) {
- apply(plugin = "com.google.devtools.ksp")
+ apply(plugin = libs.plugin("koin-compiler").get().pluginId)
- dependencies {
- "ksp"(libs.library("hilt-compiler"))
- "implementation"(libs.library("hilt-android"))
- }
+ val koinAnnotations = libs.findLibrary("koin-annotations").get()
+ val koinCore = libs.findLibrary("koin-core").get()
- // Add support for Jvm Module, base on org.jetbrains.kotlin.jvm
- pluginManager.withPlugin("org.jetbrains.kotlin.jvm") {
+ pluginManager.withPlugin("org.jetbrains.kotlin.multiplatform") {
dependencies {
- "implementation"(libs.library("hilt-core"))
+ add("commonMainApi", koinCore)
+ add("commonMainApi", koinAnnotations)
}
}
- pluginManager.withPlugin("com.android.base") {
- apply(plugin = "dagger.hilt.android.plugin")
- }
-
- pluginManager.withPlugin("org.jetbrains.kotlin.plugin.compose") {
- dependencies {
- "implementation"(libs.library("androidx-hilt-lifecycle-viewmodel-compose"))
+ pluginManager.withPlugin("org.jetbrains.kotlin.android") {
+ // If this is *only* an Android module (no KMP plugin)
+ if (!pluginManager.hasPlugin("org.jetbrains.kotlin.multiplatform")) {
+ dependencies {
+ add("implementation", koinCore)
+ add("implementation", koinAnnotations)
+ }
}
}
}
diff --git a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Dokka.kt b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Dokka.kt
index e3bb46435..6a01d75ba 100644
--- a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Dokka.kt
+++ b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Dokka.kt
@@ -29,7 +29,7 @@ fun Project.configureDokka() {
dokkaSourceSets.configureEach {
perPackageOption {
- matchingRegex.set("hilt_aggregated_deps")
+ matchingRegex.set("koin_aggregated_deps")
suppress.set(true)
}
perPackageOption {
diff --git a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Kover.kt b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Kover.kt
index b4c4deedd..20b542977 100644
--- a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Kover.kt
+++ b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Kover.kt
@@ -47,8 +47,6 @@ fun Project.configureKover() {
// Exclude declarations
annotatedBy(
- "*.HiltAndroidApp",
- "*.AndroidEntryPoint",
"*.Module",
"*.Provides",
"*.Binds",
@@ -56,7 +54,7 @@ fun Project.configureKover() {
)
// Suppress generated code
- packages("hilt_aggregated_deps")
+ packages("koin_aggregated_deps")
packages("org.meshtastic.core.resources")
}
}
diff --git a/build.gradle.kts b/build.gradle.kts
index 78b748ae5..94e4fd3c3 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -24,10 +24,10 @@ plugins {
alias(libs.plugins.compose.multiplatform) apply false
alias(libs.plugins.datadog) apply false
alias(libs.plugins.devtools.ksp) apply false
+ alias(libs.plugins.koin.compiler) apply false
alias(libs.plugins.firebase.crashlytics) apply false
alias(libs.plugins.firebase.perf) apply false
alias(libs.plugins.google.services) apply false
- alias(libs.plugins.hilt) apply false
alias(libs.plugins.room) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.jvm) apply false
diff --git a/core/ble/README.md b/core/ble/README.md
index 02b893b33..29b3d2756 100644
--- a/core/ble/README.md
+++ b/core/ble/README.md
@@ -75,7 +75,7 @@ The module follows a clean architecture approach:
- **Repository Pattern:** `BluetoothRepository` mediates data access.
- **Coroutines & Flow:** All asynchronous operations use Kotlin Coroutines and Flows.
-- **Dependency Injection:** Hilt is used for dependency injection.
+- **Dependency Injection:** Koin is used for dependency injection.
## Testing
diff --git a/core/ble/build.gradle.kts b/core/ble/build.gradle.kts
index 191a335be..a5e0d36eb 100644
--- a/core/ble/build.gradle.kts
+++ b/core/ble/build.gradle.kts
@@ -17,7 +17,7 @@
plugins {
alias(libs.plugins.meshtastic.kmp.library)
- alias(libs.plugins.devtools.ksp)
+ id("meshtastic.koin")
}
kotlin {
@@ -35,11 +35,9 @@ kotlin {
implementation(libs.kermit)
implementation(libs.kotlinx.coroutines.core)
- api(libs.javax.inject)
}
androidMain.dependencies {
- implementation(libs.hilt.android)
api(libs.nordic.client.android)
api(libs.nordic.ble.env.android)
api(libs.nordic.ble.env.android.compose)
@@ -65,5 +63,3 @@ kotlin {
}
}
}
-
-dependencies { add("kspAndroid", libs.hilt.compiler) }
diff --git a/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBleConnectionFactory.kt b/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBleConnectionFactory.kt
index 6166287ef..ff6123a59 100644
--- a/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBleConnectionFactory.kt
+++ b/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBleConnectionFactory.kt
@@ -18,13 +18,11 @@ package org.meshtastic.core.ble
import kotlinx.coroutines.CoroutineScope
import no.nordicsemi.kotlin.ble.client.android.CentralManager
-import javax.inject.Inject
-import javax.inject.Singleton
+import org.koin.core.annotation.Single
/** An Android implementation of [BleConnectionFactory]. */
-@Singleton
-class AndroidBleConnectionFactory @Inject constructor(private val centralManager: CentralManager) :
- BleConnectionFactory {
+@Single
+class AndroidBleConnectionFactory(private val centralManager: CentralManager) : BleConnectionFactory {
override fun create(scope: CoroutineScope, tag: String): BleConnection =
AndroidBleConnection(centralManager, scope, tag)
}
diff --git a/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBleScanner.kt b/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBleScanner.kt
index 828ed6d10..8d1ff6008 100644
--- a/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBleScanner.kt
+++ b/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBleScanner.kt
@@ -20,7 +20,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import no.nordicsemi.kotlin.ble.client.android.CentralManager
import no.nordicsemi.kotlin.ble.client.distinctByPeripheral
-import javax.inject.Inject
+import org.koin.core.annotation.Single
import kotlin.time.Duration
/**
@@ -28,7 +28,8 @@ import kotlin.time.Duration
*
* @param centralManager The Nordic [CentralManager] to use for scanning.
*/
-class AndroidBleScanner @Inject constructor(private val centralManager: CentralManager) : BleScanner {
+@Single
+class AndroidBleScanner(private val centralManager: CentralManager) : BleScanner {
override fun scan(timeout: Duration): Flow =
centralManager.scan(timeout).distinctByPeripheral().map { AndroidBleDevice(it.peripheral) }
diff --git a/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBluetoothRepository.kt b/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBluetoothRepository.kt
index 24137e8a2..0b5663071 100644
--- a/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBluetoothRepository.kt
+++ b/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBluetoothRepository.kt
@@ -29,20 +29,17 @@ import no.nordicsemi.kotlin.ble.client.RemoteServices
import no.nordicsemi.kotlin.ble.client.android.CentralManager
import no.nordicsemi.kotlin.ble.client.android.Peripheral
import no.nordicsemi.kotlin.ble.core.android.AndroidEnvironment
+import org.koin.core.annotation.Named
+import org.koin.core.annotation.Single
import org.meshtastic.core.ble.MeshtasticBleConstants.BLE_NAME_PATTERN
import org.meshtastic.core.ble.MeshtasticBleConstants.SERVICE_UUID
import org.meshtastic.core.di.CoroutineDispatchers
-import org.meshtastic.core.di.ProcessLifecycle
-import javax.inject.Inject
-import javax.inject.Singleton
/** Android implementation of [BluetoothRepository]. */
-@Singleton
-class AndroidBluetoothRepository
-@Inject
-constructor(
+@Single
+class AndroidBluetoothRepository(
private val dispatchers: CoroutineDispatchers,
- @ProcessLifecycle private val processLifecycle: Lifecycle,
+ @Named("ProcessLifecycle") private val processLifecycle: Lifecycle,
private val centralManager: CentralManager,
private val androidEnvironment: AndroidEnvironment,
) : BluetoothRepository {
diff --git a/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/di/CoreBleAndroidModule.kt b/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/di/CoreBleAndroidModule.kt
new file mode 100644
index 000000000..8e8a8b128
--- /dev/null
+++ b/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/di/CoreBleAndroidModule.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 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 org.meshtastic.core.ble.di
+
+import android.app.Application
+import android.location.LocationManager
+import androidx.core.content.ContextCompat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import no.nordicsemi.kotlin.ble.client.android.CentralManager
+import no.nordicsemi.kotlin.ble.client.android.native
+import no.nordicsemi.kotlin.ble.core.android.AndroidEnvironment
+import no.nordicsemi.kotlin.ble.environment.android.NativeAndroidEnvironment
+import org.koin.core.annotation.ComponentScan
+import org.koin.core.annotation.Module
+import org.koin.core.annotation.Single
+
+@Module
+@ComponentScan("org.meshtastic.core.ble")
+class CoreBleAndroidModule {
+ @Single
+ fun provideAndroidEnvironment(app: Application): AndroidEnvironment =
+ NativeAndroidEnvironment.getInstance(app, isNeverForLocationFlagSet = true)
+
+ @Single
+ fun provideCentralManager(environment: AndroidEnvironment): CentralManager = CentralManager.native(
+ environment as NativeAndroidEnvironment,
+ CoroutineScope(SupervisorJob() + Dispatchers.Default),
+ )
+
+ @Single
+ fun provideLocationManager(app: Application): LocationManager =
+ ContextCompat.getSystemService(app, LocationManager::class.java)!!
+}
diff --git a/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/di/CoreBleModule.kt b/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/di/CoreBleModule.kt
new file mode 100644
index 000000000..f064fcb63
--- /dev/null
+++ b/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/di/CoreBleModule.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 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 org.meshtastic.core.ble.di
+
+import org.koin.core.annotation.ComponentScan
+import org.koin.core.annotation.Module
+
+@Module
+@ComponentScan("org.meshtastic.core.ble")
+class CoreBleModule
diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts
index 09d77c011..21cb3a2b0 100644
--- a/core/common/build.gradle.kts
+++ b/core/common/build.gradle.kts
@@ -18,6 +18,7 @@
plugins {
alias(libs.plugins.meshtastic.kmp.library)
alias(libs.plugins.kotlin.parcelize)
+ id("meshtastic.koin")
}
kotlin {
diff --git a/core/common/src/commonMain/kotlin/org/meshtastic/core/common/di/CoreCommonModule.kt b/core/common/src/commonMain/kotlin/org/meshtastic/core/common/di/CoreCommonModule.kt
new file mode 100644
index 000000000..721a31749
--- /dev/null
+++ b/core/common/src/commonMain/kotlin/org/meshtastic/core/common/di/CoreCommonModule.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 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 org.meshtastic.core.common.di
+
+import org.koin.core.annotation.ComponentScan
+import org.koin.core.annotation.Module
+
+@Module
+@ComponentScan("org.meshtastic.core.common")
+class CoreCommonModule
diff --git a/core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/SequentialJob.kt b/core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/SequentialJob.kt
index 6046c68b6..31f103879 100644
--- a/core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/SequentialJob.kt
+++ b/core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/SequentialJob.kt
@@ -21,15 +21,16 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.withTimeout
+import org.koin.core.annotation.Factory
import java.util.concurrent.atomic.AtomicReference
-import javax.inject.Inject
/**
* A helper class that manages a single [Job]. When a new job is launched, any previous job is cancelled. This is useful
* for ensuring that only the latest operation of a certain type is running at a time (e.g. for search or settings
* updates).
*/
-class SequentialJob @Inject constructor() {
+@Factory
+class SequentialJob {
private val job = AtomicReference()
/**
diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts
index e2bd4480b..98bf7e0cd 100644
--- a/core/data/build.gradle.kts
+++ b/core/data/build.gradle.kts
@@ -18,7 +18,7 @@
plugins {
alias(libs.plugins.meshtastic.kmp.library)
alias(libs.plugins.meshtastic.kotlinx.serialization)
- alias(libs.plugins.devtools.ksp)
+ id("meshtastic.koin")
}
kotlin {
@@ -41,7 +41,6 @@ kotlin {
implementation(projects.core.prefs)
implementation(projects.core.proto)
- api(libs.javax.inject)
implementation(libs.androidx.lifecycle.runtime)
implementation(libs.androidx.paging.common)
implementation(libs.kotlinx.serialization.json)
@@ -51,7 +50,6 @@ kotlin {
}
androidMain.dependencies {
- implementation(libs.hilt.android)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.core.location.altitude)
@@ -68,5 +66,3 @@ kotlin {
}
}
}
-
-dependencies { add("kspAndroid", libs.hilt.compiler) }
diff --git a/core/data/detekt-baseline.xml b/core/data/detekt-baseline.xml
index 2354a0f89..c373eea43 100644
--- a/core/data/detekt-baseline.xml
+++ b/core/data/detekt-baseline.xml
@@ -1,7 +1,5 @@
-
- MaxLineLength:BootloaderOtaQuirksJsonDataSourceImpl.kt$BootloaderOtaQuirksJsonDataSourceImpl$class
-
+
diff --git a/core/data/src/androidMain/kotlin/org/meshtastic/core/data/datasource/BootloaderOtaQuirksJsonDataSourceImpl.kt b/core/data/src/androidMain/kotlin/org/meshtastic/core/data/datasource/BootloaderOtaQuirksJsonDataSourceImpl.kt
index aa301ed7c..3bfd72cfa 100644
--- a/core/data/src/androidMain/kotlin/org/meshtastic/core/data/datasource/BootloaderOtaQuirksJsonDataSourceImpl.kt
+++ b/core/data/src/androidMain/kotlin/org/meshtastic/core/data/datasource/BootloaderOtaQuirksJsonDataSourceImpl.kt
@@ -22,11 +22,11 @@ import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
+import org.koin.core.annotation.Single
import org.meshtastic.core.model.BootloaderOtaQuirk
-import javax.inject.Inject
-class BootloaderOtaQuirksJsonDataSourceImpl @Inject constructor(private val application: Application) :
- BootloaderOtaQuirksJsonDataSource {
+@Single
+class BootloaderOtaQuirksJsonDataSourceImpl(private val application: Application) : BootloaderOtaQuirksJsonDataSource {
@OptIn(ExperimentalSerializationApi::class)
override fun loadBootloaderOtaQuirksFromJsonAsset(): List = runCatching {
val inputStream = application.assets.open("device_bootloader_ota_quirks.json")
diff --git a/core/data/src/androidMain/kotlin/org/meshtastic/core/data/datasource/DeviceHardwareJsonDataSourceImpl.kt b/core/data/src/androidMain/kotlin/org/meshtastic/core/data/datasource/DeviceHardwareJsonDataSourceImpl.kt
index e741ad476..327cddcae 100644
--- a/core/data/src/androidMain/kotlin/org/meshtastic/core/data/datasource/DeviceHardwareJsonDataSourceImpl.kt
+++ b/core/data/src/androidMain/kotlin/org/meshtastic/core/data/datasource/DeviceHardwareJsonDataSourceImpl.kt
@@ -20,11 +20,11 @@ import android.app.Application
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
+import org.koin.core.annotation.Single
import org.meshtastic.core.model.NetworkDeviceHardware
-import javax.inject.Inject
-class DeviceHardwareJsonDataSourceImpl @Inject constructor(private val application: Application) :
- DeviceHardwareJsonDataSource {
+@Single
+class DeviceHardwareJsonDataSourceImpl(private val application: Application) : DeviceHardwareJsonDataSource {
// Use a tolerant JSON parser so that additional fields in the bundled asset
// (e.g., "key") do not break deserialization on older app versions.
diff --git a/core/data/src/androidMain/kotlin/org/meshtastic/core/data/datasource/FirmwareReleaseJsonDataSourceImpl.kt b/core/data/src/androidMain/kotlin/org/meshtastic/core/data/datasource/FirmwareReleaseJsonDataSourceImpl.kt
index bc745898c..c060f4b21 100644
--- a/core/data/src/androidMain/kotlin/org/meshtastic/core/data/datasource/FirmwareReleaseJsonDataSourceImpl.kt
+++ b/core/data/src/androidMain/kotlin/org/meshtastic/core/data/datasource/FirmwareReleaseJsonDataSourceImpl.kt
@@ -20,11 +20,11 @@ import android.app.Application
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
+import org.koin.core.annotation.Single
import org.meshtastic.core.model.NetworkFirmwareReleases
-import javax.inject.Inject
-class FirmwareReleaseJsonDataSourceImpl @Inject constructor(private val application: Application) :
- FirmwareReleaseJsonDataSource {
+@Single
+class FirmwareReleaseJsonDataSourceImpl(private val application: Application) : FirmwareReleaseJsonDataSource {
// Match the network client behavior: be tolerant of unknown fields so that
// older app versions can read newer snapshots of firmware_releases.json.
diff --git a/core/data/src/androidMain/kotlin/org/meshtastic/core/data/di/CoreDataAndroidModule.kt b/core/data/src/androidMain/kotlin/org/meshtastic/core/data/di/CoreDataAndroidModule.kt
new file mode 100644
index 000000000..e9fcd0552
--- /dev/null
+++ b/core/data/src/androidMain/kotlin/org/meshtastic/core/data/di/CoreDataAndroidModule.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 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 org.meshtastic.core.data.di
+
+import org.koin.core.annotation.ComponentScan
+import org.koin.core.annotation.Module
+
+@Module
+@ComponentScan("org.meshtastic.core.data")
+class CoreDataAndroidModule
diff --git a/core/data/src/androidMain/kotlin/org/meshtastic/core/data/repository/LocationRepositoryImpl.kt b/core/data/src/androidMain/kotlin/org/meshtastic/core/data/repository/LocationRepositoryImpl.kt
index bea36529e..72460c33e 100644
--- a/core/data/src/androidMain/kotlin/org/meshtastic/core/data/repository/LocationRepositoryImpl.kt
+++ b/core/data/src/androidMain/kotlin/org/meshtastic/core/data/repository/LocationRepositoryImpl.kt
@@ -34,19 +34,16 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow
+import org.koin.core.annotation.Single
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.repository.Location
import org.meshtastic.core.repository.LocationRepository
import org.meshtastic.core.repository.PlatformAnalytics
-import javax.inject.Inject
-import javax.inject.Singleton
-@Singleton
-class LocationRepositoryImpl
-@Inject
-constructor(
+@Single
+class LocationRepositoryImpl(
private val context: Application,
- private val locationManager: dagger.Lazy,
+ private val locationManager: Lazy,
private val analytics: PlatformAnalytics,
private val dispatchers: CoroutineDispatchers,
) : LocationRepository {
@@ -125,5 +122,5 @@ constructor(
/** Observable flow for location updates */
@RequiresPermission(anyOf = [ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION])
- override fun getLocations(): Flow = locationManager.get().requestLocationUpdates()
+ override fun getLocations(): Flow = locationManager.value.requestLocationUpdates()
}
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/DeviceHardwareLocalDataSource.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/DeviceHardwareLocalDataSource.kt
index a73a65899..918ff6c18 100644
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/DeviceHardwareLocalDataSource.kt
+++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/DeviceHardwareLocalDataSource.kt
@@ -17,16 +17,15 @@
package org.meshtastic.core.data.datasource
import kotlinx.coroutines.withContext
+import org.koin.core.annotation.Single
import org.meshtastic.core.database.DatabaseManager
import org.meshtastic.core.database.entity.DeviceHardwareEntity
import org.meshtastic.core.database.entity.asEntity
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.model.NetworkDeviceHardware
-import javax.inject.Inject
-class DeviceHardwareLocalDataSource
-@Inject
-constructor(
+@Single
+class DeviceHardwareLocalDataSource(
private val dbManager: DatabaseManager,
private val dispatchers: CoroutineDispatchers,
) {
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/FirmwareReleaseLocalDataSource.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/FirmwareReleaseLocalDataSource.kt
index 3f1a05c7f..3f93e901e 100644
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/FirmwareReleaseLocalDataSource.kt
+++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/FirmwareReleaseLocalDataSource.kt
@@ -17,6 +17,7 @@
package org.meshtastic.core.data.datasource
import kotlinx.coroutines.withContext
+import org.koin.core.annotation.Single
import org.meshtastic.core.database.DatabaseManager
import org.meshtastic.core.database.entity.FirmwareReleaseEntity
import org.meshtastic.core.database.entity.FirmwareReleaseType
@@ -24,11 +25,9 @@ import org.meshtastic.core.database.entity.asDeviceVersion
import org.meshtastic.core.database.entity.asEntity
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.model.NetworkFirmwareRelease
-import javax.inject.Inject
-class FirmwareReleaseLocalDataSource
-@Inject
-constructor(
+@Single
+class FirmwareReleaseLocalDataSource(
private val dbManager: DatabaseManager,
private val dispatchers: CoroutineDispatchers,
) {
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/SwitchingNodeInfoReadDataSource.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/SwitchingNodeInfoReadDataSource.kt
index 35d9c0848..5fd91b26f 100644
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/SwitchingNodeInfoReadDataSource.kt
+++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/SwitchingNodeInfoReadDataSource.kt
@@ -18,16 +18,14 @@ package org.meshtastic.core.data.datasource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flatMapLatest
+import org.koin.core.annotation.Single
import org.meshtastic.core.database.DatabaseManager
import org.meshtastic.core.database.entity.MyNodeEntity
import org.meshtastic.core.database.entity.NodeEntity
import org.meshtastic.core.database.entity.NodeWithRelations
-import javax.inject.Inject
-import javax.inject.Singleton
-@Singleton
-class SwitchingNodeInfoReadDataSource @Inject constructor(private val dbManager: DatabaseManager) :
- NodeInfoReadDataSource {
+@Single
+class SwitchingNodeInfoReadDataSource(private val dbManager: DatabaseManager) : NodeInfoReadDataSource {
override fun myNodeInfoFlow(): Flow =
dbManager.currentDb.flatMapLatest { db -> db.nodeInfoDao().getMyNodeInfo() }
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/SwitchingNodeInfoWriteDataSource.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/SwitchingNodeInfoWriteDataSource.kt
index 6b5501910..31d41fe9e 100644
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/SwitchingNodeInfoWriteDataSource.kt
+++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/SwitchingNodeInfoWriteDataSource.kt
@@ -17,18 +17,15 @@
package org.meshtastic.core.data.datasource
import kotlinx.coroutines.withContext
+import org.koin.core.annotation.Single
import org.meshtastic.core.database.DatabaseManager
import org.meshtastic.core.database.entity.MetadataEntity
import org.meshtastic.core.database.entity.MyNodeEntity
import org.meshtastic.core.database.entity.NodeEntity
import org.meshtastic.core.di.CoroutineDispatchers
-import javax.inject.Inject
-import javax.inject.Singleton
-@Singleton
-class SwitchingNodeInfoWriteDataSource
-@Inject
-constructor(
+@Single
+class SwitchingNodeInfoWriteDataSource(
private val dbManager: DatabaseManager,
private val dispatchers: CoroutineDispatchers,
) : NodeInfoWriteDataSource {
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/di/CoreDataModule.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/di/CoreDataModule.kt
new file mode 100644
index 000000000..834cff2c2
--- /dev/null
+++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/di/CoreDataModule.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 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 org.meshtastic.core.data.di
+
+import org.koin.core.annotation.ComponentScan
+import org.koin.core.annotation.Module
+import org.koin.core.annotation.Single
+import org.meshtastic.core.model.util.MeshDataMapper
+import org.meshtastic.core.model.util.NodeIdLookup
+
+@Module
+@ComponentScan("org.meshtastic.core.data")
+class CoreDataModule {
+ @Single fun provideMeshDataMapper(nodeIdLookup: NodeIdLookup): MeshDataMapper = MeshDataMapper(nodeIdLookup)
+}
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt
index c137ea8f6..b296cef01 100644
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt
+++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt
@@ -26,6 +26,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import okio.ByteString
import okio.ByteString.Companion.toByteString
+import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.MessageStatus
@@ -46,17 +47,13 @@ import org.meshtastic.proto.Neighbor
import org.meshtastic.proto.NeighborInfo
import org.meshtastic.proto.PortNum
import org.meshtastic.proto.Telemetry
-import javax.inject.Inject
-import javax.inject.Singleton
import kotlin.math.absoluteValue
import kotlin.random.Random
import kotlin.time.Duration.Companion.hours
@Suppress("TooManyFunctions", "CyclomaticComplexMethod")
-@Singleton
-class CommandSenderImpl
-@Inject
-constructor(
+@Single
+class CommandSenderImpl(
private val packetHandler: PacketHandler,
private val nodeManager: NodeManager,
private val radioConfigRepository: RadioConfigRepository,
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImpl.kt
index 081d1a207..34bc23128 100644
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImpl.kt
+++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImpl.kt
@@ -16,7 +16,7 @@
*/
package org.meshtastic.core.data.manager
-import dagger.Lazy
+import org.koin.core.annotation.Single
import org.meshtastic.core.repository.FromRadioPacketHandler
import org.meshtastic.core.repository.MeshRouter
import org.meshtastic.core.repository.MeshServiceNotifications
@@ -24,14 +24,10 @@ import org.meshtastic.core.repository.MqttManager
import org.meshtastic.core.repository.PacketHandler
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.proto.FromRadio
-import javax.inject.Inject
-import javax.inject.Singleton
/** Implementation of [FromRadioPacketHandler] that dispatches [FromRadio] variants to specialized handlers. */
-@Singleton
-class FromRadioPacketHandlerImpl
-@Inject
-constructor(
+@Single
+class FromRadioPacketHandlerImpl(
private val serviceRepository: ServiceRepository,
private val router: Lazy,
private val mqttManager: MqttManager,
@@ -52,18 +48,18 @@ constructor(
val clientNotification = proto.clientNotification
when {
- myInfo != null -> router.get().configFlowManager.handleMyInfo(myInfo)
- metadata != null -> router.get().configFlowManager.handleLocalMetadata(metadata)
+ myInfo != null -> router.value.configFlowManager.handleMyInfo(myInfo)
+ metadata != null -> router.value.configFlowManager.handleLocalMetadata(metadata)
nodeInfo != null -> {
- router.get().configFlowManager.handleNodeInfo(nodeInfo)
- serviceRepository.setConnectionProgress("Nodes (${router.get().configFlowManager.newNodeCount})")
+ router.value.configFlowManager.handleNodeInfo(nodeInfo)
+ serviceRepository.setConnectionProgress("Nodes (${router.value.configFlowManager.newNodeCount})")
}
- configCompleteId != null -> router.get().configFlowManager.handleConfigComplete(configCompleteId)
+ configCompleteId != null -> router.value.configFlowManager.handleConfigComplete(configCompleteId)
mqttProxyMessage != null -> mqttManager.handleMqttProxyMessage(mqttProxyMessage)
queueStatus != null -> packetHandler.handleQueueStatus(queueStatus)
- config != null -> router.get().configHandler.handleDeviceConfig(config)
- moduleConfig != null -> router.get().configHandler.handleModuleConfig(moduleConfig)
- channel != null -> router.get().configHandler.handleChannel(channel)
+ config != null -> router.value.configHandler.handleDeviceConfig(config)
+ moduleConfig != null -> router.value.configHandler.handleModuleConfig(moduleConfig)
+ channel != null -> router.value.configHandler.handleChannel(channel)
clientNotification != null -> {
serviceRepository.setClientNotification(clientNotification)
serviceNotifications.showClientNotification(clientNotification)
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/HistoryManagerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/HistoryManagerImpl.kt
index 085966a2b..09961847f 100644
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/HistoryManagerImpl.kt
+++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/HistoryManagerImpl.kt
@@ -18,6 +18,7 @@ package org.meshtastic.core.data.manager
import co.touchlab.kermit.Logger
import okio.ByteString.Companion.toByteString
+import org.koin.core.annotation.Single
import org.meshtastic.core.repository.HistoryManager
import org.meshtastic.core.repository.MeshPrefs
import org.meshtastic.core.repository.PacketHandler
@@ -26,16 +27,9 @@ import org.meshtastic.proto.MeshPacket
import org.meshtastic.proto.ModuleConfig
import org.meshtastic.proto.PortNum
import org.meshtastic.proto.StoreAndForward
-import javax.inject.Inject
-import javax.inject.Singleton
-@Singleton
-class HistoryManagerImpl
-@Inject
-constructor(
- private val meshPrefs: MeshPrefs,
- private val packetHandler: PacketHandler,
-) : HistoryManager {
+@Single
+class HistoryManagerImpl(private val meshPrefs: MeshPrefs, private val packetHandler: PacketHandler) : HistoryManager {
companion object {
private const val HISTORY_TAG = "HistoryReplay"
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImpl.kt
index f2a5e7c8b..dcc0cc4a3 100644
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImpl.kt
+++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImpl.kt
@@ -16,11 +16,11 @@
*/
package org.meshtastic.core.data.manager
-import dagger.Lazy
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import okio.ByteString.Companion.toByteString
+import org.koin.core.annotation.Single
import org.meshtastic.core.common.database.DatabaseManager
import org.meshtastic.core.common.util.handledLaunch
import org.meshtastic.core.common.util.ignoreException
@@ -49,14 +49,10 @@ import org.meshtastic.proto.ModuleConfig
import org.meshtastic.proto.OTAMode
import org.meshtastic.proto.PortNum
import org.meshtastic.proto.User
-import javax.inject.Inject
-import javax.inject.Singleton
@Suppress("LongParameterList", "TooManyFunctions", "CyclomaticComplexMethod")
-@Singleton
-class MeshActionHandlerImpl
-@Inject
-constructor(
+@Single
+class MeshActionHandlerImpl(
private val nodeManager: NodeManager,
private val commandSender: CommandSender,
private val packetRepository: Lazy,
@@ -123,7 +119,7 @@ constructor(
}
}
nodeManager.updateNode(node.num) { it.copy(isIgnored = newIgnoredStatus) }
- scope.handledLaunch { packetRepository.get().updateFilteredBySender(node.user.id, newIgnoredStatus) }
+ scope.handledLaunch { packetRepository.value.updateFilteredBySender(node.user.id, newIgnoredStatus) }
}
private fun handleMute(action: ServiceAction.Mute, myNodeNum: Int) {
@@ -177,7 +173,7 @@ constructor(
to = action.contactKey.substring(1),
channel = action.contactKey[0].digitToInt(),
)
- packetRepository.get().insertReaction(reaction, myNodeNum)
+ packetRepository.value.insertReaction(reaction, myNodeNum)
}
}
@@ -190,7 +186,7 @@ constructor(
override fun handleSend(p: DataPacket, myNodeNum: Int) {
commandSender.sendData(p)
serviceBroadcasts.broadcastMessageStatus(p.id, p.status ?: MessageStatus.UNKNOWN)
- dataHandler.get().rememberDataPacket(p, myNodeNum, false)
+ dataHandler.value.rememberDataPacket(p, myNodeNum, false)
val bytes = p.bytes ?: okio.ByteString.EMPTY
analytics.track("data_send", DataPair("num_bytes", bytes.size), DataPair("type", p.dataType))
}
@@ -348,7 +344,7 @@ constructor(
meshPrefs.setDeviceAddress(deviceAddr)
scope.handledLaunch {
nodeManager.clear()
- messageProcessor.get().clearEarlyPackets()
+ messageProcessor.value.clearEarlyPackets()
databaseManager.switchActiveDatabase(deviceAddr)
serviceNotifications.clearNotifications()
nodeManager.loadCachedNodeDB()
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.kt
index d0daf20ed..ff20feddb 100644
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.kt
+++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.kt
@@ -17,12 +17,12 @@
package org.meshtastic.core.data.manager
import co.touchlab.kermit.Logger
-import dagger.Lazy
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import okio.IOException
+import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.handledLaunch
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.repository.CommandSender
@@ -40,16 +40,12 @@ import org.meshtastic.proto.HardwareModel
import org.meshtastic.proto.Heartbeat
import org.meshtastic.proto.NodeInfo
import org.meshtastic.proto.ToRadio
-import javax.inject.Inject
-import javax.inject.Singleton
import org.meshtastic.core.model.MyNodeInfo as SharedMyNodeInfo
import org.meshtastic.proto.MyNodeInfo as ProtoMyNodeInfo
@Suppress("LongParameterList", "TooManyFunctions")
-@Singleton
-class MeshConfigFlowManagerImpl
-@Inject
-constructor(
+@Single
+class MeshConfigFlowManagerImpl(
private val nodeManager: NodeManager,
private val connectionManager: Lazy,
private val nodeRepository: NodeRepository,
@@ -101,7 +97,7 @@ constructor(
} else {
myNodeInfo = finalizedInfo
Logger.i { "myNodeInfo committed successfully (nodeNum=${finalizedInfo.myNodeNum})" }
- connectionManager.get().onRadioConfigLoaded()
+ connectionManager.value.onRadioConfigLoaded()
}
scope.handledLaunch {
@@ -109,7 +105,7 @@ constructor(
sendHeartbeat()
delay(wantConfigDelay)
Logger.i { "Requesting NodeInfo (Stage 2)" }
- connectionManager.get().startNodeInfoOnly()
+ connectionManager.value.startNodeInfoOnly()
}
}
@@ -140,7 +136,7 @@ constructor(
nodeManager.setAllowNodeDbWrites(true)
serviceRepository.setConnectionState(ConnectionState.Connected)
serviceBroadcasts.broadcastConnection()
- connectionManager.get().onNodeDbReady()
+ connectionManager.value.onNodeDbReady()
}
}
@@ -172,7 +168,7 @@ constructor(
}
override fun triggerWantConfig() {
- connectionManager.get().startConfigOnly()
+ connectionManager.value.startConfigOnly()
}
private fun regenMyNodeInfo(metadata: DeviceMetadata? = null) {
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigHandlerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigHandlerImpl.kt
index d5ff32426..652e3bb79 100644
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigHandlerImpl.kt
+++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigHandlerImpl.kt
@@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
+import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.handledLaunch
import org.meshtastic.core.repository.MeshConfigHandler
import org.meshtastic.core.repository.NodeManager
@@ -33,13 +34,9 @@ import org.meshtastic.proto.Config
import org.meshtastic.proto.LocalConfig
import org.meshtastic.proto.LocalModuleConfig
import org.meshtastic.proto.ModuleConfig
-import javax.inject.Inject
-import javax.inject.Singleton
-@Singleton
-class MeshConfigHandlerImpl
-@Inject
-constructor(
+@Single
+class MeshConfigHandlerImpl(
private val radioConfigRepository: RadioConfigRepository,
private val serviceRepository: ServiceRepository,
private val nodeManager: NodeManager,
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt
index eda76a0df..5e706c288 100644
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt
+++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt
@@ -28,6 +28,7 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
+import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.handledLaunch
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.common.util.nowSeconds
@@ -63,17 +64,13 @@ import org.meshtastic.proto.AdminMessage
import org.meshtastic.proto.Config
import org.meshtastic.proto.Telemetry
import org.meshtastic.proto.ToRadio
-import javax.inject.Inject
-import javax.inject.Singleton
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
import kotlin.time.DurationUnit
@Suppress("LongParameterList", "TooManyFunctions")
-@Singleton
-class MeshConnectionManagerImpl
-@Inject
-constructor(
+@Single
+class MeshConnectionManagerImpl(
private val radioInterfaceService: RadioInterfaceService,
private val serviceRepository: ServiceRepository,
private val serviceBroadcasts: ServiceBroadcasts,
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt
index ca8e3d01e..df1790709 100644
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt
+++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt
@@ -18,7 +18,6 @@ package org.meshtastic.core.data.manager
import co.touchlab.kermit.Logger
import co.touchlab.kermit.Severity
-import dagger.Lazy
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -29,6 +28,7 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import okio.ByteString.Companion.toByteString
import okio.IOException
+import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.handledLaunch
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.common.util.nowSeconds
@@ -76,8 +76,6 @@ import org.meshtastic.proto.StoreForwardPlusPlus
import org.meshtastic.proto.Telemetry
import org.meshtastic.proto.User
import org.meshtastic.proto.Waypoint
-import javax.inject.Inject
-import javax.inject.Singleton
import kotlin.time.Duration.Companion.milliseconds
/**
@@ -91,10 +89,8 @@ import kotlin.time.Duration.Companion.milliseconds
* 5. Tracking received telemetry for node updates.
*/
@Suppress("LongParameterList", "TooManyFunctions", "LargeClass", "CyclomaticComplexMethod")
-@Singleton
-class MeshDataHandlerImpl
-@Inject
-constructor(
+@Single
+class MeshDataHandlerImpl(
private val nodeManager: NodeManager,
private val packetHandler: PacketHandler,
private val serviceRepository: ServiceRepository,
@@ -291,17 +287,15 @@ constructor(
"to=${sfpp.encapsulated_to} myNodeNum=${nodeManager.myNodeNum} status=$status"
}
scope.handledLaunch {
- packetRepository
- .get()
- .updateSFPPStatus(
- packetId = sfpp.encapsulated_id,
- from = sfpp.encapsulated_from,
- to = sfpp.encapsulated_to,
- hash = hash,
- status = status,
- rxTime = sfpp.encapsulated_rxtime.toLong() and 0xFFFFFFFFL,
- myNodeNum = nodeManager.myNodeNum ?: 0,
- )
+ packetRepository.value.updateSFPPStatus(
+ packetId = sfpp.encapsulated_id,
+ from = sfpp.encapsulated_from,
+ to = sfpp.encapsulated_to,
+ hash = hash,
+ status = status,
+ rxTime = sfpp.encapsulated_rxtime.toLong() and 0xFFFFFFFFL,
+ myNodeNum = nodeManager.myNodeNum ?: 0,
+ )
serviceBroadcasts.broadcastMessageStatus(sfpp.encapsulated_id, status)
}
}
@@ -309,13 +303,11 @@ constructor(
StoreForwardPlusPlus.SFPP_message_type.CANON_ANNOUNCE -> {
scope.handledLaunch {
sfpp.message_hash.let {
- packetRepository
- .get()
- .updateSFPPStatusByHash(
- hash = it.toByteArray(),
- status = MessageStatus.SFPP_CONFIRMED,
- rxTime = sfpp.encapsulated_rxtime.toLong() and 0xFFFFFFFFL,
- )
+ packetRepository.value.updateSFPPStatusByHash(
+ hash = it.toByteArray(),
+ status = MessageStatus.SFPP_CONFIRMED,
+ rxTime = sfpp.encapsulated_rxtime.toLong() and 0xFFFFFFFFL,
+ )
}
}
}
@@ -359,20 +351,20 @@ constructor(
val fromNum = packet.from
u.get_module_config_response?.let {
if (fromNum == myNodeNum) {
- configHandler.get().handleModuleConfig(it)
+ configHandler.value.handleModuleConfig(it)
} else {
it.statusmessage?.node_status?.let { nodeManager.updateNodeStatus(fromNum, it) }
}
}
if (fromNum == myNodeNum) {
- u.get_config_response?.let { configHandler.get().handleDeviceConfig(it) }
- u.get_channel_response?.let { configHandler.get().handleChannel(it) }
+ u.get_config_response?.let { configHandler.value.handleDeviceConfig(it) }
+ u.get_channel_response?.let { configHandler.value.handleChannel(it) }
}
u.get_device_metadata_response?.let {
if (fromNum == myNodeNum) {
- configFlowManager.get().handleLocalMetadata(it)
+ configFlowManager.value.handleLocalMetadata(it)
} else {
nodeManager.insertMetadata(fromNum, it)
}
@@ -414,7 +406,7 @@ constructor(
val fromNum = packet.from
val isRemote = (fromNum != myNodeNum)
if (!isRemote) {
- connectionManager.get().updateTelemetry(t)
+ connectionManager.value.updateTelemetry(t)
}
nodeManager.updateNode(fromNum) { node: Node ->
@@ -508,8 +500,8 @@ constructor(
private fun handleAckNak(requestId: Int, fromId: String, routingError: Int, relayNode: Int?) {
scope.handledLaunch {
val isAck = routingError == Routing.Error.NONE.value
- val p = packetRepository.get().getPacketByPacketId(requestId)
- val reaction = packetRepository.get().getReactionByPacketId(requestId)
+ val p = packetRepository.value.getPacketByPacketId(requestId)
+ val reaction = packetRepository.value.getReactionByPacketId(requestId)
@Suppress("MaxLineLength")
Logger.d {
@@ -527,7 +519,7 @@ constructor(
if (p != null && p.status != MessageStatus.RECEIVED) {
val updatedPacket =
p.copy(status = m, relays = if (isAck) p.relays + 1 else p.relays, relayNode = relayNode)
- packetRepository.get().update(updatedPacket)
+ packetRepository.value.update(updatedPacket)
}
reaction?.let { r ->
@@ -536,7 +528,7 @@ constructor(
if (isAck) {
updated = updated.copy(relays = updated.relays + 1)
}
- packetRepository.get().updateReaction(updated)
+ packetRepository.value.updateReaction(updated)
}
}
@@ -601,7 +593,7 @@ constructor(
val contactKey = "${dataPacket.channel}$contactId"
scope.handledLaunch {
- packetRepository.get().apply {
+ packetRepository.value.apply {
// Check for duplicates before inserting
val existingPackets = findPacketsWithId(dataPacket.id)
if (existingPackets.isNotEmpty()) {
@@ -646,7 +638,7 @@ constructor(
contactKey: String,
updateNotification: Boolean,
) {
- val conversationMuted = packetRepository.get().getContactSettings(contactKey).isMuted
+ val conversationMuted = packetRepository.value.getContactSettings(contactKey).isMuted
val nodeMuted = nodeManager.nodeDBbyID[dataPacket.from]?.isMuted == true
val isSilent = conversationMuted || nodeMuted
if (dataPacket.dataType == PortNum.ALERT_APP.value && !isSilent) {
@@ -733,7 +725,7 @@ constructor(
)
// Check for duplicates before inserting
- val existingReactions = packetRepository.get().findReactionsWithId(packet.id)
+ val existingReactions = packetRepository.value.findReactionsWithId(packet.id)
if (existingReactions.isNotEmpty()) {
Logger.d {
"Skipping duplicate reaction: packetId=${packet.id} replyId=${decoded.reply_id} " +
@@ -742,15 +734,15 @@ constructor(
return@handledLaunch
}
- packetRepository.get().insertReaction(reaction, nodeManager.myNodeNum ?: 0)
+ packetRepository.value.insertReaction(reaction, nodeManager.myNodeNum ?: 0)
// Find the original packet to get the contactKey
- packetRepository.get().getPacketByPacketId(decoded.reply_id)?.let { originalPacket ->
+ packetRepository.value.getPacketByPacketId(decoded.reply_id)?.let { originalPacket ->
// Skip notification if the original message was filtered
val targetId =
if (originalPacket.from == DataPacket.ID_LOCAL) originalPacket.to else originalPacket.from
val contactKey = "${originalPacket.channel}$targetId"
- val conversationMuted = packetRepository.get().getContactSettings(contactKey).isMuted
+ val conversationMuted = packetRepository.value.getContactSettings(contactKey).isMuted
val nodeMuted = nodeManager.nodeDBbyID[fromId]?.isMuted == true
val isSilent = conversationMuted || nodeMuted
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt
index 5ba3605c4..e2d150bc8 100644
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt
+++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt
@@ -17,7 +17,6 @@
package org.meshtastic.core.data.manager
import co.touchlab.kermit.Logger
-import dagger.Lazy
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -27,6 +26,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
+import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.handledLaunch
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.common.util.nowSeconds
@@ -43,16 +43,12 @@ import org.meshtastic.proto.FromRadio
import org.meshtastic.proto.LogRecord
import org.meshtastic.proto.MeshPacket
import org.meshtastic.proto.PortNum
-import javax.inject.Inject
-import javax.inject.Singleton
import kotlin.uuid.Uuid
/** Implementation of [MeshMessageProcessor] that handles raw radio messages and prepares mesh packets for routing. */
@Suppress("TooManyFunctions")
-@Singleton
-class MeshMessageProcessorImpl
-@Inject
-constructor(
+@Single
+class MeshMessageProcessorImpl(
private val nodeManager: NodeManager,
private val serviceRepository: ServiceRepository,
private val meshLogRepository: Lazy,
@@ -246,7 +242,7 @@ constructor(
}
try {
- router.get().dataHandler.handleReceivedData(packet, myNum, log.uuid, logJob)
+ router.value.dataHandler.handleReceivedData(packet, myNum, log.uuid, logJob)
} finally {
scope.launch {
mapsMutex.withLock {
@@ -258,5 +254,5 @@ constructor(
}
}
- private fun insertMeshLog(log: MeshLog): Job = scope.handledLaunch { meshLogRepository.get().insert(log) }
+ private fun insertMeshLog(log: MeshLog): Job = scope.handledLaunch { meshLogRepository.value.insert(log) }
}
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshRouterImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshRouterImpl.kt
index b079b1d86..d783ae773 100644
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshRouterImpl.kt
+++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshRouterImpl.kt
@@ -16,8 +16,8 @@
*/
package org.meshtastic.core.data.manager
-import dagger.Lazy
import kotlinx.coroutines.CoroutineScope
+import org.koin.core.annotation.Single
import org.meshtastic.core.repository.MeshActionHandler
import org.meshtastic.core.repository.MeshConfigFlowManager
import org.meshtastic.core.repository.MeshConfigHandler
@@ -26,15 +26,11 @@ import org.meshtastic.core.repository.MeshRouter
import org.meshtastic.core.repository.MqttManager
import org.meshtastic.core.repository.NeighborInfoHandler
import org.meshtastic.core.repository.TracerouteHandler
-import javax.inject.Inject
-import javax.inject.Singleton
/** Implementation of [MeshRouter] that orchestrates specialized mesh packet handlers. */
@Suppress("LongParameterList")
-@Singleton
-class MeshRouterImpl
-@Inject
-constructor(
+@Single
+class MeshRouterImpl(
private val dataHandlerLazy: Lazy,
private val configHandlerLazy: Lazy,
private val tracerouteHandlerLazy: Lazy,
@@ -44,25 +40,25 @@ constructor(
private val actionHandlerLazy: Lazy,
) : MeshRouter {
override val dataHandler: MeshDataHandler
- get() = dataHandlerLazy.get()
+ get() = dataHandlerLazy.value
override val configHandler: MeshConfigHandler
- get() = configHandlerLazy.get()
+ get() = configHandlerLazy.value
override val tracerouteHandler: TracerouteHandler
- get() = tracerouteHandlerLazy.get()
+ get() = tracerouteHandlerLazy.value
override val neighborInfoHandler: NeighborInfoHandler
- get() = neighborInfoHandlerLazy.get()
+ get() = neighborInfoHandlerLazy.value
override val configFlowManager: MeshConfigFlowManager
- get() = configFlowManagerLazy.get()
+ get() = configFlowManagerLazy.value
override val mqttManager: MqttManager
- get() = mqttManagerLazy.get()
+ get() = mqttManagerLazy.value
override val actionHandler: MeshActionHandler
- get() = actionHandlerLazy.get()
+ get() = actionHandlerLazy.value
override fun start(scope: CoroutineScope) {
dataHandler.start(scope)
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MessageFilterImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MessageFilterImpl.kt
index 17e7c5091..85693a2b4 100644
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MessageFilterImpl.kt
+++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MessageFilterImpl.kt
@@ -17,14 +17,13 @@
package org.meshtastic.core.data.manager
import co.touchlab.kermit.Logger
+import org.koin.core.annotation.Single
import org.meshtastic.core.repository.FilterPrefs
import org.meshtastic.core.repository.MessageFilter
-import javax.inject.Inject
-import javax.inject.Singleton
/** Implementation of [MessageFilter] that uses regex and plain text matching. */
-@Singleton
-class MessageFilterImpl @Inject constructor(private val filterPrefs: FilterPrefs) : MessageFilter {
+@Single
+class MessageFilterImpl(private val filterPrefs: FilterPrefs) : MessageFilter {
private var compiledPatterns: List = emptyList()
init {
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MqttManagerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MqttManagerImpl.kt
index 7684ebd20..d57fcc2b3 100644
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MqttManagerImpl.kt
+++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MqttManagerImpl.kt
@@ -25,19 +25,16 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
+import org.koin.core.annotation.Single
import org.meshtastic.core.network.repository.MQTTRepository
import org.meshtastic.core.repository.MqttManager
import org.meshtastic.core.repository.PacketHandler
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.proto.MqttClientProxyMessage
import org.meshtastic.proto.ToRadio
-import javax.inject.Inject
-import javax.inject.Singleton
-@Singleton
-class MqttManagerImpl
-@Inject
-constructor(
+@Single
+class MqttManagerImpl(
private val mqttRepository: MQTTRepository,
private val packetHandler: PacketHandler,
private val serviceRepository: ServiceRepository,
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/NeighborInfoHandlerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/NeighborInfoHandlerImpl.kt
index df19abacf..a9b63086a 100644
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/NeighborInfoHandlerImpl.kt
+++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/NeighborInfoHandlerImpl.kt
@@ -20,6 +20,7 @@ import co.touchlab.kermit.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
+import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.repository.CommandSender
import org.meshtastic.core.repository.NeighborInfoHandler
@@ -29,13 +30,9 @@ import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.proto.MeshPacket
import org.meshtastic.proto.NeighborInfo
import java.util.Locale
-import javax.inject.Inject
-import javax.inject.Singleton
-@Singleton
-class NeighborInfoHandlerImpl
-@Inject
-constructor(
+@Single
+class NeighborInfoHandlerImpl(
private val nodeManager: NodeManager,
private val serviceRepository: ServiceRepository,
private val commandSender: CommandSender,
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt
index 120d79b08..ad477c446 100644
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt
+++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt
@@ -26,6 +26,7 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import okio.ByteString
+import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.handledLaunch
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.DeviceMetrics
@@ -35,6 +36,7 @@ import org.meshtastic.core.model.MyNodeInfo
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.NodeInfo
import org.meshtastic.core.model.Position
+import org.meshtastic.core.model.util.NodeIdLookup
import org.meshtastic.core.repository.MeshServiceNotifications
import org.meshtastic.core.repository.NodeManager
import org.meshtastic.core.repository.NodeRepository
@@ -45,17 +47,13 @@ import org.meshtastic.proto.Paxcount
import org.meshtastic.proto.StatusMessage
import org.meshtastic.proto.Telemetry
import org.meshtastic.proto.User
-import javax.inject.Inject
-import javax.inject.Singleton
import org.meshtastic.proto.NodeInfo as ProtoNodeInfo
import org.meshtastic.proto.Position as ProtoPosition
/** Implementation of [NodeManager] that maintains an in-memory database of the mesh. */
@Suppress("LongParameterList", "TooManyFunctions", "CyclomaticComplexMethod")
-@Singleton
-class NodeManagerImpl
-@Inject
-constructor(
+@Single(binds = [NodeManager::class, NodeIdLookup::class])
+class NodeManagerImpl(
private val nodeRepository: NodeRepository,
private val serviceBroadcasts: ServiceBroadcasts,
private val serviceNotifications: MeshServiceNotifications,
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt
index 1e6d37f67..85716ce44 100644
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt
+++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt
@@ -17,7 +17,6 @@
package org.meshtastic.core.data.manager
import co.touchlab.kermit.Logger
-import dagger.Lazy
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -29,6 +28,7 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withTimeout
import kotlinx.coroutines.withTimeoutOrNull
+import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.handledLaunch
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.database.entity.MeshLog
@@ -48,17 +48,13 @@ import org.meshtastic.proto.FromRadio
import org.meshtastic.proto.MeshPacket
import org.meshtastic.proto.QueueStatus
import org.meshtastic.proto.ToRadio
-import javax.inject.Inject
-import javax.inject.Singleton
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
import kotlin.uuid.Uuid
@Suppress("TooManyFunctions")
-@Singleton
-class PacketHandlerImpl
-@Inject
-constructor(
+@Single
+class PacketHandlerImpl(
private val packetRepository: Lazy,
private val serviceBroadcasts: ServiceBroadcasts,
private val radioInterfaceService: RadioInterfaceService,
@@ -182,7 +178,7 @@ constructor(
if (packetId != 0) {
getDataPacketById(packetId)?.let { p ->
if (p.status == m) return@handledLaunch
- packetRepository.get().updateMessageStatus(p, m)
+ packetRepository.value.updateMessageStatus(p, m)
serviceBroadcasts.broadcastMessageStatus(packetId, m)
}
}
@@ -191,7 +187,7 @@ constructor(
private suspend fun getDataPacketById(packetId: Int): DataPacket? = withTimeoutOrNull(1.seconds) {
var dataPacket: DataPacket? = null
while (dataPacket == null) {
- dataPacket = packetRepository.get().getPacketById(packetId)
+ dataPacket = packetRepository.value.getPacketById(packetId)
if (dataPacket == null) delay(100.milliseconds)
}
dataPacket
@@ -222,7 +218,7 @@ constructor(
"insert: ${packetToSave.message_type} = " +
"${packetToSave.raw_message.toOneLineString()} from=${packetToSave.fromNum}"
}
- meshLogRepository.get().insert(packetToSave)
+ meshLogRepository.value.insert(packetToSave)
}
}
}
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/TracerouteHandlerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/TracerouteHandlerImpl.kt
index 2524e8301..a3d3c5491 100644
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/TracerouteHandlerImpl.kt
+++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/TracerouteHandlerImpl.kt
@@ -20,6 +20,7 @@ import co.touchlab.kermit.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
+import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.handledLaunch
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.data.repository.TracerouteSnapshotRepository
@@ -34,13 +35,9 @@ import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.repository.TracerouteHandler
import org.meshtastic.proto.MeshPacket
import java.util.Locale
-import javax.inject.Inject
-import javax.inject.Singleton
-@Singleton
-class TracerouteHandlerImpl
-@Inject
-constructor(
+@Single
+class TracerouteHandlerImpl(
private val nodeManager: NodeManager,
private val serviceRepository: ServiceRepository,
private val tracerouteSnapshotRepository: TracerouteSnapshotRepository,
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryImpl.kt
index d4901d02b..338a0d6ea 100644
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryImpl.kt
+++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryImpl.kt
@@ -18,6 +18,7 @@ package org.meshtastic.core.data.repository
import co.touchlab.kermit.Logger
import kotlinx.coroutines.withContext
+import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.data.datasource.BootloaderOtaQuirksJsonDataSource
import org.meshtastic.core.data.datasource.DeviceHardwareJsonDataSource
@@ -30,14 +31,10 @@ import org.meshtastic.core.model.DeviceHardware
import org.meshtastic.core.model.util.TimeConstants
import org.meshtastic.core.network.DeviceHardwareRemoteDataSource
import org.meshtastic.core.repository.DeviceHardwareRepository
-import javax.inject.Inject
-import javax.inject.Singleton
// Annotating with Singleton to ensure a single instance manages the cache
-@Singleton
-class DeviceHardwareRepositoryImpl
-@Inject
-constructor(
+@Single
+class DeviceHardwareRepositoryImpl(
private val remoteDataSource: DeviceHardwareRemoteDataSource,
private val localDataSource: DeviceHardwareLocalDataSource,
private val jsonDataSource: DeviceHardwareJsonDataSource,
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/FirmwareReleaseRepository.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/FirmwareReleaseRepository.kt
index 67ccdc091..d7b8340b3 100644
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/FirmwareReleaseRepository.kt
+++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/FirmwareReleaseRepository.kt
@@ -19,6 +19,7 @@ package org.meshtastic.core.data.repository
import co.touchlab.kermit.Logger
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
+import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.data.datasource.FirmwareReleaseJsonDataSource
import org.meshtastic.core.data.datasource.FirmwareReleaseLocalDataSource
@@ -28,13 +29,9 @@ import org.meshtastic.core.database.entity.FirmwareReleaseType
import org.meshtastic.core.database.entity.asExternalModel
import org.meshtastic.core.model.util.TimeConstants
import org.meshtastic.core.network.FirmwareReleaseRemoteDataSource
-import javax.inject.Inject
-import javax.inject.Singleton
-@Singleton
-class FirmwareReleaseRepository
-@Inject
-constructor(
+@Single
+class FirmwareReleaseRepository(
private val remoteDataSource: FirmwareReleaseRemoteDataSource,
private val localDataSource: FirmwareReleaseLocalDataSource,
private val jsonDataSource: FirmwareReleaseJsonDataSource,
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/MeshLogRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/MeshLogRepositoryImpl.kt
index 7c09f1582..b620984f6 100644
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/MeshLogRepositoryImpl.kt
+++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/MeshLogRepositoryImpl.kt
@@ -25,6 +25,7 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.withContext
+import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.data.datasource.NodeInfoReadDataSource
import org.meshtastic.core.database.DatabaseManager
@@ -37,8 +38,6 @@ import org.meshtastic.proto.MeshPacket
import org.meshtastic.proto.MyNodeInfo
import org.meshtastic.proto.PortNum
import org.meshtastic.proto.Telemetry
-import javax.inject.Inject
-import javax.inject.Singleton
/**
* Repository implementation for managing and retrieving logs from the local database.
@@ -47,10 +46,8 @@ import javax.inject.Singleton
* telemetry and traceroute data.
*/
@Suppress("TooManyFunctions")
-@Singleton
-class MeshLogRepositoryImpl
-@Inject
-constructor(
+@Single
+class MeshLogRepositoryImpl(
private val dbManager: DatabaseManager,
private val dispatchers: CoroutineDispatchers,
private val meshLogPrefs: MeshLogPrefs,
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt
index 0b08c806f..8c4a3c1f6 100644
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt
+++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt
@@ -34,6 +34,8 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import org.koin.core.annotation.Named
+import org.koin.core.annotation.Single
import org.meshtastic.core.data.datasource.NodeInfoReadDataSource
import org.meshtastic.core.data.datasource.NodeInfoWriteDataSource
import org.meshtastic.core.database.entity.MeshLog
@@ -42,7 +44,6 @@ import org.meshtastic.core.database.entity.MyNodeEntity
import org.meshtastic.core.database.entity.NodeEntity
import org.meshtastic.core.datastore.LocalStatsDataSource
import org.meshtastic.core.di.CoroutineDispatchers
-import org.meshtastic.core.di.ProcessLifecycle
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.MyNodeInfo
import org.meshtastic.core.model.Node
@@ -53,16 +54,12 @@ import org.meshtastic.proto.DeviceMetadata
import org.meshtastic.proto.HardwareModel
import org.meshtastic.proto.LocalStats
import org.meshtastic.proto.User
-import javax.inject.Inject
-import javax.inject.Singleton
/** Repository for managing node-related data, including hardware info, node database, and identity. */
-@Singleton
+@Single
@Suppress("TooManyFunctions")
-class NodeRepositoryImpl
-@Inject
-constructor(
- @ProcessLifecycle private val processLifecycle: Lifecycle,
+class NodeRepositoryImpl(
+ @Named("ProcessLifecycle") private val processLifecycle: Lifecycle,
private val nodeInfoReadDataSource: NodeInfoReadDataSource,
private val nodeInfoWriteDataSource: NodeInfoWriteDataSource,
private val dispatchers: CoroutineDispatchers,
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/PacketRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/PacketRepositoryImpl.kt
index 7164d6876..32ac3f3f2 100644
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/PacketRepositoryImpl.kt
+++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/PacketRepositoryImpl.kt
@@ -26,6 +26,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.withContext
import okio.ByteString.Companion.toByteString
+import org.koin.core.annotation.Single
import org.meshtastic.core.database.DatabaseManager
import org.meshtastic.core.database.entity.toReaction
import org.meshtastic.core.di.CoroutineDispatchers
@@ -37,19 +38,15 @@ import org.meshtastic.core.model.Node
import org.meshtastic.core.model.Reaction
import org.meshtastic.proto.ChannelSettings
import org.meshtastic.proto.PortNum
-import javax.inject.Inject
import org.meshtastic.core.database.entity.ContactSettings as ContactSettingsEntity
import org.meshtastic.core.database.entity.Packet as RoomPacket
import org.meshtastic.core.database.entity.ReactionEntity as RoomReaction
import org.meshtastic.core.repository.PacketRepository as SharedPacketRepository
@Suppress("TooManyFunctions", "LongParameterList")
-class PacketRepositoryImpl
-@Inject
-constructor(
- private val dbManager: DatabaseManager,
- private val dispatchers: CoroutineDispatchers,
-) : SharedPacketRepository {
+@Single
+class PacketRepositoryImpl(private val dbManager: DatabaseManager, private val dispatchers: CoroutineDispatchers) :
+ SharedPacketRepository {
override fun getWaypoints(): Flow> = dbManager.currentDb
.flatMapLatest { db -> db.packetDao().getAllWaypointsFlow() }
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/QuickChatActionRepository.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/QuickChatActionRepository.kt
index 025518f86..94f4afaea 100644
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/QuickChatActionRepository.kt
+++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/QuickChatActionRepository.kt
@@ -19,17 +19,13 @@ package org.meshtastic.core.data.repository
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.withContext
+import org.koin.core.annotation.Single
import org.meshtastic.core.database.DatabaseManager
import org.meshtastic.core.database.entity.QuickChatAction
import org.meshtastic.core.di.CoroutineDispatchers
-import javax.inject.Inject
-class QuickChatActionRepository
-@Inject
-constructor(
- private val dbManager: DatabaseManager,
- private val dispatchers: CoroutineDispatchers,
-) {
+@Single
+class QuickChatActionRepository(private val dbManager: DatabaseManager, private val dispatchers: CoroutineDispatchers) {
fun getAllActions() = dbManager.currentDb.flatMapLatest { it.quickChatActionDao().getAll() }.flowOn(dispatchers.io)
suspend fun upsert(action: QuickChatAction) =
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/RadioConfigRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/RadioConfigRepositoryImpl.kt
index d76ac8eee..b702d9cab 100644
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/RadioConfigRepositoryImpl.kt
+++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/RadioConfigRepositoryImpl.kt
@@ -18,6 +18,7 @@ package org.meshtastic.core.data.repository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
+import org.koin.core.annotation.Single
import org.meshtastic.core.datastore.ChannelSetDataSource
import org.meshtastic.core.datastore.LocalConfigDataSource
import org.meshtastic.core.datastore.ModuleConfigDataSource
@@ -32,15 +33,13 @@ import org.meshtastic.proto.DeviceProfile
import org.meshtastic.proto.LocalConfig
import org.meshtastic.proto.LocalModuleConfig
import org.meshtastic.proto.ModuleConfig
-import javax.inject.Inject
/**
* Class responsible for radio configuration data. Combines access to [nodeDB], [ChannelSet], [LocalConfig] &
* [LocalModuleConfig].
*/
-open class RadioConfigRepositoryImpl
-@Inject
-constructor(
+@Single
+open class RadioConfigRepositoryImpl(
private val nodeDB: NodeRepository,
private val channelSetDataSource: ChannelSetDataSource,
private val localConfigDataSource: LocalConfigDataSource,
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/TracerouteSnapshotRepository.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/TracerouteSnapshotRepository.kt
index e29572ac3..3b890c8f3 100644
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/TracerouteSnapshotRepository.kt
+++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/TracerouteSnapshotRepository.kt
@@ -23,15 +23,14 @@ import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.withContext
+import org.koin.core.annotation.Single
import org.meshtastic.core.database.DatabaseManager
import org.meshtastic.core.database.entity.TracerouteNodePositionEntity
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.proto.Position
-import javax.inject.Inject
-class TracerouteSnapshotRepository
-@Inject
-constructor(
+@Single
+class TracerouteSnapshotRepository(
private val dbManager: DatabaseManager,
private val dispatchers: CoroutineDispatchers,
) {
diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImplTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImplTest.kt
index e1b0c414f..25b609198 100644
--- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImplTest.kt
+++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImplTest.kt
@@ -46,7 +46,13 @@ class FromRadioPacketHandlerImplTest {
@Before
fun setup() {
handler =
- FromRadioPacketHandlerImpl(serviceRepository, { router }, mqttManager, packetHandler, serviceNotifications)
+ FromRadioPacketHandlerImpl(
+ serviceRepository,
+ lazy { router },
+ mqttManager,
+ packetHandler,
+ serviceNotifications,
+ )
}
@Test
diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerTest.kt
index b4eb95f9d..4ac471ec3 100644
--- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerTest.kt
+++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerTest.kt
@@ -16,7 +16,6 @@
*/
package org.meshtastic.core.data.manager
-import dagger.Lazy
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
@@ -58,19 +57,19 @@ class MeshDataHandlerTest {
private val packetHandler: PacketHandler = mockk(relaxed = true)
private val serviceRepository: ServiceRepository = mockk(relaxed = true)
private val packetRepository: PacketRepository = mockk(relaxed = true)
- private val packetRepositoryLazy: Lazy = mockk { every { get() } returns packetRepository }
+ private val packetRepositoryLazy: Lazy = lazy { packetRepository }
private val serviceBroadcasts: ServiceBroadcasts = mockk(relaxed = true)
private val serviceNotifications: MeshServiceNotifications = mockk(relaxed = true)
private val analytics: PlatformAnalytics = mockk(relaxed = true)
private val dataMapper: MeshDataMapper = mockk(relaxed = true)
private val configHandler: MeshConfigHandler = mockk(relaxed = true)
- private val configHandlerLazy: Lazy = mockk { every { get() } returns configHandler }
+ private val configHandlerLazy: Lazy = lazy { configHandler }
private val configFlowManager: MeshConfigFlowManager = mockk(relaxed = true)
- private val configFlowManagerLazy: Lazy = mockk { every { get() } returns configFlowManager }
+ private val configFlowManagerLazy: Lazy = lazy { configFlowManager }
private val commandSender: CommandSender = mockk(relaxed = true)
private val historyManager: HistoryManager = mockk(relaxed = true)
private val connectionManager: MeshConnectionManager = mockk(relaxed = true)
- private val connectionManagerLazy: Lazy = mockk { every { get() } returns connectionManager }
+ private val connectionManagerLazy: Lazy = lazy { connectionManager }
private val tracerouteHandler: TracerouteHandler = mockk(relaxed = true)
private val neighborInfoHandler: NeighborInfoHandler = mockk(relaxed = true)
private val radioConfigRepository: RadioConfigRepository = mockk(relaxed = true)
diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/PacketHandlerImplTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/PacketHandlerImplTest.kt
index 2486922ac..619184abf 100644
--- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/PacketHandlerImplTest.kt
+++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/PacketHandlerImplTest.kt
@@ -60,10 +60,10 @@ class PacketHandlerImplTest {
handler =
PacketHandlerImpl(
- { packetRepository },
+ lazy { packetRepository },
serviceBroadcasts,
radioInterfaceService,
- { meshLogRepository },
+ lazy { meshLogRepository },
serviceRepository,
)
handler.start(testScope)
diff --git a/core/database/build.gradle.kts b/core/database/build.gradle.kts
index 026a9b410..30df0a046 100644
--- a/core/database/build.gradle.kts
+++ b/core/database/build.gradle.kts
@@ -20,6 +20,7 @@ plugins {
alias(libs.plugins.meshtastic.android.room)
alias(libs.plugins.meshtastic.kotlinx.serialization)
alias(libs.plugins.kotlin.parcelize)
+ id("meshtastic.koin")
}
kotlin {
diff --git a/core/database/src/androidMain/kotlin/org/meshtastic/core/database/DatabaseManager.kt b/core/database/src/androidMain/kotlin/org/meshtastic/core/database/DatabaseManager.kt
index e5c96cd41..21e1f3f88 100644
--- a/core/database/src/androidMain/kotlin/org/meshtastic/core/database/DatabaseManager.kt
+++ b/core/database/src/androidMain/kotlin/org/meshtastic/core/database/DatabaseManager.kt
@@ -34,24 +34,19 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
+import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.database.MeshtasticDatabase.Companion.configureCommon
import org.meshtastic.core.di.CoroutineDispatchers
import java.io.File
-import javax.inject.Inject
-import javax.inject.Singleton
import org.meshtastic.core.common.database.DatabaseManager as SharedDatabaseManager
/** Manages per-device Room database instances for node data, with LRU eviction. */
-@Singleton
+@Single
@Suppress("TooManyFunctions")
@OptIn(ExperimentalCoroutinesApi::class)
-open class DatabaseManager
-@Inject
-constructor(
- private val app: Application,
- private val dispatchers: CoroutineDispatchers,
-) : SharedDatabaseManager {
+open class DatabaseManager(private val app: Application, private val dispatchers: CoroutineDispatchers) :
+ SharedDatabaseManager {
val prefs: SharedPreferences = app.getSharedPreferences("db-manager-prefs", Context.MODE_PRIVATE)
private val managerScope = CoroutineScope(SupervisorJob() + dispatchers.default)
diff --git a/core/database/src/androidMain/kotlin/org/meshtastic/core/database/di/CoreDatabaseAndroidModule.kt b/core/database/src/androidMain/kotlin/org/meshtastic/core/database/di/CoreDatabaseAndroidModule.kt
new file mode 100644
index 000000000..26b56484c
--- /dev/null
+++ b/core/database/src/androidMain/kotlin/org/meshtastic/core/database/di/CoreDatabaseAndroidModule.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 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 org.meshtastic.core.database.di
+
+import org.koin.core.annotation.ComponentScan
+import org.koin.core.annotation.Module
+
+@Module
+@ComponentScan("org.meshtastic.core.database")
+class CoreDatabaseAndroidModule
diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/di/CoreDatabaseModule.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/di/CoreDatabaseModule.kt
new file mode 100644
index 000000000..5626c6269
--- /dev/null
+++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/di/CoreDatabaseModule.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 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 org.meshtastic.core.database.di
+
+import org.koin.core.annotation.ComponentScan
+import org.koin.core.annotation.Module
+
+@Module
+@ComponentScan("org.meshtastic.core.database")
+class CoreDatabaseModule
diff --git a/core/datastore/build.gradle.kts b/core/datastore/build.gradle.kts
index f94dc4779..c5a3286cd 100644
--- a/core/datastore/build.gradle.kts
+++ b/core/datastore/build.gradle.kts
@@ -18,7 +18,7 @@ plugins {
alias(libs.plugins.meshtastic.kmp.library)
alias(libs.plugins.meshtastic.kotlinx.serialization)
alias(libs.plugins.kotlin.parcelize)
- alias(libs.plugins.devtools.ksp)
+ id("meshtastic.koin")
}
kotlin {
@@ -29,12 +29,8 @@ kotlin {
implementation(projects.core.proto)
api(libs.androidx.datastore)
api(libs.androidx.datastore.preferences)
- api(libs.javax.inject)
implementation(libs.kotlinx.serialization.json)
implementation(libs.kermit)
}
- androidMain.dependencies { implementation(libs.hilt.android) }
}
}
-
-dependencies { "kspAndroid"(libs.hilt.compiler) }
diff --git a/app/src/main/kotlin/org/meshtastic/app/di/DataStoreModule.kt b/core/datastore/src/androidMain/kotlin/org/meshtastic/core/datastore/di/CoreDatastoreAndroidModule.kt
similarity index 69%
rename from app/src/main/kotlin/org/meshtastic/app/di/DataStoreModule.kt
rename to core/datastore/src/androidMain/kotlin/org/meshtastic/core/datastore/di/CoreDatastoreAndroidModule.kt
index 55611e300..61a991207 100644
--- a/app/src/main/kotlin/org/meshtastic/app/di/DataStoreModule.kt
+++ b/core/datastore/src/androidMain/kotlin/org/meshtastic/core/datastore/di/CoreDatastoreAndroidModule.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025-2026 Meshtastic LLC
+ * Copyright (c) 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
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package org.meshtastic.app.di
+package org.meshtastic.core.datastore.di
import android.content.Context
import androidx.datastore.core.DataStore
@@ -27,16 +27,12 @@ import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.preferencesDataStoreFile
-import dagger.Module
-import dagger.Provides
-import dagger.hilt.InstallIn
-import dagger.hilt.android.qualifiers.ApplicationContext
-import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.SupervisorJob
import okio.FileSystem
import okio.Path.Companion.toOkioPath
+import org.koin.core.annotation.Module
+import org.koin.core.annotation.Named
+import org.koin.core.annotation.Single
import org.meshtastic.core.datastore.KEY_APP_INTRO_COMPLETED
import org.meshtastic.core.datastore.KEY_INCLUDE_UNKNOWN
import org.meshtastic.core.datastore.KEY_NODE_SORT
@@ -52,36 +48,23 @@ import org.meshtastic.proto.ChannelSet
import org.meshtastic.proto.LocalConfig
import org.meshtastic.proto.LocalModuleConfig
import org.meshtastic.proto.LocalStats
-import javax.inject.Qualifier
-import javax.inject.Singleton
private const val USER_PREFERENCES_NAME = "user_preferences"
-@Retention(AnnotationRetention.BINARY)
-@Qualifier
-annotation class DataStoreScope
-
-@InstallIn(SingletonComponent::class)
@Module
-object DataStoreModule {
-
- @Provides
- @Singleton
- @DataStoreScope
- fun provideDataStoreScope(): CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
-
- @Singleton
- @Provides
+class PreferencesDataStoreModule {
+ @Single
+ @Named("CorePreferencesDataStore")
fun providePreferencesDataStore(
- @ApplicationContext appContext: Context,
- @DataStoreScope scope: CoroutineScope,
+ context: Context,
+ @Named("DataStoreScope") scope: CoroutineScope,
): DataStore = PreferenceDataStoreFactory.create(
corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { emptyPreferences() }),
migrations =
listOf(
- SharedPreferencesMigration(context = appContext, sharedPreferencesName = USER_PREFERENCES_NAME),
+ SharedPreferencesMigration(context = context, sharedPreferencesName = USER_PREFERENCES_NAME),
SharedPreferencesMigration(
- context = appContext,
+ context = context,
sharedPreferencesName = "ui-prefs",
keysToMigrate =
setOf(
@@ -96,70 +79,94 @@ object DataStoreModule {
),
),
scope = scope,
- produceFile = { appContext.preferencesDataStoreFile(USER_PREFERENCES_NAME) },
+ produceFile = { context.preferencesDataStoreFile(USER_PREFERENCES_NAME) },
)
+}
- @Singleton
- @Provides
+@Module
+class LocalConfigDataStoreModule {
+ @Single
+ @Named("CoreLocalConfigDataStore")
fun provideLocalConfigDataStore(
- @ApplicationContext appContext: Context,
- @DataStoreScope scope: CoroutineScope,
+ context: Context,
+ @Named("DataStoreScope") scope: CoroutineScope,
): DataStore = DataStoreFactory.create(
storage =
OkioStorage(
fileSystem = FileSystem.SYSTEM,
serializer = LocalConfigSerializer,
- producePath = { appContext.dataStoreFile("local_config.pb").toOkioPath() },
+ producePath = { context.dataStoreFile("local_config.pb").toOkioPath() },
),
corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { LocalConfig() }),
scope = scope,
)
+}
- @Singleton
- @Provides
+@Module
+class ModuleConfigDataStoreModule {
+ @Single
+ @Named("CoreModuleConfigDataStore")
fun provideModuleConfigDataStore(
- @ApplicationContext appContext: Context,
- @DataStoreScope scope: CoroutineScope,
+ context: Context,
+ @Named("DataStoreScope") scope: CoroutineScope,
): DataStore = DataStoreFactory.create(
storage =
OkioStorage(
fileSystem = FileSystem.SYSTEM,
serializer = ModuleConfigSerializer,
- producePath = { appContext.dataStoreFile("module_config.pb").toOkioPath() },
+ producePath = { context.dataStoreFile("module_config.pb").toOkioPath() },
),
corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { LocalModuleConfig() }),
scope = scope,
)
+}
- @Singleton
- @Provides
+@Module
+class ChannelSetDataStoreModule {
+ @Single
+ @Named("CoreChannelSetDataStore")
fun provideChannelSetDataStore(
- @ApplicationContext appContext: Context,
- @DataStoreScope scope: CoroutineScope,
+ context: Context,
+ @Named("DataStoreScope") scope: CoroutineScope,
): DataStore = DataStoreFactory.create(
storage =
OkioStorage(
fileSystem = FileSystem.SYSTEM,
serializer = ChannelSetSerializer,
- producePath = { appContext.dataStoreFile("channel_set.pb").toOkioPath() },
+ producePath = { context.dataStoreFile("channel_set.pb").toOkioPath() },
),
corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { ChannelSet() }),
scope = scope,
)
+}
- @Singleton
- @Provides
+@Module
+class LocalStatsDataStoreModule {
+ @Single
+ @Named("CoreLocalStatsDataStore")
fun provideLocalStatsDataStore(
- @ApplicationContext appContext: Context,
- @DataStoreScope scope: CoroutineScope,
+ context: Context,
+ @Named("DataStoreScope") scope: CoroutineScope,
): DataStore = DataStoreFactory.create(
storage =
OkioStorage(
fileSystem = FileSystem.SYSTEM,
serializer = LocalStatsSerializer,
- producePath = { appContext.dataStoreFile("local_stats.pb").toOkioPath() },
+ producePath = { context.dataStoreFile("local_stats.pb").toOkioPath() },
),
corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { LocalStats() }),
scope = scope,
)
}
+
+@Module(
+ includes =
+ [
+ PreferencesDataStoreModule::class,
+ LocalConfigDataStoreModule::class,
+ ModuleConfigDataStoreModule::class,
+ ChannelSetDataStoreModule::class,
+ LocalStatsDataStoreModule::class,
+ ],
+)
+class CoreDatastoreAndroidModule
diff --git a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/BootloaderWarningDataSource.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/BootloaderWarningDataSource.kt
index 5eda0ca4c..c8d5a5315 100644
--- a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/BootloaderWarningDataSource.kt
+++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/BootloaderWarningDataSource.kt
@@ -25,11 +25,11 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
-import javax.inject.Inject
-import javax.inject.Singleton
+import org.koin.core.annotation.Named
+import org.koin.core.annotation.Single
-@Singleton
-class BootloaderWarningDataSource @Inject constructor(private val dataStore: DataStore) {
+@Single
+class BootloaderWarningDataSource(@Named("CorePreferencesDataStore") private val dataStore: DataStore) {
private object PreferencesKeys {
val DISMISSED_BOOTLOADER_ADDRESSES = stringPreferencesKey("dismissed-bootloader-addresses")
diff --git a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/ChannelSetDataSource.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/ChannelSetDataSource.kt
index 9e7cfbcd0..0f3b648b6 100644
--- a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/ChannelSetDataSource.kt
+++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/ChannelSetDataSource.kt
@@ -21,16 +21,16 @@ import co.touchlab.kermit.Logger
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import okio.IOException
+import org.koin.core.annotation.Named
+import org.koin.core.annotation.Single
import org.meshtastic.proto.Channel
import org.meshtastic.proto.ChannelSet
import org.meshtastic.proto.ChannelSettings
import org.meshtastic.proto.Config
-import javax.inject.Inject
-import javax.inject.Singleton
/** Class that handles saving and retrieving [ChannelSet] data. */
-@Singleton
-class ChannelSetDataSource @Inject constructor(private val channelSetStore: DataStore) {
+@Single
+class ChannelSetDataSource(@Named("CoreChannelSetDataStore") private val channelSetStore: DataStore) {
val channelSetFlow: Flow =
channelSetStore.data.catch { exception ->
// dataStore.data throws an IOException when an error is encountered when reading data
diff --git a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/LocalConfigDataSource.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/LocalConfigDataSource.kt
index f347c710b..b1fe828c5 100644
--- a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/LocalConfigDataSource.kt
+++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/LocalConfigDataSource.kt
@@ -21,14 +21,14 @@ import co.touchlab.kermit.Logger
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import okio.IOException
+import org.koin.core.annotation.Named
+import org.koin.core.annotation.Single
import org.meshtastic.proto.Config
import org.meshtastic.proto.LocalConfig
-import javax.inject.Inject
-import javax.inject.Singleton
/** Class that handles saving and retrieving [LocalConfig] data. */
-@Singleton
-class LocalConfigDataSource @Inject constructor(private val localConfigStore: DataStore) {
+@Single
+class LocalConfigDataSource(@Named("CoreLocalConfigDataStore") private val localConfigStore: DataStore) {
val localConfigFlow: Flow =
localConfigStore.data.catch { exception ->
// dataStore.data throws an IOException when an error is encountered when reading data
diff --git a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/LocalStatsDataSource.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/LocalStatsDataSource.kt
index 22ee35390..abf9ad5d3 100644
--- a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/LocalStatsDataSource.kt
+++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/LocalStatsDataSource.kt
@@ -21,13 +21,13 @@ import co.touchlab.kermit.Logger
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import okio.IOException
+import org.koin.core.annotation.Named
+import org.koin.core.annotation.Single
import org.meshtastic.proto.LocalStats
-import javax.inject.Inject
-import javax.inject.Singleton
/** Class that handles saving and retrieving [LocalStats] data. */
-@Singleton
-class LocalStatsDataSource @Inject constructor(private val localStatsStore: DataStore) {
+@Single
+class LocalStatsDataSource(@Named("CoreLocalStatsDataStore") private val localStatsStore: DataStore) {
val localStatsFlow: Flow =
localStatsStore.data.catch { exception ->
if (exception is IOException) {
diff --git a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/ModuleConfigDataSource.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/ModuleConfigDataSource.kt
index c4195d58a..54db1ad0b 100644
--- a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/ModuleConfigDataSource.kt
+++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/ModuleConfigDataSource.kt
@@ -21,14 +21,16 @@ import co.touchlab.kermit.Logger
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import okio.IOException
+import org.koin.core.annotation.Named
+import org.koin.core.annotation.Single
import org.meshtastic.proto.LocalModuleConfig
import org.meshtastic.proto.ModuleConfig
-import javax.inject.Inject
-import javax.inject.Singleton
/** Class that handles saving and retrieving [LocalModuleConfig] data. */
-@Singleton
-class ModuleConfigDataSource @Inject constructor(private val moduleConfigStore: DataStore) {
+@Single
+class ModuleConfigDataSource(
+ @Named("CoreModuleConfigDataStore") private val moduleConfigStore: DataStore,
+) {
val moduleConfigFlow: Flow =
moduleConfigStore.data.catch { exception ->
// dataStore.data throws an IOException when an error is encountered when reading data
diff --git a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/RecentAddressesDataSource.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/RecentAddressesDataSource.kt
index 0d3c4c123..82ccf1781 100644
--- a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/RecentAddressesDataSource.kt
+++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/RecentAddressesDataSource.kt
@@ -28,12 +28,12 @@ import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import org.json.JSONArray
import org.json.JSONObject
+import org.koin.core.annotation.Named
+import org.koin.core.annotation.Single
import org.meshtastic.core.datastore.model.RecentAddress
-import javax.inject.Inject
-import javax.inject.Singleton
-@Singleton
-class RecentAddressesDataSource @Inject constructor(private val dataStore: DataStore) {
+@Single
+class RecentAddressesDataSource(@Named("CorePreferencesDataStore") private val dataStore: DataStore) {
private object PreferencesKeys {
val RECENT_IP_ADDRESSES = stringPreferencesKey("recent-ip-addresses")
}
diff --git a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/UiPreferencesDataSource.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/UiPreferencesDataSource.kt
index 02634293e..f931e9078 100644
--- a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/UiPreferencesDataSource.kt
+++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/UiPreferencesDataSource.kt
@@ -29,8 +29,8 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
-import javax.inject.Inject
-import javax.inject.Singleton
+import org.koin.core.annotation.Named
+import org.koin.core.annotation.Single
const val KEY_APP_INTRO_COMPLETED = "app_intro_completed"
const val KEY_THEME = "theme"
@@ -43,8 +43,8 @@ const val KEY_ONLY_ONLINE = "only-online"
const val KEY_ONLY_DIRECT = "only-direct"
const val KEY_SHOW_IGNORED = "show-ignored"
-@Singleton
-class UiPreferencesDataSource @Inject constructor(private val dataStore: DataStore) {
+@Single
+class UiPreferencesDataSource(@Named("CorePreferencesDataStore") private val dataStore: DataStore) {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
diff --git a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/di/CoreDatastoreModule.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/di/CoreDatastoreModule.kt
new file mode 100644
index 000000000..9ef808bc3
--- /dev/null
+++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/di/CoreDatastoreModule.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 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 org.meshtastic.core.datastore.di
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import org.koin.core.annotation.ComponentScan
+import org.koin.core.annotation.Module
+import org.koin.core.annotation.Named
+import org.koin.core.annotation.Single
+
+@Module
+@ComponentScan("org.meshtastic.core.datastore")
+class CoreDatastoreModule {
+ @Single
+ @Named("DataStoreScope")
+ fun provideDataStoreScope(): CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
+}
diff --git a/core/di/README.md b/core/di/README.md
index d83fd8c50..7cd07a8a2 100644
--- a/core/di/README.md
+++ b/core/di/README.md
@@ -1,7 +1,7 @@
# `:core:di`
## Overview
-The `:core:di` module defines the core Dagger Hilt modules and provides standard dependencies that are shared across all other modules.
+The `:core:di` module defines the core Koin modules and provides standard dependencies that are shared across all other modules.
## Key Components
@@ -12,7 +12,7 @@ Defines bindings for application-wide singletons like `Application`, `Context`,
Provides a wrapper for standard Kotlin `CoroutineDispatchers` (`IO`, `Default`, `Main`), allowing for easy mocking in unit tests.
### 3. `ProcessLifecycle.kt`
-Exposes the application's global process lifecycle as a Hilt binding, enabling components to react to the app entering the foreground or background.
+Exposes the application's global process lifecycle as a Koin binding, enabling components to react to the app entering the foreground or background.
## Module dependency graph
diff --git a/core/di/build.gradle.kts b/core/di/build.gradle.kts
index 59f82dbeb..9cadd064d 100644
--- a/core/di/build.gradle.kts
+++ b/core/di/build.gradle.kts
@@ -15,7 +15,10 @@
* along with this program. If not, see .
*/
-plugins { alias(libs.plugins.meshtastic.kmp.library) }
+plugins {
+ alias(libs.plugins.meshtastic.kmp.library)
+ id("meshtastic.koin")
+}
kotlin {
@Suppress("UnstableApiUsage")
diff --git a/app/src/main/kotlin/org/meshtastic/app/di/AppModule.kt b/core/di/src/commonMain/kotlin/org/meshtastic/core/di/di/CoreDiModule.kt
similarity index 62%
rename from app/src/main/kotlin/org/meshtastic/app/di/AppModule.kt
rename to core/di/src/commonMain/kotlin/org/meshtastic/core/di/di/CoreDiModule.kt
index ec1efc74d..9ad24502a 100644
--- a/app/src/main/kotlin/org/meshtastic/app/di/AppModule.kt
+++ b/core/di/src/commonMain/kotlin/org/meshtastic/core/di/di/CoreDiModule.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025-2026 Meshtastic LLC
+ * Copyright (c) 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
@@ -14,28 +14,16 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package org.meshtastic.app.di
+package org.meshtastic.core.di.di
-import android.content.Context
-import androidx.work.WorkManager
-import dagger.Module
-import dagger.Provides
-import dagger.hilt.InstallIn
-import dagger.hilt.android.qualifiers.ApplicationContext
-import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.Dispatchers
+import org.koin.core.annotation.Module
+import org.koin.core.annotation.Single
import org.meshtastic.core.di.CoroutineDispatchers
-import javax.inject.Singleton
@Module
-@InstallIn(SingletonComponent::class)
-object AppModule {
-
- @Provides
+class CoreDiModule {
+ @Single
fun provideCoroutineDispatchers(): CoroutineDispatchers =
CoroutineDispatchers(io = Dispatchers.IO, main = Dispatchers.Main, default = Dispatchers.Default)
-
- @Provides
- @Singleton
- fun provideWorkManager(@ApplicationContext context: Context): WorkManager = WorkManager.getInstance(context)
}
diff --git a/core/domain/build.gradle.kts b/core/domain/build.gradle.kts
index 64c8fd8f5..69a0b2af8 100644
--- a/core/domain/build.gradle.kts
+++ b/core/domain/build.gradle.kts
@@ -18,6 +18,7 @@
plugins {
alias(libs.plugins.meshtastic.kmp.library)
alias(libs.plugins.devtools.ksp)
+ alias(libs.plugins.meshtastic.koin)
}
kotlin {
@@ -53,5 +54,3 @@ kotlin {
}
}
}
-
-dependencies { add("kspAndroid", libs.hilt.compiler) }
diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/di/CoreDomainModule.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/di/CoreDomainModule.kt
new file mode 100644
index 000000000..80cfb26ab
--- /dev/null
+++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/di/CoreDomainModule.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 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 org.meshtastic.core.domain.di
+
+import org.koin.core.annotation.ComponentScan
+import org.koin.core.annotation.Module
+
+@Module
+@ComponentScan("org.meshtastic.core.domain")
+class CoreDomainModule
diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/AdminActionsUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/AdminActionsUseCase.kt
index b0b7c2c8c..095fbc39c 100644
--- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/AdminActionsUseCase.kt
+++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/AdminActionsUseCase.kt
@@ -16,9 +16,9 @@
*/
package org.meshtastic.core.domain.usecase.settings
+import org.koin.core.annotation.Single
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.repository.NodeRepository
-import javax.inject.Inject
/**
* Use case for performing administrative and destructive actions on mesh nodes.
@@ -26,8 +26,8 @@ import javax.inject.Inject
* This component provides methods for rebooting, shutting down, or resetting nodes within the mesh. It also handles
* local database synchronization when these actions are performed on the locally connected device.
*/
+@Single
open class AdminActionsUseCase
-@Inject
constructor(
private val radioController: RadioController,
private val nodeRepository: NodeRepository,
diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/CleanNodeDatabaseUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/CleanNodeDatabaseUseCase.kt
index 655323caf..491497ba7 100644
--- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/CleanNodeDatabaseUseCase.kt
+++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/CleanNodeDatabaseUseCase.kt
@@ -16,15 +16,15 @@
*/
package org.meshtastic.core.domain.usecase.settings
+import org.koin.core.annotation.Single
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.repository.NodeRepository
-import javax.inject.Inject
import kotlin.time.Duration.Companion.days
/** Use case for cleaning up nodes from the database. */
+@Single
open class CleanNodeDatabaseUseCase
-@Inject
constructor(
private val nodeRepository: NodeRepository,
private val radioController: RadioController,
diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCase.kt
index 6897f4c9f..4b8863801 100644
--- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCase.kt
+++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCase.kt
@@ -21,18 +21,18 @@ import kotlinx.datetime.Instant
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import okio.BufferedSink
+import org.koin.core.annotation.Single
import org.meshtastic.core.model.Position
import org.meshtastic.core.model.util.positionToMeter
import org.meshtastic.core.repository.MeshLogRepository
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.proto.PortNum
-import javax.inject.Inject
import kotlin.math.roundToInt
import org.meshtastic.proto.Position as ProtoPosition
/** Use case for exporting persisted packet data to a CSV format. */
+@Single
open class ExportDataUseCase
-@Inject
constructor(
private val nodeRepository: NodeRepository,
private val meshLogRepository: MeshLogRepository,
diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ExportProfileUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ExportProfileUseCase.kt
index e9e8995bb..a52c73fc1 100644
--- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ExportProfileUseCase.kt
+++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ExportProfileUseCase.kt
@@ -17,11 +17,12 @@
package org.meshtastic.core.domain.usecase.settings
import okio.BufferedSink
+import org.koin.core.annotation.Single
import org.meshtastic.proto.DeviceProfile
-import javax.inject.Inject
/** Use case for exporting a device profile to an output stream. */
-open class ExportProfileUseCase @Inject constructor() {
+@Single
+open class ExportProfileUseCase {
/**
* Exports the provided [DeviceProfile] to the given [BufferedSink].
*
diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ExportSecurityConfigUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ExportSecurityConfigUseCase.kt
index 55cc5032f..309da69d2 100644
--- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ExportSecurityConfigUseCase.kt
+++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ExportSecurityConfigUseCase.kt
@@ -19,12 +19,13 @@ package org.meshtastic.core.domain.usecase.settings
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import okio.BufferedSink
+import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.proto.Config
-import javax.inject.Inject
/** Use case for exporting security configuration to a JSON format. */
-open class ExportSecurityConfigUseCase @Inject constructor() {
+@Single
+open class ExportSecurityConfigUseCase {
/**
* Exports the provided [Config.SecurityConfig] as a JSON string to the given [BufferedSink].
*
diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ImportProfileUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ImportProfileUseCase.kt
index c003b82ef..841421349 100644
--- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ImportProfileUseCase.kt
+++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ImportProfileUseCase.kt
@@ -17,11 +17,12 @@
package org.meshtastic.core.domain.usecase.settings
import okio.BufferedSource
+import org.koin.core.annotation.Single
import org.meshtastic.proto.DeviceProfile
-import javax.inject.Inject
/** Use case for importing a device profile from an input stream. */
-open class ImportProfileUseCase @Inject constructor() {
+@Single
+open class ImportProfileUseCase {
/**
* Imports a [DeviceProfile] from the provided [BufferedSource].
*
diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCase.kt
index 88e8319a5..db4ffe82e 100644
--- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCase.kt
+++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCase.kt
@@ -16,6 +16,7 @@
*/
package org.meshtastic.core.domain.usecase.settings
+import org.koin.core.annotation.Single
import org.meshtastic.core.model.Position
import org.meshtastic.core.model.RadioController
import org.meshtastic.proto.Config
@@ -24,10 +25,10 @@ import org.meshtastic.proto.LocalConfig
import org.meshtastic.proto.LocalModuleConfig
import org.meshtastic.proto.ModuleConfig
import org.meshtastic.proto.User
-import javax.inject.Inject
/** Use case for installing a device profile onto a radio. */
-open class InstallProfileUseCase @Inject constructor(private val radioController: RadioController) {
+@Single
+open class InstallProfileUseCase constructor(private val radioController: RadioController) {
/**
* Installs the provided [DeviceProfile] onto the radio at [destNum].
*
diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCase.kt
index 1707a7500..aa410028f 100644
--- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCase.kt
+++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCase.kt
@@ -20,6 +20,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
+import org.koin.core.annotation.Single
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.RadioController
@@ -29,11 +30,10 @@ import org.meshtastic.core.repository.RadioPrefs
import org.meshtastic.core.repository.isBle
import org.meshtastic.core.repository.isSerial
import org.meshtastic.core.repository.isTcp
-import javax.inject.Inject
/** Use case to determine if the currently connected device is capable of over-the-air (OTA) updates. */
+@Single
open class IsOtaCapableUseCase
-@Inject
constructor(
private val nodeRepository: NodeRepository,
private val radioController: RadioController,
diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/MeshLocationUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/MeshLocationUseCase.kt
index 6f578bc05..ec7f1defe 100644
--- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/MeshLocationUseCase.kt
+++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/MeshLocationUseCase.kt
@@ -16,11 +16,12 @@
*/
package org.meshtastic.core.domain.usecase.settings
+import org.koin.core.annotation.Single
import org.meshtastic.core.model.RadioController
-import javax.inject.Inject
/** Use case for controlling location sharing with the mesh. */
-open class MeshLocationUseCase @Inject constructor(private val radioController: RadioController) {
+@Single
+open class MeshLocationUseCase constructor(private val radioController: RadioController) {
/** Starts providing the phone's location to the mesh. */
fun startProvidingLocation() {
radioController.startProvideLocation()
diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ProcessRadioResponseUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ProcessRadioResponseUseCase.kt
index 3e1639469..bfb36de58 100644
--- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ProcessRadioResponseUseCase.kt
+++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ProcessRadioResponseUseCase.kt
@@ -17,6 +17,7 @@
package org.meshtastic.core.domain.usecase.settings
import co.touchlab.kermit.Logger
+import org.koin.core.annotation.Single
import org.meshtastic.core.model.getStringResFrom
import org.meshtastic.core.resources.UiText
import org.meshtastic.proto.AdminMessage
@@ -28,7 +29,6 @@ import org.meshtastic.proto.MeshPacket
import org.meshtastic.proto.PortNum
import org.meshtastic.proto.Routing
import org.meshtastic.proto.User
-import javax.inject.Inject
/** Sealed class representing the result of processing a radio response packet. */
sealed class RadioResponseResult {
@@ -54,7 +54,8 @@ sealed class RadioResponseResult {
}
/** Use case for processing incoming [MeshPacket]s that are responses to admin requests. */
-open class ProcessRadioResponseUseCase @Inject constructor() {
+@Single
+open class ProcessRadioResponseUseCase {
/**
* Decodes and processes the provided [packet].
*
diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/RadioConfigUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/RadioConfigUseCase.kt
index a65b75209..6db74a3c8 100644
--- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/RadioConfigUseCase.kt
+++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/RadioConfigUseCase.kt
@@ -16,16 +16,17 @@
*/
package org.meshtastic.core.domain.usecase.settings
+import org.koin.core.annotation.Single
import org.meshtastic.core.model.Position
import org.meshtastic.core.model.RadioController
import org.meshtastic.proto.Config
import org.meshtastic.proto.ModuleConfig
import org.meshtastic.proto.User
-import javax.inject.Inject
/** Use case for interacting with radio configuration components. */
@Suppress("TooManyFunctions")
-open class RadioConfigUseCase @Inject constructor(private val radioController: RadioController) {
+@Single
+open class RadioConfigUseCase constructor(private val radioController: RadioController) {
/**
* Updates the owner information on the radio.
*
diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetAppIntroCompletedUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetAppIntroCompletedUseCase.kt
index d31cc41f3..79737c439 100644
--- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetAppIntroCompletedUseCase.kt
+++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetAppIntroCompletedUseCase.kt
@@ -16,15 +16,12 @@
*/
package org.meshtastic.core.domain.usecase.settings
+import org.koin.core.annotation.Single
import org.meshtastic.core.datastore.UiPreferencesDataSource
-import javax.inject.Inject
/** Use case for setting whether the application intro has been completed. */
-open class SetAppIntroCompletedUseCase
-@Inject
-constructor(
- private val uiPreferencesDataSource: UiPreferencesDataSource,
-) {
+@Single
+open class SetAppIntroCompletedUseCase constructor(private val uiPreferencesDataSource: UiPreferencesDataSource) {
operator fun invoke(completed: Boolean) {
uiPreferencesDataSource.setAppIntroCompleted(completed)
}
diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetDatabaseCacheLimitUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetDatabaseCacheLimitUseCase.kt
index 4b46cd70c..ca23e11d0 100644
--- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetDatabaseCacheLimitUseCase.kt
+++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetDatabaseCacheLimitUseCase.kt
@@ -16,12 +16,13 @@
*/
package org.meshtastic.core.domain.usecase.settings
+import org.koin.core.annotation.Single
import org.meshtastic.core.common.database.DatabaseManager
import org.meshtastic.core.database.DatabaseConstants
-import javax.inject.Inject
/** Use case for setting the database cache limit. */
-open class SetDatabaseCacheLimitUseCase @Inject constructor(private val databaseManager: DatabaseManager) {
+@Single
+open class SetDatabaseCacheLimitUseCase constructor(private val databaseManager: DatabaseManager) {
operator fun invoke(limit: Int) {
val clamped = limit.coerceIn(DatabaseConstants.MIN_CACHE_LIMIT, DatabaseConstants.MAX_CACHE_LIMIT)
databaseManager.setCacheLimit(clamped)
diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetMeshLogSettingsUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetMeshLogSettingsUseCase.kt
index b18133635..856be35b6 100644
--- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetMeshLogSettingsUseCase.kt
+++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetMeshLogSettingsUseCase.kt
@@ -16,13 +16,13 @@
*/
package org.meshtastic.core.domain.usecase.settings
+import org.koin.core.annotation.Single
import org.meshtastic.core.repository.MeshLogPrefs
import org.meshtastic.core.repository.MeshLogRepository
-import javax.inject.Inject
/** Use case for managing mesh log settings. */
+@Single
open class SetMeshLogSettingsUseCase
-@Inject
constructor(
private val meshLogRepository: MeshLogRepository,
private val meshLogPrefs: MeshLogPrefs,
diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetProvideLocationUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetProvideLocationUseCase.kt
index e66651f9c..19e606f7a 100644
--- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetProvideLocationUseCase.kt
+++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetProvideLocationUseCase.kt
@@ -16,11 +16,12 @@
*/
package org.meshtastic.core.domain.usecase.settings
+import org.koin.core.annotation.Single
import org.meshtastic.core.repository.UiPrefs
-import javax.inject.Inject
/** Use case for setting whether to provide the node location to the mesh. */
-open class SetProvideLocationUseCase @Inject constructor(private val uiPrefs: UiPrefs) {
+@Single
+open class SetProvideLocationUseCase constructor(private val uiPrefs: UiPrefs) {
operator fun invoke(myNodeNum: Int, provideLocation: Boolean) {
uiPrefs.setShouldProvideNodeLocation(myNodeNum, provideLocation)
}
diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetThemeUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetThemeUseCase.kt
index fd1ae35a0..831d9a529 100644
--- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetThemeUseCase.kt
+++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetThemeUseCase.kt
@@ -16,11 +16,12 @@
*/
package org.meshtastic.core.domain.usecase.settings
+import org.koin.core.annotation.Single
import org.meshtastic.core.datastore.UiPreferencesDataSource
-import javax.inject.Inject
/** Use case for setting the application theme. */
-open class SetThemeUseCase @Inject constructor(private val uiPreferencesDataSource: UiPreferencesDataSource) {
+@Single
+open class SetThemeUseCase constructor(private val uiPreferencesDataSource: UiPreferencesDataSource) {
operator fun invoke(themeMode: Int) {
uiPreferencesDataSource.setTheme(themeMode)
}
diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ToggleAnalyticsUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ToggleAnalyticsUseCase.kt
index 92aa6933c..ab6e5dce4 100644
--- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ToggleAnalyticsUseCase.kt
+++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ToggleAnalyticsUseCase.kt
@@ -16,11 +16,12 @@
*/
package org.meshtastic.core.domain.usecase.settings
+import org.koin.core.annotation.Single
import org.meshtastic.core.repository.AnalyticsPrefs
-import javax.inject.Inject
/** Use case for toggling the analytics preference. */
-open class ToggleAnalyticsUseCase @Inject constructor(private val analyticsPrefs: AnalyticsPrefs) {
+@Single
+open class ToggleAnalyticsUseCase constructor(private val analyticsPrefs: AnalyticsPrefs) {
operator fun invoke() {
analyticsPrefs.setAnalyticsAllowed(!analyticsPrefs.analyticsAllowed.value)
}
diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ToggleHomoglyphEncodingUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ToggleHomoglyphEncodingUseCase.kt
index 37d693e1f..5c403b2dd 100644
--- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ToggleHomoglyphEncodingUseCase.kt
+++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ToggleHomoglyphEncodingUseCase.kt
@@ -16,11 +16,12 @@
*/
package org.meshtastic.core.domain.usecase.settings
+import org.koin.core.annotation.Single
import org.meshtastic.core.repository.HomoglyphPrefs
-import javax.inject.Inject
/** Use case for toggling the homoglyph encoding preference. */
-open class ToggleHomoglyphEncodingUseCase @Inject constructor(private val homoglyphEncodingPrefs: HomoglyphPrefs) {
+@Single
+open class ToggleHomoglyphEncodingUseCase constructor(private val homoglyphEncodingPrefs: HomoglyphPrefs) {
operator fun invoke() {
homoglyphEncodingPrefs.setHomoglyphEncodingEnabled(!homoglyphEncodingPrefs.homoglyphEncodingEnabled.value)
}
diff --git a/core/navigation/build.gradle.kts b/core/navigation/build.gradle.kts
index 4d7f209df..9d6c56a7b 100644
--- a/core/navigation/build.gradle.kts
+++ b/core/navigation/build.gradle.kts
@@ -14,30 +14,14 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-import com.android.build.api.dsl.LibraryExtension
-
-/*
- * Copyright (c) 2025 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 .
- */
plugins {
- alias(libs.plugins.meshtastic.android.library)
+ alias(libs.plugins.meshtastic.kmp.library)
alias(libs.plugins.meshtastic.kotlinx.serialization)
}
-configure { namespace = "org.meshtastic.core.navigation" }
+kotlin {
+ android { namespace = "org.meshtastic.core.navigation" }
-dependencies { implementation(libs.kotlinx.serialization.core) }
+ sourceSets { commonMain.dependencies { implementation(libs.kotlinx.serialization.core) } }
+}
diff --git a/core/navigation/src/main/kotlin/org/meshtastic/core/navigation/Routes.kt b/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/Routes.kt
similarity index 100%
rename from core/navigation/src/main/kotlin/org/meshtastic/core/navigation/Routes.kt
rename to core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/Routes.kt
diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts
index 7085433ce..5ff29055d 100644
--- a/core/network/build.gradle.kts
+++ b/core/network/build.gradle.kts
@@ -18,7 +18,7 @@
plugins {
alias(libs.plugins.meshtastic.kmp.library)
alias(libs.plugins.meshtastic.kotlinx.serialization)
- alias(libs.plugins.devtools.ksp)
+ id("meshtastic.koin")
}
kotlin {
@@ -35,7 +35,6 @@ kotlin {
implementation(projects.core.model)
implementation(projects.core.proto)
- api(libs.javax.inject)
implementation(libs.okio)
implementation(libs.kotlinx.serialization.json)
implementation(libs.ktor.client.core)
@@ -43,8 +42,8 @@ kotlin {
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.kermit)
}
+
androidMain.dependencies {
- implementation(libs.hilt.android)
implementation(libs.org.eclipse.paho.client.mqttv3)
implementation(libs.coil.network.okhttp)
implementation(libs.coil.svg)
@@ -61,5 +60,3 @@ configurations.all {
attributes.attribute(marketplaceAttr, "fdroid")
}
}
-
-dependencies { add("kspAndroid", libs.hilt.compiler) }
diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/di/FdroidPlatformAnalyticsModule.kt b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/di/CoreNetworkAndroidModule.kt
similarity index 51%
rename from app/src/fdroid/kotlin/org/meshtastic/app/di/FdroidPlatformAnalyticsModule.kt
rename to core/network/src/androidMain/kotlin/org/meshtastic/core/network/di/CoreNetworkAndroidModule.kt
index 47d3e7fd5..ab46023eb 100644
--- a/app/src/fdroid/kotlin/org/meshtastic/app/di/FdroidPlatformAnalyticsModule.kt
+++ b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/di/CoreNetworkAndroidModule.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025-2026 Meshtastic LLC
+ * Copyright (c) 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
@@ -14,22 +14,20 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package org.meshtastic.app.di
+package org.meshtastic.core.network.di
-import dagger.Binds
-import dagger.Module
-import dagger.hilt.InstallIn
-import dagger.hilt.components.SingletonComponent
-import org.meshtastic.app.analytics.FdroidPlatformAnalytics
-import org.meshtastic.core.repository.PlatformAnalytics
-import javax.inject.Singleton
+import io.ktor.client.HttpClient
+import io.ktor.client.engine.okhttp.OkHttp
+import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
+import io.ktor.serialization.kotlinx.json.json
+import kotlinx.serialization.json.Json
+import org.koin.core.annotation.ComponentScan
+import org.koin.core.annotation.Module
+import org.koin.core.annotation.Single
-/** Hilt module to provide the [FdroidPlatformAnalytics] for the fdroid flavor. */
@Module
-@InstallIn(SingletonComponent::class)
-abstract class FdroidPlatformAnalyticsModule {
-
- @Binds
- @Singleton
- abstract fun bindPlatformHelper(fdroidPlatformAnalytics: FdroidPlatformAnalytics): PlatformAnalytics
+@ComponentScan("org.meshtastic.core.network")
+class CoreNetworkAndroidModule {
+ @Single
+ fun provideHttpClient(json: Json): HttpClient = HttpClient(OkHttp) { install(ContentNegotiation) { json(json) } }
}
diff --git a/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImpl.kt b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImpl.kt
index 86590e6cb..d9589eb0a 100644
--- a/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImpl.kt
+++ b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImpl.kt
@@ -30,6 +30,7 @@ import org.eclipse.paho.client.mqttv3.MqttCallbackExtended
import org.eclipse.paho.client.mqttv3.MqttConnectOptions
import org.eclipse.paho.client.mqttv3.MqttMessage
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence
+import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.ignoreException
import org.meshtastic.core.model.util.subscribeList
import org.meshtastic.core.repository.NodeRepository
@@ -37,14 +38,11 @@ import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.proto.MqttClientProxyMessage
import java.net.URI
import java.security.SecureRandom
-import javax.inject.Inject
-import javax.inject.Singleton
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
-@Singleton
+@Single
class MQTTRepositoryImpl
-@Inject
constructor(
private val radioConfigRepository: RadioConfigRepository,
private val nodeRepository: NodeRepository,
diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/DeviceHardwareRemoteDataSource.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/DeviceHardwareRemoteDataSource.kt
index 826de8c12..99f93dbf7 100644
--- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/DeviceHardwareRemoteDataSource.kt
+++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/DeviceHardwareRemoteDataSource.kt
@@ -17,14 +17,13 @@
package org.meshtastic.core.network
import kotlinx.coroutines.withContext
+import org.koin.core.annotation.Single
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.model.NetworkDeviceHardware
import org.meshtastic.core.network.service.ApiService
-import javax.inject.Inject
-class DeviceHardwareRemoteDataSource
-@Inject
-constructor(
+@Single
+class DeviceHardwareRemoteDataSource(
private val apiService: ApiService,
private val dispatchers: CoroutineDispatchers,
) {
diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/FirmwareReleaseRemoteDataSource.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/FirmwareReleaseRemoteDataSource.kt
index 056cdce43..0248110a9 100644
--- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/FirmwareReleaseRemoteDataSource.kt
+++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/FirmwareReleaseRemoteDataSource.kt
@@ -17,14 +17,13 @@
package org.meshtastic.core.network
import kotlinx.coroutines.withContext
+import org.koin.core.annotation.Single
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.model.NetworkFirmwareReleases
import org.meshtastic.core.network.service.ApiService
-import javax.inject.Inject
-class FirmwareReleaseRemoteDataSource
-@Inject
-constructor(
+@Single
+class FirmwareReleaseRemoteDataSource(
private val apiService: ApiService,
private val dispatchers: CoroutineDispatchers,
) {
diff --git a/app/src/main/kotlin/org/meshtastic/app/messaging/di/MessagingModule.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/di/CoreNetworkModule.kt
similarity index 61%
rename from app/src/main/kotlin/org/meshtastic/app/messaging/di/MessagingModule.kt
rename to core/network/src/commonMain/kotlin/org/meshtastic/core/network/di/CoreNetworkModule.kt
index 055f5c0cb..37d5726b9 100644
--- a/app/src/main/kotlin/org/meshtastic/app/messaging/di/MessagingModule.kt
+++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/di/CoreNetworkModule.kt
@@ -14,18 +14,19 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package org.meshtastic.app.messaging.di
+package org.meshtastic.core.network.di
-import dagger.Binds
-import dagger.Module
-import dagger.hilt.InstallIn
-import dagger.hilt.components.SingletonComponent
-import org.meshtastic.app.messaging.domain.worker.WorkManagerMessageQueue
-import org.meshtastic.core.repository.MessageQueue
+import kotlinx.serialization.json.Json
+import org.koin.core.annotation.ComponentScan
+import org.koin.core.annotation.Module
+import org.koin.core.annotation.Single
@Module
-@InstallIn(SingletonComponent::class)
-abstract class MessagingModule {
-
- @Binds abstract fun bindMessageQueue(impl: WorkManagerMessageQueue): MessageQueue
+@ComponentScan("org.meshtastic.core.network")
+class CoreNetworkModule {
+ @Single
+ fun provideJson(): Json = Json {
+ ignoreUnknownKeys = true
+ coerceInputValues = true
+ }
}
diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/service/ApiService.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/service/ApiService.kt
index a8a813614..1e12344b4 100644
--- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/service/ApiService.kt
+++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/service/ApiService.kt
@@ -19,9 +19,9 @@ package org.meshtastic.core.network.service
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.get
+import org.koin.core.annotation.Single
import org.meshtastic.core.model.NetworkDeviceHardware
import org.meshtastic.core.model.NetworkFirmwareReleases
-import javax.inject.Inject
interface ApiService {
suspend fun getDeviceHardware(): List
@@ -29,7 +29,8 @@ interface ApiService {
suspend fun getFirmwareReleases(): NetworkFirmwareReleases
}
-class ApiServiceImpl @Inject constructor(private val client: HttpClient) : ApiService {
+@Single
+class ApiServiceImpl(private val client: HttpClient) : ApiService {
override suspend fun getDeviceHardware(): List =
client.get("https://api.meshtastic.org/resource/deviceHardware").body()
diff --git a/core/prefs/build.gradle.kts b/core/prefs/build.gradle.kts
index f2d34d56e..6939dc64a 100644
--- a/core/prefs/build.gradle.kts
+++ b/core/prefs/build.gradle.kts
@@ -17,7 +17,7 @@
plugins {
alias(libs.plugins.meshtastic.kmp.library)
- alias(libs.plugins.devtools.ksp)
+ id("meshtastic.koin")
}
kotlin {
@@ -34,7 +34,6 @@ kotlin {
implementation(projects.core.common)
implementation(projects.core.di)
- api(libs.javax.inject)
implementation(libs.androidx.datastore.preferences)
implementation(libs.kotlinx.coroutines.core)
}
@@ -46,5 +45,3 @@ kotlin {
}
}
}
-
-dependencies { add("kspAndroid", libs.hilt.compiler) }
diff --git a/core/prefs/src/androidMain/kotlin/org/meshtastic/core/prefs/di/CorePrefsAndroidModule.kt b/core/prefs/src/androidMain/kotlin/org/meshtastic/core/prefs/di/CorePrefsAndroidModule.kt
new file mode 100644
index 000000000..dfd9d048c
--- /dev/null
+++ b/core/prefs/src/androidMain/kotlin/org/meshtastic/core/prefs/di/CorePrefsAndroidModule.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright (c) 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 org.meshtastic.core.prefs.di
+
+import android.content.Context
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.SharedPreferencesMigration
+import androidx.datastore.preferences.core.PreferenceDataStoreFactory
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.preferencesDataStoreFile
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import org.koin.core.annotation.Module
+import org.koin.core.annotation.Named
+import org.koin.core.annotation.Single
+
+@Suppress("TooManyFunctions")
+@Module
+class CorePrefsAndroidModule {
+ private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
+
+ @Single
+ @Named("AnalyticsDataStore")
+ fun provideAnalyticsDataStore(context: Context): DataStore = PreferenceDataStoreFactory.create(
+ migrations = listOf(SharedPreferencesMigration(context, "analytics-prefs")),
+ scope = scope,
+ produceFile = { context.preferencesDataStoreFile("analytics_ds") },
+ )
+
+ @Single
+ @Named("HomoglyphEncodingDataStore")
+ fun provideHomoglyphEncodingDataStore(context: Context): DataStore = PreferenceDataStoreFactory.create(
+ migrations = listOf(SharedPreferencesMigration(context, "homoglyph-encoding-prefs")),
+ scope = scope,
+ produceFile = { context.preferencesDataStoreFile("homoglyph_encoding_ds") },
+ )
+
+ @Single
+ @Named("AppDataStore")
+ fun provideAppDataStore(context: Context): DataStore = PreferenceDataStoreFactory.create(
+ migrations = listOf(SharedPreferencesMigration(context, "prefs")),
+ scope = scope,
+ produceFile = { context.preferencesDataStoreFile("app_ds") },
+ )
+
+ @Single
+ @Named("CustomEmojiDataStore")
+ fun provideCustomEmojiDataStore(context: Context): DataStore = PreferenceDataStoreFactory.create(
+ migrations = listOf(SharedPreferencesMigration(context, "org.geeksville.emoji.prefs")),
+ scope = scope,
+ produceFile = { context.preferencesDataStoreFile("custom_emoji_ds") },
+ )
+
+ @Single
+ @Named("MapDataStore")
+ fun provideMapDataStore(context: Context): DataStore = PreferenceDataStoreFactory.create(
+ migrations = listOf(SharedPreferencesMigration(context, "map_prefs")),
+ scope = scope,
+ produceFile = { context.preferencesDataStoreFile("map_ds") },
+ )
+
+ @Single
+ @Named("MapConsentDataStore")
+ fun provideMapConsentDataStore(context: Context): DataStore = PreferenceDataStoreFactory.create(
+ migrations = listOf(SharedPreferencesMigration(context, "map_consent_preferences")),
+ scope = scope,
+ produceFile = { context.preferencesDataStoreFile("map_consent_ds") },
+ )
+
+ @Single
+ @Named("MapTileProviderDataStore")
+ fun provideMapTileProviderDataStore(context: Context): DataStore = PreferenceDataStoreFactory.create(
+ migrations = listOf(SharedPreferencesMigration(context, "map_tile_provider_prefs")),
+ scope = scope,
+ produceFile = { context.preferencesDataStoreFile("map_tile_provider_ds") },
+ )
+
+ @Single
+ @Named("MeshDataStore")
+ fun provideMeshDataStore(context: Context): DataStore = PreferenceDataStoreFactory.create(
+ migrations = listOf(SharedPreferencesMigration(context, "mesh-prefs")),
+ scope = scope,
+ produceFile = { context.preferencesDataStoreFile("mesh_ds") },
+ )
+
+ @Single
+ @Named("RadioDataStore")
+ fun provideRadioDataStore(context: Context): DataStore = PreferenceDataStoreFactory.create(
+ migrations = listOf(SharedPreferencesMigration(context, "radio-prefs")),
+ scope = scope,
+ produceFile = { context.preferencesDataStoreFile("radio_ds") },
+ )
+
+ @Single
+ @Named("UiDataStore")
+ fun provideUiDataStore(context: Context): DataStore = PreferenceDataStoreFactory.create(
+ migrations = listOf(SharedPreferencesMigration(context, "ui-prefs")),
+ scope = scope,
+ produceFile = { context.preferencesDataStoreFile("ui_ds") },
+ )
+
+ @Single
+ @Named("MeshLogDataStore")
+ fun provideMeshLogDataStore(context: Context): DataStore = PreferenceDataStoreFactory.create(
+ migrations = listOf(SharedPreferencesMigration(context, "meshlog-prefs")),
+ scope = scope,
+ produceFile = { context.preferencesDataStoreFile("meshlog_ds") },
+ )
+
+ @Single
+ @Named("FilterDataStore")
+ fun provideFilterDataStore(context: Context): DataStore = PreferenceDataStoreFactory.create(
+ migrations = listOf(SharedPreferencesMigration(context, "filter-prefs")),
+ scope = scope,
+ produceFile = { context.preferencesDataStoreFile("filter_ds") },
+ )
+}
diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/analytics/AnalyticsPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/analytics/AnalyticsPrefsImpl.kt
index 4fe087be0..8d52c4c0b 100644
--- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/analytics/AnalyticsPrefsImpl.kt
+++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/analytics/AnalyticsPrefsImpl.kt
@@ -28,20 +28,16 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
+import org.koin.core.annotation.Named
+import org.koin.core.annotation.Single
import org.meshtastic.core.di.CoroutineDispatchers
-import org.meshtastic.core.prefs.di.AnalyticsDataStore
-import org.meshtastic.core.prefs.di.AppDataStore
import org.meshtastic.core.repository.AnalyticsPrefs
-import javax.inject.Inject
-import javax.inject.Singleton
import kotlin.uuid.Uuid
-@Singleton
-class AnalyticsPrefsImpl
-@Inject
-constructor(
- @AnalyticsDataStore private val analyticsDataStore: DataStore,
- @AppDataStore private val appDataStore: DataStore,
+@Single
+class AnalyticsPrefsImpl(
+ @Named("AnalyticsDataStore") private val analyticsDataStore: DataStore,
+ @Named("AppDataStore") private val appDataStore: DataStore,
dispatchers: CoroutineDispatchers,
) : AnalyticsPrefs {
private val scope = CoroutineScope(SupervisorJob() + dispatchers.default)
diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/di/CorePrefsModule.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/di/CorePrefsModule.kt
new file mode 100644
index 000000000..ef11bac13
--- /dev/null
+++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/di/CorePrefsModule.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 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 org.meshtastic.core.prefs.di
+
+import org.koin.core.annotation.ComponentScan
+import org.koin.core.annotation.Module
+
+@Module
+@ComponentScan("org.meshtastic.core.prefs")
+class CorePrefsModule
diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/emoji/CustomEmojiPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/emoji/CustomEmojiPrefsImpl.kt
index 9bc7f1805..257ffba81 100644
--- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/emoji/CustomEmojiPrefsImpl.kt
+++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/emoji/CustomEmojiPrefsImpl.kt
@@ -27,17 +27,14 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
+import org.koin.core.annotation.Named
+import org.koin.core.annotation.Single
import org.meshtastic.core.di.CoroutineDispatchers
-import org.meshtastic.core.prefs.di.CustomEmojiDataStore
import org.meshtastic.core.repository.CustomEmojiPrefs
-import javax.inject.Inject
-import javax.inject.Singleton
-@Singleton
-class CustomEmojiPrefsImpl
-@Inject
-constructor(
- @CustomEmojiDataStore private val dataStore: DataStore,
+@Single
+class CustomEmojiPrefsImpl(
+ @Named("CustomEmojiDataStore") private val dataStore: DataStore,
dispatchers: CoroutineDispatchers,
) : CustomEmojiPrefs {
private val scope = CoroutineScope(SupervisorJob() + dispatchers.default)
diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/filter/FilterPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/filter/FilterPrefsImpl.kt
index 6ea9e24dd..121925e71 100644
--- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/filter/FilterPrefsImpl.kt
+++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/filter/FilterPrefsImpl.kt
@@ -28,17 +28,14 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
+import org.koin.core.annotation.Named
+import org.koin.core.annotation.Single
import org.meshtastic.core.di.CoroutineDispatchers
-import org.meshtastic.core.prefs.di.FilterDataStore
import org.meshtastic.core.repository.FilterPrefs
-import javax.inject.Inject
-import javax.inject.Singleton
-@Singleton
-class FilterPrefsImpl
-@Inject
-constructor(
- @FilterDataStore private val dataStore: DataStore,
+@Single
+class FilterPrefsImpl(
+ @Named("FilterDataStore") private val dataStore: DataStore,
dispatchers: CoroutineDispatchers,
) : FilterPrefs {
private val scope = CoroutineScope(SupervisorJob() + dispatchers.default)
diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/homoglyph/HomoglyphPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/homoglyph/HomoglyphPrefsImpl.kt
index 42b4f8faa..092367db5 100644
--- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/homoglyph/HomoglyphPrefsImpl.kt
+++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/homoglyph/HomoglyphPrefsImpl.kt
@@ -27,17 +27,14 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
+import org.koin.core.annotation.Named
+import org.koin.core.annotation.Single
import org.meshtastic.core.di.CoroutineDispatchers
-import org.meshtastic.core.prefs.di.HomoglyphEncodingDataStore
import org.meshtastic.core.repository.HomoglyphPrefs
-import javax.inject.Inject
-import javax.inject.Singleton
-@Singleton
-class HomoglyphPrefsImpl
-@Inject
-constructor(
- @HomoglyphEncodingDataStore private val dataStore: DataStore,
+@Single
+class HomoglyphPrefsImpl(
+ @Named("HomoglyphEncodingDataStore") private val dataStore: DataStore,
dispatchers: CoroutineDispatchers,
) : HomoglyphPrefs {
private val scope = CoroutineScope(SupervisorJob() + dispatchers.default)
diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapConsentPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapConsentPrefsImpl.kt
index bf22eb27d..86a6ab40d 100644
--- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapConsentPrefsImpl.kt
+++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapConsentPrefsImpl.kt
@@ -27,18 +27,15 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
+import org.koin.core.annotation.Named
+import org.koin.core.annotation.Single
import org.meshtastic.core.di.CoroutineDispatchers
-import org.meshtastic.core.prefs.di.MapConsentDataStore
import org.meshtastic.core.repository.MapConsentPrefs
import java.util.concurrent.ConcurrentHashMap
-import javax.inject.Inject
-import javax.inject.Singleton
-@Singleton
-class MapConsentPrefsImpl
-@Inject
-constructor(
- @MapConsentDataStore private val dataStore: DataStore,
+@Single
+class MapConsentPrefsImpl(
+ @Named("MapConsentDataStore") private val dataStore: DataStore,
dispatchers: CoroutineDispatchers,
) : MapConsentPrefs {
private val scope = CoroutineScope(SupervisorJob() + dispatchers.default)
diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapPrefsImpl.kt
index 52167812f..506d5ac5e 100644
--- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapPrefsImpl.kt
+++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapPrefsImpl.kt
@@ -29,17 +29,14 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
+import org.koin.core.annotation.Named
+import org.koin.core.annotation.Single
import org.meshtastic.core.di.CoroutineDispatchers
-import org.meshtastic.core.prefs.di.MapDataStore
import org.meshtastic.core.repository.MapPrefs
-import javax.inject.Inject
-import javax.inject.Singleton
-@Singleton
-class MapPrefsImpl
-@Inject
-constructor(
- @MapDataStore private val dataStore: DataStore,
+@Single
+class MapPrefsImpl(
+ @Named("MapDataStore") private val dataStore: DataStore,
dispatchers: CoroutineDispatchers,
) : MapPrefs {
private val scope = CoroutineScope(SupervisorJob() + dispatchers.default)
diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapTileProviderPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapTileProviderPrefsImpl.kt
index c3a686e97..30192f98a 100644
--- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapTileProviderPrefsImpl.kt
+++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapTileProviderPrefsImpl.kt
@@ -27,17 +27,14 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
+import org.koin.core.annotation.Named
+import org.koin.core.annotation.Single
import org.meshtastic.core.di.CoroutineDispatchers
-import org.meshtastic.core.prefs.di.MapTileProviderDataStore
import org.meshtastic.core.repository.MapTileProviderPrefs
-import javax.inject.Inject
-import javax.inject.Singleton
-@Singleton
-class MapTileProviderPrefsImpl
-@Inject
-constructor(
- @MapTileProviderDataStore private val dataStore: DataStore,
+@Single
+class MapTileProviderPrefsImpl(
+ @Named("MapTileProviderDataStore") private val dataStore: DataStore,
dispatchers: CoroutineDispatchers,
) : MapTileProviderPrefs {
private val scope = CoroutineScope(SupervisorJob() + dispatchers.default)
diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/mesh/MeshPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/mesh/MeshPrefsImpl.kt
index c247788f2..7807a6c32 100644
--- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/mesh/MeshPrefsImpl.kt
+++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/mesh/MeshPrefsImpl.kt
@@ -29,19 +29,16 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
+import org.koin.core.annotation.Named
+import org.koin.core.annotation.Single
import org.meshtastic.core.di.CoroutineDispatchers
-import org.meshtastic.core.prefs.di.MeshDataStore
import org.meshtastic.core.repository.MeshPrefs
import java.util.Locale
import java.util.concurrent.ConcurrentHashMap
-import javax.inject.Inject
-import javax.inject.Singleton
-@Singleton
-class MeshPrefsImpl
-@Inject
-constructor(
- @MeshDataStore private val dataStore: DataStore,
+@Single
+class MeshPrefsImpl(
+ @Named("MeshDataStore") private val dataStore: DataStore,
dispatchers: CoroutineDispatchers,
) : MeshPrefs {
private val scope = CoroutineScope(SupervisorJob() + dispatchers.default)
diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/meshlog/MeshLogPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/meshlog/MeshLogPrefsImpl.kt
index a10c27da8..494579e72 100644
--- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/meshlog/MeshLogPrefsImpl.kt
+++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/meshlog/MeshLogPrefsImpl.kt
@@ -28,17 +28,14 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
+import org.koin.core.annotation.Named
+import org.koin.core.annotation.Single
import org.meshtastic.core.di.CoroutineDispatchers
-import org.meshtastic.core.prefs.di.MeshLogDataStore
import org.meshtastic.core.repository.MeshLogPrefs
-import javax.inject.Inject
-import javax.inject.Singleton
-@Singleton
-class MeshLogPrefsImpl
-@Inject
-constructor(
- @MeshLogDataStore private val dataStore: DataStore,
+@Single
+class MeshLogPrefsImpl(
+ @Named("MeshLogDataStore") private val dataStore: DataStore,
dispatchers: CoroutineDispatchers,
) : MeshLogPrefs {
private val scope = CoroutineScope(SupervisorJob() + dispatchers.default)
diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/radio/RadioPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/radio/RadioPrefsImpl.kt
index 916bb892c..d551f9333 100644
--- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/radio/RadioPrefsImpl.kt
+++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/radio/RadioPrefsImpl.kt
@@ -27,17 +27,14 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
+import org.koin.core.annotation.Named
+import org.koin.core.annotation.Single
import org.meshtastic.core.di.CoroutineDispatchers
-import org.meshtastic.core.prefs.di.RadioDataStore
import org.meshtastic.core.repository.RadioPrefs
-import javax.inject.Inject
-import javax.inject.Singleton
-@Singleton
-class RadioPrefsImpl
-@Inject
-constructor(
- @RadioDataStore private val dataStore: DataStore,
+@Single
+class RadioPrefsImpl(
+ @Named("RadioDataStore") private val dataStore: DataStore,
dispatchers: CoroutineDispatchers,
) : RadioPrefs {
private val scope = CoroutineScope(SupervisorJob() + dispatchers.default)
diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/ui/UiPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/ui/UiPrefsImpl.kt
index 13c8ed336..0393a762f 100644
--- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/ui/UiPrefsImpl.kt
+++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/ui/UiPrefsImpl.kt
@@ -27,18 +27,15 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
+import org.koin.core.annotation.Named
+import org.koin.core.annotation.Single
import org.meshtastic.core.di.CoroutineDispatchers
-import org.meshtastic.core.prefs.di.UiDataStore
import org.meshtastic.core.repository.UiPrefs
import java.util.concurrent.ConcurrentHashMap
-import javax.inject.Inject
-import javax.inject.Singleton
-@Singleton
-class UiPrefsImpl
-@Inject
-constructor(
- @UiDataStore private val dataStore: DataStore,
+@Single
+class UiPrefsImpl(
+ @Named("UiDataStore") private val dataStore: DataStore,
dispatchers: CoroutineDispatchers,
) : UiPrefs {
private val scope = CoroutineScope(SupervisorJob() + dispatchers.default)
diff --git a/core/repository/build.gradle.kts b/core/repository/build.gradle.kts
index 44e49f491..9a74a9c32 100644
--- a/core/repository/build.gradle.kts
+++ b/core/repository/build.gradle.kts
@@ -15,7 +15,10 @@
* along with this program. If not, see .
*/
-plugins { alias(libs.plugins.meshtastic.kmp.library) }
+plugins {
+ alias(libs.plugins.meshtastic.kmp.library)
+ alias(libs.plugins.meshtastic.koin)
+}
kotlin {
@Suppress("UnstableApiUsage")
diff --git a/app/src/main/kotlin/org/meshtastic/app/di/UseCaseModule.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/di/CoreRepositoryModule.kt
similarity index 81%
rename from app/src/main/kotlin/org/meshtastic/app/di/UseCaseModule.kt
rename to core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/di/CoreRepositoryModule.kt
index f0b078cea..e0f08ee86 100644
--- a/app/src/main/kotlin/org/meshtastic/app/di/UseCaseModule.kt
+++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/di/CoreRepositoryModule.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025-2026 Meshtastic LLC
+ * Copyright (c) 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
@@ -14,26 +14,22 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package org.meshtastic.app.di
+package org.meshtastic.core.repository.di
-import dagger.Module
-import dagger.Provides
-import dagger.hilt.InstallIn
-import dagger.hilt.components.SingletonComponent
+import org.koin.core.annotation.ComponentScan
+import org.koin.core.annotation.Module
+import org.koin.core.annotation.Single
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.repository.HomoglyphPrefs
import org.meshtastic.core.repository.MessageQueue
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.PacketRepository
import org.meshtastic.core.repository.usecase.SendMessageUseCase
-import javax.inject.Singleton
@Module
-@InstallIn(SingletonComponent::class)
-object UseCaseModule {
-
- @Provides
- @Singleton
+@ComponentScan("org.meshtastic.core.repository")
+class CoreRepositoryModule {
+ @Single
fun provideSendMessageUseCase(
nodeRepository: NodeRepository,
packetRepository: PacketRepository,
diff --git a/core/service/build.gradle.kts b/core/service/build.gradle.kts
index 93f251c88..790cb73c6 100644
--- a/core/service/build.gradle.kts
+++ b/core/service/build.gradle.kts
@@ -17,7 +17,7 @@
plugins {
alias(libs.plugins.meshtastic.kmp.library)
- alias(libs.plugins.devtools.ksp)
+ id("meshtastic.koin")
}
kotlin {
@@ -35,15 +35,12 @@ kotlin {
implementation(projects.core.model)
implementation(projects.core.prefs)
implementation(projects.core.proto)
- implementation(libs.javax.inject)
+
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kermit)
}
- androidMain.dependencies {
- api(projects.core.api)
- implementation(libs.hilt.android)
- }
+ androidMain.dependencies { api(projects.core.api) }
commonTest.dependencies {
implementation(libs.junit)
@@ -53,5 +50,3 @@ kotlin {
}
}
}
-
-dependencies { add("kspAndroid", libs.hilt.compiler) }
diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidRadioControllerImpl.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidRadioControllerImpl.kt
index 9790eeec3..b6a1b7273 100644
--- a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidRadioControllerImpl.kt
+++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidRadioControllerImpl.kt
@@ -17,23 +17,19 @@
package org.meshtastic.core.service
import android.content.Context
-import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.StateFlow
+import org.koin.core.annotation.Single
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.model.service.ServiceAction
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.proto.ClientNotification
-import javax.inject.Inject
-import javax.inject.Singleton
-@Singleton
+@Single
@Suppress("TooManyFunctions")
-class AndroidRadioControllerImpl
-@Inject
-constructor(
- @ApplicationContext private val context: Context,
+class AndroidRadioControllerImpl(
+ private val context: Context,
private val serviceRepository: AndroidServiceRepository,
private val nodeRepository: NodeRepository,
) : RadioController {
diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidServiceRepository.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidServiceRepository.kt
index 07a53aa16..91cac4d41 100644
--- a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidServiceRepository.kt
+++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidServiceRepository.kt
@@ -25,19 +25,18 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.receiveAsFlow
+import org.koin.core.annotation.Single
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.service.ServiceAction
import org.meshtastic.core.model.service.TracerouteResponse
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.proto.ClientNotification
import org.meshtastic.proto.MeshPacket
-import javax.inject.Inject
-import javax.inject.Singleton
/** Repository class for managing the [IMeshService] instance and connection state */
@Suppress("TooManyFunctions")
-@Singleton
-open class AndroidServiceRepository @Inject constructor() : ServiceRepository {
+@Single(binds = [ServiceRepository::class, AndroidServiceRepository::class])
+open class AndroidServiceRepository : ServiceRepository {
var meshService: IMeshService? = null
private set
diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/di/CoreServiceAndroidModule.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/di/CoreServiceAndroidModule.kt
new file mode 100644
index 000000000..f5104739c
--- /dev/null
+++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/di/CoreServiceAndroidModule.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 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 org.meshtastic.core.service.di
+
+import org.koin.core.annotation.ComponentScan
+import org.koin.core.annotation.Module
+
+@Module
+@ComponentScan("org.meshtastic.core.service")
+class CoreServiceAndroidModule
diff --git a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/di/CoreServiceModule.kt b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/di/CoreServiceModule.kt
new file mode 100644
index 000000000..d007f1ea3
--- /dev/null
+++ b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/di/CoreServiceModule.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 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 org.meshtastic.core.service.di
+
+import org.koin.core.annotation.ComponentScan
+import org.koin.core.annotation.Module
+
+@Module
+@ComponentScan("org.meshtastic.core.service")
+class CoreServiceModule
diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts
index a25a6b8bb..58b31de48 100644
--- a/core/ui/build.gradle.kts
+++ b/core/ui/build.gradle.kts
@@ -19,7 +19,7 @@ import com.android.build.api.dsl.LibraryExtension
plugins {
alias(libs.plugins.meshtastic.android.library)
alias(libs.plugins.meshtastic.android.library.compose)
- alias(libs.plugins.meshtastic.hilt)
+ alias(libs.plugins.meshtastic.koin)
}
configure { namespace = "org.meshtastic.core.ui" }
@@ -44,6 +44,7 @@ dependencies {
implementation(libs.zxing.core)
implementation(libs.kermit)
implementation(libs.nordic.common.core)
+ implementation(libs.koin.compose.viewmodel)
debugImplementation(libs.androidx.compose.ui.test.manifest)
diff --git a/core/ui/detekt-baseline.xml b/core/ui/detekt-baseline.xml
index 6748a79ba..cbe00c8b4 100644
--- a/core/ui/detekt-baseline.xml
+++ b/core/ui/detekt-baseline.xml
@@ -9,5 +9,6 @@
MagicNumber:EditListPreference.kt$12345
MagicNumber:EditListPreference.kt$67890
MagicNumber:LazyColumnDragAndDropDemo.kt$50
+ MatchingDeclarationName:LocalTracerouteMapOverlayInsetsProvider.kt$TracerouteMapOverlayInsets
diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/di/CoreUiModule.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/di/CoreUiModule.kt
new file mode 100644
index 000000000..077533641
--- /dev/null
+++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/di/CoreUiModule.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 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 org.meshtastic.core.ui.di
+
+import org.koin.core.annotation.ComponentScan
+import org.koin.core.annotation.Module
+
+@Module
+@ComponentScan("org.meshtastic.core.ui")
+class CoreUiModule
diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/emoji/EmojiPicker.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/emoji/EmojiPicker.kt
index 21536eeda..5421b22d5 100644
--- a/core/ui/src/main/kotlin/org/meshtastic/core/ui/emoji/EmojiPicker.kt
+++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/emoji/EmojiPicker.kt
@@ -26,12 +26,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import androidx.emoji2.emojipicker.RecentEmojiProviderAdapter
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
+import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.core.ui.component.BottomSheetDialog
@Composable
fun EmojiPicker(
- viewModel: EmojiPickerViewModel = hiltViewModel(),
+ viewModel: EmojiPickerViewModel = koinViewModel(),
onDismiss: () -> Unit = {},
onConfirm: (String) -> Unit,
) {
diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/emoji/EmojiPickerViewModel.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/emoji/EmojiPickerViewModel.kt
index 8a30006d8..097a58048 100644
--- a/core/ui/src/main/kotlin/org/meshtastic/core/ui/emoji/EmojiPickerViewModel.kt
+++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/emoji/EmojiPickerViewModel.kt
@@ -17,12 +17,11 @@
package org.meshtastic.core.ui.emoji
import androidx.lifecycle.ViewModel
-import dagger.hilt.android.lifecycle.HiltViewModel
+import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.repository.CustomEmojiPrefs
-import javax.inject.Inject
-@HiltViewModel
-class EmojiPickerViewModel @Inject constructor(private val customEmojiPrefs: CustomEmojiPrefs) : ViewModel() {
+@KoinViewModel
+class EmojiPickerViewModel(private val customEmojiPrefs: CustomEmojiPrefs) : ViewModel() {
var customEmojiFrequency: String?
get() = customEmojiPrefs.customEmojiFrequency.value
diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeDialog.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeDialog.kt
index 33e721a3e..7f64f18b5 100644
--- a/core/ui/src/main/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeDialog.kt
+++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeDialog.kt
@@ -47,9 +47,9 @@ import androidx.compose.ui.tooling.preview.PreviewScreenSizes
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
+import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.core.model.Channel
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.accept
@@ -66,7 +66,7 @@ import org.meshtastic.proto.ChannelSet
fun ScannedQrCodeDialog(
incoming: ChannelSet,
onDismiss: () -> Unit,
- viewModel: ScannedQrCodeViewModel = hiltViewModel(),
+ viewModel: ScannedQrCodeViewModel = koinViewModel(),
) {
val channels by viewModel.channels.collectAsStateWithLifecycle()
diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeViewModel.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeViewModel.kt
index cf3ab3404..2c10206aa 100644
--- a/core/ui/src/main/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeViewModel.kt
+++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeViewModel.kt
@@ -18,8 +18,8 @@ package org.meshtastic.core.ui.qr
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
+import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.ui.util.getChannelList
@@ -28,12 +28,9 @@ import org.meshtastic.proto.Channel
import org.meshtastic.proto.ChannelSet
import org.meshtastic.proto.Config
import org.meshtastic.proto.LocalConfig
-import javax.inject.Inject
-@HiltViewModel
-class ScannedQrCodeViewModel
-@Inject
-constructor(
+@KoinViewModel
+class ScannedQrCodeViewModel(
private val radioConfigRepository: RadioConfigRepository,
private val radioController: RadioController,
) : ViewModel() {
diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/share/SharedContactDialog.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/share/SharedContactDialog.kt
index 50588f547..549af6072 100644
--- a/core/ui/src/main/kotlin/org/meshtastic/core/ui/share/SharedContactDialog.kt
+++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/share/SharedContactDialog.kt
@@ -22,9 +22,9 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
+import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.core.model.util.compareUsers
import org.meshtastic.core.model.util.userFieldsToString
import org.meshtastic.core.resources.Res
@@ -42,7 +42,7 @@ import org.meshtastic.proto.User
fun SharedContactDialog(
sharedContact: SharedContact,
onDismiss: () -> Unit,
- viewModel: SharedContactViewModel = hiltViewModel(),
+ viewModel: SharedContactViewModel = koinViewModel(),
) {
val unfilteredNodes by viewModel.unfilteredNodes.collectAsStateWithLifecycle()
diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/share/SharedContactViewModel.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/share/SharedContactViewModel.kt
index d0feb933d..345c5b8ed 100644
--- a/core/ui/src/main/kotlin/org/meshtastic/core/ui/share/SharedContactViewModel.kt
+++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/share/SharedContactViewModel.kt
@@ -18,24 +18,19 @@ package org.meshtastic.core.ui.share
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
+import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.service.ServiceAction
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.proto.SharedContact
-import javax.inject.Inject
-@HiltViewModel
-class SharedContactViewModel
-@Inject
-constructor(
- nodeRepository: NodeRepository,
- private val serviceRepository: ServiceRepository,
-) : ViewModel() {
+@KoinViewModel
+class SharedContactViewModel(nodeRepository: NodeRepository, private val serviceRepository: ServiceRepository) :
+ ViewModel() {
val unfilteredNodes: StateFlow> =
nodeRepository.getNodes().stateInWhileSubscribed(initialValue = emptyList())
diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/AlertManager.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/AlertManager.kt
index d6282b5c2..623939bbd 100644
--- a/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/AlertManager.kt
+++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/AlertManager.kt
@@ -21,8 +21,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.jetbrains.compose.resources.StringResource
-import javax.inject.Inject
-import javax.inject.Singleton
+import org.koin.core.annotation.Single
fun interface ComposableContent {
@Composable fun Content()
@@ -32,8 +31,8 @@ fun interface ComposableContent {
* A global manager for displaying alerts across the application. This allows ViewModels to trigger alerts without
* direct dependencies on UI components.
*/
-@Singleton
-class AlertManager @Inject constructor() {
+@Single
+class AlertManager {
data class AlertData(
val title: String? = null,
val titleRes: StringResource? = null,
diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/LocalInlineMapProvider.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/LocalInlineMapProvider.kt
new file mode 100644
index 000000000..e2a3206d1
--- /dev/null
+++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/LocalInlineMapProvider.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 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 org.meshtastic.core.ui.util
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.ui.Modifier
+import org.meshtastic.core.model.Node
+
+val LocalInlineMapProvider = compositionLocalOf<@Composable (node: Node, modifier: Modifier) -> Unit> { { _, _ -> } }
diff --git a/feature/node/src/google/kotlin/org/meshtastic/feature/node/metrics/TracerouteMapOverlayInsets.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/LocalTracerouteMapOverlayInsetsProvider.kt
similarity index 71%
rename from feature/node/src/google/kotlin/org/meshtastic/feature/node/metrics/TracerouteMapOverlayInsets.kt
rename to core/ui/src/main/kotlin/org/meshtastic/core/ui/util/LocalTracerouteMapOverlayInsetsProvider.kt
index ad5d33784..40b174e8d 100644
--- a/feature/node/src/google/kotlin/org/meshtastic/feature/node/metrics/TracerouteMapOverlayInsets.kt
+++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/LocalTracerouteMapOverlayInsetsProvider.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025 Meshtastic LLC
+ * Copyright (c) 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
@@ -14,15 +14,17 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-
-package org.meshtastic.feature.node.metrics
+package org.meshtastic.core.ui.util
import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.dp
-internal object TracerouteMapOverlayInsets {
- val overlayAlignment: Alignment = Alignment.BottomCenter
- val overlayPadding: PaddingValues = PaddingValues(bottom = 16.dp)
- val contentHorizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally
-}
+data class TracerouteMapOverlayInsets(
+ val overlayAlignment: Alignment = Alignment.BottomCenter,
+ val overlayPadding: PaddingValues = PaddingValues(bottom = 16.dp),
+ val contentHorizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
+)
+
+val LocalTracerouteMapOverlayInsetsProvider = compositionLocalOf { TracerouteMapOverlayInsets() }
diff --git a/feature/firmware/build.gradle.kts b/feature/firmware/build.gradle.kts
index 9305aa57b..32b845ad0 100644
--- a/feature/firmware/build.gradle.kts
+++ b/feature/firmware/build.gradle.kts
@@ -14,54 +14,77 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-import com.android.build.api.dsl.LibraryExtension
plugins {
- alias(libs.plugins.meshtastic.android.library)
- alias(libs.plugins.meshtastic.android.library.compose)
- alias(libs.plugins.meshtastic.hilt)
- alias(libs.plugins.kover)
+ alias(libs.plugins.meshtastic.kmp.library)
+ alias(libs.plugins.meshtastic.kmp.library.compose)
+ alias(libs.plugins.meshtastic.kotlinx.serialization)
+ alias(libs.plugins.meshtastic.koin)
}
-configure { namespace = "org.meshtastic.feature.firmware" }
+kotlin {
+ @Suppress("UnstableApiUsage")
+ android {
+ namespace = "org.meshtastic.feature.firmware"
+ androidResources.enable = false
+ withHostTest { isIncludeAndroidResources = true }
+ }
-dependencies {
- implementation(projects.core.ble)
- implementation(projects.core.common)
- implementation(projects.core.data)
- implementation(projects.core.database)
- implementation(projects.core.datastore)
- implementation(projects.core.model)
- implementation(projects.core.navigation)
- implementation(projects.core.network)
- implementation(projects.core.prefs)
- implementation(projects.core.proto)
- implementation(projects.core.service)
- implementation(projects.core.resources)
- implementation(projects.core.ui)
+ sourceSets {
+ commonMain.dependencies {
+ implementation(projects.core.ble)
+ implementation(projects.core.common)
+ implementation(projects.core.data)
+ implementation(projects.core.database)
+ implementation(projects.core.datastore)
+ implementation(projects.core.model)
+ implementation(projects.core.navigation)
+ implementation(projects.core.network)
+ implementation(projects.core.prefs)
+ implementation(projects.core.proto)
+ implementation(projects.core.service)
+ implementation(projects.core.resources)
+ implementation(projects.core.ui)
- implementation(libs.accompanist.permissions)
- implementation(libs.androidx.appcompat)
- implementation(libs.androidx.compose.material.iconsExtended)
- implementation(libs.androidx.compose.material3)
- implementation(libs.androidx.compose.ui.text)
- implementation(libs.androidx.compose.ui.tooling.preview)
- implementation(libs.androidx.navigation.compose)
- implementation(libs.kotlinx.collections.immutable)
- implementation(libs.kermit)
- implementation(libs.ktor.client.core)
+ implementation(libs.androidx.lifecycle.viewmodel.compose)
+ implementation(libs.koin.compose.viewmodel)
+ implementation(libs.kermit)
+ implementation(libs.kotlinx.collections.immutable)
+ implementation(libs.ktor.client.core)
+ }
- implementation(libs.nordic.client.android)
- implementation(libs.nordic.dfu)
- implementation(libs.coil)
- implementation(libs.coil.network.okhttp)
- implementation(libs.markdown.renderer)
- implementation(libs.markdown.renderer.m3)
+ androidMain.dependencies {
+ implementation(project.dependencies.platform(libs.androidx.compose.bom))
+ implementation(libs.accompanist.permissions)
+ implementation(libs.androidx.activity.compose)
+ implementation(libs.androidx.appcompat)
+ implementation(libs.androidx.compose.material.iconsExtended)
+ implementation(libs.androidx.compose.material3)
+ implementation(libs.androidx.compose.ui.text)
+ implementation(libs.androidx.compose.ui.tooling.preview)
+ implementation(libs.androidx.navigation.common)
+ implementation(libs.coil)
+ implementation(libs.coil.network.okhttp)
+ implementation(libs.markdown.renderer.android)
+ implementation(libs.markdown.renderer.m3)
+ implementation(libs.markdown.renderer)
- testImplementation(libs.junit)
- testImplementation(libs.kotlinx.coroutines.test)
- testImplementation(libs.nordic.client.android.mock)
- testImplementation(libs.nordic.client.core.mock)
- testImplementation(libs.nordic.core.mock)
- testImplementation(libs.mockk)
+ // DFU / Nordic specific dependencies
+ implementation(libs.nordic.client.android)
+ implementation(libs.nordic.dfu)
+ }
+
+ androidUnitTest.dependencies {
+ implementation(libs.junit)
+ implementation(libs.mockk)
+ implementation(libs.robolectric)
+ implementation(libs.turbine)
+ implementation(libs.kotlinx.coroutines.test)
+ implementation(libs.androidx.compose.ui.test.junit4)
+ implementation(libs.androidx.test.ext.junit)
+ implementation(libs.nordic.client.android.mock)
+ implementation(libs.nordic.client.core.mock)
+ implementation(libs.nordic.core.mock)
+ }
+ }
}
diff --git a/feature/firmware/src/main/AndroidManifest.xml b/feature/firmware/src/androidMain/AndroidManifest.xml
similarity index 100%
rename from feature/firmware/src/main/AndroidManifest.xml
rename to feature/firmware/src/androidMain/AndroidManifest.xml
diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareFileHandler.kt b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/AndroidFirmwareFileHandler.kt
similarity index 72%
rename from feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareFileHandler.kt
rename to feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/AndroidFirmwareFileHandler.kt
index 75985a0ed..505d263c1 100644
--- a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareFileHandler.kt
+++ b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/AndroidFirmwareFileHandler.kt
@@ -17,9 +17,7 @@
package org.meshtastic.feature.firmware
import android.content.Context
-import android.net.Uri
import co.touchlab.kermit.Logger
-import dagger.hilt.android.qualifiers.ApplicationContext
import io.ktor.client.HttpClient
import io.ktor.client.request.get
import io.ktor.client.request.head
@@ -31,14 +29,15 @@ import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
+import org.koin.core.annotation.Single
+import org.meshtastic.core.common.util.CommonUri
+import org.meshtastic.core.common.util.toPlatformUri
import org.meshtastic.core.model.DeviceHardware
import java.io.File
-import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
-import javax.inject.Inject
private const val DOWNLOAD_BUFFER_SIZE = 8192
@@ -46,15 +45,11 @@ private const val DOWNLOAD_BUFFER_SIZE = 8192
* Helper class to handle file operations related to firmware updates, such as downloading, copying from URI, and
* extracting specific files from Zip archives.
*/
-class FirmwareFileHandler
-@Inject
-constructor(
- @ApplicationContext private val context: Context,
- private val client: HttpClient,
-) {
+@Single
+class AndroidFirmwareFileHandler(private val context: Context, private val client: HttpClient) : FirmwareFileHandler {
private val tempDir = File(context.cacheDir, "firmware_update")
- fun cleanupAllTemporaryFiles() {
+ override fun cleanupAllTemporaryFiles() {
runCatching {
if (tempDir.exists()) {
tempDir.deleteRecursively()
@@ -64,7 +59,7 @@ constructor(
.onFailure { e -> Logger.w(e) { "Failed to cleanup temp directory" } }
}
- suspend fun checkUrlExists(url: String): Boolean = withContext(Dispatchers.IO) {
+ override suspend fun checkUrlExists(url: String): Boolean = withContext(Dispatchers.IO) {
try {
client.head(url).status.isSuccess()
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
@@ -73,7 +68,7 @@ constructor(
}
}
- suspend fun downloadFile(url: String, fileName: String, onProgress: (Float) -> Unit): File? =
+ override suspend fun downloadFile(url: String, fileName: String, onProgress: (Float) -> Unit): String? =
withContext(Dispatchers.IO) {
val response =
try {
@@ -93,10 +88,10 @@ constructor(
if (!tempDir.exists()) tempDir.mkdirs()
- val targetFile = File(tempDir, fileName)
+ val targetFile = java.io.File(tempDir, fileName)
body.toInputStream().use { input ->
- FileOutputStream(targetFile).use { output ->
+ java.io.FileOutputStream(targetFile).use { output ->
val buffer = ByteArray(DOWNLOAD_BUFFER_SIZE)
var bytesRead: Int
var totalBytesRead = 0L
@@ -116,15 +111,16 @@ constructor(
}
}
}
- targetFile
+ targetFile.absolutePath
}
- suspend fun extractFirmware(
- zipFile: File,
+ override suspend fun extractFirmwareFromZip(
+ zipFilePath: String,
hardware: DeviceHardware,
fileExtension: String,
- preferredFilename: String? = null,
- ): File? = withContext(Dispatchers.IO) {
+ preferredFilename: String?,
+ ): String? = withContext(Dispatchers.IO) {
+ val zipFile = java.io.File(zipFilePath)
val target = hardware.platformioTarget.ifEmpty { hardware.hwModelSlug }
if (target.isEmpty() && preferredFilename == null) return@withContext null
@@ -153,21 +149,21 @@ constructor(
matchingEntries.add(entry to outFile)
if (preferredFilenameLower != null) {
- return@withContext outFile
+ return@withContext outFile.absolutePath
}
}
entry = zipInput.nextEntry
}
}
- matchingEntries.minByOrNull { it.first.name.length }?.second
+ matchingEntries.minByOrNull { it.first.name.length }?.second?.absolutePath
}
- suspend fun extractFirmware(
- uri: Uri,
+ override suspend fun extractFirmware(
+ uri: CommonUri,
hardware: DeviceHardware,
fileExtension: String,
- preferredFilename: String? = null,
- ): File? = withContext(Dispatchers.IO) {
+ preferredFilename: String?,
+ ): String? = withContext(Dispatchers.IO) {
val target = hardware.platformioTarget.ifEmpty { hardware.hwModelSlug }
if (target.isEmpty() && preferredFilename == null) return@withContext null
@@ -178,7 +174,8 @@ constructor(
if (!tempDir.exists()) tempDir.mkdirs()
try {
- val inputStream = context.contentResolver.openInputStream(uri) ?: return@withContext null
+ val platformUri = uri.toPlatformUri() as android.net.Uri
+ val inputStream = context.contentResolver.openInputStream(platformUri) ?: return@withContext null
ZipInputStream(inputStream).use { zipInput ->
var entry = zipInput.nextEntry
while (entry != null) {
@@ -198,7 +195,7 @@ constructor(
matchingEntries.add(entry to outFile)
if (preferredFilenameLower != null) {
- return@withContext outFile
+ return@withContext outFile.absolutePath
}
}
entry = zipInput.nextEntry
@@ -208,7 +205,17 @@ constructor(
Logger.w(e) { "Failed to extract firmware from URI" }
return@withContext null
}
- matchingEntries.minByOrNull { it.first.name.length }?.second
+ matchingEntries.minByOrNull { it.first.name.length }?.second?.absolutePath
+ }
+
+ override suspend fun getFileSize(path: String): Long = withContext(Dispatchers.IO) {
+ val file = File(path)
+ if (file.exists()) file.length() else 0L
+ }
+
+ override suspend fun deleteFile(path: String) = withContext(Dispatchers.IO) {
+ val file = File(path)
+ if (file.exists()) file.delete()
}
private fun isValidFirmwareFile(filename: String, target: String, fileExtension: String): Boolean {
@@ -218,22 +225,25 @@ constructor(
(regex.matches(filename) || filename.startsWith("$target-") || filename.startsWith("$target."))
}
- suspend fun copyFileToUri(sourceFile: File, destinationUri: Uri) = withContext(Dispatchers.IO) {
- val inputStream = FileInputStream(sourceFile)
- val outputStream =
- context.contentResolver.openOutputStream(destinationUri)
- ?: throw IOException("Cannot open content URI for writing")
+ override suspend fun copyFileToUri(sourcePath: String, destinationUri: CommonUri): Long =
+ withContext(Dispatchers.IO) {
+ val inputStream = java.io.FileInputStream(java.io.File(sourcePath))
+ val outputStream =
+ context.contentResolver.openOutputStream(destinationUri.toPlatformUri() as android.net.Uri)
+ ?: throw IOException("Cannot open content URI for writing")
- inputStream.use { input -> outputStream.use { output -> input.copyTo(output) } }
- }
+ inputStream.use { input -> outputStream.use { output -> input.copyTo(output) } }
+ }
- suspend fun copyUriToUri(sourceUri: Uri, destinationUri: Uri) = withContext(Dispatchers.IO) {
- val inputStream =
- context.contentResolver.openInputStream(sourceUri) ?: throw IOException("Cannot open source URI")
- val outputStream =
- context.contentResolver.openOutputStream(destinationUri)
- ?: throw IOException("Cannot open destination URI")
+ override suspend fun copyUriToUri(sourceUri: CommonUri, destinationUri: CommonUri): Long =
+ withContext(Dispatchers.IO) {
+ val inputStream =
+ context.contentResolver.openInputStream(sourceUri.toPlatformUri() as android.net.Uri)
+ ?: throw IOException("Cannot open source URI")
+ val outputStream =
+ context.contentResolver.openOutputStream(destinationUri.toPlatformUri() as android.net.Uri)
+ ?: throw IOException("Cannot open destination URI")
- inputStream.use { input -> outputStream.use { output -> input.copyTo(output) } }
- }
+ inputStream.use { input -> outputStream.use { output -> input.copyTo(output) } }
+ }
}
diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateManager.kt b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/AndroidFirmwareUpdateManager.kt
similarity index 91%
rename from feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateManager.kt
rename to feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/AndroidFirmwareUpdateManager.kt
index 16c5f5cfb..0d9cb38eb 100644
--- a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateManager.kt
+++ b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/AndroidFirmwareUpdateManager.kt
@@ -16,8 +16,9 @@
*/
package org.meshtastic.feature.firmware
-import android.net.Uri
import kotlinx.coroutines.flow.Flow
+import org.koin.core.annotation.Single
+import org.meshtastic.core.common.util.CommonUri
import org.meshtastic.core.database.entity.FirmwareRelease
import org.meshtastic.core.model.DeviceHardware
import org.meshtastic.core.repository.RadioPrefs
@@ -25,29 +26,24 @@ import org.meshtastic.core.repository.isBle
import org.meshtastic.core.repository.isSerial
import org.meshtastic.core.repository.isTcp
import org.meshtastic.feature.firmware.ota.Esp32OtaUpdateHandler
-import java.io.File
-import javax.inject.Inject
-import javax.inject.Singleton
/** Orchestrates the firmware update process by choosing the correct handler. */
-@Singleton
-class FirmwareUpdateManager
-@Inject
-constructor(
+@Single
+class AndroidFirmwareUpdateManager(
private val radioPrefs: RadioPrefs,
private val nordicDfuHandler: NordicDfuHandler,
private val usbUpdateHandler: UsbUpdateHandler,
private val esp32OtaUpdateHandler: Esp32OtaUpdateHandler,
-) {
+) : FirmwareUpdateManager {
/** Start the update process based on the current connection and hardware. */
- suspend fun startUpdate(
+ override suspend fun startUpdate(
release: FirmwareRelease,
hardware: DeviceHardware,
address: String,
updateState: (FirmwareUpdateState) -> Unit,
- firmwareUri: Uri? = null,
- ): File? {
+ firmwareUri: CommonUri?,
+ ): String? {
val handler = getHandler(hardware)
val target = getTarget(address)
@@ -60,7 +56,7 @@ constructor(
)
}
- fun dfuProgressFlow(): Flow = nordicDfuHandler.progressFlow()
+ override fun dfuProgressFlow(): Flow = nordicDfuHandler.progressFlow()
private fun getHandler(hardware: DeviceHardware): FirmwareUpdateHandler = when {
radioPrefs.isSerial() -> {
diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/UsbManager.kt b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/AndroidFirmwareUsbManager.kt
similarity index 88%
rename from feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/UsbManager.kt
rename to feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/AndroidFirmwareUsbManager.kt
index 9e8954280..0bf674f84 100644
--- a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/UsbManager.kt
+++ b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/AndroidFirmwareUsbManager.kt
@@ -23,18 +23,16 @@ import android.content.IntentFilter
import android.hardware.usb.UsbManager
import android.os.Build
import co.touchlab.kermit.Logger
-import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
-import javax.inject.Inject
-import javax.inject.Singleton
+import org.koin.core.annotation.Single
/** Manages USB-related interactions for firmware updates. */
-@Singleton
-class UsbManager @Inject constructor(@ApplicationContext private val context: Context) {
+@Single
+class AndroidFirmwareUsbManager(private val context: Context) : FirmwareUsbManager {
/** Observe when a USB device is detached. */
- fun deviceDetachFlow(): Flow = callbackFlow {
+ override fun deviceDetachFlow(): Flow = callbackFlow {
val receiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareDfuService.kt b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/FirmwareDfuService.kt
similarity index 86%
rename from feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareDfuService.kt
rename to feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/FirmwareDfuService.kt
index d23274478..79a5a48a0 100644
--- a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareDfuService.kt
+++ b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/FirmwareDfuService.kt
@@ -50,11 +50,13 @@ class FirmwareDfuService : DfuBaseService() {
}
override fun getNotificationTarget(): Class? = try {
- // Best effort to find the main activity
+ // Best effort to find the main activity dynamically
+ val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
+ val className = launchIntent?.component?.className ?: "org.meshtastic.app.MainActivity"
@Suppress("UNCHECKED_CAST")
- Class.forName("com.geeksville.mesh.MainActivity") as Class
- } catch (_: ClassNotFoundException) {
- null
+ Class.forName(className) as Class
+ } catch (_: Exception) {
+ Activity::class.java
}
override fun isDebug(): Boolean = isDebugFlag
diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareRetriever.kt b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/FirmwareRetriever.kt
similarity index 92%
rename from feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareRetriever.kt
rename to feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/FirmwareRetriever.kt
index a485c1957..6d9f83286 100644
--- a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareRetriever.kt
+++ b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/FirmwareRetriever.kt
@@ -17,18 +17,18 @@
package org.meshtastic.feature.firmware
import co.touchlab.kermit.Logger
+import org.koin.core.annotation.Single
import org.meshtastic.core.database.entity.FirmwareRelease
import org.meshtastic.core.model.DeviceHardware
-import java.io.File
-import javax.inject.Inject
/** Retrieves firmware files, either by direct download or by extracting from a release asset. */
-class FirmwareRetriever @Inject constructor(private val fileHandler: FirmwareFileHandler) {
+@Single
+class FirmwareRetriever(private val fileHandler: FirmwareFileHandler) {
suspend fun retrieveOtaFirmware(
release: FirmwareRelease,
hardware: DeviceHardware,
onProgress: (Float) -> Unit,
- ): File? = retrieve(
+ ): String? = retrieve(
release = release,
hardware = hardware,
onProgress = onProgress,
@@ -40,7 +40,7 @@ class FirmwareRetriever @Inject constructor(private val fileHandler: FirmwareFil
release: FirmwareRelease,
hardware: DeviceHardware,
onProgress: (Float) -> Unit,
- ): File? = retrieve(
+ ): String? = retrieve(
release = release,
hardware = hardware,
onProgress = onProgress,
@@ -52,7 +52,7 @@ class FirmwareRetriever @Inject constructor(private val fileHandler: FirmwareFil
release: FirmwareRelease,
hardware: DeviceHardware,
onProgress: (Float) -> Unit,
- ): File? {
+ ): String? {
val mcu = hardware.architecture.replace("-", "")
val otaFilename = "mt-$mcu-ota.bin"
retrieve(
@@ -84,7 +84,7 @@ class FirmwareRetriever @Inject constructor(private val fileHandler: FirmwareFil
fileSuffix: String,
internalFileExtension: String,
preferredFilename: String? = null,
- ): File? {
+ ): String? {
val version = release.id.removePrefix("v")
val target = hardware.platformioTarget.ifEmpty { hardware.hwModelSlug }
val filename = preferredFilename ?: "firmware-$target-$version$fileSuffix"
@@ -105,7 +105,7 @@ class FirmwareRetriever @Inject constructor(private val fileHandler: FirmwareFil
val zipUrl = getDeviceFirmwareUrl(release.zipUrl, hardware.architecture)
val downloadedZip = fileHandler.downloadFile(zipUrl, "firmware_release.zip", onProgress)
return downloadedZip?.let {
- fileHandler.extractFirmware(it, hardware, internalFileExtension, preferredFilename)
+ fileHandler.extractFirmwareFromZip(it, hardware, internalFileExtension, preferredFilename)
}
}
diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.kt b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.kt
similarity index 97%
rename from feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.kt
rename to feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.kt
index d00daacba..c3e986d7d 100644
--- a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.kt
+++ b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.kt
@@ -79,15 +79,14 @@ import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import androidx.navigation.NavController
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import coil3.request.crossfade
import com.mikepenz.markdown.m3.Markdown
import kotlinx.coroutines.delay
import org.jetbrains.compose.resources.stringResource
+import org.meshtastic.core.common.util.CommonUri
import org.meshtastic.core.database.entity.FirmwareRelease
import org.meshtastic.core.database.entity.FirmwareReleaseType
import org.meshtastic.core.model.DeviceHardware
@@ -153,11 +152,7 @@ private const val CYCLE_DELAY_MS = 4500L
@Composable
@Suppress("LongMethod")
-fun FirmwareUpdateScreen(
- navController: NavController,
- modifier: Modifier = Modifier,
- viewModel: FirmwareUpdateViewModel = hiltViewModel(),
-) {
+fun FirmwareUpdateScreen(onNavigateUp: () -> Unit, viewModel: FirmwareUpdateViewModel, modifier: Modifier = Modifier) {
val state by viewModel.state.collectAsStateWithLifecycle()
val selectedReleaseType by viewModel.selectedReleaseType.collectAsStateWithLifecycle()
val deviceHardware by viewModel.deviceHardware.collectAsStateWithLifecycle()
@@ -165,21 +160,19 @@ fun FirmwareUpdateScreen(
val selectedRelease by viewModel.selectedRelease.collectAsStateWithLifecycle()
var showExitConfirmation by remember { mutableStateOf(false) }
-
- val getFileLauncher =
+ val filePickerLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
- uri?.let { viewModel.startUpdateFromFile(it) }
+ uri?.let { viewModel.startUpdateFromFile(CommonUri(it)) }
}
- val saveFileLauncher =
+ val createDocumentLauncher =
rememberLauncherForActivityResult(
ActivityResultContracts.CreateDocument("application/octet-stream"),
) { uri: Uri? ->
- uri?.let { viewModel.saveDfuFile(it) }
+ uri?.let { viewModel.saveDfuFile(CommonUri(it)) }
}
-
val actions =
- remember(viewModel, navController, state) {
+ remember(viewModel, onNavigateUp, state) {
FirmwareUpdateActions(
onReleaseTypeSelect = viewModel::setReleaseType,
onStartUpdate = viewModel::startUpdate,
@@ -190,16 +183,16 @@ fun FirmwareUpdateScreen(
readyState.updateMethod is FirmwareUpdateMethod.Ble ||
readyState.updateMethod is FirmwareUpdateMethod.Wifi
) {
- getFileLauncher.launch("*/*")
+ filePickerLauncher.launch("*/*")
} else if (readyState.updateMethod is FirmwareUpdateMethod.Usb) {
- getFileLauncher.launch("*/*")
+ filePickerLauncher.launch("*/*")
}
}
},
- onSaveFile = { fileName -> saveFileLauncher.launch(fileName) },
+ onSaveFile = { fileName -> createDocumentLauncher.launch(fileName) },
onRetry = viewModel::checkForUpdates,
onCancel = { showExitConfirmation = true },
- onDone = { navController.navigateUp() },
+ onDone = { onNavigateUp() },
onDismissBootloaderWarning = viewModel::dismissBootloaderWarningForCurrentDevice,
)
}
@@ -217,7 +210,7 @@ fun FirmwareUpdateScreen(
onConfirm = {
showExitConfirmation = false
viewModel.cancelUpdate()
- navController.navigateUp()
+ onNavigateUp()
},
dismissText = stringResource(Res.string.back),
)
@@ -225,7 +218,7 @@ fun FirmwareUpdateScreen(
FirmwareUpdateScaffold(
modifier = modifier,
- navController = navController,
+ onNavigateUp = onNavigateUp,
state = state,
selectedReleaseType = selectedReleaseType,
actions = actions,
@@ -237,7 +230,7 @@ fun FirmwareUpdateScreen(
@Composable
private fun FirmwareUpdateScaffold(
- navController: NavController,
+ onNavigateUp: () -> Unit,
state: FirmwareUpdateState,
selectedReleaseType: FirmwareReleaseType,
actions: FirmwareUpdateActions,
@@ -252,7 +245,7 @@ private fun FirmwareUpdateScaffold(
CenterAlignedTopAppBar(
title = { Text(stringResource(Res.string.firmware_update_title)) },
navigationIcon = {
- IconButton(onClick = { navController.navigateUp() }) {
+ IconButton(onClick = { onNavigateUp() }) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(Res.string.back))
}
},
diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/NordicDfuHandler.kt b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/NordicDfuHandler.kt
similarity index 85%
rename from feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/NordicDfuHandler.kt
rename to feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/NordicDfuHandler.kt
index 72cd5ed5f..d9ae92624 100644
--- a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/NordicDfuHandler.kt
+++ b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/NordicDfuHandler.kt
@@ -17,10 +17,8 @@
package org.meshtastic.feature.firmware
import android.content.Context
-import android.net.Uri
import co.touchlab.kermit.Logger
import co.touchlab.kermit.Severity
-import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
@@ -31,6 +29,9 @@ import no.nordicsemi.android.dfu.DfuProgressListenerAdapter
import no.nordicsemi.android.dfu.DfuServiceInitiator
import no.nordicsemi.android.dfu.DfuServiceListenerHelper
import org.jetbrains.compose.resources.getString
+import org.koin.core.annotation.Single
+import org.meshtastic.core.common.util.CommonUri
+import org.meshtastic.core.common.util.toPlatformUri
import org.meshtastic.core.database.entity.FirmwareRelease
import org.meshtastic.core.model.DeviceHardware
import org.meshtastic.core.model.RadioController
@@ -39,8 +40,6 @@ import org.meshtastic.core.resources.firmware_update_downloading_percent
import org.meshtastic.core.resources.firmware_update_nordic_failed
import org.meshtastic.core.resources.firmware_update_not_found_in_release
import org.meshtastic.core.resources.firmware_update_starting_service
-import java.io.File
-import javax.inject.Inject
private const val SCAN_TIMEOUT = 5000L
private const val PACKETS_BEFORE_PRN = 8
@@ -48,11 +47,10 @@ private const val PERCENT_MAX = 100
private const val PREPARE_DATA_DELAY = 400L
/** Handles Over-the-Air (OTA) firmware updates for nRF52-based devices using the Nordic DFU library. */
-class NordicDfuHandler
-@Inject
-constructor(
+@Single
+class NordicDfuHandler(
private val firmwareRetriever: FirmwareRetriever,
- @ApplicationContext private val context: Context,
+ private val context: Context,
private val radioController: RadioController,
) : FirmwareUpdateHandler {
@@ -61,8 +59,8 @@ constructor(
hardware: DeviceHardware,
target: String, // Bluetooth address
updateState: (FirmwareUpdateState) -> Unit,
- firmwareUri: Uri?,
- ): File? =
+ firmwareUri: CommonUri?,
+ ): String? =
try {
val downloadingMsg =
getString(Res.string.firmware_update_downloading_percent, 0)
@@ -90,7 +88,7 @@ constructor(
updateState(FirmwareUpdateState.Error(errorMsg))
null
} else {
- initiateDfu(target, hardware, Uri.fromFile(firmwareFile), updateState)
+ initiateDfu(target, hardware, CommonUri.parse("file://$firmwareFile"), updateState)
firmwareFile
}
}
@@ -106,7 +104,7 @@ constructor(
private suspend fun initiateDfu(
address: String,
deviceHardware: DeviceHardware,
- firmwareUri: Uri,
+ firmwareUri: CommonUri,
updateState: (FirmwareUpdateState) -> Unit,
) {
val startingMsg = getString(Res.string.firmware_update_starting_service)
@@ -127,7 +125,7 @@ constructor(
.setPacketsReceiptNotificationsEnabled(true)
.setScanTimeout(SCAN_TIMEOUT)
.setUnsafeExperimentalButtonlessServiceInSecureDfuEnabled(true)
- .setZip(firmwareUri)
+ .setZip(firmwareUri.toPlatformUri() as android.net.Uri)
.start(context, FirmwareDfuService::class.java)
}
@@ -215,36 +213,3 @@ constructor(
}
}
}
-
-sealed interface DfuInternalState {
- val address: String
-
- data class Connecting(override val address: String) : DfuInternalState
-
- data class Connected(override val address: String) : DfuInternalState
-
- data class Starting(override val address: String) : DfuInternalState
-
- data class EnablingDfuMode(override val address: String) : DfuInternalState
-
- data class Progress(
- override val address: String,
- val percent: Int,
- val speed: Float,
- val avgSpeed: Float,
- val currentPart: Int,
- val partsTotal: Int,
- ) : DfuInternalState
-
- data class Validating(override val address: String) : DfuInternalState
-
- data class Disconnecting(override val address: String) : DfuInternalState
-
- data class Disconnected(override val address: String) : DfuInternalState
-
- data class Completed(override val address: String) : DfuInternalState
-
- data class Aborted(override val address: String) : DfuInternalState
-
- data class Error(override val address: String, val message: String?) : DfuInternalState
-}
diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/UsbUpdateHandler.kt b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/UsbUpdateHandler.kt
similarity index 95%
rename from feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/UsbUpdateHandler.kt
rename to feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/UsbUpdateHandler.kt
index 19534440c..50d1361fa 100644
--- a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/UsbUpdateHandler.kt
+++ b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/UsbUpdateHandler.kt
@@ -16,11 +16,12 @@
*/
package org.meshtastic.feature.firmware
-import android.net.Uri
import co.touchlab.kermit.Logger
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay
import org.jetbrains.compose.resources.getString
+import org.koin.core.annotation.Single
+import org.meshtastic.core.common.util.CommonUri
import org.meshtastic.core.database.entity.FirmwareRelease
import org.meshtastic.core.model.DeviceHardware
import org.meshtastic.core.model.RadioController
@@ -30,16 +31,13 @@ import org.meshtastic.core.resources.firmware_update_downloading_percent
import org.meshtastic.core.resources.firmware_update_rebooting
import org.meshtastic.core.resources.firmware_update_retrieval_failed
import org.meshtastic.core.resources.firmware_update_usb_failed
-import java.io.File
-import javax.inject.Inject
private const val REBOOT_DELAY = 5000L
private const val PERCENT_MAX = 100
/** Handles firmware updates via USB Mass Storage (UF2). */
-class UsbUpdateHandler
-@Inject
-constructor(
+@Single
+class UsbUpdateHandler(
private val firmwareRetriever: FirmwareRetriever,
private val radioController: RadioController,
private val nodeRepository: NodeRepository,
@@ -50,8 +48,8 @@ constructor(
hardware: DeviceHardware,
target: String, // Unused for USB
updateState: (FirmwareUpdateState) -> Unit,
- firmwareUri: Uri?,
- ): File? =
+ firmwareUri: CommonUri?,
+ ): String? =
try {
val downloadingMsg =
getString(Res.string.firmware_update_downloading_percent, 0)
@@ -91,7 +89,7 @@ constructor(
radioController.rebootToDfu(myNodeNum)
delay(REBOOT_DELAY)
- updateState(FirmwareUpdateState.AwaitingFileSave(firmwareFile, firmwareFile.name))
+ updateState(FirmwareUpdateState.AwaitingFileSave(firmwareFile, java.io.File(firmwareFile).name))
firmwareFile
}
}
diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransport.kt b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransport.kt
similarity index 100%
rename from feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransport.kt
rename to feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransport.kt
diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandler.kt b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandler.kt
similarity index 82%
rename from feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandler.kt
rename to feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandler.kt
index 890c23a3e..2f992b6f4 100644
--- a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandler.kt
+++ b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandler.kt
@@ -17,9 +17,7 @@
package org.meshtastic.feature.firmware.ota
import android.content.Context
-import android.net.Uri
import co.touchlab.kermit.Logger
-import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -27,9 +25,12 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jetbrains.compose.resources.getString
+import org.koin.core.annotation.Single
import org.meshtastic.core.ble.BleConnectionFactory
import org.meshtastic.core.ble.BleScanner
+import org.meshtastic.core.common.util.CommonUri
import org.meshtastic.core.common.util.nowMillis
+import org.meshtastic.core.common.util.toPlatformUri
import org.meshtastic.core.database.entity.FirmwareRelease
import org.meshtastic.core.model.DeviceHardware
import org.meshtastic.core.model.RadioController
@@ -38,10 +39,10 @@ import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.firmware_update_connecting_attempt
import org.meshtastic.core.resources.firmware_update_downloading_percent
import org.meshtastic.core.resources.firmware_update_erasing
+import org.meshtastic.core.resources.firmware_update_extracting
import org.meshtastic.core.resources.firmware_update_hash_rejected
-import org.meshtastic.core.resources.firmware_update_loading
+import org.meshtastic.core.resources.firmware_update_not_found_in_release
import org.meshtastic.core.resources.firmware_update_ota_failed
-import org.meshtastic.core.resources.firmware_update_retrieval_failed
import org.meshtastic.core.resources.firmware_update_starting_ota
import org.meshtastic.core.resources.firmware_update_uploading
import org.meshtastic.core.resources.firmware_update_waiting_reboot
@@ -49,8 +50,6 @@ import org.meshtastic.feature.firmware.FirmwareRetriever
import org.meshtastic.feature.firmware.FirmwareUpdateHandler
import org.meshtastic.feature.firmware.FirmwareUpdateState
import org.meshtastic.feature.firmware.ProgressState
-import java.io.File
-import javax.inject.Inject
private const val RETRY_DELAY = 2000L
private const val PERCENT_MAX = 100
@@ -68,15 +67,14 @@ private const val GATT_RELEASE_DELAY_MS = 1000L
* UnifiedOtaProtocol.
*/
@Suppress("TooManyFunctions")
-class Esp32OtaUpdateHandler
-@Inject
-constructor(
+@Single
+class Esp32OtaUpdateHandler(
private val firmwareRetriever: FirmwareRetriever,
private val radioController: RadioController,
private val nodeRepository: NodeRepository,
private val bleScanner: BleScanner,
private val bleConnectionFactory: BleConnectionFactory,
- @ApplicationContext private val context: Context,
+ private val context: Context,
) : FirmwareUpdateHandler {
/** Entry point for FirmwareUpdateHandler interface. Decides between BLE and WiFi based on target format. */
@@ -85,8 +83,8 @@ constructor(
hardware: DeviceHardware,
target: String,
updateState: (FirmwareUpdateState) -> Unit,
- firmwareUri: Uri?,
- ): File? = if (target.contains(":")) {
+ firmwareUri: CommonUri?,
+ ): String? = if (target.contains(":")) {
startBleUpdate(release, hardware, target, updateState, firmwareUri)
} else {
startWifiUpdate(release, hardware, target, updateState, firmwareUri)
@@ -97,8 +95,8 @@ constructor(
hardware: DeviceHardware,
address: String,
updateState: (FirmwareUpdateState) -> Unit,
- firmwareUri: Uri? = null,
- ): File? = performUpdate(
+ firmwareUri: CommonUri? = null,
+ ): String? = performUpdate(
release = release,
hardware = hardware,
updateState = updateState,
@@ -113,8 +111,8 @@ constructor(
hardware: DeviceHardware,
deviceIp: String,
updateState: (FirmwareUpdateState) -> Unit,
- firmwareUri: Uri? = null,
- ): File? = performUpdate(
+ firmwareUri: CommonUri? = null,
+ ): String? = performUpdate(
release = release,
hardware = hardware,
updateState = updateState,
@@ -128,18 +126,18 @@ constructor(
release: FirmwareRelease,
hardware: DeviceHardware,
updateState: (FirmwareUpdateState) -> Unit,
- firmwareUri: Uri?,
+ firmwareUri: CommonUri?,
transportFactory: () -> UnifiedOtaProtocol,
rebootMode: Int,
connectionAttempts: Int,
- ): File? = try {
+ ): String? = try {
withContext(Dispatchers.IO) {
// Step 1: Get firmware file
val firmwareFile =
obtainFirmwareFile(release, hardware, firmwareUri, updateState) ?: return@withContext null
// Step 2: Calculate Hash and Trigger Reboot
- val sha256Bytes = FirmwareHashUtil.calculateSha256Bytes(firmwareFile)
+ val sha256Bytes = FirmwareHashUtil.calculateSha256Bytes(java.io.File(firmwareFile))
val sha256Hash = FirmwareHashUtil.bytesToHex(sha256Bytes)
Logger.i { "ESP32 OTA: Firmware hash: $sha256Hash" }
triggerRebootOta(rebootMode, sha256Bytes)
@@ -180,11 +178,12 @@ constructor(
null
}
+ @Suppress("UnusedPrivateMember")
private suspend fun downloadFirmware(
release: FirmwareRelease,
hardware: DeviceHardware,
updateState: (FirmwareUpdateState) -> Unit,
- ): File? {
+ ): String? {
val downloadingMsg =
getString(Res.string.firmware_update_downloading_percent, 0).replace(Regex(":?\\s*%1\\\$d%?"), "").trim()
updateState(FirmwareUpdateState.Downloading(ProgressState(message = downloadingMsg, progress = 0f)))
@@ -198,12 +197,14 @@ constructor(
}
}
- private suspend fun getFirmwareFromUri(uri: Uri): File? = withContext(Dispatchers.IO) {
- val inputStream = context.contentResolver.openInputStream(uri) ?: return@withContext null
- val tempFile = File(context.cacheDir, "firmware_update/ota_firmware.bin")
+ private suspend fun getFirmwareFromUri(uri: CommonUri): String? = withContext(Dispatchers.IO) {
+ val inputStream =
+ context.contentResolver.openInputStream(uri.toPlatformUri() as android.net.Uri)
+ ?: return@withContext null
+ val tempFile = java.io.File(context.cacheDir, "firmware_update/ota_firmware.bin")
tempFile.parentFile?.mkdirs()
inputStream.use { input -> tempFile.outputStream().use { output -> input.copyTo(output) } }
- tempFile
+ tempFile.absolutePath
}
private fun triggerRebootOta(mode: Int, hash: ByteArray?) {
@@ -227,24 +228,37 @@ constructor(
private suspend fun obtainFirmwareFile(
release: FirmwareRelease,
hardware: DeviceHardware,
- firmwareUri: Uri?,
+ firmwareUri: CommonUri?,
updateState: (FirmwareUpdateState) -> Unit,
- ): File? {
- val firmwareFile =
- if (firmwareUri != null) {
- val loadingMsg = getString(Res.string.firmware_update_loading)
- updateState(FirmwareUpdateState.Processing(ProgressState(loadingMsg)))
- getFirmwareFromUri(firmwareUri)
- } else {
- downloadFirmware(release, hardware, updateState)
- }
+ ): String? {
+ val downloadingMsg =
+ getString(Res.string.firmware_update_downloading_percent, 0).replace(Regex(":?\\s*%1\\\$d%?"), "").trim()
- if (firmwareFile == null) {
- val retrievalFailedMsg = getString(Res.string.firmware_update_retrieval_failed)
- updateState(FirmwareUpdateState.Error(retrievalFailedMsg))
- return null
+ updateState(FirmwareUpdateState.Downloading(ProgressState(message = downloadingMsg, progress = 0f)))
+
+ return if (firmwareUri != null) {
+ val extractingMsg = getString(Res.string.firmware_update_extracting)
+ updateState(FirmwareUpdateState.Processing(ProgressState(message = extractingMsg)))
+ getFirmwareFromUri(firmwareUri)
+ } else {
+ val firmwareFile =
+ firmwareRetriever.retrieveEsp32Firmware(release, hardware) { progress ->
+ val percent = (progress * PERCENT_MAX).toInt()
+ updateState(
+ FirmwareUpdateState.Downloading(
+ ProgressState(message = downloadingMsg, progress = progress, details = "$percent%"),
+ ),
+ )
+ }
+
+ if (firmwareFile == null) {
+ val errorMsg = getString(Res.string.firmware_update_not_found_in_release, hardware.displayName)
+ updateState(FirmwareUpdateState.Error(errorMsg))
+ null
+ } else {
+ firmwareFile
+ }
}
- return firmwareFile
}
private suspend fun connectToDevice(
@@ -273,16 +287,17 @@ constructor(
@Suppress("LongMethod")
private suspend fun executeOtaSequence(
transport: UnifiedOtaProtocol,
- firmwareFile: File,
+ firmwareFile: String,
sha256Hash: String,
rebootMode: Int,
updateState: (FirmwareUpdateState) -> Unit,
) {
+ val file = java.io.File(firmwareFile)
// Step 5: Start OTA
val startingOtaMsg = getString(Res.string.firmware_update_starting_ota)
updateState(FirmwareUpdateState.Processing(ProgressState(startingOtaMsg)))
transport
- .startOta(sizeBytes = firmwareFile.length(), sha256Hash = sha256Hash) { status ->
+ .startOta(sizeBytes = file.length(), sha256Hash = sha256Hash) { status ->
when (status) {
OtaHandshakeStatus.Erasing -> {
val erasingMsg = getString(Res.string.firmware_update_erasing)
@@ -295,7 +310,7 @@ constructor(
// Step 6: Stream
val uploadingMsg = getString(Res.string.firmware_update_uploading)
updateState(FirmwareUpdateState.Updating(ProgressState(uploadingMsg, 0f)))
- val firmwareData = firmwareFile.readBytes()
+ val firmwareData = file.readBytes()
val chunkSize =
if (rebootMode == 1) {
BleOtaTransport.RECOMMENDED_CHUNK_SIZE
diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/ota/FirmwareHashUtil.kt b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/ota/FirmwareHashUtil.kt
similarity index 100%
rename from feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/ota/FirmwareHashUtil.kt
rename to feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/ota/FirmwareHashUtil.kt
diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/ota/UnifiedOtaProtocol.kt b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/ota/UnifiedOtaProtocol.kt
similarity index 100%
rename from feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/ota/UnifiedOtaProtocol.kt
rename to feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/ota/UnifiedOtaProtocol.kt
diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/ota/WifiOtaTransport.kt b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/ota/WifiOtaTransport.kt
similarity index 100%
rename from feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/ota/WifiOtaTransport.kt
rename to feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/ota/WifiOtaTransport.kt
diff --git a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/FirmwareRetrieverTest.kt b/feature/firmware/src/androidUnitTest/kotlin/org/meshtastic/feature/firmware/FirmwareRetrieverTest.kt
similarity index 100%
rename from feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/FirmwareRetrieverTest.kt
rename to feature/firmware/src/androidUnitTest/kotlin/org/meshtastic/feature/firmware/FirmwareRetrieverTest.kt
diff --git a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportErrorTest.kt b/feature/firmware/src/androidUnitTest/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportErrorTest.kt
similarity index 100%
rename from feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportErrorTest.kt
rename to feature/firmware/src/androidUnitTest/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportErrorTest.kt
diff --git a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportMtuTest.kt b/feature/firmware/src/androidUnitTest/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportMtuTest.kt
similarity index 100%
rename from feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportMtuTest.kt
rename to feature/firmware/src/androidUnitTest/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportMtuTest.kt
diff --git a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportNordicMockTest.kt b/feature/firmware/src/androidUnitTest/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportNordicMockTest.kt
similarity index 100%
rename from feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportNordicMockTest.kt
rename to feature/firmware/src/androidUnitTest/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportNordicMockTest.kt
diff --git a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportServiceDiscoveryTest.kt b/feature/firmware/src/androidUnitTest/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportServiceDiscoveryTest.kt
similarity index 100%
rename from feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportServiceDiscoveryTest.kt
rename to feature/firmware/src/androidUnitTest/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportServiceDiscoveryTest.kt
diff --git a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportTest.kt b/feature/firmware/src/androidUnitTest/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportTest.kt
similarity index 100%
rename from feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportTest.kt
rename to feature/firmware/src/androidUnitTest/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportTest.kt
diff --git a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandlerTest.kt b/feature/firmware/src/androidUnitTest/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandlerTest.kt
similarity index 100%
rename from feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandlerTest.kt
rename to feature/firmware/src/androidUnitTest/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandlerTest.kt
diff --git a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/UnifiedOtaProtocolTest.kt b/feature/firmware/src/androidUnitTest/kotlin/org/meshtastic/feature/firmware/ota/UnifiedOtaProtocolTest.kt
similarity index 100%
rename from feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/UnifiedOtaProtocolTest.kt
rename to feature/firmware/src/androidUnitTest/kotlin/org/meshtastic/feature/firmware/ota/UnifiedOtaProtocolTest.kt
diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/DfuInternalState.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/DfuInternalState.kt
new file mode 100644
index 000000000..a7253ba53
--- /dev/null
+++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/DfuInternalState.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 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 org.meshtastic.feature.firmware
+
+sealed interface DfuInternalState {
+ val address: String
+
+ data class Connecting(override val address: String) : DfuInternalState
+
+ data class Connected(override val address: String) : DfuInternalState
+
+ data class Starting(override val address: String) : DfuInternalState
+
+ data class EnablingDfuMode(override val address: String) : DfuInternalState
+
+ data class Progress(
+ override val address: String,
+ val percent: Int,
+ val speed: Float,
+ val avgSpeed: Float,
+ val currentPart: Int,
+ val partsTotal: Int,
+ ) : DfuInternalState
+
+ data class Validating(override val address: String) : DfuInternalState
+
+ data class Disconnecting(override val address: String) : DfuInternalState
+
+ data class Disconnected(override val address: String) : DfuInternalState
+
+ data class Completed(override val address: String) : DfuInternalState
+
+ data class Aborted(override val address: String) : DfuInternalState
+
+ data class Error(override val address: String, val message: String?) : DfuInternalState
+}
diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareFileHandler.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareFileHandler.kt
new file mode 100644
index 000000000..b746c1a8c
--- /dev/null
+++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareFileHandler.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 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 org.meshtastic.feature.firmware
+
+import org.meshtastic.core.common.util.CommonUri
+import org.meshtastic.core.model.DeviceHardware
+
+interface FirmwareFileHandler {
+ fun cleanupAllTemporaryFiles()
+
+ suspend fun checkUrlExists(url: String): Boolean
+
+ suspend fun downloadFile(url: String, fileName: String, onProgress: (Float) -> Unit): String?
+
+ suspend fun extractFirmware(
+ uri: CommonUri,
+ hardware: DeviceHardware,
+ fileExtension: String,
+ preferredFilename: String? = null,
+ ): String?
+
+ suspend fun extractFirmwareFromZip(
+ zipFilePath: String,
+ hardware: DeviceHardware,
+ fileExtension: String,
+ preferredFilename: String? = null,
+ ): String?
+
+ suspend fun getFileSize(path: String): Long
+
+ suspend fun deleteFile(path: String)
+
+ suspend fun copyFileToUri(sourcePath: String, destinationUri: CommonUri): Long
+
+ suspend fun copyUriToUri(sourceUri: CommonUri, destinationUri: CommonUri): Long
+}
diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateActions.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateActions.kt
similarity index 100%
rename from feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateActions.kt
rename to feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateActions.kt
diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateHandler.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateHandler.kt
similarity index 87%
rename from feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateHandler.kt
rename to feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateHandler.kt
index df5ce6e78..b2bce3696 100644
--- a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateHandler.kt
+++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateHandler.kt
@@ -16,10 +16,9 @@
*/
package org.meshtastic.feature.firmware
-import android.net.Uri
+import org.meshtastic.core.common.util.CommonUri
import org.meshtastic.core.database.entity.FirmwareRelease
import org.meshtastic.core.model.DeviceHardware
-import java.io.File
/** Common interface for all firmware update handlers (BLE DFU, ESP32 OTA, USB). */
interface FirmwareUpdateHandler {
@@ -31,13 +30,13 @@ interface FirmwareUpdateHandler {
* @param target The target identifier (e.g., Bluetooth address, IP address, or empty for USB)
* @param updateState Callback to report back state changes
* @param firmwareUri Optional URI for a local firmware file (bypasses download)
- * @return The downloaded/extracted firmware file, or null if it was a local file or update finished
+ * @return The downloaded/extracted firmware file path, or null if it was a local file or update finished
*/
suspend fun startUpdate(
release: FirmwareRelease,
hardware: DeviceHardware,
target: String,
updateState: (FirmwareUpdateState) -> Unit,
- firmwareUri: Uri? = null,
- ): File?
+ firmwareUri: CommonUri? = null,
+ ): String?
}
diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateManager.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateManager.kt
new file mode 100644
index 000000000..bbe804178
--- /dev/null
+++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateManager.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 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 org.meshtastic.feature.firmware
+
+import org.meshtastic.core.common.util.CommonUri
+import org.meshtastic.core.database.entity.FirmwareRelease
+import org.meshtastic.core.model.DeviceHardware
+
+interface FirmwareUpdateManager {
+ suspend fun startUpdate(
+ release: FirmwareRelease,
+ hardware: DeviceHardware,
+ address: String,
+ updateState: (FirmwareUpdateState) -> Unit,
+ firmwareUri: CommonUri? = null,
+ ): String?
+
+ fun dfuProgressFlow(): kotlinx.coroutines.flow.Flow
+}
diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateState.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateState.kt
similarity index 93%
rename from feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateState.kt
rename to feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateState.kt
index 3a3055391..48dc7cef5 100644
--- a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateState.kt
+++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateState.kt
@@ -16,10 +16,9 @@
*/
package org.meshtastic.feature.firmware
-import android.net.Uri
+import org.meshtastic.core.common.util.CommonUri
import org.meshtastic.core.database.entity.FirmwareRelease
import org.meshtastic.core.model.DeviceHardware
-import java.io.File
/**
* Represents the progress of a long-running firmware update task.
@@ -58,6 +57,6 @@ sealed interface FirmwareUpdateState {
data object Success : FirmwareUpdateState
- data class AwaitingFileSave(val uf2File: File?, val fileName: String, val sourceUri: Uri? = null) :
+ data class AwaitingFileSave(val uf2FilePath: String?, val fileName: String, val sourceUri: CommonUri? = null) :
FirmwareUpdateState
}
diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt
similarity index 96%
rename from feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt
rename to feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt
index 2f3b9e449..4ae8b6af6 100644
--- a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt
+++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt
@@ -16,11 +16,9 @@
*/
package org.meshtastic.feature.firmware
-import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
-import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -37,6 +35,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeoutOrNull
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.getString
+import org.meshtastic.core.common.util.CommonUri
import org.meshtastic.core.data.repository.FirmwareReleaseRepository
import org.meshtastic.core.database.entity.FirmwareRelease
import org.meshtastic.core.database.entity.FirmwareReleaseType
@@ -73,8 +72,6 @@ import org.meshtastic.core.resources.firmware_update_unknown_hardware
import org.meshtastic.core.resources.firmware_update_updating
import org.meshtastic.core.resources.firmware_update_validating
import org.meshtastic.core.resources.unknown
-import java.io.File
-import javax.inject.Inject
private const val DFU_RECONNECT_PREFIX = "x"
private const val PERCENT_MAX_VALUE = 100f
@@ -87,11 +84,8 @@ private const val MILLIS_PER_SECOND = 1000L
private val BLUETOOTH_ADDRESS_REGEX = Regex("([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}")
-@HiltViewModel
@Suppress("LongParameterList", "TooManyFunctions")
-class FirmwareUpdateViewModel
-@Inject
-constructor(
+open class FirmwareUpdateViewModel(
private val firmwareReleaseRepository: FirmwareReleaseRepository,
private val deviceHardwareRepository: DeviceHardwareRepository,
private val nodeRepository: NodeRepository,
@@ -99,7 +93,7 @@ constructor(
private val radioPrefs: RadioPrefs,
private val bootloaderWarningDataSource: BootloaderWarningDataSource,
private val firmwareUpdateManager: FirmwareUpdateManager,
- private val usbManager: UsbManager,
+ private val usbManager: FirmwareUsbManager,
private val fileHandler: FirmwareFileHandler,
) : ViewModel() {
@@ -121,7 +115,7 @@ constructor(
val currentFirmwareVersion = _currentFirmwareVersion.asStateFlow()
private var updateJob: Job? = null
- private var tempFirmwareFile: File? = null
+ private var tempFirmwareFile: String? = null
private var originalDeviceAddress: String? = null
init {
@@ -135,7 +129,7 @@ constructor(
override fun onCleared() {
super.onCleared()
- tempFirmwareFile = cleanupTemporaryFiles(fileHandler, tempFirmwareFile)
+ viewModelScope.launch { tempFirmwareFile = cleanupTemporaryFiles(fileHandler, tempFirmwareFile) }
}
fun setReleaseType(type: FirmwareReleaseType) {
@@ -251,9 +245,9 @@ constructor(
}
}
- fun saveDfuFile(uri: Uri) {
+ fun saveDfuFile(uri: CommonUri) {
val currentState = _state.value as? FirmwareUpdateState.AwaitingFileSave ?: return
- val firmwareFile = currentState.uf2File
+ val firmwareFile = currentState.uf2FilePath
val sourceUri = currentState.sourceUri
viewModelScope.launch {
@@ -284,7 +278,7 @@ constructor(
}
}
- fun startUpdateFromFile(uri: Uri) {
+ fun startUpdateFromFile(uri: CommonUri) {
val currentState = _state.value as? FirmwareUpdateState.Ready ?: return
if (currentState.updateMethod is FirmwareUpdateMethod.Ble && !isValidBluetoothAddress(currentState.address)) {
viewModelScope.launch {
@@ -305,7 +299,7 @@ constructor(
val extractedFile = fileHandler.extractFirmware(uri, currentState.deviceHardware, extension)
tempFirmwareFile = extractedFile
- val firmwareUri = if (extractedFile != null) Uri.fromFile(extractedFile) else uri
+ val firmwareUri = if (extractedFile != null) CommonUri.parse("file://$extractedFile") else uri
tempFirmwareFile =
firmwareUpdateManager.startUpdate(
@@ -385,7 +379,7 @@ constructor(
}
}
- private fun handleDfuProgress(dfuState: DfuInternalState.Progress) {
+ private suspend fun handleDfuProgress(dfuState: DfuInternalState.Progress) {
val progress = dfuState.percent / PERCENT_MAX_VALUE
val percentText = "${dfuState.percent}%"
@@ -394,7 +388,7 @@ constructor(
val speedKib = speedBytesPerSec / KIB_DIVISOR
// Calculate ETA
- val totalBytes = tempFirmwareFile?.length() ?: 0L
+ val totalBytes = tempFirmwareFile?.let { fileHandler.getFileSize(it) } ?: 0L
val etaText =
if (totalBytes > 0 && speedBytesPerSec > 0 && dfuState.percent > 0) {
val remainingBytes = totalBytes * (1f - progress)
@@ -483,9 +477,9 @@ constructor(
}
}
-private fun cleanupTemporaryFiles(fileHandler: FirmwareFileHandler, tempFirmwareFile: File?): File? {
+private suspend fun cleanupTemporaryFiles(fileHandler: FirmwareFileHandler, tempFirmwareFile: String?): String? {
runCatching {
- tempFirmwareFile?.takeIf { it.exists() }?.delete()
+ tempFirmwareFile?.let { fileHandler.deleteFile(it) }
fileHandler.cleanupAllTemporaryFiles()
}
.onFailure { e -> Logger.w(e) { "Failed to cleanup temp files" } }
diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUsbManager.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUsbManager.kt
new file mode 100644
index 000000000..d102ed4e4
--- /dev/null
+++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUsbManager.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 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 org.meshtastic.feature.firmware
+
+import kotlinx.coroutines.flow.Flow
+
+interface FirmwareUsbManager {
+ fun deviceDetachFlow(): Flow
+}
diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/di/FeatureFirmwareModule.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/di/FeatureFirmwareModule.kt
new file mode 100644
index 000000000..fbb78ffd9
--- /dev/null
+++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/di/FeatureFirmwareModule.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 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 org.meshtastic.feature.firmware.di
+
+import org.koin.core.annotation.ComponentScan
+import org.koin.core.annotation.Module
+
+@Module
+@ComponentScan("org.meshtastic.feature.firmware")
+class FeatureFirmwareModule
diff --git a/feature/intro/build.gradle.kts b/feature/intro/build.gradle.kts
index bf7667a61..f3f63c7ea 100644
--- a/feature/intro/build.gradle.kts
+++ b/feature/intro/build.gradle.kts
@@ -19,7 +19,7 @@ plugins {
alias(libs.plugins.meshtastic.kmp.library)
alias(libs.plugins.meshtastic.kmp.library.compose)
alias(libs.plugins.meshtastic.kotlinx.serialization)
- alias(libs.plugins.devtools.ksp)
+ alias(libs.plugins.meshtastic.koin)
}
kotlin {
@@ -39,13 +39,12 @@ kotlin {
implementation(projects.core.resources)
implementation(libs.androidx.lifecycle.viewmodel.compose)
+ implementation(libs.koin.compose.viewmodel)
implementation(libs.androidx.navigation3.runtime)
- implementation(libs.javax.inject)
}
androidMain.dependencies {
implementation(project.dependencies.platform(libs.androidx.compose.bom))
- implementation(libs.androidx.hilt.lifecycle.viewmodel.compose)
implementation(libs.accompanist.permissions)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.compose.material3)
@@ -53,7 +52,6 @@ kotlin {
implementation(libs.androidx.compose.ui.text)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.navigation3.ui)
- implementation(libs.hilt.android)
}
androidUnitTest.dependencies {
@@ -67,8 +65,3 @@ kotlin {
}
}
}
-
-dependencies {
- add("kspAndroid", libs.androidx.hilt.compiler)
- add("kspAndroid", libs.hilt.compiler)
-}
diff --git a/feature/intro/src/commonMain/kotlin/org/meshtastic/feature/intro/di/FeatureIntroModule.kt b/feature/intro/src/commonMain/kotlin/org/meshtastic/feature/intro/di/FeatureIntroModule.kt
new file mode 100644
index 000000000..4d15389be
--- /dev/null
+++ b/feature/intro/src/commonMain/kotlin/org/meshtastic/feature/intro/di/FeatureIntroModule.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 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 org.meshtastic.feature.intro.di
+
+import org.koin.core.annotation.ComponentScan
+import org.koin.core.annotation.Module
+
+@Module
+@ComponentScan("org.meshtastic.feature.intro")
+class FeatureIntroModule
diff --git a/feature/map/build.gradle.kts b/feature/map/build.gradle.kts
index d701a243b..a03257bcc 100644
--- a/feature/map/build.gradle.kts
+++ b/feature/map/build.gradle.kts
@@ -18,7 +18,7 @@ plugins {
alias(libs.plugins.meshtastic.kmp.library)
alias(libs.plugins.meshtastic.kmp.library.compose)
alias(libs.plugins.meshtastic.kotlinx.serialization)
- alias(libs.plugins.devtools.ksp)
+ alias(libs.plugins.meshtastic.koin)
}
kotlin {
@@ -45,12 +45,11 @@ kotlin {
implementation(projects.core.di)
implementation(libs.androidx.lifecycle.viewmodel.compose)
- implementation(libs.javax.inject)
+ implementation(libs.koin.compose.viewmodel)
}
androidMain.dependencies {
implementation(project.dependencies.platform(libs.androidx.compose.bom))
- implementation(libs.androidx.hilt.lifecycle.viewmodel.compose)
implementation(libs.androidx.datastore)
implementation(libs.androidx.datastore.preferences)
implementation(libs.accompanist.permissions)
@@ -68,7 +67,6 @@ kotlin {
implementation(libs.androidx.savedstate.ktx)
implementation(libs.material)
implementation(libs.kermit)
- implementation(libs.hilt.android)
}
androidUnitTest.dependencies {
@@ -81,8 +79,3 @@ kotlin {
}
}
}
-
-dependencies {
- add("kspAndroid", libs.androidx.hilt.compiler)
- add("kspAndroid", libs.hilt.compiler)
-}
diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/SharedMapViewModel.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/SharedMapViewModel.kt
index df3787a31..7443b2e6d 100644
--- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/SharedMapViewModel.kt
+++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/SharedMapViewModel.kt
@@ -16,15 +16,14 @@
*/
package org.meshtastic.feature.map
+import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.repository.MapPrefs
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.PacketRepository
-import javax.inject.Inject
-open class SharedMapViewModel
-@Inject
-constructor(
+@KoinViewModel
+open class SharedMapViewModel(
mapPrefs: MapPrefs,
nodeRepository: NodeRepository,
packetRepository: PacketRepository,
diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/di/FeatureMapModule.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/di/FeatureMapModule.kt
new file mode 100644
index 000000000..a6ff74b17
--- /dev/null
+++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/di/FeatureMapModule.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 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 org.meshtastic.feature.map.di
+
+import org.koin.core.annotation.ComponentScan
+import org.koin.core.annotation.Module
+
+@Module
+@ComponentScan("org.meshtastic.feature.map")
+class FeatureMapModule
diff --git a/feature/messaging/build.gradle.kts b/feature/messaging/build.gradle.kts
index 481737827..de7ea9d28 100644
--- a/feature/messaging/build.gradle.kts
+++ b/feature/messaging/build.gradle.kts
@@ -18,7 +18,7 @@
plugins {
alias(libs.plugins.meshtastic.kmp.library)
alias(libs.plugins.meshtastic.kmp.library.compose)
- alias(libs.plugins.devtools.ksp)
+ alias(libs.plugins.meshtastic.koin)
}
kotlin {
@@ -44,13 +44,12 @@ kotlin {
implementation(projects.core.ui)
implementation(libs.androidx.lifecycle.viewmodel.compose)
+ implementation(libs.koin.compose.viewmodel)
implementation(libs.kermit)
- implementation(libs.javax.inject)
}
androidMain.dependencies {
implementation(project.dependencies.platform(libs.androidx.compose.bom))
- implementation(libs.androidx.hilt.lifecycle.viewmodel.compose)
implementation(libs.accompanist.permissions)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.compose.material3)
@@ -64,8 +63,6 @@ kotlin {
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.paging.compose)
implementation(libs.androidx.work.runtime.ktx)
- implementation(libs.androidx.hilt.work)
- implementation(libs.hilt.android)
}
commonTest.dependencies {
@@ -82,8 +79,3 @@ kotlin {
}
}
}
-
-dependencies {
- add("kspAndroid", libs.androidx.hilt.compiler)
- add("kspAndroid", libs.hilt.compiler)
-}
diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/di/FeatureMessagingModule.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/di/FeatureMessagingModule.kt
new file mode 100644
index 000000000..bbb7679f2
--- /dev/null
+++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/di/FeatureMessagingModule.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 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 org.meshtastic.feature.messaging.di
+
+import org.koin.core.annotation.ComponentScan
+import org.koin.core.annotation.Module
+
+@Module
+@ComponentScan("org.meshtastic.feature.messaging")
+class FeatureMessagingModule
diff --git a/feature/node/build.gradle.kts b/feature/node/build.gradle.kts
index de857e9d9..e875ce3c1 100644
--- a/feature/node/build.gradle.kts
+++ b/feature/node/build.gradle.kts
@@ -14,60 +14,82 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-import com.android.build.api.dsl.LibraryExtension
plugins {
- alias(libs.plugins.meshtastic.android.library)
- alias(libs.plugins.meshtastic.android.library.flavors)
- alias(libs.plugins.meshtastic.android.library.compose)
- alias(libs.plugins.meshtastic.hilt)
+ alias(libs.plugins.meshtastic.kmp.library)
+ alias(libs.plugins.meshtastic.kmp.library.compose)
+ alias(libs.plugins.meshtastic.kotlinx.serialization)
+ alias(libs.plugins.meshtastic.koin)
}
-configure {
- namespace = "org.meshtastic.feature.node"
+kotlin {
+ @Suppress("UnstableApiUsage")
+ android {
+ namespace = "org.meshtastic.feature.node"
+ androidResources.enable = false
+ withHostTest { isIncludeAndroidResources = true }
+ }
- defaultConfig { manifestPlaceholders["MAPS_API_KEY"] = "DEBUG_KEY" }
+ sourceSets {
+ commonMain.dependencies {
+ implementation(projects.core.common)
+ implementation(projects.core.data)
+ implementation(projects.core.database)
+ implementation(projects.core.datastore)
+ implementation(projects.core.domain)
+ implementation(projects.core.model)
+ implementation(projects.core.navigation)
+ implementation(projects.core.proto)
+ implementation(projects.core.repository)
+ implementation(projects.core.resources)
+ implementation(projects.core.service)
+ implementation(projects.core.ui)
+ implementation(projects.core.di)
+ implementation(projects.feature.map)
- testOptions { unitTests { isIncludeAndroidResources = true } }
-}
-
-dependencies {
- implementation(projects.core.common)
- implementation(projects.core.data)
- implementation(projects.core.database)
- implementation(projects.core.datastore)
- implementation(projects.core.di)
- implementation(projects.core.model)
- implementation(projects.core.proto)
- implementation(projects.core.service)
- implementation(projects.core.resources)
- implementation(projects.core.ui)
- implementation(projects.core.navigation)
- implementation(projects.feature.map)
-
- implementation(libs.androidx.activity.compose)
- implementation(libs.androidx.compose.material.iconsExtended)
- implementation(libs.androidx.compose.material3)
- implementation(libs.androidx.compose.ui.text)
- implementation(libs.androidx.compose.ui.tooling.preview)
- implementation(libs.androidx.navigation.common)
- implementation(libs.androidx.lifecycle.viewmodel.compose)
- implementation(libs.kermit)
- implementation(libs.coil)
- implementation(libs.markdown.renderer.android)
- implementation(libs.markdown.renderer.m3)
- implementation(libs.markdown.renderer)
- implementation(libs.vico.compose)
- implementation(libs.vico.compose.m2)
- implementation(libs.vico.compose.m3)
-
- googleImplementation(libs.location.services)
- googleImplementation(libs.maps.compose)
- testImplementation(libs.junit)
- testImplementation(libs.mockk)
- testImplementation(libs.kotlinx.coroutines.test)
- testImplementation(libs.androidx.compose.ui.test.junit4)
- testImplementation(libs.androidx.test.ext.junit)
- testImplementation(libs.robolectric)
- debugImplementation(libs.androidx.compose.ui.test.manifest)
+ implementation(libs.androidx.lifecycle.viewmodel.compose)
+ implementation(libs.koin.compose.viewmodel)
+ implementation(libs.kermit)
+ implementation(libs.kotlinx.collections.immutable)
+ }
+
+ androidMain.dependencies {
+ implementation(project.dependencies.platform(libs.androidx.compose.bom))
+ implementation(libs.accompanist.permissions)
+ implementation(libs.androidx.activity.compose)
+ implementation(libs.androidx.appcompat)
+ implementation(libs.androidx.compose.material.iconsExtended)
+ implementation(libs.androidx.compose.material3)
+ implementation(libs.androidx.compose.ui.text)
+ implementation(libs.androidx.compose.ui.tooling.preview)
+ implementation(libs.androidx.navigation.common)
+ implementation(libs.coil)
+ implementation(libs.markdown.renderer.android)
+ implementation(libs.markdown.renderer.m3)
+ implementation(libs.markdown.renderer)
+ implementation(libs.vico.compose)
+ implementation(libs.vico.compose.m2)
+ implementation(libs.vico.compose.m3)
+ implementation(libs.nordic.common.core)
+ implementation(libs.nordic.common.permissions.ble)
+
+ // These were in googleImplementation, but KMP with android-kotlin-multiplatform-library
+ // handles flavors differently. For now, we put them in androidMain if they are needed.
+ // In a real KMP flavored module, we'd use different source sets.
+ // But Priority 4b suggests Option A: extract flavored stuff to app module.
+ // So InlineMap will move to app module soon.
+ implementation(libs.location.services)
+ implementation(libs.maps.compose)
+ }
+
+ androidUnitTest.dependencies {
+ implementation(libs.junit)
+ implementation(libs.mockk)
+ implementation(libs.robolectric)
+ implementation(libs.turbine)
+ implementation(libs.kotlinx.coroutines.test)
+ implementation(libs.androidx.compose.ui.test.junit4)
+ implementation(libs.androidx.test.ext.junit)
+ }
+ }
}
diff --git a/feature/node/detekt-baseline.xml b/feature/node/detekt-baseline.xml
index 2465cc012..c71bc233d 100644
--- a/feature/node/detekt-baseline.xml
+++ b/feature/node/detekt-baseline.xml
@@ -5,8 +5,8 @@
CyclomaticComplexMethod:CompassViewModel.kt$CompassViewModel$@Suppress("ReturnCount") private fun calculatePositionalAccuracyMeters(): Float?
CyclomaticComplexMethod:NodeDetailActions.kt$NodeDetailActions$fun handleNodeMenuAction(scope: CoroutineScope, action: NodeMenuAction)
CyclomaticComplexMethod:NodeDetailViewModel.kt$NodeDetailViewModel$fun handleNodeMenuAction(action: NodeMenuAction)
- MagicNumber:MetricsViewModel.kt$MetricsViewModel$1000L
- MagicNumber:MetricsViewModel.kt$MetricsViewModel$1e-5
- MagicNumber:MetricsViewModel.kt$MetricsViewModel$1e-7
+ MagicNumber:CompassViewModel.kt$CompassViewModel$180.0
+ TooGenericExceptionCaught:MetricsViewModel.kt$MetricsViewModel$e: Exception
+ TooGenericExceptionCaught:NodeManagementActions.kt$NodeManagementActions$ex: Exception
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/compass/CompassHeadingProvider.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/compass/AndroidCompassHeadingProvider.kt
similarity index 86%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/compass/CompassHeadingProvider.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/compass/AndroidCompassHeadingProvider.kt
index 5bbda223a..416abc37c 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/compass/CompassHeadingProvider.kt
+++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/compass/AndroidCompassHeadingProvider.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025 Meshtastic LLC
+ * 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
@@ -14,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-
package org.meshtastic.feature.node.compass
import android.content.Context
@@ -22,29 +21,19 @@ import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
-import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
-import javax.inject.Inject
+import org.koin.core.annotation.Single
private const val ROTATION_MATRIX_SIZE = 9
private const val ORIENTATION_SIZE = 3
private const val FULL_CIRCLE_DEGREES = 360f
-data class HeadingState(
- val heading: Float? = null, // 0..360 degrees
- val hasSensor: Boolean = true,
- val accuracy: Int = SensorManager.SENSOR_STATUS_ACCURACY_MEDIUM,
-)
+@Single
+class AndroidCompassHeadingProvider(private val context: Context) : CompassHeadingProvider {
-class CompassHeadingProvider @Inject constructor(@ApplicationContext private val context: Context) {
-
- /**
- * Emits compass heading in degrees (magnetic). Callers can correct for true north using the latest location data
- * when available.
- */
- fun headingUpdates(): Flow = callbackFlow {
+ override fun headingUpdates(): Flow = callbackFlow {
val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as? SensorManager
if (sensorManager == null) {
trySend(HeadingState(hasSensor = false))
@@ -93,7 +82,7 @@ class CompassHeadingProvider @Inject constructor(@ApplicationContext private val
}
SensorManager.getOrientation(rotationMatrix, orientation)
- var azimuth = Math.toDegrees(orientation[0].toDouble()).toFloat()
+ val azimuth = Math.toDegrees(orientation[0].toDouble()).toFloat()
val heading = (azimuth + FULL_CIRCLE_DEGREES) % FULL_CIRCLE_DEGREES
trySend(HeadingState(heading = heading, hasSensor = true, accuracy = event.accuracy))
diff --git a/app/src/androidTest/kotlin/org/meshtastic/app/TestRunner.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/compass/AndroidMagneticFieldProvider.kt
similarity index 59%
rename from app/src/androidTest/kotlin/org/meshtastic/app/TestRunner.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/compass/AndroidMagneticFieldProvider.kt
index 7a5f389ae..9cdac1e2d 100644
--- a/app/src/androidTest/kotlin/org/meshtastic/app/TestRunner.kt
+++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/compass/AndroidMagneticFieldProvider.kt
@@ -14,15 +14,15 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package org.meshtastic.app
+package org.meshtastic.feature.node.compass
-import android.app.Application
-import android.content.Context
-import androidx.test.runner.AndroidJUnitRunner
-import dagger.hilt.android.testing.HiltTestApplication
+import android.hardware.GeomagneticField
+import org.koin.core.annotation.Single
-@Suppress("unused")
-class TestRunner : AndroidJUnitRunner() {
- override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application =
- super.newApplication(cl, HiltTestApplication::class.java.name, context)
+@Single
+class AndroidMagneticFieldProvider : MagneticFieldProvider {
+ override fun getDeclination(latitude: Double, longitude: Double, altitude: Double, timeMillis: Long): Float {
+ val geomagneticField = GeomagneticField(latitude.toFloat(), longitude.toFloat(), altitude.toFloat(), timeMillis)
+ return geomagneticField.declination
+ }
}
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/compass/PhoneLocationProvider.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/compass/AndroidPhoneLocationProvider.kt
similarity index 84%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/compass/PhoneLocationProvider.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/compass/AndroidPhoneLocationProvider.kt
index ade08492e..48241dd12 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/compass/PhoneLocationProvider.kt
+++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/compass/AndroidPhoneLocationProvider.kt
@@ -25,31 +25,18 @@ import androidx.core.content.ContextCompat
import androidx.core.location.LocationListenerCompat
import androidx.core.location.LocationManagerCompat
import androidx.core.location.LocationRequestCompat
-import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flowOn
+import org.koin.core.annotation.Single
import org.meshtastic.core.di.CoroutineDispatchers
-import javax.inject.Inject
-data class PhoneLocationState(
- val permissionGranted: Boolean,
- val providerEnabled: Boolean,
- val location: Location? = null,
-) {
- val hasFix: Boolean
- get() = location != null
-}
+@Single
+class AndroidPhoneLocationProvider(private val context: Context, private val dispatchers: CoroutineDispatchers) :
+ PhoneLocationProvider {
-class PhoneLocationProvider
-@Inject
-constructor(
- @ApplicationContext private val context: Context,
- private val dispatchers: CoroutineDispatchers,
-) {
- // Streams phone location (and permission/provider state) so the compass stays gated on real fixes.
- fun locationUpdates(): Flow = callbackFlow {
+ override fun locationUpdates(): Flow = callbackFlow {
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as? LocationManager
if (locationManager == null) {
trySend(PhoneLocationState(permissionGranted = false, providerEnabled = false))
@@ -59,7 +46,7 @@ constructor(
if (!hasLocationPermission()) {
trySend(PhoneLocationState(permissionGranted = false, providerEnabled = false))
- close() // Just closing it off, like how I'll close my legs around your waist
+ close()
return@callbackFlow
}
@@ -70,7 +57,7 @@ constructor(
PhoneLocationState(
permissionGranted = true,
providerEnabled = LocationManagerCompat.isLocationEnabled(locationManager),
- location = lastLocation,
+ location = lastLocation?.toPhoneLocation(),
),
)
}
@@ -96,7 +83,6 @@ constructor(
val providers = listOf(LocationManager.GPS_PROVIDER, LocationManager.NETWORK_PROVIDER)
try {
- // Get initial fix if available
lastLocation =
providers
.mapNotNull { provider -> locationManager.getLastKnownLocation(provider) }
@@ -131,6 +117,9 @@ constructor(
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) ==
android.content.pm.PackageManager.PERMISSION_GRANTED
+ private fun Location.toPhoneLocation() =
+ PhoneLocation(latitude = latitude, longitude = longitude, altitude = altitude, timeMillis = time)
+
companion object {
private const val MIN_UPDATE_INTERVAL_MS = 1_000L
}
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt
similarity index 100%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/ChannelInfo.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/ChannelInfo.kt
similarity index 100%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/ChannelInfo.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/ChannelInfo.kt
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/CompassBottomSheet.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/CompassBottomSheet.kt
similarity index 100%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/CompassBottomSheet.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/CompassBottomSheet.kt
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/CooldownOutlinedIconButton.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/CooldownOutlinedIconButton.kt
similarity index 100%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/CooldownOutlinedIconButton.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/CooldownOutlinedIconButton.kt
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/DeviceActions.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/DeviceActions.kt
similarity index 100%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/DeviceActions.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/DeviceActions.kt
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/DeviceDetailsSection.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/DeviceDetailsSection.kt
similarity index 100%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/DeviceDetailsSection.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/DeviceDetailsSection.kt
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/DistanceInfo.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/DistanceInfo.kt
similarity index 100%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/DistanceInfo.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/DistanceInfo.kt
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/ElevationInfo.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/ElevationInfo.kt
similarity index 100%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/ElevationInfo.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/ElevationInfo.kt
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/EnvironmentMetrics.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/EnvironmentMetrics.kt
similarity index 100%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/EnvironmentMetrics.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/EnvironmentMetrics.kt
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/FirmwareReleaseSheetContent.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/FirmwareReleaseSheetContent.kt
similarity index 100%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/FirmwareReleaseSheetContent.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/FirmwareReleaseSheetContent.kt
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/HopsInfo.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/HopsInfo.kt
similarity index 100%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/HopsInfo.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/HopsInfo.kt
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/IconInfo.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/IconInfo.kt
similarity index 100%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/IconInfo.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/IconInfo.kt
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/InfoCard.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/InfoCard.kt
similarity index 100%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/InfoCard.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/InfoCard.kt
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/InfoCardPreview.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/InfoCardPreview.kt
similarity index 98%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/InfoCardPreview.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/InfoCardPreview.kt
index f7d46a939..1eb5a75b1 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/InfoCardPreview.kt
+++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/InfoCardPreview.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025 Meshtastic LLC
+ * 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
@@ -14,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-
package org.meshtastic.feature.node.component
import androidx.compose.material.icons.Icons
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/LastHeardInfo.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/LastHeardInfo.kt
similarity index 97%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/LastHeardInfo.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/LastHeardInfo.kt
index 8821065a0..5bdf6b125 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/LastHeardInfo.kt
+++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/LastHeardInfo.kt
@@ -20,7 +20,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.PreviewLightDark
import org.jetbrains.compose.resources.stringResource
import org.jetbrains.compose.resources.vectorResource
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/LinkedCoordinatesItem.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/LinkedCoordinatesItem.kt
similarity index 100%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/LinkedCoordinatesItem.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/LinkedCoordinatesItem.kt
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeDetailComponents.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponents.kt
similarity index 100%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeDetailComponents.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponents.kt
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt
similarity index 100%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeFilterTextField.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/NodeFilterTextField.kt
similarity index 100%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeFilterTextField.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/NodeFilterTextField.kt
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeItem.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/NodeItem.kt
similarity index 100%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeItem.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/NodeItem.kt
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeStatusIcons.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/NodeStatusIcons.kt
similarity index 100%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeStatusIcons.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/NodeStatusIcons.kt
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NotesSection.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/NotesSection.kt
similarity index 100%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NotesSection.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/NotesSection.kt
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/PositionSection.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/PositionSection.kt
similarity index 97%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/PositionSection.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/PositionSection.kt
index f4e3bb454..57c7980df 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/PositionSection.kt
+++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/PositionSection.kt
@@ -52,6 +52,7 @@ import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.exchange_position
import org.meshtastic.core.resources.open_compass
import org.meshtastic.core.resources.position
+import org.meshtastic.core.ui.util.LocalInlineMapProvider
import org.meshtastic.feature.node.model.LogsType
import org.meshtastic.feature.node.model.MetricsState
import org.meshtastic.feature.node.model.NodeDetailAction
@@ -59,6 +60,7 @@ import org.meshtastic.proto.Config
private const val EXCHANGE_BUTTON_WEIGHT = 1.1f
private const val COMPASS_BUTTON_WEIGHT = 0.9f
+private const val MAP_HEIGHT_DP = 200
/**
* Displays node position details, last update time, distance, and related actions like requesting position and
@@ -126,8 +128,8 @@ fun PositionSection(
@Composable
private fun PositionMap(node: Node, distance: String?) {
Box(modifier = Modifier.padding(vertical = 4.dp)) {
- Surface(shape = MaterialTheme.shapes.large, modifier = Modifier.fillMaxWidth().height(200.dp)) {
- InlineMap(node = node, Modifier.fillMaxSize())
+ Surface(shape = MaterialTheme.shapes.large, modifier = Modifier.fillMaxWidth().height(MAP_HEIGHT_DP.dp)) {
+ LocalInlineMapProvider.current(node, Modifier.fillMaxSize())
}
if (distance != null && distance.isNotEmpty()) {
Surface(
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/PowerMetrics.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/PowerMetrics.kt
similarity index 100%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/PowerMetrics.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/PowerMetrics.kt
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/SatelliteCountInfo.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/SatelliteCountInfo.kt
similarity index 100%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/SatelliteCountInfo.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/SatelliteCountInfo.kt
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/TelemetricActionsSection.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/TelemetricActionsSection.kt
similarity index 100%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/TelemetricActionsSection.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/TelemetricActionsSection.kt
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/TelemetryInfo.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/TelemetryInfo.kt
similarity index 100%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/TelemetryInfo.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/TelemetryInfo.kt
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailActions.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailActions.kt
similarity index 97%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailActions.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailActions.kt
index c43829787..1dc5d2905 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailActions.kt
+++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailActions.kt
@@ -17,15 +17,13 @@
package org.meshtastic.feature.node.detail
import kotlinx.coroutines.CoroutineScope
+import org.koin.core.annotation.Single
import org.meshtastic.core.model.Position
import org.meshtastic.core.model.TelemetryType
import org.meshtastic.feature.node.component.NodeMenuAction
-import javax.inject.Inject
-import javax.inject.Singleton
-@Singleton
+@Single
class NodeDetailActions
-@Inject
constructor(
private val nodeManagementActions: NodeManagementActions,
private val nodeRequestActions: NodeRequestActions,
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailScreen.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailScreen.kt
similarity index 93%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailScreen.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailScreen.kt
index 8f4c9dd09..223cc5e5e 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailScreen.kt
+++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailScreen.kt
@@ -21,10 +21,7 @@ import android.content.Intent
import android.provider.Settings
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
-import androidx.compose.animation.AnimatedContent
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.togetherWith
+import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
@@ -55,9 +52,9 @@ import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
+import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.core.database.entity.FirmwareRelease
import org.meshtastic.core.model.Node
import org.meshtastic.core.navigation.Route
@@ -94,10 +91,11 @@ private sealed interface NodeDetailOverlay {
fun NodeDetailScreen(
nodeId: Int,
modifier: Modifier = Modifier,
- viewModel: NodeDetailViewModel = hiltViewModel(),
+ viewModel: NodeDetailViewModel,
navigateToMessages: (String) -> Unit = {},
onNavigate: (Route) -> Unit = {},
onNavigateUp: () -> Unit = {},
+ compassViewModel: CompassViewModel? = null,
) {
LaunchedEffect(nodeId) { viewModel.start(nodeId) }
@@ -120,6 +118,7 @@ fun NodeDetailScreen(
navigateToMessages = navigateToMessages,
onNavigate = onNavigate,
onNavigateUp = onNavigateUp,
+ compassViewModel = compassViewModel,
)
}
@@ -133,12 +132,13 @@ private fun NodeDetailScaffold(
navigateToMessages: (String) -> Unit,
onNavigate: (Route) -> Unit,
onNavigateUp: () -> Unit,
+ compassViewModel: CompassViewModel? = null,
) {
var activeOverlay by remember { mutableStateOf(null) }
val inspectionMode = LocalInspectionMode.current
- val compassViewModel = if (inspectionMode) null else hiltViewModel()
+ val actualCompassViewModel = compassViewModel ?: if (inspectionMode) null else koinViewModel()
val compassUiState by
- compassViewModel?.uiState?.collectAsStateWithLifecycle() ?: remember { mutableStateOf(CompassUiState()) }
+ actualCompassViewModel?.uiState?.collectAsStateWithLifecycle() ?: remember { mutableStateOf(CompassUiState()) }
val node = uiState.node
val listState = rememberLazyListState()
@@ -167,7 +167,7 @@ private fun NodeDetailScaffold(
when (action) {
is NodeDetailAction.ShareContact -> activeOverlay = NodeDetailOverlay.SharedContact
is NodeDetailAction.OpenCompass -> {
- compassViewModel?.start(action.node, action.displayUnits)
+ actualCompassViewModel?.start(action.node, action.displayUnits)
activeOverlay = NodeDetailOverlay.Compass
}
else ->
@@ -186,7 +186,7 @@ private fun NodeDetailScaffold(
)
}
- NodeDetailOverlays(activeOverlay, node, compassUiState, compassViewModel, { activeOverlay = null }) {
+ NodeDetailOverlays(activeOverlay, node, compassUiState, actualCompassViewModel, { activeOverlay = null }) {
viewModel.handleNodeMenuAction(NodeMenuAction.RequestPosition(it))
}
}
@@ -200,12 +200,7 @@ private fun NodeDetailContent(
onFirmwareSelect: (FirmwareRelease) -> Unit,
modifier: Modifier = Modifier,
) {
- AnimatedContent(
- targetState = uiState.node != null,
- transitionSpec = { fadeIn().togetherWith(fadeOut()) },
- label = "NodeDetailContent",
- modifier = modifier,
- ) { isNodePresent ->
+ Crossfade(targetState = uiState.node != null, label = "NodeDetailContent", modifier = modifier) { isNodePresent ->
if (isNodePresent && uiState.node != null) {
NodeDetailList(
node = uiState.node,
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt
similarity index 98%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt
index bdaa2a97a..107a0a9dc 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt
+++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt
@@ -61,7 +61,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
@@ -96,8 +95,8 @@ import org.meshtastic.proto.SharedContact
@Composable
fun NodeListScreen(
navigateToNodeDetails: (Int) -> Unit,
+ viewModel: NodeListViewModel,
onNavigateToChannels: () -> Unit = {},
- viewModel: NodeListViewModel = hiltViewModel(),
scrollToTopEvents: Flow? = null,
activeNodeId: Int? = null,
) {
@@ -156,7 +155,9 @@ fun NodeListScreen(
alignment = Alignment.BottomEnd,
),
onImport = { uri ->
- viewModel.handleScannedUri(uri) { scope.launch { context.showToast(Res.string.channel_invalid) } }
+ viewModel.handleScannedUri(uri.toString()) {
+ scope.launch { context.showToast(Res.string.channel_invalid) }
+ }
},
onDismissSharedContact = { viewModel.setSharedContactRequested(null) },
isContactContext = true,
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/BaseMetricChart.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/BaseMetricChart.kt
similarity index 100%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/BaseMetricChart.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/BaseMetricChart.kt
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/ChartStyling.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/ChartStyling.kt
similarity index 100%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/ChartStyling.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/ChartStyling.kt
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/CommonCharts.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/CommonCharts.kt
similarity index 100%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/CommonCharts.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/CommonCharts.kt
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/DeviceMetrics.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/DeviceMetrics.kt
similarity index 99%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/DeviceMetrics.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/DeviceMetrics.kt
index d7ee8782e..851f199a3 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/DeviceMetrics.kt
+++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/DeviceMetrics.kt
@@ -51,7 +51,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.patrykandpatrick.vico.compose.cartesian.VicoScrollState
import com.patrykandpatrick.vico.compose.cartesian.axis.Axis
@@ -123,7 +122,7 @@ private val LEGEND_DATA =
@Suppress("LongMethod")
@Composable
-fun DeviceMetricsScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigateUp: () -> Unit) {
+fun DeviceMetricsScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Unit) {
val state by viewModel.state.collectAsStateWithLifecycle()
val timeFrame by viewModel.timeFrame.collectAsStateWithLifecycle()
val availableTimeFrames by viewModel.availableTimeFrames.collectAsStateWithLifecycle()
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/EnvironmentCharts.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentCharts.kt
similarity index 100%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/EnvironmentCharts.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentCharts.kt
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt
similarity index 99%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt
index cffc3d383..376f8b0ef 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt
+++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt
@@ -46,7 +46,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.common.util.nowSeconds
@@ -73,7 +72,7 @@ import org.meshtastic.feature.node.metrics.CommonCharts.MS_PER_SEC
import org.meshtastic.proto.Telemetry
@Composable
-fun EnvironmentMetricsScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigateUp: () -> Unit) {
+fun EnvironmentMetricsScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Unit) {
val state by viewModel.state.collectAsStateWithLifecycle()
val graphData by viewModel.environmentGraphingData.collectAsStateWithLifecycle()
val filteredTelemetries by viewModel.filteredEnvironmentMetrics.collectAsStateWithLifecycle()
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/HardwareModelExtensions.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/HardwareModelExtensions.kt
similarity index 100%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/HardwareModelExtensions.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/HardwareModelExtensions.kt
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/HostMetricsLog.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/HostMetricsLog.kt
similarity index 98%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/HostMetricsLog.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/HostMetricsLog.kt
index c870b5e2c..d3d29dc05 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/HostMetricsLog.kt
+++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/HostMetricsLog.kt
@@ -53,7 +53,6 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.common.util.nowSeconds
@@ -78,7 +77,7 @@ import java.text.DecimalFormat
@OptIn(ExperimentalFoundationApi::class)
@Composable
-fun HostMetricsLogScreen(metricsViewModel: MetricsViewModel = hiltViewModel(), onNavigateUp: () -> Unit) {
+fun HostMetricsLogScreen(metricsViewModel: MetricsViewModel, onNavigateUp: () -> Unit) {
val state by metricsViewModel.state.collectAsStateWithLifecycle()
val snackbarHostState = remember { SnackbarHostState() }
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/NeighborInfoLog.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/NeighborInfoLog.kt
similarity index 97%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/NeighborInfoLog.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/NeighborInfoLog.kt
index 006e02fcf..a9f5d8c00 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/NeighborInfoLog.kt
+++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/NeighborInfoLog.kt
@@ -38,7 +38,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.common.util.DateFormatter
@@ -61,11 +60,7 @@ import org.meshtastic.feature.node.detail.NodeRequestEffect
@OptIn(ExperimentalFoundationApi::class)
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
-fun NeighborInfoLogScreen(
- modifier: Modifier = Modifier,
- viewModel: MetricsViewModel = hiltViewModel(),
- onNavigateUp: () -> Unit,
-) {
+fun NeighborInfoLogScreen(modifier: Modifier = Modifier, viewModel: MetricsViewModel, onNavigateUp: () -> Unit) {
val state by viewModel.state.collectAsStateWithLifecycle()
val snackbarHostState = remember { SnackbarHostState() }
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/PaxMetrics.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/PaxMetrics.kt
similarity index 98%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/PaxMetrics.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/PaxMetrics.kt
index f566fd088..4873d0c0a 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/PaxMetrics.kt
+++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/PaxMetrics.kt
@@ -43,7 +43,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.patrykandpatrick.vico.compose.cartesian.VicoScrollState
import com.patrykandpatrick.vico.compose.cartesian.axis.HorizontalAxis
@@ -174,7 +173,7 @@ private fun PaxMetricsChart(
@Composable
@Suppress("MagicNumber", "LongMethod")
-fun PaxMetricsScreen(metricsViewModel: MetricsViewModel = hiltViewModel(), onNavigateUp: () -> Unit) {
+fun PaxMetricsScreen(metricsViewModel: MetricsViewModel, onNavigateUp: () -> Unit) {
val state by metricsViewModel.state.collectAsStateWithLifecycle()
val paxMetrics by metricsViewModel.filteredPaxMetrics.collectAsStateWithLifecycle()
val timeFrame by metricsViewModel.timeFrame.collectAsStateWithLifecycle()
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/PositionLog.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/PositionLog.kt
similarity index 98%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/PositionLog.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/PositionLog.kt
index 55d793957..551fe54f2 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/PositionLog.kt
+++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/PositionLog.kt
@@ -59,7 +59,6 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewScreenSizes
import androidx.compose.ui.unit.dp
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.common.util.nowSeconds
@@ -172,7 +171,7 @@ private fun ActionButtons(
@Suppress("LongMethod")
@Composable
-fun PositionLogScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigateUp: () -> Unit) {
+fun PositionLogScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Unit) {
val state by viewModel.state.collectAsStateWithLifecycle()
val snackbarHostState = remember { SnackbarHostState() }
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/PowerMetrics.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/PowerMetrics.kt
similarity index 99%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/PowerMetrics.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/PowerMetrics.kt
index bdd89a059..f07feed67 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/PowerMetrics.kt
+++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/PowerMetrics.kt
@@ -51,7 +51,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.patrykandpatrick.vico.compose.cartesian.VicoScrollState
import com.patrykandpatrick.vico.compose.cartesian.axis.Axis
@@ -107,7 +106,7 @@ private val LEGEND_DATA =
@Suppress("LongMethod")
@Composable
-fun PowerMetricsScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigateUp: () -> Unit) {
+fun PowerMetricsScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Unit) {
val state by viewModel.state.collectAsStateWithLifecycle()
val timeFrame by viewModel.timeFrame.collectAsStateWithLifecycle()
val availableTimeFrames by viewModel.availableTimeFrames.collectAsStateWithLifecycle()
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/SignalMetrics.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/SignalMetrics.kt
similarity index 98%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/SignalMetrics.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/SignalMetrics.kt
index 0cee152ce..a3a8feec8 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/SignalMetrics.kt
+++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/SignalMetrics.kt
@@ -47,7 +47,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.patrykandpatrick.vico.compose.cartesian.VicoScrollState
import com.patrykandpatrick.vico.compose.cartesian.axis.Axis
@@ -85,7 +84,7 @@ private val LEGEND_DATA =
@Suppress("LongMethod")
@Composable
-fun SignalMetricsScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigateUp: () -> Unit) {
+fun SignalMetricsScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Unit) {
val state by viewModel.state.collectAsStateWithLifecycle()
val timeFrame by viewModel.timeFrame.collectAsStateWithLifecycle()
val availableTimeFrames by viewModel.availableTimeFrames.collectAsStateWithLifecycle()
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/TimeFrameSelector.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/TimeFrameSelector.kt
similarity index 100%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/TimeFrameSelector.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/TimeFrameSelector.kt
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.kt
similarity index 99%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.kt
index 1fdd5cf5b..602bcebae 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.kt
+++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.kt
@@ -42,7 +42,6 @@ import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.pluralStringResource
import org.jetbrains.compose.resources.stringResource
@@ -83,7 +82,7 @@ import org.meshtastic.proto.RouteDiscovery
@Composable
fun TracerouteLogScreen(
modifier: Modifier = Modifier,
- viewModel: MetricsViewModel = hiltViewModel(),
+ viewModel: MetricsViewModel,
onNavigateUp: () -> Unit,
onViewOnMap: (requestId: Int, responseLogUuid: String) -> Unit = { _, _ -> },
) {
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/TracerouteMapScreen.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/TracerouteMapScreen.kt
similarity index 94%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/TracerouteMapScreen.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/TracerouteMapScreen.kt
index 162af7350..ec3cf5ea5 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/TracerouteMapScreen.kt
+++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/TracerouteMapScreen.kt
@@ -38,7 +38,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.flow.flowOf
import org.jetbrains.compose.resources.stringResource
@@ -53,12 +52,13 @@ import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Route
import org.meshtastic.core.ui.theme.TracerouteColors
import org.meshtastic.core.ui.util.LocalMapViewProvider
+import org.meshtastic.core.ui.util.LocalTracerouteMapOverlayInsetsProvider
import org.meshtastic.feature.map.model.TracerouteOverlay
import org.meshtastic.proto.Position
@Composable
fun TracerouteMapScreen(
- metricsViewModel: MetricsViewModel = hiltViewModel(),
+ metricsViewModel: MetricsViewModel,
requestId: Int,
logUuid: String? = null,
onNavigateUp: () -> Unit,
@@ -102,6 +102,7 @@ private fun TracerouteMapScaffold(
) {
var tracerouteNodesShown by remember { mutableStateOf(0) }
var tracerouteNodesTotal by remember { mutableStateOf(0) }
+ val insets = LocalTracerouteMapOverlayInsetsProvider.current
Scaffold(
topBar = {
MainAppBar(
@@ -128,10 +129,8 @@ private fun TracerouteMapScaffold(
},
)
Column(
- modifier =
- Modifier.align(TracerouteMapOverlayInsets.overlayAlignment)
- .padding(TracerouteMapOverlayInsets.overlayPadding),
- horizontalAlignment = TracerouteMapOverlayInsets.contentHorizontalAlignment,
+ modifier = Modifier.align(insets.overlayAlignment).padding(insets.overlayPadding),
+ horizontalAlignment = insets.contentHorizontalAlignment,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
TracerouteNodeCount(shown = tracerouteNodesShown, total = tracerouteNodesTotal)
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/model/MetricInfo.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/model/MetricInfo.kt
similarity index 100%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/model/MetricInfo.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/model/MetricInfo.kt
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/model/NodeDetailAction.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/model/NodeDetailAction.kt
similarity index 100%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/model/NodeDetailAction.kt
rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/model/NodeDetailAction.kt
diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/InterfaceMapKey.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/CompassHeadingProvider.kt
similarity index 63%
rename from app/src/main/kotlin/org/meshtastic/app/repository/radio/InterfaceMapKey.kt
rename to feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/CompassHeadingProvider.kt
index 4864abe7a..4680fc111 100644
--- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/InterfaceMapKey.kt
+++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/CompassHeadingProvider.kt
@@ -14,13 +14,16 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package org.meshtastic.app.repository.radio
+package org.meshtastic.feature.node.compass
-import dagger.MapKey
-import org.meshtastic.core.model.InterfaceId
+import kotlinx.coroutines.flow.Flow
-/** Dagger `MapKey` implementation allowing for `InterfaceId` to be used as a map key. */
-@MapKey
-@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_SETTER, AnnotationTarget.PROPERTY_GETTER)
-@Retention(AnnotationRetention.RUNTIME)
-annotation class InterfaceMapKey(val value: InterfaceId)
+data class HeadingState(
+ val heading: Float? = null, // 0..360 degrees
+ val hasSensor: Boolean = true,
+ val accuracy: Int = 0,
+)
+
+interface CompassHeadingProvider {
+ fun headingUpdates(): Flow
+}
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/compass/CompassUiState.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/CompassUiState.kt
similarity index 100%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/compass/CompassUiState.kt
rename to feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/CompassUiState.kt
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/compass/CompassViewModel.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/CompassViewModel.kt
similarity index 95%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/compass/CompassViewModel.kt
rename to feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/CompassViewModel.kt
index 3043ef499..9ce9d789c 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/compass/CompassViewModel.kt
+++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/CompassViewModel.kt
@@ -16,11 +16,9 @@
*/
package org.meshtastic.feature.node.compass
-import android.hardware.GeomagneticField
import androidx.compose.ui.graphics.Color
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -39,7 +37,6 @@ import org.meshtastic.core.model.util.toDistanceString
import org.meshtastic.core.ui.component.precisionBitsToMeters
import org.meshtastic.proto.Config
import org.meshtastic.proto.Position
-import javax.inject.Inject
import kotlin.math.abs
import kotlin.math.atan2
import kotlin.math.min
@@ -54,13 +51,11 @@ private const val SECONDS_PER_MINUTE = 60
private const val HUNDRED = 100f
private const val MILLIMETERS_PER_METER = 1000f
-@HiltViewModel
@Suppress("TooManyFunctions")
-class CompassViewModel
-@Inject
-constructor(
+open class CompassViewModel(
private val headingProvider: CompassHeadingProvider,
private val phoneLocationProvider: PhoneLocationProvider,
+ private val magneticFieldProvider: MagneticFieldProvider,
private val dispatchers: CoroutineDispatchers,
) : ViewModel() {
@@ -192,9 +187,8 @@ constructor(
private fun applyTrueNorthCorrection(heading: Float?, locationState: PhoneLocationState): Float? {
val loc = locationState.location ?: return heading
val baseHeading = heading ?: return null
- val geomagnetic =
- GeomagneticField(loc.latitude.toFloat(), loc.longitude.toFloat(), loc.altitude.toFloat(), nowMillis)
- return (baseHeading + geomagnetic.declination + FULL_CIRCLE_DEGREES) % FULL_CIRCLE_DEGREES
+ val declination = magneticFieldProvider.getDeclination(loc.latitude, loc.longitude, loc.altitude, nowMillis)
+ return (baseHeading + declination + FULL_CIRCLE_DEGREES) % FULL_CIRCLE_DEGREES
}
private fun formatElapsed(timestampSec: Long): String {
@@ -246,6 +240,8 @@ constructor(
if (distance <= 0) return FULL_CIRCLE_DEGREES / 2
val radians = atan2(accuracy.toDouble(), distance.toDouble())
- return Math.toDegrees(radians).toFloat().coerceIn(0f, FULL_CIRCLE_DEGREES / 2)
+ return radiansToDegrees(radians).toFloat().coerceIn(0f, FULL_CIRCLE_DEGREES / 2)
}
+
+ private fun radiansToDegrees(radians: Double): Double = radians * 180.0 / kotlin.math.PI
}
diff --git a/app/src/main/kotlin/org/meshtastic/app/di/DatabaseModule.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/MagneticFieldProvider.kt
similarity index 62%
rename from app/src/main/kotlin/org/meshtastic/app/di/DatabaseModule.kt
rename to feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/MagneticFieldProvider.kt
index 059330e7a..7e0ce4983 100644
--- a/app/src/main/kotlin/org/meshtastic/app/di/DatabaseModule.kt
+++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/MagneticFieldProvider.kt
@@ -14,21 +14,8 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package org.meshtastic.app.di
+package org.meshtastic.feature.node.compass
-import dagger.Binds
-import dagger.Module
-import dagger.hilt.InstallIn
-import dagger.hilt.components.SingletonComponent
-import javax.inject.Singleton
-
-@InstallIn(SingletonComponent::class)
-@Module
-interface DatabaseModule {
-
- @Binds
- @Singleton
- fun bindDatabaseManager(
- impl: org.meshtastic.core.database.DatabaseManager,
- ): org.meshtastic.core.common.database.DatabaseManager
+interface MagneticFieldProvider {
+ fun getDeclination(latitude: Double, longitude: Double, altitude: Double, timeMillis: Long): Float
}
diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/PhoneLocationProvider.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/PhoneLocationProvider.kt
new file mode 100644
index 000000000..e7f39b9a5
--- /dev/null
+++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/PhoneLocationProvider.kt
@@ -0,0 +1,34 @@
+/*
+ * 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 org.meshtastic.feature.node.compass
+
+import kotlinx.coroutines.flow.Flow
+
+data class PhoneLocation(val latitude: Double, val longitude: Double, val altitude: Double, val timeMillis: Long)
+
+data class PhoneLocationState(
+ val permissionGranted: Boolean,
+ val providerEnabled: Boolean,
+ val location: PhoneLocation? = null,
+) {
+ val hasFix: Boolean
+ get() = location != null
+}
+
+interface PhoneLocationProvider {
+ fun locationUpdates(): Flow
+}
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeMenuAction.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeMenuAction.kt
similarity index 100%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeMenuAction.kt
rename to feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeMenuAction.kt
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt
similarity index 88%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt
rename to feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt
index 8d6bb18ae..ebe720bb3 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt
+++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt
@@ -20,7 +20,6 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.toRoute
-import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
@@ -43,20 +42,8 @@ import org.meshtastic.feature.node.domain.usecase.GetNodeDetailsUseCase
import org.meshtastic.feature.node.metrics.EnvironmentMetricsState
import org.meshtastic.feature.node.model.LogsType
import org.meshtastic.feature.node.model.MetricsState
-import javax.inject.Inject
-/**
- * UI state for the Node Details screen.
- *
- * @property node The node being viewed, or null if loading.
- * @property nodeName The display name for the node, resolved in the UI.
- * @property ourNode Information about the locally connected node.
- * @property metricsState Aggregated sensor and signal metrics.
- * @property environmentState Standardized environmental sensor data.
- * @property availableLogs a set of log types available for this node.
- * @property lastTracerouteTime Timestamp of the last successful traceroute request.
- * @property lastRequestNeighborsTime Timestamp of the last successful neighbor info request.
- */
+/** UI state for the Node Details screen. */
@androidx.compose.runtime.Stable
data class NodeDetailUiState(
val node: Node? = null,
@@ -73,11 +60,8 @@ data class NodeDetailUiState(
* ViewModel for the Node Details screen, coordinating data from the node database, mesh logs, and radio configuration.
*/
@OptIn(ExperimentalCoroutinesApi::class)
-@HiltViewModel
-class NodeDetailViewModel
-@Inject
-constructor(
- savedStateHandle: SavedStateHandle,
+open class NodeDetailViewModel(
+ private val savedStateHandle: SavedStateHandle,
private val nodeManagementActions: NodeManagementActions,
private val nodeRequestActions: NodeRequestActions,
private val serviceRepository: ServiceRepository,
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeManagementActions.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeManagementActions.kt
similarity index 93%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeManagementActions.kt
rename to feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeManagementActions.kt
index fbf79a4d7..3dcc1c593 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeManagementActions.kt
+++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeManagementActions.kt
@@ -21,6 +21,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.getString
+import org.koin.core.annotation.Single
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.model.service.ServiceAction
@@ -40,12 +41,9 @@ import org.meshtastic.core.resources.remove
import org.meshtastic.core.resources.remove_node_text
import org.meshtastic.core.resources.unmute
import org.meshtastic.core.ui.util.AlertManager
-import javax.inject.Inject
-import javax.inject.Singleton
-@Singleton
+@Single
class NodeManagementActions
-@Inject
constructor(
private val nodeRepository: NodeRepository,
private val serviceRepository: ServiceRepository,
@@ -127,10 +125,8 @@ constructor(
scope.launch(Dispatchers.IO) {
try {
nodeRepository.setNodeNotes(nodeNum, notes)
- } catch (ex: java.io.IOException) {
- Logger.e { "Set node notes IO error: ${ex.message}" }
- } catch (ex: java.sql.SQLException) {
- Logger.e { "Set node notes SQL error: ${ex.message}" }
+ } catch (ex: Exception) {
+ Logger.e(ex) { "Set node notes error" }
}
}
}
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeRequestActions.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeRequestActions.kt
similarity index 97%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeRequestActions.kt
rename to feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeRequestActions.kt
index 1ca64fae9..45bfb95a5 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeRequestActions.kt
+++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeRequestActions.kt
@@ -27,6 +27,7 @@ import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
+import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.model.Position
import org.meshtastic.core.model.RadioController
@@ -45,15 +46,13 @@ import org.meshtastic.core.resources.requesting_from
import org.meshtastic.core.resources.signal_quality
import org.meshtastic.core.resources.traceroute
import org.meshtastic.core.resources.user_info
-import javax.inject.Inject
-import javax.inject.Singleton
sealed class NodeRequestEffect {
data class ShowFeedback(val text: UiText) : NodeRequestEffect()
}
-@Singleton
-class NodeRequestActions @Inject constructor(private val radioController: RadioController) {
+@Single
+class NodeRequestActions constructor(private val radioController: RadioController) {
private val _effects = MutableSharedFlow()
val effects: SharedFlow = _effects.asSharedFlow()
diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/di/FeatureNodeModule.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/di/FeatureNodeModule.kt
new file mode 100644
index 000000000..e32e96818
--- /dev/null
+++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/di/FeatureNodeModule.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 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 org.meshtastic.feature.node.di
+
+import org.koin.core.annotation.ComponentScan
+import org.koin.core.annotation.Module
+
+@Module
+@ComponentScan("org.meshtastic.feature.node")
+class FeatureNodeModule
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCase.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCase.kt
similarity index 94%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCase.kt
rename to feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCase.kt
index bf5b7e4f4..039939871 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCase.kt
+++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCase.kt
@@ -18,15 +18,16 @@ package org.meshtastic.feature.node.domain.usecase
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
+import org.koin.core.annotation.Single
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.NodeSortOption
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.feature.node.list.NodeFilterState
import org.meshtastic.feature.node.model.isEffectivelyUnmessageable
import org.meshtastic.proto.Config
-import javax.inject.Inject
-class GetFilteredNodesUseCase @Inject constructor(private val nodeRepository: NodeRepository) {
+@Single
+class GetFilteredNodesUseCase constructor(private val nodeRepository: NodeRepository) {
@Suppress("CyclomaticComplexMethod", "LongMethod")
operator fun invoke(filter: NodeFilterState, sort: NodeSortOption): Flow> = nodeRepository
.getNodes(
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/domain/usecase/GetNodeDetailsUseCase.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/domain/usecase/GetNodeDetailsUseCase.kt
similarity index 99%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/domain/usecase/GetNodeDetailsUseCase.kt
rename to feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/domain/usecase/GetNodeDetailsUseCase.kt
index 16614f012..d4e6280da 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/domain/usecase/GetNodeDetailsUseCase.kt
+++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/domain/usecase/GetNodeDetailsUseCase.kt
@@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
+import org.koin.core.annotation.Single
import org.meshtastic.core.data.repository.FirmwareReleaseRepository
import org.meshtastic.core.database.entity.FirmwareRelease
import org.meshtastic.core.database.entity.MeshLog
@@ -49,10 +50,9 @@ import org.meshtastic.proto.FirmwareEdition
import org.meshtastic.proto.MeshPacket
import org.meshtastic.proto.PortNum
import org.meshtastic.proto.Telemetry
-import javax.inject.Inject
+@Single
class GetNodeDetailsUseCase
-@Inject
constructor(
private val nodeRepository: NodeRepository,
private val meshLogRepository: MeshLogRepository,
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeFilterPreferences.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeFilterPreferences.kt
similarity index 93%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeFilterPreferences.kt
rename to feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeFilterPreferences.kt
index 4af6eaaea..e11721371 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeFilterPreferences.kt
+++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeFilterPreferences.kt
@@ -17,11 +17,12 @@
package org.meshtastic.feature.node.list
import kotlinx.coroutines.flow.map
+import org.koin.core.annotation.Single
import org.meshtastic.core.datastore.UiPreferencesDataSource
import org.meshtastic.core.model.NodeSortOption
-import javax.inject.Inject
-class NodeFilterPreferences @Inject constructor(private val uiPreferencesDataSource: UiPreferencesDataSource) {
+@Single
+class NodeFilterPreferences constructor(private val uiPreferencesDataSource: UiPreferencesDataSource) {
val includeUnknown = uiPreferencesDataSource.includeUnknown
val excludeInfrastructure = uiPreferencesDataSource.excludeInfrastructure
val onlyOnline = uiPreferencesDataSource.onlyOnline
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt
similarity index 97%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt
rename to feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt
index 38e51602c..d4fe6243b 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt
+++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt
@@ -16,11 +16,9 @@
*/
package org.meshtastic.feature.node.list
-import android.net.Uri
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -28,6 +26,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.launch
+import org.meshtastic.core.common.util.CommonUri
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.NodeSortOption
import org.meshtastic.core.model.RadioController
@@ -41,13 +40,9 @@ import org.meshtastic.feature.node.domain.usecase.GetFilteredNodesUseCase
import org.meshtastic.proto.ChannelSet
import org.meshtastic.proto.Config
import org.meshtastic.proto.SharedContact
-import javax.inject.Inject
@Suppress("LongParameterList")
-@HiltViewModel
-class NodeListViewModel
-@Inject
-constructor(
+open class NodeListViewModel(
private val savedStateHandle: SavedStateHandle,
private val nodeRepository: NodeRepository,
private val radioConfigRepository: RadioConfigRepository,
@@ -138,7 +133,8 @@ constructor(
}
/** Unified handler for scanned Meshtastic URIs (contacts or channels). */
- fun handleScannedUri(uri: Uri, onInvalid: () -> Unit) {
+ fun handleScannedUri(uriString: String, onInvalid: () -> Unit) {
+ val uri = CommonUri.parse(uriString)
uri.dispatchMeshtasticUri(
onContact = { _sharedContactRequested.value = it },
onChannel = { _requestChannelSet.value = it },
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetricsState.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetricsState.kt
similarity index 100%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetricsState.kt
rename to feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetricsState.kt
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt
similarity index 81%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt
rename to feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt
index 29d948898..eda175a62 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt
+++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt
@@ -16,17 +16,12 @@
*/
package org.meshtastic.feature.node.metrics
-import android.app.Application
-import android.net.Uri
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.Text
import androidx.compose.ui.text.AnnotatedString
-import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import androidx.navigation.toRoute
import co.touchlab.kermit.Logger
-import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
@@ -40,11 +35,8 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
import org.jetbrains.compose.resources.StringResource
import org.meshtastic.core.common.util.nowSeconds
-import org.meshtastic.core.common.util.toDate
-import org.meshtastic.core.common.util.toInstant
import org.meshtastic.core.data.repository.TracerouteSnapshotRepository
import org.meshtastic.core.database.entity.MeshLog
import org.meshtastic.core.di.CoroutineDispatchers
@@ -52,7 +44,6 @@ import org.meshtastic.core.model.Node
import org.meshtastic.core.model.TelemetryType
import org.meshtastic.core.model.evaluateTracerouteMapAvailability
import org.meshtastic.core.model.util.UnitConversions
-import org.meshtastic.core.navigation.NodesRoutes
import org.meshtastic.core.repository.MeshLogRepository
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.ServiceRepository
@@ -71,26 +62,15 @@ import org.meshtastic.feature.node.model.MetricsState
import org.meshtastic.feature.node.model.TimeFrame
import org.meshtastic.proto.PortNum
import org.meshtastic.proto.Telemetry
-import java.io.BufferedWriter
-import java.io.FileNotFoundException
-import java.io.FileWriter
-import java.io.IOException
-import java.text.SimpleDateFormat
-import java.util.Locale
-import javax.inject.Inject
import org.meshtastic.proto.Paxcount as ProtoPaxcount
/**
* ViewModel responsible for managing and graphing metrics (telemetry, signal strength, paxcount) for a specific node.
*/
@Suppress("LongParameterList", "TooManyFunctions")
-@HiltViewModel
-class MetricsViewModel
-@Inject
-constructor(
- savedStateHandle: SavedStateHandle,
- private val app: Application,
- private val dispatchers: CoroutineDispatchers,
+open class MetricsViewModel(
+ val destNum: Int,
+ protected val dispatchers: CoroutineDispatchers,
private val meshLogRepository: MeshLogRepository,
private val serviceRepository: ServiceRepository,
private val nodeRepository: NodeRepository,
@@ -100,8 +80,8 @@ constructor(
private val getNodeDetailsUseCase: GetNodeDetailsUseCase,
) : ViewModel() {
- private val nodeIdFromRoute: Int? =
- runCatching { savedStateHandle.toRoute().destNum }.getOrNull()
+ private val nodeIdFromRoute: Int?
+ get() = destNum
private val manualNodeId = MutableStateFlow(null)
private val activeNodeId =
@@ -134,7 +114,8 @@ constructor(
val availableTimeFrames: StateFlow> =
combine(state, environmentState) { currentState, envState ->
val stateOldest = currentState.oldestTimestampSeconds()
- val envOldest = envState.environmentMetrics.minOfOrNull { it.time.toLong() }?.takeIf { it > 0 }
+ val envOldest =
+ envState.environmentMetrics.minOfOrNull { it.time.toLong() }?.takeIf { it > 0 } ?: nowSeconds
val oldest = listOfNotNull(stateOldest, envOldest).minOrNull() ?: nowSeconds
TimeFrame.entries.filter { it.isAvailable(oldest) }
}
@@ -331,44 +312,10 @@ constructor(
Logger.d { "MetricsViewModel cleared" }
}
- fun savePositionCSV(uri: Uri) = viewModelScope.launch(dispatchers.main) {
- val positions = state.value.positionLogs
- writeToUri(uri) { writer ->
- writer.appendLine(
- "\"date\",\"time\",\"latitude\",\"longitude\",\"altitude\",\"satsInView\",\"speed\",\"heading\"",
- )
-
- val dateFormat = SimpleDateFormat("\"yyyy-MM-dd\",\"HH:mm:ss\"", Locale.getDefault())
-
- positions.forEach { position ->
- val rxDateTime = dateFormat.format((position.time.toLong() * 1000L).toInstant().toDate())
- val latitude = (position.latitude_i ?: 0) * 1e-7
- val longitude = (position.longitude_i ?: 0) * 1e-7
- val altitude = position.altitude
- val satsInView = position.sats_in_view
- val speed = position.ground_speed
- val heading = "%.2f".format((position.ground_track ?: 0) * 1e-5)
-
- writer.appendLine(
- "$rxDateTime,\"$latitude\",\"$longitude\",\"$altitude\",\"$satsInView\",\"$speed\",\"$heading\"",
- )
- }
- }
+ open fun savePositionCSV(uri: Any) {
+ // To be implemented in platform-specific subclass
}
- private suspend inline fun writeToUri(uri: Uri, crossinline block: suspend (BufferedWriter) -> Unit) =
- withContext(dispatchers.io) {
- try {
- app.contentResolver.openFileDescriptor(uri, "wt")?.use { parcelFileDescriptor ->
- FileWriter(parcelFileDescriptor.fileDescriptor).use { fileWriter ->
- BufferedWriter(fileWriter).use { writer -> block.invoke(writer) }
- }
- }
- } catch (ex: FileNotFoundException) {
- Logger.e(ex) { "Can't write file error" }
- }
- }
-
@Suppress("MagicNumber", "CyclomaticComplexMethod", "ReturnCount")
fun decodePaxFromLog(log: MeshLog): ProtoPaxcount? {
try {
@@ -379,25 +326,26 @@ constructor(
val pax = ProtoPaxcount.ADAPTER.decode(decoded.payload)
if (pax.ble != 0 || pax.wifi != 0 || pax.uptime != 0) return pax
}
- } catch (e: IOException) {
+ } catch (e: Exception) {
Logger.e(e) { "Failed to parse Paxcount from binary data" }
}
try {
val base64 = log.raw_message.trim()
if (base64.matches(Regex("^[A-Za-z0-9+/=\\r\\n]+$"))) {
- val bytes = android.util.Base64.decode(base64, android.util.Base64.DEFAULT)
+ val bytes = decodeBase64(base64)
return ProtoPaxcount.ADAPTER.decode(bytes)
} else if (base64.matches(Regex("^[0-9a-fA-F]+$")) && base64.length % 2 == 0) {
val bytes = base64.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
return ProtoPaxcount.ADAPTER.decode(bytes)
}
- } catch (e: IllegalArgumentException) {
- Logger.e(e) { "Failed to parse Paxcount from decoded data" }
- } catch (e: IOException) {
- Logger.e(e) { "Failed to parse Paxcount from decoded data" }
- } catch (e: NumberFormatException) {
+ } catch (e: Exception) {
Logger.e(e) { "Failed to parse Paxcount from decoded data" }
}
return null
}
+
+ protected open fun decodeBase64(base64: String): ByteArray {
+ // To be overridden in platform-specific subclass or use KMP library
+ return ByteArray(0)
+ }
}
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/model/IsEffectivelyUnmessageable.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/model/IsEffectivelyUnmessageable.kt
similarity index 100%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/model/IsEffectivelyUnmessageable.kt
rename to feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/model/IsEffectivelyUnmessageable.kt
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/model/LogsType.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/model/LogsType.kt
similarity index 100%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/model/LogsType.kt
rename to feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/model/LogsType.kt
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/model/MetricsState.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/model/MetricsState.kt
similarity index 100%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/model/MetricsState.kt
rename to feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/model/MetricsState.kt
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/model/TimeFrame.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/model/TimeFrame.kt
similarity index 100%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/model/TimeFrame.kt
rename to feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/model/TimeFrame.kt
diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts
index 5c02a427e..e40e40e91 100644
--- a/feature/settings/build.gradle.kts
+++ b/feature/settings/build.gradle.kts
@@ -14,57 +14,86 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-import com.android.build.api.dsl.LibraryExtension
plugins {
- alias(libs.plugins.meshtastic.android.library)
- alias(libs.plugins.meshtastic.android.library.compose)
- alias(libs.plugins.meshtastic.android.library.flavors)
- alias(libs.plugins.meshtastic.hilt)
+ alias(libs.plugins.meshtastic.kmp.library)
+ alias(libs.plugins.meshtastic.kmp.library.compose)
+ alias(libs.plugins.meshtastic.kotlinx.serialization)
+ alias(libs.plugins.meshtastic.koin)
}
-configure {
- namespace = "org.meshtastic.feature.settings"
- testOptions { unitTests { isIncludeAndroidResources = true } }
+kotlin {
+ android {
+ namespace = "org.meshtastic.feature.settings"
+ androidResources.enable = false
+ withHostTest { isIncludeAndroidResources = true }
+ }
+
+ sourceSets {
+ commonMain.dependencies {
+ implementation(projects.core.common)
+ implementation(projects.core.data)
+ implementation(projects.core.database)
+ implementation(projects.core.datastore)
+ implementation(projects.core.domain)
+ implementation(projects.core.model)
+ implementation(projects.core.navigation)
+ implementation(projects.core.proto)
+ implementation(projects.core.repository)
+ implementation(projects.core.service)
+ implementation(projects.core.resources)
+ implementation(projects.core.ui)
+ implementation(projects.core.di)
+
+ implementation(libs.androidx.lifecycle.viewmodel.compose)
+ implementation(libs.koin.compose.viewmodel)
+ implementation(libs.kermit)
+ implementation(libs.kotlinx.collections.immutable)
+ }
+
+ androidMain.dependencies {
+ implementation(projects.core.barcode)
+ implementation(projects.core.nfc)
+ implementation(project.dependencies.platform(libs.androidx.compose.bom))
+ implementation(libs.accompanist.permissions)
+ implementation(libs.androidx.activity.compose)
+ implementation(libs.androidx.appcompat)
+ implementation(libs.androidx.compose.material.iconsExtended)
+ implementation(libs.androidx.compose.material3)
+ implementation(libs.androidx.compose.ui.text)
+ implementation(libs.androidx.compose.ui.tooling.preview)
+ implementation(libs.androidx.navigation.common)
+ implementation(libs.coil)
+ implementation(libs.markdown.renderer.android)
+ implementation(libs.markdown.renderer.m3)
+ implementation(libs.markdown.renderer)
+ implementation(libs.aboutlibraries.compose.m3)
+ implementation(libs.nordic.common.core)
+ implementation(libs.nordic.common.permissions.ble)
+
+ // These were in googleImplementation
+ implementation(libs.location.services)
+ implementation(libs.maps.compose)
+ }
+
+ androidUnitTest.dependencies {
+ implementation(libs.junit)
+ implementation(libs.mockk)
+ implementation(libs.robolectric)
+ implementation(libs.turbine)
+ implementation(libs.kotlinx.coroutines.test)
+ implementation(libs.androidx.compose.ui.test.junit4)
+ implementation(libs.androidx.test.ext.junit)
+ }
+ }
}
-dependencies {
- implementation(projects.core.common)
- implementation(projects.core.data)
- implementation(projects.core.database)
- implementation(projects.core.datastore)
- implementation(projects.core.domain)
- implementation(projects.core.model)
- implementation(projects.core.navigation)
- implementation(projects.core.nfc)
- implementation(projects.core.prefs)
- implementation(projects.core.proto)
- implementation(projects.core.service)
- implementation(projects.core.resources)
- implementation(projects.core.ui)
- implementation(projects.core.barcode)
+val marketplaceAttr = Attribute.of("com.android.build.api.attributes.ProductFlavor:marketplace", String::class.java)
- implementation(libs.aboutlibraries.compose.m3)
- implementation(libs.accompanist.permissions)
- implementation(libs.androidx.appcompat)
- implementation(libs.androidx.compose.material.iconsExtended)
- implementation(libs.androidx.compose.material3)
- implementation(libs.androidx.compose.ui.text)
- implementation(libs.androidx.compose.ui.tooling.preview)
- implementation(libs.androidx.navigation.compose)
- implementation(libs.kotlinx.collections.immutable)
- implementation(libs.kermit)
- implementation(libs.nordic.common.core)
- implementation(libs.nordic.common.permissions.ble)
-
- testImplementation(libs.junit)
- testImplementation(libs.mockk)
- testImplementation(libs.robolectric)
- testImplementation(libs.turbine)
- testImplementation(libs.kotlinx.coroutines.test)
- testImplementation(libs.androidx.compose.ui.test.junit4)
- testImplementation(libs.androidx.test.ext.junit)
-
- androidTestImplementation(libs.androidx.compose.ui.test.junit4)
- androidTestImplementation(libs.androidx.test.ext.junit)
+configurations.all {
+ if (isCanBeResolved && !isCanBeConsumed) {
+ if (name.contains("android", ignoreCase = true)) {
+ attributes.attribute(marketplaceAttr, "google")
+ }
+ }
}
diff --git a/feature/settings/detekt-baseline.xml b/feature/settings/detekt-baseline.xml
index 21932a978..11b95ac86 100644
--- a/feature/settings/detekt-baseline.xml
+++ b/feature/settings/detekt-baseline.xml
@@ -2,25 +2,27 @@
- CyclomaticComplexMethod:DisplayConfigItemList.kt$@Composable fun DisplayConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)
- CyclomaticComplexMethod:ExternalNotificationConfigItemList.kt$@Suppress("LongMethod", "TooGenericExceptionCaught") @Composable fun ExternalNotificationConfigScreen( onBack: () -> Unit, modifier: Modifier = Modifier, viewModel: RadioConfigViewModel = hiltViewModel(), )
- CyclomaticComplexMethod:MQTTConfigItemList.kt$@Composable fun MQTTConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)
- CyclomaticComplexMethod:NetworkConfigItemList.kt$@Composable fun NetworkConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)
+ CyclomaticComplexMethod:DisplayConfigItemList.kt$@Composable fun DisplayConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit)
+ CyclomaticComplexMethod:ExternalNotificationConfigItemList.kt$@Suppress("LongMethod", "TooGenericExceptionCaught") @Composable fun ExternalNotificationConfigScreen( onBack: () -> Unit, modifier: Modifier = Modifier, viewModel: RadioConfigViewModel, )
+ CyclomaticComplexMethod:MQTTConfigItemList.kt$@Composable fun MQTTConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit)
+ CyclomaticComplexMethod:NetworkConfigItemList.kt$@Composable fun NetworkConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit)
CyclomaticComplexMethod:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshPacket)
- LongMethod:AudioConfigItemList.kt$@Composable fun AudioConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)
- LongMethod:CannedMessageConfigItemList.kt$@Composable fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)
- LongMethod:DetectionSensorConfigItemList.kt$@Composable fun DetectionSensorConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)
- LongMethod:DeviceConfigItemList.kt$@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun DeviceConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)
- LongMethod:DisplayConfigItemList.kt$@Composable fun DisplayConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)
+ LongMethod:AudioConfigItemList.kt$@Composable fun AudioConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit)
+ LongMethod:CannedMessageConfigItemList.kt$@Composable fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit)
+ LongMethod:DetectionSensorConfigItemList.kt$@Composable fun DetectionSensorConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit)
+ LongMethod:DeviceConfigItemList.kt$@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun DeviceConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit)
+ LongMethod:DisplayConfigItemList.kt$@Composable fun DisplayConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit)
LongMethod:LoRaConfigItemList.kt$@Composable fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit)
- LongMethod:NetworkConfigItemList.kt$@Composable fun NetworkConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)
- LongMethod:PowerConfigItemList.kt$@Composable fun PowerConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)
+ LongMethod:NetworkConfigItemList.kt$@Composable fun NetworkConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit)
+ LongMethod:PowerConfigItemList.kt$@Composable fun PowerConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit)
+ LongMethod:RadioConfigViewModel.kt$RadioConfigViewModel$fun setResponseStateLoading(route: Enum<*>)
LongMethod:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshPacket)
- LongMethod:SerialConfigItemList.kt$@Composable fun SerialConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)
- LongMethod:StoreForwardConfigItemList.kt$@Composable fun StoreForwardConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)
- LongMethod:TelemetryConfigItemList.kt$@Composable fun TelemetryConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)
- LongMethod:UserConfigItemList.kt$@Composable fun UserConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)
+ LongMethod:SerialConfigItemList.kt$@Composable fun SerialConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit)
+ LongMethod:StoreForwardConfigItemList.kt$@Composable fun StoreForwardConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit)
+ LongMethod:TelemetryConfigItemList.kt$@Composable fun TelemetryConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit)
+ LongMethod:UserConfigItemList.kt$@Composable fun UserConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit)
MagicNumber:Debug.kt$3
+ MagicNumber:DebugViewModel.kt$DebugViewModel$8
MagicNumber:EditChannelDialog.kt$16
MagicNumber:EditChannelDialog.kt$32
MagicNumber:EditDeviceProfileDialog.kt$ProfileField.CHANNEL_URL$3
@@ -31,7 +33,7 @@
ReturnCount:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshPacket)
TooGenericExceptionCaught:DebugViewModel.kt$DebugViewModel$e: Exception
TooGenericExceptionCaught:LanguageUtils.kt$LanguageUtils$e: Exception
- TooGenericExceptionCaught:RadioConfigViewModel.kt$RadioConfigViewModel$ex: Exception
TooManyFunctions:RadioConfigViewModel.kt$RadioConfigViewModel : ViewModel
+ UnusedPrivateProperty:RadioConfigViewModel.kt$RadioConfigViewModel$private val locationRepository: LocationRepository
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/AboutScreen.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/AboutScreen.kt
similarity index 100%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/AboutScreen.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/AboutScreen.kt
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/AdministrationScreen.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/AdministrationScreen.kt
similarity index 97%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/AdministrationScreen.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/AdministrationScreen.kt
index 477f1b5b4..d63620ff7 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/AdministrationScreen.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/AdministrationScreen.kt
@@ -37,7 +37,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.model.Node
@@ -58,7 +57,7 @@ import org.meshtastic.feature.settings.radio.component.ShutdownConfirmationDialo
import org.meshtastic.feature.settings.radio.component.WarningDialog
@Composable
-fun AdministrationScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
+fun AdministrationScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val destNode by viewModel.destNode.collectAsStateWithLifecycle()
val enabled = state.connected && !state.responseState.isWaiting()
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/DeviceConfigurationScreen.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/DeviceConfigurationScreen.kt
similarity index 94%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/DeviceConfigurationScreen.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/DeviceConfigurationScreen.kt
index 61d551d8e..0c3ec91f7 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/DeviceConfigurationScreen.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/DeviceConfigurationScreen.kt
@@ -26,7 +26,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.navigation.Route
@@ -40,11 +39,7 @@ import org.meshtastic.feature.settings.navigation.ConfigRoute
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
@Composable
-fun DeviceConfigurationScreen(
- viewModel: RadioConfigViewModel = hiltViewModel(),
- onBack: () -> Unit,
- onNavigate: (Route) -> Unit,
-) {
+fun DeviceConfigurationScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, onNavigate: (Route) -> Unit) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val destNode by viewModel.destNode.collectAsStateWithLifecycle()
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/ModuleConfigurationScreen.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/ModuleConfigurationScreen.kt
similarity index 95%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/ModuleConfigurationScreen.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/ModuleConfigurationScreen.kt
index 788292573..faf2f792e 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/ModuleConfigurationScreen.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/ModuleConfigurationScreen.kt
@@ -27,7 +27,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.navigation.Route
@@ -42,8 +41,8 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel
@Composable
fun ModuleConfigurationScreen(
- viewModel: RadioConfigViewModel = hiltViewModel(),
- excludedModulesUnlocked: Boolean = false,
+ viewModel: RadioConfigViewModel,
+ excludedModulesUnlocked: Boolean,
onBack: () -> Unit,
onNavigate: (Route) -> Unit,
) {
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsScreen.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/SettingsScreen.kt
similarity index 100%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsScreen.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/SettingsScreen.kt
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/component/AppInfoSection.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/AppInfoSection.kt
similarity index 100%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/component/AppInfoSection.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/AppInfoSection.kt
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/component/AppearanceSection.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/AppearanceSection.kt
similarity index 100%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/component/AppearanceSection.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/AppearanceSection.kt
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/component/HomoglyphSetting.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/HomoglyphSetting.kt
similarity index 100%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/component/HomoglyphSetting.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/HomoglyphSetting.kt
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/component/PersistenceSection.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/PersistenceSection.kt
similarity index 100%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/component/PersistenceSection.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/PersistenceSection.kt
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/component/PrivacySection.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/PrivacySection.kt
similarity index 100%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/component/PrivacySection.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/PrivacySection.kt
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/Debug.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/debugging/Debug.kt
similarity index 99%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/Debug.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/debugging/Debug.kt
index ea91f78fe..d0328e23c 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/Debug.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/debugging/Debug.kt
@@ -74,7 +74,6 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.touchlab.kermit.Logger
import kotlinx.collections.immutable.toImmutableList
@@ -125,7 +124,7 @@ private var redactedKeys: List = listOf("session_passkey", "private_key"
@Suppress("LongMethod")
@Composable
-fun DebugScreen(onNavigateUp: () -> Unit, viewModel: DebugViewModel = hiltViewModel()) {
+fun DebugScreen(onNavigateUp: () -> Unit, viewModel: DebugViewModel) {
val listState = rememberLazyListState()
val logs by viewModel.meshLog.collectAsStateWithLifecycle()
val searchState by viewModel.searchState.collectAsStateWithLifecycle()
@@ -194,7 +193,8 @@ fun DebugScreen(onNavigateUp: () -> Unit, viewModel: DebugViewModel = hiltViewMo
targetValue = if (!listState.isScrollInProgress) 1.0f else 0f,
label = "alpha",
)
- DebugSearchStateviewModelDefaults(
+ DebugSearchStateWithViewModel(
+ viewModel = viewModel,
modifier = Modifier.graphicsLayer(alpha = animatedAlpha),
searchState = searchState,
filterTexts = filterTexts,
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugSearch.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/debugging/DebugSearch.kt
similarity index 98%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugSearch.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/debugging/DebugSearch.kt
index f1db9005b..430c935e9 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugSearch.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/debugging/DebugSearch.kt
@@ -50,7 +50,6 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.debug_default_search
@@ -208,7 +207,8 @@ fun DebugSearchState(
}
@Composable
-fun DebugSearchStateviewModelDefaults(
+fun DebugSearchStateWithViewModel(
+ viewModel: DebugViewModel,
modifier: Modifier = Modifier,
searchState: SearchState,
filterTexts: List,
@@ -218,7 +218,6 @@ fun DebugSearchStateviewModelDefaults(
onFilterModeChange: (FilterMode) -> Unit,
onExportLogs: (() -> Unit)? = null,
) {
- val viewModel: DebugViewModel = hiltViewModel()
DebugSearchState(
modifier = modifier,
searchState = searchState,
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsScreen.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsScreen.kt
similarity index 98%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsScreen.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsScreen.kt
index 0c8737e52..0a6b4d814 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsScreen.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsScreen.kt
@@ -45,7 +45,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.resources.Res
@@ -63,7 +62,7 @@ import org.meshtastic.core.resources.filter_words_summary
import org.meshtastic.core.ui.component.MainAppBar
@Composable
-fun FilterSettingsScreen(viewModel: FilterSettingsViewModel = hiltViewModel(), onBack: () -> Unit) {
+fun FilterSettingsScreen(viewModel: FilterSettingsViewModel, onBack: () -> Unit) {
val filterEnabled by viewModel.filterEnabled.collectAsStateWithLifecycle()
val filterWords by viewModel.filterWords.collectAsStateWithLifecycle()
var newWord by remember { mutableStateOf("") }
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavUtils.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavUtils.kt
similarity index 95%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavUtils.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavUtils.kt
index 59b533579..ae0e03a15 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavUtils.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavUtils.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025 Meshtastic LLC
+ * 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
@@ -14,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-
package org.meshtastic.feature.settings.navigation
import org.meshtastic.core.navigation.Route
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseScreen.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseScreen.kt
similarity index 98%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseScreen.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseScreen.kt
index daa04a79d..b8bf1715a 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseScreen.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseScreen.kt
@@ -37,7 +37,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.model.Node
@@ -55,7 +54,7 @@ import org.meshtastic.core.ui.component.NodeChip
* nodes to be deleted updates automatically as filter criteria change.
*/
@Composable
-fun CleanNodeDatabaseScreen(viewModel: CleanNodeDatabaseViewModel = hiltViewModel()) {
+fun CleanNodeDatabaseScreen(viewModel: CleanNodeDatabaseViewModel) {
val olderThanDays by viewModel.olderThanDays.collectAsStateWithLifecycle()
val onlyUnknownNodes by viewModel.onlyUnknownNodes.collectAsStateWithLifecycle()
val nodesToDelete by viewModel.nodesToDelete.collectAsStateWithLifecycle()
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelConfigScreen.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelConfigScreen.kt
similarity index 100%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelConfigScreen.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelConfigScreen.kt
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/channel/component/ChannelCard.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/channel/component/ChannelCard.kt
similarity index 100%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/channel/component/ChannelCard.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/channel/component/ChannelCard.kt
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/channel/component/ChannelConfigHeader.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/channel/component/ChannelConfigHeader.kt
similarity index 100%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/channel/component/ChannelConfigHeader.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/channel/component/ChannelConfigHeader.kt
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/channel/component/ChannelLegend.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/channel/component/ChannelLegend.kt
similarity index 100%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/channel/component/ChannelLegend.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/channel/component/ChannelLegend.kt
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/channel/component/EditChannelDialog.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/channel/component/EditChannelDialog.kt
similarity index 100%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/channel/component/EditChannelDialog.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/channel/component/EditChannelDialog.kt
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/AmbientLightingConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/AmbientLightingConfigItemList.kt
similarity index 96%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/AmbientLightingConfigItemList.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/AmbientLightingConfigItemList.kt
index fe6efefe9..f3b96fa52 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/AmbientLightingConfigItemList.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/AmbientLightingConfigItemList.kt
@@ -22,7 +22,6 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalFocusManager
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.resources.Res
@@ -40,7 +39,7 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.proto.ModuleConfig
@Composable
-fun AmbientLightingConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
+fun AmbientLightingConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val ambientLightingConfig = state.moduleConfig.ambient_lighting ?: ModuleConfig.AmbientLightingConfig()
val formState = rememberConfigState(initialValue = ambientLightingConfig)
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/AudioConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/AudioConfigItemList.kt
similarity index 97%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/AudioConfigItemList.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/AudioConfigItemList.kt
index 9b009352b..c03dd0c3b 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/AudioConfigItemList.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/AudioConfigItemList.kt
@@ -22,7 +22,6 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalFocusManager
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.resources.Res
@@ -43,7 +42,7 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.proto.ModuleConfig
@Composable
-fun AudioConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
+fun AudioConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val audioConfig = state.moduleConfig.audio ?: ModuleConfig.AudioConfig()
val formState = rememberConfigState(initialValue = audioConfig)
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/BluetoothConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/BluetoothConfigItemList.kt
similarity index 96%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/BluetoothConfigItemList.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/BluetoothConfigItemList.kt
index f05efd1f8..43eaee5dc 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/BluetoothConfigItemList.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/BluetoothConfigItemList.kt
@@ -22,7 +22,6 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalFocusManager
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.resources.Res
@@ -39,7 +38,7 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.proto.Config
@Composable
-fun BluetoothConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
+fun BluetoothConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val bluetoothConfig = state.radioConfig.bluetooth ?: Config.BluetoothConfig()
val formState = rememberConfigState(initialValue = bluetoothConfig)
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/CannedMessageConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/CannedMessageConfigItemList.kt
similarity index 98%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/CannedMessageConfigItemList.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/CannedMessageConfigItemList.kt
index e96e00f0a..a53a022ae 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/CannedMessageConfigItemList.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/CannedMessageConfigItemList.kt
@@ -28,7 +28,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.resources.Res
@@ -54,7 +53,7 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.proto.ModuleConfig
@Composable
-fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
+fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val cannedMessageConfig = state.moduleConfig.canned_message ?: ModuleConfig.CannedMessageConfig()
val messages = state.cannedMessageMessages
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/ConfigState.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/ConfigState.kt
similarity index 100%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/ConfigState.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/ConfigState.kt
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/DetectionSensorConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/DetectionSensorConfigItemList.kt
similarity index 97%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/DetectionSensorConfigItemList.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/DetectionSensorConfigItemList.kt
index e6c8d9a17..4f91e4d40 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/DetectionSensorConfigItemList.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/DetectionSensorConfigItemList.kt
@@ -26,7 +26,6 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.resources.Res
@@ -51,7 +50,7 @@ import org.meshtastic.feature.settings.util.toDisplayString
import org.meshtastic.proto.ModuleConfig
@Composable
-fun DetectionSensorConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
+fun DetectionSensorConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val detectionSensorConfig = state.moduleConfig.detection_sensor ?: ModuleConfig.DetectionSensorConfig()
val formState = rememberConfigState(initialValue = detectionSensorConfig)
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigItemList.kt
similarity index 99%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigItemList.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigItemList.kt
index d2151165f..5a13cacd8 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigItemList.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigItemList.kt
@@ -58,7 +58,6 @@ import androidx.compose.ui.text.fromHtml
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import no.nordicsemi.android.common.core.registerReceiver
import org.jetbrains.compose.resources.StringResource
@@ -155,7 +154,7 @@ private val Config.DeviceConfig.RebroadcastMode.description: StringResource
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
-fun DeviceConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
+fun DeviceConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val deviceConfig = state.radioConfig.device ?: Config.DeviceConfig()
val formState = rememberConfigState(initialValue = deviceConfig)
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/DisplayConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/DisplayConfigItemList.kt
similarity index 98%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/DisplayConfigItemList.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/DisplayConfigItemList.kt
index a7f05cb6b..1e8e658db 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/DisplayConfigItemList.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/DisplayConfigItemList.kt
@@ -21,7 +21,6 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.resources.Res
@@ -58,7 +57,7 @@ import org.meshtastic.feature.settings.util.toDisplayString
import org.meshtastic.proto.Config
@Composable
-fun DisplayConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
+fun DisplayConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val displayConfig = state.radioConfig.display ?: Config.DisplayConfig()
val formState = rememberConfigState(initialValue = displayConfig)
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/EditDeviceProfileDialog.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/EditDeviceProfileDialog.kt
similarity index 100%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/EditDeviceProfileDialog.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/EditDeviceProfileDialog.kt
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/ExternalNotificationConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/ExternalNotificationConfigItemList.kt
similarity index 99%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/ExternalNotificationConfigItemList.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/ExternalNotificationConfigItemList.kt
index 00800c844..d5ae5aa33 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/ExternalNotificationConfigItemList.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/ExternalNotificationConfigItemList.kt
@@ -41,7 +41,6 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.touchlab.kermit.Logger
import org.jetbrains.compose.resources.stringResource
@@ -87,7 +86,7 @@ private const val MAX_RINGTONE_SIZE = 230
fun ExternalNotificationConfigScreen(
onBack: () -> Unit,
modifier: Modifier = Modifier,
- viewModel: RadioConfigViewModel = hiltViewModel(),
+ viewModel: RadioConfigViewModel,
) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val extNotificationConfig = state.moduleConfig.external_notification ?: ModuleConfig.ExternalNotificationConfig()
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/LoRaConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/LoRaConfigItemList.kt
similarity index 100%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/LoRaConfigItemList.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/LoRaConfigItemList.kt
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/LoadingOverlay.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/LoadingOverlay.kt
similarity index 100%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/LoadingOverlay.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/LoadingOverlay.kt
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/MQTTConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/MQTTConfigItemList.kt
similarity index 98%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/MQTTConfigItemList.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/MQTTConfigItemList.kt
index 47c98eaf8..92c72ff54 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/MQTTConfigItemList.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/MQTTConfigItemList.kt
@@ -27,7 +27,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.resources.Res
@@ -52,7 +51,7 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.proto.ModuleConfig
@Composable
-fun MQTTConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
+fun MQTTConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val destNode by viewModel.destNode.collectAsStateWithLifecycle()
val destNum = destNode?.num
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/MapReportingPreference.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/MapReportingPreference.kt
similarity index 100%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/MapReportingPreference.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/MapReportingPreference.kt
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/NeighborInfoConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/NeighborInfoConfigItemList.kt
similarity index 96%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/NeighborInfoConfigItemList.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/NeighborInfoConfigItemList.kt
index 4a2944195..ff2e6069a 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/NeighborInfoConfigItemList.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/NeighborInfoConfigItemList.kt
@@ -22,7 +22,6 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalFocusManager
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.resources.Res
@@ -39,7 +38,7 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.proto.ModuleConfig
@Composable
-fun NeighborInfoConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
+fun NeighborInfoConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val neighborInfoConfig = state.moduleConfig.neighbor_info ?: ModuleConfig.NeighborInfoConfig()
val formState = rememberConfigState(initialValue = neighborInfoConfig)
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/NetworkConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/NetworkConfigItemList.kt
similarity index 99%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/NetworkConfigItemList.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/NetworkConfigItemList.kt
index edb4a4950..b9373c6fe 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/NetworkConfigItemList.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/NetworkConfigItemList.kt
@@ -37,7 +37,6 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.barcode.extractWifiCredentials
@@ -91,7 +90,7 @@ private fun ScanErrorDialog(onDismiss: () -> Unit = {}) =
MeshtasticDialog(titleRes = Res.string.error, messageRes = Res.string.wifi_qr_code_error, onDismiss = onDismiss)
@Composable
-fun NetworkConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
+fun NetworkConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val networkConfig = state.radioConfig.network ?: Config.NetworkConfig()
val formState = rememberConfigState(initialValue = networkConfig)
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/NodeActionButton.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/NodeActionButton.kt
similarity index 98%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/NodeActionButton.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/NodeActionButton.kt
index 804ae8f4a..fe9675e6d 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/NodeActionButton.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/NodeActionButton.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025 Meshtastic LLC
+ * 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
@@ -14,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-
package org.meshtastic.feature.settings.radio.component
import androidx.compose.foundation.layout.Row
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/PacketResponseStateDialog.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/PacketResponseStateDialog.kt
similarity index 100%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/PacketResponseStateDialog.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/PacketResponseStateDialog.kt
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/PaxcounterConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/PaxcounterConfigItemList.kt
similarity index 96%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/PaxcounterConfigItemList.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/PaxcounterConfigItemList.kt
index b268bbece..68c7322f6 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/PaxcounterConfigItemList.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/PaxcounterConfigItemList.kt
@@ -23,7 +23,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalFocusManager
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.resources.Res
@@ -43,7 +42,7 @@ import org.meshtastic.feature.settings.util.toDisplayString
import org.meshtastic.proto.ModuleConfig
@Composable
-fun PaxcounterConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
+fun PaxcounterConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val paxcounterConfig = state.moduleConfig.paxcounter ?: ModuleConfig.PaxcounterConfig()
val formState = rememberConfigState(initialValue = paxcounterConfig)
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/PositionConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/PositionConfigItemList.kt
similarity index 98%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/PositionConfigItemList.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/PositionConfigItemList.kt
index 7b33f74ac..c0c34b16b 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/PositionConfigItemList.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/PositionConfigItemList.kt
@@ -34,7 +34,6 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalFocusManager
import androidx.core.location.LocationCompat
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.launch
import no.nordicsemi.android.common.permissions.ble.RequireLocation
@@ -79,7 +78,7 @@ import org.meshtastic.proto.Config
@Composable
@Suppress("LongMethod", "CyclomaticComplexMethod")
-fun PositionConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
+fun PositionConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val coroutineScope = rememberCoroutineScope()
var phoneLocation: Location? by remember { mutableStateOf(null) }
@@ -257,7 +256,9 @@ fun PositionConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBa
enabled = state.connected && !isLocationRequiredAndDisabled,
onClick = {
@SuppressLint("MissingPermission")
- coroutineScope.launch { phoneLocation = viewModel.getCurrentLocation() }
+ coroutineScope.launch {
+ phoneLocation = viewModel.getCurrentLocation() as? android.location.Location
+ }
},
) {
Text(text = stringResource(Res.string.position_config_set_fixed_from_phone))
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/PowerConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/PowerConfigItemList.kt
similarity index 98%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/PowerConfigItemList.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/PowerConfigItemList.kt
index 6b6b349c1..4184a141e 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/PowerConfigItemList.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/PowerConfigItemList.kt
@@ -23,7 +23,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalFocusManager
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.resources.Res
@@ -48,7 +47,7 @@ import org.meshtastic.feature.settings.util.toDisplayString
import org.meshtastic.proto.Config
@Composable
-fun PowerConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
+fun PowerConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val powerConfig = state.radioConfig.power ?: Config.PowerConfig()
val formState = rememberConfigState(initialValue = powerConfig)
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/RadioConfigScreenList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/RadioConfigScreenList.kt
similarity index 100%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/RadioConfigScreenList.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/RadioConfigScreenList.kt
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/RangeTestConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/RangeTestConfigItemList.kt
similarity index 96%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/RangeTestConfigItemList.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/RangeTestConfigItemList.kt
index ea78843d0..1bd6ebeb6 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/RangeTestConfigItemList.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/RangeTestConfigItemList.kt
@@ -21,7 +21,6 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.resources.Res
@@ -39,7 +38,7 @@ import org.meshtastic.feature.settings.util.toDisplayString
import org.meshtastic.proto.ModuleConfig
@Composable
-fun RangeTestConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
+fun RangeTestConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val rangeTestConfig = state.moduleConfig.range_test ?: ModuleConfig.RangeTestConfig()
val formState = rememberConfigState(initialValue = rangeTestConfig)
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/RemoteHardwareConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/RemoteHardwareConfigItemList.kt
similarity index 96%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/RemoteHardwareConfigItemList.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/RemoteHardwareConfigItemList.kt
index 1fba75ddb..b245f5561 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/RemoteHardwareConfigItemList.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/RemoteHardwareConfigItemList.kt
@@ -22,7 +22,6 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalFocusManager
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.resources.Res
@@ -38,7 +37,7 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.proto.ModuleConfig
@Composable
-fun RemoteHardwareConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
+fun RemoteHardwareConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val remoteHardwareConfig = state.moduleConfig.remote_hardware ?: ModuleConfig.RemoteHardwareConfig()
val formState = rememberConfigState(initialValue = remoteHardwareConfig)
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/SecurityConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/SecurityConfigItemList.kt
similarity index 98%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/SecurityConfigItemList.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/SecurityConfigItemList.kt
index 561048393..94627644f 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/SecurityConfigItemList.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/SecurityConfigItemList.kt
@@ -35,7 +35,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.unit.dp
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import okio.ByteString
import okio.ByteString.Companion.toByteString
@@ -77,7 +76,7 @@ import java.security.SecureRandom
@Composable
@Suppress("LongMethod")
-fun SecurityConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
+fun SecurityConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val node by viewModel.destNode.collectAsStateWithLifecycle()
val securityConfig = state.radioConfig.security ?: Config.SecurityConfig()
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/SerialConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/SerialConfigItemList.kt
similarity index 97%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/SerialConfigItemList.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/SerialConfigItemList.kt
index 779030aad..5cc441c64 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/SerialConfigItemList.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/SerialConfigItemList.kt
@@ -22,7 +22,6 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalFocusManager
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.resources.Res
@@ -42,7 +41,7 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.proto.ModuleConfig
@Composable
-fun SerialConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
+fun SerialConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val serialConfig = state.moduleConfig.serial ?: ModuleConfig.SerialConfig()
val formState = rememberConfigState(initialValue = serialConfig)
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/ShutdownConfirmationDialog.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/ShutdownConfirmationDialog.kt
similarity index 100%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/ShutdownConfirmationDialog.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/ShutdownConfirmationDialog.kt
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/StatusMessageConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/StatusMessageConfigItemList.kt
similarity index 96%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/StatusMessageConfigItemList.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/StatusMessageConfigItemList.kt
index de0e0b4cc..a81867265 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/StatusMessageConfigItemList.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/StatusMessageConfigItemList.kt
@@ -29,7 +29,6 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.resources.Res
@@ -42,7 +41,7 @@ import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
@Composable
-fun StatusMessageConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
+fun StatusMessageConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val destNode by viewModel.destNode.collectAsStateWithLifecycle()
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/StoreForwardConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/StoreForwardConfigItemList.kt
similarity index 97%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/StoreForwardConfigItemList.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/StoreForwardConfigItemList.kt
index 11a75d37e..4d702c317 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/StoreForwardConfigItemList.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/StoreForwardConfigItemList.kt
@@ -22,7 +22,6 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalFocusManager
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.resources.Res
@@ -41,7 +40,7 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.proto.ModuleConfig
@Composable
-fun StoreForwardConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
+fun StoreForwardConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val storeForwardConfig = state.moduleConfig.store_forward ?: ModuleConfig.StoreForwardConfig()
val formState = rememberConfigState(initialValue = storeForwardConfig)
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/TAKConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/TAKConfigItemList.kt
similarity index 95%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/TAKConfigItemList.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/TAKConfigItemList.kt
index 7da9f7b3c..800ef7042 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/TAKConfigItemList.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/TAKConfigItemList.kt
@@ -21,7 +21,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.graphics.Color
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.model.getColorFrom
@@ -37,7 +36,7 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.proto.ModuleConfig
@Composable
-fun TAKConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
+fun TAKConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val takConfig = state.moduleConfig.tak ?: ModuleConfig.TAKConfig()
val formState = rememberConfigState(initialValue = takConfig)
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/TelemetryConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/TelemetryConfigItemList.kt
similarity index 98%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/TelemetryConfigItemList.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/TelemetryConfigItemList.kt
index 2921adccd..04c74876f 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/TelemetryConfigItemList.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/TelemetryConfigItemList.kt
@@ -21,7 +21,6 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.model.Capabilities
@@ -49,7 +48,7 @@ import org.meshtastic.feature.settings.util.toDisplayString
import org.meshtastic.proto.ModuleConfig
@Composable
-fun TelemetryConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
+fun TelemetryConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val telemetryConfig = state.moduleConfig.telemetry ?: ModuleConfig.TelemetryConfig()
val formState = rememberConfigState(initialValue = telemetryConfig)
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/TrafficManagementConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/TrafficManagementConfigItemList.kt
similarity index 99%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/TrafficManagementConfigItemList.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/TrafficManagementConfigItemList.kt
index c05ff42d1..4fea68b9d 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/TrafficManagementConfigItemList.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/TrafficManagementConfigItemList.kt
@@ -23,7 +23,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalFocusManager
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.resources.Res
@@ -51,7 +50,7 @@ import org.meshtastic.proto.ModuleConfig
@Suppress("LongMethod")
@Composable
-fun TrafficManagementConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
+fun TrafficManagementConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val tmConfig = state.moduleConfig.traffic_management ?: ModuleConfig.TrafficManagementConfig()
val formState = rememberConfigState(initialValue = tmConfig)
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/UserConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/UserConfigItemList.kt
similarity index 97%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/UserConfigItemList.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/UserConfigItemList.kt
index 55ae3ab75..9599d5f16 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/UserConfigItemList.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/UserConfigItemList.kt
@@ -26,7 +26,6 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.model.Capabilities
@@ -49,7 +48,7 @@ import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
@Composable
-fun UserConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
+fun UserConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val userConfig = state.userConfig
val formState = rememberConfigState(initialValue = userConfig)
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/WarningDialog.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/WarningDialog.kt
similarity index 100%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/WarningDialog.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/WarningDialog.kt
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/util/FixedUpdateIntervals.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/util/FixedUpdateIntervals.kt
similarity index 100%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/util/FixedUpdateIntervals.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/util/FixedUpdateIntervals.kt
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/util/Formatting.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/util/Formatting.kt
similarity index 96%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/util/Formatting.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/util/Formatting.kt
index c56946c1d..ad2444799 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/util/Formatting.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/util/Formatting.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025 Meshtastic LLC
+ * 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
@@ -14,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-
package org.meshtastic.feature.settings.util
import androidx.compose.runtime.Composable
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/util/LanguageUtils.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/util/LanguageUtils.kt
similarity index 66%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/util/LanguageUtils.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/util/LanguageUtils.kt
index 2553d2561..64d0295b4 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/util/LanguageUtils.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/util/LanguageUtils.kt
@@ -19,9 +19,7 @@ package org.meshtastic.feature.settings.util
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
-import androidx.compose.ui.platform.LocalResources
import androidx.core.os.LocaleListCompat
-import co.touchlab.kermit.Logger
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.fr_HT
@@ -29,7 +27,6 @@ import org.meshtastic.core.resources.preferences_system_default
import org.meshtastic.core.resources.pt_BR
import org.meshtastic.core.resources.zh_CN
import org.meshtastic.core.resources.zh_TW
-import org.xmlpull.v1.XmlPullParser
import java.util.Locale
object LanguageUtils {
@@ -50,32 +47,54 @@ object LanguageUtils {
)
}
- /** Using locales_config.xml, maps language tags to their localized language names (e.g.: "en" -> "English") */
- @Suppress("CyclomaticComplexMethod")
+ /** Using a hardcoded list, maps language tags to their localized language names (e.g.: "en" -> "English") */
+ @Suppress("CyclomaticComplexMethod", "LongMethod")
@Composable
fun languageMap(): Map {
- val resources = LocalResources.current
- val languageTags =
- remember(resources) {
- buildList {
- add(SYSTEM_DEFAULT)
-
- try {
- resources.getXml(org.meshtastic.feature.settings.R.xml.locales_config).use { parser ->
- while (parser.eventType != XmlPullParser.END_DOCUMENT) {
- if (parser.eventType == XmlPullParser.START_TAG && parser.name == "locale") {
- val languageTag =
- parser.getAttributeValue("http://schemas.android.com/apk/res/android", "name")
- languageTag?.let { add(it) }
- }
- parser.next()
- }
- }
- } catch (e: Exception) {
- Logger.e { "Error parsing locale_config.xml: ${e.message}" }
- }
- }
- }
+ val languageTags = remember {
+ listOf(
+ SYSTEM_DEFAULT,
+ "en",
+ "ar",
+ "bg",
+ "ca",
+ "cs",
+ "de",
+ "el",
+ "es",
+ "et",
+ "fi",
+ "fr",
+ "ga",
+ "gl",
+ "hr",
+ "ht",
+ "hu",
+ "is",
+ "it",
+ "iw",
+ "ja",
+ "ko",
+ "lt",
+ "nl",
+ "nb",
+ "pl",
+ "pt",
+ "pt-BR",
+ "ro",
+ "ru",
+ "sk",
+ "sl",
+ "sq",
+ "sr",
+ "srp",
+ "sv",
+ "tr",
+ "uk",
+ "zh-CN",
+ "zh-TW",
+ )
+ }
return languageTags.associateWith { languageTag ->
when (languageTag) {
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/util/SettingsIntervals.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/util/SettingsIntervals.kt
similarity index 95%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/util/SettingsIntervals.kt
rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/util/SettingsIntervals.kt
index 779e8b878..66dd171de 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/util/SettingsIntervals.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/util/SettingsIntervals.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025 Meshtastic LLC
+ * 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
@@ -14,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-
package org.meshtastic.feature.settings.util
val gpioPins = (0..48).map { it to "Pin $it" }
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt
similarity index 79%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt
rename to feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt
index e609b2565..77acc7d98 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt
+++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt
@@ -16,12 +16,8 @@
*/
package org.meshtastic.feature.settings
-import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import co.touchlab.kermit.Logger
-import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -30,10 +26,7 @@ import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
import okio.BufferedSink
-import okio.buffer
-import okio.sink
import org.meshtastic.core.common.BuildConfigProvider
import org.meshtastic.core.common.database.DatabaseManager
import org.meshtastic.core.domain.usecase.settings.ExportDataUseCase
@@ -53,16 +46,9 @@ import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.repository.UiPrefs
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.proto.LocalConfig
-import java.io.FileNotFoundException
-import java.io.FileOutputStream
-import javax.inject.Inject
@Suppress("LongParameterList", "TooManyFunctions")
-@HiltViewModel
-class SettingsViewModel
-@Inject
-constructor(
- private val app: android.app.Application,
+open class SettingsViewModel(
radioConfigRepository: RadioConfigRepository,
private val radioController: RadioController,
private val nodeRepository: NodeRepository,
@@ -163,32 +149,15 @@ constructor(
/**
* Export all persisted packet data to a CSV file at the given URI.
*
- * The CSV will include all packets, or only those matching the given port number if specified. Each row contains:
- * date, time, sender node number, sender name, sender latitude, sender longitude, receiver latitude, receiver
- * longitude, receiver elevation, received SNR, distance, hop limit, and payload.
- *
* @param uri The destination URI for the CSV file.
* @param filterPortnum If provided, only packets with this port number will be exported.
*/
- @Suppress("detekt:CyclomaticComplexMethod", "detekt:LongMethod")
- fun saveDataCsv(uri: Uri, filterPortnum: Int? = null) {
- viewModelScope.launch {
- val myNodeNum = myNodeNum ?: return@launch
- writeToUri(uri) { writer -> exportDataUseCase(writer, myNodeNum, filterPortnum) }
- }
+ open fun saveDataCsv(uri: Any, filterPortnum: Int? = null) {
+ // To be implemented in platform-specific subclass
}
- private suspend inline fun writeToUri(uri: Uri, crossinline block: suspend (BufferedSink) -> Unit) {
- withContext(Dispatchers.IO) {
- try {
- app.contentResolver.openFileDescriptor(uri, "wt")?.use { parcelFileDescriptor ->
- FileOutputStream(parcelFileDescriptor.fileDescriptor).sink().buffer().use { writer ->
- block.invoke(writer)
- }
- }
- } catch (ex: FileNotFoundException) {
- Logger.e { "Can't write file error: ${ex.message}" }
- }
- }
+ protected suspend fun performDataExport(writer: BufferedSink, filterPortnum: Int?) {
+ val myNodeNum = myNodeNum ?: return
+ exportDataUseCase(writer, myNodeNum, filterPortnum)
}
}
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/component/ExpressiveSection.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/ExpressiveSection.kt
similarity index 100%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/component/ExpressiveSection.kt
rename to feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/ExpressiveSection.kt
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugFilters.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugFilters.kt
similarity index 99%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugFilters.kt
rename to feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugFilters.kt
index 9a9addff3..09185904c 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugFilters.kt
+++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugFilters.kt
@@ -290,8 +290,3 @@ fun DebugActiveFilters(
}
}
}
-
-enum class FilterMode {
- OR,
- AND,
-}
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt
similarity index 86%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt
rename to feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt
index deccdc951..0f4c889d0 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt
+++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt
@@ -20,7 +20,6 @@ import androidx.compose.runtime.Immutable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
-import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@@ -33,9 +32,8 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import org.meshtastic.core.common.util.DateFormatter
import org.meshtastic.core.common.util.nowInstant
-import org.meshtastic.core.common.util.toDate
-import org.meshtastic.core.common.util.toInstant
import org.meshtastic.core.database.entity.MeshLog
import org.meshtastic.core.database.entity.Packet
import org.meshtastic.core.model.getTracerouteResponse
@@ -62,9 +60,6 @@ import org.meshtastic.proto.StoreForwardPlusPlus
import org.meshtastic.proto.Telemetry
import org.meshtastic.proto.User
import org.meshtastic.proto.Waypoint
-import java.text.DateFormat
-import java.util.Locale
-import javax.inject.Inject
data class SearchMatch(val logIndex: Int, val start: Int, val end: Int, val field: String)
@@ -75,6 +70,11 @@ data class SearchState(
val hasMatches: Boolean = false,
)
+enum class FilterMode {
+ AND,
+ OR,
+}
+
// --- Search and Filter Managers ---
class LogSearchManager {
data class SearchMatch(val logIndex: Int, val start: Int, val end: Int, val field: String)
@@ -141,24 +141,24 @@ class LogSearchManager {
return filteredLogs
.flatMapIndexed { logIndex, log ->
searchText.split(" ").flatMap { term ->
- val escapedTerm = Regex.escape(term)
+ val escapedTerm = term // Simple regex escape or just use contains
val regex = escapedTerm.toRegex(RegexOption.IGNORE_CASE)
val messageMatches =
- regex.findAll(log.logMessage).map { match ->
- SearchMatch(logIndex, match.range.first, match.range.last, "message")
+ regex.findAll(log.logMessage).map {
+ SearchMatch(logIndex, it.range.first, it.range.last, "message")
}
val typeMatches =
- regex.findAll(log.messageType).map { match ->
- SearchMatch(logIndex, match.range.first, match.range.last, "type")
+ regex.findAll(log.messageType).map {
+ SearchMatch(logIndex, it.range.first, it.range.last, "type")
}
val dateMatches =
- regex.findAll(log.formattedReceivedDate).map { match ->
- SearchMatch(logIndex, match.range.first, match.range.last, "date")
+ regex.findAll(log.formattedReceivedDate).map {
+ SearchMatch(logIndex, it.range.first, it.range.last, "date")
}
val decodedPayloadMatches =
- log.decodedPayload?.let { decoded ->
- regex.findAll(decoded).map { match ->
- SearchMatch(logIndex, match.range.first, match.range.last, "decodedPayload")
+ log.decodedPayload?.let {
+ regex.findAll(it).map {
+ SearchMatch(logIndex, it.range.first, it.range.last, "decodedPayload")
}
} ?: emptySequence()
messageMatches + typeMatches + dateMatches + decodedPayloadMatches
@@ -189,35 +189,30 @@ class LogFilterManager {
filterMode: FilterMode,
): List {
if (filterTexts.isEmpty()) return logs
- return logs.filter { log ->
+ return logs.filter { logItem ->
when (filterMode) {
FilterMode.OR ->
- filterTexts.any { filterText ->
- log.logMessage.contains(filterText, ignoreCase = true) ||
- log.messageType.contains(filterText, ignoreCase = true) ||
- log.formattedReceivedDate.contains(filterText, ignoreCase = true) ||
- (log.decodedPayload?.contains(filterText, ignoreCase = true) == true)
+ filterTexts.any {
+ it.contains(logItem.logMessage, ignoreCase = true) ||
+ it.contains(logItem.messageType, ignoreCase = true) ||
+ it.contains(logItem.formattedReceivedDate, ignoreCase = true) ||
+ (logItem.decodedPayload?.contains(it, ignoreCase = true) == true)
}
FilterMode.AND ->
- filterTexts.all { filterText ->
- log.logMessage.contains(filterText, ignoreCase = true) ||
- log.messageType.contains(filterText, ignoreCase = true) ||
- log.formattedReceivedDate.contains(filterText, ignoreCase = true) ||
- (log.decodedPayload?.contains(filterText, ignoreCase = true) == true)
+ filterTexts.all {
+ it.contains(logItem.logMessage, ignoreCase = true) ||
+ it.contains(logItem.messageType, ignoreCase = true) ||
+ it.contains(logItem.formattedReceivedDate, ignoreCase = true) ||
+ (logItem.decodedPayload?.contains(it, ignoreCase = true) == true)
}
}
}
}
}
-private const val HEX_FORMAT = "%02x"
-
@Suppress("TooManyFunctions")
-@HiltViewModel
-class DebugViewModel
-@Inject
-constructor(
+open class DebugViewModel(
private val meshLogRepository: MeshLogRepository,
private val nodeRepository: NodeRepository,
private val meshLogPrefs: MeshLogPrefs,
@@ -304,13 +299,13 @@ constructor(
}
private fun toUiState(databaseLogs: List) = databaseLogs
- .map { log ->
+ .map {
UiMeshLog(
- uuid = log.uuid,
- messageType = log.message_type,
- formattedReceivedDate = TIME_FORMAT.format(log.received_date.toInstant().toDate()),
- logMessage = annotateMeshLogMessage(log),
- decodedPayload = decodePayloadFromMeshLog(log),
+ uuid = it.uuid,
+ messageType = it.message_type,
+ formattedReceivedDate = DateFormatter.formatDateTime(it.received_date),
+ logMessage = annotateMeshLogMessage(it),
+ decodedPayload = decodePayloadFromMeshLog(it),
)
}
.toImmutableList()
@@ -387,18 +382,21 @@ constructor(
private fun StringBuilder.annotateNodeId(nodeId: Int): Boolean {
val nodeIdStr = nodeId.toUInt().toString()
// Only match if whitespace before and after
- val regex = Regex("""(?<=\s|^)${Regex.escape(nodeIdStr)}(?=\s|$)""")
+ val regex = Regex("""(?<=\s|^)${Regex.escape(nodeIdStr)}(?=\s|$)""", RegexOption.DOT_MATCHES_ALL)
regex.find(this)?.let { _ ->
- regex.findAll(this).toList().asReversed().forEach { match ->
- val idx = match.range.last + 1
- insert(idx, " (${nodeId.asNodeId()})")
+ regex.findAll(this).toList().asReversed().forEach {
+ val idx = it.range.last + 1
+ insert(idx, " (${nodeId.toHex(8)})")
}
return true
}
return false
}
- private fun Int.asNodeId(): String = "!%08x".format(Locale.getDefault(), this)
+ protected open fun Int.toHex(length: Int): String {
+ // Platform specific hex implementation
+ return "!$this"
+ }
fun requestDeleteAllLogs() {
alertManager.showAlert(
@@ -419,20 +417,16 @@ constructor(
val decodedPayload: String? = null,
)
- companion object {
- private val TIME_FORMAT = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM)
- }
-
val presetFilters: List
get() = buildList {
// Our address if available
- nodeRepository.myNodeInfo.value?.myNodeNum?.let { add("!%08x".format(it)) }
+ nodeRepository.myNodeInfo.value?.myNodeNum?.let { add(it.toHex(8)) }
// broadcast
add("!ffffffff")
// decoded
add("decoded")
// today (locale-dependent short date format)
- add(DateFormat.getDateInstance(DateFormat.SHORT).format(nowInstant.toDate()))
+ add(DateFormatter.formatShortDate(nowInstant.toEpochMilliseconds()))
// Each app name
addAll(PortNum.entries.map { it.name })
}
@@ -464,7 +458,7 @@ constructor(
when (portnumValue) {
PortNum.TEXT_MESSAGE_APP.value,
PortNum.ALERT_APP.value,
- -> payload.toString(Charsets.UTF_8)
+ -> payload.decodeToString()
PortNum.POSITION_APP.value ->
Position.ADAPTER.decodeOrNull(payload)?.let { Position.ADAPTER.toReadableString(it) }
?: "Failed to decode Position"
@@ -495,17 +489,19 @@ constructor(
} ?: "Failed to decode StoreForwardPlusPlus"
PortNum.NEIGHBORINFO_APP.value -> decodeNeighborInfo(payload)
PortNum.TRACEROUTE_APP.value -> decodeTraceroute(packet, payload)
- else -> payload.joinToString(" ") { HEX_FORMAT.format(it) }
+ else -> payload.joinToString(" ") { it.toHex() }
}
} catch (e: Exception) {
"Failed to decode payload: ${e.message}"
}
}
+ protected open fun Byte.toHex(): String = this.toString()
+
private fun formatNodeWithShortName(nodeNum: Int): String {
val user = nodeRepository.nodeDBbyNum.value[nodeNum]?.user
val shortName = user?.short_name?.takeIf { it.isNotEmpty() } ?: ""
- val nodeId = "!%08x".format(nodeNum)
+ val nodeId = nodeNum.toHex(8)
return if (shortName.isNotEmpty()) "$nodeId ($shortName)" else nodeId
}
@@ -518,8 +514,8 @@ constructor(
appendLine(" node_broadcast_interval_secs: ${info.node_broadcast_interval_secs}")
if (info.neighbors.isNotEmpty()) {
appendLine(" neighbors:")
- info.neighbors.forEach { n ->
- appendLine(" - node_id: ${formatNodeWithShortName(n.node_id ?: 0)} snr: ${n.snr}")
+ info.neighbors.forEach {
+ appendLine(" - node_id: ${formatNodeWithShortName(it.node_id ?: 0)} snr: ${it.snr}")
}
}
}
@@ -529,6 +525,6 @@ constructor(
val getUsername: (Int) -> String = { nodeNum -> formatNodeWithShortName(nodeNum) }
return packet.getTracerouteResponse(getUsername)
?: runCatching { RouteDiscovery.ADAPTER.decode(payload).toString() }.getOrNull()
- ?: payload.joinToString(" ") { HEX_FORMAT.format(it) }
+ ?: payload.joinToString(" ") { it.toHex() }
}
}
diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/di/FeatureSettingsModule.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/di/FeatureSettingsModule.kt
new file mode 100644
index 000000000..cc2d81ce8
--- /dev/null
+++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/di/FeatureSettingsModule.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 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 org.meshtastic.feature.settings.di
+
+import org.koin.core.annotation.ComponentScan
+import org.koin.core.annotation.Module
+
+@Module
+@ComponentScan("org.meshtastic.feature.settings")
+class FeatureSettingsModule
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModel.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModel.kt
similarity index 89%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModel.kt
rename to feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModel.kt
index e851b4880..ade5e6373 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModel.kt
+++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModel.kt
@@ -17,21 +17,14 @@
package org.meshtastic.feature.settings.filter
import androidx.lifecycle.ViewModel
-import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.meshtastic.core.repository.FilterPrefs
import org.meshtastic.core.repository.MessageFilter
-import javax.inject.Inject
-@HiltViewModel
-class FilterSettingsViewModel
-@Inject
-constructor(
- private val filterPrefs: FilterPrefs,
- private val messageFilter: MessageFilter,
-) : ViewModel() {
+open class FilterSettingsViewModel(private val filterPrefs: FilterPrefs, private val messageFilter: MessageFilter) :
+ ViewModel() {
private val _filterEnabled = MutableStateFlow(filterPrefs.filterEnabled.value)
val filterEnabled: StateFlow = _filterEnabled.asStateFlow()
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/navigation/ConfigRoute.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/navigation/ConfigRoute.kt
similarity index 100%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/navigation/ConfigRoute.kt
rename to feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/navigation/ConfigRoute.kt
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/navigation/ModuleRoute.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/navigation/ModuleRoute.kt
similarity index 100%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/navigation/ModuleRoute.kt
rename to feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/navigation/ModuleRoute.kt
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseViewModel.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseViewModel.kt
similarity index 96%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseViewModel.kt
rename to feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseViewModel.kt
index 15f1f6d05..2f1f19868 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseViewModel.kt
+++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseViewModel.kt
@@ -18,7 +18,6 @@ package org.meshtastic.feature.settings.radio
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
@@ -31,7 +30,6 @@ import org.meshtastic.core.resources.are_you_sure
import org.meshtastic.core.resources.clean_node_database_confirmation
import org.meshtastic.core.resources.clean_now
import org.meshtastic.core.ui.util.AlertManager
-import javax.inject.Inject
private const val MIN_DAYS_THRESHOLD = 7f
@@ -39,10 +37,7 @@ private const val MIN_DAYS_THRESHOLD = 7f
* ViewModel for [CleanNodeDatabaseScreen]. Manages the state and logic for cleaning the node database based on
* specified criteria. The "older than X days" filter is always active.
*/
-@HiltViewModel
-class CleanNodeDatabaseViewModel
-@Inject
-constructor(
+open class CleanNodeDatabaseViewModel(
private val cleanNodeDatabaseUseCase: CleanNodeDatabaseUseCase,
private val alertManager: AlertManager,
) : ViewModel() {
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfig.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfig.kt
similarity index 100%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfig.kt
rename to feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfig.kt
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt
similarity index 78%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt
rename to feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt
index 2756e8003..57c947724 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt
+++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt
@@ -16,34 +16,20 @@
*/
package org.meshtastic.feature.settings.radio
-import android.Manifest
-import android.app.Application
-import android.content.pm.PackageManager
-import android.location.Location
-import android.net.Uri
-import androidx.annotation.RequiresPermission
-import androidx.core.content.ContextCompat
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.toRoute
import co.touchlab.kermit.Logger
-import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import okio.buffer
-import okio.sink
-import okio.source
import org.jetbrains.compose.resources.StringResource
import org.meshtastic.core.domain.usecase.settings.AdminActionsUseCase
import org.meshtastic.core.domain.usecase.settings.ExportProfileUseCase
@@ -87,8 +73,6 @@ import org.meshtastic.proto.LocalModuleConfig
import org.meshtastic.proto.MeshPacket
import org.meshtastic.proto.ModuleConfig
import org.meshtastic.proto.User
-import java.io.FileOutputStream
-import javax.inject.Inject
/** Data class that represents the current RadioConfig state. */
data class RadioConfigState(
@@ -110,12 +94,8 @@ data class RadioConfigState(
)
@Suppress("LongParameterList")
-@HiltViewModel
-class RadioConfigViewModel
-@Inject
-constructor(
+open class RadioConfigViewModel(
savedStateHandle: SavedStateHandle,
- private val app: Application,
private val radioConfigRepository: RadioConfigRepository,
private val packetRepository: PacketRepository,
private val serviceRepository: ServiceRepository,
@@ -126,9 +106,9 @@ constructor(
private val homoglyphEncodingPrefs: HomoglyphPrefs,
private val toggleAnalyticsUseCase: ToggleAnalyticsUseCase,
private val toggleHomoglyphEncodingUseCase: ToggleHomoglyphEncodingUseCase,
- private val importProfileUseCase: ImportProfileUseCase,
- private val exportProfileUseCase: ExportProfileUseCase,
- private val exportSecurityConfigUseCase: ExportSecurityConfigUseCase,
+ protected val importProfileUseCase: ImportProfileUseCase,
+ protected val exportProfileUseCase: ExportProfileUseCase,
+ protected val exportSecurityConfigUseCase: ExportSecurityConfigUseCase,
private val installProfileUseCase: InstallProfileUseCase,
private val radioConfigUseCase: RadioConfigUseCase,
private val adminActionsUseCase: AdminActionsUseCase,
@@ -166,15 +146,7 @@ constructor(
val currentDeviceProfile
get() = _currentDeviceProfile.value
- @RequiresPermission(Manifest.permission.ACCESS_FINE_LOCATION)
- suspend fun getCurrentLocation(): Location? = if (
- ContextCompat.checkSelfPermission(app, Manifest.permission.ACCESS_FINE_LOCATION) ==
- PackageManager.PERMISSION_GRANTED
- ) {
- locationRepository.getLocations().firstOrNull()
- } else {
- null
- }
+ open suspend fun getCurrentLocation(): Any? = null
init {
nodeRepository.nodeDBbyNum
@@ -254,13 +226,6 @@ constructor(
}
}
- private fun getOwner(destNum: Int) {
- viewModelScope.launch {
- val packetId = radioConfigUseCase.getOwner(destNum)
- registerRequestId(packetId)
- }
- }
-
fun updateChannels(new: List, old: List) {
val destNum = destNode.value?.num ?: return
getChannelList(new, old).forEach { channel ->
@@ -279,13 +244,6 @@ constructor(
_radioConfigState.update { it.copy(channelList = new) }
}
- private fun getChannel(destNum: Int, index: Int) {
- viewModelScope.launch {
- val packetId = radioConfigUseCase.getChannel(destNum, index)
- registerRequestId(packetId)
- }
- }
-
fun setConfig(config: Config) {
val destNum = destNode.value?.num ?: return
viewModelScope.launch {
@@ -309,13 +267,6 @@ constructor(
}
}
- private fun getConfig(destNum: Int, configType: Int) {
- viewModelScope.launch {
- val packetId = radioConfigUseCase.getConfig(destNum, configType)
- registerRequestId(packetId)
- }
- }
-
@Suppress("CyclomaticComplexMethod")
fun setModuleConfig(config: ModuleConfig) {
val destNum = destNode.value?.num ?: return
@@ -349,76 +300,18 @@ constructor(
}
}
- private fun getModuleConfig(destNum: Int, configType: Int) {
- viewModelScope.launch {
- val packetId = radioConfigUseCase.getModuleConfig(destNum, configType)
- registerRequestId(packetId)
- }
- }
-
fun setRingtone(ringtone: String) {
val destNum = destNode.value?.num ?: return
_radioConfigState.update { it.copy(ringtone = ringtone) }
viewModelScope.launch { radioConfigUseCase.setRingtone(destNum, ringtone) }
}
- private fun getRingtone(destNum: Int) {
- viewModelScope.launch {
- val packetId = radioConfigUseCase.getRingtone(destNum)
- registerRequestId(packetId)
- }
- }
-
fun setCannedMessages(messages: String) {
val destNum = destNode.value?.num ?: return
_radioConfigState.update { it.copy(cannedMessageMessages = messages) }
viewModelScope.launch { radioConfigUseCase.setCannedMessages(destNum, messages) }
}
- private fun getCannedMessages(destNum: Int) {
- viewModelScope.launch {
- val packetId = radioConfigUseCase.getCannedMessages(destNum)
- registerRequestId(packetId)
- }
- }
-
- private fun getDeviceConnectionStatus(destNum: Int) {
- viewModelScope.launch {
- val packetId = radioConfigUseCase.getDeviceConnectionStatus(destNum)
- registerRequestId(packetId)
- }
- }
-
- private fun requestShutdown(destNum: Int) {
- viewModelScope.launch {
- val packetId = adminActionsUseCase.shutdown(destNum)
- registerRequestId(packetId)
- }
- }
-
- private fun requestReboot(destNum: Int) {
- viewModelScope.launch {
- val packetId = adminActionsUseCase.reboot(destNum)
- registerRequestId(packetId)
- }
- }
-
- private fun requestFactoryReset(destNum: Int) {
- viewModelScope.launch {
- val isLocal = (destNum == myNodeNum)
- val packetId = adminActionsUseCase.factoryReset(destNum, isLocal)
- registerRequestId(packetId)
- }
- }
-
- private fun requestNodedbReset(destNum: Int, preserveFavorites: Boolean) {
- viewModelScope.launch {
- val isLocal = (destNum == myNodeNum)
- val packetId = adminActionsUseCase.nodedbReset(destNum, preserveFavorites, isLocal)
- registerRequestId(packetId)
- }
- }
-
private fun sendAdminRequest(destNum: Int) {
val route = radioConfigState.value.route
_radioConfigState.update { it.copy(route = "") } // setter (response is PortNum.ROUTING_APP)
@@ -426,18 +319,35 @@ constructor(
val preserveFavorites = radioConfigState.value.nodeDbResetPreserveFavorites
when (route) {
- AdminRoute.REBOOT.name -> requestReboot(destNum)
+ AdminRoute.REBOOT.name ->
+ viewModelScope.launch {
+ val packetId = adminActionsUseCase.reboot(destNum)
+ registerRequestId(packetId)
+ }
AdminRoute.SHUTDOWN.name ->
with(radioConfigState.value) {
if (metadata?.canShutdown != true) {
sendError(Res.string.cant_shutdown)
} else {
- requestShutdown(destNum)
+ viewModelScope.launch {
+ val packetId = adminActionsUseCase.shutdown(destNum)
+ registerRequestId(packetId)
+ }
}
}
- AdminRoute.FACTORY_RESET.name -> requestFactoryReset(destNum)
- AdminRoute.NODEDB_RESET.name -> requestNodedbReset(destNum, preserveFavorites)
+ AdminRoute.FACTORY_RESET.name ->
+ viewModelScope.launch {
+ val isLocal = (destNum == myNodeNum)
+ val packetId = adminActionsUseCase.factoryReset(destNum, isLocal)
+ registerRequestId(packetId)
+ }
+ AdminRoute.NODEDB_RESET.name ->
+ viewModelScope.launch {
+ val isLocal = (destNum == myNodeNum)
+ val packetId = adminActionsUseCase.nodedbReset(destNum, preserveFavorites, isLocal)
+ registerRequestId(packetId)
+ }
}
}
@@ -451,50 +361,16 @@ constructor(
viewModelScope.launch { radioConfigUseCase.removeFixedPosition(destNum) }
}
- fun importProfile(uri: Uri, onResult: (DeviceProfile) -> Unit) = viewModelScope.launch(Dispatchers.IO) {
- try {
- app.contentResolver.openInputStream(uri)?.source()?.buffer()?.use { inputStream ->
- importProfileUseCase(inputStream).onSuccess(onResult).onFailure { throw it }
- }
- } catch (ex: Exception) {
- Logger.e { "Import DeviceProfile error: ${ex.message}" }
- sendError(ex.customMessage)
- }
+ open fun importProfile(uri: Any, onResult: (DeviceProfile) -> Unit) {
+ // To be implemented in platform-specific subclass
}
- fun exportProfile(uri: Uri, profile: DeviceProfile) = viewModelScope.launch {
- withContext(Dispatchers.IO) {
- try {
- app.contentResolver.openFileDescriptor(uri, "wt")?.use { parcelFileDescriptor ->
- FileOutputStream(parcelFileDescriptor.fileDescriptor).sink().buffer().use { outputStream ->
- exportProfileUseCase(outputStream, profile)
- .onSuccess { setResponseStateSuccess() }
- .onFailure { throw it }
- }
- }
- } catch (ex: Exception) {
- Logger.e { "Can't write file error: ${ex.message}" }
- sendError(ex.customMessage)
- }
- }
+ open fun exportProfile(uri: Any, profile: DeviceProfile) {
+ // To be implemented in platform-specific subclass
}
- fun exportSecurityConfig(uri: Uri, securityConfig: Config.SecurityConfig) = viewModelScope.launch {
- withContext(Dispatchers.IO) {
- try {
- app.contentResolver.openFileDescriptor(uri, "wt")?.use { parcelFileDescriptor ->
- FileOutputStream(parcelFileDescriptor.fileDescriptor).sink().buffer().use { outputStream ->
- exportSecurityConfigUseCase(outputStream, securityConfig)
- .onSuccess { setResponseStateSuccess() }
- .onFailure { throw it }
- }
- }
- } catch (ex: Exception) {
- val errorMessage = "Can't write security keys JSON error: ${ex.message}"
- Logger.e { errorMessage }
- sendError(ex.customMessage)
- }
- }
+ open fun exportSecurityConfig(uri: Any, securityConfig: Config.SecurityConfig) {
+ // To be implemented in platform-specific subclass
}
fun installProfile(protobuf: DeviceProfile) {
@@ -513,38 +389,70 @@ constructor(
_radioConfigState.update { it.copy(route = route.name, responseState = ResponseState.Loading()) }
when (route) {
- ConfigRoute.USER -> getOwner(destNum)
+ ConfigRoute.USER ->
+ viewModelScope.launch {
+ val packetId = radioConfigUseCase.getOwner(destNum)
+ registerRequestId(packetId)
+ }
ConfigRoute.CHANNELS -> {
- getChannel(destNum, 0)
- getConfig(destNum, AdminMessage.ConfigType.LORA_CONFIG.value)
+ viewModelScope.launch {
+ val packetId = radioConfigUseCase.getChannel(destNum, 0)
+ registerRequestId(packetId)
+ }
+ viewModelScope.launch {
+ val packetId = radioConfigUseCase.getConfig(destNum, AdminMessage.ConfigType.LORA_CONFIG.value)
+ registerRequestId(packetId)
+ }
// channel editor is synchronous, so we don't use requestIds as total
setResponseStateTotal(maxChannels + 1)
}
is AdminRoute -> {
- getConfig(destNum, AdminMessage.ConfigType.SESSIONKEY_CONFIG.value)
+ viewModelScope.launch {
+ val packetId =
+ radioConfigUseCase.getConfig(destNum, AdminMessage.ConfigType.SESSIONKEY_CONFIG.value)
+ registerRequestId(packetId)
+ }
setResponseStateTotal(2)
}
is ConfigRoute -> {
if (route == ConfigRoute.LORA) {
- getChannel(destNum, 0)
+ viewModelScope.launch {
+ val packetId = radioConfigUseCase.getChannel(destNum, 0)
+ registerRequestId(packetId)
+ }
}
if (route == ConfigRoute.NETWORK) {
- getDeviceConnectionStatus(destNum)
+ viewModelScope.launch {
+ val packetId = radioConfigUseCase.getDeviceConnectionStatus(destNum)
+ registerRequestId(packetId)
+ }
+ }
+ viewModelScope.launch {
+ val packetId = radioConfigUseCase.getConfig(destNum, route.type)
+ registerRequestId(packetId)
}
- getConfig(destNum, route.type)
}
is ModuleRoute -> {
if (route == ModuleRoute.CANNED_MESSAGE) {
- getCannedMessages(destNum)
+ viewModelScope.launch {
+ val packetId = radioConfigUseCase.getCannedMessages(destNum)
+ registerRequestId(packetId)
+ }
}
if (route == ModuleRoute.EXT_NOTIFICATION) {
- getRingtone(destNum)
+ viewModelScope.launch {
+ val packetId = radioConfigUseCase.getRingtone(destNum)
+ registerRequestId(packetId)
+ }
+ }
+ viewModelScope.launch {
+ val packetId = radioConfigUseCase.getModuleConfig(destNum, route.type)
+ registerRequestId(packetId)
}
- getModuleConfig(destNum, route.type)
}
}
}
@@ -565,7 +473,7 @@ constructor(
}
}
- private fun setResponseStateSuccess() {
+ protected fun setResponseStateSuccess() {
_radioConfigState.update { state ->
if (state.responseState is ResponseState.Loading) {
state.copy(responseState = ResponseState.Success(true))
@@ -575,14 +483,11 @@ constructor(
}
}
- private val Exception.customMessage: String
- get() = "${javaClass.simpleName}: $message"
+ protected fun sendError(error: String) = setResponseStateError(UiText.DynamicString(error))
- private fun sendError(error: String) = setResponseStateError(UiText.DynamicString(error))
+ protected fun sendError(id: StringResource) = setResponseStateError(UiText.Resource(id))
- private fun sendError(id: StringResource) = setResponseStateError(UiText.Resource(id))
-
- private fun sendError(error: UiText) = setResponseStateError(error)
+ protected fun sendError(error: UiText) = setResponseStateError(error)
private fun setResponseStateError(error: UiText) {
_radioConfigState.update { it.copy(responseState = ResponseState.Error(error)) }
@@ -658,7 +563,10 @@ constructor(
val index = response.index
if (index + 1 < maxChannels && route == ConfigRoute.CHANNELS.name) {
// Not done yet, request next channel
- getChannel(destNum, index + 1)
+ viewModelScope.launch {
+ val packetId = radioConfigUseCase.getChannel(destNum, index + 1)
+ registerRequestId(packetId)
+ }
}
} else {
// Received last channel, update total and start channel editor
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/ResponseState.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/ResponseState.kt
similarity index 100%
rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/ResponseState.kt
rename to feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/ResponseState.kt
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index be9d0241a..ed5394fdb 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -6,7 +6,6 @@ accompanist = "0.37.3"
# androidx
androidxComposeMaterial3Adaptive = "1.2.0"
-androidxHilt = "1.3.0"
androidxTracing = "1.10.4"
datastore = "1.2.0"
glance = "1.2.0-rc01"
@@ -16,6 +15,9 @@ navigation3 = "1.0.1"
paging = "3.4.1"
room = "2.8.4"
savedstate = "1.4.0"
+koin = "4.2.0-RC1"
+koin-annotations = "2.1.0"
+koin-plugin = "0.3.0"
# Kotlin
kotlin = "2.3.10"
@@ -32,7 +34,6 @@ turbine = "1.2.1"
compose-multiplatform = "1.11.0-alpha03"
# Google
-hilt = "2.59.2"
maps-compose = "8.2.0"
# ML Kit
@@ -83,10 +84,6 @@ androidx-glance-appwidget = { module = "androidx.glance:glance-appwidget", versi
androidx-glance-appwidget-preview = { module = "androidx.glance:glance-appwidget-preview", version.ref = "glance" }
androidx-glance-preview = { module = "androidx.glance:glance-preview", version.ref = "glance" }
androidx-glance-material3 = { module = "androidx.glance:glance-material3", version.ref = "glance" }
-androidx-hilt-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "androidxHilt" }
-androidx-hilt-lifecycle-viewmodel-compose = { module = "androidx.hilt:hilt-lifecycle-viewmodel-compose", version.ref = "androidxHilt" }
-androidx-hilt-work = { module = "androidx.hilt:hilt-work", version.ref = "androidxHilt" }
-androidx-hilt-common = { module = "androidx.hilt:hilt-common", version.ref = "androidxHilt" }
androidx-lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycle" }
androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle" }
androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime", version.ref = "lifecycle" }
@@ -139,11 +136,14 @@ firebase-analytics = { module = "com.google.firebase:firebase-analytics" }
firebase-bom = { module = "com.google.firebase:firebase-bom", version = "34.10.0" }
firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics" }
guava = { module = "com.google.guava:guava", version = "33.5.0-jre" }
-hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
-hilt-android-testing = { module = "com.google.dagger:hilt-android-testing", version.ref = "hilt" }
-hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" }
-hilt-core = { module = "com.google.dagger:hilt-core", version.ref = "hilt" }
location-services = { module = "com.google.android.gms:play-services-location", version = "21.3.0" }
+koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" }
+koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin" }
+koin-androidx-workmanager = { module = "io.insert-koin:koin-androidx-workmanager", version.ref = "koin" }
+koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koin" }
+koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
+koin-test = { module = "io.insert-koin:koin-test", version.ref = "koin" }
+koin-annotations = { module = "io.insert-koin:koin-annotations", version.ref = "koin" }
maps-compose = { module = "com.google.maps.android:maps-compose", version.ref = "maps-compose" }
maps-compose-utils = { module = "com.google.maps.android:maps-compose-utils", version.ref = "maps-compose" }
maps-compose-widgets = { module = "com.google.maps.android:maps-compose-widgets", version.ref = "maps-compose" }
@@ -243,7 +243,7 @@ detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting",
detekt-gradlePlugin = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" }
firebase-crashlytics-gradlePlugin = { module = "com.google.firebase:firebase-crashlytics-gradle", version = "3.0.6" }
google-services-gradlePlugin = { module = "com.google.gms.google-services:com.google.gms.google-services.gradle.plugin", version = "4.4.4" }
-hilt-gradlePlugin = { module = "com.google.dagger:hilt-android-gradle-plugin", version.ref = "hilt" }
+koin-gradlePlugin = { module = "io.insert-koin.compiler.plugin:io.insert-koin.compiler.plugin.gradle.plugin", version.ref = "koin-plugin" }
kover-gradlePlugin = { module = "org.jetbrains.kotlinx.kover:org.jetbrains.kotlinx.kover.gradle.plugin", version.ref = "kover" }
ksp-gradlePlugin = { module = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin", version.ref = "devtools-ksp" }
secrets-gradlePlugin = {module = "com.google.android.secrets-gradle-plugin:com.google.android.secrets-gradle-plugin.gradle.plugin", version = "1.1.0"}
@@ -259,6 +259,7 @@ android-kotlin-multiplatform-library = { id = "com.android.kotlin.multiplatform.
# Jetbrains
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
compose-multiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
+koin-compiler = { id = "io.insert-koin.compiler.plugin", version.ref = "koin-plugin" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
@@ -269,7 +270,6 @@ kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" }
# Google
devtools-ksp = { id = "com.google.devtools.ksp", version.ref = "devtools-ksp" }
google-services = { id = "com.google.gms.google-services", version = "4.4.4" }
-hilt = { id = "com.google.dagger.hilt.android" , version.ref = "hilt" }
secrets = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version = "2.0.1" }
# Firebase
@@ -300,7 +300,7 @@ meshtastic-android-lint = { id = "meshtastic.android.lint" }
meshtastic-android-room = { id = "meshtastic.android.room" }
meshtastic-android-test = { id = "meshtastic.android.test" }
meshtastic-detekt = { id = "meshtastic.detekt" }
-meshtastic-hilt = { id = "meshtastic.hilt" }
+meshtastic-koin = { id = "meshtastic.koin" }
meshtastic-kotlinx-serialization = { id = "meshtastic.kotlinx.serialization" }
meshtastic-kmp-library = { id = "meshtastic.kmp.library" }
meshtastic-kmp-library-compose = { id = "meshtastic.kmp.library.compose" }