Save to a document stock DocumentsUI created #248

Merged
JMR-dev merged 2 commits from test/publish-to-a-real-saf-destination into main 2026-09-06 15:48:21 +00:00
JMR-dev commented 2026-09-06 15:04:29 +00:00 (Migrated from github.com)

Closes #226, and answers the question it was actually filed for.

The premise holds

publish deletes a destination it could not write to — docs/defect-audit.md D4's fix — but only when that destination was positively zero bytes first. destinationIsKnownEmpty is careful that "I could not tell" never authorises a delete, which makes the precondition load-bearing.

Until now it was asserted only against a fake built to match it: OutputPublisherPublishTest writes ByteArray(0) into FakeSafProvider before each case, under a comment stating this is how CreateDocument behaves. If it were false in production, D4's fix would be inert and every existing test would still pass.

Measured against the real dialog on API 34: the document SAF hands back is a document URI and does report a size of exactly zero before anything writes to it. RecordingPublisher reads both at the moment publish sees them, through the ConversionDependencies seam, then lets the real copy proceed so the bytes are checked too.

So D4's fix is live. That is a "no defect found", and it is the result — it could not have been known without this.

There is no cheap half, and now both halves are measured

E7 already recorded that a DocumentsProvider is reachable only through a picker-issued grant. This adds the other constraint, found while trying to avoid the picker: a host Activity in this source set owning its own CreateDocument launcher cannot be started at all —

Intent in process org.libremediaconverter resolved to different process
  org.libremediaconverter.test

— because instrumentation runs in the target app's process. So the app's own Save button is the only launcher available to drive, which is also the more faithful thing to drive.

Three things the flow needed, each measured rather than guessed

what why
Both taps scroll first On Ready the screen carries a file card, five pickers, then the button. performClick on an off-screen node dispatches where nothing is and throws nothing, while assertIsEnabled passes either way — the first version waited for a Converted that could never come.
The format stays at its default FixtureDocumentsProvider advertises video/mp4 so the picker's MIME filter has a mutation with a shape. DocumentsUI honours that on the save side too: choosing MP3 makes the destination audio/mpeg and the fixture root is filtered out of the dialog entirely.
The notification dialog is dismissed, not pre-granted Convert converts from the permission callback whichever way the answer goes, so denying is a real user's path and enough. Granting programmatically did not take — GrantPermissionsActivity appeared anyway and swallowed the tap.

Baseline

FixtureDocumentsProvider gains create/write/delete, which it needs to be a save target at all. The test carries @FailsOnEmulatorApi37 because anything that puts DocumentsUI on screen aborts system_server on that image, as #245 established for the other two. Baseline 5 → 6.

Verification — local API 34 emulator

run 1: tests=70 failures=0 errors=0 skipped=3
run 2: tests=70 failures=0 errors=0 skipped=3
run 3: tests=70 failures=0 errors=0 skipped=3

Mutation — publish opens the stream and writes no bytes:

the destination did not receive what was staged: array lengths differed,
  expected.length=58677 actual.length=0

🤖 Generated with Claude Code

Closes #226, and answers the question it was actually filed for. ## The premise holds `publish` deletes a destination it could not write to — `docs/defect-audit.md` **D4**'s fix — but only when that destination was **positively zero bytes** first. `destinationIsKnownEmpty` is careful that *"I could not tell"* never authorises a delete, which makes the precondition load-bearing. Until now it was asserted only against a fake built to match it: `OutputPublisherPublishTest` writes `ByteArray(0)` into `FakeSafProvider` before each case, under a comment stating this is how `CreateDocument` behaves. **If it were false in production, D4's fix would be inert and every existing test would still pass.** Measured against the real dialog on API 34: the document SAF hands back **is** a document URI and **does** report a size of exactly zero before anything writes to it. `RecordingPublisher` reads both at the moment `publish` sees them, through the `ConversionDependencies` seam, then lets the real copy proceed so the bytes are checked too. So D4's fix is live. That is a "no defect found", and it is the result — it could not have been known without this. ## There is no cheap half, and now both halves are measured **E7** already recorded that a `DocumentsProvider` is reachable only through a picker-issued grant. This adds the other constraint, found while trying to avoid the picker: a host Activity in this source set owning its own `CreateDocument` launcher **cannot be started at all** — ``` Intent in process org.libremediaconverter resolved to different process org.libremediaconverter.test ``` — because instrumentation runs in the target app's process. So the app's own Save button is the only launcher available to drive, which is also the more faithful thing to drive. ## Three things the flow needed, each measured rather than guessed | what | why | |---|---| | **Both taps scroll first** | On `Ready` the screen carries a file card, five pickers, then the button. `performClick` on an off-screen node dispatches where nothing is and throws nothing, while `assertIsEnabled` passes either way — the first version waited for a `Converted` that could never come. | | **The format stays at its default** | `FixtureDocumentsProvider` advertises `video/mp4` so the picker's MIME filter has a mutation with a shape. DocumentsUI honours that on the **save** side too: choosing MP3 makes the destination `audio/mpeg` and the fixture root is filtered out of the dialog entirely. | | **The notification dialog is dismissed, not pre-granted** | Convert converts from the permission callback *whichever way the answer goes*, so denying is a real user's path and enough. Granting programmatically did not take — `GrantPermissionsActivity` appeared anyway and swallowed the tap. | ## Baseline `FixtureDocumentsProvider` gains create/write/delete, which it needs to be a save target at all. The test carries `@FailsOnEmulatorApi37` because anything that puts DocumentsUI on screen aborts `system_server` on that image, as #245 established for the other two. **Baseline 5 → 6.** ## Verification — local API 34 emulator ``` run 1: tests=70 failures=0 errors=0 skipped=3 run 2: tests=70 failures=0 errors=0 skipped=3 run 3: tests=70 failures=0 errors=0 skipped=3 ``` Mutation — `publish` opens the stream and writes no bytes: ``` the destination did not receive what was staged: array lengths differed, expected.length=58677 actual.length=0 ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.