feat(#3508): Optionally preserve Favorites on nodeDb reset (#3633)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
renovate[bot] 2025-11-11 02:38:45 +00:00 committed by GitHub
parent 81dc625c70
commit e701ad6aee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 68 additions and 14 deletions

View file

@ -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(

View file

@ -26,7 +26,7 @@ interface NodeInfoWriteDataSource {
suspend fun installConfig(mi: MyNodeEntity, nodes: List<NodeEntity>)
suspend fun clearNodeDB()
suspend fun clearNodeDB(preserveFavorites: Boolean)
suspend fun deleteNode(num: Int)

View file

@ -40,8 +40,8 @@ constructor(
override suspend fun installConfig(mi: MyNodeEntity, nodes: List<NodeEntity>) =
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) } }

View file

@ -136,7 +136,8 @@ constructor(
suspend fun installConfig(mi: MyNodeEntity, nodes: List<NodeEntity>) =
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)

View file

@ -182,8 +182,20 @@ interface NodeInfoDao {
lastHeardMin: Int,
): Flow<List<NodeWithRelations>>
@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)

@ -1 +1 @@
Subproject commit fbe1538c21f87e6717e6617ac21bc0799e594ec7
Subproject commit 7654db2e2d1834aebde40090a9b74162ad1048ae

View file

@ -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();

View file

@ -950,4 +950,5 @@
<string name="privacy_url" translatable="false">" https://meshtastic.org/docs/legal/privacy/"</string>
<string name="unset">Unset - 0</string>
<string name="relayed_by">Relayed by: %s</string>
<string name="preserve_favorites">Preserve Favorites?</string>
</resources>

View file

@ -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)

View file

@ -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) },
)

View file

@ -100,6 +100,7 @@ data class RadioConfigState(
val responseState: ResponseState<Boolean> = 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> = _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(),
)
}

View file

@ -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(