refactor: extract EditBase64Preference from EditChannelDialog

closes #944
This commit is contained in:
andrekir
2024-08-22 19:58:24 -03:00
parent d387c7bd04
commit fa85955e85
4 changed files with 128 additions and 72 deletions

View File

@@ -14,12 +14,10 @@ import com.geeksville.mesh.channelSettings
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.Channel
import com.geeksville.mesh.ui.components.ScannedQrCodeDialog
import com.google.protobuf.ByteString
import org.junit.Assert
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.security.SecureRandom
@RunWith(AndroidJUnit4::class)
class ScannedQrCodeDialogTest {
@@ -30,12 +28,7 @@ class ScannedQrCodeDialogTest {
private fun getString(id: Int): String =
InstrumentationRegistry.getInstrumentation().targetContext.getString(id)
private fun getRandomKey(): ByteString {
val random = SecureRandom()
val bytes = ByteArray(32)
random.nextBytes(bytes)
return ByteString.copyFrom(bytes)
}
private fun getRandomKey() = Channel.getRandomKey()
private val channels = channelSet {
settings.add(Channel.default.settings)

View File

@@ -6,6 +6,7 @@ import com.geeksville.mesh.ConfigKt.loRaConfig
import com.geeksville.mesh.ConfigProtos
import com.geeksville.mesh.channelSettings
import com.google.protobuf.ByteString
import java.security.SecureRandom
/** Utility function to make it easy to declare byte arrays - FIXME move someplace better */
fun byteArrayOfInts(vararg ints: Int) = ByteArray(ints.size) { pos -> ints[pos].toByte() }
@@ -37,6 +38,13 @@ data class Channel(
txEnabled = true
}
)
fun getRandomKey(size: Int = 32): ByteString {
val bytes = ByteArray(size)
val random = SecureRandom()
random.nextBytes(bytes)
return ByteString.copyFrom(bytes)
}
}
/// Return the name of our channel as a human readable string. If empty string, assume "Default" per mesh.proto spec

View File

@@ -0,0 +1,105 @@
package com.geeksville.mesh.ui.components
import android.util.Base64
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.LocalContentAlpha
import androidx.compose.material.LocalContentColor
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.twotone.Close
import androidx.compose.material.icons.twotone.Refresh
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.R
import com.geeksville.mesh.model.Channel
import com.google.protobuf.ByteString
import com.google.protobuf.kotlin.toByteString
@Composable
fun EditBase64Preference(
title: String,
value: ByteString,
enabled: Boolean,
keyboardActions: KeyboardActions,
onValueChange: (ByteString) -> Unit,
modifier: Modifier = Modifier,
) {
fun ByteString.encodeToString() = Base64.encodeToString(this.toByteArray(), Base64.NO_WRAP)
fun String.toByteString() = Base64.decode(this, Base64.NO_WRAP).toByteString()
var valueState by remember { mutableStateOf(value.encodeToString()) }
val isError = value.encodeToString() != valueState
// don't update values while the user is editing
var isFocused by remember { mutableStateOf(false) }
LaunchedEffect(value) {
if (!isFocused) {
valueState = value.encodeToString()
}
}
OutlinedTextField(
value = valueState,
onValueChange = {
valueState = it
runCatching { it.toByteString() }.onSuccess(onValueChange)
},
modifier = modifier
.fillMaxWidth()
.onFocusChanged { focusState -> isFocused = focusState.isFocused },
enabled = enabled,
label = { Text(text = title) },
isError = isError,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Password, imeAction = ImeAction.Done
),
keyboardActions = keyboardActions,
trailingIcon = {
IconButton(
onClick = {
val psk = if (isError) value else Channel.getRandomKey()
valueState = psk.encodeToString()
onValueChange(psk)
}
) {
Icon(
imageVector = if (isError) Icons.TwoTone.Close else Icons.TwoTone.Refresh,
contentDescription = stringResource(if (isError) R.string.error else R.string.reset),
tint = if (isError) MaterialTheme.colors.error
else LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
)
}
},
)
}
@Preview(showBackground = true)
@Composable
private fun EditBase64PreferencePreview() {
EditBase64Preference(
title = "Title",
value = Channel.getRandomKey(),
enabled = true,
keyboardActions = KeyboardActions {},
onValueChange = {},
modifier = Modifier.padding(16.dp)
)
}

