How Bitchat Android Handles Fragmentation of Large Messages: A Deep Dive into the Bluetooth Mesh Protocol
Bitchat Android splits oversized packets into smaller fragment packets using a three-component system—FragmentManager, FragmentPayload, and FragmentingPacketSender—that ensures compatibility with the iOS implementation of Bitchat's Bluetooth mesh protocol.
Large message fragmentation is essential for any mesh networking application constrained by MTU limits. In Bitchat Android, this capability is implemented through a unified pipeline shared across Wi-Fi Aware and Bluetooth transports. According to the permissionlesstech/bitchat-android source code, the system transparently handles packets exceeding 512 bytes while maintaining cross-platform parity with the iOS reference implementation.
Core Components of the Fragmentation System
The fragmentation architecture centers on three tightly integrated classes:
| Component | Role | Source File |
|---|---|---|
| FragmentManager | Generates fragment plans, stores incoming fragments, reassembles complete messages | FragmentManager.kt |
| FragmentPayload | Defines the 13-byte fragment header and handles payload encoding/decoding | FragmentPayload.kt |
| FragmentingPacketSender | Wraps transport sends, streams fragments with progress tracking | FragmentingPacketSender.kt |
These components work together to ensure fragmentation of large messages happens automatically whenever payload size exceeds protocol limits.
When Fragmentation Is Triggered
The decision to fragment occurs in FragmentManager.createFragments, located in [FragmentManager.kt](https://github.com/permissionlesstech/bitchat-android/blob/main/app/src/main/java/com/bitchat/android/mesh/FragmentManager.kt) lines 84-86:
if (fullData.size <= FRAGMENT_SIZE_THRESHOLD) {
return listOf(packet) // No fragmentation needed
}
The FRAGMENT_SIZE_THRESHOLD constant is defined in AppConstants.Fragmentation at approximately 512 bytes. This threshold accommodates the maximum transmission unit (MTU) for Bluetooth Low Energy while reserving headroom for protocol headers and padding.
How the Fragment Plan Gets Built
When fragmentation is required, FragmentManager constructs a deterministic splitting strategy through four sequential steps:
-
Fragment ID generation —
FragmentPayload.generateFragmentID()creates a random 8-byte identifier (lines 74-78 inFragmentPayload.kt) -
Dynamic fragment size calculation — The system computes available payload space after accounting for headers, route information, and fixed padding overhead (lines 106-108 in
FragmentManager.kt) -
Payload chunking — The
stridehelper function (lines 24-31) slices the original payload into equal-sized chunks, each wrapped in aFragmentPayloadwith proper sequencing metadata (lines 33-41) -
Fragment packet assembly — Each
FragmentPayloadis encoded and placed into a newBitchatPacketwith version-appropriate formatting (v1 or v2) and typeMessageType.FRAGMENT(lines 44-54)
The resulting list of BitchatPacket objects represents the complete fragment sequence ready for transmission.
Sending Fragments with Progress Tracking
The [FragmentingPacketSender.kt](https://github.com/permissionlesstech/bitchat-android/blob/main/app/src/main/java/com/bitchat/android/mesh/FragmentingPacketSender.kt) class handles the actual transmission orchestration. Its send method performs the following operations:
// Obtains fragment list from FragmentManager (lines 21-53)
val fragments = fragmentManager.createFragments(routed.packet, maxFragments)
// Starts progress tracking for multi-fragment transfers (line 67)
if (fragments.size > 1) {
progressManager.start(description, fragments.size)
}
// Serial transmission with configurable inter-fragment delay (lines 70-99)
fragments.forEachIndexed { index, fragment ->
delay(interFragmentDelayMs) // Default: 20ms
val success = sendSingle(routed.copy(packet = fragment))
if (!success) {
progressManager.fail(description)
return@send false
}
progressManager.progress(description, index + 1)
}
Key transmission behaviors include:
- Serial sending — Fragments transmit sequentially, not in parallel, to prevent receiver buffer overflow
- Configurable pacing —
interFragmentDelayMsdefaults to 20 milliseconds between fragments - Atomic failure — Any single fragment failure aborts the entire transfer
- Progress callbacks —
TransferProgressManagerupdates UI components with real-time completion status
Reassembly on the Receiving Side
Incoming fragment handling occurs in FragmentManager.handleFragment (lines 70-88). The reassembly process follows a strict state machine:
Validation and Storage
// Decode and validate fragment header (lines 71-86)
val fragmentPayload = FragmentPayload.decode(packet.payload) ?: return null
if (!fragmentPayload.isValid) return null
// Enforce resource limits (lines 91-48)
if (incomingFragments[fragmentId]?.size ?: 0 >= MAX_FRAGMENTS_PER_ID) {
return null // Reject excessive fragments for this ID
}
Completion Detection and Reconstruction
When the stored fragment count equals fragmentPayload.total, reassembly triggers:
// Concatenate fragments in correct order (lines 62-68)
val orderedData = (0 until total).flatMap { index ->
fragmentsMap[index] ?: return null
}.toByteArray()
// Decode original packet and clear state (lines 71-76)
val originalPacket = BitchatPacket.fromBinaryData(orderedData)
incomingFragments.remove(fragmentId)
fragmentMetadata.remove(fragmentId)
// Return with TTL=0 for fresh processing
return PacketWithTtl(originalPacket, ttl = 0)
Automated Cleanup
A background coroutine (startPeriodicCleanup) runs every 10 seconds to purge stale fragments older than 30 seconds, preventing memory exhaustion from incomplete transfers.
Integration with Transport Layers
The fragmentation of large messages operates transparently across both supported transports:
Wi-Fi Aware
WifiAwareMeshService instantiates FragmentingPacketSender at line 91, injecting the shared FragmentManager from MeshCore:
val fragmentingSender = FragmentingPacketSender(
scope = coroutineScope,
fragmentManager = meshCore.fragmentManager,
logTag = "WifiAwareMesh"
)
Bluetooth
BluetoothPacketBroadcaster follows the identical pattern at line 127, ensuring BLE broadcasts benefit from identical fragmentation semantics.
This unified approach guarantees that iOS ↔ Android message compatibility is preserved regardless of underlying transport technology.
Direct API Usage Examples
Developers can interact with the fragmentation system programmatically:
Manual Fragment Generation
val fragmentManager = FragmentManager()
val fragments: List<BitchatPacket> = fragmentManager.createFragments(
packet = oversizedPacket,
maxFragments = 0xFFFF // Allow full UInt16 range
)
// fragments ready for custom transport injection
Custom Sender Configuration
val sender = FragmentingPacketSender(
scope = CoroutineScope(Dispatchers.IO),
fragmentManager = meshCore.fragmentManager,
logTag = "CustomModule"
)
sender.send(
routed = myRoutedPacket,
description = "Large file broadcast",
sendSingle = { routedPacket ->
myCustomTransport.send(routedPacket.packet.bytes)
}
)
Summary
- Threshold-based triggering — Fragmentation activates automatically when un-padded payload exceeds ~512 bytes
- Three-layer architecture —
FragmentManagerplans and reassembles,FragmentPayloadstructures headers,FragmentingPacketSenderorchestrates transmission - Cross-platform compatibility — Fragment format matches iOS Bitchat implementation exactly
- Robust resource management — Per-ID limits, global byte caps, and periodic cleanup prevent DoS via fragment flooding
- Transport-agnostic design — Wi-Fi Aware and Bluetooth layers share identical fragmentation pipelines through dependency injection
Frequently Asked Questions
What is the maximum message size Bitchat Android can fragment?
The system supports up to 65,535 fragments (0xFFFF) per message ID. With approximately 500 bytes of payload per fragment after headers, this yields a theoretical maximum around 32 MB. However, practical limits are lower due to memory constraints and the 30-second reassembly timeout.
How does Bitchat ensure fragments arrive in order?
Reassembly uses the index field in the 13-byte FragmentPayload header to place chunks correctly regardless of arrival order. The FragmentManager stores fragments in a ConcurrentHashMap indexed by (fragmentId, index) tuples.
What happens if a fragment is lost in transmission?
The FragmentingPacketSender detects send failures and aborts the entire transfer, marking it failed through TransferProgressManager. The receiving side's 30-second timeout eventually cleans up partial fragment sets. There is no automatic retransmission—higher layers must detect failure and retry the complete message.
Is the fragmentation protocol compatible with other mesh networks?
No. The 13-byte FragmentPayload header structure—including 8-byte random ID, 2-byte index, 2-byte total count, and 1-byte original type—is specific to Bitchat's protocol. While designed for iOS ↔ Android compatibility, it does not interoperate with standard Bluetooth mesh or Thread fragmentation schemes.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →