udp socket binding istead of process binding to allow internet over network for maps etc
This commit is contained in:
@@ -18,5 +18,9 @@ class MainActivity : FlutterActivity() {
|
||||
flutterEngine.dartExecutor.binaryMessenger,
|
||||
MlrsNetworkPlugin.EVENT_CHANNEL,
|
||||
).setStreamHandler(plugin)
|
||||
EventChannel(
|
||||
flutterEngine.dartExecutor.binaryMessenger,
|
||||
MlrsNetworkPlugin.SOCKET_EVENT_CHANNEL,
|
||||
).setStreamHandler(plugin.socketStreamHandler)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,12 @@ import android.os.PatternMatcher
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import java.net.BindException
|
||||
import java.net.DatagramPacket
|
||||
import java.net.DatagramSocket
|
||||
import java.net.InetAddress
|
||||
import java.net.InetSocketAddress
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
/**
|
||||
* Verbindungsaufbau zur mLRS-WLAN-Bruecke ausschliesslich aus der App heraus
|
||||
@@ -25,12 +31,17 @@ import io.flutter.plugin.common.MethodChannel
|
||||
* Ablauf: [requestNetwork] loest `ConnectivityManager.requestNetwork()` aus,
|
||||
* das System zeigt dabei automatisch einen Bestaetigungsdialog fuer die
|
||||
* passende SSID. `onAvailable`/`onUnavailable`/`onLost` werden ueber den
|
||||
* EventChannel nach Dart gemeldet, das den restlichen Ablauf (Socket binden,
|
||||
* lauschen, Fehleranzeige) uebernimmt.
|
||||
* EventChannel nach Dart gemeldet.
|
||||
*
|
||||
* [bindProcessToNetwork]/[unbindProcessFromNetwork] setzen/loeschen die
|
||||
* Prozessbindung fuer das kurze Fenster, in dem Dart den UDP-Socket erzeugt -
|
||||
* siehe UdpTransport.connect() fuer die Gegenseite dieses Musters.
|
||||
* Der UDP-Socket selbst wird ebenfalls hier (nativ) gehalten und per
|
||||
* `Network.bindSocket()` **socket-genau** an das mLRS-Netz gebunden (Doku
|
||||
* Abschnitt 4, Stufe 3) - bewusst NICHT ueber
|
||||
* `ConnectivityManager.bindProcessToNetwork()`, das den gesamten Prozess
|
||||
* (inkl. gleichzeitiger Kartenkacheln-/Wetter-HTTP-Requests) betrifft und in
|
||||
* der Praxis auf Geraeten ohne STA-Concurrency zu genau diesen Requests
|
||||
* gefuehrt hat, die "auf dem falschen Netz" haengen blieben. Socket-genaue
|
||||
* Bindung ist Androids eigene Empfehlung fuer Apps, die gleichzeitig ein
|
||||
* spezielles lokales Netz UND normales Internet brauchen.
|
||||
*/
|
||||
class MlrsNetworkPlugin(
|
||||
private val context: Context,
|
||||
@@ -39,23 +50,41 @@ class MlrsNetworkPlugin(
|
||||
companion object {
|
||||
const val METHOD_CHANNEL = "com.dmc.dmc_app/mlrs_network"
|
||||
const val EVENT_CHANNEL = "com.dmc.dmc_app/mlrs_network_events"
|
||||
const val SOCKET_EVENT_CHANNEL = "com.dmc.dmc_app/mlrs_socket_events"
|
||||
private const val RECEIVE_BUFFER_SIZE = 2048
|
||||
}
|
||||
|
||||
private val connectivityManager =
|
||||
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
|
||||
// NetworkCallback-Methoden laufen ohne expliziten Handler auf einem
|
||||
// internen ConnectivityThread, nicht auf dem Main-Thread - Flutters
|
||||
// EventChannel.EventSink.success() verlangt aber @UiThread (schlaegt
|
||||
// sonst mit "Methods marked with @UiThread must be executed on the main
|
||||
// thread" fehl). Deshalb Callback-Dispatch explizit auf den Main-Looper
|
||||
// legen.
|
||||
// NetworkCallback-Methoden und der Socket-Empfangs-Thread laufen nicht auf
|
||||
// dem Main-Thread - Flutters EventChannel.EventSink.success()/
|
||||
// MethodChannel.Result verlangen aber @UiThread (siehe Crash-Historie:
|
||||
// "Methods marked with @UiThread must be executed on the main thread").
|
||||
// Deshalb jeden Ruecksprung nach Dart explizit ueber den Main-Looper.
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
private val socketExecutor = Executors.newSingleThreadExecutor()
|
||||
|
||||
private var eventSink: EventChannel.EventSink? = null
|
||||
private var socketEventSink: EventChannel.EventSink? = null
|
||||
private var networkCallback: ConnectivityManager.NetworkCallback? = null
|
||||
private var currentNetwork: Network? = null
|
||||
|
||||
private var socket: DatagramSocket? = null
|
||||
@Volatile
|
||||
private var receiveLoopRunning = false
|
||||
|
||||
/** Separater StreamHandler fuer den Socket-Daten-Kanal (siehe MainActivity). */
|
||||
val socketStreamHandler = object : EventChannel.StreamHandler {
|
||||
override fun onListen(arguments: Any?, sink: EventChannel.EventSink?) {
|
||||
socketEventSink = sink
|
||||
}
|
||||
|
||||
override fun onCancel(arguments: Any?) {
|
||||
socketEventSink = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onListen(arguments: Any?, sink: EventChannel.EventSink?) {
|
||||
eventSink = sink
|
||||
}
|
||||
@@ -79,21 +108,27 @@ class MlrsNetworkPlugin(
|
||||
releaseNetwork()
|
||||
result.success(null)
|
||||
}
|
||||
"bindProcessToNetwork" -> {
|
||||
val network = currentNetwork
|
||||
if (network == null) {
|
||||
result.success(false)
|
||||
} else {
|
||||
result.success(connectivityManager.bindProcessToNetwork(network))
|
||||
}
|
||||
}
|
||||
"unbindProcessFromNetwork" -> {
|
||||
connectivityManager.bindProcessToNetwork(null)
|
||||
result.success(null)
|
||||
}
|
||||
"isStaConcurrencySupported" -> {
|
||||
result.success(isStaConcurrencySupported())
|
||||
}
|
||||
"openSocket" -> {
|
||||
val port = call.argument<Int>("port") ?: 0
|
||||
openSocket(port, result)
|
||||
}
|
||||
"closeSocket" -> {
|
||||
closeSocket()
|
||||
result.success(null)
|
||||
}
|
||||
"sendPacket" -> {
|
||||
val data = call.argument<ByteArray>("data")
|
||||
val host = call.argument<String>("host")
|
||||
val port = call.argument<Int>("port")
|
||||
if (data == null || host == null || port == null) {
|
||||
result.error("invalid_argument", "data/host/port erforderlich", null)
|
||||
return
|
||||
}
|
||||
sendPacket(data, host, port, result)
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
@@ -161,4 +196,103 @@ class MlrsNetworkPlugin(
|
||||
?: return null
|
||||
return wifiManager.isStaConcurrencyForLocalOnlyConnectionsSupported
|
||||
}
|
||||
|
||||
private fun openSocket(requestedPort: Int, result: MethodChannel.Result) {
|
||||
val network = currentNetwork
|
||||
if (network == null) {
|
||||
result.error("no_network", "Kein mLRS-Netz verfuegbar, um den Socket zu binden.", null)
|
||||
return
|
||||
}
|
||||
socketExecutor.execute {
|
||||
try {
|
||||
val newSocket = DatagramSocket(null)
|
||||
// reuseAddress: false, damit ein tatsaechlich belegter Port
|
||||
// hier erkennbar mit BindException scheitert (Doku 3/9: "ist
|
||||
// 14550 belegt, ausweichen, aber protokollieren").
|
||||
newSocket.reuseAddress = false
|
||||
var fallbackFromPort: Int? = null
|
||||
try {
|
||||
newSocket.bind(InetSocketAddress(requestedPort))
|
||||
} catch (e: BindException) {
|
||||
newSocket.bind(InetSocketAddress(0))
|
||||
fallbackFromPort = requestedPort
|
||||
}
|
||||
|
||||
// Socket-genaue Bindung an das mLRS-Netz (siehe Klassendoku
|
||||
// oben) - betrifft ausschliesslich diesen einen Socket, nicht
|
||||
// den restlichen Prozess.
|
||||
network.bindSocket(newSocket)
|
||||
|
||||
socket = newSocket
|
||||
startReceiveLoop(newSocket)
|
||||
|
||||
val boundPort = newSocket.localPort
|
||||
mainHandler.post {
|
||||
result.success(
|
||||
mapOf("boundPort" to boundPort, "fallbackFromPort" to fallbackFromPort),
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
mainHandler.post {
|
||||
result.error("socket_error", e.message, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startReceiveLoop(socket: DatagramSocket) {
|
||||
receiveLoopRunning = true
|
||||
Thread {
|
||||
val buffer = ByteArray(RECEIVE_BUFFER_SIZE)
|
||||
while (receiveLoopRunning) {
|
||||
try {
|
||||
val packet = DatagramPacket(buffer, buffer.size)
|
||||
socket.receive(packet)
|
||||
val data = packet.data.copyOfRange(packet.offset, packet.offset + packet.length)
|
||||
val address = packet.address.hostAddress
|
||||
val port = packet.port
|
||||
mainHandler.post {
|
||||
socketEventSink?.success(
|
||||
mapOf("address" to address, "port" to port, "data" to data),
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Socket wurde geschlossen (closeSocket()) oder das Netz
|
||||
// ist weg - Netzverlust wird bereits ueber den
|
||||
// Network-Events-Kanal gemeldet, hier reicht es, die
|
||||
// Empfangsschleife zu beenden statt in eine Busy-Loop zu
|
||||
// laufen.
|
||||
break
|
||||
}
|
||||
}
|
||||
}.apply {
|
||||
isDaemon = true
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
private fun closeSocket() {
|
||||
receiveLoopRunning = false
|
||||
socket?.close()
|
||||
socket = null
|
||||
}
|
||||
|
||||
private fun sendPacket(data: ByteArray, host: String, port: Int, result: MethodChannel.Result) {
|
||||
val currentSocket = socket
|
||||
if (currentSocket == null) {
|
||||
mainHandler.post {
|
||||
result.error("no_socket", "Kein offener Socket zum Senden.", null)
|
||||
}
|
||||
return
|
||||
}
|
||||
socketExecutor.execute {
|
||||
try {
|
||||
val address = InetAddress.getByName(host)
|
||||
currentSocket.send(DatagramPacket(data, data.size, address, port))
|
||||
mainHandler.post { result.success(null) }
|
||||
} catch (e: Exception) {
|
||||
mainHandler.post { result.error("send_failed", e.message, null) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user