Merge pull request #48 from ShilohEye/fix/android-ios-fragmentation-compatibility

Fix Android Message fragmentation for iOS compatibility
This commit is contained in:
callebtc
2025-07-13 23:29:32 +02:00
committed by GitHub
4 changed files with 51 additions and 20 deletions
@@ -14,7 +14,8 @@ import kotlinx.coroutines.*
*/ */
class BluetoothConnectionManager( class BluetoothConnectionManager(
private val context: Context, private val context: Context,
private val myPeerID: String private val myPeerID: String,
private val fragmentManager: FragmentManager? = null
) : PowerManagerDelegate { ) : PowerManagerDelegate {
companion object { companion object {
@@ -35,7 +36,7 @@ class BluetoothConnectionManager(
// Component managers // Component managers
private val permissionManager = BluetoothPermissionManager(context) private val permissionManager = BluetoothPermissionManager(context)
private val connectionTracker = BluetoothConnectionTracker(connectionScope, powerManager) private val connectionTracker = BluetoothConnectionTracker(connectionScope, powerManager)
private val packetBroadcaster = BluetoothPacketBroadcaster(connectionScope, connectionTracker) private val packetBroadcaster = BluetoothPacketBroadcaster(connectionScope, connectionTracker, fragmentManager)
// Delegate for component managers to call back to main manager // Delegate for component managers to call back to main manager
private val componentDelegate = object : BluetoothConnectionManagerDelegate { private val componentDelegate = object : BluetoothConnectionManagerDelegate {
@@ -156,6 +157,7 @@ class BluetoothConnectionManager(
/** /**
* Broadcast packet to connected devices with connection limit enforcement * Broadcast packet to connected devices with connection limit enforcement
* Automatically fragments large packets to fit within BLE MTU limits
*/ */
fun broadcastPacket(routed: RoutedPacket) { fun broadcastPacket(routed: RoutedPacket) {
if (!isActive) return if (!isActive) return
@@ -45,7 +45,7 @@ class BluetoothMeshService(private val context: Context) {
private val securityManager = SecurityManager(encryptionService, myPeerID) private val securityManager = SecurityManager(encryptionService, myPeerID)
private val storeForwardManager = StoreForwardManager() private val storeForwardManager = StoreForwardManager()
private val messageHandler = MessageHandler(myPeerID) private val messageHandler = MessageHandler(myPeerID)
internal val connectionManager = BluetoothConnectionManager(context, myPeerID) // Made internal for access internal val connectionManager = BluetoothConnectionManager(context, myPeerID, fragmentManager) // Made internal for access
private val packetProcessor = PacketProcessor(myPeerID) private val packetProcessor = PacketProcessor(myPeerID)
// Service state management // Service state management
@@ -16,7 +16,8 @@ import kotlinx.coroutines.launch
*/ */
class BluetoothPacketBroadcaster( class BluetoothPacketBroadcaster(
private val connectionScope: CoroutineScope, private val connectionScope: CoroutineScope,
private val connectionTracker: BluetoothConnectionTracker private val connectionTracker: BluetoothConnectionTracker,
private val fragmentManager: FragmentManager?
) { ) {
companion object { companion object {
@@ -24,9 +25,6 @@ class BluetoothPacketBroadcaster(
private const val CLEANUP_DELAY = 500L private const val CLEANUP_DELAY = 500L
} }
/**
* Broadcast packet to connected devices with connection limit enforcement
*/
fun broadcastPacket( fun broadcastPacket(
routed: RoutedPacket, routed: RoutedPacket,
gattServer: BluetoothGattServer?, gattServer: BluetoothGattServer?,
@@ -34,6 +32,37 @@ class BluetoothPacketBroadcaster(
) { ) {
val packet = routed.packet val packet = routed.packet
val data = packet.toBinaryData() ?: return val data = packet.toBinaryData() ?: return
// Check if we need to fragment
if (fragmentManager != null) {
val fragments = fragmentManager.createFragments(packet)
if (fragments.size > 1) {
Log.d(TAG, "Fragmenting packet into ${fragments.size} fragments")
connectionScope.launch {
fragments.forEach { fragment ->
broadcastSinglePacket(RoutedPacket(fragment), gattServer, characteristic)
// 20ms delay between fragments (matching iOS/Rust)
delay(20)
}
}
return
}
}
// Send single packet if no fragmentation needed
broadcastSinglePacket(routed, gattServer, characteristic)
}
/**
* Broadcast single packet to connected devices with connection limit enforcement
*/
fun broadcastSinglePacket(
routed: RoutedPacket,
gattServer: BluetoothGattServer?,
characteristic: BluetoothGattCharacteristic?
) {
val packet = routed.packet
val data = packet.toBinaryData() ?: return
if (packet.recipientID != SpecialRecipients.BROADCAST) { if (packet.recipientID != SpecialRecipients.BROADCAST) {
val recipientID = packet.recipientID?.let { val recipientID = packet.recipientID?.let {
@@ -14,7 +14,7 @@ class FragmentManager {
companion object { companion object {
private const val TAG = "FragmentManager" private const val TAG = "FragmentManager"
private const val MAX_FRAGMENT_SIZE = 500 private const val MAX_FRAGMENT_SIZE = 150 // Match iOS/Rust for BLE compatibility (185 byte MTU limit)
private const val FRAGMENT_TIMEOUT = 30000L // 30 seconds private const val FRAGMENT_TIMEOUT = 30000L // 30 seconds
private const val CLEANUP_INTERVAL = 10000L // 10 seconds private const val CLEANUP_INTERVAL = 10000L // 10 seconds
} }
@@ -46,16 +46,15 @@ class FragmentManager {
val fragments = mutableListOf<BitchatPacket>() val fragments = mutableListOf<BitchatPacket>()
val fragmentID = generateFragmentID() val fragmentID = generateFragmentID()
// Calculate header size (13 bytes for fragment metadata) // Fragment overhead: 13 bytes (fragment metadata) + 21 bytes (packet header) = 34 bytes total
val headerSize = 13 // With 150 byte fragments, total packet = ~184 bytes (within iOS 185 byte MTU)
val dataPerFragment = MAX_FRAGMENT_SIZE - headerSize val totalFragments = (data.size + MAX_FRAGMENT_SIZE - 1) / MAX_FRAGMENT_SIZE
val totalFragments = (data.size + dataPerFragment - 1) / dataPerFragment
Log.d(TAG, "Creating ${totalFragments} fragments for ${data.size} byte packet") Log.d(TAG, "Creating ${totalFragments} fragments for ${data.size} byte packet")
for (i in 0 until totalFragments) { for (i in 0 until totalFragments) {
val start = i * dataPerFragment val start = i * MAX_FRAGMENT_SIZE
val end = minOf(start + dataPerFragment, data.size) val end = minOf(start + MAX_FRAGMENT_SIZE, data.size)
val fragmentData = data.sliceArray(start until end) val fragmentData = data.sliceArray(start until end)
val fragmentPayload = createFragmentPayload( val fragmentPayload = createFragmentPayload(
@@ -158,7 +157,7 @@ class FragmentManager {
* Create fragment payload with metadata * Create fragment payload with metadata
*/ */
private fun createFragmentPayload( private fun createFragmentPayload(
fragmentID: String, fragmentID: ByteArray,
index: Int, index: Int,
total: Int, total: Int,
originalType: UByte, originalType: UByte,
@@ -167,8 +166,7 @@ class FragmentManager {
val payload = ByteArray(13 + data.size) val payload = ByteArray(13 + data.size)
// Fragment ID (8 bytes) // Fragment ID (8 bytes)
val idBytes = fragmentID.toByteArray() System.arraycopy(fragmentID, 0, payload, 0, 8)
System.arraycopy(idBytes, 0, payload, 0, minOf(8, idBytes.size))
// Index (2 bytes, big-endian) // Index (2 bytes, big-endian)
payload[8] = ((index shr 8) and 0xFF).toByte() payload[8] = ((index shr 8) and 0xFF).toByte()
@@ -188,10 +186,12 @@ class FragmentManager {
} }
/** /**
* Generate unique fragment ID * Generate unique fragment ID (8 random bytes to match iOS/Rust)
*/ */
private fun generateFragmentID(): String { private fun generateFragmentID(): ByteArray {
return "${System.currentTimeMillis()}-${kotlin.random.Random.nextInt()}" val fragmentID = ByteArray(8)
kotlin.random.Random.nextBytes(fragmentID)
return fragmentID
} }
/** /**