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