refactor: convert ChannelSet to protobuf extensions

This commit is contained in:
andrekir 2023-10-07 08:22:12 -03:00 committed by Andre K
parent 3288b07e5e
commit 4e7ea67da0
9 changed files with 103 additions and 116 deletions

View file

@ -3,76 +3,64 @@ package com.geeksville.mesh.model
import android.graphics.Bitmap
import android.net.Uri
import android.util.Base64
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.AppOnlyProtos
import com.geeksville.mesh.AppOnlyProtos.ChannelSet
import com.geeksville.mesh.android.BuildUtils.errormsg
import com.google.zxing.BarcodeFormat
import com.google.zxing.MultiFormatWriter
import com.journeyapps.barcodescanner.BarcodeEncoder
import java.net.MalformedURLException
import kotlin.jvm.Throws
data class ChannelSet(
val protobuf: AppOnlyProtos.ChannelSet = AppOnlyProtos.ChannelSet.getDefaultInstance()
) : Logging {
companion object {
internal const val URL_PREFIX = "https://meshtastic.org/e/#"
private const val BASE64FLAGS = Base64.URL_SAFE + Base64.NO_WRAP + Base64.NO_PADDING
const val prefix = "https://meshtastic.org/e/#"
/**
* Return a [ChannelSet] that represents the URL
* @throws MalformedURLException when not recognized as a valid Meshtastic URL
*/
@Throws(MalformedURLException::class)
fun Uri.toChannelSet(): ChannelSet {
val urlStr = this.toString()
private const val base64Flags = Base64.URL_SAFE + Base64.NO_WRAP + Base64.NO_PADDING
val pathRegex = Regex("$URL_PREFIX(.*)", RegexOption.IGNORE_CASE)
val (base64) = pathRegex.find(urlStr)?.destructured
?: throw MalformedURLException("Not a Meshtastic URL: ${urlStr.take(40)}")
val bytes = Base64.decode(base64, BASE64FLAGS)
private fun urlToChannels(url: Uri): AppOnlyProtos.ChannelSet {
val urlStr = url.toString()
val pathRegex = Regex("$prefix(.*)", RegexOption.IGNORE_CASE)
val (base64) = pathRegex.find(urlStr)?.destructured
?: throw MalformedURLException("Not a meshtastic URL: ${urlStr.take(40)}")
val bytes = Base64.decode(base64, base64Flags)
return AppOnlyProtos.ChannelSet.parseFrom(bytes)
}
}
constructor(url: Uri) : this(urlToChannels(url))
/// Can this channel be changed right now?
var editable = false
/**
* Return the primary channel info
*/
val primaryChannel: Channel?
get() = with(protobuf) {
if (settingsCount > 0) Channel(getSettings(0), loraConfig) else null
}
/// Return an URL that represents the current channel values
/// @param upperCasePrefix - portions of the URL can be upper case to make for more efficient QR codes
fun getChannelUrl(upperCasePrefix: Boolean = false): Uri {
// If we have a valid radio config use it, otherwise use whatever we have saved in the prefs
val channelBytes = protobuf.toByteArray() ?: ByteArray(0) // if unset just use empty
val enc = Base64.encodeToString(channelBytes, base64Flags)
val p = if (upperCasePrefix) prefix.uppercase() else prefix
return Uri.parse("$p$enc")
}
val qrCode
get(): Bitmap? = try {
val multiFormatWriter = MultiFormatWriter()
// We encode as UPPER case for the QR code URL because QR codes are more efficient for that special case
val bitMatrix =
multiFormatWriter.encode(
getChannelUrl(false).toString(),
BarcodeFormat.QR_CODE,
960,
960
)
val barcodeEncoder = BarcodeEncoder()
barcodeEncoder.createBitmap(bitMatrix)
} catch (ex: Throwable) {
errormsg("URL was too complex to render as barcode")
null
}
return ChannelSet.parseFrom(bytes)
}
/**
* Return the primary channel info
*/
val ChannelSet.primaryChannel: Channel?
get() = if (settingsCount > 0) Channel(getSettings(0), loraConfig) else null
/**
* Return a URL that represents the [ChannelSet]
* @param upperCasePrefix portions of the URL can be upper case to make for more efficient QR codes
*/
fun ChannelSet.getChannelUrl(upperCasePrefix: Boolean = false): Uri {
val channelBytes = this.toByteArray() ?: ByteArray(0) // if unset just use empty
val enc = Base64.encodeToString(channelBytes, BASE64FLAGS)
val p = if (upperCasePrefix) URL_PREFIX.uppercase() else URL_PREFIX
return Uri.parse("$p$enc")
}
val ChannelSet.qrCode: Bitmap?
get() = try {
val multiFormatWriter = MultiFormatWriter()
val bitMatrix =
multiFormatWriter.encode(
getChannelUrl(false).toString(),
BarcodeFormat.QR_CODE,
960,
960
)
val barcodeEncoder = BarcodeEncoder()
barcodeEncoder.createBitmap(bitMatrix)
} catch (ex: Throwable) {
errormsg("URL was too complex to render as barcode")
null
}

View file

@ -181,7 +181,7 @@ class RadioConfigViewModel @Inject constructor(
}
private fun setChannels(channelUrl: String) = viewModelScope.launch {
val new = ChannelSet(Uri.parse(channelUrl)).protobuf
val new = Uri.parse(channelUrl).toChannelSet()
val old = radioConfigRepository.channelSetFlow.firstOrNull() ?: return@launch
updateChannels(myNodeNum ?: return@launch, new.settingsList, old.settingsList)
}
@ -308,9 +308,8 @@ class RadioConfigViewModel @Inject constructor(
_deviceProfile.value = protobuf
}
} catch (ex: Exception) {
val error = "${ex.javaClass.simpleName}: ${ex.message}"
errormsg("Import DeviceProfile error: ${ex.message}")
setResponseStateError(error)
setResponseStateError(ex.customMessage)
}
}
@ -329,9 +328,8 @@ class RadioConfigViewModel @Inject constructor(
}
setResponseStateSuccess()
} catch (ex: Exception) {
val error = "${ex.javaClass.simpleName}: ${ex.message}"
errormsg("Can't write file error: ${ex.message}")
setResponseStateError(error)
setResponseStateError(ex.customMessage)
}
}
@ -345,8 +343,11 @@ class RadioConfigViewModel @Inject constructor(
)
setOwner(user.toProto())
}
if (hasChannelUrl()) {
if (hasChannelUrl()) try {
setChannels(channelUrl)
} catch (ex: Exception) {
errormsg("DeviceProfile channel import error", ex)
setResponseStateError(ex.customMessage)
}
if (hasConfig()) {
setConfig(config { device = config.device })
@ -406,6 +407,7 @@ class RadioConfigViewModel @Inject constructor(
}
}
private val Exception.customMessage: String get() = "${javaClass.simpleName}: $message"
private fun setResponseStateError(error: String) {
_radioConfigState.update { it.copy(responseState = ResponseState.Error(error)) }
}

View file

@ -130,8 +130,9 @@ class UIViewModel @Inject constructor(
val moduleConfig: StateFlow<LocalModuleConfig> = _moduleConfig
val module get() = _moduleConfig.value
private val _channels = MutableStateFlow(ChannelSet())
val channels: StateFlow<ChannelSet> = _channels
private val _channels = MutableStateFlow(channelSet {})
val channels: StateFlow<AppOnlyProtos.ChannelSet> get() = _channels
val channelSet get() = channels.value
private val _quickChatActions = MutableStateFlow<List<QuickChatAction>>(emptyList())
val quickChatActions: StateFlow<List<QuickChatAction>> = _quickChatActions
@ -167,7 +168,7 @@ class UIViewModel @Inject constructor(
}
}
radioConfigRepository.channelSetFlow.onEach { channelSet ->
_channels.value = ChannelSet(channelSet)
_channels.value = channelSet
}.launchIn(viewModelScope)
viewModelScope.launch {
@ -396,27 +397,13 @@ class UIViewModel @Inject constructor(
meshService?.setChannel(channel.toByteArray())
}
/**
* Convert the [channels] array to and from [ChannelSet]
*/
private var _channelSet: AppOnlyProtos.ChannelSet
get() = channels.value.protobuf
set(value) {
val new = value.settingsList
val old = channelSet.settingsList
viewModelScope.launch {
getChannelList(new, old).forEach(::setChannel)
radioConfigRepository.replaceAllSettings(new)
// Set the radio config (also updates our saved copy in preferences)
fun setChannels(channelSet: AppOnlyProtos.ChannelSet) = viewModelScope.launch {
getChannelList(channelSet.settingsList, channels.value.settingsList).forEach(::setChannel)
radioConfigRepository.replaceAllSettings(channelSet.settingsList)
val newConfig = config { lora = value.loraConfig }
if (config.lora != newConfig.lora) setConfig(newConfig)
}
}
val channelSet get() = _channelSet
/// Set the radio config (also updates our saved copy in preferences)
fun setChannels(channelSet: ChannelSet) {
this._channelSet = channelSet.protobuf
val newConfig = config { lora = channelSet.loraConfig }
if (config.lora != newConfig.lora) setConfig(newConfig)
}
val provideLocation = object : MutableLiveData<Boolean>(preferences.getBoolean("provide-location", false)) {