feat: support add=true in QR codes (#1151)

This commit is contained in:
AddisonTustin 2024-07-28 04:50:54 -07:00 committed by GitHub
parent cc5543f4c9
commit e4c6000a10
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 105 additions and 18 deletions

View file

@ -35,6 +35,7 @@ import com.geeksville.mesh.model.BluetoothViewModel
import com.geeksville.mesh.model.DeviceVersion
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.model.primaryChannel
import com.geeksville.mesh.model.shouldAddChannels
import com.geeksville.mesh.model.toChannelSet
import com.geeksville.mesh.repository.radio.BluetoothInterface
import com.geeksville.mesh.service.*
@ -399,27 +400,37 @@ class MainActivity : AppCompatActivity(), Logging {
}
}
@Suppress("NestedBlockDepth")
private fun perhapsChangeChannel(url: Uri? = requestedChannelUrl) {
// if the device is connected already, process it now
if (url != null && model.isConnected()) {
requestedChannelUrl = null
try {
val channels = url.toChannelSet()
val shouldAdd = url.shouldAddChannels()
val primary = channels.primaryChannel
if (primary == null)
showSnackbar(R.string.channel_invalid)
else {
val dialogMessage = if (!shouldAdd) {
getString(R.string.do_you_want_switch).format(primary.name)
} else {
resources.getQuantityString(
R.plurals.add_channel_from_qr,
channels.settingsCount,
channels.settingsCount
)
}
MaterialAlertDialogBuilder(this)
.setTitle(R.string.new_channel_rcvd)
.setMessage(getString(R.string.do_you_want_switch).format(primary.name))
.setMessage(dialogMessage)
.setNeutralButton(R.string.cancel) { _, _ ->
// Do nothing
}
.setPositiveButton(R.string.accept) { _, _ ->
debug("Setting channel from URL")
try {
model.setChannels(channels)
model.setChannels(channels, !shouldAdd)
} catch (ex: RemoteException) {
errormsg("Couldn't change channel ${ex.message}")
showSnackbar(R.string.cant_change_no_radio)

View file

@ -12,22 +12,48 @@ import java.net.MalformedURLException
import kotlin.jvm.Throws
internal const val URL_PREFIX = "https://meshtastic.org/e/#"
private const val MESHTASTIC_DOMAIN = "meshtastic.org"
private const val MESHTASTIC_CHANNEL_CONFIG_PATH = "/e/"
private const val BASE64FLAGS = Base64.URL_SAFE + Base64.NO_WRAP + Base64.NO_PADDING
/**
* Return a [ChannelSet] that represents the URL
* Return a [ChannelSet] that represents the ChannelSet encoded by the URL.
* @throws MalformedURLException when not recognized as a valid Meshtastic URL
*/
@Throws(MalformedURLException::class)
fun Uri.toChannelSet(): ChannelSet {
val urlStr = this.toString()
if (fragment.isNullOrBlank() ||
!host.equals(MESHTASTIC_DOMAIN, true) ||
!path.equals(MESHTASTIC_CHANNEL_CONFIG_PATH, true)
) {
throw MalformedURLException("Not a valid Meshtastic URL: ${toString().take(40)}")
}
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)
// Older versions of Meshtastic clients (Apple/web) included `?add=true` within the URL fragment.
// This gracefully handles those cases until the newer version are generally available/used.
return ChannelSet.parseFrom(Base64.decode(fragment!!.substringBefore('?'), BASE64FLAGS))
}
return ChannelSet.parseFrom(bytes)
/**
* Return a [Boolean] if the URL indicates the associated [ChannelSet] should be added to the
* existing configuration.
* @throws MalformedURLException when not recognized as a valid Meshtastic URL
*/
@Throws(MalformedURLException::class)
fun Uri.shouldAddChannels(): Boolean {
if (fragment.isNullOrBlank() ||
!host.equals(MESHTASTIC_DOMAIN, true) ||
!path.equals(MESHTASTIC_CHANNEL_CONFIG_PATH, true)
) {
throw MalformedURLException("Not a valid Meshtastic URL: ${toString().take(40)}")
}
// Older versions of Meshtastic clients (Apple/web) included `?add=true` within the URL fragment.
// This gracefully handles those cases until the newer version are generally available/used.
return fragment?.substringAfter('?', "")
?.takeUnless { it.isBlank() }
?.equals("add=true")
?: getBooleanQueryParameter("add", false)
}
/**

View file

@ -78,7 +78,7 @@ fun getInitials(nameIn: String): String {
* Only changes are included in the resulting list.
*
* @param new The updated [ChannelSettings] list.
* @param old The current [ChannelSettings] list (required to disable unused channels).
* @param old The current [ChannelSettings] list (required when disabling unused channels).
* @return A [Channel] list containing only the modified channels.
*/
internal fun getChannelList(
@ -394,13 +394,25 @@ class UIViewModel @Inject constructor(
meshService?.setChannel(channel.toByteArray())
}
// 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)
/**
* Set the radio config (also updates our saved copy in preferences). By default, this will replace
* all channels in the existing radio config. Otherwise, it will append all [ChannelSettings] that
* are unique in [channelSet] to the existing radio config.
*/
fun setChannels(channelSet: AppOnlyProtos.ChannelSet, overwrite: Boolean = true) = viewModelScope.launch {
val newRadioSettings: List<ChannelSettings> = if (overwrite) {
channelSet.settingsList
} else {
// To guarantee consistent ordering, using a LinkedHashSet which iterates through it's
// entries according to the order an item was *first* inserted.
// https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-linked-hash-set/
LinkedHashSet(channels.value.settingsList + channelSet.settingsList).toList()
}
getChannelList(newRadioSettings, channels.value.settingsList).forEach(::setChannel)
radioConfigRepository.replaceAllSettings(newRadioSettings)
val newConfig = config { lora = channelSet.loraConfig }
if (config.lora != newConfig.lora) setConfig(newConfig)
if (overwrite && config.lora != newConfig.lora) setConfig(newConfig)
}
val provideLocation = object : MutableLiveData<Boolean>(preferences.getBoolean("provide-location", false)) {