mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
refactor(feature/settings): Extract Settings and RadioConfig ViewModels to commonMain
This commit is contained in:
parent
3c872b2d01
commit
091452a559
9 changed files with 72 additions and 296 deletions
|
|
@ -27,8 +27,8 @@ import androidx.navigation3.runtime.NavBackStack
|
||||||
import androidx.navigation3.runtime.NavKey
|
import androidx.navigation3.runtime.NavKey
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
import org.meshtastic.app.settings.AndroidDebugViewModel
|
import org.meshtastic.app.settings.AndroidDebugViewModel
|
||||||
import org.meshtastic.app.settings.AndroidRadioConfigViewModel
|
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||||
import org.meshtastic.app.settings.AndroidSettingsViewModel
|
import org.meshtastic.feature.settings.SettingsViewModel
|
||||||
import org.meshtastic.core.navigation.NodesRoutes
|
import org.meshtastic.core.navigation.NodesRoutes
|
||||||
import org.meshtastic.core.navigation.Route
|
import org.meshtastic.core.navigation.Route
|
||||||
import org.meshtastic.core.navigation.SettingsRoutes
|
import org.meshtastic.core.navigation.SettingsRoutes
|
||||||
|
|
@ -74,8 +74,8 @@ import kotlin.reflect.KClass
|
||||||
|
|
||||||
@PublishedApi
|
@PublishedApi
|
||||||
@Composable
|
@Composable
|
||||||
internal fun getRadioConfigViewModel(backStack: NavBackStack<NavKey>): AndroidRadioConfigViewModel {
|
internal fun getRadioConfigViewModel(backStack: NavBackStack<NavKey>): RadioConfigViewModel {
|
||||||
val viewModel = koinViewModel<AndroidRadioConfigViewModel>()
|
val viewModel = koinViewModel<RadioConfigViewModel>()
|
||||||
LaunchedEffect(backStack) {
|
LaunchedEffect(backStack) {
|
||||||
val destNum =
|
val destNum =
|
||||||
backStack.lastOrNull { it is SettingsRoutes.Settings }?.let { (it as SettingsRoutes.Settings).destNum }
|
backStack.lastOrNull { it is SettingsRoutes.Settings }?.let { (it as SettingsRoutes.Settings).destNum }
|
||||||
|
|
@ -91,7 +91,7 @@ internal fun getRadioConfigViewModel(backStack: NavBackStack<NavKey>): AndroidRa
|
||||||
fun EntryProviderScope<NavKey>.settingsGraph(backStack: NavBackStack<NavKey>) {
|
fun EntryProviderScope<NavKey>.settingsGraph(backStack: NavBackStack<NavKey>) {
|
||||||
entry<SettingsRoutes.SettingsGraph> {
|
entry<SettingsRoutes.SettingsGraph> {
|
||||||
SettingsScreen(
|
SettingsScreen(
|
||||||
settingsViewModel = koinViewModel<AndroidSettingsViewModel>(),
|
settingsViewModel = koinViewModel<SettingsViewModel>(),
|
||||||
viewModel = getRadioConfigViewModel(backStack),
|
viewModel = getRadioConfigViewModel(backStack),
|
||||||
onClickNodeChip = { backStack.add(NodesRoutes.NodeDetailGraph(it)) },
|
onClickNodeChip = { backStack.add(NodesRoutes.NodeDetailGraph(it)) },
|
||||||
) {
|
) {
|
||||||
|
|
@ -101,7 +101,7 @@ fun EntryProviderScope<NavKey>.settingsGraph(backStack: NavBackStack<NavKey>) {
|
||||||
|
|
||||||
entry<SettingsRoutes.Settings> {
|
entry<SettingsRoutes.Settings> {
|
||||||
SettingsScreen(
|
SettingsScreen(
|
||||||
settingsViewModel = koinViewModel<AndroidSettingsViewModel>(),
|
settingsViewModel = koinViewModel<SettingsViewModel>(),
|
||||||
viewModel = getRadioConfigViewModel(backStack),
|
viewModel = getRadioConfigViewModel(backStack),
|
||||||
onClickNodeChip = { backStack.add(NodesRoutes.NodeDetailGraph(it)) },
|
onClickNodeChip = { backStack.add(NodesRoutes.NodeDetailGraph(it)) },
|
||||||
) {
|
) {
|
||||||
|
|
@ -118,7 +118,7 @@ fun EntryProviderScope<NavKey>.settingsGraph(backStack: NavBackStack<NavKey>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
entry<SettingsRoutes.ModuleConfiguration> {
|
entry<SettingsRoutes.ModuleConfiguration> {
|
||||||
val settingsViewModel: AndroidSettingsViewModel = koinViewModel()
|
val settingsViewModel: SettingsViewModel = koinViewModel()
|
||||||
val excludedModulesUnlocked by settingsViewModel.excludedModulesUnlocked.collectAsStateWithLifecycle()
|
val excludedModulesUnlocked by settingsViewModel.excludedModulesUnlocked.collectAsStateWithLifecycle()
|
||||||
ModuleConfigurationScreen(
|
ModuleConfigurationScreen(
|
||||||
viewModel = getRadioConfigViewModel(backStack),
|
viewModel = getRadioConfigViewModel(backStack),
|
||||||
|
|
@ -209,14 +209,14 @@ fun EntryProviderScope<NavKey>.settingsGraph(backStack: NavBackStack<NavKey>) {
|
||||||
fun <R : Route> EntryProviderScope<NavKey>.configComposable(
|
fun <R : Route> EntryProviderScope<NavKey>.configComposable(
|
||||||
route: KClass<R>,
|
route: KClass<R>,
|
||||||
backStack: NavBackStack<NavKey>,
|
backStack: NavBackStack<NavKey>,
|
||||||
content: @Composable (AndroidRadioConfigViewModel) -> Unit,
|
content: @Composable (RadioConfigViewModel) -> Unit,
|
||||||
) {
|
) {
|
||||||
addEntryProvider(route) { content(getRadioConfigViewModel(backStack)) }
|
addEntryProvider(route) { content(getRadioConfigViewModel(backStack)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <reified R : Route> EntryProviderScope<NavKey>.configComposable(
|
inline fun <reified R : Route> EntryProviderScope<NavKey>.configComposable(
|
||||||
backStack: NavBackStack<NavKey>,
|
backStack: NavBackStack<NavKey>,
|
||||||
noinline content: @Composable (AndroidRadioConfigViewModel) -> Unit,
|
noinline content: @Composable (RadioConfigViewModel) -> Unit,
|
||||||
) {
|
) {
|
||||||
entry<R> { content(getRadioConfigViewModel(backStack)) }
|
entry<R> { content(getRadioConfigViewModel(backStack)) }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,164 +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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
package org.meshtastic.app.settings
|
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.location.Location
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.annotation.RequiresPermission
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import co.touchlab.kermit.Logger
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import okio.buffer
|
|
||||||
import okio.sink
|
|
||||||
import okio.source
|
|
||||||
import org.koin.core.annotation.KoinViewModel
|
|
||||||
import org.meshtastic.core.domain.usecase.settings.AdminActionsUseCase
|
|
||||||
import org.meshtastic.core.domain.usecase.settings.ExportProfileUseCase
|
|
||||||
import org.meshtastic.core.domain.usecase.settings.ExportSecurityConfigUseCase
|
|
||||||
import org.meshtastic.core.domain.usecase.settings.ImportProfileUseCase
|
|
||||||
import org.meshtastic.core.domain.usecase.settings.InstallProfileUseCase
|
|
||||||
import org.meshtastic.core.domain.usecase.settings.ProcessRadioResponseUseCase
|
|
||||||
import org.meshtastic.core.domain.usecase.settings.RadioConfigUseCase
|
|
||||||
import org.meshtastic.core.domain.usecase.settings.ToggleAnalyticsUseCase
|
|
||||||
import org.meshtastic.core.domain.usecase.settings.ToggleHomoglyphEncodingUseCase
|
|
||||||
import org.meshtastic.core.repository.AnalyticsPrefs
|
|
||||||
import org.meshtastic.core.repository.HomoglyphPrefs
|
|
||||||
import org.meshtastic.core.repository.LocationRepository
|
|
||||||
import org.meshtastic.core.repository.MapConsentPrefs
|
|
||||||
import org.meshtastic.core.repository.NodeRepository
|
|
||||||
import org.meshtastic.core.repository.PacketRepository
|
|
||||||
import org.meshtastic.core.repository.RadioConfigRepository
|
|
||||||
import org.meshtastic.core.repository.ServiceRepository
|
|
||||||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
|
||||||
import org.meshtastic.proto.Config
|
|
||||||
import org.meshtastic.proto.DeviceProfile
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
|
|
||||||
@KoinViewModel
|
|
||||||
class AndroidRadioConfigViewModel(
|
|
||||||
savedStateHandle: SavedStateHandle,
|
|
||||||
private val app: Application,
|
|
||||||
radioConfigRepository: RadioConfigRepository,
|
|
||||||
packetRepository: PacketRepository,
|
|
||||||
serviceRepository: ServiceRepository,
|
|
||||||
nodeRepository: NodeRepository,
|
|
||||||
private val locationRepository: LocationRepository,
|
|
||||||
mapConsentPrefs: MapConsentPrefs,
|
|
||||||
analyticsPrefs: AnalyticsPrefs,
|
|
||||||
homoglyphEncodingPrefs: HomoglyphPrefs,
|
|
||||||
toggleAnalyticsUseCase: ToggleAnalyticsUseCase,
|
|
||||||
toggleHomoglyphEncodingUseCase: ToggleHomoglyphEncodingUseCase,
|
|
||||||
importProfileUseCase: ImportProfileUseCase,
|
|
||||||
exportProfileUseCase: ExportProfileUseCase,
|
|
||||||
exportSecurityConfigUseCase: ExportSecurityConfigUseCase,
|
|
||||||
installProfileUseCase: InstallProfileUseCase,
|
|
||||||
radioConfigUseCase: RadioConfigUseCase,
|
|
||||||
adminActionsUseCase: AdminActionsUseCase,
|
|
||||||
processRadioResponseUseCase: ProcessRadioResponseUseCase,
|
|
||||||
) : RadioConfigViewModel(
|
|
||||||
savedStateHandle,
|
|
||||||
radioConfigRepository,
|
|
||||||
packetRepository,
|
|
||||||
serviceRepository,
|
|
||||||
nodeRepository,
|
|
||||||
locationRepository,
|
|
||||||
mapConsentPrefs,
|
|
||||||
analyticsPrefs,
|
|
||||||
homoglyphEncodingPrefs,
|
|
||||||
toggleAnalyticsUseCase,
|
|
||||||
toggleHomoglyphEncodingUseCase,
|
|
||||||
importProfileUseCase,
|
|
||||||
exportProfileUseCase,
|
|
||||||
exportSecurityConfigUseCase,
|
|
||||||
installProfileUseCase,
|
|
||||||
radioConfigUseCase,
|
|
||||||
adminActionsUseCase,
|
|
||||||
processRadioResponseUseCase,
|
|
||||||
) {
|
|
||||||
@RequiresPermission(Manifest.permission.ACCESS_FINE_LOCATION)
|
|
||||||
override suspend fun getCurrentLocation(): Location? = if (
|
|
||||||
ContextCompat.checkSelfPermission(app, Manifest.permission.ACCESS_FINE_LOCATION) ==
|
|
||||||
PackageManager.PERMISSION_GRANTED
|
|
||||||
) {
|
|
||||||
locationRepository.getLocations().firstOrNull()
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun importProfile(uri: Any, onResult: (DeviceProfile) -> Unit) {
|
|
||||||
if (uri is Uri) {
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
app.contentResolver.openInputStream(uri)?.source()?.buffer()?.use { inputStream ->
|
|
||||||
importProfileUseCase(inputStream).onSuccess(onResult).onFailure { throw it }
|
|
||||||
}
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
Logger.e { "Import DeviceProfile error: ${ex.message}" }
|
|
||||||
// Error handling simplified for this example
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun exportProfile(uri: Any, profile: DeviceProfile) {
|
|
||||||
if (uri is Uri) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
app.contentResolver.openFileDescriptor(uri, "wt")?.use { parcelFileDescriptor ->
|
|
||||||
FileOutputStream(parcelFileDescriptor.fileDescriptor).sink().buffer().use { outputStream ->
|
|
||||||
exportProfileUseCase(outputStream, profile)
|
|
||||||
.onSuccess { /* Success */ }
|
|
||||||
.onFailure { throw it }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
Logger.e { "Can't write file error: ${ex.message}" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun exportSecurityConfig(uri: Any, securityConfig: Config.SecurityConfig) {
|
|
||||||
if (uri is Uri) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
app.contentResolver.openFileDescriptor(uri, "wt")?.use { parcelFileDescriptor ->
|
|
||||||
FileOutputStream(parcelFileDescriptor.fileDescriptor).sink().buffer().use { outputStream ->
|
|
||||||
exportSecurityConfigUseCase(outputStream, securityConfig)
|
|
||||||
.onSuccess { /* Success */ }
|
|
||||||
.onFailure { throw it }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
Logger.e { "Can't write security keys JSON error: ${ex.message}" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,107 +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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
package org.meshtastic.app.settings
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import co.touchlab.kermit.Logger
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import okio.BufferedSink
|
|
||||||
import okio.buffer
|
|
||||||
import okio.sink
|
|
||||||
import org.koin.core.annotation.KoinViewModel
|
|
||||||
import org.meshtastic.core.common.BuildConfigProvider
|
|
||||||
import org.meshtastic.core.common.database.DatabaseManager
|
|
||||||
import org.meshtastic.core.domain.usecase.settings.ExportDataUseCase
|
|
||||||
import org.meshtastic.core.domain.usecase.settings.IsOtaCapableUseCase
|
|
||||||
import org.meshtastic.core.domain.usecase.settings.MeshLocationUseCase
|
|
||||||
import org.meshtastic.core.domain.usecase.settings.SetAppIntroCompletedUseCase
|
|
||||||
import org.meshtastic.core.domain.usecase.settings.SetDatabaseCacheLimitUseCase
|
|
||||||
import org.meshtastic.core.domain.usecase.settings.SetLocaleUseCase
|
|
||||||
import org.meshtastic.core.domain.usecase.settings.SetMeshLogSettingsUseCase
|
|
||||||
import org.meshtastic.core.domain.usecase.settings.SetProvideLocationUseCase
|
|
||||||
import org.meshtastic.core.domain.usecase.settings.SetThemeUseCase
|
|
||||||
import org.meshtastic.core.model.RadioController
|
|
||||||
import org.meshtastic.core.repository.MeshLogPrefs
|
|
||||||
import org.meshtastic.core.repository.NodeRepository
|
|
||||||
import org.meshtastic.core.repository.RadioConfigRepository
|
|
||||||
import org.meshtastic.core.repository.UiPrefs
|
|
||||||
import org.meshtastic.feature.settings.SettingsViewModel
|
|
||||||
import java.io.FileNotFoundException
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
|
|
||||||
@KoinViewModel
|
|
||||||
@Suppress("LongParameterList")
|
|
||||||
class AndroidSettingsViewModel(
|
|
||||||
private val app: Application,
|
|
||||||
radioConfigRepository: RadioConfigRepository,
|
|
||||||
radioController: RadioController,
|
|
||||||
nodeRepository: NodeRepository,
|
|
||||||
uiPrefs: UiPrefs,
|
|
||||||
buildConfigProvider: BuildConfigProvider,
|
|
||||||
databaseManager: DatabaseManager,
|
|
||||||
meshLogPrefs: MeshLogPrefs,
|
|
||||||
setThemeUseCase: SetThemeUseCase,
|
|
||||||
setLocaleUseCase: SetLocaleUseCase,
|
|
||||||
setAppIntroCompletedUseCase: SetAppIntroCompletedUseCase,
|
|
||||||
setProvideLocationUseCase: SetProvideLocationUseCase,
|
|
||||||
setDatabaseCacheLimitUseCase: SetDatabaseCacheLimitUseCase,
|
|
||||||
setMeshLogSettingsUseCase: SetMeshLogSettingsUseCase,
|
|
||||||
meshLocationUseCase: MeshLocationUseCase,
|
|
||||||
exportDataUseCase: ExportDataUseCase,
|
|
||||||
isOtaCapableUseCase: IsOtaCapableUseCase,
|
|
||||||
) : SettingsViewModel(
|
|
||||||
radioConfigRepository,
|
|
||||||
radioController,
|
|
||||||
nodeRepository,
|
|
||||||
uiPrefs,
|
|
||||||
buildConfigProvider,
|
|
||||||
databaseManager,
|
|
||||||
meshLogPrefs,
|
|
||||||
setThemeUseCase,
|
|
||||||
setLocaleUseCase,
|
|
||||||
setAppIntroCompletedUseCase,
|
|
||||||
setProvideLocationUseCase,
|
|
||||||
setDatabaseCacheLimitUseCase,
|
|
||||||
setMeshLogSettingsUseCase,
|
|
||||||
meshLocationUseCase,
|
|
||||||
exportDataUseCase,
|
|
||||||
isOtaCapableUseCase,
|
|
||||||
) {
|
|
||||||
override fun saveDataCsv(uri: Any, filterPortnum: Int?) {
|
|
||||||
if (uri is Uri) {
|
|
||||||
viewModelScope.launch { writeToUri(uri) { writer -> performDataExport(writer, filterPortnum) } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend inline fun writeToUri(uri: Uri, crossinline block: suspend (BufferedSink) -> Unit) {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
app.contentResolver.openFileDescriptor(uri, "wt")?.use { parcelFileDescriptor ->
|
|
||||||
FileOutputStream(parcelFileDescriptor.fileDescriptor).sink().buffer().use { writer ->
|
|
||||||
block.invoke(writer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (ex: FileNotFoundException) {
|
|
||||||
Logger.e { "Can't write file error: ${ex.message}" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -16,6 +16,8 @@
|
||||||
*/
|
*/
|
||||||
package org.meshtastic.feature.settings
|
package org.meshtastic.feature.settings
|
||||||
|
|
||||||
|
import org.meshtastic.core.common.util.toMeshtasticUri
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
|
@ -97,14 +99,14 @@ fun SettingsScreen(
|
||||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
if (it.resultCode == Activity.RESULT_OK) {
|
if (it.resultCode == Activity.RESULT_OK) {
|
||||||
showEditDeviceProfileDialog = true
|
showEditDeviceProfileDialog = true
|
||||||
it.data?.data?.let { uri -> viewModel.importProfile(uri) { profile -> deviceProfile = profile } }
|
it.data?.data?.let { uri -> viewModel.importProfile(uri.toMeshtasticUri()) { profile -> deviceProfile = profile } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val exportConfigLauncher =
|
val exportConfigLauncher =
|
||||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
if (it.resultCode == Activity.RESULT_OK) {
|
if (it.resultCode == Activity.RESULT_OK) {
|
||||||
it.data?.data?.let { uri -> viewModel.exportProfile(uri, deviceProfile!!) }
|
it.data?.data?.let { uri -> viewModel.exportProfile(uri.toMeshtasticUri(), deviceProfile!!) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -234,7 +236,7 @@ fun SettingsScreen(
|
||||||
cacheLimit = settingsViewModel.dbCacheLimit.collectAsStateWithLifecycle().value,
|
cacheLimit = settingsViewModel.dbCacheLimit.collectAsStateWithLifecycle().value,
|
||||||
onSetCacheLimit = { settingsViewModel.setDbCacheLimit(it) },
|
onSetCacheLimit = { settingsViewModel.setDbCacheLimit(it) },
|
||||||
nodeShortName = ourNode?.user?.short_name ?: "",
|
nodeShortName = ourNode?.user?.short_name ?: "",
|
||||||
onExportData = { settingsViewModel.saveDataCsv(it) },
|
onExportData = { settingsViewModel.saveDataCsv(it.toMeshtasticUri()) },
|
||||||
)
|
)
|
||||||
|
|
||||||
AppInfoSection(
|
AppInfoSection(
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,9 @@
|
||||||
*/
|
*/
|
||||||
package org.meshtastic.feature.settings.radio.component
|
package org.meshtastic.feature.settings.radio.component
|
||||||
|
|
||||||
|
import org.meshtastic.core.common.util.toMeshtasticUri
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
|
@ -94,7 +96,7 @@ fun SecurityConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||||
val exportConfigLauncher =
|
val exportConfigLauncher =
|
||||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
if (it.resultCode == Activity.RESULT_OK) {
|
if (it.resultCode == Activity.RESULT_OK) {
|
||||||
it.data?.data?.let { uri -> viewModel.exportSecurityConfig(uri, securityConfig) }
|
it.data?.data?.let { uri -> viewModel.exportSecurityConfig(uri.toMeshtasticUri(), securityConfig) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,11 +47,13 @@ import org.meshtastic.core.repository.NodeRepository
|
||||||
import org.meshtastic.core.repository.RadioConfigRepository
|
import org.meshtastic.core.repository.RadioConfigRepository
|
||||||
import org.meshtastic.core.repository.UiPrefs
|
import org.meshtastic.core.repository.UiPrefs
|
||||||
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
|
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
|
||||||
|
import org.meshtastic.core.repository.FileService
|
||||||
|
import org.meshtastic.core.common.util.MeshtasticUri
|
||||||
import org.meshtastic.proto.LocalConfig
|
import org.meshtastic.proto.LocalConfig
|
||||||
|
|
||||||
@KoinViewModel
|
@KoinViewModel
|
||||||
@Suppress("LongParameterList", "TooManyFunctions")
|
@Suppress("LongParameterList", "TooManyFunctions")
|
||||||
open class SettingsViewModel(
|
class SettingsViewModel(
|
||||||
radioConfigRepository: RadioConfigRepository,
|
radioConfigRepository: RadioConfigRepository,
|
||||||
private val radioController: RadioController,
|
private val radioController: RadioController,
|
||||||
private val nodeRepository: NodeRepository,
|
private val nodeRepository: NodeRepository,
|
||||||
|
|
@ -68,6 +70,7 @@ open class SettingsViewModel(
|
||||||
private val meshLocationUseCase: MeshLocationUseCase,
|
private val meshLocationUseCase: MeshLocationUseCase,
|
||||||
private val exportDataUseCase: ExportDataUseCase,
|
private val exportDataUseCase: ExportDataUseCase,
|
||||||
private val isOtaCapableUseCase: IsOtaCapableUseCase,
|
private val isOtaCapableUseCase: IsOtaCapableUseCase,
|
||||||
|
private val fileService: FileService,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val myNodeInfo: StateFlow<MyNodeInfo?> = nodeRepository.myNodeInfo
|
val myNodeInfo: StateFlow<MyNodeInfo?> = nodeRepository.myNodeInfo
|
||||||
|
|
||||||
|
|
@ -161,11 +164,13 @@ open class SettingsViewModel(
|
||||||
* @param uri The destination URI for the CSV file.
|
* @param uri The destination URI for the CSV file.
|
||||||
* @param filterPortnum If provided, only packets with this port number will be exported.
|
* @param filterPortnum If provided, only packets with this port number will be exported.
|
||||||
*/
|
*/
|
||||||
open fun saveDataCsv(uri: Any, filterPortnum: Int? = null) {
|
fun saveDataCsv(uri: MeshtasticUri, filterPortnum: Int? = null) {
|
||||||
// To be implemented in platform-specific subclass
|
viewModelScope.launch {
|
||||||
|
fileService.write(uri) { writer -> performDataExport(writer, filterPortnum) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected suspend fun performDataExport(writer: BufferedSink, filterPortnum: Int?) {
|
private suspend fun performDataExport(writer: BufferedSink, filterPortnum: Int?) {
|
||||||
val myNodeNum = myNodeNum ?: return
|
val myNodeNum = myNodeNum ?: return
|
||||||
exportDataUseCase(writer, myNodeNum, filterPortnum)
|
exportDataUseCase(writer, myNodeNum, filterPortnum)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,10 @@ import org.meshtastic.proto.MeshPacket
|
||||||
import org.meshtastic.proto.ModuleConfig
|
import org.meshtastic.proto.ModuleConfig
|
||||||
import org.meshtastic.proto.User
|
import org.meshtastic.proto.User
|
||||||
|
|
||||||
|
import org.meshtastic.core.repository.FileService
|
||||||
|
import org.meshtastic.core.repository.LocationService
|
||||||
|
import org.meshtastic.core.common.util.MeshtasticUri
|
||||||
|
|
||||||
/** Data class that represents the current RadioConfig state. */
|
/** Data class that represents the current RadioConfig state. */
|
||||||
data class RadioConfigState(
|
data class RadioConfigState(
|
||||||
val isLocal: Boolean = false,
|
val isLocal: Boolean = false,
|
||||||
|
|
@ -113,6 +117,8 @@ open class RadioConfigViewModel(
|
||||||
private val radioConfigUseCase: RadioConfigUseCase,
|
private val radioConfigUseCase: RadioConfigUseCase,
|
||||||
private val adminActionsUseCase: AdminActionsUseCase,
|
private val adminActionsUseCase: AdminActionsUseCase,
|
||||||
private val processRadioResponseUseCase: ProcessRadioResponseUseCase,
|
private val processRadioResponseUseCase: ProcessRadioResponseUseCase,
|
||||||
|
private val locationService: LocationService,
|
||||||
|
private val fileService: FileService,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
var analyticsAllowedFlow = analyticsPrefs.analyticsAllowed
|
var analyticsAllowedFlow = analyticsPrefs.analyticsAllowed
|
||||||
|
|
||||||
|
|
@ -150,7 +156,7 @@ open class RadioConfigViewModel(
|
||||||
val currentDeviceProfile
|
val currentDeviceProfile
|
||||||
get() = _currentDeviceProfile.value
|
get() = _currentDeviceProfile.value
|
||||||
|
|
||||||
open suspend fun getCurrentLocation(): Any? = null
|
suspend fun getCurrentLocation(): Any? = locationService.getCurrentLocation()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
combine(destNumFlow, nodeRepository.nodeDBbyNum) { id, nodes -> nodes[id] ?: nodes.values.firstOrNull() }
|
combine(destNumFlow, nodeRepository.nodeDBbyNum) { id, nodes -> nodes[id] ?: nodes.values.firstOrNull() }
|
||||||
|
|
@ -363,16 +369,44 @@ open class RadioConfigViewModel(
|
||||||
viewModelScope.launch { radioConfigUseCase.removeFixedPosition(destNum) }
|
viewModelScope.launch { radioConfigUseCase.removeFixedPosition(destNum) }
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun importProfile(uri: Any, onResult: (DeviceProfile) -> Unit) {
|
fun importProfile(uri: MeshtasticUri, onResult: (DeviceProfile) -> Unit) {
|
||||||
// To be implemented in platform-specific subclass
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
fileService.read(uri) { source ->
|
||||||
|
importProfileUseCase(source).onSuccess(onResult).onFailure { throw it }
|
||||||
|
}
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Logger.e { "Import DeviceProfile error: ${ex.message}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun exportProfile(uri: Any, profile: DeviceProfile) {
|
fun exportProfile(uri: MeshtasticUri, profile: DeviceProfile) {
|
||||||
// To be implemented in platform-specific subclass
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
fileService.write(uri) { sink ->
|
||||||
|
exportProfileUseCase(sink, profile)
|
||||||
|
.onSuccess { /* Success */ }
|
||||||
|
.onFailure { throw it }
|
||||||
|
}
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Logger.e { "Can't write file error: ${ex.message}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun exportSecurityConfig(uri: Any, securityConfig: Config.SecurityConfig) {
|
fun exportSecurityConfig(uri: MeshtasticUri, securityConfig: Config.SecurityConfig) {
|
||||||
// To be implemented in platform-specific subclass
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
fileService.write(uri) { sink ->
|
||||||
|
exportSecurityConfigUseCase(sink, securityConfig)
|
||||||
|
.onSuccess { /* Success */ }
|
||||||
|
.onFailure { throw it }
|
||||||
|
}
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Logger.e { "Can't write security keys JSON error: ${ex.message}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun installProfile(protobuf: DeviceProfile) {
|
fun installProfile(protobuf: DeviceProfile) {
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,7 @@ class SettingsViewModelTest {
|
||||||
meshLocationUseCase = mockk(relaxed = true),
|
meshLocationUseCase = mockk(relaxed = true),
|
||||||
exportDataUseCase = mockk(relaxed = true),
|
exportDataUseCase = mockk(relaxed = true),
|
||||||
isOtaCapableUseCase = mockk(relaxed = true),
|
isOtaCapableUseCase = mockk(relaxed = true),
|
||||||
|
fileService = mockk(relaxed = true),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,8 @@ class RadioConfigViewModelTest {
|
||||||
private val radioConfigUseCase: RadioConfigUseCase = mockk(relaxed = true)
|
private val radioConfigUseCase: RadioConfigUseCase = mockk(relaxed = true)
|
||||||
private val adminActionsUseCase: AdminActionsUseCase = mockk(relaxed = true)
|
private val adminActionsUseCase: AdminActionsUseCase = mockk(relaxed = true)
|
||||||
private val processRadioResponseUseCase: ProcessRadioResponseUseCase = mockk(relaxed = true)
|
private val processRadioResponseUseCase: ProcessRadioResponseUseCase = mockk(relaxed = true)
|
||||||
|
private val locationService: org.meshtastic.core.repository.LocationService = mockk(relaxed = true)
|
||||||
|
private val fileService: org.meshtastic.core.repository.FileService = mockk(relaxed = true)
|
||||||
|
|
||||||
private lateinit var viewModel: RadioConfigViewModel
|
private lateinit var viewModel: RadioConfigViewModel
|
||||||
|
|
||||||
|
|
@ -110,7 +112,6 @@ class RadioConfigViewModelTest {
|
||||||
|
|
||||||
private fun createViewModel() = RadioConfigViewModel(
|
private fun createViewModel() = RadioConfigViewModel(
|
||||||
savedStateHandle = SavedStateHandle(),
|
savedStateHandle = SavedStateHandle(),
|
||||||
app = mockk(),
|
|
||||||
radioConfigRepository = radioConfigRepository,
|
radioConfigRepository = radioConfigRepository,
|
||||||
packetRepository = packetRepository,
|
packetRepository = packetRepository,
|
||||||
serviceRepository = serviceRepository,
|
serviceRepository = serviceRepository,
|
||||||
|
|
@ -128,6 +129,8 @@ class RadioConfigViewModelTest {
|
||||||
radioConfigUseCase = radioConfigUseCase,
|
radioConfigUseCase = radioConfigUseCase,
|
||||||
adminActionsUseCase = adminActionsUseCase,
|
adminActionsUseCase = adminActionsUseCase,
|
||||||
processRadioResponseUseCase = processRadioResponseUseCase,
|
processRadioResponseUseCase = processRadioResponseUseCase,
|
||||||
|
locationService = locationService,
|
||||||
|
fileService = fileService,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue