preliminary support for astra desktop

This commit is contained in:
Boof2015
2026-07-01 00:43:51 -04:00
parent 8f23c6a1e2
commit 76a5183dfb
25 changed files with 3145 additions and 0 deletions
@@ -0,0 +1,18 @@
plugins {
id 'com.android.library'
id 'expo-module-gradle-plugin'
}
group = 'expo.modules.astradesktopdiscovery'
version = '0.1.0'
android {
namespace "expo.modules.astradesktopdiscovery"
defaultConfig {
versionCode 1
versionName "0.1.0"
}
lintOptions {
abortOnError false
}
}
@@ -0,0 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
</manifest>
@@ -0,0 +1,120 @@
package expo.modules.astradesktopdiscovery
import android.content.Context
import android.net.nsd.NsdManager
import android.net.nsd.NsdServiceInfo
import android.os.Build
import expo.modules.kotlin.exception.Exceptions
import expo.modules.kotlin.functions.Coroutine
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.net.Inet4Address
import java.util.concurrent.ConcurrentHashMap
class AstraDesktopDiscoveryModule : Module() {
private val serviceType = "_astra-remote._tcp."
private var discoveryListener: NsdManager.DiscoveryListener? = null
private val cached = ConcurrentHashMap<String, Map<String, Any?>>()
override fun definition() = ModuleDefinition {
Name("AstraDesktopDiscovery")
Events("onDesktopRemoteFound", "onDesktopRemoteLost")
AsyncFunction("start").Coroutine<Unit> {
withContext(Dispatchers.Main) { startDiscovery() }
}
AsyncFunction("stop").Coroutine<Unit> {
withContext(Dispatchers.Main) { stopDiscovery() }
}
Function("getCached") {
cached.values.toList()
}
}
private fun requireContext(): Context =
appContext.reactContext ?: throw Exceptions.ReactContextLost()
private fun nsdManager(): NsdManager =
requireContext().getSystemService(Context.NSD_SERVICE) as NsdManager
private fun startDiscovery() {
if (discoveryListener != null) return
val listener = object : NsdManager.DiscoveryListener {
override fun onDiscoveryStarted(regType: String) = Unit
override fun onDiscoveryStopped(serviceType: String) = Unit
override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
stopDiscovery()
}
override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {
stopDiscovery()
}
override fun onServiceFound(serviceInfo: NsdServiceInfo) {
if (serviceInfo.serviceType != serviceType) return
resolve(serviceInfo)
}
override fun onServiceLost(serviceInfo: NsdServiceInfo) {
cached.remove(serviceInfo.serviceName)
sendEvent("onDesktopRemoteLost", mapOf("name" to serviceInfo.serviceName))
}
}
discoveryListener = listener
nsdManager().discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, listener)
}
private fun stopDiscovery() {
val listener = discoveryListener ?: return
discoveryListener = null
try {
nsdManager().stopServiceDiscovery(listener)
} catch (_: Throwable) {
// Android can throw if discovery already stopped. Treat stop as idempotent.
}
}
private fun resolve(serviceInfo: NsdServiceInfo) {
try {
nsdManager().resolveService(serviceInfo, object : NsdManager.ResolveListener {
override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) = Unit
override fun onServiceResolved(resolved: NsdServiceInfo) {
val host = resolved.host
val address = host?.hostAddress ?: return
if (host !is Inet4Address) return
val port = resolved.port
if (port <= 0) return
val endpointUuid = txt(resolved, "endpoint_uuid")
val desktopName = txt(resolved, "name")
val protocolVersion = txt(resolved, "protocol_version")?.toIntOrNull() ?: 1
val payload = mapOf(
"endpointUuid" to endpointUuid,
"desktopName" to desktopName,
"protocolVersion" to protocolVersion,
"name" to (desktopName ?: resolved.serviceName),
"baseUrl" to "http://$address:$port",
"address" to address,
"port" to port,
"lastSeenAt" to System.currentTimeMillis(),
)
cached[resolved.serviceName] = payload
sendEvent("onDesktopRemoteFound", payload)
}
})
} catch (_: Throwable) {
// Resolve races are normal while browsing; ignore and wait for the next mDNS packet.
}
}
private fun txt(serviceInfo: NsdServiceInfo, key: String): String? {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return null
val value = serviceInfo.attributes[key] ?: return null
return value.toString(Charsets.UTF_8).trim().ifEmpty { null }
}
}