mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
42efc2e809
commit
299fdbc059
2 changed files with 519 additions and 148 deletions
|
|
@ -17,21 +17,44 @@
|
|||
|
||||
package com.geeksville.mesh.ui.common.components
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Lock
|
||||
import androidx.compose.material.icons.filled.LockOpen
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Badge
|
||||
import androidx.compose.material3.BadgedBox
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.foundation.layout.Arrangement
|
||||
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.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.geeksville.mesh.AppOnlyProtos
|
||||
|
|
@ -42,183 +65,484 @@ import com.geeksville.mesh.model.getChannel
|
|||
private const val PRECISE_POSITION_BITS = 32
|
||||
|
||||
/**
|
||||
* Returns the appropriate security icon composable based on the channel's security settings.
|
||||
* Represents the various visual states of the security icon as an enum.
|
||||
* Each enum constant encapsulates the icon, color, descriptive text, and optional badge details.
|
||||
*
|
||||
* @param isLowEntropyKey Whether the channel uses a low entropy key (0 or 1 byte PSK)
|
||||
* @param isPreciseLocation Whether the channel has precise location enabled (32 bits)
|
||||
* @param isMqttEnabled Whether MQTT is enabled (adds warning icon)
|
||||
* @param contentDescription The content description for the icon
|
||||
* @return A composable Icon element with appropriate imageVector and tint
|
||||
* @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 badgeIcon Optional vector graphic for a badge to be displayed on the icon.
|
||||
* @property badgeIconColor Optional tint color for the badge icon.
|
||||
*/
|
||||
@Immutable
|
||||
enum class SecurityState(
|
||||
@Stable val icon: ImageVector,
|
||||
@Stable val color: Color,
|
||||
@StringRes val descriptionResId: Int,
|
||||
@StringRes val helpTextResId: Int,
|
||||
@Stable val badgeIcon: ImageVector? = null,
|
||||
@Stable val badgeIconColor: Color? = null,
|
||||
) {
|
||||
/** State for a secure channel (green lock). */
|
||||
SECURE(
|
||||
icon = Icons.Filled.Lock,
|
||||
color = Color.Green,
|
||||
descriptionResId = R.string.security_icon_secure,
|
||||
helpTextResId = R.string.security_icon_help_green_lock,
|
||||
),
|
||||
|
||||
/** State for an insecure channel,
|
||||
* not used for precise location,
|
||||
* and MQTT not the primary concern for a higher warning.
|
||||
* (yellow open lock) */
|
||||
INSECURE_NO_PRECISE(
|
||||
icon = Icons.Filled.LockOpen,
|
||||
color = Color.Yellow,
|
||||
descriptionResId = R.string.security_icon_insecure_no_precise,
|
||||
helpTextResId = R.string.security_icon_help_yellow_open_lock,
|
||||
),
|
||||
|
||||
/** State for an insecure channel
|
||||
* with precise location enabled,
|
||||
* but MQTT not causing the highest
|
||||
* warning. (red open lock) */
|
||||
INSECURE_PRECISE_ONLY(
|
||||
icon = Icons.Filled.LockOpen,
|
||||
color = Color.Red,
|
||||
descriptionResId = R.string.security_icon_insecure_precise_only,
|
||||
helpTextResId = R.string.security_icon_help_red_open_lock,
|
||||
),
|
||||
|
||||
/** State indicating an insecure channel
|
||||
* with precise location and MQTT enabled
|
||||
* (red open lock with yellow warning badge). */
|
||||
INSECURE_PRECISE_MQTT_WARNING(
|
||||
icon = Icons.Filled.LockOpen,
|
||||
color = Color.Red,
|
||||
descriptionResId = R.string.security_icon_warning_precise_mqtt,
|
||||
helpTextResId = R.string.security_icon_help_warning_precise_mqtt,
|
||||
badgeIcon = Icons.Filled.Warning,
|
||||
badgeIconColor = Color.Yellow,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal composable to display the security icon, potentially with a badge.
|
||||
*
|
||||
* @param icon The main vector graphic for the icon.
|
||||
* @param mainIconTint The tint color for the main icon.
|
||||
* @param contentDescription The accessibility description for the icon.
|
||||
* @param modifier Modifier for this composable.
|
||||
* @param badgeIcon Optional vector graphic for the badge.
|
||||
* @param badgeIconColor Optional tint color for the badge icon.
|
||||
*/
|
||||
@Composable
|
||||
private fun SecurityIconDisplay(
|
||||
icon: ImageVector,
|
||||
mainIconTint: Color,
|
||||
contentDescription: String,
|
||||
modifier: Modifier = Modifier,
|
||||
badgeIcon: ImageVector? = null,
|
||||
badgeIconColor: Color? = null,
|
||||
) {
|
||||
BadgedBox(
|
||||
badge = {
|
||||
if (badgeIcon != null) {
|
||||
Badge(
|
||||
containerColor = Color.Transparent, // Allows badgeIconColor to define appearance
|
||||
) {
|
||||
Icon(
|
||||
imageVector = badgeIcon,
|
||||
contentDescription = stringResource(R.string.security_icon_badge_warning_description),
|
||||
tint = badgeIconColor
|
||||
?: MaterialTheme.colorScheme.onError, // Default for contrast
|
||||
modifier = Modifier.size(16.dp), // Adjusted badge icon size
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = modifier,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = contentDescription,
|
||||
tint = mainIconTint,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the [SecurityState] based on channel properties.
|
||||
* The priority of states is: MQTT warning, then secure, then insecure variations.
|
||||
*
|
||||
* @param isLowEntropyKey True if the channel uses a low entropy key (not securely encrypted).
|
||||
* @param isPreciseLocation True if precise location is enabled.
|
||||
* @param isMqttEnabled True if MQTT is enabled for the channel.
|
||||
* @return The determined [SecurityState].
|
||||
*/
|
||||
private fun determineSecurityState(
|
||||
isLowEntropyKey: Boolean,
|
||||
isPreciseLocation: Boolean,
|
||||
isMqttEnabled: Boolean,
|
||||
): SecurityState = when {
|
||||
!isLowEntropyKey -> SecurityState.SECURE
|
||||
|
||||
isMqttEnabled && isPreciseLocation -> SecurityState.INSECURE_PRECISE_MQTT_WARNING
|
||||
|
||||
isPreciseLocation -> SecurityState.INSECURE_PRECISE_ONLY
|
||||
|
||||
else -> SecurityState.INSECURE_NO_PRECISE
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays an icon representing the security status of a channel.
|
||||
* Clicking the icon shows a detailed help dialog.
|
||||
*
|
||||
* @param securityState The current [SecurityState] to display.
|
||||
* @param baseContentDescription The base content description for the icon, to which the specific
|
||||
* state description will be appended. Defaults to a generic security icon description.
|
||||
* @param externalOnClick Optional lambda to be invoked when the icon is clicked,
|
||||
* in addition to its primary action (showing a help dialog).
|
||||
* This allows callers to inject custom side effects.
|
||||
*/
|
||||
@Composable
|
||||
fun SecurityIcon(
|
||||
securityState: SecurityState,
|
||||
baseContentDescription: String = stringResource(id = R.string.security_icon_description),
|
||||
externalOnClick: (() -> Unit)? = null,
|
||||
) {
|
||||
var showHelpDialog by rememberSaveable { mutableStateOf(false) }
|
||||
val fullContentDescription =
|
||||
baseContentDescription + " " + stringResource(id = securityState.descriptionResId)
|
||||
|
||||
IconButton(
|
||||
onClick = {
|
||||
showHelpDialog = true
|
||||
externalOnClick?.invoke()
|
||||
},
|
||||
) {
|
||||
SecurityIconDisplay(
|
||||
icon = securityState.icon,
|
||||
mainIconTint = securityState.color,
|
||||
contentDescription = fullContentDescription,
|
||||
badgeIcon = securityState.badgeIcon,
|
||||
badgeIconColor = securityState.badgeIconColor,
|
||||
)
|
||||
}
|
||||
|
||||
if (showHelpDialog) {
|
||||
SecurityHelpDialog(
|
||||
securityState = securityState,
|
||||
onDismiss = { showHelpDialog = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Overload for [SecurityIcon] that derives the [SecurityState] from boolean flags.
|
||||
*
|
||||
* @param isLowEntropyKey Whether the channel uses a low entropy key.
|
||||
* @param isPreciseLocation Whether the channel has precise location enabled. Defaults to false.
|
||||
* @param isMqttEnabled Whether MQTT is enabled for the channel. Defaults to false.
|
||||
* @param baseContentDescription The base content description for the icon.
|
||||
* @param externalOnClick Optional lambda to be invoked when the icon is clicked,
|
||||
* in addition to its primary action (showing a help dialog).
|
||||
* This allows callers to inject custom side effects.
|
||||
*/
|
||||
@Composable
|
||||
fun SecurityIcon(
|
||||
isLowEntropyKey: Boolean,
|
||||
isPreciseLocation: Boolean = false,
|
||||
isMqttEnabled: Boolean = false,
|
||||
contentDescription: String = stringResource(id = R.string.security_icon_description)
|
||||
baseContentDescription: String = stringResource(id = R.string.security_icon_description),
|
||||
externalOnClick: (() -> Unit)? = null,
|
||||
) {
|
||||
val (icon, color, computedDescription) = when {
|
||||
!isLowEntropyKey -> {
|
||||
Triple(Icons.Default.Lock, Color.Green, stringResource(id = R.string.security_icon_secure))
|
||||
}
|
||||
isPreciseLocation && isMqttEnabled -> {
|
||||
Triple(Icons.Default.Warning, Color.Red, stringResource(id = R.string.security_icon_warning))
|
||||
}
|
||||
isPreciseLocation -> {
|
||||
Triple(ImageVector.vectorResource(R.drawable.ic_lock_open_right_24),
|
||||
Color.Red,
|
||||
stringResource(id = R.string.security_icon_insecure_precise))
|
||||
}
|
||||
else -> {
|
||||
Triple(ImageVector.vectorResource(R.drawable.ic_lock_open_right_24),
|
||||
Color.Yellow,
|
||||
stringResource(id = R.string.security_icon_insecure))
|
||||
}
|
||||
}
|
||||
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = contentDescription + computedDescription,
|
||||
tint = color
|
||||
val securityState = determineSecurityState(isLowEntropyKey, isPreciseLocation, isMqttEnabled)
|
||||
SecurityIcon(
|
||||
securityState = securityState,
|
||||
baseContentDescription = baseContentDescription,
|
||||
externalOnClick = externalOnClick,
|
||||
)
|
||||
}
|
||||
|
||||
fun Channel.isLowEntropyKey(): Boolean = settings.psk.size() <= 1
|
||||
fun Channel.isPreciseLocation(): Boolean = settings.getModuleSettings().positionPrecision == PRECISE_POSITION_BITS
|
||||
fun Channel.isMqttEnabled(): Boolean = settings.uplinkEnabled
|
||||
/** Extension property to check if the channel uses a low entropy PSK (not securely encrypted). */
|
||||
val Channel.isLowEntropyKey: Boolean get() = settings.psk.size() <= 1
|
||||
|
||||
/** Extension property to check if the channel has precise location enabled. */
|
||||
val Channel.isPreciseLocation: Boolean get() = settings.moduleSettings.positionPrecision == PRECISE_POSITION_BITS
|
||||
|
||||
/** Extension property to check if MQTT is enabled for the channel. */
|
||||
val Channel.isMqttEnabled: Boolean get() = settings.uplinkEnabled
|
||||
|
||||
/**
|
||||
* Overload for [SecurityIcon] that takes a [Channel] object to determine its security state.
|
||||
*
|
||||
* @param channel The channel whose security status is to be displayed.
|
||||
* @param baseContentDescription The base content description for the icon.
|
||||
* @param externalOnClick Optional lambda for external actions, invoked when the icon is clicked.
|
||||
*/
|
||||
@Composable
|
||||
fun SecurityIcon(
|
||||
channel: Channel,
|
||||
contentDescription: String = stringResource(id = R.string.security_icon_description)
|
||||
baseContentDescription: String = stringResource(id = R.string.security_icon_description),
|
||||
externalOnClick: (() -> Unit)? = null,
|
||||
) = SecurityIcon(
|
||||
channel.isLowEntropyKey(),
|
||||
channel.isPreciseLocation(),
|
||||
channel.isMqttEnabled(),
|
||||
contentDescription
|
||||
isLowEntropyKey = channel.isLowEntropyKey,
|
||||
isPreciseLocation = channel.isPreciseLocation,
|
||||
isMqttEnabled = channel.isMqttEnabled,
|
||||
baseContentDescription = baseContentDescription,
|
||||
externalOnClick = externalOnClick,
|
||||
)
|
||||
|
||||
/**
|
||||
* Overload for [SecurityIcon] that takes an [AppOnlyProtos.ChannelSet] and a channel index.
|
||||
* If the channel at the given index is not found, nothing is rendered.
|
||||
*
|
||||
* @param channelSet The set of channels.
|
||||
* @param channelIndex The index of the channel within the set.
|
||||
* @param baseContentDescription The base content description for the icon.
|
||||
* @param externalOnClick Optional lambda for external actions, invoked when the icon is clicked.
|
||||
*/
|
||||
@Composable
|
||||
fun SecurityIcon(
|
||||
channelSet: AppOnlyProtos.ChannelSet,
|
||||
channelIndex: Int,
|
||||
contentDescription: String = stringResource(id = R.string.security_icon_description)
|
||||
baseContentDescription: String = stringResource(id = R.string.security_icon_description),
|
||||
externalOnClick: (() -> Unit)? = null,
|
||||
) {
|
||||
val channel = channelSet.getChannel(channelIndex) ?: return
|
||||
SecurityIcon(channel, contentDescription)
|
||||
channelSet.getChannel(channelIndex)?.let { channel ->
|
||||
SecurityIcon(
|
||||
channel = channel,
|
||||
baseContentDescription = baseContentDescription,
|
||||
externalOnClick = externalOnClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Overload for [SecurityIcon] that takes an [AppOnlyProtos.ChannelSet] and a channel name.
|
||||
* If a channel with the given name is not found, nothing is rendered.
|
||||
* This overload optimizes lookup by name by memoizing a map of channel names to settings.
|
||||
*
|
||||
* @param channelSet The set of channels.
|
||||
* @param channelName The name of the channel to find.
|
||||
* @param baseContentDescription The base content description for the icon.
|
||||
* @param externalOnClick Optional lambda for external actions, invoked when the icon is clicked.
|
||||
*/
|
||||
@Composable
|
||||
fun SecurityIcon(
|
||||
channelSet: AppOnlyProtos.ChannelSet,
|
||||
channelName: String,
|
||||
contentDescription: String = stringResource(id = R.string.security_icon_description)
|
||||
baseContentDescription: String = stringResource(id = R.string.security_icon_description),
|
||||
externalOnClick: (() -> Unit)? = null,
|
||||
) {
|
||||
val channel = channelSet.settingsList.find {
|
||||
Channel(it, channelSet.loraConfig).name == channelName
|
||||
}?.let { Channel(it, channelSet.loraConfig) } ?: return
|
||||
SecurityIcon(channel, contentDescription)
|
||||
}
|
||||
val channelByNameMap = remember(channelSet) {
|
||||
channelSet.settingsList.associateBy {
|
||||
Channel(it, channelSet.loraConfig).name
|
||||
}
|
||||
}
|
||||
|
||||
// Preview functions for development and testing
|
||||
@Preview(name = "Secure Channel - Green Lock")
|
||||
@Composable
|
||||
private fun PreviewSecureChannel() {
|
||||
SecurityIcon(
|
||||
isLowEntropyKey = false,
|
||||
isPreciseLocation = false,
|
||||
isMqttEnabled = false
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(name = "Insecure Channel with Precise Location - Red Unlock")
|
||||
@Composable
|
||||
private fun PreviewInsecureChannelWithPreciseLocation() {
|
||||
SecurityIcon(
|
||||
isLowEntropyKey = true,
|
||||
isPreciseLocation = true,
|
||||
isMqttEnabled = false
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(name = "Insecure Channel without Precise Location - Yellow Unlock")
|
||||
@Composable
|
||||
private fun PreviewInsecureChannelWithoutPreciseLocation() {
|
||||
SecurityIcon(
|
||||
isLowEntropyKey = true,
|
||||
isPreciseLocation = false,
|
||||
isMqttEnabled = false
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(name = "MQTT Enabled - Red Warning")
|
||||
@Composable
|
||||
private fun PreviewMqttEnabled() {
|
||||
SecurityIcon(
|
||||
isLowEntropyKey = false,
|
||||
isPreciseLocation = false,
|
||||
isMqttEnabled = true
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(name = "All Security Icons")
|
||||
@Composable
|
||||
private fun PreviewAllSecurityIcons() {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text(
|
||||
"Security Icons Preview",
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
channelByNameMap[channelName]?.let { channelSetting ->
|
||||
SecurityIcon(
|
||||
channel = Channel(channelSetting, channelSet.loraConfig),
|
||||
baseContentDescription = baseContentDescription,
|
||||
externalOnClick = externalOnClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
SecurityIcon(
|
||||
isLowEntropyKey = false,
|
||||
isPreciseLocation = false,
|
||||
isMqttEnabled = false
|
||||
)
|
||||
Text("Secure")
|
||||
}
|
||||
/**
|
||||
* Displays a help dialog explaining the meaning of different security icons.
|
||||
* The dialog can show details for a specific [SecurityState] or a list of all states.
|
||||
*
|
||||
* @param securityState The initial security state to display contextually.
|
||||
* @param onDismiss Lambda invoked when the dialog is dismissed.
|
||||
*/
|
||||
@Composable
|
||||
private fun SecurityHelpDialog(
|
||||
securityState: SecurityState,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
var showAll by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
SecurityIcon(
|
||||
isLowEntropyKey = true,
|
||||
isPreciseLocation = true,
|
||||
isMqttEnabled = false
|
||||
AlertDialog(
|
||||
modifier = if (showAll) {
|
||||
Modifier.fillMaxSize()
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
Text(
|
||||
if (showAll) {
|
||||
stringResource(R.string.security_icon_help_title_all)
|
||||
} else {
|
||||
stringResource(R.string.security_icon_help_title)
|
||||
},
|
||||
)
|
||||
Text("Insecure + Precise Location")
|
||||
}
|
||||
},
|
||||
text = {
|
||||
if (showAll) {
|
||||
AllSecurityStates()
|
||||
} else {
|
||||
ContextualSecurityState(securityState)
|
||||
}
|
||||
},
|
||||
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))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
SecurityIcon(
|
||||
isLowEntropyKey = true,
|
||||
isPreciseLocation = false,
|
||||
isMqttEnabled = false
|
||||
)
|
||||
Text("Insecure")
|
||||
}
|
||||
/**
|
||||
* Displays details for a single, specific security state within the help dialog.
|
||||
*
|
||||
* @param securityState The state to display.
|
||||
*/
|
||||
@Composable
|
||||
private fun ContextualSecurityState(securityState: SecurityState) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
SecurityIconDisplay(
|
||||
icon = securityState.icon,
|
||||
mainIconTint = securityState.color,
|
||||
contentDescription = stringResource(securityState.descriptionResId),
|
||||
modifier = Modifier.size(48.dp),
|
||||
badgeIcon = securityState.badgeIcon,
|
||||
badgeIconColor = securityState.badgeIconColor,
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Text(
|
||||
text = stringResource(securityState.helpTextResId),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
SecurityIcon(
|
||||
isLowEntropyKey = false,
|
||||
isPreciseLocation = false,
|
||||
isMqttEnabled = true
|
||||
)
|
||||
Text("MQTT Enabled")
|
||||
/**
|
||||
* Displays a list of all possible security states with their icons and descriptions
|
||||
* within the help dialog. Iterates over `SecurityState.entries` which is provided
|
||||
* by the enum class.
|
||||
*/
|
||||
@Composable
|
||||
private fun AllSecurityStates() {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
SecurityState.entries.forEach { state -> // Uses enum entries
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
SecurityIconDisplay(
|
||||
icon = state.icon,
|
||||
mainIconTint = state.color,
|
||||
contentDescription = stringResource(state.descriptionResId),
|
||||
modifier = Modifier.size(48.dp),
|
||||
badgeIcon = state.badgeIcon,
|
||||
badgeIconColor = state.badgeIconColor,
|
||||
)
|
||||
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 != SecurityState.entries.lastOrNull()) {
|
||||
HorizontalDivider(modifier = Modifier.padding(top = 8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Preview functions for development and testing
|
||||
|
||||
@Preview(name = "Secure Channel Icon")
|
||||
@Composable
|
||||
private fun PreviewSecureChannel() {
|
||||
SecurityIcon(securityState = SecurityState.SECURE)
|
||||
}
|
||||
|
||||
@Preview(name = "Insecure Precise Icon")
|
||||
@Composable
|
||||
private fun PreviewInsecureChannelWithPreciseLocation() {
|
||||
SecurityIcon(securityState = SecurityState.INSECURE_PRECISE_ONLY)
|
||||
}
|
||||
|
||||
@Preview(name = "Insecure Channel Icon")
|
||||
@Composable
|
||||
private fun PreviewInsecureChannelWithoutPreciseLocation() {
|
||||
SecurityIcon(securityState = SecurityState.INSECURE_NO_PRECISE)
|
||||
}
|
||||
|
||||
@Preview(name = "MQTT Enabled Icon")
|
||||
@Composable
|
||||
private fun PreviewMqttEnabled() {
|
||||
SecurityIcon(securityState = SecurityState.INSECURE_PRECISE_MQTT_WARNING)
|
||||
}
|
||||
|
||||
@Preview(name = "All Security Icons with Dialog")
|
||||
@Composable
|
||||
private fun PreviewAllSecurityIconsWithDialog() {
|
||||
var showHelpDialogFor by remember { mutableStateOf<SecurityState?>(null) }
|
||||
val stateLabels = remember { // Using SecurityState.entries to build the map keys
|
||||
mapOf(
|
||||
SecurityState.SECURE to "Secure",
|
||||
SecurityState.INSECURE_NO_PRECISE to "Insecure (No Precise Location)",
|
||||
SecurityState.INSECURE_PRECISE_ONLY to "Insecure (Precise Location Only)",
|
||||
SecurityState.INSECURE_PRECISE_MQTT_WARNING to "Insecure (Precise Location + MQTT Warning)",
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "Security Icons Preview (Click for Help)",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
)
|
||||
|
||||
SecurityState.entries.forEach { state -> // Iterate over enum entries
|
||||
val label =
|
||||
stateLabels[state] ?: "Unknown State (${state.name})" // Fallback to enum name
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
SecurityIcon(
|
||||
securityState = state,
|
||||
externalOnClick = { showHelpDialogFor = state },
|
||||
)
|
||||
Text(label)
|
||||
}
|
||||
}
|
||||
showHelpDialogFor?.let {
|
||||
SecurityHelpDialog(
|
||||
securityState = it,
|
||||
onDismiss = { showHelpDialogFor = null },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -725,12 +725,59 @@
|
|||
<string name="app_intro_done_button">Done</string>
|
||||
<string name="app_intro_skip_button">Skip</string>
|
||||
<string name="security_icon_description">Security Status</string>
|
||||
<string name="security_icon_secure"> - Secure</string>
|
||||
<string name="security_icon_warning"> - WARN, insecure MQTT</string>
|
||||
<string name="security_icon_insecure"> - WARN, low entropy key</string>
|
||||
<string name="security_icon_secure">Secure</string>
|
||||
<string name="security_icon_insecure_precise"> - WARNING, insecure location enabled</string>
|
||||
<string name="security_icon_badge_warning_description">Warning Badge</string>
|
||||
<string name="unknown_channel">Unknown Channel</string>
|
||||
<string name="warning">Warning</string>
|
||||
<string name="overflow_menu">Overflow menu</string>
|
||||
<string name="uv_lux">UV Lux</string>
|
||||
<string name="unknown">Unknown</string>
|
||||
<string name="map_type_normal">Normal</string>
|
||||
<string name="map_type_satellite">Satellite</string>
|
||||
<string name="map_type_terrain">Terrain</string>
|
||||
<string name="map_type_hybrid">Hybrid</string>
|
||||
<string name="manage_map_layers">Manage Map Layers</string>
|
||||
<string name="map_layers_title">Map Layers</string>
|
||||
<string name="no_map_layers_loaded">No custom layers loaded.</string>
|
||||
<string name="add_layer_button">Add Layer</string>
|
||||
<string name="hide_layer">Hide Layer</string>
|
||||
<string name="show_layer">Show Layer</string>
|
||||
<string name="remove_layer">Remove Layer</string>
|
||||
<string name="add_layer">Add Layer</string>
|
||||
<string name="nodes_at_this_location">Nodes at this location</string>
|
||||
<string name="selected_map_type">Selected Map Type</string>
|
||||
<string name="manage_custom_tile_sources">Manage Custom Tile Sources</string>
|
||||
<string name="add_custom_tile_source">Add Custom Tile Source</string>
|
||||
<string name="no_custom_tile_sources_found">No Custom Tile Sources</string>
|
||||
<string name="edit_custom_tile_source">Edit Custom Tile Source</string>
|
||||
<string name="delete_custom_tile_source">Delete Custom Tile Source</string>
|
||||
<string name="name_cannot_be_empty">Name cannot be empty.</string>
|
||||
<string name="provider_name_exists">Provider name exists.</string>
|
||||
<string name="url_cannot_be_empty">URL cannot be empty.</string>
|
||||
<string name="url_must_contain_placeholders">URL must contain placeholders.</string>
|
||||
<string name="url_template">URL Template</string>
|
||||
<string name="url_template_hint">e.g. https://a.tile.openstreetmap.org/{z}/{x}/{y}.png</string>
|
||||
|
||||
<string name="security_icon_help_green_lock">A green lock means the channel is securely encrypted with either a 128 or 256 bit AES key.</string>
|
||||
|
||||
<!-- INSECURE_NO_PRECISE State (Yellow Open Lock) -->
|
||||
<string name="security_icon_insecure_no_precise">Insecure Channel, Not Precise</string>
|
||||
<string name="security_icon_help_yellow_open_lock">A yellow open lock means the channel is not securely encrypted, is not used for precise location data, and uses either no key at all or a 1 byte known key.</string>
|
||||
|
||||
<!-- INSECURE_PRECISE_ONLY State (Red Open Lock) -->
|
||||
<string name="security_icon_insecure_precise_only">Insecure Channel, Precise Location</string>
|
||||
<string name="security_icon_help_red_open_lock">A red open lock means the channel is not securely encrypted, is used for precise location data, and uses either no key at all or a 1 byte known key.</string>
|
||||
|
||||
<!-- INSECURE_PRECISE_MQTT_WARNING State (Red Open Lock with Warning Badge) -->
|
||||
<string name="security_icon_warning_precise_mqtt">Warning: Insecure, Precise Location & MQTT Uplink</string>
|
||||
<string name="security_icon_help_warning_precise_mqtt">A red open lock with a warning means the channel is not securely encrypted, is used for precise location data which is being uplinked to the internet via MQTT, and uses either no key at all or a 1 byte known key.</string>
|
||||
|
||||
<!-- Security Help Dialog Titles and Buttons (from your existing code structure) -->
|
||||
<string name="security_icon_help_title">Channel Security</string>
|
||||
<string name="security_icon_help_title_all">Channel Security Meanings</string>
|
||||
<string name="security_icon_help_show_all">Show All Meanings</string>
|
||||
<string name="security_icon_help_show_less">Show Current Status</string>
|
||||
<string name="security_icon_help_dismiss">Dismiss</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue