diff --git a/app/src/main/java/com/bitchat/android/services/MessageRouter.kt b/app/src/main/java/com/bitchat/android/services/MessageRouter.kt index 81aa1e73..bd33be21 100644 --- a/app/src/main/java/com/bitchat/android/services/MessageRouter.kt +++ b/app/src/main/java/com/bitchat/android/services/MessageRouter.kt @@ -3,11 +3,12 @@ package com.bitchat.android.services import android.content.Context import android.util.Log import com.bitchat.android.mesh.BluetoothMeshService +import com.bitchat.android.mesh.MeshService import com.bitchat.android.model.ReadReceipt import com.bitchat.android.nostr.NostrTransport /** - * Routes messages between BLE mesh and Nostr transports, matching iOS behavior. + * Routes messages between local mesh transports and Nostr, matching iOS behavior. */ class MessageRouter private constructor( private val context: Context, @@ -70,15 +71,13 @@ class MessageRouter private constructor( } } - val hasMesh = mesh.getPeerInfo(toPeerID)?.isConnected == true - val hasEstablished = mesh.hasEstablishedSession(toPeerID) - // Check Wi‑Fi Aware availability as a secondary transport - val aware = try { com.bitchat.android.wifiaware.WifiAwareController.getService() } catch (_: Exception) { null } - val hasAware = try { aware?.getPeerInfo(toPeerID)?.isConnected == true && aware.hasEstablishedSession(toPeerID) } catch (_: Exception) { false } - if (hasMesh && hasEstablished) { + val aware = getAwareService() + val hasMesh = isConnected(mesh, toPeerID) + val hasAwareConnection = isConnected(aware, toPeerID) + if (isReady(mesh, toPeerID)) { Log.d(TAG, "Routing PM via mesh to ${toPeerID} msg_id=${messageID.take(8)}…") mesh.sendPrivateMessage(content, toPeerID, recipientNickname, messageID) - } else if (hasAware) { + } else if (isReady(aware, toPeerID)) { Log.d(TAG, "Routing PM via Wi‑Fi Aware to ${toPeerID} msg_id=${messageID.take(8)}…") aware?.sendPrivateMessage(content, toPeerID, recipientNickname, messageID) } else if (canSendViaNostr(toPeerID)) { @@ -89,18 +88,20 @@ class MessageRouter private constructor( val q = outbox.getOrPut(toPeerID) { mutableListOf() } q.add(Triple(content, recipientNickname, messageID)) Log.d(TAG, "Initiating noise handshake after queueing PM for ${toPeerID.take(8)}…") - if (hasMesh) mesh.initiateNoiseHandshake(toPeerID) else aware?.initiateNoiseHandshake(toPeerID) + when { + hasMesh -> mesh.initiateNoiseHandshake(toPeerID) + hasAwareConnection -> aware?.initiateNoiseHandshake(toPeerID) + else -> aware?.initiateNoiseHandshake(toPeerID) + } } } fun sendReadReceipt(receipt: ReadReceipt, toPeerID: String) { - val aware = try { com.bitchat.android.wifiaware.WifiAwareController.getService() } catch (_: Exception) { null } - val viaMesh = (mesh.getPeerInfo(toPeerID)?.isConnected == true) && mesh.hasEstablishedSession(toPeerID) - val viaAware = try { aware?.getPeerInfo(toPeerID)?.isConnected == true && aware.hasEstablishedSession(toPeerID) } catch (_: Exception) { false } - if (viaMesh) { + val aware = getAwareService() + if (isReady(mesh, toPeerID)) { Log.d(TAG, "Routing READ via mesh to ${toPeerID.take(8)}… id=${receipt.originalMessageID.take(8)}…") mesh.sendReadReceipt(receipt.originalMessageID, toPeerID, mesh.getPeerNicknames()[toPeerID] ?: mesh.myPeerID) - } else if (viaAware) { + } else if (isReady(aware, toPeerID)) { Log.d(TAG, "Routing READ via Wi‑Fi Aware to ${toPeerID.take(8)}… id=${receipt.originalMessageID.take(8)}…") val me = try { aware?.myPeerID } catch (_: Exception) { null } aware?.sendReadReceipt(receipt.originalMessageID, toPeerID, me ?: "") @@ -141,23 +142,34 @@ class MessageRouter private constructor( val queued = outbox[peerID] ?: return if (queued.isEmpty()) return Log.d(TAG, "Flushing outbox for ${peerID.take(8)}… count=${queued.size}") + val aware = getAwareService() val iterator = queued.iterator() while (iterator.hasNext()) { val (content, nickname, messageID) = iterator.next() - var hasMesh = mesh.getPeerInfo(peerID)?.isConnected == true && mesh.hasEstablishedSession(peerID) + val hasMesh = isReady(mesh, peerID) + val hasAware = isReady(aware, peerID) // If this is a noiseHex key, see if there is a connected mesh peer for this identity - if (!hasMesh && peerID.length == 64 && peerID.matches(Regex("^[0-9a-fA-F]+$"))) { - val meshPeer = resolveMeshPeerForNoiseHex(peerID) - if (meshPeer != null && mesh.getPeerInfo(meshPeer)?.isConnected == true && mesh.hasEstablishedSession(meshPeer)) { + if (!hasMesh && !hasAware && peerID.length == 64 && peerID.matches(Regex("^[0-9a-fA-F]+$"))) { + val meshPeer = resolvePeerForNoiseHex(peerID, mesh) + if (meshPeer != null && isReady(mesh, meshPeer)) { mesh.sendPrivateMessage(content, meshPeer, nickname, messageID) iterator.remove() continue } + val awarePeer = resolvePeerForNoiseHex(peerID, aware) + if (awarePeer != null && isReady(aware, awarePeer)) { + aware?.sendPrivateMessage(content, awarePeer, nickname, messageID) + iterator.remove() + continue + } } val canNostr = canSendViaNostr(peerID) if (hasMesh) { mesh.sendPrivateMessage(content, peerID, nickname, messageID) iterator.remove() + } else if (hasAware) { + aware?.sendPrivateMessage(content, peerID, nickname, messageID) + iterator.remove() } else if (canNostr) { nostr.sendPrivateMessage(content, peerID, nickname, messageID) iterator.remove() @@ -195,10 +207,60 @@ class MessageRouter private constructor( return clean.chunked(2).map { it.toInt(16).toByte() }.toByteArray() } - private fun resolveMeshPeerForNoiseHex(noiseHex: String): String? { + private fun getAwareService(): MeshService? { + return try { com.bitchat.android.wifiaware.WifiAwareController.getService() } catch (_: Exception) { null } + } + + private fun isConnected(service: BluetoothMeshService, peerID: String): Boolean { return try { - mesh.getPeerNicknames().keys.firstOrNull { pid -> - val info = mesh.getPeerInfo(pid) + service.getPeerInfo(peerID)?.isConnected == true + } catch (_: Exception) { + false + } + } + + private fun isConnected(service: MeshService?, peerID: String): Boolean { + return try { + service != null && service.getPeerInfo(peerID)?.isConnected == true + } catch (_: Exception) { + false + } + } + + private fun isReady(service: BluetoothMeshService, peerID: String): Boolean { + return try { + service.getPeerInfo(peerID)?.isConnected == true && + service.hasEstablishedSession(peerID) + } catch (_: Exception) { + false + } + } + + private fun isReady(service: MeshService?, peerID: String): Boolean { + return try { + service != null && + service.getPeerInfo(peerID)?.isConnected == true && + service.hasEstablishedSession(peerID) + } catch (_: Exception) { + false + } + } + + private fun resolvePeerForNoiseHex(noiseHex: String, service: BluetoothMeshService): String? { + return try { + service.getPeerNicknames().keys.firstOrNull { pid -> + val info = service.getPeerInfo(pid) + val keyHex = info?.noisePublicKey?.joinToString("") { b -> "%02x".format(b) } + keyHex != null && keyHex.equals(noiseHex, ignoreCase = true) + } + } catch (_: Exception) { null } + } + + private fun resolvePeerForNoiseHex(noiseHex: String, service: MeshService?): String? { + return try { + if (service == null) return null + service.getPeerNicknames().keys.firstOrNull { pid -> + val info = service.getPeerInfo(pid) val keyHex = info?.noisePublicKey?.joinToString("") { b -> "%02x".format(b) } keyHex != null && keyHex.equals(noiseHex, ignoreCase = true) } @@ -213,6 +275,10 @@ class MessageRouter private constructor( mesh.getPeerInfo(pid)?.noisePublicKey?.joinToString("") { b -> "%02x".format(b) } } catch (_: Exception) { null } noiseHex?.let { flushOutboxFor(it) } + val awareNoiseHex = try { + getAwareService()?.getPeerInfo(pid)?.noisePublicKey?.joinToString("") { b -> "%02x".format(b) } + } catch (_: Exception) { null } + awareNoiseHex?.let { flushOutboxFor(it) } } } @@ -223,5 +289,9 @@ class MessageRouter private constructor( mesh.getPeerInfo(peerID)?.noisePublicKey?.joinToString("") { b -> "%02x".format(b) } } catch (_: Exception) { null } noiseHex?.let { flushOutboxFor(it) } + val awareNoiseHex = try { + getAwareService()?.getPeerInfo(peerID)?.noisePublicKey?.joinToString("") { b -> "%02x".format(b) } + } catch (_: Exception) { null } + awareNoiseHex?.let { flushOutboxFor(it) } } } diff --git a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt index 12b5f72f..d38b1169 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt @@ -93,8 +93,8 @@ class ChatViewModel( // Create Noise session delegate for clean dependency injection private val noiseSessionDelegate = object : NoiseSessionDelegate { - override fun hasEstablishedSession(peerID: String): Boolean = meshService.hasEstablishedSession(peerID) - override fun initiateHandshake(peerID: String) = meshService.initiateNoiseHandshake(peerID) + override fun hasEstablishedSession(peerID: String): Boolean = hasEstablishedSessionOnAnyLocalTransport(peerID) + override fun initiateHandshake(peerID: String) = initiateNoiseHandshakeOnBestLocalTransport(peerID) override fun getMyPeerID(): String = meshService.myPeerID } @@ -490,7 +490,7 @@ class ChatViewModel( meshService.sendMessage(messageContent, mentions, channel) try { com.bitchat.android.wifiaware.WifiAwareController.getService()?.sendMessage(messageContent, mentions, channel) } catch (_: Exception) {} } - }) + }, this) return } @@ -520,7 +520,7 @@ class ChatViewModel( } } // Send private message - val recipientNickname = meshService.getPeerNicknames()[selectedPeer] + val recipientNickname = nicknameForPeer(selectedPeer) privateChatManager.sendPrivateMessage( content, selectedPeer, @@ -661,6 +661,79 @@ class ChatViewModel( Log.i("ChatViewModel", "Peer fingerprints: ${privateChatManager.getAllPeerFingerprints()}") Log.i("ChatViewModel", "==============================") } + + private fun getWifiAwareService(): com.bitchat.android.mesh.MeshService? { + return try { com.bitchat.android.wifiaware.WifiAwareController.getService() } catch (_: Exception) { null } + } + + private fun isConnectedOnMesh(peerID: String): Boolean { + return try { + meshService.getPeerInfo(peerID)?.isConnected == true + } catch (_: Exception) { + false + } + } + + private fun isConnectedOnService(service: com.bitchat.android.mesh.MeshService?, peerID: String): Boolean { + return try { + service != null && service.getPeerInfo(peerID)?.isConnected == true + } catch (_: Exception) { + false + } + } + + private fun hasEstablishedSessionOnMesh(peerID: String): Boolean { + return try { + meshService.getPeerInfo(peerID)?.isConnected == true && + meshService.hasEstablishedSession(peerID) + } catch (_: Exception) { + false + } + } + + private fun hasEstablishedSessionOnService(service: com.bitchat.android.mesh.MeshService?, peerID: String): Boolean { + return try { + service != null && + service.getPeerInfo(peerID)?.isConnected == true && + service.hasEstablishedSession(peerID) + } catch (_: Exception) { + false + } + } + + private fun hasEstablishedSessionOnAnyLocalTransport(peerID: String): Boolean { + return hasEstablishedSessionOnMesh(peerID) || + hasEstablishedSessionOnService(getWifiAwareService(), peerID) + } + + private fun initiateNoiseHandshakeOnBestLocalTransport(peerID: String) { + val aware = getWifiAwareService() + when { + isConnectedOnMesh(peerID) -> meshService.initiateNoiseHandshake(peerID) + isConnectedOnService(aware, peerID) -> aware?.initiateNoiseHandshake(peerID) + else -> meshService.initiateNoiseHandshake(peerID) + } + } + + private fun nicknameForPeer(peerID: String): String? { + return state.peerNicknames.value[peerID] + ?: try { meshService.getPeerNicknames()[peerID] } catch (_: Exception) { null } + ?: try { getWifiAwareService()?.getPeerNicknames()?.get(peerID) } catch (_: Exception) { null } + } + + private fun sessionStateForPeer(peerID: String): NoiseSession.NoiseSessionState { + val meshState = try { meshService.getSessionState(peerID) } catch (_: Exception) { NoiseSession.NoiseSessionState.Uninitialized } + val awareState = try { getWifiAwareService()?.getSessionState(peerID) } catch (_: Exception) { null } + return when { + meshState is NoiseSession.NoiseSessionState.Established -> meshState + awareState is NoiseSession.NoiseSessionState.Established -> awareState + meshState is NoiseSession.NoiseSessionState.Handshaking -> meshState + awareState is NoiseSession.NoiseSessionState.Handshaking -> awareState + meshState !is NoiseSession.NoiseSessionState.Uninitialized -> meshState + awareState != null -> awareState + else -> meshState + } + } /** * Initialize session state monitoring for reactive UI updates @@ -685,7 +758,7 @@ class ChatViewModel( // Update session states val prevStates = state.getPeerSessionStatesValue() val sessionStates = currentPeers.associateWith { peerID -> - meshService.getSessionState(peerID).toString() + sessionStateForPeer(peerID).toString() } state.setPeerSessionStates(sessionStates) // Detect new established sessions and flush router outbox for them and their noiseHex aliases @@ -736,7 +809,7 @@ class ChatViewModel( // Flush any pending QR verification once a Noise session is established currentPeers.forEach { peerID -> - if (meshService.getSessionState(peerID) is NoiseSession.NoiseSessionState.Established) { + if (sessionStateForPeer(peerID) is NoiseSession.NoiseSessionState.Established) { verificationHandler.sendPendingVerificationIfNeeded(peerID) } } diff --git a/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt b/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt index 499b3926..18048f35 100644 --- a/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt +++ b/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt @@ -36,14 +36,14 @@ class CommandProcessor( val cmd = parts.first().lowercase() when (cmd) { "/j", "/join" -> handleJoinCommand(parts, myPeerID) - "/m", "/msg" -> handleMessageCommand(parts, meshService) + "/m", "/msg" -> handleMessageCommand(parts, meshService, viewModel) "/w" -> handleWhoCommand(meshService, viewModel) "/clear" -> handleClearCommand() "/pass" -> handlePassCommand(parts, myPeerID) "/block" -> handleBlockCommand(parts, meshService) "/unblock" -> handleUnblockCommand(parts, meshService) - "/hug" -> handleActionCommand(parts, "gives", "a warm hug 🫂", meshService, myPeerID, onSendMessage) - "/slap" -> handleActionCommand(parts, "slaps", "around a bit with a large trout 🐟", meshService, myPeerID, onSendMessage) + "/hug" -> handleActionCommand(parts, "gives", "a warm hug 🫂", meshService, myPeerID, onSendMessage, viewModel) + "/slap" -> handleActionCommand(parts, "slaps", "around a bit with a large trout 🐟", meshService, myPeerID, onSendMessage, viewModel) "/channels" -> handleChannelsCommand() else -> handleUnknownCommand(cmd) } @@ -77,7 +77,7 @@ class CommandProcessor( } } - private fun handleMessageCommand(parts: List, meshService: BluetoothMeshService) { + private fun handleMessageCommand(parts: List, meshService: BluetoothMeshService, viewModel: ChatViewModel?) { if (parts.size > 1) { val targetName = parts[1].removePrefix("@") val peerID = getPeerIDForNickname(targetName, meshService) @@ -96,8 +96,7 @@ class CommandProcessor( state.getNicknameValue(), getMyPeerID(meshService) ) { content, peerIdParam, recipientNicknameParam, messageId -> - // This would trigger the actual mesh service send - sendPrivateMessageVia(meshService, content, peerIdParam, recipientNicknameParam, messageId) + sendPrivateMessageVia(meshService, content, peerIdParam, recipientNicknameParam, messageId, viewModel) } } else { val systemMessage = BitchatMessage( @@ -286,7 +285,8 @@ class CommandProcessor( object_: String, meshService: BluetoothMeshService, myPeerID: String, - onSendMessage: (String, List, String?) -> Unit + onSendMessage: (String, List, String?) -> Unit, + viewModel: ChatViewModel? ) { if (parts.size > 1) { val targetName = parts[1].removePrefix("@") @@ -306,7 +306,7 @@ class CommandProcessor( state.getNicknameValue(), myPeerID ) { content, peerIdParam, recipientNicknameParam, messageId -> - sendPrivateMessageVia(meshService, content, peerIdParam, recipientNicknameParam, messageId) + sendPrivateMessageVia(meshService, content, peerIdParam, recipientNicknameParam, messageId, viewModel) } } else if (isInLocationChannel) { // Let the transport layer add the echo; just send it out @@ -505,17 +505,41 @@ class CommandProcessor( private fun getPeerIDForNickname(nickname: String, meshService: BluetoothMeshService): String? { return meshService.getPeerNicknames().entries.find { it.value == nickname }?.key + ?: try { + com.bitchat.android.wifiaware.WifiAwareController.getService() + ?.getPeerNicknames() + ?.entries + ?.find { it.value == nickname } + ?.key + } catch (_: Exception) { + null + } } private fun getPeerNickname(peerID: String, meshService: BluetoothMeshService): String { - return meshService.getPeerNicknames()[peerID] ?: peerID + return meshService.getPeerNicknames()[peerID] + ?: try { com.bitchat.android.wifiaware.WifiAwareController.getService()?.getPeerNicknames()?.get(peerID) } catch (_: Exception) { null } + ?: peerID } private fun getMyPeerID(meshService: BluetoothMeshService): String { return meshService.myPeerID } - private fun sendPrivateMessageVia(meshService: BluetoothMeshService, content: String, peerID: String, recipientNickname: String, messageId: String) { - meshService.sendPrivateMessage(content, peerID, recipientNickname, messageId) + private fun sendPrivateMessageVia( + meshService: BluetoothMeshService, + content: String, + peerID: String, + recipientNickname: String, + messageId: String, + viewModel: ChatViewModel? + ) { + if (viewModel != null) { + com.bitchat.android.services.MessageRouter + .getInstance(viewModel.getApplication(), meshService) + .sendPrivate(content, peerID, recipientNickname, messageId) + } else { + meshService.sendPrivateMessage(content, peerID, recipientNickname, messageId) + } } } diff --git a/app/src/main/java/com/bitchat/android/wifi-aware/WifiAwareMeshService.kt b/app/src/main/java/com/bitchat/android/wifi-aware/WifiAwareMeshService.kt index 6b85878c..b4e5e202 100644 --- a/app/src/main/java/com/bitchat/android/wifi-aware/WifiAwareMeshService.kt +++ b/app/src/main/java/com/bitchat/android/wifi-aware/WifiAwareMeshService.kt @@ -113,6 +113,10 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor private val connectionTracker = WifiAwareConnectionTracker(serviceScope, cm) private val handleToPeerId = ConcurrentHashMap() // discovery mapping private val discoveredTimestamps = ConcurrentHashMap() // peerID -> last seen time + // Subscribe-session-scoped handles only. PeerHandles are session-scoped, so a handle obtained + // from the publish session is NOT valid for subscribeSession.sendMessage(). Maintenance re-pings + // (subscriber -> publisher) must use a handle that originated from the subscribe session. + private val subscribeHandles = ConcurrentHashMap() // peerID -> latest subscribe handle fun isRunning(): Boolean = isActive @@ -121,6 +125,14 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor // This avoids race conditions and ensures a single gossip source/delegate com.bitchat.android.service.MeshServiceHolder.getOrCreate(context) val shared = com.bitchat.android.service.MeshServiceHolder.sharedGossipSyncManager + encryptionService.onSessionEstablished = { peerID -> + Log.d(TAG, "Wi-Fi Aware Noise session established with ${peerID.take(8)}") + try { + com.bitchat.android.services.MessageRouter + .tryGetInstance() + ?.onSessionEstablished(peerID) + } catch (_: Exception) { } + } meshCore = MeshCore( context = context.applicationContext, scope = serviceScope, @@ -414,6 +426,9 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor if (!isCurrentSession(generation)) return val peerId = try { String(serviceSpecificInfo) } catch (_: Exception) { "" } handleToPeerId[peerHandle] = peerId + // This handle came from the subscribe session, so it is valid for + // subscribeSession.sendMessage() (used by maintenance reconnection). + if (peerId.isNotBlank()) subscribeHandles[peerId] = peerHandle val msgId = (System.nanoTime() and 0x7fffffff).toInt() subscribeSession?.sendMessage(peerHandle, msgId, myPeerID.toByteArray()) if (peerId.isNotBlank()) discoveredTimestamps[peerId] = System.currentTimeMillis() @@ -507,6 +522,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor wifiAwareSession?.close(); wifiAwareSession = null handleToPeerId.clear() + subscribeHandles.clear() discoveredTimestamps.clear() meshCore.shutdown() @@ -548,6 +564,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor if (subscribeSession === oldSubscribeSession) subscribeSession = null if (wifiAwareSession === oldWifiAwareSession) wifiAwareSession = null handleToPeerId.clear() + subscribeHandles.clear() discoveredTimestamps.clear() } } finally { @@ -582,6 +599,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor if (staleIds.isNotEmpty()) { staleIds.forEach { discoveredTimestamps.remove(it) } handleToPeerId.entries.removeIf { it.value in staleIds } + staleIds.forEach { subscribeHandles.remove(it) } Log.d(TAG, "Maintenance: pruned ${staleIds.size} stale discovery entries") } @@ -597,8 +615,15 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor // 3. Attempt reconnection for (peerId in disconnectedPeers) { - // Find the PeerHandle for this peerId - val handle = handleToPeerId.entries.find { it.value == peerId }?.key ?: continue + // Reconnection is a subscriber -> publisher ping. It only succeeds when the + // remote is the server for this pair (i.e. we are the client). Re-pinging a + // peer we are the server for is dropped on their side, so skip it; that peer + // must re-ping us instead. + if (amIServerFor(peerId)) continue + + // Use a subscribe-session-scoped handle. A publish-scoped handle would be + // invalid for subscribeSession.sendMessage() and silently fail. + val handle = subscribeHandles[peerId] ?: continue // Check tracker policy if (!connectionTracker.isConnectionAttemptAllowed(peerId)) continue @@ -635,6 +660,10 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor val peerId = handleToPeerId[peerHandle] ?: return if (!amIServerFor(peerId)) return + if (connectionTracker.isConnected(peerId)) { + Log.v(TAG, "↪ already connected to $peerId, skipping serve") + return + } if (connectionTracker.hasOpenServerSocket(peerId)) { Log.v(TAG, "↪ already serving $peerId, skipping") return @@ -690,6 +719,11 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor val synced = SyncedSocket(client) activeSocket = synced connectionTracker.onClientConnected(peerId, synced) + // We only ever accept a single data socket per server request. Close the + // listening ServerSocket now so it can't block a future re-serve (its + // presence makes hasOpenServerSocket() true for the life of the process) + // and so we free the fd/port promptly. + connectionTracker.closeServerSocket(peerId) try { meshCore.setDirectConnection(peerId, true) } catch (_: Exception) {} try { meshCore.addOrUpdatePeer(peerId, peerId) } catch (_: Exception) {} listenerExec.execute { listenToPeer(synced, peerId) } @@ -762,7 +796,14 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor try { while (connectionTracker.isConnected(peerId)) { // write empty byte array effectively sends [4 bytes length=0] which is our ping - try { client.write(ByteArray(0)) } catch (_: IOException) { break } + try { + client.write(ByteArray(0)) + } catch (_: IOException) { + // The write side is dead. Don't just stop pinging: actively tear down so the + // half-open socket stops counting as "connected" and maintenance can retry. + handlePeerDisconnection(peerId, client) + break + } delay(2_000) } } catch (_: Exception) {} @@ -907,7 +948,14 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor serviceScope.launch { try { while (connectionTracker.isConnected(peerId)) { - try { sock.write(ByteArray(0)) } catch (_: IOException) { break } + try { + sock.write(ByteArray(0)) + } catch (_: IOException) { + // The write side is dead. Tear down so the half-open socket stops counting + // as "connected" and maintenance can retry instead of silently stalling. + handlePeerDisconnection(peerId, sock) + break + } delay(2_000) } } catch (_: Exception) {} @@ -956,6 +1004,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor handleToPeerId[handle] = senderPeerHex } } + subscribeHandles.remove(previousPeerId)?.let { subscribeHandles[senderPeerHex] = it } discoveredTimestamps.remove(previousPeerId) discoveredTimestamps[senderPeerHex] = System.currentTimeMillis() try { meshCore.setDirectConnection(previousPeerId, false) } catch (_: Exception) { }