From 827d431311f0cd8968f67e9322ffeb306202b482 Mon Sep 17 00:00:00 2001 From: grendervill Date: Wed, 19 Aug 2026 23:24:01 +0300 Subject: [PATCH] =?UTF-8?q?AeroToss:=20=D0=BF=D0=BE=D0=BB=D0=BD=D1=8B?= =?UTF-8?q?=D0=B9=20=D1=80=D0=B5=D1=84=D0=B0=D0=BA=D1=82=D0=BE=D1=80=D0=B8?= =?UTF-8?q?=D0=BD=D0=B3=20discovery?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TcpDiscovery принимает DeviceType в конструкторе (PHONE/DESKTOP) - Desktop: type=DESKTOP, Phone: type=PHONE (больше не захардкожено) - Удалён NSD discovery из Android (только TCP + Wi-Fi Direct) - CompositeAndroidDiscovery больше не использует AndroidNsdDiscovery - Удалён UdpBroadcastDiscovery (дублировал TcpDiscovery) - deviceType теперь корректно передаётся в DiscoveryResponse - hostAddress теперь всегда содержит реальный IP --- .../kotlin/com/aerotoss/MainActivity.kt | 61 +------ .../discovery/CompositeAndroidDiscovery.kt | 23 +-- .../com/aerotoss/discovery/TcpDiscovery.kt | 164 ++++++++---------- .../desktopMain/kotlin/com/aerotoss/Main.kt | 8 +- 4 files changed, 85 insertions(+), 171 deletions(-) diff --git a/app/src/androidMain/kotlin/com/aerotoss/MainActivity.kt b/app/src/androidMain/kotlin/com/aerotoss/MainActivity.kt index fba6a33..442034e 100644 --- a/app/src/androidMain/kotlin/com/aerotoss/MainActivity.kt +++ b/app/src/androidMain/kotlin/com/aerotoss/MainActivity.kt @@ -1,16 +1,10 @@ package com.aerotoss -import android.Manifest -import android.content.pm.PackageManager -import android.os.Build import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.activity.result.contract.ActivityResultContracts -import androidx.core.content.ContextCompat import androidx.lifecycle.lifecycleScope import com.aerotoss.core.AeroTossManager -import com.aerotoss.discovery.AndroidNsdDiscovery import com.aerotoss.discovery.CompositeAndroidDiscovery import com.aerotoss.transfer.AndroidFileTransferManager import com.aerotoss.ui.App @@ -20,73 +14,24 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class MainActivity : ComponentActivity() { - private var manager: AeroTossManager? = null - private val requiredPermissions: Array - get() { - val perms = mutableListOf( - Manifest.permission.INTERNET, - Manifest.permission.ACCESS_WIFI_STATE, - Manifest.permission.CHANGE_WIFI_STATE, - Manifest.permission.ACCESS_NETWORK_STATE, - Manifest.permission.ACCESS_FINE_LOCATION, - Manifest.permission.ACCESS_COARSE_LOCATION, - Manifest.permission.CHANGE_WIFI_MULTICAST_STATE, - ) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - perms.add(Manifest.permission.NEARBY_WIFI_DEVICES) - } - return perms.toTypedArray() - } - - private val permissionLauncher = registerForActivityResult( - ActivityResultContracts.RequestMultiplePermissions() - ) { permissions -> - val allGranted = permissions.values.all { it } - if (allGranted && manager == null) { - initializeApp() - } - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - - if (manager != null) return - - if (hasAllPermissions()) { - initializeApp() - } else { - permissionLauncher.launch(requiredPermissions) - } - } - - private fun hasAllPermissions(): Boolean { - return requiredPermissions.all { - ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED - } - } - - private fun initializeApp() { if (manager != null) return val transferManager = AndroidFileTransferManager(applicationContext) - val nsdDiscovery = AndroidNsdDiscovery(applicationContext) val wifiDirectManager = WifiDirectManager(applicationContext) - val discoveryManager = CompositeAndroidDiscovery(nsdDiscovery, wifiDirectManager) + val discoveryManager = CompositeAndroidDiscovery(wifiDirectManager) val mgr = AeroTossManager(discoveryManager, transferManager) manager = mgr lifecycleScope.launch { - val port = withContext(Dispatchers.IO) { - transferManager.startServer(0) - } + val port = withContext(Dispatchers.IO) { transferManager.startServer(0) } mgr.start(port) } - setContent { - App(mgr) - } + setContent { App(mgr) } } override fun onDestroy() { diff --git a/app/src/androidMain/kotlin/com/aerotoss/discovery/CompositeAndroidDiscovery.kt b/app/src/androidMain/kotlin/com/aerotoss/discovery/CompositeAndroidDiscovery.kt index 4d4d210..e379f19 100644 --- a/app/src/androidMain/kotlin/com/aerotoss/discovery/CompositeAndroidDiscovery.kt +++ b/app/src/androidMain/kotlin/com/aerotoss/discovery/CompositeAndroidDiscovery.kt @@ -1,6 +1,5 @@ package com.aerotoss.discovery -import android.content.Context import com.aerotoss.model.Device import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -8,17 +7,17 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import com.aerotoss.model.DeviceType import com.aerotoss.wifi.WifiDirectManager class CompositeAndroidDiscovery( - private val nsdDiscovery: AndroidNsdDiscovery, private val wifiDirectManager: WifiDirectManager ) : DiscoveryManager { private val _devices = MutableStateFlow>(emptyList()) override val devices: Flow> = _devices.asStateFlow() - private val tcpDiscovery = TcpDiscovery() + private val tcpDiscovery = TcpDiscovery(DeviceType.PHONE) @Volatile private var scope: CoroutineScope? = null @Volatile private var discoveryJob: kotlinx.coroutines.Job? = null @@ -36,19 +35,16 @@ class CompositeAndroidDiscovery( tcpDiscovery.devices, wifiDirectManager.devices ) { tcpDevices, wifiDirectDevices -> - val allDevices = mutableMapOf() - tcpDevices.forEach { allDevices["tcp:${it.id}"] = it } - wifiDirectDevices.forEach { allDevices["wifi:${it.id}"] = it } - allDevices.values.toList() - }.collect { devices -> - _devices.value = devices - } + val all = mutableMapOf() + tcpDevices.forEach { all["tcp:${it.id}"] = it } + wifiDirectDevices.forEach { all["wifi:${it.id}"] = it } + all.values.toList() + }.collect { _devices.value = it } } } override fun stopDiscovery() { - discoveryJob?.cancel() - discoveryJob = null + discoveryJob?.cancel(); discoveryJob = null tcpDiscovery.stopDiscovery() wifiDirectManager.stopDiscovery() _devices.value = emptyList() @@ -56,8 +52,7 @@ class CompositeAndroidDiscovery( override fun dispose() { stopDiscovery() - scope?.cancel() - scope = null + scope?.cancel(); scope = null tcpDiscovery.dispose() wifiDirectManager.cleanup() } diff --git a/app/src/commonMain/kotlin/com/aerotoss/discovery/TcpDiscovery.kt b/app/src/commonMain/kotlin/com/aerotoss/discovery/TcpDiscovery.kt index 5c9034e..bf70ab9 100644 --- a/app/src/commonMain/kotlin/com/aerotoss/discovery/TcpDiscovery.kt +++ b/app/src/commonMain/kotlin/com/aerotoss/discovery/TcpDiscovery.kt @@ -18,7 +18,10 @@ import java.net.ServerSocket import java.net.Socket import java.util.concurrent.ConcurrentHashMap -class TcpDiscovery : DiscoveryManager { +class TcpDiscovery( + private val myDeviceType: DeviceType = DeviceType.DESKTOP +) : DiscoveryManager { + private val _devices = MutableStateFlow>(emptyList()) override val devices: Flow> = _devices.asStateFlow() @@ -34,17 +37,16 @@ class TcpDiscovery : DiscoveryManager { override fun startDiscovery(servicePort: Int) { if (serverThread != null) return - // TCP server for incoming discovery requests + // TCP server serverThread = Thread { try { val ss = ServerSocket(TCP_PORT, 50, InetAddress.getByName("0.0.0.0")) serverSocket = ss - println("AeroToss: TCP server on port $TCP_PORT, service on $servicePort") - + println("AeroToss: TCP=$TCP_PORT, service=$servicePort, type=$myDeviceType, name=$deviceName") while (!Thread.currentThread().isInterrupted) { try { val client = ss.accept() - Thread { handleDiscoveryRequest(client, servicePort) }.start() + Thread { handleRequest(client, servicePort) }.start() } catch (_: Exception) { if (Thread.currentThread().isInterrupted) break } @@ -53,135 +55,114 @@ class TcpDiscovery : DiscoveryManager { } catch (e: Exception) { if (!Thread.currentThread().isInterrupted) println("AeroToss: Server error: ${e.message}") } - }.apply { isDaemon = true; name = "aerotoss-server"; start() } + }.apply { isDaemon = true; name = "aerotoss-tcp"; start() } - // UDP broadcast: send AND receive on same socket + // UDP broadcast discoveryThread = Thread { try { val socket = DatagramSocket(UDP_PORT, InetAddress.getByName("0.0.0.0")) socket.broadcast = true socket.soTimeout = 2000 udpSocket = socket - var lastBroadcast = System.currentTimeMillis() - while (!Thread.currentThread().isInterrupted) { val now = System.currentTimeMillis() if (now - lastBroadcast > 3_000) { - sendBroadcast(socket, servicePort) + broadcast(socket, servicePort) lastBroadcast = now } - val buf = ByteArray(1024) try { - val packet = DatagramPacket(buf, buf.size) - socket.receive(packet) - processBroadcast(packet, servicePort) + val pkt = DatagramPacket(buf, buf.size) + socket.receive(pkt) + onBroadcastReceived(pkt, 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-broadcast"; start() } + }.apply { isDaemon = true; name = "aerotoss-udp"; start() } } - private fun handleDiscoveryRequest(client: Socket, servicePort: Int) { + private fun handleRequest(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 = findNetworkAddress() + val reader = sock.getInputStream().bufferedReader() + val writer = sock.getOutputStream().bufferedWriter() + val line = reader.readLine() ?: return + if (line == "DISCOVER") { + val resp = DiscoveryResponse( + id = deviceId, + name = deviceName, + type = myDeviceType.name.lowercase(), + port = servicePort, + ip = findMyIp() ) - output.write(json.encodeToString(response)) - output.newLine() - output.flush() + writer.write(json.encodeToString(resp)) + writer.newLine() + writer.flush() } } } catch (_: Exception) {} } - private fun sendBroadcast(socket: DatagramSocket, servicePort: Int) { + private fun broadcast(socket: DatagramSocket, servicePort: Int) { try { - val message = "AEROTOSS:$TCP_PORT:$servicePort:$deviceId:$deviceName" - val data = message.toByteArray(Charsets.UTF_8) - val packet = DatagramPacket(data, data.size, InetAddress.getByName("255.255.255.255"), UDP_PORT) - socket.send(packet) + val msg = "AEROTOSS:$TCP_PORT:$servicePort:$deviceId:$deviceName" + val pkt = DatagramPacket(msg.toByteArray(Charsets.UTF_8), msg.length, InetAddress.getByName("255.255.255.255"), UDP_PORT) + socket.send(pkt) } catch (_: Exception) {} } - private fun processBroadcast(packet: DatagramPacket, myPort: Int) { + private fun onBroadcastReceived(pkt: DatagramPacket, myServicePort: Int) { try { - val message = String(packet.data, 0, packet.length, Charsets.UTF_8) - if (!message.startsWith("AEROTOSS:")) return - - val parts = message.split(":", limit = 5) + val msg = String(pkt.data, 0, pkt.length, Charsets.UTF_8) + if (!msg.startsWith("AEROTOSS:")) return + val parts = msg.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 - - val hostAddress = packet.address.hostAddress ?: return - val device = queryDevice(hostAddress, remoteTcpPort, remoteId, remoteName, remoteServicePort) + if (remoteServicePort == myServicePort) return + val host = pkt.address.hostAddress ?: return + val device = queryRemote(host, remoteTcpPort) if (device != null) { discoveredDevices[device.id] = device _devices.value = discoveredDevices.values.toList() - println("AeroToss: Found ${device.name} at ${device.hostAddress}:${device.port}") + println("AeroToss: Found ${device.name} [${device.type}] at ${device.hostAddress}:${device.port}") } } catch (_: Exception) {} } - private fun queryDevice(host: String, tcpPort: Int, fallbackId: String, fallbackName: String, fallbackPort: Int): Device? { + private fun queryRemote(host: String, tcpPort: Int): Device? { return try { - val socket = Socket() - socket.connect(java.net.InetSocketAddress(host, tcpPort), 3000) - socket.use { sock -> + Socket().use { sock -> + sock.connect(java.net.InetSocketAddress(host, tcpPort), 3000) 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(responseLine) - + val writer = sock.getOutputStream().bufferedWriter() + val reader = sock.getInputStream().bufferedReader() + writer.write("DISCOVER") + writer.newLine() + writer.flush() + val line = reader.readLine() ?: return null + val resp = json.decodeFromString(line) Device( - id = response.deviceId, - name = response.deviceName, - type = if (response.deviceType == "desktop") DeviceType.DESKTOP else DeviceType.PHONE, - hostAddress = host, - port = response.servicePort + id = resp.id, + name = resp.name, + type = if (resp.type == "desktop") DeviceType.DESKTOP else DeviceType.PHONE, + hostAddress = resp.ip, + port = resp.port ) } - } catch (_: Exception) { - Device( - id = fallbackId, - name = fallbackName, - type = DeviceType.PHONE, - hostAddress = host, - port = fallbackPort - ) - } + } catch (_: Exception) { null } } - private fun findNetworkAddress(): String { + private fun findMyIp(): String { return try { NetworkInterface.getNetworkInterfaces()?.toList() ?.filter { it.isUp && !it.isLoopback } @@ -193,36 +174,31 @@ class TcpDiscovery : DiscoveryManager { } private fun isPrivate(addr: java.net.InetAddress): Boolean { - val bytes = addr.address - return (bytes[0].toInt() and 0xFF == 192 && bytes[1].toInt() and 0xFF == 168) || - (bytes[0].toInt() and 0xFF == 10) || - (bytes[0].toInt() and 0xFF == 172 && (bytes[1].toInt() and 0xFF in 16..31)) + val b = addr.address + return (b[0].toInt() and 0xFF == 192 && b[1].toInt() and 0xFF == 168) || + (b[0].toInt() and 0xFF == 10) || + (b[0].toInt() and 0xFF == 172 && b[1].toInt() and 0xFF in 16..31) } override fun stopDiscovery() { - serverThread?.interrupt() - serverThread = null - discoveryThread?.interrupt() - discoveryThread = null + serverThread?.interrupt(); serverThread = null + discoveryThread?.interrupt(); discoveryThread = null try { serverSocket?.close() } catch (_: Exception) {} try { udpSocket?.close() } catch (_: Exception) {} - serverSocket = null - udpSocket = null + serverSocket = null; udpSocket = null discoveredDevices.clear() _devices.value = emptyList() } - override fun dispose() { - stopDiscovery() - } + override fun dispose() { stopDiscovery() } @Serializable data class DiscoveryResponse( - val deviceId: String, - val deviceName: String, - val deviceType: String, - val servicePort: Int, - val hostAddress: String + val id: String, + val name: String, + val type: String, + val port: Int, + val ip: String ) companion object { diff --git a/app/src/desktopMain/kotlin/com/aerotoss/Main.kt b/app/src/desktopMain/kotlin/com/aerotoss/Main.kt index 9ff656b..ccbe139 100644 --- a/app/src/desktopMain/kotlin/com/aerotoss/Main.kt +++ b/app/src/desktopMain/kotlin/com/aerotoss/Main.kt @@ -5,12 +5,13 @@ import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import com.aerotoss.core.AeroTossManager import com.aerotoss.discovery.TcpDiscovery +import com.aerotoss.model.DeviceType import com.aerotoss.transfer.DesktopFileTransferManager import com.aerotoss.ui.App fun main() = application { val transferManager = remember { DesktopFileTransferManager() } - val discoveryManager = remember { TcpDiscovery() } + val discoveryManager = remember { TcpDiscovery(DeviceType.DESKTOP) } val manager = remember { AeroTossManager(discoveryManager, transferManager) } LaunchedEffect(Unit) { @@ -19,10 +20,7 @@ fun main() = application { } Window( - onCloseRequest = { - manager.dispose() - exitApplication() - }, + onCloseRequest = { manager.dispose(); exitApplication() }, title = "AeroToss" ) { App(manager)