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 2ff35864b..9999edaaa 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -176,8 +176,7 @@ data class Contact( val messageCount: Int, val isMuted: Boolean, val isUnmessageable: Boolean, - val nodeColors: Pair? = null, - val isDefaultPSK: Boolean? = false + val nodeColors: Pair? = null ) @Suppress("LongParameterList", "LargeClass") @@ -520,15 +519,6 @@ class UIViewModel @Inject constructor( } else { user.longName } - val isDefaultPSK = if (toBroadcast) { - val _channel = channelSet.getChannel(data.channel) - val isDefaultPSK = (_channel?.settings?.psk?.size() == 1 && - _channel.settings.psk.byteAt(0) == 1.toByte()) || - _channel?.settings?.psk?.toByteArray()?.isEmpty() == true - isDefaultPSK - } else { - false - } Contact( contactKey = contactKey, @@ -544,8 +534,7 @@ class UIViewModel @Inject constructor( node.colors } else { null - }, - isDefaultPSK = isDefaultPSK + } ) } }.stateIn( 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 6d8699f4b..fd44cc137 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Main.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Main.kt @@ -506,7 +506,7 @@ private fun MainMenuActions( IconButton(onClick = { showMenu = true }) { Icon( imageVector = Icons.Default.MoreVert, - contentDescription = "Overflow menu", + contentDescription = stringResource(R.string.overflow_menu), ) } diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/components/IndoorAirQuality.kt b/app/src/main/java/com/geeksville/mesh/ui/common/components/IndoorAirQuality.kt index 6a0f6b7da..ac87c4f33 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/common/components/IndoorAirQuality.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/common/components/IndoorAirQuality.kt @@ -129,7 +129,7 @@ fun IndoorAirQuality(iaq: Int, displayMode: IaqDisplayMode = IaqDisplayMode.Pill ) Icon( imageVector = if (iaq < 100) Icons.Default.ThumbUp else Icons.Filled.Warning, - contentDescription = "AQI Icon", + contentDescription = stringResource(R.string.air_quality_icon), tint = Color.White ) } diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/components/ScannedQrCodeDialog.kt b/app/src/main/java/com/geeksville/mesh/ui/common/components/ScannedQrCodeDialog.kt index 41aeebc65..6cf87d36e 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/common/components/ScannedQrCodeDialog.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/common/components/ScannedQrCodeDialog.kt @@ -136,6 +136,7 @@ fun ScannedQrCodeDialog( ) } itemsIndexed(channelSet.settingsList) { index, channel -> + val channelObj = Channel(channel, channelSet.loraConfig) ChannelSelection( index = index, title = channel.name.ifEmpty { modemPresetName }, @@ -146,6 +147,7 @@ fun ScannedQrCodeDialog( channelSelections[index] = it } }, + channel = channelObj, ) } diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/components/SecurityIcon.kt b/app/src/main/java/com/geeksville/mesh/ui/common/components/SecurityIcon.kt new file mode 100644 index 000000000..c8e1fa603 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/common/components/SecurityIcon.kt @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2025 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.geeksville.mesh.ui.common.components + +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.material.icons.Icons +import androidx.compose.material.icons.filled.Lock +import androidx.compose.material.icons.filled.Warning +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.foundation.layout.Arrangement +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 +import com.geeksville.mesh.R +import com.geeksville.mesh.model.Channel +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. + * + * @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 + */ +@Composable +fun SecurityIcon( + isLowEntropyKey: Boolean, + isPreciseLocation: Boolean = false, + isMqttEnabled: Boolean = false, + contentDescription: String = stringResource(id = R.string.security_icon_description) +) { + 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 + ) +} + +fun Channel.isLowEntropyKey(): Boolean = settings.psk.size() <= 1 +fun Channel.isPreciseLocation(): Boolean = settings.getModuleSettings().positionPrecision == PRECISE_POSITION_BITS +fun Channel.isMqttEnabled(): Boolean = settings.uplinkEnabled + +@Composable +fun SecurityIcon( + channel: Channel, + contentDescription: String = stringResource(id = R.string.security_icon_description) +) = SecurityIcon( + channel.isLowEntropyKey(), + channel.isPreciseLocation(), + channel.isMqttEnabled(), + contentDescription +) + +@Composable +fun SecurityIcon( + channelSet: AppOnlyProtos.ChannelSet, + channelIndex: Int, + contentDescription: String = stringResource(id = R.string.security_icon_description) +) { + val channel = channelSet.getChannel(channelIndex) ?: return + SecurityIcon(channel, contentDescription) +} + +@Composable +fun SecurityIcon( + channelSet: AppOnlyProtos.ChannelSet, + channelName: String, + contentDescription: String = stringResource(id = R.string.security_icon_description) +) { + val channel = channelSet.settingsList.find { + Channel(it, channelSet.loraConfig).name == channelName + }?.let { Channel(it, channelSet.loraConfig) } ?: return + SecurityIcon(channel, contentDescription) +} + +// 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 + ) + + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + SecurityIcon( + isLowEntropyKey = false, + isPreciseLocation = false, + isMqttEnabled = false + ) + Text("Secure") + } + + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + SecurityIcon( + isLowEntropyKey = true, + isPreciseLocation = true, + isMqttEnabled = false + ) + Text("Insecure + Precise Location") + } + + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + SecurityIcon( + isLowEntropyKey = true, + isPreciseLocation = false, + isMqttEnabled = false + ) + Text("Insecure") + } + + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + SecurityIcon( + isLowEntropyKey = false, + isPreciseLocation = false, + isMqttEnabled = true + ) + Text("MQTT Enabled") + } + } +} diff --git a/app/src/main/java/com/geeksville/mesh/ui/contact/ContactItem.kt b/app/src/main/java/com/geeksville/mesh/ui/contact/ContactItem.kt index b82d42a7a..2f81890a0 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/contact/ContactItem.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/contact/ContactItem.kt @@ -42,16 +42,16 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp +import com.geeksville.mesh.AppOnlyProtos import com.geeksville.mesh.R import com.geeksville.mesh.model.Contact import com.geeksville.mesh.ui.common.theme.AppTheme -import androidx.compose.ui.graphics.vector.ImageVector +import com.geeksville.mesh.ui.common.components.SecurityIcon @Suppress("LongMethod") @Composable @@ -61,6 +61,7 @@ fun ContactItem( modifier: Modifier = Modifier, onClick: () -> Unit = {}, onLongClick: () -> Unit = {}, + channels: AppOnlyProtos.ChannelSet? = null, ) = with(contact) { Card( modifier = modifier @@ -111,28 +112,29 @@ fun ContactItem( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, ) { - Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.weight(1f)) { - Text( - text = longName, - ) + Text( + text = longName, + modifier = Modifier.weight(1f) + ) + Row(verticalAlignment = Alignment.CenterVertically) { // Show unlock icon for broadcast with default PSK val isBroadcast = contact.contactKey.getOrNull(1) == '^' || contact.contactKey.endsWith("^all") || contact.contactKey.endsWith("^broadcast") - - if (isBroadcast && isDefaultPSK == true) { - Spacer(modifier = Modifier.width(10.dp)) - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_lock_open_right_24), - contentDescription = "Unlocked" - ) + if (isBroadcast && channels != null) { + val channelIndex = contact.contactKey[0].digitToIntOrNull() + channelIndex?.let { index -> + SecurityIcon(channels, index) + } + Spacer(modifier = Modifier.width(8.dp)) } + Text( + text = lastMessageTime.orEmpty(), + color = MaterialTheme.colorScheme.onSurface, + fontSize = MaterialTheme.typography.labelLarge.fontSize, + modifier = Modifier.width(80.dp), + ) } - Text( - text = lastMessageTime.orEmpty(), - color = MaterialTheme.colorScheme.onSurface, - fontSize = MaterialTheme.typography.labelLarge.fontSize, - ) } Row( modifier = Modifier diff --git a/app/src/main/java/com/geeksville/mesh/ui/contact/Contacts.kt b/app/src/main/java/com/geeksville/mesh/ui/contact/Contacts.kt index 91764c11d..ab7396e64 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/contact/Contacts.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/contact/Contacts.kt @@ -58,6 +58,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.geeksville.mesh.AppOnlyProtos import com.geeksville.mesh.R import com.geeksville.mesh.model.Contact import com.geeksville.mesh.model.UIViewModel @@ -136,12 +137,14 @@ fun ContactsScreen( } } ) { paddingValues -> + val channels by uiViewModel.channels.collectAsStateWithLifecycle() ContactListView( contacts = contacts, selectedList = selectedContactKeys, onClick = onContactClick, onLongClick = onContactLongClick, - contentPadding = paddingValues + contentPadding = paddingValues, + channels = channels ) } DeleteConfirmationDialog( @@ -302,7 +305,7 @@ fun SelectionToolbar( title = { Text(text = "$selectedCount") }, navigationIcon = { IconButton(onClick = onCloseSelection) { - Icon(Icons.Default.Close, contentDescription = "Close selection") + Icon(Icons.Default.Close, contentDescription = stringResource(R.string.close_selection)) } }, actions = { @@ -321,10 +324,10 @@ fun SelectionToolbar( ) } IconButton(onClick = onDeleteSelected) { - Icon(Icons.Default.Delete, contentDescription = "Delete selected") + Icon(Icons.Default.Delete, contentDescription = stringResource(R.string.delete_selection)) } IconButton(onClick = onSelectAll) { - Icon(Icons.Default.SelectAll, contentDescription = "Select all") + Icon(Icons.Default.SelectAll, contentDescription = stringResource(R.string.select_all)) } } ) @@ -336,7 +339,8 @@ fun ContactListView( selectedList: List, onClick: (Contact) -> Unit, onLongClick: (Contact) -> Unit, - contentPadding: PaddingValues + contentPadding: PaddingValues, + channels: AppOnlyProtos.ChannelSet? = null ) { val haptics = LocalHapticFeedback.current LazyColumn( @@ -355,6 +359,7 @@ fun ContactListView( onLongClick(contact) haptics.performHapticFeedback(HapticFeedbackType.LongPress) }, + channels = channels ) } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/debug/Debug.kt b/app/src/main/java/com/geeksville/mesh/ui/debug/Debug.kt index 8454288c8..fda133ed8 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/debug/Debug.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/debug/Debug.kt @@ -494,7 +494,7 @@ private fun DebugMenuActionsPreview() { ) { Icon( imageVector = Icons.Outlined.FileDownload, - contentDescription = "Export Logs" + contentDescription = stringResource(id = R.string.debug_logs_export) ) } IconButton( @@ -503,7 +503,7 @@ private fun DebugMenuActionsPreview() { ) { Icon( imageVector = Icons.Default.Delete, - contentDescription = "Clear All" + contentDescription = stringResource(id = R.string.debug_clear) ) } } @@ -559,7 +559,7 @@ private fun DebugScreenEmptyPreview() { ) Icon( imageVector = Icons.TwoTone.FilterAltOff, - contentDescription = "Filter" + contentDescription = stringResource(id = R.string.debug_filters) ) } } @@ -709,7 +709,7 @@ fun DebugMenuActions( ) { Icon( imageVector = Icons.Outlined.FileDownload, - contentDescription = "Export Logs" + contentDescription = stringResource(id = R.string.debug_logs_export) ) } IconButton( @@ -718,7 +718,7 @@ fun DebugMenuActions( ) { Icon( imageVector = Icons.Default.Delete, - contentDescription = "Clear All" + contentDescription = stringResource(id = R.string.debug_clear) ) } if (showDeleteLogsDialog) { diff --git a/app/src/main/java/com/geeksville/mesh/ui/debug/DebugFilters.kt b/app/src/main/java/com/geeksville/mesh/ui/debug/DebugFilters.kt index 1398a65e7..51e5c45c9 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/debug/DebugFilters.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/debug/DebugFilters.kt @@ -101,7 +101,7 @@ fun DebugCustomFilterInput( ) { Icon( imageVector = Icons.Default.Add, - contentDescription = "Add filter" + contentDescription = stringResource(id = R.string.debug_filter_add) ) } } @@ -151,7 +151,7 @@ internal fun DebugPresetFilters( leadingIcon = { if (filter in filterTexts) { Icon( imageVector = Icons.Filled.Done, - contentDescription = "Done icon", + contentDescription = stringResource(id = R.string.debug_filter_included), ) } } @@ -194,7 +194,7 @@ internal fun DebugFilterBar( } else { Icons.TwoTone.FilterAltOff }, - contentDescription = "Filter" + contentDescription = stringResource(id = R.string.debug_filters) ) } } @@ -270,7 +270,7 @@ internal fun DebugActiveFilters( ) { Icon( imageVector = Icons.Default.Clear, - contentDescription = "Clear all filters" + contentDescription = stringResource(id = R.string.debug_filter_clear) ) } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/debug/DebugSearch.kt b/app/src/main/java/com/geeksville/mesh/ui/debug/DebugSearch.kt index 2787aa1fd..7c3771bf9 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/debug/DebugSearch.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/debug/DebugSearch.kt @@ -83,7 +83,7 @@ internal fun DebugSearchNavigation( ) { Icon( imageVector = Icons.Default.KeyboardArrowUp, - contentDescription = "Previous match", + contentDescription = stringResource(R.string.debug_search_prev), modifier = Modifier.size(16.dp) ) } @@ -94,7 +94,7 @@ internal fun DebugSearchNavigation( ) { Icon( imageVector = Icons.Default.KeyboardArrowDown, - contentDescription = "Next match", + contentDescription = stringResource(R.string.debug_search_next), modifier = Modifier.size(16.dp) ) } @@ -143,7 +143,7 @@ internal fun DebugSearchBar( ) { Icon( imageVector = Icons.Default.Clear, - contentDescription = "Clear search", + contentDescription = stringResource(R.string.debug_search_clear), modifier = Modifier.size(16.dp) ) } diff --git a/app/src/main/java/com/geeksville/mesh/ui/message/Message.kt b/app/src/main/java/com/geeksville/mesh/ui/message/Message.kt index a2ee72c77..781dee588 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/message/Message.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/message/Message.kt @@ -77,7 +77,6 @@ import androidx.compose.ui.platform.ClipEntry import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardCapitalization @@ -87,6 +86,7 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.geeksville.mesh.AppOnlyProtos import com.geeksville.mesh.DataPacket import com.geeksville.mesh.R import com.geeksville.mesh.database.entity.QuickChatAction @@ -99,7 +99,7 @@ import com.geeksville.mesh.ui.node.components.NodeKeyStatusIcon import com.geeksville.mesh.ui.node.components.NodeMenuAction import com.geeksville.mesh.ui.sharing.SharedContactDialog import kotlinx.coroutines.launch -import androidx.compose.ui.graphics.vector.ImageVector +import com.geeksville.mesh.ui.common.components.SecurityIcon private const val MESSAGE_CHARACTER_LIMIT = 200 private const val SNIPPET_CHARACTER_LIMIT = 50 @@ -123,22 +123,18 @@ internal fun MessageScreen( val channelIndex = contactKey[0].digitToIntOrNull() val nodeId = contactKey.substring(1) val channels by viewModel.channels.collectAsStateWithLifecycle() - val channelName by remember(channelIndex) { + val unknownChannelText = stringResource(id = R.string.unknown_channel) + val channelName by remember(channelIndex, unknownChannelText) { derivedStateOf { channelIndex?.let { val channel = channels.getChannel(it) - val name = channel?.name ?: "Unknown Channel" - // Check if PSK is the default (base64 'AQ==', i.e., single byte 0x01) - val isDefaultPSK = (channel?.settings?.psk?.size() == 1 && - channel.settings.psk.byteAt(0) == 1.toByte()) || - channel?.psk?.toByteArray()?.isEmpty() == true - Pair(name, isDefaultPSK) - } ?: Pair("Unknown Channel", false) + channel?.name ?: unknownChannelText + } ?: unknownChannelText } } - val (channelTitle, isDefaultPsk) = channelName + val title = when (nodeId) { - DataPacket.ID_BROADCAST -> channelTitle + DataPacket.ID_BROADCAST -> channelName else -> viewModel.getUser(nodeId).longName } viewModel.setTitle(title) @@ -212,8 +208,7 @@ internal fun MessageScreen( } } } else { - MessageTopBar(title, channelIndex, mismatchKey, onNavigateBack, isDefaultPsk = isDefaultPsk - ) + MessageTopBar(title, channelIndex, mismatchKey, onNavigateBack, channels, channelIndex) } }, ) { padding -> @@ -455,20 +450,18 @@ private fun MessageTopBar( channelIndex: Int?, mismatchKey: Boolean = false, onNavigateBack: () -> Unit, - isDefaultPsk: Boolean = false + channels: AppOnlyProtos.ChannelSet, + channelIndexParam: Int?, ) = TopAppBar( title = { Row(verticalAlignment = Alignment.CenterVertically) { Text(text = title) - if (isDefaultPsk - ) { - Spacer(modifier = Modifier.width(10.dp)) - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_lock_open_right_24), - contentDescription = "Unlocked" - ) - } + Spacer(modifier = Modifier.width(10.dp)) + + channelIndexParam?.let { index -> + SecurityIcon(channels, index) } + } }, navigationIcon = { IconButton(onClick = onNavigateBack) { diff --git a/app/src/main/java/com/geeksville/mesh/ui/radioconfig/RadioConfig.kt b/app/src/main/java/com/geeksville/mesh/ui/radioconfig/RadioConfig.kt index 7407c942e..a36b89853 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/radioconfig/RadioConfig.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/radioconfig/RadioConfig.kt @@ -255,7 +255,7 @@ private fun NavButton(@StringRes title: Int, enabled: Boolean, onClick: () -> Un ) { Icon( imageVector = Icons.TwoTone.Warning, - contentDescription = "warning", + contentDescription = stringResource(id = R.string.warning), modifier = Modifier.padding(end = 8.dp) ) Text( @@ -263,7 +263,7 @@ private fun NavButton(@StringRes title: Int, enabled: Boolean, onClick: () -> Un ) Icon( imageVector = Icons.TwoTone.Warning, - contentDescription = "warning", + contentDescription = stringResource(id = R.string.warning), modifier = Modifier.padding(start = 8.dp) ) } diff --git a/app/src/main/java/com/geeksville/mesh/ui/radioconfig/components/ChannelSettingsItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/radioconfig/components/ChannelSettingsItemList.kt index 316e97c85..c85aef515 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/radioconfig/components/ChannelSettingsItemList.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/radioconfig/components/ChannelSettingsItemList.kt @@ -62,7 +62,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -80,7 +79,7 @@ import com.geeksville.mesh.ui.common.components.dragContainer import com.geeksville.mesh.ui.common.components.dragDropItemsIndexed import com.geeksville.mesh.ui.common.components.rememberDragDropState import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel -import androidx.compose.ui.graphics.vector.ImageVector +import com.geeksville.mesh.ui.common.components.SecurityIcon @Composable private fun ChannelItem( @@ -125,20 +124,15 @@ fun ChannelCard( enabled: Boolean, onEditClick: () -> Unit, onDeleteClick: () -> Unit, - isDefaultPSK: Boolean = false, + channel: Channel, ) = ChannelItem( index = index, title = title, enabled = enabled, onClick = onEditClick, ) { - if (isDefaultPSK) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_lock_open_right_24), - contentDescription = "Unlocked" - ) - Spacer(modifier = Modifier.width(10.dp)) - } + SecurityIcon(channel) + Spacer(modifier = Modifier.width(10.dp)) IconButton(onClick = { onDeleteClick() }) { Icon( imageVector = Icons.TwoTone.Close, @@ -155,19 +149,15 @@ fun ChannelSelection( enabled: Boolean, isSelected: Boolean, onSelected: (Boolean) -> Unit, - isDefaultPSK: Boolean = false, + channel: Channel, ) = ChannelItem( index = index, title = title, enabled = enabled, onClick = {}, ) { - if (isDefaultPSK) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_lock_open_right_24), - contentDescription = "Unlocked" - ) - } + SecurityIcon(channel) + Spacer(modifier = Modifier.width(10.dp)) Checkbox( enabled = enabled, checked = isSelected, @@ -292,15 +282,14 @@ fun ChannelSettingsItemList( items = settingsListInput, dragDropState = dragDropState, ) { index, channel, isDragging -> - val isDefaultPSK = (channel.psk.size() == 1 && channel.psk.byteAt(0) == 1.toByte()) || - channel.psk.toByteArray().isEmpty() + val channelObj = Channel(channel, loraConfig) ChannelCard( index = index, title = channel.name.ifEmpty { primaryChannel.name }, enabled = enabled, onEditClick = { showEditChannelDialog = index }, onDeleteClick = { settingsListInput.removeAt(index) }, - isDefaultPSK = isDefaultPSK + channel = channelObj ) if (index == 0 && !isDragging) { Text( diff --git a/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt b/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt index 00536b7ea..e2dca8861 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt @@ -448,8 +448,7 @@ private fun ChannelListView( AdaptiveTwoPane( first = { channelSet.settingsList.forEachIndexed { index, channel -> - val isDefaultPSK = (channel.psk.size() == 1 && channel.psk.byteAt(0) == 1.toByte()) || - channel.psk.toByteArray().isEmpty() + val channelObj = Channel(channel, channelSet.loraConfig) val displayTitle = channel.name.ifEmpty { modemPresetName } ChannelSelection( @@ -462,7 +461,7 @@ private fun ChannelListView( channelSelections[index] = it } }, - isDefaultPSK = isDefaultPSK + channel = channelObj ) } OutlinedButton( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1a2931e37..7fc9e0c74 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -167,6 +167,12 @@ Filters Active filters Search in logs… + Next match + Previous match + Clear search + Add filter + Filter included + Clear all filters Clear Logs Match Any | All Match All | Any @@ -219,6 +225,8 @@ Delete for everyone Delete for me Select all + Close selection + Delete selected Long Range / Slow Style Selection Download Region @@ -604,6 +612,7 @@ Environment metrics use Fahrenheit Air quality metrics module enabled Air quality metrics update interval (seconds) + Air quality icon Power metrics module enabled Power metrics update interval (seconds) Power metrics on-screen enabled @@ -711,4 +720,12 @@ Next Done Skip + Security Status + - Secure + - WARN, insecure MQTT + - WARN, low entropy key + - WARNING, insecure location enabled + Unknown Channel + Warning + Overflow menu