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 3a90d043a..fc664ec50 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -2321,9 +2321,12 @@ class MeshService : Service() { ) } - override fun requestNodedbReset(requestId: Int, destNum: Int) = toRemoteExceptions { - packetHandler.sendToRadio(newMeshPacketTo(destNum).buildAdminPacket(id = requestId) { nodedbReset = 1 }) - } + override fun requestNodedbReset(requestId: Int, destNum: Int, preserveFavorites: Boolean) = + toRemoteExceptions { + packetHandler.sendToRadio( + newMeshPacketTo(destNum).buildAdminPacket(id = requestId) { nodedbReset = preserveFavorites }, + ) + } override fun getDeviceConnectionStatus(requestId: Int, destNum: Int) = toRemoteExceptions { packetHandler.sendToRadio( 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 e4f33ad95..b837ef727 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 @@ -26,7 +26,7 @@ interface NodeInfoWriteDataSource { suspend fun installConfig(mi: MyNodeEntity, nodes: List) - suspend fun clearNodeDB() + suspend fun clearNodeDB(preserveFavorites: Boolean) suspend fun deleteNode(num: Int) 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 76f9d04a2..ee24ee73b 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 @@ -40,8 +40,8 @@ constructor( override suspend fun installConfig(mi: MyNodeEntity, nodes: List) = withContext(dispatchers.io) { dbManager.withDb { it.nodeInfoDao().installConfig(mi, nodes) } } - override suspend fun clearNodeDB() = - withContext(dispatchers.io) { dbManager.withDb { it.nodeInfoDao().clearNodeInfo() } } + override suspend fun clearNodeDB(preserveFavorites: Boolean) = + withContext(dispatchers.io) { dbManager.withDb { it.nodeInfoDao().clearNodeInfo(preserveFavorites) } } override suspend fun deleteNode(num: Int) = withContext(dispatchers.io) { dbManager.withDb { it.nodeInfoDao().deleteNode(num) } } 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/NodeRepository.kt index 56ac9e2e0..3f767ef88 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/NodeRepository.kt @@ -136,7 +136,8 @@ constructor( suspend fun installConfig(mi: MyNodeEntity, nodes: List) = withContext(dispatchers.io) { nodeInfoWriteDataSource.installConfig(mi, nodes) } - suspend fun clearNodeDB() = withContext(dispatchers.io) { nodeInfoWriteDataSource.clearNodeDB() } + suspend fun clearNodeDB(preserveFavorites: Boolean = false) = + withContext(dispatchers.io) { nodeInfoWriteDataSource.clearNodeDB(preserveFavorites) } suspend fun deleteNode(num: Int) = withContext(dispatchers.io) { nodeInfoWriteDataSource.deleteNode(num) diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/dao/NodeInfoDao.kt b/core/database/src/main/kotlin/org/meshtastic/core/database/dao/NodeInfoDao.kt index ea1a13ba0..e6c99f7df 100644 --- a/core/database/src/main/kotlin/org/meshtastic/core/database/dao/NodeInfoDao.kt +++ b/core/database/src/main/kotlin/org/meshtastic/core/database/dao/NodeInfoDao.kt @@ -182,8 +182,20 @@ interface NodeInfoDao { lastHeardMin: Int, ): Flow> + @Transaction + fun clearNodeInfo(preserveFavorites: Boolean) { + if (preserveFavorites) { + deleteNonFavoriteNodes() + } else { + deleteAllNodes() + } + } + + @Query("DELETE FROM nodes WHERE is_favorite = 0") + fun deleteNonFavoriteNodes() + @Query("DELETE FROM nodes") - fun clearNodeInfo() + fun deleteAllNodes() @Query("DELETE FROM nodes WHERE num=:num") fun deleteNode(num: Int) diff --git a/core/proto/src/main/proto b/core/proto/src/main/proto index fbe1538c2..7654db2e2 160000 --- a/core/proto/src/main/proto +++ b/core/proto/src/main/proto @@ -1 +1 @@ -Subproject commit fbe1538c21f87e6717e6617ac21bc0799e594ec7 +Subproject commit 7654db2e2d1834aebde40090a9b74162ad1048ae diff --git a/core/service/src/main/aidl/org/meshtastic/core/service/IMeshService.aidl b/core/service/src/main/aidl/org/meshtastic/core/service/IMeshService.aidl index 754794ee5..d3fa0aa41 100644 --- a/core/service/src/main/aidl/org/meshtastic/core/service/IMeshService.aidl +++ b/core/service/src/main/aidl/org/meshtastic/core/service/IMeshService.aidl @@ -136,7 +136,7 @@ interface IMeshService { void requestFactoryReset(in int requestId, in int destNum); /// Send NodedbReset admin packet to nodeNum - void requestNodedbReset(in int requestId, in int destNum); + void requestNodedbReset(in int requestId, in int destNum, in boolean preserveFavorites); /// Returns a ChannelSet protobuf byte []getChannelSet(); diff --git a/core/strings/src/commonMain/composeResources/values/strings.xml b/core/strings/src/commonMain/composeResources/values/strings.xml index 62f305004..4c29a0338 100644 --- a/core/strings/src/commonMain/composeResources/values/strings.xml +++ b/core/strings/src/commonMain/composeResources/values/strings.xml @@ -950,4 +950,5 @@ " https://meshtastic.org/docs/legal/privacy/" Unset - 0 Relayed by: %s + Preserve Favorites? diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsScreen.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsScreen.kt index 441df33b4..174e3f6bc 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsScreen.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsScreen.kt @@ -237,6 +237,7 @@ fun SettingsScreen( state = state, isManaged = localConfig.security.isManaged, excludedModulesUnlocked = excludedModulesUnlocked, + onPreserveFavoritesToggle = { viewModel.setPreserveFavorites(it) }, onRouteClick = { route -> isWaiting = true viewModel.setResponseStateLoading(route) diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfig.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfig.kt index ddce1b378..650c0aeb5 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfig.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfig.kt @@ -17,7 +17,10 @@ package org.meshtastic.feature.settings.radio +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Download @@ -28,7 +31,9 @@ import androidx.compose.material.icons.rounded.PowerSettingsNew import androidx.compose.material.icons.rounded.RestartAlt import androidx.compose.material.icons.rounded.Restore import androidx.compose.material.icons.rounded.Storage +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -36,6 +41,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.tooling.preview.Preview @@ -57,6 +63,7 @@ import org.meshtastic.core.strings.import_configuration import org.meshtastic.core.strings.message_device_managed import org.meshtastic.core.strings.module_settings import org.meshtastic.core.strings.nodedb_reset +import org.meshtastic.core.strings.preserve_favorites import org.meshtastic.core.strings.radio_configuration import org.meshtastic.core.strings.reboot import org.meshtastic.core.strings.shutdown @@ -68,12 +75,14 @@ import org.meshtastic.feature.settings.navigation.ConfigRoute import org.meshtastic.feature.settings.navigation.ModuleRoute import org.meshtastic.feature.settings.radio.component.WarningDialog +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Suppress("LongMethod", "CyclomaticComplexMethod") @Composable fun RadioConfigItemList( state: RadioConfigState, isManaged: Boolean, excludedModulesUnlocked: Boolean = false, + onPreserveFavoritesToggle: (Boolean) -> Unit = {}, onRouteClick: (Enum<*>) -> Unit = {}, onImport: () -> Unit = {}, onExport: () -> Unit = {}, @@ -147,6 +156,23 @@ fun RadioConfigItemList( if (showDialog) { WarningDialog( title = "${stringResource(route.title)}?", + text = { + if (route == AdminRoute.NODEDB_RESET) { + Row( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp).fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text(text = stringResource(Res.string.preserve_favorites)) + Switch( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + enabled = enabled, + checked = state.nodeDbResetPreserveFavorites, + onCheckedChange = onPreserveFavoritesToggle, + ) + } + } + }, onDismiss = { showDialog = false }, onConfirm = { onRouteClick(route) }, ) 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 573f6830f..815628b24 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 @@ -100,6 +100,7 @@ data class RadioConfigState( val responseState: ResponseState = ResponseState.Empty, val analyticsAvailable: Boolean = true, val analyticsEnabled: Boolean = false, + val nodeDbResetPreserveFavorites: Boolean = false, ) @Suppress("LongParameterList") @@ -135,6 +136,10 @@ constructor( private val _radioConfigState = MutableStateFlow(RadioConfigState()) val radioConfigState: StateFlow = _radioConfigState + fun setPreserveFavorites(preserveFavorites: Boolean) { + viewModelScope.launch { _radioConfigState.update { it.copy(nodeDbResetPreserveFavorites = preserveFavorites) } } + } + private val _currentDeviceProfile = MutableStateFlow(deviceProfile {}) val currentDeviceProfile get() = _currentDeviceProfile.value @@ -365,14 +370,14 @@ constructor( } } - private fun requestNodedbReset(destNum: Int) { + private fun requestNodedbReset(destNum: Int, preserveFavorites: Boolean) { request( destNum, - { service, packetId, dest -> service.requestNodedbReset(packetId, dest) }, + { service, packetId, dest -> service.requestNodedbReset(packetId, dest, preserveFavorites) }, "Request NodeDB reset error", ) if (destNum == myNodeNum) { - viewModelScope.launch { nodeRepository.clearNodeDB() } + viewModelScope.launch { nodeRepository.clearNodeDB(preserveFavorites) } } } @@ -380,6 +385,8 @@ constructor( val route = radioConfigState.value.route _radioConfigState.update { it.copy(route = "") } // setter (response is PortNum.ROUTING_APP) + val preserveFavorites = radioConfigState.value.nodeDbResetPreserveFavorites + when (route) { AdminRoute.REBOOT.name -> requestReboot(destNum) AdminRoute.SHUTDOWN.name -> @@ -392,7 +399,7 @@ constructor( } AdminRoute.FACTORY_RESET.name -> requestFactoryReset(destNum) - AdminRoute.NODEDB_RESET.name -> requestNodedbReset(destNum) + AdminRoute.NODEDB_RESET.name -> requestNodedbReset(destNum, preserveFavorites) } } @@ -535,6 +542,7 @@ constructor( connected = it.connected, route = route.name, metadata = it.metadata, + nodeDbResetPreserveFavorites = it.nodeDbResetPreserveFavorites, responseState = ResponseState.Loading(), ) } diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/WarningDialog.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/WarningDialog.kt index 1e582f72a..8c4ee53ec 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/WarningDialog.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/WarningDialog.kt @@ -37,6 +37,7 @@ import org.meshtastic.core.ui.theme.AppTheme fun WarningDialog( icon: ImageVector? = Icons.Rounded.Warning, title: String, + text: @Composable () -> Unit = {}, onDismiss: () -> Unit, onConfirm: () -> Unit, ) { @@ -44,6 +45,7 @@ fun WarningDialog( onDismissRequest = {}, icon = { icon?.let { Icon(imageVector = it, contentDescription = null) } }, title = { Text(text = title) }, + text = text, dismissButton = { TextButton(onClick = { onDismiss() }) { Text(stringResource(Res.string.cancel)) } }, confirmButton = { Button(