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 — message type. File transfer uses MessageType.FILE_TRANSFER (0x22).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 — TLV file payload (see below).signature: ByteArray? — optional signature (present for private sends in our implementation, to match iOS integrity 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 are additionally encrypted at the higher layer (Noise) for text messages, but file transfers use the FILE_TRANSFER message type in the clear at the envelope level with content carried inside a TLV. See code for any deployment‑specific enforcement.
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.PayloadLength fields.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
BitchatMessage with content "[voice] <path>" and add to the appropriate timeline (public/channel/private).messageManager.addPrivateMessage(peerID, message). For public/channel: messageManager.addMessage(message) or add to channel.Packet creation
BitchatFilePacket:
fileName: basename (e.g., voice_… .m4a)fileSize: file lengthmimeType: audio/mp4content: full bytes (ensure content ≤ 64 KiB; with chosen codec params typical short notes fit fragmentation constraints)transferId = sha256Hex(payload).transferId → messageId for UI progress.Send
BluetoothMeshService.sendFileBroadcast(filePacket).BluetoothMeshService.sendFilePrivate(peerID, filePacket).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.recipientID differs. Private may have signatures; code shows a signing step consistent with iOS behavior prior to broadcast to ensure integrity.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) + valueBitchatPacket envelope with type = FILE_TRANSFER (0x22) and the correct recipientID (broadcast vs private).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.