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

pull/1159/head
AddisonTustin 2024-07-28 04:50:54 -07:00 zatwierdzone przez GitHub
rodzic cc5543f4c9
commit e4c6000a10
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
7 zmienionych plików z 105 dodań i 18 usunięć

Wyświetl plik

@ -12,8 +12,11 @@ plugins {
}
unMock {
keep "android.net.Uri"
keep "android.util.Base64"
keepStartingWith "libcore."
keep "android.net.Uri"
keepAndRename "java.nio.charset.Charsets" to "xjava.nio.charset.Charsets"
}
def keystorePropertiesFile = rootProject.file("keystore.properties")

Wyświetl plik

@ -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)

Wyświetl plik

@ -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)
}
/**

Wyświetl plik

@ -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)) {

Wyświetl plik

@ -207,6 +207,10 @@
<string name="mute_8_hours">8 hours</string>
<string name="mute_1_week">1 week</string>
<string name="mute_always">Always</string>
<plurals name="add_channel_from_qr">
<item quantity="one">Do you want to add a new channel?</item>
<item quantity="other">Do you want to add %d new channels?</item>
</plurals>
<string name="scanned_channels">Scanned Channels</string>
<string name="current_channels">Current Channels</string>
<string name="replace">Replace</string>

Wyświetl plik

@ -5,6 +5,7 @@ import org.junit.Assert
import org.junit.Test
class ChannelSetTest {
/** make sure we match the python and device code behavior */
@Test
fun matchPython() {
@ -13,4 +14,34 @@ class ChannelSetTest {
Assert.assertEquals("LongFast", cs.primaryChannel!!.name)
Assert.assertEquals(url, cs.getChannelUrl(false))
}
/** validate against the host or path in a case-insensitive way */
@Test
fun parseCaseInsensitive() {
var url = Uri.parse("HTTPS://MESHTASTIC.ORG/E/#CgMSAQESBggBQANIAQ")
Assert.assertEquals("LongFast", url.toChannelSet().primaryChannel!!.name)
url = Uri.parse("HTTPS://mEsHtAsTiC.OrG/e/#CgMSAQESBggBQANIAQ")
Assert.assertEquals("LongFast", url.toChannelSet().primaryChannel!!.name)
}
/** properly parse channel config when `?add=true` is in the fragment */
@Test
fun handleAddInFragment() {
val url = Uri.parse("https://meshtastic.org/e/#CgMSAQESBggBQANIAQ?add=true")
val cs = url.toChannelSet()
val shouldAdd = url.shouldAddChannels()
Assert.assertEquals("LongFast", cs.primaryChannel!!.name)
Assert.assertTrue(shouldAdd)
}
/** properly parse channel config when `?add=true` is in the query parameters */
@Test
fun handleAddInQueryParams() {
val url = Uri.parse("https://meshtastic.org/e/?add=true#CgMSAQESBggBQANIAQ")
val cs = url.toChannelSet()
val shouldAdd = url.shouldAddChannels()
Assert.assertEquals("LongFast", cs.primaryChannel!!.name)
Assert.assertTrue(shouldAdd)
}
}

Wyświetl plik

@ -365,7 +365,7 @@ naming:
packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*'
TopLevelPropertyNaming:
active: true
constantPattern: '[A-Z][A-Za-z0-9]*'
constantPattern: '[A-Z][_A-Za-z0-9]*'
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*'
VariableMaxLength: