mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-05-06 13:45:06 -04:00
refactor: extract EditBase64Preference from EditChannelDialog
closes #944
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
@@ -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)) }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user