package com.bitchat.android.mesh import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothGatt import android.bluetooth.BluetoothGattCharacteristic import android.bluetooth.BluetoothGattServer import android.util.Log import com.bitchat.android.protocol.SpecialRecipients import com.bitchat.android.model.RoutedPacket import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch /** * Handles packet broadcasting to connected devices */ class BluetoothPacketBroadcaster( private val connectionScope: CoroutineScope, private val connectionTracker: BluetoothConnectionTracker ) { companion object { private const val TAG = "BluetoothPacketBroadcaster" private const val CLEANUP_DELAY = 500L } /** * Broadcast packet to connected devices with connection limit enforcement */ fun broadcastPacket( routed: RoutedPacket, gattServer: BluetoothGattServer?, characteristic: BluetoothGattCharacteristic? ) { val packet = routed.packet val data = packet.toBinaryData() ?: return if (packet.recipientID != SpecialRecipients.BROADCAST) { val recipientID = packet.recipientID?.let { String(it).replace("\u0000", "").trim() } ?: "" // Try to find the recipient in server connections (subscribedDevices) val targetDevice = connectionTracker.getSubscribedDevices() .firstOrNull { connectionTracker.addressPeerMap[it.address] == recipientID } // If found, send directly if (targetDevice != null) { Log.d(TAG, "Send packet type ${packet.type} directly to target device for recipient $recipientID: ${targetDevice.address}") if (notifyDevice(targetDevice, data, gattServer, characteristic)) return // Sent, no need to continue } // Try to find the recipient in client connections (connectedDevices) val targetDeviceConn = connectionTracker.getConnectedDevices().values .firstOrNull { connectionTracker.addressPeerMap[it.device.address] == recipientID } // If found, send directly if (targetDeviceConn != null) { Log.d(TAG, "Send packet type ${packet.type} directly to target client connection for recipient $recipientID: ${targetDeviceConn.device.address}") if (writeToDeviceConn(targetDeviceConn, data)) return // Sent, no need to continue } } // Else, continue with broadcasting to all devices val subscribedDevices = connectionTracker.getSubscribedDevices() val connectedDevices = connectionTracker.getConnectedDevices() Log.d(TAG, "Broadcasting packet type ${packet.type} to ${subscribedDevices.size} server + ${connectedDevices.size} client connections") val senderID = String(packet.senderID).replace("\u0000", "") // Send to server connections (devices connected to our GATT server) subscribedDevices.forEach { device -> if (device.address == routed.relayAddress) { Log.d(TAG, "Skipping broadcast back to relayer: ${device.address}") return@forEach } if (connectionTracker.addressPeerMap[device.address] == senderID) { Log.d(TAG, "Skipping broadcast back to sender: ${device.address}") return@forEach } notifyDevice(device, data, gattServer, characteristic) } // Send to client connections connectedDevices.values.forEach { deviceConn -> if (deviceConn.isClient && deviceConn.gatt != null && deviceConn.characteristic != null) { if (deviceConn.device.address == routed.relayAddress) { Log.d(TAG, "Skipping broadcast back to relayer: ${deviceConn.device.address}") return@forEach } if (connectionTracker.addressPeerMap[deviceConn.device.address] == senderID) { Log.d(TAG, "Skipping broadcast back to sender: ${deviceConn.device.address}") return@forEach } writeToDeviceConn(deviceConn, data) } } } /** * Send data to a single device (server side) */ private fun notifyDevice( device: BluetoothDevice, data: ByteArray, gattServer: BluetoothGattServer?, characteristic: BluetoothGattCharacteristic? ): Boolean { return try { characteristic?.let { char -> char.value = data val result = gattServer?.notifyCharacteristicChanged(device, char, false) ?: false result } ?: false } catch (e: Exception) { Log.w(TAG, "Error sending to server connection ${device.address}: ${e.message}") connectionScope.launch { delay(CLEANUP_DELAY) connectionTracker.removeSubscribedDevice(device) connectionTracker.addressPeerMap.remove(device.address) } false } } /** * Send data to a single device (client side) */ private fun writeToDeviceConn( deviceConn: BluetoothConnectionTracker.DeviceConnection, data: ByteArray ): Boolean { return try { deviceConn.characteristic?.let { char -> char.value = data val result = deviceConn.gatt?.writeCharacteristic(char) ?: false result } ?: false } catch (e: Exception) { Log.w(TAG, "Error sending to client connection ${deviceConn.device.address}: ${e.message}") connectionScope.launch { delay(CLEANUP_DELAY) connectionTracker.cleanupDeviceConnection(deviceConn.device.address) } false } } }