diff --git a/app/src/main/java/com/geeksville/mesh/repository/network/NsdManager.kt b/app/src/main/java/com/geeksville/mesh/repository/network/NsdManager.kt
index 4646543b3..a255dea72 100644
--- a/app/src/main/java/com/geeksville/mesh/repository/network/NsdManager.kt
+++ b/app/src/main/java/com/geeksville/mesh/repository/network/NsdManager.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025 Meshtastic LLC
+ * Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -14,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-
package com.geeksville.mesh.repository.network
import android.annotation.SuppressLint
@@ -32,6 +31,7 @@ import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.suspendCancellableCoroutine
import java.util.concurrent.CopyOnWriteArrayList
+import java.util.concurrent.atomic.AtomicBoolean
import kotlin.coroutines.resume
@OptIn(ExperimentalCoroutinesApi::class)
@@ -89,30 +89,37 @@ private fun NsdManager.discoverServices(
@SuppressLint("NewApi")
private suspend fun NsdManager.resolveService(serviceInfo: NsdServiceInfo): NsdServiceInfo? =
suspendCancellableCoroutine { continuation ->
+ val isResumed = AtomicBoolean(false)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
val callback =
object : NsdManager.ServiceInfoCallback {
override fun onServiceInfoCallbackRegistrationFailed(errorCode: Int) {
- continuation.resume(null)
+ if (isResumed.compareAndSet(false, true)) {
+ continuation.resume(null)
+ }
}
override fun onServiceUpdated(updatedServiceInfo: NsdServiceInfo) {
if (updatedServiceInfo.hostAddresses.isNotEmpty()) {
- continuation.resume(updatedServiceInfo)
- try {
- unregisterServiceInfoCallback(this)
- } catch (e: IllegalArgumentException) {
- Logger.w(e) { "Already unregistered" }
+ if (isResumed.compareAndSet(false, true)) {
+ continuation.resume(updatedServiceInfo)
+ try {
+ unregisterServiceInfoCallback(this)
+ } catch (e: IllegalArgumentException) {
+ Logger.w(e) { "Already unregistered" }
+ }
}
}
}
override fun onServiceLost() {
- continuation.resume(null)
- try {
- unregisterServiceInfoCallback(this)
- } catch (e: IllegalArgumentException) {
- Logger.w(e) { "Already unregistered" }
+ if (isResumed.compareAndSet(false, true)) {
+ continuation.resume(null)
+ try {
+ unregisterServiceInfoCallback(this)
+ } catch (e: IllegalArgumentException) {
+ Logger.w(e) { "Already unregistered" }
+ }
}
}
@@ -132,11 +139,15 @@ private suspend fun NsdManager.resolveService(serviceInfo: NsdServiceInfo): NsdS
val listener =
object : NsdManager.ResolveListener {
override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
- continuation.resume(null)
+ if (isResumed.compareAndSet(false, true)) {
+ continuation.resume(null)
+ }
}
override fun onServiceResolved(serviceInfo: NsdServiceInfo) {
- continuation.resume(serviceInfo)
+ if (isResumed.compareAndSet(false, true)) {
+ continuation.resume(serviceInfo)
+ }
}
}
@Suppress("DEPRECATION")
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshActionHandler.kt b/app/src/main/java/com/geeksville/mesh/service/MeshActionHandler.kt
index 3ba9eaac9..0208d2582 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshActionHandler.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshActionHandler.kt
@@ -71,44 +71,10 @@ constructor(
ignoreException {
val myNodeNum = nodeManager.myNodeNum ?: return@ignoreException
when (action) {
- is ServiceAction.Favorite -> {
- val node = action.node
- commandSender.sendAdmin(myNodeNum) {
- if (node.isFavorite) removeFavoriteNode = node.num else setFavoriteNode = node.num
- }
- nodeManager.updateNodeInfo(node.num) { it.isFavorite = !node.isFavorite }
- }
- is ServiceAction.Ignore -> {
- val node = action.node
- commandSender.sendAdmin(myNodeNum) {
- if (node.isIgnored) removeIgnoredNode = node.num else setIgnoredNode = node.num
- }
- nodeManager.updateNodeInfo(node.num) { it.isIgnored = !node.isIgnored }
- }
- is ServiceAction.Reaction -> {
- val channel = action.contactKey[0].digitToInt()
- val destId = action.contactKey.substring(1)
- val dataPacket =
- org.meshtastic.core.model.DataPacket(
- to = destId,
- dataType = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
- bytes = action.emoji.encodeToByteArray(),
- channel = channel,
- replyId = action.replyId,
- wantAck = false,
- )
- commandSender.sendData(dataPacket)
- rememberReaction(action)
- }
- is ServiceAction.ImportContact -> {
- val verifiedContact = action.contact.toBuilder().setManuallyVerified(true).build()
- commandSender.sendAdmin(myNodeNum) { addContact = verifiedContact }
- nodeManager.handleReceivedUser(
- verifiedContact.nodeNum,
- verifiedContact.user,
- manuallyVerified = true,
- )
- }
+ is ServiceAction.Favorite -> handleFavorite(action, myNodeNum)
+ is ServiceAction.Ignore -> handleIgnore(action, myNodeNum)
+ is ServiceAction.Reaction -> handleReaction(action)
+ is ServiceAction.ImportContact -> handleImportContact(action, myNodeNum)
is ServiceAction.SendContact -> {
commandSender.sendAdmin(myNodeNum) { addContact = action.contact }
}
@@ -119,6 +85,45 @@ constructor(
}
}
+ private fun handleFavorite(action: ServiceAction.Favorite, myNodeNum: Int) {
+ val node = action.node
+ commandSender.sendAdmin(myNodeNum) {
+ if (node.isFavorite) removeFavoriteNode = node.num else setFavoriteNode = node.num
+ }
+ nodeManager.updateNodeInfo(node.num) { it.isFavorite = !node.isFavorite }
+ }
+
+ private fun handleIgnore(action: ServiceAction.Ignore, myNodeNum: Int) {
+ val node = action.node
+ commandSender.sendAdmin(myNodeNum) {
+ if (node.isIgnored) removeIgnoredNode = node.num else setIgnoredNode = node.num
+ }
+ nodeManager.updateNodeInfo(node.num) { it.isIgnored = !node.isIgnored }
+ }
+
+ private fun handleReaction(action: ServiceAction.Reaction) {
+ val channel = action.contactKey[0].digitToInt()
+ val destId = action.contactKey.substring(1)
+ val dataPacket =
+ org.meshtastic.core.model.DataPacket(
+ to = destId,
+ dataType = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
+ bytes = action.emoji.encodeToByteArray(),
+ channel = channel,
+ replyId = action.replyId,
+ wantAck = false,
+ emoji = action.emoji.codePointAt(0),
+ )
+ commandSender.sendData(dataPacket)
+ rememberReaction(action)
+ }
+
+ private fun handleImportContact(action: ServiceAction.ImportContact, myNodeNum: Int) {
+ val verifiedContact = action.contact.toBuilder().setManuallyVerified(true).build()
+ commandSender.sendAdmin(myNodeNum) { addContact = verifiedContact }
+ nodeManager.handleReceivedUser(verifiedContact.nodeNum, verifiedContact.user, manuallyVerified = true)
+ }
+
private fun rememberReaction(action: ServiceAction.Reaction) {
scope.handledLaunch {
val reaction =
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshCommandSender.kt b/app/src/main/java/com/geeksville/mesh/service/MeshCommandSender.kt
index faf0f3448..50592beff 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshCommandSender.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshCommandSender.kt
@@ -152,6 +152,7 @@ constructor(
portnumValue = p.dataType
payload = ByteString.copyFrom(p.bytes ?: ByteArray(0))
p.replyId?.let { if (it != 0) replyId = it }
+ if (p.emoji != 0) emoji = p.emoji
}
p.time = System.currentTimeMillis()
packetHandler?.sendToRadio(meshPacket)
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshDataMapper.kt b/app/src/main/java/com/geeksville/mesh/service/MeshDataMapper.kt
index 3ea592def..ed5df806b 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshDataMapper.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshDataMapper.kt
@@ -49,6 +49,7 @@ class MeshDataMapper @Inject constructor(private val nodeManager: MeshNodeManage
replyId = data.replyId,
relayNode = packet.relayNode,
viaMqtt = packet.viaMqtt,
+ emoji = data.emoji,
)
}
}
diff --git a/core/model/src/main/kotlin/org/meshtastic/core/model/DataPacket.kt b/core/model/src/main/kotlin/org/meshtastic/core/model/DataPacket.kt
index c1d9577d3..80e7e97f9 100644
--- a/core/model/src/main/kotlin/org/meshtastic/core/model/DataPacket.kt
+++ b/core/model/src/main/kotlin/org/meshtastic/core/model/DataPacket.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025 Meshtastic LLC
+ * Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -14,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-
package org.meshtastic.core.model
import android.os.Parcel
@@ -64,6 +63,7 @@ data class DataPacket(
var relayNode: Int? = null,
var relays: Int = 0,
var viaMqtt: Boolean = false, // True if this packet passed via MQTT somewhere along its path
+ var emoji: Int = 0,
) : Parcelable {
/** If there was an error with this message, this string describes what was wrong. */
@@ -137,6 +137,9 @@ data class DataPacket(
parcel.readInt(),
parcel.readInt().let { if (it == 0) null else it },
parcel.readInt().let { if (it == -1) null else it },
+ parcel.readInt(), // relays
+ parcel.readInt() == 1, // viaMqtt
+ parcel.readInt(), // emoji
)
@Suppress("CyclomaticComplexMethod")
@@ -161,6 +164,7 @@ data class DataPacket(
if (rssi != other.rssi) return false
if (replyId != other.replyId) return false
if (relayNode != other.relayNode) return false
+ if (emoji != other.emoji) return false
return true
}
@@ -181,6 +185,7 @@ data class DataPacket(
result = 31 * result + rssi
result = 31 * result + replyId.hashCode()
result = 31 * result + relayNode.hashCode()
+ result = 31 * result + emoji
return result
}
@@ -200,6 +205,9 @@ data class DataPacket(
parcel.writeInt(rssi)
parcel.writeInt(replyId ?: 0)
parcel.writeInt(relayNode ?: -1)
+ parcel.writeInt(relays)
+ parcel.writeInt(if (viaMqtt) 1 else 0)
+ parcel.writeInt(emoji)
}
override fun describeContents(): Int = 0
@@ -221,6 +229,9 @@ data class DataPacket(
rssi = parcel.readInt()
replyId = parcel.readInt().let { if (it == 0) null else it }
relayNode = parcel.readInt().let { if (it == -1) null else it }
+ relays = parcel.readInt()
+ viaMqtt = parcel.readInt() == 1
+ emoji = parcel.readInt()
}
companion object CREATOR : Parcelable.Creator {
diff --git a/gradle.properties b/gradle.properties
index 7ba158ca3..3e2780b6b 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -42,7 +42,7 @@ org.gradle.configuration-cache=true
# Watches the file system for changes, allowing Gradle to reuse information about the file system
# between builds.
-org.gradle.vfs.watch=true
+# org.gradle.vfs.watch=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK