AeroToss: UDP broadcast discovery + mDNS A record + имена
- Добавлен UDP broadcast discovery (порт 9999) как fallback - Desktop отправляет broadcast каждые 3 секунды - Android использует mDNS + UDP broadcast + Wi-Fi Direct - mDNS: добавлена A-запись для разрешения IP - Имена: Desktop=user@host, Android=manufacturer model - TXT record: id, name, port, type для определения устройства - Автоперерегистрация mDNS каждые 30 секунд - mDNS: отправка query каждые 5 секунд
This commit is contained in:
@@ -18,6 +18,8 @@ class CompositeAndroidDiscovery(
|
|||||||
private val _devices = MutableStateFlow<List<Device>>(emptyList())
|
private val _devices = MutableStateFlow<List<Device>>(emptyList())
|
||||||
override val devices: Flow<List<Device>> = _devices.asStateFlow()
|
override val devices: Flow<List<Device>> = _devices.asStateFlow()
|
||||||
|
|
||||||
|
private val udpDiscovery = UdpBroadcastDiscovery()
|
||||||
|
|
||||||
@Volatile private var scope: CoroutineScope? = null
|
@Volatile private var scope: CoroutineScope? = null
|
||||||
@Volatile private var discoveryJob: kotlinx.coroutines.Job? = null
|
@Volatile private var discoveryJob: kotlinx.coroutines.Job? = null
|
||||||
|
|
||||||
@@ -25,6 +27,7 @@ class CompositeAndroidDiscovery(
|
|||||||
if (discoveryJob != null) return
|
if (discoveryJob != null) return
|
||||||
|
|
||||||
nsdDiscovery.startDiscovery(servicePort)
|
nsdDiscovery.startDiscovery(servicePort)
|
||||||
|
udpDiscovery.startDiscovery(servicePort)
|
||||||
wifiDirectManager.initialize()
|
wifiDirectManager.initialize()
|
||||||
wifiDirectManager.discoverPeers()
|
wifiDirectManager.discoverPeers()
|
||||||
|
|
||||||
@@ -32,10 +35,12 @@ class CompositeAndroidDiscovery(
|
|||||||
discoveryJob = scope?.launch {
|
discoveryJob = scope?.launch {
|
||||||
combine(
|
combine(
|
||||||
nsdDiscovery.devices,
|
nsdDiscovery.devices,
|
||||||
|
udpDiscovery.devices,
|
||||||
wifiDirectManager.devices
|
wifiDirectManager.devices
|
||||||
) { nsdDevices, wifiDirectDevices ->
|
) { nsdDevices, udpDevices, wifiDirectDevices ->
|
||||||
val allDevices = mutableMapOf<String, Device>()
|
val allDevices = mutableMapOf<String, Device>()
|
||||||
nsdDevices.forEach { allDevices["nsd:${it.id}"] = it }
|
nsdDevices.forEach { allDevices["nsd:${it.id}"] = it }
|
||||||
|
udpDevices.forEach { allDevices["udp:${it.id}"] = it }
|
||||||
wifiDirectDevices.forEach { allDevices["wifi:${it.id}"] = it }
|
wifiDirectDevices.forEach { allDevices["wifi:${it.id}"] = it }
|
||||||
allDevices.values.toList()
|
allDevices.values.toList()
|
||||||
}.collect { devices ->
|
}.collect { devices ->
|
||||||
@@ -48,6 +53,7 @@ class CompositeAndroidDiscovery(
|
|||||||
discoveryJob?.cancel()
|
discoveryJob?.cancel()
|
||||||
discoveryJob = null
|
discoveryJob = null
|
||||||
nsdDiscovery.stopDiscovery()
|
nsdDiscovery.stopDiscovery()
|
||||||
|
udpDiscovery.stopDiscovery()
|
||||||
wifiDirectManager.stopDiscovery()
|
wifiDirectManager.stopDiscovery()
|
||||||
_devices.value = emptyList()
|
_devices.value = emptyList()
|
||||||
}
|
}
|
||||||
@@ -57,6 +63,7 @@ class CompositeAndroidDiscovery(
|
|||||||
scope?.cancel()
|
scope?.cancel()
|
||||||
scope = null
|
scope = null
|
||||||
nsdDiscovery.dispose()
|
nsdDiscovery.dispose()
|
||||||
|
udpDiscovery.dispose()
|
||||||
wifiDirectManager.cleanup()
|
wifiDirectManager.cleanup()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
package com.aerotoss.discovery
|
||||||
|
|
||||||
|
import com.aerotoss.model.Device
|
||||||
|
import com.aerotoss.model.DeviceType
|
||||||
|
import com.aerotoss.util.generateDeviceId
|
||||||
|
import com.aerotoss.util.getDeviceName
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import java.net.DatagramPacket
|
||||||
|
import java.net.DatagramSocket
|
||||||
|
import java.net.InetAddress
|
||||||
|
import java.net.NetworkInterface
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
class UdpBroadcastDiscovery : DiscoveryManager {
|
||||||
|
private val _devices = MutableStateFlow<List<Device>>(emptyList())
|
||||||
|
override val devices: Flow<List<Device>> = _devices.asStateFlow()
|
||||||
|
|
||||||
|
private var serverSocket: DatagramSocket? = null
|
||||||
|
private var discoveryThread: Thread? = null
|
||||||
|
private val deviceId = generateDeviceId()
|
||||||
|
private val deviceName = getDeviceName()
|
||||||
|
private val discoveredDevices = ConcurrentHashMap<String, Device>()
|
||||||
|
|
||||||
|
override fun startDiscovery(servicePort: Int) {
|
||||||
|
if (discoveryThread != null) return
|
||||||
|
|
||||||
|
discoveryThread = Thread {
|
||||||
|
try {
|
||||||
|
val socket = DatagramSocket(BROADCAST_PORT, InetAddress.getByName("0.0.0.0"))
|
||||||
|
socket.broadcast = true
|
||||||
|
socket.soTimeout = 2000
|
||||||
|
serverSocket = socket
|
||||||
|
|
||||||
|
println("AeroToss-UDP: Listening on port $BROADCAST_PORT")
|
||||||
|
|
||||||
|
// Send discovery broadcast every 3 seconds
|
||||||
|
var lastBroadcast = System.currentTimeMillis()
|
||||||
|
|
||||||
|
while (!Thread.currentThread().isInterrupted) {
|
||||||
|
// Send broadcast
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
if (now - lastBroadcast > 3_000) {
|
||||||
|
sendBroadcast(socket, servicePort)
|
||||||
|
lastBroadcast = now
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for responses
|
||||||
|
val buf = ByteArray(1024)
|
||||||
|
try {
|
||||||
|
val packet = DatagramPacket(buf, buf.size)
|
||||||
|
socket.receive(packet)
|
||||||
|
processPacket(packet, servicePort)
|
||||||
|
} catch (_: java.net.SocketTimeoutException) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
socket.close()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (!Thread.currentThread().isInterrupted) {
|
||||||
|
println("AeroToss-UDP: Error: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.apply {
|
||||||
|
isDaemon = true
|
||||||
|
name = "aerotoss-udp"
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendBroadcast(socket: DatagramSocket, servicePort: Int) {
|
||||||
|
try {
|
||||||
|
val message = "AEROTOSS:$servicePort:$deviceId:$deviceName"
|
||||||
|
val data = message.toByteArray(Charsets.UTF_8)
|
||||||
|
val broadcastAddr = InetAddress.getByName("255.255.255.255")
|
||||||
|
val packet = DatagramPacket(data, data.size, broadcastAddr, BROADCAST_PORT)
|
||||||
|
socket.send(packet)
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processPacket(packet: DatagramPacket, myPort: Int) {
|
||||||
|
try {
|
||||||
|
val message = String(packet.data, 0, packet.length, Charsets.UTF_8)
|
||||||
|
if (!message.startsWith("AEROTOSS:")) return
|
||||||
|
|
||||||
|
val parts = message.split(":", limit = 4)
|
||||||
|
if (parts.size < 4) return
|
||||||
|
|
||||||
|
val remotePort = parts[1].toIntOrNull() ?: return
|
||||||
|
val remoteId = parts[2]
|
||||||
|
val remoteName = parts[3]
|
||||||
|
|
||||||
|
// Don't add ourselves
|
||||||
|
if (remoteId == deviceId) return
|
||||||
|
if (remotePort == myPort) return
|
||||||
|
|
||||||
|
val hostAddress = packet.address.hostAddress ?: return
|
||||||
|
|
||||||
|
val device = Device(
|
||||||
|
id = remoteId,
|
||||||
|
name = remoteName,
|
||||||
|
type = if (remotePort == myPort) DeviceType.DESKTOP else DeviceType.PHONE,
|
||||||
|
hostAddress = hostAddress,
|
||||||
|
port = remotePort
|
||||||
|
)
|
||||||
|
|
||||||
|
discoveredDevices[remoteId] = device
|
||||||
|
_devices.value = discoveredDevices.values.toList()
|
||||||
|
println("AeroToss-UDP: Found $remoteName at $hostAddress:$remotePort")
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stopDiscovery() {
|
||||||
|
discoveryThread?.interrupt()
|
||||||
|
discoveryThread = null
|
||||||
|
try { serverSocket?.close() } catch (_: Exception) {}
|
||||||
|
serverSocket = null
|
||||||
|
discoveredDevices.clear()
|
||||||
|
_devices.value = emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
stopDiscovery()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val BROADCAST_PORT = 9999
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -230,13 +230,16 @@ class JmdnsDiscovery : DiscoveryManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun buildDnsResponse(servicePort: Int): ByteArray {
|
private fun buildDnsResponse(servicePort: Int): ByteArray {
|
||||||
|
val localAddr = findLocalAddress()
|
||||||
|
val localIp = localAddr?.address ?: byteArrayOf(127, 0, 0, 1)
|
||||||
|
|
||||||
val baos = java.io.ByteArrayOutputStream()
|
val baos = java.io.ByteArrayOutputStream()
|
||||||
val dos = java.io.DataOutputStream(baos)
|
val dos = java.io.DataOutputStream(baos)
|
||||||
|
|
||||||
dos.writeShort(0) // ID
|
dos.writeShort(0) // ID
|
||||||
dos.writeShort(0x8400) // Flags
|
dos.writeShort(0x8400) // Flags
|
||||||
dos.writeShort(0) // QDCOUNT
|
dos.writeShort(0) // QDCOUNT
|
||||||
dos.writeShort(3) // ANCOUNT
|
dos.writeShort(4) // ANCOUNT (PTR + SRV + TXT + A)
|
||||||
dos.writeShort(0) // NSCOUNT
|
dos.writeShort(0) // NSCOUNT
|
||||||
dos.writeShort(0) // ARCOUNT
|
dos.writeShort(0) // ARCOUNT
|
||||||
|
|
||||||
@@ -287,6 +290,14 @@ class JmdnsDiscovery : DiscoveryManager {
|
|||||||
dos.writeShort(txtBytes.size)
|
dos.writeShort(txtBytes.size)
|
||||||
dos.write(txtBytes)
|
dos.write(txtBytes)
|
||||||
|
|
||||||
|
// A: AeroToss._aerotoss._tcp.local. → IP address
|
||||||
|
writeDnsName(baos, "AeroToss._aerotoss._tcp.local.")
|
||||||
|
dos.writeShort(1) // A
|
||||||
|
dos.writeShort(1) // IN
|
||||||
|
dos.writeInt(4500) // TTL
|
||||||
|
dos.writeShort(4) // RDLENGTH = 4 bytes for IPv4
|
||||||
|
dos.write(localIp)
|
||||||
|
|
||||||
dos.flush()
|
dos.flush()
|
||||||
return baos.toByteArray()
|
return baos.toByteArray()
|
||||||
}
|
}
|
||||||
@@ -352,4 +363,19 @@ class JmdnsDiscovery : DiscoveryManager {
|
|||||||
const val SERVICE_TYPE = "_aerotoss._tcp"
|
const val SERVICE_TYPE = "_aerotoss._tcp"
|
||||||
const val SERVICE_NAME = "AeroToss"
|
const val SERVICE_NAME = "AeroToss"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun findLocalAddress(): InetAddress? {
|
||||||
|
return try {
|
||||||
|
NetworkInterface.getNetworkInterfaces()?.toList()
|
||||||
|
?.filter { iface ->
|
||||||
|
iface.isUp && !iface.isLoopback && !isVirtual(iface) &&
|
||||||
|
iface.inetAddresses.toList().any { it is java.net.Inet4Address }
|
||||||
|
}
|
||||||
|
?.flatMap { it.inetAddresses.toList() }
|
||||||
|
?.firstOrNull { it is java.net.Inet4Address }
|
||||||
|
?: InetAddress.getLocalHost()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
try { InetAddress.getLocalHost() } catch (_: Exception) { null }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user