AeroToss: полный рефакторинг discovery
- 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
This commit is contained in:
@@ -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<String>
|
||||
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() {
|
||||
|
||||
@@ -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<List<Device>>(emptyList())
|
||||
override val devices: Flow<List<Device>> = _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<String, Device>()
|
||||
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<String, Device>()
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -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<List<Device>>(emptyList())
|
||||
override val devices: Flow<List<Device>> = _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<DiscoveryResponse>(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<DiscoveryResponse>(line)
|
||||
Device(
|
||||
id = response.deviceId,
|
||||
name = response.deviceName,
|
||||
type = if (response.deviceType == "desktop") DeviceType.DESKTOP else DeviceType.PHONE,
|
||||
hostAddress = host,
|
||||
port = response.servicePort
|
||||
)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
Device(
|
||||
id = fallbackId,
|
||||
name = fallbackName,
|
||||
type = DeviceType.PHONE,
|
||||
hostAddress = host,
|
||||
port = fallbackPort
|
||||
id = resp.id,
|
||||
name = resp.name,
|
||||
type = if (resp.type == "desktop") DeviceType.DESKTOP else DeviceType.PHONE,
|
||||
hostAddress = resp.ip,
|
||||
port = resp.port
|
||||
)
|
||||
}
|
||||
} 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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user