View File

@@ -1,6 +1,5 @@
package com.geeksville.mesh.ui.components.config
import android.util.Base64
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
@@ -12,17 +11,8 @@ import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.AlertDialog
import androidx.compose.material.Button
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.LocalContentAlpha
import androidx.compose.material.LocalContentColor
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.twotone.Close
import androidx.compose.material.icons.twotone.Refresh
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -40,13 +30,11 @@ import com.geeksville.mesh.R
import com.geeksville.mesh.channelSettings
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.Channel
import com.geeksville.mesh.ui.components.EditBase64Preference
import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PositionPrecisionPreference
import com.geeksville.mesh.ui.components.SwitchPreference
import com.google.accompanist.themeadapter.appcompat.AppCompatTheme
import com.google.protobuf.ByteString
import com.google.protobuf.kotlin.toByteString
import java.security.SecureRandom
@Suppress("LongMethod")
@OptIn(ExperimentalLayoutApi::class)
@@ -58,19 +46,7 @@ fun EditChannelDialog(
modifier: Modifier = Modifier,
modemPresetName: String = "Default",
) {
fun encodeToString(input: ByteString) =
Base64.encodeToString(input.toByteArray() ?: ByteArray(0), Base64.DEFAULT)
var channelInput by remember(channelSettings) { mutableStateOf(channelSettings) }
var pskString by remember(channelInput) { mutableStateOf(encodeToString(channelInput.psk)) }
val pskError = pskString != encodeToString(channelInput.psk)
fun getRandomKey() {
val random = SecureRandom()
val bytes = ByteArray(32)
random.nextBytes(bytes)
channelInput = channelInput.copy { psk = ByteString.copyFrom(bytes) }
}
AlertDialog(
onDismissRequest = onDismissRequest,
@@ -90,49 +66,23 @@ fun EditChannelDialog(
),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
channelInput = channelInput.copy { name = it }
if (channelInput.psk == Channel.default.settings.psk) getRandomKey()
channelInput = channelInput.copy {
name = it
if (psk == Channel.default.settings.psk) psk = Channel.getRandomKey()
}
},
onFocusChanged = { isFocused = it.isFocused },
)
OutlinedTextField(
value = pskString,
onValueChange = {
try {
pskString = it // empty (no crypto), 128 or 256 bit only
val decoded = Base64.decode(it, Base64.DEFAULT).toByteString()
val fullPsk = Channel(channelSettings { psk = decoded }).psk
if (fullPsk.size() in setOf(0, 16, 32)) {
channelInput = channelInput.copy { psk = decoded }
}
} catch (ex: Throwable) {
// Base64 decode failed, pskError true
}
},
modifier = modifier.fillMaxWidth(),
EditBase64Preference(
title = "PSK",
value = channelInput.psk,
enabled = true,
label = { Text("PSK") },
isError = pskError,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Password, imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(onDone = { }),
trailingIcon = {
IconButton(
onClick = {
if (pskError) {
channelInput = channelInput.copy { psk = channelSettings.psk }
pskString = encodeToString(channelInput.psk)
} else getRandomKey()
}
) {
Icon(
if (pskError) Icons.TwoTone.Close else Icons.TwoTone.Refresh,
contentDescription = stringResource(R.string.reset),
tint = if (pskError) MaterialTheme.colors.error
else LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
)
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChange = {
val fullPsk = Channel(channelSettings { psk = it }).psk
if (fullPsk.size() in setOf(0, 16, 32)) {
channelInput = channelInput.copy { psk = it }
}
},
)
@@ -189,7 +139,7 @@ fun EditChannelDialog(
onClick = {
onAddClick(channelInput.copy { name = channelInput.name.trim() })
},
enabled = !pskError,
enabled = true,
) { Text(stringResource(R.string.save)) }
}
}