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 914ab5a91..6b3c86bba 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -206,13 +206,19 @@ class UIViewModel @Inject constructor( private val _lastTraceRouteTime = MutableStateFlow(null) val lastTraceRouteTime: StateFlow = _lastTraceRouteTime.asStateFlow() + val clientNotification: StateFlow = radioConfigRepository.clientNotification + fun clearClientNotification(notification: MeshProtos.ClientNotification) { + radioConfigRepository.clearClientNotification() + meshServiceNotifications.clearClientNotification(notification) + } + data class AlertData( val title: String, val message: String? = null, val html: String? = null, val onConfirm: (() -> Unit)? = null, val onDismiss: (() -> Unit)? = null, - val choices: Map Unit> = emptyMap() + val choices: Map Unit> = emptyMap(), ) private val _currentAlert: MutableStateFlow = MutableStateFlow(null) @@ -224,7 +230,7 @@ class UIViewModel @Inject constructor( html: String? = null, onConfirm: (() -> Unit)? = {}, dismissable: Boolean = true, - choices: Map Unit> = emptyMap() + choices: Map Unit> = emptyMap(), ) { _currentAlert.value = AlertData( @@ -238,7 +244,7 @@ class UIViewModel @Inject constructor( onDismiss = { if (dismissable) dismissAlert() }, - choices = choices + choices = choices, ) } @@ -285,7 +291,8 @@ class UIViewModel @Inject constructor( private val onlyDirect = MutableStateFlow(preferences.getBoolean("only-direct", false)) private val onlyFavorites = MutableStateFlow(preferences.getBoolean("only-favorites", false)) - private val showWaypointsOnMap = MutableStateFlow(preferences.getBoolean("show-waypoints-on-map", true)) + private val showWaypointsOnMap = + MutableStateFlow(preferences.getBoolean("show-waypoints-on-map", true)) private val showPrecisionCircleOnMap = MutableStateFlow(preferences.getBoolean("show-precision-circle-on-map", true)) @@ -441,18 +448,6 @@ class UIViewModel @Inject constructor( ) }.launchIn(viewModelScope) - radioConfigRepository.clientNotification.filterNotNull().onEach { notification -> - showAlert( - title = app.getString(R.string.client_notification), - message = notification.message, - onConfirm = { - radioConfigRepository.clearClientNotification() - meshServiceNotifications.clearClientNotification(notification) - }, - dismissable = false - ) - }.launchIn(viewModelScope) - radioConfigRepository.localConfigFlow.onEach { config -> _localConfig.value = config }.launchIn(viewModelScope) @@ -663,7 +658,8 @@ class UIViewModel @Inject constructor( showSnackbar(R.string.channel_invalid) } - val latestStableFirmwareRelease = firmwareReleaseRepository.stableRelease.mapNotNull { it?.asDeviceVersion() } + val latestStableFirmwareRelease = + firmwareReleaseRepository.stableRelease.mapNotNull { it?.asDeviceVersion() } /** * Called immediately after activity observes requestChannelUrl 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 a5a327fb5..05367408a 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Main.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Main.kt @@ -148,6 +148,30 @@ fun MainScreen( } } + val clientNotification by viewModel.clientNotification.collectAsStateWithLifecycle() + clientNotification?.let { notification -> + var message = notification.message + val compromisedKeys = + if (notification.hasLowEntropyKey() || notification.hasDuplicatedPublicKey()) { + message = stringResource(R.string.compromised_keys) + true + } else { + false + } + SimpleAlertDialog( + title = R.string.client_notification, + text = { + Text(text = message) + }, + onConfirm = { + if (compromisedKeys) { + navController.navigate(RadioConfigRoutes.Security) + } + viewModel.clearClientNotification(notification) + }, + ) + } + val traceRouteResponse by viewModel.tracerouteResponse.observeAsState() traceRouteResponse?.let { response -> SimpleAlertDialog( diff --git a/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetail.kt b/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetail.kt index 017c4e310..291b6958e 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetail.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetail.kt @@ -952,6 +952,7 @@ fun TracerouteActionButton( @Suppress("LongMethod") @Composable fun NodeActionButton( + modifier: Modifier = Modifier, title: String, enabled: Boolean, icon: ImageVector? = null, @@ -964,7 +965,7 @@ fun NodeActionButton( onClick() }, enabled = enabled, - modifier = Modifier + modifier = modifier .fillMaxWidth() .padding(vertical = 4.dp) .height(48.dp), diff --git a/app/src/main/java/com/geeksville/mesh/ui/radioconfig/components/SecurityConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/radioconfig/components/SecurityConfigItemList.kt index 2c5c6c939..79b2dff94 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/radioconfig/components/SecurityConfigItemList.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/radioconfig/components/SecurityConfigItemList.kt @@ -18,9 +18,15 @@ package com.geeksville.mesh.ui.radioconfig.components import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.twotone.Warning +import androidx.compose.material3.AlertDialog import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -30,6 +36,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.geeksville.mesh.ConfigProtos.Config.SecurityConfig @@ -42,6 +49,7 @@ import com.geeksville.mesh.ui.common.components.EditListPreference import com.geeksville.mesh.ui.common.components.PreferenceCategory import com.geeksville.mesh.ui.common.components.PreferenceFooter import com.geeksville.mesh.ui.common.components.SwitchPreference +import com.geeksville.mesh.ui.node.NodeActionButton import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel import com.geeksville.mesh.util.encodeToString @@ -78,6 +86,17 @@ fun SecurityConfigItemList( val focusManager = LocalFocusManager.current var securityInput by rememberSaveable { mutableStateOf(securityConfig) } + var showKeyGenerationDialog by rememberSaveable { mutableStateOf(false) } + PrivateKeyRegenerateDialog( + showKeyGenerationDialog = showKeyGenerationDialog, + config = securityInput, + onConfirm = { newConfig -> + securityInput = newConfig + showKeyGenerationDialog = false + }, + onDismiss = { showKeyGenerationDialog = false } + ) + LazyColumn( modifier = Modifier.fillMaxSize() ) { @@ -122,6 +141,18 @@ fun SecurityConfigItemList( ) } + item { + NodeActionButton( + modifier = Modifier.padding(16.dp), + title = stringResource(R.string.regenerate_private_key), + enabled = enabled, + icon = Icons.TwoTone.Warning, + onClick = { + showKeyGenerationDialog = true + } + ) + } + item { EditListPreference( title = stringResource(R.string.admin_key), @@ -200,6 +231,42 @@ fun SecurityConfigItemList( } } +@Composable +fun PrivateKeyRegenerateDialog( + showKeyGenerationDialog: Boolean, + config: SecurityConfig, + onConfirm: (SecurityConfig) -> Unit, + onDismiss: () -> Unit = {}, +) { + var securityInput by rememberSaveable { mutableStateOf(config) } + if (showKeyGenerationDialog) { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(text = stringResource(R.string.regenerate_private_key)) }, + text = { Text(text = stringResource(R.string.regenerate_keys_confirmation)) }, + confirmButton = { + TextButton( + onClick = { + securityInput = securityInput.copy { + clearPrivateKey() + } + onConfirm(securityInput) + }, + ) { + Text(stringResource(R.string.okay)) + } + }, + dismissButton = { + TextButton( + onClick = onDismiss, + ) { + Text(stringResource(R.string.cancel)) + } + } + ) + } +} + @Preview(showBackground = true) @Composable private fun SecurityConfigPreview() { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b5b574690..e95e1bb89 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -680,4 +680,7 @@ Show Waypoints Show Precision Circles Client Notification + Compromised keys detected, select OK to regenerate. + Regenerate Private Key + Are you sure you want to regenerate your Private Key?