tailscale / tailscale/tailscale
Taildrop (Android) truncates files when the ContentResolver-reported size is smaller than the real stream (Samsung Voice Recorder .m4a)
- Dominant language
- Go
- Stars
- 36.5k
- Forks
- 3.2k
- Avg merge
- 1d 23h
- Merged PRs (30d)
- 132
Description
### Summary
Sending a Samsung Voice Recorder `.m4a` via Taildrop from Android delivers a file that is short by a fixed number of bytes, cutting the trailing `moov` atom so the file won't open in any standard player. The audio data (`mdat`) always arrives complete — only the tail (the index) is lost, which is why the sender also reports an error near the end of the transfer.
### Environment
- **Sender:** Tailscale Android **1.98.2**, Samsung Galaxy S24, One UI 8.5, Samsung Voice Recorder 2.1.5.86.30
- **Receiver:** `tailscaled` **1.98.9** on macOS (Homebrew), drained with `tailscale file get`
### Root cause (evidence)
The number of bytes dropped equals the size of Samsung's proprietary `ampl` atom (the Voice Recorder waveform data) **+ 400**, measured independently on two different recordings:
| recording | `ampl` atom size | bytes missing | difference |
|-----------|------------------|---------------|------------|
| A | 137,923 | 138,323 | 400 |
| B | 137,487 | 137,887 | 400 |
So the phone advertises `Content-Length = actualSize - ampl - 400` for these files, and Taildrop sends exactly that many bytes and stops. Because `moov` sits at the end of the file, the index is lost and the recording is unplayable outside the Voice Recorder app (which keeps its own index).
**Confirmation:** renaming the file `.m4a` → `.bin` (so Android types it `application/octet-stream` instead of `audio/mp4`) makes the reported size correct — the transfer then completes byte-for-byte (full file, ~20 min, decodes with zero errors). Same file, same network, same moment; only the advertised size differs.
### Steps to reproduce
1. Record ~20 min in Samsung Voice Recorder on a Galaxy S24.
2. Share the `.m4a` to a peer via the Tailscale Android app.
3. The received file is ~138 KB short and won't play (`moov atom not found`).
4. Rename the source `.m4a` → `.bin` and resend → the file arrives complete.
### Suggested fix
When sending from a `content://` URI, stream to EOF rather than trusting the queried `OpenableColumns.SIZE` / `Content-Length`. At minimum, verify that bytes read == advertised size and surface a mismatch instead of silently sending a truncated file.
This lives in the Android SAF file-send path touched by #15545 and #16316.
Contributor guide
Research direction
Start at the Android SAF file-send path touched by issues #15545 and #16316, then reproduce with a Samsung Voice Recorder .m4a supplied through a content:// URI. Done means the received file matches the source through EOF, including the trailing moov atom, or a size mismatch is surfaced instead of silently truncating the transfer.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- android
- Domain
- mobile-dev
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 65/100