mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 02:45:20 +00:00
Changes bitchat protocol (#265)
* create payload * compiles and can send messages * identityannouncement * DMs work, read receipt not sent yet * works * delete old code * simplify * working * fragment wip * compression wip * use zlib compression * clean * nice * mesh * remove comments
This commit is contained in:
@@ -3,25 +3,33 @@ package com.bitchat.android.mesh
|
||||
import android.util.Log
|
||||
import com.bitchat.android.protocol.BitchatPacket
|
||||
import com.bitchat.android.protocol.MessageType
|
||||
import com.bitchat.android.model.FragmentPayload
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* Manages message fragmentation and reassembly
|
||||
* Extracted from BluetoothMeshService for better separation of concerns
|
||||
* Manages message fragmentation and reassembly - 100% iOS Compatible
|
||||
*
|
||||
* This implementation exactly matches iOS SimplifiedBluetoothService fragmentation:
|
||||
* - Same fragment payload structure (13-byte header + data)
|
||||
* - Same MTU thresholds and fragment sizes
|
||||
* - Same reassembly logic and timeout handling
|
||||
* - Uses new FragmentPayload model for type safety
|
||||
*/
|
||||
class FragmentManager {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "FragmentManager"
|
||||
private const val FRAGMENT_SIZE_THRESHOLD = 512 // 512 bytes
|
||||
private const val MAX_FRAGMENT_SIZE = 500 // Match iOS/Rust for BLE compatibility (185 byte MTU limit)
|
||||
private const val FRAGMENT_TIMEOUT = 30000L // 30 seconds
|
||||
private const val CLEANUP_INTERVAL = 10000L // 10 seconds
|
||||
// iOS values: 512 MTU threshold, 469 max fragment size (512 MTU - headers)
|
||||
private const val FRAGMENT_SIZE_THRESHOLD = 512 // Matches iOS: if data.count > 512
|
||||
private const val MAX_FRAGMENT_SIZE = 469 // Matches iOS: maxFragmentSize = 469
|
||||
private const val FRAGMENT_TIMEOUT = 30000L // Matches iOS: 30 seconds cleanup
|
||||
private const val CLEANUP_INTERVAL = 10000L // 10 seconds cleanup check
|
||||
}
|
||||
|
||||
// Fragment storage
|
||||
// Fragment storage - iOS equivalent: incomingFragments: [String: [Int: Data]]
|
||||
private val incomingFragments = ConcurrentHashMap<String, MutableMap<Int, ByteArray>>()
|
||||
// iOS equivalent: fragmentMetadata: [String: (type: UInt8, total: Int, timestamp: Date)]
|
||||
private val fragmentMetadata = ConcurrentHashMap<String, Triple<UByte, Int, Long>>() // originalType, totalFragments, timestamp
|
||||
|
||||
// Delegate for callbacks
|
||||
@@ -35,51 +43,52 @@ class FragmentManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create fragments from a large packet
|
||||
* Create fragments from a large packet - 100% iOS Compatible
|
||||
* Matches iOS sendFragmentedPacket() implementation exactly
|
||||
*/
|
||||
fun createFragments(packet: BitchatPacket): List<BitchatPacket> {
|
||||
val data = packet.toBinaryData() ?: return emptyList()
|
||||
val fullData = packet.toBinaryData() ?: return emptyList()
|
||||
|
||||
if (data.size <= FRAGMENT_SIZE_THRESHOLD) {
|
||||
// iOS logic: if data.count > 512 && packet.type != MessageType.fragment.rawValue
|
||||
if (fullData.size <= FRAGMENT_SIZE_THRESHOLD) {
|
||||
return listOf(packet) // No fragmentation needed
|
||||
}
|
||||
|
||||
val fragments = mutableListOf<BitchatPacket>()
|
||||
val fragmentID = generateFragmentID()
|
||||
|
||||
// Fragment overhead: 13 bytes (fragment metadata) + 21 bytes (packet header) = 34 bytes total
|
||||
// With 150 byte fragments, total packet = ~184 bytes (within iOS 185 byte MTU)
|
||||
val totalFragments = (data.size + MAX_FRAGMENT_SIZE - 1) / MAX_FRAGMENT_SIZE
|
||||
// iOS: let fragmentID = Data((0..<8).map { _ in UInt8.random(in: 0...255) })
|
||||
val fragmentID = FragmentPayload.generateFragmentID()
|
||||
|
||||
Log.d(TAG, "Creating ${totalFragments} fragments for ${data.size} byte packet")
|
||||
// iOS: stride(from: 0, to: fullData.count, by: maxFragmentSize)
|
||||
val fragmentChunks = stride(0, fullData.size, MAX_FRAGMENT_SIZE) { offset ->
|
||||
val endOffset = minOf(offset + MAX_FRAGMENT_SIZE, fullData.size)
|
||||
fullData.sliceArray(offset..<endOffset)
|
||||
}
|
||||
|
||||
for (i in 0 until totalFragments) {
|
||||
val start = i * MAX_FRAGMENT_SIZE
|
||||
val end = minOf(start + MAX_FRAGMENT_SIZE, data.size)
|
||||
val fragmentData = data.sliceArray(start until end)
|
||||
Log.d(TAG, "Creating ${fragmentChunks.size} fragments for ${fullData.size} byte packet (iOS compatible)")
|
||||
|
||||
// iOS: for (index, fragment) in fragments.enumerated()
|
||||
for (index in fragmentChunks.indices) {
|
||||
val fragmentData = fragmentChunks[index]
|
||||
|
||||
val fragmentPayload = createFragmentPayload(
|
||||
// Create iOS-compatible fragment payload
|
||||
val fragmentPayload = FragmentPayload(
|
||||
fragmentID = fragmentID,
|
||||
index = i,
|
||||
total = totalFragments,
|
||||
index = index,
|
||||
total = fragmentChunks.size,
|
||||
originalType = packet.type,
|
||||
data = fragmentData
|
||||
)
|
||||
|
||||
val fragmentType = when (i) {
|
||||
0 -> MessageType.FRAGMENT_START
|
||||
totalFragments - 1 -> MessageType.FRAGMENT_END
|
||||
else -> MessageType.FRAGMENT_CONTINUE
|
||||
}
|
||||
|
||||
// iOS: MessageType.fragment.rawValue (single fragment type)
|
||||
val fragmentPacket = BitchatPacket(
|
||||
type = fragmentType.value,
|
||||
type = MessageType.FRAGMENT.value,
|
||||
ttl = packet.ttl,
|
||||
senderID = packet.senderID,
|
||||
recipientID = packet.recipientID,
|
||||
timestamp = packet.timestamp,
|
||||
payload = fragmentPayload,
|
||||
signature = null // Fragments aren't individually signed
|
||||
payload = fragmentPayload.encode(),
|
||||
signature = null // iOS: signature: nil
|
||||
)
|
||||
|
||||
fragments.add(fragmentPacket)
|
||||
@@ -89,62 +98,82 @@ class FragmentManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming fragment
|
||||
* Handle incoming fragment - 100% iOS Compatible
|
||||
* Matches iOS handleFragment() implementation exactly
|
||||
*/
|
||||
fun handleFragment(packet: BitchatPacket): BitchatPacket? {
|
||||
if (packet.payload.size < 13) {
|
||||
// iOS: guard packet.payload.count > 13 else { return }
|
||||
if (packet.payload.size < FragmentPayload.HEADER_SIZE) {
|
||||
Log.w(TAG, "Fragment packet too small: ${packet.payload.size}")
|
||||
return null
|
||||
}
|
||||
|
||||
// Don't process our own fragments - iOS equivalent check
|
||||
// This would be done at a higher level but we'll include for safety
|
||||
|
||||
try {
|
||||
// Extract fragment metadata (same format as iOS)
|
||||
val fragmentIDData = packet.payload.sliceArray(0..7)
|
||||
val fragmentID = fragmentIDData.contentHashCode().toString()
|
||||
|
||||
val index = ((packet.payload[8].toInt() and 0xFF) shl 8) or (packet.payload[9].toInt() and 0xFF)
|
||||
val total = ((packet.payload[10].toInt() and 0xFF) shl 8) or (packet.payload[11].toInt() and 0xFF)
|
||||
val originalType = packet.payload[12].toUByte()
|
||||
val fragmentData = packet.payload.sliceArray(13 until packet.payload.size)
|
||||
|
||||
Log.d(TAG, "Received fragment $index/$total for fragmentID: $fragmentID, originalType: $originalType")
|
||||
|
||||
// Store fragment
|
||||
if (!incomingFragments.containsKey(fragmentID)) {
|
||||
incomingFragments[fragmentID] = mutableMapOf()
|
||||
fragmentMetadata[fragmentID] = Triple(originalType, total, System.currentTimeMillis())
|
||||
// Use FragmentPayload for type-safe decoding
|
||||
val fragmentPayload = FragmentPayload.decode(packet.payload)
|
||||
if (fragmentPayload == null || !fragmentPayload.isValid()) {
|
||||
Log.w(TAG, "Invalid fragment payload")
|
||||
return null
|
||||
}
|
||||
|
||||
incomingFragments[fragmentID]?.put(index, fragmentData)
|
||||
// iOS: let fragmentID = packet.payload[0..<8].map { String(format: "%02x", $0) }.joined()
|
||||
val fragmentIDString = fragmentPayload.getFragmentIDString()
|
||||
|
||||
// Check if we have all fragments
|
||||
if (incomingFragments[fragmentID]?.size == total) {
|
||||
Log.d(TAG, "All fragments received for $fragmentID, reassembling...")
|
||||
Log.d(TAG, "Received fragment ${fragmentPayload.index}/${fragmentPayload.total} for fragmentID: $fragmentIDString, originalType: ${fragmentPayload.originalType}")
|
||||
|
||||
// iOS: if incomingFragments[fragmentID] == nil
|
||||
if (!incomingFragments.containsKey(fragmentIDString)) {
|
||||
incomingFragments[fragmentIDString] = mutableMapOf()
|
||||
fragmentMetadata[fragmentIDString] = Triple(
|
||||
fragmentPayload.originalType,
|
||||
fragmentPayload.total,
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
}
|
||||
|
||||
// iOS: incomingFragments[fragmentID]?[index] = Data(fragmentData)
|
||||
incomingFragments[fragmentIDString]?.put(fragmentPayload.index, fragmentPayload.data)
|
||||
|
||||
// iOS: if let fragments = incomingFragments[fragmentID], fragments.count == total
|
||||
val fragmentMap = incomingFragments[fragmentIDString]
|
||||
if (fragmentMap != null && fragmentMap.size == fragmentPayload.total) {
|
||||
Log.d(TAG, "All fragments received for $fragmentIDString, reassembling...")
|
||||
|
||||
// Reassemble message
|
||||
// iOS reassembly logic: for i in 0..<total { if let fragment = fragments[i] { reassembled.append(fragment) } }
|
||||
val reassembledData = mutableListOf<Byte>()
|
||||
for (i in 0 until total) {
|
||||
incomingFragments[fragmentID]?.get(i)?.let { data ->
|
||||
for (i in 0 until fragmentPayload.total) {
|
||||
fragmentMap[i]?.let { data ->
|
||||
reassembledData.addAll(data.asIterable())
|
||||
}
|
||||
}
|
||||
|
||||
// Parse and return reassembled packet
|
||||
val reassembledPacket = BitchatPacket.fromBinaryData(reassembledData.toByteArray())
|
||||
|
||||
// Cleanup
|
||||
incomingFragments.remove(fragmentID)
|
||||
fragmentMetadata.remove(fragmentID)
|
||||
|
||||
if (reassembledPacket != null) {
|
||||
// iOS: if let metadata = fragmentMetadata[fragmentID]
|
||||
val metadata = fragmentMetadata[fragmentIDString]
|
||||
if (metadata != null) {
|
||||
// iOS: let reassembledPacket = BitchatPacket(type: metadata.type, ...)
|
||||
val reassembledPacket = BitchatPacket(
|
||||
type = metadata.first,
|
||||
senderID = packet.senderID,
|
||||
recipientID = packet.recipientID,
|
||||
timestamp = packet.timestamp,
|
||||
payload = reassembledData.toByteArray(),
|
||||
signature = packet.signature,
|
||||
ttl = if (packet.ttl > 0u) (packet.ttl - 1u).toUByte() else 0u
|
||||
)
|
||||
|
||||
// iOS cleanup: incomingFragments.removeValue(forKey: fragmentID)
|
||||
incomingFragments.remove(fragmentIDString)
|
||||
fragmentMetadata.remove(fragmentIDString)
|
||||
|
||||
Log.d(TAG, "Successfully reassembled packet of ${reassembledData.size} bytes")
|
||||
return reassembledPacket
|
||||
} else {
|
||||
Log.e(TAG, "Failed to parse reassembled packet")
|
||||
}
|
||||
} else {
|
||||
val received = incomingFragments[fragmentID]?.size ?: 0
|
||||
Log.d(TAG, "Fragment $index stored, have $received/$total fragments for $fragmentID")
|
||||
val received = fragmentMap?.size ?: 0
|
||||
Log.d(TAG, "Fragment ${fragmentPayload.index} stored, have $received/${fragmentPayload.total} fragments for $fragmentIDString")
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
@@ -155,53 +184,51 @@ class FragmentManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create fragment payload with metadata
|
||||
* Helper function to match iOS stride functionality
|
||||
* stride(from: 0, to: fullData.count, by: maxFragmentSize)
|
||||
*/
|
||||
private fun createFragmentPayload(
|
||||
fragmentID: ByteArray,
|
||||
index: Int,
|
||||
total: Int,
|
||||
originalType: UByte,
|
||||
data: ByteArray
|
||||
): ByteArray {
|
||||
val payload = ByteArray(13 + data.size)
|
||||
|
||||
// Fragment ID (8 bytes)
|
||||
System.arraycopy(fragmentID, 0, payload, 0, 8)
|
||||
|
||||
// Index (2 bytes, big-endian)
|
||||
payload[8] = ((index shr 8) and 0xFF).toByte()
|
||||
payload[9] = (index and 0xFF).toByte()
|
||||
|
||||
// Total (2 bytes, big-endian)
|
||||
payload[10] = ((total shr 8) and 0xFF).toByte()
|
||||
payload[11] = (total and 0xFF).toByte()
|
||||
|
||||
// Original type (1 byte)
|
||||
payload[12] = originalType.toByte()
|
||||
|
||||
// Fragment data
|
||||
System.arraycopy(data, 0, payload, 13, data.size)
|
||||
|
||||
return payload
|
||||
private fun <T> stride(from: Int, to: Int, by: Int, transform: (Int) -> T): List<T> {
|
||||
val result = mutableListOf<T>()
|
||||
var current = from
|
||||
while (current < to) {
|
||||
result.add(transform(current))
|
||||
current += by
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate unique fragment ID (8 random bytes to match iOS/Rust)
|
||||
* iOS cleanup - exactly matching performCleanup() implementation
|
||||
* Clean old fragments (> 30 seconds old)
|
||||
*/
|
||||
private fun generateFragmentID(): ByteArray {
|
||||
val fragmentID = ByteArray(8)
|
||||
kotlin.random.Random.nextBytes(fragmentID)
|
||||
return fragmentID
|
||||
private fun cleanupOldFragments() {
|
||||
val now = System.currentTimeMillis()
|
||||
val cutoff = now - FRAGMENT_TIMEOUT
|
||||
|
||||
// iOS: let oldFragments = fragmentMetadata.filter { $0.value.timestamp < cutoff }.map { $0.key }
|
||||
val oldFragments = fragmentMetadata.filter { it.value.third < cutoff }.map { it.key }
|
||||
|
||||
// iOS: for fragmentID in oldFragments { incomingFragments.removeValue(forKey: fragmentID) }
|
||||
for (fragmentID in oldFragments) {
|
||||
incomingFragments.remove(fragmentID)
|
||||
fragmentMetadata.remove(fragmentID)
|
||||
}
|
||||
|
||||
if (oldFragments.isNotEmpty()) {
|
||||
Log.d(TAG, "Cleaned up ${oldFragments.size} old fragment sets (iOS compatible)")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get debug information
|
||||
* Get debug information - matches iOS debugging
|
||||
*/
|
||||
fun getDebugInfo(): String {
|
||||
return buildString {
|
||||
appendLine("=== Fragment Manager Debug Info ===")
|
||||
appendLine("=== Fragment Manager Debug Info (iOS Compatible) ===")
|
||||
appendLine("Active Fragment Sets: ${incomingFragments.size}")
|
||||
appendLine("Fragment Size Threshold: $FRAGMENT_SIZE_THRESHOLD bytes")
|
||||
appendLine("Max Fragment Size: $MAX_FRAGMENT_SIZE bytes")
|
||||
|
||||
fragmentMetadata.forEach { (fragmentID, metadata) ->
|
||||
val (originalType, totalFragments, timestamp) = metadata
|
||||
val received = incomingFragments[fragmentID]?.size ?: 0
|
||||
@@ -212,7 +239,7 @@ class FragmentManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Start periodic cleanup of old fragments
|
||||
* Start periodic cleanup of old fragments - matches iOS maintenance timer
|
||||
*/
|
||||
private fun startPeriodicCleanup() {
|
||||
managerScope.launch {
|
||||
@@ -223,29 +250,6 @@ class FragmentManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up old fragments (older than 30 seconds)
|
||||
*/
|
||||
private fun cleanupOldFragments() {
|
||||
val cutoffTime = System.currentTimeMillis() - FRAGMENT_TIMEOUT
|
||||
val fragmentsToRemove = mutableListOf<String>()
|
||||
|
||||
fragmentMetadata.entries.forEach { (fragmentID, metadata) ->
|
||||
if (metadata.third < cutoffTime) {
|
||||
fragmentsToRemove.add(fragmentID)
|
||||
}
|
||||
}
|
||||
|
||||
fragmentsToRemove.forEach { fragmentID ->
|
||||
incomingFragments.remove(fragmentID)
|
||||
fragmentMetadata.remove(fragmentID)
|
||||
}
|
||||
|
||||
if (fragmentsToRemove.isNotEmpty()) {
|
||||
Log.d(TAG, "Cleaned up ${fragmentsToRemove.size} old fragment sets")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all fragments
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user