kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
feat: support add=true in QR codes (#1151)
rodzic
cc5543f4c9
commit
e4c6000a10
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
Ładowanie…
Reference in New Issue