From 4a8cd6fb41e14bad5cfacb356c62e9ba81ef85f8 Mon Sep 17 00:00:00 2001 From: Phil Oliver <3497406+poliver@users.noreply.github.com> Date: Fri, 3 Oct 2025 06:12:40 -0400 Subject: [PATCH] Decouple `ScannedQrCodeDialog` from `UiViewModel` (#3300) --- .../java/com/geeksville/mesh/model/UIState.kt | 17 ---- .../main/java/com/geeksville/mesh/ui/Main.kt | 4 +- .../common/components/ScannedQrCodeDialog.kt | 10 ++- .../components/ScannedQrCodeViewModel.kt | 86 +++++++++++++++++++ .../com/geeksville/mesh/ui/sharing/Channel.kt | 5 ++ .../mesh/ui/sharing/ChannelViewModel.kt | 4 + 6 files changed, 105 insertions(+), 21 deletions(-) create mode 100644 app/src/main/java/com/geeksville/mesh/ui/common/components/ScannedQrCodeViewModel.kt diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index 3ceeb7d29..e15419249 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -347,23 +347,6 @@ constructor( } } - fun setChannel(channel: ChannelProtos.Channel) { - try { - meshService?.setChannel(channel.toByteArray()) - } catch (ex: RemoteException) { - Timber.e(ex, "Set channel error") - } - } - - /** Set the radio config (also updates our saved copy in preferences). */ - fun setChannels(channelSet: AppOnlyProtos.ChannelSet) = viewModelScope.launch { - getChannelList(channelSet.settingsList, channels.value.settingsList).forEach(::setChannel) - radioConfigRepository.replaceAllSettings(channelSet.settingsList) - - val newConfig = config { lora = channelSet.loraConfig } - if (config.lora != newConfig.lora) setConfig(newConfig) - } - fun addQuickChatAction(action: QuickChatAction) = viewModelScope.launch(Dispatchers.IO) { quickChatActionRepository.upsert(action) } 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 0f79c842d..30fe3b5a7 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Main.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Main.kt @@ -150,7 +150,9 @@ fun MainScreen(uIViewModel: UIViewModel = hiltViewModel(), scanModel: BTScanMode } if (connectionState == ConnectionState.CONNECTED) { - requestChannelSet?.let { newChannelSet -> ScannedQrCodeDialog(uIViewModel, newChannelSet) } + requestChannelSet?.let { newChannelSet -> + ScannedQrCodeDialog(newChannelSet, onDismiss = { uIViewModel.clearRequestChannelUrl() }) + } } analytics.addNavigationTrackingEffect(navController = navController) diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/components/ScannedQrCodeDialog.kt b/app/src/main/java/com/geeksville/mesh/ui/common/components/ScannedQrCodeDialog.kt index b600f1021..591d42435 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/common/components/ScannedQrCodeDialog.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/common/components/ScannedQrCodeDialog.kt @@ -49,24 +49,28 @@ import androidx.compose.ui.tooling.preview.PreviewScreenSizes import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.geeksville.mesh.AppOnlyProtos.ChannelSet import com.geeksville.mesh.ConfigProtos.Config.LoRaConfig.ModemPreset import com.geeksville.mesh.channelSet import com.geeksville.mesh.copy -import com.geeksville.mesh.model.UIViewModel import com.geeksville.mesh.ui.settings.radio.components.ChannelSelection import org.meshtastic.core.model.Channel import org.meshtastic.core.strings.R @Composable -fun ScannedQrCodeDialog(viewModel: UIViewModel, incoming: ChannelSet) { +fun ScannedQrCodeDialog( + incoming: ChannelSet, + onDismiss: () -> Unit, + viewModel: ScannedQrCodeViewModel = hiltViewModel(), +) { val channels by viewModel.channels.collectAsStateWithLifecycle() ScannedQrCodeDialog( channels = channels, incoming = incoming, - onDismiss = viewModel::clearRequestChannelUrl, + onDismiss = onDismiss, onConfirm = viewModel::setChannels, ) } diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/components/ScannedQrCodeViewModel.kt b/app/src/main/java/com/geeksville/mesh/ui/common/components/ScannedQrCodeViewModel.kt new file mode 100644 index 000000000..38b54cb8f --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/common/components/ScannedQrCodeViewModel.kt @@ -0,0 +1,86 @@ +/* + * 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.ui.common.components + +import android.os.RemoteException +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.geeksville.mesh.AppOnlyProtos +import com.geeksville.mesh.ChannelProtos +import com.geeksville.mesh.ConfigProtos.Config +import com.geeksville.mesh.LocalOnlyProtos.LocalConfig +import com.geeksville.mesh.channelSet +import com.geeksville.mesh.config +import com.geeksville.mesh.model.getChannelList +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import org.meshtastic.core.data.repository.RadioConfigRepository +import org.meshtastic.core.service.ServiceRepository +import timber.log.Timber +import javax.inject.Inject + +@HiltViewModel +class ScannedQrCodeViewModel +@Inject +constructor( + private val radioConfigRepository: RadioConfigRepository, + private val serviceRepository: ServiceRepository, +) : ViewModel() { + + val channels = + radioConfigRepository.channelSetFlow.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000L), + channelSet {}, + ) + + private val localConfig = + radioConfigRepository.localConfigFlow.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000L), + LocalConfig.getDefaultInstance(), + ) + + /** Set the radio config (also updates our saved copy in preferences). */ + fun setChannels(channelSet: AppOnlyProtos.ChannelSet) = viewModelScope.launch { + getChannelList(channelSet.settingsList, channels.value.settingsList).forEach(::setChannel) + radioConfigRepository.replaceAllSettings(channelSet.settingsList) + + val newConfig = config { lora = channelSet.loraConfig } + if (localConfig.value.lora != newConfig.lora) setConfig(newConfig) + } + + private fun setChannel(channel: ChannelProtos.Channel) { + try { + serviceRepository.meshService?.setChannel(channel.toByteArray()) + } catch (ex: RemoteException) { + Timber.e(ex, "Set channel error") + } + } + + // Set the radio config (also updates our saved copy in preferences) + private fun setConfig(config: Config) { + try { + serviceRepository.meshService?.setConfig(config.toByteArray()) + } catch (ex: RemoteException) { + Timber.e(ex, "Set config error") + } + } +} diff --git a/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt b/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt index 38d2f08e6..b7e2d0975 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt @@ -96,6 +96,7 @@ import com.geeksville.mesh.channelSet import com.geeksville.mesh.copy import com.geeksville.mesh.navigation.ConfigRoute import com.geeksville.mesh.navigation.getNavRouteFrom +import com.geeksville.mesh.ui.common.components.ScannedQrCodeDialog import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel import com.geeksville.mesh.ui.settings.radio.components.ChannelSelection import com.geeksville.mesh.ui.settings.radio.components.PacketResponseStateDialog @@ -144,6 +145,8 @@ fun ChannelScreen( var shouldAddChannelsState by remember { mutableStateOf(true) } + val requestChannelSet by viewModel.requestChannelSet.collectAsStateWithLifecycle() + /* Animate waiting for the configurations */ var isWaiting by remember { mutableStateOf(false) } if (isWaiting) { @@ -269,6 +272,8 @@ fun ChannelScreen( ) } + requestChannelSet?.let { ScannedQrCodeDialog(it, onDismiss = { viewModel.clearRequestChannelUrl() }) } + val listState = rememberLazyListState() LazyColumn(state = listState, contentPadding = PaddingValues(horizontal = 24.dp, vertical = 16.dp)) { item { 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 01f24b7d2..4b05764f1 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 @@ -91,6 +91,10 @@ constructor( onError() } + fun clearRequestChannelUrl() { + _requestChannelSet.value = null + } + /** Set the radio config (also updates our saved copy in preferences). */ fun setChannels(channelSet: AppOnlyProtos.ChannelSet) = viewModelScope.launch { getChannelList(channelSet.settingsList, channels.value.settingsList).forEach(::setChannel)