AeroToss: TCP discovery вместо mDNS + IP в UI
- Заменён mDNS на TCP discovery (надёжнее, работает через NAT) - Desktop: TCP сервер на порту 9998 + UDP broadcast на порту 9999 - Phone: TCP клиент подключается к desktop для получения инфы - Protocol: DISCOVER → JSON response (deviceId, name, type, port, host) - DeviceCard: отображает IP адрес и порт под именем - Убрана зависимость от mDNS (может быть добавлена позже как fallback)
This commit is contained in:
@@ -18,7 +18,7 @@ class CompositeAndroidDiscovery(
|
||||
private val _devices = MutableStateFlow<List<Device>>(emptyList())
|
||||
override val devices: Flow<List<Device>> = _devices.asStateFlow()
|
||||
|
||||
private val udpDiscovery = UdpBroadcastDiscovery()
|
||||
private val tcpDiscovery = TcpDiscovery()
|
||||
|
||||
@Volatile private var scope: CoroutineScope? = null
|
||||
@Volatile private var discoveryJob: kotlinx.coroutines.Job? = null
|
||||
@@ -26,21 +26,18 @@ class CompositeAndroidDiscovery(
|
||||
override fun startDiscovery(servicePort: Int) {
|
||||
if (discoveryJob != null) return
|
||||
|
||||
nsdDiscovery.startDiscovery(servicePort)
|
||||
udpDiscovery.startDiscovery(servicePort)
|
||||
tcpDiscovery.startDiscovery(servicePort)
|
||||
wifiDirectManager.initialize()
|
||||
wifiDirectManager.discoverPeers()
|
||||
|
||||
scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
discoveryJob = scope?.launch {
|
||||
combine(
|
||||
nsdDiscovery.devices,
|
||||
udpDiscovery.devices,
|
||||
tcpDiscovery.devices,
|
||||
wifiDirectManager.devices
|
||||
) { nsdDevices, udpDevices, wifiDirectDevices ->
|
||||
) { tcpDevices, wifiDirectDevices ->
|
||||
val allDevices = mutableMapOf<String, Device>()
|
||||
nsdDevices.forEach { allDevices["nsd:${it.id}"] = it }
|
||||
udpDevices.forEach { allDevices["udp:${it.id}"] = it }
|
||||
tcpDevices.forEach { allDevices["tcp:${it.id}"] = it }
|
||||
wifiDirectDevices.forEach { allDevices["wifi:${it.id}"] = it }
|
||||
allDevices.values.toList()
|
||||
}.collect { devices ->
|
||||
@@ -52,8 +49,7 @@ class CompositeAndroidDiscovery(
|
||||
override fun stopDiscovery() {
|
||||
discoveryJob?.cancel()
|
||||
discoveryJob = null
|
||||
nsdDiscovery.stopDiscovery()
|
||||
udpDiscovery.stopDiscovery()
|
||||
tcpDiscovery.stopDiscovery()
|
||||
wifiDirectManager.stopDiscovery()
|
||||
_devices.value = emptyList()
|
||||
}
|
||||
@@ -62,8 +58,7 @@ class CompositeAndroidDiscovery(
|
||||
stopDiscovery()
|
||||
scope?.cancel()
|
||||
scope = null
|
||||
nsdDiscovery.dispose()
|
||||
udpDiscovery.dispose()
|
||||
tcpDiscovery.dispose()
|
||||
wifiDirectManager.cleanup()
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
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 kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.net.DatagramPacket
|
||||
import java.net.DatagramSocket
|
||||
import java.net.InetAddress
|
||||
import java.net.NetworkInterface
|
||||
import java.net.ServerSocket
|
||||
import java.net.Socket
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class TcpDiscovery : DiscoveryManager {
|
||||
private val _devices = MutableStateFlow<List<Device>>(emptyList())
|
||||
override val devices: Flow<List<Device>> = _devices.asStateFlow()
|
||||
|
||||
private var serverSocket: ServerSocket? = null
|
||||
private var serverThread: Thread? = null
|
||||
private var discoveryThread: Thread? = null
|
||||
private val deviceId = generateDeviceId()
|
||||
private val deviceName = getDeviceName()
|
||||
private val discoveredDevices = ConcurrentHashMap<String, Device>()
|
||||
private val json = Json { ignoreUnknownKeys = true; encodeDefaults = true }
|
||||
|
||||
override fun startDiscovery(servicePort: Int) {
|
||||
if (serverThread != null) return
|
||||
|
||||
// Start TCP server for incoming discovery requests
|
||||
serverThread = Thread {
|
||||
try {
|
||||
val ss = ServerSocket(TCP_PORT, 50, InetAddress.getByName("0.0.0.0"))
|
||||
serverSocket = ss
|
||||
println("AeroToss-TCP: Server listening on port $TCP_PORT")
|
||||
|
||||
while (!Thread.currentThread().isInterrupted) {
|
||||
try {
|
||||
val client = ss.accept()
|
||||
Thread {
|
||||
handleDiscoveryRequest(client, servicePort)
|
||||
}.start()
|
||||
} catch (_: Exception) {
|
||||
if (Thread.currentThread().isInterrupted) break
|
||||
}
|
||||
}
|
||||
ss.close()
|
||||
} catch (e: Exception) {
|
||||
if (!Thread.currentThread().isInterrupted) {
|
||||
println("AeroToss-TCP: Server error: ${e.message}")
|
||||
}
|
||||
}
|
||||
}.apply {
|
||||
isDaemon = true
|
||||
name = "aerotoss-tcp-server"
|
||||
start()
|
||||
}
|
||||
|
||||
// Start UDP broadcast sender
|
||||
discoveryThread = Thread {
|
||||
try {
|
||||
val socket = DatagramSocket()
|
||||
socket.broadcast = true
|
||||
socket.soTimeout = 2000
|
||||
|
||||
var lastBroadcast = System.currentTimeMillis()
|
||||
|
||||
while (!Thread.currentThread().isInterrupted) {
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastBroadcast > 3_000) {
|
||||
sendBroadcast(socket, servicePort)
|
||||
lastBroadcast = now
|
||||
}
|
||||
|
||||
// Listen for broadcasts from other devices
|
||||
val buf = ByteArray(1024)
|
||||
try {
|
||||
val packet = DatagramPacket(buf, buf.size)
|
||||
socket.receive(packet)
|
||||
processBroadcast(packet, servicePort)
|
||||
} catch (_: java.net.SocketTimeoutException) {}
|
||||
}
|
||||
socket.close()
|
||||
} catch (e: Exception) {
|
||||
if (!Thread.currentThread().isInterrupted) {
|
||||
println("AeroToss-TCP: Broadcast error: ${e.message}")
|
||||
}
|
||||
}
|
||||
}.apply {
|
||||
isDaemon = true
|
||||
name = "aerotoss-tcp-broadcast"
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDiscoveryRequest(client: Socket, servicePort: Int) {
|
||||
try {
|
||||
client.use { sock ->
|
||||
sock.soTimeout = 5000
|
||||
val input = sock.getInputStream().bufferedReader()
|
||||
val output = sock.getOutputStream().bufferedWriter()
|
||||
|
||||
val request = input.readLine() ?: return
|
||||
if (request == "DISCOVER") {
|
||||
val response = DiscoveryResponse(
|
||||
deviceId = deviceId,
|
||||
deviceName = deviceName,
|
||||
deviceType = "desktop",
|
||||
servicePort = servicePort,
|
||||
hostAddress = sock.localAddress.hostAddress
|
||||
)
|
||||
output.write(json.encodeToString(response))
|
||||
output.newLine()
|
||||
output.flush()
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
|
||||
private fun sendBroadcast(socket: DatagramSocket, servicePort: Int) {
|
||||
try {
|
||||
val message = "AEROTOSS:$TCP_PORT:$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, UDP_PORT)
|
||||
socket.send(packet)
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
|
||||
private fun processBroadcast(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 = 5)
|
||||
if (parts.size < 5) return
|
||||
|
||||
val remoteTcpPort = parts[1].toIntOrNull() ?: return
|
||||
val remoteServicePort = parts[2].toIntOrNull() ?: return
|
||||
val remoteId = parts[3]
|
||||
val remoteName = parts[4]
|
||||
|
||||
if (remoteId == deviceId) return
|
||||
if (remoteServicePort == myPort) return
|
||||
|
||||
// Connect to remote device to get full info
|
||||
val hostAddress = packet.address.hostAddress ?: return
|
||||
val device = queryDevice(hostAddress, remoteTcpPort, remoteId, remoteName, remoteServicePort)
|
||||
|
||||
if (device != null) {
|
||||
discoveredDevices[device.id] = device
|
||||
_devices.value = discoveredDevices.values.toList()
|
||||
println("AeroToss-TCP: Found ${device.name} at ${device.hostAddress}:${device.port}")
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
|
||||
private fun queryDevice(host: String, tcpPort: Int, fallbackId: String, fallbackName: String, fallbackPort: Int): Device? {
|
||||
return try {
|
||||
val socket = Socket()
|
||||
socket.connect(java.net.InetSocketAddress(host, tcpPort), 3000)
|
||||
socket.use { sock ->
|
||||
sock.soTimeout = 3000
|
||||
val output = sock.getOutputStream().bufferedWriter()
|
||||
val input = sock.getInputStream().bufferedReader()
|
||||
|
||||
output.write("DISCOVER")
|
||||
output.newLine()
|
||||
output.flush()
|
||||
|
||||
val responseLine = input.readLine() ?: return null
|
||||
val response = json.decodeFromString<DiscoveryResponse>(responseLine)
|
||||
|
||||
Device(
|
||||
id = response.deviceId,
|
||||
name = response.deviceName,
|
||||
type = if (response.deviceType == "desktop") DeviceType.DESKTOP else DeviceType.PHONE,
|
||||
hostAddress = host,
|
||||
port = response.servicePort
|
||||
)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
// Fallback to broadcast info
|
||||
Device(
|
||||
id = fallbackId,
|
||||
name = fallbackName,
|
||||
type = DeviceType.PHONE,
|
||||
hostAddress = host,
|
||||
port = fallbackPort
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun stopDiscovery() {
|
||||
serverThread?.interrupt()
|
||||
serverThread = null
|
||||
discoveryThread?.interrupt()
|
||||
discoveryThread = null
|
||||
try { serverSocket?.close() } catch (_: Exception) {}
|
||||
serverSocket = null
|
||||
discoveredDevices.clear()
|
||||
_devices.value = emptyList()
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
stopDiscovery()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class DiscoveryResponse(
|
||||
val deviceId: String,
|
||||
val deviceName: String,
|
||||
val deviceType: String,
|
||||
val servicePort: Int,
|
||||
val hostAddress: String
|
||||
)
|
||||
|
||||
companion object {
|
||||
const val TCP_PORT = 9998
|
||||
const val UDP_PORT = 9999
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,11 @@ fun DeviceCard(
|
||||
text = device.name,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Text(
|
||||
text = "${device.hostAddress}:${device.port}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = device.type.name,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
|
||||
@@ -4,13 +4,13 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.window.Window
|
||||
import androidx.compose.ui.window.application
|
||||
import com.aerotoss.core.AeroTossManager
|
||||
import com.aerotoss.discovery.JmdnsDiscovery
|
||||
import com.aerotoss.discovery.TcpDiscovery
|
||||
import com.aerotoss.transfer.DesktopFileTransferManager
|
||||
import com.aerotoss.ui.App
|
||||
|
||||
fun main() = application {
|
||||
val transferManager = remember { DesktopFileTransferManager() }
|
||||
val discoveryManager = remember { JmdnsDiscovery() }
|
||||
val discoveryManager = remember { TcpDiscovery() }
|
||||
val manager = remember { AeroTossManager(discoveryManager, transferManager) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
|
||||
Reference in New Issue
Block a user