diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt index b982f199..ab4f4672 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt @@ -37,7 +37,7 @@ class BluetoothConnectionManager( // Component managers private val permissionManager = BluetoothPermissionManager(context) private val connectionTracker = BluetoothConnectionTracker(connectionScope, powerManager) - private val packetBroadcaster = BluetoothPacketBroadcaster(connectionScope, connectionTracker, fragmentManager) + private val packetBroadcaster = BluetoothPacketBroadcaster(connectionScope, connectionTracker, fragmentManager, myPeerID) // Delegate for component managers to call back to main manager private val componentDelegate = object : BluetoothConnectionManagerDelegate { diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothPacketBroadcaster.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothPacketBroadcaster.kt index c003afc7..5febbf81 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothPacketBroadcaster.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothPacketBroadcaster.kt @@ -42,7 +42,8 @@ import kotlinx.coroutines.channels.actor class BluetoothPacketBroadcaster( private val connectionScope: CoroutineScope, private val connectionTracker: BluetoothConnectionTracker, - private val fragmentManager: FragmentManager? + private val fragmentManager: FragmentManager?, + private val myPeerID: String ) { companion object { @@ -349,6 +350,47 @@ class BluetoothPacketBroadcaster( val senderNick = senderPeerID.let { pid -> nicknameResolver?.invoke(pid) } val route = packet.route val routeInfo = if (!route.isNullOrEmpty()) "routed: ${route.size} hops" else null + + // Source Routing for Originating Packets + // If we are the sender and a source route is defined, we must send ONLY to the first hop. + if (packet.senderID.toHexString() == myPeerID && !packet.route.isNullOrEmpty()) { + val firstHop = packet.route!![0].toHexString() + Log.d(TAG, "Source Routing: Packet has explicit route, attempting to send to first hop: $firstHop") + + var sent = false + + // Try to find first hop in server connections (subscribedDevices) + val serverTarget = connectionTracker.getSubscribedDevices() + .firstOrNull { connectionTracker.addressPeerMap[it.address] == firstHop } + + if (serverTarget != null) { + Log.d(TAG, "Source Routing: sending directly to first hop (server conn) $firstHop: ${serverTarget.address}") + if (notifyDevice(serverTarget, data, gattServer, characteristic)) { + val toPeer = connectionTracker.addressPeerMap[serverTarget.address] + logPacketRelay(typeName, senderPeerID, senderNick, incomingPeer, incomingAddr, toPeer, serverTarget.address, packet.ttl, packet.version, routeInfo) + sent = true + } + } + + // Try to find first hop in client connections if not sent yet + if (!sent) { + val clientTarget = connectionTracker.getConnectedDevices().values + .firstOrNull { connectionTracker.addressPeerMap[it.device.address] == firstHop } + + if (clientTarget != null) { + Log.d(TAG, "Source Routing: sending directly to first hop (client conn) $firstHop: ${clientTarget.device.address}") + if (writeToDeviceConn(clientTarget, data)) { + val toPeer = connectionTracker.addressPeerMap[clientTarget.device.address] + logPacketRelay(typeName, senderPeerID, senderNick, incomingPeer, incomingAddr, toPeer, clientTarget.device.address, packet.ttl, packet.version, routeInfo) + sent = true + } + } + } + + if (sent) return + + Log.w(TAG, "Source Routing: First hop $firstHop not connected. Falling back to standard broadcast logic.") + } if (packet.recipientID != SpecialRecipients.BROADCAST) { val recipientID = packet.recipientID?.let {