docs/file_transfer.md
This document is the exhaustive implementation guide for Bitchat’s Bluetooth file transfer protocol for voice notes (audio) and images, including interactive features like waveform seeking. It describes the on‑wire packet format (both v1 and v2), fragmentation/progress/cancellation, sender/receiver behaviors, and the complete UX we implemented in the Android client so that other implementers can interoperate and match the user experience precisely.
Protocol Versions:
Interactive Features:
The guide is organized into:
Bitchat BLE transport carries application messages inside the common BitchatPacket envelope. File transfer reuses the same envelope as public and private messages, with a distinct type and a TLV‑encoded payload.
Fields (subset relevant to file transfer):
version: UByte — protocol version (1 for v1, 2 for v2 with extended payload length).type: UByte — public file transfer uses MessageType.FILE_TRANSFER (0x22);
private file transfer uses outer MessageType.NOISE_ENCRYPTED (0x11).senderID: ByteArray (8) — 8‑byte binary peer ID.recipientID: ByteArray (8) — 8‑byte recipient. For public: SpecialRecipients.BROADCAST (0xFF…FF); for private: the target peer’s 8‑byte ID.timestamp: ULong — milliseconds since epoch.payload: ByteArray — the public form contains the file TLV directly. The
private form contains Noise ciphertext which authenticates payload type
FILE_TRANSFER (0x20) followed by the same file TLV.signature: ByteArray? — packet signature added by the mesh send path.ttl: UByte — hop TTL (we use MAX_TTL for broadcast, 7 for private).Envelope creation and broadcast paths are implemented in:
app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt)app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt)app/src/main/java/com/bitchat/android/mesh/PacketProcessor.kt (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/mesh/PacketProcessor.kt)Private sends encrypt the complete file TLV as Noise payload 0x20 before
outer fragmentation. See PRIVATE_MEDIA_V1.md for the capability and mixed-
client interoperability contract.
v1 Format (original):
Header (13 bytes):
Version: 1 byte
Type: 1 byte
TTL: 1 byte
Timestamp: 8 bytes
Flags: 1 byte
PayloadLength: 2 bytes (big-endian, max 64 KiB)
v2 Format (extended):
Header (15 bytes):
Version: 1 byte (set to 2 for v2 packets)
Type: 1 byte
TTL: 1 byte
Timestamp: 8 bytes
Flags: 1 byte
PayloadLength: 4 bytes (big-endian, max ~4 GiB)
BinaryProtocol.kt with getHeaderSize(version) logic.v2 carries file payloads that exceed the v1 envelope-length field and carries source-route metadata. It does not imply multi-gigabyte mesh transfer support.
PayloadLength fields.Android's bounded decoder applies the same 10 MiB expanded-payload ceiling to every outer
message type. It also requires a non-empty compressed body, an exact declared output size, and a
complete deflate stream. The FILE_TRANSFER (0x22) byte cannot safely grant a larger ceiling: it is
attacker-controlled before packet signature verification, and the current receive pipeline must
inflate before it can perform that verification.
New Android senders cap files just below 10 MiB (reserving envelope overhead) and refuse to encode
any payload above the receiver ceiling; exceeding the cap surfaces a user-visible error in chat.
Support for legacy >10 MiB compressed transfers is explicitly ended: legacy Android senders can
still produce a highly compressible public file between 10 MiB and their 50 MiB UI limit that the
bounded decoder rejects. Grandfathering 50 MiB is not safe after only a type check: inflation
allocates the declared payload and
BitchatFilePacket.decode currently copies the content again, creating a greater than 100 MiB peak
for a maximum-size transfer.
Before enabling that legacy range, receive processing needs an authenticated admission decision made before large allocation plus streaming inflation/TLV parsing into a bounded temporary file (or another ownership-preserving design that avoids the second full-size copy). The sender limit and a wire capability/version transition must then be coordinated so old and new clients fail predictably.
The file payload is a TLV structure with mixed length field sizes to support large contents efficiently.
app/src/main/java/com/bitchat/android/model/BitchatFilePacket.kt (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/model/BitchatFilePacket.kt)Canonical TLVs (v2 spec):
0x01 FILE_NAME — UTF‑8 bytes
type(1) + len(2) + value0x02 FILE_SIZE — 4 bytes (UInt32, big‑endian)
type(1) + len(2=4) + value(4)0x03 MIME_TYPE — UTF‑8 bytes (e.g., image/jpeg, audio/mp4, application/pdf)
type(1) + len(2) + value0x04 CONTENT — raw file bytes
type(1) + len(4) + value(len)Encoding rules:
1 byte type + 2 bytes big‑endian length + value.Decoding rules (v2):
len=4 and is parsed as UInt32; receivers may upcast to 64‑bit internally.content.size.application/octet-stream.Legacy Compatibility (optional, for mixed‑version meshes):
len=8 and clamp to 32‑bit if needed.File transfers reuse the mesh broadcaster’s fragmentation logic:
BluetoothPacketBroadcaster checks if the serialized envelope exceeds the configured MTU and splits it into fragments via FragmentManager.We derive a deterministic transfer ID to track progress:
transferId = sha256Hex(packet.payload) (hex string of the file TLV payload).The broadcaster emits progress events to a shared flow:
TransferProgressManager.start(id, totalFragments)TransferProgressManager.progress(id, sent, totalFragments)TransferProgressManager.complete(id, totalFragments)The UI maps transferId → messageId, then updates DeliveryStatus.PartiallyDelivered(sent, total) as events arrive; when complete, switches to Delivered.
Transfers are cancellable mid‑flight:
transferId → Job map and cancels the job to stop sending remaining fragments.BluetoothPacketBroadcaster.cancelTransfer(transferId)BluetoothConnectionManager.cancelTransfer and BluetoothMeshService.cancelFileTransfer.ChatViewModel.cancelMediaSend(messageId) resolves messageId → transferId and cancels.Implementation files:
app/src/main/java/com/bitchat/android/mesh/BluetoothPacketBroadcaster.kt (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/mesh/BluetoothPacketBroadcaster.kt)app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt)app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt)app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt)Receiver dispatch is in MessageHandler:
BitchatFilePacket.decode(payload). If it decodes:
files/voicenotes/incoming/files/images/incoming/files/files/incoming/fileName when present; sanitize path separators." (n)" before the extension when a name exists already.fileName is absent, derive from MIME with a sensible default extension..m4a, .mp3, .wav, .ogg for audio; .jpg, .png, .webp for images; otherwise based on MIME or .bin)."[voice] /abs/path/to/file""[image] /abs/path/to/file""[file] /abs/path/to/file"senderPeerID is set to the origin, isPrivate set appropriately.Files:
app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt)Capture
MediaRecorder with AAC in MP4 (audio/mp4).files/voicenotes/outgoing/voice_YYYYMMDD_HHMMSS.m4a.Local echo
Packet creation
BitchatFilePacket:
fileName: basename (e.g., voice_… .m4a)fileSize: file lengthmimeType: audio/mp4content: full bytes; final-packet admission determines whether it fits
the 256-fragment limit.transferId = sha256Hex(payload).transferId → messageId for UI progress.Send
BluetoothMeshService.sendFileBroadcast(filePacket).prepareFilePrivate(...), followed by one-shot commit of a
ready plan. The non-interactive sendFilePrivate(...) entry point commits
only encrypted-ready plans and never silently chooses a legacy downgrade.Mixed-client private migration
0x22,
visible to relays, and must have a valid Ed25519 packet signature.Waveform
Core files:
app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt (sendVoiceNote) (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt)app/src/main/java/com/bitchat/android/model/BitchatFilePacket.kt (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/model/BitchatFilePacket.kt)app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt)app/src/main/java/com/bitchat/android/features/voice/VoiceRecorder.kt (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/features/voice/VoiceRecorder.kt)app/src/main/java/com/bitchat/android/features/voice/Waveform.kt (cache + extractor) (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/features/voice/Waveform.kt)Selection and processing
GetContent() (image/*). No storage permission required.files/images/outgoing/img_<timestamp>.jpg.ImageUtils.downscaleAndSaveToAppFiles(context, uri, maxDim=512).Local echo
"[image] <path>" in the current context (public/channel/private).Packet creation
BitchatFilePacket with mime image/jpeg and file content.transferId and map to messageId.Send
Core files:
app/src/main/java/com/bitchat/android/features/media/ImageUtils.kt (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/features/media/ImageUtils.kt)app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt (sendImageNote) (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt)app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt)This section specifies exactly what users see and how inputs behave, so alternative clients can match the experience.
Files:
app/src/main/java/com/bitchat/android/ui/InputComponents.kt (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/ui/InputComponents.kt)RealtimeScrollingWaveform (dense bars, ~240 columns, ~20 FPS) in app/src/main/java/com/bitchat/android/ui/media/RealtimeScrollingWaveform.kt.Files:
app/src/main/java/com/bitchat/android/ui/MessageComponents.kt (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt)app/src/main/java/com/bitchat/android/ui/media/WaveformViews.kt (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/ui/media/WaveformViews.kt)app/src/main/java/com/bitchat/android/features/voice/Waveform.kt (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/features/voice/Waveform.kt)Files:
app/src/main/java/com/bitchat/android/ui/media/ImagePickerButton.kt (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/ui/media/ImagePickerButton.kt)app/src/main/java/com/bitchat/android/features/media/ImageUtils.kt (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/features/media/ImageUtils.kt)app/src/main/java/com/bitchat/android/ui/media/BlockRevealImage.kt (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/ui/media/BlockRevealImage.kt)app/src/main/java/com/bitchat/android/ui/MessageComponents.kt (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt)MediaStore.Files:
app/src/main/java/com/bitchat/android/ui/media/FullScreenImageViewer.kt (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/ui/media/FullScreenImageViewer.kt)Waveform Canvas Implementation:
WaveformCanvas uses pointerInput with detectTapGestures to capture tap events.position.x / size.width.toFloat().onSeek callback is invoked with the calculated position fraction.onSeek is provided (disabled for sending in progress).VoiceNotePlayer Seeking:
seekMs = (position * durationMs).toInt().MediaPlayer.seekTo(seekMs) to jump to the exact position.Files:
app/src/main/java/com/bitchat/android/ui/MessageComponents.kt (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt) — VoiceNotePlayer with seekTo functionapp/src/main/java/com/bitchat/android/ui/media/WaveformViews.kt (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/ui/media/WaveformViews.kt) — Interactive WaveformCanvas with tap handling (n) suffix before the extension to prevent overwrites."[voice] <abs path>", "[image] <abs path>", "[file] <abs path>" for local rendering. These are not sent on the wire; the actual file bytes are inside the TLV payload.(sent / total) from TransferProgressManager (fragment‑level granularity). The block grid density can be tuned; currently 24×16.0x20; public media carries it directly in a 0x22
packet.Core protocol and transport:
app/src/main/java/com/bitchat/android/model/BitchatFilePacket.kt — TLV payload model + encode/decode. (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/model/BitchatFilePacket.kt)app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt — packet creation and broadcast for file messages. (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt)app/src/main/java/com/bitchat/android/mesh/BluetoothPacketBroadcaster.kt — fragmentation, progress, cancellation via transfer jobs. (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/mesh/BluetoothPacketBroadcaster.kt)app/src/main/java/com/bitchat/android/mesh/TransferProgressManager.kt — progress events bus. (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/mesh/TransferProgressManager.kt)app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt — receive path: decode, persist to files, create chat messages. (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt)Audio capture and waveform:
app/src/main/java/com/bitchat/android/features/voice/VoiceRecorder.kt — MediaRecorder wrapper. (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/features/voice/VoiceRecorder.kt)app/src/main/java/com/bitchat/android/features/voice/Waveform.kt — cache + extractor + resampler. (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/features/voice/Waveform.kt)app/src/main/java/com/bitchat/android/ui/media/WaveformViews.kt — Compose waveform preview components. (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/ui/media/WaveformViews.kt)Image pipeline:
app/src/main/java/com/bitchat/android/features/media/ImageUtils.kt — downscale and save to app files. (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/features/media/ImageUtils.kt)app/src/main/java/com/bitchat/android/ui/media/ImagePickerButton.kt — SAF picker button. (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/ui/media/ImagePickerButton.kt)app/src/main/java/com/bitchat/android/ui/media/BlockRevealImage.kt — block‑reveal progress renderer (no gaps, dense grid). (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/ui/media/BlockRevealImage.kt)Recording overlay:
app/src/main/java/com/bitchat/android/ui/media/RealtimeScrollingWaveform.kt — dense, real‑time scrolling waveform during recording. (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/ui/media/RealtimeScrollingWaveform.kt)UI composition and view model coordination:
app/src/main/java/com/bitchat/android/ui/InputComponents.kt — input field, overlays (recording), picker button, mic. (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/ui/InputComponents.kt)app/src/main/java/com/bitchat/android/ui/MessageComponents.kt — message rendering for text/audio/images including progress UIs and cancel overlays. (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt)app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt — sendVoiceNote/sendImageNote, progress mapping, cancelMediaSend. (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt)app/src/main/java/com/bitchat/android/ui/MessageManager.kt — add/remove/update messages across main, private, and channels. (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/ui/MessageManager.kt)Fullscreen image:
app/src/main/java/com/bitchat/android/ui/media/FullScreenImageViewer.kt — fullscreen viewer + save to Downloads. (/Users/cc/git/bitchat-android/app/src/main/java/com/bitchat/android/ui/media/FullScreenImageViewer.kt)BitchatFilePacket TLV exactly as specified:
type(1) + len(2) + valuetype(1) + len(2=4) + value(4, UInt32 BE)type(1) + len(4) + valueFILE_TRANSFER (0x22). For private media,
prefix the TLV with Noise payload type 0x20, encrypt it for the recipient,
and put the ciphertext in NOISE_ENCRYPTED (0x11).sha256(payload) so the UI can map progress to a message."[voice] path", "[image] path", or "[file] path" for local rendering.Following the above should produce an interoperable and matching experience across platforms.