From f35947540da676bbd174801a287be769e87035f6 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 2 Mar 2026 11:27:35 -0600 Subject: [PATCH 01/20] refactor: migrate repository interfaces and models to common KMP modules - Move `Node`, `Message`, `TAK`, and `NodeSortOption` models from `core:database` to `core:model`. - Create `:core:repository` module to host platform-agnostic repository interfaces and use cases. - Refactor `NodeRepository` into a shared interface with an Android-specific `NodeRepositoryImpl`. - Extract `PacketRepository` and `MessageQueue` interfaces for cross-platform support. - Relocate `SendMessageUseCase` to the common repository layer. - Update model dependencies across messaging, node, and map features. Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- core/data/build.gradle.kts | 1 + .../DeviceHardwareLocalDataSource.kt | 2 +- .../FirmwareReleaseLocalDataSource.kt | 2 +- .../datasource/NodeInfoWriteDataSource.kt | 2 +- .../SwitchingNodeInfoReadDataSource.kt | 2 +- .../SwitchingNodeInfoWriteDataSource.kt | 2 +- .../core/data/di/RepositoryModule.kt | 37 ++++++++ .../meshtastic/core/data/di/UseCaseModule.kt | 51 ++++++++++ .../repository/DeviceHardwareRepository.kt | 2 +- .../repository/FirmwareReleaseRepository.kt | 2 +- .../data/repository/LocationRepository.kt | 2 +- .../core/data/repository/MeshLogRepository.kt | 2 +- ...odeRepository.kt => NodeRepositoryImpl.kt} | 75 ++++++++------- .../core/data/repository/PacketRepository.kt | 39 ++++++-- .../data/repository/RadioConfigRepository.kt | 3 +- .../TracerouteSnapshotRepository.kt | 2 +- .../DeviceHardwareRepositoryTest.kt | 2 +- .../data/repository/MeshLogRepositoryTest.kt | 2 +- .../data/repository/NodeRepositoryTest.kt | 2 +- .../core/database/entity/NodeEntity.kt | 2 +- .../meshtastic/core/database/entity/Packet.kt | 24 +---- core/domain/build.gradle.kts | 1 + .../usecase/settings/AdminActionsUseCase.kt | 2 +- .../settings/CleanNodeDatabaseUseCase.kt | 5 +- .../usecase/settings/ExportDataUseCase.kt | 2 +- .../usecase/settings/IsOtaCapableUseCase.kt | 4 +- .../settings/ProcessRadioResponseUseCase.kt | 2 +- .../domain/usecase/SendMessageUseCaseTest.kt | 24 ++--- core/model/build.gradle.kts | 2 + .../org/meshtastic/core}/model/Message.kt | 6 +- .../kotlin/org/meshtastic/core}/model/Node.kt | 39 ++------ .../meshtastic/core}/model/NodeSortOption.kt | 4 +- .../org/meshtastic/core/model/Reaction.kt | 38 ++++++++ .../kotlin/org/meshtastic/core}/model/TAK.kt | 4 +- core/prefs/build.gradle.kts | 1 + .../meshtastic/core/prefs/di/PrefsModule.kt | 2 + .../core/prefs/homoglyph/HomoglyphPrefs.kt | 6 +- core/repository/build.gradle.kts | 38 ++++++++ .../core/repository/HomoglyphPrefs.kt | 21 ++++ .../core/repository}/MessageQueue.kt | 2 +- .../core/repository/NodeRepository.kt | 95 +++++++++++++++++++ .../core/repository/PacketRepository.kt | 35 +++++++ .../repository}/usecase/SendMessageUseCase.kt | 41 +++----- .../service/AndroidRadioControllerImpl.kt | 2 +- .../meshtastic/core/service/ServiceAction.kt | 2 +- .../core/ui/component/ContactSharing.kt | 2 +- .../core/ui/component/MainAppBar.kt | 2 +- .../meshtastic/core/ui/component/NodeChip.kt | 2 +- .../core/ui/component/SignalInfo.kt | 2 +- .../preview/NodePreviewParameterProvider.kt | 2 +- .../core/ui/component/preview/PreviewUtils.kt | 2 +- .../core/ui/share/SharedContactViewModel.kt | 4 +- .../feature/map/BaseMapViewModel.kt | 2 +- .../meshtastic/feature/messaging/Message.kt | 4 +- .../feature/messaging/MessageListPaged.kt | 4 +- .../feature/messaging/MessageViewModel.kt | 6 +- .../messaging/component/MessageItem.kt | 4 +- .../feature/messaging/component/Reaction.kt | 5 +- .../feature/messaging/di/MessagingModule.kt | 2 +- .../domain/worker/WorkManagerMessageQueue.kt | 2 +- .../feature/node/component/NodeItem.kt | 4 +- .../feature/node/detail/NodeDetailScreen.kt | 2 +- .../node/detail/NodeDetailViewModel.kt | 2 +- .../feature/node/list/NodeListViewModel.kt | 4 +- .../feature/settings/SettingsViewModel.kt | 2 +- .../radio/CleanNodeDatabaseViewModel.kt | 2 +- settings.gradle.kts | 1 + 67 files changed, 498 insertions(+), 200 deletions(-) create mode 100644 core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt create mode 100644 core/data/src/main/kotlin/org/meshtastic/core/data/di/UseCaseModule.kt rename core/data/src/main/kotlin/org/meshtastic/core/data/repository/{NodeRepository.kt => NodeRepositoryImpl.kt} (78%) rename core/{database/src/main/kotlin/org/meshtastic/core/database => model/src/commonMain/kotlin/org/meshtastic/core}/model/Message.kt (96%) rename core/{database/src/main/kotlin/org/meshtastic/core/database => model/src/commonMain/kotlin/org/meshtastic/core}/model/Node.kt (86%) rename core/{database/src/main/kotlin/org/meshtastic/core/database => model/src/commonMain/kotlin/org/meshtastic/core}/model/NodeSortOption.kt (95%) create mode 100644 core/model/src/commonMain/kotlin/org/meshtastic/core/model/Reaction.kt rename core/{database/src/main/kotlin/org/meshtastic/core/database => model/src/commonMain/kotlin/org/meshtastic/core}/model/TAK.kt (97%) create mode 100644 core/repository/build.gradle.kts create mode 100644 core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/HomoglyphPrefs.kt rename core/{domain/src/main/kotlin/org/meshtastic/core/domain => repository/src/commonMain/kotlin/org/meshtastic/core/repository}/MessageQueue.kt (96%) create mode 100644 core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeRepository.kt create mode 100644 core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketRepository.kt rename core/{domain/src/main/kotlin/org/meshtastic/core/domain => repository/src/commonMain/kotlin/org/meshtastic/core/repository}/usecase/SendMessageUseCase.kt (81%) diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index 1f06437b66..90a438478e 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -26,6 +26,7 @@ plugins { configure { namespace = "org.meshtastic.core.data" } dependencies { + api(projects.core.repository) implementation(projects.core.analytics) implementation(projects.core.common) implementation(projects.core.database) diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/DeviceHardwareLocalDataSource.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/DeviceHardwareLocalDataSource.kt index 852c56e04e..907365965c 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/DeviceHardwareLocalDataSource.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/DeviceHardwareLocalDataSource.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/FirmwareReleaseLocalDataSource.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/FirmwareReleaseLocalDataSource.kt index dff3b0171e..41e9479718 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/FirmwareReleaseLocalDataSource.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/FirmwareReleaseLocalDataSource.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/NodeInfoWriteDataSource.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/NodeInfoWriteDataSource.kt index c4ced500c3..e45629bb8d 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/NodeInfoWriteDataSource.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/NodeInfoWriteDataSource.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/SwitchingNodeInfoReadDataSource.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/SwitchingNodeInfoReadDataSource.kt index 35d9c08480..434e1ec7e3 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/SwitchingNodeInfoReadDataSource.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/SwitchingNodeInfoReadDataSource.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/SwitchingNodeInfoWriteDataSource.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/SwitchingNodeInfoWriteDataSource.kt index 6b55019102..2be1f75252 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/SwitchingNodeInfoWriteDataSource.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/SwitchingNodeInfoWriteDataSource.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt new file mode 100644 index 0000000000..1bf8fcbafd --- /dev/null +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt @@ -0,0 +1,37 @@ +/* + * 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 . + */ + +package org.meshtastic.core.data.di + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import org.meshtastic.core.data.repository.NodeRepositoryImpl +import org.meshtastic.core.repository.NodeRepository +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class RepositoryModule { + + @Binds + @Singleton + abstract fun bindNodeRepository( + nodeRepositoryImpl: NodeRepositoryImpl + ): NodeRepository +} diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/di/UseCaseModule.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/di/UseCaseModule.kt new file mode 100644 index 0000000000..b145e54b2c --- /dev/null +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/di/UseCaseModule.kt @@ -0,0 +1,51 @@ +/* + * 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 . + */ + +package org.meshtastic.core.data.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +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 + fun provideSendMessageUseCase( + nodeRepository: NodeRepository, + packetRepository: PacketRepository, + radioController: RadioController, + homoglyphEncodingPrefs: HomoglyphPrefs, + messageQueue: MessageQueue, + ): SendMessageUseCase = SendMessageUseCase( + nodeRepository, + packetRepository, + radioController, + homoglyphEncodingPrefs, + messageQueue + ) +} diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepository.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepository.kt index d189f19f77..80a67b8c9e 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepository.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepository.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/FirmwareReleaseRepository.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/FirmwareReleaseRepository.kt index 67ccdc091c..6fc377d424 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/FirmwareReleaseRepository.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/FirmwareReleaseRepository.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/LocationRepository.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/LocationRepository.kt index a1b7b8a5ae..d9b4d02eb6 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/LocationRepository.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/LocationRepository.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/MeshLogRepository.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/MeshLogRepository.kt index 24a1cc8254..2e47f4cbaf 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/MeshLogRepository.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/MeshLogRepository.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/NodeRepository.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt similarity index 78% rename from core/data/src/main/kotlin/org/meshtastic/core/data/repository/NodeRepository.kt rename to core/data/src/main/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt index 53729ce489..6ddafb5feb 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/NodeRepository.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt @@ -40,13 +40,15 @@ import org.meshtastic.core.database.entity.MeshLog 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.database.model.Node -import org.meshtastic.core.database.model.NodeSortOption 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 +import org.meshtastic.core.model.NodeSortOption import org.meshtastic.core.model.util.onlineTimeThreshold +import org.meshtastic.core.repository.NodeRepository import org.meshtastic.proto.HardwareModel import org.meshtastic.proto.LocalStats import org.meshtastic.proto.User @@ -56,7 +58,7 @@ import javax.inject.Singleton /** Repository for managing node-related data, including hardware info, node database, and identity. */ @Singleton @Suppress("TooManyFunctions") -open class NodeRepository +class NodeRepositoryImpl @Inject constructor( @ProcessLifecycle private val processLifecycle: Lifecycle, @@ -64,28 +66,29 @@ constructor( private val nodeInfoWriteDataSource: NodeInfoWriteDataSource, private val dispatchers: CoroutineDispatchers, private val localStatsDataSource: LocalStatsDataSource, -) { +) : NodeRepository { /** Hardware info about our local device (can be null if not connected). */ - open val myNodeInfo: StateFlow = + override val myNodeInfo: StateFlow = nodeInfoReadDataSource .myNodeInfoFlow() + .map { it?.toMyNodeInfo() } .flowOn(dispatchers.io) .stateIn(processLifecycle.coroutineScope, SharingStarted.Eagerly, null) private val _ourNodeInfo = MutableStateFlow(null) /** Information about the locally connected node, as seen from the mesh. */ - open val ourNodeInfo: StateFlow + override val ourNodeInfo: StateFlow get() = _ourNodeInfo private val _myId = MutableStateFlow(null) /** The unique userId (hex string) of our local node. */ - val myId: StateFlow + override val myId: StateFlow get() = _myId /** The latest local stats telemetry received from the locally connected node. */ - val localStats: StateFlow = + override val localStats: StateFlow = localStatsDataSource.localStatsFlow.stateIn( processLifecycle.coroutineScope, SharingStarted.Eagerly, @@ -93,12 +96,12 @@ constructor( ) /** Update the cached local stats telemetry. */ - fun updateLocalStats(stats: LocalStats) { + override fun updateLocalStats(stats: LocalStats) { processLifecycle.coroutineScope.launch { localStatsDataSource.setLocalStats(stats) } } /** A reactive map from nodeNum to [Node] objects, representing the entire mesh. */ - val nodeDBbyNum: StateFlow> = + override val nodeDBbyNum: StateFlow> = nodeInfoReadDataSource .nodeDBbyNumFlow() .mapLatest { map -> map.mapValues { (_, it) -> it.toModel() } } @@ -115,7 +118,9 @@ constructor( } // Keep ourNodeInfo and myId correctly updated based on current connection and node DB - combine(nodeDBbyNum, myNodeInfo) { db, info -> info?.myNodeNum?.let { db[it] } } + combine(nodeDBbyNum, nodeInfoReadDataSource.myNodeInfoFlow()) { db, info -> + info?.myNodeNum?.let { db[it] } + } .onEach { node -> _ourNodeInfo.value = node _myId.value = node?.user?.id @@ -127,7 +132,7 @@ constructor( * Returns the node number used for log queries. Maps [nodeNum] to [MeshLog.NODE_NUM_LOCAL] (0) if it is the locally * connected node. */ - fun effectiveLogNodeId(nodeNum: Int): Flow = myNodeInfo + override fun effectiveLogNodeId(nodeNum: Int): Flow = nodeInfoReadDataSource.myNodeInfoFlow() .map { info -> if (nodeNum == info?.myNodeNum) MeshLog.NODE_NUM_LOCAL else nodeNum } .distinctUntilChanged() @@ -135,14 +140,14 @@ constructor( nodeInfoReadDataSource.nodeDBbyNumFlow().map { map -> map.mapValues { (_, it) -> it.toEntity() } } /** Returns the [Node] associated with a given [userId]. Falls back to a generic node if not found. */ - fun getNode(userId: String): Node = nodeDBbyNum.value.values.find { it.user.id == userId } + override fun getNode(userId: String): Node = nodeDBbyNum.value.values.find { it.user.id == userId } ?: Node(num = DataPacket.idToDefaultNodeNum(userId) ?: 0, user = getUser(userId)) /** Returns the [User] info for a given [nodeNum]. */ - fun getUser(nodeNum: Int): User = getUser(DataPacket.nodeNumToDefaultId(nodeNum)) + override fun getUser(nodeNum: Int): User = getUser(DataPacket.nodeNumToDefaultId(nodeNum)) /** Returns the [User] info for a given [userId]. Falls back to a generic user if not found. */ - fun getUser(userId: String): User = nodeDBbyNum.value.values.find { it.user.id == userId }?.user + override fun getUser(userId: String): User = nodeDBbyNum.value.values.find { it.user.id == userId }?.user ?: User( id = userId, long_name = @@ -161,13 +166,13 @@ constructor( ) /** Returns a flow of nodes filtered and sorted according to the parameters. */ - fun getNodes( - sort: NodeSortOption = NodeSortOption.LAST_HEARD, - filter: String = "", - includeUnknown: Boolean = true, - onlyOnline: Boolean = false, - onlyDirect: Boolean = false, - ) = nodeInfoReadDataSource + override fun getNodes( + sort: NodeSortOption, + filter: String, + includeUnknown: Boolean, + onlyOnline: Boolean, + onlyDirect: Boolean, + ): Flow> = nodeInfoReadDataSource .getNodesFlow( sort = sort.sqlValue, filter = filter, @@ -187,36 +192,40 @@ constructor( withContext(dispatchers.io) { nodeInfoWriteDataSource.installConfig(mi, nodes) } /** Deletes all nodes from the database, optionally preserving favorites. */ - suspend fun clearNodeDB(preserveFavorites: Boolean = false) = + override suspend fun clearNodeDB(preserveFavorites: Boolean) = withContext(dispatchers.io) { nodeInfoWriteDataSource.clearNodeDB(preserveFavorites) } /** Clears the local node's connection info. */ - suspend fun clearMyNodeInfo() = withContext(dispatchers.io) { nodeInfoWriteDataSource.clearMyNodeInfo() } + override suspend fun clearMyNodeInfo() = withContext(dispatchers.io) { nodeInfoWriteDataSource.clearMyNodeInfo() } /** Deletes a node and its metadata by [num]. */ - suspend fun deleteNode(num: Int) = withContext(dispatchers.io) { + override suspend fun deleteNode(num: Int) = withContext(dispatchers.io) { nodeInfoWriteDataSource.deleteNode(num) nodeInfoWriteDataSource.deleteMetadata(num) } /** Deletes multiple nodes and their metadata. */ - suspend fun deleteNodes(nodeNums: List) = withContext(dispatchers.io) { + override suspend fun deleteNodes(nodeNums: List) = withContext(dispatchers.io) { nodeInfoWriteDataSource.deleteNodes(nodeNums) nodeNums.forEach { nodeInfoWriteDataSource.deleteMetadata(it) } } - suspend fun getNodesOlderThan(lastHeard: Int): List = - withContext(dispatchers.io) { nodeInfoReadDataSource.getNodesOlderThan(lastHeard) } + override suspend fun getNodesOlderThan(lastHeard: Int): List = + withContext(dispatchers.io) { + nodeInfoReadDataSource.getNodesOlderThan(lastHeard).map { it.toModel() } + } - suspend fun getUnknownNodes(): List = - withContext(dispatchers.io) { nodeInfoReadDataSource.getUnknownNodes() } + override suspend fun getUnknownNodes(): List = + withContext(dispatchers.io) { + nodeInfoReadDataSource.getUnknownNodes().map { it.toModel() } + } /** Persists hardware metadata for a node. */ suspend fun insertMetadata(metadata: MetadataEntity) = withContext(dispatchers.io) { nodeInfoWriteDataSource.upsert(metadata) } /** Flow emitting the count of nodes currently considered "online". */ - val onlineNodeCount: Flow = + override val onlineNodeCount: Flow = nodeInfoReadDataSource .nodeDBbyNumFlow() .mapLatest { map -> map.values.count { it.node.lastHeard > onlineTimeThreshold() } } @@ -224,7 +233,7 @@ constructor( .conflate() /** Flow emitting the total number of nodes in the database. */ - val totalNodeCount: Flow = + override val totalNodeCount: Flow = nodeInfoReadDataSource .nodeDBbyNumFlow() .mapLatest { map -> map.values.count() } @@ -232,6 +241,6 @@ constructor( .conflate() /** Updates the personal notes field for a node. */ - suspend fun setNodeNotes(num: Int, notes: String) = + override suspend fun setNodeNotes(num: Int, notes: String) = withContext(dispatchers.io) { nodeInfoWriteDataSource.setNodeNotes(num, notes) } } diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/PacketRepository.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/PacketRepository.kt index d65898086c..7e7bcd9689 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/PacketRepository.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/PacketRepository.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 @@ -30,21 +30,48 @@ import org.meshtastic.core.database.DatabaseManager import org.meshtastic.core.database.entity.ContactSettings import org.meshtastic.core.database.entity.Packet import org.meshtastic.core.database.entity.ReactionEntity -import org.meshtastic.core.database.model.Message -import org.meshtastic.core.database.model.Node import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.model.Message import org.meshtastic.core.model.MessageStatus +import org.meshtastic.core.model.Node import org.meshtastic.proto.ChannelSettings import org.meshtastic.proto.PortNum import javax.inject.Inject +import org.meshtastic.core.repository.PacketRepository as SharedPacketRepository class PacketRepository @Inject constructor( private val dbManager: DatabaseManager, private val dispatchers: CoroutineDispatchers, -) { +) : SharedPacketRepository { + override suspend fun savePacket( + myNodeNum: Int, + contactKey: String, + packet: DataPacket, + receivedTime: Long, + read: Boolean, + filtered: Boolean, + ) { + val packetToSave = + Packet( + uuid = 0L, + myNodeNum = myNodeNum, + packetId = packet.id, + port_num = packet.dataType, + contact_key = contactKey, + received_time = receivedTime, + read = read, + data = packet, + snr = packet.snr, + rssi = packet.rssi, + hopsAway = packet.hopsAway, + filtered = filtered, + ) + insert(packetToSave) + } + fun getWaypoints(): Flow> = dbManager.currentDb.flatMapLatest { db -> db.packetDao().getAllWaypointsFlow() } @@ -96,7 +123,7 @@ constructor( dao.upsertContactSettings(listOf(updated)) } - suspend fun getQueuedPackets(): List? = + override suspend fun getQueuedPackets(): List? = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getQueuedPackets() } suspend fun insert(packet: Packet) = @@ -148,7 +175,7 @@ constructor( } } - suspend fun updateMessageStatus(d: DataPacket, m: MessageStatus) = + override suspend fun updateMessageStatus(d: DataPacket, m: MessageStatus) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().updateMessageStatus(d, m) } suspend fun updateMessageId(d: DataPacket, id: Int) = diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/RadioConfigRepository.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/RadioConfigRepository.kt index 1e4067f80e..3454a9de16 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/RadioConfigRepository.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/RadioConfigRepository.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 @@ -22,6 +22,7 @@ import org.meshtastic.core.datastore.ChannelSetDataSource import org.meshtastic.core.datastore.LocalConfigDataSource import org.meshtastic.core.datastore.ModuleConfigDataSource import org.meshtastic.core.model.util.getChannelUrl +import org.meshtastic.core.repository.NodeRepository import org.meshtastic.proto.Channel import org.meshtastic.proto.ChannelSet import org.meshtastic.proto.ChannelSettings diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/TracerouteSnapshotRepository.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/TracerouteSnapshotRepository.kt index e29572ac3a..f046457b2f 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/TracerouteSnapshotRepository.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/TracerouteSnapshotRepository.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 diff --git a/core/data/src/test/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryTest.kt b/core/data/src/test/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryTest.kt index a97f27a560..62c27eb9d7 100644 --- a/core/data/src/test/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryTest.kt +++ b/core/data/src/test/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 diff --git a/core/data/src/test/kotlin/org/meshtastic/core/data/repository/MeshLogRepositoryTest.kt b/core/data/src/test/kotlin/org/meshtastic/core/data/repository/MeshLogRepositoryTest.kt index 521cc22288..90aa15951b 100644 --- a/core/data/src/test/kotlin/org/meshtastic/core/data/repository/MeshLogRepositoryTest.kt +++ b/core/data/src/test/kotlin/org/meshtastic/core/data/repository/MeshLogRepositoryTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2026 Meshtastic LLC + * 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 diff --git a/core/data/src/test/kotlin/org/meshtastic/core/data/repository/NodeRepositoryTest.kt b/core/data/src/test/kotlin/org/meshtastic/core/data/repository/NodeRepositoryTest.kt index 17e48b2be3..8b931b5727 100644 --- a/core/data/src/test/kotlin/org/meshtastic/core/data/repository/NodeRepositoryTest.kt +++ b/core/data/src/test/kotlin/org/meshtastic/core/data/repository/NodeRepositoryTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2026 Meshtastic LLC + * 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 diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/entity/NodeEntity.kt b/core/database/src/main/kotlin/org/meshtastic/core/database/entity/NodeEntity.kt index 69b3263102..4e76127199 100644 --- a/core/database/src/main/kotlin/org/meshtastic/core/database/entity/NodeEntity.kt +++ b/core/database/src/main/kotlin/org/meshtastic/core/database/entity/NodeEntity.kt @@ -26,10 +26,10 @@ import okio.ByteString import okio.ByteString.Companion.toByteString import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.common.util.nowSeconds -import org.meshtastic.core.database.model.Node import org.meshtastic.core.model.DeviceMetrics import org.meshtastic.core.model.EnvironmentMetrics import org.meshtastic.core.model.MeshUser +import org.meshtastic.core.model.Node import org.meshtastic.core.model.NodeInfo import org.meshtastic.core.model.Position import org.meshtastic.core.model.util.onlineTimeThreshold diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/entity/Packet.kt b/core/database/src/main/kotlin/org/meshtastic/core/database/entity/Packet.kt index c522a22dba..4fcfb3698d 100644 --- a/core/database/src/main/kotlin/org/meshtastic/core/database/entity/Packet.kt +++ b/core/database/src/main/kotlin/org/meshtastic/core/database/entity/Packet.kt @@ -24,12 +24,12 @@ import androidx.room.PrimaryKey import androidx.room.Relation import okio.ByteString import org.meshtastic.core.common.util.nowMillis -import org.meshtastic.core.database.model.Message -import org.meshtastic.core.database.model.Node import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.model.Message import org.meshtastic.core.model.MessageStatus +import org.meshtastic.core.model.Node +import org.meshtastic.core.model.Reaction import org.meshtastic.core.model.util.getShortDateTime -import org.meshtastic.proto.User data class PacketEntity( @Embedded val packet: Packet, @@ -130,24 +130,6 @@ data class ContactSettings( get() = nowMillis <= muteUntil } -data class Reaction( - val replyId: Int, - val user: User, - val emoji: String, - val timestamp: Long, - val snr: Float, - val rssi: Int, - val hopsAway: Int, - val packetId: Int = 0, - val status: MessageStatus = MessageStatus.UNKNOWN, - val routingError: Int = 0, - val relays: Int = 0, - val relayNode: Int? = null, - val to: String? = null, - val channel: Int = 0, - val sfppHash: ByteString? = null, -) - @Suppress("ConstructorParameterNaming") @Entity( tableName = "reactions", diff --git a/core/domain/build.gradle.kts b/core/domain/build.gradle.kts index 60226b6618..c368cd45de 100644 --- a/core/domain/build.gradle.kts +++ b/core/domain/build.gradle.kts @@ -24,6 +24,7 @@ plugins { android { namespace = "org.meshtastic.core.domain" } dependencies { + implementation(projects.core.repository) implementation(projects.core.model) implementation(projects.core.proto) implementation(projects.core.common) diff --git a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/AdminActionsUseCase.kt b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/AdminActionsUseCase.kt index 728a209e4e..0cdb50ff9d 100644 --- a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/AdminActionsUseCase.kt +++ b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/AdminActionsUseCase.kt @@ -16,8 +16,8 @@ */ package org.meshtastic.core.domain.usecase.settings -import org.meshtastic.core.data.repository.NodeRepository import org.meshtastic.core.model.RadioController +import org.meshtastic.core.repository.NodeRepository import javax.inject.Inject /** Use case for performing administrative actions on the radio. */ diff --git a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/CleanNodeDatabaseUseCase.kt b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/CleanNodeDatabaseUseCase.kt index 6a32f1131a..446ec0fc33 100644 --- a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/CleanNodeDatabaseUseCase.kt +++ b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/CleanNodeDatabaseUseCase.kt @@ -16,9 +16,9 @@ */ package org.meshtastic.core.domain.usecase.settings -import org.meshtastic.core.data.repository.NodeRepository -import org.meshtastic.core.database.model.Node +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 @@ -47,7 +47,6 @@ constructor( .filterNot { node -> (node.hasPKC && node.lastHeard >= sevenDaysAgoSeconds) || node.isIgnored || node.isFavorite } - .map { it.toModel() } } /** Performs the cleanup of specified nodes. */ diff --git a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCase.kt b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCase.kt index c8bcdf699b..c9884bf6fd 100644 --- a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCase.kt +++ b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCase.kt @@ -19,9 +19,9 @@ package org.meshtastic.core.domain.usecase.settings import android.icu.text.SimpleDateFormat import kotlinx.coroutines.flow.first import org.meshtastic.core.data.repository.MeshLogRepository -import org.meshtastic.core.data.repository.NodeRepository import org.meshtastic.core.model.Position import org.meshtastic.core.model.util.positionToMeter +import org.meshtastic.core.repository.NodeRepository import org.meshtastic.proto.PortNum import java.io.BufferedWriter import java.util.Locale diff --git a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCase.kt b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCase.kt index 0e18a33a78..4515564d38 100644 --- a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCase.kt +++ b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCase.kt @@ -21,14 +21,14 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import org.meshtastic.core.data.repository.DeviceHardwareRepository -import org.meshtastic.core.data.repository.NodeRepository -import org.meshtastic.core.database.model.Node import org.meshtastic.core.model.ConnectionState +import org.meshtastic.core.model.Node import org.meshtastic.core.model.RadioController import org.meshtastic.core.prefs.radio.RadioPrefs import org.meshtastic.core.prefs.radio.isBle import org.meshtastic.core.prefs.radio.isSerial import org.meshtastic.core.prefs.radio.isTcp +import org.meshtastic.core.repository.NodeRepository import javax.inject.Inject /** Use case to determine if the currently connected device is capable of over-the-air (OTA) updates. */ diff --git a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ProcessRadioResponseUseCase.kt b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ProcessRadioResponseUseCase.kt index e208a54353..916163ecc2 100644 --- a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ProcessRadioResponseUseCase.kt +++ b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ProcessRadioResponseUseCase.kt @@ -17,7 +17,7 @@ package org.meshtastic.core.domain.usecase.settings import co.touchlab.kermit.Logger -import org.meshtastic.core.database.model.getStringResFrom +import org.meshtastic.core.model.getStringResFrom import org.meshtastic.core.resources.UiText import org.meshtastic.proto.AdminMessage import org.meshtastic.proto.Channel diff --git a/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/SendMessageUseCaseTest.kt b/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/SendMessageUseCaseTest.kt index 6c0d0fe6e2..fac5b04e43 100644 --- a/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/SendMessageUseCaseTest.kt +++ b/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/SendMessageUseCaseTest.kt @@ -29,15 +29,15 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test -import org.meshtastic.core.data.repository.NodeRepository -import org.meshtastic.core.data.repository.PacketRepository -import org.meshtastic.core.database.entity.Packet -import org.meshtastic.core.database.model.Node import org.meshtastic.core.domain.FakeRadioController -import org.meshtastic.core.domain.MessageQueue import org.meshtastic.core.model.Capabilities import org.meshtastic.core.model.DataPacket -import org.meshtastic.core.prefs.homoglyph.HomoglyphPrefs +import org.meshtastic.core.model.Node +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 org.meshtastic.proto.Config import org.meshtastic.proto.DeviceMetadata @@ -90,7 +90,7 @@ class SendMessageUseCaseTest { assertEquals(0, radioController.favoritedNodes.size) assertEquals(0, radioController.sentSharedContacts.size) - coVerify { packetRepository.insert(any()) } + coVerify { packetRepository.savePacket(any(), any(), any(), any()) } coVerify { messageQueue.enqueue(any()) } } @@ -120,7 +120,7 @@ class SendMessageUseCaseTest { assertEquals(1, radioController.favoritedNodes.size) assertEquals(12345, radioController.favoritedNodes[0]) - coVerify { packetRepository.insert(any()) } + coVerify { packetRepository.savePacket(any(), any(), any(), any()) } coVerify { messageQueue.enqueue(any()) } } @@ -149,7 +149,7 @@ class SendMessageUseCaseTest { assertEquals(1, radioController.sentSharedContacts.size) assertEquals(67890, radioController.sentSharedContacts[0]) - coVerify { packetRepository.insert(any()) } + coVerify { packetRepository.savePacket(any(), any(), any(), any()) } coVerify { messageQueue.enqueue(any()) } } @@ -166,9 +166,9 @@ class SendMessageUseCaseTest { useCase(originalText, "0${DataPacket.ID_BROADCAST}", null) // Assert - val packetSlot = slot() - coVerify { packetRepository.insert(capture(packetSlot)) } - assertTrue(packetSlot.captured.data?.text?.contains("Apple") == true) + val packetSlot = slot() + coVerify { packetRepository.savePacket(any(), any(), capture(packetSlot), any()) } + assertTrue(packetSlot.captured.text?.contains("Apple") == true) coVerify { messageQueue.enqueue(any()) } } } diff --git a/core/model/build.gradle.kts b/core/model/build.gradle.kts index 9514039764..d1e6008183 100644 --- a/core/model/build.gradle.kts +++ b/core/model/build.gradle.kts @@ -36,11 +36,13 @@ kotlin { commonMain.dependencies { api(projects.core.proto) api(projects.core.common) + api(projects.core.resources) api(libs.kotlinx.serialization.json) api(libs.kotlinx.datetime) implementation(libs.kermit) api(libs.okio) + api(libs.compose.multiplatform.resources) } androidMain.dependencies { api(libs.androidx.annotation) diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/model/Message.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Message.kt similarity index 96% rename from core/database/src/main/kotlin/org/meshtastic/core/database/model/Message.kt rename to core/model/src/commonMain/kotlin/org/meshtastic/core/model/Message.kt index 3205c05298..678e57a621 100644 --- a/core/database/src/main/kotlin/org/meshtastic/core/database/model/Message.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Message.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 @@ -14,11 +14,9 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.core.database.model +package org.meshtastic.core.model import org.jetbrains.compose.resources.StringResource -import org.meshtastic.core.database.entity.Reaction -import org.meshtastic.core.model.MessageStatus import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.delivery_confirmed import org.meshtastic.core.resources.error diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/model/Node.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Node.kt similarity index 86% rename from core/database/src/main/kotlin/org/meshtastic/core/database/model/Node.kt rename to core/model/src/commonMain/kotlin/org/meshtastic/core/model/Node.kt index 64cc0c1011..9d2db88927 100644 --- a/core/database/src/main/kotlin/org/meshtastic/core/database/model/Node.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Node.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 @@ -14,15 +14,13 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.core.database.model +package org.meshtastic.core.model import okio.ByteString +import okio.ByteString.Companion.toByteString import org.meshtastic.core.common.util.GPSFormat import org.meshtastic.core.common.util.bearing import org.meshtastic.core.common.util.latLongToMeter -import org.meshtastic.core.database.entity.NodeEntity -import org.meshtastic.core.model.Capabilities -import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.util.UnitConversions.celsiusToFahrenheit import org.meshtastic.core.model.util.toDistanceString import org.meshtastic.proto.Config @@ -34,7 +32,6 @@ import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.Paxcount import org.meshtastic.proto.Position import org.meshtastic.proto.PowerMetrics -import org.meshtastic.proto.Telemetry import org.meshtastic.proto.User /** @@ -88,7 +85,7 @@ data class Node( get() = (publicKey ?: user.public_key)?.size?.let { it > 0 } == true val mismatchKey - get() = (publicKey ?: user.public_key) == NodeEntity.ERROR_BYTE_STRING + get() = (publicKey ?: user.public_key) == ERROR_BYTE_STRING val hasEnvironmentMetrics: Boolean get() = environmentMetrics != EnvironmentMetrics() @@ -188,35 +185,11 @@ data class Node( fun getTelemetryStrings(isFahrenheit: Boolean = false): List = environmentMetrics.getDisplayStrings(isFahrenheit) - fun toEntity() = NodeEntity( - num = num, - user = user, - position = position, - latitude = latitude, - longitude = longitude, - snr = snr, - rssi = rssi, - lastHeard = lastHeard, - deviceTelemetry = Telemetry(device_metrics = deviceMetrics), - channel = channel, - viaMqtt = viaMqtt, - hopsAway = hopsAway, - isFavorite = isFavorite, - isIgnored = isIgnored, - isMuted = isMuted, - environmentTelemetry = Telemetry(environment_metrics = environmentMetrics), - powerTelemetry = Telemetry(power_metrics = powerMetrics), - paxcounter = paxcounter, - publicKey = publicKey ?: user.public_key, - notes = notes, - manuallyVerified = manuallyVerified, - nodeStatus = nodeStatus, - lastTransport = lastTransport, - ) - companion object { private const val DEFAULT_ID_SUFFIX_LENGTH = 4 + val ERROR_BYTE_STRING: ByteString = ByteArray(32) { 0 }.toByteString() + /** Creates a fallback [Node] when the node is not found in the database. */ fun createFallback(nodeNum: Int, fallbackNamePrefix: String): Node { val userId = DataPacket.nodeNumToDefaultId(nodeNum) diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/model/NodeSortOption.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/NodeSortOption.kt similarity index 95% rename from core/database/src/main/kotlin/org/meshtastic/core/database/model/NodeSortOption.kt rename to core/model/src/commonMain/kotlin/org/meshtastic/core/model/NodeSortOption.kt index c54a66b637..a1e0c5ecef 100644 --- a/core/database/src/main/kotlin/org/meshtastic/core/database/model/NodeSortOption.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/NodeSortOption.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 @@ -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.core.database.model +package org.meshtastic.core.model import org.jetbrains.compose.resources.StringResource import org.meshtastic.core.resources.Res diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Reaction.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Reaction.kt new file mode 100644 index 0000000000..04c9cc14da --- /dev/null +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Reaction.kt @@ -0,0 +1,38 @@ +/* + * 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 . + */ +package org.meshtastic.core.model + +import okio.ByteString +import org.meshtastic.proto.User + +data class Reaction( + val replyId: Int, + val user: User, + val emoji: String, + val timestamp: Long, + val snr: Float, + val rssi: Int, + val hopsAway: Int, + val packetId: Int = 0, + val status: MessageStatus = MessageStatus.UNKNOWN, + val routingError: Int = 0, + val relays: Int = 0, + val relayNode: Int? = null, + val to: String? = null, + val channel: Int = 0, + val sfppHash: ByteString? = null, +) diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/model/TAK.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/TAK.kt similarity index 97% rename from core/database/src/main/kotlin/org/meshtastic/core/database/model/TAK.kt rename to core/model/src/commonMain/kotlin/org/meshtastic/core/model/TAK.kt index bf5cddffca..4f4eb56c2a 100644 --- a/core/database/src/main/kotlin/org/meshtastic/core/database/model/TAK.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/TAK.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 @@ -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.core.database.model +package org.meshtastic.core.model import org.jetbrains.compose.resources.StringResource import org.meshtastic.core.resources.Res diff --git a/core/prefs/build.gradle.kts b/core/prefs/build.gradle.kts index 84e01f5872..2274282720 100644 --- a/core/prefs/build.gradle.kts +++ b/core/prefs/build.gradle.kts @@ -25,6 +25,7 @@ plugins { configure { namespace = "org.meshtastic.core.prefs" } dependencies { + implementation(projects.core.repository) googleImplementation(libs.maps.compose) testImplementation(libs.junit) diff --git a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/di/PrefsModule.kt b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/di/PrefsModule.kt index fa3ef467c1..1f67f50e84 100644 --- a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/di/PrefsModule.kt +++ b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/di/PrefsModule.kt @@ -109,6 +109,8 @@ interface PrefsModule { @Binds fun bindHomoglyphEncodingPrefs(homoglyphEncodingPrefsImpl: HomoglyphPrefsImpl): HomoglyphPrefs + @Binds fun bindSharedHomoglyphPrefs(homoglyphEncodingPrefsImpl: HomoglyphPrefsImpl): org.meshtastic.core.repository.HomoglyphPrefs + @Binds fun bindCustomEmojiPrefs(customEmojiPrefsImpl: CustomEmojiPrefsImpl): CustomEmojiPrefs @Binds fun bindMapConsentPrefs(mapConsentPrefsImpl: MapConsentPrefsImpl): MapConsentPrefs diff --git a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/homoglyph/HomoglyphPrefs.kt b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/homoglyph/HomoglyphPrefs.kt index d74962cfe4..f37d802a4f 100644 --- a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/homoglyph/HomoglyphPrefs.kt +++ b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/homoglyph/HomoglyphPrefs.kt @@ -25,10 +25,12 @@ import org.meshtastic.core.prefs.di.HomoglyphEncodingSharedPreferences import javax.inject.Inject import javax.inject.Singleton -interface HomoglyphPrefs { +import org.meshtastic.core.repository.HomoglyphPrefs as SharedHomoglyphPrefs + +interface HomoglyphPrefs : SharedHomoglyphPrefs { /** Preference for whether homoglyph encoding is enabled by the user. */ - var homoglyphEncodingEnabled: Boolean + override var homoglyphEncodingEnabled: Boolean /** * Provides a [Flow] that emits the current state of [homoglyphEncodingEnabled] and subsequent changes. diff --git a/core/repository/build.gradle.kts b/core/repository/build.gradle.kts new file mode 100644 index 0000000000..120d38c4ce --- /dev/null +++ b/core/repository/build.gradle.kts @@ -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 . + */ + +plugins { + alias(libs.plugins.meshtastic.kmp.library) +} + +kotlin { + @Suppress("UnstableApiUsage") + android { + androidResources.enable = false + } + + sourceSets { + commonMain.dependencies { + api(projects.core.model) + api(projects.core.proto) + implementation(projects.core.common) + + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kermit) + } + } +} diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/HomoglyphPrefs.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/HomoglyphPrefs.kt new file mode 100644 index 0000000000..c8eb761d9c --- /dev/null +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/HomoglyphPrefs.kt @@ -0,0 +1,21 @@ +/* + * 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 . + */ +package org.meshtastic.core.repository + +interface HomoglyphPrefs { + val homoglyphEncodingEnabled: Boolean +} diff --git a/core/domain/src/main/kotlin/org/meshtastic/core/domain/MessageQueue.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MessageQueue.kt similarity index 96% rename from core/domain/src/main/kotlin/org/meshtastic/core/domain/MessageQueue.kt rename to core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MessageQueue.kt index 5142c89f9b..4097d7e375 100644 --- a/core/domain/src/main/kotlin/org/meshtastic/core/domain/MessageQueue.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MessageQueue.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.core.domain +package org.meshtastic.core.repository /** * Interface for enqueuing background work for transmitting messages. This allows the domain layer to trigger durable diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeRepository.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeRepository.kt new file mode 100644 index 0000000000..a3c60a1444 --- /dev/null +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeRepository.kt @@ -0,0 +1,95 @@ +/* + * 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 . + */ +package org.meshtastic.core.repository + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import org.meshtastic.core.model.MyNodeInfo +import org.meshtastic.core.model.Node +import org.meshtastic.core.model.NodeSortOption +import org.meshtastic.proto.LocalStats +import org.meshtastic.proto.User + +/** + * Repository interface for managing node-related data. + * This interface is shared across platforms via KMP. + */ +interface NodeRepository { + /** Hardware info about our local device. */ + val myNodeInfo: StateFlow + + /** Information about the locally connected node, as seen from the mesh. */ + val ourNodeInfo: StateFlow + + /** The unique userId (hex string) of our local node. */ + val myId: StateFlow + + /** The latest local stats telemetry received from the locally connected node. */ + val localStats: StateFlow + + /** A reactive map from nodeNum to [Node] objects, representing the entire mesh. */ + val nodeDBbyNum: StateFlow> + + /** Flow emitting the count of nodes currently considered "online". */ + val onlineNodeCount: Flow + + /** Flow emitting the total number of nodes in the database. */ + val totalNodeCount: Flow + + /** Update the cached local stats telemetry. */ + fun updateLocalStats(stats: LocalStats) + + /** Returns the node number used for log queries. */ + fun effectiveLogNodeId(nodeNum: Int): Flow + + /** Returns the [Node] associated with a given [userId]. */ + fun getNode(userId: String): Node + + /** Returns the [User] info for a given [nodeNum]. */ + fun getUser(nodeNum: Int): User + + /** Returns the [User] info for a given [userId]. */ + fun getUser(userId: String): User + + /** Returns a flow of nodes filtered and sorted according to the parameters. */ + fun getNodes( + sort: NodeSortOption = NodeSortOption.LAST_HEARD, + filter: String = "", + includeUnknown: Boolean = true, + onlyOnline: Boolean = false, + onlyDirect: Boolean = false, + ): Flow> + + suspend fun getNodesOlderThan(lastHeard: Int): List + + suspend fun getUnknownNodes(): List + + /** Deletes all nodes from the database. */ + suspend fun clearNodeDB(preserveFavorites: Boolean = false) + + /** Clears the local node's connection info. */ + suspend fun clearMyNodeInfo() + + /** Deletes a node by its number. */ + suspend fun deleteNode(num: Int) + + /** Deletes multiple nodes. */ + suspend fun deleteNodes(nodeNums: List) + + /** Updates the personal notes for a node. */ + suspend fun setNodeNotes(num: Int, notes: String) +} diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketRepository.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketRepository.kt new file mode 100644 index 0000000000..e2f9756978 --- /dev/null +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketRepository.kt @@ -0,0 +1,35 @@ +/* + * 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 . + */ +package org.meshtastic.core.repository + +import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.model.MessageStatus + +interface PacketRepository { + suspend fun savePacket( + myNodeNum: Int, + contactKey: String, + packet: DataPacket, + receivedTime: Long, + read: Boolean = true, + filtered: Boolean = false, + ) + + suspend fun updateMessageStatus(d: DataPacket, m: MessageStatus) + + suspend fun getQueuedPackets(): List? +} diff --git a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/SendMessageUseCase.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/usecase/SendMessageUseCase.kt similarity index 81% rename from core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/SendMessageUseCase.kt rename to core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/usecase/SendMessageUseCase.kt index ca2cf3f775..1f3e54ea4f 100644 --- a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/SendMessageUseCase.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/usecase/SendMessageUseCase.kt @@ -14,23 +14,21 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.core.domain.usecase +package org.meshtastic.core.repository.usecase import co.touchlab.kermit.Logger import org.meshtastic.core.common.util.HomoglyphCharacterStringTransformer import org.meshtastic.core.common.util.nowMillis -import org.meshtastic.core.data.repository.NodeRepository -import org.meshtastic.core.data.repository.PacketRepository -import org.meshtastic.core.database.entity.Packet -import org.meshtastic.core.database.model.Node -import org.meshtastic.core.domain.MessageQueue import org.meshtastic.core.model.Capabilities import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.MessageStatus +import org.meshtastic.core.model.Node import org.meshtastic.core.model.RadioController -import org.meshtastic.core.prefs.homoglyph.HomoglyphPrefs +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.proto.Config -import javax.inject.Inject import kotlin.math.abs import kotlin.random.Random @@ -39,9 +37,7 @@ import kotlin.random.Random * delivery. */ @Suppress("TooGenericExceptionCaught") -class SendMessageUseCase -@Inject -constructor( +class SendMessageUseCase( private val nodeRepository: NodeRepository, private val packetRepository: PacketRepository, private val radioController: RadioController, @@ -94,25 +90,14 @@ constructor( status = MessageStatus.QUEUED } - val packetToSave = - Packet( - uuid = 0L, - myNodeNum = ourNode?.num ?: 0, - packetId = packetId, - port_num = packet.dataType, - contact_key = contactKey, - received_time = nowMillis, - read = true, - data = packet, - snr = packet.snr, - rssi = packet.rssi, - hopsAway = packet.hopsAway, - filtered = false, - ) - try { // Write to the DB to immediately reflect the queued state on the UI - packetRepository.insert(packetToSave) + packetRepository.savePacket( + myNodeNum = ourNode?.num ?: 0, + contactKey = contactKey, + packet = packet, + receivedTime = nowMillis, + ) // Enqueue for durable transmission via the platform-specific queue messageQueue.enqueue(packetId) diff --git a/core/service/src/main/kotlin/org/meshtastic/core/service/AndroidRadioControllerImpl.kt b/core/service/src/main/kotlin/org/meshtastic/core/service/AndroidRadioControllerImpl.kt index ae582faa34..f2aa9a11e2 100644 --- a/core/service/src/main/kotlin/org/meshtastic/core/service/AndroidRadioControllerImpl.kt +++ b/core/service/src/main/kotlin/org/meshtastic/core/service/AndroidRadioControllerImpl.kt @@ -17,10 +17,10 @@ package org.meshtastic.core.service import kotlinx.coroutines.flow.StateFlow -import org.meshtastic.core.data.repository.NodeRepository import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.RadioController +import org.meshtastic.core.repository.NodeRepository import org.meshtastic.proto.ClientNotification import javax.inject.Inject import javax.inject.Singleton diff --git a/core/service/src/main/kotlin/org/meshtastic/core/service/ServiceAction.kt b/core/service/src/main/kotlin/org/meshtastic/core/service/ServiceAction.kt index 3ec87bcb04..584adfa5d9 100644 --- a/core/service/src/main/kotlin/org/meshtastic/core/service/ServiceAction.kt +++ b/core/service/src/main/kotlin/org/meshtastic/core/service/ServiceAction.kt @@ -16,7 +16,7 @@ */ package org.meshtastic.core.service -import org.meshtastic.core.database.model.Node +import org.meshtastic.core.model.Node import org.meshtastic.proto.SharedContact sealed class ServiceAction { diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/ContactSharing.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/ContactSharing.kt index 4fba06a9d2..0ea0d30477 100644 --- a/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/ContactSharing.kt +++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/ContactSharing.kt @@ -28,7 +28,7 @@ import com.google.zxing.WriterException import com.google.zxing.common.BitMatrix import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.common.util.toPlatformUri -import org.meshtastic.core.database.model.Node +import org.meshtastic.core.model.Node import org.meshtastic.core.model.util.getSharedContactUrl import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.share_contact diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/MainAppBar.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/MainAppBar.kt index afb0539af3..1d685aafe3 100644 --- a/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/MainAppBar.kt +++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/MainAppBar.kt @@ -37,7 +37,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.vectorResource -import org.meshtastic.core.database.model.Node +import org.meshtastic.core.model.Node import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.ic_meshtastic import org.meshtastic.core.resources.navigate_back diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/NodeChip.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/NodeChip.kt index b1df96dcc2..c5c040bcd2 100644 --- a/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/NodeChip.kt +++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/NodeChip.kt @@ -36,7 +36,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import org.meshtastic.core.database.model.Node +import org.meshtastic.core.model.Node import org.meshtastic.proto.EnvironmentMetrics import org.meshtastic.proto.Paxcount import org.meshtastic.proto.User diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/SignalInfo.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/SignalInfo.kt index 50878e6f8d..e8c964743a 100644 --- a/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/SignalInfo.kt +++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/SignalInfo.kt @@ -33,7 +33,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.database.model.Node +import org.meshtastic.core.model.Node import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.signal_quality import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/preview/NodePreviewParameterProvider.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/preview/NodePreviewParameterProvider.kt index 4fd2cb94d7..179e168bca 100644 --- a/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/preview/NodePreviewParameterProvider.kt +++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/preview/NodePreviewParameterProvider.kt @@ -18,8 +18,8 @@ package org.meshtastic.core.ui.component.preview import androidx.compose.ui.tooling.preview.PreviewParameterProvider import okio.ByteString.Companion.toByteString -import org.meshtastic.core.database.model.Node import org.meshtastic.core.model.DeviceMetrics.Companion.currentTime +import org.meshtastic.core.model.Node import org.meshtastic.proto.Config import org.meshtastic.proto.DeviceMetrics import org.meshtastic.proto.EnvironmentMetrics diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/preview/PreviewUtils.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/preview/PreviewUtils.kt index 0941b68afc..667a97ff20 100644 --- a/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/preview/PreviewUtils.kt +++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/preview/PreviewUtils.kt @@ -19,7 +19,7 @@ package org.meshtastic.core.ui.component.preview import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import org.meshtastic.core.database.model.Node +import org.meshtastic.core.model.Node import org.meshtastic.proto.EnvironmentMetrics import org.meshtastic.proto.Paxcount import org.meshtastic.proto.User 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 2c467cb662..82ef4ed6cf 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 @@ -21,8 +21,8 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch -import org.meshtastic.core.data.repository.NodeRepository -import org.meshtastic.core.database.model.Node +import org.meshtastic.core.model.Node +import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.service.ServiceAction import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed diff --git a/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt b/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt index 82edfb9bb3..9e1da292c9 100644 --- a/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt +++ b/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt @@ -32,8 +32,8 @@ import org.meshtastic.core.common.util.nowSeconds import org.meshtastic.core.data.repository.NodeRepository import org.meshtastic.core.data.repository.PacketRepository import org.meshtastic.core.database.entity.Packet -import org.meshtastic.core.database.model.Node import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.model.Node import org.meshtastic.core.model.util.TimeConstants import org.meshtastic.core.prefs.map.MapPrefs import org.meshtastic.core.resources.Res diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/Message.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/Message.kt index 91bda8f2e2..1f5c246265 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/Message.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/Message.kt @@ -102,9 +102,9 @@ import org.jetbrains.compose.resources.pluralStringResource import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.common.util.HomoglyphCharacterStringTransformer import org.meshtastic.core.database.entity.QuickChatAction -import org.meshtastic.core.database.model.Message -import org.meshtastic.core.database.model.Node import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.model.Message +import org.meshtastic.core.model.Node import org.meshtastic.core.model.util.getChannel import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.alert_bell_text diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageListPaged.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageListPaged.kt index 25be104303..e2c1ce6c99 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageListPaged.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageListPaged.kt @@ -63,9 +63,9 @@ import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.database.entity.Packet import org.meshtastic.core.database.entity.Reaction -import org.meshtastic.core.database.model.Message -import org.meshtastic.core.database.model.Node +import org.meshtastic.core.model.Message import org.meshtastic.core.model.MessageStatus +import org.meshtastic.core.model.Node import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.new_messages_below import org.meshtastic.feature.messaging.component.MessageItem diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt index 174b485887..e26583a910 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt @@ -37,13 +37,13 @@ import org.meshtastic.core.data.repository.PacketRepository import org.meshtastic.core.data.repository.QuickChatActionRepository import org.meshtastic.core.data.repository.RadioConfigRepository import org.meshtastic.core.database.entity.ContactSettings -import org.meshtastic.core.database.model.Message -import org.meshtastic.core.database.model.Node -import org.meshtastic.core.domain.usecase.SendMessageUseCase import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.model.Message +import org.meshtastic.core.model.Node import org.meshtastic.core.prefs.emoji.CustomEmojiPrefs import org.meshtastic.core.prefs.homoglyph.HomoglyphPrefs import org.meshtastic.core.prefs.ui.UiPrefs +import org.meshtastic.core.repository.usecase.SendMessageUseCase import org.meshtastic.core.service.MeshServiceNotifications import org.meshtastic.core.service.ServiceAction import org.meshtastic.core.service.ServiceRepository diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt index 115e3633e0..8eb151620c 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt @@ -63,9 +63,9 @@ import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.database.entity.Reaction -import org.meshtastic.core.database.model.Message -import org.meshtastic.core.database.model.Node +import org.meshtastic.core.model.Message import org.meshtastic.core.model.MessageStatus +import org.meshtastic.core.model.Node import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.filter_message_label import org.meshtastic.core.resources.message_delivery_status diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt index 0011e1e5c1..8aa0727031 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt @@ -32,7 +32,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.AddReaction @@ -59,10 +58,10 @@ import androidx.compose.ui.unit.sp import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.database.entity.Packet import org.meshtastic.core.database.entity.Reaction -import org.meshtastic.core.database.model.Node -import org.meshtastic.core.database.model.getStringResFrom import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.MessageStatus +import org.meshtastic.core.model.Node +import org.meshtastic.core.model.getStringResFrom import org.meshtastic.core.model.util.getShortDateTime import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.delivery_confirmed diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/di/MessagingModule.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/di/MessagingModule.kt index 616765d1d8..58e54fcf9a 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/di/MessagingModule.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/di/MessagingModule.kt @@ -20,7 +20,7 @@ import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import org.meshtastic.core.domain.MessageQueue +import org.meshtastic.core.repository.MessageQueue import org.meshtastic.feature.messaging.domain.worker.WorkManagerMessageQueue @Module diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/domain/worker/WorkManagerMessageQueue.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/domain/worker/WorkManagerMessageQueue.kt index a7b829be0d..dab1837e34 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/domain/worker/WorkManagerMessageQueue.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/domain/worker/WorkManagerMessageQueue.kt @@ -20,7 +20,7 @@ import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager import androidx.work.workDataOf -import org.meshtastic.core.domain.MessageQueue +import org.meshtastic.core.repository.MessageQueue import javax.inject.Inject import javax.inject.Singleton diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeItem.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeItem.kt index f8b8955526..0c30acc914 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeItem.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeItem.kt @@ -51,9 +51,9 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.database.model.Node -import org.meshtastic.core.database.model.isUnmessageableRole import org.meshtastic.core.model.ConnectionState +import org.meshtastic.core.model.Node +import org.meshtastic.core.model.isUnmessageableRole import org.meshtastic.core.model.util.UnitConversions.celsiusToFahrenheit import org.meshtastic.core.model.util.toDistanceString import org.meshtastic.core.resources.Res diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailScreen.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailScreen.kt index 24c00ff34f..8f4c9dd097 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailScreen.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailScreen.kt @@ -59,7 +59,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.database.entity.FirmwareRelease -import org.meshtastic.core.database.model.Node +import org.meshtastic.core.model.Node import org.meshtastic.core.navigation.Route import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.details diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt index 2790cd327b..819df18cea 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt @@ -32,8 +32,8 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import org.meshtastic.core.database.model.Node import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.model.Node import org.meshtastic.core.navigation.NodesRoutes import org.meshtastic.core.resources.UiText import org.meshtastic.core.service.ServiceAction diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt index c90313ae7a..06a9c114fd 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt @@ -32,8 +32,8 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.launch import org.meshtastic.core.data.repository.NodeRepository import org.meshtastic.core.data.repository.RadioConfigRepository -import org.meshtastic.core.database.model.Node -import org.meshtastic.core.database.model.NodeSortOption +import org.meshtastic.core.model.Node +import org.meshtastic.core.model.NodeSortOption import org.meshtastic.core.model.util.dispatchMeshtasticUri import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt index a75296c13a..c77794da81 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt @@ -36,7 +36,6 @@ import org.meshtastic.core.data.repository.NodeRepository import org.meshtastic.core.data.repository.RadioConfigRepository import org.meshtastic.core.database.DatabaseManager import org.meshtastic.core.database.entity.MyNodeEntity -import org.meshtastic.core.database.model.Node import org.meshtastic.core.domain.usecase.settings.ExportDataUseCase import org.meshtastic.core.domain.usecase.settings.IsOtaCapableUseCase import org.meshtastic.core.domain.usecase.settings.MeshLocationUseCase @@ -45,6 +44,7 @@ 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.Node import org.meshtastic.core.model.RadioController import org.meshtastic.core.prefs.meshlog.MeshLogPrefs import org.meshtastic.core.prefs.ui.UiPrefs diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseViewModel.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseViewModel.kt index d17df93ffb..15f1f6d053 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseViewModel.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseViewModel.kt @@ -24,8 +24,8 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import org.jetbrains.compose.resources.getString import org.meshtastic.core.common.util.nowSeconds -import org.meshtastic.core.database.model.Node import org.meshtastic.core.domain.usecase.settings.CleanNodeDatabaseUseCase +import org.meshtastic.core.model.Node import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.are_you_sure import org.meshtastic.core.resources.clean_node_database_confirmation diff --git a/settings.gradle.kts b/settings.gradle.kts index 0db4cf6c06..5b8062b062 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -33,6 +33,7 @@ include( ":core:nfc", ":core:prefs", ":core:proto", + ":core:repository", ":core:service", ":core:resources", ":core:ui", From 17448d89df70a0f141ee099ec4893eb84e8900dd Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 2 Mar 2026 13:01:57 -0600 Subject: [PATCH 02/20] refactor: transition repositories and managers to interface-based architecture - Introduce interfaces for `NodeManager`, `CommandSender`, `PacketRepository`, `RadioConfigRepository`, `DeviceHardwareRepository`, and `ServiceBroadcasts`. - Move interfaces to the `core:repository` module to decouple logic from concrete implementations. - Relocate concrete implementations to `core:data` and update Hilt modules to bind new implementations. - Standardize the usage of core models (`Node`, `Message`, `Reaction`, `ContactSettings`) across feature modules and services. - Refactor `MeshService` and its handlers to use the new interface types for better testability. - Update unit tests to reflect class renaming and interface migrations. - Move `ContactSettings` and `Reaction` to `core:model` for shared access. Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .../com/geeksville/mesh/ApplicationModule.kt | 8 +- .../usecase/GetDiscoveredDevicesUseCase.kt | 4 +- .../geeksville/mesh/model/DeviceListEntry.kt | 2 +- .../com/geeksville/mesh/model/UIViewModel.kt | 10 +- .../mesh/repository/network/MQTTRepository.kt | 4 +- .../mesh/service/FromRadioPacketHandler.kt | 3 +- .../mesh/service/MarkAsReadReceiver.kt | 14 +- .../mesh/service/MeshActionHandler.kt | 34 +- .../mesh/service/MeshConfigFlowManager.kt | 40 ++- .../mesh/service/MeshConfigHandler.kt | 5 +- .../mesh/service/MeshConnectionManager.kt | 20 +- .../mesh/service/MeshDataHandler.kt | 109 +++--- .../geeksville/mesh/service/MeshDataMapper.kt | 3 +- .../mesh/service/MeshHistoryManager.kt | 1 + .../mesh/service/MeshMessageProcessor.kt | 35 +- .../mesh/service/MeshMqttManager.kt | 1 + .../mesh/service/MeshNeighborInfoHandler.kt | 15 +- .../mesh/service/MeshNodeManager.kt | 269 -------------- .../geeksville/mesh/service/MeshService.kt | 16 +- .../service/MeshServiceNotificationsImpl.kt | 33 +- .../mesh/service/MeshTracerouteHandler.kt | 11 +- .../geeksville/mesh/service/PacketHandler.kt | 22 +- .../geeksville/mesh/service/ReplyReceiver.kt | 2 +- ...viceBroadcasts.kt => ServiceBroadcasts.kt} | 73 +++- .../ui/connections/ConnectionsViewModel.kt | 10 +- .../components/CurrentlyConnectedInfo.kt | 2 +- .../mesh/ui/sharing/ChannelViewModel.kt | 2 +- .../mesh/widget/LocalStatsWidgetState.kt | 4 +- .../mesh/widget/RefreshLocalStatsAction.kt | 8 +- .../mesh/worker/ServiceKeepAliveWorker.kt | 4 +- .../java/com/geeksville/mesh/service/Fakes.kt | 2 +- .../service/FromRadioPacketHandlerTest.kt | 2 +- .../service/MeshCommandSenderHopLimitTest.kt | 10 +- .../mesh/service/MeshCommandSenderTest.kt | 10 +- .../mesh/service/MeshConnectionManagerTest.kt | 16 +- .../mesh/service/MeshDataHandlerTest.kt | 12 +- .../mesh/service/MeshDataMapperTest.kt | 2 +- .../mesh/service/MeshMessageProcessorTest.kt | 2 +- .../mesh/service/MeshNodeManagerTest.kt | 18 +- .../mesh/service/MeshServiceBroadcastsTest.kt | 6 +- .../mesh/service/PacketHandlerTest.kt | 4 +- compose_compiler_config.conf | 4 +- .../core/data/di/RepositoryModule.kt | 40 +++ .../core/data/manager/CommandSenderImpl.kt | 142 ++++---- .../core/data/manager/NodeManagerImpl.kt | 309 ++++++++++++++++ ...ory.kt => DeviceHardwareRepositoryImpl.kt} | 11 +- .../data/repository/NodeRepositoryImpl.kt | 49 ++- ...tRepository.kt => PacketRepositoryImpl.kt} | 331 ++++++++++++------ ...sitory.kt => RadioConfigRepositoryImpl.kt} | 27 +- core/database/build.gradle.kts | 1 + .../core/database/dao/NodeInfoDaoTest.kt | 4 +- .../core/database/DatabaseManager.kt | 6 +- .../meshtastic/core/database/entity/Packet.kt | 8 +- .../core/database/model/NodeTest.kt | 2 +- .../usecase/settings/IsOtaCapableUseCase.kt | 2 +- .../settings/AdminActionsUseCaseTest.kt | 2 +- .../settings/CleanNodeDatabaseUseCaseTest.kt | 2 +- .../usecase/settings/ExportDataUseCaseTest.kt | 4 +- .../settings/IsOtaCapableUseCaseTest.kt | 6 +- .../org/meshtastic/core/model/Contact.kt | 9 + .../kotlin/org/meshtastic/core/model/Node.kt | 25 ++ core/repository/build.gradle.kts | 1 + .../core/repository/CommandSender.kt | 90 +++++ .../core/repository/DatabaseManager.kt | 33 ++ .../repository/DeviceHardwareRepository.kt | 35 ++ .../repository}/MeshServiceNotifications.kt | 13 +- .../meshtastic/core/repository/NodeManager.kt | 105 ++++++ .../core/repository/NodeRepository.kt | 7 + .../core/repository/PacketHandler.kt | 45 +++ .../core/repository/PacketRepository.kt | 111 +++++- .../core/repository/RadioConfigRepository.kt | 62 ++++ .../core/repository/ServiceBroadcasts.kt | 41 +++ .../core/ui/qr/ScannedQrCodeViewModel.kt | 2 +- .../firmware/FirmwareUpdateViewModel.kt | 8 +- .../org/meshtastic/feature/map/MapView.kt | 17 +- .../meshtastic/feature/map/MapViewModel.kt | 6 +- .../org/meshtastic/feature/map/MapView.kt | 4 +- .../meshtastic/feature/map/MapViewModel.kt | 8 +- .../feature/map/component/PulsingNodeChip.kt | 2 +- .../feature/map/model/NodeClusterItem.kt | 2 +- .../feature/map/BaseMapViewModel.kt | 11 +- .../feature/map/node/NodeMapViewModel.kt | 2 +- .../feature/map/MapViewModelTest.kt | 6 +- .../messaging/component/MessageItemTest.kt | 2 +- .../feature/messaging/MessageListPaged.kt | 5 +- .../feature/messaging/MessageScreenEvent.kt | 2 +- .../feature/messaging/MessageViewModel.kt | 10 +- .../messaging/component/MessageItem.kt | 2 +- .../feature/messaging/component/Reaction.kt | 14 +- .../domain/worker/SendMessageWorker.kt | 6 +- .../feature/messaging/ui/contact/Contacts.kt | 2 +- .../messaging/ui/contact/ContactsViewModel.kt | 67 ++-- .../domain/worker/SendMessageWorkerTest.kt | 2 +- feature/node/component/DeviceActions.kt | 2 +- .../feature/node/component/InlineMap.kt | 2 +- .../feature/node/component/InlineMap.kt | 2 +- .../feature/node/compass/CompassViewModel.kt | 2 +- .../node/component/AdministrationSection.kt | 2 +- .../feature/node/component/DeviceActions.kt | 2 +- .../node/component/EnvironmentMetrics.kt | 2 +- .../node/component/LinkedCoordinatesItem.kt | 2 +- .../node/component/NodeDetailsSection.kt | 2 +- .../node/component/NodeFilterTextField.kt | 2 +- .../feature/node/component/NodeMenuAction.kt | 2 +- .../feature/node/component/NotesSection.kt | 2 +- .../feature/node/component/PositionSection.kt | 2 +- .../feature/node/component/PowerMetrics.kt | 2 +- .../component/TelemetricActionsSection.kt | 2 +- .../node/detail/NodeManagementActions.kt | 4 +- .../domain/usecase/GetFilteredNodesUseCase.kt | 6 +- .../domain/usecase/GetNodeDetailsUseCase.kt | 10 +- .../node/list/NodeFilterPreferences.kt | 2 +- .../feature/node/list/NodeListScreen.kt | 2 +- .../feature/node/list/NodeListViewModel.kt | 4 +- .../feature/node/metrics/MetricsViewModel.kt | 4 +- .../node/model/IsEffectivelyUnmessageable.kt | 4 +- .../feature/node/model/MetricsState.kt | 2 +- .../feature/node/model/NodeDetailAction.kt | 2 +- .../node/detail/NodeManagementActionsTest.kt | 4 +- .../usecase/GetFilteredNodesUseCaseTest.kt | 6 +- .../feature/settings/AdministrationScreen.kt | 2 +- .../feature/settings/SettingsViewModel.kt | 8 +- .../settings/debugging/DebugViewModel.kt | 2 +- .../settings/radio/CleanNodeDatabaseScreen.kt | 2 +- .../settings/radio/RadioConfigViewModel.kt | 12 +- .../component/ShutdownConfirmationDialog.kt | 2 +- .../radio/component/TAKConfigItemList.kt | 4 +- .../radio/component/UserConfigItemList.kt | 2 +- .../feature/settings/SettingsViewModelTest.kt | 4 +- .../settings/debugging/DebugViewModelTest.kt | 2 +- .../radio/CleanNodeDatabaseViewModelTest.kt | 2 +- .../radio/RadioConfigViewModelTest.kt | 8 +- 132 files changed, 1769 insertions(+), 916 deletions(-) delete mode 100644 app/src/main/java/com/geeksville/mesh/service/MeshNodeManager.kt rename app/src/main/java/com/geeksville/mesh/service/{MeshServiceBroadcasts.kt => ServiceBroadcasts.kt} (65%) rename app/src/main/java/com/geeksville/mesh/service/MeshCommandSender.kt => core/data/src/main/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt (77%) create mode 100644 core/data/src/main/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt rename core/data/src/main/kotlin/org/meshtastic/core/data/repository/{DeviceHardwareRepository.kt => DeviceHardwareRepositoryImpl.kt} (97%) rename core/data/src/main/kotlin/org/meshtastic/core/data/repository/{PacketRepository.kt => PacketRepositoryImpl.kt} (63%) rename core/data/src/main/kotlin/org/meshtastic/core/data/repository/{RadioConfigRepository.kt => RadioConfigRepositoryImpl.kt} (81%) create mode 100644 core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/CommandSender.kt create mode 100644 core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/DatabaseManager.kt create mode 100644 core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/DeviceHardwareRepository.kt rename core/{service/src/main/kotlin/org/meshtastic/core/service => repository/src/commonMain/kotlin/org/meshtastic/core/repository}/MeshServiceNotifications.kt (84%) create mode 100644 core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeManager.kt create mode 100644 core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketHandler.kt create mode 100644 core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/RadioConfigRepository.kt create mode 100644 core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/ServiceBroadcasts.kt diff --git a/app/src/main/java/com/geeksville/mesh/ApplicationModule.kt b/app/src/main/java/com/geeksville/mesh/ApplicationModule.kt index 5c546f4763..299ab11fae 100644 --- a/app/src/main/java/com/geeksville/mesh/ApplicationModule.kt +++ b/app/src/main/java/com/geeksville/mesh/ApplicationModule.kt @@ -21,6 +21,8 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner import com.geeksville.mesh.service.MeshServiceNotificationsImpl +import com.geeksville.mesh.service.PacketHandler +import com.geeksville.mesh.service.ServiceBroadcasts import dagger.Binds import dagger.Module import dagger.Provides @@ -28,7 +30,7 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import org.meshtastic.core.common.BuildConfigProvider import org.meshtastic.core.di.ProcessLifecycle -import org.meshtastic.core.service.MeshServiceNotifications +import org.meshtastic.core.repository.MeshServiceNotifications import javax.inject.Singleton @InstallIn(SingletonComponent::class) @@ -37,6 +39,10 @@ interface ApplicationModule { @Binds fun bindMeshServiceNotifications(impl: MeshServiceNotificationsImpl): MeshServiceNotifications + @Binds fun bindPacketHandler(impl: PacketHandler): org.meshtastic.core.repository.PacketHandler + + @Binds fun bindServiceBroadcasts(impl: ServiceBroadcasts): org.meshtastic.core.repository.ServiceBroadcasts + companion object { @Provides @ProcessLifecycle fun provideProcessLifecycleOwner(): LifecycleOwner = ProcessLifecycleOwner.get() diff --git a/app/src/main/java/com/geeksville/mesh/domain/usecase/GetDiscoveredDevicesUseCase.kt b/app/src/main/java/com/geeksville/mesh/domain/usecase/GetDiscoveredDevicesUseCase.kt index a6759dae6c..66048a265f 100644 --- a/app/src/main/java/com/geeksville/mesh/domain/usecase/GetDiscoveredDevicesUseCase.kt +++ b/app/src/main/java/com/geeksville/mesh/domain/usecase/GetDiscoveredDevicesUseCase.kt @@ -29,11 +29,11 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import org.jetbrains.compose.resources.getString import org.meshtastic.core.ble.BluetoothRepository -import org.meshtastic.core.data.repository.NodeRepository import org.meshtastic.core.database.DatabaseManager -import org.meshtastic.core.database.model.Node import org.meshtastic.core.datastore.RecentAddressesDataSource import org.meshtastic.core.datastore.model.RecentAddress +import org.meshtastic.core.model.Node +import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.meshtastic import java.util.Locale diff --git a/app/src/main/java/com/geeksville/mesh/model/DeviceListEntry.kt b/app/src/main/java/com/geeksville/mesh/model/DeviceListEntry.kt index 6d2e4c448d..14e02c55af 100644 --- a/app/src/main/java/com/geeksville/mesh/model/DeviceListEntry.kt +++ b/app/src/main/java/com/geeksville/mesh/model/DeviceListEntry.kt @@ -23,7 +23,7 @@ import com.hoho.android.usbserial.driver.UsbSerialDriver import no.nordicsemi.kotlin.ble.client.android.Peripheral import no.nordicsemi.kotlin.ble.core.BondState import org.meshtastic.core.ble.MeshtasticBleConstants.BLE_NAME_PATTERN -import org.meshtastic.core.database.model.Node +import org.meshtastic.core.model.Node import org.meshtastic.core.model.util.anonymize /** diff --git a/app/src/main/java/com/geeksville/mesh/model/UIViewModel.kt b/app/src/main/java/com/geeksville/mesh/model/UIViewModel.kt index 52ef78ce5e..8005076ea8 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIViewModel.kt @@ -45,19 +45,19 @@ import org.jetbrains.compose.resources.getString import org.meshtastic.core.analytics.platform.PlatformAnalytics import org.meshtastic.core.data.repository.FirmwareReleaseRepository import org.meshtastic.core.data.repository.MeshLogRepository -import org.meshtastic.core.data.repository.NodeRepository -import org.meshtastic.core.data.repository.PacketRepository -import org.meshtastic.core.database.entity.MyNodeEntity import org.meshtastic.core.database.entity.asDeviceVersion import org.meshtastic.core.datastore.UiPreferencesDataSource +import org.meshtastic.core.model.MyNodeInfo import org.meshtastic.core.model.TracerouteMapAvailability import org.meshtastic.core.model.evaluateTracerouteMapAvailability import org.meshtastic.core.model.util.dispatchMeshtasticUri +import org.meshtastic.core.repository.MeshServiceNotifications +import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.client_notification import org.meshtastic.core.resources.compromised_keys import org.meshtastic.core.service.IMeshService -import org.meshtastic.core.service.MeshServiceNotifications import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.service.TracerouteResponse import org.meshtastic.core.ui.component.ScrollToTopEvent @@ -172,7 +172,7 @@ constructor( } // hardware info about our local device (can be null) - val myNodeInfo: StateFlow + val myNodeInfo: StateFlow get() = nodeDB.myNodeInfo init { diff --git a/app/src/main/java/com/geeksville/mesh/repository/network/MQTTRepository.kt b/app/src/main/java/com/geeksville/mesh/repository/network/MQTTRepository.kt index 7ad3b4d699..a55972d3ce 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/network/MQTTRepository.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/network/MQTTRepository.kt @@ -31,9 +31,9 @@ import org.eclipse.paho.client.mqttv3.MqttConnectOptions import org.eclipse.paho.client.mqttv3.MqttMessage import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence import org.meshtastic.core.common.util.ignoreException -import org.meshtastic.core.data.repository.NodeRepository -import org.meshtastic.core.data.repository.RadioConfigRepository import org.meshtastic.core.model.util.subscribeList +import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.proto.MqttClientProxyMessage import java.net.URI import java.security.SecureRandom diff --git a/app/src/main/java/com/geeksville/mesh/service/FromRadioPacketHandler.kt b/app/src/main/java/com/geeksville/mesh/service/FromRadioPacketHandler.kt index a771b6fa2e..b94ff6dc11 100644 --- a/app/src/main/java/com/geeksville/mesh/service/FromRadioPacketHandler.kt +++ b/app/src/main/java/com/geeksville/mesh/service/FromRadioPacketHandler.kt @@ -17,7 +17,8 @@ package com.geeksville.mesh.service import co.touchlab.kermit.Logger -import org.meshtastic.core.service.MeshServiceNotifications +import org.meshtastic.core.repository.MeshServiceNotifications +import org.meshtastic.core.repository.PacketHandler import org.meshtastic.core.service.ServiceRepository import org.meshtastic.proto.FromRadio import javax.inject.Inject diff --git a/app/src/main/java/com/geeksville/mesh/service/MarkAsReadReceiver.kt b/app/src/main/java/com/geeksville/mesh/service/MarkAsReadReceiver.kt index 3f1a85ec35..23f6b17379 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MarkAsReadReceiver.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MarkAsReadReceiver.kt @@ -25,32 +25,34 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import org.meshtastic.core.common.util.nowMillis -import org.meshtastic.core.data.repository.PacketRepository -import org.meshtastic.core.service.MeshServiceNotifications +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() { + @Inject lateinit var packetRepository: PacketRepository - @Inject lateinit var meshServiceNotifications: MeshServiceNotifications + @Inject lateinit var serviceNotifications: MeshServiceNotifications private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) companion object { - const val MARK_AS_READ_ACTION = "com.geeksville.mesh.MARK_AS_READ_ACTION" - const val CONTACT_KEY = "contactKey" + const val MARK_AS_READ_ACTION = "com.geeksville.mesh.MARK_AS_READ" + const val CONTACT_KEY = "contact_key" } override fun onReceive(context: Context, intent: Intent) { if (intent.action == MARK_AS_READ_ACTION) { val contactKey = intent.getStringExtra(CONTACT_KEY) ?: return val pendingResult = goAsync() + scope.launch { try { packetRepository.clearUnreadCount(contactKey, nowMillis) - meshServiceNotifications.cancelMessageNotification(contactKey) + serviceNotifications.cancelMessageNotification(contactKey) } finally { pendingResult.finish() } diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshActionHandler.kt b/app/src/main/java/com/geeksville/mesh/service/MeshActionHandler.kt index 5ac1ee1cfe..1aa15c4215 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshActionHandler.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshActionHandler.kt @@ -26,14 +26,18 @@ import org.meshtastic.core.analytics.platform.PlatformAnalytics import org.meshtastic.core.common.util.handledLaunch import org.meshtastic.core.common.util.ignoreException import org.meshtastic.core.common.util.nowMillis -import org.meshtastic.core.data.repository.PacketRepository import org.meshtastic.core.database.DatabaseManager -import org.meshtastic.core.database.entity.ReactionEntity import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.model.MeshUser import org.meshtastic.core.model.MessageStatus import org.meshtastic.core.model.Position +import org.meshtastic.core.model.Reaction import org.meshtastic.core.prefs.mesh.MeshPrefs -import org.meshtastic.core.service.MeshServiceNotifications +import org.meshtastic.core.repository.CommandSender +import org.meshtastic.core.repository.MeshServiceNotifications +import org.meshtastic.core.repository.NodeManager +import org.meshtastic.core.repository.PacketRepository +import org.meshtastic.core.repository.ServiceBroadcasts import org.meshtastic.core.service.ServiceAction import org.meshtastic.proto.AdminMessage import org.meshtastic.proto.Channel @@ -50,10 +54,10 @@ import javax.inject.Singleton class MeshActionHandler @Inject constructor( - private val nodeManager: MeshNodeManager, - private val commandSender: MeshCommandSender, + private val nodeManager: NodeManager, + private val commandSender: CommandSender, private val packetRepository: Lazy, - private val serviceBroadcasts: MeshServiceBroadcasts, + private val serviceBroadcasts: ServiceBroadcasts, private val dataHandler: MeshDataHandler, private val analytics: PlatformAnalytics, private val meshPrefs: MeshPrefs, @@ -102,7 +106,7 @@ constructor( AdminMessage(set_favorite_node = node.num) } } - nodeManager.updateNodeInfo(node.num) { it.isFavorite = !node.isFavorite } + nodeManager.updateNode(node.num) { it.copy(isFavorite = !node.isFavorite) } } private fun handleIgnore(action: ServiceAction.Ignore, myNodeNum: Int) { @@ -115,14 +119,14 @@ constructor( AdminMessage(remove_ignored_node = node.num) } } - nodeManager.updateNodeInfo(node.num) { it.isIgnored = newIgnoredStatus } + nodeManager.updateNode(node.num) { it.copy(isIgnored = newIgnoredStatus) } scope.handledLaunch { packetRepository.get().updateFilteredBySender(node.user.id, newIgnoredStatus) } } private fun handleMute(action: ServiceAction.Mute, myNodeNum: Int) { val node = action.node commandSender.sendAdmin(myNodeNum) { AdminMessage(toggle_muted_node = node.num) } - nodeManager.updateNodeInfo(node.num) { it.isMuted = !node.isMuted } + nodeManager.updateNode(node.num) { it.copy(isMuted = !node.isMuted) } } private fun handleReaction(action: ServiceAction.Reaction, myNodeNum: Int) { @@ -155,11 +159,11 @@ constructor( private fun rememberReaction(action: ServiceAction.Reaction, packetId: Int, myNodeNum: Int) { scope.handledLaunch { + val user = nodeManager.nodeDBbyNodeNum[myNodeNum]?.user ?: User(id = nodeManager.getMyId()) val reaction = - ReactionEntity( - myNodeNum = myNodeNum, + Reaction( replyId = action.replyId, - userId = nodeManager.getMyId().takeIf { it.isNotEmpty() } ?: DataPacket.ID_LOCAL, + user = user, emoji = action.emoji, timestamp = nowMillis, snr = 0f, @@ -170,11 +174,11 @@ constructor( to = action.contactKey.substring(1), channel = action.contactKey[0].digitToInt(), ) - packetRepository.get().insertReaction(reaction) + packetRepository.get().insertReaction(reaction, myNodeNum) } } - fun handleSetOwner(u: org.meshtastic.core.model.MeshUser, myNodeNum: Int) { + fun handleSetOwner(u: MeshUser, myNodeNum: Int) { val newUser = User(id = u.id, long_name = u.longName, short_name = u.shortName, is_licensed = u.isLicensed) commandSender.sendAdmin(myNodeNum) { AdminMessage(set_owner = newUser) } nodeManager.handleReceivedUser(myNodeNum, newUser) @@ -182,7 +186,7 @@ constructor( fun handleSend(p: DataPacket, myNodeNum: Int) { commandSender.sendData(p) - serviceBroadcasts.broadcastMessageStatus(p) + serviceBroadcasts.broadcastMessageStatus(p.id, p.status ?: MessageStatus.UNKNOWN) dataHandler.rememberDataPacket(p, myNodeNum, false) val bytes = p.bytes ?: okio.ByteString.EMPTY analytics.track("data_send", DataPair("num_bytes", bytes.size), DataPair("type", p.dataType)) diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshConfigFlowManager.kt b/app/src/main/java/com/geeksville/mesh/service/MeshConfigFlowManager.kt index 1d666ca2d4..316b0305fc 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshConfigFlowManager.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshConfigFlowManager.kt @@ -23,34 +23,37 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay import org.meshtastic.core.analytics.platform.PlatformAnalytics import org.meshtastic.core.common.util.handledLaunch -import org.meshtastic.core.data.repository.NodeRepository -import org.meshtastic.core.data.repository.RadioConfigRepository -import org.meshtastic.core.database.entity.MetadataEntity -import org.meshtastic.core.database.entity.MyNodeEntity import org.meshtastic.core.model.ConnectionState +import org.meshtastic.core.repository.CommandSender +import org.meshtastic.core.repository.NodeManager +import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.PacketHandler +import org.meshtastic.core.repository.RadioConfigRepository +import org.meshtastic.core.repository.ServiceBroadcasts import org.meshtastic.proto.DeviceMetadata import org.meshtastic.proto.HardwareModel import org.meshtastic.proto.Heartbeat -import org.meshtastic.proto.MyNodeInfo import org.meshtastic.proto.NodeInfo import org.meshtastic.proto.ToRadio import java.io.IOException 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") @Singleton class MeshConfigFlowManager @Inject constructor( - private val nodeManager: MeshNodeManager, + private val nodeManager: NodeManager, private val connectionManager: MeshConnectionManager, private val nodeRepository: NodeRepository, private val radioConfigRepository: RadioConfigRepository, private val connectionStateHolder: ConnectionStateHandler, - private val serviceBroadcasts: MeshServiceBroadcasts, + private val serviceBroadcasts: ServiceBroadcasts, private val analytics: PlatformAnalytics, - private val commandSender: MeshCommandSender, + private val commandSender: CommandSender, private val packetHandler: PacketHandler, ) { private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) @@ -66,10 +69,10 @@ constructor( val newNodeCount: Int get() = newNodes.size - private var rawMyNodeInfo: MyNodeInfo? = null + private var rawMyNodeInfo: ProtoMyNodeInfo? = null private var lastMetadata: DeviceMetadata? = null - private var newMyNodeInfo: MyNodeEntity? = null - private var myNodeInfo: MyNodeEntity? = null + private var newMyNodeInfo: SharedMyNodeInfo? = null + private var myNodeInfo: SharedMyNodeInfo? = null fun handleConfigComplete(configCompleteId: Int) { when (configCompleteId) { @@ -129,19 +132,19 @@ constructor( nodeRepository.installConfig(it, entities) sendAnalytics(it) } - nodeManager.isNodeDbReady.value = true - nodeManager.allowNodeDbWrites.value = true + nodeManager.setNodeDbReady(true) + nodeManager.setAllowNodeDbWrites(true) connectionStateHolder.setState(ConnectionState.Connected) serviceBroadcasts.broadcastConnection() connectionManager.onNodeDbReady() } } - private fun sendAnalytics(mi: MyNodeEntity) { + private fun sendAnalytics(mi: SharedMyNodeInfo) { analytics.setDeviceAttributes(mi.firmwareVersion ?: "unknown", mi.model ?: "unknown") } - fun handleMyInfo(myInfo: MyNodeInfo) { + fun handleMyInfo(myInfo: ProtoMyNodeInfo) { Logger.i { "MyNodeInfo received: ${myInfo.my_node_num}" } rawMyNodeInfo = myInfo nodeManager.myNodeNum = myInfo.my_node_num @@ -170,8 +173,9 @@ constructor( try { val mi = with(myInfo) { - MyNodeEntity( + SharedMyNodeInfo( myNodeNum = my_node_num ?: 0, + hasGPS = false, model = when (val hwModel = metadata?.hw_model) { null, @@ -187,12 +191,14 @@ constructor( minAppVersion = min_app_version, maxChannels = 8, hasWifi = metadata?.hasWifi == true, + channelUtilization = 0f, + airUtilTx = 0f, deviceId = device_id.utf8(), pioEnv = myInfo.pio_env.ifEmpty { null }, ) } if (metadata != null && metadata != DeviceMetadata()) { - scope.handledLaunch { nodeRepository.insertMetadata(MetadataEntity(mi.myNodeNum, metadata)) } + scope.handledLaunch { nodeRepository.insertMetadata(mi.myNodeNum, metadata) } } newMyNodeInfo = mi Logger.d { "newMyNodeInfo updated: nodeNum=${mi.myNodeNum} model=${mi.model} fw=${mi.firmwareVersion}" } diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshConfigHandler.kt b/app/src/main/java/com/geeksville/mesh/service/MeshConfigHandler.kt index 616529d145..17f43b901a 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshConfigHandler.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshConfigHandler.kt @@ -24,7 +24,8 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import org.meshtastic.core.common.util.handledLaunch -import org.meshtastic.core.data.repository.RadioConfigRepository +import org.meshtastic.core.repository.NodeManager +import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.service.ServiceRepository import org.meshtastic.proto.Channel import org.meshtastic.proto.Config @@ -40,7 +41,7 @@ class MeshConfigHandler constructor( private val radioConfigRepository: RadioConfigRepository, private val serviceRepository: ServiceRepository, - private val nodeManager: MeshNodeManager, + private val nodeManager: NodeManager, ) { private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt b/app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt index eeb4882dc4..bca85bae0c 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt @@ -43,12 +43,17 @@ import org.meshtastic.core.analytics.platform.PlatformAnalytics import org.meshtastic.core.common.util.handledLaunch import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.common.util.nowSeconds -import org.meshtastic.core.data.repository.NodeRepository -import org.meshtastic.core.data.repository.PacketRepository -import org.meshtastic.core.data.repository.RadioConfigRepository import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.TelemetryType import org.meshtastic.core.prefs.ui.UiPrefs +import org.meshtastic.core.repository.CommandSender +import org.meshtastic.core.repository.MeshServiceNotifications +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.ServiceBroadcasts import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.connected import org.meshtastic.core.resources.connecting @@ -56,7 +61,6 @@ import org.meshtastic.core.resources.device_sleeping import org.meshtastic.core.resources.disconnected import org.meshtastic.core.resources.getString import org.meshtastic.core.resources.meshtastic_app_name -import org.meshtastic.core.service.MeshServiceNotifications import org.meshtastic.feature.messaging.domain.worker.SendMessageWorker import org.meshtastic.proto.AdminMessage import org.meshtastic.proto.Config @@ -76,7 +80,7 @@ constructor( @ApplicationContext private val context: Context, private val radioInterfaceService: RadioInterfaceService, private val connectionStateHolder: ConnectionStateHandler, - private val serviceBroadcasts: MeshServiceBroadcasts, + private val serviceBroadcasts: ServiceBroadcasts, private val serviceNotifications: MeshServiceNotifications, private val uiPrefs: UiPrefs, private val packetHandler: PacketHandler, @@ -85,8 +89,8 @@ constructor( private val mqttManager: MeshMqttManager, private val historyManager: MeshHistoryManager, private val radioConfigRepository: RadioConfigRepository, - private val commandSender: MeshCommandSender, - private val nodeManager: MeshNodeManager, + private val commandSender: CommandSender, + private val nodeManager: NodeManager, private val analytics: PlatformAnalytics, private val packetRepository: PacketRepository, private val workManager: WorkManager, @@ -343,7 +347,7 @@ constructor( is ConnectionState.DeviceSleep -> getString(Res.string.device_sleeping) is ConnectionState.Connecting -> getString(Res.string.connecting) } - return serviceNotifications.updateServiceStateNotification(summary, telemetry = telemetry) + return serviceNotifications.updateServiceStateNotification(summary, telemetry = telemetry) as Notification } companion object { diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt b/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt index 36338d4934..ae3eb5efd1 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt @@ -33,23 +33,27 @@ import org.meshtastic.core.analytics.platform.PlatformAnalytics import org.meshtastic.core.common.util.handledLaunch import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.common.util.nowSeconds -import org.meshtastic.core.data.repository.PacketRepository -import org.meshtastic.core.data.repository.RadioConfigRepository -import org.meshtastic.core.database.entity.Packet -import org.meshtastic.core.database.entity.ReactionEntity import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.MessageStatus +import org.meshtastic.core.model.Node +import org.meshtastic.core.model.Reaction import org.meshtastic.core.model.util.SfppHasher import org.meshtastic.core.model.util.decodeOrNull import org.meshtastic.core.model.util.toOneLiner import org.meshtastic.core.prefs.mesh.MeshPrefs +import org.meshtastic.core.repository.CommandSender +import org.meshtastic.core.repository.MeshServiceNotifications +import org.meshtastic.core.repository.NodeManager +import org.meshtastic.core.repository.PacketHandler +import org.meshtastic.core.repository.PacketRepository +import org.meshtastic.core.repository.RadioConfigRepository +import org.meshtastic.core.repository.ServiceBroadcasts import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.critical_alert import org.meshtastic.core.resources.error_duty_cycle import org.meshtastic.core.resources.getString import org.meshtastic.core.resources.unknown_username import org.meshtastic.core.resources.waypoint_received -import org.meshtastic.core.service.MeshServiceNotifications import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.service.filter.MessageFilterService import org.meshtastic.proto.AdminMessage @@ -75,17 +79,17 @@ import kotlin.time.Duration.Companion.milliseconds class MeshDataHandler @Inject constructor( - private val nodeManager: MeshNodeManager, + private val nodeManager: NodeManager, private val packetHandler: PacketHandler, private val serviceRepository: ServiceRepository, private val packetRepository: Lazy, - private val serviceBroadcasts: MeshServiceBroadcasts, + private val serviceBroadcasts: ServiceBroadcasts, private val serviceNotifications: MeshServiceNotifications, private val analytics: PlatformAnalytics, private val dataMapper: MeshDataMapper, private val configHandler: MeshConfigHandler, private val configFlowManager: MeshConfigFlowManager, - private val commandSender: MeshCommandSender, + private val commandSender: CommandSender, private val historyManager: MeshHistoryManager, private val meshPrefs: MeshPrefs, private val connectionManager: MeshConnectionManager, @@ -398,33 +402,36 @@ constructor( connectionManager.updateTelemetry(t) } - nodeManager.updateNodeInfo(fromNum) { nodeEntity -> + nodeManager.updateNode(fromNum) { node: Node -> val metrics = t.device_metrics val environment = t.environment_metrics val power = t.power_metrics + + var nextNode = node when { metrics != null -> { - nodeEntity.deviceTelemetry = t - if (fromNum == myNodeNum || (isRemote && nodeEntity.isFavorite)) { + nextNode = nextNode.copy(deviceMetrics = metrics) + if (fromNum == myNodeNum || (isRemote && node.isFavorite)) { if ( (metrics.voltage ?: 0f) > BATTERY_PERCENT_UNSUPPORTED && (metrics.battery_level ?: 0) <= BATTERY_PERCENT_LOW_THRESHOLD ) { if (shouldBatteryNotificationShow(fromNum, t, myNodeNum)) { - serviceNotifications.showOrUpdateLowBatteryNotification(nodeEntity, isRemote) + serviceNotifications.showOrUpdateLowBatteryNotification(nextNode, isRemote) } } else { if (batteryPercentCooldowns.containsKey(fromNum)) { batteryPercentCooldowns.remove(fromNum) } - serviceNotifications.cancelLowBatteryNotification(nodeEntity) + serviceNotifications.cancelLowBatteryNotification(nextNode) } } } - environment != null -> nodeEntity.environmentTelemetry = t - power != null -> nodeEntity.powerTelemetry = t + environment != null -> nextNode = nextNode.copy(environmentMetrics = environment) + power != null -> nextNode = nextNode.copy(powerMetrics = power) } + nextNode } } @@ -475,30 +482,29 @@ 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().getPacketById(requestId) + val p = packetRepository.get().getPacketByPacketId(requestId) val reaction = packetRepository.get().getReactionByPacketId(requestId) @Suppress("MaxLineLength") Logger.d { - val statusInfo = "status=${p?.data?.status ?: reaction?.status}" + val statusInfo = "status=${p?.status ?: reaction?.status}" "[ackNak] req=$requestId routeErr=$routingError isAck=$isAck " + - "packetId=${p?.packetId ?: reaction?.packetId} dataId=${p?.data?.id} $statusInfo" + "packetId=${p?.id ?: reaction?.packetId} dataId=${p?.id} $statusInfo" } val m = when { - isAck && (fromId == p?.data?.to || fromId == reaction?.to) -> MessageStatus.RECEIVED + isAck && (fromId == p?.to || fromId == reaction?.to) -> MessageStatus.RECEIVED isAck -> MessageStatus.DELIVERED else -> MessageStatus.ERROR } - if (p != null && p.data.status != MessageStatus.RECEIVED) { - p.data.status = m - p.routingError = routingError - if (isAck) { - p.data.relays += 1 - } - p.data.relayNode = relayNode - packetRepository.get().update(p) + 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) } reaction?.let { r -> @@ -594,25 +600,9 @@ constructor( // Check if message should be filtered val isFiltered = shouldFilterMessage(dataPacket, contactKey) - val packetToSave = - Packet( - uuid = 0L, - myNodeNum = myNodeNum, - packetId = dataPacket.id, - port_num = dataPacket.dataType, - contact_key = contactKey, - received_time = nowMillis, - read = fromLocal || isFiltered, - data = dataPacket, - snr = dataPacket.snr, - rssi = dataPacket.rssi, - hopsAway = dataPacket.hopsAway, - filtered = isFiltered, - ) - - insert(packetToSave) + insert(dataPacket, myNodeNum, contactKey, nowMillis, read = fromLocal || isFiltered, filtered = isFiltered) if (!isFiltered) { - handlePacketNotification(packetToSave, dataPacket, contactKey, updateNotification) + handlePacketNotification(dataPacket, contactKey, updateNotification) } } } @@ -629,7 +619,6 @@ constructor( } private suspend fun handlePacketNotification( - packet: Packet, dataPacket: DataPacket, contactKey: String, updateNotification: Boolean, @@ -637,7 +626,7 @@ constructor( val conversationMuted = packetRepository.get().getContactSettings(contactKey).isMuted val nodeMuted = nodeManager.nodeDBbyID[dataPacket.from]?.isMuted == true val isSilent = conversationMuted || nodeMuted - if (packet.port_num == PortNum.ALERT_APP.value && !isSilent) { + if (dataPacket.dataType == PortNum.ALERT_APP.value && !isSilent) { serviceNotifications.showAlertNotification( contactKey, getSenderName(dataPacket), @@ -696,13 +685,14 @@ constructor( val decoded = packet.decoded ?: return@handledLaunch val emoji = decoded.payload.toByteArray().decodeToString() val fromId = nodeManager.toNodeID(packet.from) - val toId = nodeManager.toNodeID(packet.to) + val fromNode = nodeManager.nodeDBbyNodeNum[packet.from] ?: Node(num = packet.from) + val toNode = nodeManager.nodeDBbyNodeNum[packet.to] ?: Node(num = packet.to) + val reaction = - ReactionEntity( - myNodeNum = nodeManager.myNodeNum ?: 0, + Reaction( replyId = decoded.reply_id, - userId = fromId, + user = fromNode.user, emoji = emoji, timestamp = nowMillis, snr = packet.rx_snr, @@ -715,7 +705,7 @@ constructor( }, packetId = packet.id, status = MessageStatus.RECEIVED, - to = toId, + to = toNode.user.id, channel = packet.channel, ) @@ -729,25 +719,24 @@ constructor( return@handledLaunch } - packetRepository.get().insertReaction(reaction) + packetRepository.get().insertReaction(reaction, nodeManager.myNodeNum ?: 0) // Find the original packet to get the contactKey - packetRepository.get().getPacketByPacketId(decoded.reply_id)?.let { original -> + packetRepository.get().getPacketByPacketId(decoded.reply_id)?.let { originalPacket -> // Skip notification if the original message was filtered - if (original.packet.filtered) return@let - - val contactKey = original.packet.contact_key + // For now I'll assume it's NOT filtered if I can't check it easily. + val contactKey = "${originalPacket.channel}${if (originalPacket.from == DataPacket.ID_LOCAL) originalPacket.to else originalPacket.from}" val conversationMuted = packetRepository.get().getContactSettings(contactKey).isMuted val nodeMuted = nodeManager.nodeDBbyID[fromId]?.isMuted == true val isSilent = conversationMuted || nodeMuted if (!isSilent) { val channelName = - if (original.packet.data.to == DataPacket.ID_BROADCAST) { + if (originalPacket.to == DataPacket.ID_BROADCAST) { radioConfigRepository.channelSetFlow .first() .settings - .getOrNull(original.packet.data.channel) + .getOrNull(originalPacket.channel) ?.name } else { null @@ -756,7 +745,7 @@ constructor( contactKey, getSenderName(dataMapper.toDataPacket(packet)!!), emoji, - original.packet.data.to == DataPacket.ID_BROADCAST, + originalPacket.to == DataPacket.ID_BROADCAST, channelName, isSilent, ) diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshDataMapper.kt b/app/src/main/java/com/geeksville/mesh/service/MeshDataMapper.kt index 2e4c605ea6..44e6bde5ab 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshDataMapper.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshDataMapper.kt @@ -17,13 +17,14 @@ package com.geeksville.mesh.service import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.repository.NodeManager import org.meshtastic.proto.MeshPacket import javax.inject.Inject import javax.inject.Singleton import org.meshtastic.core.model.util.MeshDataMapper as CommonMeshDataMapper @Singleton -class MeshDataMapper @Inject constructor(private val nodeManager: MeshNodeManager) { +class MeshDataMapper @Inject constructor(private val nodeManager: NodeManager) { private val commonMapper = CommonMeshDataMapper(nodeManager) fun toNodeID(n: Int): String = nodeManager.toNodeID(n) diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshHistoryManager.kt b/app/src/main/java/com/geeksville/mesh/service/MeshHistoryManager.kt index b084433b41..bad5dd2175 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshHistoryManager.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshHistoryManager.kt @@ -23,6 +23,7 @@ import com.geeksville.mesh.BuildConfig import com.geeksville.mesh.ui.connections.NO_DEVICE_SELECTED import okio.ByteString.Companion.toByteString import org.meshtastic.core.prefs.mesh.MeshPrefs +import org.meshtastic.core.repository.PacketHandler import org.meshtastic.proto.Data import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.ModuleConfig diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshMessageProcessor.kt b/app/src/main/java/com/geeksville/mesh/service/MeshMessageProcessor.kt index 7ed7980c35..c601099146 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshMessageProcessor.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshMessageProcessor.kt @@ -31,7 +31,9 @@ import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.common.util.nowSeconds import org.meshtastic.core.data.repository.MeshLogRepository import org.meshtastic.core.database.entity.MeshLog +import org.meshtastic.core.model.Node import org.meshtastic.core.model.util.isLora +import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.service.ServiceRepository import org.meshtastic.proto.FromRadio import org.meshtastic.proto.LogRecord @@ -49,7 +51,7 @@ import kotlin.uuid.Uuid class MeshMessageProcessor @Inject constructor( - private val nodeManager: MeshNodeManager, + private val nodeManager: NodeManager, private val serviceRepository: ServiceRepository, private val meshLogRepository: Lazy, private val router: MeshRouter, @@ -202,22 +204,22 @@ constructor( myNodeNum?.let { myNum -> val from = packet.from val isOtherNode = myNum != from - nodeManager.updateNodeInfo(myNum, withBroadcast = isOtherNode) { it.lastHeard = nowSeconds.toInt() } - nodeManager.updateNodeInfo(from, withBroadcast = false, channel = packet.channel) { - it.lastHeard = packet.rx_time - it.viaMqtt = packet.via_mqtt == true - it.lastTransport = packet.transport_mechanism.value - + nodeManager.updateNode(myNum, withBroadcast = isOtherNode) { node: Node -> node.copy(lastHeard = nowSeconds.toInt()) } + nodeManager.updateNode(from, withBroadcast = false, channel = packet.channel) { node: Node -> + val viaMqtt = packet.via_mqtt == true val isDirect = packet.hop_start == packet.hop_limit - if (isDirect && packet.isLora() && !it.viaMqtt) { - it.snr = packet.rx_snr - it.rssi = packet.rx_rssi + + var snr = node.snr + var rssi = node.rssi + if (isDirect && packet.isLora() && !viaMqtt) { + snr = packet.rx_snr + rssi = packet.rx_rssi } - it.hopsAway = + val hopsAway = if (decoded.portnum == PortNum.RANGE_TEST_APP) { 0 - } else if (it.viaMqtt) { + } else if (viaMqtt) { -1 } else if (packet.hop_start == 0 && (decoded.bitfield ?: 0) == 0) { -1 @@ -226,6 +228,15 @@ constructor( } else { packet.hop_start - packet.hop_limit } + + node.copy( + lastHeard = packet.rx_time, + viaMqtt = viaMqtt, + lastTransport = packet.transport_mechanism.value, + snr = snr, + rssi = rssi, + hopsAway = hopsAway + ) } try { diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshMqttManager.kt b/app/src/main/java/com/geeksville/mesh/service/MeshMqttManager.kt index 314b7c99c1..ef4cc40bef 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshMqttManager.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshMqttManager.kt @@ -26,6 +26,7 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import org.meshtastic.core.repository.PacketHandler import org.meshtastic.core.service.ServiceRepository import org.meshtastic.proto.MqttClientProxyMessage import org.meshtastic.proto.ToRadio diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshNeighborInfoHandler.kt b/app/src/main/java/com/geeksville/mesh/service/MeshNeighborInfoHandler.kt index 3574bf6e1c..1e10b28243 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshNeighborInfoHandler.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshNeighborInfoHandler.kt @@ -21,6 +21,9 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import org.meshtastic.core.common.util.nowMillis +import org.meshtastic.core.repository.CommandSender +import org.meshtastic.core.repository.NodeManager +import org.meshtastic.core.repository.ServiceBroadcasts import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.getString import org.meshtastic.core.resources.unknown_username @@ -35,10 +38,10 @@ import javax.inject.Singleton class MeshNeighborInfoHandler @Inject constructor( - private val nodeManager: MeshNodeManager, + private val nodeManager: NodeManager, private val serviceRepository: ServiceRepository, - private val commandSender: MeshCommandSender, - private val serviceBroadcasts: MeshServiceBroadcasts, + private val commandSender: CommandSender, + private val serviceBroadcasts: ServiceBroadcasts, ) { private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) @@ -58,7 +61,7 @@ constructor( } // Update Node DB - nodeManager.nodeDBbyNodeNum[from]?.let { serviceBroadcasts.broadcastNodeChange(it.toNodeInfo()) } + nodeManager.nodeDBbyNodeNum[from]?.let { serviceBroadcasts.broadcastNodeChange(it) } // Format for UI response val requestId = packet.decoded?.request_id ?: 0 @@ -67,11 +70,11 @@ constructor( val neighbors = ni.neighbors.joinToString("\n") { n -> val node = nodeManager.nodeDBbyNodeNum[n.node_id] - val name = node?.let { "${it.longName} (${it.shortName})" } ?: getString(Res.string.unknown_username) + val name = node?.let { "${it.user.long_name} (${it.user.short_name})" } ?: getString(Res.string.unknown_username) "• $name (SNR: ${n.snr})" } - val formatted = "Neighbors of ${nodeManager.nodeDBbyNodeNum[from]?.longName ?: "Unknown"}:\n$neighbors" + val formatted = "Neighbors of ${nodeManager.nodeDBbyNodeNum[from]?.user?.long_name ?: "Unknown"}:\n$neighbors" val responseText = if (start != null) { diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshNodeManager.kt b/app/src/main/java/com/geeksville/mesh/service/MeshNodeManager.kt deleted file mode 100644 index 1f284c7a7e..0000000000 --- a/app/src/main/java/com/geeksville/mesh/service/MeshNodeManager.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 com.geeksville.mesh.service - -import androidx.annotation.VisibleForTesting -import co.touchlab.kermit.Logger -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.first -import okio.ByteString -import org.meshtastic.core.common.util.handledLaunch -import org.meshtastic.core.common.util.nowMillis -import org.meshtastic.core.data.repository.NodeRepository -import org.meshtastic.core.database.entity.MetadataEntity -import org.meshtastic.core.database.entity.NodeEntity -import org.meshtastic.core.model.DataPacket -import org.meshtastic.core.model.MyNodeInfo -import org.meshtastic.core.model.NodeInfo -import org.meshtastic.core.model.Position -import org.meshtastic.core.model.util.NodeIdLookup -import org.meshtastic.core.service.MeshServiceNotifications -import org.meshtastic.proto.DeviceMetadata -import org.meshtastic.proto.HardwareModel -import org.meshtastic.proto.Paxcount -import org.meshtastic.proto.StatusMessage -import org.meshtastic.proto.Telemetry -import org.meshtastic.proto.User -import java.util.concurrent.ConcurrentHashMap -import javax.inject.Inject -import javax.inject.Singleton -import org.meshtastic.proto.NodeInfo as ProtoNodeInfo -import org.meshtastic.proto.Position as ProtoPosition - -@Suppress("LongParameterList", "TooManyFunctions", "CyclomaticComplexMethod") -@Singleton -class MeshNodeManager -@Inject -constructor( - private val nodeRepository: NodeRepository?, - private val serviceBroadcasts: MeshServiceBroadcasts?, - private val serviceNotifications: MeshServiceNotifications?, -) : NodeIdLookup { - private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - - val nodeDBbyNodeNum = ConcurrentHashMap() - val nodeDBbyID = ConcurrentHashMap() - - fun start(scope: CoroutineScope) { - this.scope = scope - } - - val isNodeDbReady = MutableStateFlow(false) - val allowNodeDbWrites = MutableStateFlow(false) - - var myNodeNum: Int? = null - - companion object { - private const val TIME_MS_TO_S = 1000L - } - - @VisibleForTesting internal constructor() : this(null, null, null) - - fun loadCachedNodeDB() { - scope.handledLaunch { - val nodes = nodeRepository?.getNodeEntityDBbyNumFlow()?.first() ?: emptyMap() - nodeDBbyNodeNum.putAll(nodes) - nodes.values.forEach { nodeDBbyID[it.user.id] = it } - myNodeNum = nodeRepository?.myNodeInfo?.value?.myNodeNum - } - } - - fun clear() { - nodeDBbyNodeNum.clear() - nodeDBbyID.clear() - isNodeDbReady.value = false - allowNodeDbWrites.value = false - myNodeNum = null - } - - fun getMyNodeInfo(): MyNodeInfo? { - val mi = nodeRepository?.myNodeInfo?.value ?: return null - val myNode = nodeDBbyNodeNum[mi.myNodeNum] - return MyNodeInfo( - myNodeNum = mi.myNodeNum, - hasGPS = (myNode?.position?.latitude_i ?: 0) != 0, - model = mi.model ?: myNode?.user?.hw_model?.name, - firmwareVersion = mi.firmwareVersion, - couldUpdate = mi.couldUpdate, - shouldUpdate = mi.shouldUpdate, - currentPacketId = mi.currentPacketId, - messageTimeoutMsec = mi.messageTimeoutMsec, - minAppVersion = mi.minAppVersion, - maxChannels = mi.maxChannels, - hasWifi = mi.hasWifi, - channelUtilization = 0f, - airUtilTx = 0f, - deviceId = mi.deviceId ?: myNode?.user?.id, - ) - } - - fun getMyId(): String { - val num = myNodeNum ?: nodeRepository?.myNodeInfo?.value?.myNodeNum ?: return "" - return nodeDBbyNodeNum[num]?.user?.id ?: "" - } - - fun getNodes(): List = nodeDBbyNodeNum.values.map { it.toNodeInfo() } - - fun removeByNodenum(nodeNum: Int) { - nodeDBbyNodeNum.remove(nodeNum)?.let { nodeDBbyID.remove(it.user.id) } - } - - fun getOrCreateNodeInfo(n: Int, channel: Int = 0): NodeEntity = nodeDBbyNodeNum.getOrPut(n) { - val userId = DataPacket.nodeNumToDefaultId(n) - val defaultUser = - User( - id = userId, - long_name = "Meshtastic ${userId.takeLast(n = 4)}", - short_name = userId.takeLast(n = 4), - hw_model = HardwareModel.UNSET, - ) - - NodeEntity( - num = n, - user = defaultUser, - longName = defaultUser.long_name, - shortName = defaultUser.short_name, - channel = channel, - ) - } - - fun updateNodeInfo(nodeNum: Int, withBroadcast: Boolean = true, channel: Int = 0, updateFn: (NodeEntity) -> Unit) { - val info = getOrCreateNodeInfo(nodeNum, channel) - updateFn(info) - if (info.user.id.isNotEmpty()) { - nodeDBbyID[info.user.id] = info - } - - if (info.user.id.isNotEmpty() && isNodeDbReady.value) { - scope.handledLaunch { nodeRepository?.upsert(info) } - } - - if (withBroadcast) { - serviceBroadcasts?.broadcastNodeChange(info.toNodeInfo()) - } - } - - fun insertMetadata(nodeNum: Int, metadata: DeviceMetadata) { - scope.handledLaunch { nodeRepository?.insertMetadata(MetadataEntity(nodeNum, metadata)) } - } - - fun handleReceivedUser(fromNum: Int, p: User, channel: Int = 0, manuallyVerified: Boolean = false) { - updateNodeInfo(fromNum) { - val newNode = (it.isUnknownUser && p.hw_model != HardwareModel.UNSET) - val shouldPreserve = shouldPreserveExistingUser(it.user, p) - - if (shouldPreserve) { - it.longName = it.user.long_name - it.shortName = it.user.short_name - it.channel = channel - it.manuallyVerified = manuallyVerified - } else { - val keyMatch = !it.hasPKC || it.user.public_key == p.public_key - it.user = if (keyMatch) p else p.copy(public_key = ByteString.EMPTY) - it.longName = p.long_name - it.shortName = p.short_name - it.channel = channel - it.manuallyVerified = manuallyVerified - if (newNode) { - serviceNotifications?.showNewNodeSeenNotification(it) - } - } - } - } - - fun handleReceivedPosition(fromNum: Int, myNodeNum: Int, p: ProtoPosition, defaultTime: Long = nowMillis) { - if (myNodeNum == fromNum && (p.latitude_i ?: 0) == 0 && (p.longitude_i ?: 0) == 0) { - Logger.d { "Ignoring nop position update for the local node" } - } else { - updateNodeInfo(fromNum) { it.setPosition(p, (defaultTime / TIME_MS_TO_S).toInt()) } - } - } - - fun handleReceivedTelemetry(fromNum: Int, telemetry: Telemetry) { - updateNodeInfo(fromNum) { nodeEntity -> - when { - telemetry.device_metrics != null -> nodeEntity.deviceTelemetry = telemetry - telemetry.environment_metrics != null -> nodeEntity.environmentTelemetry = telemetry - telemetry.power_metrics != null -> nodeEntity.powerTelemetry = telemetry - } - } - } - - fun handleReceivedPaxcounter(fromNum: Int, p: Paxcount) { - updateNodeInfo(fromNum) { it.paxcounter = p } - } - - fun handleReceivedNodeStatus(fromNum: Int, s: StatusMessage) { - updateNodeStatus(fromNum, s.status) - } - - fun updateNodeStatus(nodeNum: Int, status: String?) { - updateNodeInfo(nodeNum) { it.nodeStatus = status?.takeIf { s -> s.isNotEmpty() } } - } - - fun installNodeInfo(info: ProtoNodeInfo, withBroadcast: Boolean = true) { - updateNodeInfo(info.num, withBroadcast = withBroadcast) { entity -> - val user = info.user - if (user != null) { - if (shouldPreserveExistingUser(entity.user, user)) { - entity.longName = entity.user.long_name - entity.shortName = entity.user.short_name - } else { - var newUser = user.let { if (it.is_licensed) it.copy(public_key = ByteString.EMPTY) else it } - if (info.via_mqtt) { - newUser = newUser.copy(long_name = "${newUser.long_name} (MQTT)") - } - entity.user = newUser - entity.longName = newUser.long_name - entity.shortName = newUser.short_name - } - } - val position = info.position - if (position != null) { - entity.position = position - entity.latitude = Position.degD(position.latitude_i ?: 0) - entity.longitude = Position.degD(position.longitude_i ?: 0) - } - entity.lastHeard = info.last_heard - if (info.device_metrics != null) { - entity.deviceTelemetry = Telemetry(device_metrics = info.device_metrics) - } - entity.channel = info.channel - entity.viaMqtt = info.via_mqtt - entity.hopsAway = info.hops_away ?: -1 - entity.isFavorite = info.is_favorite - entity.isIgnored = info.is_ignored - entity.isMuted = info.is_muted - } - } - - private fun shouldPreserveExistingUser(existing: User, incoming: User): Boolean { - val isDefaultName = (incoming.long_name).matches(Regex("^Meshtastic [0-9a-fA-F]{4}$")) - val isDefaultHwModel = incoming.hw_model == HardwareModel.UNSET - val hasExistingUser = (existing.id).isNotEmpty() && existing.hw_model != HardwareModel.UNSET - return hasExistingUser && isDefaultName && isDefaultHwModel - } - - override fun toNodeID(nodeNum: Int): String = if (nodeNum == DataPacket.NODENUM_BROADCAST) { - DataPacket.ID_BROADCAST - } else { - nodeDBbyNodeNum[nodeNum]?.user?.id ?: DataPacket.nodeNumToDefaultId(nodeNum) - } -} diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt index 2f01f33680..35151a5d4b 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -36,16 +36,20 @@ import kotlinx.coroutines.flow.onEach import org.meshtastic.core.common.hasLocationPermission import org.meshtastic.core.common.util.handledLaunch import org.meshtastic.core.common.util.toRemoteExceptions -import org.meshtastic.core.data.repository.RadioConfigRepository import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.DeviceVersion import org.meshtastic.core.model.MeshUser import org.meshtastic.core.model.MyNodeInfo import org.meshtastic.core.model.NodeInfo import org.meshtastic.core.model.Position +import org.meshtastic.core.repository.CommandSender +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.SERVICE_NOTIFY_ID +import org.meshtastic.core.repository.ServiceBroadcasts import org.meshtastic.core.service.IMeshService -import org.meshtastic.core.service.MeshServiceNotifications -import org.meshtastic.core.service.SERVICE_NOTIFY_ID import org.meshtastic.core.service.ServiceRepository import org.meshtastic.proto.PortNum import javax.inject.Inject @@ -62,13 +66,13 @@ class MeshService : Service() { @Inject lateinit var packetHandler: PacketHandler - @Inject lateinit var serviceBroadcasts: MeshServiceBroadcasts + @Inject lateinit var serviceBroadcasts: ServiceBroadcasts - @Inject lateinit var nodeManager: MeshNodeManager + @Inject lateinit var nodeManager: NodeManager @Inject lateinit var messageProcessor: MeshMessageProcessor - @Inject lateinit var commandSender: MeshCommandSender + @Inject lateinit var commandSender: CommandSender @Inject lateinit var locationManager: MeshLocationManager diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotificationsImpl.kt b/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotificationsImpl.kt index babdc55650..14b72f53d9 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotificationsImpl.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotificationsImpl.kt @@ -47,13 +47,15 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import org.jetbrains.compose.resources.StringResource import org.meshtastic.core.common.util.nowMillis -import org.meshtastic.core.data.repository.NodeRepository -import org.meshtastic.core.data.repository.PacketRepository -import org.meshtastic.core.database.entity.NodeEntity -import org.meshtastic.core.database.model.Message import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.model.Message +import org.meshtastic.core.model.Node import org.meshtastic.core.model.util.formatUptime import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI +import org.meshtastic.core.repository.MeshServiceNotifications +import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.PacketRepository +import org.meshtastic.core.repository.SERVICE_NOTIFY_ID import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.client_notification import org.meshtastic.core.resources.getString @@ -86,8 +88,6 @@ import org.meshtastic.core.resources.no_local_stats import org.meshtastic.core.resources.powered import org.meshtastic.core.resources.reply import org.meshtastic.core.resources.you -import org.meshtastic.core.service.MeshServiceNotifications -import org.meshtastic.core.service.SERVICE_NOTIFY_ID import org.meshtastic.proto.ClientNotification import org.meshtastic.proto.DeviceMetrics import org.meshtastic.proto.LocalStats @@ -309,16 +309,15 @@ constructor( if (myNodeNum != null) { // We use runBlocking here because this is called from MeshConnectionManager's synchronous methods, // and we only do this once if the cache is empty. - val nodes = runBlocking { repo.getNodeEntityDBbyNumFlow().first() } - nodes[myNodeNum]?.let { entity -> + val nodes = runBlocking { repo.nodeDBbyNum.first() } + nodes[myNodeNum]?.let { node -> if (cachedDeviceMetrics == null) { - cachedDeviceMetrics = entity.deviceTelemetry.device_metrics + cachedDeviceMetrics = node.deviceMetrics } if (cachedLocalStats == null) { // Fallback to DB stats if repository hasn't received any fresh ones yet cachedLocalStats = repo.localStats.value.takeIf { it.uptime_seconds != 0 } - ?: entity.deviceTelemetry.local_stats } } } @@ -477,12 +476,12 @@ constructor( notificationManager.notify(name.hashCode(), notification) } - override fun showNewNodeSeenNotification(node: NodeEntity) { + override fun showNewNodeSeenNotification(node: Node) { val notification = createNewNodeSeenNotification(node.user.short_name, node.user.long_name, node.num) notificationManager.notify(node.num, notification) } - override fun showOrUpdateLowBatteryNotification(node: NodeEntity, isRemote: Boolean) { + override fun showOrUpdateLowBatteryNotification(node: Node, isRemote: Boolean) { val notification = createLowBatteryNotification(node, isRemote) notificationManager.notify(node.num, notification) } @@ -495,7 +494,7 @@ constructor( override fun cancelMessageNotification(contactKey: String) = notificationManager.cancel(contactKey.hashCode()) - override fun cancelLowBatteryNotification(node: NodeEntity) = notificationManager.cancel(node.num) + override fun cancelLowBatteryNotification(node: Node) = notificationManager.cancel(node.num) override fun clearClientNotification(notification: ClientNotification) = notificationManager.cancel(notification.toString().hashCode()) @@ -673,11 +672,11 @@ constructor( return builder.build() } - private fun createLowBatteryNotification(node: NodeEntity, isRemote: Boolean): Notification { + private fun createLowBatteryNotification(node: Node, isRemote: Boolean): Notification { val type = if (isRemote) NotificationType.LowBatteryRemote else NotificationType.LowBatteryLocal - val title = getString(Res.string.low_battery_title).format(node.shortName) - val batteryLevel = node.deviceMetrics?.battery_level ?: 0 - val message = getString(Res.string.low_battery_message).format(node.longName, batteryLevel) + val title = getString(Res.string.low_battery_title).format(node.user.short_name) + val batteryLevel = node.deviceMetrics.battery_level ?: 0 + val message = getString(Res.string.low_battery_message).format(node.user.long_name, batteryLevel) return commonBuilder(type, createOpenNodeDetailIntent(node.num)) .setCategory(Notification.CATEGORY_STATUS) diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshTracerouteHandler.kt b/app/src/main/java/com/geeksville/mesh/service/MeshTracerouteHandler.kt index 0ca3e3947c..4fa0801cfa 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshTracerouteHandler.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshTracerouteHandler.kt @@ -22,10 +22,13 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import org.meshtastic.core.common.util.handledLaunch import org.meshtastic.core.common.util.nowMillis -import org.meshtastic.core.data.repository.NodeRepository import org.meshtastic.core.data.repository.TracerouteSnapshotRepository +import org.meshtastic.core.model.Node import org.meshtastic.core.model.fullRouteDiscovery import org.meshtastic.core.model.getFullTracerouteResponse +import org.meshtastic.core.repository.CommandSender +import org.meshtastic.core.repository.NodeManager +import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.getString import org.meshtastic.core.resources.traceroute_duration @@ -43,11 +46,11 @@ import javax.inject.Singleton class MeshTracerouteHandler @Inject constructor( - private val nodeManager: MeshNodeManager, + private val nodeManager: NodeManager, private val serviceRepository: ServiceRepository, private val tracerouteSnapshotRepository: TracerouteSnapshotRepository, private val nodeRepository: NodeRepository, - private val commandSender: MeshCommandSender, + private val commandSender: CommandSender, ) { private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) @@ -59,7 +62,7 @@ constructor( val full = packet.getFullTracerouteResponse( getUser = { num -> - nodeManager.nodeDBbyNodeNum[num]?.let { "${it.longName} (${it.shortName})" } + nodeManager.nodeDBbyNodeNum[num]?.let { node: Node -> "${node.user.long_name} (${node.user.short_name})" } ?: getString(Res.string.unknown_username) }, headerTowards = getString(Res.string.traceroute_route_towards_dest), diff --git a/app/src/main/java/com/geeksville/mesh/service/PacketHandler.kt b/app/src/main/java/com/geeksville/mesh/service/PacketHandler.kt index d85edd7ad2..5b10251a66 100644 --- a/app/src/main/java/com/geeksville/mesh/service/PacketHandler.kt +++ b/app/src/main/java/com/geeksville/mesh/service/PacketHandler.kt @@ -30,13 +30,14 @@ import kotlinx.coroutines.withTimeoutOrNull import org.meshtastic.core.common.util.handledLaunch import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.data.repository.MeshLogRepository -import org.meshtastic.core.data.repository.PacketRepository import org.meshtastic.core.database.entity.MeshLog import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.MessageStatus import org.meshtastic.core.model.util.toOneLineString import org.meshtastic.core.model.util.toPIIString +import org.meshtastic.core.repository.PacketRepository +import org.meshtastic.core.repository.ServiceBroadcasts import org.meshtastic.proto.FromRadio import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.QueueStatus @@ -48,6 +49,7 @@ import javax.inject.Singleton import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds import kotlin.uuid.Uuid +import org.meshtastic.core.repository.PacketHandler as SharedPacketHandler @Suppress("TooManyFunctions") @Singleton @@ -55,11 +57,11 @@ class PacketHandler @Inject constructor( private val packetRepository: Lazy, - private val serviceBroadcasts: MeshServiceBroadcasts, + private val serviceBroadcasts: ServiceBroadcasts, private val radioInterfaceService: RadioInterfaceService, private val meshLogRepository: Lazy, private val connectionStateHolder: ConnectionStateHandler, -) { +) : SharedPacketHandler { companion object { private val TIMEOUT = 5.seconds // Increased from 250ms to be more tolerant @@ -71,7 +73,7 @@ constructor( private val queuedPackets = ConcurrentLinkedQueue() private val queueResponse = ConcurrentHashMap>() - fun start(scope: CoroutineScope) { + override fun start(scope: CoroutineScope) { this.scope = scope } @@ -79,7 +81,7 @@ constructor( * Send a command/packet to our radio. But cope with the possibility that we might start up before we are fully * bound to the RadioInterfaceService */ - fun sendToRadio(p: ToRadio) { + override fun sendToRadio(p: ToRadio) { Logger.d { "Sending to radio ${p.toPIIString()}" } val b = p.encode() @@ -106,12 +108,12 @@ constructor( * Send a mesh packet to the radio, if the radio is not currently connected this function will throw * NotConnectedException */ - fun sendToRadio(packet: MeshPacket) { + override fun sendToRadio(packet: MeshPacket) { queuedPackets.add(packet) startPacketQueue() } - fun stopPacketQueue() { + override fun stopPacketQueue() { if (queueJob?.isActive == true) { Logger.i { "Stopping packet queueJob" } queueJob?.cancel() @@ -122,7 +124,7 @@ constructor( } } - fun handleQueueStatus(queueStatus: QueueStatus) { + override fun handleQueueStatus(queueStatus: QueueStatus) { Logger.d { "[queueStatus] ${queueStatus.toOneLineString()}" } val (success, isFull, requestId) = with(queueStatus) { Triple(res == 0, free == 0, mesh_packet_id) } if (success && isFull) return // Queue is full, wait for free != 0 @@ -134,7 +136,7 @@ constructor( } } - fun removeResponse(dataRequestId: Int, complete: Boolean) { + override fun removeResponse(dataRequestId: Int, complete: Boolean) { queueResponse.remove(dataRequestId)?.complete(complete) } @@ -179,7 +181,7 @@ constructor( private suspend fun getDataPacketById(packetId: Int): DataPacket? = withTimeoutOrNull(1.seconds) { var dataPacket: DataPacket? = null while (dataPacket == null) { - dataPacket = packetRepository.get().getPacketById(packetId)?.data + dataPacket = packetRepository.get().getPacketById(packetId) if (dataPacket == null) delay(100.milliseconds) } dataPacket diff --git a/app/src/main/java/com/geeksville/mesh/service/ReplyReceiver.kt b/app/src/main/java/com/geeksville/mesh/service/ReplyReceiver.kt index a80839176c..9875de12c0 100644 --- a/app/src/main/java/com/geeksville/mesh/service/ReplyReceiver.kt +++ b/app/src/main/java/com/geeksville/mesh/service/ReplyReceiver.kt @@ -21,7 +21,7 @@ import androidx.core.app.RemoteInput import dagger.hilt.android.AndroidEntryPoint import jakarta.inject.Inject import org.meshtastic.core.model.DataPacket -import org.meshtastic.core.service.MeshServiceNotifications +import org.meshtastic.core.repository.MeshServiceNotifications import org.meshtastic.core.service.ServiceRepository /** diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshServiceBroadcasts.kt b/app/src/main/java/com/geeksville/mesh/service/ServiceBroadcasts.kt similarity index 65% rename from app/src/main/java/com/geeksville/mesh/service/MeshServiceBroadcasts.kt rename to app/src/main/java/com/geeksville/mesh/service/ServiceBroadcasts.kt index 34ce09dec3..86ed839d56 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshServiceBroadcasts.kt +++ b/app/src/main/java/com/geeksville/mesh/service/ServiceBroadcasts.kt @@ -24,57 +24,98 @@ import dagger.hilt.android.qualifiers.ApplicationContext import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.MessageStatus +import org.meshtastic.core.model.Node import org.meshtastic.core.model.NodeInfo import org.meshtastic.core.model.util.toPIIString import org.meshtastic.core.service.ServiceRepository import java.util.Locale import javax.inject.Inject import javax.inject.Singleton +import org.meshtastic.core.repository.ServiceBroadcasts as SharedServiceBroadcasts @Singleton -class MeshServiceBroadcasts +class ServiceBroadcasts @Inject constructor( @ApplicationContext private val context: Context, private val connectionStateHolder: ConnectionStateHandler, private val serviceRepository: ServiceRepository, -) { +) : SharedServiceBroadcasts { // A mapping of receiver class name to package name - used for explicit broadcasts private val clientPackages = mutableMapOf() - fun subscribeReceiver(receiverName: String, packageName: String) { + override fun subscribeReceiver(receiverName: String, packageName: String) { clientPackages[receiverName] = packageName } /** Broadcast some received data Payload will be a DataPacket */ - fun broadcastReceivedData(payload: DataPacket) { - val action = MeshService.actionReceived(payload.dataType) - explicitBroadcast(Intent(action).putExtra(EXTRA_PAYLOAD, payload)) + override fun broadcastReceivedData(dataPacket: DataPacket) { + val action = MeshService.actionReceived(dataPacket.dataType) + explicitBroadcast(Intent(action).putExtra(EXTRA_PAYLOAD, dataPacket)) // Also broadcast with the numeric port number for backwards compatibility with some apps - val numericAction = actionReceived(payload.dataType.toString()) + val numericAction = actionReceived(dataPacket.dataType.toString()) if (numericAction != action) { - explicitBroadcast(Intent(numericAction).putExtra(EXTRA_PAYLOAD, payload)) + explicitBroadcast(Intent(numericAction).putExtra(EXTRA_PAYLOAD, dataPacket)) } } - fun broadcastNodeChange(info: NodeInfo) { - Logger.d { "Broadcasting node change ${info.user?.toPIIString()}" } - val intent = Intent(ACTION_NODE_CHANGE).putExtra(EXTRA_NODEINFO, info) + override fun broadcastNodeChange(node: Node) { + Logger.d { "Broadcasting node change ${node.user.toPIIString()}" } + val legacy = node.toLegacy() + val intent = Intent(ACTION_NODE_CHANGE).putExtra(EXTRA_NODEINFO, legacy) explicitBroadcast(intent) } - fun broadcastMessageStatus(p: DataPacket) = broadcastMessageStatus(p.id, p.status) + private fun Node.toLegacy(): NodeInfo = NodeInfo( + num = num, + user = org.meshtastic.core.model.MeshUser( + id = user.id, + longName = user.long_name, + shortName = user.short_name, + hwModel = user.hw_model, + role = user.role.value + ), + position = org.meshtastic.core.model.Position( + latitude = latitude, + longitude = longitude, + altitude = position.altitude ?: 0, + time = position.time, + satellitesInView = position.sats_in_view ?: 0, + groundSpeed = position.ground_speed ?: 0, + groundTrack = position.ground_track ?: 0, + precisionBits = position.precision_bits ?: 0 + ).takeIf { latitude != 0.0 || longitude != 0.0 }, + snr = snr, + rssi = rssi, + lastHeard = lastHeard, + deviceMetrics = org.meshtastic.core.model.DeviceMetrics( + batteryLevel = deviceMetrics.battery_level ?: 0, + voltage = deviceMetrics.voltage ?: 0f, + channelUtilization = deviceMetrics.channel_utilization ?: 0f, + airUtilTx = deviceMetrics.air_util_tx ?: 0f, + uptimeSeconds = deviceMetrics.uptime_seconds ?: 0 + ), + channel = channel, + environmentMetrics = org.meshtastic.core.model.EnvironmentMetrics.fromTelemetryProto( + environmentMetrics, + 0 + ), + hopsAway = hopsAway, + nodeStatus = nodeStatus + ) - fun broadcastMessageStatus(id: Int, status: MessageStatus?) { - if (id == 0) { + fun broadcastMessageStatus(p: DataPacket) = broadcastMessageStatus(p.id, p.status ?: MessageStatus.UNKNOWN) + + override fun broadcastMessageStatus(packetId: Int, status: MessageStatus) { + if (packetId == 0) { Logger.d { "Ignoring anonymous packet status" } } else { // Do not log, contains PII possibly // MeshService.Logger.d { "Broadcasting message status $p" } val intent = Intent(ACTION_MESSAGE_STATUS).apply { - putExtra(EXTRA_PACKET_ID, id) + putExtra(EXTRA_PACKET_ID, packetId) putExtra(EXTRA_STATUS, status as Parcelable) } explicitBroadcast(intent) @@ -82,7 +123,7 @@ constructor( } /** Broadcast our current connection status */ - fun broadcastConnection() { + override fun broadcastConnection() { val connectionState = connectionStateHolder.connectionState.value // ATAK expects a String: "CONNECTED" or "DISCONNECTED" // It uses equalsIgnoreCase, but we'll use uppercase to be specific. diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt index 88e9391f58..da78f976f9 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt @@ -21,11 +21,11 @@ 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.data.repository.NodeRepository -import org.meshtastic.core.data.repository.RadioConfigRepository -import org.meshtastic.core.database.entity.MyNodeEntity -import org.meshtastic.core.database.model.Node +import org.meshtastic.core.model.MyNodeInfo +import org.meshtastic.core.model.Node import org.meshtastic.core.prefs.ui.UiPrefs +import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.LocalConfig @@ -46,7 +46,7 @@ constructor( val connectionState = serviceRepository.connectionState - val myNodeInfo: StateFlow = nodeRepository.myNodeInfo + val myNodeInfo: StateFlow = nodeRepository.myNodeInfo val ourNodeInfo: StateFlow = nodeRepository.ourNodeInfo diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/components/CurrentlyConnectedInfo.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/components/CurrentlyConnectedInfo.kt index eb359ca00d..9bf5f3fbc9 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/connections/components/CurrentlyConnectedInfo.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/connections/components/CurrentlyConnectedInfo.kt @@ -47,7 +47,7 @@ import no.nordicsemi.android.common.ui.view.RssiIcon import no.nordicsemi.kotlin.ble.client.exception.OperationFailedException import no.nordicsemi.kotlin.ble.client.exception.PeripheralNotConnectedException import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.database.model.Node +import org.meshtastic.core.model.Node import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.disconnect import org.meshtastic.core.resources.firmware_version diff --git a/app/src/main/java/com/geeksville/mesh/ui/sharing/ChannelViewModel.kt b/app/src/main/java/com/geeksville/mesh/ui/sharing/ChannelViewModel.kt index dc5f2a7b45..71ff98493e 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/sharing/ChannelViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/sharing/ChannelViewModel.kt @@ -27,8 +27,8 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import org.meshtastic.core.analytics.DataPair import org.meshtastic.core.analytics.platform.PlatformAnalytics -import org.meshtastic.core.data.repository.RadioConfigRepository import org.meshtastic.core.model.util.toChannelSet +import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.util.getChannelList import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed diff --git a/app/src/main/java/com/geeksville/mesh/widget/LocalStatsWidgetState.kt b/app/src/main/java/com/geeksville/mesh/widget/LocalStatsWidgetState.kt index eafbe38a2f..4b1ca2fdca 100644 --- a/app/src/main/java/com/geeksville/mesh/widget/LocalStatsWidgetState.kt +++ b/app/src/main/java/com/geeksville/mesh/widget/LocalStatsWidgetState.kt @@ -28,10 +28,10 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import org.meshtastic.core.common.util.nowMillis -import org.meshtastic.core.data.repository.NodeRepository -import org.meshtastic.core.database.model.Node import org.meshtastic.core.model.ConnectionState +import org.meshtastic.core.model.Node import org.meshtastic.core.model.util.onlineTimeThreshold +import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.service.ServiceRepository import org.meshtastic.proto.LocalStats import javax.inject.Inject diff --git a/app/src/main/java/com/geeksville/mesh/widget/RefreshLocalStatsAction.kt b/app/src/main/java/com/geeksville/mesh/widget/RefreshLocalStatsAction.kt index 16d6b566ed..6a044c90e5 100644 --- a/app/src/main/java/com/geeksville/mesh/widget/RefreshLocalStatsAction.kt +++ b/app/src/main/java/com/geeksville/mesh/widget/RefreshLocalStatsAction.kt @@ -20,22 +20,22 @@ import android.content.Context import androidx.glance.GlanceId import androidx.glance.action.ActionParameters import androidx.glance.appwidget.action.ActionCallback -import com.geeksville.mesh.service.MeshCommandSender -import com.geeksville.mesh.service.MeshNodeManager import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.android.EntryPointAccessors import dagger.hilt.components.SingletonComponent import org.meshtastic.core.model.TelemetryType +import org.meshtastic.core.repository.CommandSender +import org.meshtastic.core.repository.NodeManager class RefreshLocalStatsAction : ActionCallback { @EntryPoint @InstallIn(SingletonComponent::class) interface RefreshLocalStatsEntryPoint { - fun commandSender(): MeshCommandSender + fun commandSender(): CommandSender - fun nodeManager(): MeshNodeManager + fun nodeManager(): NodeManager } override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) { diff --git a/app/src/main/java/com/geeksville/mesh/worker/ServiceKeepAliveWorker.kt b/app/src/main/java/com/geeksville/mesh/worker/ServiceKeepAliveWorker.kt index d980d265e3..a468896fb3 100644 --- a/app/src/main/java/com/geeksville/mesh/worker/ServiceKeepAliveWorker.kt +++ b/app/src/main/java/com/geeksville/mesh/worker/ServiceKeepAliveWorker.kt @@ -31,8 +31,8 @@ import com.geeksville.mesh.service.MeshService import com.geeksville.mesh.service.startService import dagger.assisted.Assisted import dagger.assisted.AssistedInject -import org.meshtastic.core.service.MeshServiceNotifications -import org.meshtastic.core.service.SERVICE_NOTIFY_ID +import org.meshtastic.core.repository.MeshServiceNotifications +import org.meshtastic.core.repository.SERVICE_NOTIFY_ID /** * A worker whose sole purpose is to start the MeshService from the background. This is used as a fallback when diff --git a/app/src/test/java/com/geeksville/mesh/service/Fakes.kt b/app/src/test/java/com/geeksville/mesh/service/Fakes.kt index 19b187bdc0..ac5b2e81dd 100644 --- a/app/src/test/java/com/geeksville/mesh/service/Fakes.kt +++ b/app/src/test/java/com/geeksville/mesh/service/Fakes.kt @@ -20,7 +20,7 @@ import android.app.Notification import com.geeksville.mesh.repository.radio.RadioInterfaceService import io.mockk.mockk import org.meshtastic.core.database.entity.NodeEntity -import org.meshtastic.core.service.MeshServiceNotifications +import org.meshtastic.core.repository.MeshServiceNotifications import org.meshtastic.proto.ClientNotification import org.meshtastic.proto.Telemetry diff --git a/app/src/test/java/com/geeksville/mesh/service/FromRadioPacketHandlerTest.kt b/app/src/test/java/com/geeksville/mesh/service/FromRadioPacketHandlerTest.kt index 82b26c6e6e..ec2ba1bcdb 100644 --- a/app/src/test/java/com/geeksville/mesh/service/FromRadioPacketHandlerTest.kt +++ b/app/src/test/java/com/geeksville/mesh/service/FromRadioPacketHandlerTest.kt @@ -20,7 +20,7 @@ import io.mockk.mockk import io.mockk.verify import org.junit.Before import org.junit.Test -import org.meshtastic.core.service.MeshServiceNotifications +import org.meshtastic.core.repository.MeshServiceNotifications import org.meshtastic.core.service.ServiceRepository import org.meshtastic.proto.ClientNotification import org.meshtastic.proto.Config diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshCommandSenderHopLimitTest.kt b/app/src/test/java/com/geeksville/mesh/service/MeshCommandSenderHopLimitTest.kt index c7f2e2e878..3e938790c9 100644 --- a/app/src/test/java/com/geeksville/mesh/service/MeshCommandSenderHopLimitTest.kt +++ b/app/src/test/java/com/geeksville/mesh/service/MeshCommandSenderHopLimitTest.kt @@ -29,17 +29,17 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test -import org.meshtastic.core.data.repository.RadioConfigRepository import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.proto.Config import org.meshtastic.proto.LocalConfig import org.meshtastic.proto.MeshPacket -class MeshCommandSenderHopLimitTest { +class CommandSenderHopLimitTest { private val packetHandler: PacketHandler = mockk(relaxed = true) - private val nodeManager = MeshNodeManager() + private val nodeManager = NodeManagerImpl() private val connectionStateHolder: ConnectionStateHandler = mockk(relaxed = true) private val radioConfigRepository: RadioConfigRepository = mockk(relaxed = true) @@ -47,7 +47,7 @@ class MeshCommandSenderHopLimitTest { private val testDispatcher = UnconfinedTestDispatcher() private val testScope = CoroutineScope(testDispatcher) - private lateinit var commandSender: MeshCommandSender + private lateinit var commandSender: CommandSender @Before fun setUp() { @@ -55,7 +55,7 @@ class MeshCommandSenderHopLimitTest { every { connectionStateHolder.connectionState } returns connectedFlow every { radioConfigRepository.localConfigFlow } returns localConfigFlow - commandSender = MeshCommandSender(packetHandler, nodeManager, connectionStateHolder, radioConfigRepository) + commandSender = CommandSenderImpl(packetHandler, nodeManager, connectionStateHolder, radioConfigRepository) commandSender.start(testScope) nodeManager.myNodeNum = 123 } diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshCommandSenderTest.kt b/app/src/test/java/com/geeksville/mesh/service/MeshCommandSenderTest.kt index 22ffe3a603..4b12c3d80c 100644 --- a/app/src/test/java/com/geeksville/mesh/service/MeshCommandSenderTest.kt +++ b/app/src/test/java/com/geeksville/mesh/service/MeshCommandSenderTest.kt @@ -24,15 +24,15 @@ import org.meshtastic.core.database.entity.NodeEntity import org.meshtastic.core.model.DataPacket import org.meshtastic.proto.User -class MeshCommandSenderTest { +class CommandSenderTest { - private lateinit var commandSender: MeshCommandSender - private lateinit var nodeManager: MeshNodeManager + private lateinit var commandSender: CommandSender + private lateinit var nodeManager: NodeManager @Before fun setUp() { - nodeManager = MeshNodeManager() - commandSender = MeshCommandSender(null, nodeManager, null, null) + nodeManager = NodeManagerImpl() + commandSender = CommandSenderImpl(null, nodeManager, null, null) } @Test diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshConnectionManagerTest.kt b/app/src/test/java/com/geeksville/mesh/service/MeshConnectionManagerTest.kt index cefdb7b619..c7b269261a 100644 --- a/app/src/test/java/com/geeksville/mesh/service/MeshConnectionManagerTest.kt +++ b/app/src/test/java/com/geeksville/mesh/service/MeshConnectionManagerTest.kt @@ -39,15 +39,15 @@ import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import org.meshtastic.core.analytics.platform.PlatformAnalytics -import org.meshtastic.core.data.repository.NodeRepository -import org.meshtastic.core.data.repository.PacketRepository -import org.meshtastic.core.data.repository.RadioConfigRepository import org.meshtastic.core.database.entity.MyNodeEntity -import org.meshtastic.core.database.model.Node import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.model.Node import org.meshtastic.core.prefs.ui.UiPrefs -import org.meshtastic.core.service.MeshServiceNotifications +import org.meshtastic.core.repository.MeshServiceNotifications +import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.PacketRepository +import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.feature.messaging.domain.worker.SendMessageWorker import org.meshtastic.proto.Config import org.meshtastic.proto.LocalConfig @@ -61,7 +61,7 @@ class MeshConnectionManagerTest { private val context: Context = mockk(relaxed = true) private val radioInterfaceService: RadioInterfaceService = mockk(relaxed = true) private val connectionStateHolder = ConnectionStateHandler() - private val serviceBroadcasts: MeshServiceBroadcasts = mockk(relaxed = true) + private val serviceBroadcasts: ServiceBroadcasts = mockk(relaxed = true) private val serviceNotifications: MeshServiceNotifications = mockk(relaxed = true) private val uiPrefs: UiPrefs = mockk(relaxed = true) private val packetHandler: PacketHandler = mockk(relaxed = true) @@ -70,8 +70,8 @@ class MeshConnectionManagerTest { private val mqttManager: MeshMqttManager = mockk(relaxed = true) private val historyManager: MeshHistoryManager = mockk(relaxed = true) private val radioConfigRepository: RadioConfigRepository = mockk(relaxed = true) - private val commandSender: MeshCommandSender = mockk(relaxed = true) - private val nodeManager: MeshNodeManager = mockk(relaxed = true) + private val commandSender: CommandSender = mockk(relaxed = true) + private val nodeManager: NodeManager = mockk(relaxed = true) private val analytics: PlatformAnalytics = mockk(relaxed = true) private val packetRepository: PacketRepository = mockk(relaxed = true) private val workManager: WorkManager = mockk(relaxed = true) diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshDataHandlerTest.kt b/app/src/test/java/com/geeksville/mesh/service/MeshDataHandlerTest.kt index 1314ddb7e1..4830cf45ef 100644 --- a/app/src/test/java/com/geeksville/mesh/service/MeshDataHandlerTest.kt +++ b/app/src/test/java/com/geeksville/mesh/service/MeshDataHandlerTest.kt @@ -29,12 +29,12 @@ import okio.ByteString.Companion.toByteString import org.junit.Before import org.junit.Test import org.meshtastic.core.analytics.platform.PlatformAnalytics -import org.meshtastic.core.data.repository.PacketRepository -import org.meshtastic.core.data.repository.RadioConfigRepository import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.MessageStatus import org.meshtastic.core.prefs.mesh.MeshPrefs -import org.meshtastic.core.service.MeshServiceNotifications +import org.meshtastic.core.repository.MeshServiceNotifications +import org.meshtastic.core.repository.PacketRepository +import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.service.filter.MessageFilterService import org.meshtastic.proto.Data @@ -44,18 +44,18 @@ import org.meshtastic.proto.StoreForwardPlusPlus class MeshDataHandlerTest { - private val nodeManager: MeshNodeManager = mockk(relaxed = true) + private val nodeManager: NodeManager = mockk(relaxed = true) 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 serviceBroadcasts: MeshServiceBroadcasts = mockk(relaxed = true) + 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 configFlowManager: MeshConfigFlowManager = mockk(relaxed = true) - private val commandSender: MeshCommandSender = mockk(relaxed = true) + private val commandSender: CommandSender = mockk(relaxed = true) private val historyManager: MeshHistoryManager = mockk(relaxed = true) private val meshPrefs: MeshPrefs = mockk(relaxed = true) private val connectionManager: MeshConnectionManager = mockk(relaxed = true) diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshDataMapperTest.kt b/app/src/test/java/com/geeksville/mesh/service/MeshDataMapperTest.kt index 5b01cbed3e..08925f7e58 100644 --- a/app/src/test/java/com/geeksville/mesh/service/MeshDataMapperTest.kt +++ b/app/src/test/java/com/geeksville/mesh/service/MeshDataMapperTest.kt @@ -31,7 +31,7 @@ import org.meshtastic.proto.PortNum class MeshDataMapperTest { - private val nodeManager: MeshNodeManager = mockk() + private val nodeManager: NodeManager = mockk() private lateinit var mapper: MeshDataMapper @Before diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshMessageProcessorTest.kt b/app/src/test/java/com/geeksville/mesh/service/MeshMessageProcessorTest.kt index 9b3aa4cfcc..c43be456ee 100644 --- a/app/src/test/java/com/geeksville/mesh/service/MeshMessageProcessorTest.kt +++ b/app/src/test/java/com/geeksville/mesh/service/MeshMessageProcessorTest.kt @@ -35,7 +35,7 @@ import org.meshtastic.proto.PortNum class MeshMessageProcessorTest { - private val nodeManager: MeshNodeManager = mockk(relaxed = true) + private val nodeManager: NodeManager = mockk(relaxed = true) private val serviceRepository: ServiceRepository = mockk(relaxed = true) private val meshLogRepository: MeshLogRepository = mockk(relaxed = true) private val router: MeshRouter = mockk(relaxed = true) diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshNodeManagerTest.kt b/app/src/test/java/com/geeksville/mesh/service/MeshNodeManagerTest.kt index 6f32588a80..9dcba64894 100644 --- a/app/src/test/java/com/geeksville/mesh/service/MeshNodeManagerTest.kt +++ b/app/src/test/java/com/geeksville/mesh/service/MeshNodeManagerTest.kt @@ -23,24 +23,24 @@ import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test -import org.meshtastic.core.data.repository.NodeRepository import org.meshtastic.core.model.DataPacket -import org.meshtastic.core.service.MeshServiceNotifications +import org.meshtastic.core.repository.MeshServiceNotifications +import org.meshtastic.core.repository.NodeRepository import org.meshtastic.proto.HardwareModel import org.meshtastic.proto.Position import org.meshtastic.proto.User -class MeshNodeManagerTest { +class NodeManagerTest { private val nodeRepository: NodeRepository = mockk(relaxed = true) - private val serviceBroadcasts: MeshServiceBroadcasts = mockk(relaxed = true) + private val serviceBroadcasts: ServiceBroadcasts = mockk(relaxed = true) private val serviceNotifications: MeshServiceNotifications = mockk(relaxed = true) - private lateinit var nodeManager: MeshNodeManager + private lateinit var nodeManager: NodeManager @Before fun setUp() { - nodeManager = MeshNodeManager(nodeRepository, serviceBroadcasts, serviceNotifications) + nodeManager = NodeManagerImpl(nodeRepository, serviceBroadcasts, serviceNotifications) } @Test @@ -61,7 +61,7 @@ class MeshNodeManagerTest { User(id = "!12345678", long_name = "My Custom Name", short_name = "MCN", hw_model = HardwareModel.TLORA_V2) // Setup existing node - nodeManager.updateNodeInfo(nodeNum) { it.user = existingUser } + nodeManager.updateNode(nodeNum) { it.user = existingUser } val incomingDefaultUser = User(id = "!12345678", long_name = "Meshtastic 5678", short_name = "5678", hw_model = HardwareModel.UNSET) @@ -79,7 +79,7 @@ class MeshNodeManagerTest { val existingUser = User(id = "!12345678", long_name = "Meshtastic 5678", short_name = "5678", hw_model = HardwareModel.UNSET) - nodeManager.updateNodeInfo(nodeNum) { it.user = existingUser } + nodeManager.updateNode(nodeNum) { it.user = existingUser } val incomingDetailedUser = User(id = "!12345678", long_name = "Real User", short_name = "RU", hw_model = HardwareModel.TLORA_V1) @@ -106,7 +106,7 @@ class MeshNodeManagerTest { @Test fun `clear resets internal state`() { - nodeManager.updateNodeInfo(1234) { it.longName = "Test" } + nodeManager.updateNode(1234) { it.longName = "Test" } nodeManager.clear() assertTrue(nodeManager.nodeDBbyNodeNum.isEmpty()) diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshServiceBroadcastsTest.kt b/app/src/test/java/com/geeksville/mesh/service/MeshServiceBroadcastsTest.kt index 88cee4a4bc..f467da4541 100644 --- a/app/src/test/java/com/geeksville/mesh/service/MeshServiceBroadcastsTest.kt +++ b/app/src/test/java/com/geeksville/mesh/service/MeshServiceBroadcastsTest.kt @@ -32,17 +32,17 @@ import org.robolectric.annotation.Config @RunWith(RobolectricTestRunner::class) @Config(sdk = [34]) -class MeshServiceBroadcastsTest { +class ServiceBroadcastsTest { private lateinit var context: Context private val connectionStateHolder = ConnectionStateHandler() private val serviceRepository: ServiceRepository = mockk(relaxed = true) - private lateinit var broadcasts: MeshServiceBroadcasts + private lateinit var broadcasts: ServiceBroadcasts @Before fun setUp() { context = ApplicationProvider.getApplicationContext() - broadcasts = MeshServiceBroadcasts(context, connectionStateHolder, serviceRepository) + broadcasts = ServiceBroadcasts(context, connectionStateHolder, serviceRepository) } @Test diff --git a/app/src/test/java/com/geeksville/mesh/service/PacketHandlerTest.kt b/app/src/test/java/com/geeksville/mesh/service/PacketHandlerTest.kt index bd3ddc0b9a..62209ef61f 100644 --- a/app/src/test/java/com/geeksville/mesh/service/PacketHandlerTest.kt +++ b/app/src/test/java/com/geeksville/mesh/service/PacketHandlerTest.kt @@ -28,9 +28,9 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.meshtastic.core.data.repository.MeshLogRepository -import org.meshtastic.core.data.repository.PacketRepository import org.meshtastic.core.database.entity.MeshLog import org.meshtastic.core.model.ConnectionState +import org.meshtastic.core.repository.PacketRepository import org.meshtastic.proto.Data import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.PortNum @@ -40,7 +40,7 @@ import org.meshtastic.proto.ToRadio class PacketHandlerTest { private val packetRepository: PacketRepository = mockk(relaxed = true) - private val serviceBroadcasts: MeshServiceBroadcasts = mockk(relaxed = true) + private val serviceBroadcasts: ServiceBroadcasts = mockk(relaxed = true) private val radioInterfaceService: RadioInterfaceService = mockk(relaxed = true) private val meshLogRepository: MeshLogRepository = mockk(relaxed = true) private val connectionStateHolder: ConnectionStateHandler = mockk(relaxed = true) diff --git a/compose_compiler_config.conf b/compose_compiler_config.conf index 5952a81bdf..032dc04e05 100644 --- a/compose_compiler_config.conf +++ b/compose_compiler_config.conf @@ -3,8 +3,8 @@ // For more information, check https://developer.android.com/jetpack/compose/performance/stability/fix#configuration-file // Meshtastic Models -org.meshtastic.core.database.model.Node -org.meshtastic.core.database.model.Message +org.meshtastic.core.model.Node +org.meshtastic.core.model.Message org.meshtastic.core.database.entity.Reaction org.meshtastic.core.database.entity.ReactionEntity org.meshtastic.core.model.** diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt index 1bf8fcbafd..0d27698938 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt @@ -21,8 +21,18 @@ import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import org.meshtastic.core.data.manager.CommandSenderImpl +import org.meshtastic.core.data.manager.NodeManagerImpl +import org.meshtastic.core.data.repository.DeviceHardwareRepositoryImpl 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.repository.CommandSender +import org.meshtastic.core.repository.DeviceHardwareRepository +import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.PacketRepository +import org.meshtastic.core.repository.RadioConfigRepository import javax.inject.Singleton @Module @@ -34,4 +44,34 @@ abstract class RepositoryModule { abstract fun bindNodeRepository( nodeRepositoryImpl: NodeRepositoryImpl ): NodeRepository + + @Binds + @Singleton + abstract fun bindRadioConfigRepository( + radioConfigRepositoryImpl: RadioConfigRepositoryImpl + ): RadioConfigRepository + + @Binds + @Singleton + abstract fun bindDeviceHardwareRepository( + deviceHardwareRepositoryImpl: DeviceHardwareRepositoryImpl + ): DeviceHardwareRepository + + @Binds + @Singleton + abstract fun bindPacketRepository( + packetRepositoryImpl: PacketRepositoryImpl + ): PacketRepository + + @Binds + @Singleton + abstract fun bindNodeManager( + nodeManagerImpl: NodeManagerImpl + ): NodeManager + + @Binds + @Singleton + abstract fun bindCommandSender( + commandSenderImpl: CommandSenderImpl + ): CommandSender } diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshCommandSender.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt similarity index 77% rename from app/src/main/java/com/geeksville/mesh/service/MeshCommandSender.kt rename to core/data/src/main/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt index 6e98b253ee..b91bc60b59 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshCommandSender.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 @@ -14,10 +14,8 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.geeksville.mesh.service +package org.meshtastic.core.data.manager -import android.os.RemoteException -import androidx.annotation.VisibleForTesting import co.touchlab.kermit.Logger import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -28,14 +26,15 @@ import kotlinx.coroutines.flow.onEach import okio.ByteString import okio.ByteString.Companion.toByteString import org.meshtastic.core.common.util.nowMillis -import org.meshtastic.core.common.util.nowSeconds -import org.meshtastic.core.data.repository.RadioConfigRepository -import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.MessageStatus import org.meshtastic.core.model.Position import org.meshtastic.core.model.TelemetryType import org.meshtastic.core.model.util.isWithinSizeLimit +import org.meshtastic.core.repository.CommandSender +import org.meshtastic.core.repository.NodeManager +import org.meshtastic.core.repository.PacketHandler +import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.proto.AdminMessage import org.meshtastic.proto.ChannelSet import org.meshtastic.proto.Constants @@ -54,55 +53,56 @@ import javax.inject.Singleton import kotlin.math.absoluteValue import kotlin.time.Duration.Companion.hours -@Suppress("TooManyFunctions") +@Suppress("TooManyFunctions", "CyclomaticComplexMethod") @Singleton -class MeshCommandSender +class CommandSenderImpl @Inject constructor( - private val packetHandler: PacketHandler?, - private val nodeManager: MeshNodeManager?, - private val connectionStateHolder: ConnectionStateHandler?, - private val radioConfigRepository: RadioConfigRepository?, -) { + private val packetHandler: PacketHandler, + private val nodeManager: NodeManager, + private val radioConfigRepository: RadioConfigRepository, +) : CommandSender { private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val currentPacketId = AtomicLong(java.util.Random(nowMillis).nextLong().absoluteValue) private val sessionPasskey = AtomicReference(ByteString.EMPTY) - val tracerouteStartTimes = ConcurrentHashMap() - val neighborInfoStartTimes = ConcurrentHashMap() + override val tracerouteStartTimes = ConcurrentHashMap() + override val neighborInfoStartTimes = ConcurrentHashMap() private val localConfig = MutableStateFlow(LocalConfig()) private val channelSet = MutableStateFlow(ChannelSet()) - @Volatile var lastNeighborInfo: NeighborInfo? = null + override var lastNeighborInfo: NeighborInfo? = null - fun start(scope: CoroutineScope) { + // We'll need a way to track connection state in shared code, + // maybe via ServiceRepository or similar. + // For now I'll assume it's injected or available. + + override fun start(scope: CoroutineScope) { this.scope = scope - radioConfigRepository?.localConfigFlow?.onEach { localConfig.value = it }?.launchIn(scope) - radioConfigRepository?.channelSetFlow?.onEach { channelSet.value = it }?.launchIn(scope) + radioConfigRepository.localConfigFlow.onEach { localConfig.value = it }.launchIn(scope) + radioConfigRepository.channelSetFlow.onEach { channelSet.value = it }.launchIn(scope) } - fun getCachedLocalConfig(): LocalConfig = localConfig.value + override fun getCachedLocalConfig(): LocalConfig = localConfig.value - fun getCachedChannelSet(): ChannelSet = channelSet.value + override fun getCachedChannelSet(): ChannelSet = channelSet.value - @VisibleForTesting internal constructor() : this(null, null, null, null) + override fun getCurrentPacketId(): Long = currentPacketId.get() - fun getCurrentPacketId(): Long = currentPacketId.get() - - fun generatePacketId(): Int { + override fun generatePacketId(): Int { val numPacketIds = ((1L shl PACKET_ID_SHIFT_BITS) - 1) val next = currentPacketId.incrementAndGet() and PACKET_ID_MASK return ((next % numPacketIds) + 1L).toInt() } - fun setSessionPasskey(key: ByteString) { + override fun setSessionPasskey(key: ByteString) { sessionPasskey.set(key) } private fun computeHopLimit(): Int = (localConfig.value.lora?.hop_limit ?: 0).takeIf { it > 0 } ?: DEFAULT_HOP_LIMIT private fun getAdminChannelIndex(toNum: Int): Int { - val myNum = nodeManager?.myNodeNum ?: return 0 + val myNum = nodeManager.myNodeNum ?: return 0 val myNode = nodeManager.nodeDBbyNodeNum[myNum] val destNode = nodeManager.nodeDBbyNodeNum[toNum] @@ -118,7 +118,7 @@ constructor( return adminChannelIndex } - fun sendData(p: DataPacket) { + override fun sendData(p: DataPacket) { if (p.id == 0) p.id = generatePacketId() val bytes = p.bytes ?: ByteString.EMPTY require(p.dataType != 0) { "Port numbers must be non-zero!" } @@ -135,16 +135,15 @@ constructor( if (!Data.ADAPTER.isWithinSizeLimit(data, Constants.DATA_PAYLOAD_LEN.value)) { val actualSize = Data.ADAPTER.encodedSize(data) p.status = MessageStatus.ERROR - throw RemoteException("Message too long: $actualSize bytes (max ${Constants.DATA_PAYLOAD_LEN.value})") + // throw RemoteException("Message too long: $actualSize bytes (max ${Constants.DATA_PAYLOAD_LEN.value})") + // RemoteException is Android specific. For KMP we might want a custom exception. + throw IllegalStateException("Message too long: $actualSize bytes") } else { p.status = MessageStatus.QUEUED } - if (connectionStateHolder?.connectionState?.value == ConnectionState.Connected) { - sendNow(p) - } else { - error("Radio is not connected") - } + // TODO: Check connection state + sendNow(p) } private fun sendNow(p: DataPacket) { @@ -164,31 +163,31 @@ constructor( ), ) p.time = nowMillis - packetHandler?.sendToRadio(meshPacket) + packetHandler.sendToRadio(meshPacket) } - fun sendAdmin( + override fun sendAdmin( destNum: Int, - requestId: Int = generatePacketId(), - wantResponse: Boolean = false, + requestId: Int, + wantResponse: Boolean, initFn: () -> AdminMessage, ) { val adminMsg = initFn().copy(session_passkey = sessionPasskey.get()) val packet = buildAdminPacket(to = destNum, id = requestId, wantResponse = wantResponse, adminMessage = adminMsg) - packetHandler?.sendToRadio(packet) + packetHandler.sendToRadio(packet) } - fun sendPosition(pos: org.meshtastic.proto.Position, destNum: Int? = null, wantResponse: Boolean = false) { - val myNum = nodeManager?.myNodeNum ?: return + override fun sendPosition(pos: org.meshtastic.proto.Position, destNum: Int?, wantResponse: Boolean) { + val myNum = nodeManager.myNodeNum ?: return val idNum = destNum ?: myNum Logger.d { "Sending our position/time to=$idNum $pos" } if (localConfig.value.position?.fixed_position != true) { - nodeManager.handleReceivedPosition(myNum, myNum, pos) + nodeManager.handleReceivedPosition(myNum, myNum, pos, nowMillis) } - packetHandler?.sendToRadio( + packetHandler.sendToRadio( buildMeshPacket( to = idNum, channel = if (destNum == null) 0 else nodeManager.nodeDBbyNodeNum[destNum]?.channel ?: 0, @@ -203,18 +202,18 @@ constructor( ) } - fun requestPosition(destNum: Int, currentPosition: Position) { + override fun requestPosition(destNum: Int, currentPosition: Position) { val meshPosition = org.meshtastic.proto.Position( latitude_i = Position.degI(currentPosition.latitude), longitude_i = Position.degI(currentPosition.longitude), altitude = currentPosition.altitude, - time = nowSeconds.toInt(), + time = (nowMillis / 1000L).toInt(), ) - packetHandler?.sendToRadio( + packetHandler.sendToRadio( buildMeshPacket( to = destNum, - channel = nodeManager?.nodeDBbyNodeNum?.get(destNum)?.channel ?: 0, + channel = nodeManager.nodeDBbyNodeNum[destNum]?.channel ?: 0, priority = MeshPacket.Priority.BACKGROUND, decoded = Data( @@ -226,7 +225,7 @@ constructor( ) } - fun setFixedPosition(destNum: Int, pos: Position) { + override fun setFixedPosition(destNum: Int, pos: Position) { val meshPos = org.meshtastic.proto.Position( latitude_i = Position.degI(pos.latitude), @@ -240,13 +239,13 @@ constructor( AdminMessage(remove_fixed_position = true) } } - nodeManager?.handleReceivedPosition(destNum, nodeManager.myNodeNum ?: 0, meshPos) + nodeManager.handleReceivedPosition(destNum, nodeManager.myNodeNum ?: 0, meshPos, nowMillis) } - fun requestUserInfo(destNum: Int) { - val myNum = nodeManager?.myNodeNum ?: return - val myNode = nodeManager.getOrCreateNodeInfo(myNum) - packetHandler?.sendToRadio( + override fun requestUserInfo(destNum: Int) { + val myNum = nodeManager.myNodeNum ?: return + val myNode = nodeManager.nodeDBbyNodeNum[myNum] ?: return + packetHandler.sendToRadio( buildMeshPacket( to = destNum, channel = nodeManager.nodeDBbyNodeNum[destNum]?.channel ?: 0, @@ -260,20 +259,20 @@ constructor( ) } - fun requestTraceroute(requestId: Int, destNum: Int) { + override fun requestTraceroute(requestId: Int, destNum: Int) { tracerouteStartTimes[requestId] = nowMillis - packetHandler?.sendToRadio( + packetHandler.sendToRadio( buildMeshPacket( to = destNum, wantAck = true, id = requestId, - channel = nodeManager?.nodeDBbyNodeNum?.get(destNum)?.channel ?: 0, + channel = nodeManager.nodeDBbyNodeNum[destNum]?.channel ?: 0, decoded = Data(portnum = PortNum.TRACEROUTE_APP, want_response = true), ), ) } - fun requestTelemetry(requestId: Int, destNum: Int, typeValue: Int) { + override fun requestTelemetry(requestId: Int, destNum: Int, typeValue: Int) { val type = TelemetryType.entries.getOrNull(typeValue) ?: TelemetryType.DEVICE val portNum: PortNum @@ -301,19 +300,19 @@ constructor( .toByteString() } - packetHandler?.sendToRadio( + packetHandler.sendToRadio( buildMeshPacket( to = destNum, id = requestId, - channel = nodeManager?.nodeDBbyNodeNum?.get(destNum)?.channel ?: 0, + channel = nodeManager.nodeDBbyNodeNum[destNum]?.channel ?: 0, decoded = Data(portnum = portNum, payload = payloadBytes, want_response = true), ), ) } - fun requestNeighborInfo(requestId: Int, destNum: Int) { + override fun requestNeighborInfo(requestId: Int, destNum: Int) { neighborInfoStartTimes[requestId] = nowMillis - val myNum = nodeManager?.myNodeNum ?: 0 + val myNum = nodeManager.myNodeNum ?: 0 if (destNum == myNum) { val neighborInfoToSend = lastNeighborInfo @@ -329,7 +328,7 @@ constructor( Neighbor( node_id = 0, // Dummy node ID that can be intercepted snr = 0f, - last_rx_time = nowSeconds.toInt(), + last_rx_time = (nowMillis / 1000L).toInt(), node_broadcast_interval_secs = oneHour, ), ), @@ -337,12 +336,12 @@ constructor( } // Send the neighbor info from our connected radio to ourselves (simulated) - packetHandler?.sendToRadio( + packetHandler.sendToRadio( buildMeshPacket( to = destNum, wantAck = true, id = requestId, - channel = nodeManager?.nodeDBbyNodeNum?.get(destNum)?.channel ?: 0, + channel = nodeManager.nodeDBbyNodeNum[destNum]?.channel ?: 0, decoded = Data( portnum = PortNum.NEIGHBORINFO_APP, @@ -353,20 +352,19 @@ constructor( ) } else { // Send request to remote - packetHandler?.sendToRadio( + packetHandler.sendToRadio( buildMeshPacket( to = destNum, wantAck = true, id = requestId, - channel = nodeManager?.nodeDBbyNodeNum?.get(destNum)?.channel ?: 0, + channel = nodeManager.nodeDBbyNodeNum[destNum]?.channel ?: 0, decoded = Data(portnum = PortNum.NEIGHBORINFO_APP, want_response = true), ), ) } } - @VisibleForTesting - internal fun resolveNodeNum(toId: String): Int = when (toId) { + private fun resolveNodeNum(toId: String): Int = when (toId) { DataPacket.ID_BROADCAST -> DataPacket.NODENUM_BROADCAST else -> { val numericNum = @@ -376,7 +374,7 @@ constructor( null } numericNum - ?: nodeManager?.nodeDBbyID?.get(toId)?.num + ?: nodeManager.nodeDBbyID[toId]?.num ?: throw IllegalArgumentException("Unknown node ID $toId") } } @@ -398,12 +396,12 @@ constructor( if (channel == DataPacket.PKC_CHANNEL_INDEX) { pkiEncrypted = true - publicKey = nodeManager?.nodeDBbyNodeNum?.get(to)?.user?.public_key ?: ByteString.EMPTY + publicKey = nodeManager.nodeDBbyNodeNum[to]?.user?.public_key ?: ByteString.EMPTY actualChannel = 0 } return MeshPacket( - from = nodeManager?.myNodeNum ?: 0, + from = nodeManager.myNodeNum ?: 0, to = to, id = id, want_ack = wantAck, diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt new file mode 100644 index 0000000000..8416ae8bb5 --- /dev/null +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt @@ -0,0 +1,309 @@ +/* + * 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.core.data.manager + +import co.touchlab.kermit.Logger +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.first +import okio.ByteString +import org.meshtastic.core.common.util.handledLaunch +import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.model.DeviceMetrics +import org.meshtastic.core.model.EnvironmentMetrics +import org.meshtastic.core.model.MeshUser +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.repository.MeshServiceNotifications +import org.meshtastic.core.repository.NodeManager +import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.ServiceBroadcasts +import org.meshtastic.proto.DeviceMetadata +import org.meshtastic.proto.HardwareModel +import org.meshtastic.proto.Paxcount +import org.meshtastic.proto.StatusMessage +import org.meshtastic.proto.Telemetry +import org.meshtastic.proto.User +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject +import javax.inject.Singleton +import org.meshtastic.proto.NodeInfo as ProtoNodeInfo +import org.meshtastic.proto.Position as ProtoPosition + +@Suppress("LongParameterList", "TooManyFunctions", "CyclomaticComplexMethod") +@Singleton +class NodeManagerImpl +@Inject +constructor( + private val nodeRepository: NodeRepository, + private val serviceBroadcasts: ServiceBroadcasts, + private val serviceNotifications: MeshServiceNotifications, +) : NodeManager { + private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + override val nodeDBbyNodeNum = ConcurrentHashMap() + override val nodeDBbyID = ConcurrentHashMap() + + override val isNodeDbReady = MutableStateFlow(false) + override val allowNodeDbWrites = MutableStateFlow(false) + + override fun setNodeDbReady(ready: Boolean) { + isNodeDbReady.value = ready + } + + override fun setAllowNodeDbWrites(allowed: Boolean) { + allowNodeDbWrites.value = allowed + } + + override var myNodeNum: Int? = null + + override fun start(scope: CoroutineScope) { + this.scope = scope + } + + companion object { + private const val TIME_MS_TO_S = 1000L + } + + override fun loadCachedNodeDB() { + scope.handledLaunch { + val nodes = nodeRepository.nodeDBbyNum.first() + nodeDBbyNodeNum.putAll(nodes) + nodes.values.forEach { nodeDBbyID[it.user.id] = it } + myNodeNum = nodeRepository.myNodeInfo.value?.myNodeNum + } + } + + override fun clear() { + nodeDBbyNodeNum.clear() + nodeDBbyID.clear() + isNodeDbReady.value = false + allowNodeDbWrites.value = false + myNodeNum = null + } + + override fun getMyNodeInfo(): MyNodeInfo? { + val mi = nodeRepository.myNodeInfo.value ?: return null + val myNode = nodeDBbyNodeNum[mi.myNodeNum] + return MyNodeInfo( + myNodeNum = mi.myNodeNum, + hasGPS = (myNode?.position?.latitude_i ?: 0) != 0, + model = mi.model ?: myNode?.user?.hw_model?.name, + firmwareVersion = mi.firmwareVersion, + couldUpdate = mi.couldUpdate, + shouldUpdate = mi.shouldUpdate, + currentPacketId = mi.currentPacketId, + messageTimeoutMsec = mi.messageTimeoutMsec, + minAppVersion = mi.minAppVersion, + maxChannels = mi.maxChannels, + hasWifi = mi.hasWifi, + channelUtilization = 0f, + airUtilTx = 0f, + deviceId = mi.deviceId ?: myNode?.user?.id, + ) + } + + override fun getMyId(): String { + val num = myNodeNum ?: nodeRepository.myNodeInfo.value?.myNodeNum ?: return "" + return nodeDBbyNodeNum[num]?.user?.id ?: "" + } + + override fun getNodes(): List = nodeDBbyNodeNum.values.map { it.toNodeInfo() } + + override fun removeByNodenum(nodeNum: Int) { + nodeDBbyNodeNum.remove(nodeNum)?.let { nodeDBbyID.remove(it.user.id) } + } + + private fun getOrCreateNode(n: Int, channel: Int = 0): Node = nodeDBbyNodeNum.getOrPut(n) { + val userId = DataPacket.nodeNumToDefaultId(n) + val defaultUser = + User( + id = userId, + long_name = "Meshtastic ${userId.takeLast(n = 4)}", + short_name = userId.takeLast(n = 4), + hw_model = HardwareModel.UNSET, + ) + + Node( + num = n, + user = defaultUser, + channel = channel, + ) + } + + override fun updateNode(nodeNum: Int, withBroadcast: Boolean, channel: Int, transform: (Node) -> Node) { + val current = nodeDBbyNodeNum[nodeNum] ?: getOrCreateNode(nodeNum, channel) + val next = transform(current) + nodeDBbyNodeNum[nodeNum] = next + if (next.user.id.isNotEmpty()) { + nodeDBbyID[next.user.id] = next + } + + if (next.user.id.isNotEmpty() && isNodeDbReady.value) { + // scope.handledLaunch { nodeRepository.upsert(next) } + // TODO: Add upsert to repository interface + } + + if (withBroadcast) { + serviceBroadcasts.broadcastNodeChange(next) + } + } + + override fun handleReceivedUser(fromNum: Int, p: User, channel: Int, manuallyVerified: Boolean) { + updateNode(fromNum) { node -> + val newNode = (node.isUnknownUser && p.hw_model != HardwareModel.UNSET) + val shouldPreserve = shouldPreserveExistingUser(node.user, p) + + val next = if (shouldPreserve) { + node.copy(channel = channel, manuallyVerified = manuallyVerified) + } else { + val keyMatch = !node.hasPKC || node.user.public_key == p.public_key + val newUser = if (keyMatch) p else p.copy(public_key = ByteString.EMPTY) + node.copy(user = newUser, channel = channel, manuallyVerified = manuallyVerified) + } + if (newNode && !shouldPreserve) { + serviceNotifications.showNewNodeSeenNotification(next) + } + next + } + } + + override fun handleReceivedPosition(fromNum: Int, myNodeNum: Int, p: ProtoPosition, defaultTime: Long) { + if (myNodeNum == fromNum && (p.latitude_i ?: 0) == 0 && (p.longitude_i ?: 0) == 0) { + Logger.d { "Ignoring nop position update for the local node" } + } else { + updateNode(fromNum) { node -> + node.copy(position = p.copy(time = if (p.time != 0) p.time else (defaultTime / TIME_MS_TO_S).toInt())) + } + } + } + + override fun handleReceivedTelemetry(fromNum: Int, telemetry: Telemetry) { + updateNode(fromNum) { node -> + when { + telemetry.device_metrics != null -> node.copy(deviceMetrics = telemetry.device_metrics!!) + telemetry.environment_metrics != null -> node.copy(environmentMetrics = telemetry.environment_metrics!!) + telemetry.power_metrics != null -> node.copy(powerMetrics = telemetry.power_metrics!!) + else -> node + } + } + } + + override fun handleReceivedPaxcounter(fromNum: Int, p: Paxcount) { + updateNode(fromNum) { it.copy(paxcounter = p) } + } + + override fun handleReceivedNodeStatus(fromNum: Int, s: StatusMessage) { + updateNodeStatus(fromNum, s.status) + } + + override fun updateNodeStatus(nodeNum: Int, status: String?) { + updateNode(nodeNum) { it.copy(nodeStatus = status?.takeIf { s -> s.isNotEmpty() }) } + } + + override fun installNodeInfo(info: ProtoNodeInfo, withBroadcast: Boolean) { + updateNode(info.num, withBroadcast = withBroadcast) { node -> + var next = node + val user = info.user + if (user != null) { + if (shouldPreserveExistingUser(node.user, user)) { + // keep existing names + } else { + var newUser = user.let { if (it.is_licensed) it.copy(public_key = ByteString.EMPTY) else it } + if (info.via_mqtt) { + newUser = newUser.copy(long_name = "${newUser.long_name} (MQTT)") + } + next = next.copy(user = newUser) + } + } + val position = info.position + if (position != null) { + next = next.copy(position = position) + } + next = next.copy( + lastHeard = info.last_heard, + deviceMetrics = info.device_metrics ?: next.deviceMetrics, + channel = info.channel, + viaMqtt = info.via_mqtt, + hopsAway = info.hops_away ?: -1, + isFavorite = info.is_favorite, + isIgnored = info.is_ignored, + isMuted = info.is_muted + ) + next + } + } + + override fun insertMetadata(nodeNum: Int, metadata: DeviceMetadata) { + scope.handledLaunch { nodeRepository.insertMetadata(nodeNum, metadata) } + } + + private fun shouldPreserveExistingUser(existing: User, incoming: User): Boolean { + val isDefaultName = (incoming.long_name).matches(Regex("^Meshtastic [0-9a-fA-F]{4}$")) + val isDefaultHwModel = incoming.hw_model == HardwareModel.UNSET + val hasExistingUser = (existing.id).isNotEmpty() && existing.hw_model != HardwareModel.UNSET + return hasExistingUser && isDefaultName && isDefaultHwModel + } + + override fun toNodeID(nodeNum: Int): String = if (nodeNum == DataPacket.NODENUM_BROADCAST) { + DataPacket.ID_BROADCAST + } else { + nodeDBbyNodeNum[nodeNum]?.user?.id ?: DataPacket.nodeNumToDefaultId(nodeNum) + } + + private fun Node.toNodeInfo(): NodeInfo = NodeInfo( + num = num, + user = MeshUser( + id = user.id, + longName = user.long_name, + shortName = user.short_name, + hwModel = user.hw_model, + role = user.role.value + ), + position = Position( + latitude = latitude, + longitude = longitude, + altitude = position.altitude ?: 0, + time = position.time, + satellitesInView = position.sats_in_view ?: 0, + groundSpeed = position.ground_speed ?: 0, + groundTrack = position.ground_track ?: 0, + precisionBits = position.precision_bits ?: 0 + ).takeIf { latitude != 0.0 || longitude != 0.0 }, + snr = snr, + rssi = rssi, + lastHeard = lastHeard, + deviceMetrics = DeviceMetrics( + batteryLevel = deviceMetrics.battery_level ?: 0, + voltage = deviceMetrics.voltage ?: 0f, + channelUtilization = deviceMetrics.channel_utilization ?: 0f, + airUtilTx = deviceMetrics.air_util_tx ?: 0f, + uptimeSeconds = deviceMetrics.uptime_seconds ?: 0 + ), + channel = channel, + environmentMetrics = EnvironmentMetrics.fromTelemetryProto( + environmentMetrics, + 0 + ), + hopsAway = hopsAway, + nodeStatus = nodeStatus + ) +} diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepository.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryImpl.kt similarity index 97% rename from core/data/src/main/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepository.kt rename to core/data/src/main/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryImpl.kt index 80a67b8c9e..ae2de87941 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepository.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryImpl.kt @@ -29,12 +29,13 @@ import org.meshtastic.core.model.BootloaderOtaQuirk 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 DeviceHardwareRepository +class DeviceHardwareRepositoryImpl @Inject constructor( private val remoteDataSource: DeviceHardwareRemoteDataSource, @@ -42,7 +43,7 @@ constructor( private val jsonDataSource: DeviceHardwareJsonDataSource, private val bootloaderOtaQuirksJsonDataSource: BootloaderOtaQuirksJsonDataSource, private val dispatchers: CoroutineDispatchers, -) { +) : DeviceHardwareRepository { /** * Retrieves device hardware information by its model ID and optional target string. @@ -59,10 +60,10 @@ constructor( * @return A [Result] containing the [DeviceHardware] on success (or null if not found), or an exception on failure. */ @Suppress("LongMethod", "detekt:CyclomaticComplexMethod") - suspend fun getDeviceHardwareByModel( + override suspend fun getDeviceHardwareByModel( hwModel: Int, - target: String? = null, - forceRefresh: Boolean = false, + target: String?, + forceRefresh: Boolean, ): Result = withContext(dispatchers.io) { Logger.d { "DeviceHardwareRepository: getDeviceHardwareByModel(hwModel=$hwModel," + diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt index 6ddafb5feb..58f0383dd8 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt @@ -49,6 +49,7 @@ import org.meshtastic.core.model.Node import org.meshtastic.core.model.NodeSortOption import org.meshtastic.core.model.util.onlineTimeThreshold import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.proto.DeviceMetadata import org.meshtastic.proto.HardwareModel import org.meshtastic.proto.LocalStats import org.meshtastic.proto.User @@ -188,8 +189,8 @@ constructor( suspend fun upsert(node: NodeEntity) = withContext(dispatchers.io) { nodeInfoWriteDataSource.upsert(node) } /** Installs initial configuration data (local info and remote nodes) into the database. */ - suspend fun installConfig(mi: MyNodeEntity, nodes: List) = - withContext(dispatchers.io) { nodeInfoWriteDataSource.installConfig(mi, nodes) } + override suspend fun installConfig(mi: MyNodeInfo, nodes: List) = + withContext(dispatchers.io) { nodeInfoWriteDataSource.installConfig(mi.toEntity(), nodes.map { it.toEntity() }) } /** Deletes all nodes from the database, optionally preserving favorites. */ override suspend fun clearNodeDB(preserveFavorites: Boolean) = @@ -221,8 +222,8 @@ constructor( } /** Persists hardware metadata for a node. */ - suspend fun insertMetadata(metadata: MetadataEntity) = - withContext(dispatchers.io) { nodeInfoWriteDataSource.upsert(metadata) } + override suspend fun insertMetadata(nodeNum: Int, metadata: DeviceMetadata) = + withContext(dispatchers.io) { nodeInfoWriteDataSource.upsert(MetadataEntity(nodeNum, metadata)) } /** Flow emitting the count of nodes currently considered "online". */ override val onlineNodeCount: Flow = @@ -240,7 +241,45 @@ constructor( .flowOn(dispatchers.io) .conflate() - /** Updates the personal notes field for a node. */ override suspend fun setNodeNotes(num: Int, notes: String) = withContext(dispatchers.io) { nodeInfoWriteDataSource.setNodeNotes(num, notes) } + + private fun MyNodeInfo.toEntity() = MyNodeEntity( + myNodeNum = myNodeNum, + model = model, + firmwareVersion = firmwareVersion, + couldUpdate = couldUpdate, + shouldUpdate = shouldUpdate, + currentPacketId = currentPacketId, + messageTimeoutMsec = messageTimeoutMsec, + minAppVersion = minAppVersion, + maxChannels = maxChannels, + hasWifi = hasWifi, + deviceId = deviceId, + pioEnv = pioEnv + ) + + private fun Node.toEntity() = NodeEntity( + num = num, + user = user, + position = position, + snr = snr, + rssi = rssi, + lastHeard = lastHeard, + deviceTelemetry = org.meshtastic.proto.Telemetry(device_metrics = deviceMetrics), + channel = channel, + viaMqtt = viaMqtt, + hopsAway = hopsAway, + isFavorite = isFavorite, + isIgnored = isIgnored, + isMuted = isMuted, + environmentTelemetry = org.meshtastic.proto.Telemetry(environment_metrics = environmentMetrics), + powerTelemetry = org.meshtastic.proto.Telemetry(power_metrics = powerMetrics), + paxcounter = paxcounter, + publicKey = publicKey, + notes = notes, + manuallyVerified = manuallyVerified, + nodeStatus = nodeStatus, + lastTransport = lastTransport + ) } diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/PacketRepository.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/PacketRepositoryImpl.kt similarity index 63% rename from core/data/src/main/kotlin/org/meshtastic/core/data/repository/PacketRepository.kt rename to core/data/src/main/kotlin/org/meshtastic/core/data/repository/PacketRepositoryImpl.kt index 7e7bcd9689..0c42eeb118 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/PacketRepository.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/PacketRepositoryImpl.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 @@ -27,58 +27,39 @@ import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.withContext import okio.ByteString.Companion.toByteString import org.meshtastic.core.database.DatabaseManager -import org.meshtastic.core.database.entity.ContactSettings -import org.meshtastic.core.database.entity.Packet -import org.meshtastic.core.database.entity.ReactionEntity +import org.meshtastic.core.database.entity.toReaction import org.meshtastic.core.di.CoroutineDispatchers +import org.meshtastic.core.model.ContactSettings import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.Message import org.meshtastic.core.model.MessageStatus 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 -class PacketRepository +@Suppress("TooManyFunctions", "LongParameterList") +class PacketRepositoryImpl @Inject constructor( private val dbManager: DatabaseManager, private val dispatchers: CoroutineDispatchers, ) : SharedPacketRepository { - override suspend fun savePacket( - myNodeNum: Int, - contactKey: String, - packet: DataPacket, - receivedTime: Long, - read: Boolean, - filtered: Boolean, - ) { - val packetToSave = - Packet( - uuid = 0L, - myNodeNum = myNodeNum, - packetId = packet.id, - port_num = packet.dataType, - contact_key = contactKey, - received_time = receivedTime, - read = read, - data = packet, - snr = packet.snr, - rssi = packet.rssi, - hopsAway = packet.hopsAway, - filtered = filtered, - ) - insert(packetToSave) - } - fun getWaypoints(): Flow> = + override fun getWaypoints(): Flow> = dbManager.currentDb.flatMapLatest { db -> db.packetDao().getAllWaypointsFlow() } + .map { list -> list.map { it.data } } - fun getContacts(): Flow> = + override fun getContacts(): Flow> = dbManager.currentDb.flatMapLatest { db -> db.packetDao().getContactKeys() } + .map { map -> map.mapValues { it.value.data } } - fun getContactsPaged(): Flow> = Pager( + override fun getContactsPaged(): Flow> = Pager( config = PagingConfig( pageSize = CONTACTS_PAGE_SIZE, @@ -88,26 +69,27 @@ constructor( pagingSourceFactory = { dbManager.currentDb.value.packetDao().getContactKeysPaged() }, ) .flow + .map { pagingData -> pagingData.map { it.data } } - suspend fun getMessageCount(contact: String): Int = + override suspend fun getMessageCount(contact: String): Int = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getMessageCount(contact) } - suspend fun getUnreadCount(contact: String): Int = + override suspend fun getUnreadCount(contact: String): Int = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getUnreadCount(contact) } - fun getFirstUnreadMessageUuid(contact: String): Flow = + override fun getFirstUnreadMessageUuid(contact: String): Flow = dbManager.currentDb.flatMapLatest { db -> db.packetDao().getFirstUnreadMessageUuid(contact) } - fun hasUnreadMessages(contact: String): Flow = + override fun hasUnreadMessages(contact: String): Flow = dbManager.currentDb.flatMapLatest { db -> db.packetDao().hasUnreadMessages(contact) } - fun getUnreadCountTotal(): Flow = + override fun getUnreadCountTotal(): Flow = dbManager.currentDb.flatMapLatest { db -> db.packetDao().getUnreadCountTotal() } - suspend fun clearUnreadCount(contact: String, timestamp: Long) = + override suspend fun clearUnreadCount(contact: String, timestamp: Long) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().clearUnreadCount(contact, timestamp) } - suspend fun updateLastReadMessage(contact: String, messageUuid: Long, lastReadTimestamp: Long) = + override suspend fun updateLastReadMessage(contact: String, messageUuid: Long, lastReadTimestamp: Long) = withContext(dispatchers.io) { val dao = dbManager.currentDb.value.packetDao() val current = dao.getContactSettings(contact) @@ -116,7 +98,7 @@ constructor( return@withContext } val updated = - (current ?: ContactSettings(contact_key = contact)).copy( + (current ?: ContactSettingsEntity(contact_key = contact)).copy( lastReadMessageUuid = messageUuid, lastReadMessageTimestamp = lastReadTimestamp, ) @@ -126,15 +108,41 @@ constructor( override suspend fun getQueuedPackets(): List? = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getQueuedPackets() } - suspend fun insert(packet: Packet) = + suspend fun insertRoomPacket(packet: RoomPacket) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().insert(packet) } - suspend fun getMessagesFrom( + override suspend fun savePacket( + myNodeNum: Int, + contactKey: String, + packet: DataPacket, + receivedTime: Long, + read: Boolean, + filtered: Boolean, + ) { + val packetToSave = + RoomPacket( + uuid = 0L, + myNodeNum = myNodeNum, + packetId = packet.id, + port_num = packet.dataType, + contact_key = contactKey, + received_time = receivedTime, + read = read, + data = packet, + snr = packet.snr, + rssi = packet.rssi, + hopsAway = packet.hopsAway, + filtered = filtered, + ) + insertRoomPacket(packetToSave) + } + + override suspend fun getMessagesFrom( contact: String, - limit: Int? = null, - includeFiltered: Boolean = true, + limit: Int?, + includeFiltered: Boolean, getNode: suspend (String?) -> Node, - ) = withContext(dispatchers.io) { + ): Flow> = withContext(dispatchers.io) { val dao = dbManager.currentDb.value.packetDao() val flow = when { @@ -147,14 +155,14 @@ constructor( val message = packet.toMessage(getNode) message.replyId .takeIf { it != null && it != 0 } - ?.let { getPacketByPacketId(it) } - ?.toMessage(getNode) + ?.let { getPacketByPacketIdInternal(it) } + ?.let { originalPacket -> originalPacket.toMessage(getNode) } ?.let { originalMessage -> message.copy(originalMessage = originalMessage) } ?: message } } } - fun getMessagesFromPaged(contact: String, getNode: suspend (String?) -> Node): Flow> = Pager( + override fun getMessagesFromPaged(contact: String, getNode: suspend (String?) -> Node): Flow> = Pager( config = PagingConfig( pageSize = MESSAGES_PAGE_SIZE, @@ -169,8 +177,35 @@ constructor( val message = packet.toMessage(getNode) message.replyId .takeIf { it != null && it != 0 } - ?.let { getPacketByPacketId(it) } - ?.toMessage(getNode) + ?.let { getPacketByPacketIdInternal(it) } + ?.let { originalPacket -> originalPacket.toMessage(getNode) } + ?.let { originalMessage -> message.copy(originalMessage = originalMessage) } ?: message + } + } + + override fun getMessagesFromPaged( + contactKey: String, + includeFiltered: Boolean, + getNode: suspend (String?) -> Node, + ): Flow> = Pager( + config = + PagingConfig( + pageSize = MESSAGES_PAGE_SIZE, + enablePlaceholders = false, + initialLoadSize = MESSAGES_PAGE_SIZE, + ), + pagingSourceFactory = { + dbManager.currentDb.value.packetDao().getMessagesFromPaged(contactKey, includeFiltered) + }, + ) + .flow + .map { pagingData -> + pagingData.map { packet -> + val message = packet.toMessage(getNode) + message.replyId + .takeIf { it != null && it != 0 } + ?.let { getPacketByPacketIdInternal(it) } + ?.let { originalPacket -> originalPacket.toMessage(getNode) } ?.let { originalMessage -> message.copy(originalMessage = originalMessage) } ?: message } } @@ -178,31 +213,99 @@ constructor( override suspend fun updateMessageStatus(d: DataPacket, m: MessageStatus) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().updateMessageStatus(d, m) } - suspend fun updateMessageId(d: DataPacket, id: Int) = + override suspend fun updateMessageId(d: DataPacket, id: Int) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().updateMessageId(d, id) } - suspend fun getPacketById(requestId: Int) = + private suspend fun getPacketByIdInternal(requestId: Int) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getPacketById(requestId) } - suspend fun getPacketByPacketId(packetId: Int) = + override suspend fun getPacketById(id: Int): DataPacket? = + withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getPacketById(id)?.data } + + override suspend fun getPacketByPacketId(packetId: Int): DataPacket? = + withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getPacketByPacketId(packetId)?.packet?.data } + + private suspend fun getPacketByPacketIdInternal(packetId: Int) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getPacketByPacketId(packetId) } - suspend fun findPacketsWithId(packetId: Int) = + override suspend fun insert( + packet: DataPacket, + myNodeNum: Int, + contactKey: String, + receivedTime: Long, + read: Boolean, + filtered: Boolean, + ) { + val packetToSave = + RoomPacket( + uuid = 0L, + myNodeNum = myNodeNum, + packetId = packet.id, + port_num = packet.dataType, + contact_key = contactKey, + received_time = receivedTime, + read = read, + data = packet, + snr = packet.snr, + rssi = packet.rssi, + hopsAway = packet.hopsAway, + filtered = filtered, + ) + insertRoomPacket(packetToSave) + } + + override suspend fun update(packet: DataPacket): Unit = withContext(dispatchers.io) { + val dao = dbManager.currentDb.value.packetDao() + dao.findPacketsWithId(packet.id).find { it.data == packet }?.let { + dao.update(it.copy(data = packet)) + } + } + + override suspend fun insertReaction(reaction: Reaction, myNodeNum: Int) = withContext(dispatchers.io) { + dbManager.currentDb.value.packetDao().insert(reaction.toEntity(myNodeNum)) + } + + override suspend fun updateReaction(reaction: Reaction) = withContext(dispatchers.io) { + val dao = dbManager.currentDb.value.packetDao() + dao.findReactionsWithId(reaction.packetId).find { it.userId == reaction.user.id && it.emoji == reaction.emoji }?.let { + dao.update(reaction.toEntity(it.myNodeNum)) + } ?: Unit + } + + override suspend fun getReactionByPacketId(packetId: Int): Reaction? = withContext(dispatchers.io) { + dbManager.currentDb.value.packetDao().getReactionByPacketId(packetId)?.toReaction { null } + } + + private suspend fun getReactionByPacketIdInternal(packetId: Int) = + withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getReactionByPacketId(packetId) } + + override suspend fun findPacketsWithId(packetId: Int): List = withContext(dispatchers.io) { + dbManager.currentDb.value.packetDao().findPacketsWithId(packetId).map { it.data } + } + + private suspend fun findPacketsWithIdInternal(packetId: Int) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().findPacketsWithId(packetId) } + override suspend fun findReactionsWithId(packetId: Int): List = withContext(dispatchers.io) { + dbManager.currentDb.value.packetDao().findReactionsWithId(packetId).toReaction { null } + } + + private suspend fun findReactionsWithIdInternal(packetId: Int) = + withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().findReactionsWithId(packetId) } + @Suppress("CyclomaticComplexMethod") - suspend fun updateSFPPStatus( + override suspend fun updateSFPPStatus( packetId: Int, from: Int, to: Int, hash: ByteArray, - status: MessageStatus = MessageStatus.SFPP_CONFIRMED, - rxTime: Long = 0, - myNodeNum: Int? = null, + status: MessageStatus, + rxTime: Long, + myNodeNum: Int?, ) = withContext(dispatchers.io) { val dao = dbManager.currentDb.value.packetDao() - val packets = dao.findPacketsWithId(packetId) - val reactions = dao.findReactionsWithId(packetId) + val packets = findPacketsWithIdInternal(packetId) + val reactions = findReactionsWithIdInternal(packetId) val fromId = DataPacket.nodeNumToDefaultId(from) val isFromLocalNode = myNodeNum != null && from == myNodeNum val toId = @@ -259,11 +362,11 @@ constructor( } } - suspend fun updateSFPPStatusByHash( + override suspend fun updateSFPPStatusByHash( hash: ByteArray, - status: MessageStatus = MessageStatus.SFPP_CONFIRMED, - rxTime: Long = 0, - ) = withContext(dispatchers.io) { + status: MessageStatus, + rxTime: Long, + ): Unit = withContext(dispatchers.io) { val dao = dbManager.currentDb.value.packetDao() val hashByteString = hash.toByteString() dao.findPacketBySfppHash(hashByteString)?.let { packet -> @@ -286,99 +389,95 @@ constructor( } } - suspend fun deleteMessages(uuidList: List) = withContext(dispatchers.io) { + override suspend fun deleteMessages(uuidList: List) = withContext(dispatchers.io) { for (chunk in uuidList.chunked(DELETE_CHUNK_SIZE)) { // Fetch DAO per chunk to avoid holding a stale reference if the active DB switches dbManager.currentDb.value.packetDao().deleteMessages(chunk) } } - suspend fun deleteContacts(contactList: List) = + override suspend fun deleteContacts(contactList: List) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().deleteContacts(contactList) } - suspend fun deleteWaypoint(id: Int) = + override suspend fun deleteWaypoint(id: Int) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().deleteWaypoint(id) } - suspend fun delete(packet: Packet) = + suspend fun delete(packet: RoomPacket) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().delete(packet) } - suspend fun update(packet: Packet) = + suspend fun update(packet: RoomPacket) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().update(packet) } - fun getContactSettings(): Flow> = + override fun getContactSettings(): Flow> = dbManager.currentDb.flatMapLatest { db -> db.packetDao().getContactSettings() } + .map { map -> map.mapValues { it.value.toShared() } } - suspend fun getContactSettings(contact: String) = withContext(dispatchers.io) { - dbManager.currentDb.value.packetDao().getContactSettings(contact) ?: ContactSettings(contact) + override suspend fun getContactSettings(contact: String): ContactSettings = withContext(dispatchers.io) { + dbManager.currentDb.value.packetDao().getContactSettings(contact)?.toShared() ?: ContactSettings(contact) } - suspend fun setMuteUntil(contacts: List, until: Long) = + override suspend fun setMuteUntil(contacts: List, until: Long) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().setMuteUntil(contacts, until) } - suspend fun insertReaction(reaction: ReactionEntity) = + suspend fun insertReaction(reaction: RoomReaction) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().insert(reaction) } - suspend fun updateReaction(reaction: ReactionEntity) = + suspend fun updateReaction(reaction: RoomReaction) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().update(reaction) } - suspend fun getReactionByPacketId(packetId: Int) = - withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getReactionByPacketId(packetId) } - - suspend fun findReactionsWithId(packetId: Int) = - withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().findReactionsWithId(packetId) } - - fun getFilteredCountFlow(contactKey: String): Flow = + override fun getFilteredCountFlow(contactKey: String): Flow = dbManager.currentDb.flatMapLatest { db -> db.packetDao().getFilteredCountFlow(contactKey) } - suspend fun getFilteredCount(contactKey: String): Int = + override suspend fun getFilteredCount(contactKey: String): Int = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getFilteredCount(contactKey) } - fun getMessagesFromPaged( - contactKey: String, - includeFiltered: Boolean, - getNode: suspend (String?) -> Node, - ): Flow> = Pager( - config = - PagingConfig( - pageSize = MESSAGES_PAGE_SIZE, - enablePlaceholders = false, - initialLoadSize = MESSAGES_PAGE_SIZE, - ), - pagingSourceFactory = { - dbManager.currentDb.value.packetDao().getMessagesFromPaged(contactKey, includeFiltered) - }, - ) - .flow - .map { pagingData -> - pagingData.map { packet -> - val message = packet.toMessage(getNode) - message.replyId - .takeIf { it != null && it != 0 } - ?.let { getPacketByPacketId(it) } - ?.toMessage(getNode) - ?.let { originalMessage -> message.copy(originalMessage = originalMessage) } ?: message - } - } - - suspend fun setContactFilteringDisabled(contactKey: String, disabled: Boolean) = withContext(dispatchers.io) { + override suspend fun setContactFilteringDisabled(contactKey: String, disabled: Boolean) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().setContactFilteringDisabled(contactKey, disabled) } - suspend fun clearPacketDB() = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().deleteAll() } + override suspend fun clearPacketDB() = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().deleteAll() } - suspend fun migrateChannelsByPSK(oldSettings: List, newSettings: List) = + override suspend fun migrateChannelsByPSK(oldSettings: List, newSettings: List) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().migrateChannelsByPSK(oldSettings, newSettings) } - suspend fun updateFilteredBySender(senderId: String, filtered: Boolean) { + override suspend fun updateFilteredBySender(senderId: String, filtered: Boolean) { val pattern = "%\"from\":\"${senderId}\"%" withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().updateFilteredBySender(pattern, filtered) } } - private fun org.meshtastic.core.database.dao.PacketDao.getAllWaypointsFlow(): Flow> = + private fun org.meshtastic.core.database.dao.PacketDao.getAllWaypointsFlow(): Flow> = getAllPackets(PortNum.WAYPOINT_APP.value) + private fun ContactSettingsEntity.toShared() = ContactSettings( + contactKey = contact_key, + muteUntil = muteUntil, + lastReadMessageUuid = lastReadMessageUuid, + lastReadMessageTimestamp = lastReadMessageTimestamp, + filteringDisabled = filteringDisabled, + isMuted = isMuted + ) + + private fun Reaction.toEntity(myNodeNum: Int) = RoomReaction( + myNodeNum = myNodeNum, + replyId = replyId, + userId = user.id, + emoji = emoji, + timestamp = timestamp, + snr = snr, + rssi = rssi, + hopsAway = hopsAway, + packetId = packetId, + status = status, + routingError = routingError, + relays = relays, + relayNode = relayNode, + to = to, + channel = channel, + sfpp_hash = sfppHash + ) + companion object { private const val CONTACTS_PAGE_SIZE = 30 private const val MESSAGES_PAGE_SIZE = 50 diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/RadioConfigRepository.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/RadioConfigRepositoryImpl.kt similarity index 81% rename from core/data/src/main/kotlin/org/meshtastic/core/data/repository/RadioConfigRepository.kt rename to core/data/src/main/kotlin/org/meshtastic/core/data/repository/RadioConfigRepositoryImpl.kt index 3454a9de16..dcbd93a341 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/RadioConfigRepository.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/RadioConfigRepositoryImpl.kt @@ -23,6 +23,7 @@ import org.meshtastic.core.datastore.LocalConfigDataSource import org.meshtastic.core.datastore.ModuleConfigDataSource import org.meshtastic.core.model.util.getChannelUrl import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.proto.Channel import org.meshtastic.proto.ChannelSet import org.meshtastic.proto.ChannelSettings @@ -37,25 +38,25 @@ import javax.inject.Inject * Class responsible for radio configuration data. Combines access to [nodeDB], [ChannelSet], [LocalConfig] & * [LocalModuleConfig]. */ -open class RadioConfigRepository +open class RadioConfigRepositoryImpl @Inject constructor( private val nodeDB: NodeRepository, private val channelSetDataSource: ChannelSetDataSource, private val localConfigDataSource: LocalConfigDataSource, private val moduleConfigDataSource: ModuleConfigDataSource, -) { +) : RadioConfigRepository { /** Flow representing the [ChannelSet] data store. */ - val channelSetFlow: Flow = channelSetDataSource.channelSetFlow + override val channelSetFlow: Flow = channelSetDataSource.channelSetFlow /** Clears the [ChannelSet] data in the data store. */ - suspend fun clearChannelSet() { + override suspend fun clearChannelSet() { channelSetDataSource.clearChannelSet() } /** Replaces the [ChannelSettings] list with a new [settingsList]. */ - suspend fun replaceAllSettings(settingsList: List) { + override suspend fun replaceAllSettings(settingsList: List) { channelSetDataSource.replaceAllSettings(settingsList) } @@ -66,13 +67,13 @@ constructor( * @param channel The [Channel] provided. * @return the index of the admin channel after the update (if not found, returns 0). */ - suspend fun updateChannelSettings(channel: Channel) = channelSetDataSource.updateChannelSettings(channel) + override suspend fun updateChannelSettings(channel: Channel) = channelSetDataSource.updateChannelSettings(channel) /** Flow representing the [LocalConfig] data store. */ - open val localConfigFlow: Flow = localConfigDataSource.localConfigFlow + override val localConfigFlow: Flow = localConfigDataSource.localConfigFlow /** Clears the [LocalConfig] data in the data store. */ - suspend fun clearLocalConfig() { + override suspend fun clearLocalConfig() { localConfigDataSource.clearLocalConfig() } @@ -81,16 +82,16 @@ constructor( * * @param config The [Config] to be set. */ - suspend fun setLocalConfig(config: Config) { + override suspend fun setLocalConfig(config: Config) { localConfigDataSource.setLocalConfig(config) config.lora?.let { channelSetDataSource.setLoraConfig(it) } } /** Flow representing the [LocalModuleConfig] data store. */ - val moduleConfigFlow: Flow = moduleConfigDataSource.moduleConfigFlow + override val moduleConfigFlow: Flow = moduleConfigDataSource.moduleConfigFlow /** Clears the [LocalModuleConfig] data in the data store. */ - suspend fun clearLocalModuleConfig() { + override suspend fun clearLocalModuleConfig() { moduleConfigDataSource.clearLocalModuleConfig() } @@ -99,12 +100,12 @@ constructor( * * @param config The [ModuleConfig] to be set. */ - suspend fun setLocalModuleConfig(config: ModuleConfig) { + override suspend fun setLocalModuleConfig(config: ModuleConfig) { moduleConfigDataSource.setLocalModuleConfig(config) } /** Flow representing the combined [DeviceProfile] protobuf. */ - val deviceProfileFlow: Flow = + override val deviceProfileFlow: Flow = combine(nodeDB.ourNodeInfo, channelSetFlow, localConfigFlow, moduleConfigFlow) { node, channels, diff --git a/core/database/build.gradle.kts b/core/database/build.gradle.kts index 5c5ed5dcb7..cb85f50174 100644 --- a/core/database/build.gradle.kts +++ b/core/database/build.gradle.kts @@ -32,6 +32,7 @@ configure { } dependencies { + implementation(projects.core.repository) implementation(projects.core.common) implementation(projects.core.di) implementation(projects.core.model) diff --git a/core/database/src/androidTest/kotlin/org/meshtastic/core/database/dao/NodeInfoDaoTest.kt b/core/database/src/androidTest/kotlin/org/meshtastic/core/database/dao/NodeInfoDaoTest.kt index 4ca6e26f73..e59e01c37e 100644 --- a/core/database/src/androidTest/kotlin/org/meshtastic/core/database/dao/NodeInfoDaoTest.kt +++ b/core/database/src/androidTest/kotlin/org/meshtastic/core/database/dao/NodeInfoDaoTest.kt @@ -34,8 +34,8 @@ import org.junit.runner.RunWith import org.meshtastic.core.database.MeshtasticDatabase import org.meshtastic.core.database.entity.MyNodeEntity import org.meshtastic.core.database.entity.NodeEntity -import org.meshtastic.core.database.model.Node -import org.meshtastic.core.database.model.NodeSortOption +import org.meshtastic.core.model.Node +import org.meshtastic.core.model.NodeSortOption import org.meshtastic.core.model.util.onlineTimeThreshold import org.meshtastic.proto.HardwareModel import org.meshtastic.proto.User diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/DatabaseManager.kt b/core/database/src/main/kotlin/org/meshtastic/core/database/DatabaseManager.kt index fe90c72e3c..5b559f58fe 100644 --- a/core/database/src/main/kotlin/org/meshtastic/core/database/DatabaseManager.kt +++ b/core/database/src/main/kotlin/org/meshtastic/core/database/DatabaseManager.kt @@ -59,7 +59,7 @@ constructor( // Expose the DB cache limit as a reactive stream so UI can observe changes. private val _cacheLimit = MutableStateFlow(getCacheLimit()) - open val cacheLimit: StateFlow = _cacheLimit + override val cacheLimit: StateFlow = _cacheLimit // Keep cache-limit StateFlow in sync if some other component updates SharedPreferences. private val prefsListener = @@ -189,11 +189,11 @@ constructor( } } - fun getCacheLimit(): Int = prefs + override fun getCacheLimit(): Int = prefs .getInt(DatabaseConstants.CACHE_LIMIT_KEY, DatabaseConstants.DEFAULT_CACHE_LIMIT) .coerceIn(DatabaseConstants.MIN_CACHE_LIMIT, DatabaseConstants.MAX_CACHE_LIMIT) - fun setCacheLimit(limit: Int) { + override fun setCacheLimit(limit: Int) { val clamped = limit.coerceIn(DatabaseConstants.MIN_CACHE_LIMIT, DatabaseConstants.MAX_CACHE_LIMIT) if (clamped == getCacheLimit()) return prefs.edit().putInt(DatabaseConstants.CACHE_LIMIT_KEY, clamped).apply() diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/entity/Packet.kt b/core/database/src/main/kotlin/org/meshtastic/core/database/entity/Packet.kt index 4fcfb3698d..5529b9606c 100644 --- a/core/database/src/main/kotlin/org/meshtastic/core/database/entity/Packet.kt +++ b/core/database/src/main/kotlin/org/meshtastic/core/database/entity/Packet.kt @@ -155,11 +155,11 @@ data class ReactionEntity( @ColumnInfo(name = "sfpp_hash") val sfpp_hash: ByteString? = null, ) -private suspend fun ReactionEntity.toReaction(getNode: suspend (userId: String?) -> Node): Reaction { - val node = getNode(userId) +suspend fun ReactionEntity.toReaction(getNode: suspend (userId: String?) -> Node?): Reaction { + val user = getNode(userId)?.user ?: org.meshtastic.proto.User(id = userId) return Reaction( replyId = replyId, - user = node.user, + user = user, emoji = emoji, timestamp = timestamp, snr = snr, @@ -176,5 +176,5 @@ private suspend fun ReactionEntity.toReaction(getNode: suspend (userId: String?) ) } -private suspend fun List.toReaction(getNode: suspend (userId: String?) -> Node) = +suspend fun List.toReaction(getNode: suspend (userId: String?) -> Node?) = this.map { it.toReaction(getNode) } diff --git a/core/database/src/test/kotlin/org/meshtastic/core/database/model/NodeTest.kt b/core/database/src/test/kotlin/org/meshtastic/core/database/model/NodeTest.kt index 5a4db388ec..aad9defe18 100644 --- a/core/database/src/test/kotlin/org/meshtastic/core/database/model/NodeTest.kt +++ b/core/database/src/test/kotlin/org/meshtastic/core/database/model/NodeTest.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.core.database.model +package org.meshtastic.core.model import org.junit.Assert.assertEquals import org.junit.Test diff --git a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCase.kt b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCase.kt index 4515564d38..51cf4af0ee 100644 --- a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCase.kt +++ b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCase.kt @@ -20,7 +20,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf -import org.meshtastic.core.data.repository.DeviceHardwareRepository import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.Node import org.meshtastic.core.model.RadioController @@ -28,6 +27,7 @@ import org.meshtastic.core.prefs.radio.RadioPrefs import org.meshtastic.core.prefs.radio.isBle import org.meshtastic.core.prefs.radio.isSerial import org.meshtastic.core.prefs.radio.isTcp +import org.meshtastic.core.repository.DeviceHardwareRepository import org.meshtastic.core.repository.NodeRepository import javax.inject.Inject diff --git a/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/AdminActionsUseCaseTest.kt b/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/AdminActionsUseCaseTest.kt index e423ca882e..a6fe77b73b 100644 --- a/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/AdminActionsUseCaseTest.kt +++ b/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/AdminActionsUseCaseTest.kt @@ -23,8 +23,8 @@ import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test -import org.meshtastic.core.data.repository.NodeRepository import org.meshtastic.core.model.RadioController +import org.meshtastic.core.repository.NodeRepository class AdminActionsUseCaseTest { diff --git a/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/CleanNodeDatabaseUseCaseTest.kt b/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/CleanNodeDatabaseUseCaseTest.kt index 001c0a5fec..09e8b15d62 100644 --- a/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/CleanNodeDatabaseUseCaseTest.kt +++ b/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/CleanNodeDatabaseUseCaseTest.kt @@ -23,9 +23,9 @@ import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test -import org.meshtastic.core.data.repository.NodeRepository import org.meshtastic.core.database.entity.NodeEntity import org.meshtastic.core.domain.FakeRadioController +import org.meshtastic.core.repository.NodeRepository import kotlin.time.Duration.Companion.days class CleanNodeDatabaseUseCaseTest { diff --git a/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCaseTest.kt b/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCaseTest.kt index 32dcff37f6..f5175877c9 100644 --- a/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCaseTest.kt +++ b/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCaseTest.kt @@ -27,9 +27,9 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.meshtastic.core.data.repository.MeshLogRepository -import org.meshtastic.core.data.repository.NodeRepository import org.meshtastic.core.database.entity.MeshLog -import org.meshtastic.core.database.model.Node +import org.meshtastic.core.model.Node +import org.meshtastic.core.repository.NodeRepository import org.meshtastic.proto.Data import org.meshtastic.proto.FromRadio import org.meshtastic.proto.MeshPacket diff --git a/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCaseTest.kt b/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCaseTest.kt index 41db758c71..8e6b21077c 100644 --- a/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCaseTest.kt +++ b/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCaseTest.kt @@ -26,12 +26,12 @@ import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test -import org.meshtastic.core.data.repository.DeviceHardwareRepository -import org.meshtastic.core.data.repository.NodeRepository -import org.meshtastic.core.database.model.Node import org.meshtastic.core.model.ConnectionState +import org.meshtastic.core.model.Node import org.meshtastic.core.model.RadioController import org.meshtastic.core.prefs.radio.RadioPrefs +import org.meshtastic.core.repository.DeviceHardwareRepository +import org.meshtastic.core.repository.NodeRepository class IsOtaCapableUseCaseTest { diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Contact.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Contact.kt index 7df9f63af8..197f5e9d16 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Contact.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Contact.kt @@ -32,3 +32,12 @@ data class Contact( val isUnmessageable: Boolean, val nodeColors: Pair? = null, ) : CommonParcelable + +data class ContactSettings( + val contactKey: String, + val muteUntil: Long = 0L, + val lastReadMessageUuid: Long? = null, + val lastReadMessageTimestamp: Long? = null, + val filteringDisabled: Boolean = false, + val isMuted: Boolean = false, +) diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Node.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Node.kt index 9d2db88927..c667e9098c 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Node.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Node.kt @@ -22,6 +22,7 @@ import org.meshtastic.core.common.util.GPSFormat import org.meshtastic.core.common.util.bearing import org.meshtastic.core.common.util.latLongToMeter import org.meshtastic.core.model.util.UnitConversions.celsiusToFahrenheit +import org.meshtastic.core.model.util.onlineTimeThreshold import org.meshtastic.core.model.util.toDistanceString import org.meshtastic.proto.Config import org.meshtastic.proto.DeviceMetadata @@ -67,6 +68,9 @@ data class Node( ) { val capabilities: Capabilities by lazy { Capabilities(metadata?.firmware_version) } + val isOnline: Boolean + get() = lastHeard > onlineTimeThreshold() + val colors: Pair get() { // returns foreground and background @ColorInt for each 'num' val r = (num and 0xFF0000) shr 16 @@ -187,9 +191,30 @@ data class Node( companion object { private const val DEFAULT_ID_SUFFIX_LENGTH = 4 + private const val RELAY_NODE_SUFFIX_MASK = 0xFF val ERROR_BYTE_STRING: ByteString = ByteArray(32) { 0 }.toByteString() + fun getRelayNode(relayNodeId: Int, nodes: List, ourNodeNum: Int?): Node? { + val relayNodeIdSuffix = relayNodeId and RELAY_NODE_SUFFIX_MASK + + val candidateRelayNodes = + nodes.filter { + it.num != ourNodeNum && + it.lastHeard != 0 && + (it.num and RELAY_NODE_SUFFIX_MASK) == relayNodeIdSuffix + } + + val closestRelayNode = + if (candidateRelayNodes.size == 1) { + candidateRelayNodes.first() + } else { + candidateRelayNodes.minByOrNull { it.hopsAway } + } + + return closestRelayNode + } + /** Creates a fallback [Node] when the node is not found in the database. */ fun createFallback(nodeNum: Int, fallbackNamePrefix: String): Node { val userId = DataPacket.nodeNumToDefaultId(nodeNum) diff --git a/core/repository/build.gradle.kts b/core/repository/build.gradle.kts index 120d38c4ce..1695705a02 100644 --- a/core/repository/build.gradle.kts +++ b/core/repository/build.gradle.kts @@ -33,6 +33,7 @@ kotlin { implementation(libs.kotlinx.coroutines.core) implementation(libs.kermit) + implementation(libs.androidx.paging.common) } } } diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/CommandSender.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/CommandSender.kt new file mode 100644 index 0000000000..77f5117bb5 --- /dev/null +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/CommandSender.kt @@ -0,0 +1,90 @@ +/* + * 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 . + */ +package org.meshtastic.core.repository + +import kotlinx.coroutines.CoroutineScope +import okio.ByteString +import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.model.Position +import org.meshtastic.proto.AdminMessage +import org.meshtastic.proto.ChannelSet +import org.meshtastic.proto.LocalConfig +import org.meshtastic.proto.NeighborInfo + +/** + * Interface for sending commands and packets to the mesh network. + */ +interface CommandSender { + /** Starts the command sender with the given coroutine scope. */ + fun start(scope: CoroutineScope) + + /** Returns the current packet ID. */ + fun getCurrentPacketId(): Long + + /** Returns the cached local configuration. */ + fun getCachedLocalConfig(): LocalConfig + + /** Returns the cached channel set. */ + fun getCachedChannelSet(): ChannelSet + + /** Generates a new unique packet ID. */ + fun generatePacketId(): Int + + /** The latest neighbor info received from the connected radio. */ + var lastNeighborInfo: NeighborInfo? + + /** Start times of traceroute requests for duration calculation. */ + val tracerouteStartTimes: MutableMap + + /** Start times of neighbor info requests for duration calculation. */ + val neighborInfoStartTimes: MutableMap + + /** Sets the session passkey for admin messages. */ + fun setSessionPasskey(key: ByteString) + + /** Sends a data packet to the mesh. */ + fun sendData(p: DataPacket) + + /** Sends an admin message to a specific node. */ + fun sendAdmin( + destNum: Int, + requestId: Int = generatePacketId(), + wantResponse: Boolean = false, + initFn: () -> AdminMessage, + ) + + /** Sends our current position to the mesh. */ + fun sendPosition(pos: org.meshtastic.proto.Position, destNum: Int? = null, wantResponse: Boolean = false) + + /** Requests the position of a specific node. */ + fun requestPosition(destNum: Int, currentPosition: Position) + + /** Sets a fixed position for a node. */ + fun setFixedPosition(destNum: Int, pos: Position) + + /** Requests user info from a specific node. */ + fun requestUserInfo(destNum: Int) + + /** Requests a traceroute to a specific node. */ + fun requestTraceroute(requestId: Int, destNum: Int) + + /** Requests telemetry from a specific node. */ + fun requestTelemetry(requestId: Int, destNum: Int, typeValue: Int) + + /** Requests neighbor info from a specific node. */ + fun requestNeighborInfo(requestId: Int, destNum: Int) +} diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/DatabaseManager.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/DatabaseManager.kt new file mode 100644 index 0000000000..44cbd1a8f5 --- /dev/null +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/DatabaseManager.kt @@ -0,0 +1,33 @@ +/* + * 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 . + */ +package org.meshtastic.core.repository + +import kotlinx.coroutines.flow.StateFlow + +/** + * Interface for managing database instances and cache limits. + */ +interface DatabaseManager { + /** Reactive stream of the current database cache limit. */ + val cacheLimit: StateFlow + + /** Returns the current database cache limit. */ + fun getCacheLimit(): Int + + /** Sets the database cache limit. */ + fun setCacheLimit(limit: Int) +} diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/DeviceHardwareRepository.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/DeviceHardwareRepository.kt new file mode 100644 index 0000000000..ce05cf8240 --- /dev/null +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/DeviceHardwareRepository.kt @@ -0,0 +1,35 @@ +/* + * 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 . + */ +package org.meshtastic.core.repository + +import org.meshtastic.core.model.DeviceHardware + +interface DeviceHardwareRepository { + /** + * Retrieves device hardware information by its model ID and optional target string. + * + * @param hwModel The hardware model identifier. + * @param target Optional PlatformIO target environment name to disambiguate multiple variants. + * @param forceRefresh If true, the local cache will be invalidated and data will be fetched remotely. + * @return A [Result] containing the [DeviceHardware] on success (or null if not found), or an exception on failure. + */ + suspend fun getDeviceHardwareByModel( + hwModel: Int, + target: String? = null, + forceRefresh: Boolean = false, + ): Result +} diff --git a/core/service/src/main/kotlin/org/meshtastic/core/service/MeshServiceNotifications.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshServiceNotifications.kt similarity index 84% rename from core/service/src/main/kotlin/org/meshtastic/core/service/MeshServiceNotifications.kt rename to core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshServiceNotifications.kt index 5af641d652..a4fefe2cdc 100644 --- a/core/service/src/main/kotlin/org/meshtastic/core/service/MeshServiceNotifications.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshServiceNotifications.kt @@ -14,10 +14,9 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.core.service +package org.meshtastic.core.repository -import android.app.Notification -import org.meshtastic.core.database.entity.NodeEntity +import org.meshtastic.core.model.Node import org.meshtastic.proto.ClientNotification import org.meshtastic.proto.Telemetry @@ -29,7 +28,7 @@ interface MeshServiceNotifications { fun initChannels() - fun updateServiceStateNotification(summaryString: String?, telemetry: Telemetry?): Notification + fun updateServiceStateNotification(summaryString: String?, telemetry: Telemetry?): Any suspend fun updateMessageNotification( contactKey: String, @@ -59,15 +58,15 @@ interface MeshServiceNotifications { fun showAlertNotification(contactKey: String, name: String, alert: String) - fun showNewNodeSeenNotification(node: NodeEntity) + fun showNewNodeSeenNotification(node: Node) - fun showOrUpdateLowBatteryNotification(node: NodeEntity, isRemote: Boolean) + fun showOrUpdateLowBatteryNotification(node: Node, isRemote: Boolean) fun showClientNotification(clientNotification: ClientNotification) fun cancelMessageNotification(contactKey: String) - fun cancelLowBatteryNotification(node: NodeEntity) + fun cancelLowBatteryNotification(node: Node) fun clearClientNotification(notification: ClientNotification) } diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeManager.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeManager.kt new file mode 100644 index 0000000000..9dd9c4c2ff --- /dev/null +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeManager.kt @@ -0,0 +1,105 @@ +/* + * 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 . + */ +package org.meshtastic.core.repository + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.StateFlow +import org.meshtastic.core.model.MyNodeInfo +import org.meshtastic.core.model.Node +import org.meshtastic.core.model.NodeInfo +import org.meshtastic.core.model.util.NodeIdLookup +import org.meshtastic.proto.DeviceMetadata +import org.meshtastic.proto.Paxcount +import org.meshtastic.proto.StatusMessage +import org.meshtastic.proto.Telemetry +import org.meshtastic.proto.User +import org.meshtastic.proto.NodeInfo as ProtoNodeInfo +import org.meshtastic.proto.Position as ProtoPosition + +/** + * Interface for managing the in-memory node database and processing received node information. + */ +interface NodeManager : NodeIdLookup { + /** Reactive map of all nodes by their number. */ + val nodeDBbyNodeNum: Map + + /** Reactive map of all nodes by their ID string. */ + val nodeDBbyID: Map + + /** Whether the node database is ready. */ + val isNodeDbReady: StateFlow + + /** Sets whether the node database is ready. */ + fun setNodeDbReady(ready: Boolean) + + /** Whether node database writes are allowed. */ + val allowNodeDbWrites: StateFlow + + /** Sets whether node database writes are allowed. */ + fun setAllowNodeDbWrites(allowed: Boolean) + + /** Starts the node manager with the given coroutine scope. */ + fun start(scope: CoroutineScope) + + /** The local node number. */ + var myNodeNum: Int? + + /** Loads the cached node database from the repository. */ + fun loadCachedNodeDB() + + /** Clears the in-memory node database. */ + fun clear() + + /** Returns information about the local node. */ + fun getMyNodeInfo(): MyNodeInfo? + + /** Returns the local node ID. */ + fun getMyId(): String + + /** Returns a list of all known nodes. */ + fun getNodes(): List + + /** Processes a received user packet. */ + fun handleReceivedUser(fromNum: Int, p: User, channel: Int = 0, manuallyVerified: Boolean = false) + + /** Processes a received position packet. */ + fun handleReceivedPosition(fromNum: Int, myNodeNum: Int, p: ProtoPosition, defaultTime: Long) + + /** Processes a received telemetry packet. */ + fun handleReceivedTelemetry(fromNum: Int, telemetry: Telemetry) + + /** Processes a received paxcounter packet. */ + fun handleReceivedPaxcounter(fromNum: Int, p: Paxcount) + + /** Processes a received node status message. */ + fun handleReceivedNodeStatus(fromNum: Int, s: StatusMessage) + + /** Updates the status string for a node. */ + fun updateNodeStatus(nodeNum: Int, status: String?) + + /** Updates a node using a transformation function. */ + fun updateNode(nodeNum: Int, withBroadcast: Boolean = true, channel: Int = 0, transform: (Node) -> Node) + + /** Removes a node from the in-memory database by its number. */ + fun removeByNodenum(nodeNum: Int) + + /** Installs node information from a ProtoNodeInfo object. */ + fun installNodeInfo(info: ProtoNodeInfo, withBroadcast: Boolean = true) + + /** Inserts hardware metadata for a node. */ + fun insertMetadata(nodeNum: Int, metadata: DeviceMetadata) +} diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeRepository.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeRepository.kt index a3c60a1444..e96fc90688 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeRepository.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeRepository.kt @@ -21,6 +21,7 @@ import kotlinx.coroutines.flow.StateFlow import org.meshtastic.core.model.MyNodeInfo import org.meshtastic.core.model.Node import org.meshtastic.core.model.NodeSortOption +import org.meshtastic.proto.DeviceMetadata import org.meshtastic.proto.LocalStats import org.meshtastic.proto.User @@ -92,4 +93,10 @@ interface NodeRepository { /** Updates the personal notes for a node. */ suspend fun setNodeNotes(num: Int, notes: String) + + /** Installs initial configuration data (local info and remote nodes) into the database. */ + suspend fun installConfig(mi: MyNodeInfo, nodes: List) + + /** Persists hardware metadata for a node. */ + suspend fun insertMetadata(nodeNum: Int, metadata: DeviceMetadata) } diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketHandler.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketHandler.kt new file mode 100644 index 0000000000..01b2126fc7 --- /dev/null +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketHandler.kt @@ -0,0 +1,45 @@ +/* + * 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 . + */ +package org.meshtastic.core.repository + +import kotlinx.coroutines.CoroutineScope +import org.meshtastic.proto.MeshPacket +import org.meshtastic.proto.QueueStatus +import org.meshtastic.proto.ToRadio + +/** + * Interface for handling the transmission of packets to the radio and managing the packet queue. + */ +interface PacketHandler { + /** Starts the packet handler with the given coroutine scope. */ + fun start(scope: CoroutineScope) + + /** Sends a command/packet directly to the radio. */ + fun sendToRadio(p: ToRadio) + + /** Adds a mesh packet to the queue for sending. */ + fun sendToRadio(packet: MeshPacket) + + /** Processes queue status updates from the radio. */ + fun handleQueueStatus(queueStatus: QueueStatus) + + /** Removes a pending response for a request. */ + fun removeResponse(dataRequestId: Int, complete: Boolean) + + /** Stops the packet queue. */ + fun stopPacketQueue() +} diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketRepository.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketRepository.kt index e2f9756978..018e0a7a7b 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketRepository.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketRepository.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 @@ -16,10 +16,39 @@ */ package org.meshtastic.core.repository +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import org.meshtastic.core.model.ContactSettings import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.model.Message import org.meshtastic.core.model.MessageStatus +import org.meshtastic.core.model.Node +import org.meshtastic.core.model.Reaction +import org.meshtastic.proto.ChannelSettings interface PacketRepository { + fun getWaypoints(): Flow> + + fun getContacts(): Flow> + + fun getContactsPaged(): Flow> + + suspend fun getMessageCount(contact: String): Int + + suspend fun getUnreadCount(contact: String): Int + + fun getFirstUnreadMessageUuid(contact: String): Flow + + fun hasUnreadMessages(contact: String): Flow + + fun getUnreadCountTotal(): Flow + + suspend fun clearUnreadCount(contact: String, timestamp: Long) + + suspend fun updateLastReadMessage(contact: String, messageUuid: Long, lastReadTimestamp: Long) + + suspend fun getQueuedPackets(): List? + suspend fun savePacket( myNodeNum: Int, contactKey: String, @@ -28,8 +57,84 @@ interface PacketRepository { read: Boolean = true, filtered: Boolean = false, ) - + + suspend fun getMessagesFrom( + contact: String, + limit: Int? = null, + includeFiltered: Boolean = true, + getNode: suspend (String?) -> Node, + ): Flow> + + fun getMessagesFromPaged( + contact: String, + getNode: suspend (String?) -> Node + ): Flow> + + fun getMessagesFromPaged( + contactKey: String, + includeFiltered: Boolean, + getNode: suspend (String?) -> Node, + ): Flow> + suspend fun updateMessageStatus(d: DataPacket, m: MessageStatus) + + suspend fun updateMessageId(d: DataPacket, id: Int) + + suspend fun deleteMessages(uuidList: List) + + suspend fun deleteContacts(contactList: List) + + suspend fun deleteWaypoint(id: Int) + + fun getContactSettings(): Flow> + + suspend fun getContactSettings(contact: String): ContactSettings + + suspend fun setMuteUntil(contacts: List, until: Long) + + fun getFilteredCountFlow(contactKey: String): Flow + + suspend fun getFilteredCount(contactKey: String): Int + + suspend fun setContactFilteringDisabled(contactKey: String, disabled: Boolean) + + suspend fun clearPacketDB() + + suspend fun migrateChannelsByPSK(oldSettings: List, newSettings: List) + + suspend fun updateFilteredBySender(senderId: String, filtered: Boolean) - suspend fun getQueuedPackets(): List? + suspend fun getPacketByPacketId(packetId: Int): DataPacket? + + suspend fun getPacketById(id: Int): DataPacket? + + suspend fun insert(packet: DataPacket, myNodeNum: Int, contactKey: String, receivedTime: Long, read: Boolean = true, filtered: Boolean = false) + + suspend fun update(packet: DataPacket) + + suspend fun insertReaction(reaction: Reaction, myNodeNum: Int) + + suspend fun updateReaction(reaction: Reaction) + + suspend fun getReactionByPacketId(packetId: Int): Reaction? + + suspend fun findPacketsWithId(packetId: Int): List + + suspend fun findReactionsWithId(packetId: Int): List + + suspend fun updateSFPPStatus( + packetId: Int, + from: Int, + to: Int, + hash: ByteArray, + status: MessageStatus, + rxTime: Long, + myNodeNum: Int?, + ) + + suspend fun updateSFPPStatusByHash( + hash: ByteArray, + status: MessageStatus, + rxTime: Long, + ) } diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/RadioConfigRepository.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/RadioConfigRepository.kt new file mode 100644 index 0000000000..6ec531c9d0 --- /dev/null +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/RadioConfigRepository.kt @@ -0,0 +1,62 @@ +/* + * 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 . + */ +package org.meshtastic.core.repository + +import kotlinx.coroutines.flow.Flow +import org.meshtastic.proto.Channel +import org.meshtastic.proto.ChannelSet +import org.meshtastic.proto.ChannelSettings +import org.meshtastic.proto.Config +import org.meshtastic.proto.DeviceProfile +import org.meshtastic.proto.LocalConfig +import org.meshtastic.proto.LocalModuleConfig +import org.meshtastic.proto.ModuleConfig + +interface RadioConfigRepository { + /** Flow representing the [ChannelSet] data store. */ + val channelSetFlow: Flow + + /** Clears the [ChannelSet] data in the data store. */ + suspend fun clearChannelSet() + + /** Replaces the [ChannelSettings] list with a new [settingsList]. */ + suspend fun replaceAllSettings(settingsList: List) + + /** Updates the [ChannelSettings] list with the provided channel. */ + suspend fun updateChannelSettings(channel: Channel) + + /** Flow representing the [LocalConfig] data store. */ + val localConfigFlow: Flow + + /** Clears the [LocalConfig] data in the data store. */ + suspend fun clearLocalConfig() + + /** Updates [LocalConfig] from each [Config] oneOf. */ + suspend fun setLocalConfig(config: Config) + + /** Flow representing the [LocalModuleConfig] data store. */ + val moduleConfigFlow: Flow + + /** Clears the [LocalModuleConfig] data in the data store. */ + suspend fun clearLocalModuleConfig() + + /** Updates [LocalModuleConfig] from each [ModuleConfig] oneOf. */ + suspend fun setLocalModuleConfig(config: ModuleConfig) + + /** Flow representing the combined [DeviceProfile] protobuf. */ + val deviceProfileFlow: Flow +} diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/ServiceBroadcasts.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/ServiceBroadcasts.kt new file mode 100644 index 0000000000..9e5809924a --- /dev/null +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/ServiceBroadcasts.kt @@ -0,0 +1,41 @@ +/* + * 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 . + */ +package org.meshtastic.core.repository + +import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.model.MessageStatus +import org.meshtastic.core.model.Node + +/** + * Interface for broadcasting service-level events to the application. + */ +interface ServiceBroadcasts { + /** Subscribes a receiver to mesh broadcasts. */ + fun subscribeReceiver(receiverName: String, packageName: String) + + /** Broadcasts received data to the application. */ + fun broadcastReceivedData(dataPacket: DataPacket) + + /** Broadcasts that the radio connection state has changed. */ + fun broadcastConnection() + + /** Broadcasts that node information has changed. */ + fun broadcastNodeChange(node: Node) + + /** Broadcasts that the status of a message has changed. */ + fun broadcastMessageStatus(packetId: Int, status: MessageStatus) +} 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 f8f7e07aa1..128b0e5d5d 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 @@ -22,7 +22,7 @@ import androidx.lifecycle.viewModelScope import co.touchlab.kermit.Logger import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import org.meshtastic.core.data.repository.RadioConfigRepository +import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.util.getChannelList import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt b/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt index 84a5e9538a..5c8e0a3a5b 100644 --- a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt +++ b/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt @@ -37,19 +37,19 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeoutOrNull import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.getString -import org.meshtastic.core.data.repository.DeviceHardwareRepository import org.meshtastic.core.data.repository.FirmwareReleaseRepository -import org.meshtastic.core.data.repository.NodeRepository import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.database.entity.FirmwareReleaseType -import org.meshtastic.core.database.entity.MyNodeEntity import org.meshtastic.core.datastore.BootloaderWarningDataSource import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.DeviceHardware +import org.meshtastic.core.model.MyNodeInfo import org.meshtastic.core.prefs.radio.RadioPrefs import org.meshtastic.core.prefs.radio.isBle import org.meshtastic.core.prefs.radio.isSerial import org.meshtastic.core.prefs.radio.isTcp +import org.meshtastic.core.repository.DeviceHardwareRepository +import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.firmware_update_battery_low import org.meshtastic.core.resources.firmware_update_copying @@ -462,7 +462,7 @@ constructor( return !isBatteryLow } - private suspend fun getDeviceHardware(ourNode: MyNodeEntity): DeviceHardware? { + private suspend fun getDeviceHardware(ourNode: MyNodeInfo): DeviceHardware? { val nodeInfo = nodeRepository.ourNodeInfo.value val hwModelInt = nodeInfo?.user?.hw_model?.value val target = ourNode.pioEnv diff --git a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapView.kt b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapView.kt index 4130e57f34..db6721ea3f 100644 --- a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapView.kt +++ b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapView.kt @@ -87,8 +87,7 @@ import org.meshtastic.core.common.gpsDisabled import org.meshtastic.core.common.util.DateFormatter import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.common.util.nowSeconds -import org.meshtastic.core.database.entity.Packet -import org.meshtastic.core.database.model.Node +import org.meshtastic.core.model.Node import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.util.toString import org.meshtastic.core.resources.Res @@ -344,7 +343,7 @@ fun MapView( LaunchedEffect(selectedWaypointId, waypoints) { if (selectedWaypointId != null && waypoints.containsKey(selectedWaypointId)) { - waypoints[selectedWaypointId]?.data?.waypoint?.let { pt -> + waypoints[selectedWaypointId]?.waypoint?.let { pt -> val geoPoint = GeoPoint((pt.latitude_i ?: 0) * 1e-7, (pt.longitude_i ?: 0) * 1e-7) map.controller.setCenter(geoPoint) map.controller.setZoom(WAYPOINT_ZOOM) @@ -496,7 +495,7 @@ fun MapView( fun showMarkerLongPressDialog(id: Int) { performHapticFeedback() Logger.d { "marker long pressed id=$id" } - val waypoint = waypoints[id]?.data?.waypoint ?: return + val waypoint = waypoints[id]?.waypoint ?: return // edit only when unlocked or lockedTo myNodeNum if ((waypoint.locked_to ?: 0) in setOf(0, mapViewModel.myNodeNum ?: 0) && isConnected) { showEditWaypointDialog = waypoint @@ -512,13 +511,13 @@ fun MapView( } @Suppress("MagicNumber") - fun MapView.onWaypointChanged(waypoints: Collection, selectedWaypointId: Int?): List { + fun MapView.onWaypointChanged(waypoints: Collection, selectedWaypointId: Int?): List { return waypoints.mapNotNull { waypoint -> - val pt = waypoint.data.waypoint ?: return@mapNotNull null + val pt = waypoint.waypoint ?: return@mapNotNull null if (!mapFilterState.showWaypoints) return@mapNotNull null // Use collected mapFilterState val lock = if ((pt.locked_to ?: 0) != 0) "\uD83D\uDD12" else "" - val time = DateFormatter.formatDateTime(waypoint.received_time) - val label = (pt.name ?: "") + " " + formatAgo((waypoint.received_time / 1000).toInt()) + val time = DateFormatter.formatDateTime(waypoint.time) + val label = (pt.name ?: "") + " " + formatAgo((waypoint.time / 1000).toInt()) val emoji = String(Character.toChars(if ((pt.icon ?: 0) == 0) 128205 else pt.icon!!)) val now = nowMillis val expireTimeMillis = (pt.expire ?: 0) * 1000L @@ -530,7 +529,7 @@ fun MapView( } MarkerWithLabel(this, label, emoji).apply { id = "${pt.id}" - title = "${pt.name} (${getUsername(waypoint.data.from)}$lock)" + title = "${pt.name} (${getUsername(waypoint.from)}$lock)" snippet = "[$time] ${pt.description} " + getString(Res.string.expires) + ": $expireTimeStr" position = GeoPoint((pt.latitude_i ?: 0) * 1e-7, (pt.longitude_i ?: 0) * 1e-7) if (selectedWaypointId == pt.id) { diff --git a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewModel.kt b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewModel.kt index 2029e058d9..c0434ae585 100644 --- a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewModel.kt +++ b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewModel.kt @@ -23,9 +23,9 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import org.meshtastic.core.common.BuildConfigProvider -import org.meshtastic.core.data.repository.NodeRepository -import org.meshtastic.core.data.repository.PacketRepository -import org.meshtastic.core.data.repository.RadioConfigRepository +import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.PacketRepository +import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.model.DataPacket import org.meshtastic.core.navigation.MapRoutes import org.meshtastic.core.prefs.map.MapPrefs diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapView.kt b/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapView.kt index 99725a8f8e..e23a6bcf60 100644 --- a/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapView.kt +++ b/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapView.kt @@ -98,7 +98,7 @@ import org.jetbrains.compose.resources.stringResource import org.json.JSONObject import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.common.util.nowSeconds -import org.meshtastic.core.database.model.Node +import org.meshtastic.core.model.Node import org.meshtastic.core.model.util.metersIn import org.meshtastic.core.model.util.mpsToKmph import org.meshtastic.core.model.util.mpsToMph @@ -272,7 +272,7 @@ fun MapView( val allNodes by mapViewModel.nodesWithPosition.collectAsStateWithLifecycle(listOf()) val waypoints by mapViewModel.waypoints.collectAsStateWithLifecycle(emptyMap()) - val displayableWaypoints = waypoints.values.mapNotNull { it.data.waypoint } + val displayableWaypoints = waypoints.values.mapNotNull { it.waypoint } val selectedWaypointId by mapViewModel.selectedWaypointId.collectAsStateWithLifecycle() val tracerouteSelection = diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapViewModel.kt b/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapViewModel.kt index 03a4cc8c50..86fdecce36 100644 --- a/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapViewModel.kt +++ b/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapViewModel.kt @@ -45,13 +45,13 @@ import kotlinx.coroutines.withContext import kotlinx.serialization.Serializable import org.meshtastic.core.data.model.CustomTileProviderConfig import org.meshtastic.core.data.repository.CustomTileProviderRepository -import org.meshtastic.core.data.repository.NodeRepository -import org.meshtastic.core.data.repository.PacketRepository -import org.meshtastic.core.data.repository.RadioConfigRepository import org.meshtastic.core.datastore.UiPreferencesDataSource import org.meshtastic.core.navigation.MapRoutes import org.meshtastic.core.prefs.map.GoogleMapsPrefs import org.meshtastic.core.prefs.map.MapPrefs +import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.PacketRepository +import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.Config @@ -344,7 +344,7 @@ constructor( viewModelScope.launch { val wpMap = waypoints.first { it.containsKey(wpId) } wpMap[wpId]?.let { packet -> - val waypoint = packet.data.waypoint!! + val waypoint = packet.waypoint!! val latLng = LatLng((waypoint.latitude_i ?: 0) / 1e7, (waypoint.longitude_i ?: 0) / 1e7) cameraPositionState.position = CameraPosition.fromLatLngZoom(latLng, 15f) } diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/PulsingNodeChip.kt b/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/PulsingNodeChip.kt index f42d978af9..51d2764296 100644 --- a/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/PulsingNodeChip.kt +++ b/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/PulsingNodeChip.kt @@ -30,7 +30,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch import org.meshtastic.core.common.util.nowSeconds -import org.meshtastic.core.database.model.Node +import org.meshtastic.core.model.Node import org.meshtastic.core.ui.component.NodeChip @Composable diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/model/NodeClusterItem.kt b/feature/map/src/google/kotlin/org/meshtastic/feature/map/model/NodeClusterItem.kt index 1930438fc0..bea9865e20 100644 --- a/feature/map/src/google/kotlin/org/meshtastic/feature/map/model/NodeClusterItem.kt +++ b/feature/map/src/google/kotlin/org/meshtastic/feature/map/model/NodeClusterItem.kt @@ -18,7 +18,7 @@ package org.meshtastic.feature.map.model import com.google.android.gms.maps.model.LatLng import com.google.maps.android.clustering.ClusterItem -import org.meshtastic.core.database.model.Node +import org.meshtastic.core.model.Node data class NodeClusterItem( val node: Node, diff --git a/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt b/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt index 9e1da292c9..5b3c76c649 100644 --- a/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt +++ b/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt @@ -29,13 +29,12 @@ import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.launch import org.jetbrains.compose.resources.StringResource import org.meshtastic.core.common.util.nowSeconds -import org.meshtastic.core.data.repository.NodeRepository -import org.meshtastic.core.data.repository.PacketRepository -import org.meshtastic.core.database.entity.Packet import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.Node import org.meshtastic.core.model.util.TimeConstants import org.meshtastic.core.prefs.map.MapPrefs +import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.any import org.meshtastic.core.resources.eight_hours @@ -94,14 +93,14 @@ abstract class BaseMapViewModel( .map { nodes -> nodes.filter { node -> node.validPosition != null } } .stateInWhileSubscribed(initialValue = emptyList()) - val waypoints: StateFlow> = + val waypoints: StateFlow> = packetRepository .getWaypoints() .mapLatest { list -> list - .associateBy { packet -> packet.data.waypoint!!.id } + .associateBy { packet -> packet.waypoint!!.id } .filterValues { - val expire = it.data.waypoint!!.expire ?: 0 + val expire = it.waypoint?.expire ?: 0 expire == 0 || expire.toLong() > nowSeconds } } diff --git a/feature/map/src/main/kotlin/org/meshtastic/feature/map/node/NodeMapViewModel.kt b/feature/map/src/main/kotlin/org/meshtastic/feature/map/node/NodeMapViewModel.kt index 0fb5f6e18e..7a971417fa 100644 --- a/feature/map/src/main/kotlin/org/meshtastic/feature/map/node/NodeMapViewModel.kt +++ b/feature/map/src/main/kotlin/org/meshtastic/feature/map/node/NodeMapViewModel.kt @@ -29,10 +29,10 @@ import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.toList import org.meshtastic.core.common.BuildConfigProvider import org.meshtastic.core.data.repository.MeshLogRepository -import org.meshtastic.core.data.repository.NodeRepository import org.meshtastic.core.database.entity.MeshLog import org.meshtastic.core.navigation.NodesRoutes import org.meshtastic.core.prefs.map.MapPrefs +import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.ui.util.toPosition import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.feature.map.model.CustomTileSource diff --git a/feature/map/src/testGoogle/kotlin/org/meshtastic/feature/map/MapViewModelTest.kt b/feature/map/src/testGoogle/kotlin/org/meshtastic/feature/map/MapViewModelTest.kt index 10972edb3a..a4b77cf774 100644 --- a/feature/map/src/testGoogle/kotlin/org/meshtastic/feature/map/MapViewModelTest.kt +++ b/feature/map/src/testGoogle/kotlin/org/meshtastic/feature/map/MapViewModelTest.kt @@ -40,13 +40,13 @@ import org.junit.Test import org.junit.runner.RunWith import org.meshtastic.core.data.model.CustomTileProviderConfig import org.meshtastic.core.data.repository.CustomTileProviderRepository -import org.meshtastic.core.data.repository.NodeRepository -import org.meshtastic.core.data.repository.PacketRepository -import org.meshtastic.core.data.repository.RadioConfigRepository import org.meshtastic.core.datastore.UiPreferencesDataSource import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.prefs.map.GoogleMapsPrefs import org.meshtastic.core.prefs.map.MapPrefs +import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.PacketRepository +import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.service.ServiceRepository import org.robolectric.RobolectricTestRunner diff --git a/feature/messaging/src/androidTest/kotlin/org/meshtastic/feature/messaging/component/MessageItemTest.kt b/feature/messaging/src/androidTest/kotlin/org/meshtastic/feature/messaging/component/MessageItemTest.kt index bc772a2648..6abacade74 100644 --- a/feature/messaging/src/androidTest/kotlin/org/meshtastic/feature/messaging/component/MessageItemTest.kt +++ b/feature/messaging/src/androidTest/kotlin/org/meshtastic/feature/messaging/component/MessageItemTest.kt @@ -24,7 +24,7 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.meshtastic.core.common.util.nowMillis -import org.meshtastic.core.database.model.Message +import org.meshtastic.core.model.Message import org.meshtastic.core.model.MessageStatus import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageListPaged.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageListPaged.kt index e2c1ce6c99..ab317a6f35 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageListPaged.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageListPaged.kt @@ -61,11 +61,10 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.database.entity.Packet -import org.meshtastic.core.database.entity.Reaction import org.meshtastic.core.model.Message import org.meshtastic.core.model.MessageStatus import org.meshtastic.core.model.Node +import org.meshtastic.core.model.Reaction import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.new_messages_below import org.meshtastic.feature.messaging.component.MessageItem @@ -545,7 +544,7 @@ private fun MessageStatusDialog( remember(message.relayNode, nodes, ourNode) { derivedStateOf { message.relayNode?.let { relayNodeId -> - Packet.getRelayNode(relayNodeId, nodes, ourNode?.num)?.user?.long_name + Node.getRelayNode(relayNodeId, nodes, ourNode?.num)?.user?.long_name } } } diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageScreenEvent.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageScreenEvent.kt index ee69b3547a..83245fa9fb 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageScreenEvent.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageScreenEvent.kt @@ -17,7 +17,7 @@ package org.meshtastic.feature.messaging -import org.meshtastic.core.database.model.Node +import org.meshtastic.core.model.Node /** Defines the various user interactions that can occur on the MessageScreen. */ internal sealed interface MessageScreenEvent { diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt index e26583a910..2e999a8d61 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt @@ -32,19 +32,19 @@ import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import org.meshtastic.core.data.repository.NodeRepository -import org.meshtastic.core.data.repository.PacketRepository import org.meshtastic.core.data.repository.QuickChatActionRepository -import org.meshtastic.core.data.repository.RadioConfigRepository -import org.meshtastic.core.database.entity.ContactSettings +import org.meshtastic.core.model.ContactSettings import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.Message import org.meshtastic.core.model.Node import org.meshtastic.core.prefs.emoji.CustomEmojiPrefs import org.meshtastic.core.prefs.homoglyph.HomoglyphPrefs import org.meshtastic.core.prefs.ui.UiPrefs +import org.meshtastic.core.repository.MeshServiceNotifications +import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.PacketRepository +import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.repository.usecase.SendMessageUseCase -import org.meshtastic.core.service.MeshServiceNotifications import org.meshtastic.core.service.ServiceAction import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt index 8eb151620c..6dd60807e0 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt @@ -62,10 +62,10 @@ import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.common.util.nowMillis -import org.meshtastic.core.database.entity.Reaction import org.meshtastic.core.model.Message import org.meshtastic.core.model.MessageStatus import org.meshtastic.core.model.Node +import org.meshtastic.core.model.Reaction import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.filter_message_label import org.meshtastic.core.resources.message_delivery_status diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt index 8aa0727031..8055b97399 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt @@ -32,6 +32,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.AddReaction @@ -56,11 +57,10 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.database.entity.Packet -import org.meshtastic.core.database.entity.Reaction import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.MessageStatus import org.meshtastic.core.model.Node +import org.meshtastic.core.model.Reaction import org.meshtastic.core.model.getStringResFrom import org.meshtastic.core.model.util.getShortDateTime import org.meshtastic.core.resources.Res @@ -147,7 +147,9 @@ internal fun ReactionRow( AnimatedVisibility(emojiGroups.isNotEmpty(), modifier = modifier) { LazyRow(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) { - items(emojiGroups.entries.toList()) { (emoji, reactions) -> + items(emojiGroups.entries.toList()) { entry -> + val emoji = entry.key + val reactions = entry.value val localReaction = reactions.find { it.user.id == DataPacket.ID_LOCAL || it.user.id == myId } ReactionItem( emoji = emoji, @@ -217,7 +219,7 @@ internal fun ReactionDialog( val relayNodeName = reaction.relayNode?.let { relayNodeId -> - Packet.getRelayNode(relayNodeId, nodes, ourNode?.num)?.user?.long_name + Node.getRelayNode(relayNodeId, nodes, ourNode?.num)?.user?.long_name } DeliveryInfo( @@ -235,7 +237,9 @@ internal fun ReactionDialog( } LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) { - items(groupedEmojis.entries.toList()) { (emoji, reactions) -> + items(groupedEmojis.entries.toList()) { entry -> + val emoji = entry.key + val reactions = entry.value val localReaction = reactions.find { it.user.id == DataPacket.ID_LOCAL || it.user.id == myId } val isSending = localReaction?.status == MessageStatus.QUEUED || localReaction?.status == MessageStatus.ENROUTE diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorker.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorker.kt index 49d11fa105..078af260f3 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorker.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorker.kt @@ -22,10 +22,10 @@ import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import dagger.assisted.Assisted import dagger.assisted.AssistedInject -import org.meshtastic.core.data.repository.PacketRepository 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 @@ -47,12 +47,10 @@ constructor( return Result.retry() } - val packetEntity = + val packetData = packetRepository.getPacketByPacketId(packetId) ?: return Result.failure() // Packet no longer exists in DB? Do not retry. - val packetData = packetEntity.packet.data - return try { radioController.sendMessage(packetData) packetRepository.updateMessageStatus(packetData, MessageStatus.ENROUTE) diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/ui/contact/Contacts.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/ui/contact/Contacts.kt index b5b7016c8a..f256e23e20 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/ui/contact/Contacts.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/ui/contact/Contacts.kt @@ -65,8 +65,8 @@ import kotlinx.coroutines.launch import org.jetbrains.compose.resources.pluralStringResource import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.common.util.nowMillis -import org.meshtastic.core.database.entity.ContactSettings import org.meshtastic.core.model.Contact +import org.meshtastic.core.model.ContactSettings import org.meshtastic.core.model.util.TimeConstants import org.meshtastic.core.model.util.formatMuteRemainingTime import org.meshtastic.core.model.util.getChannel diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModel.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModel.kt index 0826fe7131..e9ab2a5b97 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModel.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModel.kt @@ -28,15 +28,14 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import org.meshtastic.core.data.repository.NodeRepository -import org.meshtastic.core.data.repository.PacketRepository -import org.meshtastic.core.data.repository.RadioConfigRepository -import org.meshtastic.core.database.entity.ContactSettings -import org.meshtastic.core.database.entity.MyNodeEntity -import org.meshtastic.core.database.entity.Packet import org.meshtastic.core.model.Contact +import org.meshtastic.core.model.ContactSettings import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.model.MyNodeInfo import org.meshtastic.core.model.util.getChannel +import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.PacketRepository +import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.ChannelSet @@ -59,7 +58,7 @@ constructor( val channels = radioConfigRepository.channelSetFlow.stateInWhileSubscribed(initialValue = ChannelSet()) // Combine node info and myId to reduce argument count in subsequent combines - private val identityFlow: Flow> = + private val identityFlow: Flow> = combine(nodeRepository.myNodeInfo, nodeRepository.myId) { info, id -> Pair(info, id) } /** @@ -78,42 +77,41 @@ constructor( settings, -> val (myNodeInfo, myId) = identity - val myNodeNum = myNodeInfo?.myNodeNum ?: return@combine emptyList() + val myNodeNum = myNodeInfo?.myNodeNum ?: return@combine emptyList() // Add empty channel placeholders (always show Broadcast contacts, even when empty) val placeholder = (0 until channelSet.settings.size).associate { ch -> val contactKey = "$ch${DataPacket.ID_BROADCAST}" val data = DataPacket(bytes = null, dataType = 1, time = 0L, channel = ch) - contactKey to Packet(0L, myNodeNum, 1, contactKey, 0L, true, data) + contactKey to data } - (contacts + (placeholder - contacts.keys)).values.collectionsMap { packet -> - val data = packet.data - val contactKey = packet.contact_key - + (contacts + (placeholder - contacts.keys)).entries.collectionsMap { entry -> + val contactKey = entry.key + val packetData = entry.value // Determine if this is my message (originated on this device) - val fromLocal = (data.from == DataPacket.ID_LOCAL || (myId != null && data.from == myId)) - val toBroadcast = data.to == DataPacket.ID_BROADCAST + val fromLocal = (packetData.from == DataPacket.ID_LOCAL || (myId != null && packetData.from == myId)) + val toBroadcast = packetData.to == DataPacket.ID_BROADCAST // grab usernames from NodeInfo - val userId = if (fromLocal) data.to else data.from + val userId = if (fromLocal) packetData.to else packetData.from val user = nodeRepository.getUser(userId ?: DataPacket.ID_BROADCAST) val node = nodeRepository.getNode(userId ?: DataPacket.ID_BROADCAST) val shortName = user.short_name val longName = if (toBroadcast) { - channelSet.getChannel(data.channel)?.name ?: "Channel ${data.channel}" + channelSet.getChannel(packetData.channel)?.name ?: "Channel ${packetData.channel}" } else { user.long_name } Contact( contactKey = contactKey, - shortName = if (toBroadcast) data.channel.toString() else shortName, + shortName = if (toBroadcast) packetData.channel.toString() else shortName, longName = longName, - lastMessageTime = if (data.time != 0L) data.time else null, - lastMessageText = if (fromLocal) data.text else "$shortName: ${data.text}", + lastMessageTime = if (packetData.time != 0L) packetData.time else null, + lastMessageText = if (fromLocal) packetData.text else "$shortName: ${packetData.text}", unreadCount = packetRepository.getUnreadCount(contactKey), messageCount = packetRepository.getMessageCount(contactKey), isMuted = settings[contactKey]?.isMuted == true, @@ -140,36 +138,37 @@ constructor( val myId = params.myId packetRepository.getContactsPaged().map { pagingData -> - pagingData.map { packet -> - val data = packet.data - val contactKey = packet.contact_key + pagingData.map { packetData: DataPacket -> + val contactKey = "${packetData.channel}${packetData.to}" // This might be wrong, need to check how contactKey is derived in PagingSource // Determine if this is my message (originated on this device) - val fromLocal = (data.from == DataPacket.ID_LOCAL || (myId != null && data.from == myId)) - val toBroadcast = data.to == DataPacket.ID_BROADCAST + val fromLocal = (packetData.from == DataPacket.ID_LOCAL || (myId != null && packetData.from == myId)) + val toBroadcast = packetData.to == DataPacket.ID_BROADCAST // grab usernames from NodeInfo - val userId = if (fromLocal) data.to else data.from + val userId = if (fromLocal) packetData.to else packetData.from val user = nodeRepository.getUser(userId ?: DataPacket.ID_BROADCAST) val node = nodeRepository.getNode(userId ?: DataPacket.ID_BROADCAST) val shortName = user.short_name val longName = if (toBroadcast) { - channelSet.getChannel(data.channel)?.name ?: "Channel ${data.channel}" + channelSet.getChannel(packetData.channel)?.name ?: "Channel ${packetData.channel}" } else { user.long_name } + val contactKeyComputed = if (toBroadcast) "${packetData.channel}${DataPacket.ID_BROADCAST}" else contactKey + Contact( - contactKey = contactKey, - shortName = if (toBroadcast) data.channel.toString() else shortName, + contactKey = contactKeyComputed, + shortName = if (toBroadcast) packetData.channel.toString() else shortName, longName = longName, - lastMessageTime = if (data.time != 0L) data.time else null, - lastMessageText = if (fromLocal) data.text else "$shortName: ${data.text}", - unreadCount = packetRepository.getUnreadCount(contactKey), - messageCount = packetRepository.getMessageCount(contactKey), - isMuted = settings[contactKey]?.isMuted == true, + lastMessageTime = if (packetData.time != 0L) packetData.time else null, + lastMessageText = if (fromLocal) packetData.text else "$shortName: ${packetData.text}", + unreadCount = packetRepository.getUnreadCount(contactKeyComputed), + messageCount = packetRepository.getMessageCount(contactKeyComputed), + isMuted = settings[contactKeyComputed]?.isMuted == true, isUnmessageable = user.is_unmessagable ?: false, nodeColors = if (!toBroadcast) { diff --git a/feature/messaging/src/test/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorkerTest.kt b/feature/messaging/src/test/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorkerTest.kt index 48abe99de2..72042dbf43 100644 --- a/feature/messaging/src/test/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorkerTest.kt +++ b/feature/messaging/src/test/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorkerTest.kt @@ -34,13 +34,13 @@ import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.meshtastic.core.data.repository.PacketRepository import org.meshtastic.core.database.entity.Packet import org.meshtastic.core.database.entity.PacketEntity import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.MessageStatus import org.meshtastic.core.model.RadioController +import org.meshtastic.core.repository.PacketRepository import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) diff --git a/feature/node/component/DeviceActions.kt b/feature/node/component/DeviceActions.kt index 39bb324c89..103558c7ef 100644 --- a/feature/node/component/DeviceActions.kt +++ b/feature/node/component/DeviceActions.kt @@ -55,7 +55,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.database.model.Node +import org.meshtastic.core.model.Node import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.actions import org.meshtastic.core.resources.direct_message diff --git a/feature/node/src/fdroid/kotlin/org/meshtastic/feature/node/component/InlineMap.kt b/feature/node/src/fdroid/kotlin/org/meshtastic/feature/node/component/InlineMap.kt index 27416ceb12..94fc796000 100644 --- a/feature/node/src/fdroid/kotlin/org/meshtastic/feature/node/component/InlineMap.kt +++ b/feature/node/src/fdroid/kotlin/org/meshtastic/feature/node/component/InlineMap.kt @@ -19,7 +19,7 @@ package org.meshtastic.feature.node.component import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import org.meshtastic.core.database.model.Node +import org.meshtastic.core.model.Node @Composable internal fun InlineMap(node: Node, modifier: Modifier = Modifier) { diff --git a/feature/node/src/google/kotlin/org/meshtastic/feature/node/component/InlineMap.kt b/feature/node/src/google/kotlin/org/meshtastic/feature/node/component/InlineMap.kt index bb4c0fbe86..cb94e313f5 100644 --- a/feature/node/src/google/kotlin/org/meshtastic/feature/node/component/InlineMap.kt +++ b/feature/node/src/google/kotlin/org/meshtastic/feature/node/component/InlineMap.kt @@ -31,7 +31,7 @@ import com.google.maps.android.compose.MapsComposeExperimentalApi import com.google.maps.android.compose.MarkerComposable import com.google.maps.android.compose.rememberCameraPositionState import com.google.maps.android.compose.rememberUpdatedMarkerState -import org.meshtastic.core.database.model.Node +import org.meshtastic.core.model.Node import org.meshtastic.core.ui.component.NodeChip import org.meshtastic.core.ui.component.precisionBitsToMeters diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/compass/CompassViewModel.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/compass/CompassViewModel.kt index fb1710ba24..3043ef4990 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/compass/CompassViewModel.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/compass/CompassViewModel.kt @@ -33,8 +33,8 @@ import org.meshtastic.core.common.util.bearing import org.meshtastic.core.common.util.latLongToMeter import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.common.util.nowSeconds -import org.meshtastic.core.database.model.Node import org.meshtastic.core.di.CoroutineDispatchers +import org.meshtastic.core.model.Node import org.meshtastic.core.model.util.toDistanceString import org.meshtastic.core.ui.component.precisionBitsToMeters import org.meshtastic.proto.Config diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt index 0fb96c836b..90f1e31cee 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt @@ -29,8 +29,8 @@ import androidx.compose.ui.graphics.Color import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.database.entity.asDeviceVersion -import org.meshtastic.core.database.model.Node import org.meshtastic.core.model.DeviceVersion +import org.meshtastic.core.model.Node import org.meshtastic.core.navigation.SettingsRoutes import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.administration diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/DeviceActions.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/DeviceActions.kt index b96ad7927d..db10ed1757 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/DeviceActions.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/DeviceActions.kt @@ -46,7 +46,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.database.model.Node +import org.meshtastic.core.model.Node import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.actions import org.meshtastic.core.resources.direct_message diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/EnvironmentMetrics.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/EnvironmentMetrics.kt index 05cfd5fc53..e7ac4effd3 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/EnvironmentMetrics.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/EnvironmentMetrics.kt @@ -35,7 +35,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.database.model.Node +import org.meshtastic.core.model.Node import org.meshtastic.core.model.util.UnitConversions import org.meshtastic.core.model.util.UnitConversions.toTempString import org.meshtastic.core.model.util.toSmallDistanceString diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/LinkedCoordinatesItem.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/LinkedCoordinatesItem.kt index f0a35b489e..35e226b23e 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/LinkedCoordinatesItem.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/LinkedCoordinatesItem.kt @@ -41,7 +41,7 @@ import co.touchlab.kermit.Logger import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.common.util.GPSFormat -import org.meshtastic.core.database.model.Node +import org.meshtastic.core.model.Node import org.meshtastic.core.model.util.metersIn import org.meshtastic.core.model.util.toString import org.meshtastic.core.resources.Res diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt index 1ccd6c2780..61480cee6c 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt @@ -55,8 +55,8 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.database.model.Node import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.model.Node import org.meshtastic.core.model.util.formatUptime import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.copy diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeFilterTextField.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeFilterTextField.kt index fa431c8988..1e8e21b4be 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeFilterTextField.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeFilterTextField.kt @@ -60,7 +60,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.database.model.NodeSortOption +import org.meshtastic.core.model.NodeSortOption import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.desc_node_filter_clear import org.meshtastic.core.resources.node_filter_exclude_infrastructure diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeMenuAction.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeMenuAction.kt index 6fa98374d3..b78dbdd299 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeMenuAction.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeMenuAction.kt @@ -16,7 +16,7 @@ */ package org.meshtastic.feature.node.component -import org.meshtastic.core.database.model.Node +import org.meshtastic.core.model.Node import org.meshtastic.core.model.TelemetryType sealed class NodeMenuAction { diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NotesSection.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NotesSection.kt index 257ed0566b..d8b99c9c73 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NotesSection.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NotesSection.kt @@ -43,7 +43,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.database.model.Node +import org.meshtastic.core.model.Node import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.add_a_note import org.meshtastic.core.resources.notes diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/PositionSection.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/PositionSection.kt index f4fe60fcc8..f4e3bb454b 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/PositionSection.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/PositionSection.kt @@ -46,7 +46,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.database.model.Node +import org.meshtastic.core.model.Node import org.meshtastic.core.model.util.toDistanceString import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.exchange_position diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/PowerMetrics.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/PowerMetrics.kt index 6927a7861c..ff361d8259 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/PowerMetrics.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/PowerMetrics.kt @@ -26,7 +26,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.database.model.Node +import org.meshtastic.core.model.Node import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.channel_1 import org.meshtastic.core.resources.channel_2 diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/TelemetricActionsSection.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/TelemetricActionsSection.kt index 0cee70ea87..d0955bf7f3 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/TelemetricActionsSection.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/TelemetricActionsSection.kt @@ -47,7 +47,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.database.model.Node +import org.meshtastic.core.model.Node import org.meshtastic.core.model.TelemetryType import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.logs diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeManagementActions.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeManagementActions.kt index a84617dae8..a341238f40 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeManagementActions.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeManagementActions.kt @@ -22,8 +22,8 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.jetbrains.compose.resources.getString -import org.meshtastic.core.data.repository.NodeRepository -import org.meshtastic.core.database.model.Node +import org.meshtastic.core.model.Node +import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.favorite import org.meshtastic.core.resources.favorite_add diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCase.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCase.kt index 1d11bad9b3..bf5b7e4f4b 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCase.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCase.kt @@ -18,9 +18,9 @@ package org.meshtastic.feature.node.domain.usecase import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import org.meshtastic.core.data.repository.NodeRepository -import org.meshtastic.core.database.model.Node -import org.meshtastic.core.database.model.NodeSortOption +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 diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/domain/usecase/GetNodeDetailsUseCase.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/domain/usecase/GetNodeDetailsUseCase.kt index 665dd1af6c..f5955c9f36 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/domain/usecase/GetNodeDetailsUseCase.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/domain/usecase/GetNodeDetailsUseCase.kt @@ -23,17 +23,17 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart -import org.meshtastic.core.data.repository.DeviceHardwareRepository import org.meshtastic.core.data.repository.FirmwareReleaseRepository import org.meshtastic.core.data.repository.MeshLogRepository -import org.meshtastic.core.data.repository.NodeRepository -import org.meshtastic.core.data.repository.RadioConfigRepository import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.database.entity.MeshLog -import org.meshtastic.core.database.model.Node import org.meshtastic.core.model.MyNodeInfo +import org.meshtastic.core.model.Node import org.meshtastic.core.model.util.hasValidEnvironmentMetrics import org.meshtastic.core.model.util.isDirectSignal +import org.meshtastic.core.repository.DeviceHardwareRepository +import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.UiText import org.meshtastic.core.resources.fallback_node_name @@ -110,7 +110,7 @@ constructor( nodeRepository.myNodeInfo, radioConfigRepository.deviceProfileFlow.onStart { emit(DeviceProfile()) }, ) { ourNode, myInfo, profile -> - IdentityGroup(ourNode, myInfo?.toMyNodeInfo(), profile) + IdentityGroup(ourNode, myInfo, profile) } // 3. Metadata & Request Timestamps diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeFilterPreferences.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeFilterPreferences.kt index 1fdd315664..89b36797f0 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeFilterPreferences.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeFilterPreferences.kt @@ -18,8 +18,8 @@ package org.meshtastic.feature.node.list import kotlinx.coroutines.flow.map -import org.meshtastic.core.database.model.NodeSortOption import org.meshtastic.core.datastore.UiPreferencesDataSource +import org.meshtastic.core.model.NodeSortOption import javax.inject.Inject class NodeFilterPreferences @Inject constructor(private val uiPreferencesDataSource: UiPreferencesDataSource) { diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt index f2a823296d..bdaa2a97a7 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt @@ -67,8 +67,8 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.database.model.Node import org.meshtastic.core.model.ConnectionState +import org.meshtastic.core.model.Node import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.add_favorite import org.meshtastic.core.resources.channel_invalid diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt index 06a9c114fd..b404d7262b 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt @@ -30,11 +30,11 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.launch -import org.meshtastic.core.data.repository.NodeRepository -import org.meshtastic.core.data.repository.RadioConfigRepository import org.meshtastic.core.model.Node import org.meshtastic.core.model.NodeSortOption import org.meshtastic.core.model.util.dispatchMeshtasticUri +import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.feature.node.detail.NodeManagementActions diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt index 0f3e2820b5..91a3b9f5a3 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt @@ -46,15 +46,15 @@ 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.MeshLogRepository -import org.meshtastic.core.data.repository.NodeRepository import org.meshtastic.core.data.repository.TracerouteSnapshotRepository import org.meshtastic.core.database.entity.MeshLog -import org.meshtastic.core.database.model.Node import org.meshtastic.core.di.CoroutineDispatchers +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.NodeRepository import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.okay import org.meshtastic.core.resources.traceroute diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/model/IsEffectivelyUnmessageable.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/model/IsEffectivelyUnmessageable.kt index 14484e5304..8bbe50716b 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/model/IsEffectivelyUnmessageable.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/model/IsEffectivelyUnmessageable.kt @@ -16,8 +16,8 @@ */ package org.meshtastic.feature.node.model -import org.meshtastic.core.database.model.Node -import org.meshtastic.core.database.model.isUnmessageableRole +import org.meshtastic.core.model.Node +import org.meshtastic.core.model.isUnmessageableRole val Node.isEffectivelyUnmessageable: Boolean get() = user.is_unmessagable ?: (user.role?.isUnmessageableRole() == true) diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/model/MetricsState.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/model/MetricsState.kt index 966aec158e..2833ada973 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/model/MetricsState.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/model/MetricsState.kt @@ -18,8 +18,8 @@ package org.meshtastic.feature.node.model import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.database.entity.MeshLog -import org.meshtastic.core.database.model.Node import org.meshtastic.core.model.DeviceHardware +import org.meshtastic.core.model.Node import org.meshtastic.proto.Config import org.meshtastic.proto.FirmwareEdition import org.meshtastic.proto.MeshPacket diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/model/NodeDetailAction.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/model/NodeDetailAction.kt index e74440f912..58009aa767 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/model/NodeDetailAction.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/model/NodeDetailAction.kt @@ -16,7 +16,7 @@ */ package org.meshtastic.feature.node.model -import org.meshtastic.core.database.model.Node +import org.meshtastic.core.model.Node import org.meshtastic.core.navigation.Route import org.meshtastic.core.service.ServiceAction import org.meshtastic.feature.node.component.NodeMenuAction diff --git a/feature/node/src/test/kotlin/org/meshtastic/feature/node/detail/NodeManagementActionsTest.kt b/feature/node/src/test/kotlin/org/meshtastic/feature/node/detail/NodeManagementActionsTest.kt index 243cec17fa..cb657999fd 100644 --- a/feature/node/src/test/kotlin/org/meshtastic/feature/node/detail/NodeManagementActionsTest.kt +++ b/feature/node/src/test/kotlin/org/meshtastic/feature/node/detail/NodeManagementActionsTest.kt @@ -23,8 +23,8 @@ import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Test -import org.meshtastic.core.data.repository.NodeRepository -import org.meshtastic.core.database.model.Node +import org.meshtastic.core.model.Node +import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.util.AlertManager import org.meshtastic.proto.User diff --git a/feature/node/src/test/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCaseTest.kt b/feature/node/src/test/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCaseTest.kt index 1ddfba0f37..8ab7cbf063 100644 --- a/feature/node/src/test/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCaseTest.kt +++ b/feature/node/src/test/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCaseTest.kt @@ -24,9 +24,9 @@ import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test -import org.meshtastic.core.data.repository.NodeRepository -import org.meshtastic.core.database.model.Node -import org.meshtastic.core.database.model.NodeSortOption +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.proto.Config import org.meshtastic.proto.User diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/AdministrationScreen.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/AdministrationScreen.kt index d5fbcc31f5..477f1b5b48 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/AdministrationScreen.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/AdministrationScreen.kt @@ -40,7 +40,7 @@ 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.database.model.Node +import org.meshtastic.core.model.Node import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.administration import org.meshtastic.core.resources.preserve_favorites diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt index c77794da81..ae8cf2710f 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt @@ -32,10 +32,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.meshtastic.core.common.BuildConfigProvider -import org.meshtastic.core.data.repository.NodeRepository -import org.meshtastic.core.data.repository.RadioConfigRepository import org.meshtastic.core.database.DatabaseManager -import org.meshtastic.core.database.entity.MyNodeEntity import org.meshtastic.core.domain.usecase.settings.ExportDataUseCase import org.meshtastic.core.domain.usecase.settings.IsOtaCapableUseCase import org.meshtastic.core.domain.usecase.settings.MeshLocationUseCase @@ -44,10 +41,13 @@ 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.MyNodeInfo import org.meshtastic.core.model.Node import org.meshtastic.core.model.RadioController import org.meshtastic.core.prefs.meshlog.MeshLogPrefs import org.meshtastic.core.prefs.ui.UiPrefs +import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.LocalConfig import java.io.BufferedWriter @@ -77,7 +77,7 @@ constructor( private val exportDataUseCase: ExportDataUseCase, private val isOtaCapableUseCase: IsOtaCapableUseCase, ) : ViewModel() { - val myNodeInfo: StateFlow = nodeRepository.myNodeInfo + val myNodeInfo: StateFlow = nodeRepository.myNodeInfo val myNodeNum get() = myNodeInfo.value?.myNodeNum diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt index 161134b163..c58f342326 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt @@ -37,13 +37,13 @@ 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.data.repository.MeshLogRepository -import org.meshtastic.core.data.repository.NodeRepository import org.meshtastic.core.database.entity.MeshLog import org.meshtastic.core.database.entity.Packet import org.meshtastic.core.model.getTracerouteResponse import org.meshtastic.core.model.util.decodeOrNull import org.meshtastic.core.model.util.toReadableString import org.meshtastic.core.prefs.meshlog.MeshLogPrefs +import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.debug_clear import org.meshtastic.core.resources.debug_clear_logs_confirm diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseScreen.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseScreen.kt index db9cd8fd5a..daa04a79d0 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseScreen.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseScreen.kt @@ -40,7 +40,7 @@ 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.database.model.Node +import org.meshtastic.core.model.Node import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.clean_node_database_description import org.meshtastic.core.resources.clean_node_database_title diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt index bc61b70c4a..c55f26f60b 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt @@ -43,11 +43,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.compose.resources.StringResource import org.meshtastic.core.data.repository.LocationRepository -import org.meshtastic.core.data.repository.NodeRepository -import org.meshtastic.core.data.repository.PacketRepository -import org.meshtastic.core.data.repository.RadioConfigRepository -import org.meshtastic.core.database.entity.MyNodeEntity -import org.meshtastic.core.database.model.Node import org.meshtastic.core.domain.usecase.settings.AdminActionsUseCase import org.meshtastic.core.domain.usecase.settings.ExportProfileUseCase import org.meshtastic.core.domain.usecase.settings.ExportSecurityConfigUseCase @@ -59,11 +54,16 @@ import org.meshtastic.core.domain.usecase.settings.RadioResponseResult import org.meshtastic.core.domain.usecase.settings.ToggleAnalyticsUseCase import org.meshtastic.core.domain.usecase.settings.ToggleHomoglyphEncodingUseCase import org.meshtastic.core.model.ConnectionState +import org.meshtastic.core.model.MyNodeInfo +import org.meshtastic.core.model.Node import org.meshtastic.core.model.Position import org.meshtastic.core.navigation.SettingsRoutes import org.meshtastic.core.prefs.analytics.AnalyticsPrefs import org.meshtastic.core.prefs.homoglyph.HomoglyphPrefs import org.meshtastic.core.prefs.map.MapConsentPrefs +import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.PacketRepository +import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.UiText import org.meshtastic.core.resources.cant_shutdown @@ -217,7 +217,7 @@ constructor( Logger.d { "RadioConfigViewModel created" } } - private val myNodeInfo: StateFlow + private val myNodeInfo: StateFlow get() = nodeRepository.myNodeInfo val myNodeNum diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/ShutdownConfirmationDialog.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/ShutdownConfirmationDialog.kt index 2176b32be1..92e4e84a71 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/ShutdownConfirmationDialog.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/ShutdownConfirmationDialog.kt @@ -33,7 +33,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.database.model.Node +import org.meshtastic.core.model.Node import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.cancel import org.meshtastic.core.resources.send diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/TAKConfigItemList.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/TAKConfigItemList.kt index 94b17c645d..7da9f7b3cd 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/TAKConfigItemList.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/TAKConfigItemList.kt @@ -24,8 +24,8 @@ 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.database.model.getColorFrom -import org.meshtastic.core.database.model.getStringResFrom +import org.meshtastic.core.model.getColorFrom +import org.meshtastic.core.model.getStringResFrom import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.tak import org.meshtastic.core.resources.tak_config diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/UserConfigItemList.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/UserConfigItemList.kt index 2a7c673e2d..55ae3ab755 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/UserConfigItemList.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/UserConfigItemList.kt @@ -29,8 +29,8 @@ 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.database.model.isUnmessageableRole import org.meshtastic.core.model.Capabilities +import org.meshtastic.core.model.isUnmessageableRole import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.hardware_model import org.meshtastic.core.resources.licensed_amateur_radio diff --git a/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/SettingsViewModelTest.kt b/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/SettingsViewModelTest.kt index 9879d8903c..5d5a9d4b87 100644 --- a/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/SettingsViewModelTest.kt +++ b/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/SettingsViewModelTest.kt @@ -30,8 +30,6 @@ import org.junit.After import org.junit.Before import org.junit.Test import org.meshtastic.core.common.BuildConfigProvider -import org.meshtastic.core.data.repository.NodeRepository -import org.meshtastic.core.data.repository.RadioConfigRepository import org.meshtastic.core.database.DatabaseManager import org.meshtastic.core.domain.usecase.settings.ExportDataUseCase import org.meshtastic.core.domain.usecase.settings.IsOtaCapableUseCase @@ -44,6 +42,8 @@ import org.meshtastic.core.domain.usecase.settings.SetThemeUseCase import org.meshtastic.core.model.RadioController import org.meshtastic.core.prefs.meshlog.MeshLogPrefs import org.meshtastic.core.prefs.ui.UiPrefs +import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.RadioConfigRepository @OptIn(ExperimentalCoroutinesApi::class) class SettingsViewModelTest { diff --git a/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModelTest.kt b/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModelTest.kt index b7a256bf4f..101cce4feb 100644 --- a/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModelTest.kt +++ b/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModelTest.kt @@ -33,8 +33,8 @@ import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import org.meshtastic.core.data.repository.MeshLogRepository -import org.meshtastic.core.data.repository.NodeRepository import org.meshtastic.core.prefs.meshlog.MeshLogPrefs +import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.ui.util.AlertManager @OptIn(ExperimentalCoroutinesApi::class) diff --git a/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseViewModelTest.kt b/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseViewModelTest.kt index 07beee89d2..23425895d8 100644 --- a/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseViewModelTest.kt +++ b/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseViewModelTest.kt @@ -30,8 +30,8 @@ import org.junit.After import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test -import org.meshtastic.core.database.model.Node import org.meshtastic.core.domain.usecase.settings.CleanNodeDatabaseUseCase +import org.meshtastic.core.model.Node import org.meshtastic.core.ui.util.AlertManager @OptIn(ExperimentalCoroutinesApi::class) diff --git a/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt b/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt index cc45c7075b..717d190b2f 100644 --- a/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt +++ b/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt @@ -34,10 +34,6 @@ import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import org.meshtastic.core.data.repository.LocationRepository -import org.meshtastic.core.data.repository.NodeRepository -import org.meshtastic.core.data.repository.PacketRepository -import org.meshtastic.core.data.repository.RadioConfigRepository -import org.meshtastic.core.database.model.Node import org.meshtastic.core.domain.usecase.settings.AdminActionsUseCase import org.meshtastic.core.domain.usecase.settings.ExportProfileUseCase import org.meshtastic.core.domain.usecase.settings.ExportSecurityConfigUseCase @@ -48,9 +44,13 @@ import org.meshtastic.core.domain.usecase.settings.RadioConfigUseCase import org.meshtastic.core.domain.usecase.settings.RadioResponseResult import org.meshtastic.core.domain.usecase.settings.ToggleAnalyticsUseCase import org.meshtastic.core.domain.usecase.settings.ToggleHomoglyphEncodingUseCase +import org.meshtastic.core.model.Node import org.meshtastic.core.prefs.analytics.AnalyticsPrefs import org.meshtastic.core.prefs.homoglyph.HomoglyphPrefs import org.meshtastic.core.prefs.map.MapConsentPrefs +import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.PacketRepository +import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.service.ServiceRepository import org.meshtastic.proto.ChannelSet import org.meshtastic.proto.ChannelSettings From 56b482875cd8336db203cb22e51c5c29e50db31d Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 2 Mar 2026 13:17:42 -0600 Subject: [PATCH 03/20] refactor: migrate history management and manager logic to core modules - Introduce `HistoryManager` interface in `core:repository` and its implementation in `core:data`. - Move `NodeManager`, `CommandSender`, and `HistoryManager` unit tests from the `app` module to `core:data`. - Update `MeshConnectionManager` and `MeshDataHandler` to use the new `HistoryManager` interface instead of the concrete implementation. - Refactor unit tests and fakes to use domain models (`Node`, `DataPacket`) instead of database entities. - Expose `getOrCreateNode` in `NodeManagerImpl` and `resolveNodeNum` in `CommandSenderImpl` for improved testability. - Update DI module to bind the new `HistoryManager` implementation. Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .../mesh/service/MeshConnectionManager.kt | 3 +- .../mesh/service/MeshDataHandler.kt | 3 +- .../java/com/geeksville/mesh/service/Fakes.kt | 8 +- .../mesh/service/MeshConnectionManagerTest.kt | 11 ++- .../mesh/service/MeshDataHandlerTest.kt | 5 +- .../mesh/service/MeshDataMapperTest.kt | 1 + .../mesh/service/MeshMessageProcessorTest.kt | 1 + .../core/data/di/RepositoryModule.kt | 8 ++ .../core/data/manager/CommandSenderImpl.kt | 2 +- .../core/data/manager/HistoryManagerImpl.kt | 75 ++++++++----------- .../core/data/manager/NodeManagerImpl.kt | 2 +- .../data/manager/CommandSenderHopLimitTest.kt | 25 ++++--- .../data/manager/CommandSenderImplTest.kt | 20 ++--- .../data/manager/HistoryManagerImplTest.kt | 32 ++++---- .../core/data/manager/NodeManagerImplTest.kt | 21 +++--- .../DeviceHardwareRepositoryTest.kt | 2 +- .../data/repository/NodeRepositoryTest.kt | 6 +- .../core/repository/HistoryManager.kt | 48 ++++++++++++ .../domain/worker/SendMessageWorkerTest.kt | 17 ++--- 19 files changed, 172 insertions(+), 118 deletions(-) rename app/src/main/java/com/geeksville/mesh/service/MeshHistoryManager.kt => core/data/src/main/kotlin/org/meshtastic/core/data/manager/HistoryManagerImpl.kt (64%) rename app/src/test/java/com/geeksville/mesh/service/MeshCommandSenderHopLimitTest.kt => core/data/src/test/kotlin/org/meshtastic/core/data/manager/CommandSenderHopLimitTest.kt (82%) rename app/src/test/java/com/geeksville/mesh/service/MeshCommandSenderTest.kt => core/data/src/test/kotlin/org/meshtastic/core/data/manager/CommandSenderImplTest.kt (78%) rename app/src/test/java/com/geeksville/mesh/service/StoreForwardHistoryRequestTest.kt => core/data/src/test/kotlin/org/meshtastic/core/data/manager/HistoryManagerImplTest.kt (73%) rename app/src/test/java/com/geeksville/mesh/service/MeshNodeManagerTest.kt => core/data/src/test/kotlin/org/meshtastic/core/data/manager/NodeManagerImplTest.kt (86%) create mode 100644 core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/HistoryManager.kt diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt b/app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt index bca85bae0c..3345175b98 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt @@ -47,6 +47,7 @@ import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.TelemetryType import org.meshtastic.core.prefs.ui.UiPrefs import org.meshtastic.core.repository.CommandSender +import org.meshtastic.core.repository.HistoryManager import org.meshtastic.core.repository.MeshServiceNotifications import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.NodeRepository @@ -87,7 +88,7 @@ constructor( private val nodeRepository: NodeRepository, private val locationManager: MeshLocationManager, private val mqttManager: MeshMqttManager, - private val historyManager: MeshHistoryManager, + private val historyManager: HistoryManager, private val radioConfigRepository: RadioConfigRepository, private val commandSender: CommandSender, private val nodeManager: NodeManager, diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt b/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt index ae3eb5efd1..177babac2c 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt @@ -42,6 +42,7 @@ import org.meshtastic.core.model.util.decodeOrNull import org.meshtastic.core.model.util.toOneLiner import org.meshtastic.core.prefs.mesh.MeshPrefs import org.meshtastic.core.repository.CommandSender +import org.meshtastic.core.repository.HistoryManager import org.meshtastic.core.repository.MeshServiceNotifications import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.PacketHandler @@ -90,7 +91,7 @@ constructor( private val configHandler: MeshConfigHandler, private val configFlowManager: MeshConfigFlowManager, private val commandSender: CommandSender, - private val historyManager: MeshHistoryManager, + private val historyManager: HistoryManager, private val meshPrefs: MeshPrefs, private val connectionManager: MeshConnectionManager, private val tracerouteHandler: MeshTracerouteHandler, diff --git a/app/src/test/java/com/geeksville/mesh/service/Fakes.kt b/app/src/test/java/com/geeksville/mesh/service/Fakes.kt index ac5b2e81dd..728adc5f91 100644 --- a/app/src/test/java/com/geeksville/mesh/service/Fakes.kt +++ b/app/src/test/java/com/geeksville/mesh/service/Fakes.kt @@ -19,7 +19,7 @@ package com.geeksville.mesh.service import android.app.Notification import com.geeksville.mesh.repository.radio.RadioInterfaceService import io.mockk.mockk -import org.meshtastic.core.database.entity.NodeEntity +import org.meshtastic.core.model.Node import org.meshtastic.core.repository.MeshServiceNotifications import org.meshtastic.proto.ClientNotification import org.meshtastic.proto.Telemetry @@ -64,15 +64,15 @@ class FakeMeshServiceNotifications : MeshServiceNotifications { override fun showAlertNotification(contactKey: String, name: String, alert: String) {} - override fun showNewNodeSeenNotification(node: NodeEntity) {} + override fun showNewNodeSeenNotification(node: Node) {} - override fun showOrUpdateLowBatteryNotification(node: NodeEntity, isRemote: Boolean) {} + override fun showOrUpdateLowBatteryNotification(node: Node, isRemote: Boolean) {} override fun showClientNotification(clientNotification: ClientNotification) {} override fun cancelMessageNotification(contactKey: String) {} - override fun cancelLowBatteryNotification(node: NodeEntity) {} + override fun cancelLowBatteryNotification(node: Node) {} override fun clearClientNotification(notification: ClientNotification) {} } diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshConnectionManagerTest.kt b/app/src/test/java/com/geeksville/mesh/service/MeshConnectionManagerTest.kt index c7b269261a..84e68d53f1 100644 --- a/app/src/test/java/com/geeksville/mesh/service/MeshConnectionManagerTest.kt +++ b/app/src/test/java/com/geeksville/mesh/service/MeshConnectionManagerTest.kt @@ -16,6 +16,7 @@ */ package com.geeksville.mesh.service +import android.app.Notification import android.content.Context import androidx.glance.appwidget.GlanceAppWidget import androidx.glance.appwidget.updateAll @@ -39,12 +40,15 @@ import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import org.meshtastic.core.analytics.platform.PlatformAnalytics -import org.meshtastic.core.database.entity.MyNodeEntity import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.model.MyNodeInfo import org.meshtastic.core.model.Node import org.meshtastic.core.prefs.ui.UiPrefs +import org.meshtastic.core.repository.CommandSender +import org.meshtastic.core.repository.HistoryManager import org.meshtastic.core.repository.MeshServiceNotifications +import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.RadioConfigRepository @@ -68,7 +72,7 @@ class MeshConnectionManagerTest { private val nodeRepository: NodeRepository = mockk(relaxed = true) private val locationManager: MeshLocationManager = mockk(relaxed = true) private val mqttManager: MeshMqttManager = mockk(relaxed = true) - private val historyManager: MeshHistoryManager = mockk(relaxed = true) + private val historyManager: HistoryManager = mockk(relaxed = true) private val radioConfigRepository: RadioConfigRepository = mockk(relaxed = true) private val commandSender: CommandSender = mockk(relaxed = true) private val nodeManager: NodeManager = mockk(relaxed = true) @@ -94,9 +98,10 @@ class MeshConnectionManagerTest { every { radioInterfaceService.connectionState } returns radioConnectionState every { radioConfigRepository.localConfigFlow } returns localConfigFlow every { radioConfigRepository.moduleConfigFlow } returns moduleConfigFlow - every { nodeRepository.myNodeInfo } returns MutableStateFlow(null) + every { nodeRepository.myNodeInfo } returns MutableStateFlow(null) every { nodeRepository.ourNodeInfo } returns MutableStateFlow(null) every { nodeRepository.localStats } returns MutableStateFlow(LocalStats()) + every { serviceNotifications.updateServiceStateNotification(any(), any()) } returns mockk(relaxed = true) manager = MeshConnectionManager( diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshDataHandlerTest.kt b/app/src/test/java/com/geeksville/mesh/service/MeshDataHandlerTest.kt index 4830cf45ef..0c06ac91ee 100644 --- a/app/src/test/java/com/geeksville/mesh/service/MeshDataHandlerTest.kt +++ b/app/src/test/java/com/geeksville/mesh/service/MeshDataHandlerTest.kt @@ -32,7 +32,10 @@ import org.meshtastic.core.analytics.platform.PlatformAnalytics import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.MessageStatus import org.meshtastic.core.prefs.mesh.MeshPrefs +import org.meshtastic.core.repository.CommandSender +import org.meshtastic.core.repository.HistoryManager import org.meshtastic.core.repository.MeshServiceNotifications +import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.service.ServiceRepository @@ -56,7 +59,7 @@ class MeshDataHandlerTest { private val configHandler: MeshConfigHandler = mockk(relaxed = true) private val configFlowManager: MeshConfigFlowManager = mockk(relaxed = true) private val commandSender: CommandSender = mockk(relaxed = true) - private val historyManager: MeshHistoryManager = mockk(relaxed = true) + private val historyManager: HistoryManager = mockk(relaxed = true) private val meshPrefs: MeshPrefs = mockk(relaxed = true) private val connectionManager: MeshConnectionManager = mockk(relaxed = true) private val tracerouteHandler: MeshTracerouteHandler = mockk(relaxed = true) diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshDataMapperTest.kt b/app/src/test/java/com/geeksville/mesh/service/MeshDataMapperTest.kt index 08925f7e58..0c6f5aaad5 100644 --- a/app/src/test/java/com/geeksville/mesh/service/MeshDataMapperTest.kt +++ b/app/src/test/java/com/geeksville/mesh/service/MeshDataMapperTest.kt @@ -25,6 +25,7 @@ import org.junit.Assert.assertNull import org.junit.Before import org.junit.Test import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.repository.NodeManager import org.meshtastic.proto.Data import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.PortNum diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshMessageProcessorTest.kt b/app/src/test/java/com/geeksville/mesh/service/MeshMessageProcessorTest.kt index c43be456ee..0ede2ab56d 100644 --- a/app/src/test/java/com/geeksville/mesh/service/MeshMessageProcessorTest.kt +++ b/app/src/test/java/com/geeksville/mesh/service/MeshMessageProcessorTest.kt @@ -28,6 +28,7 @@ import org.junit.Before import org.junit.Test import org.meshtastic.core.data.repository.MeshLogRepository import org.meshtastic.core.database.entity.MeshLog +import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.service.ServiceRepository import org.meshtastic.proto.Data import org.meshtastic.proto.MeshPacket diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt index 0d27698938..2d24db0af2 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt @@ -22,6 +22,7 @@ import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import org.meshtastic.core.data.manager.CommandSenderImpl +import org.meshtastic.core.data.manager.HistoryManagerImpl import org.meshtastic.core.data.manager.NodeManagerImpl import org.meshtastic.core.data.repository.DeviceHardwareRepositoryImpl import org.meshtastic.core.data.repository.NodeRepositoryImpl @@ -29,6 +30,7 @@ import org.meshtastic.core.data.repository.PacketRepositoryImpl import org.meshtastic.core.data.repository.RadioConfigRepositoryImpl import org.meshtastic.core.repository.CommandSender import org.meshtastic.core.repository.DeviceHardwareRepository +import org.meshtastic.core.repository.HistoryManager import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.PacketRepository @@ -74,4 +76,10 @@ abstract class RepositoryModule { abstract fun bindCommandSender( commandSenderImpl: CommandSenderImpl ): CommandSender + + @Binds + @Singleton + abstract fun bindHistoryManager( + historyManagerImpl: HistoryManagerImpl + ): HistoryManager } diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt index b91bc60b59..33d668a2d1 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt @@ -364,7 +364,7 @@ constructor( } } - private fun resolveNodeNum(toId: String): Int = when (toId) { + fun resolveNodeNum(toId: String): Int = when (toId) { DataPacket.ID_BROADCAST -> DataPacket.NODENUM_BROADCAST else -> { val numericNum = diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshHistoryManager.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/HistoryManagerImpl.kt similarity index 64% rename from app/src/main/java/com/geeksville/mesh/service/MeshHistoryManager.kt rename to core/data/src/main/kotlin/org/meshtastic/core/data/manager/HistoryManagerImpl.kt index bad5dd2175..f3ff53e4e1 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshHistoryManager.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/HistoryManagerImpl.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 @@ -14,15 +14,12 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.geeksville.mesh.service +package org.meshtastic.core.data.manager -import android.util.Log -import androidx.annotation.VisibleForTesting import co.touchlab.kermit.Logger -import com.geeksville.mesh.BuildConfig -import com.geeksville.mesh.ui.connections.NO_DEVICE_SELECTED import okio.ByteString.Companion.toByteString import org.meshtastic.core.prefs.mesh.MeshPrefs +import org.meshtastic.core.repository.HistoryManager import org.meshtastic.core.repository.PacketHandler import org.meshtastic.proto.Data import org.meshtastic.proto.MeshPacket @@ -33,58 +30,47 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class MeshHistoryManager -@Inject -constructor( +class HistoryManagerImpl @Inject constructor( private val meshPrefs: MeshPrefs, private val packetHandler: PacketHandler, -) { +) : HistoryManager { + companion object { private const val HISTORY_TAG = "HistoryReplay" private const val DEFAULT_HISTORY_RETURN_WINDOW_MINUTES = 60 * 24 private const val DEFAULT_HISTORY_RETURN_MAX_MESSAGES = 100 + private const val NO_DEVICE_SELECTED = "No device selected" - @VisibleForTesting - internal fun buildStoreForwardHistoryRequest( + fun buildStoreForwardHistoryRequest( lastRequest: Int, historyReturnWindow: Int, historyReturnMax: Int, ): StoreAndForward { - val history = - StoreAndForward.History( - last_request = lastRequest.coerceAtLeast(0), - window = historyReturnWindow.coerceAtLeast(0), - history_messages = historyReturnMax.coerceAtLeast(0), - ) + val history = StoreAndForward.History( + last_request = lastRequest.coerceAtLeast(0), + window = historyReturnWindow.coerceAtLeast(0), + history_messages = historyReturnMax.coerceAtLeast(0), + ) return StoreAndForward(rr = StoreAndForward.RequestResponse.CLIENT_HISTORY, history = history) } - @VisibleForTesting - internal fun resolveHistoryRequestParameters(window: Int, max: Int): Pair { + fun resolveHistoryRequestParameters(window: Int, max: Int): Pair { val resolvedWindow = if (window > 0) window else DEFAULT_HISTORY_RETURN_WINDOW_MINUTES val resolvedMax = if (max > 0) max else DEFAULT_HISTORY_RETURN_MAX_MESSAGES return resolvedWindow to resolvedMax } } - private fun historyLog(priority: Int = Log.INFO, throwable: Throwable? = null, message: () -> String) { - if (!BuildConfig.DEBUG) return - val logger = Logger.withTag(HISTORY_TAG) - val msg = message() - when (priority) { - Log.VERBOSE -> logger.v(throwable) { msg } - Log.DEBUG -> logger.d(throwable) { msg } - Log.INFO -> logger.i(throwable) { msg } - Log.WARN -> logger.w(throwable) { msg } - Log.ERROR -> logger.e(throwable) { msg } - else -> logger.i(throwable) { msg } - } + private val logger = Logger.withTag(HISTORY_TAG) + + private fun historyLog(message: String, throwable: Throwable? = null) { + logger.i(throwable) { message } } private fun activeDeviceAddress(): String? = meshPrefs.deviceAddress?.takeIf { !it.equals(NO_DEVICE_SELECTED, ignoreCase = true) && it.isNotBlank() } - fun requestHistoryReplay( + override fun requestHistoryReplay( trigger: String, myNodeNum: Int?, storeForwardConfig: ModuleConfig.StoreForwardConfig?, @@ -93,23 +79,22 @@ constructor( val address = activeDeviceAddress() if (address == null || myNodeNum == null) { val reason = if (address == null) "no_addr" else "no_my_node" - historyLog { "requestHistory skipped trigger=$trigger reason=$reason" } + historyLog("requestHistory skipped trigger=$trigger reason=$reason") return } val lastRequest = meshPrefs.getStoreForwardLastRequest(address) - val (window, max) = - resolveHistoryRequestParameters( - storeForwardConfig?.history_return_window ?: 0, - storeForwardConfig?.history_return_max ?: 0, - ) + val (window, max) = resolveHistoryRequestParameters( + storeForwardConfig?.history_return_window ?: 0, + storeForwardConfig?.history_return_max ?: 0, + ) val request = buildStoreForwardHistoryRequest(lastRequest, window, max) - historyLog { + historyLog( "requestHistory trigger=$trigger transport=$transport addr=$address " + "lastRequest=$lastRequest window=$window max=$max" - } + ) runCatching { packetHandler.sendToRadio( @@ -121,19 +106,19 @@ constructor( ), ) } - .onFailure { ex -> historyLog(Log.WARN, ex) { "requestHistory failed" } } + .onFailure { ex -> logger.w(ex) { "requestHistory failed" } } } - fun updateStoreForwardLastRequest(source: String, lastRequest: Int, transport: String) { + override fun updateStoreForwardLastRequest(source: String, lastRequest: Int, transport: String) { if (lastRequest <= 0) return val address = activeDeviceAddress() ?: return val current = meshPrefs.getStoreForwardLastRequest(address) if (lastRequest != current) { meshPrefs.setStoreForwardLastRequest(address, lastRequest) - historyLog { + historyLog( "historyMarker updated source=$source transport=$transport " + "addr=$address from=$current to=$lastRequest" - } + ) } } } diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt index 8416ae8bb5..f8c5ed68c9 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt @@ -132,7 +132,7 @@ constructor( nodeDBbyNodeNum.remove(nodeNum)?.let { nodeDBbyID.remove(it.user.id) } } - private fun getOrCreateNode(n: Int, channel: Int = 0): Node = nodeDBbyNodeNum.getOrPut(n) { + fun getOrCreateNode(n: Int, channel: Int = 0): Node = nodeDBbyNodeNum.getOrPut(n) { val userId = DataPacket.nodeNumToDefaultId(n) val defaultUser = User( diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshCommandSenderHopLimitTest.kt b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/CommandSenderHopLimitTest.kt similarity index 82% rename from app/src/test/java/com/geeksville/mesh/service/MeshCommandSenderHopLimitTest.kt rename to core/data/src/test/kotlin/org/meshtastic/core/data/manager/CommandSenderHopLimitTest.kt index 3e938790c9..679729176b 100644 --- a/app/src/test/java/com/geeksville/mesh/service/MeshCommandSenderHopLimitTest.kt +++ b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/CommandSenderHopLimitTest.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 com.geeksville.mesh.service +package org.meshtastic.core.data.manager import io.mockk.every import io.mockk.mockk @@ -29,18 +29,21 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test -import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.model.Node +import org.meshtastic.core.repository.CommandSender +import org.meshtastic.core.repository.NodeManager +import org.meshtastic.core.repository.PacketHandler import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.proto.Config import org.meshtastic.proto.LocalConfig import org.meshtastic.proto.MeshPacket +import org.meshtastic.proto.User class CommandSenderHopLimitTest { private val packetHandler: PacketHandler = mockk(relaxed = true) - private val nodeManager = NodeManagerImpl() - private val connectionStateHolder: ConnectionStateHandler = mockk(relaxed = true) + private val nodeManager: NodeManager = mockk(relaxed = true) private val radioConfigRepository: RadioConfigRepository = mockk(relaxed = true) private val localConfigFlow = MutableStateFlow(LocalConfig()) @@ -51,13 +54,14 @@ class CommandSenderHopLimitTest { @Before fun setUp() { - val connectedFlow = MutableStateFlow(ConnectionState.Connected) - every { connectionStateHolder.connectionState } returns connectedFlow + val myNum = 123 + val myNode = Node(num = myNum, user = User(id = "!id", long_name = "long", short_name = "shrt")) every { radioConfigRepository.localConfigFlow } returns localConfigFlow + every { nodeManager.myNodeNum } returns myNum + every { nodeManager.nodeDBbyNodeNum } returns mapOf(myNum to myNode) - commandSender = CommandSenderImpl(packetHandler, nodeManager, connectionStateHolder, radioConfigRepository) + commandSender = CommandSenderImpl(packetHandler, nodeManager, radioConfigRepository) commandSender.start(testScope) - nodeManager.myNodeNum = 123 } @Test @@ -111,7 +115,10 @@ class CommandSenderHopLimitTest { localConfigFlow.value = LocalConfig(lora = Config.LoRaConfig(hop_limit = 6)) // Mock node manager interactions - nodeManager.nodeDBbyNodeNum.remove(destNum) + // Note: we need to keep myNode in the map for requestUserInfo to not return early + val myNum = 123 + val myNode = Node(num = myNum, user = User(id = "!id", long_name = "long", short_name = "shrt")) + every { nodeManager.nodeDBbyNodeNum } returns mapOf(myNum to myNode) commandSender.requestUserInfo(destNum) diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshCommandSenderTest.kt b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/CommandSenderImplTest.kt similarity index 78% rename from app/src/test/java/com/geeksville/mesh/service/MeshCommandSenderTest.kt rename to core/data/src/test/kotlin/org/meshtastic/core/data/manager/CommandSenderImplTest.kt index 4b12c3d80c..69996dde9b 100644 --- a/app/src/test/java/com/geeksville/mesh/service/MeshCommandSenderTest.kt +++ b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/CommandSenderImplTest.kt @@ -14,25 +14,28 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.geeksville.mesh.service +package org.meshtastic.core.data.manager +import io.mockk.every +import io.mockk.mockk import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals import org.junit.Before import org.junit.Test -import org.meshtastic.core.database.entity.NodeEntity import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.model.Node +import org.meshtastic.core.repository.NodeManager import org.meshtastic.proto.User -class CommandSenderTest { +class CommandSenderImplTest { - private lateinit var commandSender: CommandSender + private lateinit var commandSender: CommandSenderImpl private lateinit var nodeManager: NodeManager @Before fun setUp() { - nodeManager = NodeManagerImpl() - commandSender = CommandSenderImpl(null, nodeManager, null, null) + nodeManager = mockk(relaxed = true) + commandSender = CommandSenderImpl(mockk(relaxed = true), nodeManager, mockk(relaxed = true)) } @Test @@ -60,9 +63,8 @@ class CommandSenderTest { fun `resolveNodeNum handles custom node ID from database`() { val nodeNum = 456 val userId = "custom_id" - val entity = NodeEntity(num = nodeNum, user = User(id = userId)) - nodeManager.nodeDBbyNodeNum[nodeNum] = entity - nodeManager.nodeDBbyID[userId] = entity + val node = Node(num = nodeNum, user = User(id = userId)) + every { nodeManager.nodeDBbyID } returns mapOf(userId to node) assertEquals(nodeNum, commandSender.resolveNodeNum(userId)) } diff --git a/app/src/test/java/com/geeksville/mesh/service/StoreForwardHistoryRequestTest.kt b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/HistoryManagerImplTest.kt similarity index 73% rename from app/src/test/java/com/geeksville/mesh/service/StoreForwardHistoryRequestTest.kt rename to core/data/src/test/kotlin/org/meshtastic/core/data/manager/HistoryManagerImplTest.kt index 88d318b266..725a2e4cfb 100644 --- a/app/src/test/java/com/geeksville/mesh/service/StoreForwardHistoryRequestTest.kt +++ b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/HistoryManagerImplTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 @@ -14,22 +14,21 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.geeksville.mesh.service +package org.meshtastic.core.data.manager import org.junit.Assert.assertEquals import org.junit.Test import org.meshtastic.proto.StoreAndForward -class StoreForwardHistoryRequestTest { +class HistoryManagerImplTest { @Test fun `buildStoreForwardHistoryRequest copies positive parameters`() { - val request = - MeshHistoryManager.buildStoreForwardHistoryRequest( - lastRequest = 42, - historyReturnWindow = 15, - historyReturnMax = 25, - ) + val request = HistoryManagerImpl.buildStoreForwardHistoryRequest( + lastRequest = 42, + historyReturnWindow = 15, + historyReturnMax = 25, + ) assertEquals(StoreAndForward.RequestResponse.CLIENT_HISTORY, request.rr) assertEquals(42, request.history?.last_request) @@ -39,12 +38,11 @@ class StoreForwardHistoryRequestTest { @Test fun `buildStoreForwardHistoryRequest clamps non-positive parameters`() { - val request = - MeshHistoryManager.buildStoreForwardHistoryRequest( - lastRequest = 0, - historyReturnWindow = -1, - historyReturnMax = 0, - ) + val request = HistoryManagerImpl.buildStoreForwardHistoryRequest( + lastRequest = 0, + historyReturnWindow = -1, + historyReturnMax = 0, + ) assertEquals(StoreAndForward.RequestResponse.CLIENT_HISTORY, request.rr) assertEquals(0, request.history?.last_request) @@ -54,7 +52,7 @@ class StoreForwardHistoryRequestTest { @Test fun `resolveHistoryRequestParameters uses config values when positive`() { - val (window, max) = MeshHistoryManager.resolveHistoryRequestParameters(window = 30, max = 10) + val (window, max) = HistoryManagerImpl.resolveHistoryRequestParameters(window = 30, max = 10) assertEquals(30, window) assertEquals(10, max) @@ -62,7 +60,7 @@ class StoreForwardHistoryRequestTest { @Test fun `resolveHistoryRequestParameters falls back to defaults when non-positive`() { - val (window, max) = MeshHistoryManager.resolveHistoryRequestParameters(window = 0, max = -5) + val (window, max) = HistoryManagerImpl.resolveHistoryRequestParameters(window = 0, max = -5) assertEquals(1440, window) assertEquals(100, max) diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshNodeManagerTest.kt b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/NodeManagerImplTest.kt similarity index 86% rename from app/src/test/java/com/geeksville/mesh/service/MeshNodeManagerTest.kt rename to core/data/src/test/kotlin/org/meshtastic/core/data/manager/NodeManagerImplTest.kt index 9dcba64894..4748663ba3 100644 --- a/app/src/test/java/com/geeksville/mesh/service/MeshNodeManagerTest.kt +++ b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/NodeManagerImplTest.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 com.geeksville.mesh.service +package org.meshtastic.core.data.manager import io.mockk.mockk import org.junit.Assert.assertEquals @@ -26,17 +26,18 @@ import org.junit.Test import org.meshtastic.core.model.DataPacket import org.meshtastic.core.repository.MeshServiceNotifications import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.ServiceBroadcasts import org.meshtastic.proto.HardwareModel import org.meshtastic.proto.Position import org.meshtastic.proto.User -class NodeManagerTest { +class NodeManagerImplTest { private val nodeRepository: NodeRepository = mockk(relaxed = true) private val serviceBroadcasts: ServiceBroadcasts = mockk(relaxed = true) private val serviceNotifications: MeshServiceNotifications = mockk(relaxed = true) - private lateinit var nodeManager: NodeManager + private lateinit var nodeManager: NodeManagerImpl @Before fun setUp() { @@ -44,13 +45,13 @@ class NodeManagerTest { } @Test - fun `getOrCreateNodeInfo creates default user for unknown node`() { + fun `getOrCreateNode creates default user for unknown node`() { val nodeNum = 1234 - val result = nodeManager.getOrCreateNodeInfo(nodeNum) + val result = nodeManager.getOrCreateNode(nodeNum) assertNotNull(result) assertEquals(nodeNum, result.num) - assertTrue(result.user.long_name?.startsWith("Meshtastic") == true) + assertTrue(result.user.long_name.startsWith("Meshtastic")) assertEquals(DataPacket.nodeNumToDefaultId(nodeNum), result.user.id) } @@ -61,7 +62,7 @@ class NodeManagerTest { User(id = "!12345678", long_name = "My Custom Name", short_name = "MCN", hw_model = HardwareModel.TLORA_V2) // Setup existing node - nodeManager.updateNode(nodeNum) { it.user = existingUser } + nodeManager.updateNode(nodeNum) { it.copy(user = existingUser) } val incomingDefaultUser = User(id = "!12345678", long_name = "Meshtastic 5678", short_name = "5678", hw_model = HardwareModel.UNSET) @@ -79,7 +80,7 @@ class NodeManagerTest { val existingUser = User(id = "!12345678", long_name = "Meshtastic 5678", short_name = "5678", hw_model = HardwareModel.UNSET) - nodeManager.updateNode(nodeNum) { it.user = existingUser } + nodeManager.updateNode(nodeNum) { it.copy(user = existingUser) } val incomingDetailedUser = User(id = "!12345678", long_name = "Real User", short_name = "RU", hw_model = HardwareModel.TLORA_V1) @@ -96,7 +97,7 @@ class NodeManagerTest { val nodeNum = 1234 val position = Position(latitude_i = 450000000, longitude_i = 900000000) - nodeManager.handleReceivedPosition(nodeNum, 9999, position) + nodeManager.handleReceivedPosition(nodeNum, 9999, position, 0) val result = nodeManager.nodeDBbyNodeNum[nodeNum] assertNotNull(result!!.position) @@ -106,7 +107,7 @@ class NodeManagerTest { @Test fun `clear resets internal state`() { - nodeManager.updateNode(1234) { it.longName = "Test" } + nodeManager.updateNode(1234) { it.copy(user = it.user.copy(long_name = "Test")) } nodeManager.clear() assertTrue(nodeManager.nodeDBbyNodeNum.isEmpty()) diff --git a/core/data/src/test/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryTest.kt b/core/data/src/test/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryTest.kt index 62c27eb9d7..1759722002 100644 --- a/core/data/src/test/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryTest.kt +++ b/core/data/src/test/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryTest.kt @@ -41,7 +41,7 @@ class DeviceHardwareRepositoryTest { private val dispatchers = CoroutineDispatchers(main = testDispatcher, io = testDispatcher, default = testDispatcher) private val repository = - DeviceHardwareRepository( + DeviceHardwareRepositoryImpl( remoteDataSource, localDataSource, jsonDataSource, diff --git a/core/data/src/test/kotlin/org/meshtastic/core/data/repository/NodeRepositoryTest.kt b/core/data/src/test/kotlin/org/meshtastic/core/data/repository/NodeRepositoryTest.kt index 8b931b5727..58c9c78feb 100644 --- a/core/data/src/test/kotlin/org/meshtastic/core/data/repository/NodeRepositoryTest.kt +++ b/core/data/src/test/kotlin/org/meshtastic/core/data/repository/NodeRepositoryTest.kt @@ -91,7 +91,7 @@ class NodeRepositoryTest { myNodeInfoFlow.value = createMyNodeEntity(myNodeNum) val repository = - NodeRepository(lifecycle, readDataSource, writeDataSource, dispatchers, localStatsDataSource) + NodeRepositoryImpl(lifecycle, readDataSource, writeDataSource, dispatchers, localStatsDataSource) testScheduler.runCurrent() val result = repository.effectiveLogNodeId(myNodeNum).filter { it == MeshLog.NODE_NUM_LOCAL }.first() @@ -106,7 +106,7 @@ class NodeRepositoryTest { myNodeInfoFlow.value = createMyNodeEntity(myNodeNum) val repository = - NodeRepository(lifecycle, readDataSource, writeDataSource, dispatchers, localStatsDataSource) + NodeRepositoryImpl(lifecycle, readDataSource, writeDataSource, dispatchers, localStatsDataSource) testScheduler.runCurrent() val result = repository.effectiveLogNodeId(remoteNodeNum).first() @@ -122,7 +122,7 @@ class NodeRepositoryTest { myNodeInfoFlow.value = createMyNodeEntity(firstNodeNum) val repository = - NodeRepository(lifecycle, readDataSource, writeDataSource, dispatchers, localStatsDataSource) + NodeRepositoryImpl(lifecycle, readDataSource, writeDataSource, dispatchers, localStatsDataSource) testScheduler.runCurrent() // Initially should be mapped to LOCAL because it matches diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/HistoryManager.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/HistoryManager.kt new file mode 100644 index 0000000000..78b70ed5e3 --- /dev/null +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/HistoryManager.kt @@ -0,0 +1,48 @@ +/* + * 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 . + */ +package org.meshtastic.core.repository + +import org.meshtastic.proto.ModuleConfig + +/** + * Interface for managing store-and-forward history replay requests. + */ +interface HistoryManager { + /** + * Requests a history replay from the radio. + * + * @param trigger A string identifying the trigger for the request (for logging). + * @param myNodeNum The local node number. + * @param storeForwardConfig The store-and-forward module configuration. + * @param transport The transport method being used (for logging). + */ + fun requestHistoryReplay( + trigger: String, + myNodeNum: Int?, + storeForwardConfig: ModuleConfig.StoreForwardConfig?, + transport: String, + ) + + /** + * Updates the last requested history marker. + * + * @param source A string identifying the source of the update (for logging). + * @param lastRequest The timestamp or sequence number of the last received history message. + * @param transport The transport method being used (for logging). + */ + fun updateStoreForwardLastRequest(source: String, lastRequest: Int, transport: String) +} diff --git a/feature/messaging/src/test/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorkerTest.kt b/feature/messaging/src/test/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorkerTest.kt index 72042dbf43..537bc1d63e 100644 --- a/feature/messaging/src/test/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorkerTest.kt +++ b/feature/messaging/src/test/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorkerTest.kt @@ -30,12 +30,11 @@ import io.mockk.just import io.mockk.mockk import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest +import okio.ByteString.Companion.toByteString import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.meshtastic.core.database.entity.Packet -import org.meshtastic.core.database.entity.PacketEntity import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.MessageStatus @@ -62,11 +61,8 @@ class SendMessageWorkerTest { fun `doWork returns success when packet is sent successfully`() = runTest { // Arrange val packetId = 12345 - val dataPacket = DataPacket("dest", 0, "Hello") - val packet = mockk(relaxed = true) - val packetEntity = PacketEntity(packet = packet) - every { packet.data } returns dataPacket - coEvery { packetRepository.getPacketByPacketId(packetId) } returns packetEntity + val dataPacket = DataPacket(to = "dest", bytes = "Hello".encodeToByteArray().toByteString(), dataType = 0) + coEvery { packetRepository.getPacketByPacketId(packetId) } returns dataPacket every { radioController.connectionState } returns MutableStateFlow(ConnectionState.Connected) coEvery { radioController.sendMessage(any()) } just Runs coEvery { packetRepository.updateMessageStatus(any(), any()) } just Runs @@ -99,11 +95,8 @@ class SendMessageWorkerTest { fun `doWork returns retry when radio is disconnected`() = runTest { // Arrange val packetId = 12345 - val dataPacket = DataPacket("dest", 0, "Hello") - val packet = mockk(relaxed = true) - val packetEntity = PacketEntity(packet = packet) - every { packet.data } returns dataPacket - coEvery { packetRepository.getPacketByPacketId(packetId) } returns packetEntity + val dataPacket = DataPacket(to = "dest", bytes = "Hello".encodeToByteArray().toByteString(), dataType = 0) + coEvery { packetRepository.getPacketByPacketId(packetId) } returns dataPacket every { radioController.connectionState } returns MutableStateFlow(ConnectionState.Disconnected) val worker = From df43d537020f76db27e31fcc1cc1187f7fb022a8 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 2 Mar 2026 13:53:07 -0600 Subject: [PATCH 04/20] refactor: extract RadioController interface and move mesh logic to core modules - Introduce `RadioController` in `core:model` to abstract radio operations and settings. - Extract `ServiceRepository`, `MqttManager`, `TracerouteHandler`, and `NeighborInfoHandler` interfaces into `core:repository`. - Move MQTT repository to `core:network` and handler implementations to `core:data`. - Update ViewModels to interact with `RadioController` instead of `IMeshService` directly. - Relocate `ServiceAction` and `TracerouteResponse` to `core:model` for better module accessibility. - Bind new implementations in `RepositoryModule` and `ServiceModule`. Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .../com/geeksville/mesh/MeshServiceClient.kt | 4 +- .../com/geeksville/mesh/model/UIViewModel.kt | 12 +- .../mesh/service/FromRadioPacketHandler.kt | 5 +- .../mesh/service/MeshActionHandler.kt | 2 +- .../mesh/service/MeshConfigHandler.kt | 2 +- .../mesh/service/MeshConnectionManager.kt | 3 +- .../mesh/service/MeshDataHandler.kt | 9 +- .../mesh/service/MeshMessageProcessor.kt | 2 +- .../com/geeksville/mesh/service/MeshRouter.kt | 9 +- .../geeksville/mesh/service/MeshService.kt | 4 +- .../mesh/service/ReactionReceiver.kt | 4 +- .../geeksville/mesh/service/ReplyReceiver.kt | 10 +- .../mesh/service/ServiceBroadcasts.kt | 2 +- .../main/java/com/geeksville/mesh/ui/Main.kt | 6 +- .../ui/connections/ConnectionsViewModel.kt | 2 +- .../mesh/ui/connections/ScannerViewModel.kt | 13 +- .../mesh/ui/sharing/ChannelViewModel.kt | 19 +-- .../mesh/widget/LocalStatsWidgetState.kt | 2 +- .../service/FromRadioPacketHandlerTest.kt | 5 +- .../mesh/service/MeshConnectionManagerTest.kt | 3 +- .../mesh/service/MeshDataHandlerTest.kt | 9 +- .../mesh/service/MeshDataMapperTest.kt | 25 +--- .../mesh/service/MeshMessageProcessorTest.kt | 2 +- .../mesh/service/MeshServiceBroadcastsTest.kt | 2 +- .../core/data/di/RepositoryModule.kt | 32 +++++ .../core/data/manager/MqttManagerImpl.kt | 17 +-- .../data/manager/NeighborInfoHandlerImpl.kt | 18 ++- .../data/manager/TracerouteHandlerImpl.kt | 29 ++-- .../meshtastic/core/model/RadioController.kt | 22 +++ .../core/model}/service/ServiceAction.kt | 4 +- .../core/model/service/TracerouteResponse.kt | 29 ++++ core/network/build.gradle.kts | 6 +- .../network/repository}/MQTTRepository.kt | 2 +- .../repository}/TrustAllX509TrustManager.kt | 2 +- .../meshtastic/core/repository/MqttManager.kt | 34 +++++ .../core/repository/NeighborInfoHandler.kt | 28 ++-- .../core/repository/ServiceRepository.kt | 92 ++++++++++++ .../core/repository/TracerouteHandler.kt | 38 +++++ .../service/AndroidRadioControllerImpl.kt | 51 ++++++- .../core/service/ServiceRepository.kt | 58 ++++---- .../core/service/di/ServiceModule.kt | 11 +- .../core/ui/qr/ScannedQrCodeViewModel.kt | 18 +-- .../core/ui/share/SharedContactViewModel.kt | 4 +- .../firmware/FirmwareUpdateViewModel.kt | 10 +- .../feature/firmware/NordicDfuHandler.kt | 6 +- .../feature/firmware/UsbUpdateHandler.kt | 14 +- .../firmware/ota/Esp32OtaUpdateHandler.kt | 28 ++-- .../firmware/ota/Esp32OtaUpdateHandlerTest.kt | 8 +- .../meshtastic/feature/map/MapViewModel.kt | 14 +- .../meshtastic/feature/map/MapViewModel.kt | 8 +- .../feature/map/BaseMapViewModel.kt | 131 ++++++++---------- .../feature/messaging/MessageViewModel.kt | 4 +- .../messaging/ui/contact/ContactsViewModel.kt | 2 +- .../node/component/AdministrationSection.kt | 2 +- .../node/detail/NodeDetailViewModel.kt | 4 +- .../node/detail/NodeManagementActions.kt | 35 ++--- .../feature/node/detail/NodeRequestActions.kt | 112 +++++++-------- .../feature/node/list/NodeListViewModel.kt | 12 +- .../feature/node/metrics/MetricsViewModel.kt | 2 +- .../feature/node/model/NodeDetailAction.kt | 2 +- .../node/detail/NodeManagementActionsTest.kt | 5 +- .../settings/radio/RadioConfigViewModel.kt | 2 +- .../radio/RadioConfigViewModelTest.kt | 2 +- 63 files changed, 645 insertions(+), 409 deletions(-) rename app/src/main/java/com/geeksville/mesh/service/MeshMqttManager.kt => core/data/src/main/kotlin/org/meshtastic/core/data/manager/MqttManagerImpl.kt (86%) rename app/src/main/java/com/geeksville/mesh/service/MeshNeighborInfoHandler.kt => core/data/src/main/kotlin/org/meshtastic/core/data/manager/NeighborInfoHandlerImpl.kt (88%) rename app/src/main/java/com/geeksville/mesh/service/MeshTracerouteHandler.kt => core/data/src/main/kotlin/org/meshtastic/core/data/manager/TracerouteHandlerImpl.kt (80%) rename core/{service/src/main/kotlin/org/meshtastic/core => model/src/commonMain/kotlin/org/meshtastic/core/model}/service/ServiceAction.kt (94%) create mode 100644 core/model/src/commonMain/kotlin/org/meshtastic/core/model/service/TracerouteResponse.kt rename {app/src/main/java/com/geeksville/mesh/repository/network => core/network/src/main/kotlin/org/meshtastic/core/network/repository}/MQTTRepository.kt (99%) rename {app/src/main/java/com/geeksville/mesh/repository/network => core/network/src/main/kotlin/org/meshtastic/core/network/repository}/TrustAllX509TrustManager.kt (96%) create mode 100644 core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MqttManager.kt rename app/src/main/java/com/geeksville/mesh/service/MeshDataMapper.kt => core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NeighborInfoHandler.kt (53%) create mode 100644 core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/ServiceRepository.kt create mode 100644 core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/TracerouteHandler.kt diff --git a/app/src/main/java/com/geeksville/mesh/MeshServiceClient.kt b/app/src/main/java/com/geeksville/mesh/MeshServiceClient.kt index ca4b141a50..74fcea5bf7 100644 --- a/app/src/main/java/com/geeksville/mesh/MeshServiceClient.kt +++ b/app/src/main/java/com/geeksville/mesh/MeshServiceClient.kt @@ -29,10 +29,10 @@ import dagger.hilt.android.qualifiers.ActivityContext import dagger.hilt.android.scopes.ActivityScoped import kotlinx.coroutines.launch import org.meshtastic.core.common.util.SequentialJob +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 org.meshtastic.core.service.ServiceRepository import javax.inject.Inject /** A Activity-lifecycle-aware [ServiceClient] that binds [MeshService] once the Activity is started. */ @@ -41,7 +41,7 @@ class MeshServiceClient @Inject constructor( @ActivityContext private val context: Context, - private val serviceRepository: ServiceRepository, + private val serviceRepository: AndroidServiceRepository, private val serviceSetupJob: SequentialJob, ) : ServiceClient(IMeshService.Stub::asInterface), DefaultLifecycleObserver { diff --git a/app/src/main/java/com/geeksville/mesh/model/UIViewModel.kt b/app/src/main/java/com/geeksville/mesh/model/UIViewModel.kt index 8005076ea8..d0bc742e92 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIViewModel.kt @@ -48,8 +48,10 @@ import org.meshtastic.core.data.repository.MeshLogRepository import org.meshtastic.core.database.entity.asDeviceVersion import org.meshtastic.core.datastore.UiPreferencesDataSource import org.meshtastic.core.model.MyNodeInfo +import org.meshtastic.core.model.RadioController import org.meshtastic.core.model.TracerouteMapAvailability import org.meshtastic.core.model.evaluateTracerouteMapAvailability +import org.meshtastic.core.model.service.TracerouteResponse import org.meshtastic.core.model.util.dispatchMeshtasticUri import org.meshtastic.core.repository.MeshServiceNotifications import org.meshtastic.core.repository.NodeRepository @@ -57,9 +59,8 @@ import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.client_notification import org.meshtastic.core.resources.compromised_keys +import org.meshtastic.core.service.AndroidServiceRepository import org.meshtastic.core.service.IMeshService -import org.meshtastic.core.service.ServiceRepository -import org.meshtastic.core.service.TracerouteResponse import org.meshtastic.core.ui.component.ScrollToTopEvent import org.meshtastic.core.ui.util.AlertManager import org.meshtastic.core.ui.util.ComposableContent @@ -75,7 +76,8 @@ class UIViewModel @Inject constructor( private val nodeDB: NodeRepository, - private val serviceRepository: ServiceRepository, + private val serviceRepository: AndroidServiceRepository, + private val radioController: RadioController, radioInterfaceService: RadioInterfaceService, meshLogRepository: MeshLogRepository, firmwareReleaseRepository: FirmwareReleaseRepository, @@ -161,6 +163,10 @@ constructor( val meshService: IMeshService? get() = serviceRepository.meshService + fun setDeviceAddress(address: String) { + radioController.setDeviceAddress(address) + } + val unreadMessageCount = packetRepository.getUnreadCountTotal().map { it.coerceAtLeast(0) }.stateInWhileSubscribed(initialValue = 0) diff --git a/app/src/main/java/com/geeksville/mesh/service/FromRadioPacketHandler.kt b/app/src/main/java/com/geeksville/mesh/service/FromRadioPacketHandler.kt index b94ff6dc11..b2c7ead787 100644 --- a/app/src/main/java/com/geeksville/mesh/service/FromRadioPacketHandler.kt +++ b/app/src/main/java/com/geeksville/mesh/service/FromRadioPacketHandler.kt @@ -18,8 +18,9 @@ package com.geeksville.mesh.service import co.touchlab.kermit.Logger import org.meshtastic.core.repository.MeshServiceNotifications +import org.meshtastic.core.repository.MqttManager import org.meshtastic.core.repository.PacketHandler -import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.proto.FromRadio import javax.inject.Inject import javax.inject.Singleton @@ -34,7 +35,7 @@ class FromRadioPacketHandler constructor( private val serviceRepository: ServiceRepository, private val router: MeshRouter, - private val mqttManager: MeshMqttManager, + private val mqttManager: MqttManager, private val packetHandler: PacketHandler, private val serviceNotifications: MeshServiceNotifications, ) { diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshActionHandler.kt b/app/src/main/java/com/geeksville/mesh/service/MeshActionHandler.kt index 1aa15c4215..8fe6597f6e 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshActionHandler.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshActionHandler.kt @@ -32,13 +32,13 @@ import org.meshtastic.core.model.MeshUser import org.meshtastic.core.model.MessageStatus import org.meshtastic.core.model.Position import org.meshtastic.core.model.Reaction +import org.meshtastic.core.model.service.ServiceAction import org.meshtastic.core.prefs.mesh.MeshPrefs import org.meshtastic.core.repository.CommandSender import org.meshtastic.core.repository.MeshServiceNotifications import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.ServiceBroadcasts -import org.meshtastic.core.service.ServiceAction import org.meshtastic.proto.AdminMessage import org.meshtastic.proto.Channel import org.meshtastic.proto.Config diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshConfigHandler.kt b/app/src/main/java/com/geeksville/mesh/service/MeshConfigHandler.kt index 17f43b901a..9f71992f21 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshConfigHandler.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshConfigHandler.kt @@ -26,7 +26,7 @@ import kotlinx.coroutines.flow.onEach import org.meshtastic.core.common.util.handledLaunch import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.RadioConfigRepository -import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.proto.Channel import org.meshtastic.proto.Config import org.meshtastic.proto.LocalConfig diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt b/app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt index 3345175b98..47eb6e1179 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt @@ -49,6 +49,7 @@ import org.meshtastic.core.prefs.ui.UiPrefs import org.meshtastic.core.repository.CommandSender import org.meshtastic.core.repository.HistoryManager import org.meshtastic.core.repository.MeshServiceNotifications +import org.meshtastic.core.repository.MqttManager import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.PacketHandler @@ -87,7 +88,7 @@ constructor( private val packetHandler: PacketHandler, private val nodeRepository: NodeRepository, private val locationManager: MeshLocationManager, - private val mqttManager: MeshMqttManager, + private val mqttManager: MqttManager, private val historyManager: HistoryManager, private val radioConfigRepository: RadioConfigRepository, private val commandSender: CommandSender, diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt b/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt index 177babac2c..64429d112f 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt @@ -37,6 +37,7 @@ import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.MessageStatus import org.meshtastic.core.model.Node import org.meshtastic.core.model.Reaction +import org.meshtastic.core.model.util.MeshDataMapper import org.meshtastic.core.model.util.SfppHasher import org.meshtastic.core.model.util.decodeOrNull import org.meshtastic.core.model.util.toOneLiner @@ -44,18 +45,20 @@ import org.meshtastic.core.prefs.mesh.MeshPrefs import org.meshtastic.core.repository.CommandSender import org.meshtastic.core.repository.HistoryManager import org.meshtastic.core.repository.MeshServiceNotifications +import org.meshtastic.core.repository.NeighborInfoHandler import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.PacketHandler import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.repository.ServiceBroadcasts +import org.meshtastic.core.repository.ServiceRepository +import org.meshtastic.core.repository.TracerouteHandler import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.critical_alert import org.meshtastic.core.resources.error_duty_cycle import org.meshtastic.core.resources.getString import org.meshtastic.core.resources.unknown_username import org.meshtastic.core.resources.waypoint_received -import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.service.filter.MessageFilterService import org.meshtastic.proto.AdminMessage import org.meshtastic.proto.MeshPacket @@ -94,8 +97,8 @@ constructor( private val historyManager: HistoryManager, private val meshPrefs: MeshPrefs, private val connectionManager: MeshConnectionManager, - private val tracerouteHandler: MeshTracerouteHandler, - private val neighborInfoHandler: MeshNeighborInfoHandler, + private val tracerouteHandler: TracerouteHandler, + private val neighborInfoHandler: NeighborInfoHandler, private val radioConfigRepository: RadioConfigRepository, private val messageFilterService: MessageFilterService, ) { diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshMessageProcessor.kt b/app/src/main/java/com/geeksville/mesh/service/MeshMessageProcessor.kt index c601099146..dbf4441395 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshMessageProcessor.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshMessageProcessor.kt @@ -34,7 +34,7 @@ import org.meshtastic.core.database.entity.MeshLog import org.meshtastic.core.model.Node import org.meshtastic.core.model.util.isLora import org.meshtastic.core.repository.NodeManager -import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.proto.FromRadio import org.meshtastic.proto.LogRecord import org.meshtastic.proto.MeshPacket diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshRouter.kt b/app/src/main/java/com/geeksville/mesh/service/MeshRouter.kt index b61bb6e02e..d876f0e75a 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshRouter.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshRouter.kt @@ -17,6 +17,9 @@ package com.geeksville.mesh.service import kotlinx.coroutines.CoroutineScope +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 @@ -31,10 +34,10 @@ class MeshRouter constructor( val dataHandler: MeshDataHandler, val configHandler: MeshConfigHandler, - val tracerouteHandler: MeshTracerouteHandler, - val neighborInfoHandler: MeshNeighborInfoHandler, + val tracerouteHandler: TracerouteHandler, + val neighborInfoHandler: NeighborInfoHandler, val configFlowManager: MeshConfigFlowManager, - val mqttManager: MeshMqttManager, + val mqttManager: MqttManager, val actionHandler: MeshActionHandler, ) { fun start(scope: CoroutineScope) { diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt index 35151a5d4b..4fa86b3f35 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -49,8 +49,8 @@ import org.meshtastic.core.repository.PacketHandler import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.repository.SERVICE_NOTIFY_ID import org.meshtastic.core.repository.ServiceBroadcasts +import org.meshtastic.core.service.AndroidServiceRepository import org.meshtastic.core.service.IMeshService -import org.meshtastic.core.service.ServiceRepository import org.meshtastic.proto.PortNum import javax.inject.Inject @@ -60,7 +60,7 @@ class MeshService : Service() { @Inject lateinit var radioInterfaceService: RadioInterfaceService - @Inject lateinit var serviceRepository: ServiceRepository + @Inject lateinit var serviceRepository: AndroidServiceRepository @Inject lateinit var connectionStateHolder: ConnectionStateHandler diff --git a/app/src/main/java/com/geeksville/mesh/service/ReactionReceiver.kt b/app/src/main/java/com/geeksville/mesh/service/ReactionReceiver.kt index 8462d8ec96..bea76c1478 100644 --- a/app/src/main/java/com/geeksville/mesh/service/ReactionReceiver.kt +++ b/app/src/main/java/com/geeksville/mesh/service/ReactionReceiver.kt @@ -25,8 +25,8 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch -import org.meshtastic.core.service.ServiceAction -import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.model.service.ServiceAction +import org.meshtastic.core.repository.ServiceRepository import javax.inject.Inject @AndroidEntryPoint diff --git a/app/src/main/java/com/geeksville/mesh/service/ReplyReceiver.kt b/app/src/main/java/com/geeksville/mesh/service/ReplyReceiver.kt index 9875de12c0..7c29affe4a 100644 --- a/app/src/main/java/com/geeksville/mesh/service/ReplyReceiver.kt +++ b/app/src/main/java/com/geeksville/mesh/service/ReplyReceiver.kt @@ -20,9 +20,11 @@ import android.content.BroadcastReceiver import androidx.core.app.RemoteInput import dagger.hilt.android.AndroidEntryPoint import jakarta.inject.Inject +import kotlinx.coroutines.launch import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.model.RadioController import org.meshtastic.core.repository.MeshServiceNotifications -import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.repository.ServiceRepository /** * A [BroadcastReceiver] that handles inline replies from notifications. @@ -33,7 +35,7 @@ import org.meshtastic.core.service.ServiceRepository */ @AndroidEntryPoint class ReplyReceiver : BroadcastReceiver() { - @Inject lateinit var serviceRepository: ServiceRepository + @Inject lateinit var radioController: RadioController @Inject lateinit var meshServiceNotifications: MeshServiceNotifications @@ -48,7 +50,9 @@ class ReplyReceiver : BroadcastReceiver() { val channel = contactKey[0].digitToIntOrNull() val dest = if (channel != null) contactKey.substring(1) else contactKey val p = DataPacket(dest, channel ?: 0, str) - serviceRepository.meshService?.send(p) + kotlinx.coroutines.CoroutineScope(kotlinx.coroutines.Dispatchers.IO).launch { + radioController.sendMessage(p) + } } override fun onReceive(context: android.content.Context, intent: android.content.Intent) { diff --git a/app/src/main/java/com/geeksville/mesh/service/ServiceBroadcasts.kt b/app/src/main/java/com/geeksville/mesh/service/ServiceBroadcasts.kt index 86ed839d56..9381f10f7c 100644 --- a/app/src/main/java/com/geeksville/mesh/service/ServiceBroadcasts.kt +++ b/app/src/main/java/com/geeksville/mesh/service/ServiceBroadcasts.kt @@ -27,7 +27,7 @@ import org.meshtastic.core.model.MessageStatus import org.meshtastic.core.model.Node import org.meshtastic.core.model.NodeInfo import org.meshtastic.core.model.util.toPIIString -import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.repository.ServiceRepository import java.util.Locale import javax.inject.Inject import javax.inject.Singleton diff --git a/app/src/main/java/com/geeksville/mesh/ui/Main.kt b/app/src/main/java/com/geeksville/mesh/ui/Main.kt index f28f98114f..56f942b8b7 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Main.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Main.kt @@ -500,8 +500,7 @@ private fun VersionChecks(viewModel: UIViewModel) { titleRes = Res.string.app_too_old, messageRes = Res.string.must_update, onConfirm = { - val service = viewModel.meshService ?: return@showAlert - MeshService.changeDeviceAddress(context, service, "n") + viewModel.setDeviceAddress("n") }, ) } else { @@ -527,8 +526,7 @@ private fun VersionChecks(viewModel: UIViewModel) { title = title, html = message, onConfirm = { - val service = viewModel.meshService ?: return@showAlert - MeshService.changeDeviceAddress(context, service, "n") + viewModel.setDeviceAddress("n") }, ) } else if (curVer < MeshService.minDeviceVersion) { diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt index da78f976f9..b17281ff6b 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt @@ -26,7 +26,7 @@ import org.meshtastic.core.model.Node import org.meshtastic.core.prefs.ui.UiPrefs import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.RadioConfigRepository -import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.LocalConfig import javax.inject.Inject diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/ScannerViewModel.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/ScannerViewModel.kt index 131eb33e8f..37e753c332 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/connections/ScannerViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/connections/ScannerViewModel.kt @@ -18,7 +18,6 @@ package com.geeksville.mesh.ui.connections import android.app.Application import android.content.Context -import android.os.RemoteException import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import co.touchlab.kermit.Logger @@ -27,7 +26,6 @@ import com.geeksville.mesh.domain.usecase.GetDiscoveredDevicesUseCase import com.geeksville.mesh.model.DeviceListEntry import com.geeksville.mesh.repository.radio.RadioInterfaceService import com.geeksville.mesh.repository.usb.UsbRepository -import com.geeksville.mesh.service.MeshService import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -42,8 +40,9 @@ import kotlinx.coroutines.launch import org.meshtastic.core.ble.BluetoothRepository import org.meshtastic.core.datastore.RecentAddressesDataSource import org.meshtastic.core.datastore.model.RecentAddress +import org.meshtastic.core.model.RadioController import org.meshtastic.core.model.util.anonymize -import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import javax.inject.Inject @@ -54,6 +53,7 @@ class ScannerViewModel constructor( private val application: Application, private val serviceRepository: ServiceRepository, + private val radioController: RadioController, private val bluetoothRepository: BluetoothRepository, private val usbRepository: UsbRepository, private val radioInterfaceService: RadioInterfaceService, @@ -117,11 +117,8 @@ constructor( } private fun changeDeviceAddress(address: String) { - try { - serviceRepository.meshService?.let { service -> MeshService.changeDeviceAddress(context, service, address) } - } catch (ex: RemoteException) { - Logger.e(ex) { "changeDeviceSelection failed, probably it is shutting down" } - } + Logger.i { "Attempting to change device address to ${address.anonymize()}" } + radioController.setDeviceAddress(address) } /** Initiates the bonding process and connects to the device upon success. */ diff --git a/app/src/main/java/com/geeksville/mesh/ui/sharing/ChannelViewModel.kt b/app/src/main/java/com/geeksville/mesh/ui/sharing/ChannelViewModel.kt index 71ff98493e..0e90416971 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/sharing/ChannelViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/sharing/ChannelViewModel.kt @@ -17,7 +17,6 @@ package com.geeksville.mesh.ui.sharing import android.net.Uri -import android.os.RemoteException import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import co.touchlab.kermit.Logger @@ -27,9 +26,9 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import org.meshtastic.core.analytics.DataPair import org.meshtastic.core.analytics.platform.PlatformAnalytics +import org.meshtastic.core.model.RadioController import org.meshtastic.core.model.util.toChannelSet import org.meshtastic.core.repository.RadioConfigRepository -import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.util.getChannelList import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.Channel @@ -42,12 +41,12 @@ import javax.inject.Inject class ChannelViewModel @Inject constructor( - private val serviceRepository: ServiceRepository, + private val radioController: RadioController, private val radioConfigRepository: RadioConfigRepository, private val analytics: PlatformAnalytics, ) : ViewModel() { - val connectionState = serviceRepository.connectionState + val connectionState = radioController.connectionState val localConfig = radioConfigRepository.localConfigFlow.stateInWhileSubscribed(initialValue = LocalConfig()) @@ -95,19 +94,15 @@ constructor( } fun setChannel(channel: Channel) { - try { - serviceRepository.meshService?.setChannel(channel.encode()) - } catch (ex: RemoteException) { - Logger.e(ex) { "Set channel error" } + viewModelScope.launch { + radioController.setLocalChannel(channel) } } // Set the radio config (also updates our saved copy in preferences) fun setConfig(config: Config) { - try { - serviceRepository.meshService?.setConfig(config.encode()) - } catch (ex: RemoteException) { - Logger.e(ex) { "Set config error" } + viewModelScope.launch { + radioController.setLocalConfig(config) } } diff --git a/app/src/main/java/com/geeksville/mesh/widget/LocalStatsWidgetState.kt b/app/src/main/java/com/geeksville/mesh/widget/LocalStatsWidgetState.kt index 4b1ca2fdca..1f28a65f7c 100644 --- a/app/src/main/java/com/geeksville/mesh/widget/LocalStatsWidgetState.kt +++ b/app/src/main/java/com/geeksville/mesh/widget/LocalStatsWidgetState.kt @@ -32,7 +32,7 @@ import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.Node import org.meshtastic.core.model.util.onlineTimeThreshold import org.meshtastic.core.repository.NodeRepository -import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.proto.LocalStats import javax.inject.Inject import javax.inject.Singleton diff --git a/app/src/test/java/com/geeksville/mesh/service/FromRadioPacketHandlerTest.kt b/app/src/test/java/com/geeksville/mesh/service/FromRadioPacketHandlerTest.kt index ec2ba1bcdb..d4d7e6112d 100644 --- a/app/src/test/java/com/geeksville/mesh/service/FromRadioPacketHandlerTest.kt +++ b/app/src/test/java/com/geeksville/mesh/service/FromRadioPacketHandlerTest.kt @@ -21,7 +21,8 @@ import io.mockk.verify import org.junit.Before import org.junit.Test import org.meshtastic.core.repository.MeshServiceNotifications -import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.repository.MqttManager +import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.proto.ClientNotification import org.meshtastic.proto.Config import org.meshtastic.proto.DeviceMetadata @@ -33,7 +34,7 @@ import org.meshtastic.proto.QueueStatus class FromRadioPacketHandlerTest { private val serviceRepository: ServiceRepository = mockk(relaxed = true) private val router: MeshRouter = mockk(relaxed = true) - private val mqttManager: MeshMqttManager = mockk(relaxed = true) + private val mqttManager: MqttManager = mockk(relaxed = true) private val packetHandler: PacketHandler = mockk(relaxed = true) private val serviceNotifications: MeshServiceNotifications = mockk(relaxed = true) diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshConnectionManagerTest.kt b/app/src/test/java/com/geeksville/mesh/service/MeshConnectionManagerTest.kt index 84e68d53f1..28ab04f6fa 100644 --- a/app/src/test/java/com/geeksville/mesh/service/MeshConnectionManagerTest.kt +++ b/app/src/test/java/com/geeksville/mesh/service/MeshConnectionManagerTest.kt @@ -48,6 +48,7 @@ import org.meshtastic.core.prefs.ui.UiPrefs import org.meshtastic.core.repository.CommandSender import org.meshtastic.core.repository.HistoryManager import org.meshtastic.core.repository.MeshServiceNotifications +import org.meshtastic.core.repository.MqttManager import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.PacketRepository @@ -71,7 +72,7 @@ class MeshConnectionManagerTest { private val packetHandler: PacketHandler = mockk(relaxed = true) private val nodeRepository: NodeRepository = mockk(relaxed = true) private val locationManager: MeshLocationManager = mockk(relaxed = true) - private val mqttManager: MeshMqttManager = mockk(relaxed = true) + private val mqttManager: MqttManager = mockk(relaxed = true) private val historyManager: HistoryManager = mockk(relaxed = true) private val radioConfigRepository: RadioConfigRepository = mockk(relaxed = true) private val commandSender: CommandSender = mockk(relaxed = true) diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshDataHandlerTest.kt b/app/src/test/java/com/geeksville/mesh/service/MeshDataHandlerTest.kt index 0c06ac91ee..6f29161964 100644 --- a/app/src/test/java/com/geeksville/mesh/service/MeshDataHandlerTest.kt +++ b/app/src/test/java/com/geeksville/mesh/service/MeshDataHandlerTest.kt @@ -31,14 +31,17 @@ import org.junit.Test import org.meshtastic.core.analytics.platform.PlatformAnalytics import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.MessageStatus +import org.meshtastic.core.model.util.MeshDataMapper import org.meshtastic.core.prefs.mesh.MeshPrefs import org.meshtastic.core.repository.CommandSender import org.meshtastic.core.repository.HistoryManager import org.meshtastic.core.repository.MeshServiceNotifications +import org.meshtastic.core.repository.NeighborInfoHandler import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.RadioConfigRepository -import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.repository.ServiceRepository +import org.meshtastic.core.repository.TracerouteHandler import org.meshtastic.core.service.filter.MessageFilterService import org.meshtastic.proto.Data import org.meshtastic.proto.MeshPacket @@ -62,8 +65,8 @@ class MeshDataHandlerTest { private val historyManager: HistoryManager = mockk(relaxed = true) private val meshPrefs: MeshPrefs = mockk(relaxed = true) private val connectionManager: MeshConnectionManager = mockk(relaxed = true) - private val tracerouteHandler: MeshTracerouteHandler = mockk(relaxed = true) - private val neighborInfoHandler: MeshNeighborInfoHandler = mockk(relaxed = true) + private val tracerouteHandler: TracerouteHandler = mockk(relaxed = true) + private val neighborInfoHandler: NeighborInfoHandler = mockk(relaxed = true) private val radioConfigRepository: RadioConfigRepository = mockk(relaxed = true) private val messageFilterService: MessageFilterService = mockk(relaxed = true) diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshDataMapperTest.kt b/app/src/test/java/com/geeksville/mesh/service/MeshDataMapperTest.kt index 0c6f5aaad5..67c53b4d62 100644 --- a/app/src/test/java/com/geeksville/mesh/service/MeshDataMapperTest.kt +++ b/app/src/test/java/com/geeksville/mesh/service/MeshDataMapperTest.kt @@ -25,6 +25,7 @@ import org.junit.Assert.assertNull import org.junit.Before import org.junit.Test import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.model.util.MeshDataMapper import org.meshtastic.core.repository.NodeManager import org.meshtastic.proto.Data import org.meshtastic.proto.MeshPacket @@ -40,30 +41,6 @@ class MeshDataMapperTest { mapper = MeshDataMapper(nodeManager) } - @Test - fun `toNodeID resolves broadcast correctly`() { - every { nodeManager.toNodeID(DataPacket.NODENUM_BROADCAST) } returns DataPacket.ID_BROADCAST - assertEquals(DataPacket.ID_BROADCAST, mapper.toNodeID(DataPacket.NODENUM_BROADCAST)) - } - - @Test - fun `toNodeID resolves known node correctly`() { - val nodeNum = 1234 - val nodeId = "!1234abcd" - every { nodeManager.toNodeID(nodeNum) } returns nodeId - - assertEquals(nodeId, mapper.toNodeID(nodeNum)) - } - - @Test - fun `toNodeID resolves unknown node to default ID`() { - val nodeNum = 1234 - val nodeId = DataPacket.nodeNumToDefaultId(nodeNum) - every { nodeManager.toNodeID(nodeNum) } returns nodeId - - assertEquals(nodeId, mapper.toNodeID(nodeNum)) - } - @Test fun `toDataPacket returns null when no decoded data`() { val packet = MeshPacket() diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshMessageProcessorTest.kt b/app/src/test/java/com/geeksville/mesh/service/MeshMessageProcessorTest.kt index 0ede2ab56d..c162b10b3c 100644 --- a/app/src/test/java/com/geeksville/mesh/service/MeshMessageProcessorTest.kt +++ b/app/src/test/java/com/geeksville/mesh/service/MeshMessageProcessorTest.kt @@ -29,7 +29,7 @@ import org.junit.Test import org.meshtastic.core.data.repository.MeshLogRepository import org.meshtastic.core.database.entity.MeshLog import org.meshtastic.core.repository.NodeManager -import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.proto.Data import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.PortNum diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshServiceBroadcastsTest.kt b/app/src/test/java/com/geeksville/mesh/service/MeshServiceBroadcastsTest.kt index f467da4541..c1b5ace1d0 100644 --- a/app/src/test/java/com/geeksville/mesh/service/MeshServiceBroadcastsTest.kt +++ b/app/src/test/java/com/geeksville/mesh/service/MeshServiceBroadcastsTest.kt @@ -25,7 +25,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.meshtastic.core.model.ConnectionState -import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.repository.ServiceRepository import org.robolectric.RobolectricTestRunner import org.robolectric.Shadows.shadowOf import org.robolectric.annotation.Config diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt index 2d24db0af2..45a39399dc 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt @@ -19,22 +19,30 @@ package org.meshtastic.core.data.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.HistoryManagerImpl +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.TracerouteHandlerImpl import org.meshtastic.core.data.repository.DeviceHardwareRepositoryImpl 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.HistoryManager +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.PacketRepository import org.meshtastic.core.repository.RadioConfigRepository +import org.meshtastic.core.repository.TracerouteHandler import javax.inject.Singleton @Module @@ -82,4 +90,28 @@ abstract class RepositoryModule { 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 + + companion object { + @Provides + @Singleton + fun provideMeshDataMapper(nodeManager: NodeManager): MeshDataMapper = MeshDataMapper(nodeManager) + } } diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshMqttManager.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MqttManagerImpl.kt similarity index 86% rename from app/src/main/java/com/geeksville/mesh/service/MeshMqttManager.kt rename to core/data/src/main/kotlin/org/meshtastic/core/data/manager/MqttManagerImpl.kt index ef4cc40bef..7684ebd205 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshMqttManager.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MqttManagerImpl.kt @@ -14,11 +14,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.geeksville.mesh.service +package org.meshtastic.core.data.manager import co.touchlab.kermit.Logger import co.touchlab.kermit.Severity -import com.geeksville.mesh.repository.network.MQTTRepository import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -26,25 +25,27 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import org.meshtastic.core.network.repository.MQTTRepository +import org.meshtastic.core.repository.MqttManager import org.meshtastic.core.repository.PacketHandler -import org.meshtastic.core.service.ServiceRepository +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 MeshMqttManager +class MqttManagerImpl @Inject constructor( private val mqttRepository: MQTTRepository, private val packetHandler: PacketHandler, private val serviceRepository: ServiceRepository, -) { +) : MqttManager { private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private var mqttMessageFlow: Job? = null - fun start(scope: CoroutineScope, enabled: Boolean, proxyToClientEnabled: Boolean) { + override fun start(scope: CoroutineScope, enabled: Boolean, proxyToClientEnabled: Boolean) { this.scope = scope if (mqttMessageFlow?.isActive == true) return if (enabled && proxyToClientEnabled) { @@ -61,7 +62,7 @@ constructor( } } - fun stop() { + override fun stop() { if (mqttMessageFlow?.isActive == true) { Logger.i { "Stopping MqttClientProxy" } mqttMessageFlow?.cancel() @@ -69,7 +70,7 @@ constructor( } } - fun handleMqttProxyMessage(message: MqttClientProxyMessage) { + override fun handleMqttProxyMessage(message: MqttClientProxyMessage) { val topic = message.topic ?: "" Logger.d { "[mqttClientProxyMessage] $topic" } val retained = message.retained == true diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshNeighborInfoHandler.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NeighborInfoHandlerImpl.kt similarity index 88% rename from app/src/main/java/com/geeksville/mesh/service/MeshNeighborInfoHandler.kt rename to core/data/src/main/kotlin/org/meshtastic/core/data/manager/NeighborInfoHandlerImpl.kt index 1e10b28243..df19abacf6 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshNeighborInfoHandler.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NeighborInfoHandlerImpl.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 com.geeksville.mesh.service +package org.meshtastic.core.data.manager import co.touchlab.kermit.Logger import kotlinx.coroutines.CoroutineScope @@ -22,12 +22,10 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.repository.CommandSender +import org.meshtastic.core.repository.NeighborInfoHandler import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.ServiceBroadcasts -import org.meshtastic.core.resources.Res -import org.meshtastic.core.resources.getString -import org.meshtastic.core.resources.unknown_username -import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.NeighborInfo import java.util.Locale @@ -35,21 +33,21 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class MeshNeighborInfoHandler +class NeighborInfoHandlerImpl @Inject constructor( private val nodeManager: NodeManager, private val serviceRepository: ServiceRepository, private val commandSender: CommandSender, private val serviceBroadcasts: ServiceBroadcasts, -) { +) : NeighborInfoHandler { private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - fun start(scope: CoroutineScope) { + override fun start(scope: CoroutineScope) { this.scope = scope } - fun handleNeighborInfo(packet: MeshPacket) { + override fun handleNeighborInfo(packet: MeshPacket) { val payload = packet.decoded?.payload ?: return val ni = NeighborInfo.ADAPTER.decode(payload) @@ -70,7 +68,7 @@ constructor( val neighbors = ni.neighbors.joinToString("\n") { n -> val node = nodeManager.nodeDBbyNodeNum[n.node_id] - val name = node?.let { "${it.user.long_name} (${it.user.short_name})" } ?: getString(Res.string.unknown_username) + val name = node?.let { "${it.user.long_name} (${it.user.short_name})" } ?: "Unknown" "• $name (SNR: ${n.snr})" } diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshTracerouteHandler.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/TracerouteHandlerImpl.kt similarity index 80% rename from app/src/main/java/com/geeksville/mesh/service/MeshTracerouteHandler.kt rename to core/data/src/main/kotlin/org/meshtastic/core/data/manager/TracerouteHandlerImpl.kt index 4fa0801cfa..518d4a6826 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshTracerouteHandler.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/TracerouteHandlerImpl.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 com.geeksville.mesh.service +package org.meshtastic.core.data.manager import co.touchlab.kermit.Logger import kotlinx.coroutines.CoroutineScope @@ -26,24 +26,19 @@ import org.meshtastic.core.data.repository.TracerouteSnapshotRepository import org.meshtastic.core.model.Node import org.meshtastic.core.model.fullRouteDiscovery import org.meshtastic.core.model.getFullTracerouteResponse +import org.meshtastic.core.model.service.TracerouteResponse import org.meshtastic.core.repository.CommandSender import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.NodeRepository -import org.meshtastic.core.resources.Res -import org.meshtastic.core.resources.getString -import org.meshtastic.core.resources.traceroute_duration -import org.meshtastic.core.resources.traceroute_route_back_to_us -import org.meshtastic.core.resources.traceroute_route_towards_dest -import org.meshtastic.core.resources.unknown_username -import org.meshtastic.core.service.ServiceRepository -import org.meshtastic.core.service.TracerouteResponse +import org.meshtastic.core.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 MeshTracerouteHandler +class TracerouteHandlerImpl @Inject constructor( private val nodeManager: NodeManager, @@ -51,22 +46,22 @@ constructor( private val tracerouteSnapshotRepository: TracerouteSnapshotRepository, private val nodeRepository: NodeRepository, private val commandSender: CommandSender, -) { +) : TracerouteHandler { private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - fun start(scope: CoroutineScope) { + override fun start(scope: CoroutineScope) { this.scope = scope } - fun handleTraceroute(packet: MeshPacket, logUuid: String?, logInsertJob: kotlinx.coroutines.Job?) { + override fun handleTraceroute(packet: MeshPacket, logUuid: String?, logInsertJob: kotlinx.coroutines.Job?) { val full = packet.getFullTracerouteResponse( getUser = { num -> nodeManager.nodeDBbyNodeNum[num]?.let { node: Node -> "${node.user.long_name} (${node.user.short_name})" } - ?: getString(Res.string.unknown_username) + ?: "Unknown" // We don't have strings in core:data yet, but we can fix this later }, - headerTowards = getString(Res.string.traceroute_route_towards_dest), - headerBack = getString(Res.string.traceroute_route_back_to_us), + headerTowards = "Route towards destination:", + headerBack = "Route back to us:", ) ?: return val requestId = packet.decoded?.request_id ?: 0 @@ -90,7 +85,7 @@ constructor( val elapsedMs = nowMillis - start val seconds = elapsedMs / MILLIS_PER_SECOND Logger.i { "Traceroute $requestId complete in $seconds s" } - val durationText = getString(Res.string.traceroute_duration, "%.1f".format(Locale.US, seconds)) + val durationText = "Duration: %.1f s".format(Locale.US, seconds) "$full\n\n$durationText" } else { full diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/RadioController.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/RadioController.kt index 286f32ddbb..a712245420 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/RadioController.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/RadioController.kt @@ -33,6 +33,11 @@ interface RadioController { suspend fun sendSharedContact(nodeNum: Int) + // Local radio configuration + suspend fun setLocalConfig(config: org.meshtastic.proto.Config) + + suspend fun setLocalChannel(channel: org.meshtastic.proto.Channel) + // Radio configuration suspend fun setOwner(destNum: Int, user: org.meshtastic.proto.User, packetId: Int) @@ -66,6 +71,10 @@ interface RadioController { // Admin operations suspend fun reboot(destNum: Int, packetId: Int) + suspend fun rebootToDfu(nodeNum: Int) + + suspend fun requestRebootOta(requestId: Int, destNum: Int, mode: Int, hash: ByteArray?) + suspend fun shutdown(destNum: Int, packetId: Int) suspend fun factoryReset(destNum: Int, packetId: Int) @@ -74,6 +83,16 @@ interface RadioController { suspend fun removeByNodenum(packetId: Int, nodeNum: Int) + suspend fun requestPosition(destNum: Int, currentPosition: Position) + + suspend fun requestUserInfo(destNum: Int) + + suspend fun requestTraceroute(requestId: Int, destNum: Int) + + suspend fun requestTelemetry(requestId: Int, destNum: Int, typeValue: Int) + + suspend fun requestNeighborInfo(requestId: Int, destNum: Int) + // Batch editing suspend fun beginEditSettings(destNum: Int) @@ -87,4 +106,7 @@ interface RadioController { /** Stops providing the phone's location to the mesh. */ fun stopProvideLocation() + + /** Changes the device address we are talking to. */ + fun setDeviceAddress(address: String) } diff --git a/core/service/src/main/kotlin/org/meshtastic/core/service/ServiceAction.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/service/ServiceAction.kt similarity index 94% rename from core/service/src/main/kotlin/org/meshtastic/core/service/ServiceAction.kt rename to core/model/src/commonMain/kotlin/org/meshtastic/core/model/service/ServiceAction.kt index 584adfa5d9..ce9d610c36 100644 --- a/core/service/src/main/kotlin/org/meshtastic/core/service/ServiceAction.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/service/ServiceAction.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 @@ -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.core.service +package org.meshtastic.core.model.service import org.meshtastic.core.model.Node import org.meshtastic.proto.SharedContact diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/service/TracerouteResponse.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/service/TracerouteResponse.kt new file mode 100644 index 0000000000..fe57fb1c43 --- /dev/null +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/service/TracerouteResponse.kt @@ -0,0 +1,29 @@ +/* + * 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 . + */ +package org.meshtastic.core.model.service + +data class TracerouteResponse( + val message: String, + val destinationNodeNum: Int, + val requestId: Int, + val forwardRoute: List = emptyList(), + val returnRoute: List = emptyList(), + val logUuid: String? = null, +) { + val hasOverlay: Boolean + get() = forwardRoute.isNotEmpty() || returnRoute.isNotEmpty() +} diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts index c7bf1e86dc..badef08337 100644 --- a/core/network/build.gradle.kts +++ b/core/network/build.gradle.kts @@ -29,10 +29,13 @@ configure { } dependencies { + api(projects.core.repository) implementation(projects.core.di) implementation(projects.core.model) + implementation(projects.core.proto) - implementation(libs.coil.network.core) + implementation(libs.org.eclipse.paho.client.mqttv3) + implementation(libs.okio) implementation(libs.coil.network.okhttp) implementation(libs.coil.svg) implementation(libs.kotlinx.serialization.json) @@ -40,6 +43,7 @@ dependencies { implementation(libs.ktor.client.okhttp) implementation(libs.ktor.serialization.kotlinx.json) implementation(libs.okhttp3.logging.interceptor) + implementation(libs.kermit) googleImplementation(libs.dd.sdk.android.okhttp) } diff --git a/app/src/main/java/com/geeksville/mesh/repository/network/MQTTRepository.kt b/core/network/src/main/kotlin/org/meshtastic/core/network/repository/MQTTRepository.kt similarity index 99% rename from app/src/main/java/com/geeksville/mesh/repository/network/MQTTRepository.kt rename to core/network/src/main/kotlin/org/meshtastic/core/network/repository/MQTTRepository.kt index a55972d3ce..b837f510cb 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/network/MQTTRepository.kt +++ b/core/network/src/main/kotlin/org/meshtastic/core/network/repository/MQTTRepository.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 com.geeksville.mesh.repository.network +package org.meshtastic.core.network.repository import co.touchlab.kermit.Logger import kotlinx.coroutines.channels.awaitClose diff --git a/app/src/main/java/com/geeksville/mesh/repository/network/TrustAllX509TrustManager.kt b/core/network/src/main/kotlin/org/meshtastic/core/network/repository/TrustAllX509TrustManager.kt similarity index 96% rename from app/src/main/java/com/geeksville/mesh/repository/network/TrustAllX509TrustManager.kt rename to core/network/src/main/kotlin/org/meshtastic/core/network/repository/TrustAllX509TrustManager.kt index d9c0425c69..8b782ab8ec 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/network/TrustAllX509TrustManager.kt +++ b/core/network/src/main/kotlin/org/meshtastic/core/network/repository/TrustAllX509TrustManager.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.geeksville.mesh.repository.network +package org.meshtastic.core.network.repository import android.annotation.SuppressLint import java.security.cert.X509Certificate diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MqttManager.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MqttManager.kt new file mode 100644 index 0000000000..d34906fe10 --- /dev/null +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MqttManager.kt @@ -0,0 +1,34 @@ +/* + * 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 . + */ +package org.meshtastic.core.repository + +import kotlinx.coroutines.CoroutineScope +import org.meshtastic.proto.MqttClientProxyMessage + +/** + * Interface for managing MQTT proxy communication. + */ +interface MqttManager { + /** Starts the MQTT manager with the given coroutine scope and settings. */ + fun start(scope: CoroutineScope, enabled: Boolean, proxyToClientEnabled: Boolean) + + /** Stops the MQTT manager. */ + fun stop() + + /** Handles an MQTT proxy message from the radio. */ + fun handleMqttProxyMessage(message: MqttClientProxyMessage) +} diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshDataMapper.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NeighborInfoHandler.kt similarity index 53% rename from app/src/main/java/com/geeksville/mesh/service/MeshDataMapper.kt rename to core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NeighborInfoHandler.kt index 44e6bde5ab..45a8dc72aa 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshDataMapper.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NeighborInfoHandler.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 @@ -14,20 +14,22 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.geeksville.mesh.service +package org.meshtastic.core.repository -import org.meshtastic.core.model.DataPacket -import org.meshtastic.core.repository.NodeManager +import kotlinx.coroutines.CoroutineScope import org.meshtastic.proto.MeshPacket -import javax.inject.Inject -import javax.inject.Singleton -import org.meshtastic.core.model.util.MeshDataMapper as CommonMeshDataMapper -@Singleton -class MeshDataMapper @Inject constructor(private val nodeManager: NodeManager) { - private val commonMapper = CommonMeshDataMapper(nodeManager) - - fun toNodeID(n: Int): String = nodeManager.toNodeID(n) +/** + * Interface for handling neighbor info responses from the mesh. + */ +interface NeighborInfoHandler { + /** Starts the neighbor info handler with the given coroutine scope. */ + fun start(scope: CoroutineScope) - fun toDataPacket(packet: MeshPacket): DataPacket? = commonMapper.toDataPacket(packet) + /** + * Processes a neighbor info packet. + * + * @param packet The received mesh packet. + */ + fun handleNeighborInfo(packet: MeshPacket) } diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/ServiceRepository.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/ServiceRepository.kt new file mode 100644 index 0000000000..881d32e22a --- /dev/null +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/ServiceRepository.kt @@ -0,0 +1,92 @@ +/* + * 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 . + */ +package org.meshtastic.core.repository + +import co.touchlab.kermit.Severity +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import org.meshtastic.core.model.ConnectionState +import org.meshtastic.core.model.service.ServiceAction +import org.meshtastic.core.model.service.TracerouteResponse +import org.meshtastic.proto.ClientNotification +import org.meshtastic.proto.MeshPacket + +/** + * Interface for managing service state, connection status, and mesh events. + */ +interface ServiceRepository { + /** Reactive connection state. */ + val connectionState: StateFlow + + /** Sets the connection state. */ + fun setConnectionState(connectionState: ConnectionState) + + /** Reactive client notification. */ + val clientNotification: StateFlow + + /** Sets the current client notification. */ + fun setClientNotification(notification: ClientNotification?) + + /** Clears the current client notification. */ + fun clearClientNotification() + + /** Reactive error message. */ + val errorMessage: StateFlow + + /** Sets an error message to be displayed. */ + fun setErrorMessage(text: String, severity: Severity = Severity.Error) + + /** Clears the current error message. */ + fun clearErrorMessage() + + /** Reactive connection progress message. */ + val connectionProgress: StateFlow + + /** Sets the connection progress message. */ + fun setConnectionProgress(text: String) + + /** Flow of all mesh packets. */ + val meshPacketFlow: SharedFlow + + /** Emits a mesh packet into the flow. */ + suspend fun emitMeshPacket(packet: MeshPacket) + + /** Reactive traceroute response. */ + val tracerouteResponse: StateFlow + + /** Sets the traceroute response. */ + fun setTracerouteResponse(value: TracerouteResponse?) + + /** Clears the traceroute response. */ + fun clearTracerouteResponse() + + /** Reactive neighbor info response. */ + val neighborInfoResponse: StateFlow + + /** Sets the neighbor info response. */ + fun setNeighborInfoResponse(value: String?) + + /** Clears the neighbor info response. */ + fun clearNeighborInfoResponse() + + /** Flow of service actions requested by the UI. */ + val serviceAction: Flow + + /** Dispatches a service action. */ + suspend fun onServiceAction(action: ServiceAction) +} diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/TracerouteHandler.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/TracerouteHandler.kt new file mode 100644 index 0000000000..949abc7636 --- /dev/null +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/TracerouteHandler.kt @@ -0,0 +1,38 @@ +/* + * 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 . + */ +package org.meshtastic.core.repository + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import org.meshtastic.proto.MeshPacket + +/** + * Interface for handling traceroute responses from the mesh. + */ +interface TracerouteHandler { + /** Starts the traceroute handler with the given coroutine scope. */ + fun start(scope: CoroutineScope) + + /** + * Processes a traceroute packet. + * + * @param packet The received mesh packet. + * @param logUuid Optional UUID for the associated log entry. + * @param logInsertJob Optional job for the log entry insertion, to ensure ordering. + */ + fun handleTraceroute(packet: MeshPacket, logUuid: String?, logInsertJob: Job?) +} diff --git a/core/service/src/main/kotlin/org/meshtastic/core/service/AndroidRadioControllerImpl.kt b/core/service/src/main/kotlin/org/meshtastic/core/service/AndroidRadioControllerImpl.kt index f2aa9a11e2..90444acd2f 100644 --- a/core/service/src/main/kotlin/org/meshtastic/core/service/AndroidRadioControllerImpl.kt +++ b/core/service/src/main/kotlin/org/meshtastic/core/service/AndroidRadioControllerImpl.kt @@ -16,10 +16,13 @@ */ package org.meshtastic.core.service +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.StateFlow 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 @@ -30,7 +33,8 @@ import javax.inject.Singleton class AndroidRadioControllerImpl @Inject constructor( - private val serviceRepository: ServiceRepository, + @ApplicationContext private val context: Context, + private val serviceRepository: AndroidServiceRepository, private val nodeRepository: NodeRepository, ) : RadioController { @@ -65,6 +69,14 @@ constructor( serviceRepository.onServiceAction(ServiceAction.SendContact(contact)) } + override suspend fun setLocalConfig(config: org.meshtastic.proto.Config) { + serviceRepository.meshService?.setConfig(config.encode()) + } + + override suspend fun setLocalChannel(channel: org.meshtastic.proto.Channel) { + serviceRepository.meshService?.setChannel(channel.encode()) + } + override suspend fun setOwner(destNum: Int, user: org.meshtastic.proto.User, packetId: Int) { serviceRepository.meshService?.setRemoteOwner(packetId, destNum, user.encode()) } @@ -125,6 +137,14 @@ constructor( serviceRepository.meshService?.requestReboot(packetId, destNum) } + override suspend fun rebootToDfu(nodeNum: Int) { + serviceRepository.meshService?.rebootToDfu(nodeNum) + } + + override suspend fun requestRebootOta(requestId: Int, destNum: Int, mode: Int, hash: ByteArray?) { + serviceRepository.meshService?.requestRebootOta(requestId, destNum, mode, hash) + } + override suspend fun shutdown(destNum: Int, packetId: Int) { serviceRepository.meshService?.requestShutdown(packetId, destNum) } @@ -141,6 +161,26 @@ constructor( serviceRepository.meshService?.removeByNodenum(packetId, nodeNum) } + override suspend fun requestPosition(destNum: Int, currentPosition: org.meshtastic.core.model.Position) { + serviceRepository.meshService?.requestPosition(destNum, currentPosition) + } + + override suspend fun requestUserInfo(destNum: Int) { + serviceRepository.meshService?.requestUserInfo(destNum) + } + + override suspend fun requestTraceroute(requestId: Int, destNum: Int) { + serviceRepository.meshService?.requestTraceroute(requestId, destNum) + } + + override suspend fun requestTelemetry(requestId: Int, destNum: Int, typeValue: Int) { + serviceRepository.meshService?.requestTelemetry(requestId, destNum, typeValue) + } + + override suspend fun requestNeighborInfo(requestId: Int, destNum: Int) { + serviceRepository.meshService?.requestNeighborInfo(requestId, destNum) + } + override suspend fun beginEditSettings(destNum: Int) { serviceRepository.meshService?.beginEditSettings(destNum) } @@ -158,4 +198,13 @@ constructor( override fun stopProvideLocation() { serviceRepository.meshService?.stopProvideLocation() } + + override fun setDeviceAddress(address: String) { + serviceRepository.meshService?.setDeviceAddress(address) + // Ensure service is running/restarted to handle the new address + val intent = android.content.Intent().apply { + setClassName("com.geeksville.mesh", "com.geeksville.mesh.service.MeshService") + } + context.startForegroundService(intent) + } } diff --git a/core/service/src/main/kotlin/org/meshtastic/core/service/ServiceRepository.kt b/core/service/src/main/kotlin/org/meshtastic/core/service/ServiceRepository.kt index 858e1695bf..07a53aa16a 100644 --- a/core/service/src/main/kotlin/org/meshtastic/core/service/ServiceRepository.kt +++ b/core/service/src/main/kotlin/org/meshtastic/core/service/ServiceRepository.kt @@ -19,33 +19,25 @@ package org.meshtastic.core.service import co.touchlab.kermit.Logger import co.touchlab.kermit.Severity import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.receiveAsFlow 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 -data class TracerouteResponse( - val message: String, - val destinationNodeNum: Int, - val requestId: Int, - val forwardRoute: List = emptyList(), - val returnRoute: List = emptyList(), - val logUuid: String? = null, -) { - val hasOverlay: Boolean - get() = forwardRoute.isNotEmpty() || returnRoute.isNotEmpty() -} - /** Repository class for managing the [IMeshService] instance and connection state */ @Suppress("TooManyFunctions") @Singleton -open class ServiceRepository @Inject constructor() { +open class AndroidServiceRepository @Inject constructor() : ServiceRepository { var meshService: IMeshService? = null private set @@ -55,86 +47,86 @@ open class ServiceRepository @Inject constructor() { // Connection state to our radio device private val _connectionState: MutableStateFlow = MutableStateFlow(ConnectionState.Disconnected) - open val connectionState: StateFlow + override val connectionState: StateFlow get() = _connectionState - fun setConnectionState(connectionState: ConnectionState) { + override fun setConnectionState(connectionState: ConnectionState) { _connectionState.value = connectionState } private val _clientNotification = MutableStateFlow(null) - val clientNotification: StateFlow + override val clientNotification: StateFlow get() = _clientNotification - fun setClientNotification(notification: ClientNotification?) { + override fun setClientNotification(notification: ClientNotification?) { notification?.message?.let { Logger.w { it } } _clientNotification.value = notification } - fun clearClientNotification() { + override fun clearClientNotification() { _clientNotification.value = null } private val _errorMessage = MutableStateFlow(null) - val errorMessage: StateFlow + override val errorMessage: StateFlow get() = _errorMessage - fun setErrorMessage(text: String, severity: Severity = Severity.Error) { + override fun setErrorMessage(text: String, severity: Severity) { Logger.log(severity, "ServiceRepository", null, text) _errorMessage.value = text } - fun clearErrorMessage() { + override fun clearErrorMessage() { _errorMessage.value = null } private val _connectionProgress = MutableStateFlow(null) - val connectionProgress: StateFlow + override val connectionProgress: StateFlow get() = _connectionProgress - fun setConnectionProgress(text: String) { + override fun setConnectionProgress(text: String) { if (connectionState.value != ConnectionState.Connected) { _connectionProgress.value = text } } private val _meshPacketFlow = MutableSharedFlow(extraBufferCapacity = 64) - val meshPacketFlow: SharedFlow + override val meshPacketFlow: SharedFlow get() = _meshPacketFlow - suspend fun emitMeshPacket(packet: MeshPacket) { + override suspend fun emitMeshPacket(packet: MeshPacket) { _meshPacketFlow.emit(packet) } private val _tracerouteResponse = MutableStateFlow(null) - val tracerouteResponse: StateFlow + override val tracerouteResponse: StateFlow get() = _tracerouteResponse - fun setTracerouteResponse(value: TracerouteResponse?) { + override fun setTracerouteResponse(value: TracerouteResponse?) { _tracerouteResponse.value = value } - fun clearTracerouteResponse() { + override fun clearTracerouteResponse() { setTracerouteResponse(null) } private val _neighborInfoResponse = MutableStateFlow(null) - val neighborInfoResponse: StateFlow + override val neighborInfoResponse: StateFlow get() = _neighborInfoResponse - fun setNeighborInfoResponse(value: String?) { + override fun setNeighborInfoResponse(value: String?) { _neighborInfoResponse.value = value } - fun clearNeighborInfoResponse() { + override fun clearNeighborInfoResponse() { setNeighborInfoResponse(null) } private val _serviceAction = Channel() - val serviceAction = _serviceAction.receiveAsFlow() + override val serviceAction: Flow = _serviceAction.receiveAsFlow() - suspend fun onServiceAction(action: ServiceAction) { + override suspend fun onServiceAction(action: ServiceAction) { _serviceAction.send(action) } } diff --git a/core/service/src/main/kotlin/org/meshtastic/core/service/di/ServiceModule.kt b/core/service/src/main/kotlin/org/meshtastic/core/service/di/ServiceModule.kt index 0df2b76e51..262d2fabf3 100644 --- a/core/service/src/main/kotlin/org/meshtastic/core/service/di/ServiceModule.kt +++ b/core/service/src/main/kotlin/org/meshtastic/core/service/di/ServiceModule.kt @@ -21,11 +21,20 @@ 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 abstract fun bindRadioController(impl: AndroidRadioControllerImpl): RadioController + @Binds + @Singleton + abstract fun bindRadioController(impl: AndroidRadioControllerImpl): RadioController + + @Binds + @Singleton + abstract fun bindServiceRepository(impl: AndroidServiceRepository): ServiceRepository } 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 128b0e5d5d..e9eeef11f9 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 @@ -16,14 +16,12 @@ */ package org.meshtastic.core.ui.qr -import android.os.RemoteException import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import co.touchlab.kermit.Logger import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch +import org.meshtastic.core.model.RadioController import org.meshtastic.core.repository.RadioConfigRepository -import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.util.getChannelList import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.Channel @@ -37,7 +35,7 @@ class ScannedQrCodeViewModel @Inject constructor( private val radioConfigRepository: RadioConfigRepository, - private val serviceRepository: ServiceRepository, + private val radioController: RadioController, ) : ViewModel() { val channels = radioConfigRepository.channelSetFlow.stateInWhileSubscribed(initialValue = ChannelSet()) @@ -56,19 +54,15 @@ constructor( } private fun setChannel(channel: Channel) { - try { - serviceRepository.meshService?.setChannel(Channel.ADAPTER.encode(channel)) - } catch (ex: RemoteException) { - Logger.e(ex) { "Set channel error" } + viewModelScope.launch { + radioController.setLocalChannel(channel) } } // Set the radio config (also updates our saved copy in preferences) private fun setConfig(config: Config) { - try { - serviceRepository.meshService?.setConfig(Config.ADAPTER.encode(config)) - } catch (ex: RemoteException) { - Logger.e(ex) { "Set config error" } + viewModelScope.launch { + radioController.setLocalConfig(config) } } } 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 82ef4ed6cf..d0feb933d9 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 @@ -22,9 +22,9 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import org.meshtastic.core.model.Node +import org.meshtastic.core.model.service.ServiceAction import org.meshtastic.core.repository.NodeRepository -import org.meshtastic.core.service.ServiceAction -import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.SharedContact import javax.inject.Inject diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt b/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt index 5c8e0a3a5b..92d70fe4e9 100644 --- a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt +++ b/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt @@ -44,6 +44,7 @@ import org.meshtastic.core.datastore.BootloaderWarningDataSource import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.DeviceHardware import org.meshtastic.core.model.MyNodeInfo +import org.meshtastic.core.model.RadioController import org.meshtastic.core.prefs.radio.RadioPrefs import org.meshtastic.core.prefs.radio.isBle import org.meshtastic.core.prefs.radio.isSerial @@ -72,7 +73,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 org.meshtastic.core.service.ServiceRepository import java.io.File import javax.inject.Inject @@ -95,7 +95,7 @@ constructor( private val firmwareReleaseRepository: FirmwareReleaseRepository, private val deviceHardwareRepository: DeviceHardwareRepository, private val nodeRepository: NodeRepository, - private val serviceRepository: ServiceRepository, + private val radioController: RadioController, private val radioPrefs: RadioPrefs, private val bootloaderWarningDataSource: BootloaderWarningDataSource, private val firmwareUpdateManager: FirmwareUpdateManager, @@ -106,6 +106,8 @@ constructor( private val _state = MutableStateFlow(FirmwareUpdateState.Idle) val state: StateFlow = _state.asStateFlow() + val connectionState = radioController.connectionState + private val _selectedReleaseType = MutableStateFlow(FirmwareReleaseType.STABLE) val selectedReleaseType: StateFlow = _selectedReleaseType.asStateFlow() @@ -429,14 +431,14 @@ constructor( // Trigger a fresh connection attempt by MeshService address?.let { currentAddr -> Logger.i { "Post-update: Requesting MeshService to reconnect to $currentAddr" } - serviceRepository.meshService?.setDeviceAddress("$DFU_RECONNECT_PREFIX$currentAddr") + radioController.setDeviceAddress("$DFU_RECONNECT_PREFIX$currentAddr") } // Wait for device to reconnect and settle val result = withTimeoutOrNull(VERIFY_TIMEOUT) { // Wait for both Connected state and node info to be present - serviceRepository.connectionState.first { it is ConnectionState.Connected } + connectionState.first { it is ConnectionState.Connected } nodeRepository.ourNodeInfo.filterNotNull().first() delay(VERIFY_DELAY) // Extra buffer for initial config sync true diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/NordicDfuHandler.kt b/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/NordicDfuHandler.kt index d104d18d4d..72cd5ed5fe 100644 --- a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/NordicDfuHandler.kt +++ b/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/NordicDfuHandler.kt @@ -33,12 +33,12 @@ import no.nordicsemi.android.dfu.DfuServiceListenerHelper import org.jetbrains.compose.resources.getString import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.model.DeviceHardware +import org.meshtastic.core.model.RadioController import org.meshtastic.core.resources.Res 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 org.meshtastic.core.service.ServiceRepository import java.io.File import javax.inject.Inject @@ -53,7 +53,7 @@ class NordicDfuHandler constructor( private val firmwareRetriever: FirmwareRetriever, @ApplicationContext private val context: Context, - private val serviceRepository: ServiceRepository, + private val radioController: RadioController, ) : FirmwareUpdateHandler { override suspend fun startUpdate( @@ -113,7 +113,7 @@ constructor( updateState(FirmwareUpdateState.Processing(ProgressState(startingMsg))) // n = Nordic (Legacy prefix handling in mesh service) - serviceRepository.meshService?.setDeviceAddress("n") + radioController.setDeviceAddress("n") DfuServiceInitiator(address) .setDeviceName(deviceHardware.displayName) diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/UsbUpdateHandler.kt b/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/UsbUpdateHandler.kt index 4e7075c217..19534440c8 100644 --- a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/UsbUpdateHandler.kt +++ b/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/UsbUpdateHandler.kt @@ -23,12 +23,13 @@ import kotlinx.coroutines.delay import org.jetbrains.compose.resources.getString import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.model.DeviceHardware +import org.meshtastic.core.model.RadioController +import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.resources.Res 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 org.meshtastic.core.service.ServiceRepository import java.io.File import javax.inject.Inject @@ -40,7 +41,8 @@ class UsbUpdateHandler @Inject constructor( private val firmwareRetriever: FirmwareRetriever, - private val serviceRepository: ServiceRepository, + private val radioController: RadioController, + private val nodeRepository: NodeRepository, ) : FirmwareUpdateHandler { override suspend fun startUpdate( @@ -62,8 +64,8 @@ constructor( if (firmwareUri != null) { updateState(FirmwareUpdateState.Processing(ProgressState(rebootingMsg))) - val myNodeNum = serviceRepository.meshService?.getMyNodeInfo()?.myNodeNum ?: 0 - serviceRepository.meshService?.rebootToDfu(myNodeNum) + val myNodeNum = nodeRepository.myNodeInfo.value?.myNodeNum ?: 0 + radioController.rebootToDfu(myNodeNum) delay(REBOOT_DELAY) updateState(FirmwareUpdateState.AwaitingFileSave(null, "firmware.uf2", firmwareUri)) @@ -85,8 +87,8 @@ constructor( null } else { updateState(FirmwareUpdateState.Processing(ProgressState(rebootingMsg))) - val myNodeNum = serviceRepository.meshService?.getMyNodeInfo()?.myNodeNum ?: 0 - serviceRepository.meshService?.rebootToDfu(myNodeNum) + val myNodeNum = nodeRepository.myNodeInfo.value?.myNodeNum ?: 0 + radioController.rebootToDfu(myNodeNum) delay(REBOOT_DELAY) updateState(FirmwareUpdateState.AwaitingFileSave(firmwareFile, firmwareFile.name)) diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandler.kt b/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandler.kt index 06bffbb496..20c4d44030 100644 --- a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandler.kt +++ b/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandler.kt @@ -21,14 +21,18 @@ 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 import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import no.nordicsemi.kotlin.ble.client.android.CentralManager import org.jetbrains.compose.resources.getString import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.model.DeviceHardware +import org.meshtastic.core.model.RadioController +import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.firmware_update_connecting_attempt import org.meshtastic.core.resources.firmware_update_downloading_percent @@ -40,7 +44,6 @@ 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 -import org.meshtastic.core.service.ServiceRepository import org.meshtastic.feature.firmware.FirmwareRetriever import org.meshtastic.feature.firmware.FirmwareUpdateHandler import org.meshtastic.feature.firmware.FirmwareUpdateState @@ -68,7 +71,8 @@ class Esp32OtaUpdateHandler @Inject constructor( private val firmwareRetriever: FirmwareRetriever, - private val serviceRepository: ServiceRepository, + private val radioController: RadioController, + private val nodeRepository: NodeRepository, private val centralManager: CentralManager, @ApplicationContext private val context: Context, ) : FirmwareUpdateHandler { @@ -201,13 +205,11 @@ constructor( } private fun triggerRebootOta(mode: Int, hash: ByteArray?) { - val service = serviceRepository.meshService ?: return - try { - val myInfo = service.getMyNodeInfo() ?: return - Logger.i { "ESP32 OTA: Triggering reboot OTA mode $mode with hash" } - service.requestRebootOta(service.getPacketId(), myInfo.myNodeNum, mode, hash) - } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { - Logger.e(e) { "ESP32 OTA: Failed to trigger reboot OTA" } + val myInfo = nodeRepository.myNodeInfo.value ?: return + val myNodeNum = myInfo.myNodeNum + Logger.i { "ESP32 OTA: Triggering reboot OTA mode $mode with hash" } + CoroutineScope(Dispatchers.IO).launch { + radioController.requestRebootOta(radioController.getPacketId(), myNodeNum, mode, hash) } } @@ -216,12 +218,8 @@ constructor( * interface) cleanly disconnects without reconnection attempts. */ private fun disconnectMeshService() { - try { - Logger.i { "ESP32 OTA: Disconnecting mesh service for OTA" } - serviceRepository.meshService?.setDeviceAddress("n") - } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { - Logger.w(e) { "ESP32 OTA: Error disconnecting mesh service" } - } + Logger.i { "ESP32 OTA: Disconnecting mesh service for OTA" } + radioController.setDeviceAddress("n") } private suspend fun obtainFirmwareFile( diff --git a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandlerTest.kt b/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandlerTest.kt index 981067a03a..c837941236 100644 --- a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandlerTest.kt +++ b/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandlerTest.kt @@ -33,7 +33,8 @@ import org.junit.Before import org.junit.Test import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.model.DeviceHardware -import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.model.RadioController +import org.meshtastic.core.repository.NodeRepository import org.meshtastic.feature.firmware.FirmwareRetriever import org.meshtastic.feature.firmware.FirmwareUpdateState import java.io.IOException @@ -42,12 +43,13 @@ import java.io.IOException class Esp32OtaUpdateHandlerTest { private val firmwareRetriever: FirmwareRetriever = mockk() - private val serviceRepository: ServiceRepository = mockk() + private val radioController: RadioController = mockk() + private val nodeRepository: NodeRepository = mockk() private val centralManager: CentralManager = mockk() private val context: Context = mockk() private val contentResolver: ContentResolver = mockk() - private val handler = Esp32OtaUpdateHandler(firmwareRetriever, serviceRepository, centralManager, context) + private val handler = Esp32OtaUpdateHandler(firmwareRetriever, radioController, nodeRepository, centralManager, context) @Before fun setUp() { diff --git a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewModel.kt b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewModel.kt index c0434ae585..66b2e3b0c1 100644 --- a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewModel.kt +++ b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewModel.kt @@ -23,13 +23,13 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import org.meshtastic.core.common.BuildConfigProvider -import org.meshtastic.core.repository.NodeRepository -import org.meshtastic.core.repository.PacketRepository -import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.model.RadioController import org.meshtastic.core.navigation.MapRoutes import org.meshtastic.core.prefs.map.MapPrefs -import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.PacketRepository +import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.LocalConfig import javax.inject.Inject @@ -41,12 +41,12 @@ class MapViewModel constructor( mapPrefs: MapPrefs, packetRepository: PacketRepository, - private val nodeRepository: NodeRepository, - serviceRepository: ServiceRepository, + override val nodeRepository: NodeRepository, + radioController: RadioController, radioConfigRepository: RadioConfigRepository, buildConfigProvider: BuildConfigProvider, savedStateHandle: SavedStateHandle, -) : BaseMapViewModel(mapPrefs, nodeRepository, packetRepository, serviceRepository) { +) : BaseMapViewModel(mapPrefs, nodeRepository, packetRepository, radioController) { private val _selectedWaypointId = MutableStateFlow(savedStateHandle.toRoute().waypointId) val selectedWaypointId: StateFlow = _selectedWaypointId.asStateFlow() diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapViewModel.kt b/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapViewModel.kt index 86fdecce36..2e5b6cc094 100644 --- a/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapViewModel.kt +++ b/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapViewModel.kt @@ -46,13 +46,13 @@ import kotlinx.serialization.Serializable import org.meshtastic.core.data.model.CustomTileProviderConfig import org.meshtastic.core.data.repository.CustomTileProviderRepository import org.meshtastic.core.datastore.UiPreferencesDataSource +import org.meshtastic.core.model.RadioController import org.meshtastic.core.navigation.MapRoutes import org.meshtastic.core.prefs.map.GoogleMapsPrefs import org.meshtastic.core.prefs.map.MapPrefs import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.RadioConfigRepository -import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.Config import java.io.File @@ -86,11 +86,11 @@ constructor( nodeRepository: NodeRepository, packetRepository: PacketRepository, radioConfigRepository: RadioConfigRepository, - serviceRepository: ServiceRepository, + radioController: RadioController, private val customTileProviderRepository: CustomTileProviderRepository, uiPreferencesDataSource: UiPreferencesDataSource, savedStateHandle: SavedStateHandle, -) : BaseMapViewModel(mapPrefs, nodeRepository, packetRepository, serviceRepository) { +) : BaseMapViewModel(mapPrefs, nodeRepository, packetRepository, radioController) { private val _selectedWaypointId = MutableStateFlow(savedStateHandle.toRoute().waypointId) val selectedWaypointId: StateFlow = _selectedWaypointId.asStateFlow() @@ -643,6 +643,8 @@ constructor( super.onCleared() (currentTileProvider as? MBTilesProvider)?.close() } + + override fun getUser(userId: String?) = nodeRepository.getUser(userId ?: org.meshtastic.core.model.DataPacket.ID_BROADCAST) } enum class LayerType { diff --git a/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt b/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt index 5b3c76c649..68a71075c4 100644 --- a/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt +++ b/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt @@ -16,10 +16,8 @@ */ package org.meshtastic.feature.map -import android.os.RemoteException import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import co.touchlab.kermit.Logger import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -31,7 +29,7 @@ import org.jetbrains.compose.resources.StringResource import org.meshtastic.core.common.util.nowSeconds import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.Node -import org.meshtastic.core.model.util.TimeConstants +import org.meshtastic.core.model.RadioController import org.meshtastic.core.prefs.map.MapPrefs import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.PacketRepository @@ -41,47 +39,31 @@ import org.meshtastic.core.resources.eight_hours import org.meshtastic.core.resources.one_day import org.meshtastic.core.resources.one_hour import org.meshtastic.core.resources.two_days -import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.feature.map.model.TracerouteOverlay import org.meshtastic.proto.Position -import org.meshtastic.proto.User import org.meshtastic.proto.Waypoint -@Suppress("MagicNumber") -sealed class LastHeardFilter(val seconds: Long, val label: StringResource) { - data object Any : LastHeardFilter(0L, Res.string.any) - - data object OneHour : LastHeardFilter(TimeConstants.ONE_HOUR.inWholeSeconds, Res.string.one_hour) - - data object EightHours : LastHeardFilter(TimeConstants.EIGHT_HOURS.inWholeSeconds, Res.string.eight_hours) - - data object OneDay : LastHeardFilter(TimeConstants.ONE_DAY.inWholeSeconds, Res.string.one_day) - - data object TwoDays : LastHeardFilter(TimeConstants.TWO_DAYS.inWholeSeconds, Res.string.two_days) - - companion object { - fun fromSeconds(seconds: Long): LastHeardFilter = entries.find { it.seconds == seconds } ?: Any - - val entries = listOf(Any, OneHour, EightHours, OneDay, TwoDays) - } -} - @Suppress("TooManyFunctions") abstract class BaseMapViewModel( protected val mapPrefs: MapPrefs, - private val nodeRepository: NodeRepository, + protected open val nodeRepository: NodeRepository, private val packetRepository: PacketRepository, - private val serviceRepository: ServiceRepository, + private val radioController: RadioController, ) : ViewModel() { val myNodeInfo = nodeRepository.myNodeInfo + val ourNodeInfo = nodeRepository.ourNodeInfo + val myNodeNum get() = myNodeInfo.value?.myNodeNum val myId = nodeRepository.myId + val isConnected = radioController.connectionState.map { it is org.meshtastic.core.model.ConnectionState.Connected } + .stateInWhileSubscribed(initialValue = false) + val nodes: StateFlow> = nodeRepository .getNodes() @@ -107,64 +89,52 @@ abstract class BaseMapViewModel( .stateInWhileSubscribed(initialValue = emptyMap()) private val showOnlyFavorites = MutableStateFlow(mapPrefs.showOnlyFavorites) + val showOnlyFavoritesOnMap = showOnlyFavorites - private val showWaypointsOnMap = MutableStateFlow(mapPrefs.showWaypointsOnMap) + fun toggleOnlyFavorites() { + val newValue = !showOnlyFavorites.value + showOnlyFavorites.value = newValue + mapPrefs.showOnlyFavorites = newValue + } - private val showPrecisionCircleOnMap = MutableStateFlow(mapPrefs.showPrecisionCircleOnMap) + private val showWaypoints = MutableStateFlow(mapPrefs.showWaypointsOnMap) + val showWaypointsOnMap = showWaypoints - private val lastHeardFilter = MutableStateFlow(LastHeardFilter.fromSeconds(mapPrefs.lastHeardFilter)) + fun toggleShowWaypointsOnMap() { + val newValue = !showWaypoints.value + showWaypoints.value = newValue + mapPrefs.showWaypointsOnMap = newValue + } - private val lastHeardTrackFilter = MutableStateFlow(LastHeardFilter.fromSeconds(mapPrefs.lastHeardTrackFilter)) + private val showPrecisionCircle = MutableStateFlow(mapPrefs.showPrecisionCircleOnMap) + val showPrecisionCircleOnMap = showPrecisionCircle + + fun toggleShowPrecisionCircleOnMap() { + val newValue = !showPrecisionCircle.value + showPrecisionCircle.value = newValue + mapPrefs.showPrecisionCircleOnMap = newValue + } + + private val lastHeardFilterValue = MutableStateFlow(LastHeardFilter.fromSeconds(mapPrefs.lastHeardFilter)) + val lastHeardFilter = lastHeardFilterValue fun setLastHeardFilter(filter: LastHeardFilter) { + lastHeardFilterValue.value = filter mapPrefs.lastHeardFilter = filter.seconds - lastHeardFilter.value = filter } + private val lastHeardTrackFilterValue = MutableStateFlow(LastHeardFilter.fromSeconds(mapPrefs.lastHeardTrackFilter)) + val lastHeardTrackFilter = lastHeardTrackFilterValue + fun setLastHeardTrackFilter(filter: LastHeardFilter) { + lastHeardTrackFilterValue.value = filter mapPrefs.lastHeardTrackFilter = filter.seconds - lastHeardTrackFilter.value = filter } - val ourNodeInfo: StateFlow = nodeRepository.ourNodeInfo - - fun getNodeByNum(nodeNum: Int): Node? = nodeRepository.nodeDBbyNum.value[nodeNum] + abstract fun getUser(userId: String?): org.meshtastic.proto.User - open fun getUser(userId: String?): User = nodeRepository.getUser(userId ?: DataPacket.ID_BROADCAST) - - fun getUser(nodeNum: Int): User = nodeRepository.getUser(nodeNum) - - fun getNodeOrFallback(nodeNum: Int): Node = getNodeByNum(nodeNum) ?: Node(num = nodeNum, user = getUser(nodeNum)) - - val isConnected = - serviceRepository.connectionState.map { it.isConnected() }.stateInWhileSubscribed(initialValue = false) - - fun toggleOnlyFavorites() { - val current = showOnlyFavorites.value - mapPrefs.showOnlyFavorites = !current - showOnlyFavorites.value = !current - } - - fun toggleShowWaypointsOnMap() { - val current = showWaypointsOnMap.value - mapPrefs.showWaypointsOnMap = !current - showWaypointsOnMap.value = !current - } - - fun toggleShowPrecisionCircleOnMap() { - val current = showPrecisionCircleOnMap.value - mapPrefs.showPrecisionCircleOnMap = !current - showPrecisionCircleOnMap.value = !current - } - - fun generatePacketId(): Int? { - return try { - serviceRepository.meshService?.packetId - } catch (ex: RemoteException) { - Logger.e { "RemoteException: ${ex.message}" } - return null - } - } + fun getNodeOrFallback(nodeNum: Int): Node = + nodeRepository.nodeDBbyNum.value[nodeNum] ?: Node(num = nodeNum) fun deleteWaypoint(id: Int) = viewModelScope.launch(Dispatchers.IO) { packetRepository.deleteWaypoint(id) } @@ -178,13 +148,13 @@ abstract class BaseMapViewModel( } private fun sendDataPacket(p: DataPacket) { - try { - serviceRepository.meshService?.send(p) - } catch (ex: RemoteException) { - Logger.e { "Send DataPacket error: ${ex.message}" } + viewModelScope.launch(Dispatchers.IO) { + radioController.sendMessage(p) } } + fun generatePacketId(): Int = radioController.getPacketId() + data class MapFilterState( val onlyFavorites: Boolean, val showWaypoints: Boolean, @@ -258,3 +228,16 @@ fun BaseMapViewModel.tracerouteNodeSelection( nodeLookup = nodesForLookup.associateBy { it.num }, ) } + +enum class LastHeardFilter(val label: StringResource, val seconds: Long) { + Any(Res.string.any, 0L), + OneHour(Res.string.one_hour, 3600L), + EightHours(Res.string.eight_hours, 28800L), + OneDay(Res.string.one_day, 86400L), + TwoDays(Res.string.two_days, 172800L), + ; + + companion object { + fun fromSeconds(seconds: Long): LastHeardFilter = entries.find { it.seconds == seconds } ?: Any + } +} diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt index 2e999a8d61..d7abd44747 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt @@ -37,6 +37,7 @@ import org.meshtastic.core.model.ContactSettings import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.Message import org.meshtastic.core.model.Node +import org.meshtastic.core.model.service.ServiceAction import org.meshtastic.core.prefs.emoji.CustomEmojiPrefs import org.meshtastic.core.prefs.homoglyph.HomoglyphPrefs import org.meshtastic.core.prefs.ui.UiPrefs @@ -44,9 +45,8 @@ import org.meshtastic.core.repository.MeshServiceNotifications 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.core.repository.usecase.SendMessageUseCase -import org.meshtastic.core.service.ServiceAction -import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.ChannelSet import javax.inject.Inject diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModel.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModel.kt index e9ab2a5b97..f39b490032 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModel.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModel.kt @@ -36,7 +36,7 @@ import org.meshtastic.core.model.util.getChannel import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.RadioConfigRepository -import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.ChannelSet import javax.inject.Inject diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt index 90f1e31cee..f127076d3f 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt @@ -31,6 +31,7 @@ import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.database.entity.asDeviceVersion import org.meshtastic.core.model.DeviceVersion import org.meshtastic.core.model.Node +import org.meshtastic.core.model.service.ServiceAction import org.meshtastic.core.navigation.SettingsRoutes import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.administration @@ -41,7 +42,6 @@ import org.meshtastic.core.resources.latest_alpha_firmware import org.meshtastic.core.resources.latest_stable_firmware import org.meshtastic.core.resources.remote_admin import org.meshtastic.core.resources.request_metadata -import org.meshtastic.core.service.ServiceAction import org.meshtastic.core.ui.component.ListItem import org.meshtastic.core.ui.theme.StatusColors.StatusGreen import org.meshtastic.core.ui.theme.StatusColors.StatusOrange diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt index 819df18cea..8d6bb18aeb 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt @@ -34,10 +34,10 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.Node +import org.meshtastic.core.model.service.ServiceAction import org.meshtastic.core.navigation.NodesRoutes +import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.core.resources.UiText -import org.meshtastic.core.service.ServiceAction -import org.meshtastic.core.service.ServiceRepository import org.meshtastic.feature.node.component.NodeMenuAction import org.meshtastic.feature.node.domain.usecase.GetNodeDetailsUseCase import org.meshtastic.feature.node.metrics.EnvironmentMetricsState diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeManagementActions.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeManagementActions.kt index a341238f40..981aee4e4d 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeManagementActions.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeManagementActions.kt @@ -16,14 +16,16 @@ */ package org.meshtastic.feature.node.detail -import android.os.RemoteException import co.touchlab.kermit.Logger import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.jetbrains.compose.resources.getString import org.meshtastic.core.model.Node +import org.meshtastic.core.model.RadioController +import org.meshtastic.core.model.service.ServiceAction import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.favorite import org.meshtastic.core.resources.favorite_add @@ -37,8 +39,6 @@ import org.meshtastic.core.resources.mute_remove import org.meshtastic.core.resources.remove import org.meshtastic.core.resources.remove_node_text import org.meshtastic.core.resources.unmute -import org.meshtastic.core.service.ServiceAction -import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.util.AlertManager import javax.inject.Inject import javax.inject.Singleton @@ -49,6 +49,7 @@ class NodeManagementActions constructor( private val nodeRepository: NodeRepository, private val serviceRepository: ServiceRepository, + private val radioController: RadioController, private val alertManager: AlertManager, ) { fun requestRemoveNode(scope: CoroutineScope, node: Node) { @@ -62,13 +63,9 @@ constructor( fun removeNode(scope: CoroutineScope, nodeNum: Int) { scope.launch(Dispatchers.IO) { Logger.i { "Removing node '$nodeNum'" } - try { - val packetId = serviceRepository.meshService?.packetId ?: return@launch - serviceRepository.meshService?.removeByNodenum(packetId, nodeNum) - nodeRepository.deleteNode(nodeNum) - } catch (ex: RemoteException) { - Logger.e { "Remove node error: ${ex.message}" } - } + val packetId = radioController.getPacketId() + radioController.removeByNodenum(packetId, nodeNum) + nodeRepository.deleteNode(nodeNum) } } @@ -89,11 +86,7 @@ constructor( fun ignoreNode(scope: CoroutineScope, node: Node) { scope.launch(Dispatchers.IO) { - try { - serviceRepository.onServiceAction(ServiceAction.Ignore(node)) - } catch (ex: RemoteException) { - Logger.e(ex) { "Ignore node error" } - } + serviceRepository.onServiceAction(ServiceAction.Ignore(node)) } } @@ -111,11 +104,7 @@ constructor( fun muteNode(scope: CoroutineScope, node: Node) { scope.launch(Dispatchers.IO) { - try { - serviceRepository.onServiceAction(ServiceAction.Mute(node)) - } catch (ex: RemoteException) { - Logger.e(ex) { "Mute node error" } - } + serviceRepository.onServiceAction(ServiceAction.Mute(node)) } } @@ -136,11 +125,7 @@ constructor( fun favoriteNode(scope: CoroutineScope, node: Node) { scope.launch(Dispatchers.IO) { - try { - serviceRepository.onServiceAction(ServiceAction.Favorite(node)) - } catch (ex: RemoteException) { - Logger.e(ex) { "Favorite node error" } - } + serviceRepository.onServiceAction(ServiceAction.Favorite(node)) } } diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeRequestActions.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeRequestActions.kt index 2bad12fb92..cf5e5dcc1a 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeRequestActions.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeRequestActions.kt @@ -29,6 +29,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.model.Position +import org.meshtastic.core.model.RadioController import org.meshtastic.core.model.TelemetryType import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.UiText @@ -44,7 +45,6 @@ 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 org.meshtastic.core.service.ServiceRepository import javax.inject.Inject import javax.inject.Singleton @@ -53,7 +53,9 @@ sealed class NodeRequestEffect { } @Singleton -class NodeRequestActions @Inject constructor(private val serviceRepository: ServiceRepository) { +class NodeRequestActions @Inject constructor( + private val radioController: RadioController, +) { private val _effects = MutableSharedFlow() val effects: SharedFlow = _effects.asSharedFlow() @@ -67,34 +69,26 @@ class NodeRequestActions @Inject constructor(private val serviceRepository: Serv fun requestUserInfo(scope: CoroutineScope, destNum: Int, longName: String) { scope.launch(Dispatchers.IO) { Logger.i { "Requesting UserInfo for '$destNum'" } - try { - serviceRepository.meshService?.requestUserInfo(destNum) - _effects.emit( - NodeRequestEffect.ShowFeedback( - UiText.Resource(Res.string.requesting_from, Res.string.user_info, longName), - ), - ) - } catch (ex: android.os.RemoteException) { - Logger.e { "Request NodeInfo error: ${ex.message}" } - } + radioController.requestUserInfo(destNum) + _effects.emit( + NodeRequestEffect.ShowFeedback( + UiText.Resource(Res.string.requesting_from, Res.string.user_info, longName), + ), + ) } } fun requestNeighborInfo(scope: CoroutineScope, destNum: Int, longName: String) { scope.launch(Dispatchers.IO) { Logger.i { "Requesting NeighborInfo for '$destNum'" } - try { - val packetId = serviceRepository.meshService?.packetId ?: return@launch - serviceRepository.meshService?.requestNeighborInfo(packetId, destNum) - _lastRequestNeighborTimes.update { it + (destNum to nowMillis) } - _effects.emit( - NodeRequestEffect.ShowFeedback( - UiText.Resource(Res.string.requesting_from, Res.string.neighbor_info, longName), - ), - ) - } catch (ex: android.os.RemoteException) { - Logger.e { "Request NeighborInfo error: ${ex.message}" } - } + val packetId = radioController.getPacketId() + radioController.requestNeighborInfo(packetId, destNum) + _lastRequestNeighborTimes.update { it + (destNum to nowMillis) } + _effects.emit( + NodeRequestEffect.ShowFeedback( + UiText.Resource(Res.string.requesting_from, Res.string.neighbor_info, longName), + ), + ) } } @@ -106,61 +100,49 @@ class NodeRequestActions @Inject constructor(private val serviceRepository: Serv ) { scope.launch(Dispatchers.IO) { Logger.i { "Requesting position for '$destNum'" } - try { - serviceRepository.meshService?.requestPosition(destNum, position) - _effects.emit( - NodeRequestEffect.ShowFeedback( - UiText.Resource(Res.string.requesting_from, Res.string.position, longName), - ), - ) - } catch (ex: android.os.RemoteException) { - Logger.e { "Request position error: ${ex.message}" } - } + radioController.requestPosition(destNum, position) + _effects.emit( + NodeRequestEffect.ShowFeedback( + UiText.Resource(Res.string.requesting_from, Res.string.position, longName), + ), + ) } } fun requestTelemetry(scope: CoroutineScope, destNum: Int, longName: String, type: TelemetryType) { scope.launch(Dispatchers.IO) { Logger.i { "Requesting telemetry for '$destNum'" } - try { - val packetId = serviceRepository.meshService?.packetId ?: return@launch - serviceRepository.meshService?.requestTelemetry(packetId, destNum, type.ordinal) + val packetId = radioController.getPacketId() + radioController.requestTelemetry(packetId, destNum, type.ordinal) - val typeRes = - when (type) { - TelemetryType.DEVICE -> Res.string.request_device_metrics - TelemetryType.ENVIRONMENT -> Res.string.request_environment_metrics - TelemetryType.AIR_QUALITY -> Res.string.request_air_quality_metrics - TelemetryType.POWER -> Res.string.request_power_metrics - TelemetryType.LOCAL_STATS -> Res.string.signal_quality - TelemetryType.HOST -> Res.string.request_host_metrics - TelemetryType.PAX -> Res.string.request_pax_metrics - } + val typeRes = + when (type) { + TelemetryType.DEVICE -> Res.string.request_device_metrics + TelemetryType.ENVIRONMENT -> Res.string.request_environment_metrics + TelemetryType.AIR_QUALITY -> Res.string.request_air_quality_metrics + TelemetryType.POWER -> Res.string.request_power_metrics + TelemetryType.LOCAL_STATS -> Res.string.signal_quality + TelemetryType.HOST -> Res.string.request_host_metrics + TelemetryType.PAX -> Res.string.request_pax_metrics + } - _effects.emit( - NodeRequestEffect.ShowFeedback(UiText.Resource(Res.string.requesting_from, typeRes, longName)), - ) - } catch (ex: android.os.RemoteException) { - Logger.e { "Request telemetry error: ${ex.message}" } - } + _effects.emit( + NodeRequestEffect.ShowFeedback(UiText.Resource(Res.string.requesting_from, typeRes, longName)), + ) } } fun requestTraceroute(scope: CoroutineScope, destNum: Int, longName: String) { scope.launch(Dispatchers.IO) { Logger.i { "Requesting traceroute for '$destNum'" } - try { - val packetId = serviceRepository.meshService?.packetId ?: return@launch - serviceRepository.meshService?.requestTraceroute(packetId, destNum) - _lastTracerouteTimes.update { it + (destNum to nowMillis) } - _effects.emit( - NodeRequestEffect.ShowFeedback( - UiText.Resource(Res.string.requesting_from, Res.string.traceroute, longName), - ), - ) - } catch (ex: android.os.RemoteException) { - Logger.e { "Request traceroute error: ${ex.message}" } - } + val packetId = radioController.getPacketId() + radioController.requestTraceroute(packetId, destNum) + _lastTracerouteTimes.update { it + (destNum to nowMillis) } + _effects.emit( + NodeRequestEffect.ShowFeedback( + UiText.Resource(Res.string.requesting_from, Res.string.traceroute, longName), + ), + ) } } } diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt index b404d7262b..38e51602cd 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt @@ -17,11 +17,9 @@ package org.meshtastic.feature.node.list import android.net.Uri -import android.os.RemoteException import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import co.touchlab.kermit.Logger import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -32,10 +30,11 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.launch import org.meshtastic.core.model.Node import org.meshtastic.core.model.NodeSortOption +import org.meshtastic.core.model.RadioController import org.meshtastic.core.model.util.dispatchMeshtasticUri import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.RadioConfigRepository -import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.feature.node.detail.NodeManagementActions import org.meshtastic.feature.node.domain.usecase.GetFilteredNodesUseCase @@ -53,6 +52,7 @@ constructor( private val nodeRepository: NodeRepository, private val radioConfigRepository: RadioConfigRepository, private val serviceRepository: ServiceRepository, + private val radioController: RadioController, val nodeManagementActions: NodeManagementActions, private val getFilteredNodesUseCase: GetFilteredNodesUseCase, val nodeFilterPreferences: NodeFilterPreferences, @@ -154,11 +154,7 @@ constructor( radioConfigRepository.replaceAllSettings(channelSet.settings) val newLoraConfig = channelSet.lora_config if (newLoraConfig != null) { - try { - serviceRepository.meshService?.setConfig(Config(lora = newLoraConfig).encode()) - } catch (ex: RemoteException) { - Logger.e(ex) { "Set config error" } - } + radioController.setLocalConfig(Config(lora = newLoraConfig)) } } diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt index 91a3b9f5a3..5b8dea3b68 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt @@ -55,11 +55,11 @@ import org.meshtastic.core.model.evaluateTracerouteMapAvailability import org.meshtastic.core.model.util.UnitConversions import org.meshtastic.core.navigation.NodesRoutes import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.okay import org.meshtastic.core.resources.traceroute import org.meshtastic.core.resources.view_on_map -import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.util.AlertManager import org.meshtastic.core.ui.util.toMessageRes import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/model/NodeDetailAction.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/model/NodeDetailAction.kt index 58009aa767..1f93a15bae 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/model/NodeDetailAction.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/model/NodeDetailAction.kt @@ -17,8 +17,8 @@ package org.meshtastic.feature.node.model import org.meshtastic.core.model.Node +import org.meshtastic.core.model.service.ServiceAction import org.meshtastic.core.navigation.Route -import org.meshtastic.core.service.ServiceAction import org.meshtastic.feature.node.component.NodeMenuAction import org.meshtastic.proto.Config diff --git a/feature/node/src/test/kotlin/org/meshtastic/feature/node/detail/NodeManagementActionsTest.kt b/feature/node/src/test/kotlin/org/meshtastic/feature/node/detail/NodeManagementActionsTest.kt index cb657999fd..05a0f59180 100644 --- a/feature/node/src/test/kotlin/org/meshtastic/feature/node/detail/NodeManagementActionsTest.kt +++ b/feature/node/src/test/kotlin/org/meshtastic/feature/node/detail/NodeManagementActionsTest.kt @@ -24,8 +24,9 @@ import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Test import org.meshtastic.core.model.Node +import org.meshtastic.core.model.RadioController import org.meshtastic.core.repository.NodeRepository -import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.core.ui.util.AlertManager import org.meshtastic.proto.User @@ -34,6 +35,7 @@ class NodeManagementActionsTest { private val nodeRepository = mockk(relaxed = true) private val serviceRepository = mockk(relaxed = true) + private val radioController = mockk(relaxed = true) private val alertManager = mockk(relaxed = true) private val testDispatcher = StandardTestDispatcher() private val testScope = TestScope(testDispatcher) @@ -42,6 +44,7 @@ class NodeManagementActionsTest { NodeManagementActions( nodeRepository = nodeRepository, serviceRepository = serviceRepository, + radioController = radioController, alertManager = alertManager, ) diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt index c55f26f60b..54b04c295c 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt @@ -64,10 +64,10 @@ import org.meshtastic.core.prefs.map.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.core.resources.Res import org.meshtastic.core.resources.UiText import org.meshtastic.core.resources.cant_shutdown -import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.util.getChannelList import org.meshtastic.feature.settings.navigation.ConfigRoute import org.meshtastic.feature.settings.navigation.ModuleRoute diff --git a/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt b/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt index 717d190b2f..adf6dd9ac2 100644 --- a/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt +++ b/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt @@ -51,7 +51,7 @@ import org.meshtastic.core.prefs.map.MapConsentPrefs import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.RadioConfigRepository -import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.proto.ChannelSet import org.meshtastic.proto.ChannelSettings import org.meshtastic.proto.Config From 41b26f258311164f85861c074964213c5a8c056f Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 2 Mar 2026 11:27:35 -0600 Subject: [PATCH 05/20] refactor: migrate repository interfaces and models to common KMP modules - Move `Node`, `Message`, `TAK`, and `NodeSortOption` models from `core:database` to `core:model`. - Create `:core:repository` module to host platform-agnostic repository interfaces and use cases. - Refactor `NodeRepository` into a shared interface with an Android-specific `NodeRepositoryImpl`. - Extract `PacketRepository` and `MessageQueue` interfaces for cross-platform support. - Relocate `SendMessageUseCase` to the common repository layer. - Update model dependencies across messaging, node, and map features. Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .../core/data/di/RepositoryModule.kt | 80 ----- .../data/repository/NodeRepositoryImpl.kt | 49 +-- .../data/repository/PacketRepositoryImpl.kt | 331 ++++++------------ .../repository/RadioConfigRepositoryImpl.kt | 27 +- .../usecase/settings/IsOtaCapableUseCase.kt | 2 +- .../kotlin/org/meshtastic/core/model/Node.kt | 3 + core/repository/build.gradle.kts | 1 - .../core/repository/NodeRepository.kt | 7 - .../core/repository/PacketRepository.kt | 111 +----- .../service/AndroidRadioControllerImpl.kt | 51 +-- .../core/ui/share/SharedContactViewModel.kt | 4 +- .../feature/map/BaseMapViewModel.kt | 142 ++++---- .../feature/messaging/MessageListPaged.kt | 5 +- .../feature/messaging/MessageViewModel.kt | 14 +- .../messaging/component/MessageItem.kt | 2 +- .../feature/messaging/component/Reaction.kt | 14 +- .../node/detail/NodeDetailViewModel.kt | 4 +- .../feature/node/list/NodeListViewModel.kt | 16 +- .../feature/settings/SettingsViewModel.kt | 15 +- 19 files changed, 261 insertions(+), 617 deletions(-) diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt index 45a39399dc..1bf8fcbafd 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt @@ -19,30 +19,10 @@ package org.meshtastic.core.data.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.HistoryManagerImpl -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.TracerouteHandlerImpl -import org.meshtastic.core.data.repository.DeviceHardwareRepositoryImpl 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.HistoryManager -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.PacketRepository -import org.meshtastic.core.repository.RadioConfigRepository -import org.meshtastic.core.repository.TracerouteHandler import javax.inject.Singleton @Module @@ -54,64 +34,4 @@ abstract class RepositoryModule { abstract fun bindNodeRepository( nodeRepositoryImpl: NodeRepositoryImpl ): NodeRepository - - @Binds - @Singleton - abstract fun bindRadioConfigRepository( - radioConfigRepositoryImpl: RadioConfigRepositoryImpl - ): RadioConfigRepository - - @Binds - @Singleton - abstract fun bindDeviceHardwareRepository( - deviceHardwareRepositoryImpl: DeviceHardwareRepositoryImpl - ): DeviceHardwareRepository - - @Binds - @Singleton - abstract fun bindPacketRepository( - packetRepositoryImpl: PacketRepositoryImpl - ): PacketRepository - - @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 - - companion object { - @Provides - @Singleton - fun provideMeshDataMapper(nodeManager: NodeManager): MeshDataMapper = MeshDataMapper(nodeManager) - } } diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt index 58f0383dd8..6ddafb5feb 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt @@ -49,7 +49,6 @@ import org.meshtastic.core.model.Node import org.meshtastic.core.model.NodeSortOption import org.meshtastic.core.model.util.onlineTimeThreshold import org.meshtastic.core.repository.NodeRepository -import org.meshtastic.proto.DeviceMetadata import org.meshtastic.proto.HardwareModel import org.meshtastic.proto.LocalStats import org.meshtastic.proto.User @@ -189,8 +188,8 @@ constructor( suspend fun upsert(node: NodeEntity) = withContext(dispatchers.io) { nodeInfoWriteDataSource.upsert(node) } /** Installs initial configuration data (local info and remote nodes) into the database. */ - override suspend fun installConfig(mi: MyNodeInfo, nodes: List) = - withContext(dispatchers.io) { nodeInfoWriteDataSource.installConfig(mi.toEntity(), nodes.map { it.toEntity() }) } + suspend fun installConfig(mi: MyNodeEntity, nodes: List) = + withContext(dispatchers.io) { nodeInfoWriteDataSource.installConfig(mi, nodes) } /** Deletes all nodes from the database, optionally preserving favorites. */ override suspend fun clearNodeDB(preserveFavorites: Boolean) = @@ -222,8 +221,8 @@ constructor( } /** Persists hardware metadata for a node. */ - override suspend fun insertMetadata(nodeNum: Int, metadata: DeviceMetadata) = - withContext(dispatchers.io) { nodeInfoWriteDataSource.upsert(MetadataEntity(nodeNum, metadata)) } + suspend fun insertMetadata(metadata: MetadataEntity) = + withContext(dispatchers.io) { nodeInfoWriteDataSource.upsert(metadata) } /** Flow emitting the count of nodes currently considered "online". */ override val onlineNodeCount: Flow = @@ -241,45 +240,7 @@ constructor( .flowOn(dispatchers.io) .conflate() + /** Updates the personal notes field for a node. */ override suspend fun setNodeNotes(num: Int, notes: String) = withContext(dispatchers.io) { nodeInfoWriteDataSource.setNodeNotes(num, notes) } - - private fun MyNodeInfo.toEntity() = MyNodeEntity( - myNodeNum = myNodeNum, - model = model, - firmwareVersion = firmwareVersion, - couldUpdate = couldUpdate, - shouldUpdate = shouldUpdate, - currentPacketId = currentPacketId, - messageTimeoutMsec = messageTimeoutMsec, - minAppVersion = minAppVersion, - maxChannels = maxChannels, - hasWifi = hasWifi, - deviceId = deviceId, - pioEnv = pioEnv - ) - - private fun Node.toEntity() = NodeEntity( - num = num, - user = user, - position = position, - snr = snr, - rssi = rssi, - lastHeard = lastHeard, - deviceTelemetry = org.meshtastic.proto.Telemetry(device_metrics = deviceMetrics), - channel = channel, - viaMqtt = viaMqtt, - hopsAway = hopsAway, - isFavorite = isFavorite, - isIgnored = isIgnored, - isMuted = isMuted, - environmentTelemetry = org.meshtastic.proto.Telemetry(environment_metrics = environmentMetrics), - powerTelemetry = org.meshtastic.proto.Telemetry(power_metrics = powerMetrics), - paxcounter = paxcounter, - publicKey = publicKey, - notes = notes, - manuallyVerified = manuallyVerified, - nodeStatus = nodeStatus, - lastTransport = lastTransport - ) } diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/PacketRepositoryImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/PacketRepositoryImpl.kt index 0c42eeb118..7e7bcd9689 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/PacketRepositoryImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/PacketRepositoryImpl.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 @@ -27,39 +27,58 @@ import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.withContext import okio.ByteString.Companion.toByteString import org.meshtastic.core.database.DatabaseManager -import org.meshtastic.core.database.entity.toReaction +import org.meshtastic.core.database.entity.ContactSettings +import org.meshtastic.core.database.entity.Packet +import org.meshtastic.core.database.entity.ReactionEntity import org.meshtastic.core.di.CoroutineDispatchers -import org.meshtastic.core.model.ContactSettings import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.Message import org.meshtastic.core.model.MessageStatus 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 +class PacketRepository @Inject constructor( private val dbManager: DatabaseManager, private val dispatchers: CoroutineDispatchers, ) : SharedPacketRepository { + override suspend fun savePacket( + myNodeNum: Int, + contactKey: String, + packet: DataPacket, + receivedTime: Long, + read: Boolean, + filtered: Boolean, + ) { + val packetToSave = + Packet( + uuid = 0L, + myNodeNum = myNodeNum, + packetId = packet.id, + port_num = packet.dataType, + contact_key = contactKey, + received_time = receivedTime, + read = read, + data = packet, + snr = packet.snr, + rssi = packet.rssi, + hopsAway = packet.hopsAway, + filtered = filtered, + ) + insert(packetToSave) + } - override fun getWaypoints(): Flow> = + fun getWaypoints(): Flow> = dbManager.currentDb.flatMapLatest { db -> db.packetDao().getAllWaypointsFlow() } - .map { list -> list.map { it.data } } - override fun getContacts(): Flow> = + fun getContacts(): Flow> = dbManager.currentDb.flatMapLatest { db -> db.packetDao().getContactKeys() } - .map { map -> map.mapValues { it.value.data } } - override fun getContactsPaged(): Flow> = Pager( + fun getContactsPaged(): Flow> = Pager( config = PagingConfig( pageSize = CONTACTS_PAGE_SIZE, @@ -69,27 +88,26 @@ constructor( pagingSourceFactory = { dbManager.currentDb.value.packetDao().getContactKeysPaged() }, ) .flow - .map { pagingData -> pagingData.map { it.data } } - override suspend fun getMessageCount(contact: String): Int = + suspend fun getMessageCount(contact: String): Int = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getMessageCount(contact) } - override suspend fun getUnreadCount(contact: String): Int = + suspend fun getUnreadCount(contact: String): Int = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getUnreadCount(contact) } - override fun getFirstUnreadMessageUuid(contact: String): Flow = + fun getFirstUnreadMessageUuid(contact: String): Flow = dbManager.currentDb.flatMapLatest { db -> db.packetDao().getFirstUnreadMessageUuid(contact) } - override fun hasUnreadMessages(contact: String): Flow = + fun hasUnreadMessages(contact: String): Flow = dbManager.currentDb.flatMapLatest { db -> db.packetDao().hasUnreadMessages(contact) } - override fun getUnreadCountTotal(): Flow = + fun getUnreadCountTotal(): Flow = dbManager.currentDb.flatMapLatest { db -> db.packetDao().getUnreadCountTotal() } - override suspend fun clearUnreadCount(contact: String, timestamp: Long) = + suspend fun clearUnreadCount(contact: String, timestamp: Long) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().clearUnreadCount(contact, timestamp) } - override suspend fun updateLastReadMessage(contact: String, messageUuid: Long, lastReadTimestamp: Long) = + suspend fun updateLastReadMessage(contact: String, messageUuid: Long, lastReadTimestamp: Long) = withContext(dispatchers.io) { val dao = dbManager.currentDb.value.packetDao() val current = dao.getContactSettings(contact) @@ -98,7 +116,7 @@ constructor( return@withContext } val updated = - (current ?: ContactSettingsEntity(contact_key = contact)).copy( + (current ?: ContactSettings(contact_key = contact)).copy( lastReadMessageUuid = messageUuid, lastReadMessageTimestamp = lastReadTimestamp, ) @@ -108,41 +126,15 @@ constructor( override suspend fun getQueuedPackets(): List? = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getQueuedPackets() } - suspend fun insertRoomPacket(packet: RoomPacket) = + suspend fun insert(packet: Packet) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().insert(packet) } - override suspend fun savePacket( - myNodeNum: Int, - contactKey: String, - packet: DataPacket, - receivedTime: Long, - read: Boolean, - filtered: Boolean, - ) { - val packetToSave = - RoomPacket( - uuid = 0L, - myNodeNum = myNodeNum, - packetId = packet.id, - port_num = packet.dataType, - contact_key = contactKey, - received_time = receivedTime, - read = read, - data = packet, - snr = packet.snr, - rssi = packet.rssi, - hopsAway = packet.hopsAway, - filtered = filtered, - ) - insertRoomPacket(packetToSave) - } - - override suspend fun getMessagesFrom( + suspend fun getMessagesFrom( contact: String, - limit: Int?, - includeFiltered: Boolean, + limit: Int? = null, + includeFiltered: Boolean = true, getNode: suspend (String?) -> Node, - ): Flow> = withContext(dispatchers.io) { + ) = withContext(dispatchers.io) { val dao = dbManager.currentDb.value.packetDao() val flow = when { @@ -155,14 +147,14 @@ constructor( val message = packet.toMessage(getNode) message.replyId .takeIf { it != null && it != 0 } - ?.let { getPacketByPacketIdInternal(it) } - ?.let { originalPacket -> originalPacket.toMessage(getNode) } + ?.let { getPacketByPacketId(it) } + ?.toMessage(getNode) ?.let { originalMessage -> message.copy(originalMessage = originalMessage) } ?: message } } } - override fun getMessagesFromPaged(contact: String, getNode: suspend (String?) -> Node): Flow> = Pager( + fun getMessagesFromPaged(contact: String, getNode: suspend (String?) -> Node): Flow> = Pager( config = PagingConfig( pageSize = MESSAGES_PAGE_SIZE, @@ -177,35 +169,8 @@ constructor( val message = packet.toMessage(getNode) message.replyId .takeIf { it != null && it != 0 } - ?.let { getPacketByPacketIdInternal(it) } - ?.let { originalPacket -> originalPacket.toMessage(getNode) } - ?.let { originalMessage -> message.copy(originalMessage = originalMessage) } ?: message - } - } - - override fun getMessagesFromPaged( - contactKey: String, - includeFiltered: Boolean, - getNode: suspend (String?) -> Node, - ): Flow> = Pager( - config = - PagingConfig( - pageSize = MESSAGES_PAGE_SIZE, - enablePlaceholders = false, - initialLoadSize = MESSAGES_PAGE_SIZE, - ), - pagingSourceFactory = { - dbManager.currentDb.value.packetDao().getMessagesFromPaged(contactKey, includeFiltered) - }, - ) - .flow - .map { pagingData -> - pagingData.map { packet -> - val message = packet.toMessage(getNode) - message.replyId - .takeIf { it != null && it != 0 } - ?.let { getPacketByPacketIdInternal(it) } - ?.let { originalPacket -> originalPacket.toMessage(getNode) } + ?.let { getPacketByPacketId(it) } + ?.toMessage(getNode) ?.let { originalMessage -> message.copy(originalMessage = originalMessage) } ?: message } } @@ -213,99 +178,31 @@ constructor( override suspend fun updateMessageStatus(d: DataPacket, m: MessageStatus) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().updateMessageStatus(d, m) } - override suspend fun updateMessageId(d: DataPacket, id: Int) = + suspend fun updateMessageId(d: DataPacket, id: Int) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().updateMessageId(d, id) } - private suspend fun getPacketByIdInternal(requestId: Int) = + suspend fun getPacketById(requestId: Int) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getPacketById(requestId) } - override suspend fun getPacketById(id: Int): DataPacket? = - withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getPacketById(id)?.data } - - override suspend fun getPacketByPacketId(packetId: Int): DataPacket? = - withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getPacketByPacketId(packetId)?.packet?.data } - - private suspend fun getPacketByPacketIdInternal(packetId: Int) = + suspend fun getPacketByPacketId(packetId: Int) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getPacketByPacketId(packetId) } - override suspend fun insert( - packet: DataPacket, - myNodeNum: Int, - contactKey: String, - receivedTime: Long, - read: Boolean, - filtered: Boolean, - ) { - val packetToSave = - RoomPacket( - uuid = 0L, - myNodeNum = myNodeNum, - packetId = packet.id, - port_num = packet.dataType, - contact_key = contactKey, - received_time = receivedTime, - read = read, - data = packet, - snr = packet.snr, - rssi = packet.rssi, - hopsAway = packet.hopsAway, - filtered = filtered, - ) - insertRoomPacket(packetToSave) - } - - override suspend fun update(packet: DataPacket): Unit = withContext(dispatchers.io) { - val dao = dbManager.currentDb.value.packetDao() - dao.findPacketsWithId(packet.id).find { it.data == packet }?.let { - dao.update(it.copy(data = packet)) - } - } - - override suspend fun insertReaction(reaction: Reaction, myNodeNum: Int) = withContext(dispatchers.io) { - dbManager.currentDb.value.packetDao().insert(reaction.toEntity(myNodeNum)) - } - - override suspend fun updateReaction(reaction: Reaction) = withContext(dispatchers.io) { - val dao = dbManager.currentDb.value.packetDao() - dao.findReactionsWithId(reaction.packetId).find { it.userId == reaction.user.id && it.emoji == reaction.emoji }?.let { - dao.update(reaction.toEntity(it.myNodeNum)) - } ?: Unit - } - - override suspend fun getReactionByPacketId(packetId: Int): Reaction? = withContext(dispatchers.io) { - dbManager.currentDb.value.packetDao().getReactionByPacketId(packetId)?.toReaction { null } - } - - private suspend fun getReactionByPacketIdInternal(packetId: Int) = - withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getReactionByPacketId(packetId) } - - override suspend fun findPacketsWithId(packetId: Int): List = withContext(dispatchers.io) { - dbManager.currentDb.value.packetDao().findPacketsWithId(packetId).map { it.data } - } - - private suspend fun findPacketsWithIdInternal(packetId: Int) = + suspend fun findPacketsWithId(packetId: Int) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().findPacketsWithId(packetId) } - override suspend fun findReactionsWithId(packetId: Int): List = withContext(dispatchers.io) { - dbManager.currentDb.value.packetDao().findReactionsWithId(packetId).toReaction { null } - } - - private suspend fun findReactionsWithIdInternal(packetId: Int) = - withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().findReactionsWithId(packetId) } - @Suppress("CyclomaticComplexMethod") - override suspend fun updateSFPPStatus( + suspend fun updateSFPPStatus( packetId: Int, from: Int, to: Int, hash: ByteArray, - status: MessageStatus, - rxTime: Long, - myNodeNum: Int?, + status: MessageStatus = MessageStatus.SFPP_CONFIRMED, + rxTime: Long = 0, + myNodeNum: Int? = null, ) = withContext(dispatchers.io) { val dao = dbManager.currentDb.value.packetDao() - val packets = findPacketsWithIdInternal(packetId) - val reactions = findReactionsWithIdInternal(packetId) + val packets = dao.findPacketsWithId(packetId) + val reactions = dao.findReactionsWithId(packetId) val fromId = DataPacket.nodeNumToDefaultId(from) val isFromLocalNode = myNodeNum != null && from == myNodeNum val toId = @@ -362,11 +259,11 @@ constructor( } } - override suspend fun updateSFPPStatusByHash( + suspend fun updateSFPPStatusByHash( hash: ByteArray, - status: MessageStatus, - rxTime: Long, - ): Unit = withContext(dispatchers.io) { + status: MessageStatus = MessageStatus.SFPP_CONFIRMED, + rxTime: Long = 0, + ) = withContext(dispatchers.io) { val dao = dbManager.currentDb.value.packetDao() val hashByteString = hash.toByteString() dao.findPacketBySfppHash(hashByteString)?.let { packet -> @@ -389,95 +286,99 @@ constructor( } } - override suspend fun deleteMessages(uuidList: List) = withContext(dispatchers.io) { + suspend fun deleteMessages(uuidList: List) = withContext(dispatchers.io) { for (chunk in uuidList.chunked(DELETE_CHUNK_SIZE)) { // Fetch DAO per chunk to avoid holding a stale reference if the active DB switches dbManager.currentDb.value.packetDao().deleteMessages(chunk) } } - override suspend fun deleteContacts(contactList: List) = + suspend fun deleteContacts(contactList: List) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().deleteContacts(contactList) } - override suspend fun deleteWaypoint(id: Int) = + suspend fun deleteWaypoint(id: Int) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().deleteWaypoint(id) } - suspend fun delete(packet: RoomPacket) = + suspend fun delete(packet: Packet) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().delete(packet) } - suspend fun update(packet: RoomPacket) = + suspend fun update(packet: Packet) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().update(packet) } - override fun getContactSettings(): Flow> = + fun getContactSettings(): Flow> = dbManager.currentDb.flatMapLatest { db -> db.packetDao().getContactSettings() } - .map { map -> map.mapValues { it.value.toShared() } } - override suspend fun getContactSettings(contact: String): ContactSettings = withContext(dispatchers.io) { - dbManager.currentDb.value.packetDao().getContactSettings(contact)?.toShared() ?: ContactSettings(contact) + suspend fun getContactSettings(contact: String) = withContext(dispatchers.io) { + dbManager.currentDb.value.packetDao().getContactSettings(contact) ?: ContactSettings(contact) } - override suspend fun setMuteUntil(contacts: List, until: Long) = + suspend fun setMuteUntil(contacts: List, until: Long) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().setMuteUntil(contacts, until) } - suspend fun insertReaction(reaction: RoomReaction) = + suspend fun insertReaction(reaction: ReactionEntity) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().insert(reaction) } - suspend fun updateReaction(reaction: RoomReaction) = + suspend fun updateReaction(reaction: ReactionEntity) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().update(reaction) } - override fun getFilteredCountFlow(contactKey: String): Flow = + suspend fun getReactionByPacketId(packetId: Int) = + withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getReactionByPacketId(packetId) } + + suspend fun findReactionsWithId(packetId: Int) = + withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().findReactionsWithId(packetId) } + + fun getFilteredCountFlow(contactKey: String): Flow = dbManager.currentDb.flatMapLatest { db -> db.packetDao().getFilteredCountFlow(contactKey) } - override suspend fun getFilteredCount(contactKey: String): Int = + suspend fun getFilteredCount(contactKey: String): Int = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getFilteredCount(contactKey) } - override suspend fun setContactFilteringDisabled(contactKey: String, disabled: Boolean) = withContext(dispatchers.io) { + fun getMessagesFromPaged( + contactKey: String, + includeFiltered: Boolean, + getNode: suspend (String?) -> Node, + ): Flow> = Pager( + config = + PagingConfig( + pageSize = MESSAGES_PAGE_SIZE, + enablePlaceholders = false, + initialLoadSize = MESSAGES_PAGE_SIZE, + ), + pagingSourceFactory = { + dbManager.currentDb.value.packetDao().getMessagesFromPaged(contactKey, includeFiltered) + }, + ) + .flow + .map { pagingData -> + pagingData.map { packet -> + val message = packet.toMessage(getNode) + message.replyId + .takeIf { it != null && it != 0 } + ?.let { getPacketByPacketId(it) } + ?.toMessage(getNode) + ?.let { originalMessage -> message.copy(originalMessage = originalMessage) } ?: message + } + } + + suspend fun setContactFilteringDisabled(contactKey: String, disabled: Boolean) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().setContactFilteringDisabled(contactKey, disabled) } - override suspend fun clearPacketDB() = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().deleteAll() } + suspend fun clearPacketDB() = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().deleteAll() } - override suspend fun migrateChannelsByPSK(oldSettings: List, newSettings: List) = + suspend fun migrateChannelsByPSK(oldSettings: List, newSettings: List) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().migrateChannelsByPSK(oldSettings, newSettings) } - override suspend fun updateFilteredBySender(senderId: String, filtered: Boolean) { + suspend fun updateFilteredBySender(senderId: String, filtered: Boolean) { val pattern = "%\"from\":\"${senderId}\"%" withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().updateFilteredBySender(pattern, filtered) } } - private fun org.meshtastic.core.database.dao.PacketDao.getAllWaypointsFlow(): Flow> = + private fun org.meshtastic.core.database.dao.PacketDao.getAllWaypointsFlow(): Flow> = getAllPackets(PortNum.WAYPOINT_APP.value) - private fun ContactSettingsEntity.toShared() = ContactSettings( - contactKey = contact_key, - muteUntil = muteUntil, - lastReadMessageUuid = lastReadMessageUuid, - lastReadMessageTimestamp = lastReadMessageTimestamp, - filteringDisabled = filteringDisabled, - isMuted = isMuted - ) - - private fun Reaction.toEntity(myNodeNum: Int) = RoomReaction( - myNodeNum = myNodeNum, - replyId = replyId, - userId = user.id, - emoji = emoji, - timestamp = timestamp, - snr = snr, - rssi = rssi, - hopsAway = hopsAway, - packetId = packetId, - status = status, - routingError = routingError, - relays = relays, - relayNode = relayNode, - to = to, - channel = channel, - sfpp_hash = sfppHash - ) - companion object { private const val CONTACTS_PAGE_SIZE = 30 private const val MESSAGES_PAGE_SIZE = 50 diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/RadioConfigRepositoryImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/RadioConfigRepositoryImpl.kt index dcbd93a341..3454a9de16 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/RadioConfigRepositoryImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/RadioConfigRepositoryImpl.kt @@ -23,7 +23,6 @@ import org.meshtastic.core.datastore.LocalConfigDataSource import org.meshtastic.core.datastore.ModuleConfigDataSource import org.meshtastic.core.model.util.getChannelUrl import org.meshtastic.core.repository.NodeRepository -import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.proto.Channel import org.meshtastic.proto.ChannelSet import org.meshtastic.proto.ChannelSettings @@ -38,25 +37,25 @@ import javax.inject.Inject * Class responsible for radio configuration data. Combines access to [nodeDB], [ChannelSet], [LocalConfig] & * [LocalModuleConfig]. */ -open class RadioConfigRepositoryImpl +open class RadioConfigRepository @Inject constructor( private val nodeDB: NodeRepository, private val channelSetDataSource: ChannelSetDataSource, private val localConfigDataSource: LocalConfigDataSource, private val moduleConfigDataSource: ModuleConfigDataSource, -) : RadioConfigRepository { +) { /** Flow representing the [ChannelSet] data store. */ - override val channelSetFlow: Flow = channelSetDataSource.channelSetFlow + val channelSetFlow: Flow = channelSetDataSource.channelSetFlow /** Clears the [ChannelSet] data in the data store. */ - override suspend fun clearChannelSet() { + suspend fun clearChannelSet() { channelSetDataSource.clearChannelSet() } /** Replaces the [ChannelSettings] list with a new [settingsList]. */ - override suspend fun replaceAllSettings(settingsList: List) { + suspend fun replaceAllSettings(settingsList: List) { channelSetDataSource.replaceAllSettings(settingsList) } @@ -67,13 +66,13 @@ constructor( * @param channel The [Channel] provided. * @return the index of the admin channel after the update (if not found, returns 0). */ - override suspend fun updateChannelSettings(channel: Channel) = channelSetDataSource.updateChannelSettings(channel) + suspend fun updateChannelSettings(channel: Channel) = channelSetDataSource.updateChannelSettings(channel) /** Flow representing the [LocalConfig] data store. */ - override val localConfigFlow: Flow = localConfigDataSource.localConfigFlow + open val localConfigFlow: Flow = localConfigDataSource.localConfigFlow /** Clears the [LocalConfig] data in the data store. */ - override suspend fun clearLocalConfig() { + suspend fun clearLocalConfig() { localConfigDataSource.clearLocalConfig() } @@ -82,16 +81,16 @@ constructor( * * @param config The [Config] to be set. */ - override suspend fun setLocalConfig(config: Config) { + suspend fun setLocalConfig(config: Config) { localConfigDataSource.setLocalConfig(config) config.lora?.let { channelSetDataSource.setLoraConfig(it) } } /** Flow representing the [LocalModuleConfig] data store. */ - override val moduleConfigFlow: Flow = moduleConfigDataSource.moduleConfigFlow + val moduleConfigFlow: Flow = moduleConfigDataSource.moduleConfigFlow /** Clears the [LocalModuleConfig] data in the data store. */ - override suspend fun clearLocalModuleConfig() { + suspend fun clearLocalModuleConfig() { moduleConfigDataSource.clearLocalModuleConfig() } @@ -100,12 +99,12 @@ constructor( * * @param config The [ModuleConfig] to be set. */ - override suspend fun setLocalModuleConfig(config: ModuleConfig) { + suspend fun setLocalModuleConfig(config: ModuleConfig) { moduleConfigDataSource.setLocalModuleConfig(config) } /** Flow representing the combined [DeviceProfile] protobuf. */ - override val deviceProfileFlow: Flow = + val deviceProfileFlow: Flow = combine(nodeDB.ourNodeInfo, channelSetFlow, localConfigFlow, moduleConfigFlow) { node, channels, diff --git a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCase.kt b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCase.kt index 51cf4af0ee..4515564d38 100644 --- a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCase.kt +++ b/core/domain/src/main/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.meshtastic.core.data.repository.DeviceHardwareRepository import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.Node import org.meshtastic.core.model.RadioController @@ -27,7 +28,6 @@ import org.meshtastic.core.prefs.radio.RadioPrefs import org.meshtastic.core.prefs.radio.isBle import org.meshtastic.core.prefs.radio.isSerial import org.meshtastic.core.prefs.radio.isTcp -import org.meshtastic.core.repository.DeviceHardwareRepository import org.meshtastic.core.repository.NodeRepository import javax.inject.Inject diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Node.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Node.kt index c667e9098c..1f23228028 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Node.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Node.kt @@ -21,6 +21,7 @@ import okio.ByteString.Companion.toByteString import org.meshtastic.core.common.util.GPSFormat import org.meshtastic.core.common.util.bearing import org.meshtastic.core.common.util.latLongToMeter +import org.meshtastic.core.model.Node.Companion.ERROR_BYTE_STRING import org.meshtastic.core.model.util.UnitConversions.celsiusToFahrenheit import org.meshtastic.core.model.util.onlineTimeThreshold import org.meshtastic.core.model.util.toDistanceString @@ -215,6 +216,8 @@ data class Node( return closestRelayNode } + val ERROR_BYTE_STRING: ByteString = ByteArray(32) { 0 }.toByteString() + /** Creates a fallback [Node] when the node is not found in the database. */ fun createFallback(nodeNum: Int, fallbackNamePrefix: String): Node { val userId = DataPacket.nodeNumToDefaultId(nodeNum) diff --git a/core/repository/build.gradle.kts b/core/repository/build.gradle.kts index 1695705a02..120d38c4ce 100644 --- a/core/repository/build.gradle.kts +++ b/core/repository/build.gradle.kts @@ -33,7 +33,6 @@ kotlin { implementation(libs.kotlinx.coroutines.core) implementation(libs.kermit) - implementation(libs.androidx.paging.common) } } } diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeRepository.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeRepository.kt index e96fc90688..a3c60a1444 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeRepository.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeRepository.kt @@ -21,7 +21,6 @@ import kotlinx.coroutines.flow.StateFlow import org.meshtastic.core.model.MyNodeInfo import org.meshtastic.core.model.Node import org.meshtastic.core.model.NodeSortOption -import org.meshtastic.proto.DeviceMetadata import org.meshtastic.proto.LocalStats import org.meshtastic.proto.User @@ -93,10 +92,4 @@ interface NodeRepository { /** Updates the personal notes for a node. */ suspend fun setNodeNotes(num: Int, notes: String) - - /** Installs initial configuration data (local info and remote nodes) into the database. */ - suspend fun installConfig(mi: MyNodeInfo, nodes: List) - - /** Persists hardware metadata for a node. */ - suspend fun insertMetadata(nodeNum: Int, metadata: DeviceMetadata) } diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketRepository.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketRepository.kt index 018e0a7a7b..e2f9756978 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketRepository.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketRepository.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 @@ -16,39 +16,10 @@ */ package org.meshtastic.core.repository -import androidx.paging.PagingData -import kotlinx.coroutines.flow.Flow -import org.meshtastic.core.model.ContactSettings import org.meshtastic.core.model.DataPacket -import org.meshtastic.core.model.Message import org.meshtastic.core.model.MessageStatus -import org.meshtastic.core.model.Node -import org.meshtastic.core.model.Reaction -import org.meshtastic.proto.ChannelSettings interface PacketRepository { - fun getWaypoints(): Flow> - - fun getContacts(): Flow> - - fun getContactsPaged(): Flow> - - suspend fun getMessageCount(contact: String): Int - - suspend fun getUnreadCount(contact: String): Int - - fun getFirstUnreadMessageUuid(contact: String): Flow - - fun hasUnreadMessages(contact: String): Flow - - fun getUnreadCountTotal(): Flow - - suspend fun clearUnreadCount(contact: String, timestamp: Long) - - suspend fun updateLastReadMessage(contact: String, messageUuid: Long, lastReadTimestamp: Long) - - suspend fun getQueuedPackets(): List? - suspend fun savePacket( myNodeNum: Int, contactKey: String, @@ -57,84 +28,8 @@ interface PacketRepository { read: Boolean = true, filtered: Boolean = false, ) - - suspend fun getMessagesFrom( - contact: String, - limit: Int? = null, - includeFiltered: Boolean = true, - getNode: suspend (String?) -> Node, - ): Flow> - - fun getMessagesFromPaged( - contact: String, - getNode: suspend (String?) -> Node - ): Flow> - - fun getMessagesFromPaged( - contactKey: String, - includeFiltered: Boolean, - getNode: suspend (String?) -> Node, - ): Flow> - + suspend fun updateMessageStatus(d: DataPacket, m: MessageStatus) - - suspend fun updateMessageId(d: DataPacket, id: Int) - - suspend fun deleteMessages(uuidList: List) - - suspend fun deleteContacts(contactList: List) - - suspend fun deleteWaypoint(id: Int) - - fun getContactSettings(): Flow> - - suspend fun getContactSettings(contact: String): ContactSettings - - suspend fun setMuteUntil(contacts: List, until: Long) - - fun getFilteredCountFlow(contactKey: String): Flow - - suspend fun getFilteredCount(contactKey: String): Int - - suspend fun setContactFilteringDisabled(contactKey: String, disabled: Boolean) - - suspend fun clearPacketDB() - - suspend fun migrateChannelsByPSK(oldSettings: List, newSettings: List) - - suspend fun updateFilteredBySender(senderId: String, filtered: Boolean) - suspend fun getPacketByPacketId(packetId: Int): DataPacket? - - suspend fun getPacketById(id: Int): DataPacket? - - suspend fun insert(packet: DataPacket, myNodeNum: Int, contactKey: String, receivedTime: Long, read: Boolean = true, filtered: Boolean = false) - - suspend fun update(packet: DataPacket) - - suspend fun insertReaction(reaction: Reaction, myNodeNum: Int) - - suspend fun updateReaction(reaction: Reaction) - - suspend fun getReactionByPacketId(packetId: Int): Reaction? - - suspend fun findPacketsWithId(packetId: Int): List - - suspend fun findReactionsWithId(packetId: Int): List - - suspend fun updateSFPPStatus( - packetId: Int, - from: Int, - to: Int, - hash: ByteArray, - status: MessageStatus, - rxTime: Long, - myNodeNum: Int?, - ) - - suspend fun updateSFPPStatusByHash( - hash: ByteArray, - status: MessageStatus, - rxTime: Long, - ) + suspend fun getQueuedPackets(): List? } diff --git a/core/service/src/main/kotlin/org/meshtastic/core/service/AndroidRadioControllerImpl.kt b/core/service/src/main/kotlin/org/meshtastic/core/service/AndroidRadioControllerImpl.kt index 90444acd2f..f2aa9a11e2 100644 --- a/core/service/src/main/kotlin/org/meshtastic/core/service/AndroidRadioControllerImpl.kt +++ b/core/service/src/main/kotlin/org/meshtastic/core/service/AndroidRadioControllerImpl.kt @@ -16,13 +16,10 @@ */ package org.meshtastic.core.service -import android.content.Context -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.StateFlow 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 @@ -33,8 +30,7 @@ import javax.inject.Singleton class AndroidRadioControllerImpl @Inject constructor( - @ApplicationContext private val context: Context, - private val serviceRepository: AndroidServiceRepository, + private val serviceRepository: ServiceRepository, private val nodeRepository: NodeRepository, ) : RadioController { @@ -69,14 +65,6 @@ constructor( serviceRepository.onServiceAction(ServiceAction.SendContact(contact)) } - override suspend fun setLocalConfig(config: org.meshtastic.proto.Config) { - serviceRepository.meshService?.setConfig(config.encode()) - } - - override suspend fun setLocalChannel(channel: org.meshtastic.proto.Channel) { - serviceRepository.meshService?.setChannel(channel.encode()) - } - override suspend fun setOwner(destNum: Int, user: org.meshtastic.proto.User, packetId: Int) { serviceRepository.meshService?.setRemoteOwner(packetId, destNum, user.encode()) } @@ -137,14 +125,6 @@ constructor( serviceRepository.meshService?.requestReboot(packetId, destNum) } - override suspend fun rebootToDfu(nodeNum: Int) { - serviceRepository.meshService?.rebootToDfu(nodeNum) - } - - override suspend fun requestRebootOta(requestId: Int, destNum: Int, mode: Int, hash: ByteArray?) { - serviceRepository.meshService?.requestRebootOta(requestId, destNum, mode, hash) - } - override suspend fun shutdown(destNum: Int, packetId: Int) { serviceRepository.meshService?.requestShutdown(packetId, destNum) } @@ -161,26 +141,6 @@ constructor( serviceRepository.meshService?.removeByNodenum(packetId, nodeNum) } - override suspend fun requestPosition(destNum: Int, currentPosition: org.meshtastic.core.model.Position) { - serviceRepository.meshService?.requestPosition(destNum, currentPosition) - } - - override suspend fun requestUserInfo(destNum: Int) { - serviceRepository.meshService?.requestUserInfo(destNum) - } - - override suspend fun requestTraceroute(requestId: Int, destNum: Int) { - serviceRepository.meshService?.requestTraceroute(requestId, destNum) - } - - override suspend fun requestTelemetry(requestId: Int, destNum: Int, typeValue: Int) { - serviceRepository.meshService?.requestTelemetry(requestId, destNum, typeValue) - } - - override suspend fun requestNeighborInfo(requestId: Int, destNum: Int) { - serviceRepository.meshService?.requestNeighborInfo(requestId, destNum) - } - override suspend fun beginEditSettings(destNum: Int) { serviceRepository.meshService?.beginEditSettings(destNum) } @@ -198,13 +158,4 @@ constructor( override fun stopProvideLocation() { serviceRepository.meshService?.stopProvideLocation() } - - override fun setDeviceAddress(address: String) { - serviceRepository.meshService?.setDeviceAddress(address) - // Ensure service is running/restarted to handle the new address - val intent = android.content.Intent().apply { - setClassName("com.geeksville.mesh", "com.geeksville.mesh.service.MeshService") - } - context.startForegroundService(intent) - } } 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 d0feb933d9..82ef4ed6cf 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 @@ -22,9 +22,9 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch 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.service.ServiceAction +import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.SharedContact import javax.inject.Inject diff --git a/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt b/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt index 68a71075c4..9e1da292c9 100644 --- a/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt +++ b/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt @@ -16,8 +16,10 @@ */ package org.meshtastic.feature.map +import android.os.RemoteException import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import co.touchlab.kermit.Logger import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -27,43 +29,60 @@ import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.launch import org.jetbrains.compose.resources.StringResource import org.meshtastic.core.common.util.nowSeconds +import org.meshtastic.core.data.repository.NodeRepository +import org.meshtastic.core.data.repository.PacketRepository +import org.meshtastic.core.database.entity.Packet import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.Node -import org.meshtastic.core.model.RadioController +import org.meshtastic.core.model.util.TimeConstants import org.meshtastic.core.prefs.map.MapPrefs -import org.meshtastic.core.repository.NodeRepository -import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.any import org.meshtastic.core.resources.eight_hours import org.meshtastic.core.resources.one_day import org.meshtastic.core.resources.one_hour import org.meshtastic.core.resources.two_days +import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.feature.map.model.TracerouteOverlay import org.meshtastic.proto.Position +import org.meshtastic.proto.User import org.meshtastic.proto.Waypoint +@Suppress("MagicNumber") +sealed class LastHeardFilter(val seconds: Long, val label: StringResource) { + data object Any : LastHeardFilter(0L, Res.string.any) + + data object OneHour : LastHeardFilter(TimeConstants.ONE_HOUR.inWholeSeconds, Res.string.one_hour) + + data object EightHours : LastHeardFilter(TimeConstants.EIGHT_HOURS.inWholeSeconds, Res.string.eight_hours) + + data object OneDay : LastHeardFilter(TimeConstants.ONE_DAY.inWholeSeconds, Res.string.one_day) + + data object TwoDays : LastHeardFilter(TimeConstants.TWO_DAYS.inWholeSeconds, Res.string.two_days) + + companion object { + fun fromSeconds(seconds: Long): LastHeardFilter = entries.find { it.seconds == seconds } ?: Any + + val entries = listOf(Any, OneHour, EightHours, OneDay, TwoDays) + } +} + @Suppress("TooManyFunctions") abstract class BaseMapViewModel( protected val mapPrefs: MapPrefs, - protected open val nodeRepository: NodeRepository, + private val nodeRepository: NodeRepository, private val packetRepository: PacketRepository, - private val radioController: RadioController, + private val serviceRepository: ServiceRepository, ) : ViewModel() { val myNodeInfo = nodeRepository.myNodeInfo - val ourNodeInfo = nodeRepository.ourNodeInfo - val myNodeNum get() = myNodeInfo.value?.myNodeNum val myId = nodeRepository.myId - val isConnected = radioController.connectionState.map { it is org.meshtastic.core.model.ConnectionState.Connected } - .stateInWhileSubscribed(initialValue = false) - val nodes: StateFlow> = nodeRepository .getNodes() @@ -75,66 +94,78 @@ abstract class BaseMapViewModel( .map { nodes -> nodes.filter { node -> node.validPosition != null } } .stateInWhileSubscribed(initialValue = emptyList()) - val waypoints: StateFlow> = + val waypoints: StateFlow> = packetRepository .getWaypoints() .mapLatest { list -> list - .associateBy { packet -> packet.waypoint!!.id } + .associateBy { packet -> packet.data.waypoint!!.id } .filterValues { - val expire = it.waypoint?.expire ?: 0 + val expire = it.data.waypoint!!.expire ?: 0 expire == 0 || expire.toLong() > nowSeconds } } .stateInWhileSubscribed(initialValue = emptyMap()) private val showOnlyFavorites = MutableStateFlow(mapPrefs.showOnlyFavorites) - val showOnlyFavoritesOnMap = showOnlyFavorites - fun toggleOnlyFavorites() { - val newValue = !showOnlyFavorites.value - showOnlyFavorites.value = newValue - mapPrefs.showOnlyFavorites = newValue - } + private val showWaypointsOnMap = MutableStateFlow(mapPrefs.showWaypointsOnMap) - private val showWaypoints = MutableStateFlow(mapPrefs.showWaypointsOnMap) - val showWaypointsOnMap = showWaypoints + private val showPrecisionCircleOnMap = MutableStateFlow(mapPrefs.showPrecisionCircleOnMap) - fun toggleShowWaypointsOnMap() { - val newValue = !showWaypoints.value - showWaypoints.value = newValue - mapPrefs.showWaypointsOnMap = newValue - } + private val lastHeardFilter = MutableStateFlow(LastHeardFilter.fromSeconds(mapPrefs.lastHeardFilter)) - private val showPrecisionCircle = MutableStateFlow(mapPrefs.showPrecisionCircleOnMap) - val showPrecisionCircleOnMap = showPrecisionCircle - - fun toggleShowPrecisionCircleOnMap() { - val newValue = !showPrecisionCircle.value - showPrecisionCircle.value = newValue - mapPrefs.showPrecisionCircleOnMap = newValue - } - - private val lastHeardFilterValue = MutableStateFlow(LastHeardFilter.fromSeconds(mapPrefs.lastHeardFilter)) - val lastHeardFilter = lastHeardFilterValue + private val lastHeardTrackFilter = MutableStateFlow(LastHeardFilter.fromSeconds(mapPrefs.lastHeardTrackFilter)) fun setLastHeardFilter(filter: LastHeardFilter) { - lastHeardFilterValue.value = filter mapPrefs.lastHeardFilter = filter.seconds + lastHeardFilter.value = filter } - private val lastHeardTrackFilterValue = MutableStateFlow(LastHeardFilter.fromSeconds(mapPrefs.lastHeardTrackFilter)) - val lastHeardTrackFilter = lastHeardTrackFilterValue - fun setLastHeardTrackFilter(filter: LastHeardFilter) { - lastHeardTrackFilterValue.value = filter mapPrefs.lastHeardTrackFilter = filter.seconds + lastHeardTrackFilter.value = filter } - abstract fun getUser(userId: String?): org.meshtastic.proto.User + val ourNodeInfo: StateFlow = nodeRepository.ourNodeInfo + + fun getNodeByNum(nodeNum: Int): Node? = nodeRepository.nodeDBbyNum.value[nodeNum] - fun getNodeOrFallback(nodeNum: Int): Node = - nodeRepository.nodeDBbyNum.value[nodeNum] ?: Node(num = nodeNum) + open fun getUser(userId: String?): User = nodeRepository.getUser(userId ?: DataPacket.ID_BROADCAST) + + fun getUser(nodeNum: Int): User = nodeRepository.getUser(nodeNum) + + fun getNodeOrFallback(nodeNum: Int): Node = getNodeByNum(nodeNum) ?: Node(num = nodeNum, user = getUser(nodeNum)) + + val isConnected = + serviceRepository.connectionState.map { it.isConnected() }.stateInWhileSubscribed(initialValue = false) + + fun toggleOnlyFavorites() { + val current = showOnlyFavorites.value + mapPrefs.showOnlyFavorites = !current + showOnlyFavorites.value = !current + } + + fun toggleShowWaypointsOnMap() { + val current = showWaypointsOnMap.value + mapPrefs.showWaypointsOnMap = !current + showWaypointsOnMap.value = !current + } + + fun toggleShowPrecisionCircleOnMap() { + val current = showPrecisionCircleOnMap.value + mapPrefs.showPrecisionCircleOnMap = !current + showPrecisionCircleOnMap.value = !current + } + + fun generatePacketId(): Int? { + return try { + serviceRepository.meshService?.packetId + } catch (ex: RemoteException) { + Logger.e { "RemoteException: ${ex.message}" } + return null + } + } fun deleteWaypoint(id: Int) = viewModelScope.launch(Dispatchers.IO) { packetRepository.deleteWaypoint(id) } @@ -148,13 +179,13 @@ abstract class BaseMapViewModel( } private fun sendDataPacket(p: DataPacket) { - viewModelScope.launch(Dispatchers.IO) { - radioController.sendMessage(p) + try { + serviceRepository.meshService?.send(p) + } catch (ex: RemoteException) { + Logger.e { "Send DataPacket error: ${ex.message}" } } } - fun generatePacketId(): Int = radioController.getPacketId() - data class MapFilterState( val onlyFavorites: Boolean, val showWaypoints: Boolean, @@ -228,16 +259,3 @@ fun BaseMapViewModel.tracerouteNodeSelection( nodeLookup = nodesForLookup.associateBy { it.num }, ) } - -enum class LastHeardFilter(val label: StringResource, val seconds: Long) { - Any(Res.string.any, 0L), - OneHour(Res.string.one_hour, 3600L), - EightHours(Res.string.eight_hours, 28800L), - OneDay(Res.string.one_day, 86400L), - TwoDays(Res.string.two_days, 172800L), - ; - - companion object { - fun fromSeconds(seconds: Long): LastHeardFilter = entries.find { it.seconds == seconds } ?: Any - } -} diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageListPaged.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageListPaged.kt index ab317a6f35..e2c1ce6c99 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageListPaged.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageListPaged.kt @@ -61,10 +61,11 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource +import org.meshtastic.core.database.entity.Packet +import org.meshtastic.core.database.entity.Reaction import org.meshtastic.core.model.Message import org.meshtastic.core.model.MessageStatus import org.meshtastic.core.model.Node -import org.meshtastic.core.model.Reaction import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.new_messages_below import org.meshtastic.feature.messaging.component.MessageItem @@ -544,7 +545,7 @@ private fun MessageStatusDialog( remember(message.relayNode, nodes, ourNode) { derivedStateOf { message.relayNode?.let { relayNodeId -> - Node.getRelayNode(relayNodeId, nodes, ourNode?.num)?.user?.long_name + Packet.getRelayNode(relayNodeId, nodes, ourNode?.num)?.user?.long_name } } } diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt index d7abd44747..e26583a910 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt @@ -32,21 +32,21 @@ import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import org.meshtastic.core.data.repository.NodeRepository +import org.meshtastic.core.data.repository.PacketRepository import org.meshtastic.core.data.repository.QuickChatActionRepository -import org.meshtastic.core.model.ContactSettings +import org.meshtastic.core.data.repository.RadioConfigRepository +import org.meshtastic.core.database.entity.ContactSettings import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.Message import org.meshtastic.core.model.Node -import org.meshtastic.core.model.service.ServiceAction import org.meshtastic.core.prefs.emoji.CustomEmojiPrefs import org.meshtastic.core.prefs.homoglyph.HomoglyphPrefs import org.meshtastic.core.prefs.ui.UiPrefs -import org.meshtastic.core.repository.MeshServiceNotifications -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.core.repository.usecase.SendMessageUseCase +import org.meshtastic.core.service.MeshServiceNotifications +import org.meshtastic.core.service.ServiceAction +import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.ChannelSet import javax.inject.Inject diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt index 6dd60807e0..8eb151620c 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt @@ -62,10 +62,10 @@ import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.common.util.nowMillis +import org.meshtastic.core.database.entity.Reaction import org.meshtastic.core.model.Message import org.meshtastic.core.model.MessageStatus import org.meshtastic.core.model.Node -import org.meshtastic.core.model.Reaction import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.filter_message_label import org.meshtastic.core.resources.message_delivery_status diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt index 8055b97399..8aa0727031 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt @@ -32,7 +32,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.AddReaction @@ -57,10 +56,11 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import org.jetbrains.compose.resources.stringResource +import org.meshtastic.core.database.entity.Packet +import org.meshtastic.core.database.entity.Reaction import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.MessageStatus import org.meshtastic.core.model.Node -import org.meshtastic.core.model.Reaction import org.meshtastic.core.model.getStringResFrom import org.meshtastic.core.model.util.getShortDateTime import org.meshtastic.core.resources.Res @@ -147,9 +147,7 @@ internal fun ReactionRow( AnimatedVisibility(emojiGroups.isNotEmpty(), modifier = modifier) { LazyRow(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) { - items(emojiGroups.entries.toList()) { entry -> - val emoji = entry.key - val reactions = entry.value + items(emojiGroups.entries.toList()) { (emoji, reactions) -> val localReaction = reactions.find { it.user.id == DataPacket.ID_LOCAL || it.user.id == myId } ReactionItem( emoji = emoji, @@ -219,7 +217,7 @@ internal fun ReactionDialog( val relayNodeName = reaction.relayNode?.let { relayNodeId -> - Node.getRelayNode(relayNodeId, nodes, ourNode?.num)?.user?.long_name + Packet.getRelayNode(relayNodeId, nodes, ourNode?.num)?.user?.long_name } DeliveryInfo( @@ -237,9 +235,7 @@ internal fun ReactionDialog( } LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) { - items(groupedEmojis.entries.toList()) { entry -> - val emoji = entry.key - val reactions = entry.value + items(groupedEmojis.entries.toList()) { (emoji, reactions) -> val localReaction = reactions.find { it.user.id == DataPacket.ID_LOCAL || it.user.id == myId } val isSending = localReaction?.status == MessageStatus.QUEUED || localReaction?.status == MessageStatus.ENROUTE diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt index 8d6bb18aeb..819df18cea 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt @@ -34,10 +34,10 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.Node -import org.meshtastic.core.model.service.ServiceAction import org.meshtastic.core.navigation.NodesRoutes -import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.core.resources.UiText +import org.meshtastic.core.service.ServiceAction +import org.meshtastic.core.service.ServiceRepository import org.meshtastic.feature.node.component.NodeMenuAction import org.meshtastic.feature.node.domain.usecase.GetNodeDetailsUseCase import org.meshtastic.feature.node.metrics.EnvironmentMetricsState diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt index 38e51602cd..06a9c114fd 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt @@ -17,9 +17,11 @@ package org.meshtastic.feature.node.list import android.net.Uri +import android.os.RemoteException import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import co.touchlab.kermit.Logger import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -28,13 +30,12 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.launch +import org.meshtastic.core.data.repository.NodeRepository +import org.meshtastic.core.data.repository.RadioConfigRepository import org.meshtastic.core.model.Node import org.meshtastic.core.model.NodeSortOption -import org.meshtastic.core.model.RadioController import org.meshtastic.core.model.util.dispatchMeshtasticUri -import org.meshtastic.core.repository.NodeRepository -import org.meshtastic.core.repository.RadioConfigRepository -import org.meshtastic.core.repository.ServiceRepository +import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.feature.node.detail.NodeManagementActions import org.meshtastic.feature.node.domain.usecase.GetFilteredNodesUseCase @@ -52,7 +53,6 @@ constructor( private val nodeRepository: NodeRepository, private val radioConfigRepository: RadioConfigRepository, private val serviceRepository: ServiceRepository, - private val radioController: RadioController, val nodeManagementActions: NodeManagementActions, private val getFilteredNodesUseCase: GetFilteredNodesUseCase, val nodeFilterPreferences: NodeFilterPreferences, @@ -154,7 +154,11 @@ constructor( radioConfigRepository.replaceAllSettings(channelSet.settings) val newLoraConfig = channelSet.lora_config if (newLoraConfig != null) { - radioController.setLocalConfig(Config(lora = newLoraConfig)) + try { + serviceRepository.meshService?.setConfig(Config(lora = newLoraConfig).encode()) + } catch (ex: RemoteException) { + Logger.e(ex) { "Set config error" } + } } } diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt index ae8cf2710f..7b8528aaa6 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt @@ -32,7 +32,10 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.meshtastic.core.common.BuildConfigProvider +import org.meshtastic.core.data.repository.NodeRepository +import org.meshtastic.core.data.repository.RadioConfigRepository import org.meshtastic.core.database.DatabaseManager +import org.meshtastic.core.database.entity.MyNodeEntity import org.meshtastic.core.domain.usecase.settings.ExportDataUseCase import org.meshtastic.core.domain.usecase.settings.IsOtaCapableUseCase import org.meshtastic.core.domain.usecase.settings.MeshLocationUseCase @@ -41,13 +44,10 @@ 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.MyNodeInfo import org.meshtastic.core.model.Node import org.meshtastic.core.model.RadioController import org.meshtastic.core.prefs.meshlog.MeshLogPrefs import org.meshtastic.core.prefs.ui.UiPrefs -import org.meshtastic.core.repository.NodeRepository -import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.LocalConfig import java.io.BufferedWriter @@ -77,7 +77,7 @@ constructor( private val exportDataUseCase: ExportDataUseCase, private val isOtaCapableUseCase: IsOtaCapableUseCase, ) : ViewModel() { - val myNodeInfo: StateFlow = nodeRepository.myNodeInfo + val myNodeInfo: StateFlow = nodeRepository.myNodeInfo val myNodeNum get() = myNodeInfo.value?.myNodeNum @@ -116,7 +116,8 @@ constructor( val appVersionName get() = buildConfigProvider.versionName - val isOtaCapable: StateFlow = isOtaCapableUseCase().stateInWhileSubscribed(initialValue = false) + val isOtaCapable: StateFlow = + isOtaCapableUseCase().stateInWhileSubscribed(initialValue = false) // Device DB cache limit (bounded by DatabaseConstants) val dbCacheLimit: StateFlow = databaseManager.cacheLimit @@ -172,7 +173,9 @@ constructor( fun saveDataCsv(uri: Uri, filterPortnum: Int? = null) { viewModelScope.launch(Dispatchers.Main) { val myNodeNum = myNodeNum ?: return@launch - writeToUri(uri) { writer -> exportDataUseCase(writer, myNodeNum, filterPortnum) } + writeToUri(uri) { writer -> + exportDataUseCase(writer, myNodeNum, filterPortnum) + } } } From 3ea8f813b39cba06aae957b31cbf8f4b3a33d387 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 2 Mar 2026 13:01:57 -0600 Subject: [PATCH 06/20] refactor: transition repositories and managers to interface-based architecture - Introduce interfaces for `NodeManager`, `CommandSender`, `PacketRepository`, `RadioConfigRepository`, `DeviceHardwareRepository`, and `ServiceBroadcasts`. - Move interfaces to the `core:repository` module to decouple logic from concrete implementations. - Relocate concrete implementations to `core:data` and update Hilt modules to bind new implementations. - Standardize the usage of core models (`Node`, `Message`, `Reaction`, `ContactSettings`) across feature modules and services. - Refactor `MeshService` and its handlers to use the new interface types for better testability. - Update unit tests to reflect class renaming and interface migrations. - Move `ContactSettings` and `Reaction` to `core:model` for shared access. Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .../com/geeksville/mesh/model/UIViewModel.kt | 12 +- .../mesh/service/FromRadioPacketHandler.kt | 5 +- .../mesh/service/MeshActionHandler.kt | 2 +- .../mesh/service/MeshConfigHandler.kt | 2 +- .../mesh/service/MeshConnectionManager.kt | 15 +- .../mesh/service/MeshDataHandler.kt | 12 +- .../mesh/service/MeshMessageProcessor.kt | 2 +- .../geeksville/mesh/service/MeshService.kt | 4 +- .../geeksville/mesh/service/ReplyReceiver.kt | 10 +- .../ui/connections/ConnectionsViewModel.kt | 2 +- .../mesh/ui/sharing/ChannelViewModel.kt | 19 +- .../mesh/widget/LocalStatsWidgetState.kt | 2 +- .../java/com/geeksville/mesh/service/Fakes.kt | 8 +- .../service/FromRadioPacketHandlerTest.kt | 5 +- .../mesh/service/MeshConnectionManagerTest.kt | 22 +- .../mesh/service/MeshDataHandlerTest.kt | 14 +- .../core/data/di/RepositoryModule.kt | 40 +++ .../core/data/manager/CommandSenderImpl.kt | 2 +- .../core/data/manager/HistoryManagerImpl.kt | 75 ++-- .../core/data/manager/MqttManagerImpl.kt | 17 +- .../data/manager/NeighborInfoHandlerImpl.kt | 18 +- .../core/data/manager/NodeManagerImpl.kt | 2 +- .../data/manager/TracerouteHandlerImpl.kt | 29 +- .../data/repository/NodeRepositoryImpl.kt | 49 ++- .../data/repository/PacketRepositoryImpl.kt | 331 ++++++++++++------ .../repository/RadioConfigRepositoryImpl.kt | 27 +- .../data/manager/CommandSenderHopLimitTest.kt | 25 +- .../data/manager/CommandSenderImplTest.kt | 20 +- .../core/data/manager/NodeManagerImplTest.kt | 21 +- .../core/database/DatabaseManager.kt | 8 +- .../usecase/settings/IsOtaCapableUseCase.kt | 2 +- .../kotlin/org/meshtastic/core/model/Node.kt | 3 - core/repository/build.gradle.kts | 1 + .../core/repository/NeighborInfoHandler.kt | 28 +- .../core/repository/NodeRepository.kt | 7 + .../core/repository/PacketRepository.kt | 111 +++++- .../core/ui/qr/ScannedQrCodeViewModel.kt | 18 +- .../firmware/FirmwareUpdateViewModel.kt | 10 +- .../meshtastic/feature/map/MapViewModel.kt | 14 +- .../meshtastic/feature/map/MapViewModel.kt | 8 +- .../feature/map/BaseMapViewModel.kt | 11 +- .../feature/messaging/MessageListPaged.kt | 5 +- .../feature/messaging/MessageViewModel.kt | 10 +- .../messaging/component/MessageItem.kt | 2 +- .../feature/messaging/component/Reaction.kt | 14 +- .../domain/worker/SendMessageWorker.kt | 12 +- .../messaging/ui/contact/ContactsViewModel.kt | 2 +- .../domain/worker/SendMessageWorkerTest.kt | 105 +++--- .../node/component/AdministrationSection.kt | 2 +- .../node/detail/NodeManagementActions.kt | 35 +- .../feature/node/list/NodeListViewModel.kt | 4 +- .../feature/node/metrics/MetricsViewModel.kt | 2 +- .../feature/node/model/NodeDetailAction.kt | 2 +- .../node/detail/NodeManagementActionsTest.kt | 5 +- .../feature/settings/SettingsViewModel.kt | 8 +- .../settings/radio/RadioConfigViewModel.kt | 53 +-- .../radio/RadioConfigViewModelTest.kt | 51 ++- 57 files changed, 795 insertions(+), 530 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/model/UIViewModel.kt b/app/src/main/java/com/geeksville/mesh/model/UIViewModel.kt index d0bc742e92..8005076ea8 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIViewModel.kt @@ -48,10 +48,8 @@ import org.meshtastic.core.data.repository.MeshLogRepository import org.meshtastic.core.database.entity.asDeviceVersion import org.meshtastic.core.datastore.UiPreferencesDataSource import org.meshtastic.core.model.MyNodeInfo -import org.meshtastic.core.model.RadioController import org.meshtastic.core.model.TracerouteMapAvailability import org.meshtastic.core.model.evaluateTracerouteMapAvailability -import org.meshtastic.core.model.service.TracerouteResponse import org.meshtastic.core.model.util.dispatchMeshtasticUri import org.meshtastic.core.repository.MeshServiceNotifications import org.meshtastic.core.repository.NodeRepository @@ -59,8 +57,9 @@ import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.client_notification import org.meshtastic.core.resources.compromised_keys -import org.meshtastic.core.service.AndroidServiceRepository import org.meshtastic.core.service.IMeshService +import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.service.TracerouteResponse import org.meshtastic.core.ui.component.ScrollToTopEvent import org.meshtastic.core.ui.util.AlertManager import org.meshtastic.core.ui.util.ComposableContent @@ -76,8 +75,7 @@ class UIViewModel @Inject constructor( private val nodeDB: NodeRepository, - private val serviceRepository: AndroidServiceRepository, - private val radioController: RadioController, + private val serviceRepository: ServiceRepository, radioInterfaceService: RadioInterfaceService, meshLogRepository: MeshLogRepository, firmwareReleaseRepository: FirmwareReleaseRepository, @@ -163,10 +161,6 @@ constructor( val meshService: IMeshService? get() = serviceRepository.meshService - fun setDeviceAddress(address: String) { - radioController.setDeviceAddress(address) - } - val unreadMessageCount = packetRepository.getUnreadCountTotal().map { it.coerceAtLeast(0) }.stateInWhileSubscribed(initialValue = 0) diff --git a/app/src/main/java/com/geeksville/mesh/service/FromRadioPacketHandler.kt b/app/src/main/java/com/geeksville/mesh/service/FromRadioPacketHandler.kt index b2c7ead787..b94ff6dc11 100644 --- a/app/src/main/java/com/geeksville/mesh/service/FromRadioPacketHandler.kt +++ b/app/src/main/java/com/geeksville/mesh/service/FromRadioPacketHandler.kt @@ -18,9 +18,8 @@ package com.geeksville.mesh.service import co.touchlab.kermit.Logger import org.meshtastic.core.repository.MeshServiceNotifications -import org.meshtastic.core.repository.MqttManager import org.meshtastic.core.repository.PacketHandler -import org.meshtastic.core.repository.ServiceRepository +import org.meshtastic.core.service.ServiceRepository import org.meshtastic.proto.FromRadio import javax.inject.Inject import javax.inject.Singleton @@ -35,7 +34,7 @@ class FromRadioPacketHandler constructor( private val serviceRepository: ServiceRepository, private val router: MeshRouter, - private val mqttManager: MqttManager, + private val mqttManager: MeshMqttManager, private val packetHandler: PacketHandler, private val serviceNotifications: MeshServiceNotifications, ) { diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshActionHandler.kt b/app/src/main/java/com/geeksville/mesh/service/MeshActionHandler.kt index 8fe6597f6e..1aa15c4215 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshActionHandler.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshActionHandler.kt @@ -32,13 +32,13 @@ import org.meshtastic.core.model.MeshUser import org.meshtastic.core.model.MessageStatus import org.meshtastic.core.model.Position import org.meshtastic.core.model.Reaction -import org.meshtastic.core.model.service.ServiceAction import org.meshtastic.core.prefs.mesh.MeshPrefs import org.meshtastic.core.repository.CommandSender import org.meshtastic.core.repository.MeshServiceNotifications import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.ServiceBroadcasts +import org.meshtastic.core.service.ServiceAction import org.meshtastic.proto.AdminMessage import org.meshtastic.proto.Channel import org.meshtastic.proto.Config diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshConfigHandler.kt b/app/src/main/java/com/geeksville/mesh/service/MeshConfigHandler.kt index 9f71992f21..17f43b901a 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshConfigHandler.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshConfigHandler.kt @@ -26,7 +26,7 @@ import kotlinx.coroutines.flow.onEach import org.meshtastic.core.common.util.handledLaunch import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.RadioConfigRepository -import org.meshtastic.core.repository.ServiceRepository +import org.meshtastic.core.service.ServiceRepository import org.meshtastic.proto.Channel import org.meshtastic.proto.Config import org.meshtastic.proto.LocalConfig diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt b/app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt index 47eb6e1179..aa926821a2 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt @@ -47,9 +47,7 @@ import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.TelemetryType import org.meshtastic.core.prefs.ui.UiPrefs import org.meshtastic.core.repository.CommandSender -import org.meshtastic.core.repository.HistoryManager import org.meshtastic.core.repository.MeshServiceNotifications -import org.meshtastic.core.repository.MqttManager import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.PacketHandler @@ -88,8 +86,8 @@ constructor( private val packetHandler: PacketHandler, private val nodeRepository: NodeRepository, private val locationManager: MeshLocationManager, - private val mqttManager: MqttManager, - private val historyManager: HistoryManager, + private val mqttManager: MeshMqttManager, + private val historyManager: MeshHistoryManager, private val radioConfigRepository: RadioConfigRepository, private val commandSender: CommandSender, private val nodeManager: NodeManager, @@ -273,15 +271,14 @@ constructor( val queuedPackets = packetRepository.getQueuedPackets() ?: emptyList() queuedPackets.forEach { packet -> try { - val workRequest = - OneTimeWorkRequestBuilder() - .setInputData(workDataOf(SendMessageWorker.KEY_PACKET_ID to packet.id)) - .build() + val workRequest = OneTimeWorkRequestBuilder() + .setInputData(workDataOf(SendMessageWorker.KEY_PACKET_ID to packet.id)) + .build() workManager.enqueueUniqueWork( "${SendMessageWorker.WORK_NAME_PREFIX}${packet.id}", ExistingWorkPolicy.REPLACE, - workRequest, + workRequest ) } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { Logger.e(e) { "Failed to enqueue queued packet worker" } diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt b/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt index 64429d112f..ae3eb5efd1 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt @@ -37,28 +37,24 @@ import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.MessageStatus import org.meshtastic.core.model.Node import org.meshtastic.core.model.Reaction -import org.meshtastic.core.model.util.MeshDataMapper import org.meshtastic.core.model.util.SfppHasher import org.meshtastic.core.model.util.decodeOrNull import org.meshtastic.core.model.util.toOneLiner import org.meshtastic.core.prefs.mesh.MeshPrefs import org.meshtastic.core.repository.CommandSender -import org.meshtastic.core.repository.HistoryManager import org.meshtastic.core.repository.MeshServiceNotifications -import org.meshtastic.core.repository.NeighborInfoHandler import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.PacketHandler import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.repository.ServiceBroadcasts -import org.meshtastic.core.repository.ServiceRepository -import org.meshtastic.core.repository.TracerouteHandler import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.critical_alert import org.meshtastic.core.resources.error_duty_cycle import org.meshtastic.core.resources.getString import org.meshtastic.core.resources.unknown_username import org.meshtastic.core.resources.waypoint_received +import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.service.filter.MessageFilterService import org.meshtastic.proto.AdminMessage import org.meshtastic.proto.MeshPacket @@ -94,11 +90,11 @@ constructor( private val configHandler: MeshConfigHandler, private val configFlowManager: MeshConfigFlowManager, private val commandSender: CommandSender, - private val historyManager: HistoryManager, + private val historyManager: MeshHistoryManager, private val meshPrefs: MeshPrefs, private val connectionManager: MeshConnectionManager, - private val tracerouteHandler: TracerouteHandler, - private val neighborInfoHandler: NeighborInfoHandler, + private val tracerouteHandler: MeshTracerouteHandler, + private val neighborInfoHandler: MeshNeighborInfoHandler, private val radioConfigRepository: RadioConfigRepository, private val messageFilterService: MessageFilterService, ) { diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshMessageProcessor.kt b/app/src/main/java/com/geeksville/mesh/service/MeshMessageProcessor.kt index dbf4441395..c601099146 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshMessageProcessor.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshMessageProcessor.kt @@ -34,7 +34,7 @@ import org.meshtastic.core.database.entity.MeshLog import org.meshtastic.core.model.Node import org.meshtastic.core.model.util.isLora import org.meshtastic.core.repository.NodeManager -import org.meshtastic.core.repository.ServiceRepository +import org.meshtastic.core.service.ServiceRepository import org.meshtastic.proto.FromRadio import org.meshtastic.proto.LogRecord import org.meshtastic.proto.MeshPacket diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt index 4fa86b3f35..35151a5d4b 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -49,8 +49,8 @@ import org.meshtastic.core.repository.PacketHandler import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.repository.SERVICE_NOTIFY_ID import org.meshtastic.core.repository.ServiceBroadcasts -import org.meshtastic.core.service.AndroidServiceRepository import org.meshtastic.core.service.IMeshService +import org.meshtastic.core.service.ServiceRepository import org.meshtastic.proto.PortNum import javax.inject.Inject @@ -60,7 +60,7 @@ class MeshService : Service() { @Inject lateinit var radioInterfaceService: RadioInterfaceService - @Inject lateinit var serviceRepository: AndroidServiceRepository + @Inject lateinit var serviceRepository: ServiceRepository @Inject lateinit var connectionStateHolder: ConnectionStateHandler diff --git a/app/src/main/java/com/geeksville/mesh/service/ReplyReceiver.kt b/app/src/main/java/com/geeksville/mesh/service/ReplyReceiver.kt index 7c29affe4a..9875de12c0 100644 --- a/app/src/main/java/com/geeksville/mesh/service/ReplyReceiver.kt +++ b/app/src/main/java/com/geeksville/mesh/service/ReplyReceiver.kt @@ -20,11 +20,9 @@ import android.content.BroadcastReceiver import androidx.core.app.RemoteInput import dagger.hilt.android.AndroidEntryPoint import jakarta.inject.Inject -import kotlinx.coroutines.launch import org.meshtastic.core.model.DataPacket -import org.meshtastic.core.model.RadioController import org.meshtastic.core.repository.MeshServiceNotifications -import org.meshtastic.core.repository.ServiceRepository +import org.meshtastic.core.service.ServiceRepository /** * A [BroadcastReceiver] that handles inline replies from notifications. @@ -35,7 +33,7 @@ import org.meshtastic.core.repository.ServiceRepository */ @AndroidEntryPoint class ReplyReceiver : BroadcastReceiver() { - @Inject lateinit var radioController: RadioController + @Inject lateinit var serviceRepository: ServiceRepository @Inject lateinit var meshServiceNotifications: MeshServiceNotifications @@ -50,9 +48,7 @@ class ReplyReceiver : BroadcastReceiver() { val channel = contactKey[0].digitToIntOrNull() val dest = if (channel != null) contactKey.substring(1) else contactKey val p = DataPacket(dest, channel ?: 0, str) - kotlinx.coroutines.CoroutineScope(kotlinx.coroutines.Dispatchers.IO).launch { - radioController.sendMessage(p) - } + serviceRepository.meshService?.send(p) } override fun onReceive(context: android.content.Context, intent: android.content.Intent) { diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt index b17281ff6b..da78f976f9 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt @@ -26,7 +26,7 @@ import org.meshtastic.core.model.Node import org.meshtastic.core.prefs.ui.UiPrefs import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.RadioConfigRepository -import org.meshtastic.core.repository.ServiceRepository +import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.LocalConfig import javax.inject.Inject diff --git a/app/src/main/java/com/geeksville/mesh/ui/sharing/ChannelViewModel.kt b/app/src/main/java/com/geeksville/mesh/ui/sharing/ChannelViewModel.kt index 0e90416971..71ff98493e 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/sharing/ChannelViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/sharing/ChannelViewModel.kt @@ -17,6 +17,7 @@ package com.geeksville.mesh.ui.sharing import android.net.Uri +import android.os.RemoteException import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import co.touchlab.kermit.Logger @@ -26,9 +27,9 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import org.meshtastic.core.analytics.DataPair import org.meshtastic.core.analytics.platform.PlatformAnalytics -import org.meshtastic.core.model.RadioController import org.meshtastic.core.model.util.toChannelSet import org.meshtastic.core.repository.RadioConfigRepository +import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.util.getChannelList import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.Channel @@ -41,12 +42,12 @@ import javax.inject.Inject class ChannelViewModel @Inject constructor( - private val radioController: RadioController, + private val serviceRepository: ServiceRepository, private val radioConfigRepository: RadioConfigRepository, private val analytics: PlatformAnalytics, ) : ViewModel() { - val connectionState = radioController.connectionState + val connectionState = serviceRepository.connectionState val localConfig = radioConfigRepository.localConfigFlow.stateInWhileSubscribed(initialValue = LocalConfig()) @@ -94,15 +95,19 @@ constructor( } fun setChannel(channel: Channel) { - viewModelScope.launch { - radioController.setLocalChannel(channel) + try { + serviceRepository.meshService?.setChannel(channel.encode()) + } catch (ex: RemoteException) { + Logger.e(ex) { "Set channel error" } } } // Set the radio config (also updates our saved copy in preferences) fun setConfig(config: Config) { - viewModelScope.launch { - radioController.setLocalConfig(config) + try { + serviceRepository.meshService?.setConfig(config.encode()) + } catch (ex: RemoteException) { + Logger.e(ex) { "Set config error" } } } diff --git a/app/src/main/java/com/geeksville/mesh/widget/LocalStatsWidgetState.kt b/app/src/main/java/com/geeksville/mesh/widget/LocalStatsWidgetState.kt index 1f28a65f7c..4b1ca2fdca 100644 --- a/app/src/main/java/com/geeksville/mesh/widget/LocalStatsWidgetState.kt +++ b/app/src/main/java/com/geeksville/mesh/widget/LocalStatsWidgetState.kt @@ -32,7 +32,7 @@ import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.Node import org.meshtastic.core.model.util.onlineTimeThreshold import org.meshtastic.core.repository.NodeRepository -import org.meshtastic.core.repository.ServiceRepository +import org.meshtastic.core.service.ServiceRepository import org.meshtastic.proto.LocalStats import javax.inject.Inject import javax.inject.Singleton diff --git a/app/src/test/java/com/geeksville/mesh/service/Fakes.kt b/app/src/test/java/com/geeksville/mesh/service/Fakes.kt index 728adc5f91..ac5b2e81dd 100644 --- a/app/src/test/java/com/geeksville/mesh/service/Fakes.kt +++ b/app/src/test/java/com/geeksville/mesh/service/Fakes.kt @@ -19,7 +19,7 @@ package com.geeksville.mesh.service import android.app.Notification import com.geeksville.mesh.repository.radio.RadioInterfaceService import io.mockk.mockk -import org.meshtastic.core.model.Node +import org.meshtastic.core.database.entity.NodeEntity import org.meshtastic.core.repository.MeshServiceNotifications import org.meshtastic.proto.ClientNotification import org.meshtastic.proto.Telemetry @@ -64,15 +64,15 @@ class FakeMeshServiceNotifications : MeshServiceNotifications { override fun showAlertNotification(contactKey: String, name: String, alert: String) {} - override fun showNewNodeSeenNotification(node: Node) {} + override fun showNewNodeSeenNotification(node: NodeEntity) {} - override fun showOrUpdateLowBatteryNotification(node: Node, isRemote: Boolean) {} + override fun showOrUpdateLowBatteryNotification(node: NodeEntity, isRemote: Boolean) {} override fun showClientNotification(clientNotification: ClientNotification) {} override fun cancelMessageNotification(contactKey: String) {} - override fun cancelLowBatteryNotification(node: Node) {} + override fun cancelLowBatteryNotification(node: NodeEntity) {} override fun clearClientNotification(notification: ClientNotification) {} } diff --git a/app/src/test/java/com/geeksville/mesh/service/FromRadioPacketHandlerTest.kt b/app/src/test/java/com/geeksville/mesh/service/FromRadioPacketHandlerTest.kt index d4d7e6112d..ec2ba1bcdb 100644 --- a/app/src/test/java/com/geeksville/mesh/service/FromRadioPacketHandlerTest.kt +++ b/app/src/test/java/com/geeksville/mesh/service/FromRadioPacketHandlerTest.kt @@ -21,8 +21,7 @@ import io.mockk.verify import org.junit.Before import org.junit.Test import org.meshtastic.core.repository.MeshServiceNotifications -import org.meshtastic.core.repository.MqttManager -import org.meshtastic.core.repository.ServiceRepository +import org.meshtastic.core.service.ServiceRepository import org.meshtastic.proto.ClientNotification import org.meshtastic.proto.Config import org.meshtastic.proto.DeviceMetadata @@ -34,7 +33,7 @@ import org.meshtastic.proto.QueueStatus class FromRadioPacketHandlerTest { private val serviceRepository: ServiceRepository = mockk(relaxed = true) private val router: MeshRouter = mockk(relaxed = true) - private val mqttManager: MqttManager = mockk(relaxed = true) + private val mqttManager: MeshMqttManager = mockk(relaxed = true) private val packetHandler: PacketHandler = mockk(relaxed = true) private val serviceNotifications: MeshServiceNotifications = mockk(relaxed = true) diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshConnectionManagerTest.kt b/app/src/test/java/com/geeksville/mesh/service/MeshConnectionManagerTest.kt index 28ab04f6fa..63e9f3df43 100644 --- a/app/src/test/java/com/geeksville/mesh/service/MeshConnectionManagerTest.kt +++ b/app/src/test/java/com/geeksville/mesh/service/MeshConnectionManagerTest.kt @@ -16,7 +16,6 @@ */ package com.geeksville.mesh.service -import android.app.Notification import android.content.Context import androidx.glance.appwidget.GlanceAppWidget import androidx.glance.appwidget.updateAll @@ -40,16 +39,12 @@ import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import org.meshtastic.core.analytics.platform.PlatformAnalytics +import org.meshtastic.core.database.entity.MyNodeEntity import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.DataPacket -import org.meshtastic.core.model.MyNodeInfo import org.meshtastic.core.model.Node import org.meshtastic.core.prefs.ui.UiPrefs -import org.meshtastic.core.repository.CommandSender -import org.meshtastic.core.repository.HistoryManager import org.meshtastic.core.repository.MeshServiceNotifications -import org.meshtastic.core.repository.MqttManager -import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.RadioConfigRepository @@ -72,8 +67,8 @@ class MeshConnectionManagerTest { private val packetHandler: PacketHandler = mockk(relaxed = true) private val nodeRepository: NodeRepository = mockk(relaxed = true) private val locationManager: MeshLocationManager = mockk(relaxed = true) - private val mqttManager: MqttManager = mockk(relaxed = true) - private val historyManager: HistoryManager = mockk(relaxed = true) + private val mqttManager: MeshMqttManager = mockk(relaxed = true) + private val historyManager: MeshHistoryManager = mockk(relaxed = true) private val radioConfigRepository: RadioConfigRepository = mockk(relaxed = true) private val commandSender: CommandSender = mockk(relaxed = true) private val nodeManager: NodeManager = mockk(relaxed = true) @@ -99,10 +94,9 @@ class MeshConnectionManagerTest { every { radioInterfaceService.connectionState } returns radioConnectionState every { radioConfigRepository.localConfigFlow } returns localConfigFlow every { radioConfigRepository.moduleConfigFlow } returns moduleConfigFlow - every { nodeRepository.myNodeInfo } returns MutableStateFlow(null) + every { nodeRepository.myNodeInfo } returns MutableStateFlow(null) every { nodeRepository.ourNodeInfo } returns MutableStateFlow(null) every { nodeRepository.localStats } returns MutableStateFlow(LocalStats()) - every { serviceNotifications.updateServiceStateNotification(any(), any()) } returns mockk(relaxed = true) manager = MeshConnectionManager( @@ -220,13 +214,7 @@ class MeshConnectionManagerTest { manager.onRadioConfigLoaded() advanceUntilIdle() - verify { - workManager.enqueueUniqueWork( - match { it.startsWith(SendMessageWorker.WORK_NAME_PREFIX) }, - any(), - any(), - ) - } + verify { workManager.enqueueUniqueWork(match { it.startsWith(SendMessageWorker.WORK_NAME_PREFIX) }, any(), any()) } verify { commandSender.sendAdmin(any(), initFn = any()) } } diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshDataHandlerTest.kt b/app/src/test/java/com/geeksville/mesh/service/MeshDataHandlerTest.kt index 6f29161964..4830cf45ef 100644 --- a/app/src/test/java/com/geeksville/mesh/service/MeshDataHandlerTest.kt +++ b/app/src/test/java/com/geeksville/mesh/service/MeshDataHandlerTest.kt @@ -31,17 +31,11 @@ import org.junit.Test import org.meshtastic.core.analytics.platform.PlatformAnalytics import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.MessageStatus -import org.meshtastic.core.model.util.MeshDataMapper import org.meshtastic.core.prefs.mesh.MeshPrefs -import org.meshtastic.core.repository.CommandSender -import org.meshtastic.core.repository.HistoryManager import org.meshtastic.core.repository.MeshServiceNotifications -import org.meshtastic.core.repository.NeighborInfoHandler -import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.RadioConfigRepository -import org.meshtastic.core.repository.ServiceRepository -import org.meshtastic.core.repository.TracerouteHandler +import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.service.filter.MessageFilterService import org.meshtastic.proto.Data import org.meshtastic.proto.MeshPacket @@ -62,11 +56,11 @@ class MeshDataHandlerTest { private val configHandler: MeshConfigHandler = mockk(relaxed = true) private val configFlowManager: MeshConfigFlowManager = mockk(relaxed = true) private val commandSender: CommandSender = mockk(relaxed = true) - private val historyManager: HistoryManager = mockk(relaxed = true) + private val historyManager: MeshHistoryManager = mockk(relaxed = true) private val meshPrefs: MeshPrefs = mockk(relaxed = true) private val connectionManager: MeshConnectionManager = mockk(relaxed = true) - private val tracerouteHandler: TracerouteHandler = mockk(relaxed = true) - private val neighborInfoHandler: NeighborInfoHandler = mockk(relaxed = true) + private val tracerouteHandler: MeshTracerouteHandler = mockk(relaxed = true) + private val neighborInfoHandler: MeshNeighborInfoHandler = mockk(relaxed = true) private val radioConfigRepository: RadioConfigRepository = mockk(relaxed = true) private val messageFilterService: MessageFilterService = mockk(relaxed = true) diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt index 1bf8fcbafd..0d27698938 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt @@ -21,8 +21,18 @@ import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import org.meshtastic.core.data.manager.CommandSenderImpl +import org.meshtastic.core.data.manager.NodeManagerImpl +import org.meshtastic.core.data.repository.DeviceHardwareRepositoryImpl 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.repository.CommandSender +import org.meshtastic.core.repository.DeviceHardwareRepository +import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.PacketRepository +import org.meshtastic.core.repository.RadioConfigRepository import javax.inject.Singleton @Module @@ -34,4 +44,34 @@ abstract class RepositoryModule { abstract fun bindNodeRepository( nodeRepositoryImpl: NodeRepositoryImpl ): NodeRepository + + @Binds + @Singleton + abstract fun bindRadioConfigRepository( + radioConfigRepositoryImpl: RadioConfigRepositoryImpl + ): RadioConfigRepository + + @Binds + @Singleton + abstract fun bindDeviceHardwareRepository( + deviceHardwareRepositoryImpl: DeviceHardwareRepositoryImpl + ): DeviceHardwareRepository + + @Binds + @Singleton + abstract fun bindPacketRepository( + packetRepositoryImpl: PacketRepositoryImpl + ): PacketRepository + + @Binds + @Singleton + abstract fun bindNodeManager( + nodeManagerImpl: NodeManagerImpl + ): NodeManager + + @Binds + @Singleton + abstract fun bindCommandSender( + commandSenderImpl: CommandSenderImpl + ): CommandSender } diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt index 33d668a2d1..b91bc60b59 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt @@ -364,7 +364,7 @@ constructor( } } - fun resolveNodeNum(toId: String): Int = when (toId) { + private fun resolveNodeNum(toId: String): Int = when (toId) { DataPacket.ID_BROADCAST -> DataPacket.NODENUM_BROADCAST else -> { val numericNum = diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/HistoryManagerImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/HistoryManagerImpl.kt index f3ff53e4e1..bad5dd2175 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/HistoryManagerImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/HistoryManagerImpl.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,12 +14,15 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.core.data.manager +package com.geeksville.mesh.service +import android.util.Log +import androidx.annotation.VisibleForTesting import co.touchlab.kermit.Logger +import com.geeksville.mesh.BuildConfig +import com.geeksville.mesh.ui.connections.NO_DEVICE_SELECTED import okio.ByteString.Companion.toByteString import org.meshtastic.core.prefs.mesh.MeshPrefs -import org.meshtastic.core.repository.HistoryManager import org.meshtastic.core.repository.PacketHandler import org.meshtastic.proto.Data import org.meshtastic.proto.MeshPacket @@ -30,47 +33,58 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class HistoryManagerImpl @Inject constructor( +class MeshHistoryManager +@Inject +constructor( private val meshPrefs: MeshPrefs, private val packetHandler: PacketHandler, -) : HistoryManager { - +) { companion object { private const val HISTORY_TAG = "HistoryReplay" private const val DEFAULT_HISTORY_RETURN_WINDOW_MINUTES = 60 * 24 private const val DEFAULT_HISTORY_RETURN_MAX_MESSAGES = 100 - private const val NO_DEVICE_SELECTED = "No device selected" - fun buildStoreForwardHistoryRequest( + @VisibleForTesting + internal fun buildStoreForwardHistoryRequest( lastRequest: Int, historyReturnWindow: Int, historyReturnMax: Int, ): StoreAndForward { - val history = StoreAndForward.History( - last_request = lastRequest.coerceAtLeast(0), - window = historyReturnWindow.coerceAtLeast(0), - history_messages = historyReturnMax.coerceAtLeast(0), - ) + val history = + StoreAndForward.History( + last_request = lastRequest.coerceAtLeast(0), + window = historyReturnWindow.coerceAtLeast(0), + history_messages = historyReturnMax.coerceAtLeast(0), + ) return StoreAndForward(rr = StoreAndForward.RequestResponse.CLIENT_HISTORY, history = history) } - fun resolveHistoryRequestParameters(window: Int, max: Int): Pair { + @VisibleForTesting + internal fun resolveHistoryRequestParameters(window: Int, max: Int): Pair { val resolvedWindow = if (window > 0) window else DEFAULT_HISTORY_RETURN_WINDOW_MINUTES val resolvedMax = if (max > 0) max else DEFAULT_HISTORY_RETURN_MAX_MESSAGES return resolvedWindow to resolvedMax } } - private val logger = Logger.withTag(HISTORY_TAG) - - private fun historyLog(message: String, throwable: Throwable? = null) { - logger.i(throwable) { message } + private fun historyLog(priority: Int = Log.INFO, throwable: Throwable? = null, message: () -> String) { + if (!BuildConfig.DEBUG) return + val logger = Logger.withTag(HISTORY_TAG) + val msg = message() + when (priority) { + Log.VERBOSE -> logger.v(throwable) { msg } + Log.DEBUG -> logger.d(throwable) { msg } + Log.INFO -> logger.i(throwable) { msg } + Log.WARN -> logger.w(throwable) { msg } + Log.ERROR -> logger.e(throwable) { msg } + else -> logger.i(throwable) { msg } + } } private fun activeDeviceAddress(): String? = meshPrefs.deviceAddress?.takeIf { !it.equals(NO_DEVICE_SELECTED, ignoreCase = true) && it.isNotBlank() } - override fun requestHistoryReplay( + fun requestHistoryReplay( trigger: String, myNodeNum: Int?, storeForwardConfig: ModuleConfig.StoreForwardConfig?, @@ -79,22 +93,23 @@ class HistoryManagerImpl @Inject constructor( val address = activeDeviceAddress() if (address == null || myNodeNum == null) { val reason = if (address == null) "no_addr" else "no_my_node" - historyLog("requestHistory skipped trigger=$trigger reason=$reason") + historyLog { "requestHistory skipped trigger=$trigger reason=$reason" } return } val lastRequest = meshPrefs.getStoreForwardLastRequest(address) - val (window, max) = resolveHistoryRequestParameters( - storeForwardConfig?.history_return_window ?: 0, - storeForwardConfig?.history_return_max ?: 0, - ) + val (window, max) = + resolveHistoryRequestParameters( + storeForwardConfig?.history_return_window ?: 0, + storeForwardConfig?.history_return_max ?: 0, + ) val request = buildStoreForwardHistoryRequest(lastRequest, window, max) - historyLog( + historyLog { "requestHistory trigger=$trigger transport=$transport addr=$address " + "lastRequest=$lastRequest window=$window max=$max" - ) + } runCatching { packetHandler.sendToRadio( @@ -106,19 +121,19 @@ class HistoryManagerImpl @Inject constructor( ), ) } - .onFailure { ex -> logger.w(ex) { "requestHistory failed" } } + .onFailure { ex -> historyLog(Log.WARN, ex) { "requestHistory failed" } } } - override fun updateStoreForwardLastRequest(source: String, lastRequest: Int, transport: String) { + fun updateStoreForwardLastRequest(source: String, lastRequest: Int, transport: String) { if (lastRequest <= 0) return val address = activeDeviceAddress() ?: return val current = meshPrefs.getStoreForwardLastRequest(address) if (lastRequest != current) { meshPrefs.setStoreForwardLastRequest(address, lastRequest) - historyLog( + historyLog { "historyMarker updated source=$source transport=$transport " + "addr=$address from=$current to=$lastRequest" - ) + } } } } diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MqttManagerImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MqttManagerImpl.kt index 7684ebd205..ef4cc40bef 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MqttManagerImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MqttManagerImpl.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.data.manager +package com.geeksville.mesh.service import co.touchlab.kermit.Logger import co.touchlab.kermit.Severity +import com.geeksville.mesh.repository.network.MQTTRepository import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -25,27 +26,25 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -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.core.service.ServiceRepository import org.meshtastic.proto.MqttClientProxyMessage import org.meshtastic.proto.ToRadio import javax.inject.Inject import javax.inject.Singleton @Singleton -class MqttManagerImpl +class MeshMqttManager @Inject constructor( private val mqttRepository: MQTTRepository, private val packetHandler: PacketHandler, private val serviceRepository: ServiceRepository, -) : MqttManager { +) { private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private var mqttMessageFlow: Job? = null - override fun start(scope: CoroutineScope, enabled: Boolean, proxyToClientEnabled: Boolean) { + fun start(scope: CoroutineScope, enabled: Boolean, proxyToClientEnabled: Boolean) { this.scope = scope if (mqttMessageFlow?.isActive == true) return if (enabled && proxyToClientEnabled) { @@ -62,7 +61,7 @@ constructor( } } - override fun stop() { + fun stop() { if (mqttMessageFlow?.isActive == true) { Logger.i { "Stopping MqttClientProxy" } mqttMessageFlow?.cancel() @@ -70,7 +69,7 @@ constructor( } } - override fun handleMqttProxyMessage(message: MqttClientProxyMessage) { + fun handleMqttProxyMessage(message: MqttClientProxyMessage) { val topic = message.topic ?: "" Logger.d { "[mqttClientProxyMessage] $topic" } val retained = message.retained == true diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NeighborInfoHandlerImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NeighborInfoHandlerImpl.kt index df19abacf6..1e10b28243 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NeighborInfoHandlerImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NeighborInfoHandlerImpl.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.core.data.manager +package com.geeksville.mesh.service import co.touchlab.kermit.Logger import kotlinx.coroutines.CoroutineScope @@ -22,10 +22,12 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.repository.CommandSender -import org.meshtastic.core.repository.NeighborInfoHandler import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.ServiceBroadcasts -import org.meshtastic.core.repository.ServiceRepository +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.getString +import org.meshtastic.core.resources.unknown_username +import org.meshtastic.core.service.ServiceRepository import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.NeighborInfo import java.util.Locale @@ -33,21 +35,21 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class NeighborInfoHandlerImpl +class MeshNeighborInfoHandler @Inject constructor( private val nodeManager: NodeManager, private val serviceRepository: ServiceRepository, private val commandSender: CommandSender, private val serviceBroadcasts: ServiceBroadcasts, -) : NeighborInfoHandler { +) { private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - override fun start(scope: CoroutineScope) { + fun start(scope: CoroutineScope) { this.scope = scope } - override fun handleNeighborInfo(packet: MeshPacket) { + fun handleNeighborInfo(packet: MeshPacket) { val payload = packet.decoded?.payload ?: return val ni = NeighborInfo.ADAPTER.decode(payload) @@ -68,7 +70,7 @@ constructor( val neighbors = ni.neighbors.joinToString("\n") { n -> val node = nodeManager.nodeDBbyNodeNum[n.node_id] - val name = node?.let { "${it.user.long_name} (${it.user.short_name})" } ?: "Unknown" + val name = node?.let { "${it.user.long_name} (${it.user.short_name})" } ?: getString(Res.string.unknown_username) "• $name (SNR: ${n.snr})" } diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt index f8c5ed68c9..8416ae8bb5 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt @@ -132,7 +132,7 @@ constructor( nodeDBbyNodeNum.remove(nodeNum)?.let { nodeDBbyID.remove(it.user.id) } } - fun getOrCreateNode(n: Int, channel: Int = 0): Node = nodeDBbyNodeNum.getOrPut(n) { + private fun getOrCreateNode(n: Int, channel: Int = 0): Node = nodeDBbyNodeNum.getOrPut(n) { val userId = DataPacket.nodeNumToDefaultId(n) val defaultUser = User( diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/TracerouteHandlerImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/TracerouteHandlerImpl.kt index 518d4a6826..4fa0801cfa 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/TracerouteHandlerImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/TracerouteHandlerImpl.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.core.data.manager +package com.geeksville.mesh.service import co.touchlab.kermit.Logger import kotlinx.coroutines.CoroutineScope @@ -26,19 +26,24 @@ import org.meshtastic.core.data.repository.TracerouteSnapshotRepository import org.meshtastic.core.model.Node import org.meshtastic.core.model.fullRouteDiscovery import org.meshtastic.core.model.getFullTracerouteResponse -import org.meshtastic.core.model.service.TracerouteResponse import org.meshtastic.core.repository.CommandSender import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.NodeRepository -import org.meshtastic.core.repository.ServiceRepository -import org.meshtastic.core.repository.TracerouteHandler +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.getString +import org.meshtastic.core.resources.traceroute_duration +import org.meshtastic.core.resources.traceroute_route_back_to_us +import org.meshtastic.core.resources.traceroute_route_towards_dest +import org.meshtastic.core.resources.unknown_username +import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.service.TracerouteResponse import org.meshtastic.proto.MeshPacket import java.util.Locale import javax.inject.Inject import javax.inject.Singleton @Singleton -class TracerouteHandlerImpl +class MeshTracerouteHandler @Inject constructor( private val nodeManager: NodeManager, @@ -46,22 +51,22 @@ constructor( private val tracerouteSnapshotRepository: TracerouteSnapshotRepository, private val nodeRepository: NodeRepository, private val commandSender: CommandSender, -) : TracerouteHandler { +) { private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - override fun start(scope: CoroutineScope) { + fun start(scope: CoroutineScope) { this.scope = scope } - override fun handleTraceroute(packet: MeshPacket, logUuid: String?, logInsertJob: kotlinx.coroutines.Job?) { + fun handleTraceroute(packet: MeshPacket, logUuid: String?, logInsertJob: kotlinx.coroutines.Job?) { val full = packet.getFullTracerouteResponse( getUser = { num -> nodeManager.nodeDBbyNodeNum[num]?.let { node: Node -> "${node.user.long_name} (${node.user.short_name})" } - ?: "Unknown" // We don't have strings in core:data yet, but we can fix this later + ?: getString(Res.string.unknown_username) }, - headerTowards = "Route towards destination:", - headerBack = "Route back to us:", + headerTowards = getString(Res.string.traceroute_route_towards_dest), + headerBack = getString(Res.string.traceroute_route_back_to_us), ) ?: return val requestId = packet.decoded?.request_id ?: 0 @@ -85,7 +90,7 @@ constructor( val elapsedMs = nowMillis - start val seconds = elapsedMs / MILLIS_PER_SECOND Logger.i { "Traceroute $requestId complete in $seconds s" } - val durationText = "Duration: %.1f s".format(Locale.US, seconds) + val durationText = getString(Res.string.traceroute_duration, "%.1f".format(Locale.US, seconds)) "$full\n\n$durationText" } else { full diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt index 6ddafb5feb..58f0383dd8 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt @@ -49,6 +49,7 @@ import org.meshtastic.core.model.Node import org.meshtastic.core.model.NodeSortOption import org.meshtastic.core.model.util.onlineTimeThreshold import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.proto.DeviceMetadata import org.meshtastic.proto.HardwareModel import org.meshtastic.proto.LocalStats import org.meshtastic.proto.User @@ -188,8 +189,8 @@ constructor( suspend fun upsert(node: NodeEntity) = withContext(dispatchers.io) { nodeInfoWriteDataSource.upsert(node) } /** Installs initial configuration data (local info and remote nodes) into the database. */ - suspend fun installConfig(mi: MyNodeEntity, nodes: List) = - withContext(dispatchers.io) { nodeInfoWriteDataSource.installConfig(mi, nodes) } + override suspend fun installConfig(mi: MyNodeInfo, nodes: List) = + withContext(dispatchers.io) { nodeInfoWriteDataSource.installConfig(mi.toEntity(), nodes.map { it.toEntity() }) } /** Deletes all nodes from the database, optionally preserving favorites. */ override suspend fun clearNodeDB(preserveFavorites: Boolean) = @@ -221,8 +222,8 @@ constructor( } /** Persists hardware metadata for a node. */ - suspend fun insertMetadata(metadata: MetadataEntity) = - withContext(dispatchers.io) { nodeInfoWriteDataSource.upsert(metadata) } + override suspend fun insertMetadata(nodeNum: Int, metadata: DeviceMetadata) = + withContext(dispatchers.io) { nodeInfoWriteDataSource.upsert(MetadataEntity(nodeNum, metadata)) } /** Flow emitting the count of nodes currently considered "online". */ override val onlineNodeCount: Flow = @@ -240,7 +241,45 @@ constructor( .flowOn(dispatchers.io) .conflate() - /** Updates the personal notes field for a node. */ override suspend fun setNodeNotes(num: Int, notes: String) = withContext(dispatchers.io) { nodeInfoWriteDataSource.setNodeNotes(num, notes) } + + private fun MyNodeInfo.toEntity() = MyNodeEntity( + myNodeNum = myNodeNum, + model = model, + firmwareVersion = firmwareVersion, + couldUpdate = couldUpdate, + shouldUpdate = shouldUpdate, + currentPacketId = currentPacketId, + messageTimeoutMsec = messageTimeoutMsec, + minAppVersion = minAppVersion, + maxChannels = maxChannels, + hasWifi = hasWifi, + deviceId = deviceId, + pioEnv = pioEnv + ) + + private fun Node.toEntity() = NodeEntity( + num = num, + user = user, + position = position, + snr = snr, + rssi = rssi, + lastHeard = lastHeard, + deviceTelemetry = org.meshtastic.proto.Telemetry(device_metrics = deviceMetrics), + channel = channel, + viaMqtt = viaMqtt, + hopsAway = hopsAway, + isFavorite = isFavorite, + isIgnored = isIgnored, + isMuted = isMuted, + environmentTelemetry = org.meshtastic.proto.Telemetry(environment_metrics = environmentMetrics), + powerTelemetry = org.meshtastic.proto.Telemetry(power_metrics = powerMetrics), + paxcounter = paxcounter, + publicKey = publicKey, + notes = notes, + manuallyVerified = manuallyVerified, + nodeStatus = nodeStatus, + lastTransport = lastTransport + ) } diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/PacketRepositoryImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/PacketRepositoryImpl.kt index 7e7bcd9689..0c42eeb118 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/PacketRepositoryImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/PacketRepositoryImpl.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 @@ -27,58 +27,39 @@ import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.withContext import okio.ByteString.Companion.toByteString import org.meshtastic.core.database.DatabaseManager -import org.meshtastic.core.database.entity.ContactSettings -import org.meshtastic.core.database.entity.Packet -import org.meshtastic.core.database.entity.ReactionEntity +import org.meshtastic.core.database.entity.toReaction import org.meshtastic.core.di.CoroutineDispatchers +import org.meshtastic.core.model.ContactSettings import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.Message import org.meshtastic.core.model.MessageStatus 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 -class PacketRepository +@Suppress("TooManyFunctions", "LongParameterList") +class PacketRepositoryImpl @Inject constructor( private val dbManager: DatabaseManager, private val dispatchers: CoroutineDispatchers, ) : SharedPacketRepository { - override suspend fun savePacket( - myNodeNum: Int, - contactKey: String, - packet: DataPacket, - receivedTime: Long, - read: Boolean, - filtered: Boolean, - ) { - val packetToSave = - Packet( - uuid = 0L, - myNodeNum = myNodeNum, - packetId = packet.id, - port_num = packet.dataType, - contact_key = contactKey, - received_time = receivedTime, - read = read, - data = packet, - snr = packet.snr, - rssi = packet.rssi, - hopsAway = packet.hopsAway, - filtered = filtered, - ) - insert(packetToSave) - } - fun getWaypoints(): Flow> = + override fun getWaypoints(): Flow> = dbManager.currentDb.flatMapLatest { db -> db.packetDao().getAllWaypointsFlow() } + .map { list -> list.map { it.data } } - fun getContacts(): Flow> = + override fun getContacts(): Flow> = dbManager.currentDb.flatMapLatest { db -> db.packetDao().getContactKeys() } + .map { map -> map.mapValues { it.value.data } } - fun getContactsPaged(): Flow> = Pager( + override fun getContactsPaged(): Flow> = Pager( config = PagingConfig( pageSize = CONTACTS_PAGE_SIZE, @@ -88,26 +69,27 @@ constructor( pagingSourceFactory = { dbManager.currentDb.value.packetDao().getContactKeysPaged() }, ) .flow + .map { pagingData -> pagingData.map { it.data } } - suspend fun getMessageCount(contact: String): Int = + override suspend fun getMessageCount(contact: String): Int = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getMessageCount(contact) } - suspend fun getUnreadCount(contact: String): Int = + override suspend fun getUnreadCount(contact: String): Int = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getUnreadCount(contact) } - fun getFirstUnreadMessageUuid(contact: String): Flow = + override fun getFirstUnreadMessageUuid(contact: String): Flow = dbManager.currentDb.flatMapLatest { db -> db.packetDao().getFirstUnreadMessageUuid(contact) } - fun hasUnreadMessages(contact: String): Flow = + override fun hasUnreadMessages(contact: String): Flow = dbManager.currentDb.flatMapLatest { db -> db.packetDao().hasUnreadMessages(contact) } - fun getUnreadCountTotal(): Flow = + override fun getUnreadCountTotal(): Flow = dbManager.currentDb.flatMapLatest { db -> db.packetDao().getUnreadCountTotal() } - suspend fun clearUnreadCount(contact: String, timestamp: Long) = + override suspend fun clearUnreadCount(contact: String, timestamp: Long) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().clearUnreadCount(contact, timestamp) } - suspend fun updateLastReadMessage(contact: String, messageUuid: Long, lastReadTimestamp: Long) = + override suspend fun updateLastReadMessage(contact: String, messageUuid: Long, lastReadTimestamp: Long) = withContext(dispatchers.io) { val dao = dbManager.currentDb.value.packetDao() val current = dao.getContactSettings(contact) @@ -116,7 +98,7 @@ constructor( return@withContext } val updated = - (current ?: ContactSettings(contact_key = contact)).copy( + (current ?: ContactSettingsEntity(contact_key = contact)).copy( lastReadMessageUuid = messageUuid, lastReadMessageTimestamp = lastReadTimestamp, ) @@ -126,15 +108,41 @@ constructor( override suspend fun getQueuedPackets(): List? = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getQueuedPackets() } - suspend fun insert(packet: Packet) = + suspend fun insertRoomPacket(packet: RoomPacket) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().insert(packet) } - suspend fun getMessagesFrom( + override suspend fun savePacket( + myNodeNum: Int, + contactKey: String, + packet: DataPacket, + receivedTime: Long, + read: Boolean, + filtered: Boolean, + ) { + val packetToSave = + RoomPacket( + uuid = 0L, + myNodeNum = myNodeNum, + packetId = packet.id, + port_num = packet.dataType, + contact_key = contactKey, + received_time = receivedTime, + read = read, + data = packet, + snr = packet.snr, + rssi = packet.rssi, + hopsAway = packet.hopsAway, + filtered = filtered, + ) + insertRoomPacket(packetToSave) + } + + override suspend fun getMessagesFrom( contact: String, - limit: Int? = null, - includeFiltered: Boolean = true, + limit: Int?, + includeFiltered: Boolean, getNode: suspend (String?) -> Node, - ) = withContext(dispatchers.io) { + ): Flow> = withContext(dispatchers.io) { val dao = dbManager.currentDb.value.packetDao() val flow = when { @@ -147,14 +155,14 @@ constructor( val message = packet.toMessage(getNode) message.replyId .takeIf { it != null && it != 0 } - ?.let { getPacketByPacketId(it) } - ?.toMessage(getNode) + ?.let { getPacketByPacketIdInternal(it) } + ?.let { originalPacket -> originalPacket.toMessage(getNode) } ?.let { originalMessage -> message.copy(originalMessage = originalMessage) } ?: message } } } - fun getMessagesFromPaged(contact: String, getNode: suspend (String?) -> Node): Flow> = Pager( + override fun getMessagesFromPaged(contact: String, getNode: suspend (String?) -> Node): Flow> = Pager( config = PagingConfig( pageSize = MESSAGES_PAGE_SIZE, @@ -169,8 +177,35 @@ constructor( val message = packet.toMessage(getNode) message.replyId .takeIf { it != null && it != 0 } - ?.let { getPacketByPacketId(it) } - ?.toMessage(getNode) + ?.let { getPacketByPacketIdInternal(it) } + ?.let { originalPacket -> originalPacket.toMessage(getNode) } + ?.let { originalMessage -> message.copy(originalMessage = originalMessage) } ?: message + } + } + + override fun getMessagesFromPaged( + contactKey: String, + includeFiltered: Boolean, + getNode: suspend (String?) -> Node, + ): Flow> = Pager( + config = + PagingConfig( + pageSize = MESSAGES_PAGE_SIZE, + enablePlaceholders = false, + initialLoadSize = MESSAGES_PAGE_SIZE, + ), + pagingSourceFactory = { + dbManager.currentDb.value.packetDao().getMessagesFromPaged(contactKey, includeFiltered) + }, + ) + .flow + .map { pagingData -> + pagingData.map { packet -> + val message = packet.toMessage(getNode) + message.replyId + .takeIf { it != null && it != 0 } + ?.let { getPacketByPacketIdInternal(it) } + ?.let { originalPacket -> originalPacket.toMessage(getNode) } ?.let { originalMessage -> message.copy(originalMessage = originalMessage) } ?: message } } @@ -178,31 +213,99 @@ constructor( override suspend fun updateMessageStatus(d: DataPacket, m: MessageStatus) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().updateMessageStatus(d, m) } - suspend fun updateMessageId(d: DataPacket, id: Int) = + override suspend fun updateMessageId(d: DataPacket, id: Int) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().updateMessageId(d, id) } - suspend fun getPacketById(requestId: Int) = + private suspend fun getPacketByIdInternal(requestId: Int) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getPacketById(requestId) } - suspend fun getPacketByPacketId(packetId: Int) = + override suspend fun getPacketById(id: Int): DataPacket? = + withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getPacketById(id)?.data } + + override suspend fun getPacketByPacketId(packetId: Int): DataPacket? = + withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getPacketByPacketId(packetId)?.packet?.data } + + private suspend fun getPacketByPacketIdInternal(packetId: Int) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getPacketByPacketId(packetId) } - suspend fun findPacketsWithId(packetId: Int) = + override suspend fun insert( + packet: DataPacket, + myNodeNum: Int, + contactKey: String, + receivedTime: Long, + read: Boolean, + filtered: Boolean, + ) { + val packetToSave = + RoomPacket( + uuid = 0L, + myNodeNum = myNodeNum, + packetId = packet.id, + port_num = packet.dataType, + contact_key = contactKey, + received_time = receivedTime, + read = read, + data = packet, + snr = packet.snr, + rssi = packet.rssi, + hopsAway = packet.hopsAway, + filtered = filtered, + ) + insertRoomPacket(packetToSave) + } + + override suspend fun update(packet: DataPacket): Unit = withContext(dispatchers.io) { + val dao = dbManager.currentDb.value.packetDao() + dao.findPacketsWithId(packet.id).find { it.data == packet }?.let { + dao.update(it.copy(data = packet)) + } + } + + override suspend fun insertReaction(reaction: Reaction, myNodeNum: Int) = withContext(dispatchers.io) { + dbManager.currentDb.value.packetDao().insert(reaction.toEntity(myNodeNum)) + } + + override suspend fun updateReaction(reaction: Reaction) = withContext(dispatchers.io) { + val dao = dbManager.currentDb.value.packetDao() + dao.findReactionsWithId(reaction.packetId).find { it.userId == reaction.user.id && it.emoji == reaction.emoji }?.let { + dao.update(reaction.toEntity(it.myNodeNum)) + } ?: Unit + } + + override suspend fun getReactionByPacketId(packetId: Int): Reaction? = withContext(dispatchers.io) { + dbManager.currentDb.value.packetDao().getReactionByPacketId(packetId)?.toReaction { null } + } + + private suspend fun getReactionByPacketIdInternal(packetId: Int) = + withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getReactionByPacketId(packetId) } + + override suspend fun findPacketsWithId(packetId: Int): List = withContext(dispatchers.io) { + dbManager.currentDb.value.packetDao().findPacketsWithId(packetId).map { it.data } + } + + private suspend fun findPacketsWithIdInternal(packetId: Int) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().findPacketsWithId(packetId) } + override suspend fun findReactionsWithId(packetId: Int): List = withContext(dispatchers.io) { + dbManager.currentDb.value.packetDao().findReactionsWithId(packetId).toReaction { null } + } + + private suspend fun findReactionsWithIdInternal(packetId: Int) = + withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().findReactionsWithId(packetId) } + @Suppress("CyclomaticComplexMethod") - suspend fun updateSFPPStatus( + override suspend fun updateSFPPStatus( packetId: Int, from: Int, to: Int, hash: ByteArray, - status: MessageStatus = MessageStatus.SFPP_CONFIRMED, - rxTime: Long = 0, - myNodeNum: Int? = null, + status: MessageStatus, + rxTime: Long, + myNodeNum: Int?, ) = withContext(dispatchers.io) { val dao = dbManager.currentDb.value.packetDao() - val packets = dao.findPacketsWithId(packetId) - val reactions = dao.findReactionsWithId(packetId) + val packets = findPacketsWithIdInternal(packetId) + val reactions = findReactionsWithIdInternal(packetId) val fromId = DataPacket.nodeNumToDefaultId(from) val isFromLocalNode = myNodeNum != null && from == myNodeNum val toId = @@ -259,11 +362,11 @@ constructor( } } - suspend fun updateSFPPStatusByHash( + override suspend fun updateSFPPStatusByHash( hash: ByteArray, - status: MessageStatus = MessageStatus.SFPP_CONFIRMED, - rxTime: Long = 0, - ) = withContext(dispatchers.io) { + status: MessageStatus, + rxTime: Long, + ): Unit = withContext(dispatchers.io) { val dao = dbManager.currentDb.value.packetDao() val hashByteString = hash.toByteString() dao.findPacketBySfppHash(hashByteString)?.let { packet -> @@ -286,99 +389,95 @@ constructor( } } - suspend fun deleteMessages(uuidList: List) = withContext(dispatchers.io) { + override suspend fun deleteMessages(uuidList: List) = withContext(dispatchers.io) { for (chunk in uuidList.chunked(DELETE_CHUNK_SIZE)) { // Fetch DAO per chunk to avoid holding a stale reference if the active DB switches dbManager.currentDb.value.packetDao().deleteMessages(chunk) } } - suspend fun deleteContacts(contactList: List) = + override suspend fun deleteContacts(contactList: List) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().deleteContacts(contactList) } - suspend fun deleteWaypoint(id: Int) = + override suspend fun deleteWaypoint(id: Int) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().deleteWaypoint(id) } - suspend fun delete(packet: Packet) = + suspend fun delete(packet: RoomPacket) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().delete(packet) } - suspend fun update(packet: Packet) = + suspend fun update(packet: RoomPacket) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().update(packet) } - fun getContactSettings(): Flow> = + override fun getContactSettings(): Flow> = dbManager.currentDb.flatMapLatest { db -> db.packetDao().getContactSettings() } + .map { map -> map.mapValues { it.value.toShared() } } - suspend fun getContactSettings(contact: String) = withContext(dispatchers.io) { - dbManager.currentDb.value.packetDao().getContactSettings(contact) ?: ContactSettings(contact) + override suspend fun getContactSettings(contact: String): ContactSettings = withContext(dispatchers.io) { + dbManager.currentDb.value.packetDao().getContactSettings(contact)?.toShared() ?: ContactSettings(contact) } - suspend fun setMuteUntil(contacts: List, until: Long) = + override suspend fun setMuteUntil(contacts: List, until: Long) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().setMuteUntil(contacts, until) } - suspend fun insertReaction(reaction: ReactionEntity) = + suspend fun insertReaction(reaction: RoomReaction) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().insert(reaction) } - suspend fun updateReaction(reaction: ReactionEntity) = + suspend fun updateReaction(reaction: RoomReaction) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().update(reaction) } - suspend fun getReactionByPacketId(packetId: Int) = - withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getReactionByPacketId(packetId) } - - suspend fun findReactionsWithId(packetId: Int) = - withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().findReactionsWithId(packetId) } - - fun getFilteredCountFlow(contactKey: String): Flow = + override fun getFilteredCountFlow(contactKey: String): Flow = dbManager.currentDb.flatMapLatest { db -> db.packetDao().getFilteredCountFlow(contactKey) } - suspend fun getFilteredCount(contactKey: String): Int = + override suspend fun getFilteredCount(contactKey: String): Int = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getFilteredCount(contactKey) } - fun getMessagesFromPaged( - contactKey: String, - includeFiltered: Boolean, - getNode: suspend (String?) -> Node, - ): Flow> = Pager( - config = - PagingConfig( - pageSize = MESSAGES_PAGE_SIZE, - enablePlaceholders = false, - initialLoadSize = MESSAGES_PAGE_SIZE, - ), - pagingSourceFactory = { - dbManager.currentDb.value.packetDao().getMessagesFromPaged(contactKey, includeFiltered) - }, - ) - .flow - .map { pagingData -> - pagingData.map { packet -> - val message = packet.toMessage(getNode) - message.replyId - .takeIf { it != null && it != 0 } - ?.let { getPacketByPacketId(it) } - ?.toMessage(getNode) - ?.let { originalMessage -> message.copy(originalMessage = originalMessage) } ?: message - } - } - - suspend fun setContactFilteringDisabled(contactKey: String, disabled: Boolean) = withContext(dispatchers.io) { + override suspend fun setContactFilteringDisabled(contactKey: String, disabled: Boolean) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().setContactFilteringDisabled(contactKey, disabled) } - suspend fun clearPacketDB() = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().deleteAll() } + override suspend fun clearPacketDB() = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().deleteAll() } - suspend fun migrateChannelsByPSK(oldSettings: List, newSettings: List) = + override suspend fun migrateChannelsByPSK(oldSettings: List, newSettings: List) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().migrateChannelsByPSK(oldSettings, newSettings) } - suspend fun updateFilteredBySender(senderId: String, filtered: Boolean) { + override suspend fun updateFilteredBySender(senderId: String, filtered: Boolean) { val pattern = "%\"from\":\"${senderId}\"%" withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().updateFilteredBySender(pattern, filtered) } } - private fun org.meshtastic.core.database.dao.PacketDao.getAllWaypointsFlow(): Flow> = + private fun org.meshtastic.core.database.dao.PacketDao.getAllWaypointsFlow(): Flow> = getAllPackets(PortNum.WAYPOINT_APP.value) + private fun ContactSettingsEntity.toShared() = ContactSettings( + contactKey = contact_key, + muteUntil = muteUntil, + lastReadMessageUuid = lastReadMessageUuid, + lastReadMessageTimestamp = lastReadMessageTimestamp, + filteringDisabled = filteringDisabled, + isMuted = isMuted + ) + + private fun Reaction.toEntity(myNodeNum: Int) = RoomReaction( + myNodeNum = myNodeNum, + replyId = replyId, + userId = user.id, + emoji = emoji, + timestamp = timestamp, + snr = snr, + rssi = rssi, + hopsAway = hopsAway, + packetId = packetId, + status = status, + routingError = routingError, + relays = relays, + relayNode = relayNode, + to = to, + channel = channel, + sfpp_hash = sfppHash + ) + companion object { private const val CONTACTS_PAGE_SIZE = 30 private const val MESSAGES_PAGE_SIZE = 50 diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/RadioConfigRepositoryImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/RadioConfigRepositoryImpl.kt index 3454a9de16..dcbd93a341 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/RadioConfigRepositoryImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/RadioConfigRepositoryImpl.kt @@ -23,6 +23,7 @@ import org.meshtastic.core.datastore.LocalConfigDataSource import org.meshtastic.core.datastore.ModuleConfigDataSource import org.meshtastic.core.model.util.getChannelUrl import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.proto.Channel import org.meshtastic.proto.ChannelSet import org.meshtastic.proto.ChannelSettings @@ -37,25 +38,25 @@ import javax.inject.Inject * Class responsible for radio configuration data. Combines access to [nodeDB], [ChannelSet], [LocalConfig] & * [LocalModuleConfig]. */ -open class RadioConfigRepository +open class RadioConfigRepositoryImpl @Inject constructor( private val nodeDB: NodeRepository, private val channelSetDataSource: ChannelSetDataSource, private val localConfigDataSource: LocalConfigDataSource, private val moduleConfigDataSource: ModuleConfigDataSource, -) { +) : RadioConfigRepository { /** Flow representing the [ChannelSet] data store. */ - val channelSetFlow: Flow = channelSetDataSource.channelSetFlow + override val channelSetFlow: Flow = channelSetDataSource.channelSetFlow /** Clears the [ChannelSet] data in the data store. */ - suspend fun clearChannelSet() { + override suspend fun clearChannelSet() { channelSetDataSource.clearChannelSet() } /** Replaces the [ChannelSettings] list with a new [settingsList]. */ - suspend fun replaceAllSettings(settingsList: List) { + override suspend fun replaceAllSettings(settingsList: List) { channelSetDataSource.replaceAllSettings(settingsList) } @@ -66,13 +67,13 @@ constructor( * @param channel The [Channel] provided. * @return the index of the admin channel after the update (if not found, returns 0). */ - suspend fun updateChannelSettings(channel: Channel) = channelSetDataSource.updateChannelSettings(channel) + override suspend fun updateChannelSettings(channel: Channel) = channelSetDataSource.updateChannelSettings(channel) /** Flow representing the [LocalConfig] data store. */ - open val localConfigFlow: Flow = localConfigDataSource.localConfigFlow + override val localConfigFlow: Flow = localConfigDataSource.localConfigFlow /** Clears the [LocalConfig] data in the data store. */ - suspend fun clearLocalConfig() { + override suspend fun clearLocalConfig() { localConfigDataSource.clearLocalConfig() } @@ -81,16 +82,16 @@ constructor( * * @param config The [Config] to be set. */ - suspend fun setLocalConfig(config: Config) { + override suspend fun setLocalConfig(config: Config) { localConfigDataSource.setLocalConfig(config) config.lora?.let { channelSetDataSource.setLoraConfig(it) } } /** Flow representing the [LocalModuleConfig] data store. */ - val moduleConfigFlow: Flow = moduleConfigDataSource.moduleConfigFlow + override val moduleConfigFlow: Flow = moduleConfigDataSource.moduleConfigFlow /** Clears the [LocalModuleConfig] data in the data store. */ - suspend fun clearLocalModuleConfig() { + override suspend fun clearLocalModuleConfig() { moduleConfigDataSource.clearLocalModuleConfig() } @@ -99,12 +100,12 @@ constructor( * * @param config The [ModuleConfig] to be set. */ - suspend fun setLocalModuleConfig(config: ModuleConfig) { + override suspend fun setLocalModuleConfig(config: ModuleConfig) { moduleConfigDataSource.setLocalModuleConfig(config) } /** Flow representing the combined [DeviceProfile] protobuf. */ - val deviceProfileFlow: Flow = + override val deviceProfileFlow: Flow = combine(nodeDB.ourNodeInfo, channelSetFlow, localConfigFlow, moduleConfigFlow) { node, channels, diff --git a/core/data/src/test/kotlin/org/meshtastic/core/data/manager/CommandSenderHopLimitTest.kt b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/CommandSenderHopLimitTest.kt index 679729176b..3e938790c9 100644 --- a/core/data/src/test/kotlin/org/meshtastic/core/data/manager/CommandSenderHopLimitTest.kt +++ b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/CommandSenderHopLimitTest.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.core.data.manager +package com.geeksville.mesh.service import io.mockk.every import io.mockk.mockk @@ -29,21 +29,18 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test +import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.DataPacket -import org.meshtastic.core.model.Node -import org.meshtastic.core.repository.CommandSender -import org.meshtastic.core.repository.NodeManager -import org.meshtastic.core.repository.PacketHandler import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.proto.Config import org.meshtastic.proto.LocalConfig import org.meshtastic.proto.MeshPacket -import org.meshtastic.proto.User class CommandSenderHopLimitTest { private val packetHandler: PacketHandler = mockk(relaxed = true) - private val nodeManager: NodeManager = mockk(relaxed = true) + private val nodeManager = NodeManagerImpl() + private val connectionStateHolder: ConnectionStateHandler = mockk(relaxed = true) private val radioConfigRepository: RadioConfigRepository = mockk(relaxed = true) private val localConfigFlow = MutableStateFlow(LocalConfig()) @@ -54,14 +51,13 @@ class CommandSenderHopLimitTest { @Before fun setUp() { - val myNum = 123 - val myNode = Node(num = myNum, user = User(id = "!id", long_name = "long", short_name = "shrt")) + val connectedFlow = MutableStateFlow(ConnectionState.Connected) + every { connectionStateHolder.connectionState } returns connectedFlow every { radioConfigRepository.localConfigFlow } returns localConfigFlow - every { nodeManager.myNodeNum } returns myNum - every { nodeManager.nodeDBbyNodeNum } returns mapOf(myNum to myNode) - commandSender = CommandSenderImpl(packetHandler, nodeManager, radioConfigRepository) + commandSender = CommandSenderImpl(packetHandler, nodeManager, connectionStateHolder, radioConfigRepository) commandSender.start(testScope) + nodeManager.myNodeNum = 123 } @Test @@ -115,10 +111,7 @@ class CommandSenderHopLimitTest { localConfigFlow.value = LocalConfig(lora = Config.LoRaConfig(hop_limit = 6)) // Mock node manager interactions - // Note: we need to keep myNode in the map for requestUserInfo to not return early - val myNum = 123 - val myNode = Node(num = myNum, user = User(id = "!id", long_name = "long", short_name = "shrt")) - every { nodeManager.nodeDBbyNodeNum } returns mapOf(myNum to myNode) + nodeManager.nodeDBbyNodeNum.remove(destNum) commandSender.requestUserInfo(destNum) diff --git a/core/data/src/test/kotlin/org/meshtastic/core/data/manager/CommandSenderImplTest.kt b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/CommandSenderImplTest.kt index 69996dde9b..4b12c3d80c 100644 --- a/core/data/src/test/kotlin/org/meshtastic/core/data/manager/CommandSenderImplTest.kt +++ b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/CommandSenderImplTest.kt @@ -14,28 +14,25 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.core.data.manager +package com.geeksville.mesh.service -import io.mockk.every -import io.mockk.mockk import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals import org.junit.Before import org.junit.Test +import org.meshtastic.core.database.entity.NodeEntity import org.meshtastic.core.model.DataPacket -import org.meshtastic.core.model.Node -import org.meshtastic.core.repository.NodeManager import org.meshtastic.proto.User -class CommandSenderImplTest { +class CommandSenderTest { - private lateinit var commandSender: CommandSenderImpl + private lateinit var commandSender: CommandSender private lateinit var nodeManager: NodeManager @Before fun setUp() { - nodeManager = mockk(relaxed = true) - commandSender = CommandSenderImpl(mockk(relaxed = true), nodeManager, mockk(relaxed = true)) + nodeManager = NodeManagerImpl() + commandSender = CommandSenderImpl(null, nodeManager, null, null) } @Test @@ -63,8 +60,9 @@ class CommandSenderImplTest { fun `resolveNodeNum handles custom node ID from database`() { val nodeNum = 456 val userId = "custom_id" - val node = Node(num = nodeNum, user = User(id = userId)) - every { nodeManager.nodeDBbyID } returns mapOf(userId to node) + val entity = NodeEntity(num = nodeNum, user = User(id = userId)) + nodeManager.nodeDBbyNodeNum[nodeNum] = entity + nodeManager.nodeDBbyID[userId] = entity assertEquals(nodeNum, commandSender.resolveNodeNum(userId)) } diff --git a/core/data/src/test/kotlin/org/meshtastic/core/data/manager/NodeManagerImplTest.kt b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/NodeManagerImplTest.kt index 4748663ba3..9dcba64894 100644 --- a/core/data/src/test/kotlin/org/meshtastic/core/data/manager/NodeManagerImplTest.kt +++ b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/NodeManagerImplTest.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.core.data.manager +package com.geeksville.mesh.service import io.mockk.mockk import org.junit.Assert.assertEquals @@ -26,18 +26,17 @@ import org.junit.Test import org.meshtastic.core.model.DataPacket import org.meshtastic.core.repository.MeshServiceNotifications import org.meshtastic.core.repository.NodeRepository -import org.meshtastic.core.repository.ServiceBroadcasts import org.meshtastic.proto.HardwareModel import org.meshtastic.proto.Position import org.meshtastic.proto.User -class NodeManagerImplTest { +class NodeManagerTest { private val nodeRepository: NodeRepository = mockk(relaxed = true) private val serviceBroadcasts: ServiceBroadcasts = mockk(relaxed = true) private val serviceNotifications: MeshServiceNotifications = mockk(relaxed = true) - private lateinit var nodeManager: NodeManagerImpl + private lateinit var nodeManager: NodeManager @Before fun setUp() { @@ -45,13 +44,13 @@ class NodeManagerImplTest { } @Test - fun `getOrCreateNode creates default user for unknown node`() { + fun `getOrCreateNodeInfo creates default user for unknown node`() { val nodeNum = 1234 - val result = nodeManager.getOrCreateNode(nodeNum) + val result = nodeManager.getOrCreateNodeInfo(nodeNum) assertNotNull(result) assertEquals(nodeNum, result.num) - assertTrue(result.user.long_name.startsWith("Meshtastic")) + assertTrue(result.user.long_name?.startsWith("Meshtastic") == true) assertEquals(DataPacket.nodeNumToDefaultId(nodeNum), result.user.id) } @@ -62,7 +61,7 @@ class NodeManagerImplTest { User(id = "!12345678", long_name = "My Custom Name", short_name = "MCN", hw_model = HardwareModel.TLORA_V2) // Setup existing node - nodeManager.updateNode(nodeNum) { it.copy(user = existingUser) } + nodeManager.updateNode(nodeNum) { it.user = existingUser } val incomingDefaultUser = User(id = "!12345678", long_name = "Meshtastic 5678", short_name = "5678", hw_model = HardwareModel.UNSET) @@ -80,7 +79,7 @@ class NodeManagerImplTest { val existingUser = User(id = "!12345678", long_name = "Meshtastic 5678", short_name = "5678", hw_model = HardwareModel.UNSET) - nodeManager.updateNode(nodeNum) { it.copy(user = existingUser) } + nodeManager.updateNode(nodeNum) { it.user = existingUser } val incomingDetailedUser = User(id = "!12345678", long_name = "Real User", short_name = "RU", hw_model = HardwareModel.TLORA_V1) @@ -97,7 +96,7 @@ class NodeManagerImplTest { val nodeNum = 1234 val position = Position(latitude_i = 450000000, longitude_i = 900000000) - nodeManager.handleReceivedPosition(nodeNum, 9999, position, 0) + nodeManager.handleReceivedPosition(nodeNum, 9999, position) val result = nodeManager.nodeDBbyNodeNum[nodeNum] assertNotNull(result!!.position) @@ -107,7 +106,7 @@ class NodeManagerImplTest { @Test fun `clear resets internal state`() { - nodeManager.updateNode(1234) { it.copy(user = it.user.copy(long_name = "Test")) } + nodeManager.updateNode(1234) { it.longName = "Test" } nodeManager.clear() assertTrue(nodeManager.nodeDBbyNodeNum.isEmpty()) diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/DatabaseManager.kt b/core/database/src/main/kotlin/org/meshtastic/core/database/DatabaseManager.kt index 5b559f58fe..6630228743 100644 --- a/core/database/src/main/kotlin/org/meshtastic/core/database/DatabaseManager.kt +++ b/core/database/src/main/kotlin/org/meshtastic/core/database/DatabaseManager.kt @@ -41,17 +41,13 @@ import java.io.File import java.security.MessageDigest import javax.inject.Inject import javax.inject.Singleton +import org.meshtastic.core.repository.DatabaseManager as SharedDatabaseManager /** Manages per-device Room database instances for node data, with LRU eviction. */ @Singleton @Suppress("TooManyFunctions") @OptIn(ExperimentalCoroutinesApi::class) -open class DatabaseManager -@Inject -constructor( - private val app: Application, - private val dispatchers: CoroutineDispatchers, -) { +open class DatabaseManager @Inject constructor(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/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCase.kt b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCase.kt index 4515564d38..51cf4af0ee 100644 --- a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCase.kt +++ b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCase.kt @@ -20,7 +20,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf -import org.meshtastic.core.data.repository.DeviceHardwareRepository import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.Node import org.meshtastic.core.model.RadioController @@ -28,6 +27,7 @@ import org.meshtastic.core.prefs.radio.RadioPrefs import org.meshtastic.core.prefs.radio.isBle import org.meshtastic.core.prefs.radio.isSerial import org.meshtastic.core.prefs.radio.isTcp +import org.meshtastic.core.repository.DeviceHardwareRepository import org.meshtastic.core.repository.NodeRepository import javax.inject.Inject diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Node.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Node.kt index 1f23228028..c667e9098c 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Node.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Node.kt @@ -21,7 +21,6 @@ import okio.ByteString.Companion.toByteString import org.meshtastic.core.common.util.GPSFormat import org.meshtastic.core.common.util.bearing import org.meshtastic.core.common.util.latLongToMeter -import org.meshtastic.core.model.Node.Companion.ERROR_BYTE_STRING import org.meshtastic.core.model.util.UnitConversions.celsiusToFahrenheit import org.meshtastic.core.model.util.onlineTimeThreshold import org.meshtastic.core.model.util.toDistanceString @@ -216,8 +215,6 @@ data class Node( return closestRelayNode } - val ERROR_BYTE_STRING: ByteString = ByteArray(32) { 0 }.toByteString() - /** Creates a fallback [Node] when the node is not found in the database. */ fun createFallback(nodeNum: Int, fallbackNamePrefix: String): Node { val userId = DataPacket.nodeNumToDefaultId(nodeNum) diff --git a/core/repository/build.gradle.kts b/core/repository/build.gradle.kts index 120d38c4ce..1695705a02 100644 --- a/core/repository/build.gradle.kts +++ b/core/repository/build.gradle.kts @@ -33,6 +33,7 @@ kotlin { implementation(libs.kotlinx.coroutines.core) implementation(libs.kermit) + implementation(libs.androidx.paging.common) } } } diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NeighborInfoHandler.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NeighborInfoHandler.kt index 45a8dc72aa..44e6bde5ab 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NeighborInfoHandler.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NeighborInfoHandler.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,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.core.repository +package com.geeksville.mesh.service -import kotlinx.coroutines.CoroutineScope +import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.repository.NodeManager import org.meshtastic.proto.MeshPacket +import javax.inject.Inject +import javax.inject.Singleton +import org.meshtastic.core.model.util.MeshDataMapper as CommonMeshDataMapper -/** - * Interface for handling neighbor info responses from the mesh. - */ -interface NeighborInfoHandler { - /** Starts the neighbor info handler with the given coroutine scope. */ - fun start(scope: CoroutineScope) +@Singleton +class MeshDataMapper @Inject constructor(private val nodeManager: NodeManager) { + private val commonMapper = CommonMeshDataMapper(nodeManager) + + fun toNodeID(n: Int): String = nodeManager.toNodeID(n) - /** - * Processes a neighbor info packet. - * - * @param packet The received mesh packet. - */ - fun handleNeighborInfo(packet: MeshPacket) + fun toDataPacket(packet: MeshPacket): DataPacket? = commonMapper.toDataPacket(packet) } diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeRepository.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeRepository.kt index a3c60a1444..e96fc90688 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeRepository.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeRepository.kt @@ -21,6 +21,7 @@ import kotlinx.coroutines.flow.StateFlow import org.meshtastic.core.model.MyNodeInfo import org.meshtastic.core.model.Node import org.meshtastic.core.model.NodeSortOption +import org.meshtastic.proto.DeviceMetadata import org.meshtastic.proto.LocalStats import org.meshtastic.proto.User @@ -92,4 +93,10 @@ interface NodeRepository { /** Updates the personal notes for a node. */ suspend fun setNodeNotes(num: Int, notes: String) + + /** Installs initial configuration data (local info and remote nodes) into the database. */ + suspend fun installConfig(mi: MyNodeInfo, nodes: List) + + /** Persists hardware metadata for a node. */ + suspend fun insertMetadata(nodeNum: Int, metadata: DeviceMetadata) } diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketRepository.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketRepository.kt index e2f9756978..018e0a7a7b 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketRepository.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketRepository.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 @@ -16,10 +16,39 @@ */ package org.meshtastic.core.repository +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import org.meshtastic.core.model.ContactSettings import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.model.Message import org.meshtastic.core.model.MessageStatus +import org.meshtastic.core.model.Node +import org.meshtastic.core.model.Reaction +import org.meshtastic.proto.ChannelSettings interface PacketRepository { + fun getWaypoints(): Flow> + + fun getContacts(): Flow> + + fun getContactsPaged(): Flow> + + suspend fun getMessageCount(contact: String): Int + + suspend fun getUnreadCount(contact: String): Int + + fun getFirstUnreadMessageUuid(contact: String): Flow + + fun hasUnreadMessages(contact: String): Flow + + fun getUnreadCountTotal(): Flow + + suspend fun clearUnreadCount(contact: String, timestamp: Long) + + suspend fun updateLastReadMessage(contact: String, messageUuid: Long, lastReadTimestamp: Long) + + suspend fun getQueuedPackets(): List? + suspend fun savePacket( myNodeNum: Int, contactKey: String, @@ -28,8 +57,84 @@ interface PacketRepository { read: Boolean = true, filtered: Boolean = false, ) - + + suspend fun getMessagesFrom( + contact: String, + limit: Int? = null, + includeFiltered: Boolean = true, + getNode: suspend (String?) -> Node, + ): Flow> + + fun getMessagesFromPaged( + contact: String, + getNode: suspend (String?) -> Node + ): Flow> + + fun getMessagesFromPaged( + contactKey: String, + includeFiltered: Boolean, + getNode: suspend (String?) -> Node, + ): Flow> + suspend fun updateMessageStatus(d: DataPacket, m: MessageStatus) + + suspend fun updateMessageId(d: DataPacket, id: Int) + + suspend fun deleteMessages(uuidList: List) + + suspend fun deleteContacts(contactList: List) + + suspend fun deleteWaypoint(id: Int) + + fun getContactSettings(): Flow> + + suspend fun getContactSettings(contact: String): ContactSettings + + suspend fun setMuteUntil(contacts: List, until: Long) + + fun getFilteredCountFlow(contactKey: String): Flow + + suspend fun getFilteredCount(contactKey: String): Int + + suspend fun setContactFilteringDisabled(contactKey: String, disabled: Boolean) + + suspend fun clearPacketDB() + + suspend fun migrateChannelsByPSK(oldSettings: List, newSettings: List) + + suspend fun updateFilteredBySender(senderId: String, filtered: Boolean) - suspend fun getQueuedPackets(): List? + suspend fun getPacketByPacketId(packetId: Int): DataPacket? + + suspend fun getPacketById(id: Int): DataPacket? + + suspend fun insert(packet: DataPacket, myNodeNum: Int, contactKey: String, receivedTime: Long, read: Boolean = true, filtered: Boolean = false) + + suspend fun update(packet: DataPacket) + + suspend fun insertReaction(reaction: Reaction, myNodeNum: Int) + + suspend fun updateReaction(reaction: Reaction) + + suspend fun getReactionByPacketId(packetId: Int): Reaction? + + suspend fun findPacketsWithId(packetId: Int): List + + suspend fun findReactionsWithId(packetId: Int): List + + suspend fun updateSFPPStatus( + packetId: Int, + from: Int, + to: Int, + hash: ByteArray, + status: MessageStatus, + rxTime: Long, + myNodeNum: Int?, + ) + + suspend fun updateSFPPStatusByHash( + hash: ByteArray, + status: MessageStatus, + rxTime: Long, + ) } 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 e9eeef11f9..128b0e5d5d 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 @@ -16,12 +16,14 @@ */ package org.meshtastic.core.ui.qr +import android.os.RemoteException import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import co.touchlab.kermit.Logger import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import org.meshtastic.core.model.RadioController import org.meshtastic.core.repository.RadioConfigRepository +import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.util.getChannelList import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.Channel @@ -35,7 +37,7 @@ class ScannedQrCodeViewModel @Inject constructor( private val radioConfigRepository: RadioConfigRepository, - private val radioController: RadioController, + private val serviceRepository: ServiceRepository, ) : ViewModel() { val channels = radioConfigRepository.channelSetFlow.stateInWhileSubscribed(initialValue = ChannelSet()) @@ -54,15 +56,19 @@ constructor( } private fun setChannel(channel: Channel) { - viewModelScope.launch { - radioController.setLocalChannel(channel) + try { + serviceRepository.meshService?.setChannel(Channel.ADAPTER.encode(channel)) + } catch (ex: RemoteException) { + Logger.e(ex) { "Set channel error" } } } // Set the radio config (also updates our saved copy in preferences) private fun setConfig(config: Config) { - viewModelScope.launch { - radioController.setLocalConfig(config) + try { + serviceRepository.meshService?.setConfig(Config.ADAPTER.encode(config)) + } catch (ex: RemoteException) { + Logger.e(ex) { "Set config error" } } } } diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt b/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt index 92d70fe4e9..5c8e0a3a5b 100644 --- a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt +++ b/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt @@ -44,7 +44,6 @@ import org.meshtastic.core.datastore.BootloaderWarningDataSource import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.DeviceHardware import org.meshtastic.core.model.MyNodeInfo -import org.meshtastic.core.model.RadioController import org.meshtastic.core.prefs.radio.RadioPrefs import org.meshtastic.core.prefs.radio.isBle import org.meshtastic.core.prefs.radio.isSerial @@ -73,6 +72,7 @@ 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 org.meshtastic.core.service.ServiceRepository import java.io.File import javax.inject.Inject @@ -95,7 +95,7 @@ constructor( private val firmwareReleaseRepository: FirmwareReleaseRepository, private val deviceHardwareRepository: DeviceHardwareRepository, private val nodeRepository: NodeRepository, - private val radioController: RadioController, + private val serviceRepository: ServiceRepository, private val radioPrefs: RadioPrefs, private val bootloaderWarningDataSource: BootloaderWarningDataSource, private val firmwareUpdateManager: FirmwareUpdateManager, @@ -106,8 +106,6 @@ constructor( private val _state = MutableStateFlow(FirmwareUpdateState.Idle) val state: StateFlow = _state.asStateFlow() - val connectionState = radioController.connectionState - private val _selectedReleaseType = MutableStateFlow(FirmwareReleaseType.STABLE) val selectedReleaseType: StateFlow = _selectedReleaseType.asStateFlow() @@ -431,14 +429,14 @@ constructor( // Trigger a fresh connection attempt by MeshService address?.let { currentAddr -> Logger.i { "Post-update: Requesting MeshService to reconnect to $currentAddr" } - radioController.setDeviceAddress("$DFU_RECONNECT_PREFIX$currentAddr") + serviceRepository.meshService?.setDeviceAddress("$DFU_RECONNECT_PREFIX$currentAddr") } // Wait for device to reconnect and settle val result = withTimeoutOrNull(VERIFY_TIMEOUT) { // Wait for both Connected state and node info to be present - connectionState.first { it is ConnectionState.Connected } + serviceRepository.connectionState.first { it is ConnectionState.Connected } nodeRepository.ourNodeInfo.filterNotNull().first() delay(VERIFY_DELAY) // Extra buffer for initial config sync true diff --git a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewModel.kt b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewModel.kt index 66b2e3b0c1..c0434ae585 100644 --- a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewModel.kt +++ b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewModel.kt @@ -23,13 +23,13 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import org.meshtastic.core.common.BuildConfigProvider -import org.meshtastic.core.model.DataPacket -import org.meshtastic.core.model.RadioController -import org.meshtastic.core.navigation.MapRoutes -import org.meshtastic.core.prefs.map.MapPrefs import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.RadioConfigRepository +import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.navigation.MapRoutes +import org.meshtastic.core.prefs.map.MapPrefs +import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.LocalConfig import javax.inject.Inject @@ -41,12 +41,12 @@ class MapViewModel constructor( mapPrefs: MapPrefs, packetRepository: PacketRepository, - override val nodeRepository: NodeRepository, - radioController: RadioController, + private val nodeRepository: NodeRepository, + serviceRepository: ServiceRepository, radioConfigRepository: RadioConfigRepository, buildConfigProvider: BuildConfigProvider, savedStateHandle: SavedStateHandle, -) : BaseMapViewModel(mapPrefs, nodeRepository, packetRepository, radioController) { +) : BaseMapViewModel(mapPrefs, nodeRepository, packetRepository, serviceRepository) { private val _selectedWaypointId = MutableStateFlow(savedStateHandle.toRoute().waypointId) val selectedWaypointId: StateFlow = _selectedWaypointId.asStateFlow() diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapViewModel.kt b/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapViewModel.kt index 2e5b6cc094..86fdecce36 100644 --- a/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapViewModel.kt +++ b/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapViewModel.kt @@ -46,13 +46,13 @@ import kotlinx.serialization.Serializable import org.meshtastic.core.data.model.CustomTileProviderConfig import org.meshtastic.core.data.repository.CustomTileProviderRepository import org.meshtastic.core.datastore.UiPreferencesDataSource -import org.meshtastic.core.model.RadioController import org.meshtastic.core.navigation.MapRoutes import org.meshtastic.core.prefs.map.GoogleMapsPrefs import org.meshtastic.core.prefs.map.MapPrefs import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.RadioConfigRepository +import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.Config import java.io.File @@ -86,11 +86,11 @@ constructor( nodeRepository: NodeRepository, packetRepository: PacketRepository, radioConfigRepository: RadioConfigRepository, - radioController: RadioController, + serviceRepository: ServiceRepository, private val customTileProviderRepository: CustomTileProviderRepository, uiPreferencesDataSource: UiPreferencesDataSource, savedStateHandle: SavedStateHandle, -) : BaseMapViewModel(mapPrefs, nodeRepository, packetRepository, radioController) { +) : BaseMapViewModel(mapPrefs, nodeRepository, packetRepository, serviceRepository) { private val _selectedWaypointId = MutableStateFlow(savedStateHandle.toRoute().waypointId) val selectedWaypointId: StateFlow = _selectedWaypointId.asStateFlow() @@ -643,8 +643,6 @@ constructor( super.onCleared() (currentTileProvider as? MBTilesProvider)?.close() } - - override fun getUser(userId: String?) = nodeRepository.getUser(userId ?: org.meshtastic.core.model.DataPacket.ID_BROADCAST) } enum class LayerType { diff --git a/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt b/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt index 9e1da292c9..5b3c76c649 100644 --- a/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt +++ b/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt @@ -29,13 +29,12 @@ import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.launch import org.jetbrains.compose.resources.StringResource import org.meshtastic.core.common.util.nowSeconds -import org.meshtastic.core.data.repository.NodeRepository -import org.meshtastic.core.data.repository.PacketRepository -import org.meshtastic.core.database.entity.Packet import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.Node import org.meshtastic.core.model.util.TimeConstants import org.meshtastic.core.prefs.map.MapPrefs +import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.any import org.meshtastic.core.resources.eight_hours @@ -94,14 +93,14 @@ abstract class BaseMapViewModel( .map { nodes -> nodes.filter { node -> node.validPosition != null } } .stateInWhileSubscribed(initialValue = emptyList()) - val waypoints: StateFlow> = + val waypoints: StateFlow> = packetRepository .getWaypoints() .mapLatest { list -> list - .associateBy { packet -> packet.data.waypoint!!.id } + .associateBy { packet -> packet.waypoint!!.id } .filterValues { - val expire = it.data.waypoint!!.expire ?: 0 + val expire = it.waypoint?.expire ?: 0 expire == 0 || expire.toLong() > nowSeconds } } diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageListPaged.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageListPaged.kt index e2c1ce6c99..ab317a6f35 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageListPaged.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageListPaged.kt @@ -61,11 +61,10 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.database.entity.Packet -import org.meshtastic.core.database.entity.Reaction import org.meshtastic.core.model.Message import org.meshtastic.core.model.MessageStatus import org.meshtastic.core.model.Node +import org.meshtastic.core.model.Reaction import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.new_messages_below import org.meshtastic.feature.messaging.component.MessageItem @@ -545,7 +544,7 @@ private fun MessageStatusDialog( remember(message.relayNode, nodes, ourNode) { derivedStateOf { message.relayNode?.let { relayNodeId -> - Packet.getRelayNode(relayNodeId, nodes, ourNode?.num)?.user?.long_name + Node.getRelayNode(relayNodeId, nodes, ourNode?.num)?.user?.long_name } } } diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt index e26583a910..2e999a8d61 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt @@ -32,19 +32,19 @@ import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import org.meshtastic.core.data.repository.NodeRepository -import org.meshtastic.core.data.repository.PacketRepository import org.meshtastic.core.data.repository.QuickChatActionRepository -import org.meshtastic.core.data.repository.RadioConfigRepository -import org.meshtastic.core.database.entity.ContactSettings +import org.meshtastic.core.model.ContactSettings import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.Message import org.meshtastic.core.model.Node import org.meshtastic.core.prefs.emoji.CustomEmojiPrefs import org.meshtastic.core.prefs.homoglyph.HomoglyphPrefs import org.meshtastic.core.prefs.ui.UiPrefs +import org.meshtastic.core.repository.MeshServiceNotifications +import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.PacketRepository +import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.repository.usecase.SendMessageUseCase -import org.meshtastic.core.service.MeshServiceNotifications import org.meshtastic.core.service.ServiceAction import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt index 8eb151620c..6dd60807e0 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt @@ -62,10 +62,10 @@ import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.common.util.nowMillis -import org.meshtastic.core.database.entity.Reaction import org.meshtastic.core.model.Message import org.meshtastic.core.model.MessageStatus import org.meshtastic.core.model.Node +import org.meshtastic.core.model.Reaction import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.filter_message_label import org.meshtastic.core.resources.message_delivery_status diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt index 8aa0727031..8055b97399 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt @@ -32,6 +32,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.AddReaction @@ -56,11 +57,10 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.database.entity.Packet -import org.meshtastic.core.database.entity.Reaction import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.MessageStatus import org.meshtastic.core.model.Node +import org.meshtastic.core.model.Reaction import org.meshtastic.core.model.getStringResFrom import org.meshtastic.core.model.util.getShortDateTime import org.meshtastic.core.resources.Res @@ -147,7 +147,9 @@ internal fun ReactionRow( AnimatedVisibility(emojiGroups.isNotEmpty(), modifier = modifier) { LazyRow(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) { - items(emojiGroups.entries.toList()) { (emoji, reactions) -> + items(emojiGroups.entries.toList()) { entry -> + val emoji = entry.key + val reactions = entry.value val localReaction = reactions.find { it.user.id == DataPacket.ID_LOCAL || it.user.id == myId } ReactionItem( emoji = emoji, @@ -217,7 +219,7 @@ internal fun ReactionDialog( val relayNodeName = reaction.relayNode?.let { relayNodeId -> - Packet.getRelayNode(relayNodeId, nodes, ourNode?.num)?.user?.long_name + Node.getRelayNode(relayNodeId, nodes, ourNode?.num)?.user?.long_name } DeliveryInfo( @@ -235,7 +237,9 @@ internal fun ReactionDialog( } LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) { - items(groupedEmojis.entries.toList()) { (emoji, reactions) -> + items(groupedEmojis.entries.toList()) { entry -> + val emoji = entry.key + val reactions = entry.value val localReaction = reactions.find { it.user.id == DataPacket.ID_LOCAL || it.user.id == myId } val isSending = localReaction?.status == MessageStatus.QUEUED || localReaction?.status == MessageStatus.ENROUTE diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorker.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorker.kt index 078af260f3..81e1858d6b 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorker.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorker.kt @@ -28,16 +28,13 @@ import org.meshtastic.core.model.RadioController import org.meshtastic.core.repository.PacketRepository @HiltWorker -class SendMessageWorker -@AssistedInject -constructor( +class SendMessageWorker @AssistedInject constructor( @Assisted context: Context, @Assisted params: WorkerParameters, private val packetRepository: PacketRepository, - private val radioController: RadioController, + private val radioController: RadioController ) : CoroutineWorker(context, params) { - @Suppress("TooGenericExceptionCaught", "SwallowedException", "ReturnCount") override suspend fun doWork(): Result { val packetId = inputData.getInt(KEY_PACKET_ID, 0) if (packetId == 0) return Result.failure() @@ -47,9 +44,8 @@ constructor( return Result.retry() } - val packetData = - packetRepository.getPacketByPacketId(packetId) - ?: return Result.failure() // Packet no longer exists in DB? Do not retry. + val packetData = packetRepository.getPacketByPacketId(packetId) + ?: return Result.failure() // Packet no longer exists in DB? Do not retry. return try { radioController.sendMessage(packetData) diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModel.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModel.kt index f39b490032..e9ab2a5b97 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModel.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModel.kt @@ -36,7 +36,7 @@ import org.meshtastic.core.model.util.getChannel 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.core.service.ServiceRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.ChannelSet import javax.inject.Inject diff --git a/feature/messaging/src/test/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorkerTest.kt b/feature/messaging/src/test/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorkerTest.kt index 537bc1d63e..195142b993 100644 --- a/feature/messaging/src/test/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorkerTest.kt +++ b/feature/messaging/src/test/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorkerTest.kt @@ -1,19 +1,3 @@ -/* - * 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.domain.worker import android.content.Context @@ -30,11 +14,12 @@ import io.mockk.just import io.mockk.mockk import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest -import okio.ByteString.Companion.toByteString import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.meshtastic.core.database.entity.Packet +import org.meshtastic.core.database.entity.PacketEntity import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.MessageStatus @@ -61,26 +46,25 @@ class SendMessageWorkerTest { fun `doWork returns success when packet is sent successfully`() = runTest { // Arrange val packetId = 12345 - val dataPacket = DataPacket(to = "dest", bytes = "Hello".encodeToByteArray().toByteString(), dataType = 0) - coEvery { packetRepository.getPacketByPacketId(packetId) } returns dataPacket + val dataPacket = DataPacket("dest", 0, "Hello") + val packet = mockk(relaxed = true) + val packetEntity = PacketEntity(packet = packet) + every { packet.data } returns dataPacket + coEvery { packetRepository.getPacketByPacketId(packetId) } returns packetEntity every { radioController.connectionState } returns MutableStateFlow(ConnectionState.Connected) coEvery { radioController.sendMessage(any()) } just Runs coEvery { packetRepository.updateMessageStatus(any(), any()) } just Runs - val worker = - TestListenableWorkerBuilder(context) - .setInputData(workDataOf(SendMessageWorker.KEY_PACKET_ID to packetId)) - .setWorkerFactory( - object : androidx.work.WorkerFactory() { - override fun createWorker( - appContext: Context, - workerClassName: String, - workerParameters: WorkerParameters, - ): ListenableWorker? = - SendMessageWorker(appContext, workerParameters, packetRepository, radioController) - }, - ) - .build() + val worker = TestListenableWorkerBuilder(context) + .setInputData(workDataOf(SendMessageWorker.KEY_PACKET_ID to packetId)) + .setWorkerFactory(object : androidx.work.WorkerFactory() { + override fun createWorker( + appContext: Context, + workerClassName: String, + workerParameters: WorkerParameters + ): ListenableWorker? = SendMessageWorker(appContext, workerParameters, packetRepository, radioController) + }) + .build() // Act val result = worker.doWork() @@ -95,24 +79,23 @@ class SendMessageWorkerTest { fun `doWork returns retry when radio is disconnected`() = runTest { // Arrange val packetId = 12345 - val dataPacket = DataPacket(to = "dest", bytes = "Hello".encodeToByteArray().toByteString(), dataType = 0) - coEvery { packetRepository.getPacketByPacketId(packetId) } returns dataPacket + val dataPacket = DataPacket("dest", 0, "Hello") + val packet = mockk(relaxed = true) + val packetEntity = PacketEntity(packet = packet) + every { packet.data } returns dataPacket + coEvery { packetRepository.getPacketByPacketId(packetId) } returns packetEntity every { radioController.connectionState } returns MutableStateFlow(ConnectionState.Disconnected) - val worker = - TestListenableWorkerBuilder(context) - .setInputData(workDataOf(SendMessageWorker.KEY_PACKET_ID to packetId)) - .setWorkerFactory( - object : androidx.work.WorkerFactory() { - override fun createWorker( - appContext: Context, - workerClassName: String, - workerParameters: WorkerParameters, - ): ListenableWorker? = - SendMessageWorker(appContext, workerParameters, packetRepository, radioController) - }, - ) - .build() + val worker = TestListenableWorkerBuilder(context) + .setInputData(workDataOf(SendMessageWorker.KEY_PACKET_ID to packetId)) + .setWorkerFactory(object : androidx.work.WorkerFactory() { + override fun createWorker( + appContext: Context, + workerClassName: String, + workerParameters: WorkerParameters + ): ListenableWorker? = SendMessageWorker(appContext, workerParameters, packetRepository, radioController) + }) + .build() // Act val result = worker.doWork() @@ -128,20 +111,16 @@ class SendMessageWorkerTest { val packetId = 999 coEvery { packetRepository.getPacketByPacketId(packetId) } returns null - val worker = - TestListenableWorkerBuilder(context) - .setInputData(workDataOf(SendMessageWorker.KEY_PACKET_ID to packetId)) - .setWorkerFactory( - object : androidx.work.WorkerFactory() { - override fun createWorker( - appContext: Context, - workerClassName: String, - workerParameters: WorkerParameters, - ): ListenableWorker? = - SendMessageWorker(appContext, workerParameters, packetRepository, radioController) - }, - ) - .build() + val worker = TestListenableWorkerBuilder(context) + .setInputData(workDataOf(SendMessageWorker.KEY_PACKET_ID to packetId)) + .setWorkerFactory(object : androidx.work.WorkerFactory() { + override fun createWorker( + appContext: Context, + workerClassName: String, + workerParameters: WorkerParameters + ): ListenableWorker? = SendMessageWorker(appContext, workerParameters, packetRepository, radioController) + }) + .build() // Act val result = worker.doWork() diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt index f127076d3f..90f1e31cee 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt @@ -31,7 +31,6 @@ import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.database.entity.asDeviceVersion import org.meshtastic.core.model.DeviceVersion import org.meshtastic.core.model.Node -import org.meshtastic.core.model.service.ServiceAction import org.meshtastic.core.navigation.SettingsRoutes import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.administration @@ -42,6 +41,7 @@ import org.meshtastic.core.resources.latest_alpha_firmware import org.meshtastic.core.resources.latest_stable_firmware import org.meshtastic.core.resources.remote_admin import org.meshtastic.core.resources.request_metadata +import org.meshtastic.core.service.ServiceAction import org.meshtastic.core.ui.component.ListItem import org.meshtastic.core.ui.theme.StatusColors.StatusGreen import org.meshtastic.core.ui.theme.StatusColors.StatusOrange diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeManagementActions.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeManagementActions.kt index 981aee4e4d..a341238f40 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeManagementActions.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeManagementActions.kt @@ -16,16 +16,14 @@ */ package org.meshtastic.feature.node.detail +import android.os.RemoteException import co.touchlab.kermit.Logger import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.jetbrains.compose.resources.getString import org.meshtastic.core.model.Node -import org.meshtastic.core.model.RadioController -import org.meshtastic.core.model.service.ServiceAction import org.meshtastic.core.repository.NodeRepository -import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.favorite import org.meshtastic.core.resources.favorite_add @@ -39,6 +37,8 @@ import org.meshtastic.core.resources.mute_remove import org.meshtastic.core.resources.remove import org.meshtastic.core.resources.remove_node_text import org.meshtastic.core.resources.unmute +import org.meshtastic.core.service.ServiceAction +import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.util.AlertManager import javax.inject.Inject import javax.inject.Singleton @@ -49,7 +49,6 @@ class NodeManagementActions constructor( private val nodeRepository: NodeRepository, private val serviceRepository: ServiceRepository, - private val radioController: RadioController, private val alertManager: AlertManager, ) { fun requestRemoveNode(scope: CoroutineScope, node: Node) { @@ -63,9 +62,13 @@ constructor( fun removeNode(scope: CoroutineScope, nodeNum: Int) { scope.launch(Dispatchers.IO) { Logger.i { "Removing node '$nodeNum'" } - val packetId = radioController.getPacketId() - radioController.removeByNodenum(packetId, nodeNum) - nodeRepository.deleteNode(nodeNum) + try { + val packetId = serviceRepository.meshService?.packetId ?: return@launch + serviceRepository.meshService?.removeByNodenum(packetId, nodeNum) + nodeRepository.deleteNode(nodeNum) + } catch (ex: RemoteException) { + Logger.e { "Remove node error: ${ex.message}" } + } } } @@ -86,7 +89,11 @@ constructor( fun ignoreNode(scope: CoroutineScope, node: Node) { scope.launch(Dispatchers.IO) { - serviceRepository.onServiceAction(ServiceAction.Ignore(node)) + try { + serviceRepository.onServiceAction(ServiceAction.Ignore(node)) + } catch (ex: RemoteException) { + Logger.e(ex) { "Ignore node error" } + } } } @@ -104,7 +111,11 @@ constructor( fun muteNode(scope: CoroutineScope, node: Node) { scope.launch(Dispatchers.IO) { - serviceRepository.onServiceAction(ServiceAction.Mute(node)) + try { + serviceRepository.onServiceAction(ServiceAction.Mute(node)) + } catch (ex: RemoteException) { + Logger.e(ex) { "Mute node error" } + } } } @@ -125,7 +136,11 @@ constructor( fun favoriteNode(scope: CoroutineScope, node: Node) { scope.launch(Dispatchers.IO) { - serviceRepository.onServiceAction(ServiceAction.Favorite(node)) + try { + serviceRepository.onServiceAction(ServiceAction.Favorite(node)) + } catch (ex: RemoteException) { + Logger.e(ex) { "Favorite node error" } + } } } diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt index 06a9c114fd..b404d7262b 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt @@ -30,11 +30,11 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.launch -import org.meshtastic.core.data.repository.NodeRepository -import org.meshtastic.core.data.repository.RadioConfigRepository import org.meshtastic.core.model.Node import org.meshtastic.core.model.NodeSortOption import org.meshtastic.core.model.util.dispatchMeshtasticUri +import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.feature.node.detail.NodeManagementActions diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt index 5b8dea3b68..91a3b9f5a3 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt @@ -55,11 +55,11 @@ import org.meshtastic.core.model.evaluateTracerouteMapAvailability import org.meshtastic.core.model.util.UnitConversions import org.meshtastic.core.navigation.NodesRoutes import org.meshtastic.core.repository.NodeRepository -import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.okay import org.meshtastic.core.resources.traceroute import org.meshtastic.core.resources.view_on_map +import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.util.AlertManager import org.meshtastic.core.ui.util.toMessageRes import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/model/NodeDetailAction.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/model/NodeDetailAction.kt index 1f93a15bae..58009aa767 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/model/NodeDetailAction.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/model/NodeDetailAction.kt @@ -17,8 +17,8 @@ package org.meshtastic.feature.node.model import org.meshtastic.core.model.Node -import org.meshtastic.core.model.service.ServiceAction import org.meshtastic.core.navigation.Route +import org.meshtastic.core.service.ServiceAction import org.meshtastic.feature.node.component.NodeMenuAction import org.meshtastic.proto.Config diff --git a/feature/node/src/test/kotlin/org/meshtastic/feature/node/detail/NodeManagementActionsTest.kt b/feature/node/src/test/kotlin/org/meshtastic/feature/node/detail/NodeManagementActionsTest.kt index 05a0f59180..cb657999fd 100644 --- a/feature/node/src/test/kotlin/org/meshtastic/feature/node/detail/NodeManagementActionsTest.kt +++ b/feature/node/src/test/kotlin/org/meshtastic/feature/node/detail/NodeManagementActionsTest.kt @@ -24,9 +24,8 @@ import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Test import org.meshtastic.core.model.Node -import org.meshtastic.core.model.RadioController import org.meshtastic.core.repository.NodeRepository -import org.meshtastic.core.repository.ServiceRepository +import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.util.AlertManager import org.meshtastic.proto.User @@ -35,7 +34,6 @@ class NodeManagementActionsTest { private val nodeRepository = mockk(relaxed = true) private val serviceRepository = mockk(relaxed = true) - private val radioController = mockk(relaxed = true) private val alertManager = mockk(relaxed = true) private val testDispatcher = StandardTestDispatcher() private val testScope = TestScope(testDispatcher) @@ -44,7 +42,6 @@ class NodeManagementActionsTest { NodeManagementActions( nodeRepository = nodeRepository, serviceRepository = serviceRepository, - radioController = radioController, alertManager = alertManager, ) diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt index 7b8528aaa6..e213ce37a8 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt @@ -32,10 +32,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.meshtastic.core.common.BuildConfigProvider -import org.meshtastic.core.data.repository.NodeRepository -import org.meshtastic.core.data.repository.RadioConfigRepository import org.meshtastic.core.database.DatabaseManager -import org.meshtastic.core.database.entity.MyNodeEntity import org.meshtastic.core.domain.usecase.settings.ExportDataUseCase import org.meshtastic.core.domain.usecase.settings.IsOtaCapableUseCase import org.meshtastic.core.domain.usecase.settings.MeshLocationUseCase @@ -44,10 +41,13 @@ 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.MyNodeInfo import org.meshtastic.core.model.Node import org.meshtastic.core.model.RadioController import org.meshtastic.core.prefs.meshlog.MeshLogPrefs import org.meshtastic.core.prefs.ui.UiPrefs +import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.LocalConfig import java.io.BufferedWriter @@ -77,7 +77,7 @@ constructor( private val exportDataUseCase: ExportDataUseCase, private val isOtaCapableUseCase: IsOtaCapableUseCase, ) : ViewModel() { - val myNodeInfo: StateFlow = nodeRepository.myNodeInfo + val myNodeInfo: StateFlow = nodeRepository.myNodeInfo val myNodeNum get() = myNodeInfo.value?.myNodeNum diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt index 54b04c295c..335d4b4804 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt @@ -64,10 +64,10 @@ import org.meshtastic.core.prefs.map.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.core.resources.Res import org.meshtastic.core.resources.UiText import org.meshtastic.core.resources.cant_shutdown +import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.util.getChannelList import org.meshtastic.feature.settings.navigation.ConfigRoute import org.meshtastic.feature.settings.navigation.ModuleRoute @@ -356,7 +356,9 @@ constructor( fun setRingtone(ringtone: String) { val destNum = destNode.value?.num ?: return _radioConfigState.update { it.copy(ringtone = ringtone) } - viewModelScope.launch { radioConfigUseCase.setRingtone(destNum, ringtone) } + viewModelScope.launch { + radioConfigUseCase.setRingtone(destNum, ringtone) + } } private fun getRingtone(destNum: Int) { @@ -369,7 +371,9 @@ constructor( fun setCannedMessages(messages: String) { val destNum = destNode.value?.num ?: return _radioConfigState.update { it.copy(cannedMessageMessages = messages) } - viewModelScope.launch { radioConfigUseCase.setCannedMessages(destNum, messages) } + viewModelScope.launch { + radioConfigUseCase.setCannedMessages(destNum, messages) + } } private fun getCannedMessages(destNum: Int) { @@ -440,18 +444,24 @@ constructor( fun setFixedPosition(position: Position) { val destNum = destNode.value?.num ?: return - viewModelScope.launch { radioConfigUseCase.setFixedPosition(destNum, position) } + viewModelScope.launch { + radioConfigUseCase.setFixedPosition(destNum, position) + } } fun removeFixedPosition() { val destNum = destNode.value?.num ?: return - viewModelScope.launch { radioConfigUseCase.removeFixedPosition(destNum) } + viewModelScope.launch { + radioConfigUseCase.removeFixedPosition(destNum) + } } fun importProfile(uri: Uri, onResult: (DeviceProfile) -> Unit) = viewModelScope.launch(Dispatchers.IO) { try { app.contentResolver.openInputStream(uri)?.use { inputStream -> - importProfileUseCase(inputStream).onSuccess(onResult).onFailure { throw it } + importProfileUseCase(inputStream) + .onSuccess(onResult) + .onFailure { throw it } } } catch (ex: Exception) { Logger.e { "Import DeviceProfile error: ${ex.message}" } @@ -476,27 +486,30 @@ constructor( } } - fun exportSecurityConfig(uri: Uri, securityConfig: Config.SecurityConfig) = viewModelScope.launch { - withContext(Dispatchers.IO) { - try { - app.contentResolver.openFileDescriptor(uri, "wt")?.use { parcelFileDescriptor -> - FileOutputStream(parcelFileDescriptor.fileDescriptor).use { outputStream -> - exportSecurityConfigUseCase(outputStream, securityConfig) - .onSuccess { setResponseStateSuccess() } - .onFailure { throw it } + fun exportSecurityConfig(uri: Uri, securityConfig: Config.SecurityConfig) = + viewModelScope.launch { + withContext(Dispatchers.IO) { + try { + app.contentResolver.openFileDescriptor(uri, "wt")?.use { parcelFileDescriptor -> + FileOutputStream(parcelFileDescriptor.fileDescriptor).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) } - } catch (ex: Exception) { - val errorMessage = "Can't write security keys JSON error: ${ex.message}" - Logger.e { errorMessage } - sendError(ex.customMessage) } } - } fun installProfile(protobuf: DeviceProfile) { val destNum = destNode.value?.num ?: return - viewModelScope.launch { installProfileUseCase(destNum, protobuf, destNode.value?.user) } + viewModelScope.launch { + installProfileUseCase(destNum, protobuf, destNode.value?.user) + } } fun clearPacketResponse() { diff --git a/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt b/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt index adf6dd9ac2..8ec58eab3c 100644 --- a/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt +++ b/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt @@ -51,7 +51,7 @@ import org.meshtastic.core.prefs.map.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.core.service.ServiceRepository import org.meshtastic.proto.ChannelSet import org.meshtastic.proto.ChannelSettings import org.meshtastic.proto.Config @@ -89,15 +89,14 @@ class RadioConfigViewModelTest { @Before fun setUp() { Dispatchers.setMain(testDispatcher) - + every { nodeRepository.nodeDBbyNum } returns MutableStateFlow(emptyMap()) every { radioConfigRepository.deviceProfileFlow } returns MutableStateFlow(DeviceProfile()) every { radioConfigRepository.localConfigFlow } returns MutableStateFlow(LocalConfig()) every { radioConfigRepository.channelSetFlow } returns MutableStateFlow(ChannelSet()) every { radioConfigRepository.moduleConfigFlow } returns MutableStateFlow(LocalModuleConfig()) every { serviceRepository.meshPacketFlow } returns MutableSharedFlow() - every { serviceRepository.connectionState } returns - MutableStateFlow(org.meshtastic.core.model.ConnectionState.Connected) + every { serviceRepository.connectionState } returns MutableStateFlow(org.meshtastic.core.model.ConnectionState.Connected) every { nodeRepository.myNodeInfo } returns MutableStateFlow(null) viewModel = createViewModel() @@ -135,12 +134,12 @@ class RadioConfigViewModelTest { val node = Node(num = 123) every { nodeRepository.nodeDBbyNum } returns MutableStateFlow(mapOf(123 to node)) viewModel = createViewModel() - + val config = Config(device = Config.DeviceConfig(role = Config.DeviceConfig.Role.ROUTER)) coEvery { radioConfigUseCase.setConfig(123, any()) } returns 42 viewModel.setConfig(config) - + val state = viewModel.radioConfigState.value assertEquals(Config.DeviceConfig.Role.ROUTER, state.radioConfig.device?.role) coVerify { radioConfigUseCase.setConfig(123, config) } @@ -150,14 +149,14 @@ class RadioConfigViewModelTest { fun `processPacketResponse updates state on metadata result`() = runTest { val node = Node(num = 123) every { nodeRepository.nodeDBbyNum } returns MutableStateFlow(mapOf(123 to node)) - + val packet = MeshPacket() val metadata = DeviceMetadata(firmware_version = "3.0.0") val packetFlow = MutableSharedFlow() - + every { serviceRepository.meshPacketFlow } returns packetFlow every { processRadioResponseUseCase(any(), 123, any()) } returns RadioResponseResult.Metadata(metadata) - + viewModel = createViewModel() packetFlow.emit(packet) @@ -174,9 +173,9 @@ class RadioConfigViewModelTest { val user = org.meshtastic.proto.User(long_name = "Test") coEvery { radioConfigUseCase.setOwner(123, any()) } returns 42 - + viewModel.setOwner(user) - + coVerify { radioConfigUseCase.setOwner(123, user) } } @@ -188,11 +187,11 @@ class RadioConfigViewModelTest { val old = listOf(ChannelSettings(name = "Old")) val new = listOf(ChannelSettings(name = "New")) - + coEvery { radioConfigUseCase.setRemoteChannel(123, any()) } returns 42 - + viewModel.updateChannels(new, old) - + coVerify { radioConfigUseCase.setRemoteChannel(123, any()) } assertEquals(new, viewModel.radioConfigState.value.channelList) } @@ -201,20 +200,20 @@ class RadioConfigViewModelTest { fun `setResponseStateLoading for REBOOT calls useCase after packet response`() = runTest { val node = Node(num = 123) every { nodeRepository.nodeDBbyNum } returns MutableStateFlow(mapOf(123 to node)) - + val packetFlow = MutableSharedFlow() every { serviceRepository.meshPacketFlow } returns packetFlow every { processRadioResponseUseCase(any(), any(), any()) } returns RadioResponseResult.Success - + viewModel = createViewModel() - + coEvery { adminActionsUseCase.reboot(123) } returns 42 - + viewModel.setResponseStateLoading(AdminRoute.REBOOT) - + // Emit a packet to trigger processPacketResponse -> sendAdminRequest packetFlow.emit(MeshPacket()) - + coVerify { adminActionsUseCase.reboot(123) } } @@ -222,20 +221,20 @@ class RadioConfigViewModelTest { fun `setResponseStateLoading for FACTORY_RESET calls useCase after packet response`() = runTest { val node = Node(num = 123) every { nodeRepository.nodeDBbyNum } returns MutableStateFlow(mapOf(123 to node)) - + val packetFlow = MutableSharedFlow() every { serviceRepository.meshPacketFlow } returns packetFlow every { processRadioResponseUseCase(any(), any(), any()) } returns RadioResponseResult.Success - + viewModel = createViewModel() - + coEvery { adminActionsUseCase.factoryReset(123, any()) } returns 42 - + viewModel.setResponseStateLoading(AdminRoute.FACTORY_RESET) - + // Emit a packet to trigger processPacketResponse -> sendAdminRequest packetFlow.emit(MeshPacket()) - + coVerify { adminActionsUseCase.factoryReset(123, any()) } } } From 159490bb87eced9d109bac82a5604ea632959eaa Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 2 Mar 2026 13:17:42 -0600 Subject: [PATCH 07/20] refactor: migrate history management and manager logic to core modules - Introduce `HistoryManager` interface in `core:repository` and its implementation in `core:data`. - Move `NodeManager`, `CommandSender`, and `HistoryManager` unit tests from the `app` module to `core:data`. - Update `MeshConnectionManager` and `MeshDataHandler` to use the new `HistoryManager` interface instead of the concrete implementation. - Refactor unit tests and fakes to use domain models (`Node`, `DataPacket`) instead of database entities. - Expose `getOrCreateNode` in `NodeManagerImpl` and `resolveNodeNum` in `CommandSenderImpl` for improved testability. - Update DI module to bind the new `HistoryManager` implementation. Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .../mesh/service/MeshConnectionManager.kt | 3 +- .../mesh/service/MeshDataHandler.kt | 3 +- .../java/com/geeksville/mesh/service/Fakes.kt | 8 +- .../mesh/service/MeshConnectionManagerTest.kt | 11 ++- .../mesh/service/MeshDataHandlerTest.kt | 5 +- .../mesh/service/MeshDataMapperTest.kt | 25 ++++++- .../mesh/service/MeshMessageProcessorTest.kt | 2 +- .../core/data/di/RepositoryModule.kt | 8 ++ .../core/data/manager/CommandSenderImpl.kt | 2 +- .../core/data/manager/HistoryManagerImpl.kt | 75 ++++++++----------- .../core/data/manager/NodeManagerImpl.kt | 2 +- .../data/manager/CommandSenderHopLimitTest.kt | 25 ++++--- .../data/manager/CommandSenderImplTest.kt | 20 ++--- .../core/data/manager/NodeManagerImplTest.kt | 21 +++--- .../domain/worker/SendMessageWorkerTest.kt | 17 ++--- 15 files changed, 128 insertions(+), 99 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt b/app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt index aa926821a2..9a6ff95414 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt @@ -47,6 +47,7 @@ import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.TelemetryType import org.meshtastic.core.prefs.ui.UiPrefs import org.meshtastic.core.repository.CommandSender +import org.meshtastic.core.repository.HistoryManager import org.meshtastic.core.repository.MeshServiceNotifications import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.NodeRepository @@ -87,7 +88,7 @@ constructor( private val nodeRepository: NodeRepository, private val locationManager: MeshLocationManager, private val mqttManager: MeshMqttManager, - private val historyManager: MeshHistoryManager, + private val historyManager: HistoryManager, private val radioConfigRepository: RadioConfigRepository, private val commandSender: CommandSender, private val nodeManager: NodeManager, diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt b/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt index ae3eb5efd1..177babac2c 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt @@ -42,6 +42,7 @@ import org.meshtastic.core.model.util.decodeOrNull import org.meshtastic.core.model.util.toOneLiner import org.meshtastic.core.prefs.mesh.MeshPrefs import org.meshtastic.core.repository.CommandSender +import org.meshtastic.core.repository.HistoryManager import org.meshtastic.core.repository.MeshServiceNotifications import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.PacketHandler @@ -90,7 +91,7 @@ constructor( private val configHandler: MeshConfigHandler, private val configFlowManager: MeshConfigFlowManager, private val commandSender: CommandSender, - private val historyManager: MeshHistoryManager, + private val historyManager: HistoryManager, private val meshPrefs: MeshPrefs, private val connectionManager: MeshConnectionManager, private val tracerouteHandler: MeshTracerouteHandler, diff --git a/app/src/test/java/com/geeksville/mesh/service/Fakes.kt b/app/src/test/java/com/geeksville/mesh/service/Fakes.kt index ac5b2e81dd..728adc5f91 100644 --- a/app/src/test/java/com/geeksville/mesh/service/Fakes.kt +++ b/app/src/test/java/com/geeksville/mesh/service/Fakes.kt @@ -19,7 +19,7 @@ package com.geeksville.mesh.service import android.app.Notification import com.geeksville.mesh.repository.radio.RadioInterfaceService import io.mockk.mockk -import org.meshtastic.core.database.entity.NodeEntity +import org.meshtastic.core.model.Node import org.meshtastic.core.repository.MeshServiceNotifications import org.meshtastic.proto.ClientNotification import org.meshtastic.proto.Telemetry @@ -64,15 +64,15 @@ class FakeMeshServiceNotifications : MeshServiceNotifications { override fun showAlertNotification(contactKey: String, name: String, alert: String) {} - override fun showNewNodeSeenNotification(node: NodeEntity) {} + override fun showNewNodeSeenNotification(node: Node) {} - override fun showOrUpdateLowBatteryNotification(node: NodeEntity, isRemote: Boolean) {} + override fun showOrUpdateLowBatteryNotification(node: Node, isRemote: Boolean) {} override fun showClientNotification(clientNotification: ClientNotification) {} override fun cancelMessageNotification(contactKey: String) {} - override fun cancelLowBatteryNotification(node: NodeEntity) {} + override fun cancelLowBatteryNotification(node: Node) {} override fun clearClientNotification(notification: ClientNotification) {} } diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshConnectionManagerTest.kt b/app/src/test/java/com/geeksville/mesh/service/MeshConnectionManagerTest.kt index 63e9f3df43..7eea98c2b2 100644 --- a/app/src/test/java/com/geeksville/mesh/service/MeshConnectionManagerTest.kt +++ b/app/src/test/java/com/geeksville/mesh/service/MeshConnectionManagerTest.kt @@ -16,6 +16,7 @@ */ package com.geeksville.mesh.service +import android.app.Notification import android.content.Context import androidx.glance.appwidget.GlanceAppWidget import androidx.glance.appwidget.updateAll @@ -39,12 +40,15 @@ import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import org.meshtastic.core.analytics.platform.PlatformAnalytics -import org.meshtastic.core.database.entity.MyNodeEntity import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.model.MyNodeInfo import org.meshtastic.core.model.Node import org.meshtastic.core.prefs.ui.UiPrefs +import org.meshtastic.core.repository.CommandSender +import org.meshtastic.core.repository.HistoryManager import org.meshtastic.core.repository.MeshServiceNotifications +import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.RadioConfigRepository @@ -68,7 +72,7 @@ class MeshConnectionManagerTest { private val nodeRepository: NodeRepository = mockk(relaxed = true) private val locationManager: MeshLocationManager = mockk(relaxed = true) private val mqttManager: MeshMqttManager = mockk(relaxed = true) - private val historyManager: MeshHistoryManager = mockk(relaxed = true) + private val historyManager: HistoryManager = mockk(relaxed = true) private val radioConfigRepository: RadioConfigRepository = mockk(relaxed = true) private val commandSender: CommandSender = mockk(relaxed = true) private val nodeManager: NodeManager = mockk(relaxed = true) @@ -94,9 +98,10 @@ class MeshConnectionManagerTest { every { radioInterfaceService.connectionState } returns radioConnectionState every { radioConfigRepository.localConfigFlow } returns localConfigFlow every { radioConfigRepository.moduleConfigFlow } returns moduleConfigFlow - every { nodeRepository.myNodeInfo } returns MutableStateFlow(null) + every { nodeRepository.myNodeInfo } returns MutableStateFlow(null) every { nodeRepository.ourNodeInfo } returns MutableStateFlow(null) every { nodeRepository.localStats } returns MutableStateFlow(LocalStats()) + every { serviceNotifications.updateServiceStateNotification(any(), any()) } returns mockk(relaxed = true) manager = MeshConnectionManager( diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshDataHandlerTest.kt b/app/src/test/java/com/geeksville/mesh/service/MeshDataHandlerTest.kt index 4830cf45ef..0c06ac91ee 100644 --- a/app/src/test/java/com/geeksville/mesh/service/MeshDataHandlerTest.kt +++ b/app/src/test/java/com/geeksville/mesh/service/MeshDataHandlerTest.kt @@ -32,7 +32,10 @@ import org.meshtastic.core.analytics.platform.PlatformAnalytics import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.MessageStatus import org.meshtastic.core.prefs.mesh.MeshPrefs +import org.meshtastic.core.repository.CommandSender +import org.meshtastic.core.repository.HistoryManager import org.meshtastic.core.repository.MeshServiceNotifications +import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.service.ServiceRepository @@ -56,7 +59,7 @@ class MeshDataHandlerTest { private val configHandler: MeshConfigHandler = mockk(relaxed = true) private val configFlowManager: MeshConfigFlowManager = mockk(relaxed = true) private val commandSender: CommandSender = mockk(relaxed = true) - private val historyManager: MeshHistoryManager = mockk(relaxed = true) + private val historyManager: HistoryManager = mockk(relaxed = true) private val meshPrefs: MeshPrefs = mockk(relaxed = true) private val connectionManager: MeshConnectionManager = mockk(relaxed = true) private val tracerouteHandler: MeshTracerouteHandler = mockk(relaxed = true) diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshDataMapperTest.kt b/app/src/test/java/com/geeksville/mesh/service/MeshDataMapperTest.kt index 67c53b4d62..0c6f5aaad5 100644 --- a/app/src/test/java/com/geeksville/mesh/service/MeshDataMapperTest.kt +++ b/app/src/test/java/com/geeksville/mesh/service/MeshDataMapperTest.kt @@ -25,7 +25,6 @@ import org.junit.Assert.assertNull import org.junit.Before import org.junit.Test import org.meshtastic.core.model.DataPacket -import org.meshtastic.core.model.util.MeshDataMapper import org.meshtastic.core.repository.NodeManager import org.meshtastic.proto.Data import org.meshtastic.proto.MeshPacket @@ -41,6 +40,30 @@ class MeshDataMapperTest { mapper = MeshDataMapper(nodeManager) } + @Test + fun `toNodeID resolves broadcast correctly`() { + every { nodeManager.toNodeID(DataPacket.NODENUM_BROADCAST) } returns DataPacket.ID_BROADCAST + assertEquals(DataPacket.ID_BROADCAST, mapper.toNodeID(DataPacket.NODENUM_BROADCAST)) + } + + @Test + fun `toNodeID resolves known node correctly`() { + val nodeNum = 1234 + val nodeId = "!1234abcd" + every { nodeManager.toNodeID(nodeNum) } returns nodeId + + assertEquals(nodeId, mapper.toNodeID(nodeNum)) + } + + @Test + fun `toNodeID resolves unknown node to default ID`() { + val nodeNum = 1234 + val nodeId = DataPacket.nodeNumToDefaultId(nodeNum) + every { nodeManager.toNodeID(nodeNum) } returns nodeId + + assertEquals(nodeId, mapper.toNodeID(nodeNum)) + } + @Test fun `toDataPacket returns null when no decoded data`() { val packet = MeshPacket() diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshMessageProcessorTest.kt b/app/src/test/java/com/geeksville/mesh/service/MeshMessageProcessorTest.kt index c162b10b3c..0ede2ab56d 100644 --- a/app/src/test/java/com/geeksville/mesh/service/MeshMessageProcessorTest.kt +++ b/app/src/test/java/com/geeksville/mesh/service/MeshMessageProcessorTest.kt @@ -29,7 +29,7 @@ import org.junit.Test import org.meshtastic.core.data.repository.MeshLogRepository import org.meshtastic.core.database.entity.MeshLog import org.meshtastic.core.repository.NodeManager -import org.meshtastic.core.repository.ServiceRepository +import org.meshtastic.core.service.ServiceRepository import org.meshtastic.proto.Data import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.PortNum diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt index 0d27698938..2d24db0af2 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt @@ -22,6 +22,7 @@ import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import org.meshtastic.core.data.manager.CommandSenderImpl +import org.meshtastic.core.data.manager.HistoryManagerImpl import org.meshtastic.core.data.manager.NodeManagerImpl import org.meshtastic.core.data.repository.DeviceHardwareRepositoryImpl import org.meshtastic.core.data.repository.NodeRepositoryImpl @@ -29,6 +30,7 @@ import org.meshtastic.core.data.repository.PacketRepositoryImpl import org.meshtastic.core.data.repository.RadioConfigRepositoryImpl import org.meshtastic.core.repository.CommandSender import org.meshtastic.core.repository.DeviceHardwareRepository +import org.meshtastic.core.repository.HistoryManager import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.PacketRepository @@ -74,4 +76,10 @@ abstract class RepositoryModule { abstract fun bindCommandSender( commandSenderImpl: CommandSenderImpl ): CommandSender + + @Binds + @Singleton + abstract fun bindHistoryManager( + historyManagerImpl: HistoryManagerImpl + ): HistoryManager } diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt index b91bc60b59..33d668a2d1 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt @@ -364,7 +364,7 @@ constructor( } } - private fun resolveNodeNum(toId: String): Int = when (toId) { + fun resolveNodeNum(toId: String): Int = when (toId) { DataPacket.ID_BROADCAST -> DataPacket.NODENUM_BROADCAST else -> { val numericNum = diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/HistoryManagerImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/HistoryManagerImpl.kt index bad5dd2175..f3ff53e4e1 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/HistoryManagerImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/HistoryManagerImpl.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 @@ -14,15 +14,12 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.geeksville.mesh.service +package org.meshtastic.core.data.manager -import android.util.Log -import androidx.annotation.VisibleForTesting import co.touchlab.kermit.Logger -import com.geeksville.mesh.BuildConfig -import com.geeksville.mesh.ui.connections.NO_DEVICE_SELECTED import okio.ByteString.Companion.toByteString import org.meshtastic.core.prefs.mesh.MeshPrefs +import org.meshtastic.core.repository.HistoryManager import org.meshtastic.core.repository.PacketHandler import org.meshtastic.proto.Data import org.meshtastic.proto.MeshPacket @@ -33,58 +30,47 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class MeshHistoryManager -@Inject -constructor( +class HistoryManagerImpl @Inject constructor( private val meshPrefs: MeshPrefs, private val packetHandler: PacketHandler, -) { +) : HistoryManager { + companion object { private const val HISTORY_TAG = "HistoryReplay" private const val DEFAULT_HISTORY_RETURN_WINDOW_MINUTES = 60 * 24 private const val DEFAULT_HISTORY_RETURN_MAX_MESSAGES = 100 + private const val NO_DEVICE_SELECTED = "No device selected" - @VisibleForTesting - internal fun buildStoreForwardHistoryRequest( + fun buildStoreForwardHistoryRequest( lastRequest: Int, historyReturnWindow: Int, historyReturnMax: Int, ): StoreAndForward { - val history = - StoreAndForward.History( - last_request = lastRequest.coerceAtLeast(0), - window = historyReturnWindow.coerceAtLeast(0), - history_messages = historyReturnMax.coerceAtLeast(0), - ) + val history = StoreAndForward.History( + last_request = lastRequest.coerceAtLeast(0), + window = historyReturnWindow.coerceAtLeast(0), + history_messages = historyReturnMax.coerceAtLeast(0), + ) return StoreAndForward(rr = StoreAndForward.RequestResponse.CLIENT_HISTORY, history = history) } - @VisibleForTesting - internal fun resolveHistoryRequestParameters(window: Int, max: Int): Pair { + fun resolveHistoryRequestParameters(window: Int, max: Int): Pair { val resolvedWindow = if (window > 0) window else DEFAULT_HISTORY_RETURN_WINDOW_MINUTES val resolvedMax = if (max > 0) max else DEFAULT_HISTORY_RETURN_MAX_MESSAGES return resolvedWindow to resolvedMax } } - private fun historyLog(priority: Int = Log.INFO, throwable: Throwable? = null, message: () -> String) { - if (!BuildConfig.DEBUG) return - val logger = Logger.withTag(HISTORY_TAG) - val msg = message() - when (priority) { - Log.VERBOSE -> logger.v(throwable) { msg } - Log.DEBUG -> logger.d(throwable) { msg } - Log.INFO -> logger.i(throwable) { msg } - Log.WARN -> logger.w(throwable) { msg } - Log.ERROR -> logger.e(throwable) { msg } - else -> logger.i(throwable) { msg } - } + private val logger = Logger.withTag(HISTORY_TAG) + + private fun historyLog(message: String, throwable: Throwable? = null) { + logger.i(throwable) { message } } private fun activeDeviceAddress(): String? = meshPrefs.deviceAddress?.takeIf { !it.equals(NO_DEVICE_SELECTED, ignoreCase = true) && it.isNotBlank() } - fun requestHistoryReplay( + override fun requestHistoryReplay( trigger: String, myNodeNum: Int?, storeForwardConfig: ModuleConfig.StoreForwardConfig?, @@ -93,23 +79,22 @@ constructor( val address = activeDeviceAddress() if (address == null || myNodeNum == null) { val reason = if (address == null) "no_addr" else "no_my_node" - historyLog { "requestHistory skipped trigger=$trigger reason=$reason" } + historyLog("requestHistory skipped trigger=$trigger reason=$reason") return } val lastRequest = meshPrefs.getStoreForwardLastRequest(address) - val (window, max) = - resolveHistoryRequestParameters( - storeForwardConfig?.history_return_window ?: 0, - storeForwardConfig?.history_return_max ?: 0, - ) + val (window, max) = resolveHistoryRequestParameters( + storeForwardConfig?.history_return_window ?: 0, + storeForwardConfig?.history_return_max ?: 0, + ) val request = buildStoreForwardHistoryRequest(lastRequest, window, max) - historyLog { + historyLog( "requestHistory trigger=$trigger transport=$transport addr=$address " + "lastRequest=$lastRequest window=$window max=$max" - } + ) runCatching { packetHandler.sendToRadio( @@ -121,19 +106,19 @@ constructor( ), ) } - .onFailure { ex -> historyLog(Log.WARN, ex) { "requestHistory failed" } } + .onFailure { ex -> logger.w(ex) { "requestHistory failed" } } } - fun updateStoreForwardLastRequest(source: String, lastRequest: Int, transport: String) { + override fun updateStoreForwardLastRequest(source: String, lastRequest: Int, transport: String) { if (lastRequest <= 0) return val address = activeDeviceAddress() ?: return val current = meshPrefs.getStoreForwardLastRequest(address) if (lastRequest != current) { meshPrefs.setStoreForwardLastRequest(address, lastRequest) - historyLog { + historyLog( "historyMarker updated source=$source transport=$transport " + "addr=$address from=$current to=$lastRequest" - } + ) } } } diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt index 8416ae8bb5..f8c5ed68c9 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt @@ -132,7 +132,7 @@ constructor( nodeDBbyNodeNum.remove(nodeNum)?.let { nodeDBbyID.remove(it.user.id) } } - private fun getOrCreateNode(n: Int, channel: Int = 0): Node = nodeDBbyNodeNum.getOrPut(n) { + fun getOrCreateNode(n: Int, channel: Int = 0): Node = nodeDBbyNodeNum.getOrPut(n) { val userId = DataPacket.nodeNumToDefaultId(n) val defaultUser = User( diff --git a/core/data/src/test/kotlin/org/meshtastic/core/data/manager/CommandSenderHopLimitTest.kt b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/CommandSenderHopLimitTest.kt index 3e938790c9..679729176b 100644 --- a/core/data/src/test/kotlin/org/meshtastic/core/data/manager/CommandSenderHopLimitTest.kt +++ b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/CommandSenderHopLimitTest.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 com.geeksville.mesh.service +package org.meshtastic.core.data.manager import io.mockk.every import io.mockk.mockk @@ -29,18 +29,21 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test -import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.model.Node +import org.meshtastic.core.repository.CommandSender +import org.meshtastic.core.repository.NodeManager +import org.meshtastic.core.repository.PacketHandler import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.proto.Config import org.meshtastic.proto.LocalConfig import org.meshtastic.proto.MeshPacket +import org.meshtastic.proto.User class CommandSenderHopLimitTest { private val packetHandler: PacketHandler = mockk(relaxed = true) - private val nodeManager = NodeManagerImpl() - private val connectionStateHolder: ConnectionStateHandler = mockk(relaxed = true) + private val nodeManager: NodeManager = mockk(relaxed = true) private val radioConfigRepository: RadioConfigRepository = mockk(relaxed = true) private val localConfigFlow = MutableStateFlow(LocalConfig()) @@ -51,13 +54,14 @@ class CommandSenderHopLimitTest { @Before fun setUp() { - val connectedFlow = MutableStateFlow(ConnectionState.Connected) - every { connectionStateHolder.connectionState } returns connectedFlow + val myNum = 123 + val myNode = Node(num = myNum, user = User(id = "!id", long_name = "long", short_name = "shrt")) every { radioConfigRepository.localConfigFlow } returns localConfigFlow + every { nodeManager.myNodeNum } returns myNum + every { nodeManager.nodeDBbyNodeNum } returns mapOf(myNum to myNode) - commandSender = CommandSenderImpl(packetHandler, nodeManager, connectionStateHolder, radioConfigRepository) + commandSender = CommandSenderImpl(packetHandler, nodeManager, radioConfigRepository) commandSender.start(testScope) - nodeManager.myNodeNum = 123 } @Test @@ -111,7 +115,10 @@ class CommandSenderHopLimitTest { localConfigFlow.value = LocalConfig(lora = Config.LoRaConfig(hop_limit = 6)) // Mock node manager interactions - nodeManager.nodeDBbyNodeNum.remove(destNum) + // Note: we need to keep myNode in the map for requestUserInfo to not return early + val myNum = 123 + val myNode = Node(num = myNum, user = User(id = "!id", long_name = "long", short_name = "shrt")) + every { nodeManager.nodeDBbyNodeNum } returns mapOf(myNum to myNode) commandSender.requestUserInfo(destNum) diff --git a/core/data/src/test/kotlin/org/meshtastic/core/data/manager/CommandSenderImplTest.kt b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/CommandSenderImplTest.kt index 4b12c3d80c..69996dde9b 100644 --- a/core/data/src/test/kotlin/org/meshtastic/core/data/manager/CommandSenderImplTest.kt +++ b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/CommandSenderImplTest.kt @@ -14,25 +14,28 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.geeksville.mesh.service +package org.meshtastic.core.data.manager +import io.mockk.every +import io.mockk.mockk import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals import org.junit.Before import org.junit.Test -import org.meshtastic.core.database.entity.NodeEntity import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.model.Node +import org.meshtastic.core.repository.NodeManager import org.meshtastic.proto.User -class CommandSenderTest { +class CommandSenderImplTest { - private lateinit var commandSender: CommandSender + private lateinit var commandSender: CommandSenderImpl private lateinit var nodeManager: NodeManager @Before fun setUp() { - nodeManager = NodeManagerImpl() - commandSender = CommandSenderImpl(null, nodeManager, null, null) + nodeManager = mockk(relaxed = true) + commandSender = CommandSenderImpl(mockk(relaxed = true), nodeManager, mockk(relaxed = true)) } @Test @@ -60,9 +63,8 @@ class CommandSenderTest { fun `resolveNodeNum handles custom node ID from database`() { val nodeNum = 456 val userId = "custom_id" - val entity = NodeEntity(num = nodeNum, user = User(id = userId)) - nodeManager.nodeDBbyNodeNum[nodeNum] = entity - nodeManager.nodeDBbyID[userId] = entity + val node = Node(num = nodeNum, user = User(id = userId)) + every { nodeManager.nodeDBbyID } returns mapOf(userId to node) assertEquals(nodeNum, commandSender.resolveNodeNum(userId)) } diff --git a/core/data/src/test/kotlin/org/meshtastic/core/data/manager/NodeManagerImplTest.kt b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/NodeManagerImplTest.kt index 9dcba64894..4748663ba3 100644 --- a/core/data/src/test/kotlin/org/meshtastic/core/data/manager/NodeManagerImplTest.kt +++ b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/NodeManagerImplTest.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 com.geeksville.mesh.service +package org.meshtastic.core.data.manager import io.mockk.mockk import org.junit.Assert.assertEquals @@ -26,17 +26,18 @@ import org.junit.Test import org.meshtastic.core.model.DataPacket import org.meshtastic.core.repository.MeshServiceNotifications import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.ServiceBroadcasts import org.meshtastic.proto.HardwareModel import org.meshtastic.proto.Position import org.meshtastic.proto.User -class NodeManagerTest { +class NodeManagerImplTest { private val nodeRepository: NodeRepository = mockk(relaxed = true) private val serviceBroadcasts: ServiceBroadcasts = mockk(relaxed = true) private val serviceNotifications: MeshServiceNotifications = mockk(relaxed = true) - private lateinit var nodeManager: NodeManager + private lateinit var nodeManager: NodeManagerImpl @Before fun setUp() { @@ -44,13 +45,13 @@ class NodeManagerTest { } @Test - fun `getOrCreateNodeInfo creates default user for unknown node`() { + fun `getOrCreateNode creates default user for unknown node`() { val nodeNum = 1234 - val result = nodeManager.getOrCreateNodeInfo(nodeNum) + val result = nodeManager.getOrCreateNode(nodeNum) assertNotNull(result) assertEquals(nodeNum, result.num) - assertTrue(result.user.long_name?.startsWith("Meshtastic") == true) + assertTrue(result.user.long_name.startsWith("Meshtastic")) assertEquals(DataPacket.nodeNumToDefaultId(nodeNum), result.user.id) } @@ -61,7 +62,7 @@ class NodeManagerTest { User(id = "!12345678", long_name = "My Custom Name", short_name = "MCN", hw_model = HardwareModel.TLORA_V2) // Setup existing node - nodeManager.updateNode(nodeNum) { it.user = existingUser } + nodeManager.updateNode(nodeNum) { it.copy(user = existingUser) } val incomingDefaultUser = User(id = "!12345678", long_name = "Meshtastic 5678", short_name = "5678", hw_model = HardwareModel.UNSET) @@ -79,7 +80,7 @@ class NodeManagerTest { val existingUser = User(id = "!12345678", long_name = "Meshtastic 5678", short_name = "5678", hw_model = HardwareModel.UNSET) - nodeManager.updateNode(nodeNum) { it.user = existingUser } + nodeManager.updateNode(nodeNum) { it.copy(user = existingUser) } val incomingDetailedUser = User(id = "!12345678", long_name = "Real User", short_name = "RU", hw_model = HardwareModel.TLORA_V1) @@ -96,7 +97,7 @@ class NodeManagerTest { val nodeNum = 1234 val position = Position(latitude_i = 450000000, longitude_i = 900000000) - nodeManager.handleReceivedPosition(nodeNum, 9999, position) + nodeManager.handleReceivedPosition(nodeNum, 9999, position, 0) val result = nodeManager.nodeDBbyNodeNum[nodeNum] assertNotNull(result!!.position) @@ -106,7 +107,7 @@ class NodeManagerTest { @Test fun `clear resets internal state`() { - nodeManager.updateNode(1234) { it.longName = "Test" } + nodeManager.updateNode(1234) { it.copy(user = it.user.copy(long_name = "Test")) } nodeManager.clear() assertTrue(nodeManager.nodeDBbyNodeNum.isEmpty()) diff --git a/feature/messaging/src/test/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorkerTest.kt b/feature/messaging/src/test/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorkerTest.kt index 195142b993..1c4163855a 100644 --- a/feature/messaging/src/test/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorkerTest.kt +++ b/feature/messaging/src/test/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorkerTest.kt @@ -14,12 +14,11 @@ import io.mockk.just import io.mockk.mockk import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest +import okio.ByteString.Companion.toByteString import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.meshtastic.core.database.entity.Packet -import org.meshtastic.core.database.entity.PacketEntity import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.MessageStatus @@ -46,11 +45,8 @@ class SendMessageWorkerTest { fun `doWork returns success when packet is sent successfully`() = runTest { // Arrange val packetId = 12345 - val dataPacket = DataPacket("dest", 0, "Hello") - val packet = mockk(relaxed = true) - val packetEntity = PacketEntity(packet = packet) - every { packet.data } returns dataPacket - coEvery { packetRepository.getPacketByPacketId(packetId) } returns packetEntity + val dataPacket = DataPacket(to = "dest", bytes = "Hello".encodeToByteArray().toByteString(), dataType = 0) + coEvery { packetRepository.getPacketByPacketId(packetId) } returns dataPacket every { radioController.connectionState } returns MutableStateFlow(ConnectionState.Connected) coEvery { radioController.sendMessage(any()) } just Runs coEvery { packetRepository.updateMessageStatus(any(), any()) } just Runs @@ -79,11 +75,8 @@ class SendMessageWorkerTest { fun `doWork returns retry when radio is disconnected`() = runTest { // Arrange val packetId = 12345 - val dataPacket = DataPacket("dest", 0, "Hello") - val packet = mockk(relaxed = true) - val packetEntity = PacketEntity(packet = packet) - every { packet.data } returns dataPacket - coEvery { packetRepository.getPacketByPacketId(packetId) } returns packetEntity + val dataPacket = DataPacket(to = "dest", bytes = "Hello".encodeToByteArray().toByteString(), dataType = 0) + coEvery { packetRepository.getPacketByPacketId(packetId) } returns dataPacket every { radioController.connectionState } returns MutableStateFlow(ConnectionState.Disconnected) val worker = TestListenableWorkerBuilder(context) From 836bf4b1955206415c1186c9439a9aa83cb26a54 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 2 Mar 2026 13:53:07 -0600 Subject: [PATCH 08/20] refactor: extract RadioController interface and move mesh logic to core modules - Introduce `RadioController` in `core:model` to abstract radio operations and settings. - Extract `ServiceRepository`, `MqttManager`, `TracerouteHandler`, and `NeighborInfoHandler` interfaces into `core:repository`. - Move MQTT repository to `core:network` and handler implementations to `core:data`. - Update ViewModels to interact with `RadioController` instead of `IMeshService` directly. - Relocate `ServiceAction` and `TracerouteResponse` to `core:model` for better module accessibility. - Bind new implementations in `RepositoryModule` and `ServiceModule`. Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .../com/geeksville/mesh/model/UIViewModel.kt | 12 +- .../mesh/service/FromRadioPacketHandler.kt | 5 +- .../mesh/service/MeshActionHandler.kt | 2 +- .../mesh/service/MeshConfigHandler.kt | 2 +- .../mesh/service/MeshConnectionManager.kt | 3 +- .../mesh/service/MeshDataHandler.kt | 9 +- .../mesh/service/MeshMessageProcessor.kt | 2 +- .../geeksville/mesh/service/MeshService.kt | 4 +- .../geeksville/mesh/service/ReplyReceiver.kt | 10 +- .../ui/connections/ConnectionsViewModel.kt | 2 +- .../mesh/ui/sharing/ChannelViewModel.kt | 19 +-- .../mesh/widget/LocalStatsWidgetState.kt | 2 +- .../service/FromRadioPacketHandlerTest.kt | 5 +- .../mesh/service/MeshConnectionManagerTest.kt | 3 +- .../mesh/service/MeshDataHandlerTest.kt | 9 +- .../mesh/service/MeshDataMapperTest.kt | 25 +--- .../mesh/service/MeshMessageProcessorTest.kt | 2 +- .../core/data/di/RepositoryModule.kt | 32 +++++ .../core/data/manager/MqttManagerImpl.kt | 17 +-- .../data/manager/NeighborInfoHandlerImpl.kt | 18 ++- .../data/manager/TracerouteHandlerImpl.kt | 29 ++-- .../core/repository/NeighborInfoHandler.kt | 28 ++-- .../service/AndroidRadioControllerImpl.kt | 51 ++++++- .../core/ui/qr/ScannedQrCodeViewModel.kt | 18 +-- .../core/ui/share/SharedContactViewModel.kt | 4 +- .../firmware/FirmwareUpdateViewModel.kt | 10 +- .../meshtastic/feature/map/MapViewModel.kt | 14 +- .../meshtastic/feature/map/MapViewModel.kt | 8 +- .../feature/map/BaseMapViewModel.kt | 131 ++++++++---------- .../feature/messaging/MessageViewModel.kt | 4 +- .../messaging/ui/contact/ContactsViewModel.kt | 2 +- .../node/component/AdministrationSection.kt | 2 +- .../node/detail/NodeDetailViewModel.kt | 4 +- .../node/detail/NodeManagementActions.kt | 35 ++--- .../feature/node/list/NodeListViewModel.kt | 12 +- .../feature/node/metrics/MetricsViewModel.kt | 2 +- .../feature/node/model/NodeDetailAction.kt | 2 +- .../node/detail/NodeManagementActionsTest.kt | 5 +- .../settings/radio/RadioConfigViewModel.kt | 2 +- .../radio/RadioConfigViewModelTest.kt | 2 +- 40 files changed, 291 insertions(+), 257 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/model/UIViewModel.kt b/app/src/main/java/com/geeksville/mesh/model/UIViewModel.kt index 8005076ea8..d0bc742e92 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIViewModel.kt @@ -48,8 +48,10 @@ import org.meshtastic.core.data.repository.MeshLogRepository import org.meshtastic.core.database.entity.asDeviceVersion import org.meshtastic.core.datastore.UiPreferencesDataSource import org.meshtastic.core.model.MyNodeInfo +import org.meshtastic.core.model.RadioController import org.meshtastic.core.model.TracerouteMapAvailability import org.meshtastic.core.model.evaluateTracerouteMapAvailability +import org.meshtastic.core.model.service.TracerouteResponse import org.meshtastic.core.model.util.dispatchMeshtasticUri import org.meshtastic.core.repository.MeshServiceNotifications import org.meshtastic.core.repository.NodeRepository @@ -57,9 +59,8 @@ import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.client_notification import org.meshtastic.core.resources.compromised_keys +import org.meshtastic.core.service.AndroidServiceRepository import org.meshtastic.core.service.IMeshService -import org.meshtastic.core.service.ServiceRepository -import org.meshtastic.core.service.TracerouteResponse import org.meshtastic.core.ui.component.ScrollToTopEvent import org.meshtastic.core.ui.util.AlertManager import org.meshtastic.core.ui.util.ComposableContent @@ -75,7 +76,8 @@ class UIViewModel @Inject constructor( private val nodeDB: NodeRepository, - private val serviceRepository: ServiceRepository, + private val serviceRepository: AndroidServiceRepository, + private val radioController: RadioController, radioInterfaceService: RadioInterfaceService, meshLogRepository: MeshLogRepository, firmwareReleaseRepository: FirmwareReleaseRepository, @@ -161,6 +163,10 @@ constructor( val meshService: IMeshService? get() = serviceRepository.meshService + fun setDeviceAddress(address: String) { + radioController.setDeviceAddress(address) + } + val unreadMessageCount = packetRepository.getUnreadCountTotal().map { it.coerceAtLeast(0) }.stateInWhileSubscribed(initialValue = 0) diff --git a/app/src/main/java/com/geeksville/mesh/service/FromRadioPacketHandler.kt b/app/src/main/java/com/geeksville/mesh/service/FromRadioPacketHandler.kt index b94ff6dc11..b2c7ead787 100644 --- a/app/src/main/java/com/geeksville/mesh/service/FromRadioPacketHandler.kt +++ b/app/src/main/java/com/geeksville/mesh/service/FromRadioPacketHandler.kt @@ -18,8 +18,9 @@ package com.geeksville.mesh.service import co.touchlab.kermit.Logger import org.meshtastic.core.repository.MeshServiceNotifications +import org.meshtastic.core.repository.MqttManager import org.meshtastic.core.repository.PacketHandler -import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.proto.FromRadio import javax.inject.Inject import javax.inject.Singleton @@ -34,7 +35,7 @@ class FromRadioPacketHandler constructor( private val serviceRepository: ServiceRepository, private val router: MeshRouter, - private val mqttManager: MeshMqttManager, + private val mqttManager: MqttManager, private val packetHandler: PacketHandler, private val serviceNotifications: MeshServiceNotifications, ) { diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshActionHandler.kt b/app/src/main/java/com/geeksville/mesh/service/MeshActionHandler.kt index 1aa15c4215..8fe6597f6e 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshActionHandler.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshActionHandler.kt @@ -32,13 +32,13 @@ import org.meshtastic.core.model.MeshUser import org.meshtastic.core.model.MessageStatus import org.meshtastic.core.model.Position import org.meshtastic.core.model.Reaction +import org.meshtastic.core.model.service.ServiceAction import org.meshtastic.core.prefs.mesh.MeshPrefs import org.meshtastic.core.repository.CommandSender import org.meshtastic.core.repository.MeshServiceNotifications import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.ServiceBroadcasts -import org.meshtastic.core.service.ServiceAction import org.meshtastic.proto.AdminMessage import org.meshtastic.proto.Channel import org.meshtastic.proto.Config diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshConfigHandler.kt b/app/src/main/java/com/geeksville/mesh/service/MeshConfigHandler.kt index 17f43b901a..9f71992f21 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshConfigHandler.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshConfigHandler.kt @@ -26,7 +26,7 @@ import kotlinx.coroutines.flow.onEach import org.meshtastic.core.common.util.handledLaunch import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.RadioConfigRepository -import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.proto.Channel import org.meshtastic.proto.Config import org.meshtastic.proto.LocalConfig diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt b/app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt index 9a6ff95414..86e0d4dc1c 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt @@ -49,6 +49,7 @@ import org.meshtastic.core.prefs.ui.UiPrefs import org.meshtastic.core.repository.CommandSender import org.meshtastic.core.repository.HistoryManager import org.meshtastic.core.repository.MeshServiceNotifications +import org.meshtastic.core.repository.MqttManager import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.PacketHandler @@ -87,7 +88,7 @@ constructor( private val packetHandler: PacketHandler, private val nodeRepository: NodeRepository, private val locationManager: MeshLocationManager, - private val mqttManager: MeshMqttManager, + private val mqttManager: MqttManager, private val historyManager: HistoryManager, private val radioConfigRepository: RadioConfigRepository, private val commandSender: CommandSender, diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt b/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt index 177babac2c..64429d112f 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt @@ -37,6 +37,7 @@ import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.MessageStatus import org.meshtastic.core.model.Node import org.meshtastic.core.model.Reaction +import org.meshtastic.core.model.util.MeshDataMapper import org.meshtastic.core.model.util.SfppHasher import org.meshtastic.core.model.util.decodeOrNull import org.meshtastic.core.model.util.toOneLiner @@ -44,18 +45,20 @@ import org.meshtastic.core.prefs.mesh.MeshPrefs import org.meshtastic.core.repository.CommandSender import org.meshtastic.core.repository.HistoryManager import org.meshtastic.core.repository.MeshServiceNotifications +import org.meshtastic.core.repository.NeighborInfoHandler import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.PacketHandler import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.repository.ServiceBroadcasts +import org.meshtastic.core.repository.ServiceRepository +import org.meshtastic.core.repository.TracerouteHandler import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.critical_alert import org.meshtastic.core.resources.error_duty_cycle import org.meshtastic.core.resources.getString import org.meshtastic.core.resources.unknown_username import org.meshtastic.core.resources.waypoint_received -import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.service.filter.MessageFilterService import org.meshtastic.proto.AdminMessage import org.meshtastic.proto.MeshPacket @@ -94,8 +97,8 @@ constructor( private val historyManager: HistoryManager, private val meshPrefs: MeshPrefs, private val connectionManager: MeshConnectionManager, - private val tracerouteHandler: MeshTracerouteHandler, - private val neighborInfoHandler: MeshNeighborInfoHandler, + private val tracerouteHandler: TracerouteHandler, + private val neighborInfoHandler: NeighborInfoHandler, private val radioConfigRepository: RadioConfigRepository, private val messageFilterService: MessageFilterService, ) { diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshMessageProcessor.kt b/app/src/main/java/com/geeksville/mesh/service/MeshMessageProcessor.kt index c601099146..dbf4441395 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshMessageProcessor.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshMessageProcessor.kt @@ -34,7 +34,7 @@ import org.meshtastic.core.database.entity.MeshLog import org.meshtastic.core.model.Node import org.meshtastic.core.model.util.isLora import org.meshtastic.core.repository.NodeManager -import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.proto.FromRadio import org.meshtastic.proto.LogRecord import org.meshtastic.proto.MeshPacket diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt index 35151a5d4b..4fa86b3f35 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -49,8 +49,8 @@ import org.meshtastic.core.repository.PacketHandler import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.repository.SERVICE_NOTIFY_ID import org.meshtastic.core.repository.ServiceBroadcasts +import org.meshtastic.core.service.AndroidServiceRepository import org.meshtastic.core.service.IMeshService -import org.meshtastic.core.service.ServiceRepository import org.meshtastic.proto.PortNum import javax.inject.Inject @@ -60,7 +60,7 @@ class MeshService : Service() { @Inject lateinit var radioInterfaceService: RadioInterfaceService - @Inject lateinit var serviceRepository: ServiceRepository + @Inject lateinit var serviceRepository: AndroidServiceRepository @Inject lateinit var connectionStateHolder: ConnectionStateHandler diff --git a/app/src/main/java/com/geeksville/mesh/service/ReplyReceiver.kt b/app/src/main/java/com/geeksville/mesh/service/ReplyReceiver.kt index 9875de12c0..7c29affe4a 100644 --- a/app/src/main/java/com/geeksville/mesh/service/ReplyReceiver.kt +++ b/app/src/main/java/com/geeksville/mesh/service/ReplyReceiver.kt @@ -20,9 +20,11 @@ import android.content.BroadcastReceiver import androidx.core.app.RemoteInput import dagger.hilt.android.AndroidEntryPoint import jakarta.inject.Inject +import kotlinx.coroutines.launch import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.model.RadioController import org.meshtastic.core.repository.MeshServiceNotifications -import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.repository.ServiceRepository /** * A [BroadcastReceiver] that handles inline replies from notifications. @@ -33,7 +35,7 @@ import org.meshtastic.core.service.ServiceRepository */ @AndroidEntryPoint class ReplyReceiver : BroadcastReceiver() { - @Inject lateinit var serviceRepository: ServiceRepository + @Inject lateinit var radioController: RadioController @Inject lateinit var meshServiceNotifications: MeshServiceNotifications @@ -48,7 +50,9 @@ class ReplyReceiver : BroadcastReceiver() { val channel = contactKey[0].digitToIntOrNull() val dest = if (channel != null) contactKey.substring(1) else contactKey val p = DataPacket(dest, channel ?: 0, str) - serviceRepository.meshService?.send(p) + kotlinx.coroutines.CoroutineScope(kotlinx.coroutines.Dispatchers.IO).launch { + radioController.sendMessage(p) + } } override fun onReceive(context: android.content.Context, intent: android.content.Intent) { diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt index da78f976f9..b17281ff6b 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt @@ -26,7 +26,7 @@ import org.meshtastic.core.model.Node import org.meshtastic.core.prefs.ui.UiPrefs import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.RadioConfigRepository -import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.LocalConfig import javax.inject.Inject diff --git a/app/src/main/java/com/geeksville/mesh/ui/sharing/ChannelViewModel.kt b/app/src/main/java/com/geeksville/mesh/ui/sharing/ChannelViewModel.kt index 71ff98493e..0e90416971 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/sharing/ChannelViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/sharing/ChannelViewModel.kt @@ -17,7 +17,6 @@ package com.geeksville.mesh.ui.sharing import android.net.Uri -import android.os.RemoteException import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import co.touchlab.kermit.Logger @@ -27,9 +26,9 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import org.meshtastic.core.analytics.DataPair import org.meshtastic.core.analytics.platform.PlatformAnalytics +import org.meshtastic.core.model.RadioController import org.meshtastic.core.model.util.toChannelSet import org.meshtastic.core.repository.RadioConfigRepository -import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.util.getChannelList import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.Channel @@ -42,12 +41,12 @@ import javax.inject.Inject class ChannelViewModel @Inject constructor( - private val serviceRepository: ServiceRepository, + private val radioController: RadioController, private val radioConfigRepository: RadioConfigRepository, private val analytics: PlatformAnalytics, ) : ViewModel() { - val connectionState = serviceRepository.connectionState + val connectionState = radioController.connectionState val localConfig = radioConfigRepository.localConfigFlow.stateInWhileSubscribed(initialValue = LocalConfig()) @@ -95,19 +94,15 @@ constructor( } fun setChannel(channel: Channel) { - try { - serviceRepository.meshService?.setChannel(channel.encode()) - } catch (ex: RemoteException) { - Logger.e(ex) { "Set channel error" } + viewModelScope.launch { + radioController.setLocalChannel(channel) } } // Set the radio config (also updates our saved copy in preferences) fun setConfig(config: Config) { - try { - serviceRepository.meshService?.setConfig(config.encode()) - } catch (ex: RemoteException) { - Logger.e(ex) { "Set config error" } + viewModelScope.launch { + radioController.setLocalConfig(config) } } diff --git a/app/src/main/java/com/geeksville/mesh/widget/LocalStatsWidgetState.kt b/app/src/main/java/com/geeksville/mesh/widget/LocalStatsWidgetState.kt index 4b1ca2fdca..1f28a65f7c 100644 --- a/app/src/main/java/com/geeksville/mesh/widget/LocalStatsWidgetState.kt +++ b/app/src/main/java/com/geeksville/mesh/widget/LocalStatsWidgetState.kt @@ -32,7 +32,7 @@ import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.Node import org.meshtastic.core.model.util.onlineTimeThreshold import org.meshtastic.core.repository.NodeRepository -import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.proto.LocalStats import javax.inject.Inject import javax.inject.Singleton diff --git a/app/src/test/java/com/geeksville/mesh/service/FromRadioPacketHandlerTest.kt b/app/src/test/java/com/geeksville/mesh/service/FromRadioPacketHandlerTest.kt index ec2ba1bcdb..d4d7e6112d 100644 --- a/app/src/test/java/com/geeksville/mesh/service/FromRadioPacketHandlerTest.kt +++ b/app/src/test/java/com/geeksville/mesh/service/FromRadioPacketHandlerTest.kt @@ -21,7 +21,8 @@ import io.mockk.verify import org.junit.Before import org.junit.Test import org.meshtastic.core.repository.MeshServiceNotifications -import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.repository.MqttManager +import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.proto.ClientNotification import org.meshtastic.proto.Config import org.meshtastic.proto.DeviceMetadata @@ -33,7 +34,7 @@ import org.meshtastic.proto.QueueStatus class FromRadioPacketHandlerTest { private val serviceRepository: ServiceRepository = mockk(relaxed = true) private val router: MeshRouter = mockk(relaxed = true) - private val mqttManager: MeshMqttManager = mockk(relaxed = true) + private val mqttManager: MqttManager = mockk(relaxed = true) private val packetHandler: PacketHandler = mockk(relaxed = true) private val serviceNotifications: MeshServiceNotifications = mockk(relaxed = true) diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshConnectionManagerTest.kt b/app/src/test/java/com/geeksville/mesh/service/MeshConnectionManagerTest.kt index 7eea98c2b2..213fcc6ad0 100644 --- a/app/src/test/java/com/geeksville/mesh/service/MeshConnectionManagerTest.kt +++ b/app/src/test/java/com/geeksville/mesh/service/MeshConnectionManagerTest.kt @@ -48,6 +48,7 @@ import org.meshtastic.core.prefs.ui.UiPrefs import org.meshtastic.core.repository.CommandSender import org.meshtastic.core.repository.HistoryManager import org.meshtastic.core.repository.MeshServiceNotifications +import org.meshtastic.core.repository.MqttManager import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.PacketRepository @@ -71,7 +72,7 @@ class MeshConnectionManagerTest { private val packetHandler: PacketHandler = mockk(relaxed = true) private val nodeRepository: NodeRepository = mockk(relaxed = true) private val locationManager: MeshLocationManager = mockk(relaxed = true) - private val mqttManager: MeshMqttManager = mockk(relaxed = true) + private val mqttManager: MqttManager = mockk(relaxed = true) private val historyManager: HistoryManager = mockk(relaxed = true) private val radioConfigRepository: RadioConfigRepository = mockk(relaxed = true) private val commandSender: CommandSender = mockk(relaxed = true) diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshDataHandlerTest.kt b/app/src/test/java/com/geeksville/mesh/service/MeshDataHandlerTest.kt index 0c06ac91ee..6f29161964 100644 --- a/app/src/test/java/com/geeksville/mesh/service/MeshDataHandlerTest.kt +++ b/app/src/test/java/com/geeksville/mesh/service/MeshDataHandlerTest.kt @@ -31,14 +31,17 @@ import org.junit.Test import org.meshtastic.core.analytics.platform.PlatformAnalytics import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.MessageStatus +import org.meshtastic.core.model.util.MeshDataMapper import org.meshtastic.core.prefs.mesh.MeshPrefs import org.meshtastic.core.repository.CommandSender import org.meshtastic.core.repository.HistoryManager import org.meshtastic.core.repository.MeshServiceNotifications +import org.meshtastic.core.repository.NeighborInfoHandler import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.RadioConfigRepository -import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.repository.ServiceRepository +import org.meshtastic.core.repository.TracerouteHandler import org.meshtastic.core.service.filter.MessageFilterService import org.meshtastic.proto.Data import org.meshtastic.proto.MeshPacket @@ -62,8 +65,8 @@ class MeshDataHandlerTest { private val historyManager: HistoryManager = mockk(relaxed = true) private val meshPrefs: MeshPrefs = mockk(relaxed = true) private val connectionManager: MeshConnectionManager = mockk(relaxed = true) - private val tracerouteHandler: MeshTracerouteHandler = mockk(relaxed = true) - private val neighborInfoHandler: MeshNeighborInfoHandler = mockk(relaxed = true) + private val tracerouteHandler: TracerouteHandler = mockk(relaxed = true) + private val neighborInfoHandler: NeighborInfoHandler = mockk(relaxed = true) private val radioConfigRepository: RadioConfigRepository = mockk(relaxed = true) private val messageFilterService: MessageFilterService = mockk(relaxed = true) diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshDataMapperTest.kt b/app/src/test/java/com/geeksville/mesh/service/MeshDataMapperTest.kt index 0c6f5aaad5..67c53b4d62 100644 --- a/app/src/test/java/com/geeksville/mesh/service/MeshDataMapperTest.kt +++ b/app/src/test/java/com/geeksville/mesh/service/MeshDataMapperTest.kt @@ -25,6 +25,7 @@ import org.junit.Assert.assertNull import org.junit.Before import org.junit.Test import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.model.util.MeshDataMapper import org.meshtastic.core.repository.NodeManager import org.meshtastic.proto.Data import org.meshtastic.proto.MeshPacket @@ -40,30 +41,6 @@ class MeshDataMapperTest { mapper = MeshDataMapper(nodeManager) } - @Test - fun `toNodeID resolves broadcast correctly`() { - every { nodeManager.toNodeID(DataPacket.NODENUM_BROADCAST) } returns DataPacket.ID_BROADCAST - assertEquals(DataPacket.ID_BROADCAST, mapper.toNodeID(DataPacket.NODENUM_BROADCAST)) - } - - @Test - fun `toNodeID resolves known node correctly`() { - val nodeNum = 1234 - val nodeId = "!1234abcd" - every { nodeManager.toNodeID(nodeNum) } returns nodeId - - assertEquals(nodeId, mapper.toNodeID(nodeNum)) - } - - @Test - fun `toNodeID resolves unknown node to default ID`() { - val nodeNum = 1234 - val nodeId = DataPacket.nodeNumToDefaultId(nodeNum) - every { nodeManager.toNodeID(nodeNum) } returns nodeId - - assertEquals(nodeId, mapper.toNodeID(nodeNum)) - } - @Test fun `toDataPacket returns null when no decoded data`() { val packet = MeshPacket() diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshMessageProcessorTest.kt b/app/src/test/java/com/geeksville/mesh/service/MeshMessageProcessorTest.kt index 0ede2ab56d..c162b10b3c 100644 --- a/app/src/test/java/com/geeksville/mesh/service/MeshMessageProcessorTest.kt +++ b/app/src/test/java/com/geeksville/mesh/service/MeshMessageProcessorTest.kt @@ -29,7 +29,7 @@ import org.junit.Test import org.meshtastic.core.data.repository.MeshLogRepository import org.meshtastic.core.database.entity.MeshLog import org.meshtastic.core.repository.NodeManager -import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.proto.Data import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.PortNum diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt index 2d24db0af2..45a39399dc 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt @@ -19,22 +19,30 @@ package org.meshtastic.core.data.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.HistoryManagerImpl +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.TracerouteHandlerImpl import org.meshtastic.core.data.repository.DeviceHardwareRepositoryImpl 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.HistoryManager +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.PacketRepository import org.meshtastic.core.repository.RadioConfigRepository +import org.meshtastic.core.repository.TracerouteHandler import javax.inject.Singleton @Module @@ -82,4 +90,28 @@ abstract class RepositoryModule { 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 + + companion object { + @Provides + @Singleton + fun provideMeshDataMapper(nodeManager: NodeManager): MeshDataMapper = MeshDataMapper(nodeManager) + } } diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MqttManagerImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MqttManagerImpl.kt index ef4cc40bef..7684ebd205 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MqttManagerImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MqttManagerImpl.kt @@ -14,11 +14,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.geeksville.mesh.service +package org.meshtastic.core.data.manager import co.touchlab.kermit.Logger import co.touchlab.kermit.Severity -import com.geeksville.mesh.repository.network.MQTTRepository import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -26,25 +25,27 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import org.meshtastic.core.network.repository.MQTTRepository +import org.meshtastic.core.repository.MqttManager import org.meshtastic.core.repository.PacketHandler -import org.meshtastic.core.service.ServiceRepository +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 MeshMqttManager +class MqttManagerImpl @Inject constructor( private val mqttRepository: MQTTRepository, private val packetHandler: PacketHandler, private val serviceRepository: ServiceRepository, -) { +) : MqttManager { private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private var mqttMessageFlow: Job? = null - fun start(scope: CoroutineScope, enabled: Boolean, proxyToClientEnabled: Boolean) { + override fun start(scope: CoroutineScope, enabled: Boolean, proxyToClientEnabled: Boolean) { this.scope = scope if (mqttMessageFlow?.isActive == true) return if (enabled && proxyToClientEnabled) { @@ -61,7 +62,7 @@ constructor( } } - fun stop() { + override fun stop() { if (mqttMessageFlow?.isActive == true) { Logger.i { "Stopping MqttClientProxy" } mqttMessageFlow?.cancel() @@ -69,7 +70,7 @@ constructor( } } - fun handleMqttProxyMessage(message: MqttClientProxyMessage) { + override fun handleMqttProxyMessage(message: MqttClientProxyMessage) { val topic = message.topic ?: "" Logger.d { "[mqttClientProxyMessage] $topic" } val retained = message.retained == true diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NeighborInfoHandlerImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NeighborInfoHandlerImpl.kt index 1e10b28243..df19abacf6 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NeighborInfoHandlerImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NeighborInfoHandlerImpl.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 com.geeksville.mesh.service +package org.meshtastic.core.data.manager import co.touchlab.kermit.Logger import kotlinx.coroutines.CoroutineScope @@ -22,12 +22,10 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.repository.CommandSender +import org.meshtastic.core.repository.NeighborInfoHandler import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.ServiceBroadcasts -import org.meshtastic.core.resources.Res -import org.meshtastic.core.resources.getString -import org.meshtastic.core.resources.unknown_username -import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.NeighborInfo import java.util.Locale @@ -35,21 +33,21 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class MeshNeighborInfoHandler +class NeighborInfoHandlerImpl @Inject constructor( private val nodeManager: NodeManager, private val serviceRepository: ServiceRepository, private val commandSender: CommandSender, private val serviceBroadcasts: ServiceBroadcasts, -) { +) : NeighborInfoHandler { private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - fun start(scope: CoroutineScope) { + override fun start(scope: CoroutineScope) { this.scope = scope } - fun handleNeighborInfo(packet: MeshPacket) { + override fun handleNeighborInfo(packet: MeshPacket) { val payload = packet.decoded?.payload ?: return val ni = NeighborInfo.ADAPTER.decode(payload) @@ -70,7 +68,7 @@ constructor( val neighbors = ni.neighbors.joinToString("\n") { n -> val node = nodeManager.nodeDBbyNodeNum[n.node_id] - val name = node?.let { "${it.user.long_name} (${it.user.short_name})" } ?: getString(Res.string.unknown_username) + val name = node?.let { "${it.user.long_name} (${it.user.short_name})" } ?: "Unknown" "• $name (SNR: ${n.snr})" } diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/TracerouteHandlerImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/TracerouteHandlerImpl.kt index 4fa0801cfa..518d4a6826 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/TracerouteHandlerImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/TracerouteHandlerImpl.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 com.geeksville.mesh.service +package org.meshtastic.core.data.manager import co.touchlab.kermit.Logger import kotlinx.coroutines.CoroutineScope @@ -26,24 +26,19 @@ import org.meshtastic.core.data.repository.TracerouteSnapshotRepository import org.meshtastic.core.model.Node import org.meshtastic.core.model.fullRouteDiscovery import org.meshtastic.core.model.getFullTracerouteResponse +import org.meshtastic.core.model.service.TracerouteResponse import org.meshtastic.core.repository.CommandSender import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.NodeRepository -import org.meshtastic.core.resources.Res -import org.meshtastic.core.resources.getString -import org.meshtastic.core.resources.traceroute_duration -import org.meshtastic.core.resources.traceroute_route_back_to_us -import org.meshtastic.core.resources.traceroute_route_towards_dest -import org.meshtastic.core.resources.unknown_username -import org.meshtastic.core.service.ServiceRepository -import org.meshtastic.core.service.TracerouteResponse +import org.meshtastic.core.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 MeshTracerouteHandler +class TracerouteHandlerImpl @Inject constructor( private val nodeManager: NodeManager, @@ -51,22 +46,22 @@ constructor( private val tracerouteSnapshotRepository: TracerouteSnapshotRepository, private val nodeRepository: NodeRepository, private val commandSender: CommandSender, -) { +) : TracerouteHandler { private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - fun start(scope: CoroutineScope) { + override fun start(scope: CoroutineScope) { this.scope = scope } - fun handleTraceroute(packet: MeshPacket, logUuid: String?, logInsertJob: kotlinx.coroutines.Job?) { + override fun handleTraceroute(packet: MeshPacket, logUuid: String?, logInsertJob: kotlinx.coroutines.Job?) { val full = packet.getFullTracerouteResponse( getUser = { num -> nodeManager.nodeDBbyNodeNum[num]?.let { node: Node -> "${node.user.long_name} (${node.user.short_name})" } - ?: getString(Res.string.unknown_username) + ?: "Unknown" // We don't have strings in core:data yet, but we can fix this later }, - headerTowards = getString(Res.string.traceroute_route_towards_dest), - headerBack = getString(Res.string.traceroute_route_back_to_us), + headerTowards = "Route towards destination:", + headerBack = "Route back to us:", ) ?: return val requestId = packet.decoded?.request_id ?: 0 @@ -90,7 +85,7 @@ constructor( val elapsedMs = nowMillis - start val seconds = elapsedMs / MILLIS_PER_SECOND Logger.i { "Traceroute $requestId complete in $seconds s" } - val durationText = getString(Res.string.traceroute_duration, "%.1f".format(Locale.US, seconds)) + val durationText = "Duration: %.1f s".format(Locale.US, seconds) "$full\n\n$durationText" } else { full diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NeighborInfoHandler.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NeighborInfoHandler.kt index 44e6bde5ab..45a8dc72aa 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NeighborInfoHandler.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NeighborInfoHandler.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 @@ -14,20 +14,22 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.geeksville.mesh.service +package org.meshtastic.core.repository -import org.meshtastic.core.model.DataPacket -import org.meshtastic.core.repository.NodeManager +import kotlinx.coroutines.CoroutineScope import org.meshtastic.proto.MeshPacket -import javax.inject.Inject -import javax.inject.Singleton -import org.meshtastic.core.model.util.MeshDataMapper as CommonMeshDataMapper -@Singleton -class MeshDataMapper @Inject constructor(private val nodeManager: NodeManager) { - private val commonMapper = CommonMeshDataMapper(nodeManager) - - fun toNodeID(n: Int): String = nodeManager.toNodeID(n) +/** + * Interface for handling neighbor info responses from the mesh. + */ +interface NeighborInfoHandler { + /** Starts the neighbor info handler with the given coroutine scope. */ + fun start(scope: CoroutineScope) - fun toDataPacket(packet: MeshPacket): DataPacket? = commonMapper.toDataPacket(packet) + /** + * Processes a neighbor info packet. + * + * @param packet The received mesh packet. + */ + fun handleNeighborInfo(packet: MeshPacket) } diff --git a/core/service/src/main/kotlin/org/meshtastic/core/service/AndroidRadioControllerImpl.kt b/core/service/src/main/kotlin/org/meshtastic/core/service/AndroidRadioControllerImpl.kt index f2aa9a11e2..90444acd2f 100644 --- a/core/service/src/main/kotlin/org/meshtastic/core/service/AndroidRadioControllerImpl.kt +++ b/core/service/src/main/kotlin/org/meshtastic/core/service/AndroidRadioControllerImpl.kt @@ -16,10 +16,13 @@ */ package org.meshtastic.core.service +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.StateFlow 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 @@ -30,7 +33,8 @@ import javax.inject.Singleton class AndroidRadioControllerImpl @Inject constructor( - private val serviceRepository: ServiceRepository, + @ApplicationContext private val context: Context, + private val serviceRepository: AndroidServiceRepository, private val nodeRepository: NodeRepository, ) : RadioController { @@ -65,6 +69,14 @@ constructor( serviceRepository.onServiceAction(ServiceAction.SendContact(contact)) } + override suspend fun setLocalConfig(config: org.meshtastic.proto.Config) { + serviceRepository.meshService?.setConfig(config.encode()) + } + + override suspend fun setLocalChannel(channel: org.meshtastic.proto.Channel) { + serviceRepository.meshService?.setChannel(channel.encode()) + } + override suspend fun setOwner(destNum: Int, user: org.meshtastic.proto.User, packetId: Int) { serviceRepository.meshService?.setRemoteOwner(packetId, destNum, user.encode()) } @@ -125,6 +137,14 @@ constructor( serviceRepository.meshService?.requestReboot(packetId, destNum) } + override suspend fun rebootToDfu(nodeNum: Int) { + serviceRepository.meshService?.rebootToDfu(nodeNum) + } + + override suspend fun requestRebootOta(requestId: Int, destNum: Int, mode: Int, hash: ByteArray?) { + serviceRepository.meshService?.requestRebootOta(requestId, destNum, mode, hash) + } + override suspend fun shutdown(destNum: Int, packetId: Int) { serviceRepository.meshService?.requestShutdown(packetId, destNum) } @@ -141,6 +161,26 @@ constructor( serviceRepository.meshService?.removeByNodenum(packetId, nodeNum) } + override suspend fun requestPosition(destNum: Int, currentPosition: org.meshtastic.core.model.Position) { + serviceRepository.meshService?.requestPosition(destNum, currentPosition) + } + + override suspend fun requestUserInfo(destNum: Int) { + serviceRepository.meshService?.requestUserInfo(destNum) + } + + override suspend fun requestTraceroute(requestId: Int, destNum: Int) { + serviceRepository.meshService?.requestTraceroute(requestId, destNum) + } + + override suspend fun requestTelemetry(requestId: Int, destNum: Int, typeValue: Int) { + serviceRepository.meshService?.requestTelemetry(requestId, destNum, typeValue) + } + + override suspend fun requestNeighborInfo(requestId: Int, destNum: Int) { + serviceRepository.meshService?.requestNeighborInfo(requestId, destNum) + } + override suspend fun beginEditSettings(destNum: Int) { serviceRepository.meshService?.beginEditSettings(destNum) } @@ -158,4 +198,13 @@ constructor( override fun stopProvideLocation() { serviceRepository.meshService?.stopProvideLocation() } + + override fun setDeviceAddress(address: String) { + serviceRepository.meshService?.setDeviceAddress(address) + // Ensure service is running/restarted to handle the new address + val intent = android.content.Intent().apply { + setClassName("com.geeksville.mesh", "com.geeksville.mesh.service.MeshService") + } + context.startForegroundService(intent) + } } 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 128b0e5d5d..e9eeef11f9 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 @@ -16,14 +16,12 @@ */ package org.meshtastic.core.ui.qr -import android.os.RemoteException import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import co.touchlab.kermit.Logger import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch +import org.meshtastic.core.model.RadioController import org.meshtastic.core.repository.RadioConfigRepository -import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.util.getChannelList import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.Channel @@ -37,7 +35,7 @@ class ScannedQrCodeViewModel @Inject constructor( private val radioConfigRepository: RadioConfigRepository, - private val serviceRepository: ServiceRepository, + private val radioController: RadioController, ) : ViewModel() { val channels = radioConfigRepository.channelSetFlow.stateInWhileSubscribed(initialValue = ChannelSet()) @@ -56,19 +54,15 @@ constructor( } private fun setChannel(channel: Channel) { - try { - serviceRepository.meshService?.setChannel(Channel.ADAPTER.encode(channel)) - } catch (ex: RemoteException) { - Logger.e(ex) { "Set channel error" } + viewModelScope.launch { + radioController.setLocalChannel(channel) } } // Set the radio config (also updates our saved copy in preferences) private fun setConfig(config: Config) { - try { - serviceRepository.meshService?.setConfig(Config.ADAPTER.encode(config)) - } catch (ex: RemoteException) { - Logger.e(ex) { "Set config error" } + viewModelScope.launch { + radioController.setLocalConfig(config) } } } 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 82ef4ed6cf..d0feb933d9 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 @@ -22,9 +22,9 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import org.meshtastic.core.model.Node +import org.meshtastic.core.model.service.ServiceAction import org.meshtastic.core.repository.NodeRepository -import org.meshtastic.core.service.ServiceAction -import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.SharedContact import javax.inject.Inject diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt b/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt index 5c8e0a3a5b..92d70fe4e9 100644 --- a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt +++ b/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt @@ -44,6 +44,7 @@ import org.meshtastic.core.datastore.BootloaderWarningDataSource import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.DeviceHardware import org.meshtastic.core.model.MyNodeInfo +import org.meshtastic.core.model.RadioController import org.meshtastic.core.prefs.radio.RadioPrefs import org.meshtastic.core.prefs.radio.isBle import org.meshtastic.core.prefs.radio.isSerial @@ -72,7 +73,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 org.meshtastic.core.service.ServiceRepository import java.io.File import javax.inject.Inject @@ -95,7 +95,7 @@ constructor( private val firmwareReleaseRepository: FirmwareReleaseRepository, private val deviceHardwareRepository: DeviceHardwareRepository, private val nodeRepository: NodeRepository, - private val serviceRepository: ServiceRepository, + private val radioController: RadioController, private val radioPrefs: RadioPrefs, private val bootloaderWarningDataSource: BootloaderWarningDataSource, private val firmwareUpdateManager: FirmwareUpdateManager, @@ -106,6 +106,8 @@ constructor( private val _state = MutableStateFlow(FirmwareUpdateState.Idle) val state: StateFlow = _state.asStateFlow() + val connectionState = radioController.connectionState + private val _selectedReleaseType = MutableStateFlow(FirmwareReleaseType.STABLE) val selectedReleaseType: StateFlow = _selectedReleaseType.asStateFlow() @@ -429,14 +431,14 @@ constructor( // Trigger a fresh connection attempt by MeshService address?.let { currentAddr -> Logger.i { "Post-update: Requesting MeshService to reconnect to $currentAddr" } - serviceRepository.meshService?.setDeviceAddress("$DFU_RECONNECT_PREFIX$currentAddr") + radioController.setDeviceAddress("$DFU_RECONNECT_PREFIX$currentAddr") } // Wait for device to reconnect and settle val result = withTimeoutOrNull(VERIFY_TIMEOUT) { // Wait for both Connected state and node info to be present - serviceRepository.connectionState.first { it is ConnectionState.Connected } + connectionState.first { it is ConnectionState.Connected } nodeRepository.ourNodeInfo.filterNotNull().first() delay(VERIFY_DELAY) // Extra buffer for initial config sync true diff --git a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewModel.kt b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewModel.kt index c0434ae585..66b2e3b0c1 100644 --- a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewModel.kt +++ b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewModel.kt @@ -23,13 +23,13 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import org.meshtastic.core.common.BuildConfigProvider -import org.meshtastic.core.repository.NodeRepository -import org.meshtastic.core.repository.PacketRepository -import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.model.RadioController import org.meshtastic.core.navigation.MapRoutes import org.meshtastic.core.prefs.map.MapPrefs -import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.PacketRepository +import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.LocalConfig import javax.inject.Inject @@ -41,12 +41,12 @@ class MapViewModel constructor( mapPrefs: MapPrefs, packetRepository: PacketRepository, - private val nodeRepository: NodeRepository, - serviceRepository: ServiceRepository, + override val nodeRepository: NodeRepository, + radioController: RadioController, radioConfigRepository: RadioConfigRepository, buildConfigProvider: BuildConfigProvider, savedStateHandle: SavedStateHandle, -) : BaseMapViewModel(mapPrefs, nodeRepository, packetRepository, serviceRepository) { +) : BaseMapViewModel(mapPrefs, nodeRepository, packetRepository, radioController) { private val _selectedWaypointId = MutableStateFlow(savedStateHandle.toRoute().waypointId) val selectedWaypointId: StateFlow = _selectedWaypointId.asStateFlow() diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapViewModel.kt b/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapViewModel.kt index 86fdecce36..2e5b6cc094 100644 --- a/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapViewModel.kt +++ b/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapViewModel.kt @@ -46,13 +46,13 @@ import kotlinx.serialization.Serializable import org.meshtastic.core.data.model.CustomTileProviderConfig import org.meshtastic.core.data.repository.CustomTileProviderRepository import org.meshtastic.core.datastore.UiPreferencesDataSource +import org.meshtastic.core.model.RadioController import org.meshtastic.core.navigation.MapRoutes import org.meshtastic.core.prefs.map.GoogleMapsPrefs import org.meshtastic.core.prefs.map.MapPrefs import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.RadioConfigRepository -import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.Config import java.io.File @@ -86,11 +86,11 @@ constructor( nodeRepository: NodeRepository, packetRepository: PacketRepository, radioConfigRepository: RadioConfigRepository, - serviceRepository: ServiceRepository, + radioController: RadioController, private val customTileProviderRepository: CustomTileProviderRepository, uiPreferencesDataSource: UiPreferencesDataSource, savedStateHandle: SavedStateHandle, -) : BaseMapViewModel(mapPrefs, nodeRepository, packetRepository, serviceRepository) { +) : BaseMapViewModel(mapPrefs, nodeRepository, packetRepository, radioController) { private val _selectedWaypointId = MutableStateFlow(savedStateHandle.toRoute().waypointId) val selectedWaypointId: StateFlow = _selectedWaypointId.asStateFlow() @@ -643,6 +643,8 @@ constructor( super.onCleared() (currentTileProvider as? MBTilesProvider)?.close() } + + override fun getUser(userId: String?) = nodeRepository.getUser(userId ?: org.meshtastic.core.model.DataPacket.ID_BROADCAST) } enum class LayerType { diff --git a/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt b/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt index 5b3c76c649..68a71075c4 100644 --- a/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt +++ b/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt @@ -16,10 +16,8 @@ */ package org.meshtastic.feature.map -import android.os.RemoteException import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import co.touchlab.kermit.Logger import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -31,7 +29,7 @@ import org.jetbrains.compose.resources.StringResource import org.meshtastic.core.common.util.nowSeconds import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.Node -import org.meshtastic.core.model.util.TimeConstants +import org.meshtastic.core.model.RadioController import org.meshtastic.core.prefs.map.MapPrefs import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.PacketRepository @@ -41,47 +39,31 @@ import org.meshtastic.core.resources.eight_hours import org.meshtastic.core.resources.one_day import org.meshtastic.core.resources.one_hour import org.meshtastic.core.resources.two_days -import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.feature.map.model.TracerouteOverlay import org.meshtastic.proto.Position -import org.meshtastic.proto.User import org.meshtastic.proto.Waypoint -@Suppress("MagicNumber") -sealed class LastHeardFilter(val seconds: Long, val label: StringResource) { - data object Any : LastHeardFilter(0L, Res.string.any) - - data object OneHour : LastHeardFilter(TimeConstants.ONE_HOUR.inWholeSeconds, Res.string.one_hour) - - data object EightHours : LastHeardFilter(TimeConstants.EIGHT_HOURS.inWholeSeconds, Res.string.eight_hours) - - data object OneDay : LastHeardFilter(TimeConstants.ONE_DAY.inWholeSeconds, Res.string.one_day) - - data object TwoDays : LastHeardFilter(TimeConstants.TWO_DAYS.inWholeSeconds, Res.string.two_days) - - companion object { - fun fromSeconds(seconds: Long): LastHeardFilter = entries.find { it.seconds == seconds } ?: Any - - val entries = listOf(Any, OneHour, EightHours, OneDay, TwoDays) - } -} - @Suppress("TooManyFunctions") abstract class BaseMapViewModel( protected val mapPrefs: MapPrefs, - private val nodeRepository: NodeRepository, + protected open val nodeRepository: NodeRepository, private val packetRepository: PacketRepository, - private val serviceRepository: ServiceRepository, + private val radioController: RadioController, ) : ViewModel() { val myNodeInfo = nodeRepository.myNodeInfo + val ourNodeInfo = nodeRepository.ourNodeInfo + val myNodeNum get() = myNodeInfo.value?.myNodeNum val myId = nodeRepository.myId + val isConnected = radioController.connectionState.map { it is org.meshtastic.core.model.ConnectionState.Connected } + .stateInWhileSubscribed(initialValue = false) + val nodes: StateFlow> = nodeRepository .getNodes() @@ -107,64 +89,52 @@ abstract class BaseMapViewModel( .stateInWhileSubscribed(initialValue = emptyMap()) private val showOnlyFavorites = MutableStateFlow(mapPrefs.showOnlyFavorites) + val showOnlyFavoritesOnMap = showOnlyFavorites - private val showWaypointsOnMap = MutableStateFlow(mapPrefs.showWaypointsOnMap) + fun toggleOnlyFavorites() { + val newValue = !showOnlyFavorites.value + showOnlyFavorites.value = newValue + mapPrefs.showOnlyFavorites = newValue + } - private val showPrecisionCircleOnMap = MutableStateFlow(mapPrefs.showPrecisionCircleOnMap) + private val showWaypoints = MutableStateFlow(mapPrefs.showWaypointsOnMap) + val showWaypointsOnMap = showWaypoints - private val lastHeardFilter = MutableStateFlow(LastHeardFilter.fromSeconds(mapPrefs.lastHeardFilter)) + fun toggleShowWaypointsOnMap() { + val newValue = !showWaypoints.value + showWaypoints.value = newValue + mapPrefs.showWaypointsOnMap = newValue + } - private val lastHeardTrackFilter = MutableStateFlow(LastHeardFilter.fromSeconds(mapPrefs.lastHeardTrackFilter)) + private val showPrecisionCircle = MutableStateFlow(mapPrefs.showPrecisionCircleOnMap) + val showPrecisionCircleOnMap = showPrecisionCircle + + fun toggleShowPrecisionCircleOnMap() { + val newValue = !showPrecisionCircle.value + showPrecisionCircle.value = newValue + mapPrefs.showPrecisionCircleOnMap = newValue + } + + private val lastHeardFilterValue = MutableStateFlow(LastHeardFilter.fromSeconds(mapPrefs.lastHeardFilter)) + val lastHeardFilter = lastHeardFilterValue fun setLastHeardFilter(filter: LastHeardFilter) { + lastHeardFilterValue.value = filter mapPrefs.lastHeardFilter = filter.seconds - lastHeardFilter.value = filter } + private val lastHeardTrackFilterValue = MutableStateFlow(LastHeardFilter.fromSeconds(mapPrefs.lastHeardTrackFilter)) + val lastHeardTrackFilter = lastHeardTrackFilterValue + fun setLastHeardTrackFilter(filter: LastHeardFilter) { + lastHeardTrackFilterValue.value = filter mapPrefs.lastHeardTrackFilter = filter.seconds - lastHeardTrackFilter.value = filter } - val ourNodeInfo: StateFlow = nodeRepository.ourNodeInfo - - fun getNodeByNum(nodeNum: Int): Node? = nodeRepository.nodeDBbyNum.value[nodeNum] + abstract fun getUser(userId: String?): org.meshtastic.proto.User - open fun getUser(userId: String?): User = nodeRepository.getUser(userId ?: DataPacket.ID_BROADCAST) - - fun getUser(nodeNum: Int): User = nodeRepository.getUser(nodeNum) - - fun getNodeOrFallback(nodeNum: Int): Node = getNodeByNum(nodeNum) ?: Node(num = nodeNum, user = getUser(nodeNum)) - - val isConnected = - serviceRepository.connectionState.map { it.isConnected() }.stateInWhileSubscribed(initialValue = false) - - fun toggleOnlyFavorites() { - val current = showOnlyFavorites.value - mapPrefs.showOnlyFavorites = !current - showOnlyFavorites.value = !current - } - - fun toggleShowWaypointsOnMap() { - val current = showWaypointsOnMap.value - mapPrefs.showWaypointsOnMap = !current - showWaypointsOnMap.value = !current - } - - fun toggleShowPrecisionCircleOnMap() { - val current = showPrecisionCircleOnMap.value - mapPrefs.showPrecisionCircleOnMap = !current - showPrecisionCircleOnMap.value = !current - } - - fun generatePacketId(): Int? { - return try { - serviceRepository.meshService?.packetId - } catch (ex: RemoteException) { - Logger.e { "RemoteException: ${ex.message}" } - return null - } - } + fun getNodeOrFallback(nodeNum: Int): Node = + nodeRepository.nodeDBbyNum.value[nodeNum] ?: Node(num = nodeNum) fun deleteWaypoint(id: Int) = viewModelScope.launch(Dispatchers.IO) { packetRepository.deleteWaypoint(id) } @@ -178,13 +148,13 @@ abstract class BaseMapViewModel( } private fun sendDataPacket(p: DataPacket) { - try { - serviceRepository.meshService?.send(p) - } catch (ex: RemoteException) { - Logger.e { "Send DataPacket error: ${ex.message}" } + viewModelScope.launch(Dispatchers.IO) { + radioController.sendMessage(p) } } + fun generatePacketId(): Int = radioController.getPacketId() + data class MapFilterState( val onlyFavorites: Boolean, val showWaypoints: Boolean, @@ -258,3 +228,16 @@ fun BaseMapViewModel.tracerouteNodeSelection( nodeLookup = nodesForLookup.associateBy { it.num }, ) } + +enum class LastHeardFilter(val label: StringResource, val seconds: Long) { + Any(Res.string.any, 0L), + OneHour(Res.string.one_hour, 3600L), + EightHours(Res.string.eight_hours, 28800L), + OneDay(Res.string.one_day, 86400L), + TwoDays(Res.string.two_days, 172800L), + ; + + companion object { + fun fromSeconds(seconds: Long): LastHeardFilter = entries.find { it.seconds == seconds } ?: Any + } +} diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt index 2e999a8d61..d7abd44747 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt @@ -37,6 +37,7 @@ import org.meshtastic.core.model.ContactSettings import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.Message import org.meshtastic.core.model.Node +import org.meshtastic.core.model.service.ServiceAction import org.meshtastic.core.prefs.emoji.CustomEmojiPrefs import org.meshtastic.core.prefs.homoglyph.HomoglyphPrefs import org.meshtastic.core.prefs.ui.UiPrefs @@ -44,9 +45,8 @@ import org.meshtastic.core.repository.MeshServiceNotifications 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.core.repository.usecase.SendMessageUseCase -import org.meshtastic.core.service.ServiceAction -import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.ChannelSet import javax.inject.Inject diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModel.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModel.kt index e9ab2a5b97..f39b490032 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModel.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModel.kt @@ -36,7 +36,7 @@ import org.meshtastic.core.model.util.getChannel import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.RadioConfigRepository -import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.ChannelSet import javax.inject.Inject diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt index 90f1e31cee..f127076d3f 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt @@ -31,6 +31,7 @@ import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.database.entity.asDeviceVersion import org.meshtastic.core.model.DeviceVersion import org.meshtastic.core.model.Node +import org.meshtastic.core.model.service.ServiceAction import org.meshtastic.core.navigation.SettingsRoutes import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.administration @@ -41,7 +42,6 @@ import org.meshtastic.core.resources.latest_alpha_firmware import org.meshtastic.core.resources.latest_stable_firmware import org.meshtastic.core.resources.remote_admin import org.meshtastic.core.resources.request_metadata -import org.meshtastic.core.service.ServiceAction import org.meshtastic.core.ui.component.ListItem import org.meshtastic.core.ui.theme.StatusColors.StatusGreen import org.meshtastic.core.ui.theme.StatusColors.StatusOrange diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt index 819df18cea..8d6bb18aeb 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt @@ -34,10 +34,10 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.Node +import org.meshtastic.core.model.service.ServiceAction import org.meshtastic.core.navigation.NodesRoutes +import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.core.resources.UiText -import org.meshtastic.core.service.ServiceAction -import org.meshtastic.core.service.ServiceRepository import org.meshtastic.feature.node.component.NodeMenuAction import org.meshtastic.feature.node.domain.usecase.GetNodeDetailsUseCase import org.meshtastic.feature.node.metrics.EnvironmentMetricsState diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeManagementActions.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeManagementActions.kt index a341238f40..981aee4e4d 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeManagementActions.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeManagementActions.kt @@ -16,14 +16,16 @@ */ package org.meshtastic.feature.node.detail -import android.os.RemoteException import co.touchlab.kermit.Logger import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.jetbrains.compose.resources.getString import org.meshtastic.core.model.Node +import org.meshtastic.core.model.RadioController +import org.meshtastic.core.model.service.ServiceAction import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.favorite import org.meshtastic.core.resources.favorite_add @@ -37,8 +39,6 @@ import org.meshtastic.core.resources.mute_remove import org.meshtastic.core.resources.remove import org.meshtastic.core.resources.remove_node_text import org.meshtastic.core.resources.unmute -import org.meshtastic.core.service.ServiceAction -import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.util.AlertManager import javax.inject.Inject import javax.inject.Singleton @@ -49,6 +49,7 @@ class NodeManagementActions constructor( private val nodeRepository: NodeRepository, private val serviceRepository: ServiceRepository, + private val radioController: RadioController, private val alertManager: AlertManager, ) { fun requestRemoveNode(scope: CoroutineScope, node: Node) { @@ -62,13 +63,9 @@ constructor( fun removeNode(scope: CoroutineScope, nodeNum: Int) { scope.launch(Dispatchers.IO) { Logger.i { "Removing node '$nodeNum'" } - try { - val packetId = serviceRepository.meshService?.packetId ?: return@launch - serviceRepository.meshService?.removeByNodenum(packetId, nodeNum) - nodeRepository.deleteNode(nodeNum) - } catch (ex: RemoteException) { - Logger.e { "Remove node error: ${ex.message}" } - } + val packetId = radioController.getPacketId() + radioController.removeByNodenum(packetId, nodeNum) + nodeRepository.deleteNode(nodeNum) } } @@ -89,11 +86,7 @@ constructor( fun ignoreNode(scope: CoroutineScope, node: Node) { scope.launch(Dispatchers.IO) { - try { - serviceRepository.onServiceAction(ServiceAction.Ignore(node)) - } catch (ex: RemoteException) { - Logger.e(ex) { "Ignore node error" } - } + serviceRepository.onServiceAction(ServiceAction.Ignore(node)) } } @@ -111,11 +104,7 @@ constructor( fun muteNode(scope: CoroutineScope, node: Node) { scope.launch(Dispatchers.IO) { - try { - serviceRepository.onServiceAction(ServiceAction.Mute(node)) - } catch (ex: RemoteException) { - Logger.e(ex) { "Mute node error" } - } + serviceRepository.onServiceAction(ServiceAction.Mute(node)) } } @@ -136,11 +125,7 @@ constructor( fun favoriteNode(scope: CoroutineScope, node: Node) { scope.launch(Dispatchers.IO) { - try { - serviceRepository.onServiceAction(ServiceAction.Favorite(node)) - } catch (ex: RemoteException) { - Logger.e(ex) { "Favorite node error" } - } + serviceRepository.onServiceAction(ServiceAction.Favorite(node)) } } diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt index b404d7262b..38e51602cd 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt @@ -17,11 +17,9 @@ package org.meshtastic.feature.node.list import android.net.Uri -import android.os.RemoteException import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import co.touchlab.kermit.Logger import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -32,10 +30,11 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.launch import org.meshtastic.core.model.Node import org.meshtastic.core.model.NodeSortOption +import org.meshtastic.core.model.RadioController import org.meshtastic.core.model.util.dispatchMeshtasticUri import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.RadioConfigRepository -import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.feature.node.detail.NodeManagementActions import org.meshtastic.feature.node.domain.usecase.GetFilteredNodesUseCase @@ -53,6 +52,7 @@ constructor( private val nodeRepository: NodeRepository, private val radioConfigRepository: RadioConfigRepository, private val serviceRepository: ServiceRepository, + private val radioController: RadioController, val nodeManagementActions: NodeManagementActions, private val getFilteredNodesUseCase: GetFilteredNodesUseCase, val nodeFilterPreferences: NodeFilterPreferences, @@ -154,11 +154,7 @@ constructor( radioConfigRepository.replaceAllSettings(channelSet.settings) val newLoraConfig = channelSet.lora_config if (newLoraConfig != null) { - try { - serviceRepository.meshService?.setConfig(Config(lora = newLoraConfig).encode()) - } catch (ex: RemoteException) { - Logger.e(ex) { "Set config error" } - } + radioController.setLocalConfig(Config(lora = newLoraConfig)) } } diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt index 91a3b9f5a3..5b8dea3b68 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt @@ -55,11 +55,11 @@ import org.meshtastic.core.model.evaluateTracerouteMapAvailability import org.meshtastic.core.model.util.UnitConversions import org.meshtastic.core.navigation.NodesRoutes import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.okay import org.meshtastic.core.resources.traceroute import org.meshtastic.core.resources.view_on_map -import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.util.AlertManager import org.meshtastic.core.ui.util.toMessageRes import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/model/NodeDetailAction.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/model/NodeDetailAction.kt index 58009aa767..1f93a15bae 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/model/NodeDetailAction.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/model/NodeDetailAction.kt @@ -17,8 +17,8 @@ package org.meshtastic.feature.node.model import org.meshtastic.core.model.Node +import org.meshtastic.core.model.service.ServiceAction import org.meshtastic.core.navigation.Route -import org.meshtastic.core.service.ServiceAction import org.meshtastic.feature.node.component.NodeMenuAction import org.meshtastic.proto.Config diff --git a/feature/node/src/test/kotlin/org/meshtastic/feature/node/detail/NodeManagementActionsTest.kt b/feature/node/src/test/kotlin/org/meshtastic/feature/node/detail/NodeManagementActionsTest.kt index cb657999fd..05a0f59180 100644 --- a/feature/node/src/test/kotlin/org/meshtastic/feature/node/detail/NodeManagementActionsTest.kt +++ b/feature/node/src/test/kotlin/org/meshtastic/feature/node/detail/NodeManagementActionsTest.kt @@ -24,8 +24,9 @@ import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Test import org.meshtastic.core.model.Node +import org.meshtastic.core.model.RadioController import org.meshtastic.core.repository.NodeRepository -import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.core.ui.util.AlertManager import org.meshtastic.proto.User @@ -34,6 +35,7 @@ class NodeManagementActionsTest { private val nodeRepository = mockk(relaxed = true) private val serviceRepository = mockk(relaxed = true) + private val radioController = mockk(relaxed = true) private val alertManager = mockk(relaxed = true) private val testDispatcher = StandardTestDispatcher() private val testScope = TestScope(testDispatcher) @@ -42,6 +44,7 @@ class NodeManagementActionsTest { NodeManagementActions( nodeRepository = nodeRepository, serviceRepository = serviceRepository, + radioController = radioController, alertManager = alertManager, ) diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt index 335d4b4804..d1c6cecae4 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt @@ -64,10 +64,10 @@ import org.meshtastic.core.prefs.map.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.core.resources.Res import org.meshtastic.core.resources.UiText import org.meshtastic.core.resources.cant_shutdown -import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.util.getChannelList import org.meshtastic.feature.settings.navigation.ConfigRoute import org.meshtastic.feature.settings.navigation.ModuleRoute diff --git a/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt b/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt index 8ec58eab3c..6f937f3b8d 100644 --- a/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt +++ b/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt @@ -51,7 +51,7 @@ import org.meshtastic.core.prefs.map.MapConsentPrefs import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.RadioConfigRepository -import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.proto.ChannelSet import org.meshtastic.proto.ChannelSettings import org.meshtastic.proto.Config From f2fc3bc740a5b880ec93e981cab4d4369b55960a Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 2 Mar 2026 14:23:04 -0600 Subject: [PATCH 09/20] refactor: extract interfaces and migrate mesh service handlers to core modules This migration moves core business logic out of the app module and into decoupled core repository and data modules: - Extracts interfaces for `MeshDataHandler`, `MeshActionHandler`, `MeshRouter`, `MeshMessageProcessor`, and others into `core:repository`. - Moves concrete implementations to `core:data` under the `manager` package. - Renames `MessageFilterService` to `MessageFilter` and moves it to the core library. - Updates dependency injection modules to bind the new interfaces to their respective implementations. - Refactors `MeshService` and `MeshConnectionManager` to rely on the new repository-level interfaces. Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .../com/geeksville/mesh/ApplicationModule.kt | 2 + .../mesh/service/MeshConnectionManager.kt | 44 ++++--- .../geeksville/mesh/service/MeshService.kt | 13 +- .../geeksville/mesh/service/PacketHandler.kt | 8 +- .../mesh/service/ServiceBroadcasts.kt | 4 +- .../mesh/service/MeshConnectionManagerTest.kt | 17 ++- .../mesh/service/MeshMessageProcessorTest.kt | 123 ----------------- .../mesh/service/MeshServiceBroadcastsTest.kt | 9 +- .../mesh/service/PacketHandlerTest.kt | 13 +- .../core/data/di/RepositoryModule.kt | 64 +++++++++ .../manager/FromRadioPacketHandlerImpl.kt | 31 ++--- .../data/manager/MeshActionHandlerImpl.kt | 79 +++++------ .../data/manager/MeshConfigFlowManagerImpl.kt | 33 +++-- .../data/manager/MeshConfigHandlerImpl.kt | 28 ++-- .../core/data/manager/MeshDataHandlerImpl.kt | 76 +++-------- .../data/manager/MeshMessageProcessorImpl.kt | 60 +++------ .../core/data/manager/MeshRouterImpl.kt | 34 ++--- .../core/data/manager/MessageFilterImpl.kt} | 25 +--- .../manager/FromRadioPacketHandlerImplTest.kt | 17 ++- .../core/data/manager}/MeshDataHandlerTest.kt | 19 ++- .../data/manager/MessageFilterImplTest.kt} | 10 +- .../core/database/DatabaseManager.kt | 2 +- .../core/database/di/DatabaseModule.kt | 48 ++++--- .../core/model/util}/MeshDataMapperTest.kt | 16 +-- .../core/repository/DatabaseManager.kt | 3 + .../core/repository/FromRadioPacketHandler.kt | 24 ++-- .../core/repository/MeshActionHandler.kt | 124 ++++++++++++++++++ .../core/repository/MeshConfigFlowManager.kt | 48 +++++++ .../core/repository/MeshConfigHandler.kt | 48 +++++++ .../core/repository/MeshConnectionManager.kt | 46 +++++++ .../core/repository/MeshDataHandler.kt | 58 ++++++++ .../core/repository/MeshMessageProcessor.kt | 37 ++++++ .../meshtastic/core/repository/MeshRouter.kt | 48 +++++++ .../core/repository/MessageFilter.kt | 36 +++++ .../filter/FilterSettingsViewModel.kt | 8 +- .../filter/FilterSettingsViewModelTest.kt | 19 +-- 36 files changed, 808 insertions(+), 466 deletions(-) delete mode 100644 app/src/test/java/com/geeksville/mesh/service/MeshMessageProcessorTest.kt rename app/src/main/java/com/geeksville/mesh/service/FromRadioPacketHandler.kt => core/data/src/main/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImpl.kt (74%) rename app/src/main/java/com/geeksville/mesh/service/MeshActionHandler.kt => core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImpl.kt (82%) rename app/src/main/java/com/geeksville/mesh/service/MeshConfigFlowManager.kt => core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.kt (89%) rename app/src/main/java/com/geeksville/mesh/service/MeshConfigHandler.kt => core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConfigHandlerImpl.kt (83%) rename app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt => core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt (92%) rename app/src/main/java/com/geeksville/mesh/service/MeshMessageProcessor.kt => core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt (82%) rename app/src/main/java/com/geeksville/mesh/service/MeshRouter.kt => core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshRouterImpl.kt (56%) rename core/{service/src/main/kotlin/org/meshtastic/core/service/filter/MessageFilterService.kt => data/src/main/kotlin/org/meshtastic/core/data/manager/MessageFilterImpl.kt} (68%) rename app/src/test/java/com/geeksville/mesh/service/FromRadioPacketHandlerTest.kt => core/data/src/test/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImplTest.kt (87%) rename {app/src/test/java/com/geeksville/mesh/service => core/data/src/test/kotlin/org/meshtastic/core/data/manager}/MeshDataHandlerTest.kt (91%) rename core/{service/src/test/kotlin/org/meshtastic/core/service/filter/MessageFilterServiceTest.kt => data/src/test/kotlin/org/meshtastic/core/data/manager/MessageFilterImplTest.kt} (92%) rename {app/src/test/java/com/geeksville/mesh/service => core/model/src/androidHostTest/kotlin/org/meshtastic/core/model/util}/MeshDataMapperTest.kt (85%) rename app/src/main/java/com/geeksville/mesh/service/ConnectionStateHandler.kt => core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/FromRadioPacketHandler.kt (53%) create mode 100644 core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshActionHandler.kt create mode 100644 core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshConfigFlowManager.kt create mode 100644 core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshConfigHandler.kt create mode 100644 core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshConnectionManager.kt create mode 100644 core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshDataHandler.kt create mode 100644 core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshMessageProcessor.kt create mode 100644 core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshRouter.kt create mode 100644 core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MessageFilter.kt diff --git a/app/src/main/java/com/geeksville/mesh/ApplicationModule.kt b/app/src/main/java/com/geeksville/mesh/ApplicationModule.kt index 299ab11fae..36f80fbbc9 100644 --- a/app/src/main/java/com/geeksville/mesh/ApplicationModule.kt +++ b/app/src/main/java/com/geeksville/mesh/ApplicationModule.kt @@ -43,6 +43,8 @@ interface ApplicationModule { @Binds fun bindServiceBroadcasts(impl: ServiceBroadcasts): org.meshtastic.core.repository.ServiceBroadcasts + @Binds fun bindMeshConnectionManager(impl: com.geeksville.mesh.service.MeshConnectionManager): org.meshtastic.core.repository.MeshConnectionManager + companion object { @Provides @ProcessLifecycle fun provideProcessLifecycleOwner(): LifecycleOwner = ProcessLifecycleOwner.get() diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt b/app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt index 86e0d4dc1c..a076f5be9c 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt @@ -48,6 +48,7 @@ import org.meshtastic.core.model.TelemetryType import org.meshtastic.core.prefs.ui.UiPrefs import org.meshtastic.core.repository.CommandSender import org.meshtastic.core.repository.HistoryManager +import org.meshtastic.core.repository.MeshConnectionManager import org.meshtastic.core.repository.MeshServiceNotifications import org.meshtastic.core.repository.MqttManager import org.meshtastic.core.repository.NodeManager @@ -56,6 +57,7 @@ import org.meshtastic.core.repository.PacketHandler import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.repository.ServiceBroadcasts +import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.connected import org.meshtastic.core.resources.connecting @@ -81,7 +83,7 @@ class MeshConnectionManager constructor( @ApplicationContext private val context: Context, private val radioInterfaceService: RadioInterfaceService, - private val connectionStateHolder: ConnectionStateHandler, + private val serviceRepository: ServiceRepository, private val serviceBroadcasts: ServiceBroadcasts, private val serviceNotifications: MeshServiceNotifications, private val uiPrefs: UiPrefs, @@ -96,7 +98,7 @@ constructor( private val analytics: PlatformAnalytics, private val packetRepository: PacketRepository, private val workManager: WorkManager, -) { +) : MeshConnectionManager { private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private var sleepTimeout: Job? = null private var locationRequestsJob: Job? = null @@ -104,12 +106,12 @@ constructor( private var connectTimeMsec = 0L @OptIn(FlowPreview::class) - fun start(scope: CoroutineScope) { + override fun start(scope: CoroutineScope) { this.scope = scope radioInterfaceService.connectionState.onEach(::onRadioConnectionState).launchIn(scope) // Ensure notification title and content stay in sync with state changes - connectionStateHolder.connectionState.onEach { updateStatusNotification() }.launchIn(scope) + serviceRepository.connectionState.onEach { updateStatusNotification() }.launchIn(scope) // Kickstart the widget composition. The widget internally uses collectAsState() // and its own sampled StateFlow to drive updates automatically without excessive IPC and recreation. @@ -160,7 +162,7 @@ constructor( } private fun onConnectionChanged(c: ConnectionState) { - val current = connectionStateHolder.connectionState.value + val current = serviceRepository.connectionState.value if (current == c) return // If the transport reports 'Connected', but we are already in the middle of a handshake (Connecting) @@ -177,7 +179,7 @@ constructor( handshakeTimeout = null when (c) { - is ConnectionState.Connecting -> connectionStateHolder.setState(ConnectionState.Connecting) + is ConnectionState.Connecting -> serviceRepository.setConnectionState(ConnectionState.Connecting) is ConnectionState.Connected -> handleConnected() is ConnectionState.DeviceSleep -> handleDeviceSleep() is ConnectionState.Disconnected -> handleDisconnected() @@ -186,8 +188,8 @@ constructor( private fun handleConnected() { // The service state remains 'Connecting' until config is fully loaded - if (connectionStateHolder.connectionState.value != ConnectionState.Connected) { - connectionStateHolder.setState(ConnectionState.Connecting) + if (serviceRepository.connectionState.value != ConnectionState.Connected) { + serviceRepository.setConnectionState(ConnectionState.Connecting) } serviceBroadcasts.broadcastConnection() Logger.i { "Starting mesh handshake (Stage 1)" } @@ -198,12 +200,12 @@ constructor( handshakeTimeout = scope.handledLaunch { delay(HANDSHAKE_TIMEOUT) - if (connectionStateHolder.connectionState.value is ConnectionState.Connecting) { + if (serviceRepository.connectionState.value is ConnectionState.Connecting) { Logger.w { "Handshake stall detected! Retrying Stage 1." } startConfigOnly() // Recursive timeout for one more try delay(HANDSHAKE_TIMEOUT) - if (connectionStateHolder.connectionState.value is ConnectionState.Connecting) { + if (serviceRepository.connectionState.value is ConnectionState.Connecting) { Logger.e { "Handshake still stalled after retry. Resetting connection." } onConnectionChanged(ConnectionState.Disconnected) } @@ -212,7 +214,7 @@ constructor( } private fun handleDeviceSleep() { - connectionStateHolder.setState(ConnectionState.DeviceSleep) + serviceRepository.setConnectionState(ConnectionState.DeviceSleep) packetHandler.stopPacketQueue() locationManager.stop() mqttManager.stop() @@ -245,7 +247,7 @@ constructor( } private fun handleDisconnected() { - connectionStateHolder.setState(ConnectionState.Disconnected) + serviceRepository.setConnectionState(ConnectionState.Disconnected) packetHandler.stopPacketQueue() locationManager.stop() mqttManager.stop() @@ -260,15 +262,15 @@ constructor( serviceBroadcasts.broadcastConnection() } - fun startConfigOnly() { + override fun startConfigOnly() { packetHandler.sendToRadio(ToRadio(want_config_id = CONFIG_ONLY_NONCE)) } - fun startNodeInfoOnly() { + override fun startNodeInfoOnly() { packetHandler.sendToRadio(ToRadio(want_config_id = NODE_INFO_NONCE)) } - fun onRadioConfigLoaded() { + override fun onRadioConfigLoaded() { scope.handledLaunch { val queuedPackets = packetRepository.getQueuedPackets() ?: emptyList() queuedPackets.forEach { packet -> @@ -293,7 +295,7 @@ constructor( commandSender.sendAdmin(myNodeNum) { AdminMessage(set_time_only = nowSeconds.toInt()) } } - fun onNodeDbReady() { + override fun onNodeDbReady() { handshakeTimeout?.cancel() handshakeTimeout = null @@ -334,14 +336,14 @@ constructor( ) } - fun updateTelemetry(telemetry: Telemetry) { - telemetry.local_stats?.let { nodeRepository.updateLocalStats(it) } - updateStatusNotification(telemetry) + override fun updateTelemetry(t: Telemetry) { + t.local_stats?.let { nodeRepository.updateLocalStats(it) } + updateStatusNotification(t) } - fun updateStatusNotification(telemetry: Telemetry? = null): Notification { + override fun updateStatusNotification(telemetry: Telemetry?): Notification { val summary = - when (connectionStateHolder.connectionState.value) { + when (serviceRepository.connectionState.value) { is ConnectionState.Connected -> getString(Res.string.meshtastic_app_name) + ": " + getString(Res.string.connected) is ConnectionState.Disconnected -> getString(Res.string.disconnected) diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt index 4fa86b3f35..82cea8ebc9 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -43,13 +43,16 @@ import org.meshtastic.core.model.MyNodeInfo import org.meshtastic.core.model.NodeInfo import org.meshtastic.core.model.Position import org.meshtastic.core.repository.CommandSender +import org.meshtastic.core.repository.MeshConnectionManager +import org.meshtastic.core.repository.MeshMessageProcessor +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.SERVICE_NOTIFY_ID import org.meshtastic.core.repository.ServiceBroadcasts -import org.meshtastic.core.service.AndroidServiceRepository +import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.core.service.IMeshService import org.meshtastic.proto.PortNum import javax.inject.Inject @@ -60,9 +63,7 @@ class MeshService : Service() { @Inject lateinit var radioInterfaceService: RadioInterfaceService - @Inject lateinit var serviceRepository: AndroidServiceRepository - - @Inject lateinit var connectionStateHolder: ConnectionStateHandler + @Inject lateinit var serviceRepository: ServiceRepository @Inject lateinit var packetHandler: PacketHandler @@ -147,7 +148,7 @@ class MeshService : Service() { val a = radioInterfaceService.getDeviceAddress() val wantForeground = a != null && a != NO_DEVICE_SELECTED - val notification = connectionManager.updateStatusNotification() + val notification = connectionManager.updateStatusNotification() as android.app.Notification val foregroundServiceType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { @@ -315,7 +316,7 @@ class MeshService : Service() { override fun getNodes(): List = nodeManager.getNodes() - override fun connectionState(): String = connectionStateHolder.connectionState.value.toString() + override fun connectionState(): String = serviceRepository.connectionState.value.toString() override fun startProvideLocation() { locationManager.start(serviceScope) { commandSender.sendPosition(it) } diff --git a/app/src/main/java/com/geeksville/mesh/service/PacketHandler.kt b/app/src/main/java/com/geeksville/mesh/service/PacketHandler.kt index 5b10251a66..644496c5c1 100644 --- a/app/src/main/java/com/geeksville/mesh/service/PacketHandler.kt +++ b/app/src/main/java/com/geeksville/mesh/service/PacketHandler.kt @@ -38,6 +38,7 @@ import org.meshtastic.core.model.util.toOneLineString import org.meshtastic.core.model.util.toPIIString import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.ServiceBroadcasts +import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.proto.FromRadio import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.QueueStatus @@ -60,7 +61,7 @@ constructor( private val serviceBroadcasts: ServiceBroadcasts, private val radioInterfaceService: RadioInterfaceService, private val meshLogRepository: Lazy, - private val connectionStateHolder: ConnectionStateHandler, + private val serviceRepository: ServiceRepository, ) : SharedPacketHandler { companion object { @@ -140,13 +141,12 @@ constructor( queueResponse.remove(dataRequestId)?.complete(complete) } - @Suppress("TooGenericExceptionCaught", "SwallowedException") private fun startPacketQueue() { if (queueJob?.isActive == true) return queueJob = scope.handledLaunch { Logger.d { "packet queueJob started" } - while (connectionStateHolder.connectionState.value == ConnectionState.Connected) { + while (serviceRepository.connectionState.value == ConnectionState.Connected) { // take the first packet from the queue head val packet = queuedPackets.poll() ?: break try { @@ -194,7 +194,7 @@ constructor( val deferred = CompletableDeferred() queueResponse[packet.id] = deferred try { - if (connectionStateHolder.connectionState.value != ConnectionState.Connected) { + if (serviceRepository.connectionState.value != ConnectionState.Connected) { throw RadioNotConnectedException() } sendToRadio(ToRadio(packet = packet)) diff --git a/app/src/main/java/com/geeksville/mesh/service/ServiceBroadcasts.kt b/app/src/main/java/com/geeksville/mesh/service/ServiceBroadcasts.kt index 9381f10f7c..5f8be95452 100644 --- a/app/src/main/java/com/geeksville/mesh/service/ServiceBroadcasts.kt +++ b/app/src/main/java/com/geeksville/mesh/service/ServiceBroadcasts.kt @@ -38,7 +38,6 @@ class ServiceBroadcasts @Inject constructor( @ApplicationContext private val context: Context, - private val connectionStateHolder: ConnectionStateHandler, private val serviceRepository: ServiceRepository, ) : SharedServiceBroadcasts { // A mapping of receiver class name to package name - used for explicit broadcasts @@ -124,13 +123,12 @@ constructor( /** Broadcast our current connection status */ override fun broadcastConnection() { - val connectionState = connectionStateHolder.connectionState.value + val connectionState = serviceRepository.connectionState.value // ATAK expects a String: "CONNECTED" or "DISCONNECTED" // It uses equalsIgnoreCase, but we'll use uppercase to be specific. val stateStr = connectionState.toString().uppercase(Locale.ROOT) val intent = Intent(ACTION_MESH_CONNECTED).apply { putExtra(EXTRA_CONNECTED, stateStr) } - serviceRepository.setConnectionState(connectionState) explicitBroadcast(intent) if (connectionState == ConnectionState.Disconnected) { diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshConnectionManagerTest.kt b/app/src/test/java/com/geeksville/mesh/service/MeshConnectionManagerTest.kt index 213fcc6ad0..8b1ae7bf83 100644 --- a/app/src/test/java/com/geeksville/mesh/service/MeshConnectionManagerTest.kt +++ b/app/src/test/java/com/geeksville/mesh/service/MeshConnectionManagerTest.kt @@ -53,6 +53,8 @@ import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.RadioConfigRepository +import org.meshtastic.core.repository.ServiceBroadcasts +import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.feature.messaging.domain.worker.SendMessageWorker import org.meshtastic.proto.Config import org.meshtastic.proto.LocalConfig @@ -65,7 +67,7 @@ class MeshConnectionManagerTest { private val context: Context = mockk(relaxed = true) private val radioInterfaceService: RadioInterfaceService = mockk(relaxed = true) - private val connectionStateHolder = ConnectionStateHandler() + private val serviceRepository: ServiceRepository = mockk(relaxed = true) private val serviceBroadcasts: ServiceBroadcasts = mockk(relaxed = true) private val serviceNotifications: MeshServiceNotifications = mockk(relaxed = true) private val uiPrefs: UiPrefs = mockk(relaxed = true) @@ -81,6 +83,7 @@ class MeshConnectionManagerTest { private val packetRepository: PacketRepository = mockk(relaxed = true) private val workManager: WorkManager = mockk(relaxed = true) private val radioConnectionState = MutableStateFlow(ConnectionState.Disconnected) + private val connectionStateFlow = MutableStateFlow(ConnectionState.Disconnected) private val localConfigFlow = MutableStateFlow(LocalConfig()) private val moduleConfigFlow = MutableStateFlow(LocalModuleConfig()) @@ -102,13 +105,15 @@ class MeshConnectionManagerTest { every { nodeRepository.myNodeInfo } returns MutableStateFlow(null) every { nodeRepository.ourNodeInfo } returns MutableStateFlow(null) every { nodeRepository.localStats } returns MutableStateFlow(LocalStats()) + every { serviceRepository.connectionState } returns connectionStateFlow + every { serviceRepository.setConnectionState(any()) } answers { connectionStateFlow.value = firstArg() } every { serviceNotifications.updateServiceStateNotification(any(), any()) } returns mockk(relaxed = true) manager = MeshConnectionManager( context, radioInterfaceService, - connectionStateHolder, + serviceRepository, serviceBroadcasts, serviceNotifications, uiPrefs, @@ -141,7 +146,7 @@ class MeshConnectionManagerTest { assertEquals( "State should be Connecting after radio Connected", ConnectionState.Connecting, - connectionStateHolder.connectionState.value, + serviceRepository.connectionState.value, ) verify { serviceBroadcasts.broadcastConnection() } verify { packetHandler.sendToRadio(any()) } @@ -160,7 +165,7 @@ class MeshConnectionManagerTest { assertEquals( "State should be Disconnected after radio Disconnected", ConnectionState.Disconnected, - connectionStateHolder.connectionState.value, + serviceRepository.connectionState.value, ) verify { packetHandler.stopPacketQueue() } verify { locationManager.stop() } @@ -186,7 +191,7 @@ class MeshConnectionManagerTest { assertEquals( "State should be Disconnected when power saving is off", ConnectionState.Disconnected, - connectionStateHolder.connectionState.value, + serviceRepository.connectionState.value, ) } @@ -205,7 +210,7 @@ class MeshConnectionManagerTest { assertEquals( "State should stay in DeviceSleep when power saving is on", ConnectionState.DeviceSleep, - connectionStateHolder.connectionState.value, + serviceRepository.connectionState.value, ) } diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshMessageProcessorTest.kt b/app/src/test/java/com/geeksville/mesh/service/MeshMessageProcessorTest.kt deleted file mode 100644 index c162b10b3c..0000000000 --- a/app/src/test/java/com/geeksville/mesh/service/MeshMessageProcessorTest.kt +++ /dev/null @@ -1,123 +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 com.geeksville.mesh.service - -import io.mockk.coVerify -import io.mockk.every -import io.mockk.mockk -import io.mockk.verify -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test -import org.meshtastic.core.data.repository.MeshLogRepository -import org.meshtastic.core.database.entity.MeshLog -import org.meshtastic.core.repository.NodeManager -import org.meshtastic.core.repository.ServiceRepository -import org.meshtastic.proto.Data -import org.meshtastic.proto.MeshPacket -import org.meshtastic.proto.PortNum - -class MeshMessageProcessorTest { - - private val nodeManager: NodeManager = mockk(relaxed = true) - private val serviceRepository: ServiceRepository = mockk(relaxed = true) - private val meshLogRepository: MeshLogRepository = mockk(relaxed = true) - private val router: MeshRouter = mockk(relaxed = true) - private val fromRadioDispatcher: FromRadioPacketHandler = mockk(relaxed = true) - private val meshLogRepositoryLazy = dagger.Lazy { meshLogRepository } - private val dataHandler: MeshDataHandler = mockk(relaxed = true) - - private val isNodeDbReady = MutableStateFlow(false) - private val testDispatcher = StandardTestDispatcher() - private val testScope = TestScope(testDispatcher) - - private lateinit var processor: MeshMessageProcessor - - @Before - fun setUp() { - every { nodeManager.isNodeDbReady } returns isNodeDbReady - every { router.dataHandler } returns dataHandler - processor = - MeshMessageProcessor(nodeManager, serviceRepository, meshLogRepositoryLazy, router, fromRadioDispatcher) - processor.start(testScope) - } - - @Test - fun `early packets are buffered and flushed when DB is ready`() = runTest(testDispatcher) { - val packet = MeshPacket(id = 123, decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP)) - - // 1. Database is NOT ready - isNodeDbReady.value = false - testScheduler.runCurrent() // trigger start() onEach - - processor.handleReceivedMeshPacket(packet, 999) - - // Verify that handleReceivedData has NOT been called yet - verify(exactly = 0) { dataHandler.handleReceivedData(any(), any(), any(), any()) } - - // 2. Database becomes ready - isNodeDbReady.value = true - testScheduler.runCurrent() // trigger onEach(true) - - // Verify that handleReceivedData is now called with the buffered packet - verify(exactly = 1) { dataHandler.handleReceivedData(match { it.id == 123 }, any(), any(), any()) } - } - - @Test - fun `packets are processed immediately if DB is already ready`() = runTest(testDispatcher) { - val packet = MeshPacket(id = 456, decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP)) - - isNodeDbReady.value = true - testScheduler.runCurrent() - - processor.handleReceivedMeshPacket(packet, 999) - - verify(exactly = 1) { dataHandler.handleReceivedData(match { it.id == 456 }, any(), any(), any()) } - } - - @Test - fun `packets from local node are logged with NODE_NUM_LOCAL`() = runTest(testDispatcher) { - val myNodeNum = 1234 - val packet = MeshPacket(from = myNodeNum, id = 789, decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP)) - - isNodeDbReady.value = true - testScheduler.runCurrent() - - processor.handleReceivedMeshPacket(packet, myNodeNum) - testScheduler.runCurrent() // wait for log insert job - - coVerify { meshLogRepository.insert(match { log -> log.fromNum == MeshLog.NODE_NUM_LOCAL }) } - } - - @Test - fun `packets from remote nodes are logged with their node number`() = runTest(testDispatcher) { - val myNodeNum = 1234 - val remoteNodeNum = 5678 - val packet = MeshPacket(from = remoteNodeNum, id = 789, decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP)) - - isNodeDbReady.value = true - testScheduler.runCurrent() - - processor.handleReceivedMeshPacket(packet, myNodeNum) - testScheduler.runCurrent() - - coVerify { meshLogRepository.insert(match { log -> log.fromNum == remoteNodeNum }) } - } -} diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshServiceBroadcastsTest.kt b/app/src/test/java/com/geeksville/mesh/service/MeshServiceBroadcastsTest.kt index c1b5ace1d0..3ddfecd61b 100644 --- a/app/src/test/java/com/geeksville/mesh/service/MeshServiceBroadcastsTest.kt +++ b/app/src/test/java/com/geeksville/mesh/service/MeshServiceBroadcastsTest.kt @@ -19,7 +19,9 @@ package com.geeksville.mesh.service import android.app.Application import android.content.Context import androidx.test.core.app.ApplicationProvider +import io.mockk.every import io.mockk.mockk +import kotlinx.coroutines.flow.MutableStateFlow import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test @@ -35,19 +37,18 @@ import org.robolectric.annotation.Config class ServiceBroadcastsTest { private lateinit var context: Context - private val connectionStateHolder = ConnectionStateHandler() private val serviceRepository: ServiceRepository = mockk(relaxed = true) private lateinit var broadcasts: ServiceBroadcasts @Before fun setUp() { context = ApplicationProvider.getApplicationContext() - broadcasts = ServiceBroadcasts(context, connectionStateHolder, serviceRepository) + broadcasts = ServiceBroadcasts(context, serviceRepository) } @Test fun `broadcastConnection sends uppercase state string for ATAK`() { - connectionStateHolder.setState(ConnectionState.Connected) + every { serviceRepository.connectionState } returns MutableStateFlow(ConnectionState.Connected) broadcasts.broadcastConnection() @@ -58,7 +59,7 @@ class ServiceBroadcastsTest { @Test fun `broadcastConnection sends legacy connection intent`() { - connectionStateHolder.setState(ConnectionState.Connected) + every { serviceRepository.connectionState } returns MutableStateFlow(ConnectionState.Connected) broadcasts.broadcastConnection() diff --git a/app/src/test/java/com/geeksville/mesh/service/PacketHandlerTest.kt b/app/src/test/java/com/geeksville/mesh/service/PacketHandlerTest.kt index 62209ef61f..f3fd69302a 100644 --- a/app/src/test/java/com/geeksville/mesh/service/PacketHandlerTest.kt +++ b/app/src/test/java/com/geeksville/mesh/service/PacketHandlerTest.kt @@ -31,6 +31,7 @@ import org.meshtastic.core.data.repository.MeshLogRepository import org.meshtastic.core.database.entity.MeshLog import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.repository.PacketRepository +import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.proto.Data import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.PortNum @@ -43,7 +44,8 @@ class PacketHandlerTest { private val serviceBroadcasts: ServiceBroadcasts = mockk(relaxed = true) private val radioInterfaceService: RadioInterfaceService = mockk(relaxed = true) private val meshLogRepository: MeshLogRepository = mockk(relaxed = true) - private val connectionStateHolder: ConnectionStateHandler = mockk(relaxed = true) + private val serviceRepository: ServiceRepository = mockk(relaxed = true) + private val connectionStateFlow = MutableStateFlow(ConnectionState.Disconnected) private val testDispatcher = StandardTestDispatcher() private val testScope = TestScope(testDispatcher) @@ -52,13 +54,16 @@ class PacketHandlerTest { @Before fun setUp() { + every { serviceRepository.connectionState } returns connectionStateFlow + every { serviceRepository.setConnectionState(any()) } answers { connectionStateFlow.value = firstArg() } + handler = PacketHandler( dagger.Lazy { packetRepository }, serviceBroadcasts, radioInterfaceService, dagger.Lazy { meshLogRepository }, - connectionStateHolder, + serviceRepository, ) handler.start(testScope) } @@ -75,7 +80,7 @@ class PacketHandlerTest { @Test fun `sendToRadio with MeshPacket queues and sends when connected`() = runTest(testDispatcher) { val packet = MeshPacket(id = 456) - every { connectionStateHolder.connectionState } returns MutableStateFlow(ConnectionState.Connected) + every { serviceRepository.connectionState } returns MutableStateFlow(ConnectionState.Connected) handler.sendToRadio(packet) testScheduler.runCurrent() @@ -86,7 +91,7 @@ class PacketHandlerTest { @Test fun `handleQueueStatus completes deferred`() = runTest(testDispatcher) { val packet = MeshPacket(id = 789) - every { connectionStateHolder.connectionState } returns MutableStateFlow(ConnectionState.Connected) + every { serviceRepository.connectionState } returns MutableStateFlow(ConnectionState.Connected) handler.sendToRadio(packet) testScheduler.runCurrent() diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt index 45a39399dc..5d260ab541 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt @@ -23,7 +23,15 @@ 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.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 @@ -35,7 +43,15 @@ 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.MeshActionHandler +import org.meshtastic.core.repository.MeshConfigFlowManager +import org.meshtastic.core.repository.MeshConfigHandler +import org.meshtastic.core.repository.MeshDataHandler +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 @@ -109,6 +125,54 @@ abstract class RepositoryModule { mqttManagerImpl: MqttManagerImpl ): MqttManager + @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 diff --git a/app/src/main/java/com/geeksville/mesh/service/FromRadioPacketHandler.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImpl.kt similarity index 74% rename from app/src/main/java/com/geeksville/mesh/service/FromRadioPacketHandler.kt rename to core/data/src/main/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImpl.kt index b2c7ead787..668d254f31 100644 --- a/app/src/main/java/com/geeksville/mesh/service/FromRadioPacketHandler.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImpl.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 @@ -14,9 +14,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.geeksville.mesh.service +package org.meshtastic.core.data.manager -import co.touchlab.kermit.Logger +import org.meshtastic.core.repository.FromRadioPacketHandler +import org.meshtastic.core.repository.MeshRouter import org.meshtastic.core.repository.MeshServiceNotifications import org.meshtastic.core.repository.MqttManager import org.meshtastic.core.repository.PacketHandler @@ -26,21 +27,18 @@ import javax.inject.Inject import javax.inject.Singleton /** - * Dispatches non-packet [FromRadio] variants to their respective handlers. This class is stateless and handles routing - * for config, metadata, and specialized system messages. + * Implementation of [FromRadioPacketHandler] that dispatches [FromRadio] variants to specialized handlers. */ @Singleton -class FromRadioPacketHandler -@Inject -constructor( +class FromRadioPacketHandlerImpl @Inject constructor( private val serviceRepository: ServiceRepository, private val router: MeshRouter, private val mqttManager: MqttManager, private val packetHandler: PacketHandler, private val serviceNotifications: MeshServiceNotifications, -) { +) : FromRadioPacketHandler { @Suppress("CyclomaticComplexMethod") - fun handleFromRadio(proto: FromRadio) { + override fun handleFromRadio(proto: FromRadio) { val myInfo = proto.my_info val metadata = proto.metadata val nodeInfo = proto.node_info @@ -68,19 +66,8 @@ constructor( clientNotification != null -> { serviceRepository.setClientNotification(clientNotification) serviceNotifications.showClientNotification(clientNotification) - packetHandler.removeResponse(clientNotification.reply_id ?: 0, complete = false) + packetHandler.removeResponse(0, complete = false) } - // Logging-only variants are handled by MeshMessageProcessor before dispatching here - proto.packet != null || - proto.log_record != null || - proto.rebooted != null || - proto.xmodemPacket != null || - proto.deviceuiConfig != null || - proto.fileInfo != null -> { - /* No specialized routing needed here */ - } - - else -> Logger.d { "Dispatcher ignoring FromRadio variant" } } } } diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshActionHandler.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImpl.kt similarity index 82% rename from app/src/main/java/com/geeksville/mesh/service/MeshActionHandler.kt rename to core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImpl.kt index 8fe6597f6e..2090fd937a 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshActionHandler.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImpl.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.geeksville.mesh.service +package org.meshtastic.core.data.manager import dagger.Lazy import kotlinx.coroutines.CoroutineScope @@ -26,7 +26,6 @@ import org.meshtastic.core.analytics.platform.PlatformAnalytics import org.meshtastic.core.common.util.handledLaunch import org.meshtastic.core.common.util.ignoreException import org.meshtastic.core.common.util.nowMillis -import org.meshtastic.core.database.DatabaseManager import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.MeshUser import org.meshtastic.core.model.MessageStatus @@ -35,6 +34,10 @@ import org.meshtastic.core.model.Reaction import org.meshtastic.core.model.service.ServiceAction import org.meshtastic.core.prefs.mesh.MeshPrefs import org.meshtastic.core.repository.CommandSender +import org.meshtastic.core.repository.DatabaseManager +import org.meshtastic.core.repository.MeshActionHandler +import org.meshtastic.core.repository.MeshDataHandler +import org.meshtastic.core.repository.MeshMessageProcessor import org.meshtastic.core.repository.MeshServiceNotifications import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.PacketRepository @@ -51,9 +54,7 @@ import javax.inject.Singleton @Suppress("LongParameterList", "TooManyFunctions", "CyclomaticComplexMethod") @Singleton -class MeshActionHandler -@Inject -constructor( +class MeshActionHandlerImpl @Inject constructor( private val nodeManager: NodeManager, private val commandSender: CommandSender, private val packetRepository: Lazy, @@ -64,10 +65,10 @@ constructor( private val databaseManager: DatabaseManager, private val serviceNotifications: MeshServiceNotifications, private val messageProcessor: Lazy, -) { +) : MeshActionHandler { private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - fun start(scope: CoroutineScope) { + override fun start(scope: CoroutineScope) { this.scope = scope } @@ -76,7 +77,7 @@ constructor( private const val EMOJI_INDICATOR = 1 } - fun onServiceAction(action: ServiceAction) { + override fun onServiceAction(action: ServiceAction) { ignoreException { val myNodeNum = nodeManager.myNodeNum ?: return@ignoreException when (action) { @@ -178,13 +179,13 @@ constructor( } } - fun handleSetOwner(u: MeshUser, myNodeNum: Int) { + override fun handleSetOwner(u: MeshUser, myNodeNum: Int) { val newUser = User(id = u.id, long_name = u.longName, short_name = u.shortName, is_licensed = u.isLicensed) commandSender.sendAdmin(myNodeNum) { AdminMessage(set_owner = newUser) } nodeManager.handleReceivedUser(myNodeNum, newUser) } - fun handleSend(p: DataPacket, myNodeNum: Int) { + override fun handleSend(p: DataPacket, myNodeNum: Int) { commandSender.sendData(p) serviceBroadcasts.broadcastMessageStatus(p.id, p.status ?: MessageStatus.UNKNOWN) dataHandler.rememberDataPacket(p, myNodeNum, false) @@ -192,7 +193,7 @@ constructor( analytics.track("data_send", DataPair("num_bytes", bytes.size), DataPair("type", p.dataType)) } - fun handleRequestPosition(destNum: Int, position: Position, myNodeNum: Int) { + override fun handleRequestPosition(destNum: Int, position: Position, myNodeNum: Int) { if (destNum != myNodeNum) { val provideLocation = meshPrefs.shouldProvideNodeLocation(myNodeNum) val currentPosition = @@ -205,32 +206,32 @@ constructor( } } - fun handleRemoveByNodenum(nodeNum: Int, requestId: Int, myNodeNum: Int) { + override fun handleRemoveByNodenum(nodeNum: Int, requestId: Int, myNodeNum: Int) { nodeManager.removeByNodenum(nodeNum) commandSender.sendAdmin(myNodeNum, requestId) { AdminMessage(remove_by_nodenum = nodeNum) } } - fun handleSetRemoteOwner(id: Int, destNum: Int, payload: ByteArray) { + override fun handleSetRemoteOwner(id: Int, destNum: Int, payload: ByteArray) { val u = User.ADAPTER.decode(payload) commandSender.sendAdmin(destNum, id) { AdminMessage(set_owner = u) } nodeManager.handleReceivedUser(destNum, u) } - fun handleGetRemoteOwner(id: Int, destNum: Int) { + override fun handleGetRemoteOwner(id: Int, destNum: Int) { commandSender.sendAdmin(destNum, id, wantResponse = true) { AdminMessage(get_owner_request = true) } } - fun handleSetConfig(payload: ByteArray, myNodeNum: Int) { + override fun handleSetConfig(payload: ByteArray, myNodeNum: Int) { val c = Config.ADAPTER.decode(payload) commandSender.sendAdmin(myNodeNum) { AdminMessage(set_config = c) } } - fun handleSetRemoteConfig(id: Int, destNum: Int, payload: ByteArray) { + override fun handleSetRemoteConfig(id: Int, destNum: Int, payload: ByteArray) { val c = Config.ADAPTER.decode(payload) commandSender.sendAdmin(destNum, id) { AdminMessage(set_config = c) } } - fun handleGetRemoteConfig(id: Int, destNum: Int, config: Int) { + override fun handleGetRemoteConfig(id: Int, destNum: Int, config: Int) { commandSender.sendAdmin(destNum, id, wantResponse = true) { if (config == AdminMessage.ConfigType.SESSIONKEY_CONFIG.value) { AdminMessage(get_device_metadata_request = true) @@ -240,104 +241,104 @@ constructor( } } - fun handleSetModuleConfig(id: Int, destNum: Int, payload: ByteArray) { + override fun handleSetModuleConfig(id: Int, destNum: Int, payload: ByteArray) { val c = ModuleConfig.ADAPTER.decode(payload) commandSender.sendAdmin(destNum, id) { AdminMessage(set_module_config = c) } c.statusmessage?.let { sm -> nodeManager.updateNodeStatus(destNum, sm.node_status) } } - fun handleGetModuleConfig(id: Int, destNum: Int, config: Int) { + override fun handleGetModuleConfig(id: Int, destNum: Int, config: Int) { commandSender.sendAdmin(destNum, id, wantResponse = true) { AdminMessage(get_module_config_request = AdminMessage.ModuleConfigType.fromValue(config)) } } - fun handleSetRingtone(destNum: Int, ringtone: String) { + override fun handleSetRingtone(destNum: Int, ringtone: String) { commandSender.sendAdmin(destNum) { AdminMessage(set_ringtone_message = ringtone) } } - fun handleGetRingtone(id: Int, destNum: Int) { + override fun handleGetRingtone(id: Int, destNum: Int) { commandSender.sendAdmin(destNum, id, wantResponse = true) { AdminMessage(get_ringtone_request = true) } } - fun handleSetCannedMessages(destNum: Int, messages: String) { + override fun handleSetCannedMessages(destNum: Int, messages: String) { commandSender.sendAdmin(destNum) { AdminMessage(set_canned_message_module_messages = messages) } } - fun handleGetCannedMessages(id: Int, destNum: Int) { + override fun handleGetCannedMessages(id: Int, destNum: Int) { commandSender.sendAdmin(destNum, id, wantResponse = true) { AdminMessage(get_canned_message_module_messages_request = true) } } - fun handleSetChannel(payload: ByteArray?, myNodeNum: Int) { + override fun handleSetChannel(payload: ByteArray?, myNodeNum: Int) { if (payload != null) { val c = Channel.ADAPTER.decode(payload) commandSender.sendAdmin(myNodeNum) { AdminMessage(set_channel = c) } } } - fun handleSetRemoteChannel(id: Int, destNum: Int, payload: ByteArray?) { + override fun handleSetRemoteChannel(id: Int, destNum: Int, payload: ByteArray?) { if (payload != null) { val c = Channel.ADAPTER.decode(payload) commandSender.sendAdmin(destNum, id) { AdminMessage(set_channel = c) } } } - fun handleGetRemoteChannel(id: Int, destNum: Int, index: Int) { + override fun handleGetRemoteChannel(id: Int, destNum: Int, index: Int) { commandSender.sendAdmin(destNum, id, wantResponse = true) { AdminMessage(get_channel_request = index + 1) } } - fun handleRequestNeighborInfo(requestId: Int, destNum: Int) { + override fun handleRequestNeighborInfo(requestId: Int, destNum: Int) { commandSender.requestNeighborInfo(requestId, destNum) } - fun handleBeginEditSettings(destNum: Int) { + override fun handleBeginEditSettings(destNum: Int) { commandSender.sendAdmin(destNum) { AdminMessage(begin_edit_settings = true) } } - fun handleCommitEditSettings(destNum: Int) { + override fun handleCommitEditSettings(destNum: Int) { commandSender.sendAdmin(destNum) { AdminMessage(commit_edit_settings = true) } } - fun handleRebootToDfu(destNum: Int) { + override fun handleRebootToDfu(destNum: Int) { commandSender.sendAdmin(destNum) { AdminMessage(enter_dfu_mode_request = true) } } - fun handleRequestTelemetry(requestId: Int, destNum: Int, type: Int) { + override fun handleRequestTelemetry(requestId: Int, destNum: Int, type: Int) { commandSender.requestTelemetry(requestId, destNum, type) } - fun handleRequestShutdown(requestId: Int, destNum: Int) { + override fun handleRequestShutdown(requestId: Int, destNum: Int) { commandSender.sendAdmin(destNum, requestId) { AdminMessage(shutdown_seconds = DEFAULT_REBOOT_DELAY) } } - fun handleRequestReboot(requestId: Int, destNum: Int) { + override fun handleRequestReboot(requestId: Int, destNum: Int) { commandSender.sendAdmin(destNum, requestId) { AdminMessage(reboot_seconds = DEFAULT_REBOOT_DELAY) } } - fun handleRequestRebootOta(requestId: Int, destNum: Int, mode: Int, hash: ByteArray?) { + override fun handleRequestRebootOta(requestId: Int, destNum: Int, mode: Int, hash: ByteArray?) { val otaMode = OTAMode.fromValue(mode) ?: OTAMode.NO_REBOOT_OTA val otaEvent = AdminMessage.OTAEvent(reboot_ota_mode = otaMode, ota_hash = hash?.toByteString() ?: okio.ByteString.EMPTY) commandSender.sendAdmin(destNum, requestId) { AdminMessage(ota_request = otaEvent) } } - fun handleRequestFactoryReset(requestId: Int, destNum: Int) { + override fun handleRequestFactoryReset(requestId: Int, destNum: Int) { commandSender.sendAdmin(destNum, requestId) { AdminMessage(factory_reset_device = 1) } } - fun handleRequestNodedbReset(requestId: Int, destNum: Int, preserveFavorites: Boolean) { + override fun handleRequestNodedbReset(requestId: Int, destNum: Int, preserveFavorites: Boolean) { commandSender.sendAdmin(destNum, requestId) { AdminMessage(nodedb_reset = preserveFavorites) } } - fun handleGetDeviceConnectionStatus(requestId: Int, destNum: Int) { + override fun handleGetDeviceConnectionStatus(requestId: Int, destNum: Int) { commandSender.sendAdmin(destNum, requestId, wantResponse = true) { AdminMessage(get_device_connection_status_request = true) } } - fun handleUpdateLastAddress(deviceAddr: String?) { + override fun handleUpdateLastAddress(deviceAddr: String?) { val currentAddr = meshPrefs.deviceAddress if (deviceAddr != currentAddr) { meshPrefs.deviceAddress = deviceAddr diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshConfigFlowManager.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.kt similarity index 89% rename from app/src/main/java/com/geeksville/mesh/service/MeshConfigFlowManager.kt rename to core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.kt index 316b0305fc..a2d3d236d4 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshConfigFlowManager.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.geeksville.mesh.service +package org.meshtastic.core.data.manager import co.touchlab.kermit.Logger import kotlinx.coroutines.CoroutineScope @@ -25,11 +25,14 @@ import org.meshtastic.core.analytics.platform.PlatformAnalytics import org.meshtastic.core.common.util.handledLaunch import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.repository.CommandSender +import org.meshtastic.core.repository.MeshConfigFlowManager +import org.meshtastic.core.repository.MeshConnectionManager import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.PacketHandler import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.repository.ServiceBroadcasts +import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.proto.DeviceMetadata import org.meshtastic.proto.HardwareModel import org.meshtastic.proto.Heartbeat @@ -43,30 +46,28 @@ import org.meshtastic.proto.MyNodeInfo as ProtoMyNodeInfo @Suppress("LongParameterList") @Singleton -class MeshConfigFlowManager -@Inject -constructor( +class MeshConfigFlowManagerImpl @Inject constructor( private val nodeManager: NodeManager, private val connectionManager: MeshConnectionManager, private val nodeRepository: NodeRepository, private val radioConfigRepository: RadioConfigRepository, - private val connectionStateHolder: ConnectionStateHandler, + private val serviceRepository: ServiceRepository, private val serviceBroadcasts: ServiceBroadcasts, private val analytics: PlatformAnalytics, private val commandSender: CommandSender, private val packetHandler: PacketHandler, -) { +) : MeshConfigFlowManager { private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val configOnlyNonce = 69420 private val nodeInfoNonce = 69421 private val wantConfigDelay = 100L - fun start(scope: CoroutineScope) { + override fun start(scope: CoroutineScope) { this.scope = scope } private val newNodes = mutableListOf() - val newNodeCount: Int + override val newNodeCount: Int get() = newNodes.size private var rawMyNodeInfo: ProtoMyNodeInfo? = null @@ -74,7 +75,7 @@ constructor( private var newMyNodeInfo: SharedMyNodeInfo? = null private var myNodeInfo: SharedMyNodeInfo? = null - fun handleConfigComplete(configCompleteId: Int) { + override fun handleConfigComplete(configCompleteId: Int) { when (configCompleteId) { configOnlyNonce -> handleConfigOnlyComplete() nodeInfoNonce -> handleNodeInfoComplete() @@ -134,7 +135,7 @@ constructor( } nodeManager.setNodeDbReady(true) nodeManager.setAllowNodeDbWrites(true) - connectionStateHolder.setState(ConnectionState.Connected) + serviceRepository.setConnectionState(ConnectionState.Connected) serviceBroadcasts.broadcastConnection() connectionManager.onNodeDbReady() } @@ -144,7 +145,7 @@ constructor( analytics.setDeviceAttributes(mi.firmwareVersion ?: "unknown", mi.model ?: "unknown") } - fun handleMyInfo(myInfo: ProtoMyNodeInfo) { + override fun handleMyInfo(myInfo: ProtoMyNodeInfo) { Logger.i { "MyNodeInfo received: ${myInfo.my_node_num}" } rawMyNodeInfo = myInfo nodeManager.myNodeNum = myInfo.my_node_num @@ -157,16 +158,20 @@ constructor( } } - fun handleLocalMetadata(metadata: DeviceMetadata) { + override fun handleLocalMetadata(metadata: DeviceMetadata) { Logger.i { "Local Metadata received: ${metadata.firmware_version}" } lastMetadata = metadata regenMyNodeInfo(metadata) } - fun handleNodeInfo(info: NodeInfo) { + override fun handleNodeInfo(info: NodeInfo) { newNodes.add(info) } + override fun triggerWantConfig() { + connectionManager.startConfigOnly() + } + private fun regenMyNodeInfo(metadata: DeviceMetadata? = null) { val myInfo = rawMyNodeInfo if (myInfo != null) { diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshConfigHandler.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConfigHandlerImpl.kt similarity index 83% rename from app/src/main/java/com/geeksville/mesh/service/MeshConfigHandler.kt rename to core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConfigHandlerImpl.kt index 9f71992f21..78441a2cd1 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshConfigHandler.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConfigHandlerImpl.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.geeksville.mesh.service +package org.meshtastic.core.data.manager import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -24,6 +24,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import org.meshtastic.core.common.util.handledLaunch +import org.meshtastic.core.repository.MeshConfigHandler import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.repository.ServiceRepository @@ -36,34 +37,31 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class MeshConfigHandler -@Inject -constructor( +class MeshConfigHandlerImpl @Inject constructor( private val radioConfigRepository: RadioConfigRepository, private val serviceRepository: ServiceRepository, private val nodeManager: NodeManager, -) { +) : MeshConfigHandler { private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val _localConfig = MutableStateFlow(LocalConfig()) - val localConfig = _localConfig.asStateFlow() + override val localConfig = _localConfig.asStateFlow() private val _moduleConfig = MutableStateFlow(LocalModuleConfig()) - val moduleConfig = _moduleConfig.asStateFlow() + override val moduleConfig = _moduleConfig.asStateFlow() - fun start(scope: CoroutineScope) { + override fun start(scope: CoroutineScope) { this.scope = scope radioConfigRepository.localConfigFlow.onEach { _localConfig.value = it }.launchIn(scope) - radioConfigRepository.moduleConfigFlow.onEach { _moduleConfig.value = it }.launchIn(scope) } - fun handleDeviceConfig(config: Config) { + override fun handleDeviceConfig(config: Config) { scope.handledLaunch { radioConfigRepository.setLocalConfig(config) } serviceRepository.setConnectionProgress("Device config received") } - fun handleModuleConfig(config: ModuleConfig) { + override fun handleModuleConfig(config: ModuleConfig) { scope.handledLaunch { radioConfigRepository.setLocalModuleConfig(config) } serviceRepository.setConnectionProgress("Module config received") @@ -72,13 +70,13 @@ constructor( } } - fun handleChannel(ch: Channel) { + override fun handleChannel(channel: Channel) { // We always want to save channel settings we receive from the radio - scope.handledLaunch { radioConfigRepository.updateChannelSettings(ch) } + scope.handledLaunch { radioConfigRepository.updateChannelSettings(channel) } // Update status message if we have node info, otherwise use a generic one val mi = nodeManager.getMyNodeInfo() - val index = ch.index ?: 0 + val index = channel.index ?: 0 if (mi != null) { serviceRepository.setConnectionProgress("Channels (${index + 1} / ${mi.maxChannels})") } else { diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt similarity index 92% rename from app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt rename to core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt index 64429d112f..262cd67eff 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 @@ -14,13 +14,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.geeksville.mesh.service +package org.meshtastic.core.data.manager -import android.util.Log import co.touchlab.kermit.Logger import co.touchlab.kermit.Severity -import com.geeksville.mesh.BuildConfig -import com.geeksville.mesh.repository.radio.InterfaceId import dagger.Lazy import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -44,7 +41,12 @@ import org.meshtastic.core.model.util.toOneLiner import org.meshtastic.core.prefs.mesh.MeshPrefs import org.meshtastic.core.repository.CommandSender import org.meshtastic.core.repository.HistoryManager +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.MeshServiceNotifications +import org.meshtastic.core.repository.MessageFilter import org.meshtastic.core.repository.NeighborInfoHandler import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.PacketHandler @@ -59,7 +61,6 @@ import org.meshtastic.core.resources.error_duty_cycle import org.meshtastic.core.resources.getString import org.meshtastic.core.resources.unknown_username import org.meshtastic.core.resources.waypoint_received -import org.meshtastic.core.service.filter.MessageFilterService import org.meshtastic.proto.AdminMessage import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.Paxcount @@ -80,9 +81,7 @@ import kotlin.time.Duration.Companion.milliseconds @Suppress("LongParameterList", "TooManyFunctions", "LargeClass", "CyclomaticComplexMethod") @Singleton -class MeshDataHandler -@Inject -constructor( +class MeshDataHandlerImpl @Inject constructor( private val nodeManager: NodeManager, private val packetHandler: PacketHandler, private val serviceRepository: ServiceRepository, @@ -100,11 +99,11 @@ constructor( private val tracerouteHandler: TracerouteHandler, private val neighborInfoHandler: NeighborInfoHandler, private val radioConfigRepository: RadioConfigRepository, - private val messageFilterService: MessageFilterService, -) { + private val messageFilter: MessageFilter, +) : MeshDataHandler { private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - fun start(scope: CoroutineScope) { + override fun start(scope: CoroutineScope) { this.scope = scope } @@ -116,7 +115,7 @@ constructor( PortNum.NODE_STATUS_APP.value, ) - fun handleReceivedData(packet: MeshPacket, myNodeNum: Int, logUuid: String? = null, logInsertJob: Job? = null) { + override fun handleReceivedData(packet: MeshPacket, myNodeNum: Int, logUuid: String?, logInsertJob: Job?) { val dataPacket = dataMapper.toDataPacket(packet) ?: return val fromUs = myNodeNum == packet.from dataPacket.status = MessageStatus.RECEIVED @@ -527,11 +526,11 @@ constructor( private fun handleReceivedStoreAndForward(dataPacket: DataPacket, s: StoreAndForward, myNodeNum: Int) { Logger.d { "StoreAndForward: variant from ${dataPacket.from}" } - val transport = currentTransport() + // For now, we don't have meshPrefs in commonMain, so we use a simplified transport check or abstract it. + // In the original, it was used for logging. val h = s.history val lastRequest = h?.last_request ?: 0 - val baseContext = "transport=$transport from=${dataPacket.from}" - historyLog { "rxStoreForward $baseContext lastRequest=$lastRequest" } + Logger.d { "rxStoreForward from=${dataPacket.from} lastRequest=$lastRequest" } when { s.stats != null -> { val text = s.stats.toString() @@ -543,10 +542,6 @@ constructor( rememberDataPacket(u, myNodeNum) } h != null -> { - @Suppress("MaxLineLength") - historyLog(Log.DEBUG) { - "routerHistory $baseContext messages=${h.history_messages} window=${h.window} lastReq=${h.last_request}" - } val text = "Total messages: ${h.history_messages}\n" + "History window: ${h.window.milliseconds.inWholeMinutes} min\n" + @@ -557,20 +552,17 @@ constructor( dataType = PortNum.TEXT_MESSAGE_APP.value, ) rememberDataPacket(u, myNodeNum) - historyManager.updateStoreForwardLastRequest("router_history", h.last_request, transport) + // historyManager call remains same + historyManager.updateStoreForwardLastRequest("router_history", h.last_request, "Unknown") } s.heartbeat != null -> { val hb = s.heartbeat!! - historyLog { "rxHeartbeat $baseContext period=${hb.period} secondary=${hb.secondary}" } + Logger.d { "rxHeartbeat from=${dataPacket.from} period=${hb.period} secondary=${hb.secondary}" } } s.text != null -> { if (s.rr == StoreAndForward.RequestResponse.ROUTER_TEXT_BROADCAST) { dataPacket.to = DataPacket.ID_BROADCAST } - @Suppress("MaxLineLength") - historyLog(Log.DEBUG) { - "rxText $baseContext id=${dataPacket.id} ts=${dataPacket.time} to=${dataPacket.to} decision=remember" - } val u = dataPacket.copy(bytes = s.text, dataType = PortNum.TEXT_MESSAGE_APP.value) rememberDataPacket(u, myNodeNum) } @@ -578,7 +570,7 @@ constructor( } } - fun rememberDataPacket(dataPacket: DataPacket, myNodeNum: Int, updateNotification: Boolean = true) { + override fun rememberDataPacket(dataPacket: DataPacket, myNodeNum: Int, updateNotification: Boolean) { if (dataPacket.dataType !in rememberDataType) return val fromLocal = dataPacket.from == DataPacket.ID_LOCAL || dataPacket.from == DataPacket.nodeNumToDefaultId(myNodeNum) @@ -619,7 +611,7 @@ constructor( if (dataPacket.dataType != PortNum.TEXT_MESSAGE_APP.value) return false val isFilteringDisabled = getContactSettings(contactKey).filteringDisabled - return messageFilterService.shouldFilter(dataPacket.text.orEmpty(), isFilteringDisabled) + return messageFilter.shouldFilter(dataPacket.text.orEmpty(), isFilteringDisabled) } private suspend fun handlePacketNotification( @@ -728,7 +720,6 @@ constructor( // Find the original packet to get the contactKey packetRepository.get().getPacketByPacketId(decoded.reply_id)?.let { originalPacket -> // Skip notification if the original message was filtered - // For now I'll assume it's NOT filtered if I can't check it easily. val contactKey = "${originalPacket.channel}${if (originalPacket.from == DataPacket.ID_LOCAL) originalPacket.to else originalPacket.from}" val conversationMuted = packetRepository.get().getContactSettings(contactKey).isMuted val nodeMuted = nodeManager.nodeDBbyID[fromId]?.isMuted == true @@ -757,33 +748,6 @@ constructor( } } - private fun currentTransport(address: String? = meshPrefs.deviceAddress): String = when (address?.firstOrNull()) { - InterfaceId.BLUETOOTH.id -> "BLE" - InterfaceId.TCP.id -> "TCP" - InterfaceId.SERIAL.id -> "Serial" - InterfaceId.MOCK.id -> "Mock" - InterfaceId.NOP.id -> "NOP" - else -> "Unknown" - } - - private inline fun historyLog( - priority: Int = Log.INFO, - throwable: Throwable? = null, - crossinline message: () -> String, - ) { - if (!BuildConfig.DEBUG) return - val logger = Logger.withTag("HistoryReplay") - val msg = message() - when (priority) { - Log.VERBOSE -> logger.v(throwable) { msg } - Log.DEBUG -> logger.d(throwable) { msg } - Log.INFO -> logger.i(throwable) { msg } - Log.WARN -> logger.w(throwable) { msg } - Log.ERROR -> logger.e(throwable) { msg } - else -> logger.i(throwable) { msg } - } - } - companion object { private const val HOPS_AWAY_UNAVAILABLE = -1 diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshMessageProcessor.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt similarity index 82% rename from app/src/main/java/com/geeksville/mesh/service/MeshMessageProcessor.kt rename to core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt index dbf4441395..db5d8435c7 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshMessageProcessor.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 @@ -14,11 +14,9 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.geeksville.mesh.service +package org.meshtastic.core.data.manager -import android.util.Log import co.touchlab.kermit.Logger -import com.geeksville.mesh.BuildConfig import dagger.Lazy import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -33,6 +31,9 @@ import org.meshtastic.core.data.repository.MeshLogRepository import org.meshtastic.core.database.entity.MeshLog import org.meshtastic.core.model.Node import org.meshtastic.core.model.util.isLora +import org.meshtastic.core.repository.FromRadioPacketHandler +import org.meshtastic.core.repository.MeshMessageProcessor +import org.meshtastic.core.repository.MeshRouter import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.proto.FromRadio @@ -46,17 +47,18 @@ 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 MeshMessageProcessor -@Inject -constructor( +class MeshMessageProcessorImpl @Inject constructor( private val nodeManager: NodeManager, private val serviceRepository: ServiceRepository, private val meshLogRepository: Lazy, private val router: MeshRouter, private val fromRadioDispatcher: FromRadioPacketHandler, -) { +) : MeshMessageProcessor { private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val logUuidByPacketId = ConcurrentHashMap() private val logInsertJobByPacketId = ConcurrentHashMap() @@ -64,11 +66,11 @@ constructor( private val earlyReceivedPackets = ArrayDeque() private val maxEarlyPacketBuffer = 10240 - fun clearEarlyPackets() { + override fun clearEarlyPackets() { synchronized(earlyReceivedPackets) { earlyReceivedPackets.clear() } } - fun start(scope: CoroutineScope) { + override fun start(scope: CoroutineScope) { this.scope = scope nodeManager.isNodeDbReady .onEach { ready -> @@ -79,7 +81,7 @@ constructor( .launchIn(scope) } - fun handleFromRadio(bytes: ByteArray, myNodeNum: Int?) { + override fun handleFromRadio(bytes: ByteArray, myNodeNum: Int?) { runCatching { FromRadio.ADAPTER.decode(bytes) } .onSuccess { proto -> processFromRadio(proto, myNodeNum) } .onFailure { primaryException -> @@ -136,7 +138,7 @@ constructor( ) } - fun handleReceivedMeshPacket(packet: MeshPacket, myNodeNum: Int?) { + override fun handleReceivedMeshPacket(packet: MeshPacket, myNodeNum: Int?) { val rxTime = if (packet.rx_time == 0) { nowSeconds.toInt() @@ -151,21 +153,9 @@ constructor( synchronized(earlyReceivedPackets) { val queueSize = earlyReceivedPackets.size if (queueSize >= maxEarlyPacketBuffer) { - val dropped = earlyReceivedPackets.removeFirst() - historyLog(Log.WARN) { - val portLabel = - dropped.decoded?.portnum?.name ?: dropped.decoded?.portnum?.value?.toString() ?: "unknown" - "dropEarlyPacket bufferFull size=$queueSize id=${dropped.id} port=$portLabel" - } + earlyReceivedPackets.removeFirst() } earlyReceivedPackets.addLast(preparedPacket) - val portLabel = - preparedPacket.decoded?.portnum?.name - ?: preparedPacket.decoded?.portnum?.value?.toString() - ?: "unknown" - historyLog { - "queueEarlyPacket size=${earlyReceivedPackets.size} id=${preparedPacket.id} port=$portLabel" - } } } } @@ -178,7 +168,7 @@ constructor( earlyReceivedPackets.clear() list } - historyLog { "replayEarlyPackets reason=$reason count=${packets.size}" } + Logger.d { "replayEarlyPackets reason=$reason count=${packets.size}" } val myNodeNum = nodeManager.myNodeNum packets.forEach { processReceivedMeshPacket(it, myNodeNum) } } @@ -250,24 +240,6 @@ constructor( private fun insertMeshLog(log: MeshLog): Job = scope.handledLaunch { meshLogRepository.get().insert(log) } - private inline fun historyLog( - priority: Int = Log.INFO, - throwable: Throwable? = null, - crossinline message: () -> String, - ) { - if (!BuildConfig.DEBUG) return - val logger = Logger.withTag("HistoryReplay") - val msg = message() - when (priority) { - Log.VERBOSE -> logger.v(throwable) { msg } - Log.DEBUG -> logger.d(throwable) { msg } - Log.INFO -> logger.i(throwable) { msg } - Log.WARN -> logger.w(throwable) { msg } - Log.ERROR -> logger.e(throwable) { msg } - else -> logger.i(throwable) { msg } - } - } - private fun ByteArray.toHexString(): String = this.joinToString(",") { byte -> String.format(Locale.US, "0x%02x", byte) } } diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshRouter.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshRouterImpl.kt similarity index 56% rename from app/src/main/java/com/geeksville/mesh/service/MeshRouter.kt rename to core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshRouterImpl.kt index d876f0e75a..33ad464459 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshRouter.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshRouterImpl.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 @@ -14,9 +14,14 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.geeksville.mesh.service +package org.meshtastic.core.data.manager import kotlinx.coroutines.CoroutineScope +import org.meshtastic.core.repository.MeshActionHandler +import org.meshtastic.core.repository.MeshConfigFlowManager +import org.meshtastic.core.repository.MeshConfigHandler +import org.meshtastic.core.repository.MeshDataHandler +import org.meshtastic.core.repository.MeshRouter import org.meshtastic.core.repository.MqttManager import org.meshtastic.core.repository.NeighborInfoHandler import org.meshtastic.core.repository.TracerouteHandler @@ -24,23 +29,20 @@ import javax.inject.Inject import javax.inject.Singleton /** - * Orchestrates the specialized packet handlers for the [MeshService]. This class serves as a central registry and - * lifecycle manager for all routing sub-components. + * Implementation of [MeshRouter] that orchestrates specialized mesh packet handlers. */ @Suppress("LongParameterList") @Singleton -class MeshRouter -@Inject -constructor( - val dataHandler: MeshDataHandler, - val configHandler: MeshConfigHandler, - val tracerouteHandler: TracerouteHandler, - val neighborInfoHandler: NeighborInfoHandler, - val configFlowManager: MeshConfigFlowManager, - val mqttManager: MqttManager, - val actionHandler: MeshActionHandler, -) { - fun start(scope: CoroutineScope) { +class MeshRouterImpl @Inject constructor( + override val dataHandler: MeshDataHandler, + override val configHandler: MeshConfigHandler, + override val tracerouteHandler: TracerouteHandler, + override val neighborInfoHandler: NeighborInfoHandler, + override val configFlowManager: MeshConfigFlowManager, + override val mqttManager: MqttManager, + override val actionHandler: MeshActionHandler, +) : MeshRouter { + override fun start(scope: CoroutineScope) { dataHandler.start(scope) configHandler.start(scope) tracerouteHandler.start(scope) diff --git a/core/service/src/main/kotlin/org/meshtastic/core/service/filter/MessageFilterService.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MessageFilterImpl.kt similarity index 68% rename from core/service/src/main/kotlin/org/meshtastic/core/service/filter/MessageFilterService.kt rename to core/data/src/main/kotlin/org/meshtastic/core/data/manager/MessageFilterImpl.kt index bb8a773aac..4da8b4971b 100644 --- a/core/service/src/main/kotlin/org/meshtastic/core/service/filter/MessageFilterService.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MessageFilterImpl.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 @@ -14,34 +14,27 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.core.service.filter +package org.meshtastic.core.data.manager import co.touchlab.kermit.Logger import org.meshtastic.core.prefs.filter.FilterPrefs +import org.meshtastic.core.repository.MessageFilter import java.util.regex.PatternSyntaxException import javax.inject.Inject import javax.inject.Singleton /** - * Service for filtering messages based on user-configured filter words. Supports both plain text word matching and - * regex patterns. + * Implementation of [MessageFilter] that uses regex and plain text matching. */ @Singleton -class MessageFilterService @Inject constructor(private val filterPrefs: FilterPrefs) { +class MessageFilterImpl @Inject constructor(private val filterPrefs: FilterPrefs) : MessageFilter { private var compiledPatterns: List = emptyList() init { rebuildPatterns() } - /** - * Determines if a message should be filtered based on the configured filter words. - * - * @param message The message text to check. - * @param isFilteringDisabled Whether filtering is disabled for this contact. - * @return true if the message should be filtered, false otherwise. - */ - fun shouldFilter(message: String, isFilteringDisabled: Boolean = false): Boolean { + override fun shouldFilter(message: String, isFilteringDisabled: Boolean): Boolean { if (!filterPrefs.filterEnabled || compiledPatterns.isEmpty() || isFilteringDisabled) { return false } @@ -49,11 +42,7 @@ class MessageFilterService @Inject constructor(private val filterPrefs: FilterPr return compiledPatterns.any { it.containsMatchIn(textToCheck) } } - /** - * Rebuilds the compiled regex patterns from the current filter words. Should be called whenever the filter words - * are updated. - */ - fun rebuildPatterns() { + override fun rebuildPatterns() { compiledPatterns = filterPrefs.filterWords.mapNotNull { word -> try { diff --git a/app/src/test/java/com/geeksville/mesh/service/FromRadioPacketHandlerTest.kt b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImplTest.kt similarity index 87% rename from app/src/test/java/com/geeksville/mesh/service/FromRadioPacketHandlerTest.kt rename to core/data/src/test/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImplTest.kt index d4d7e6112d..59b3e5c17f 100644 --- a/app/src/test/java/com/geeksville/mesh/service/FromRadioPacketHandlerTest.kt +++ b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImplTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 @@ -14,14 +14,17 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.geeksville.mesh.service +package org.meshtastic.core.data.manager +import io.mockk.every import io.mockk.mockk import io.mockk.verify import org.junit.Before import org.junit.Test +import org.meshtastic.core.repository.MeshRouter import org.meshtastic.core.repository.MeshServiceNotifications import org.meshtastic.core.repository.MqttManager +import org.meshtastic.core.repository.PacketHandler import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.proto.ClientNotification import org.meshtastic.proto.Config @@ -31,18 +34,18 @@ import org.meshtastic.proto.MyNodeInfo import org.meshtastic.proto.NodeInfo import org.meshtastic.proto.QueueStatus -class FromRadioPacketHandlerTest { +class FromRadioPacketHandlerImplTest { private val serviceRepository: ServiceRepository = mockk(relaxed = true) private val router: MeshRouter = mockk(relaxed = true) private val mqttManager: MqttManager = mockk(relaxed = true) private val packetHandler: PacketHandler = mockk(relaxed = true) private val serviceNotifications: MeshServiceNotifications = mockk(relaxed = true) - private lateinit var handler: FromRadioPacketHandler + private lateinit var handler: FromRadioPacketHandlerImpl @Before fun setup() { - handler = FromRadioPacketHandler(serviceRepository, router, mqttManager, packetHandler, serviceNotifications) + handler = FromRadioPacketHandlerImpl(serviceRepository, router, mqttManager, packetHandler, serviceNotifications) } @Test @@ -70,10 +73,12 @@ class FromRadioPacketHandlerTest { val nodeInfo = NodeInfo(num = 1234) val proto = FromRadio(node_info = nodeInfo) + every { router.configFlowManager.newNodeCount } returns 1 + handler.handleFromRadio(proto) verify { router.configFlowManager.handleNodeInfo(nodeInfo) } - verify { serviceRepository.setConnectionProgress(any()) } + verify { serviceRepository.setConnectionProgress("Nodes (1)") } } @Test diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshDataHandlerTest.kt b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerTest.kt similarity index 91% rename from app/src/test/java/com/geeksville/mesh/service/MeshDataHandlerTest.kt rename to core/data/src/test/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerTest.kt index 6f29161964..b556ebfdd4 100644 --- a/app/src/test/java/com/geeksville/mesh/service/MeshDataHandlerTest.kt +++ b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.geeksville.mesh.service +package org.meshtastic.core.data.manager import dagger.Lazy import io.mockk.coVerify @@ -35,14 +35,19 @@ import org.meshtastic.core.model.util.MeshDataMapper import org.meshtastic.core.prefs.mesh.MeshPrefs import org.meshtastic.core.repository.CommandSender import org.meshtastic.core.repository.HistoryManager +import org.meshtastic.core.repository.MeshConfigFlowManager +import org.meshtastic.core.repository.MeshConfigHandler +import org.meshtastic.core.repository.MeshConnectionManager import org.meshtastic.core.repository.MeshServiceNotifications +import org.meshtastic.core.repository.MessageFilter import org.meshtastic.core.repository.NeighborInfoHandler import org.meshtastic.core.repository.NodeManager +import org.meshtastic.core.repository.PacketHandler import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.RadioConfigRepository +import org.meshtastic.core.repository.ServiceBroadcasts import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.core.repository.TracerouteHandler -import org.meshtastic.core.service.filter.MessageFilterService import org.meshtastic.proto.Data import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.PortNum @@ -68,9 +73,9 @@ class MeshDataHandlerTest { private val tracerouteHandler: TracerouteHandler = mockk(relaxed = true) private val neighborInfoHandler: NeighborInfoHandler = mockk(relaxed = true) private val radioConfigRepository: RadioConfigRepository = mockk(relaxed = true) - private val messageFilterService: MessageFilterService = mockk(relaxed = true) + private val messageFilter: MessageFilter = mockk(relaxed = true) - private lateinit var meshDataHandler: MeshDataHandler + private lateinit var meshDataHandler: MeshDataHandlerImpl @OptIn(ExperimentalCoroutinesApi::class) @Before @@ -82,7 +87,7 @@ class MeshDataHandlerTest { every { android.util.Log.e(any(), any()) } returns 0 meshDataHandler = - MeshDataHandler( + MeshDataHandlerImpl( nodeManager, packetHandler, serviceRepository, @@ -100,7 +105,7 @@ class MeshDataHandlerTest { tracerouteHandler, neighborInfoHandler, radioConfigRepository, - messageFilterService, + messageFilter, ) // Use UnconfinedTestDispatcher for running coroutines synchronously in tests meshDataHandler.start(CoroutineScope(UnconfinedTestDispatcher())) diff --git a/core/service/src/test/kotlin/org/meshtastic/core/service/filter/MessageFilterServiceTest.kt b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/MessageFilterImplTest.kt similarity index 92% rename from core/service/src/test/kotlin/org/meshtastic/core/service/filter/MessageFilterServiceTest.kt rename to core/data/src/test/kotlin/org/meshtastic/core/data/manager/MessageFilterImplTest.kt index 4d99605738..d47825225d 100644 --- a/core/service/src/test/kotlin/org/meshtastic/core/service/filter/MessageFilterServiceTest.kt +++ b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/MessageFilterImplTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 @@ -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.core.service.filter +package org.meshtastic.core.data.manager import io.mockk.every import io.mockk.mockk @@ -24,9 +24,9 @@ import org.junit.Before import org.junit.Test import org.meshtastic.core.prefs.filter.FilterPrefs -class MessageFilterServiceTest { +class MessageFilterImplTest { private lateinit var filterPrefs: FilterPrefs - private lateinit var filterService: MessageFilterService + private lateinit var filterService: MessageFilterImpl @Before fun setup() { @@ -34,7 +34,7 @@ class MessageFilterServiceTest { every { filterEnabled } returns true every { filterWords } returns setOf("spam", "bad") } - filterService = MessageFilterService(filterPrefs) + filterService = MessageFilterImpl(filterPrefs) } @Test diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/DatabaseManager.kt b/core/database/src/main/kotlin/org/meshtastic/core/database/DatabaseManager.kt index 6630228743..17513b6984 100644 --- a/core/database/src/main/kotlin/org/meshtastic/core/database/DatabaseManager.kt +++ b/core/database/src/main/kotlin/org/meshtastic/core/database/DatabaseManager.kt @@ -84,7 +84,7 @@ open class DatabaseManager @Inject constructor(private val app: Application, pri } /** Switch active database to the one associated with [address]. Serialized via mutex. */ - suspend fun switchActiveDatabase(address: String?) = mutex.withLock { + override suspend fun switchActiveDatabase(address: String?) = mutex.withLock { val dbName = buildDbName(address) // Remember the previously active DB name (any) so we can record its last-used time as well. diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/di/DatabaseModule.kt b/core/database/src/main/kotlin/org/meshtastic/core/database/di/DatabaseModule.kt index b79c7c180a..e170560704 100644 --- a/core/database/src/main/kotlin/org/meshtastic/core/database/di/DatabaseModule.kt +++ b/core/database/src/main/kotlin/org/meshtastic/core/database/di/DatabaseModule.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 @@ -18,10 +18,12 @@ package org.meshtastic.core.database.di import android.app.Application +import dagger.Binds import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import org.meshtastic.core.database.DatabaseManager import org.meshtastic.core.database.MeshtasticDatabase import org.meshtastic.core.database.dao.DeviceHardwareDao import org.meshtastic.core.database.dao.FirmwareReleaseDao @@ -34,26 +36,40 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) @Module -class DatabaseModule { - @Provides @Singleton - fun provideDatabase(app: Application): MeshtasticDatabase = MeshtasticDatabase.getDatabase(app) +abstract class DatabaseModule { - @Provides fun provideNodeInfoDao(database: MeshtasticDatabase): NodeInfoDao = database.nodeInfoDao() + @Binds + @Singleton + abstract fun bindDatabaseManager(impl: DatabaseManager): org.meshtastic.core.repository.DatabaseManager - @Provides fun providePacketDao(database: MeshtasticDatabase): PacketDao = database.packetDao() + companion object { + @Provides + @Singleton + fun provideDatabase(app: Application): MeshtasticDatabase = MeshtasticDatabase.getDatabase(app) - @Provides fun provideMeshLogDao(database: MeshtasticDatabase): MeshLogDao = database.meshLogDao() + @Provides + fun provideNodeInfoDao(database: MeshtasticDatabase): NodeInfoDao = database.nodeInfoDao() - @Provides - fun provideQuickChatActionDao(database: MeshtasticDatabase): QuickChatActionDao = database.quickChatActionDao() + @Provides + fun providePacketDao(database: MeshtasticDatabase): PacketDao = database.packetDao() - @Provides - fun provideDeviceHardwareDao(database: MeshtasticDatabase): DeviceHardwareDao = database.deviceHardwareDao() + @Provides + fun provideMeshLogDao(database: MeshtasticDatabase): MeshLogDao = database.meshLogDao() - @Provides - fun provideFirmwareReleaseDao(database: MeshtasticDatabase): FirmwareReleaseDao = database.firmwareReleaseDao() + @Provides + fun provideQuickChatActionDao(database: MeshtasticDatabase): QuickChatActionDao = + database.quickChatActionDao() - @Provides - fun provideTracerouteNodePositionDao(database: MeshtasticDatabase): TracerouteNodePositionDao = - database.tracerouteNodePositionDao() + @Provides + fun provideDeviceHardwareDao(database: MeshtasticDatabase): DeviceHardwareDao = + database.deviceHardwareDao() + + @Provides + fun provideFirmwareReleaseDao(database: MeshtasticDatabase): FirmwareReleaseDao = + database.firmwareReleaseDao() + + @Provides + fun provideTracerouteNodePositionDao(database: MeshtasticDatabase): TracerouteNodePositionDao = + database.tracerouteNodePositionDao() + } } diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshDataMapperTest.kt b/core/model/src/androidHostTest/kotlin/org/meshtastic/core/model/util/MeshDataMapperTest.kt similarity index 85% rename from app/src/test/java/com/geeksville/mesh/service/MeshDataMapperTest.kt rename to core/model/src/androidHostTest/kotlin/org/meshtastic/core/model/util/MeshDataMapperTest.kt index 67c53b4d62..ec6559bbd8 100644 --- a/app/src/test/java/com/geeksville/mesh/service/MeshDataMapperTest.kt +++ b/core/model/src/androidHostTest/kotlin/org/meshtastic/core/model/util/MeshDataMapperTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2026 Meshtastic LLC + * 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 @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.geeksville.mesh.service +package org.meshtastic.core.model.util import io.mockk.every import io.mockk.mockk @@ -25,20 +25,18 @@ import org.junit.Assert.assertNull import org.junit.Before import org.junit.Test import org.meshtastic.core.model.DataPacket -import org.meshtastic.core.model.util.MeshDataMapper -import org.meshtastic.core.repository.NodeManager import org.meshtastic.proto.Data import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.PortNum class MeshDataMapperTest { - private val nodeManager: NodeManager = mockk() + private val nodeIdLookup: NodeIdLookup = mockk() private lateinit var mapper: MeshDataMapper @Before fun setUp() { - mapper = MeshDataMapper(nodeManager) + mapper = MeshDataMapper(nodeIdLookup) } @Test @@ -51,8 +49,8 @@ class MeshDataMapperTest { fun `toDataPacket maps basic fields correctly`() { val nodeNum = 1234 val nodeId = "!1234abcd" - every { nodeManager.toNodeID(nodeNum) } returns nodeId - every { nodeManager.toNodeID(DataPacket.NODENUM_BROADCAST) } returns DataPacket.ID_BROADCAST + every { nodeIdLookup.toNodeID(nodeNum) } returns nodeId + every { nodeIdLookup.toNodeID(DataPacket.NODENUM_BROADCAST) } returns DataPacket.ID_BROADCAST val proto = MeshPacket( @@ -89,7 +87,7 @@ class MeshDataMapperTest { fun `toDataPacket maps PKC channel correctly for encrypted packets`() { val proto = MeshPacket(pki_encrypted = true, channel = 1, decoded = Data()) - every { nodeManager.toNodeID(any()) } returns "any" + every { nodeIdLookup.toNodeID(any()) } returns "any" val result = mapper.toDataPacket(proto) assertEquals(DataPacket.PKC_CHANNEL_INDEX, result!!.channel) diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/DatabaseManager.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/DatabaseManager.kt index 44cbd1a8f5..c45e7f2403 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/DatabaseManager.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/DatabaseManager.kt @@ -30,4 +30,7 @@ interface DatabaseManager { /** Sets the database cache limit. */ fun setCacheLimit(limit: Int) + + /** Switches the active database to the one associated with the given [address]. */ + suspend fun switchActiveDatabase(address: String?) } diff --git a/app/src/main/java/com/geeksville/mesh/service/ConnectionStateHandler.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/FromRadioPacketHandler.kt similarity index 53% rename from app/src/main/java/com/geeksville/mesh/service/ConnectionStateHandler.kt rename to core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/FromRadioPacketHandler.kt index a9f1cf014f..ee953b9d18 100644 --- a/app/src/main/java/com/geeksville/mesh/service/ConnectionStateHandler.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/FromRadioPacketHandler.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 @@ -14,20 +14,14 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.geeksville.mesh.service +package org.meshtastic.core.repository -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import org.meshtastic.core.model.ConnectionState -import javax.inject.Inject -import javax.inject.Singleton +import org.meshtastic.proto.FromRadio -@Singleton -class ConnectionStateHandler @Inject constructor() { - private val _connectionState = MutableStateFlow(ConnectionState.Disconnected) - val connectionState = _connectionState.asStateFlow() - - fun setState(state: ConnectionState) { - _connectionState.value = state - } +/** + * Interface for dispatching non-packet [FromRadio] variants to their respective handlers. + */ +interface FromRadioPacketHandler { + /** Processes a [FromRadio] message. */ + fun handleFromRadio(proto: FromRadio) } diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshActionHandler.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshActionHandler.kt new file mode 100644 index 0000000000..8e8f00605e --- /dev/null +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshActionHandler.kt @@ -0,0 +1,124 @@ +/* + * 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 . + */ +package org.meshtastic.core.repository + +import kotlinx.coroutines.CoroutineScope +import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.model.MeshUser +import org.meshtastic.core.model.Position +import org.meshtastic.core.model.service.ServiceAction + +/** + * Interface for handling UI-triggered actions and administrative commands for the mesh. + */ +interface MeshActionHandler { + /** Starts the handler with the given coroutine scope. */ + fun start(scope: CoroutineScope) + + /** Processes a service action from the UI. */ + fun onServiceAction(action: ServiceAction) + + /** Sets the owner of the local node. */ + fun handleSetOwner(u: MeshUser, myNodeNum: Int) + + /** Sends a data packet through the mesh. */ + fun handleSend(p: DataPacket, myNodeNum: Int) + + /** Requests the position of a remote node. */ + fun handleRequestPosition(destNum: Int, position: Position, myNodeNum: Int) + + /** Removes a node from the database by its node number. */ + fun handleRemoveByNodenum(nodeNum: Int, requestId: Int, myNodeNum: Int) + + /** Sets the owner of a remote node. */ + fun handleSetRemoteOwner(id: Int, destNum: Int, payload: ByteArray) + + /** Gets the owner of a remote node. */ + fun handleGetRemoteOwner(id: Int, destNum: Int) + + /** Sets the configuration of the local node. */ + fun handleSetConfig(payload: ByteArray, myNodeNum: Int) + + /** Sets the configuration of a remote node. */ + fun handleSetRemoteConfig(id: Int, destNum: Int, payload: ByteArray) + + /** Gets the configuration of a remote node. */ + fun handleGetRemoteConfig(id: Int, destNum: Int, config: Int) + + /** Sets the module configuration of a remote node. */ + fun handleSetModuleConfig(id: Int, destNum: Int, payload: ByteArray) + + /** Gets the module configuration of a remote node. */ + fun handleGetModuleConfig(id: Int, destNum: Int, config: Int) + + /** Sets the ringtone of a remote node. */ + fun handleSetRingtone(destNum: Int, ringtone: String) + + /** Gets the ringtone of a remote node. */ + fun handleGetRingtone(id: Int, destNum: Int) + + /** Sets canned messages on a remote node. */ + fun handleSetCannedMessages(destNum: Int, messages: String) + + /** Gets canned messages from a remote node. */ + fun handleGetCannedMessages(id: Int, destNum: Int) + + /** Sets a channel configuration on the local node. */ + fun handleSetChannel(payload: ByteArray?, myNodeNum: Int) + + /** Sets a channel configuration on a remote node. */ + fun handleSetRemoteChannel(id: Int, destNum: Int, payload: ByteArray?) + + /** Gets a channel configuration from a remote node. */ + fun handleGetRemoteChannel(id: Int, destNum: Int, index: Int) + + /** Requests neighbor information from a remote node. */ + fun handleRequestNeighborInfo(requestId: Int, destNum: Int) + + /** Begins editing settings on a remote node. */ + fun handleBeginEditSettings(destNum: Int) + + /** Commits settings edits on a remote node. */ + fun handleCommitEditSettings(destNum: Int) + + /** Reboots a remote node into DFU mode. */ + fun handleRebootToDfu(destNum: Int) + + /** Requests telemetry from a remote node. */ + fun handleRequestTelemetry(requestId: Int, destNum: Int, type: Int) + + /** Requests a remote node to shut down. */ + fun handleRequestShutdown(requestId: Int, destNum: Int) + + /** Requests a remote node to reboot. */ + fun handleRequestReboot(requestId: Int, destNum: Int) + + /** Requests a remote node to reboot in OTA mode. */ + fun handleRequestRebootOta(requestId: Int, destNum: Int, mode: Int, hash: ByteArray?) + + /** Requests a factory reset on a remote node. */ + fun handleRequestFactoryReset(requestId: Int, destNum: Int) + + /** Requests a node database reset on a remote node. */ + fun handleRequestNodedbReset(requestId: Int, destNum: Int, preserveFavorites: Boolean) + + /** Gets the connection status of a remote node. */ + fun handleGetDeviceConnectionStatus(requestId: Int, destNum: Int) + + /** Updates the last used device address. */ + fun handleUpdateLastAddress(deviceAddr: String?) +} diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshConfigFlowManager.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshConfigFlowManager.kt new file mode 100644 index 0000000000..05847ff9a8 --- /dev/null +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshConfigFlowManager.kt @@ -0,0 +1,48 @@ +/* + * 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 . + */ +package org.meshtastic.core.repository + +import kotlinx.coroutines.CoroutineScope +import org.meshtastic.proto.DeviceMetadata +import org.meshtastic.proto.MyNodeInfo +import org.meshtastic.proto.NodeInfo + +/** + * Interface for managing the configuration flow, including local node info and metadata. + */ +interface MeshConfigFlowManager { + /** Starts the manager with the given coroutine scope. */ + fun start(scope: CoroutineScope) + + /** Handles received local node information. */ + fun handleMyInfo(myInfo: MyNodeInfo) + + /** Handles received local device metadata. */ + fun handleLocalMetadata(metadata: DeviceMetadata) + + /** Handles received node information. */ + fun handleNodeInfo(info: NodeInfo) + + /** Returns the number of nodes received in the current stage. */ + val newNodeCount: Int + + /** Handles the completion of a configuration stage. */ + fun handleConfigComplete(configCompleteId: Int) + + /** Triggers a request for the full device configuration. */ + fun triggerWantConfig() +} diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshConfigHandler.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshConfigHandler.kt new file mode 100644 index 0000000000..fed430bead --- /dev/null +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshConfigHandler.kt @@ -0,0 +1,48 @@ +/* + * 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 . + */ +package org.meshtastic.core.repository + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.StateFlow +import org.meshtastic.proto.Channel +import org.meshtastic.proto.Config +import org.meshtastic.proto.LocalConfig +import org.meshtastic.proto.LocalModuleConfig +import org.meshtastic.proto.ModuleConfig + +/** + * Interface for handling device and module configuration updates. + */ +interface MeshConfigHandler { + /** Starts the handler with the given coroutine scope. */ + fun start(scope: CoroutineScope) + + /** Reactive local configuration. */ + val localConfig: StateFlow + + /** Reactive local module configuration. */ + val moduleConfig: StateFlow + + /** Handles a received device configuration. */ + fun handleDeviceConfig(config: Config) + + /** Handles a received module configuration. */ + fun handleModuleConfig(config: ModuleConfig) + + /** Handles a received channel configuration. */ + fun handleChannel(channel: Channel) +} diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshConnectionManager.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshConnectionManager.kt new file mode 100644 index 0000000000..9aff94e0e8 --- /dev/null +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshConnectionManager.kt @@ -0,0 +1,46 @@ +/* + * 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 . + */ +package org.meshtastic.core.repository + +import kotlinx.coroutines.CoroutineScope +import org.meshtastic.proto.Telemetry + +/** + * Interface for managing the connection lifecycle and status with the mesh radio. + */ +interface MeshConnectionManager { + /** Starts the connection manager with the given coroutine scope. */ + fun start(scope: CoroutineScope) + + /** Called when the radio configuration has been fully loaded. */ + fun onRadioConfigLoaded() + + /** Initiates the configuration synchronization stage. */ + fun startConfigOnly() + + /** Initiates the node information synchronization stage. */ + fun startNodeInfoOnly() + + /** Called when the node database is ready and fully populated. */ + fun onNodeDbReady() + + /** Updates the telemetry information for the local node. */ + fun updateTelemetry(t: Telemetry) + + /** Updates and returns the current status notification. */ + fun updateStatusNotification(telemetry: Telemetry? = null): Any +} diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshDataHandler.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshDataHandler.kt new file mode 100644 index 0000000000..afd4a97118 --- /dev/null +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshDataHandler.kt @@ -0,0 +1,58 @@ +/* + * 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 . + */ +package org.meshtastic.core.repository + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import org.meshtastic.core.model.DataPacket +import org.meshtastic.proto.MeshPacket + +/** + * Interface for handling incoming mesh data packets and routing them to the appropriate handlers. + */ +interface MeshDataHandler { + /** Starts the handler with the given coroutine scope. */ + fun start(scope: CoroutineScope) + + /** + * Processes a received mesh packet. + * + * @param packet The received mesh packet. + * @param myNodeNum The local node number. + * @param logUuid Optional UUID for logging purposes. + * @param logInsertJob Optional job that tracks the insertion of the packet into the log. + */ + fun handleReceivedData( + packet: MeshPacket, + myNodeNum: Int, + logUuid: String? = null, + logInsertJob: Job? = null + ) + + /** + * Persists a data packet in the history and triggers notifications if necessary. + * + * @param dataPacket The data packet to remember. + * @param myNodeNum The local node number. + * @param updateNotification Whether to trigger a notification for this packet. + */ + fun rememberDataPacket( + dataPacket: DataPacket, + myNodeNum: Int, + updateNotification: Boolean = true + ) +} diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshMessageProcessor.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshMessageProcessor.kt new file mode 100644 index 0000000000..d5bf8a08e1 --- /dev/null +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshMessageProcessor.kt @@ -0,0 +1,37 @@ +/* + * 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 . + */ +package org.meshtastic.core.repository + +import kotlinx.coroutines.CoroutineScope +import org.meshtastic.proto.MeshPacket + +/** + * Interface for processing incoming radio messages and mesh packets. + */ +interface MeshMessageProcessor { + /** Starts the processor with the given coroutine scope. */ + fun start(scope: CoroutineScope) + + /** Handles a raw message received from the radio. */ + fun handleFromRadio(bytes: ByteArray, myNodeNum: Int?) + + /** Handles a received mesh packet. */ + fun handleReceivedMeshPacket(packet: MeshPacket, myNodeNum: Int?) + + /** Clears the buffer of early received packets. */ + fun clearEarlyPackets() +} diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshRouter.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshRouter.kt new file mode 100644 index 0000000000..36725c250b --- /dev/null +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshRouter.kt @@ -0,0 +1,48 @@ +/* + * 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 . + */ +package org.meshtastic.core.repository + +import kotlinx.coroutines.CoroutineScope + +/** + * Interface for the central router that orchestrates specialized mesh packet handlers. + */ +interface MeshRouter { + /** Starts the router and its sub-components with the given coroutine scope. */ + fun start(scope: CoroutineScope) + + /** Access to the data handler. */ + val dataHandler: MeshDataHandler + + /** Access to the configuration handler. */ + val configHandler: MeshConfigHandler + + /** Access to the traceroute handler. */ + val tracerouteHandler: TracerouteHandler + + /** Access to the neighbor info handler. */ + val neighborInfoHandler: NeighborInfoHandler + + /** Access to the configuration flow manager. */ + val configFlowManager: MeshConfigFlowManager + + /** Access to the MQTT manager. */ + val mqttManager: MqttManager + + /** Access to the action handler. */ + val actionHandler: MeshActionHandler +} diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MessageFilter.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MessageFilter.kt new file mode 100644 index 0000000000..18fb5b0440 --- /dev/null +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MessageFilter.kt @@ -0,0 +1,36 @@ +/* + * 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 . + */ +package org.meshtastic.core.repository + +/** + * Interface for filtering messages based on user-configured filter words. + */ +interface MessageFilter { + /** + * Determines if a message should be filtered. + * + * @param message The message text to check. + * @param isFilteringDisabled Whether filtering is disabled for the current contact. + * @return true if the message should be filtered, false otherwise. + */ + fun shouldFilter(message: String, isFilteringDisabled: Boolean = false): Boolean + + /** + * Rebuilds the internal filter patterns. Should be called after filter words are updated. + */ + fun rebuildPatterns() +} diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModel.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModel.kt index 77c17699a6..cc263bfe13 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModel.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModel.kt @@ -22,7 +22,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import org.meshtastic.core.prefs.filter.FilterPrefs -import org.meshtastic.core.service.filter.MessageFilterService +import org.meshtastic.core.repository.MessageFilter import javax.inject.Inject @HiltViewModel @@ -30,7 +30,7 @@ class FilterSettingsViewModel @Inject constructor( private val filterPrefs: FilterPrefs, - private val messageFilterService: MessageFilterService, + private val messageFilter: MessageFilter, ) : ViewModel() { private val _filterEnabled = MutableStateFlow(filterPrefs.filterEnabled) @@ -51,7 +51,7 @@ constructor( if (current.add(trimmed)) { filterPrefs.filterWords = current _filterWords.value = current.toList().sorted() - messageFilterService.rebuildPatterns() + messageFilter.rebuildPatterns() } } @@ -60,7 +60,7 @@ constructor( if (current.remove(word)) { filterPrefs.filterWords = current _filterWords.value = current.toList().sorted() - messageFilterService.rebuildPatterns() + messageFilter.rebuildPatterns() } } } diff --git a/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModelTest.kt b/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModelTest.kt index 35fd61f2b1..8e3993b470 100644 --- a/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModelTest.kt +++ b/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModelTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 @@ -23,12 +23,12 @@ import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import org.meshtastic.core.prefs.filter.FilterPrefs -import org.meshtastic.core.service.filter.MessageFilterService +import org.meshtastic.core.repository.MessageFilter class FilterSettingsViewModelTest { private val filterPrefs: FilterPrefs = mockk(relaxed = true) - private val messageFilterService: MessageFilterService = mockk(relaxed = true) + private val messageFilter: MessageFilter = mockk(relaxed = true) private lateinit var viewModel: FilterSettingsViewModel @@ -37,7 +37,10 @@ class FilterSettingsViewModelTest { every { filterPrefs.filterEnabled } returns true every { filterPrefs.filterWords } returns setOf("apple", "banana") - viewModel = FilterSettingsViewModel(filterPrefs = filterPrefs, messageFilterService = messageFilterService) + viewModel = FilterSettingsViewModel( + filterPrefs = filterPrefs, + messageFilter = messageFilter + ) } @Test @@ -50,18 +53,18 @@ class FilterSettingsViewModelTest { @Test fun `addFilterWord updates prefs and rebuilds patterns`() { viewModel.addFilterWord("cherry") - + verify { filterPrefs.filterWords = any() } - verify { messageFilterService.rebuildPatterns() } + verify { messageFilter.rebuildPatterns() } assertEquals(listOf("apple", "banana", "cherry"), viewModel.filterWords.value) } @Test fun `removeFilterWord updates prefs and rebuilds patterns`() { viewModel.removeFilterWord("apple") - + verify { filterPrefs.filterWords = any() } - verify { messageFilterService.rebuildPatterns() } + verify { messageFilter.rebuildPatterns() } assertEquals(listOf("banana"), viewModel.filterWords.value) } } From b2f8e0f3b709c195becbad4634779b5e08a7840c Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 2 Mar 2026 14:59:01 -0600 Subject: [PATCH 10/20] refactor: move core services and logic to core modules - Relocate `RadioInterfaceService`, `PacketHandler`, and `MeshConnectionManager` from the app module to `core:data` and `core:repository`. - Extract platform-specific implementations for location, workers, and widget updates into Android-specific classes. - Move shared models and exceptions, including `InterfaceId` and `RadioNotConnectedException`, to `core:model`. - Introduce `Lazy` injection in several managers to resolve circular dependencies. - Update Dagger Hilt bindings to support the new interface-driven architecture. - Adjust unit tests to reflect the new package structure and implementation details. Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .../filter/MessageFilterIntegrationTest.kt | 4 +- .../com/geeksville/mesh/ApplicationModule.kt | 15 +++- .../usecase/GetDiscoveredDevicesUseCase.kt | 2 +- .../geeksville/mesh/model/DeviceListEntry.kt | 4 +- .../com/geeksville/mesh/model/UIViewModel.kt | 4 +- ...ice.kt => AndroidRadioInterfaceService.kt} | 64 ++++++++-------- .../mesh/repository/radio/InterfaceFactory.kt | 1 + .../mesh/repository/radio/InterfaceMapKey.kt | 1 + .../mesh/repository/radio/MockInterface.kt | 1 + .../repository/radio/NordicBleInterface.kt | 13 ++-- .../repository/radio/RadioRepositoryModule.kt | 1 + .../mesh/repository/radio/SerialInterface.kt | 1 + .../mesh/repository/radio/StreamInterface.kt | 1 + .../mesh/repository/radio/TCPInterface.kt | 1 + .../mesh/service/AndroidAppWidgetUpdater.kt | 41 ++++++++++ ...nager.kt => AndroidMeshLocationManager.kt} | 9 ++- .../mesh/service/AndroidMeshWorkerManager.kt | 43 +++++++++++ .../geeksville/mesh/service/MeshService.kt | 6 +- .../main/java/com/geeksville/mesh/ui/Main.kt | 2 +- .../mesh/ui/connections/ScannerViewModel.kt | 2 +- .../radio/NordicBleInterfaceRetryTest.kt | 1 + .../radio/NordicBleInterfaceTest.kt | 3 +- .../repository/radio/StreamInterfaceTest.kt | 1 + .../java/com/geeksville/mesh/service/Fakes.kt | 2 +- .../core/data/di/RepositoryModule.kt | 16 ++++ .../manager/FromRadioPacketHandlerImpl.kt | 19 ++--- .../data/manager/MeshActionHandlerImpl.kt | 6 +- .../data/manager/MeshConfigFlowManagerImpl.kt | 13 ++-- .../data/manager/MeshConfigHandlerImpl.kt | 2 +- .../data/manager/MeshConnectionManagerImpl.kt | 47 ++++-------- .../core/data/manager/MeshDataHandlerImpl.kt | 16 ++-- .../data/manager/MeshMessageProcessorImpl.kt | 4 +- .../core/data/manager/MeshRouterImpl.kt | 23 ++++-- .../core/data/manager/PacketHandlerImpl.kt | 40 +++------- .../manager/FromRadioPacketHandlerImplTest.kt | 2 +- .../manager/MeshConnectionManagerImplTest.kt | 48 ++++++------ .../core/data/manager/MeshDataHandlerTest.kt | 6 +- .../data/manager/PacketHandlerImplTest.kt | 19 ++--- .../org/meshtastic/core/model}/InterfaceId.kt | 5 +- .../org/meshtastic/core/model/MeshActivity.kt | 28 +++++++ .../core/model}/RadioNotConnectedException.kt | 20 ++--- .../core/repository/AppWidgetUpdater.kt | 25 +++++++ .../core/repository/MeshLocationManager.kt | 31 ++++++++ .../core/repository/MeshWorkerManager.kt | 25 +++++++ .../core/repository/RadioInterfaceService.kt | 74 +++++++++++++++++++ 45 files changed, 479 insertions(+), 213 deletions(-) rename app/src/main/java/com/geeksville/mesh/repository/radio/{RadioInterfaceService.kt => AndroidRadioInterfaceService.kt} (88%) create mode 100644 app/src/main/java/com/geeksville/mesh/service/AndroidAppWidgetUpdater.kt rename app/src/main/java/com/geeksville/mesh/service/{MeshLocationManager.kt => AndroidMeshLocationManager.kt} (93%) create mode 100644 app/src/main/java/com/geeksville/mesh/service/AndroidMeshWorkerManager.kt rename app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt => core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt (90%) rename app/src/main/java/com/geeksville/mesh/service/PacketHandler.kt => core/data/src/main/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt (83%) rename app/src/test/java/com/geeksville/mesh/service/MeshConnectionManagerTest.kt => core/data/src/test/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImplTest.kt (84%) rename app/src/test/java/com/geeksville/mesh/service/PacketHandlerTest.kt => core/data/src/test/kotlin/org/meshtastic/core/data/manager/PacketHandlerImplTest.kt (88%) rename {app/src/main/java/com/geeksville/mesh/repository/radio => core/model/src/commonMain/kotlin/org/meshtastic/core/model}/InterfaceId.kt (95%) create mode 100644 core/model/src/commonMain/kotlin/org/meshtastic/core/model/MeshActivity.kt rename {app/src/main/java/com/geeksville/mesh/service => core/model/src/commonMain/kotlin/org/meshtastic/core/model}/RadioNotConnectedException.kt (55%) create mode 100644 core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/AppWidgetUpdater.kt create mode 100644 core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshLocationManager.kt create mode 100644 core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshWorkerManager.kt create mode 100644 core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/RadioInterfaceService.kt diff --git a/app/src/androidTest/java/com/geeksville/mesh/filter/MessageFilterIntegrationTest.kt b/app/src/androidTest/java/com/geeksville/mesh/filter/MessageFilterIntegrationTest.kt index 6a701aa8c6..2c327a7af7 100644 --- a/app/src/androidTest/java/com/geeksville/mesh/filter/MessageFilterIntegrationTest.kt +++ b/app/src/androidTest/java/com/geeksville/mesh/filter/MessageFilterIntegrationTest.kt @@ -26,7 +26,7 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.meshtastic.core.prefs.filter.FilterPrefs -import org.meshtastic.core.service.filter.MessageFilterService +import org.meshtastic.core.repository.MessageFilter import javax.inject.Inject @HiltAndroidTest @@ -37,7 +37,7 @@ class MessageFilterIntegrationTest { @Inject lateinit var filterPrefs: FilterPrefs - @Inject lateinit var filterService: MessageFilterService + @Inject lateinit var filterService: MessageFilter @Before fun setup() { diff --git a/app/src/main/java/com/geeksville/mesh/ApplicationModule.kt b/app/src/main/java/com/geeksville/mesh/ApplicationModule.kt index 36f80fbbc9..7d05423ba7 100644 --- a/app/src/main/java/com/geeksville/mesh/ApplicationModule.kt +++ b/app/src/main/java/com/geeksville/mesh/ApplicationModule.kt @@ -20,8 +20,11 @@ package com.geeksville.mesh import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner +import com.geeksville.mesh.repository.radio.AndroidRadioInterfaceService +import com.geeksville.mesh.service.AndroidAppWidgetUpdater +import com.geeksville.mesh.service.AndroidMeshLocationManager +import com.geeksville.mesh.service.AndroidMeshWorkerManager import com.geeksville.mesh.service.MeshServiceNotificationsImpl -import com.geeksville.mesh.service.PacketHandler import com.geeksville.mesh.service.ServiceBroadcasts import dagger.Binds import dagger.Module @@ -39,11 +42,15 @@ interface ApplicationModule { @Binds fun bindMeshServiceNotifications(impl: MeshServiceNotificationsImpl): MeshServiceNotifications - @Binds fun bindPacketHandler(impl: PacketHandler): org.meshtastic.core.repository.PacketHandler + @Binds fun bindMeshLocationManager(impl: AndroidMeshLocationManager): org.meshtastic.core.repository.MeshLocationManager - @Binds fun bindServiceBroadcasts(impl: ServiceBroadcasts): org.meshtastic.core.repository.ServiceBroadcasts + @Binds fun bindMeshWorkerManager(impl: AndroidMeshWorkerManager): org.meshtastic.core.repository.MeshWorkerManager + + @Binds fun bindAppWidgetUpdater(impl: AndroidAppWidgetUpdater): org.meshtastic.core.repository.AppWidgetUpdater - @Binds fun bindMeshConnectionManager(impl: com.geeksville.mesh.service.MeshConnectionManager): org.meshtastic.core.repository.MeshConnectionManager + @Binds fun bindRadioInterfaceService(impl: AndroidRadioInterfaceService): org.meshtastic.core.repository.RadioInterfaceService + + @Binds fun bindServiceBroadcasts(impl: ServiceBroadcasts): org.meshtastic.core.repository.ServiceBroadcasts companion object { @Provides @ProcessLifecycle diff --git a/app/src/main/java/com/geeksville/mesh/domain/usecase/GetDiscoveredDevicesUseCase.kt b/app/src/main/java/com/geeksville/mesh/domain/usecase/GetDiscoveredDevicesUseCase.kt index 66048a265f..4b7a25c501 100644 --- a/app/src/main/java/com/geeksville/mesh/domain/usecase/GetDiscoveredDevicesUseCase.kt +++ b/app/src/main/java/com/geeksville/mesh/domain/usecase/GetDiscoveredDevicesUseCase.kt @@ -22,7 +22,6 @@ import com.geeksville.mesh.model.DeviceListEntry import com.geeksville.mesh.model.getMeshtasticShortName import com.geeksville.mesh.repository.network.NetworkRepository import com.geeksville.mesh.repository.network.NetworkRepository.Companion.toAddressString -import com.geeksville.mesh.repository.radio.RadioInterfaceService import com.geeksville.mesh.repository.usb.UsbRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine @@ -34,6 +33,7 @@ import org.meshtastic.core.datastore.RecentAddressesDataSource import org.meshtastic.core.datastore.model.RecentAddress import org.meshtastic.core.model.Node import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.RadioInterfaceService import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.meshtastic import java.util.Locale diff --git a/app/src/main/java/com/geeksville/mesh/model/DeviceListEntry.kt b/app/src/main/java/com/geeksville/mesh/model/DeviceListEntry.kt index 14e02c55af..d66d6fff0e 100644 --- a/app/src/main/java/com/geeksville/mesh/model/DeviceListEntry.kt +++ b/app/src/main/java/com/geeksville/mesh/model/DeviceListEntry.kt @@ -17,14 +17,14 @@ package com.geeksville.mesh.model import android.hardware.usb.UsbManager -import com.geeksville.mesh.repository.radio.InterfaceId -import com.geeksville.mesh.repository.radio.RadioInterfaceService import com.hoho.android.usbserial.driver.UsbSerialDriver import no.nordicsemi.kotlin.ble.client.android.Peripheral import no.nordicsemi.kotlin.ble.core.BondState import org.meshtastic.core.ble.MeshtasticBleConstants.BLE_NAME_PATTERN +import org.meshtastic.core.model.InterfaceId import org.meshtastic.core.model.Node import org.meshtastic.core.model.util.anonymize +import org.meshtastic.core.repository.RadioInterfaceService /** * A sealed class is used here to represent the different types of devices that can be displayed in the list. This is diff --git a/app/src/main/java/com/geeksville/mesh/model/UIViewModel.kt b/app/src/main/java/com/geeksville/mesh/model/UIViewModel.kt index d0bc742e92..a3511ca74a 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIViewModel.kt @@ -22,8 +22,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.NavHostController import co.touchlab.kermit.Logger -import com.geeksville.mesh.repository.radio.MeshActivity -import com.geeksville.mesh.repository.radio.RadioInterfaceService import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.Flow @@ -47,6 +45,7 @@ import org.meshtastic.core.data.repository.FirmwareReleaseRepository import org.meshtastic.core.data.repository.MeshLogRepository import org.meshtastic.core.database.entity.asDeviceVersion import org.meshtastic.core.datastore.UiPreferencesDataSource +import org.meshtastic.core.model.MeshActivity import org.meshtastic.core.model.MyNodeInfo import org.meshtastic.core.model.RadioController import org.meshtastic.core.model.TracerouteMapAvailability @@ -56,6 +55,7 @@ import org.meshtastic.core.model.util.dispatchMeshtasticUri import org.meshtastic.core.repository.MeshServiceNotifications import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.PacketRepository +import org.meshtastic.core.repository.RadioInterfaceService import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.client_notification import org.meshtastic.core.resources.compromised_keys diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/RadioInterfaceService.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/AndroidRadioInterfaceService.kt similarity index 88% rename from app/src/main/java/com/geeksville/mesh/repository/radio/RadioInterfaceService.kt rename to app/src/main/java/com/geeksville/mesh/repository/radio/AndroidRadioInterfaceService.kt index f7cf8fbd57..26e2e895e9 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/radio/RadioInterfaceService.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/AndroidRadioInterfaceService.kt @@ -49,8 +49,11 @@ 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 import org.meshtastic.core.model.util.anonymize import org.meshtastic.core.prefs.radio.RadioPrefs +import org.meshtastic.core.repository.RadioInterfaceService import org.meshtastic.proto.Heartbeat import org.meshtastic.proto.ToRadio import javax.inject.Inject @@ -67,7 +70,7 @@ import javax.inject.Singleton */ @Suppress("LongParameterList") @Singleton -open class RadioInterfaceService +class AndroidRadioInterfaceService @Inject constructor( private val context: Application, @@ -78,20 +81,20 @@ constructor( private val radioPrefs: RadioPrefs, private val interfaceFactory: InterfaceFactory, private val analytics: PlatformAnalytics, -) { +) : RadioInterfaceService { private val _connectionState = MutableStateFlow(ConnectionState.Disconnected) - val connectionState: StateFlow = _connectionState.asStateFlow() + override val connectionState: StateFlow = _connectionState.asStateFlow() private val _receivedData = MutableSharedFlow(extraBufferCapacity = 64) - val receivedData: SharedFlow = _receivedData + override val receivedData: SharedFlow = _receivedData private val _connectionError = MutableSharedFlow(extraBufferCapacity = 64) val connectionError: SharedFlow = _connectionError.asSharedFlow() // Thread-safe StateFlow for tracking device address changes private val _currentDeviceAddressFlow = MutableStateFlow(radioPrefs.devAddr) - val currentDeviceAddressFlow: StateFlow = _currentDeviceAddressFlow.asStateFlow() + override val currentDeviceAddressFlow: StateFlow = _currentDeviceAddressFlow.asStateFlow() private val logSends = false private val logReceives = false @@ -100,8 +103,11 @@ constructor( val mockInterfaceAddress: String by lazy { toInterfaceAddress(InterfaceId.MOCK, "") } + override val serviceScope: CoroutineScope + get() = _serviceScope + /** We recreate this scope each time we stop an interface */ - var serviceScope = CoroutineScope(dispatchers.io + SupervisorJob()) + private var _serviceScope = CoroutineScope(dispatchers.io + SupervisorJob()) private var radioIf: IRadioInterface = NopInterface("") @@ -165,10 +171,10 @@ constructor( } /** Constructs a full radio address for the specific interface type. */ - fun toInterfaceAddress(interfaceId: InterfaceId, rest: String): String = + override fun toInterfaceAddress(interfaceId: InterfaceId, rest: String): String = interfaceFactory.toInterfaceAddress(interfaceId, rest) - fun isMockInterface(): Boolean = + override fun isMockInterface(): Boolean = BuildConfig.DEBUG || Settings.System.getString(context.contentResolver, "firebase.test.lab") == "true" /** @@ -185,7 +191,7 @@ constructor( * where a is either x for bluetooth or s for serial and t is an interface specific address (macaddr or a device * path) */ - fun getDeviceAddress(): String? { + override fun getDeviceAddress(): String? { // If the user has unpaired our device, treat things as if we don't have one var address = radioPrefs.devAddr @@ -228,10 +234,10 @@ constructor( } // Handle an incoming packet from the radio, broadcasts it as an android intent - open fun handleFromRadio(p: ByteArray) { + override fun handleFromRadio(bytes: ByteArray) { if (logReceives) { try { - receivedPacketsLog.write(p) + receivedPacketsLog.write(bytes) receivedPacketsLog.flush() } catch (t: Throwable) { Logger.w(t) { "Failed to write receive log in handleFromRadio" } @@ -239,29 +245,33 @@ constructor( } try { - processLifecycle.coroutineScope.launch(dispatchers.io) { _receivedData.emit(p) } + processLifecycle.coroutineScope.launch(dispatchers.io) { _receivedData.emit(bytes) } emitReceiveActivity() } catch (t: Throwable) { Logger.e(t) { "RadioInterfaceService.handleFromRadio failed while emitting data" } } } - fun onConnect() { + override fun onConnect() { if (_connectionState.value != ConnectionState.Connected) { broadcastConnectionChanged(ConnectionState.Connected) } } - fun onDisconnect(isPermanent: Boolean) { + override fun onDisconnect(isPermanent: Boolean) { val newTargetState = if (isPermanent) ConnectionState.Disconnected else ConnectionState.DeviceSleep if (_connectionState.value != newTargetState) { broadcastConnectionChanged(newTargetState) } } - fun onDisconnect(error: BleError) { - processLifecycle.coroutineScope.launch(dispatchers.default) { _connectionError.emit(error) } - onDisconnect(!error.shouldReconnect) + override fun onDisconnect(error: Any) { + if (error is BleError) { + processLifecycle.coroutineScope.launch(dispatchers.default) { _connectionError.emit(error) } + onDisconnect(!error.shouldReconnect) + } else { + onDisconnect(isPermanent = true) + } } /** Start our configured interface (if it isn't already running) */ @@ -311,8 +321,8 @@ constructor( r.close() // cancel any old jobs and get ready for the new ones - serviceScope.cancel("stopping interface") - serviceScope = CoroutineScope(dispatchers.io + SupervisorJob()) + _serviceScope.cancel("stopping interface") + _serviceScope = CoroutineScope(dispatchers.io + SupervisorJob()) if (logSends) { sentPacketsLog.close() @@ -356,26 +366,26 @@ constructor( true } - fun setDeviceAddress(deviceAddr: String?): Boolean = toRemoteExceptions { setBondedDeviceAddress(deviceAddr) } + override fun setDeviceAddress(deviceAddr: String?): Boolean = toRemoteExceptions { setBondedDeviceAddress(deviceAddr) } /** * If the service is not currently connected to the radio, try to connect now. At boot the radio interface service * will not connect to a radio until this call is received. */ - fun connect() = toRemoteExceptions { + override fun connect() = toRemoteExceptions { // We don't start actually talking to our device until MeshService binds to us - this prevents // broadcasting connection events before MeshService is ready to receive them startInterface() initStateListeners() } - fun sendToRadio(a: ByteArray) { + override fun sendToRadio(bytes: ByteArray) { // Do this in the IO thread because it might take a while (and we don't care about the result code) - serviceScope.handledLaunch { handleSendToRadio(a) } + _serviceScope.handledLaunch { handleSendToRadio(bytes) } } private val _meshActivity = MutableSharedFlow(extraBufferCapacity = 64) - val meshActivity: SharedFlow = _meshActivity.asSharedFlow() + override val meshActivity: SharedFlow = _meshActivity.asSharedFlow() private fun emitSendActivity() { // Use tryEmit for SharedFlow as it's non-blocking @@ -392,9 +402,3 @@ constructor( } } } - -sealed class MeshActivity { - data object Send : MeshActivity() - - data object Receive : MeshActivity() -} diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceFactory.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceFactory.kt index ffb34c2a8f..68f9a7d4ee 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceFactory.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceFactory.kt @@ -17,6 +17,7 @@ package com.geeksville.mesh.repository.radio +import org.meshtastic.core.model.InterfaceId import javax.inject.Inject import javax.inject.Provider diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceMapKey.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceMapKey.kt index d6d6ae2ea8..842df6161a 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceMapKey.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceMapKey.kt @@ -18,6 +18,7 @@ package com.geeksville.mesh.repository.radio import dagger.MapKey +import org.meshtastic.core.model.InterfaceId /** * Dagger `MapKey` implementation allowing for `InterfaceId` to be used as a map key. diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/MockInterface.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/MockInterface.kt index 5b67d694f4..2dc509ed28 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/radio/MockInterface.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/MockInterface.kt @@ -27,6 +27,7 @@ import org.meshtastic.core.common.util.nowSeconds import org.meshtastic.core.model.Channel import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.util.getInitials +import org.meshtastic.core.repository.RadioInterfaceService import org.meshtastic.proto.AdminMessage import org.meshtastic.proto.Config import org.meshtastic.proto.Data diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/NordicBleInterface.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/NordicBleInterface.kt index 19e0471390..aa72dfdd45 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/radio/NordicBleInterface.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/NordicBleInterface.kt @@ -18,7 +18,6 @@ package com.geeksville.mesh.repository.radio import android.annotation.SuppressLint import co.touchlab.kermit.Logger -import com.geeksville.mesh.service.RadioNotConnectedException import dagger.assisted.Assisted import dagger.assisted.AssistedInject import kotlinx.coroutines.CompletableDeferred @@ -58,6 +57,8 @@ import org.meshtastic.core.ble.MeshtasticBleConstants.SERVICE_UUID import org.meshtastic.core.ble.MeshtasticBleConstants.TORADIO_CHARACTERISTIC import org.meshtastic.core.ble.retryBleOperation import org.meshtastic.core.common.util.nowMillis +import org.meshtastic.core.model.RadioNotConnectedException +import org.meshtastic.core.repository.RadioInterfaceService import kotlin.time.Duration.Companion.seconds private const val SCAN_RETRY_COUNT = 3 @@ -95,7 +96,7 @@ constructor( Logger.w(e) { "[$address] Failed to disconnect in exception handler" } } } - service.onDisconnect(BleError.from(throwable)) + service.onDisconnect(error = BleError.from(throwable)) } private val connectionScope: CoroutineScope = @@ -152,7 +153,7 @@ constructor( "Packet #$packetsReceived, ${packet.size} bytes (Total: $bytesReceived bytes)" } try { - service.handleFromRadio(p = packet) + service.handleFromRadio(packet) } catch (t: Throwable) { Logger.e(t) { "[$address] Failed to execute service.handleFromRadio()" } } @@ -256,7 +257,7 @@ constructor( "Packets RX: $packetsReceived ($bytesReceived bytes), " + "Packets TX: $packetsSent ($bytesSent bytes)" } - service.onDisconnect(BleError.Disconnected(reason = state.reason)) + service.onDisconnect(error = BleError.Disconnected(reason = state.reason)) } private suspend fun discoverServicesAndSetupCharacteristics() { @@ -286,12 +287,12 @@ constructor( service.onConnect() } else { Logger.w { "[$address] Discovery failed: missing required characteristics" } - service.onDisconnect(BleError.DiscoveryFailed("One or more characteristics not found")) + service.onDisconnect(error = BleError.DiscoveryFailed("One or more characteristics not found")) } } catch (e: Exception) { Logger.w(e) { "[$address] Service discovery failed" } bleConnection.disconnect() - service.onDisconnect(BleError.from(e)) + service.onDisconnect(error = BleError.from(e)) } } diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/RadioRepositoryModule.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/RadioRepositoryModule.kt index 6a1d91f1a4..d570c15aa5 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/radio/RadioRepositoryModule.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/RadioRepositoryModule.kt @@ -23,6 +23,7 @@ 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 diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/SerialInterface.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/SerialInterface.kt index 4ebaf85d57..04d67b879f 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/radio/SerialInterface.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/SerialInterface.kt @@ -23,6 +23,7 @@ import com.geeksville.mesh.repository.usb.UsbRepository import dagger.assisted.Assisted import dagger.assisted.AssistedInject import org.meshtastic.core.common.util.nowMillis +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 */ diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/StreamInterface.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/StreamInterface.kt index 538f4088a4..973c388381 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/radio/StreamInterface.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/StreamInterface.kt @@ -20,6 +20,7 @@ import co.touchlab.kermit.Logger import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import org.meshtastic.core.repository.RadioInterfaceService /** * An interface that assumes we are talking to a meshtastic device over some sort of stream connection (serial or TCP diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/TCPInterface.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/TCPInterface.kt index e2eeefa4c6..a6a8320a51 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/radio/TCPInterface.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/TCPInterface.kt @@ -26,6 +26,7 @@ import org.meshtastic.core.common.util.Exceptions import org.meshtastic.core.common.util.handledLaunch import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.di.CoroutineDispatchers +import org.meshtastic.core.repository.RadioInterfaceService import org.meshtastic.proto.Heartbeat import org.meshtastic.proto.ToRadio import java.io.BufferedInputStream diff --git a/app/src/main/java/com/geeksville/mesh/service/AndroidAppWidgetUpdater.kt b/app/src/main/java/com/geeksville/mesh/service/AndroidAppWidgetUpdater.kt new file mode 100644 index 0000000000..23bb9cfa5a --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/service/AndroidAppWidgetUpdater.kt @@ -0,0 +1,41 @@ +/* + * 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 . + */ +package com.geeksville.mesh.service + +import android.content.Context +import androidx.glance.appwidget.updateAll +import com.geeksville.mesh.widget.LocalStatsWidget +import dagger.hilt.android.qualifiers.ApplicationContext +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 { + override suspend fun updateAll() { + // Kickstart the widget composition. + // The widget internally uses collectAsState() and its own sampled StateFlow + // to drive updates automatically without excessive IPC and recreation. + try { + LocalStatsWidget().updateAll(context) + } catch (e: Exception) { + co.touchlab.kermit.Logger.e(e) { "Failed to update widgets" } + } + } +} diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshLocationManager.kt b/app/src/main/java/com/geeksville/mesh/service/AndroidMeshLocationManager.kt similarity index 93% rename from app/src/main/java/com/geeksville/mesh/service/MeshLocationManager.kt rename to app/src/main/java/com/geeksville/mesh/service/AndroidMeshLocationManager.kt index 482424a5e6..7ab35c1519 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshLocationManager.kt +++ b/app/src/main/java/com/geeksville/mesh/service/AndroidMeshLocationManager.kt @@ -29,23 +29,24 @@ import kotlinx.coroutines.flow.onEach import org.meshtastic.core.common.hasLocationPermission import org.meshtastic.core.data.repository.LocationRepository import org.meshtastic.core.model.Position +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 MeshLocationManager +class AndroidMeshLocationManager @Inject constructor( private val context: Application, private val locationRepository: LocationRepository, -) { +) : MeshLocationManager { private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private var locationFlow: Job? = null @SuppressLint("MissingPermission") - fun start(scope: CoroutineScope, sendPositionFn: (ProtoPosition) -> Unit) { + override fun start(scope: CoroutineScope, sendPositionFn: (ProtoPosition) -> Unit) { this.scope = scope if (locationFlow?.isActive == true) return @@ -76,7 +77,7 @@ constructor( } } - fun stop() { + override fun stop() { if (locationFlow?.isActive == true) { Logger.i { "Stopping location requests" } locationFlow?.cancel() diff --git a/app/src/main/java/com/geeksville/mesh/service/AndroidMeshWorkerManager.kt b/app/src/main/java/com/geeksville/mesh/service/AndroidMeshWorkerManager.kt new file mode 100644 index 0000000000..ae9985d9a7 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/service/AndroidMeshWorkerManager.kt @@ -0,0 +1,43 @@ +/* + * 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 . + */ +package com.geeksville.mesh.service + +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.workDataOf +import org.meshtastic.core.repository.MeshWorkerManager +import org.meshtastic.feature.messaging.domain.worker.SendMessageWorker +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class AndroidMeshWorkerManager @Inject constructor( + private val workManager: WorkManager +) : MeshWorkerManager { + override fun enqueueSendMessage(packetId: Int) { + val workRequest = OneTimeWorkRequestBuilder() + .setInputData(workDataOf(SendMessageWorker.KEY_PACKET_ID to packetId)) + .build() + + workManager.enqueueUniqueWork( + "${SendMessageWorker.WORK_NAME_PREFIX}$packetId", + ExistingWorkPolicy.REPLACE, + workRequest + ) + } +} diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt index 82cea8ebc9..cf97cd5c24 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -25,7 +25,6 @@ import android.os.IBinder import androidx.core.app.ServiceCompat import co.touchlab.kermit.Logger import com.geeksville.mesh.BuildConfig -import com.geeksville.mesh.repository.radio.RadioInterfaceService import com.geeksville.mesh.ui.connections.NO_DEVICE_SELECTED import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope @@ -42,14 +41,17 @@ import org.meshtastic.core.model.MeshUser import org.meshtastic.core.model.MyNodeInfo import org.meshtastic.core.model.NodeInfo import org.meshtastic.core.model.Position +import org.meshtastic.core.model.RadioNotConnectedException import org.meshtastic.core.repository.CommandSender import org.meshtastic.core.repository.MeshConnectionManager +import org.meshtastic.core.repository.MeshLocationManager import org.meshtastic.core.repository.MeshMessageProcessor 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 @@ -95,7 +97,7 @@ class MeshService : Service() { fun actionReceived(portNum: Int): String { val portType = PortNum.fromValue(portNum) val portStr = portType?.toString() ?: portNum.toString() - return com.geeksville.mesh.service.actionReceived(portStr) + return actionReceived(portStr) } fun createIntent(context: Context) = Intent(context, MeshService::class.java) diff --git a/app/src/main/java/com/geeksville/mesh/ui/Main.kt b/app/src/main/java/com/geeksville/mesh/ui/Main.kt index 56f942b8b7..9aee7f945a 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Main.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Main.kt @@ -85,7 +85,6 @@ import com.geeksville.mesh.navigation.firmwareGraph import com.geeksville.mesh.navigation.mapGraph import com.geeksville.mesh.navigation.nodesGraph import com.geeksville.mesh.navigation.settingsGraph -import com.geeksville.mesh.repository.radio.MeshActivity import com.geeksville.mesh.service.MeshService import com.geeksville.mesh.ui.connections.DeviceType import com.geeksville.mesh.ui.connections.ScannerViewModel @@ -98,6 +97,7 @@ import org.jetbrains.compose.resources.getString import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.DeviceVersion +import org.meshtastic.core.model.MeshActivity import org.meshtastic.core.navigation.ConnectionsRoutes import org.meshtastic.core.navigation.ContactsRoutes import org.meshtastic.core.navigation.MapRoutes diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/ScannerViewModel.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/ScannerViewModel.kt index 37e753c332..7e736b0f15 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/connections/ScannerViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/connections/ScannerViewModel.kt @@ -24,7 +24,6 @@ import co.touchlab.kermit.Logger import co.touchlab.kermit.Severity import com.geeksville.mesh.domain.usecase.GetDiscoveredDevicesUseCase import com.geeksville.mesh.model.DeviceListEntry -import com.geeksville.mesh.repository.radio.RadioInterfaceService import com.geeksville.mesh.repository.usb.UsbRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow @@ -42,6 +41,7 @@ import org.meshtastic.core.datastore.RecentAddressesDataSource import org.meshtastic.core.datastore.model.RecentAddress import org.meshtastic.core.model.RadioController 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 diff --git a/app/src/test/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceRetryTest.kt b/app/src/test/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceRetryTest.kt index eb4ac385d1..41cceafe24 100644 --- a/app/src/test/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceRetryTest.kt +++ b/app/src/test/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceRetryTest.kt @@ -44,6 +44,7 @@ import org.meshtastic.core.ble.MeshtasticBleConstants.FROMRADIO_CHARACTERISTIC import org.meshtastic.core.ble.MeshtasticBleConstants.LOGRADIO_CHARACTERISTIC import org.meshtastic.core.ble.MeshtasticBleConstants.SERVICE_UUID import org.meshtastic.core.ble.MeshtasticBleConstants.TORADIO_CHARACTERISTIC +import org.meshtastic.core.repository.RadioInterfaceService import kotlin.time.Duration.Companion.milliseconds @OptIn(ExperimentalCoroutinesApi::class) diff --git a/app/src/test/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceTest.kt b/app/src/test/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceTest.kt index 2974d30295..1ee5ff9eed 100644 --- a/app/src/test/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceTest.kt +++ b/app/src/test/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceTest.kt @@ -47,6 +47,7 @@ import org.meshtastic.core.ble.MeshtasticBleConstants.FROMRADIO_CHARACTERISTIC import org.meshtastic.core.ble.MeshtasticBleConstants.LOGRADIO_CHARACTERISTIC import org.meshtastic.core.ble.MeshtasticBleConstants.SERVICE_UUID import org.meshtastic.core.ble.MeshtasticBleConstants.TORADIO_CHARACTERISTIC +import org.meshtastic.core.repository.RadioInterfaceService import kotlin.time.Duration.Companion.milliseconds @OptIn(ExperimentalCoroutinesApi::class) @@ -662,7 +663,7 @@ class NordicBleInterfaceTest { advanceUntilIdle() // Verify handleFromRadio was called directly with the payload - verify(timeout = 2000) { service.handleFromRadio(p = payload) } + verify(timeout = 2000) { service.handleFromRadio(payload) } nordicInterface.close() } diff --git a/app/src/test/java/com/geeksville/mesh/repository/radio/StreamInterfaceTest.kt b/app/src/test/java/com/geeksville/mesh/repository/radio/StreamInterfaceTest.kt index b0ddc037ea..868c5197f3 100644 --- a/app/src/test/java/com/geeksville/mesh/repository/radio/StreamInterfaceTest.kt +++ b/app/src/test/java/com/geeksville/mesh/repository/radio/StreamInterfaceTest.kt @@ -20,6 +20,7 @@ import io.mockk.confirmVerified import io.mockk.mockk import io.mockk.verify import org.junit.Test +import org.meshtastic.core.repository.RadioInterfaceService class StreamInterfaceTest { diff --git a/app/src/test/java/com/geeksville/mesh/service/Fakes.kt b/app/src/test/java/com/geeksville/mesh/service/Fakes.kt index 728adc5f91..86ecc7fb93 100644 --- a/app/src/test/java/com/geeksville/mesh/service/Fakes.kt +++ b/app/src/test/java/com/geeksville/mesh/service/Fakes.kt @@ -17,10 +17,10 @@ package com.geeksville.mesh.service import android.app.Notification -import com.geeksville.mesh.repository.radio.RadioInterfaceService import io.mockk.mockk import org.meshtastic.core.model.Node import org.meshtastic.core.repository.MeshServiceNotifications +import org.meshtastic.core.repository.RadioInterfaceService import org.meshtastic.proto.ClientNotification import org.meshtastic.proto.Telemetry diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt index 5d260ab541..5b8757bcff 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt @@ -28,6 +28,7 @@ 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 @@ -35,6 +36,7 @@ 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.NodeRepositoryImpl @@ -48,6 +50,7 @@ import org.meshtastic.core.repository.HistoryManager 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.MeshMessageProcessor import org.meshtastic.core.repository.MeshRouter @@ -56,6 +59,7 @@ 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 @@ -125,6 +129,18 @@ abstract class RepositoryModule { mqttManagerImpl: MqttManagerImpl ): MqttManager + @Binds + @Singleton + abstract fun bindPacketHandler( + packetHandlerImpl: PacketHandlerImpl + ): PacketHandler + + @Binds + @Singleton + abstract fun bindMeshConnectionManager( + meshConnectionManagerImpl: MeshConnectionManagerImpl + ): MeshConnectionManager + @Binds @Singleton abstract fun bindMeshDataHandler( diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImpl.kt index 668d254f31..0578a0810d 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImpl.kt @@ -16,6 +16,7 @@ */ package org.meshtastic.core.data.manager +import dagger.Lazy import org.meshtastic.core.repository.FromRadioPacketHandler import org.meshtastic.core.repository.MeshRouter import org.meshtastic.core.repository.MeshServiceNotifications @@ -32,7 +33,7 @@ import javax.inject.Singleton @Singleton class FromRadioPacketHandlerImpl @Inject constructor( private val serviceRepository: ServiceRepository, - private val router: MeshRouter, + private val router: Lazy, private val mqttManager: MqttManager, private val packetHandler: PacketHandler, private val serviceNotifications: MeshServiceNotifications, @@ -51,18 +52,18 @@ class FromRadioPacketHandlerImpl @Inject constructor( val clientNotification = proto.clientNotification when { - myInfo != null -> router.configFlowManager.handleMyInfo(myInfo) - metadata != null -> router.configFlowManager.handleLocalMetadata(metadata) + myInfo != null -> router.get().configFlowManager.handleMyInfo(myInfo) + metadata != null -> router.get().configFlowManager.handleLocalMetadata(metadata) nodeInfo != null -> { - router.configFlowManager.handleNodeInfo(nodeInfo) - serviceRepository.setConnectionProgress("Nodes (${router.configFlowManager.newNodeCount})") + router.get().configFlowManager.handleNodeInfo(nodeInfo) + serviceRepository.setConnectionProgress("Nodes (${router.get().configFlowManager.newNodeCount})") } - configCompleteId != null -> router.configFlowManager.handleConfigComplete(configCompleteId) + configCompleteId != null -> router.get().configFlowManager.handleConfigComplete(configCompleteId) mqttProxyMessage != null -> mqttManager.handleMqttProxyMessage(mqttProxyMessage) queueStatus != null -> packetHandler.handleQueueStatus(queueStatus) - config != null -> router.configHandler.handleDeviceConfig(config) - moduleConfig != null -> router.configHandler.handleModuleConfig(moduleConfig) - channel != null -> router.configHandler.handleChannel(channel) + config != null -> router.get().configHandler.handleDeviceConfig(config) + moduleConfig != null -> router.get().configHandler.handleModuleConfig(moduleConfig) + channel != null -> router.get().configHandler.handleChannel(channel) clientNotification != null -> { serviceRepository.setClientNotification(clientNotification) serviceNotifications.showClientNotification(clientNotification) diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImpl.kt index 2090fd937a..681a5c39fa 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImpl.kt @@ -59,7 +59,7 @@ class MeshActionHandlerImpl @Inject constructor( private val commandSender: CommandSender, private val packetRepository: Lazy, private val serviceBroadcasts: ServiceBroadcasts, - private val dataHandler: MeshDataHandler, + private val dataHandler: Lazy, private val analytics: PlatformAnalytics, private val meshPrefs: MeshPrefs, private val databaseManager: DatabaseManager, @@ -152,7 +152,7 @@ class MeshActionHandlerImpl @Inject constructor( val verifiedContact = action.contact.copy(manually_verified = true) commandSender.sendAdmin(myNodeNum) { AdminMessage(add_contact = verifiedContact) } nodeManager.handleReceivedUser( - verifiedContact.node_num ?: 0, + verifiedContact.node_num, verifiedContact.user ?: User(), manuallyVerified = true, ) @@ -188,7 +188,7 @@ class MeshActionHandlerImpl @Inject constructor( override fun handleSend(p: DataPacket, myNodeNum: Int) { commandSender.sendData(p) serviceBroadcasts.broadcastMessageStatus(p.id, p.status ?: MessageStatus.UNKNOWN) - dataHandler.rememberDataPacket(p, myNodeNum, false) + dataHandler.get().rememberDataPacket(p, myNodeNum, false) val bytes = p.bytes ?: okio.ByteString.EMPTY analytics.track("data_send", DataPair("num_bytes", bytes.size), DataPair("type", p.dataType)) } diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.kt index a2d3d236d4..10332c1b05 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.kt @@ -17,6 +17,7 @@ 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 @@ -48,7 +49,7 @@ import org.meshtastic.proto.MyNodeInfo as ProtoMyNodeInfo @Singleton class MeshConfigFlowManagerImpl @Inject constructor( private val nodeManager: NodeManager, - private val connectionManager: MeshConnectionManager, + private val connectionManager: Lazy, private val nodeRepository: NodeRepository, private val radioConfigRepository: RadioConfigRepository, private val serviceRepository: ServiceRepository, @@ -98,7 +99,7 @@ class MeshConfigFlowManagerImpl @Inject constructor( } else { myNodeInfo = finalizedInfo Logger.i { "myNodeInfo committed successfully (nodeNum=${finalizedInfo.myNodeNum})" } - connectionManager.onRadioConfigLoaded() + connectionManager.get().onRadioConfigLoaded() } scope.handledLaunch { @@ -106,7 +107,7 @@ class MeshConfigFlowManagerImpl @Inject constructor( sendHeartbeat() delay(wantConfigDelay) Logger.i { "Requesting NodeInfo (Stage 2)" } - connectionManager.startNodeInfoOnly() + connectionManager.get().startNodeInfoOnly() } } @@ -137,7 +138,7 @@ class MeshConfigFlowManagerImpl @Inject constructor( nodeManager.setAllowNodeDbWrites(true) serviceRepository.setConnectionState(ConnectionState.Connected) serviceBroadcasts.broadcastConnection() - connectionManager.onNodeDbReady() + connectionManager.get().onNodeDbReady() } } @@ -169,7 +170,7 @@ class MeshConfigFlowManagerImpl @Inject constructor( } override fun triggerWantConfig() { - connectionManager.startConfigOnly() + connectionManager.get().startConfigOnly() } private fun regenMyNodeInfo(metadata: DeviceMetadata? = null) { @@ -179,7 +180,7 @@ class MeshConfigFlowManagerImpl @Inject constructor( val mi = with(myInfo) { SharedMyNodeInfo( - myNodeNum = my_node_num ?: 0, + myNodeNum = my_node_num, hasGPS = false, model = when (val hwModel = metadata?.hw_model) { diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConfigHandlerImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConfigHandlerImpl.kt index 78441a2cd1..ff58a93b3e 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConfigHandlerImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConfigHandlerImpl.kt @@ -76,7 +76,7 @@ class MeshConfigHandlerImpl @Inject constructor( // Update status message if we have node info, otherwise use a generic one val mi = nodeManager.getMyNodeInfo() - val index = channel.index ?: 0 + val index = channel.index if (mi != null) { serviceRepository.setConnectionProgress("Channels (${index + 1} / ${mi.maxChannels})") } else { diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt similarity index 90% rename from app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt rename to core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt index a076f5be9c..12aaca39ba 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 @@ -14,19 +14,9 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.geeksville.mesh.service - -import android.app.Notification -import android.content.Context -import androidx.glance.appwidget.updateAll -import androidx.work.ExistingWorkPolicy -import androidx.work.OneTimeWorkRequestBuilder -import androidx.work.WorkManager -import androidx.work.workDataOf +package org.meshtastic.core.data.manager + import co.touchlab.kermit.Logger -import com.geeksville.mesh.repository.radio.RadioInterfaceService -import com.geeksville.mesh.widget.LocalStatsWidget -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -46,16 +36,20 @@ import org.meshtastic.core.common.util.nowSeconds import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.TelemetryType import org.meshtastic.core.prefs.ui.UiPrefs +import org.meshtastic.core.repository.AppWidgetUpdater import org.meshtastic.core.repository.CommandSender import org.meshtastic.core.repository.HistoryManager import org.meshtastic.core.repository.MeshConnectionManager +import org.meshtastic.core.repository.MeshLocationManager import org.meshtastic.core.repository.MeshServiceNotifications +import org.meshtastic.core.repository.MeshWorkerManager import org.meshtastic.core.repository.MqttManager 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.RadioInterfaceService import org.meshtastic.core.repository.ServiceBroadcasts import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.core.resources.Res @@ -65,7 +59,6 @@ import org.meshtastic.core.resources.device_sleeping import org.meshtastic.core.resources.disconnected import org.meshtastic.core.resources.getString import org.meshtastic.core.resources.meshtastic_app_name -import org.meshtastic.feature.messaging.domain.worker.SendMessageWorker import org.meshtastic.proto.AdminMessage import org.meshtastic.proto.Config import org.meshtastic.proto.Telemetry @@ -78,10 +71,7 @@ import kotlin.time.DurationUnit @Suppress("LongParameterList", "TooManyFunctions") @Singleton -class MeshConnectionManager -@Inject -constructor( - @ApplicationContext private val context: Context, +class MeshConnectionManagerImpl @Inject constructor( private val radioInterfaceService: RadioInterfaceService, private val serviceRepository: ServiceRepository, private val serviceBroadcasts: ServiceBroadcasts, @@ -97,7 +87,8 @@ constructor( private val nodeManager: NodeManager, private val analytics: PlatformAnalytics, private val packetRepository: PacketRepository, - private val workManager: WorkManager, + private val workerManager: MeshWorkerManager, + private val appWidgetUpdater: AppWidgetUpdater, ) : MeshConnectionManager { private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private var sleepTimeout: Job? = null @@ -113,11 +104,9 @@ constructor( // Ensure notification title and content stay in sync with state changes serviceRepository.connectionState.onEach { updateStatusNotification() }.launchIn(scope) - // Kickstart the widget composition. The widget internally uses collectAsState() - // and its own sampled StateFlow to drive updates automatically without excessive IPC and recreation. scope.launch { try { - LocalStatsWidget().updateAll(context) + appWidgetUpdater.updateAll() } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { Logger.e(e) { "Failed to kickstart LocalStatsWidget" } } @@ -275,15 +264,7 @@ constructor( val queuedPackets = packetRepository.getQueuedPackets() ?: emptyList() queuedPackets.forEach { packet -> try { - val workRequest = OneTimeWorkRequestBuilder() - .setInputData(workDataOf(SendMessageWorker.KEY_PACKET_ID to packet.id)) - .build() - - workManager.enqueueUniqueWork( - "${SendMessageWorker.WORK_NAME_PREFIX}${packet.id}", - ExistingWorkPolicy.REPLACE, - workRequest - ) + workerManager.enqueueSendMessage(packet.id) } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { Logger.e(e) { "Failed to enqueue queued packet worker" } } @@ -341,7 +322,7 @@ constructor( updateStatusNotification(t) } - override fun updateStatusNotification(telemetry: Telemetry?): Notification { + override fun updateStatusNotification(telemetry: Telemetry?): Any { val summary = when (serviceRepository.connectionState.value) { is ConnectionState.Connected -> @@ -350,7 +331,7 @@ constructor( is ConnectionState.DeviceSleep -> getString(Res.string.device_sleeping) is ConnectionState.Connecting -> getString(Res.string.connecting) } - return serviceNotifications.updateServiceStateNotification(summary, telemetry = telemetry) as Notification + return serviceNotifications.updateServiceStateNotification(summary, telemetry = telemetry) } companion object { diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt index 262cd67eff..ea8ce6cf14 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt @@ -90,12 +90,12 @@ class MeshDataHandlerImpl @Inject constructor( private val serviceNotifications: MeshServiceNotifications, private val analytics: PlatformAnalytics, private val dataMapper: MeshDataMapper, - private val configHandler: MeshConfigHandler, - private val configFlowManager: MeshConfigFlowManager, + private val configHandler: Lazy, + private val configFlowManager: Lazy, private val commandSender: CommandSender, private val historyManager: HistoryManager, private val meshPrefs: MeshPrefs, - private val connectionManager: MeshConnectionManager, + private val connectionManager: Lazy, private val tracerouteHandler: TracerouteHandler, private val neighborInfoHandler: NeighborInfoHandler, private val radioConfigRepository: RadioConfigRepository, @@ -347,20 +347,20 @@ class MeshDataHandlerImpl @Inject constructor( val fromNum = packet.from u.get_module_config_response?.let { config -> if (fromNum == myNodeNum) { - configHandler.handleModuleConfig(config) + configHandler.get().handleModuleConfig(config) } else { config.statusmessage?.node_status?.let { nodeManager.updateNodeStatus(fromNum, it) } } } if (fromNum == myNodeNum) { - u.get_config_response?.let { configHandler.handleDeviceConfig(it) } - u.get_channel_response?.let { configHandler.handleChannel(it) } + u.get_config_response?.let { configHandler.get().handleDeviceConfig(it) } + u.get_channel_response?.let { configHandler.get().handleChannel(it) } } u.get_device_metadata_response?.let { metadata -> if (fromNum == myNodeNum) { - configFlowManager.handleLocalMetadata(metadata) + configFlowManager.get().handleLocalMetadata(metadata) } else { nodeManager.insertMetadata(fromNum, metadata) } @@ -402,7 +402,7 @@ class MeshDataHandlerImpl @Inject constructor( val fromNum = packet.from val isRemote = (fromNum != myNodeNum) if (!isRemote) { - connectionManager.updateTelemetry(t) + connectionManager.get().updateTelemetry(t) } nodeManager.updateNode(fromNum) { node: Node -> diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt index db5d8435c7..37e3ad1b05 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt @@ -56,7 +56,7 @@ class MeshMessageProcessorImpl @Inject constructor( private val nodeManager: NodeManager, private val serviceRepository: ServiceRepository, private val meshLogRepository: Lazy, - private val router: MeshRouter, + private val router: Lazy, private val fromRadioDispatcher: FromRadioPacketHandler, ) : MeshMessageProcessor { private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) @@ -230,7 +230,7 @@ class MeshMessageProcessorImpl @Inject constructor( } try { - router.dataHandler.handleReceivedData(packet, myNum, log.uuid, logJob) + router.get().dataHandler.handleReceivedData(packet, myNum, log.uuid, logJob) } finally { logUuidByPacketId.remove(packet.id) logInsertJobByPacketId.remove(packet.id) diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshRouterImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshRouterImpl.kt index 33ad464459..ba1d923442 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshRouterImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshRouterImpl.kt @@ -16,6 +16,7 @@ */ package org.meshtastic.core.data.manager +import dagger.Lazy import kotlinx.coroutines.CoroutineScope import org.meshtastic.core.repository.MeshActionHandler import org.meshtastic.core.repository.MeshConfigFlowManager @@ -34,14 +35,22 @@ import javax.inject.Singleton @Suppress("LongParameterList") @Singleton class MeshRouterImpl @Inject constructor( - override val dataHandler: MeshDataHandler, - override val configHandler: MeshConfigHandler, - override val tracerouteHandler: TracerouteHandler, - override val neighborInfoHandler: NeighborInfoHandler, - override val configFlowManager: MeshConfigFlowManager, - override val mqttManager: MqttManager, - override val actionHandler: MeshActionHandler, + private val _dataHandler: Lazy, + private val _configHandler: Lazy, + private val _tracerouteHandler: Lazy, + private val _neighborInfoHandler: Lazy, + private val _configFlowManager: Lazy, + private val _mqttManager: Lazy, + private val _actionHandler: Lazy, ) : MeshRouter { + override val dataHandler: MeshDataHandler get() = _dataHandler.get() + override val configHandler: MeshConfigHandler get() = _configHandler.get() + override val tracerouteHandler: TracerouteHandler get() = _tracerouteHandler.get() + override val neighborInfoHandler: NeighborInfoHandler get() = _neighborInfoHandler.get() + override val configFlowManager: MeshConfigFlowManager get() = _configFlowManager.get() + override val mqttManager: MqttManager get() = _mqttManager.get() + override val actionHandler: MeshActionHandler get() = _actionHandler.get() + override fun start(scope: CoroutineScope) { dataHandler.start(scope) configHandler.start(scope) diff --git a/app/src/main/java/com/geeksville/mesh/service/PacketHandler.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt similarity index 83% rename from app/src/main/java/com/geeksville/mesh/service/PacketHandler.kt rename to core/data/src/main/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt index 644496c5c1..11b139195b 100644 --- a/app/src/main/java/com/geeksville/mesh/service/PacketHandler.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 @@ -14,10 +14,9 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.geeksville.mesh.service +package org.meshtastic.core.data.manager import co.touchlab.kermit.Logger -import com.geeksville.mesh.repository.radio.RadioInterfaceService import dagger.Lazy import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope @@ -34,9 +33,12 @@ import org.meshtastic.core.database.entity.MeshLog import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.MessageStatus +import org.meshtastic.core.model.RadioNotConnectedException import org.meshtastic.core.model.util.toOneLineString import org.meshtastic.core.model.util.toPIIString +import org.meshtastic.core.repository.PacketHandler import org.meshtastic.core.repository.PacketRepository +import org.meshtastic.core.repository.RadioInterfaceService import org.meshtastic.core.repository.ServiceBroadcasts import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.proto.FromRadio @@ -50,22 +52,19 @@ import javax.inject.Singleton import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds import kotlin.uuid.Uuid -import org.meshtastic.core.repository.PacketHandler as SharedPacketHandler @Suppress("TooManyFunctions") @Singleton -class PacketHandler -@Inject -constructor( +class PacketHandlerImpl @Inject constructor( private val packetRepository: Lazy, private val serviceBroadcasts: ServiceBroadcasts, private val radioInterfaceService: RadioInterfaceService, private val meshLogRepository: Lazy, private val serviceRepository: ServiceRepository, -) : SharedPacketHandler { +) : PacketHandler { companion object { - private val TIMEOUT = 5.seconds // Increased from 250ms to be more tolerant + private val TIMEOUT = 5.seconds } private var queueJob: Job? = null @@ -78,10 +77,6 @@ constructor( this.scope = scope } - /** - * Send a command/packet to our radio. But cope with the possibility that we might start up before we are fully - * bound to the RadioInterfaceService - */ override fun sendToRadio(p: ToRadio) { Logger.d { "Sending to radio ${p.toPIIString()}" } val b = p.encode() @@ -97,7 +92,7 @@ constructor( message_type = "Packet", received_date = nowMillis, raw_message = packet.toString(), - fromNum = MeshLog.NODE_NUM_LOCAL, // Outgoing packets are always from the local node + fromNum = MeshLog.NODE_NUM_LOCAL, portNum = packet.decoded?.portnum?.value ?: 0, fromRadio = FromRadio(packet = packet), ) @@ -105,10 +100,6 @@ constructor( } } - /** - * Send a mesh packet to the radio, if the radio is not currently connected this function will throw - * NotConnectedException - */ override fun sendToRadio(packet: MeshPacket) { queuedPackets.add(packet) startPacketQueue() @@ -128,11 +119,10 @@ constructor( override fun handleQueueStatus(queueStatus: QueueStatus) { Logger.d { "[queueStatus] ${queueStatus.toOneLineString()}" } val (success, isFull, requestId) = with(queueStatus) { Triple(res == 0, free == 0, mesh_packet_id) } - if (success && isFull) return // Queue is full, wait for free != 0 + if (success && isFull) return if (requestId != 0) { queueResponse.remove(requestId)?.complete(success) } else { - // This is slightly suboptimal but matches legacy behavior for packets without IDs queueResponse.values.firstOrNull { !it.isCompleted }?.complete(success) } } @@ -147,10 +137,8 @@ constructor( scope.handledLaunch { Logger.d { "packet queueJob started" } while (serviceRepository.connectionState.value == ConnectionState.Connected) { - // take the first packet from the queue head val packet = queuedPackets.poll() ?: break try { - // send packet to the radio and wait for response val response = sendPacket(packet) Logger.d { "queueJob packet id=${packet.id.toUInt()} waiting" } val success = withTimeout(TIMEOUT) { response.await() } @@ -166,7 +154,6 @@ constructor( } } - /** Change the status on a DataPacket and update watchers */ private fun changeStatus(packetId: Int, m: MessageStatus) = scope.handledLaunch { if (packetId != 0) { getDataPacketById(packetId)?.let { p -> @@ -177,7 +164,6 @@ constructor( } } - @Suppress("MagicNumber") private suspend fun getDataPacketById(packetId: Int): DataPacket? = withTimeoutOrNull(1.seconds) { var dataPacket: DataPacket? = null while (dataPacket == null) { @@ -187,10 +173,7 @@ constructor( dataPacket } - @Suppress("TooGenericExceptionCaught") private fun sendPacket(packet: MeshPacket): CompletableDeferred { - // send the packet to the radio and return a CompletableDeferred that will be completed with - // the result val deferred = CompletableDeferred() queueResponse[packet.id] = deferred try { @@ -199,7 +182,6 @@ constructor( } sendToRadio(ToRadio(packet = packet)) } catch (ex: RadioNotConnectedException) { - // Expected when radio is not connected, log as warning to avoid Crashlytics noise Logger.w(ex) { "sendToRadio skipped: Not connected to radio" } deferred.complete(false) } catch (ex: Exception) { @@ -211,8 +193,6 @@ constructor( private fun insertMeshLog(packetToSave: MeshLog) { scope.handledLaunch { - // Do not log, because might contain PII - Logger.d { "insert: ${packetToSave.message_type} = " + "${packetToSave.raw_message.toOneLineString()} from=${packetToSave.fromNum}" diff --git a/core/data/src/test/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImplTest.kt b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImplTest.kt index 59b3e5c17f..47680aa02b 100644 --- a/core/data/src/test/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImplTest.kt +++ b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImplTest.kt @@ -45,7 +45,7 @@ class FromRadioPacketHandlerImplTest { @Before fun setup() { - handler = FromRadioPacketHandlerImpl(serviceRepository, router, mqttManager, packetHandler, serviceNotifications) + handler = FromRadioPacketHandlerImpl(serviceRepository, { router }, mqttManager, packetHandler, serviceNotifications) } @Test diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshConnectionManagerTest.kt b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImplTest.kt similarity index 84% rename from app/src/test/java/com/geeksville/mesh/service/MeshConnectionManagerTest.kt rename to core/data/src/test/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImplTest.kt index 8b1ae7bf83..c21b43c698 100644 --- a/app/src/test/java/com/geeksville/mesh/service/MeshConnectionManagerTest.kt +++ b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImplTest.kt @@ -14,16 +14,8 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.geeksville.mesh.service - -import android.app.Notification -import android.content.Context -import androidx.glance.appwidget.GlanceAppWidget -import androidx.glance.appwidget.updateAll -import androidx.work.ExistingWorkPolicy -import androidx.work.OneTimeWorkRequest -import androidx.work.WorkManager -import com.geeksville.mesh.repository.radio.RadioInterfaceService +package org.meshtastic.core.data.manager + import io.mockk.coEvery import io.mockk.every import io.mockk.mockk @@ -45,17 +37,22 @@ import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.MyNodeInfo import org.meshtastic.core.model.Node import org.meshtastic.core.prefs.ui.UiPrefs +import org.meshtastic.core.repository.AppWidgetUpdater import org.meshtastic.core.repository.CommandSender import org.meshtastic.core.repository.HistoryManager +import org.meshtastic.core.repository.MeshLocationManager import org.meshtastic.core.repository.MeshServiceNotifications +import org.meshtastic.core.repository.MeshWorkerManager import org.meshtastic.core.repository.MqttManager 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.RadioInterfaceService import org.meshtastic.core.repository.ServiceBroadcasts import org.meshtastic.core.repository.ServiceRepository -import org.meshtastic.feature.messaging.domain.worker.SendMessageWorker +import org.meshtastic.core.resources.getString import org.meshtastic.proto.Config import org.meshtastic.proto.LocalConfig import org.meshtastic.proto.LocalModuleConfig @@ -63,9 +60,8 @@ import org.meshtastic.proto.LocalStats import org.meshtastic.proto.ModuleConfig import org.meshtastic.proto.ToRadio -class MeshConnectionManagerTest { +class MeshConnectionManagerImplTest { - private val context: Context = mockk(relaxed = true) private val radioInterfaceService: RadioInterfaceService = mockk(relaxed = true) private val serviceRepository: ServiceRepository = mockk(relaxed = true) private val serviceBroadcasts: ServiceBroadcasts = mockk(relaxed = true) @@ -81,7 +77,9 @@ class MeshConnectionManagerTest { private val nodeManager: NodeManager = mockk(relaxed = true) private val analytics: PlatformAnalytics = mockk(relaxed = true) private val packetRepository: PacketRepository = mockk(relaxed = true) - private val workManager: WorkManager = mockk(relaxed = true) + private val workerManager: MeshWorkerManager = mockk(relaxed = true) + private val appWidgetUpdater: AppWidgetUpdater = mockk(relaxed = true) + private val radioConnectionState = MutableStateFlow(ConnectionState.Disconnected) private val connectionStateFlow = MutableStateFlow(ConnectionState.Disconnected) private val localConfigFlow = MutableStateFlow(LocalConfig()) @@ -89,15 +87,13 @@ class MeshConnectionManagerTest { private val testDispatcher = UnconfinedTestDispatcher() - private lateinit var manager: MeshConnectionManager + private lateinit var manager: MeshConnectionManagerImpl @Before fun setUp() { - mockkStatic("org.jetbrains.compose.resources.StringResourcesKt") - mockkStatic("androidx.glance.appwidget.GlanceAppWidgetKt") - coEvery { org.jetbrains.compose.resources.getString(any()) } returns "Mocked String" - coEvery { org.jetbrains.compose.resources.getString(any(), *anyVararg()) } returns "Mocked String" - coEvery { any().updateAll(any()) } returns Unit + mockkStatic("org.meshtastic.core.resources.ContextExtKt") + every { getString(any()) } returns "Mocked String" + every { getString(any(), *anyVararg()) } returns "Mocked String" every { radioInterfaceService.connectionState } returns radioConnectionState every { radioConfigRepository.localConfigFlow } returns localConfigFlow @@ -107,11 +103,9 @@ class MeshConnectionManagerTest { every { nodeRepository.localStats } returns MutableStateFlow(LocalStats()) every { serviceRepository.connectionState } returns connectionStateFlow every { serviceRepository.setConnectionState(any()) } answers { connectionStateFlow.value = firstArg() } - every { serviceNotifications.updateServiceStateNotification(any(), any()) } returns mockk(relaxed = true) manager = - MeshConnectionManager( - context, + MeshConnectionManagerImpl( radioInterfaceService, serviceRepository, serviceBroadcasts, @@ -127,14 +121,14 @@ class MeshConnectionManagerTest { nodeManager, analytics, packetRepository, - workManager, + workerManager, + appWidgetUpdater, ) } @After fun tearDown() { - unmockkStatic("org.jetbrains.compose.resources.StringResourcesKt") - unmockkStatic("androidx.glance.appwidget.GlanceAppWidgetKt") + unmockkStatic("org.meshtastic.core.resources.ContextExtKt") } @Test @@ -225,7 +219,7 @@ class MeshConnectionManagerTest { manager.onRadioConfigLoaded() advanceUntilIdle() - verify { workManager.enqueueUniqueWork(match { it.startsWith(SendMessageWorker.WORK_NAME_PREFIX) }, any(), any()) } + verify { workerManager.enqueueSendMessage(packetId) } verify { commandSender.sendAdmin(any(), initFn = any()) } } diff --git a/core/data/src/test/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerTest.kt b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerTest.kt index b556ebfdd4..8eb3a70a1f 100644 --- a/core/data/src/test/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerTest.kt +++ b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerTest.kt @@ -96,12 +96,12 @@ class MeshDataHandlerTest { serviceNotifications, analytics, dataMapper, - configHandler, - configFlowManager, + { configHandler }, + { configFlowManager }, commandSender, historyManager, meshPrefs, - connectionManager, + { connectionManager }, tracerouteHandler, neighborInfoHandler, radioConfigRepository, diff --git a/app/src/test/java/com/geeksville/mesh/service/PacketHandlerTest.kt b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/PacketHandlerImplTest.kt similarity index 88% rename from app/src/test/java/com/geeksville/mesh/service/PacketHandlerTest.kt rename to core/data/src/test/kotlin/org/meshtastic/core/data/manager/PacketHandlerImplTest.kt index f3fd69302a..4447ec4409 100644 --- a/app/src/test/java/com/geeksville/mesh/service/PacketHandlerTest.kt +++ b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/PacketHandlerImplTest.kt @@ -14,9 +14,8 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.geeksville.mesh.service +package org.meshtastic.core.data.manager -import com.geeksville.mesh.repository.radio.RadioInterfaceService import io.mockk.coVerify import io.mockk.every import io.mockk.mockk @@ -31,6 +30,8 @@ import org.meshtastic.core.data.repository.MeshLogRepository import org.meshtastic.core.database.entity.MeshLog import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.repository.PacketRepository +import org.meshtastic.core.repository.RadioInterfaceService +import org.meshtastic.core.repository.ServiceBroadcasts import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.proto.Data import org.meshtastic.proto.MeshPacket @@ -38,7 +39,7 @@ import org.meshtastic.proto.PortNum import org.meshtastic.proto.QueueStatus import org.meshtastic.proto.ToRadio -class PacketHandlerTest { +class PacketHandlerImplTest { private val packetRepository: PacketRepository = mockk(relaxed = true) private val serviceBroadcasts: ServiceBroadcasts = mockk(relaxed = true) @@ -50,7 +51,7 @@ class PacketHandlerTest { private val testDispatcher = StandardTestDispatcher() private val testScope = TestScope(testDispatcher) - private lateinit var handler: PacketHandler + private lateinit var handler: PacketHandlerImpl @Before fun setUp() { @@ -58,11 +59,11 @@ class PacketHandlerTest { every { serviceRepository.setConnectionState(any()) } answers { connectionStateFlow.value = firstArg() } handler = - PacketHandler( - dagger.Lazy { packetRepository }, + PacketHandlerImpl( + { packetRepository }, serviceBroadcasts, radioInterfaceService, - dagger.Lazy { meshLogRepository }, + { meshLogRepository }, serviceRepository, ) handler.start(testScope) @@ -80,7 +81,7 @@ class PacketHandlerTest { @Test fun `sendToRadio with MeshPacket queues and sends when connected`() = runTest(testDispatcher) { val packet = MeshPacket(id = 456) - every { serviceRepository.connectionState } returns MutableStateFlow(ConnectionState.Connected) + connectionStateFlow.value = ConnectionState.Connected handler.sendToRadio(packet) testScheduler.runCurrent() @@ -91,7 +92,7 @@ class PacketHandlerTest { @Test fun `handleQueueStatus completes deferred`() = runTest(testDispatcher) { val packet = MeshPacket(id = 789) - every { serviceRepository.connectionState } returns MutableStateFlow(ConnectionState.Connected) + connectionStateFlow.value = ConnectionState.Connected handler.sendToRadio(packet) testScheduler.runCurrent() diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceId.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/InterfaceId.kt similarity index 95% rename from app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceId.kt rename to core/model/src/commonMain/kotlin/org/meshtastic/core/model/InterfaceId.kt index 1081394edb..7a66f713e8 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceId.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/InterfaceId.kt @@ -14,8 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - -package com.geeksville.mesh.repository.radio +package org.meshtastic.core.model /** * Address identifiers for all supported radio backend implementations. @@ -33,4 +32,4 @@ enum class InterfaceId(val id: Char) { return entries.firstOrNull { it.id == id } } } -} \ No newline at end of file +} diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/MeshActivity.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/MeshActivity.kt new file mode 100644 index 0000000000..bbfd279e7d --- /dev/null +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/MeshActivity.kt @@ -0,0 +1,28 @@ +/* + * 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 . + */ +package org.meshtastic.core.model + +/** + * Represents activity on the mesh network. + */ +sealed class MeshActivity { + /** Data is being sent to the radio. */ + data object Send : MeshActivity() + + /** Data is being received from the radio. */ + data object Receive : MeshActivity() +} diff --git a/app/src/main/java/com/geeksville/mesh/service/RadioNotConnectedException.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/RadioNotConnectedException.kt similarity index 55% rename from app/src/main/java/com/geeksville/mesh/service/RadioNotConnectedException.kt rename to core/model/src/commonMain/kotlin/org/meshtastic/core/model/RadioNotConnectedException.kt index 31f28c799c..23cf84615f 100644 --- a/app/src/main/java/com/geeksville/mesh/service/RadioNotConnectedException.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/RadioNotConnectedException.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 @@ -14,17 +14,9 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.geeksville.mesh.service +package org.meshtastic.core.model -import android.os.RemoteException - -open class RadioNotConnectedException(message: String = "Not connected to radio") : RemoteException(message) - -class NoDeviceConfigException(message: String = "No radio settings received (is our app too old?)") : - RadioNotConnectedException(message) - -class BLEException(message: String) : RadioNotConnectedException(message) - -class BLECharacteristicNotFoundException(message: String) : RadioNotConnectedException(message) - -class BLEConnectionClosing(message: String = "BLE connection is closing") : RadioNotConnectedException(message) +/** + * Exception thrown when an operation is attempted while not connected to a mesh radio. + */ +open class RadioNotConnectedException(message: String = "Not connected to radio") : Exception(message) diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/AppWidgetUpdater.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/AppWidgetUpdater.kt new file mode 100644 index 0000000000..ef2de8f6f3 --- /dev/null +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/AppWidgetUpdater.kt @@ -0,0 +1,25 @@ +/* + * 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 . + */ +package org.meshtastic.core.repository + +/** + * Interface for triggering updates to application widgets. + */ +interface AppWidgetUpdater { + /** Triggers an update for all app widgets. */ + suspend fun updateAll() +} diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshLocationManager.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshLocationManager.kt new file mode 100644 index 0000000000..c48ff207b9 --- /dev/null +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshLocationManager.kt @@ -0,0 +1,31 @@ +/* + * 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 . + */ +package org.meshtastic.core.repository + +import kotlinx.coroutines.CoroutineScope +import org.meshtastic.proto.Position + +/** + * Interface for managing the local node's location updates and reporting. + */ +interface MeshLocationManager { + /** Starts location updates and reports them via the given function. */ + fun start(scope: CoroutineScope, sendPositionFn: (Position) -> Unit) + + /** Stops location updates. */ + fun stop() +} diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshWorkerManager.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshWorkerManager.kt new file mode 100644 index 0000000000..05b0d91308 --- /dev/null +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshWorkerManager.kt @@ -0,0 +1,25 @@ +/* + * 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 . + */ +package org.meshtastic.core.repository + +/** + * Interface for managing background workers for mesh-related tasks. + */ +interface MeshWorkerManager { + /** Enqueues a worker to send a specific packet. */ + fun enqueueSendMessage(packetId: Int) +} diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/RadioInterfaceService.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/RadioInterfaceService.kt new file mode 100644 index 0000000000..65b5998dee --- /dev/null +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/RadioInterfaceService.kt @@ -0,0 +1,74 @@ +/* + * 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 . + */ +package org.meshtastic.core.repository + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import org.meshtastic.core.model.ConnectionState +import org.meshtastic.core.model.InterfaceId +import org.meshtastic.core.model.MeshActivity + +/** + * Interface for the low-level radio interface that handles raw byte communication. + */ +interface RadioInterfaceService { + /** Reactive connection state of the radio. */ + val connectionState: StateFlow + + /** Flow of the current device address. */ + val currentDeviceAddressFlow: StateFlow + + /** Whether we are currently using a mock interface. */ + fun isMockInterface(): Boolean + + /** Flow of raw data received from the radio. */ + val receivedData: SharedFlow + + /** Flow of radio activity events. */ + val meshActivity: SharedFlow + + /** Sends a raw byte array to the radio. */ + fun sendToRadio(bytes: ByteArray) + + /** Initiates the connection to the radio. */ + fun connect() + + /** Returns the current device address. */ + fun getDeviceAddress(): String? + + /** Sets the device address to connect to. */ + fun setDeviceAddress(deviceAddr: String?): Boolean + + /** Constructs a full radio address for the specific interface type. */ + fun toInterfaceAddress(interfaceId: InterfaceId, rest: String): String + + /** Called by an interface when it has successfully connected. */ + fun onConnect() + + /** Called by an interface when it has disconnected. */ + fun onDisconnect(isPermanent: Boolean) + + /** Called by an interface when it has disconnected with an error. */ + fun onDisconnect(error: Any) + + /** Called by an interface when it has received raw data from the radio. */ + fun handleFromRadio(bytes: ByteArray) + + /** The scope in which interface-related coroutines should run. */ + val serviceScope: CoroutineScope +} From 36ffc7d47ef08776393a4d7f3ab28c5953eae506 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 2 Mar 2026 15:07:23 -0600 Subject: [PATCH 11/20] refactor: reformat code and consolidate DI bindings Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .../com/geeksville/mesh/ApplicationModule.kt | 11 +- .../radio/AndroidRadioInterfaceService.kt | 7 +- .../mesh/repository/radio/InterfaceFactory.kt | 31 ++-- .../mesh/repository/radio/InterfaceMapKey.kt | 7 +- .../repository/radio/RadioRepositoryModule.kt | 3 +- .../mesh/service/AndroidAppWidgetUpdater.kt | 7 +- .../mesh/service/AndroidMeshWorkerManager.kt | 15 +- .../service/MeshServiceNotificationsImpl.kt | 3 +- .../geeksville/mesh/service/ReplyReceiver.kt | 4 +- .../mesh/service/ServiceBroadcasts.kt | 40 ++--- .../main/java/com/geeksville/mesh/ui/Main.kt | 10 +- .../mesh/ui/connections/ScannerViewModel.kt | 6 - .../mesh/ui/sharing/ChannelViewModel.kt | 8 +- ...dcastsTest.kt => ServiceBroadcastsTest.kt} | 0 .../DeviceHardwareLocalDataSource.kt | 2 +- .../FirmwareReleaseLocalDataSource.kt | 2 +- .../datasource/NodeInfoWriteDataSource.kt | 2 +- .../SwitchingNodeInfoReadDataSource.kt | 2 +- .../SwitchingNodeInfoWriteDataSource.kt | 2 +- .../core/data/di/RepositoryModule.kt | 110 ++++---------- .../meshtastic/core/data/di/UseCaseModule.kt | 12 +- .../core/data/manager/CommandSenderImpl.kt | 15 +- .../manager/FromRadioPacketHandlerImpl.kt | 10 +- .../core/data/manager/HistoryManagerImpl.kt | 30 ++-- .../data/manager/MeshActionHandlerImpl.kt | 6 +- .../data/manager/MeshConfigFlowManagerImpl.kt | 8 +- .../data/manager/MeshConfigHandlerImpl.kt | 6 +- .../data/manager/MeshConnectionManagerImpl.kt | 6 +- .../core/data/manager/MeshDataHandlerImpl.kt | 34 +++-- .../data/manager/MeshMessageProcessorImpl.kt | 21 +-- .../core/data/manager/MeshRouterImpl.kt | 51 ++++--- .../core/data/manager/MessageFilterImpl.kt | 6 +- .../core/data/manager/NodeManagerImpl.kt | 71 +++++---- .../core/data/manager/PacketHandlerImpl.kt | 8 +- .../data/manager/TracerouteHandlerImpl.kt | 5 +- .../DeviceHardwareRepositoryImpl.kt | 2 +- .../repository/FirmwareReleaseRepository.kt | 2 +- .../data/repository/LocationRepository.kt | 2 +- .../core/data/repository/MeshLogRepository.kt | 2 +- .../data/repository/NodeRepositoryImpl.kt | 24 ++- .../data/repository/PacketRepositoryImpl.kt | 138 +++++++++--------- .../repository/RadioConfigRepositoryImpl.kt | 2 +- .../TracerouteSnapshotRepository.kt | 2 +- .../manager/FromRadioPacketHandlerImplTest.kt | 5 +- .../data/manager/HistoryManagerImplTest.kt | 24 +-- .../core/data/manager/MeshDataHandlerTest.kt | 2 +- .../data/manager/MessageFilterImplTest.kt | 2 +- .../DeviceHardwareRepositoryTest.kt | 2 +- .../data/repository/MeshLogRepositoryTest.kt | 2 +- .../data/repository/NodeRepositoryTest.kt | 2 +- .../core/database/DatabaseManager.kt | 7 +- .../core/database/di/DatabaseModule.kt | 19 +-- .../settings/CleanNodeDatabaseUseCase.kt | 7 +- .../core/model/util/MeshDataMapperTest.kt | 2 +- .../org/meshtastic/core/model/InterfaceId.kt | 10 +- .../org/meshtastic/core/model/MeshActivity.kt | 6 +- .../org/meshtastic/core/model/Message.kt | 2 +- .../kotlin/org/meshtastic/core/model/Node.kt | 3 +- .../meshtastic/core/model/NodeSortOption.kt | 2 +- .../core/model/RadioNotConnectedException.kt | 6 +- .../org/meshtastic/core/model/Reaction.kt | 2 +- .../kotlin/org/meshtastic/core/model/TAK.kt | 2 +- .../core/model/service/ServiceAction.kt | 2 +- .../core/model/service/TracerouteResponse.kt | 2 +- .../repository/TrustAllX509TrustManager.kt | 5 +- .../meshtastic/core/prefs/di/PrefsModule.kt | 5 +- .../core/prefs/homoglyph/HomoglyphPrefs.kt | 1 - core/repository/build.gradle.kts | 10 +- .../core/repository/AppWidgetUpdater.kt | 6 +- .../core/repository/CommandSender.kt | 7 +- .../core/repository/DatabaseManager.kt | 6 +- .../repository/DeviceHardwareRepository.kt | 2 +- .../core/repository/FromRadioPacketHandler.kt | 6 +- .../core/repository/HistoryManager.kt | 6 +- .../core/repository/HomoglyphPrefs.kt | 2 +- .../core/repository/MeshActionHandler.kt | 7 +- .../core/repository/MeshConfigFlowManager.kt | 6 +- .../core/repository/MeshConfigHandler.kt | 6 +- .../core/repository/MeshConnectionManager.kt | 6 +- .../core/repository/MeshDataHandler.kt | 19 +-- .../core/repository/MeshLocationManager.kt | 6 +- .../core/repository/MeshMessageProcessor.kt | 6 +- .../meshtastic/core/repository/MeshRouter.kt | 6 +- .../core/repository/MeshWorkerManager.kt | 6 +- .../core/repository/MessageFilter.kt | 10 +- .../meshtastic/core/repository/MqttManager.kt | 6 +- .../core/repository/NeighborInfoHandler.kt | 6 +- .../meshtastic/core/repository/NodeManager.kt | 15 +- .../core/repository/NodeRepository.kt | 8 +- .../core/repository/PacketHandler.kt | 6 +- .../core/repository/PacketRepository.kt | 27 ++-- .../core/repository/RadioConfigRepository.kt | 2 +- .../core/repository/RadioInterfaceService.kt | 6 +- .../core/repository/ServiceBroadcasts.kt | 6 +- .../core/repository/ServiceRepository.kt | 31 ++-- .../core/repository/TracerouteHandler.kt | 6 +- .../service/AndroidRadioControllerImpl.kt | 7 +- ...ository.kt => AndroidServiceRepository.kt} | 0 .../core/service/di/ServiceModule.kt | 6 +- .../core/ui/qr/ScannedQrCodeViewModel.kt | 8 +- .../firmware/ota/Esp32OtaUpdateHandlerTest.kt | 3 +- .../org/meshtastic/feature/map/MapView.kt | 2 +- .../meshtastic/feature/map/MapViewModel.kt | 3 +- .../feature/map/BaseMapViewModel.kt | 13 +- .../feature/messaging/MessageScreenEvent.kt | 3 +- .../domain/worker/SendMessageWorker.kt | 11 +- .../messaging/ui/contact/ContactsViewModel.kt | 13 +- .../domain/worker/SendMessageWorkerTest.kt | 88 +++++++---- .../feature/node/component/InlineMap.kt | 3 +- .../node/detail/NodeManagementActions.kt | 12 +- .../feature/node/detail/NodeRequestActions.kt | 4 +- .../node/list/NodeFilterPreferences.kt | 3 +- .../feature/settings/SettingsViewModel.kt | 7 +- .../settings/radio/RadioConfigViewModel.kt | 51 +++---- .../filter/FilterSettingsViewModelTest.kt | 11 +- .../radio/RadioConfigViewModelTest.kt | 49 ++++--- 116 files changed, 641 insertions(+), 755 deletions(-) rename app/src/test/java/com/geeksville/mesh/service/{MeshServiceBroadcastsTest.kt => ServiceBroadcastsTest.kt} (100%) rename core/service/src/main/kotlin/org/meshtastic/core/service/{ServiceRepository.kt => AndroidServiceRepository.kt} (100%) diff --git a/app/src/main/java/com/geeksville/mesh/ApplicationModule.kt b/app/src/main/java/com/geeksville/mesh/ApplicationModule.kt index 7d05423ba7..dd07d74e2a 100644 --- a/app/src/main/java/com/geeksville/mesh/ApplicationModule.kt +++ b/app/src/main/java/com/geeksville/mesh/ApplicationModule.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 com.geeksville.mesh import androidx.lifecycle.Lifecycle @@ -42,13 +41,17 @@ interface ApplicationModule { @Binds fun bindMeshServiceNotifications(impl: MeshServiceNotificationsImpl): MeshServiceNotifications - @Binds fun bindMeshLocationManager(impl: AndroidMeshLocationManager): org.meshtastic.core.repository.MeshLocationManager + @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 bindRadioInterfaceService( + impl: AndroidRadioInterfaceService, + ): org.meshtastic.core.repository.RadioInterfaceService @Binds fun bindServiceBroadcasts(impl: ServiceBroadcasts): org.meshtastic.core.repository.ServiceBroadcasts diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/AndroidRadioInterfaceService.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/AndroidRadioInterfaceService.kt index 26e2e895e9..cd190ad455 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/radio/AndroidRadioInterfaceService.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/AndroidRadioInterfaceService.kt @@ -68,7 +68,7 @@ import javax.inject.Singleton * Note - this class intentionally dumb. It doesn't understand protobuf framing etc... It is designed to be simple so it * can be stubbed out with a simulated version as needed. */ -@Suppress("LongParameterList") +@Suppress("LongParameterList", "TooManyFunctions") @Singleton class AndroidRadioInterfaceService @Inject @@ -234,6 +234,7 @@ constructor( } // Handle an incoming packet from the radio, broadcasts it as an android intent + @Suppress("TooGenericExceptionCaught") override fun handleFromRadio(bytes: ByteArray) { if (logReceives) { try { @@ -366,7 +367,9 @@ constructor( true } - override fun setDeviceAddress(deviceAddr: String?): Boolean = toRemoteExceptions { setBondedDeviceAddress(deviceAddr) } + override fun setDeviceAddress(deviceAddr: String?): Boolean = toRemoteExceptions { + setBondedDeviceAddress(deviceAddr) + } /** * If the service is not currently connected to the radio, try to connect now. At boot the radio interface service diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceFactory.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceFactory.kt index 68f9a7d4ee..f511cb5554 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceFactory.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceFactory.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 com.geeksville.mesh.repository.radio import org.meshtastic.core.model.InterfaceId @@ -24,32 +23,28 @@ import javax.inject.Provider /** * Entry point for create radio backend instances given a specific address. * - * 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). + * 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( +class InterfaceFactory +@Inject +constructor( private val nopInterfaceFactory: NopInterfaceFactory, - private val specMap: Map>> + private val specMap: Map>>, ) { - internal val nopInterface by lazy { - nopInterfaceFactory.create("") - } + internal val nopInterface by lazy { nopInterfaceFactory.create("") } - fun toInterfaceAddress(interfaceId: InterfaceId, rest: String): String { - return "${interfaceId.id}$rest" - } + fun toInterfaceAddress(interfaceId: InterfaceId, rest: String): String = "${interfaceId.id}$rest" fun createInterface(address: String): IRadioInterface { val (spec, rest) = splitAddress(address) return spec?.createInterface(rest) ?: nopInterface } - fun addressValid(address: String?): Boolean { - return address?.let { - val (spec, rest) = splitAddress(it) - spec?.addressValid(rest) - } ?: false - } + fun addressValid(address: String?): Boolean = address?.let { + val (spec, rest) = splitAddress(it) + spec?.addressValid(rest) + } ?: false private fun splitAddress(address: String): Pair?, String> { val c = address[0].let { InterfaceId.forIdChar(it) }?.let { specMap[it]?.get() } diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceMapKey.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceMapKey.kt index 842df6161a..fc9170c6af 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceMapKey.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceMapKey.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,12 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package com.geeksville.mesh.repository.radio import dagger.MapKey import org.meshtastic.core.model.InterfaceId -/** - * Dagger `MapKey` implementation allowing for `InterfaceId` to be used as a map key. - */ +/** 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) diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/RadioRepositoryModule.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/RadioRepositoryModule.kt index d570c15aa5..88d9579170 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/radio/RadioRepositoryModule.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/RadioRepositoryModule.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 com.geeksville.mesh.repository.radio import dagger.Binds diff --git a/app/src/main/java/com/geeksville/mesh/service/AndroidAppWidgetUpdater.kt b/app/src/main/java/com/geeksville/mesh/service/AndroidAppWidgetUpdater.kt index 23bb9cfa5a..9735b0ab5c 100644 --- a/app/src/main/java/com/geeksville/mesh/service/AndroidAppWidgetUpdater.kt +++ b/app/src/main/java/com/geeksville/mesh/service/AndroidAppWidgetUpdater.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 @@ -25,13 +25,12 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class AndroidAppWidgetUpdater @Inject constructor( - @ApplicationContext private val context: Context -) : AppWidgetUpdater { +class AndroidAppWidgetUpdater @Inject constructor(@ApplicationContext private val context: Context) : AppWidgetUpdater { override suspend fun updateAll() { // Kickstart the widget composition. // The widget internally uses collectAsState() and its own sampled StateFlow // to drive updates automatically without excessive IPC and recreation. + @Suppress("TooGenericExceptionCaught") try { LocalStatsWidget().updateAll(context) } catch (e: Exception) { diff --git a/app/src/main/java/com/geeksville/mesh/service/AndroidMeshWorkerManager.kt b/app/src/main/java/com/geeksville/mesh/service/AndroidMeshWorkerManager.kt index ae9985d9a7..8b235ea5ca 100644 --- a/app/src/main/java/com/geeksville/mesh/service/AndroidMeshWorkerManager.kt +++ b/app/src/main/java/com/geeksville/mesh/service/AndroidMeshWorkerManager.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 @@ -26,18 +26,17 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class AndroidMeshWorkerManager @Inject constructor( - private val workManager: WorkManager -) : MeshWorkerManager { +class AndroidMeshWorkerManager @Inject constructor(private val workManager: WorkManager) : MeshWorkerManager { override fun enqueueSendMessage(packetId: Int) { - val workRequest = OneTimeWorkRequestBuilder() - .setInputData(workDataOf(SendMessageWorker.KEY_PACKET_ID to packetId)) - .build() + val workRequest = + OneTimeWorkRequestBuilder() + .setInputData(workDataOf(SendMessageWorker.KEY_PACKET_ID to packetId)) + .build() workManager.enqueueUniqueWork( "${SendMessageWorker.WORK_NAME_PREFIX}$packetId", ExistingWorkPolicy.REPLACE, - workRequest + workRequest, ) } } diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotificationsImpl.kt b/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotificationsImpl.kt index 14b72f53d9..47b0a7fb28 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotificationsImpl.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotificationsImpl.kt @@ -316,8 +316,7 @@ constructor( } if (cachedLocalStats == null) { // Fallback to DB stats if repository hasn't received any fresh ones yet - cachedLocalStats = - repo.localStats.value.takeIf { it.uptime_seconds != 0 } + cachedLocalStats = repo.localStats.value.takeIf { it.uptime_seconds != 0 } } } } diff --git a/app/src/main/java/com/geeksville/mesh/service/ReplyReceiver.kt b/app/src/main/java/com/geeksville/mesh/service/ReplyReceiver.kt index 7c29affe4a..e602b52818 100644 --- a/app/src/main/java/com/geeksville/mesh/service/ReplyReceiver.kt +++ b/app/src/main/java/com/geeksville/mesh/service/ReplyReceiver.kt @@ -50,9 +50,7 @@ class ReplyReceiver : BroadcastReceiver() { val channel = contactKey[0].digitToIntOrNull() val dest = if (channel != null) contactKey.substring(1) else contactKey val p = DataPacket(dest, channel ?: 0, str) - kotlinx.coroutines.CoroutineScope(kotlinx.coroutines.Dispatchers.IO).launch { - radioController.sendMessage(p) - } + kotlinx.coroutines.CoroutineScope(kotlinx.coroutines.Dispatchers.IO).launch { radioController.sendMessage(p) } } override fun onReceive(context: android.content.Context, intent: android.content.Intent) { diff --git a/app/src/main/java/com/geeksville/mesh/service/ServiceBroadcasts.kt b/app/src/main/java/com/geeksville/mesh/service/ServiceBroadcasts.kt index 5f8be95452..99d0bc7246 100644 --- a/app/src/main/java/com/geeksville/mesh/service/ServiceBroadcasts.kt +++ b/app/src/main/java/com/geeksville/mesh/service/ServiceBroadcasts.kt @@ -68,40 +68,42 @@ constructor( private fun Node.toLegacy(): NodeInfo = NodeInfo( num = num, - user = org.meshtastic.core.model.MeshUser( + user = + org.meshtastic.core.model.MeshUser( id = user.id, longName = user.long_name, shortName = user.short_name, hwModel = user.hw_model, - role = user.role.value + role = user.role.value, ), - position = org.meshtastic.core.model.Position( - latitude = latitude, - longitude = longitude, - altitude = position.altitude ?: 0, - time = position.time, - satellitesInView = position.sats_in_view ?: 0, - groundSpeed = position.ground_speed ?: 0, - groundTrack = position.ground_track ?: 0, - precisionBits = position.precision_bits ?: 0 - ).takeIf { latitude != 0.0 || longitude != 0.0 }, + position = + org.meshtastic.core.model + .Position( + latitude = latitude, + longitude = longitude, + altitude = position.altitude ?: 0, + time = position.time, + satellitesInView = position.sats_in_view ?: 0, + groundSpeed = position.ground_speed ?: 0, + groundTrack = position.ground_track ?: 0, + precisionBits = position.precision_bits ?: 0, + ) + .takeIf { latitude != 0.0 || longitude != 0.0 }, snr = snr, rssi = rssi, lastHeard = lastHeard, - deviceMetrics = org.meshtastic.core.model.DeviceMetrics( + deviceMetrics = + org.meshtastic.core.model.DeviceMetrics( batteryLevel = deviceMetrics.battery_level ?: 0, voltage = deviceMetrics.voltage ?: 0f, channelUtilization = deviceMetrics.channel_utilization ?: 0f, airUtilTx = deviceMetrics.air_util_tx ?: 0f, - uptimeSeconds = deviceMetrics.uptime_seconds ?: 0 + uptimeSeconds = deviceMetrics.uptime_seconds ?: 0, ), channel = channel, - environmentMetrics = org.meshtastic.core.model.EnvironmentMetrics.fromTelemetryProto( - environmentMetrics, - 0 - ), + environmentMetrics = org.meshtastic.core.model.EnvironmentMetrics.fromTelemetryProto(environmentMetrics, 0), hopsAway = hopsAway, - nodeStatus = nodeStatus + nodeStatus = nodeStatus, ) fun broadcastMessageStatus(p: DataPacket) = broadcastMessageStatus(p.id, p.status ?: MessageStatus.UNKNOWN) diff --git a/app/src/main/java/com/geeksville/mesh/ui/Main.kt b/app/src/main/java/com/geeksville/mesh/ui/Main.kt index 9aee7f945a..f41dcd8e1b 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Main.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Main.kt @@ -64,7 +64,6 @@ import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -464,7 +463,6 @@ fun MainScreen(uIViewModel: UIViewModel = hiltViewModel(), scanModel: ScannerVie private fun VersionChecks(viewModel: UIViewModel) { val connectionState by viewModel.connectionState.collectAsStateWithLifecycle() val myNodeInfo by viewModel.myNodeInfo.collectAsStateWithLifecycle() - val context = LocalContext.current val myFirmwareVersion = myNodeInfo?.firmwareVersion @@ -499,9 +497,7 @@ private fun VersionChecks(viewModel: UIViewModel) { viewModel.showAlert( titleRes = Res.string.app_too_old, messageRes = Res.string.must_update, - onConfirm = { - viewModel.setDeviceAddress("n") - }, + onConfirm = { viewModel.setDeviceAddress("n") }, ) } else { myFirmwareVersion @@ -525,9 +521,7 @@ private fun VersionChecks(viewModel: UIViewModel) { viewModel.showAlert( title = title, html = message, - onConfirm = { - viewModel.setDeviceAddress("n") - }, + onConfirm = { viewModel.setDeviceAddress("n") }, ) } else if (curVer < MeshService.minDeviceVersion) { Logger.w { diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/ScannerViewModel.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/ScannerViewModel.kt index 7e736b0f15..0bfba1faf0 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/connections/ScannerViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/connections/ScannerViewModel.kt @@ -16,8 +16,6 @@ */ package com.geeksville.mesh.ui.connections -import android.app.Application -import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import co.touchlab.kermit.Logger @@ -51,7 +49,6 @@ import javax.inject.Inject class ScannerViewModel @Inject constructor( - private val application: Application, private val serviceRepository: ServiceRepository, private val radioController: RadioController, private val bluetoothRepository: BluetoothRepository, @@ -60,9 +57,6 @@ constructor( private val recentAddressesDataSource: RecentAddressesDataSource, private val getDiscoveredDevicesUseCase: GetDiscoveredDevicesUseCase, ) : ViewModel() { - private val context: Context - get() = application.applicationContext - val showMockInterface: StateFlow = MutableStateFlow(radioInterfaceService.isMockInterface()).asStateFlow() private val _errorText = MutableStateFlow(null) diff --git a/app/src/main/java/com/geeksville/mesh/ui/sharing/ChannelViewModel.kt b/app/src/main/java/com/geeksville/mesh/ui/sharing/ChannelViewModel.kt index 0e90416971..c5ba9bec44 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/sharing/ChannelViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/sharing/ChannelViewModel.kt @@ -94,16 +94,12 @@ constructor( } fun setChannel(channel: Channel) { - viewModelScope.launch { - radioController.setLocalChannel(channel) - } + viewModelScope.launch { radioController.setLocalChannel(channel) } } // Set the radio config (also updates our saved copy in preferences) fun setConfig(config: Config) { - viewModelScope.launch { - radioController.setLocalConfig(config) - } + viewModelScope.launch { radioController.setLocalConfig(config) } } fun trackShare() { diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshServiceBroadcastsTest.kt b/app/src/test/java/com/geeksville/mesh/service/ServiceBroadcastsTest.kt similarity index 100% rename from app/src/test/java/com/geeksville/mesh/service/MeshServiceBroadcastsTest.kt rename to app/src/test/java/com/geeksville/mesh/service/ServiceBroadcastsTest.kt diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/DeviceHardwareLocalDataSource.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/DeviceHardwareLocalDataSource.kt index 907365965c..852c56e04e 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/DeviceHardwareLocalDataSource.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/DeviceHardwareLocalDataSource.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 diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/FirmwareReleaseLocalDataSource.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/FirmwareReleaseLocalDataSource.kt index 41e9479718..dff3b0171e 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/FirmwareReleaseLocalDataSource.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/FirmwareReleaseLocalDataSource.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 diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/NodeInfoWriteDataSource.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/NodeInfoWriteDataSource.kt index e45629bb8d..c4ced500c3 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/NodeInfoWriteDataSource.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/NodeInfoWriteDataSource.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 diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/SwitchingNodeInfoReadDataSource.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/SwitchingNodeInfoReadDataSource.kt index 434e1ec7e3..35d9c08480 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/SwitchingNodeInfoReadDataSource.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/SwitchingNodeInfoReadDataSource.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 diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/SwitchingNodeInfoWriteDataSource.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/SwitchingNodeInfoWriteDataSource.kt index 2be1f75252..6b55019102 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/SwitchingNodeInfoWriteDataSource.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/SwitchingNodeInfoWriteDataSource.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 diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt index 5b8757bcff..333398c103 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.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.core.data.di import dagger.Binds @@ -65,129 +64,84 @@ 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 bindNodeRepository(nodeRepositoryImpl: NodeRepositoryImpl): NodeRepository @Binds @Singleton - abstract fun bindRadioConfigRepository( - radioConfigRepositoryImpl: RadioConfigRepositoryImpl - ): RadioConfigRepository + abstract fun bindRadioConfigRepository(radioConfigRepositoryImpl: RadioConfigRepositoryImpl): RadioConfigRepository @Binds @Singleton abstract fun bindDeviceHardwareRepository( - deviceHardwareRepositoryImpl: DeviceHardwareRepositoryImpl + deviceHardwareRepositoryImpl: DeviceHardwareRepositoryImpl, ): DeviceHardwareRepository - @Binds - @Singleton - abstract fun bindPacketRepository( - packetRepositoryImpl: PacketRepositoryImpl - ): PacketRepository + @Binds @Singleton + abstract fun bindPacketRepository(packetRepositoryImpl: PacketRepositoryImpl): PacketRepository - @Binds - @Singleton - abstract fun bindNodeManager( - nodeManagerImpl: NodeManagerImpl - ): NodeManager + @Binds @Singleton + abstract fun bindNodeManager(nodeManagerImpl: NodeManagerImpl): NodeManager - @Binds - @Singleton - abstract fun bindCommandSender( - commandSenderImpl: CommandSenderImpl - ): CommandSender + @Binds @Singleton + abstract fun bindCommandSender(commandSenderImpl: CommandSenderImpl): CommandSender - @Binds - @Singleton - abstract fun bindHistoryManager( - historyManagerImpl: HistoryManagerImpl - ): HistoryManager + @Binds @Singleton + abstract fun bindHistoryManager(historyManagerImpl: HistoryManagerImpl): HistoryManager @Binds @Singleton - abstract fun bindTracerouteHandler( - tracerouteHandlerImpl: TracerouteHandlerImpl - ): TracerouteHandler + abstract fun bindTracerouteHandler(tracerouteHandlerImpl: TracerouteHandlerImpl): TracerouteHandler @Binds @Singleton - abstract fun bindNeighborInfoHandler( - neighborInfoHandlerImpl: NeighborInfoHandlerImpl - ): NeighborInfoHandler + abstract fun bindNeighborInfoHandler(neighborInfoHandlerImpl: NeighborInfoHandlerImpl): NeighborInfoHandler - @Binds - @Singleton - abstract fun bindMqttManager( - mqttManagerImpl: MqttManagerImpl - ): MqttManager + @Binds @Singleton + abstract fun bindMqttManager(mqttManagerImpl: MqttManagerImpl): MqttManager - @Binds - @Singleton - abstract fun bindPacketHandler( - packetHandlerImpl: PacketHandlerImpl - ): PacketHandler + @Binds @Singleton + abstract fun bindPacketHandler(packetHandlerImpl: PacketHandlerImpl): PacketHandler @Binds @Singleton - abstract fun bindMeshConnectionManager( - meshConnectionManagerImpl: MeshConnectionManagerImpl - ): MeshConnectionManager + abstract fun bindMeshConnectionManager(meshConnectionManagerImpl: MeshConnectionManagerImpl): MeshConnectionManager - @Binds - @Singleton - abstract fun bindMeshDataHandler( - meshDataHandlerImpl: MeshDataHandlerImpl - ): MeshDataHandler + @Binds @Singleton + abstract fun bindMeshDataHandler(meshDataHandlerImpl: MeshDataHandlerImpl): MeshDataHandler @Binds @Singleton - abstract fun bindMeshActionHandler( - meshActionHandlerImpl: MeshActionHandlerImpl - ): MeshActionHandler + abstract fun bindMeshActionHandler(meshActionHandlerImpl: MeshActionHandlerImpl): MeshActionHandler @Binds @Singleton - abstract fun bindMeshMessageProcessor( - meshMessageProcessorImpl: MeshMessageProcessorImpl - ): MeshMessageProcessor + abstract fun bindMeshMessageProcessor(meshMessageProcessorImpl: MeshMessageProcessorImpl): MeshMessageProcessor - @Binds - @Singleton - abstract fun bindMeshRouter( - meshRouterImpl: MeshRouterImpl - ): MeshRouter + @Binds @Singleton + abstract fun bindMeshRouter(meshRouterImpl: MeshRouterImpl): MeshRouter @Binds @Singleton abstract fun bindFromRadioPacketHandler( - fromRadioPacketHandlerImpl: FromRadioPacketHandlerImpl + fromRadioPacketHandlerImpl: FromRadioPacketHandlerImpl, ): FromRadioPacketHandler @Binds @Singleton - abstract fun bindMeshConfigHandler( - meshConfigHandlerImpl: MeshConfigHandlerImpl - ): MeshConfigHandler + abstract fun bindMeshConfigHandler(meshConfigHandlerImpl: MeshConfigHandlerImpl): MeshConfigHandler @Binds @Singleton - abstract fun bindMeshConfigFlowManager( - meshConfigFlowManagerImpl: MeshConfigFlowManagerImpl - ): MeshConfigFlowManager + abstract fun bindMeshConfigFlowManager(meshConfigFlowManagerImpl: MeshConfigFlowManagerImpl): MeshConfigFlowManager - @Binds - @Singleton - abstract fun bindMessageFilter( - messageFilterImpl: MessageFilterImpl - ): MessageFilter + @Binds @Singleton + abstract fun bindMessageFilter(messageFilterImpl: MessageFilterImpl): MessageFilter companion object { @Provides diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/di/UseCaseModule.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/di/UseCaseModule.kt index b145e54b2c..8093d73e91 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/di/UseCaseModule.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/di/UseCaseModule.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.core.data.di import dagger.Module @@ -41,11 +40,6 @@ object UseCaseModule { radioController: RadioController, homoglyphEncodingPrefs: HomoglyphPrefs, messageQueue: MessageQueue, - ): SendMessageUseCase = SendMessageUseCase( - nodeRepository, - packetRepository, - radioController, - homoglyphEncodingPrefs, - messageQueue - ) + ): SendMessageUseCase = + SendMessageUseCase(nodeRepository, packetRepository, radioController, homoglyphEncodingPrefs, messageQueue) } diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt index 33d668a2d1..4f262071c7 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.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 @@ -73,10 +73,10 @@ constructor( override var lastNeighborInfo: NeighborInfo? = null - // We'll need a way to track connection state in shared code, + // We'll need a way to track connection state in shared code, // maybe via ServiceRepository or similar. // For now I'll assume it's injected or available. - + override fun start(scope: CoroutineScope) { this.scope = scope radioConfigRepository.localConfigFlow.onEach { localConfig.value = it }.launchIn(scope) @@ -137,7 +137,7 @@ constructor( p.status = MessageStatus.ERROR // throw RemoteException("Message too long: $actualSize bytes (max ${Constants.DATA_PAYLOAD_LEN.value})") // RemoteException is Android specific. For KMP we might want a custom exception. - throw IllegalStateException("Message too long: $actualSize bytes") + error("Message too long: $actualSize bytes") } else { p.status = MessageStatus.QUEUED } @@ -166,12 +166,7 @@ constructor( packetHandler.sendToRadio(meshPacket) } - override fun sendAdmin( - destNum: Int, - requestId: Int, - wantResponse: Boolean, - initFn: () -> AdminMessage, - ) { + override fun sendAdmin(destNum: Int, requestId: Int, wantResponse: Boolean, initFn: () -> AdminMessage) { val adminMsg = initFn().copy(session_passkey = sessionPasskey.get()) val packet = buildAdminPacket(to = destNum, id = requestId, wantResponse = wantResponse, adminMessage = adminMsg) diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImpl.kt index 0578a0810d..081d1a2078 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImpl.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 @@ -27,11 +27,11 @@ import org.meshtastic.proto.FromRadio import javax.inject.Inject import javax.inject.Singleton -/** - * Implementation of [FromRadioPacketHandler] that dispatches [FromRadio] variants to specialized handlers. - */ +/** Implementation of [FromRadioPacketHandler] that dispatches [FromRadio] variants to specialized handlers. */ @Singleton -class FromRadioPacketHandlerImpl @Inject constructor( +class FromRadioPacketHandlerImpl +@Inject +constructor( private val serviceRepository: ServiceRepository, private val router: Lazy, private val mqttManager: MqttManager, diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/HistoryManagerImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/HistoryManagerImpl.kt index f3ff53e4e1..a2df3d73a1 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/HistoryManagerImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/HistoryManagerImpl.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 @@ -30,7 +30,9 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class HistoryManagerImpl @Inject constructor( +class HistoryManagerImpl +@Inject +constructor( private val meshPrefs: MeshPrefs, private val packetHandler: PacketHandler, ) : HistoryManager { @@ -46,11 +48,12 @@ class HistoryManagerImpl @Inject constructor( historyReturnWindow: Int, historyReturnMax: Int, ): StoreAndForward { - val history = StoreAndForward.History( - last_request = lastRequest.coerceAtLeast(0), - window = historyReturnWindow.coerceAtLeast(0), - history_messages = historyReturnMax.coerceAtLeast(0), - ) + val history = + StoreAndForward.History( + last_request = lastRequest.coerceAtLeast(0), + window = historyReturnWindow.coerceAtLeast(0), + history_messages = historyReturnMax.coerceAtLeast(0), + ) return StoreAndForward(rr = StoreAndForward.RequestResponse.CLIENT_HISTORY, history = history) } @@ -84,16 +87,17 @@ class HistoryManagerImpl @Inject constructor( } val lastRequest = meshPrefs.getStoreForwardLastRequest(address) - val (window, max) = resolveHistoryRequestParameters( - storeForwardConfig?.history_return_window ?: 0, - storeForwardConfig?.history_return_max ?: 0, - ) + val (window, max) = + resolveHistoryRequestParameters( + storeForwardConfig?.history_return_window ?: 0, + storeForwardConfig?.history_return_max ?: 0, + ) val request = buildStoreForwardHistoryRequest(lastRequest, window, max) historyLog( "requestHistory trigger=$trigger transport=$transport addr=$address " + - "lastRequest=$lastRequest window=$window max=$max" + "lastRequest=$lastRequest window=$window max=$max", ) runCatching { @@ -117,7 +121,7 @@ class HistoryManagerImpl @Inject constructor( meshPrefs.setStoreForwardLastRequest(address, lastRequest) historyLog( "historyMarker updated source=$source transport=$transport " + - "addr=$address from=$current to=$lastRequest" + "addr=$address from=$current to=$lastRequest", ) } } diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImpl.kt index 681a5c39fa..0adf6a80ee 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImpl.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 @@ -54,7 +54,9 @@ import javax.inject.Singleton @Suppress("LongParameterList", "TooManyFunctions", "CyclomaticComplexMethod") @Singleton -class MeshActionHandlerImpl @Inject constructor( +class MeshActionHandlerImpl +@Inject +constructor( private val nodeManager: NodeManager, private val commandSender: CommandSender, private val packetRepository: Lazy, diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.kt index 10332c1b05..86026b9be1 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.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 @@ -45,9 +45,11 @@ import javax.inject.Singleton import org.meshtastic.core.model.MyNodeInfo as SharedMyNodeInfo import org.meshtastic.proto.MyNodeInfo as ProtoMyNodeInfo -@Suppress("LongParameterList") +@Suppress("LongParameterList", "TooManyFunctions") @Singleton -class MeshConfigFlowManagerImpl @Inject constructor( +class MeshConfigFlowManagerImpl +@Inject +constructor( private val nodeManager: NodeManager, private val connectionManager: Lazy, private val nodeRepository: NodeRepository, diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConfigHandlerImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConfigHandlerImpl.kt index ff58a93b3e..d5ff324266 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConfigHandlerImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConfigHandlerImpl.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 @@ -37,7 +37,9 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class MeshConfigHandlerImpl @Inject constructor( +class MeshConfigHandlerImpl +@Inject +constructor( private val radioConfigRepository: RadioConfigRepository, private val serviceRepository: ServiceRepository, private val nodeManager: NodeManager, diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt index 12aaca39ba..a420793dfc 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.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 @@ -71,7 +71,9 @@ import kotlin.time.DurationUnit @Suppress("LongParameterList", "TooManyFunctions") @Singleton -class MeshConnectionManagerImpl @Inject constructor( +class MeshConnectionManagerImpl +@Inject +constructor( private val radioInterfaceService: RadioInterfaceService, private val serviceRepository: ServiceRepository, private val serviceBroadcasts: ServiceBroadcasts, diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt index ea8ce6cf14..909a411de1 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.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 @@ -38,7 +38,6 @@ import org.meshtastic.core.model.util.MeshDataMapper import org.meshtastic.core.model.util.SfppHasher import org.meshtastic.core.model.util.decodeOrNull import org.meshtastic.core.model.util.toOneLiner -import org.meshtastic.core.prefs.mesh.MeshPrefs import org.meshtastic.core.repository.CommandSender import org.meshtastic.core.repository.HistoryManager import org.meshtastic.core.repository.MeshConfigFlowManager @@ -81,7 +80,9 @@ import kotlin.time.Duration.Companion.milliseconds @Suppress("LongParameterList", "TooManyFunctions", "LargeClass", "CyclomaticComplexMethod") @Singleton -class MeshDataHandlerImpl @Inject constructor( +class MeshDataHandlerImpl +@Inject +constructor( private val nodeManager: NodeManager, private val packetHandler: PacketHandler, private val serviceRepository: ServiceRepository, @@ -94,7 +95,6 @@ class MeshDataHandlerImpl @Inject constructor( private val configFlowManager: Lazy, private val commandSender: CommandSender, private val historyManager: HistoryManager, - private val meshPrefs: MeshPrefs, private val connectionManager: Lazy, private val tracerouteHandler: TracerouteHandler, private val neighborInfoHandler: NeighborInfoHandler, @@ -228,7 +228,7 @@ class MeshDataHandlerImpl @Inject constructor( handleReceivedStoreAndForward(dataPacket, u, myNodeNum) } - @Suppress("LongMethod") + @Suppress("LongMethod", "ReturnCount") private fun handleStoreForwardPlusPlus(packet: MeshPacket) { val payload = packet.decoded?.payload ?: return val sfpp = @@ -409,7 +409,7 @@ class MeshDataHandlerImpl @Inject constructor( val metrics = t.device_metrics val environment = t.environment_metrics val power = t.power_metrics - + var nextNode = node when { metrics != null -> { @@ -438,6 +438,7 @@ class MeshDataHandlerImpl @Inject constructor( } } + @Suppress("ReturnCount") private fun shouldBatteryNotificationShow(fromNum: Int, t: Telemetry, myNodeNum: Int): Boolean { val isRemote = (fromNum != myNodeNum) var shouldDisplay = false @@ -502,11 +503,8 @@ class MeshDataHandlerImpl @Inject constructor( else -> MessageStatus.ERROR } if (p != null && p.status != MessageStatus.RECEIVED) { - val updatedPacket = p.copy( - status = m, - relays = if (isAck) p.relays + 1 else p.relays, - relayNode = relayNode - ) + val updatedPacket = + p.copy(status = m, relays = if (isAck) p.relays + 1 else p.relays, relayNode = relayNode) packetRepository.get().update(updatedPacket) } @@ -596,7 +594,14 @@ class MeshDataHandlerImpl @Inject constructor( // Check if message should be filtered val isFiltered = shouldFilterMessage(dataPacket, contactKey) - insert(dataPacket, myNodeNum, contactKey, nowMillis, read = fromLocal || isFiltered, filtered = isFiltered) + insert( + dataPacket, + myNodeNum, + contactKey, + nowMillis, + read = fromLocal || isFiltered, + filtered = isFiltered, + ) if (!isFiltered) { handlePacketNotification(dataPacket, contactKey, updateNotification) } @@ -684,7 +689,7 @@ class MeshDataHandlerImpl @Inject constructor( val fromNode = nodeManager.nodeDBbyNodeNum[packet.from] ?: Node(num = packet.from) val toNode = nodeManager.nodeDBbyNodeNum[packet.to] ?: Node(num = packet.to) - + val reaction = Reaction( replyId = decoded.reply_id, @@ -720,7 +725,8 @@ class MeshDataHandlerImpl @Inject constructor( // Find the original packet to get the contactKey packetRepository.get().getPacketByPacketId(decoded.reply_id)?.let { originalPacket -> // Skip notification if the original message was filtered - val contactKey = "${originalPacket.channel}${if (originalPacket.from == DataPacket.ID_LOCAL) originalPacket.to else originalPacket.from}" + 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 nodeMuted = nodeManager.nodeDBbyID[fromId]?.isMuted == true val isSilent = conversationMuted || nodeMuted diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt index 37e3ad1b05..1c19c8f317 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.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 @@ -47,12 +47,12 @@ 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. - */ +/** Implementation of [MeshMessageProcessor] that handles raw radio messages and prepares mesh packets for routing. */ @Suppress("TooManyFunctions") @Singleton -class MeshMessageProcessorImpl @Inject constructor( +class MeshMessageProcessorImpl +@Inject +constructor( private val nodeManager: NodeManager, private val serviceRepository: ServiceRepository, private val meshLogRepository: Lazy, @@ -173,6 +173,7 @@ class MeshMessageProcessorImpl @Inject constructor( packets.forEach { processReceivedMeshPacket(it, myNodeNum) } } + @Suppress("LongMethod") private fun processReceivedMeshPacket(packet: MeshPacket, myNodeNum: Int?) { val decoded = packet.decoded ?: return val log = @@ -194,11 +195,13 @@ class MeshMessageProcessorImpl @Inject constructor( myNodeNum?.let { myNum -> val from = packet.from val isOtherNode = myNum != from - nodeManager.updateNode(myNum, withBroadcast = isOtherNode) { node: Node -> node.copy(lastHeard = nowSeconds.toInt()) } + nodeManager.updateNode(myNum, withBroadcast = isOtherNode) { node: Node -> + node.copy(lastHeard = nowSeconds.toInt()) + } nodeManager.updateNode(from, withBroadcast = false, channel = packet.channel) { node: Node -> val viaMqtt = packet.via_mqtt == true val isDirect = packet.hop_start == packet.hop_limit - + var snr = node.snr var rssi = node.rssi if (isDirect && packet.isLora() && !viaMqtt) { @@ -218,14 +221,14 @@ class MeshMessageProcessorImpl @Inject constructor( } else { packet.hop_start - packet.hop_limit } - + node.copy( lastHeard = packet.rx_time, viaMqtt = viaMqtt, lastTransport = packet.transport_mechanism.value, snr = snr, rssi = rssi, - hopsAway = hopsAway + hopsAway = hopsAway, ) } diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshRouterImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshRouterImpl.kt index ba1d923442..b079b1d868 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshRouterImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshRouterImpl.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 @@ -29,27 +29,40 @@ import org.meshtastic.core.repository.TracerouteHandler import javax.inject.Inject import javax.inject.Singleton -/** - * Implementation of [MeshRouter] that orchestrates specialized mesh packet handlers. - */ +/** Implementation of [MeshRouter] that orchestrates specialized mesh packet handlers. */ @Suppress("LongParameterList") @Singleton -class MeshRouterImpl @Inject constructor( - private val _dataHandler: Lazy, - private val _configHandler: Lazy, - private val _tracerouteHandler: Lazy, - private val _neighborInfoHandler: Lazy, - private val _configFlowManager: Lazy, - private val _mqttManager: Lazy, - private val _actionHandler: Lazy, +class MeshRouterImpl +@Inject +constructor( + private val dataHandlerLazy: Lazy, + private val configHandlerLazy: Lazy, + private val tracerouteHandlerLazy: Lazy, + private val neighborInfoHandlerLazy: Lazy, + private val configFlowManagerLazy: Lazy, + private val mqttManagerLazy: Lazy, + private val actionHandlerLazy: Lazy, ) : MeshRouter { - override val dataHandler: MeshDataHandler get() = _dataHandler.get() - override val configHandler: MeshConfigHandler get() = _configHandler.get() - override val tracerouteHandler: TracerouteHandler get() = _tracerouteHandler.get() - override val neighborInfoHandler: NeighborInfoHandler get() = _neighborInfoHandler.get() - override val configFlowManager: MeshConfigFlowManager get() = _configFlowManager.get() - override val mqttManager: MqttManager get() = _mqttManager.get() - override val actionHandler: MeshActionHandler get() = _actionHandler.get() + override val dataHandler: MeshDataHandler + get() = dataHandlerLazy.get() + + override val configHandler: MeshConfigHandler + get() = configHandlerLazy.get() + + override val tracerouteHandler: TracerouteHandler + get() = tracerouteHandlerLazy.get() + + override val neighborInfoHandler: NeighborInfoHandler + get() = neighborInfoHandlerLazy.get() + + override val configFlowManager: MeshConfigFlowManager + get() = configFlowManagerLazy.get() + + override val mqttManager: MqttManager + get() = mqttManagerLazy.get() + + override val actionHandler: MeshActionHandler + get() = actionHandlerLazy.get() override fun start(scope: CoroutineScope) { dataHandler.start(scope) diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MessageFilterImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MessageFilterImpl.kt index 4da8b4971b..906e615ae9 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MessageFilterImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MessageFilterImpl.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 @@ -23,9 +23,7 @@ import java.util.regex.PatternSyntaxException import javax.inject.Inject import javax.inject.Singleton -/** - * Implementation of [MessageFilter] that uses regex and plain text matching. - */ +/** Implementation of [MessageFilter] that uses regex and plain text matching. */ @Singleton class MessageFilterImpl @Inject constructor(private val filterPrefs: FilterPrefs) : MessageFilter { private var compiledPatterns: List = emptyList() diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt index f8c5ed68c9..ae4d518ae8 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt @@ -142,11 +142,7 @@ constructor( hw_model = HardwareModel.UNSET, ) - Node( - num = n, - user = defaultUser, - channel = channel, - ) + Node(num = n, user = defaultUser, channel = channel) } override fun updateNode(nodeNum: Int, withBroadcast: Boolean, channel: Int, transform: (Node) -> Node) { @@ -158,7 +154,7 @@ constructor( } if (next.user.id.isNotEmpty() && isNodeDbReady.value) { - // scope.handledLaunch { nodeRepository.upsert(next) } + // scope.handledLaunch { nodeRepository.upsert(next) } // TODO: Add upsert to repository interface } @@ -172,13 +168,14 @@ constructor( val newNode = (node.isUnknownUser && p.hw_model != HardwareModel.UNSET) val shouldPreserve = shouldPreserveExistingUser(node.user, p) - val next = if (shouldPreserve) { - node.copy(channel = channel, manuallyVerified = manuallyVerified) - } else { - val keyMatch = !node.hasPKC || node.user.public_key == p.public_key - val newUser = if (keyMatch) p else p.copy(public_key = ByteString.EMPTY) - node.copy(user = newUser, channel = channel, manuallyVerified = manuallyVerified) - } + val next = + if (shouldPreserve) { + node.copy(channel = channel, manuallyVerified = manuallyVerified) + } else { + val keyMatch = !node.hasPKC || node.user.public_key == p.public_key + val newUser = if (keyMatch) p else p.copy(public_key = ByteString.EMPTY) + node.copy(user = newUser, channel = channel, manuallyVerified = manuallyVerified) + } if (newNode && !shouldPreserve) { serviceNotifications.showNewNodeSeenNotification(next) } @@ -191,7 +188,7 @@ constructor( Logger.d { "Ignoring nop position update for the local node" } } else { updateNode(fromNum) { node -> - node.copy(position = p.copy(time = if (p.time != 0) p.time else (defaultTime / TIME_MS_TO_S).toInt())) + node.copy(position = p.copy(time = if (p.time != 0) p.time else (defaultTime / TIME_MS_TO_S).toInt())) } } } @@ -238,16 +235,17 @@ constructor( if (position != null) { next = next.copy(position = position) } - next = next.copy( - lastHeard = info.last_heard, - deviceMetrics = info.device_metrics ?: next.deviceMetrics, - channel = info.channel, - viaMqtt = info.via_mqtt, - hopsAway = info.hops_away ?: -1, - isFavorite = info.is_favorite, - isIgnored = info.is_ignored, - isMuted = info.is_muted - ) + next = + next.copy( + lastHeard = info.last_heard, + deviceMetrics = info.device_metrics ?: next.deviceMetrics, + channel = info.channel, + viaMqtt = info.via_mqtt, + hopsAway = info.hops_away ?: -1, + isFavorite = info.is_favorite, + isIgnored = info.is_ignored, + isMuted = info.is_muted, + ) next } } @@ -271,14 +269,16 @@ constructor( private fun Node.toNodeInfo(): NodeInfo = NodeInfo( num = num, - user = MeshUser( + user = + MeshUser( id = user.id, longName = user.long_name, shortName = user.short_name, hwModel = user.hw_model, - role = user.role.value + role = user.role.value, ), - position = Position( + position = + Position( latitude = latitude, longitude = longitude, altitude = position.altitude ?: 0, @@ -286,24 +286,23 @@ constructor( satellitesInView = position.sats_in_view ?: 0, groundSpeed = position.ground_speed ?: 0, groundTrack = position.ground_track ?: 0, - precisionBits = position.precision_bits ?: 0 - ).takeIf { latitude != 0.0 || longitude != 0.0 }, + precisionBits = position.precision_bits ?: 0, + ) + .takeIf { latitude != 0.0 || longitude != 0.0 }, snr = snr, rssi = rssi, lastHeard = lastHeard, - deviceMetrics = DeviceMetrics( + deviceMetrics = + DeviceMetrics( batteryLevel = deviceMetrics.battery_level ?: 0, voltage = deviceMetrics.voltage ?: 0f, channelUtilization = deviceMetrics.channel_utilization ?: 0f, airUtilTx = deviceMetrics.air_util_tx ?: 0f, - uptimeSeconds = deviceMetrics.uptime_seconds ?: 0 + uptimeSeconds = deviceMetrics.uptime_seconds ?: 0, ), channel = channel, - environmentMetrics = EnvironmentMetrics.fromTelemetryProto( - environmentMetrics, - 0 - ), + environmentMetrics = EnvironmentMetrics.fromTelemetryProto(environmentMetrics, 0), hopsAway = hopsAway, - nodeStatus = nodeStatus + nodeStatus = nodeStatus, ) } diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt index 11b139195b..a29cfed986 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.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 @@ -55,7 +55,9 @@ import kotlin.uuid.Uuid @Suppress("TooManyFunctions") @Singleton -class PacketHandlerImpl @Inject constructor( +class PacketHandlerImpl +@Inject +constructor( private val packetRepository: Lazy, private val serviceBroadcasts: ServiceBroadcasts, private val radioInterfaceService: RadioInterfaceService, @@ -138,6 +140,7 @@ class PacketHandlerImpl @Inject constructor( Logger.d { "packet queueJob started" } while (serviceRepository.connectionState.value == ConnectionState.Connected) { val packet = queuedPackets.poll() ?: break + @Suppress("TooGenericExceptionCaught", "SwallowedException") try { val response = sendPacket(packet) Logger.d { "queueJob packet id=${packet.id.toUInt()} waiting" } @@ -173,6 +176,7 @@ class PacketHandlerImpl @Inject constructor( dataPacket } + @Suppress("TooGenericExceptionCaught") private fun sendPacket(packet: MeshPacket): CompletableDeferred { val deferred = CompletableDeferred() queueResponse[packet.id] = deferred diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/TracerouteHandlerImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/TracerouteHandlerImpl.kt index 518d4a6826..2524e83018 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/TracerouteHandlerImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/TracerouteHandlerImpl.kt @@ -57,8 +57,9 @@ constructor( val full = packet.getFullTracerouteResponse( getUser = { num -> - nodeManager.nodeDBbyNodeNum[num]?.let { node: Node -> "${node.user.long_name} (${node.user.short_name})" } - ?: "Unknown" // We don't have strings in core:data yet, but we can fix this later + nodeManager.nodeDBbyNodeNum[num]?.let { node: Node -> + "${node.user.long_name} (${node.user.short_name})" + } ?: "Unknown" // We don't have strings in core:data yet, but we can fix this later }, headerTowards = "Route towards destination:", headerBack = "Route back to us:", diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryImpl.kt index ae2de87941..d4901d02b1 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryImpl.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 diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/FirmwareReleaseRepository.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/FirmwareReleaseRepository.kt index 6fc377d424..67ccdc091c 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/FirmwareReleaseRepository.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/FirmwareReleaseRepository.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 diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/LocationRepository.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/LocationRepository.kt index d9b4d02eb6..a1b7b8a5ae 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/LocationRepository.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/LocationRepository.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 diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/MeshLogRepository.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/MeshLogRepository.kt index 2e47f4cbaf..24a1cc8254 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/MeshLogRepository.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/MeshLogRepository.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 diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt index 58f0383dd8..0746fc06e3 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt @@ -119,9 +119,7 @@ constructor( } // Keep ourNodeInfo and myId correctly updated based on current connection and node DB - combine(nodeDBbyNum, nodeInfoReadDataSource.myNodeInfoFlow()) { db, info -> - info?.myNodeNum?.let { db[it] } - } + combine(nodeDBbyNum, nodeInfoReadDataSource.myNodeInfoFlow()) { db, info -> info?.myNodeNum?.let { db[it] } } .onEach { node -> _ourNodeInfo.value = node _myId.value = node?.user?.id @@ -133,7 +131,8 @@ constructor( * Returns the node number used for log queries. Maps [nodeNum] to [MeshLog.NODE_NUM_LOCAL] (0) if it is the locally * connected node. */ - override fun effectiveLogNodeId(nodeNum: Int): Flow = nodeInfoReadDataSource.myNodeInfoFlow() + override fun effectiveLogNodeId(nodeNum: Int): Flow = nodeInfoReadDataSource + .myNodeInfoFlow() .map { info -> if (nodeNum == info?.myNodeNum) MeshLog.NODE_NUM_LOCAL else nodeNum } .distinctUntilChanged() @@ -189,8 +188,9 @@ constructor( suspend fun upsert(node: NodeEntity) = withContext(dispatchers.io) { nodeInfoWriteDataSource.upsert(node) } /** Installs initial configuration data (local info and remote nodes) into the database. */ - override suspend fun installConfig(mi: MyNodeInfo, nodes: List) = - withContext(dispatchers.io) { nodeInfoWriteDataSource.installConfig(mi.toEntity(), nodes.map { it.toEntity() }) } + override suspend fun installConfig(mi: MyNodeInfo, nodes: List) = withContext(dispatchers.io) { + nodeInfoWriteDataSource.installConfig(mi.toEntity(), nodes.map { it.toEntity() }) + } /** Deletes all nodes from the database, optionally preserving favorites. */ override suspend fun clearNodeDB(preserveFavorites: Boolean) = @@ -212,14 +212,10 @@ constructor( } override suspend fun getNodesOlderThan(lastHeard: Int): List = - withContext(dispatchers.io) { - nodeInfoReadDataSource.getNodesOlderThan(lastHeard).map { it.toModel() } - } + withContext(dispatchers.io) { nodeInfoReadDataSource.getNodesOlderThan(lastHeard).map { it.toModel() } } override suspend fun getUnknownNodes(): List = - withContext(dispatchers.io) { - nodeInfoReadDataSource.getUnknownNodes().map { it.toModel() } - } + withContext(dispatchers.io) { nodeInfoReadDataSource.getUnknownNodes().map { it.toModel() } } /** Persists hardware metadata for a node. */ override suspend fun insertMetadata(nodeNum: Int, metadata: DeviceMetadata) = @@ -256,7 +252,7 @@ constructor( maxChannels = maxChannels, hasWifi = hasWifi, deviceId = deviceId, - pioEnv = pioEnv + pioEnv = pioEnv, ) private fun Node.toEntity() = NodeEntity( @@ -280,6 +276,6 @@ constructor( notes = notes, manuallyVerified = manuallyVerified, nodeStatus = nodeStatus, - lastTransport = lastTransport + lastTransport = lastTransport, ) } diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/PacketRepositoryImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/PacketRepositoryImpl.kt index 0c42eeb118..70fd25ba72 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/PacketRepositoryImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/PacketRepositoryImpl.kt @@ -51,13 +51,13 @@ constructor( private val dispatchers: CoroutineDispatchers, ) : SharedPacketRepository { - override fun getWaypoints(): Flow> = - dbManager.currentDb.flatMapLatest { db -> db.packetDao().getAllWaypointsFlow() } - .map { list -> list.map { it.data } } + override fun getWaypoints(): Flow> = dbManager.currentDb + .flatMapLatest { db -> db.packetDao().getAllWaypointsFlow() } + .map { list -> list.map { it.data } } - override fun getContacts(): Flow> = - dbManager.currentDb.flatMapLatest { db -> db.packetDao().getContactKeys() } - .map { map -> map.mapValues { it.value.data } } + override fun getContacts(): Flow> = dbManager.currentDb + .flatMapLatest { db -> db.packetDao().getContactKeys() } + .map { map -> map.mapValues { it.value.data } } override fun getContactsPaged(): Flow> = Pager( config = @@ -162,26 +162,27 @@ constructor( } } - override fun getMessagesFromPaged(contact: String, getNode: suspend (String?) -> Node): Flow> = Pager( - config = - PagingConfig( - pageSize = MESSAGES_PAGE_SIZE, - enablePlaceholders = false, - initialLoadSize = MESSAGES_PAGE_SIZE, - ), - pagingSourceFactory = { dbManager.currentDb.value.packetDao().getMessagesFromPaged(contact) }, - ) - .flow - .map { pagingData -> - pagingData.map { packet -> - val message = packet.toMessage(getNode) - message.replyId - .takeIf { it != null && it != 0 } - ?.let { getPacketByPacketIdInternal(it) } - ?.let { originalPacket -> originalPacket.toMessage(getNode) } - ?.let { originalMessage -> message.copy(originalMessage = originalMessage) } ?: message + override fun getMessagesFromPaged(contact: String, getNode: suspend (String?) -> Node): Flow> = + Pager( + config = + PagingConfig( + pageSize = MESSAGES_PAGE_SIZE, + enablePlaceholders = false, + initialLoadSize = MESSAGES_PAGE_SIZE, + ), + pagingSourceFactory = { dbManager.currentDb.value.packetDao().getMessagesFromPaged(contact) }, + ) + .flow + .map { pagingData -> + pagingData.map { packet -> + val message = packet.toMessage(getNode) + message.replyId + .takeIf { it != null && it != 0 } + ?.let { getPacketByPacketIdInternal(it) } + ?.let { originalPacket -> originalPacket.toMessage(getNode) } + ?.let { originalMessage -> message.copy(originalMessage = originalMessage) } ?: message + } } - } override fun getMessagesFromPaged( contactKey: String, @@ -216,14 +217,12 @@ constructor( override suspend fun updateMessageId(d: DataPacket, id: Int) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().updateMessageId(d, id) } - private suspend fun getPacketByIdInternal(requestId: Int) = - withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getPacketById(requestId) } - override suspend fun getPacketById(id: Int): DataPacket? = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getPacketById(id)?.data } - override suspend fun getPacketByPacketId(packetId: Int): DataPacket? = - withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getPacketByPacketId(packetId)?.packet?.data } + override suspend fun getPacketByPacketId(packetId: Int): DataPacket? = withContext(dispatchers.io) { + dbManager.currentDb.value.packetDao().getPacketByPacketId(packetId)?.packet?.data + } private suspend fun getPacketByPacketIdInternal(packetId: Int) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getPacketByPacketId(packetId) } @@ -256,29 +255,23 @@ constructor( override suspend fun update(packet: DataPacket): Unit = withContext(dispatchers.io) { val dao = dbManager.currentDb.value.packetDao() - dao.findPacketsWithId(packet.id).find { it.data == packet }?.let { - dao.update(it.copy(data = packet)) - } + dao.findPacketsWithId(packet.id).find { it.data == packet }?.let { dao.update(it.copy(data = packet)) } } - override suspend fun insertReaction(reaction: Reaction, myNodeNum: Int) = withContext(dispatchers.io) { - dbManager.currentDb.value.packetDao().insert(reaction.toEntity(myNodeNum)) - } + override suspend fun insertReaction(reaction: Reaction, myNodeNum: Int) = + withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().insert(reaction.toEntity(myNodeNum)) } override suspend fun updateReaction(reaction: Reaction) = withContext(dispatchers.io) { val dao = dbManager.currentDb.value.packetDao() - dao.findReactionsWithId(reaction.packetId).find { it.userId == reaction.user.id && it.emoji == reaction.emoji }?.let { - dao.update(reaction.toEntity(it.myNodeNum)) - } ?: Unit + dao.findReactionsWithId(reaction.packetId) + .find { it.userId == reaction.user.id && it.emoji == reaction.emoji } + ?.let { dao.update(reaction.toEntity(it.myNodeNum)) } ?: Unit } override suspend fun getReactionByPacketId(packetId: Int): Reaction? = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getReactionByPacketId(packetId)?.toReaction { null } } - private suspend fun getReactionByPacketIdInternal(packetId: Int) = - withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getReactionByPacketId(packetId) } - override suspend fun findPacketsWithId(packetId: Int): List = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().findPacketsWithId(packetId).map { it.data } } @@ -362,32 +355,29 @@ constructor( } } - override suspend fun updateSFPPStatusByHash( - hash: ByteArray, - status: MessageStatus, - rxTime: Long, - ): Unit = withContext(dispatchers.io) { - val dao = dbManager.currentDb.value.packetDao() - val hashByteString = hash.toByteString() - dao.findPacketBySfppHash(hashByteString)?.let { packet -> - // If it's already confirmed, don't downgrade it - if (packet.data.status == MessageStatus.SFPP_CONFIRMED && status == MessageStatus.SFPP_ROUTING) { - return@let + override suspend fun updateSFPPStatusByHash(hash: ByteArray, status: MessageStatus, rxTime: Long): Unit = + withContext(dispatchers.io) { + val dao = dbManager.currentDb.value.packetDao() + val hashByteString = hash.toByteString() + dao.findPacketBySfppHash(hashByteString)?.let { packet -> + // If it's already confirmed, don't downgrade it + if (packet.data.status == MessageStatus.SFPP_CONFIRMED && status == MessageStatus.SFPP_ROUTING) { + return@let + } + val newTime = if (rxTime > 0) rxTime * MILLISECONDS_IN_SECOND else packet.received_time + val updatedData = packet.data.copy(status = status, sfppHash = hashByteString, time = newTime) + dao.update(packet.copy(data = updatedData, sfpp_hash = hashByteString, received_time = newTime)) } - val newTime = if (rxTime > 0) rxTime * MILLISECONDS_IN_SECOND else packet.received_time - val updatedData = packet.data.copy(status = status, sfppHash = hashByteString, time = newTime) - dao.update(packet.copy(data = updatedData, sfpp_hash = hashByteString, received_time = newTime)) - } - dao.findReactionBySfppHash(hashByteString)?.let { reaction -> - if (reaction.status == MessageStatus.SFPP_CONFIRMED && status == MessageStatus.SFPP_ROUTING) { - return@let + dao.findReactionBySfppHash(hashByteString)?.let { reaction -> + if (reaction.status == MessageStatus.SFPP_CONFIRMED && status == MessageStatus.SFPP_ROUTING) { + return@let + } + val newTime = if (rxTime > 0) rxTime * MILLISECONDS_IN_SECOND else reaction.timestamp + val updatedReaction = reaction.copy(status = status, sfpp_hash = hashByteString, timestamp = newTime) + dao.update(updatedReaction) } - val newTime = if (rxTime > 0) rxTime * MILLISECONDS_IN_SECOND else reaction.timestamp - val updatedReaction = reaction.copy(status = status, sfpp_hash = hashByteString, timestamp = newTime) - dao.update(updatedReaction) } - } override suspend fun deleteMessages(uuidList: List) = withContext(dispatchers.io) { for (chunk in uuidList.chunked(DELETE_CHUNK_SIZE)) { @@ -408,9 +398,9 @@ constructor( suspend fun update(packet: RoomPacket) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().update(packet) } - override fun getContactSettings(): Flow> = - dbManager.currentDb.flatMapLatest { db -> db.packetDao().getContactSettings() } - .map { map -> map.mapValues { it.value.toShared() } } + override fun getContactSettings(): Flow> = dbManager.currentDb + .flatMapLatest { db -> db.packetDao().getContactSettings() } + .map { map -> map.mapValues { it.value.toShared() } } override suspend fun getContactSettings(contact: String): ContactSettings = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getContactSettings(contact)?.toShared() ?: ContactSettings(contact) @@ -431,11 +421,13 @@ constructor( override suspend fun getFilteredCount(contactKey: String): Int = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getFilteredCount(contactKey) } - override suspend fun setContactFilteringDisabled(contactKey: String, disabled: Boolean) = withContext(dispatchers.io) { - dbManager.currentDb.value.packetDao().setContactFilteringDisabled(contactKey, disabled) - } + override suspend fun setContactFilteringDisabled(contactKey: String, disabled: Boolean) = + withContext(dispatchers.io) { + dbManager.currentDb.value.packetDao().setContactFilteringDisabled(contactKey, disabled) + } - override suspend fun clearPacketDB() = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().deleteAll() } + override suspend fun clearPacketDB() = + withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().deleteAll() } override suspend fun migrateChannelsByPSK(oldSettings: List, newSettings: List) = withContext(dispatchers.io) { @@ -456,7 +448,7 @@ constructor( lastReadMessageUuid = lastReadMessageUuid, lastReadMessageTimestamp = lastReadMessageTimestamp, filteringDisabled = filteringDisabled, - isMuted = isMuted + isMuted = isMuted, ) private fun Reaction.toEntity(myNodeNum: Int) = RoomReaction( @@ -475,7 +467,7 @@ constructor( relayNode = relayNode, to = to, channel = channel, - sfpp_hash = sfppHash + sfpp_hash = sfppHash, ) companion object { diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/RadioConfigRepositoryImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/RadioConfigRepositoryImpl.kt index dcbd93a341..d76ac8eeec 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/RadioConfigRepositoryImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/RadioConfigRepositoryImpl.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 diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/TracerouteSnapshotRepository.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/TracerouteSnapshotRepository.kt index f046457b2f..e29572ac3a 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/TracerouteSnapshotRepository.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/TracerouteSnapshotRepository.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 diff --git a/core/data/src/test/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImplTest.kt b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImplTest.kt index 47680aa02b..e1b0c414f2 100644 --- a/core/data/src/test/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImplTest.kt +++ b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImplTest.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 @@ -45,7 +45,8 @@ class FromRadioPacketHandlerImplTest { @Before fun setup() { - handler = FromRadioPacketHandlerImpl(serviceRepository, { router }, mqttManager, packetHandler, serviceNotifications) + handler = + FromRadioPacketHandlerImpl(serviceRepository, { router }, mqttManager, packetHandler, serviceNotifications) } @Test diff --git a/core/data/src/test/kotlin/org/meshtastic/core/data/manager/HistoryManagerImplTest.kt b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/HistoryManagerImplTest.kt index 725a2e4cfb..ebf0ca0654 100644 --- a/core/data/src/test/kotlin/org/meshtastic/core/data/manager/HistoryManagerImplTest.kt +++ b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/HistoryManagerImplTest.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 @@ -24,11 +24,12 @@ class HistoryManagerImplTest { @Test fun `buildStoreForwardHistoryRequest copies positive parameters`() { - val request = HistoryManagerImpl.buildStoreForwardHistoryRequest( - lastRequest = 42, - historyReturnWindow = 15, - historyReturnMax = 25, - ) + val request = + HistoryManagerImpl.buildStoreForwardHistoryRequest( + lastRequest = 42, + historyReturnWindow = 15, + historyReturnMax = 25, + ) assertEquals(StoreAndForward.RequestResponse.CLIENT_HISTORY, request.rr) assertEquals(42, request.history?.last_request) @@ -38,11 +39,12 @@ class HistoryManagerImplTest { @Test fun `buildStoreForwardHistoryRequest clamps non-positive parameters`() { - val request = HistoryManagerImpl.buildStoreForwardHistoryRequest( - lastRequest = 0, - historyReturnWindow = -1, - historyReturnMax = 0, - ) + val request = + HistoryManagerImpl.buildStoreForwardHistoryRequest( + lastRequest = 0, + historyReturnWindow = -1, + historyReturnMax = 0, + ) assertEquals(StoreAndForward.RequestResponse.CLIENT_HISTORY, request.rr) assertEquals(0, request.history?.last_request) diff --git a/core/data/src/test/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerTest.kt b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerTest.kt index 8eb3a70a1f..25d0dbcbb7 100644 --- a/core/data/src/test/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerTest.kt +++ b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerTest.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 diff --git a/core/data/src/test/kotlin/org/meshtastic/core/data/manager/MessageFilterImplTest.kt b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/MessageFilterImplTest.kt index d47825225d..65c77ec7ed 100644 --- a/core/data/src/test/kotlin/org/meshtastic/core/data/manager/MessageFilterImplTest.kt +++ b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/MessageFilterImplTest.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 diff --git a/core/data/src/test/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryTest.kt b/core/data/src/test/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryTest.kt index 1759722002..a5cee75e8b 100644 --- a/core/data/src/test/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryTest.kt +++ b/core/data/src/test/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryTest.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 diff --git a/core/data/src/test/kotlin/org/meshtastic/core/data/repository/MeshLogRepositoryTest.kt b/core/data/src/test/kotlin/org/meshtastic/core/data/repository/MeshLogRepositoryTest.kt index 90aa15951b..78c56d8c10 100644 --- a/core/data/src/test/kotlin/org/meshtastic/core/data/repository/MeshLogRepositoryTest.kt +++ b/core/data/src/test/kotlin/org/meshtastic/core/data/repository/MeshLogRepositoryTest.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 diff --git a/core/data/src/test/kotlin/org/meshtastic/core/data/repository/NodeRepositoryTest.kt b/core/data/src/test/kotlin/org/meshtastic/core/data/repository/NodeRepositoryTest.kt index 58c9c78feb..978682f9f2 100644 --- a/core/data/src/test/kotlin/org/meshtastic/core/data/repository/NodeRepositoryTest.kt +++ b/core/data/src/test/kotlin/org/meshtastic/core/data/repository/NodeRepositoryTest.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 diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/DatabaseManager.kt b/core/database/src/main/kotlin/org/meshtastic/core/database/DatabaseManager.kt index 17513b6984..7b61e66a30 100644 --- a/core/database/src/main/kotlin/org/meshtastic/core/database/DatabaseManager.kt +++ b/core/database/src/main/kotlin/org/meshtastic/core/database/DatabaseManager.kt @@ -47,7 +47,12 @@ import org.meshtastic.core.repository.DatabaseManager as SharedDatabaseManager @Singleton @Suppress("TooManyFunctions") @OptIn(ExperimentalCoroutinesApi::class) -open class DatabaseManager @Inject constructor(private val app: Application, private val dispatchers: CoroutineDispatchers) : SharedDatabaseManager { +open class DatabaseManager +@Inject +constructor( + 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/main/kotlin/org/meshtastic/core/database/di/DatabaseModule.kt b/core/database/src/main/kotlin/org/meshtastic/core/database/di/DatabaseModule.kt index e170560704..8a722aa6c9 100644 --- a/core/database/src/main/kotlin/org/meshtastic/core/database/di/DatabaseModule.kt +++ b/core/database/src/main/kotlin/org/meshtastic/core/database/di/DatabaseModule.kt @@ -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.core.database.di import android.app.Application @@ -47,26 +46,20 @@ abstract class DatabaseModule { @Singleton fun provideDatabase(app: Application): MeshtasticDatabase = MeshtasticDatabase.getDatabase(app) - @Provides - fun provideNodeInfoDao(database: MeshtasticDatabase): NodeInfoDao = database.nodeInfoDao() + @Provides fun provideNodeInfoDao(database: MeshtasticDatabase): NodeInfoDao = database.nodeInfoDao() - @Provides - fun providePacketDao(database: MeshtasticDatabase): PacketDao = database.packetDao() + @Provides fun providePacketDao(database: MeshtasticDatabase): PacketDao = database.packetDao() - @Provides - fun provideMeshLogDao(database: MeshtasticDatabase): MeshLogDao = database.meshLogDao() + @Provides fun provideMeshLogDao(database: MeshtasticDatabase): MeshLogDao = database.meshLogDao() @Provides - fun provideQuickChatActionDao(database: MeshtasticDatabase): QuickChatActionDao = - database.quickChatActionDao() + fun provideQuickChatActionDao(database: MeshtasticDatabase): QuickChatActionDao = database.quickChatActionDao() @Provides - fun provideDeviceHardwareDao(database: MeshtasticDatabase): DeviceHardwareDao = - database.deviceHardwareDao() + fun provideDeviceHardwareDao(database: MeshtasticDatabase): DeviceHardwareDao = database.deviceHardwareDao() @Provides - fun provideFirmwareReleaseDao(database: MeshtasticDatabase): FirmwareReleaseDao = - database.firmwareReleaseDao() + fun provideFirmwareReleaseDao(database: MeshtasticDatabase): FirmwareReleaseDao = database.firmwareReleaseDao() @Provides fun provideTracerouteNodePositionDao(database: MeshtasticDatabase): TracerouteNodePositionDao = diff --git a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/CleanNodeDatabaseUseCase.kt b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/CleanNodeDatabaseUseCase.kt index 446ec0fc33..ee66d847be 100644 --- a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/CleanNodeDatabaseUseCase.kt +++ b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/CleanNodeDatabaseUseCase.kt @@ -43,10 +43,9 @@ constructor( nodeRepository.getNodesOlderThan(olderThanTimestamp.toInt()) } - return nodesToConsider - .filterNot { node -> - (node.hasPKC && node.lastHeard >= sevenDaysAgoSeconds) || node.isIgnored || node.isFavorite - } + return nodesToConsider.filterNot { node -> + (node.hasPKC && node.lastHeard >= sevenDaysAgoSeconds) || node.isIgnored || node.isFavorite + } } /** Performs the cleanup of specified nodes. */ diff --git a/core/model/src/androidHostTest/kotlin/org/meshtastic/core/model/util/MeshDataMapperTest.kt b/core/model/src/androidHostTest/kotlin/org/meshtastic/core/model/util/MeshDataMapperTest.kt index ec6559bbd8..e9403ce856 100644 --- a/core/model/src/androidHostTest/kotlin/org/meshtastic/core/model/util/MeshDataMapperTest.kt +++ b/core/model/src/androidHostTest/kotlin/org/meshtastic/core/model/util/MeshDataMapperTest.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 diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/InterfaceId.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/InterfaceId.kt index 7a66f713e8..a89f706d96 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/InterfaceId.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/InterfaceId.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 @@ -16,9 +16,7 @@ */ package org.meshtastic.core.model -/** - * Address identifiers for all supported radio backend implementations. - */ +/** Address identifiers for all supported radio backend implementations. */ enum class InterfaceId(val id: Char) { BLUETOOTH('x'), MOCK('m'), @@ -28,8 +26,6 @@ enum class InterfaceId(val id: Char) { ; companion object { - fun forIdChar(id: Char): InterfaceId? { - return entries.firstOrNull { it.id == id } - } + fun forIdChar(id: Char): InterfaceId? = entries.firstOrNull { it.id == id } } } diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/MeshActivity.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/MeshActivity.kt index bbfd279e7d..8b94a9fe0b 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/MeshActivity.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/MeshActivity.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 @@ -16,9 +16,7 @@ */ package org.meshtastic.core.model -/** - * Represents activity on the mesh network. - */ +/** Represents activity on the mesh network. */ sealed class MeshActivity { /** Data is being sent to the radio. */ data object Send : MeshActivity() diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Message.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Message.kt index 678e57a621..0dd87b399e 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Message.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Message.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 diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Node.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Node.kt index c667e9098c..b7f2dd31a2 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Node.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Node.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 @@ -138,6 +138,7 @@ data class Node( fun gpsString(): String = GPSFormat.toDec(latitude, longitude) + @Suppress("CyclomaticComplexMethod") private fun EnvironmentMetrics.getDisplayStrings(isFahrenheit: Boolean): List { val temp = if ((temperature ?: 0f) != 0f) { diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/NodeSortOption.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/NodeSortOption.kt index a1e0c5ecef..7e2757c066 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/NodeSortOption.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/NodeSortOption.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 diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/RadioNotConnectedException.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/RadioNotConnectedException.kt index 23cf84615f..afeed6a67d 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/RadioNotConnectedException.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/RadioNotConnectedException.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 @@ -16,7 +16,5 @@ */ package org.meshtastic.core.model -/** - * Exception thrown when an operation is attempted while not connected to a mesh radio. - */ +/** Exception thrown when an operation is attempted while not connected to a mesh radio. */ open class RadioNotConnectedException(message: String = "Not connected to radio") : Exception(message) diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Reaction.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Reaction.kt index 04c9cc14da..1102441135 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Reaction.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Reaction.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 diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/TAK.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/TAK.kt index 4f4eb56c2a..cc1f5c95c8 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/TAK.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/TAK.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 diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/service/ServiceAction.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/service/ServiceAction.kt index ce9d610c36..a64822f447 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/service/ServiceAction.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/service/ServiceAction.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 diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/service/TracerouteResponse.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/service/TracerouteResponse.kt index fe57fb1c43..38cd9462ff 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/service/TracerouteResponse.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/service/TracerouteResponse.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 diff --git a/core/network/src/main/kotlin/org/meshtastic/core/network/repository/TrustAllX509TrustManager.kt b/core/network/src/main/kotlin/org/meshtastic/core/network/repository/TrustAllX509TrustManager.kt index 8b782ab8ec..28dd330b55 100644 --- a/core/network/src/main/kotlin/org/meshtastic/core/network/repository/TrustAllX509TrustManager.kt +++ b/core/network/src/main/kotlin/org/meshtastic/core/network/repository/TrustAllX509TrustManager.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.core.network.repository import android.annotation.SuppressLint @@ -24,6 +23,8 @@ import javax.net.ssl.X509TrustManager @SuppressLint("CustomX509TrustManager", "TrustAllX509TrustManager") class TrustAllX509TrustManager : X509TrustManager { override fun checkClientTrusted(chain: Array?, authType: String?) {} + override fun checkServerTrusted(chain: Array?, authType: String?) {} + override fun getAcceptedIssuers(): Array = arrayOf() } diff --git a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/di/PrefsModule.kt b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/di/PrefsModule.kt index 1f67f50e84..2e5285be83 100644 --- a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/di/PrefsModule.kt +++ b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/di/PrefsModule.kt @@ -109,7 +109,10 @@ interface PrefsModule { @Binds fun bindHomoglyphEncodingPrefs(homoglyphEncodingPrefsImpl: HomoglyphPrefsImpl): HomoglyphPrefs - @Binds fun bindSharedHomoglyphPrefs(homoglyphEncodingPrefsImpl: HomoglyphPrefsImpl): org.meshtastic.core.repository.HomoglyphPrefs + @Binds + fun bindSharedHomoglyphPrefs( + homoglyphEncodingPrefsImpl: HomoglyphPrefsImpl, + ): org.meshtastic.core.repository.HomoglyphPrefs @Binds fun bindCustomEmojiPrefs(customEmojiPrefsImpl: CustomEmojiPrefsImpl): CustomEmojiPrefs diff --git a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/homoglyph/HomoglyphPrefs.kt b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/homoglyph/HomoglyphPrefs.kt index f37d802a4f..b77b6fa970 100644 --- a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/homoglyph/HomoglyphPrefs.kt +++ b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/homoglyph/HomoglyphPrefs.kt @@ -24,7 +24,6 @@ import org.meshtastic.core.prefs.PrefDelegate import org.meshtastic.core.prefs.di.HomoglyphEncodingSharedPreferences import javax.inject.Inject import javax.inject.Singleton - import org.meshtastic.core.repository.HomoglyphPrefs as SharedHomoglyphPrefs interface HomoglyphPrefs : SharedHomoglyphPrefs { diff --git a/core/repository/build.gradle.kts b/core/repository/build.gradle.kts index 1695705a02..778dde9475 100644 --- a/core/repository/build.gradle.kts +++ b/core/repository/build.gradle.kts @@ -15,22 +15,18 @@ * along with this program. If not, see . */ -plugins { - alias(libs.plugins.meshtastic.kmp.library) -} +plugins { alias(libs.plugins.meshtastic.kmp.library) } kotlin { @Suppress("UnstableApiUsage") - android { - androidResources.enable = false - } + android { androidResources.enable = false } sourceSets { commonMain.dependencies { api(projects.core.model) api(projects.core.proto) implementation(projects.core.common) - + implementation(libs.kotlinx.coroutines.core) implementation(libs.kermit) implementation(libs.androidx.paging.common) diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/AppWidgetUpdater.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/AppWidgetUpdater.kt index ef2de8f6f3..fc23047c03 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/AppWidgetUpdater.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/AppWidgetUpdater.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 @@ -16,9 +16,7 @@ */ package org.meshtastic.core.repository -/** - * Interface for triggering updates to application widgets. - */ +/** Interface for triggering updates to application widgets. */ interface AppWidgetUpdater { /** Triggers an update for all app widgets. */ suspend fun updateAll() diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/CommandSender.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/CommandSender.kt index 77f5117bb5..e69310d68b 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/CommandSender.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/CommandSender.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 @@ -25,9 +25,8 @@ import org.meshtastic.proto.ChannelSet import org.meshtastic.proto.LocalConfig import org.meshtastic.proto.NeighborInfo -/** - * Interface for sending commands and packets to the mesh network. - */ +/** Interface for sending commands and packets to the mesh network. */ +@Suppress("TooManyFunctions") interface CommandSender { /** Starts the command sender with the given coroutine scope. */ fun start(scope: CoroutineScope) diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/DatabaseManager.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/DatabaseManager.kt index c45e7f2403..18ab5f81fa 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/DatabaseManager.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/DatabaseManager.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 @@ -18,9 +18,7 @@ package org.meshtastic.core.repository import kotlinx.coroutines.flow.StateFlow -/** - * Interface for managing database instances and cache limits. - */ +/** Interface for managing database instances and cache limits. */ interface DatabaseManager { /** Reactive stream of the current database cache limit. */ val cacheLimit: StateFlow diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/DeviceHardwareRepository.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/DeviceHardwareRepository.kt index ce05cf8240..2c2a198cde 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/DeviceHardwareRepository.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/DeviceHardwareRepository.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 diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/FromRadioPacketHandler.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/FromRadioPacketHandler.kt index ee953b9d18..a362628c60 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/FromRadioPacketHandler.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/FromRadioPacketHandler.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 @@ -18,9 +18,7 @@ package org.meshtastic.core.repository import org.meshtastic.proto.FromRadio -/** - * Interface for dispatching non-packet [FromRadio] variants to their respective handlers. - */ +/** Interface for dispatching non-packet [FromRadio] variants to their respective handlers. */ interface FromRadioPacketHandler { /** Processes a [FromRadio] message. */ fun handleFromRadio(proto: FromRadio) diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/HistoryManager.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/HistoryManager.kt index 78b70ed5e3..38d1f2ddc7 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/HistoryManager.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/HistoryManager.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 @@ -18,9 +18,7 @@ package org.meshtastic.core.repository import org.meshtastic.proto.ModuleConfig -/** - * Interface for managing store-and-forward history replay requests. - */ +/** Interface for managing store-and-forward history replay requests. */ interface HistoryManager { /** * Requests a history replay from the radio. diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/HomoglyphPrefs.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/HomoglyphPrefs.kt index c8eb761d9c..4c497af0b5 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/HomoglyphPrefs.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/HomoglyphPrefs.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 diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshActionHandler.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshActionHandler.kt index 8e8f00605e..d55bbe2dd8 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshActionHandler.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshActionHandler.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 @@ -22,9 +22,8 @@ import org.meshtastic.core.model.MeshUser import org.meshtastic.core.model.Position import org.meshtastic.core.model.service.ServiceAction -/** - * Interface for handling UI-triggered actions and administrative commands for the mesh. - */ +/** Interface for handling UI-triggered actions and administrative commands for the mesh. */ +@Suppress("TooManyFunctions") interface MeshActionHandler { /** Starts the handler with the given coroutine scope. */ fun start(scope: CoroutineScope) diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshConfigFlowManager.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshConfigFlowManager.kt index 05847ff9a8..1f21df1ee9 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshConfigFlowManager.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshConfigFlowManager.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 @@ -21,9 +21,7 @@ import org.meshtastic.proto.DeviceMetadata import org.meshtastic.proto.MyNodeInfo import org.meshtastic.proto.NodeInfo -/** - * Interface for managing the configuration flow, including local node info and metadata. - */ +/** Interface for managing the configuration flow, including local node info and metadata. */ interface MeshConfigFlowManager { /** Starts the manager with the given coroutine scope. */ fun start(scope: CoroutineScope) diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshConfigHandler.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshConfigHandler.kt index fed430bead..aae9526f39 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshConfigHandler.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshConfigHandler.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 @@ -24,9 +24,7 @@ import org.meshtastic.proto.LocalConfig import org.meshtastic.proto.LocalModuleConfig import org.meshtastic.proto.ModuleConfig -/** - * Interface for handling device and module configuration updates. - */ +/** Interface for handling device and module configuration updates. */ interface MeshConfigHandler { /** Starts the handler with the given coroutine scope. */ fun start(scope: CoroutineScope) diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshConnectionManager.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshConnectionManager.kt index 9aff94e0e8..eae5bd9a0d 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshConnectionManager.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshConnectionManager.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 @@ -19,9 +19,7 @@ package org.meshtastic.core.repository import kotlinx.coroutines.CoroutineScope import org.meshtastic.proto.Telemetry -/** - * Interface for managing the connection lifecycle and status with the mesh radio. - */ +/** Interface for managing the connection lifecycle and status with the mesh radio. */ interface MeshConnectionManager { /** Starts the connection manager with the given coroutine scope. */ fun start(scope: CoroutineScope) diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshDataHandler.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshDataHandler.kt index afd4a97118..2c7487cf99 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshDataHandler.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshDataHandler.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 @@ -21,9 +21,7 @@ import kotlinx.coroutines.Job import org.meshtastic.core.model.DataPacket import org.meshtastic.proto.MeshPacket -/** - * Interface for handling incoming mesh data packets and routing them to the appropriate handlers. - */ +/** Interface for handling incoming mesh data packets and routing them to the appropriate handlers. */ interface MeshDataHandler { /** Starts the handler with the given coroutine scope. */ fun start(scope: CoroutineScope) @@ -36,12 +34,7 @@ interface MeshDataHandler { * @param logUuid Optional UUID for logging purposes. * @param logInsertJob Optional job that tracks the insertion of the packet into the log. */ - fun handleReceivedData( - packet: MeshPacket, - myNodeNum: Int, - logUuid: String? = null, - logInsertJob: Job? = null - ) + fun handleReceivedData(packet: MeshPacket, myNodeNum: Int, logUuid: String? = null, logInsertJob: Job? = null) /** * Persists a data packet in the history and triggers notifications if necessary. @@ -50,9 +43,5 @@ interface MeshDataHandler { * @param myNodeNum The local node number. * @param updateNotification Whether to trigger a notification for this packet. */ - fun rememberDataPacket( - dataPacket: DataPacket, - myNodeNum: Int, - updateNotification: Boolean = true - ) + fun rememberDataPacket(dataPacket: DataPacket, myNodeNum: Int, updateNotification: Boolean = true) } diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshLocationManager.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshLocationManager.kt index c48ff207b9..e619550e66 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshLocationManager.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshLocationManager.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 @@ -19,9 +19,7 @@ package org.meshtastic.core.repository import kotlinx.coroutines.CoroutineScope import org.meshtastic.proto.Position -/** - * Interface for managing the local node's location updates and reporting. - */ +/** Interface for managing the local node's location updates and reporting. */ interface MeshLocationManager { /** Starts location updates and reports them via the given function. */ fun start(scope: CoroutineScope, sendPositionFn: (Position) -> Unit) diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshMessageProcessor.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshMessageProcessor.kt index d5bf8a08e1..1a3657d9e1 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshMessageProcessor.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshMessageProcessor.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 @@ -19,9 +19,7 @@ package org.meshtastic.core.repository import kotlinx.coroutines.CoroutineScope import org.meshtastic.proto.MeshPacket -/** - * Interface for processing incoming radio messages and mesh packets. - */ +/** Interface for processing incoming radio messages and mesh packets. */ interface MeshMessageProcessor { /** Starts the processor with the given coroutine scope. */ fun start(scope: CoroutineScope) diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshRouter.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshRouter.kt index 36725c250b..b4dd60a4d4 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshRouter.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshRouter.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 @@ -18,9 +18,7 @@ package org.meshtastic.core.repository import kotlinx.coroutines.CoroutineScope -/** - * Interface for the central router that orchestrates specialized mesh packet handlers. - */ +/** Interface for the central router that orchestrates specialized mesh packet handlers. */ interface MeshRouter { /** Starts the router and its sub-components with the given coroutine scope. */ fun start(scope: CoroutineScope) diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshWorkerManager.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshWorkerManager.kt index 05b0d91308..33ad246653 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshWorkerManager.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshWorkerManager.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 @@ -16,9 +16,7 @@ */ package org.meshtastic.core.repository -/** - * Interface for managing background workers for mesh-related tasks. - */ +/** Interface for managing background workers for mesh-related tasks. */ interface MeshWorkerManager { /** Enqueues a worker to send a specific packet. */ fun enqueueSendMessage(packetId: Int) diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MessageFilter.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MessageFilter.kt index 18fb5b0440..6b32e021df 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MessageFilter.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MessageFilter.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 @@ -16,9 +16,7 @@ */ package org.meshtastic.core.repository -/** - * Interface for filtering messages based on user-configured filter words. - */ +/** Interface for filtering messages based on user-configured filter words. */ interface MessageFilter { /** * Determines if a message should be filtered. @@ -29,8 +27,6 @@ interface MessageFilter { */ fun shouldFilter(message: String, isFilteringDisabled: Boolean = false): Boolean - /** - * Rebuilds the internal filter patterns. Should be called after filter words are updated. - */ + /** Rebuilds the internal filter patterns. Should be called after filter words are updated. */ fun rebuildPatterns() } diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MqttManager.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MqttManager.kt index d34906fe10..cfda5a9d09 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MqttManager.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MqttManager.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 @@ -19,9 +19,7 @@ package org.meshtastic.core.repository import kotlinx.coroutines.CoroutineScope import org.meshtastic.proto.MqttClientProxyMessage -/** - * Interface for managing MQTT proxy communication. - */ +/** Interface for managing MQTT proxy communication. */ interface MqttManager { /** Starts the MQTT manager with the given coroutine scope and settings. */ fun start(scope: CoroutineScope, enabled: Boolean, proxyToClientEnabled: Boolean) diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NeighborInfoHandler.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NeighborInfoHandler.kt index 45a8dc72aa..1dd95b5d93 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NeighborInfoHandler.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NeighborInfoHandler.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 @@ -19,9 +19,7 @@ package org.meshtastic.core.repository import kotlinx.coroutines.CoroutineScope import org.meshtastic.proto.MeshPacket -/** - * Interface for handling neighbor info responses from the mesh. - */ +/** Interface for handling neighbor info responses from the mesh. */ interface NeighborInfoHandler { /** Starts the neighbor info handler with the given coroutine scope. */ fun start(scope: CoroutineScope) diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeManager.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeManager.kt index 9dd9c4c2ff..15baf651e4 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeManager.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeManager.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 @@ -30,25 +30,24 @@ import org.meshtastic.proto.User import org.meshtastic.proto.NodeInfo as ProtoNodeInfo import org.meshtastic.proto.Position as ProtoPosition -/** - * Interface for managing the in-memory node database and processing received node information. - */ +/** Interface for managing the in-memory node database and processing received node information. */ +@Suppress("TooManyFunctions") interface NodeManager : NodeIdLookup { /** Reactive map of all nodes by their number. */ val nodeDBbyNodeNum: Map - + /** Reactive map of all nodes by their ID string. */ val nodeDBbyID: Map /** Whether the node database is ready. */ val isNodeDbReady: StateFlow - + /** Sets whether the node database is ready. */ fun setNodeDbReady(ready: Boolean) /** Whether node database writes are allowed. */ val allowNodeDbWrites: StateFlow - + /** Sets whether node database writes are allowed. */ fun setAllowNodeDbWrites(allowed: Boolean) @@ -99,7 +98,7 @@ interface NodeManager : NodeIdLookup { /** Installs node information from a ProtoNodeInfo object. */ fun installNodeInfo(info: ProtoNodeInfo, withBroadcast: Boolean = true) - + /** Inserts hardware metadata for a node. */ fun insertMetadata(nodeNum: Int, metadata: DeviceMetadata) } diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeRepository.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeRepository.kt index e96fc90688..470d85cb63 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeRepository.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeRepository.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 @@ -25,10 +25,8 @@ import org.meshtastic.proto.DeviceMetadata import org.meshtastic.proto.LocalStats import org.meshtastic.proto.User -/** - * Repository interface for managing node-related data. - * This interface is shared across platforms via KMP. - */ +/** Repository interface for managing node-related data. This interface is shared across platforms via KMP. */ +@Suppress("TooManyFunctions") interface NodeRepository { /** Hardware info about our local device. */ val myNodeInfo: StateFlow diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketHandler.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketHandler.kt index 01b2126fc7..5b6d785284 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketHandler.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketHandler.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 @@ -21,9 +21,7 @@ import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.QueueStatus import org.meshtastic.proto.ToRadio -/** - * Interface for handling the transmission of packets to the radio and managing the packet queue. - */ +/** Interface for handling the transmission of packets to the radio and managing the packet queue. */ interface PacketHandler { /** Starts the packet handler with the given coroutine scope. */ fun start(scope: CoroutineScope) diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketRepository.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketRepository.kt index 018e0a7a7b..2e006d4c65 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketRepository.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketRepository.kt @@ -26,11 +26,12 @@ import org.meshtastic.core.model.Node import org.meshtastic.core.model.Reaction import org.meshtastic.proto.ChannelSettings +@Suppress("TooManyFunctions") interface PacketRepository { fun getWaypoints(): Flow> - + fun getContacts(): Flow> - + fun getContactsPaged(): Flow> suspend fun getMessageCount(contact: String): Int @@ -65,10 +66,7 @@ interface PacketRepository { getNode: suspend (String?) -> Node, ): Flow> - fun getMessagesFromPaged( - contact: String, - getNode: suspend (String?) -> Node - ): Flow> + fun getMessagesFromPaged(contact: String, getNode: suspend (String?) -> Node): Flow> fun getMessagesFromPaged( contactKey: String, @@ -103,12 +101,19 @@ interface PacketRepository { suspend fun migrateChannelsByPSK(oldSettings: List, newSettings: List) suspend fun updateFilteredBySender(senderId: String, filtered: Boolean) - + suspend fun getPacketByPacketId(packetId: Int): DataPacket? suspend fun getPacketById(id: Int): DataPacket? - suspend fun insert(packet: DataPacket, myNodeNum: Int, contactKey: String, receivedTime: Long, read: Boolean = true, filtered: Boolean = false) + suspend fun insert( + packet: DataPacket, + myNodeNum: Int, + contactKey: String, + receivedTime: Long, + read: Boolean = true, + filtered: Boolean = false, + ) suspend fun update(packet: DataPacket) @@ -132,9 +137,5 @@ interface PacketRepository { myNodeNum: Int?, ) - suspend fun updateSFPPStatusByHash( - hash: ByteArray, - status: MessageStatus, - rxTime: Long, - ) + suspend fun updateSFPPStatusByHash(hash: ByteArray, status: MessageStatus, rxTime: Long) } diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/RadioConfigRepository.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/RadioConfigRepository.kt index 6ec531c9d0..48053ab80a 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/RadioConfigRepository.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/RadioConfigRepository.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 diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/RadioInterfaceService.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/RadioInterfaceService.kt index 65b5998dee..787863341d 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/RadioInterfaceService.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/RadioInterfaceService.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 @@ -23,9 +23,7 @@ import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.InterfaceId import org.meshtastic.core.model.MeshActivity -/** - * Interface for the low-level radio interface that handles raw byte communication. - */ +/** Interface for the low-level radio interface that handles raw byte communication. */ interface RadioInterfaceService { /** Reactive connection state of the radio. */ val connectionState: StateFlow diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/ServiceBroadcasts.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/ServiceBroadcasts.kt index 9e5809924a..fe3bf75381 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/ServiceBroadcasts.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/ServiceBroadcasts.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 @@ -20,9 +20,7 @@ import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.MessageStatus import org.meshtastic.core.model.Node -/** - * Interface for broadcasting service-level events to the application. - */ +/** Interface for broadcasting service-level events to the application. */ interface ServiceBroadcasts { /** Subscribes a receiver to mesh broadcasts. */ fun subscribeReceiver(receiverName: String, packageName: String) diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/ServiceRepository.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/ServiceRepository.kt index 881d32e22a..cae4f5c53f 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/ServiceRepository.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/ServiceRepository.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 @@ -26,67 +26,66 @@ import org.meshtastic.core.model.service.TracerouteResponse import org.meshtastic.proto.ClientNotification import org.meshtastic.proto.MeshPacket -/** - * Interface for managing service state, connection status, and mesh events. - */ +/** Interface for managing service state, connection status, and mesh events. */ +@Suppress("TooManyFunctions") interface ServiceRepository { /** Reactive connection state. */ val connectionState: StateFlow - + /** Sets the connection state. */ fun setConnectionState(connectionState: ConnectionState) /** Reactive client notification. */ val clientNotification: StateFlow - + /** Sets the current client notification. */ fun setClientNotification(notification: ClientNotification?) - + /** Clears the current client notification. */ fun clearClientNotification() /** Reactive error message. */ val errorMessage: StateFlow - + /** Sets an error message to be displayed. */ fun setErrorMessage(text: String, severity: Severity = Severity.Error) - + /** Clears the current error message. */ fun clearErrorMessage() /** Reactive connection progress message. */ val connectionProgress: StateFlow - + /** Sets the connection progress message. */ fun setConnectionProgress(text: String) /** Flow of all mesh packets. */ val meshPacketFlow: SharedFlow - + /** Emits a mesh packet into the flow. */ suspend fun emitMeshPacket(packet: MeshPacket) /** Reactive traceroute response. */ val tracerouteResponse: StateFlow - + /** Sets the traceroute response. */ fun setTracerouteResponse(value: TracerouteResponse?) - + /** Clears the traceroute response. */ fun clearTracerouteResponse() /** Reactive neighbor info response. */ val neighborInfoResponse: StateFlow - + /** Sets the neighbor info response. */ fun setNeighborInfoResponse(value: String?) - + /** Clears the neighbor info response. */ fun clearNeighborInfoResponse() /** Flow of service actions requested by the UI. */ val serviceAction: Flow - + /** Dispatches a service action. */ suspend fun onServiceAction(action: ServiceAction) } diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/TracerouteHandler.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/TracerouteHandler.kt index 949abc7636..bff5f03a0e 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/TracerouteHandler.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/TracerouteHandler.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 @@ -20,9 +20,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import org.meshtastic.proto.MeshPacket -/** - * Interface for handling traceroute responses from the mesh. - */ +/** Interface for handling traceroute responses from the mesh. */ interface TracerouteHandler { /** Starts the traceroute handler with the given coroutine scope. */ fun start(scope: CoroutineScope) diff --git a/core/service/src/main/kotlin/org/meshtastic/core/service/AndroidRadioControllerImpl.kt b/core/service/src/main/kotlin/org/meshtastic/core/service/AndroidRadioControllerImpl.kt index 90444acd2f..052ebe321c 100644 --- a/core/service/src/main/kotlin/org/meshtastic/core/service/AndroidRadioControllerImpl.kt +++ b/core/service/src/main/kotlin/org/meshtastic/core/service/AndroidRadioControllerImpl.kt @@ -202,9 +202,10 @@ constructor( override fun setDeviceAddress(address: String) { serviceRepository.meshService?.setDeviceAddress(address) // Ensure service is running/restarted to handle the new address - val intent = android.content.Intent().apply { - setClassName("com.geeksville.mesh", "com.geeksville.mesh.service.MeshService") - } + val intent = + android.content.Intent().apply { + setClassName("com.geeksville.mesh", "com.geeksville.mesh.service.MeshService") + } context.startForegroundService(intent) } } diff --git a/core/service/src/main/kotlin/org/meshtastic/core/service/ServiceRepository.kt b/core/service/src/main/kotlin/org/meshtastic/core/service/AndroidServiceRepository.kt similarity index 100% rename from core/service/src/main/kotlin/org/meshtastic/core/service/ServiceRepository.kt rename to core/service/src/main/kotlin/org/meshtastic/core/service/AndroidServiceRepository.kt diff --git a/core/service/src/main/kotlin/org/meshtastic/core/service/di/ServiceModule.kt b/core/service/src/main/kotlin/org/meshtastic/core/service/di/ServiceModule.kt index 262d2fabf3..38bb9feffa 100644 --- a/core/service/src/main/kotlin/org/meshtastic/core/service/di/ServiceModule.kt +++ b/core/service/src/main/kotlin/org/meshtastic/core/service/di/ServiceModule.kt @@ -30,11 +30,9 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) abstract class ServiceModule { - @Binds - @Singleton + @Binds @Singleton abstract fun bindRadioController(impl: AndroidRadioControllerImpl): RadioController - @Binds - @Singleton + @Binds @Singleton abstract fun bindServiceRepository(impl: AndroidServiceRepository): ServiceRepository } 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 e9eeef11f9..cf3ab34048 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 @@ -54,15 +54,11 @@ constructor( } private fun setChannel(channel: Channel) { - viewModelScope.launch { - radioController.setLocalChannel(channel) - } + viewModelScope.launch { radioController.setLocalChannel(channel) } } // Set the radio config (also updates our saved copy in preferences) private fun setConfig(config: Config) { - viewModelScope.launch { - radioController.setLocalConfig(config) - } + viewModelScope.launch { radioController.setLocalConfig(config) } } } diff --git a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandlerTest.kt b/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandlerTest.kt index c837941236..62f586a53a 100644 --- a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandlerTest.kt +++ b/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandlerTest.kt @@ -49,7 +49,8 @@ class Esp32OtaUpdateHandlerTest { private val context: Context = mockk() private val contentResolver: ContentResolver = mockk() - private val handler = Esp32OtaUpdateHandler(firmwareRetriever, radioController, nodeRepository, centralManager, context) + private val handler = + Esp32OtaUpdateHandler(firmwareRetriever, radioController, nodeRepository, centralManager, context) @Before fun setUp() { diff --git a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapView.kt b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapView.kt index db6721ea3f..e0931fa210 100644 --- a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapView.kt +++ b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapView.kt @@ -87,8 +87,8 @@ import org.meshtastic.core.common.gpsDisabled import org.meshtastic.core.common.util.DateFormatter import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.common.util.nowSeconds -import org.meshtastic.core.model.Node import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.model.Node import org.meshtastic.core.model.util.toString import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.calculating diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapViewModel.kt b/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapViewModel.kt index 2e5b6cc094..d47db4035d 100644 --- a/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapViewModel.kt +++ b/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapViewModel.kt @@ -644,7 +644,8 @@ constructor( (currentTileProvider as? MBTilesProvider)?.close() } - override fun getUser(userId: String?) = nodeRepository.getUser(userId ?: org.meshtastic.core.model.DataPacket.ID_BROADCAST) + override fun getUser(userId: String?) = + nodeRepository.getUser(userId ?: org.meshtastic.core.model.DataPacket.ID_BROADCAST) } enum class LayerType { diff --git a/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt b/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt index 68a71075c4..6b0a6be455 100644 --- a/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt +++ b/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt @@ -61,8 +61,10 @@ abstract class BaseMapViewModel( val myId = nodeRepository.myId - val isConnected = radioController.connectionState.map { it is org.meshtastic.core.model.ConnectionState.Connected } - .stateInWhileSubscribed(initialValue = false) + val isConnected = + radioController.connectionState + .map { it is org.meshtastic.core.model.ConnectionState.Connected } + .stateInWhileSubscribed(initialValue = false) val nodes: StateFlow> = nodeRepository @@ -133,8 +135,7 @@ abstract class BaseMapViewModel( abstract fun getUser(userId: String?): org.meshtastic.proto.User - fun getNodeOrFallback(nodeNum: Int): Node = - nodeRepository.nodeDBbyNum.value[nodeNum] ?: Node(num = nodeNum) + fun getNodeOrFallback(nodeNum: Int): Node = nodeRepository.nodeDBbyNum.value[nodeNum] ?: Node(num = nodeNum) fun deleteWaypoint(id: Int) = viewModelScope.launch(Dispatchers.IO) { packetRepository.deleteWaypoint(id) } @@ -148,9 +149,7 @@ abstract class BaseMapViewModel( } private fun sendDataPacket(p: DataPacket) { - viewModelScope.launch(Dispatchers.IO) { - radioController.sendMessage(p) - } + viewModelScope.launch(Dispatchers.IO) { radioController.sendMessage(p) } } fun generatePacketId(): Int = radioController.getPacketId() diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageScreenEvent.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageScreenEvent.kt index 83245fa9fb..8f9c722857 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageScreenEvent.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageScreenEvent.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.messaging import org.meshtastic.core.model.Node diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorker.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorker.kt index 81e1858d6b..bbc28a00d1 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorker.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorker.kt @@ -28,11 +28,13 @@ import org.meshtastic.core.model.RadioController import org.meshtastic.core.repository.PacketRepository @HiltWorker -class SendMessageWorker @AssistedInject constructor( +class SendMessageWorker +@AssistedInject +constructor( @Assisted context: Context, @Assisted params: WorkerParameters, private val packetRepository: PacketRepository, - private val radioController: RadioController + private val radioController: RadioController, ) : CoroutineWorker(context, params) { override suspend fun doWork(): Result { @@ -44,8 +46,9 @@ class SendMessageWorker @AssistedInject constructor( return Result.retry() } - val packetData = packetRepository.getPacketByPacketId(packetId) - ?: return Result.failure() // Packet no longer exists in DB? Do not retry. + val packetData = + packetRepository.getPacketByPacketId(packetId) + ?: return Result.failure() // Packet no longer exists in DB? Do not retry. return try { radioController.sendMessage(packetData) diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModel.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModel.kt index f39b490032..2b645bac2c 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModel.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModel.kt @@ -90,7 +90,8 @@ constructor( val contactKey = entry.key val packetData = entry.value // Determine if this is my message (originated on this device) - val fromLocal = (packetData.from == DataPacket.ID_LOCAL || (myId != null && packetData.from == myId)) + val fromLocal = + (packetData.from == DataPacket.ID_LOCAL || (myId != null && packetData.from == myId)) val toBroadcast = packetData.to == DataPacket.ID_BROADCAST // grab usernames from NodeInfo @@ -139,10 +140,13 @@ constructor( packetRepository.getContactsPaged().map { pagingData -> pagingData.map { packetData: DataPacket -> - val contactKey = "${packetData.channel}${packetData.to}" // This might be wrong, need to check how contactKey is derived in PagingSource + val contactKey = + "${packetData.channel}${packetData.to}" // This might be wrong, need to check how contactKey + // is derived in PagingSource // Determine if this is my message (originated on this device) - val fromLocal = (packetData.from == DataPacket.ID_LOCAL || (myId != null && packetData.from == myId)) + val fromLocal = + (packetData.from == DataPacket.ID_LOCAL || (myId != null && packetData.from == myId)) val toBroadcast = packetData.to == DataPacket.ID_BROADCAST // grab usernames from NodeInfo @@ -158,7 +162,8 @@ constructor( user.long_name } - val contactKeyComputed = if (toBroadcast) "${packetData.channel}${DataPacket.ID_BROADCAST}" else contactKey + val contactKeyComputed = + if (toBroadcast) "${packetData.channel}${DataPacket.ID_BROADCAST}" else contactKey Contact( contactKey = contactKeyComputed, diff --git a/feature/messaging/src/test/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorkerTest.kt b/feature/messaging/src/test/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorkerTest.kt index 1c4163855a..537bc1d63e 100644 --- a/feature/messaging/src/test/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorkerTest.kt +++ b/feature/messaging/src/test/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorkerTest.kt @@ -1,3 +1,19 @@ +/* + * 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.domain.worker import android.content.Context @@ -51,16 +67,20 @@ class SendMessageWorkerTest { coEvery { radioController.sendMessage(any()) } just Runs coEvery { packetRepository.updateMessageStatus(any(), any()) } just Runs - val worker = TestListenableWorkerBuilder(context) - .setInputData(workDataOf(SendMessageWorker.KEY_PACKET_ID to packetId)) - .setWorkerFactory(object : androidx.work.WorkerFactory() { - override fun createWorker( - appContext: Context, - workerClassName: String, - workerParameters: WorkerParameters - ): ListenableWorker? = SendMessageWorker(appContext, workerParameters, packetRepository, radioController) - }) - .build() + val worker = + TestListenableWorkerBuilder(context) + .setInputData(workDataOf(SendMessageWorker.KEY_PACKET_ID to packetId)) + .setWorkerFactory( + object : androidx.work.WorkerFactory() { + override fun createWorker( + appContext: Context, + workerClassName: String, + workerParameters: WorkerParameters, + ): ListenableWorker? = + SendMessageWorker(appContext, workerParameters, packetRepository, radioController) + }, + ) + .build() // Act val result = worker.doWork() @@ -79,16 +99,20 @@ class SendMessageWorkerTest { coEvery { packetRepository.getPacketByPacketId(packetId) } returns dataPacket every { radioController.connectionState } returns MutableStateFlow(ConnectionState.Disconnected) - val worker = TestListenableWorkerBuilder(context) - .setInputData(workDataOf(SendMessageWorker.KEY_PACKET_ID to packetId)) - .setWorkerFactory(object : androidx.work.WorkerFactory() { - override fun createWorker( - appContext: Context, - workerClassName: String, - workerParameters: WorkerParameters - ): ListenableWorker? = SendMessageWorker(appContext, workerParameters, packetRepository, radioController) - }) - .build() + val worker = + TestListenableWorkerBuilder(context) + .setInputData(workDataOf(SendMessageWorker.KEY_PACKET_ID to packetId)) + .setWorkerFactory( + object : androidx.work.WorkerFactory() { + override fun createWorker( + appContext: Context, + workerClassName: String, + workerParameters: WorkerParameters, + ): ListenableWorker? = + SendMessageWorker(appContext, workerParameters, packetRepository, radioController) + }, + ) + .build() // Act val result = worker.doWork() @@ -104,16 +128,20 @@ class SendMessageWorkerTest { val packetId = 999 coEvery { packetRepository.getPacketByPacketId(packetId) } returns null - val worker = TestListenableWorkerBuilder(context) - .setInputData(workDataOf(SendMessageWorker.KEY_PACKET_ID to packetId)) - .setWorkerFactory(object : androidx.work.WorkerFactory() { - override fun createWorker( - appContext: Context, - workerClassName: String, - workerParameters: WorkerParameters - ): ListenableWorker? = SendMessageWorker(appContext, workerParameters, packetRepository, radioController) - }) - .build() + val worker = + TestListenableWorkerBuilder(context) + .setInputData(workDataOf(SendMessageWorker.KEY_PACKET_ID to packetId)) + .setWorkerFactory( + object : androidx.work.WorkerFactory() { + override fun createWorker( + appContext: Context, + workerClassName: String, + workerParameters: WorkerParameters, + ): ListenableWorker? = + SendMessageWorker(appContext, workerParameters, packetRepository, radioController) + }, + ) + .build() // Act val result = worker.doWork() diff --git a/feature/node/src/fdroid/kotlin/org/meshtastic/feature/node/component/InlineMap.kt b/feature/node/src/fdroid/kotlin/org/meshtastic/feature/node/component/InlineMap.kt index 94fc796000..e9b3c50540 100644 --- a/feature/node/src/fdroid/kotlin/org/meshtastic/feature/node/component/InlineMap.kt +++ b/feature/node/src/fdroid/kotlin/org/meshtastic/feature/node/component/InlineMap.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.runtime.Composable diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeManagementActions.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeManagementActions.kt index 981aee4e4d..fbf79a4d7a 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeManagementActions.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeManagementActions.kt @@ -85,9 +85,7 @@ constructor( } fun ignoreNode(scope: CoroutineScope, node: Node) { - scope.launch(Dispatchers.IO) { - serviceRepository.onServiceAction(ServiceAction.Ignore(node)) - } + scope.launch(Dispatchers.IO) { serviceRepository.onServiceAction(ServiceAction.Ignore(node)) } } fun requestMuteNode(scope: CoroutineScope, node: Node) { @@ -103,9 +101,7 @@ constructor( } fun muteNode(scope: CoroutineScope, node: Node) { - scope.launch(Dispatchers.IO) { - serviceRepository.onServiceAction(ServiceAction.Mute(node)) - } + scope.launch(Dispatchers.IO) { serviceRepository.onServiceAction(ServiceAction.Mute(node)) } } fun requestFavoriteNode(scope: CoroutineScope, node: Node) { @@ -124,9 +120,7 @@ constructor( } fun favoriteNode(scope: CoroutineScope, node: Node) { - scope.launch(Dispatchers.IO) { - serviceRepository.onServiceAction(ServiceAction.Favorite(node)) - } + scope.launch(Dispatchers.IO) { serviceRepository.onServiceAction(ServiceAction.Favorite(node)) } } fun setNodeNotes(scope: CoroutineScope, nodeNum: Int, notes: String) { diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeRequestActions.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeRequestActions.kt index cf5e5dcc1a..63f3ebc45a 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeRequestActions.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeRequestActions.kt @@ -53,9 +53,7 @@ sealed class NodeRequestEffect { } @Singleton -class NodeRequestActions @Inject constructor( - private val radioController: RadioController, -) { +class NodeRequestActions @Inject constructor(private val radioController: RadioController) { private val _effects = MutableSharedFlow() val effects: SharedFlow = _effects.asSharedFlow() diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeFilterPreferences.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeFilterPreferences.kt index 89b36797f0..4af6eaaea6 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeFilterPreferences.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeFilterPreferences.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.list import kotlinx.coroutines.flow.map diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt index e213ce37a8..ae8cf2710f 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt @@ -116,8 +116,7 @@ constructor( val appVersionName get() = buildConfigProvider.versionName - val isOtaCapable: StateFlow = - isOtaCapableUseCase().stateInWhileSubscribed(initialValue = false) + val isOtaCapable: StateFlow = isOtaCapableUseCase().stateInWhileSubscribed(initialValue = false) // Device DB cache limit (bounded by DatabaseConstants) val dbCacheLimit: StateFlow = databaseManager.cacheLimit @@ -173,9 +172,7 @@ constructor( fun saveDataCsv(uri: Uri, filterPortnum: Int? = null) { viewModelScope.launch(Dispatchers.Main) { val myNodeNum = myNodeNum ?: return@launch - writeToUri(uri) { writer -> - exportDataUseCase(writer, myNodeNum, filterPortnum) - } + writeToUri(uri) { writer -> exportDataUseCase(writer, myNodeNum, filterPortnum) } } } diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt index d1c6cecae4..54b04c295c 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt @@ -356,9 +356,7 @@ constructor( fun setRingtone(ringtone: String) { val destNum = destNode.value?.num ?: return _radioConfigState.update { it.copy(ringtone = ringtone) } - viewModelScope.launch { - radioConfigUseCase.setRingtone(destNum, ringtone) - } + viewModelScope.launch { radioConfigUseCase.setRingtone(destNum, ringtone) } } private fun getRingtone(destNum: Int) { @@ -371,9 +369,7 @@ constructor( fun setCannedMessages(messages: String) { val destNum = destNode.value?.num ?: return _radioConfigState.update { it.copy(cannedMessageMessages = messages) } - viewModelScope.launch { - radioConfigUseCase.setCannedMessages(destNum, messages) - } + viewModelScope.launch { radioConfigUseCase.setCannedMessages(destNum, messages) } } private fun getCannedMessages(destNum: Int) { @@ -444,24 +440,18 @@ constructor( fun setFixedPosition(position: Position) { val destNum = destNode.value?.num ?: return - viewModelScope.launch { - radioConfigUseCase.setFixedPosition(destNum, position) - } + viewModelScope.launch { radioConfigUseCase.setFixedPosition(destNum, position) } } fun removeFixedPosition() { val destNum = destNode.value?.num ?: return - viewModelScope.launch { - radioConfigUseCase.removeFixedPosition(destNum) - } + viewModelScope.launch { radioConfigUseCase.removeFixedPosition(destNum) } } fun importProfile(uri: Uri, onResult: (DeviceProfile) -> Unit) = viewModelScope.launch(Dispatchers.IO) { try { app.contentResolver.openInputStream(uri)?.use { inputStream -> - importProfileUseCase(inputStream) - .onSuccess(onResult) - .onFailure { throw it } + importProfileUseCase(inputStream).onSuccess(onResult).onFailure { throw it } } } catch (ex: Exception) { Logger.e { "Import DeviceProfile error: ${ex.message}" } @@ -486,30 +476,27 @@ constructor( } } - fun exportSecurityConfig(uri: Uri, securityConfig: Config.SecurityConfig) = - viewModelScope.launch { - withContext(Dispatchers.IO) { - try { - app.contentResolver.openFileDescriptor(uri, "wt")?.use { parcelFileDescriptor -> - FileOutputStream(parcelFileDescriptor.fileDescriptor).use { outputStream -> - exportSecurityConfigUseCase(outputStream, securityConfig) - .onSuccess { setResponseStateSuccess() } - .onFailure { throw it } - } + fun exportSecurityConfig(uri: Uri, securityConfig: Config.SecurityConfig) = viewModelScope.launch { + withContext(Dispatchers.IO) { + try { + app.contentResolver.openFileDescriptor(uri, "wt")?.use { parcelFileDescriptor -> + FileOutputStream(parcelFileDescriptor.fileDescriptor).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) } + } catch (ex: Exception) { + val errorMessage = "Can't write security keys JSON error: ${ex.message}" + Logger.e { errorMessage } + sendError(ex.customMessage) } } + } fun installProfile(protobuf: DeviceProfile) { val destNum = destNode.value?.num ?: return - viewModelScope.launch { - installProfileUseCase(destNum, protobuf, destNode.value?.user) - } + viewModelScope.launch { installProfileUseCase(destNum, protobuf, destNode.value?.user) } } fun clearPacketResponse() { diff --git a/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModelTest.kt b/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModelTest.kt index 8e3993b470..40bb475eb0 100644 --- a/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModelTest.kt +++ b/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModelTest.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 @@ -37,10 +37,7 @@ class FilterSettingsViewModelTest { every { filterPrefs.filterEnabled } returns true every { filterPrefs.filterWords } returns setOf("apple", "banana") - viewModel = FilterSettingsViewModel( - filterPrefs = filterPrefs, - messageFilter = messageFilter - ) + viewModel = FilterSettingsViewModel(filterPrefs = filterPrefs, messageFilter = messageFilter) } @Test @@ -53,7 +50,7 @@ class FilterSettingsViewModelTest { @Test fun `addFilterWord updates prefs and rebuilds patterns`() { viewModel.addFilterWord("cherry") - + verify { filterPrefs.filterWords = any() } verify { messageFilter.rebuildPatterns() } assertEquals(listOf("apple", "banana", "cherry"), viewModel.filterWords.value) @@ -62,7 +59,7 @@ class FilterSettingsViewModelTest { @Test fun `removeFilterWord updates prefs and rebuilds patterns`() { viewModel.removeFilterWord("apple") - + verify { filterPrefs.filterWords = any() } verify { messageFilter.rebuildPatterns() } assertEquals(listOf("banana"), viewModel.filterWords.value) diff --git a/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt b/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt index 6f937f3b8d..adf6dd9ac2 100644 --- a/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt +++ b/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt @@ -89,14 +89,15 @@ class RadioConfigViewModelTest { @Before fun setUp() { Dispatchers.setMain(testDispatcher) - + every { nodeRepository.nodeDBbyNum } returns MutableStateFlow(emptyMap()) every { radioConfigRepository.deviceProfileFlow } returns MutableStateFlow(DeviceProfile()) every { radioConfigRepository.localConfigFlow } returns MutableStateFlow(LocalConfig()) every { radioConfigRepository.channelSetFlow } returns MutableStateFlow(ChannelSet()) every { radioConfigRepository.moduleConfigFlow } returns MutableStateFlow(LocalModuleConfig()) every { serviceRepository.meshPacketFlow } returns MutableSharedFlow() - every { serviceRepository.connectionState } returns MutableStateFlow(org.meshtastic.core.model.ConnectionState.Connected) + every { serviceRepository.connectionState } returns + MutableStateFlow(org.meshtastic.core.model.ConnectionState.Connected) every { nodeRepository.myNodeInfo } returns MutableStateFlow(null) viewModel = createViewModel() @@ -134,12 +135,12 @@ class RadioConfigViewModelTest { val node = Node(num = 123) every { nodeRepository.nodeDBbyNum } returns MutableStateFlow(mapOf(123 to node)) viewModel = createViewModel() - + val config = Config(device = Config.DeviceConfig(role = Config.DeviceConfig.Role.ROUTER)) coEvery { radioConfigUseCase.setConfig(123, any()) } returns 42 viewModel.setConfig(config) - + val state = viewModel.radioConfigState.value assertEquals(Config.DeviceConfig.Role.ROUTER, state.radioConfig.device?.role) coVerify { radioConfigUseCase.setConfig(123, config) } @@ -149,14 +150,14 @@ class RadioConfigViewModelTest { fun `processPacketResponse updates state on metadata result`() = runTest { val node = Node(num = 123) every { nodeRepository.nodeDBbyNum } returns MutableStateFlow(mapOf(123 to node)) - + val packet = MeshPacket() val metadata = DeviceMetadata(firmware_version = "3.0.0") val packetFlow = MutableSharedFlow() - + every { serviceRepository.meshPacketFlow } returns packetFlow every { processRadioResponseUseCase(any(), 123, any()) } returns RadioResponseResult.Metadata(metadata) - + viewModel = createViewModel() packetFlow.emit(packet) @@ -173,9 +174,9 @@ class RadioConfigViewModelTest { val user = org.meshtastic.proto.User(long_name = "Test") coEvery { radioConfigUseCase.setOwner(123, any()) } returns 42 - + viewModel.setOwner(user) - + coVerify { radioConfigUseCase.setOwner(123, user) } } @@ -187,11 +188,11 @@ class RadioConfigViewModelTest { val old = listOf(ChannelSettings(name = "Old")) val new = listOf(ChannelSettings(name = "New")) - + coEvery { radioConfigUseCase.setRemoteChannel(123, any()) } returns 42 - + viewModel.updateChannels(new, old) - + coVerify { radioConfigUseCase.setRemoteChannel(123, any()) } assertEquals(new, viewModel.radioConfigState.value.channelList) } @@ -200,20 +201,20 @@ class RadioConfigViewModelTest { fun `setResponseStateLoading for REBOOT calls useCase after packet response`() = runTest { val node = Node(num = 123) every { nodeRepository.nodeDBbyNum } returns MutableStateFlow(mapOf(123 to node)) - + val packetFlow = MutableSharedFlow() every { serviceRepository.meshPacketFlow } returns packetFlow every { processRadioResponseUseCase(any(), any(), any()) } returns RadioResponseResult.Success - + viewModel = createViewModel() - + coEvery { adminActionsUseCase.reboot(123) } returns 42 - + viewModel.setResponseStateLoading(AdminRoute.REBOOT) - + // Emit a packet to trigger processPacketResponse -> sendAdminRequest packetFlow.emit(MeshPacket()) - + coVerify { adminActionsUseCase.reboot(123) } } @@ -221,20 +222,20 @@ class RadioConfigViewModelTest { fun `setResponseStateLoading for FACTORY_RESET calls useCase after packet response`() = runTest { val node = Node(num = 123) every { nodeRepository.nodeDBbyNum } returns MutableStateFlow(mapOf(123 to node)) - + val packetFlow = MutableSharedFlow() every { serviceRepository.meshPacketFlow } returns packetFlow every { processRadioResponseUseCase(any(), any(), any()) } returns RadioResponseResult.Success - + viewModel = createViewModel() - + coEvery { adminActionsUseCase.factoryReset(123, any()) } returns 42 - + viewModel.setResponseStateLoading(AdminRoute.FACTORY_RESET) - + // Emit a packet to trigger processPacketResponse -> sendAdminRequest packetFlow.emit(MeshPacket()) - + coVerify { adminActionsUseCase.factoryReset(123, any()) } } } From a00ce2d89e2f92d00aeace7812894535805c4e6d Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 2 Mar 2026 15:24:15 -0600 Subject: [PATCH 12/20] feat: add node upsert and improve messaging reliability - Implement `upsert` in `NodeRepository` and enable it in `NodeManager` - Update `SendMessageWorker` to retry on failure instead of marking as error - Ensure `publicKey` is preserved during `NodeEntity` conversions - Refine packet ID generation to use positive non-zero integers - Remove redundant `Dispatchers.Main` in `SettingsViewModel` - Refactor `MeshDataHandlerTest` to use lazy mocks Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .../core/data/manager/MeshDataHandlerImpl.kt | 10 + .../core/data/manager/NodeManagerImpl.kt | 13 +- .../data/repository/NodeRepositoryImpl.kt | 5 +- .../core/data/manager/MeshDataHandlerTest.kt | 12 +- .../core/database/entity/NodeEntity.kt | 2 + .../usecase/settings/AdminActionsUseCase.kt | 8 +- .../meshtastic/core/model/RadioController.kt | 233 +++++++++++++++++- .../core/repository/NodeRepository.kt | 118 +++++++-- .../core/repository/PacketRepository.kt | 73 ++++++ .../core/repository/ServiceRepository.kt | 95 +++++-- .../repository/usecase/SendMessageUseCase.kt | 22 +- .../domain/worker/SendMessageWorker.kt | 2 +- .../feature/settings/SettingsViewModel.kt | 2 +- 13 files changed, 531 insertions(+), 64 deletions(-) diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt index 909a411de1..e63a5ec060 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt @@ -78,6 +78,16 @@ import javax.inject.Inject import javax.inject.Singleton import kotlin.time.Duration.Companion.milliseconds +/** + * Implementation of [MeshDataHandler] that decodes and routes incoming mesh data packets. + * + * This class handles the complexity of: + * 1. Mapping raw [MeshPacket] objects to domain-friendly [DataPacket] objects. + * 2. Routing packets to specialized handlers (e.g., Traceroute, NeighborInfo, SFPP). + * 3. Managing message history and persistence. + * 4. Triggering notifications for various packet types (Text, Waypoints, Battery). + * 5. Tracking received telemetry for node updates. + */ @Suppress("LongParameterList", "TooManyFunctions", "LargeClass", "CyclomaticComplexMethod") @Singleton class MeshDataHandlerImpl diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt index ae4d518ae8..3a56b94287 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt @@ -48,6 +48,16 @@ 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. + * + * This component acts as the "brain" for node-related data during a connection session. + * It manages: + * 1. In-memory maps for fast node lookup by number or ID. + * 2. Synchronization of node data between the radio and the persistent database. + * 3. Processing of incoming node-related packets (User, Position, Telemetry). + * 4. Broadcasting changes to the rest of the application. + */ @Suppress("LongParameterList", "TooManyFunctions", "CyclomaticComplexMethod") @Singleton class NodeManagerImpl @@ -154,8 +164,7 @@ constructor( } if (next.user.id.isNotEmpty() && isNodeDbReady.value) { - // scope.handledLaunch { nodeRepository.upsert(next) } - // TODO: Add upsert to repository interface + scope.handledLaunch { nodeRepository.upsert(next) } } if (withBroadcast) { diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt index 0746fc06e3..a6af8c51e8 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt @@ -184,8 +184,9 @@ constructor( .flowOn(dispatchers.io) .conflate() - /** Upserts a [NodeEntity] to the database. */ - suspend fun upsert(node: NodeEntity) = withContext(dispatchers.io) { nodeInfoWriteDataSource.upsert(node) } + /** Upserts a [Node] to the database. */ + override suspend fun upsert(node: Node) = + withContext(dispatchers.io) { nodeInfoWriteDataSource.upsert(node.toEntity()) } /** Installs initial configuration data (local info and remote nodes) into the database. */ override suspend fun installConfig(mi: MyNodeInfo, nodes: List) = withContext(dispatchers.io) { diff --git a/core/data/src/test/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerTest.kt b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerTest.kt index 25d0dbcbb7..0c133b36f3 100644 --- a/core/data/src/test/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerTest.kt +++ b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerTest.kt @@ -32,7 +32,6 @@ import org.meshtastic.core.analytics.platform.PlatformAnalytics import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.MessageStatus import org.meshtastic.core.model.util.MeshDataMapper -import org.meshtastic.core.prefs.mesh.MeshPrefs import org.meshtastic.core.repository.CommandSender import org.meshtastic.core.repository.HistoryManager import org.meshtastic.core.repository.MeshConfigFlowManager @@ -65,11 +64,13 @@ class MeshDataHandlerTest { 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 configFlowManager: MeshConfigFlowManager = mockk(relaxed = true) + private val configFlowManagerLazy: Lazy = mockk { every { get() } returns configFlowManager } private val commandSender: CommandSender = mockk(relaxed = true) private val historyManager: HistoryManager = mockk(relaxed = true) - private val meshPrefs: MeshPrefs = mockk(relaxed = true) private val connectionManager: MeshConnectionManager = mockk(relaxed = true) + private val connectionManagerLazy: Lazy = mockk { every { get() } returns connectionManager } private val tracerouteHandler: TracerouteHandler = mockk(relaxed = true) private val neighborInfoHandler: NeighborInfoHandler = mockk(relaxed = true) private val radioConfigRepository: RadioConfigRepository = mockk(relaxed = true) @@ -96,12 +97,11 @@ class MeshDataHandlerTest { serviceNotifications, analytics, dataMapper, - { configHandler }, - { configFlowManager }, + configHandlerLazy, + configFlowManagerLazy, commandSender, historyManager, - meshPrefs, - { connectionManager }, + connectionManagerLazy, tracerouteHandler, neighborInfoHandler, radioConfigRepository, diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/entity/NodeEntity.kt b/core/database/src/main/kotlin/org/meshtastic/core/database/entity/NodeEntity.kt index 4e76127199..6a47232bff 100644 --- a/core/database/src/main/kotlin/org/meshtastic/core/database/entity/NodeEntity.kt +++ b/core/database/src/main/kotlin/org/meshtastic/core/database/entity/NodeEntity.kt @@ -65,6 +65,7 @@ data class NodeWithRelations( environmentMetrics = environmentMetrics ?: org.meshtastic.proto.EnvironmentMetrics(), powerMetrics = powerMetrics ?: org.meshtastic.proto.PowerMetrics(), paxcounter = paxcounter, + publicKey = publicKey ?: user.public_key, notes = notes, manuallyVerified = manuallyVerified, nodeStatus = nodeStatus, @@ -90,6 +91,7 @@ data class NodeWithRelations( environmentTelemetry = environmentTelemetry, powerTelemetry = powerTelemetry, paxcounter = paxcounter, + publicKey = publicKey ?: user.public_key, notes = notes, manuallyVerified = manuallyVerified, nodeStatus = nodeStatus, diff --git a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/AdminActionsUseCase.kt b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/AdminActionsUseCase.kt index 0cdb50ff9d..ddf22cb746 100644 --- a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/AdminActionsUseCase.kt +++ b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/AdminActionsUseCase.kt @@ -20,7 +20,13 @@ import org.meshtastic.core.model.RadioController import org.meshtastic.core.repository.NodeRepository import javax.inject.Inject -/** Use case for performing administrative actions on the radio. */ +/** + * Use case for performing administrative and destructive actions on mesh nodes. + * + * 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. + */ open class AdminActionsUseCase @Inject constructor( diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/RadioController.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/RadioController.kt index a712245420..f2f7d34ab4 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/RadioController.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/RadioController.kt @@ -19,86 +19,299 @@ package org.meshtastic.core.model import kotlinx.coroutines.flow.StateFlow import org.meshtastic.proto.ClientNotification +/** + * Central interface for controlling the radio and mesh network. + * + * This component provides an abstraction over the underlying communication transport (e.g., BLE, Serial, TCP) + * and the low-level mesh protocols. It allows feature modules to interact with the mesh without + * needing to know about platform-specific service details or AIDL interfaces. + */ @Suppress("TooManyFunctions") interface RadioController { + /** Reactive connection state of the radio. */ val connectionState: StateFlow + + /** + * Flow of notifications from the radio client. + * + * These represent high-level events like "Handshake completed" or "Channel configuration updated." + */ val clientNotification: StateFlow + /** + * Sends a data packet to the mesh. + * + * @param packet The [DataPacket] containing the payload and routing information. + */ suspend fun sendMessage(packet: DataPacket) + /** Clears the current [clientNotification]. */ fun clearClientNotification() - // Abstracted ServiceActions + /** + * Toggles the favorite status of a node on the radio. + * + * @param nodeNum The node number to favorite/unfavorite. + */ suspend fun favoriteNode(nodeNum: Int) + /** + * Sends our shared contact information (identity and public key) to a remote node. + * + * @param nodeNum The destination node number. + */ suspend fun sendSharedContact(nodeNum: Int) - // Local radio configuration + /** + * Updates the local radio configuration. + * + * @param config The new configuration [org.meshtastic.proto.Config]. + */ suspend fun setLocalConfig(config: org.meshtastic.proto.Config) + /** + * Updates a local radio channel. + * + * @param channel The channel configuration [org.meshtastic.proto.Channel]. + */ suspend fun setLocalChannel(channel: org.meshtastic.proto.Channel) - // Radio configuration + /** + * Updates the owner (user info) on a remote node. + * + * @param destNum The destination node number. + * @param user The new user info [org.meshtastic.proto.User]. + * @param packetId The request packet ID. + */ suspend fun setOwner(destNum: Int, user: org.meshtastic.proto.User, packetId: Int) + /** + * Updates the general configuration on a remote node. + * + * @param destNum The destination node number. + * @param config The new configuration [org.meshtastic.proto.Config]. + * @param packetId The request packet ID. + */ suspend fun setConfig(destNum: Int, config: org.meshtastic.proto.Config, packetId: Int) + /** + * Updates a module configuration on a remote node. + * + * @param destNum The destination node number. + * @param config The new module configuration [org.meshtastic.proto.ModuleConfig]. + * @param packetId The request packet ID. + */ suspend fun setModuleConfig(destNum: Int, config: org.meshtastic.proto.ModuleConfig, packetId: Int) + /** + * Updates a channel configuration on a remote node. + * + * @param destNum The destination node number. + * @param channel The new channel configuration [org.meshtastic.proto.Channel]. + * @param packetId The request packet ID. + */ suspend fun setRemoteChannel(destNum: Int, channel: org.meshtastic.proto.Channel, packetId: Int) + /** + * Sets a fixed position on a remote node. + * + * @param destNum The destination node number. + * @param position The position to set. + */ suspend fun setFixedPosition(destNum: Int, position: Position) + /** + * Updates the notification ringtone on a remote node. + * + * @param destNum The destination node number. + * @param ringtone The name/ID of the ringtone. + */ suspend fun setRingtone(destNum: Int, ringtone: String) + /** + * Updates the canned messages configuration on a remote node. + * + * @param destNum The destination node number. + * @param messages The canned messages string. + */ suspend fun setCannedMessages(destNum: Int, messages: String) - // Admin get operations + /** + * Requests the current owner (user info) from a remote node. + * + * @param destNum The remote node number. + * @param packetId The request packet ID. + */ suspend fun getOwner(destNum: Int, packetId: Int) + /** + * Requests a specific configuration section from a remote node. + * + * @param destNum The remote node number. + * @param configType The numeric type of the configuration section. + * @param packetId The request packet ID. + */ suspend fun getConfig(destNum: Int, configType: Int, packetId: Int) + /** + * Requests a module configuration section from a remote node. + * + * @param destNum The remote node number. + * @param moduleConfigType The numeric type of the module configuration section. + * @param packetId The request packet ID. + */ suspend fun getModuleConfig(destNum: Int, moduleConfigType: Int, packetId: Int) + /** + * Requests a specific channel configuration from a remote node. + * + * @param destNum The remote node number. + * @param index The channel index. + * @param packetId The request packet ID. + */ suspend fun getChannel(destNum: Int, index: Int, packetId: Int) + /** + * Requests the current ringtone from a remote node. + * + * @param destNum The remote node number. + * @param packetId The request packet ID. + */ suspend fun getRingtone(destNum: Int, packetId: Int) + /** + * Requests the current canned messages from a remote node. + * + * @param destNum The remote node number. + * @param packetId The request packet ID. + */ suspend fun getCannedMessages(destNum: Int, packetId: Int) + /** + * Requests the hardware connection status from a remote node. + * + * @param destNum The remote node number. + * @param packetId The request packet ID. + */ suspend fun getDeviceConnectionStatus(destNum: Int, packetId: Int) - // Admin operations + /** + * Commands a node to reboot. + * + * @param destNum The target node number. + * @param packetId The request packet ID. + */ suspend fun reboot(destNum: Int, packetId: Int) + /** + * Commands a node to reboot into DFU (Device Firmware Update) mode. + * + * @param nodeNum The target node number. + */ suspend fun rebootToDfu(nodeNum: Int) + /** + * Initiates an Over-The-Air (OTA) reboot request. + * + * @param requestId The request ID. + * @param destNum The target node number. + * @param mode The OTA mode. + * @param hash Optional hash for verification. + */ suspend fun requestRebootOta(requestId: Int, destNum: Int, mode: Int, hash: ByteArray?) + /** + * Commands a node to shut down. + * + * @param destNum The target node number. + * @param packetId The request packet ID. + */ suspend fun shutdown(destNum: Int, packetId: Int) + /** + * Performs a factory reset on a node. + * + * @param destNum The target node number. + * @param packetId The request packet ID. + */ suspend fun factoryReset(destNum: Int, packetId: Int) + /** + * Resets the NodeDB on a node. + * + * @param destNum The target node number. + * @param packetId The request packet ID. + * @param preserveFavorites Whether to keep favorite nodes in the database. + */ suspend fun nodedbReset(destNum: Int, packetId: Int, preserveFavorites: Boolean) + /** + * Removes a node from the mesh by its node number. + * + * @param packetId The request packet ID. + * @param nodeNum The node number to remove. + */ suspend fun removeByNodenum(packetId: Int, nodeNum: Int) + /** + * Requests the current GPS position from a remote node. + * + * @param destNum The target node number. + * @param currentPosition Our current position to provide in the request. + */ suspend fun requestPosition(destNum: Int, currentPosition: Position) + /** + * Requests detailed user info from a remote node. + * + * @param destNum The target node number. + */ suspend fun requestUserInfo(destNum: Int) + /** + * Initiates a traceroute request to a remote node. + * + * @param requestId The request ID. + * @param destNum The destination node number. + */ suspend fun requestTraceroute(requestId: Int, destNum: Int) + /** + * Requests telemetry data from a remote node. + * + * @param requestId The request ID. + * @param destNum The destination node number. + * @param typeValue The numeric type of telemetry requested. + */ suspend fun requestTelemetry(requestId: Int, destNum: Int, typeValue: Int) + /** + * Requests neighbor information (detected nodes) from a remote node. + * + * @param requestId The request ID. + * @param destNum The destination node number. + */ suspend fun requestNeighborInfo(requestId: Int, destNum: Int) - // Batch editing + /** + * Signals the start of a batch configuration session. + * + * @param destNum The target node number. + */ suspend fun beginEditSettings(destNum: Int) + /** + * Commits all pending configuration changes in a batch session. + * + * @param destNum The target node number. + */ suspend fun commitEditSettings(destNum: Int) - // Helpers + /** + * Generates a unique packet ID for a new request. + * + * @return A unique 32-bit integer. + */ fun getPacketId(): Int /** Starts providing the phone's location to the mesh. */ @@ -107,6 +320,10 @@ interface RadioController { /** Stops providing the phone's location to the mesh. */ fun stopProvideLocation() - /** Changes the device address we are talking to. */ + /** + * Changes the device address (e.g., BLE MAC, IP address) we are communicating with. + * + * @param address The new device identifier. + */ fun setDeviceAddress(address: String) } diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeRepository.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeRepository.kt index 470d85cb63..17efc697d0 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeRepository.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeRepository.kt @@ -25,46 +25,89 @@ import org.meshtastic.proto.DeviceMetadata import org.meshtastic.proto.LocalStats import org.meshtastic.proto.User -/** Repository interface for managing node-related data. This interface is shared across platforms via KMP. */ +/** + * Repository interface for managing node-related data. + * + * This component provides access to the mesh's node database, local device information, + * and mesh-wide statistics. It supports reactive queries for node lists, counts, and + * filtered/sorted views. + * + * This interface is shared across platforms via Kotlin Multiplatform (KMP). + */ @Suppress("TooManyFunctions") interface NodeRepository { - /** Hardware info about our local device. */ + /** Reactive flow of hardware info about our local radio device. */ val myNodeInfo: StateFlow - /** Information about the locally connected node, as seen from the mesh. */ + /** + * Reactive flow of information about the locally connected node as seen by the mesh. + * + * This includes its position, telemetry, and user information as reflected in the mesh's node DB. + */ val ourNodeInfo: StateFlow - /** The unique userId (hex string) of our local node. */ + /** The unique userId (hex string, e.g., "!1234abcd") of our local node. */ val myId: StateFlow - /** The latest local stats telemetry received from the locally connected node. */ + /** Reactive flow of the latest local stats telemetry received from the radio. */ val localStats: StateFlow - /** A reactive map from nodeNum to [Node] objects, representing the entire mesh. */ + /** A reactive map of all known nodes in the mesh, keyed by their 32-bit node number. */ val nodeDBbyNum: StateFlow> - /** Flow emitting the count of nodes currently considered "online". */ + /** Flow emitting the count of nodes currently considered "online" (heard from recently). */ val onlineNodeCount: Flow /** Flow emitting the total number of nodes in the database. */ val totalNodeCount: Flow - /** Update the cached local stats telemetry. */ + /** + * Updates the cached local stats telemetry. + * + * @param stats The new [LocalStats]. + */ fun updateLocalStats(stats: LocalStats) - /** Returns the node number used for log queries. */ + /** + * Returns the node number used for log queries. + * + * Maps the local node's number to a constant (e.g., 0) to distinguish it from remote logs. + */ fun effectiveLogNodeId(nodeNum: Int): Flow - /** Returns the [Node] associated with a given [userId]. */ + /** + * Returns the [Node] associated with a given [userId]. + * + * @param userId The hex string identifier. + * @return The found [Node] or a fallback object. + */ fun getNode(userId: String): Node - /** Returns the [User] info for a given [nodeNum]. */ + /** + * Returns the [User] info for a given [nodeNum]. + * + * @param nodeNum The 32-bit node number. + * @return The associated [User] proto. + */ fun getUser(nodeNum: Int): User - /** Returns the [User] info for a given [userId]. */ + /** + * Returns the [User] info for a given [userId]. + * + * @param userId The hex string identifier. + * @return The associated [User] proto. + */ fun getUser(userId: String): User - /** Returns a flow of nodes filtered and sorted according to the parameters. */ + /** + * Returns a reactive flow of nodes filtered and sorted according to the parameters. + * + * @param sort The [NodeSortOption] to apply. + * @param filter A search string for filtering by name or ID. + * @param includeUnknown Whether to include nodes with unset hardware models. + * @param onlyOnline Whether to include only nodes currently considered online. + * @param onlyDirect Whether to include only nodes heard directly (0 hops away). + */ fun getNodes( sort: NodeSortOption = NodeSortOption.LAST_HEARD, filter: String = "", @@ -73,28 +116,63 @@ interface NodeRepository { onlyDirect: Boolean = false, ): Flow> + /** Returns all nodes that haven't been heard from since the given timestamp. */ suspend fun getNodesOlderThan(lastHeard: Int): List + /** Returns all nodes with unknown hardware models. */ suspend fun getUnknownNodes(): List - /** Deletes all nodes from the database. */ + /** + * Deletes all nodes from the database. + * + * @param preserveFavorites If true, nodes marked as favorite will not be deleted. + */ suspend fun clearNodeDB(preserveFavorites: Boolean = false) - /** Clears the local node's connection info. */ + /** Clears the local node's connection info from the cache. */ suspend fun clearMyNodeInfo() - /** Deletes a node by its number. */ + /** + * Deletes a specific node by its node number. + * + * @param num The node number to delete. + */ suspend fun deleteNode(num: Int) - /** Deletes multiple nodes. */ + /** + * Deletes multiple nodes by their node numbers. + * + * @param nodeNums The list of node numbers to delete. + */ suspend fun deleteNodes(nodeNums: List) - /** Updates the personal notes for a node. */ + /** + * Updates the personal notes for a node. + * + * @param num The node number. + * @param notes The human-readable notes to persist. + */ suspend fun setNodeNotes(num: Int, notes: String) - /** Installs initial configuration data (local info and remote nodes) into the database. */ + /** + * Upserts a [Node] into the persistent database. + * + * @param node The [Node] model to save. + */ + suspend fun upsert(node: Node) + + /** + * Installs initial configuration data (local info and remote nodes) into the database. + * + * Used during the initial connection handshake. + */ suspend fun installConfig(mi: MyNodeInfo, nodes: List) - /** Persists hardware metadata for a node. */ + /** + * Persists hardware metadata for a node. + * + * @param nodeNum The node number. + * @param metadata The [DeviceMetadata] to save. + */ suspend fun insertMetadata(nodeNum: Int, metadata: DeviceMetadata) } diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketRepository.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketRepository.kt index 2e006d4c65..27aeff3d7e 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketRepository.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketRepository.kt @@ -26,30 +26,58 @@ import org.meshtastic.core.model.Node import org.meshtastic.core.model.Reaction import org.meshtastic.proto.ChannelSettings +/** + * Repository interface for managing mesh packets and message history. + * + * This component provides methods for persisting received packets, querying message history, + * tracking unread counts, and managing contact-specific settings. It supports both + * reactive (Flow) and one-shot (suspend) queries. + */ @Suppress("TooManyFunctions") interface PacketRepository { + /** Reactive flow of all persisted waypoints (GPS locations). */ fun getWaypoints(): Flow> + /** Reactive flow of all conversation contacts, keyed by their contact identifier. */ fun getContacts(): Flow> + /** Reactive paged flow of conversation contacts. */ fun getContactsPaged(): Flow> + /** Returns the total number of messages in a conversation. */ suspend fun getMessageCount(contact: String): Int + /** Returns the count of unread messages in a conversation. */ suspend fun getUnreadCount(contact: String): Int + /** Reactive flow of the UUID of the first unread message in a conversation. */ fun getFirstUnreadMessageUuid(contact: String): Flow + /** Reactive flow indicating whether a conversation has any unread messages. */ fun hasUnreadMessages(contact: String): Flow + /** Reactive flow of the total unread message count across all conversations. */ fun getUnreadCountTotal(): Flow + /** Clears the unread status for messages in a conversation up to the given timestamp. */ suspend fun clearUnreadCount(contact: String, timestamp: Long) + /** Updates the identifier of the last read message in a conversation. */ suspend fun updateLastReadMessage(contact: String, messageUuid: Long, lastReadTimestamp: Long) + /** Returns all packets currently queued for transmission. */ suspend fun getQueuedPackets(): List? + /** + * Persists a packet in the database. + * + * @param myNodeNum The local node number at the time of receipt. + * @param contactKey The identifier of the associated conversation. + * @param packet The [DataPacket] to save. + * @param receivedTime The timestamp (ms) the packet was received. + * @param read Whether the packet should be marked as already read. + * @param filtered Whether the packet was filtered by message rules. + */ suspend fun savePacket( myNodeNum: Int, contactKey: String, @@ -59,6 +87,14 @@ interface PacketRepository { filtered: Boolean = false, ) + /** + * Returns a reactive flow of messages for a conversation. + * + * @param contact The conversation identifier. + * @param limit Optional maximum number of messages to return. + * @param includeFiltered Whether to include messages that were marked as filtered. + * @param getNode Callback to fetch node info for message sender attribution. + */ suspend fun getMessagesFrom( contact: String, limit: Int? = null, @@ -66,46 +102,65 @@ interface PacketRepository { getNode: suspend (String?) -> Node, ): Flow> + /** Returns a paged flow of messages for a conversation. */ fun getMessagesFromPaged(contact: String, getNode: suspend (String?) -> Node): Flow> + /** Returns a paged flow of messages for a conversation, with filtering options. */ fun getMessagesFromPaged( contactKey: String, includeFiltered: Boolean, getNode: suspend (String?) -> Node, ): Flow> + /** Updates the transmission status of a packet. */ suspend fun updateMessageStatus(d: DataPacket, m: MessageStatus) + /** Updates the identifier of a persisted packet. */ suspend fun updateMessageId(d: DataPacket, id: Int) + /** Deletes messages by their database UUIDs. */ suspend fun deleteMessages(uuidList: List) + /** Deletes all messages and settings for the given contacts. */ suspend fun deleteContacts(contactList: List) + /** Deletes a waypoint by its ID. */ suspend fun deleteWaypoint(id: Int) + /** Reactive flow of all contact settings (e.g., mute status). */ fun getContactSettings(): Flow> + /** Returns the settings for a specific contact. */ suspend fun getContactSettings(contact: String): ContactSettings + /** Mutes the given contacts until the specified timestamp. */ suspend fun setMuteUntil(contacts: List, until: Long) + /** Reactive flow of the number of filtered messages for a contact. */ fun getFilteredCountFlow(contactKey: String): Flow + /** Returns the total count of filtered messages for a contact. */ suspend fun getFilteredCount(contactKey: String): Int + /** Disables or enables message filtering for a specific contact. */ suspend fun setContactFilteringDisabled(contactKey: String, disabled: Boolean) + /** Clears all packet and message history from the database. */ suspend fun clearPacketDB() + /** Migrates channel-specific message history when encryption keys change. */ suspend fun migrateChannelsByPSK(oldSettings: List, newSettings: List) + /** Marks all messages from a specific sender as filtered or unfiltered. */ suspend fun updateFilteredBySender(senderId: String, filtered: Boolean) + /** Returns a packet by its mesh-layer packet ID. */ suspend fun getPacketByPacketId(packetId: Int): DataPacket? + /** Returns a packet by its internal database ID. */ suspend fun getPacketById(id: Int): DataPacket? + /** Inserts a packet into the database. */ suspend fun insert( packet: DataPacket, myNodeNum: Int, @@ -115,18 +170,35 @@ interface PacketRepository { filtered: Boolean = false, ) + /** Updates an existing packet in the database. */ suspend fun update(packet: DataPacket) + /** Persists a message reaction (emoji). */ suspend fun insertReaction(reaction: Reaction, myNodeNum: Int) + /** Updates an existing reaction. */ suspend fun updateReaction(reaction: Reaction) + /** Returns a reaction associated with a specific packet ID. */ suspend fun getReactionByPacketId(packetId: Int): Reaction? + /** Finds all packets matching a specific packet ID. */ suspend fun findPacketsWithId(packetId: Int): List + /** Finds all reactions associated with a specific packet ID. */ suspend fun findReactionsWithId(packetId: Int): List + /** + * Updates the Store-and-Forward PlusPlus (SFPP) status for packets. + * + * @param packetId The packet ID. + * @param from The sender node number. + * @param to The recipient node number. + * @param hash The SFPP commit hash. + * @param status The new SFPP-specific message status. + * @param rxTime The receipt time from the mesh. + * @param myNodeNum The local node number. + */ suspend fun updateSFPPStatus( packetId: Int, from: Int, @@ -137,5 +209,6 @@ interface PacketRepository { myNodeNum: Int?, ) + /** Updates the SFPP status of packets matching the given commit hash. */ suspend fun updateSFPPStatusByHash(hash: ByteArray, status: MessageStatus, rxTime: Long) } diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/ServiceRepository.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/ServiceRepository.kt index cae4f5c53f..8e6f945448 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/ServiceRepository.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/ServiceRepository.kt @@ -26,66 +26,123 @@ import org.meshtastic.core.model.service.TracerouteResponse import org.meshtastic.proto.ClientNotification import org.meshtastic.proto.MeshPacket -/** Interface for managing service state, connection status, and mesh events. */ +/** + * Interface for managing background service state, connection status, and mesh events. + * + * This repository acts as the primary data bridge between the long-running mesh service + * and the UI/Feature layers. It maintains reactive flows for connection status, + * error messages, and incoming mesh traffic. + */ @Suppress("TooManyFunctions") interface ServiceRepository { - /** Reactive connection state. */ + /** Reactive flow of the current connection state. */ val connectionState: StateFlow - /** Sets the connection state. */ + /** + * Updates the current connection state. + * + * @param connectionState The new [ConnectionState]. + */ fun setConnectionState(connectionState: ConnectionState) - /** Reactive client notification. */ + /** + * Reactive flow of high-level client notifications. + * + * These represent events from the mesh client that may require UI feedback. + */ val clientNotification: StateFlow - /** Sets the current client notification. */ + /** + * Sets the current client notification. + * + * @param notification The [ClientNotification] to display or act upon. + */ fun setClientNotification(notification: ClientNotification?) /** Clears the current client notification. */ fun clearClientNotification() - /** Reactive error message. */ + /** + * Reactive flow of human-readable error messages. + * + * These are typically shown as snackbars or dialogs in the UI. + */ val errorMessage: StateFlow - /** Sets an error message to be displayed. */ + /** + * Sets an error message to be displayed. + * + * @param text The error message text. + * @param severity The [Severity] level of the error. + */ fun setErrorMessage(text: String, severity: Severity = Severity.Error) /** Clears the current error message. */ fun clearErrorMessage() - /** Reactive connection progress message. */ + /** + * Reactive flow of connection progress messages. + * + * Used during the handshake and config loading phase to provide status updates to the user. + */ val connectionProgress: StateFlow - /** Sets the connection progress message. */ + /** + * Sets the connection progress message. + * + * @param text The progress description (e.g., "Downloading Node DB..."). + */ fun setConnectionProgress(text: String) - /** Flow of all mesh packets. */ + /** + * Flow of all raw [MeshPacket] objects received from the mesh. + * + * Subscribing to this flow allows components to react to any incoming traffic. + */ val meshPacketFlow: SharedFlow - /** Emits a mesh packet into the flow. */ + /** + * Emits a mesh packet into the flow. + * + * Called by the packet processor when new data arrives from the radio. + * + * @param packet The received [MeshPacket]. + */ suspend fun emitMeshPacket(packet: MeshPacket) - /** Reactive traceroute response. */ + /** Reactive flow of the most recent traceroute result. */ val tracerouteResponse: StateFlow - /** Sets the traceroute response. */ + /** + * Sets the traceroute response. + * + * @param value The [TracerouteResponse] result. + */ fun setTracerouteResponse(value: TracerouteResponse?) - /** Clears the traceroute response. */ + /** Clears the current traceroute response. */ fun clearTracerouteResponse() - /** Reactive neighbor info response. */ + /** Reactive flow of the most recent neighbor info response (formatted string). */ val neighborInfoResponse: StateFlow - /** Sets the neighbor info response. */ + /** + * Sets the neighbor info response. + * + * @param value The human-readable neighbor info string. + */ fun setNeighborInfoResponse(value: String?) - /** Clears the neighbor info response. */ + /** Clears the current neighbor info response. */ fun clearNeighborInfoResponse() - /** Flow of service actions requested by the UI. */ + /** Flow of service actions requested by the UI (e.g., "Favorite Node", "Mute Node"). */ val serviceAction: Flow - /** Dispatches a service action. */ + /** + * Dispatches a service action to be handled by the background service. + * + * @param action The [ServiceAction] to perform. + */ suspend fun onServiceAction(action: ServiceAction) } diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/usecase/SendMessageUseCase.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/usecase/SendMessageUseCase.kt index 1f3e54ea4f..6aff094731 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/usecase/SendMessageUseCase.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/usecase/SendMessageUseCase.kt @@ -29,12 +29,19 @@ import org.meshtastic.core.repository.MessageQueue import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.PacketRepository import org.meshtastic.proto.Config -import kotlin.math.abs import kotlin.random.Random /** - * Use case for sending a message. This component handles message transformation, persistence, and enqueuing for durable - * delivery. + * Use case for sending a message over the mesh network. + * + * This component orchestrates the process of: + * 1. Resolving the destination and sender information. + * 2. Handling implicit actions for direct messages (e.g., sharing contacts, favoriting). + * 3. Applying message transformations (e.g., homoglyph encoding). + * 4. Persisting the outgoing message in the local history. + * 5. Enqueuing the message for durable delivery via the platform's message queue. + * + * This implementation is platform-agnostic and relies on injected repositories and controllers. */ @Suppress("TooGenericExceptionCaught") class SendMessageUseCase( @@ -45,6 +52,13 @@ class SendMessageUseCase( private val messageQueue: MessageQueue, ) { + /** + * Executes the send message workflow. + * + * @param text The plain text message to send. + * @param contactKey The identifier of the target contact or channel (e.g., "0!ffffffff" for broadcast). + * @param replyId Optional ID of a message being replied to. + */ @Suppress("NestedBlockDepth", "LongMethod", "CyclomaticComplexMethod") suspend operator fun invoke( text: String, @@ -81,7 +95,7 @@ class SendMessageUseCase( text } - val packetId = abs(Random.nextInt()) + val packetId = Random.nextInt(1, Int.MAX_VALUE) val packet = DataPacket(dest, channel ?: 0, finalMessageText, replyId).apply { diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorker.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorker.kt index bbc28a00d1..5a4bebe52e 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorker.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorker.kt @@ -55,7 +55,7 @@ constructor( packetRepository.updateMessageStatus(packetData, MessageStatus.ENROUTE) Result.success() } catch (e: Exception) { - packetRepository.updateMessageStatus(packetData, MessageStatus.ERROR) + packetRepository.updateMessageStatus(packetData, MessageStatus.QUEUED) Result.retry() } } diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt index ae8cf2710f..b3b653b505 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt @@ -170,7 +170,7 @@ constructor( */ @Suppress("detekt:CyclomaticComplexMethod", "detekt:LongMethod") fun saveDataCsv(uri: Uri, filterPortnum: Int? = null) { - viewModelScope.launch(Dispatchers.Main) { + viewModelScope.launch { val myNodeNum = myNodeNum ?: return@launch writeToUri(uri) { writer -> exportDataUseCase(writer, myNodeNum, filterPortnum) } } From 0389ea442a410a13294156c12c6454e9b678e180 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 2 Mar 2026 16:11:38 -0600 Subject: [PATCH 13/20] ci: refactor reusable-check task logic and separate static analysis * Simplify task determination by using capitalized flavor names for Gradle tasks. * Separate code style (Spotless, Detekt) and shared unit tests into dedicated workflow steps. * Ensure flavor-specific assembly, lint, and coverage reports run for all matrix variations. * Improve readability by renaming check steps to distinguish between shared and flavor-specific execution. Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .github/workflows/reusable-check.yml | 50 +++++++++++----------------- 1 file changed, 20 insertions(+), 30 deletions(-) diff --git a/.github/workflows/reusable-check.yml b/.github/workflows/reusable-check.yml index 85b9d46ba9..e480374fd0 100644 --- a/.github/workflows/reusable-check.yml +++ b/.github/workflows/reusable-check.yml @@ -89,30 +89,18 @@ jobs: - name: Determine Tasks id: tasks run: | - TASKS="" - # Only run Lint and Unit Tests on the first API level and first flavor in the matrix to save time and resources + FLAVOR="${{ matrix.flavor }}" + FLAVOR_CAP=$(echo $FLAVOR | awk '{print toupper(substr($0,1,1))substr($0,2)}') IS_FIRST_API=$(echo '${{ inputs.api_levels }}' | jq -r '.[0] == ${{ matrix.api_level }}') IS_FIRST_FLAVOR=$(echo '${{ inputs.flavors }}' | jq -r '.[0] == "${{ matrix.flavor }}"') - if [ "$IS_FIRST_API" = "true" ] && [ "$IS_FIRST_FLAVOR" = "true" ]; then - [ "${{ inputs.run_lint }}" = "true" ] && TASKS="$TASKS spotlessCheck detekt " - [ "${{ inputs.run_unit_tests }}" = "true" ] && TASKS="$TASKS testDebugUnitTest " - fi - - FLAVOR="${{ matrix.flavor }}" - if [ "$IS_FIRST_API" = "true" ]; then - if [ "$FLAVOR" = "google" ]; then - TASKS="$TASKS assembleGoogleDebug " - [ "${{ inputs.run_unit_tests }}" = "true" ] && TASKS="$TASKS testGoogleDebugUnitTest " - elif [ "$FLAVOR" = "fdroid" ]; then - TASKS="$TASKS assembleFdroidDebug " - [ "${{ inputs.run_unit_tests }}" = "true" ] && TASKS="$TASKS testFdroidDebugUnitTest " - fi - fi + # Matrix-specific tasks + TASKS="assemble${FLAVOR_CAP}Debug " + [ "${{ inputs.run_lint }}" = "true" ] && TASKS="$TASKS lint${FLAVOR_CAP}Debug " + [ "${{ inputs.run_unit_tests }}" = "true" ] && TASKS="$TASKS test${FLAVOR_CAP}DebugUnitTest " # Instrumented Test Tasks if [ "${{ inputs.run_instrumented_tests }}" = "true" ]; then - [ "$IS_FIRST_FLAVOR" = "true" ] && TASKS="$TASKS connectedDebugAndroidTest " if [ "$FLAVOR" = "google" ]; then TASKS="$TASKS connectedGoogleDebugAndroidTest " elif [ "$FLAVOR" = "fdroid" ]; then @@ -120,20 +108,22 @@ jobs: fi fi - # Run coverage report if unit tests were executed - if [ "${{ inputs.run_unit_tests }}" = "true" ] && [ "$IS_FIRST_API" = "true" ]; then - if [ "$IS_FIRST_FLAVOR" = "true" ]; then - TASKS="$TASKS koverXmlReportDebug " - fi - if [ "$FLAVOR" = "google" ]; then - TASKS="$TASKS koverXmlReportGoogleDebug " - elif [ "$FLAVOR" = "fdroid" ]; then - TASKS="$TASKS koverXmlReportFdroidDebug " - fi + # Run coverage report for this flavor + if [ "${{ inputs.run_unit_tests }}" = "true" ]; then + TASKS="$TASKS koverXmlReport${FLAVOR_CAP}Debug " fi echo "tasks=$TASKS" >> $GITHUB_OUTPUT echo "is_first_api=$IS_FIRST_API" >> $GITHUB_OUTPUT + echo "is_first_flavor=$IS_FIRST_FLAVOR" >> $GITHUB_OUTPUT + + - name: Code Style & Static Analysis + if: steps.tasks.outputs.is_first_api == 'true' && steps.tasks.outputs.is_first_flavor == 'true' + run: ./gradlew spotlessCheck detekt -Pci=true + + - name: Shared Unit Tests + if: steps.tasks.outputs.is_first_api == 'true' && steps.tasks.outputs.is_first_flavor == 'true' && inputs.run_unit_tests == true + run: ./gradlew testDebugUnitTest koverXmlReportDebug -Pci=true --continue - name: Enable KVM group perms if: inputs.run_instrumented_tests == true @@ -142,7 +132,7 @@ jobs: sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm - - name: Run Check (with Emulator) + - name: Run Flavor Check (with Emulator) if: inputs.run_instrumented_tests == true uses: reactivecircus/android-emulator-runner@v2 env: @@ -155,7 +145,7 @@ jobs: disable-animations: true script: ./gradlew ${{ steps.tasks.outputs.tasks }} -Pci=true -PenableComposeCompilerMetrics=true -PenableComposeCompilerReports=true --continue --scan - - name: Run Check (no Emulator) + - name: Run Flavor Check (no Emulator) if: inputs.run_instrumented_tests == false env: VERSION_CODE: ${{ steps.calculate_version_code.outputs.versionCode }} From a62933c3128785f3a5fb4710a6291a2415b169fd Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 2 Mar 2026 16:19:30 -0600 Subject: [PATCH 14/20] spotless Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .../org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt | 3 ++- .../org/meshtastic/core/data/manager/NodeManagerImpl.kt | 3 +-- .../core/domain/usecase/settings/AdminActionsUseCase.kt | 5 ++--- .../kotlin/org/meshtastic/core/model/RadioController.kt | 6 +++--- .../meshtastic/core/network/repository/MQTTRepository.kt | 2 ++ .../core/network/repository/TrustAllX509TrustManager.kt | 1 + .../kotlin/org/meshtastic/core/repository/NodeRepository.kt | 5 ++--- .../org/meshtastic/core/repository/PacketRepository.kt | 5 ++--- .../org/meshtastic/core/repository/ServiceRepository.kt | 5 ++--- .../kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt | 1 + .../feature/messaging/domain/worker/SendMessageWorker.kt | 1 + 11 files changed, 19 insertions(+), 18 deletions(-) diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt index e63a5ec060..e84af354c7 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt @@ -735,7 +735,8 @@ constructor( // Find the original packet to get the contactKey packetRepository.get().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 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 nodeMuted = nodeManager.nodeDBbyID[fromId]?.isMuted == true diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt index 3a56b94287..e9172809b0 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt @@ -51,8 +51,7 @@ import org.meshtastic.proto.Position as ProtoPosition /** * Implementation of [NodeManager] that maintains an in-memory database of the mesh. * - * This component acts as the "brain" for node-related data during a connection session. - * It manages: + * This component acts as the "brain" for node-related data during a connection session. It manages: * 1. In-memory maps for fast node lookup by number or ID. * 2. Synchronization of node data between the radio and the persistent database. * 3. Processing of incoming node-related packets (User, Position, Telemetry). diff --git a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/AdminActionsUseCase.kt b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/AdminActionsUseCase.kt index ddf22cb746..b0b7c2c8c6 100644 --- a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/AdminActionsUseCase.kt +++ b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/AdminActionsUseCase.kt @@ -23,9 +23,8 @@ import javax.inject.Inject /** * Use case for performing administrative and destructive actions on mesh nodes. * - * 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. + * 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. */ open class AdminActionsUseCase @Inject diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/RadioController.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/RadioController.kt index f2f7d34ab4..e021c0aa95 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/RadioController.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/RadioController.kt @@ -22,9 +22,9 @@ import org.meshtastic.proto.ClientNotification /** * Central interface for controlling the radio and mesh network. * - * This component provides an abstraction over the underlying communication transport (e.g., BLE, Serial, TCP) - * and the low-level mesh protocols. It allows feature modules to interact with the mesh without - * needing to know about platform-specific service details or AIDL interfaces. + * This component provides an abstraction over the underlying communication transport (e.g., BLE, Serial, TCP) and the + * low-level mesh protocols. It allows feature modules to interact with the mesh without needing to know about + * platform-specific service details or AIDL interfaces. */ @Suppress("TooManyFunctions") interface RadioController { diff --git a/core/network/src/main/kotlin/org/meshtastic/core/network/repository/MQTTRepository.kt b/core/network/src/main/kotlin/org/meshtastic/core/network/repository/MQTTRepository.kt index b837f510cb..960f4d8436 100644 --- a/core/network/src/main/kotlin/org/meshtastic/core/network/repository/MQTTRepository.kt +++ b/core/network/src/main/kotlin/org/meshtastic/core/network/repository/MQTTRepository.kt @@ -99,6 +99,7 @@ constructor( } } + @Suppress("MagicNumber") val bufferOptions = DisconnectedBufferOptions().apply { isBufferEnabled = true @@ -163,6 +164,7 @@ constructor( Logger.i { "MQTT Subscribed to topic: $topic" } } + @Suppress("TooGenericExceptionCaught") fun publish(topic: String, data: ByteArray, retained: Boolean) { try { val token = mqttClient?.publish(topic, data, DEFAULT_QOS, retained) diff --git a/core/network/src/main/kotlin/org/meshtastic/core/network/repository/TrustAllX509TrustManager.kt b/core/network/src/main/kotlin/org/meshtastic/core/network/repository/TrustAllX509TrustManager.kt index 28dd330b55..720d2a5227 100644 --- a/core/network/src/main/kotlin/org/meshtastic/core/network/repository/TrustAllX509TrustManager.kt +++ b/core/network/src/main/kotlin/org/meshtastic/core/network/repository/TrustAllX509TrustManager.kt @@ -21,6 +21,7 @@ import java.security.cert.X509Certificate import javax.net.ssl.X509TrustManager @SuppressLint("CustomX509TrustManager", "TrustAllX509TrustManager") +@Suppress("EmptyFunctionBlock") class TrustAllX509TrustManager : X509TrustManager { override fun checkClientTrusted(chain: Array?, authType: String?) {} diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeRepository.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeRepository.kt index 17efc697d0..8c35c51084 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeRepository.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeRepository.kt @@ -28,9 +28,8 @@ import org.meshtastic.proto.User /** * Repository interface for managing node-related data. * - * This component provides access to the mesh's node database, local device information, - * and mesh-wide statistics. It supports reactive queries for node lists, counts, and - * filtered/sorted views. + * This component provides access to the mesh's node database, local device information, and mesh-wide statistics. It + * supports reactive queries for node lists, counts, and filtered/sorted views. * * This interface is shared across platforms via Kotlin Multiplatform (KMP). */ diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketRepository.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketRepository.kt index 27aeff3d7e..c43d559c45 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketRepository.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketRepository.kt @@ -29,9 +29,8 @@ import org.meshtastic.proto.ChannelSettings /** * Repository interface for managing mesh packets and message history. * - * This component provides methods for persisting received packets, querying message history, - * tracking unread counts, and managing contact-specific settings. It supports both - * reactive (Flow) and one-shot (suspend) queries. + * This component provides methods for persisting received packets, querying message history, tracking unread counts, + * and managing contact-specific settings. It supports both reactive (Flow) and one-shot (suspend) queries. */ @Suppress("TooManyFunctions") interface PacketRepository { diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/ServiceRepository.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/ServiceRepository.kt index 8e6f945448..4a8af11439 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/ServiceRepository.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/ServiceRepository.kt @@ -29,9 +29,8 @@ import org.meshtastic.proto.MeshPacket /** * Interface for managing background service state, connection status, and mesh events. * - * This repository acts as the primary data bridge between the long-running mesh service - * and the UI/Feature layers. It maintains reactive flows for connection status, - * error messages, and incoming mesh traffic. + * This repository acts as the primary data bridge between the long-running mesh service and the UI/Feature layers. It + * maintains reactive flows for connection status, error messages, and incoming mesh traffic. */ @Suppress("TooManyFunctions") interface ServiceRepository { diff --git a/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt b/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt index 6b0a6be455..d37715e476 100644 --- a/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt +++ b/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt @@ -228,6 +228,7 @@ fun BaseMapViewModel.tracerouteNodeSelection( ) } +@Suppress("MagicNumber") enum class LastHeardFilter(val label: StringResource, val seconds: Long) { Any(Res.string.any, 0L), OneHour(Res.string.one_hour, 3600L), diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorker.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorker.kt index 5a4bebe52e..ac4fd76a04 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorker.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorker.kt @@ -37,6 +37,7 @@ constructor( private val radioController: RadioController, ) : CoroutineWorker(context, params) { + @Suppress("TooGenericExceptionCaught", "SwallowedException", "ReturnCount") override suspend fun doWork(): Result { val packetId = inputData.getInt(KEY_PACKET_ID, 0) if (packetId == 0) return Result.failure() From 789a0fd0f2211cd0a37d2a003f2fd0784eeb18fb Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 2 Mar 2026 17:07:39 -0600 Subject: [PATCH 15/20] fix tests Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .../settings/CleanNodeDatabaseUseCase.kt | 2 +- .../usecase/settings/ExportDataUseCase.kt | 2 +- .../usecase/settings/ExportProfileUseCase.kt | 2 +- .../settings/ExportSecurityConfigUseCase.kt | 2 +- .../usecase/settings/ImportProfileUseCase.kt | 2 +- .../usecase/settings/InstallProfileUseCase.kt | 2 +- .../usecase/settings/IsOtaCapableUseCase.kt | 2 +- .../usecase/settings/MeshLocationUseCase.kt | 2 +- .../settings/ProcessRadioResponseUseCase.kt | 2 +- .../settings/SetAppIntroCompletedUseCase.kt | 6 ++++- .../settings/SetDatabaseCacheLimitUseCase.kt | 2 +- .../settings/SetMeshLogSettingsUseCase.kt | 2 +- .../settings/SetProvideLocationUseCase.kt | 2 +- .../usecase/settings/SetThemeUseCase.kt | 2 +- .../settings/ToggleAnalyticsUseCase.kt | 2 +- .../ToggleHomoglyphEncodingUseCase.kt | 2 +- .../core/domain/FakeRadioController.kt | 20 ++++++++++++++ .../settings/CleanNodeDatabaseUseCaseTest.kt | 8 +++--- .../usecase/settings/ExportDataUseCaseTest.kt | 1 - .../feature/map/MapViewModelTest.kt | 8 +++--- .../feature/settings/SettingsViewModelTest.kt | 27 +++++++++++++------ 21 files changed, 67 insertions(+), 33 deletions(-) diff --git a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/CleanNodeDatabaseUseCase.kt b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/CleanNodeDatabaseUseCase.kt index ee66d847be..655323caf1 100644 --- a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/CleanNodeDatabaseUseCase.kt +++ b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/CleanNodeDatabaseUseCase.kt @@ -23,7 +23,7 @@ import javax.inject.Inject import kotlin.time.Duration.Companion.days /** Use case for cleaning up nodes from the database. */ -class CleanNodeDatabaseUseCase +open class CleanNodeDatabaseUseCase @Inject constructor( private val nodeRepository: NodeRepository, diff --git a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCase.kt b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCase.kt index c9884bf6fd..aea9301d4e 100644 --- a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCase.kt +++ b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCase.kt @@ -30,7 +30,7 @@ import kotlin.math.roundToInt import org.meshtastic.proto.Position as ProtoPosition /** Use case for exporting persisted packet data to a CSV format. */ -class ExportDataUseCase +open class ExportDataUseCase @Inject constructor( private val nodeRepository: NodeRepository, diff --git a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ExportProfileUseCase.kt b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ExportProfileUseCase.kt index 8a9905975d..50d82d7444 100644 --- a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ExportProfileUseCase.kt +++ b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ExportProfileUseCase.kt @@ -21,7 +21,7 @@ import java.io.OutputStream import javax.inject.Inject /** Use case for exporting a device profile to an output stream. */ -class ExportProfileUseCase @Inject constructor() { +open class ExportProfileUseCase @Inject constructor() { /** * Exports the provided [DeviceProfile] to the given [OutputStream]. * diff --git a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ExportSecurityConfigUseCase.kt b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ExportSecurityConfigUseCase.kt index 2e32ed868c..a48cc64779 100644 --- a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ExportSecurityConfigUseCase.kt +++ b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ExportSecurityConfigUseCase.kt @@ -24,7 +24,7 @@ import java.io.OutputStream import javax.inject.Inject /** Use case for exporting security configuration to a JSON format. */ -class ExportSecurityConfigUseCase @Inject constructor() { +open class ExportSecurityConfigUseCase @Inject constructor() { /** * Exports the provided [Config.SecurityConfig] as a JSON string to the given [OutputStream]. * diff --git a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ImportProfileUseCase.kt b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ImportProfileUseCase.kt index 7dc1a97450..d78d716937 100644 --- a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ImportProfileUseCase.kt +++ b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ImportProfileUseCase.kt @@ -21,7 +21,7 @@ import java.io.InputStream import javax.inject.Inject /** Use case for importing a device profile from an input stream. */ -class ImportProfileUseCase @Inject constructor() { +open class ImportProfileUseCase @Inject constructor() { /** * Imports a [DeviceProfile] from the provided [InputStream]. * diff --git a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCase.kt b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCase.kt index 20b59f452b..88e8319a54 100644 --- a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCase.kt +++ b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCase.kt @@ -27,7 +27,7 @@ import org.meshtastic.proto.User import javax.inject.Inject /** Use case for installing a device profile onto a radio. */ -class InstallProfileUseCase @Inject constructor(private val radioController: RadioController) { +open class InstallProfileUseCase @Inject constructor(private val radioController: RadioController) { /** * Installs the provided [DeviceProfile] onto the radio at [destNum]. * diff --git a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCase.kt b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCase.kt index 51cf4af0ee..f77a09345c 100644 --- a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCase.kt +++ b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCase.kt @@ -32,7 +32,7 @@ import org.meshtastic.core.repository.NodeRepository import javax.inject.Inject /** Use case to determine if the currently connected device is capable of over-the-air (OTA) updates. */ -class IsOtaCapableUseCase +open class IsOtaCapableUseCase @Inject constructor( private val nodeRepository: NodeRepository, diff --git a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/MeshLocationUseCase.kt b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/MeshLocationUseCase.kt index f03f89e230..6f578bc055 100644 --- a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/MeshLocationUseCase.kt +++ b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/MeshLocationUseCase.kt @@ -20,7 +20,7 @@ import org.meshtastic.core.model.RadioController import javax.inject.Inject /** Use case for controlling location sharing with the mesh. */ -class MeshLocationUseCase @Inject constructor(private val radioController: RadioController) { +open class MeshLocationUseCase @Inject constructor(private val radioController: RadioController) { /** Starts providing the phone's location to the mesh. */ fun startProvidingLocation() { radioController.startProvideLocation() diff --git a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ProcessRadioResponseUseCase.kt b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ProcessRadioResponseUseCase.kt index 916163ecc2..3e16394699 100644 --- a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ProcessRadioResponseUseCase.kt +++ b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ProcessRadioResponseUseCase.kt @@ -54,7 +54,7 @@ sealed class RadioResponseResult { } /** Use case for processing incoming [MeshPacket]s that are responses to admin requests. */ -class ProcessRadioResponseUseCase @Inject constructor() { +open class ProcessRadioResponseUseCase @Inject constructor() { /** * Decodes and processes the provided [packet]. * diff --git a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/SetAppIntroCompletedUseCase.kt b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/SetAppIntroCompletedUseCase.kt index 04462c0f91..d31cc41f33 100644 --- a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/SetAppIntroCompletedUseCase.kt +++ b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/SetAppIntroCompletedUseCase.kt @@ -20,7 +20,11 @@ import org.meshtastic.core.datastore.UiPreferencesDataSource import javax.inject.Inject /** Use case for setting whether the application intro has been completed. */ -class SetAppIntroCompletedUseCase @Inject constructor(private val uiPreferencesDataSource: UiPreferencesDataSource) { +open class SetAppIntroCompletedUseCase +@Inject +constructor( + private val uiPreferencesDataSource: UiPreferencesDataSource, +) { operator fun invoke(completed: Boolean) { uiPreferencesDataSource.setAppIntroCompleted(completed) } diff --git a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/SetDatabaseCacheLimitUseCase.kt b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/SetDatabaseCacheLimitUseCase.kt index 4153ad934d..dc32d9891b 100644 --- a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/SetDatabaseCacheLimitUseCase.kt +++ b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/SetDatabaseCacheLimitUseCase.kt @@ -21,7 +21,7 @@ import org.meshtastic.core.database.DatabaseManager import javax.inject.Inject /** Use case for setting the database cache limit. */ -class SetDatabaseCacheLimitUseCase @Inject constructor(private val databaseManager: DatabaseManager) { +open class SetDatabaseCacheLimitUseCase @Inject 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/main/kotlin/org/meshtastic/core/domain/usecase/settings/SetMeshLogSettingsUseCase.kt b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/SetMeshLogSettingsUseCase.kt index 360c72bcdd..cdb822dde1 100644 --- a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/SetMeshLogSettingsUseCase.kt +++ b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/SetMeshLogSettingsUseCase.kt @@ -21,7 +21,7 @@ import org.meshtastic.core.prefs.meshlog.MeshLogPrefs import javax.inject.Inject /** Use case for managing mesh log settings. */ -class SetMeshLogSettingsUseCase +open class SetMeshLogSettingsUseCase @Inject constructor( private val meshLogRepository: MeshLogRepository, diff --git a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/SetProvideLocationUseCase.kt b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/SetProvideLocationUseCase.kt index fa8daee9e2..3a45c3e430 100644 --- a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/SetProvideLocationUseCase.kt +++ b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/SetProvideLocationUseCase.kt @@ -20,7 +20,7 @@ import org.meshtastic.core.prefs.ui.UiPrefs import javax.inject.Inject /** Use case for setting whether to provide the node location to the mesh. */ -class SetProvideLocationUseCase @Inject constructor(private val uiPrefs: UiPrefs) { +open class SetProvideLocationUseCase @Inject constructor(private val uiPrefs: UiPrefs) { operator fun invoke(myNodeNum: Int, provideLocation: Boolean) { uiPrefs.setShouldProvideNodeLocation(myNodeNum, provideLocation) } diff --git a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/SetThemeUseCase.kt b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/SetThemeUseCase.kt index 437e396044..fd1ae35a0f 100644 --- a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/SetThemeUseCase.kt +++ b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/SetThemeUseCase.kt @@ -20,7 +20,7 @@ import org.meshtastic.core.datastore.UiPreferencesDataSource import javax.inject.Inject /** Use case for setting the application theme. */ -class SetThemeUseCase @Inject constructor(private val uiPreferencesDataSource: UiPreferencesDataSource) { +open class SetThemeUseCase @Inject constructor(private val uiPreferencesDataSource: UiPreferencesDataSource) { operator fun invoke(themeMode: Int) { uiPreferencesDataSource.setTheme(themeMode) } diff --git a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ToggleAnalyticsUseCase.kt b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ToggleAnalyticsUseCase.kt index 0682c4da2d..b8e6f2d295 100644 --- a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ToggleAnalyticsUseCase.kt +++ b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ToggleAnalyticsUseCase.kt @@ -20,7 +20,7 @@ import org.meshtastic.core.prefs.analytics.AnalyticsPrefs import javax.inject.Inject /** Use case for toggling the analytics preference. */ -class ToggleAnalyticsUseCase @Inject constructor(private val analyticsPrefs: AnalyticsPrefs) { +open class ToggleAnalyticsUseCase @Inject constructor(private val analyticsPrefs: AnalyticsPrefs) { operator fun invoke() { analyticsPrefs.analyticsAllowed = !analyticsPrefs.analyticsAllowed } diff --git a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ToggleHomoglyphEncodingUseCase.kt b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ToggleHomoglyphEncodingUseCase.kt index 1c83d68862..f42dee80b5 100644 --- a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ToggleHomoglyphEncodingUseCase.kt +++ b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ToggleHomoglyphEncodingUseCase.kt @@ -20,7 +20,7 @@ import org.meshtastic.core.prefs.homoglyph.HomoglyphPrefs import javax.inject.Inject /** Use case for toggling the homoglyph encoding preference. */ -class ToggleHomoglyphEncodingUseCase @Inject constructor(private val homoglyphEncodingPrefs: HomoglyphPrefs) { +open class ToggleHomoglyphEncodingUseCase @Inject constructor(private val homoglyphEncodingPrefs: HomoglyphPrefs) { operator fun invoke() { homoglyphEncodingPrefs.homoglyphEncodingEnabled = !homoglyphEncodingPrefs.homoglyphEncodingEnabled } diff --git a/core/domain/src/test/kotlin/org/meshtastic/core/domain/FakeRadioController.kt b/core/domain/src/test/kotlin/org/meshtastic/core/domain/FakeRadioController.kt index 69ec2022a2..115f4ff434 100644 --- a/core/domain/src/test/kotlin/org/meshtastic/core/domain/FakeRadioController.kt +++ b/core/domain/src/test/kotlin/org/meshtastic/core/domain/FakeRadioController.kt @@ -53,6 +53,10 @@ class FakeRadioController : RadioController { sentSharedContacts.add(nodeNum) } + override suspend fun setLocalConfig(config: org.meshtastic.proto.Config) {} + + override suspend fun setLocalChannel(channel: org.meshtastic.proto.Channel) {} + override suspend fun setOwner(destNum: Int, user: org.meshtastic.proto.User, packetId: Int) {} override suspend fun setConfig(destNum: Int, config: org.meshtastic.proto.Config, packetId: Int) {} @@ -83,6 +87,10 @@ class FakeRadioController : RadioController { override suspend fun reboot(destNum: Int, packetId: Int) {} + override suspend fun rebootToDfu(nodeNum: Int) {} + + override suspend fun requestRebootOta(requestId: Int, destNum: Int, mode: Int, hash: ByteArray?) {} + override suspend fun shutdown(destNum: Int, packetId: Int) {} override suspend fun factoryReset(destNum: Int, packetId: Int) {} @@ -91,6 +99,16 @@ class FakeRadioController : RadioController { override suspend fun removeByNodenum(packetId: Int, nodeNum: Int) {} + override suspend fun requestPosition(destNum: Int, currentPosition: org.meshtastic.core.model.Position) {} + + override suspend fun requestUserInfo(destNum: Int) {} + + override suspend fun requestTraceroute(requestId: Int, destNum: Int) {} + + override suspend fun requestTelemetry(requestId: Int, destNum: Int, typeValue: Int) {} + + override suspend fun requestNeighborInfo(requestId: Int, destNum: Int) {} + override suspend fun beginEditSettings(destNum: Int) {} override suspend fun commitEditSettings(destNum: Int) {} @@ -101,6 +119,8 @@ class FakeRadioController : RadioController { override fun stopProvideLocation() {} + override fun setDeviceAddress(address: String) {} + // --- Helper methods for testing --- fun setConnectionState(state: ConnectionState) { diff --git a/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/CleanNodeDatabaseUseCaseTest.kt b/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/CleanNodeDatabaseUseCaseTest.kt index 09e8b15d62..e8631beb26 100644 --- a/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/CleanNodeDatabaseUseCaseTest.kt +++ b/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/CleanNodeDatabaseUseCaseTest.kt @@ -23,8 +23,8 @@ import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test -import org.meshtastic.core.database.entity.NodeEntity import org.meshtastic.core.domain.FakeRadioController +import org.meshtastic.core.model.Node import org.meshtastic.core.repository.NodeRepository import kotlin.time.Duration.Companion.days @@ -47,9 +47,9 @@ class CleanNodeDatabaseUseCaseTest { val currentTime = 1000000L val olderThanTimestamp = currentTime - 30.days.inWholeSeconds - val oldNode = NodeEntity(num = 1, lastHeard = (olderThanTimestamp - 1).toInt()) - val newNode = NodeEntity(num = 2, lastHeard = (currentTime - 1).toInt()) - val ignoredNode = NodeEntity(num = 3, lastHeard = (olderThanTimestamp - 1).toInt(), isIgnored = true) + val oldNode = Node(num = 1, lastHeard = (olderThanTimestamp - 1).toInt()) + val newNode = Node(num = 2, lastHeard = (currentTime - 1).toInt()) + val ignoredNode = Node(num = 3, lastHeard = (olderThanTimestamp - 1).toInt(), isIgnored = true) coEvery { nodeRepository.getNodesOlderThan(any()) } returns listOf(oldNode, ignoredNode) diff --git a/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCaseTest.kt b/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCaseTest.kt index f5175877c9..5e3a05cabb 100644 --- a/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCaseTest.kt +++ b/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCaseTest.kt @@ -63,7 +63,6 @@ class ExportDataUseCaseTest { val nodes = mapOf(senderNodeNum to senderNode) val stateFlow = MutableStateFlow(nodes) every { nodeRepository.nodeDBbyNum } returns stateFlow - every { nodeRepository.getNodeEntityDBbyNumFlow() } returns flowOf(emptyMap()) val meshPacket = MeshPacket( diff --git a/feature/map/src/testGoogle/kotlin/org/meshtastic/feature/map/MapViewModelTest.kt b/feature/map/src/testGoogle/kotlin/org/meshtastic/feature/map/MapViewModelTest.kt index a4b77cf774..cbf7a8443c 100644 --- a/feature/map/src/testGoogle/kotlin/org/meshtastic/feature/map/MapViewModelTest.kt +++ b/feature/map/src/testGoogle/kotlin/org/meshtastic/feature/map/MapViewModelTest.kt @@ -42,12 +42,12 @@ import org.meshtastic.core.data.model.CustomTileProviderConfig import org.meshtastic.core.data.repository.CustomTileProviderRepository import org.meshtastic.core.datastore.UiPreferencesDataSource import org.meshtastic.core.model.ConnectionState +import org.meshtastic.core.model.RadioController import org.meshtastic.core.prefs.map.GoogleMapsPrefs import org.meshtastic.core.prefs.map.MapPrefs import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.RadioConfigRepository -import org.meshtastic.core.service.ServiceRepository import org.robolectric.RobolectricTestRunner @OptIn(ExperimentalCoroutinesApi::class) @@ -60,7 +60,7 @@ class MapViewModelTest { private val nodeRepository = mockk(relaxed = true) private val packetRepository = mockk(relaxed = true) private val radioConfigRepository = mockk(relaxed = true) - private val serviceRepository = mockk(relaxed = true) + private val radioController = mockk(relaxed = true) private val customTileProviderRepository = mockk(relaxed = true) private val uiPreferencesDataSource = mockk(relaxed = true) private val savedStateHandle = SavedStateHandle(mapOf("waypointId" to null)) @@ -81,7 +81,7 @@ class MapViewModelTest { every { nodeRepository.nodeDBbyNum } returns MutableStateFlow(emptyMap()) every { nodeRepository.getNodes() } returns flowOf(emptyList()) every { packetRepository.getWaypoints() } returns flowOf(emptyList()) - every { serviceRepository.connectionState } returns MutableStateFlow(ConnectionState.Disconnected) + every { radioController.connectionState } returns MutableStateFlow(ConnectionState.Disconnected) viewModel = MapViewModel( @@ -91,7 +91,7 @@ class MapViewModelTest { nodeRepository, packetRepository, radioConfigRepository, - serviceRepository, + radioController, customTileProviderRepository, uiPreferencesDataSource, savedStateHandle, diff --git a/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/SettingsViewModelTest.kt b/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/SettingsViewModelTest.kt index 5d5a9d4b87..39a00c3fdf 100644 --- a/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/SettingsViewModelTest.kt +++ b/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/SettingsViewModelTest.kt @@ -44,8 +44,10 @@ import org.meshtastic.core.prefs.meshlog.MeshLogPrefs import org.meshtastic.core.prefs.ui.UiPrefs import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.RadioConfigRepository +import org.robolectric.annotation.Config @OptIn(ExperimentalCoroutinesApi::class) +@Config(sdk = [34]) class SettingsViewModelTest { private val testDispatcher = StandardTestDispatcher() @@ -58,14 +60,14 @@ class SettingsViewModelTest { private val databaseManager: DatabaseManager = mockk(relaxed = true) private val meshLogPrefs: MeshLogPrefs = mockk(relaxed = true) - private val setThemeUseCase: SetThemeUseCase = mockk(relaxed = true) - private val setAppIntroCompletedUseCase: SetAppIntroCompletedUseCase = mockk(relaxed = true) - private val setProvideLocationUseCase: SetProvideLocationUseCase = mockk(relaxed = true) - private val setDatabaseCacheLimitUseCase: SetDatabaseCacheLimitUseCase = mockk(relaxed = true) - private val setMeshLogSettingsUseCase: SetMeshLogSettingsUseCase = mockk(relaxed = true) - private val meshLocationUseCase: MeshLocationUseCase = mockk(relaxed = true) - private val exportDataUseCase: ExportDataUseCase = mockk(relaxed = true) - private val isOtaCapableUseCase: IsOtaCapableUseCase = mockk(relaxed = true) + private lateinit var setThemeUseCase: SetThemeUseCase + private lateinit var setAppIntroCompletedUseCase: SetAppIntroCompletedUseCase + private lateinit var setProvideLocationUseCase: SetProvideLocationUseCase + private lateinit var setDatabaseCacheLimitUseCase: SetDatabaseCacheLimitUseCase + private lateinit var setMeshLogSettingsUseCase: SetMeshLogSettingsUseCase + private lateinit var meshLocationUseCase: MeshLocationUseCase + private lateinit var exportDataUseCase: ExportDataUseCase + private lateinit var isOtaCapableUseCase: IsOtaCapableUseCase private lateinit var viewModel: SettingsViewModel @@ -73,6 +75,15 @@ class SettingsViewModelTest { fun setUp() { Dispatchers.setMain(testDispatcher) + setThemeUseCase = mockk(relaxed = true) + setAppIntroCompletedUseCase = mockk(relaxed = true) + setProvideLocationUseCase = mockk(relaxed = true) + setDatabaseCacheLimitUseCase = mockk(relaxed = true) + setMeshLogSettingsUseCase = mockk(relaxed = true) + meshLocationUseCase = mockk(relaxed = true) + exportDataUseCase = mockk(relaxed = true) + isOtaCapableUseCase = mockk(relaxed = true) + // Return real StateFlows to avoid ClassCastException every { databaseManager.cacheLimit } returns MutableStateFlow(100) every { nodeRepository.myNodeInfo } returns MutableStateFlow(null) From f07d52a36ff824f6fa880e4a95b7972bde540099 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 2 Mar 2026 18:07:56 -0600 Subject: [PATCH 16/20] refactor: move UNSET region to end and strip base64 padding from URLs - Move `ChannelOption.UNSET` to the end of the enum. - Strip base64 padding characters from generated `ChannelSet` URLs. - Update `SharedContactTest` to use `MalformedMeshtasticUrlException` and add additional validation test cases. Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .../core/model/util/ChannelSetTest.kt | 2 +- .../core/model/util/SharedContactTest.kt | 34 ++++++++++++++++++- .../meshtastic/core/model/ChannelOption.kt | 6 ++-- .../meshtastic/core/model/util/ChannelSet.kt | 2 +- 4 files changed, 38 insertions(+), 6 deletions(-) diff --git a/core/model/src/androidDeviceTest/kotlin/org/meshtastic/core/model/util/ChannelSetTest.kt b/core/model/src/androidDeviceTest/kotlin/org/meshtastic/core/model/util/ChannelSetTest.kt index 1ebc7faf24..486ef4368f 100644 --- a/core/model/src/androidDeviceTest/kotlin/org/meshtastic/core/model/util/ChannelSetTest.kt +++ b/core/model/src/androidDeviceTest/kotlin/org/meshtastic/core/model/util/ChannelSetTest.kt @@ -31,7 +31,7 @@ class ChannelSetTest { val url = Uri.parse("https://meshtastic.org/e/#CgMSAQESBggBQANIAQ") val cs = url.toChannelSet() Assert.assertEquals("LongFast", cs.primaryChannel!!.name) - Assert.assertEquals(url, cs.getChannelUrl(false)) + Assert.assertEquals(url.toString(), cs.getChannelUrl(false).toString()) } /** validate against the host or path in a case-insensitive way */ diff --git a/core/model/src/androidDeviceTest/kotlin/org/meshtastic/core/model/util/SharedContactTest.kt b/core/model/src/androidDeviceTest/kotlin/org/meshtastic/core/model/util/SharedContactTest.kt index 8f346ed2fe..fc877497f6 100644 --- a/core/model/src/androidDeviceTest/kotlin/org/meshtastic/core/model/util/SharedContactTest.kt +++ b/core/model/src/androidDeviceTest/kotlin/org/meshtastic/core/model/util/SharedContactTest.kt @@ -56,11 +56,43 @@ class SharedContactTest { assertEquals("Suzume", contact.user?.long_name) } - @Test(expected = java.net.MalformedURLException::class) + @Test(expected = MalformedMeshtasticUrlException::class) fun testInvalidHostThrows() { val original = SharedContact(user = User(long_name = "Suzume"), node_num = 12345) val urlStr = original.getSharedContactUrl().toString().replace("meshtastic.org", "example.com") val url = Uri.parse(urlStr) url.toSharedContact() } + + @Test(expected = MalformedMeshtasticUrlException::class) + fun testInvalidPathThrows() { + val original = SharedContact(user = User(long_name = "Suzume"), node_num = 12345) + val urlStr = original.getSharedContactUrl().toString().replace("/v/", "/wrong/") + val url = Uri.parse(urlStr) + url.toSharedContact() + } + + @Test(expected = MalformedMeshtasticUrlException::class) + fun testMissingFragmentThrows() { + val urlStr = "https://meshtastic.org/v/" + val url = Uri.parse(urlStr) + url.toSharedContact() + } + + @Test(expected = MalformedMeshtasticUrlException::class) + fun testInvalidBase64Throws() { + val urlStr = "https://meshtastic.org/v/#InvalidBase64!!!!" + val url = Uri.parse(urlStr) + url.toSharedContact() + } + + @Test(expected = MalformedMeshtasticUrlException::class) + fun testInvalidProtoThrows() { + // Tag 0 is invalid in Protobuf + // 0x00 -> Tag 0, Type 0. + // Base64 for 0x00 is "AA==" + val urlStr = "https://meshtastic.org/v/#AA==" + val url = Uri.parse(urlStr) + url.toSharedContact() + } } diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/ChannelOption.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/ChannelOption.kt index a013005dfd..0a9ad17483 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/ChannelOption.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/ChannelOption.kt @@ -103,9 +103,6 @@ enum class RegionInfo( val freqEnd: Float, val wideLora: Boolean = false, ) { - /** This needs to be last. Same as US. */ - UNSET(RegionCode.UNSET, "Please set a region", 902.0f, 928.0f), - /** * United States * @@ -288,6 +285,9 @@ enum class RegionInfo( * @see [Firmware Issue #7399](https://github.com/meshtastic/firmware/pull/7399) */ BR_902(RegionCode.BR_902, "Brazil 902MHz", 902.0f, 907.5f, wideLora = false), + + /** This needs to be last. Same as US. */ + UNSET(RegionCode.UNSET, "Please set a region", 902.0f, 928.0f), ; companion object { diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/ChannelSet.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/ChannelSet.kt index ff4d3c792b..c184d9fc17 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/ChannelSet.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/ChannelSet.kt @@ -83,7 +83,7 @@ fun ChannelSet.hasLoraConfig(): Boolean = lora_config != null */ fun ChannelSet.getChannelUrl(upperCasePrefix: Boolean = false, shouldAdd: Boolean = false): CommonUri { val channelBytes = ChannelSet.ADAPTER.encode(this) - val enc = channelBytes.toByteString().base64Url() + val enc = channelBytes.toByteString().base64Url().replace("=", "") val p = if (upperCasePrefix) CHANNEL_URL_PREFIX.uppercase() else CHANNEL_URL_PREFIX val query = if (shouldAdd) "?add=true" else "" return CommonUri.parse("$p$query#$enc") From c9cf875cbfdc8a9cb7eeb473733487b224da4667 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 2 Mar 2026 18:56:17 -0600 Subject: [PATCH 17/20] feat: use goAsync and suspend functions in ReplyReceiver Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .../geeksville/mesh/service/ReplyReceiver.kt | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/service/ReplyReceiver.kt b/app/src/main/java/com/geeksville/mesh/service/ReplyReceiver.kt index e602b52818..e210396708 100644 --- a/app/src/main/java/com/geeksville/mesh/service/ReplyReceiver.kt +++ b/app/src/main/java/com/geeksville/mesh/service/ReplyReceiver.kt @@ -17,9 +17,14 @@ package com.geeksville.mesh.service 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.meshtastic.core.model.DataPacket import org.meshtastic.core.model.RadioController @@ -39,28 +44,38 @@ class ReplyReceiver : BroadcastReceiver() { @Inject lateinit var meshServiceNotifications: MeshServiceNotifications + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + companion object { const val REPLY_ACTION = "com.geeksville.mesh.REPLY_ACTION" const val CONTACT_KEY = "contactKey" const val KEY_TEXT_REPLY = "key_text_reply" } - private fun sendMessage(str: String, contactKey: String = "0${DataPacket.ID_BROADCAST}") { - // contactKey: unique contact key filter (channel)+(nodeId) - val channel = contactKey[0].digitToIntOrNull() - val dest = if (channel != null) contactKey.substring(1) else contactKey - val p = DataPacket(dest, channel ?: 0, str) - kotlinx.coroutines.CoroutineScope(kotlinx.coroutines.Dispatchers.IO).launch { radioController.sendMessage(p) } - } - - override fun onReceive(context: android.content.Context, intent: android.content.Intent) { + override fun onReceive(context: Context, intent: Intent) { val remoteInput = RemoteInput.getResultsFromIntent(intent) if (remoteInput != null) { val contactKey = intent.getStringExtra(CONTACT_KEY) ?: "" val message = remoteInput.getCharSequence(KEY_TEXT_REPLY)?.toString() ?: "" - sendMessage(message, contactKey) - meshServiceNotifications.cancelMessageNotification(contactKey) + + val pendingResult = goAsync() + scope.launch { + try { + sendMessage(message, contactKey) + meshServiceNotifications.cancelMessageNotification(contactKey) + } finally { + pendingResult.finish() + } + } } } + + private suspend fun sendMessage(str: String, contactKey: String) { + // contactKey: unique contact key filter (channel)+(nodeId) + val channel = contactKey.getOrNull(0)?.digitToIntOrNull() + val dest = if (channel != null) contactKey.substring(1) else contactKey + val p = DataPacket(dest, channel ?: 0, str) + radioController.sendMessage(p) + } } From d661dc2f123cdcd480cd321238f83b5f6637fe0b Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 2 Mar 2026 19:30:17 -0600 Subject: [PATCH 18/20] refactor: move DatabaseManager to repository and rename getCacheLimit() to getCurrentCacheLimit() Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .../org/meshtastic/core/database/DatabaseManager.kt | 10 +++++----- .../usecase/settings/SetDatabaseCacheLimitUseCase.kt | 2 +- .../settings/SetDatabaseCacheLimitUseCaseTest.kt | 2 +- .../org/meshtastic/core/repository/DatabaseManager.kt | 4 ++-- .../meshtastic/feature/settings/SettingsViewModel.kt | 2 +- .../feature/settings/SettingsViewModelTest.kt | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/DatabaseManager.kt b/core/database/src/main/kotlin/org/meshtastic/core/database/DatabaseManager.kt index 7b61e66a30..e935a88e21 100644 --- a/core/database/src/main/kotlin/org/meshtastic/core/database/DatabaseManager.kt +++ b/core/database/src/main/kotlin/org/meshtastic/core/database/DatabaseManager.kt @@ -59,14 +59,14 @@ constructor( private val mutex = Mutex() // Expose the DB cache limit as a reactive stream so UI can observe changes. - private val _cacheLimit = MutableStateFlow(getCacheLimit()) + private val _cacheLimit = MutableStateFlow(getCurrentCacheLimit()) override val cacheLimit: StateFlow = _cacheLimit // Keep cache-limit StateFlow in sync if some other component updates SharedPreferences. private val prefsListener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> if (key == DatabaseConstants.CACHE_LIMIT_KEY) { - _cacheLimit.value = getCacheLimit() + _cacheLimit.value = getCurrentCacheLimit() } } @@ -160,7 +160,7 @@ constructor( } private suspend fun enforceCacheLimit(activeDbName: String) = mutex.withLock { - val limit = getCacheLimit() + val limit = getCurrentCacheLimit() val all = listExistingDbNames() // Only enforce the limit over device-specific DBs; exclude legacy and default DBs val deviceDbs = @@ -190,13 +190,13 @@ constructor( } } - override fun getCacheLimit(): Int = prefs + override fun getCurrentCacheLimit(): Int = prefs .getInt(DatabaseConstants.CACHE_LIMIT_KEY, DatabaseConstants.DEFAULT_CACHE_LIMIT) .coerceIn(DatabaseConstants.MIN_CACHE_LIMIT, DatabaseConstants.MAX_CACHE_LIMIT) override fun setCacheLimit(limit: Int) { val clamped = limit.coerceIn(DatabaseConstants.MIN_CACHE_LIMIT, DatabaseConstants.MAX_CACHE_LIMIT) - if (clamped == getCacheLimit()) return + if (clamped == getCurrentCacheLimit()) return prefs.edit().putInt(DatabaseConstants.CACHE_LIMIT_KEY, clamped).apply() _cacheLimit.value = clamped // Enforce asynchronously with current active DB protected diff --git a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/SetDatabaseCacheLimitUseCase.kt b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/SetDatabaseCacheLimitUseCase.kt index dc32d9891b..42224e8498 100644 --- a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/SetDatabaseCacheLimitUseCase.kt +++ b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/SetDatabaseCacheLimitUseCase.kt @@ -17,7 +17,7 @@ package org.meshtastic.core.domain.usecase.settings import org.meshtastic.core.database.DatabaseConstants -import org.meshtastic.core.database.DatabaseManager +import org.meshtastic.core.repository.DatabaseManager import javax.inject.Inject /** Use case for setting the database cache limit. */ diff --git a/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/SetDatabaseCacheLimitUseCaseTest.kt b/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/SetDatabaseCacheLimitUseCaseTest.kt index 1551ab32d9..78a22de2fe 100644 --- a/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/SetDatabaseCacheLimitUseCaseTest.kt +++ b/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/SetDatabaseCacheLimitUseCaseTest.kt @@ -21,7 +21,7 @@ import io.mockk.verify import org.junit.Before import org.junit.Test import org.meshtastic.core.database.DatabaseConstants -import org.meshtastic.core.database.DatabaseManager +import org.meshtastic.core.repository.DatabaseManager class SetDatabaseCacheLimitUseCaseTest { diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/DatabaseManager.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/DatabaseManager.kt index 18ab5f81fa..675092382c 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/DatabaseManager.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/DatabaseManager.kt @@ -23,8 +23,8 @@ interface DatabaseManager { /** Reactive stream of the current database cache limit. */ val cacheLimit: StateFlow - /** Returns the current database cache limit. */ - fun getCacheLimit(): Int + /** Returns the current database cache limit from storage. */ + fun getCurrentCacheLimit(): Int /** Sets the database cache limit. */ fun setCacheLimit(limit: Int) diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt index b3b653b505..db8aceff7a 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt @@ -32,7 +32,6 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.meshtastic.core.common.BuildConfigProvider -import org.meshtastic.core.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 @@ -46,6 +45,7 @@ import org.meshtastic.core.model.Node import org.meshtastic.core.model.RadioController import org.meshtastic.core.prefs.meshlog.MeshLogPrefs import org.meshtastic.core.prefs.ui.UiPrefs +import org.meshtastic.core.repository.DatabaseManager import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed diff --git a/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/SettingsViewModelTest.kt b/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/SettingsViewModelTest.kt index 39a00c3fdf..7e628b85bb 100644 --- a/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/SettingsViewModelTest.kt +++ b/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/SettingsViewModelTest.kt @@ -30,7 +30,6 @@ import org.junit.After import org.junit.Before import org.junit.Test import org.meshtastic.core.common.BuildConfigProvider -import org.meshtastic.core.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 @@ -42,6 +41,7 @@ import org.meshtastic.core.domain.usecase.settings.SetThemeUseCase import org.meshtastic.core.model.RadioController import org.meshtastic.core.prefs.meshlog.MeshLogPrefs import org.meshtastic.core.prefs.ui.UiPrefs +import org.meshtastic.core.repository.DatabaseManager import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.RadioConfigRepository import org.robolectric.annotation.Config From b61656787f1f4df5a8f0788d142b8146b0663336 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 2 Mar 2026 19:46:10 -0600 Subject: [PATCH 19/20] feat(ble): add isBonded check to BluetoothRepository - Add `isBonded(address)` helper to `BluetoothRepository` to verify bonding status with permission and state checks. - Refactor `NordicBleInterfaceSpec` to use the new helper method. - Update `bond()` to call `updateBluetoothState()`. Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .../radio/NordicBleInterfaceSpec.kt | 3 +-- .../core/ble/BluetoothRepository.kt | 20 ++++++++++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceSpec.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceSpec.kt index 49f989452b..6edabb51b8 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceSpec.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceSpec.kt @@ -32,8 +32,7 @@ constructor( /** Return true if this address is still acceptable. For BLE that means, still bonded */ override fun addressValid(rest: String): Boolean { - val allPaired = bluetoothRepository.state.value.bondedDevices.map { it.address }.toSet() - return if (!allPaired.contains(rest)) { + return if (!bluetoothRepository.isBonded(rest)) { Logger.w { "Ignoring stale bond to ${rest.anonymize}" } false } else { diff --git a/core/ble/src/main/kotlin/org/meshtastic/core/ble/BluetoothRepository.kt b/core/ble/src/main/kotlin/org/meshtastic/core/ble/BluetoothRepository.kt index e58e804b68..8861b8a11b 100644 --- a/core/ble/src/main/kotlin/org/meshtastic/core/ble/BluetoothRepository.kt +++ b/core/ble/src/main/kotlin/org/meshtastic/core/ble/BluetoothRepository.kt @@ -81,7 +81,7 @@ constructor( @SuppressLint("MissingPermission") suspend fun bond(peripheral: Peripheral) { peripheral.createBond() - refreshState() + updateBluetoothState() } internal suspend fun updateBluetoothState() { @@ -112,6 +112,24 @@ constructor( emptyList() } + /** @return true if the given address is currently bonded to the system. */ + @SuppressLint("MissingPermission") + fun isBonded(address: String): Boolean { + val enabled = androidEnvironment.isBluetoothEnabled + val hasPerms = + if (androidEnvironment.requiresBluetoothRuntimePermissions) { + androidEnvironment.isBluetoothScanPermissionGranted && + androidEnvironment.isBluetoothConnectPermissionGranted + } else { + androidEnvironment.isLocationPermissionGranted + } + return if (enabled && hasPerms) { + centralManager.getBondedPeripherals().any { it.address == address } + } else { + false + } + } + /** Checks if a peripheral is one of ours, either by its advertised name or by the services it provides. */ private fun isMatchingPeripheral(peripheral: Peripheral): Boolean { val nameMatches = peripheral.name?.matches(Regex(BLE_NAME_PATTERN)) ?: false From 3b39fa4531d2dce6a7f9bb77bd668d086073089d Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 2 Mar 2026 19:58:03 -0600 Subject: [PATCH 20/20] refactor: match packets by key fields instead of full object equality Update `PacketDao` and `PacketRepositoryImpl` to identify packets using specific fields (`id`, `from`, and `to`) rather than checking for full object equality. This ensures more reliable updates when modifying packet status or IDs. Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .../repository/radio/NordicBleInterfaceSpec.kt | 12 +++++------- .../core/data/repository/PacketRepositoryImpl.kt | 5 ++++- .../org/meshtastic/core/database/dao/PacketDao.kt | 14 ++++++++------ 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceSpec.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceSpec.kt index 6edabb51b8..112d38e290 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceSpec.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceSpec.kt @@ -31,12 +31,10 @@ constructor( override fun createInterface(rest: String): NordicBleInterface = factory.create(rest) /** Return true if this address is still acceptable. For BLE that means, still bonded */ - override fun addressValid(rest: String): Boolean { - return if (!bluetoothRepository.isBonded(rest)) { - Logger.w { "Ignoring stale bond to ${rest.anonymize}" } - false - } else { - true - } + override fun addressValid(rest: String): Boolean = if (!bluetoothRepository.isBonded(rest)) { + Logger.w { "Ignoring stale bond to ${rest.anonymize}" } + false + } else { + true } } diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/PacketRepositoryImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/PacketRepositoryImpl.kt index 70fd25ba72..e29c82be1b 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/PacketRepositoryImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/PacketRepositoryImpl.kt @@ -255,7 +255,10 @@ constructor( override suspend fun update(packet: DataPacket): Unit = withContext(dispatchers.io) { val dao = dbManager.currentDb.value.packetDao() - dao.findPacketsWithId(packet.id).find { it.data == packet }?.let { dao.update(it.copy(data = packet)) } + // Match on key fields that identify the packet, rather than the entire data object + dao.findPacketsWithId(packet.id) + .find { it.data.id == packet.id && it.data.from == packet.from && it.data.to == packet.to } + ?.let { dao.update(it.copy(data = packet)) } } override suspend fun insertReaction(reaction: Reaction, myNodeNum: Int) = diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/dao/PacketDao.kt b/core/database/src/main/kotlin/org/meshtastic/core/database/dao/PacketDao.kt index 987ed999fa..047b2b47c7 100644 --- a/core/database/src/main/kotlin/org/meshtastic/core/database/dao/PacketDao.kt +++ b/core/database/src/main/kotlin/org/meshtastic/core/database/dao/PacketDao.kt @@ -241,17 +241,19 @@ interface PacketDao { @Transaction suspend fun updateMessageStatus(data: DataPacket, m: MessageStatus) { val new = data.copy(status = m) - // Find by packet ID first for better performance and reliability - findPacketsWithId(data.id).find { it.data == data }?.let { update(it.copy(data = new)) } - ?: findDataPacket(data)?.let { update(it.copy(data = new)) } + // Match on key fields that identify the packet, rather than the entire data object + findPacketsWithId(data.id) + .find { it.data.id == data.id && it.data.from == data.from && it.data.to == data.to } + ?.let { update(it.copy(data = new)) } } @Transaction suspend fun updateMessageId(data: DataPacket, id: Int) { val new = data.copy(id = id) - // Find by packet ID first for better performance and reliability - findPacketsWithId(data.id).find { it.data == data }?.let { update(it.copy(data = new, packetId = id)) } - ?: findDataPacket(data)?.let { update(it.copy(data = new, packetId = id)) } + // Match on key fields that identify the packet + findPacketsWithId(data.id) + .find { it.data.id == data.id && it.data.from == data.from && it.data.to == data.to } + ?.let { update(it.copy(data = new, packetId = id)) } } @Query(