mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
Enhancement - Add 'show all meanings' to node key encryption dialog (#3437)
Co-authored-by: ChrisDeardeuff <chris.deardeuff@proton.me>
This commit is contained in:
parent
241b46da3d
commit
f6487518f8
2 changed files with 161 additions and 51 deletions
|
|
@ -354,6 +354,7 @@
|
|||
<string name="ch_util_definition">Utilization for the current channel, including well formed TX, RX and malformed RX (aka noise).</string>
|
||||
<string name="air_util_definition">Percent of airtime for transmission used within the last hour.</string>
|
||||
<string name="iaq">IAQ</string>
|
||||
<string name="show_all_key_title">Encryption Key Meanings</string>
|
||||
<string name="encryption_psk">Shared Key</string>
|
||||
<string name="encryption_psk_text">Only channel messages can be sent/received. Direct Messages require the Public Key Infrastructure feature in 2.5+ firmware.</string>
|
||||
<string name="encryption_pkc">Public Key Encryption</string>
|
||||
|
|
|
|||
|
|
@ -20,40 +20,45 @@ package org.meshtastic.core.ui.component
|
|||
import android.util.Base64
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.KeyOff
|
||||
import androidx.compose.material.icons.filled.Lock
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material.icons.filled.LockOpen
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import com.google.protobuf.ByteString
|
||||
import org.meshtastic.core.model.Channel
|
||||
import org.meshtastic.core.strings.R
|
||||
|
|
@ -62,56 +67,19 @@ import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
|
|||
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusYellow
|
||||
|
||||
@Composable
|
||||
private fun KeyStatusDialog(@StringRes title: Int, @StringRes text: Int, key: ByteString?, onDismiss: () -> Unit = {}) =
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
color = MaterialTheme.colorScheme.background,
|
||||
) {
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
item {
|
||||
Text(text = stringResource(id = title), textAlign = TextAlign.Center)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Text(text = stringResource(id = text), textAlign = TextAlign.Center)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
if (key != null && title == R.string.encryption_pkc) {
|
||||
val keyString = Base64.encodeToString(key.toByteArray(), Base64.NO_WRAP)
|
||||
Text(
|
||||
text = stringResource(id = R.string.config_security_public_key) + ":",
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
SelectionContainer { Text(text = keyString, textAlign = TextAlign.Center) }
|
||||
Spacer(Modifier.height(8.dp))
|
||||
CopyIconButton(valueToCopy = keyString, modifier = Modifier.padding(start = 8.dp))
|
||||
Spacer(Modifier.height(16.dp))
|
||||
}
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
|
||||
TextButton(
|
||||
onClick = onDismiss,
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||
),
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.close))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* function to display information about the current node's encryption key.
|
||||
*
|
||||
* @property hasPKC boolean if the node has public key encryption
|
||||
* @property mismatchKey boolean if the public key does not match the recorded key.
|
||||
* @property publicKey boolean if the node has a shared public key.
|
||||
*/
|
||||
@Composable
|
||||
fun NodeKeyStatusIcon(
|
||||
modifier: Modifier = Modifier,
|
||||
hasPKC: Boolean,
|
||||
mismatchKey: Boolean,
|
||||
publicKey: ByteString? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var showEncryptionDialog by remember { mutableStateOf(false) }
|
||||
if (showEncryptionDialog) {
|
||||
|
|
@ -150,6 +118,141 @@ fun NodeKeyStatusIcon(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the various visual states of the node key as an enum. Each enum constant encapsulates the icon, color,
|
||||
* descriptive text, and optional badge details.
|
||||
*
|
||||
* @property icon The primary vector graphic for the icon.
|
||||
* @property color The tint color for the primary icon.
|
||||
* @property descriptionResId The string resource ID for the accessibility description of the icon's state.
|
||||
* @property helpTextResId The string resource ID for the detailed help text associated with this state.
|
||||
* @property title The string resource ID for the title associated with this state.
|
||||
*/
|
||||
@Immutable
|
||||
enum class NodeKeySecurityState(
|
||||
@Stable val icon: ImageVector,
|
||||
@Stable val color: @Composable () -> Color,
|
||||
@StringRes val descriptionResId: Int,
|
||||
@StringRes val helpTextResId: Int,
|
||||
@Stable val title: Int,
|
||||
) {
|
||||
// State for public key mismatch
|
||||
PKM(
|
||||
icon = Icons.Default.KeyOff,
|
||||
color = { colorScheme.StatusRed },
|
||||
descriptionResId = R.string.encryption_error,
|
||||
helpTextResId = R.string.encryption_error_text,
|
||||
title = R.string.encryption_error,
|
||||
),
|
||||
|
||||
// State for public key encryption
|
||||
PKC(
|
||||
icon = Icons.Default.Lock,
|
||||
color = { colorScheme.StatusGreen },
|
||||
title = R.string.encryption_pkc,
|
||||
helpTextResId = R.string.encryption_pkc_text,
|
||||
descriptionResId = R.string.encryption_pkc,
|
||||
),
|
||||
|
||||
// State for shared key encryption
|
||||
PSK(
|
||||
icon = Icons.Default.LockOpen,
|
||||
color = { colorScheme.StatusYellow },
|
||||
title = R.string.encryption_psk,
|
||||
helpTextResId = R.string.encryption_psk_text,
|
||||
descriptionResId = R.string.encryption_psk,
|
||||
),
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun KeyStatusDialog(@StringRes title: Int, @StringRes text: Int, key: ByteString?, onDismiss: () -> Unit = {}) {
|
||||
var showAll by rememberSaveable { mutableStateOf(false) }
|
||||
AlertDialog(
|
||||
modifier = Modifier,
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
if (showAll) {
|
||||
Text(stringResource(R.string.show_all_key_title))
|
||||
} else {
|
||||
Text(stringResource(id = title))
|
||||
}
|
||||
},
|
||||
text = {
|
||||
if (showAll) {
|
||||
AllKeyStates()
|
||||
} else {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(text = stringResource(id = text), textAlign = TextAlign.Center)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
if (key != null && title == R.string.encryption_pkc) {
|
||||
val keyString = Base64.encodeToString(key.toByteArray(), Base64.NO_WRAP)
|
||||
Text(
|
||||
text = stringResource(id = R.string.config_security_public_key) + ":",
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
SelectionContainer { Text(text = keyString, textAlign = TextAlign.Center) }
|
||||
Spacer(Modifier.height(8.dp))
|
||||
CopyIconButton(valueToCopy = keyString, modifier = Modifier.padding(start = 8.dp))
|
||||
Spacer(Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
TextButton(onClick = { showAll = !showAll }) {
|
||||
Text(
|
||||
if (showAll) {
|
||||
stringResource(R.string.security_icon_help_show_less)
|
||||
} else {
|
||||
stringResource(R.string.security_icon_help_show_all)
|
||||
},
|
||||
)
|
||||
}
|
||||
TextButton(onClick = onDismiss) { Text(stringResource(R.string.security_icon_help_dismiss)) }
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a list of all possible node key states with their icons and descriptions within the help dialog. Iterates
|
||||
* over `NodeKeySecurityState.entries` which is provided by the enum class.
|
||||
*/
|
||||
@Composable
|
||||
private fun AllKeyStates() {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
NodeKeySecurityState.entries.forEach { state ->
|
||||
// Uses enum entries
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
when (state) {
|
||||
NodeKeySecurityState.PKM -> NodeKeyStatusIcon(hasPKC = false, mismatchKey = true)
|
||||
|
||||
NodeKeySecurityState.PKC -> NodeKeyStatusIcon(hasPKC = true, mismatchKey = false)
|
||||
|
||||
else -> NodeKeyStatusIcon(hasPKC = false, mismatchKey = false)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.padding(start = 16.dp)) {
|
||||
Text(text = stringResource(state.descriptionResId), style = MaterialTheme.typography.titleMedium)
|
||||
Text(text = stringResource(state.helpTextResId), style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
if (state != NodeKeySecurityState.entries.lastOrNull()) {
|
||||
HorizontalDivider(modifier = Modifier.padding(top = 8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun KeyStatusDialogErrorPreview() {
|
||||
|
|
@ -173,3 +276,9 @@ private fun KeyStatusDialogPkcPreview() {
|
|||
private fun KeyStatusDialogPskPreview() {
|
||||
AppTheme { KeyStatusDialog(title = R.string.encryption_psk, text = R.string.encryption_psk_text, key = null) }
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun AllKeyStatusDialogPreview() {
|
||||
AppTheme { AllKeyStates() }
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue