mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-05-12 08:42:01 -04:00
refactor: extract MqttProbeCoordinator and ProfileCoordinator from RadioConfigViewModel
Extract self-contained logic into plain coordinator classes: - MqttProbeCoordinator: MQTT broker probe state + cancellation - ProfileCoordinator: import/export/install profile file I/O RadioConfigViewModel delegates to coordinators while retaining state ownership and config-loading orchestration (its core job). Reduces VM body by ~60 lines of implementation detail. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.settings.radio
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import org.meshtastic.core.common.util.safeCatching
|
||||
import org.meshtastic.core.model.MqttConnectionState
|
||||
import org.meshtastic.core.model.MqttProbeStatus
|
||||
import org.meshtastic.core.repository.MqttManager
|
||||
|
||||
/**
|
||||
* Encapsulates MQTT broker reachability probing logic.
|
||||
* Injected into [RadioConfigViewModel] to keep probe state and cancellation self-contained.
|
||||
*/
|
||||
class MqttProbeCoordinator(
|
||||
private val mqttManager: MqttManager,
|
||||
private val scope: CoroutineScope,
|
||||
) {
|
||||
/** MQTT proxy connection state for the settings UI. */
|
||||
val mqttConnectionState: StateFlow<MqttConnectionState> = mqttManager.mqttConnectionState
|
||||
|
||||
private val _probeStatus = MutableStateFlow<MqttProbeStatus?>(null)
|
||||
|
||||
/** Latest result from a [probe] call, or `null` if no probe has been run. */
|
||||
val probeStatus: StateFlow<MqttProbeStatus?> = _probeStatus.asStateFlow()
|
||||
|
||||
private var probeJob: Job? = null
|
||||
|
||||
/**
|
||||
* Run a one-shot reachability/credentials probe against an MQTT broker.
|
||||
* Cancels any in-flight probe before starting a new one.
|
||||
*/
|
||||
fun probe(address: String, tlsEnabled: Boolean, username: String?, password: String?) {
|
||||
probeJob?.cancel()
|
||||
_probeStatus.value = MqttProbeStatus.Probing
|
||||
probeJob = scope.launch {
|
||||
val result =
|
||||
safeCatching { mqttManager.probe(address, tlsEnabled, username, password) }
|
||||
.getOrElse { e ->
|
||||
Logger.w(e) { "MQTT probe threw" }
|
||||
MqttProbeStatus.Other(message = e.message)
|
||||
}
|
||||
_probeStatus.value = result
|
||||
}
|
||||
}
|
||||
|
||||
/** Clear the latest probe result (e.g. when the user edits the address). */
|
||||
fun clearProbeStatus() {
|
||||
probeJob?.cancel()
|
||||
_probeStatus.value = null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.settings.radio
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.meshtastic.core.common.util.CommonUri
|
||||
import org.meshtastic.core.domain.usecase.settings.ExportProfileUseCase
|
||||
import org.meshtastic.core.domain.usecase.settings.ExportSecurityConfigUseCase
|
||||
import org.meshtastic.core.domain.usecase.settings.ImportProfileUseCase
|
||||
import org.meshtastic.core.domain.usecase.settings.InstallProfileUseCase
|
||||
import org.meshtastic.core.repository.FileService
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.DeviceProfile
|
||||
import org.meshtastic.proto.User
|
||||
|
||||
/**
|
||||
* Encapsulates device-profile import/export/install operations.
|
||||
* Injected into [RadioConfigViewModel] to keep file I/O logic self-contained.
|
||||
*/
|
||||
class ProfileCoordinator(
|
||||
private val fileService: FileService,
|
||||
private val importProfileUseCase: ImportProfileUseCase,
|
||||
private val exportProfileUseCase: ExportProfileUseCase,
|
||||
private val exportSecurityConfigUseCase: ExportSecurityConfigUseCase,
|
||||
private val installProfileUseCase: InstallProfileUseCase,
|
||||
private val scope: CoroutineScope,
|
||||
) {
|
||||
fun importProfile(uri: CommonUri, onResult: (DeviceProfile) -> Unit) {
|
||||
scope.launch {
|
||||
try {
|
||||
var profile: DeviceProfile? = null
|
||||
fileService.read(uri) { source ->
|
||||
importProfileUseCase(source).onSuccess { profile = it }.onFailure { throw it }
|
||||
}
|
||||
profile?.let { onResult(it) }
|
||||
} catch (e: Exception) {
|
||||
Logger.e(e) { "[importProfile] failed" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun exportProfile(uri: CommonUri, profile: DeviceProfile) {
|
||||
scope.launch {
|
||||
try {
|
||||
fileService.write(uri) { sink ->
|
||||
exportProfileUseCase(sink, profile).onSuccess { /* Success */ }.onFailure { throw it }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.e(e) { "[exportProfile] failed" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun exportSecurityConfig(uri: CommonUri, securityConfig: Config.SecurityConfig) {
|
||||
scope.launch {
|
||||
try {
|
||||
fileService.write(uri) { sink ->
|
||||
exportSecurityConfigUseCase(sink, securityConfig).onSuccess { /* Success */ }.onFailure { throw it }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.e(e) { "[exportSecurityConfig] failed" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun installProfile(destNum: Int, protobuf: DeviceProfile, user: User?) {
|
||||
scope.launch {
|
||||
try {
|
||||
installProfileUseCase(destNum, protobuf, user)
|
||||
} catch (e: Exception) {
|
||||
Logger.e(e) { "[installProfile] failed" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,6 @@ import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
@@ -37,7 +36,6 @@ import org.jetbrains.compose.resources.StringResource
|
||||
import org.koin.core.annotation.InjectedParam
|
||||
import org.koin.core.annotation.KoinViewModel
|
||||
import org.meshtastic.core.common.util.CommonUri
|
||||
import org.meshtastic.core.common.util.safeCatching
|
||||
import org.meshtastic.core.domain.usecase.settings.AdminActionsUseCase
|
||||
import org.meshtastic.core.domain.usecase.settings.ExportProfileUseCase
|
||||
import org.meshtastic.core.domain.usecase.settings.ExportSecurityConfigUseCase
|
||||
@@ -142,39 +140,25 @@ open class RadioConfigViewModel(
|
||||
homoglyphEncodingPrefs.setHomoglyphEncodingEnabled(!homoglyphEncodingPrefs.homoglyphEncodingEnabled.value)
|
||||
}
|
||||
|
||||
/** MQTT proxy connection state for the settings UI. */
|
||||
val mqttConnectionState: StateFlow<MqttConnectionState> = mqttManager.mqttConnectionState
|
||||
private val mqttProbeCoordinator = MqttProbeCoordinator(mqttManager, viewModelScope)
|
||||
|
||||
private val _mqttProbeStatus = MutableStateFlow<MqttProbeStatus?>(null)
|
||||
/** MQTT proxy connection state for the settings UI. */
|
||||
val mqttConnectionState: StateFlow<MqttConnectionState> = mqttProbeCoordinator.mqttConnectionState
|
||||
|
||||
/** Latest result from a [probeMqttConnection] call, or `null` if no probe has been run. */
|
||||
val mqttProbeStatus: StateFlow<MqttProbeStatus?> = _mqttProbeStatus.asStateFlow()
|
||||
|
||||
private var probeJob: Job? = null
|
||||
val mqttProbeStatus: StateFlow<MqttProbeStatus?> = mqttProbeCoordinator.probeStatus
|
||||
|
||||
/**
|
||||
* Run a one-shot reachability/credentials probe against an MQTT broker. Cancels any in-flight probe before starting
|
||||
* a new one. Result is exposed via [mqttProbeStatus].
|
||||
*/
|
||||
fun probeMqttConnection(address: String, tlsEnabled: Boolean, username: String?, password: String?) {
|
||||
probeJob?.cancel()
|
||||
_mqttProbeStatus.value = MqttProbeStatus.Probing
|
||||
probeJob =
|
||||
viewModelScope.launch {
|
||||
val result =
|
||||
safeCatching { mqttManager.probe(address, tlsEnabled, username, password) }
|
||||
.getOrElse { e ->
|
||||
Logger.w(e) { "MQTT probe threw" }
|
||||
MqttProbeStatus.Other(message = e.message)
|
||||
}
|
||||
_mqttProbeStatus.value = result
|
||||
}
|
||||
mqttProbeCoordinator.probe(address, tlsEnabled, username, password)
|
||||
}
|
||||
|
||||
/** Clear the latest probe result (e.g. when the user edits the address). */
|
||||
fun clearMqttProbeStatus() {
|
||||
probeJob?.cancel()
|
||||
_mqttProbeStatus.value = null
|
||||
mqttProbeCoordinator.clearProbeStatus()
|
||||
}
|
||||
|
||||
private val destNumFlow = MutableStateFlow(savedStateHandle.get<Int>("destNum"))
|
||||
@@ -372,35 +356,26 @@ open class RadioConfigViewModel(
|
||||
writeAction("removeFixedPosition") { radioConfigUseCase.removeFixedPosition(destNum) }
|
||||
}
|
||||
|
||||
private val profileCoordinator = ProfileCoordinator(
|
||||
fileService, importProfileUseCase, exportProfileUseCase,
|
||||
exportSecurityConfigUseCase, installProfileUseCase, viewModelScope,
|
||||
)
|
||||
|
||||
fun importProfile(uri: CommonUri, onResult: (DeviceProfile) -> Unit) {
|
||||
safeLaunch(tag = "importProfile") {
|
||||
var profile: DeviceProfile? = null
|
||||
fileService.read(uri) { source ->
|
||||
importProfileUseCase(source).onSuccess { profile = it }.onFailure { throw it }
|
||||
}
|
||||
profile?.let { onResult(it) }
|
||||
}
|
||||
profileCoordinator.importProfile(uri, onResult)
|
||||
}
|
||||
|
||||
fun exportProfile(uri: CommonUri, profile: DeviceProfile) {
|
||||
safeLaunch(tag = "exportProfile") {
|
||||
fileService.write(uri) { sink ->
|
||||
exportProfileUseCase(sink, profile).onSuccess { /* Success */ }.onFailure { throw it }
|
||||
}
|
||||
}
|
||||
profileCoordinator.exportProfile(uri, profile)
|
||||
}
|
||||
|
||||
fun exportSecurityConfig(uri: CommonUri, securityConfig: Config.SecurityConfig) {
|
||||
safeLaunch(tag = "exportSecurityConfig") {
|
||||
fileService.write(uri) { sink ->
|
||||
exportSecurityConfigUseCase(sink, securityConfig).onSuccess { /* Success */ }.onFailure { throw it }
|
||||
}
|
||||
}
|
||||
profileCoordinator.exportSecurityConfig(uri, securityConfig)
|
||||
}
|
||||
|
||||
fun installProfile(protobuf: DeviceProfile) {
|
||||
val destNum = destNode.value?.num ?: return
|
||||
safeLaunch(tag = "installProfile") { installProfileUseCase(destNum, protobuf, destNode.value?.user) }
|
||||
profileCoordinator.installProfile(destNum, protobuf, destNode.value?.user)
|
||||
}
|
||||
|
||||
// region RadioConfigStateProvider implementation
|
||||
|
||||
@@ -43,6 +43,7 @@ import org.meshtastic.core.domain.usecase.settings.InstallProfileUseCase
|
||||
import org.meshtastic.core.domain.usecase.settings.RadioConfigUseCase
|
||||
import org.meshtastic.core.model.AdminException
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.model.ResponseState
|
||||
import org.meshtastic.core.repository.AnalyticsPrefs
|
||||
import org.meshtastic.core.repository.FileService
|
||||
import org.meshtastic.core.repository.HomoglyphPrefs
|
||||
|
||||
Reference in New Issue
Block a user