Fixes#238. Closes#225. Settles the open question on #226.
Joining files picked through the system picker failed outright whenever the strategy was stream copy — the matched-files case the UI advertises as "joined without re-encoding — no quality loss".
[ffkitsaf @ ...] Protocol 'ffkitsaf' not on whitelist 'file,crypto,data'!
Error opening input file .../joined_from_content.concat_list.txt
This was found by writing #225's test, not by looking for it.
The defect
JoinScreen picks with OpenMultipleDocuments, so real inputs are alwayscontent://. ConcatEngine maps each through getSafParameterForRead, and FFmpegConcatCommand writes the resulting ffkitsaf: paths into the concat list file. The demuxer applies its own protocol whitelist, defaulting to file,crypto,data.
-safe 0 does not help: it permits absolute paths, this permits the scheme they carry. Two separate gates, and only one was open.
Why nothing caught it
The two halves never met:
Only STREAM_COPY feeds the demuxer a list file.REENCODE passes each input with its own -i, where the whitelist does not apply — so joining over SAF worked for mismatched clips.
Every join test passed Uri.fromFile, taking ConcatEngine's uri.path arm instead of the bridge. ConcatEngineTest.matchingClipsAreJoinedByStreamCopy exercised stream copy with a file: path and passed.
So the one broken combination — stream copy and a content URI — was the one no test produced and the only one a user can reach.
FFmpegKitConfig.getSafParameterForRead is on every real conversion and join and was on no passing test; only UnopenableUriTest touched it, against an authority that does not exist, which proves the error message rather than the bridge. ContentUriInputTest now drives both paths from a real content:// URI.
It uses a plain ContentProvider, because the documents provider cannot be reached — measured three ways on API 34:
approach
result
DOCUMENTS_PROVIDER without MANAGE_DOCUMENTS
refused at install: "Provider must be protected by MANAGE_DOCUMENTS"
Instrumentation.getContext()
denied — instrumentation runs in the target app's process, so it carries the app's uid
adoptShellPermissionIdentity(MANAGE_DOCUMENTS)
denied identically
Each denial names the only way in: "you obtain access using ACTION_OPEN_DOCUMENT or related APIs". That settles #226: its cheap headless half does not exist, so the work is one picker-driven item, not two.
The bridge needs no documents provider — it opens a descriptor through the resolver, so any readable content:// URI exercises it, and an ordinary provider may be exported unprotected. The class stays headless: no DocumentsUI, none of #190's flake.
Verification — local API 34 emulator
tests=66 failures=0 errors=0 skipped=3
Mutation — remove the -protocol_whitelist pair:
ContentUriInputTest > contentUriInputsJoinThroughTheSafBridge FAILED
Protocol 'ffkitsaf' not on whitelist 'file,crypto,data'!
It reproduces the production failure verbatim, in that test and nothing else. FFmpegConcatCommandTest pins the flag on the JVM, next to the existing -safe 0 case.
Fixes #238. Closes #225. Settles the open question on #226.
**Joining files picked through the system picker failed outright** whenever the strategy was stream copy — the matched-files case the UI advertises as *"joined without re-encoding — no quality loss"*.
```
[ffkitsaf @ ...] Protocol 'ffkitsaf' not on whitelist 'file,crypto,data'!
Error opening input file .../joined_from_content.concat_list.txt
```
This was found by writing #225's test, not by looking for it.
## The defect
`JoinScreen` picks with `OpenMultipleDocuments`, so real inputs are **always** `content://`. `ConcatEngine` maps each through `getSafParameterForRead`, and `FFmpegConcatCommand` writes the resulting `ffkitsaf:` paths into the concat list file. The demuxer applies its own protocol whitelist, defaulting to `file,crypto,data`.
`-safe 0` does not help: it permits absolute **paths**, this permits the **scheme** they carry. Two separate gates, and only one was open.
## Why nothing caught it
The two halves never met:
- **Only `STREAM_COPY` feeds the demuxer a list file.** `REENCODE` passes each input with its own `-i`, where the whitelist does not apply — so joining over SAF worked for *mismatched* clips.
- **Every join test passed `Uri.fromFile`**, taking `ConcatEngine`'s `uri.path` arm instead of the bridge. `ConcatEngineTest.matchingClipsAreJoinedByStreamCopy` exercised stream copy with a `file:` path and passed.
So the one broken combination — stream copy **and** a content URI — was the one no test produced and the only one a user can reach.
## The #225 half
`FFmpegKitConfig.getSafParameterForRead` is on every real conversion and join and was on no passing test; only `UnopenableUriTest` touched it, against an authority that does not exist, which proves the error message rather than the bridge. `ContentUriInputTest` now drives both paths from a real `content://` URI.
**It uses a plain `ContentProvider`, because the documents provider cannot be reached** — measured three ways on API 34:
| approach | result |
|---|---|
| `DOCUMENTS_PROVIDER` without `MANAGE_DOCUMENTS` | refused at install: *"Provider must be protected by MANAGE_DOCUMENTS"* |
| `Instrumentation.getContext()` | denied — instrumentation runs in the **target app's process**, so it carries the app's uid |
| `adoptShellPermissionIdentity(MANAGE_DOCUMENTS)` | denied identically |
Each denial names the only way in: *"you obtain access using ACTION_OPEN_DOCUMENT or related APIs"*. **That settles #226**: its cheap headless half does not exist, so the work is one picker-driven item, not two.
The bridge needs no documents provider — it opens a descriptor through the resolver, so any readable `content://` URI exercises it, and an ordinary provider may be exported unprotected. The class stays headless: no DocumentsUI, none of #190's flake.
## Verification — local API 34 emulator
```
tests=66 failures=0 errors=0 skipped=3
```
Mutation — remove the `-protocol_whitelist` pair:
```
ContentUriInputTest > contentUriInputsJoinThroughTheSafBridge FAILED
Protocol 'ffkitsaf' not on whitelist 'file,crypto,data'!
```
It reproduces the production failure verbatim, in that test and nothing else. `FFmpegConcatCommandTest` pins the flag on the JVM, next to the existing `-safe 0` case.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Fixes #238. Closes #225. Settles the open question on #226.
Joining files picked through the system picker failed outright whenever the strategy was stream copy — the matched-files case the UI advertises as "joined without re-encoding — no quality loss".
This was found by writing #225's test, not by looking for it.
The defect
JoinScreenpicks withOpenMultipleDocuments, so real inputs are alwayscontent://.ConcatEnginemaps each throughgetSafParameterForRead, andFFmpegConcatCommandwrites the resultingffkitsaf:paths into the concat list file. The demuxer applies its own protocol whitelist, defaulting tofile,crypto,data.-safe 0does not help: it permits absolute paths, this permits the scheme they carry. Two separate gates, and only one was open.Why nothing caught it
The two halves never met:
STREAM_COPYfeeds the demuxer a list file.REENCODEpasses each input with its own-i, where the whitelist does not apply — so joining over SAF worked for mismatched clips.Uri.fromFile, takingConcatEngine'suri.patharm instead of the bridge.ConcatEngineTest.matchingClipsAreJoinedByStreamCopyexercised stream copy with afile:path and passed.So the one broken combination — stream copy and a content URI — was the one no test produced and the only one a user can reach.
The #225 half
FFmpegKitConfig.getSafParameterForReadis on every real conversion and join and was on no passing test; onlyUnopenableUriTesttouched it, against an authority that does not exist, which proves the error message rather than the bridge.ContentUriInputTestnow drives both paths from a realcontent://URI.It uses a plain
ContentProvider, because the documents provider cannot be reached — measured three ways on API 34:DOCUMENTS_PROVIDERwithoutMANAGE_DOCUMENTSInstrumentation.getContext()adoptShellPermissionIdentity(MANAGE_DOCUMENTS)Each denial names the only way in: "you obtain access using ACTION_OPEN_DOCUMENT or related APIs". That settles #226: its cheap headless half does not exist, so the work is one picker-driven item, not two.
The bridge needs no documents provider — it opens a descriptor through the resolver, so any readable
content://URI exercises it, and an ordinary provider may be exported unprotected. The class stays headless: no DocumentsUI, none of #190's flake.Verification — local API 34 emulator
Mutation — remove the
-protocol_whitelistpair:It reproduces the production failure verbatim, in that test and nothing else.
FFmpegConcatCommandTestpins the flag on the JVM, next to the existing-safe 0case.🤖 Generated with Claude Code