docs: the video's link is its Gitea page, not the raw URL, which its own CSP blocks
Checked the way a browser does it rather than the way curl does, and curl had given a false green: Gitea sends `Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; sandbox` with every /raw/ response. That is deliberate - it is what makes serving raw HTML and SVG safe - but it also blocks the media load when a browser navigates straight to a video, so the tab stays blank with no error. The same URL plays fine as a subresource, which is exactly why the file's page in Gitea shows a player: that page embeds <video controls src=".../raw/branch/main/...mp4">. So the declaration's link is the /src/ page URL, and /raw/ is documented as the curl-and-download URL instead. Everything verified about both URLs is written down in step 5 - including the two ways to get a player with no Gitea chrome around it, if that is ever wanted: an unlisted YouTube upload, or dropping the header for this one path on the Gitea host's Caddy proxy, which must stay scoped to store-assets/ precisely because that header is what protects raw HTML and SVG. The file itself was never the problem: 200, video/mp4, Content-Disposition: inline, bytes sha256-identical to the recording, and Range: bytes=0-99 -> 206 with the matching Content-Range.
This commit is contained in:
@@ -192,8 +192,10 @@ device, the tablet sets on AOSP AVDs; see [docs/STORE_LISTING.md](docs/STORE_LIS
|
||||
`python3 tools/make-demo-photos.py` draws the synthetic photo library the results screenshot is
|
||||
captured over, so the listing never shows a real one. The finished assets are **committed** —
|
||||
including the foreground-service demonstration video that [the declaration](docs/PLAY_RELEASE_CHECKLIST.md)
|
||||
links to — and Gitea serves them without a login (`…/raw/branch/main/store-assets/…`), so pushing
|
||||
is the deployment step and that URL is the link itself.
|
||||
links to — and Gitea serves them without a login, so pushing is the deployment step:
|
||||
`…/src/branch/main/store-assets/…` is the page a browser opens (and the shape Play's video link
|
||||
takes, because a `/raw/` URL carries a `default-src 'none'` CSP that stops a video playing inline),
|
||||
`…/raw/branch/main/…` is the bytes.
|
||||
|
||||
User-visible changes are tracked in [CHANGELOG.md](CHANGELOG.md); the Play "What's new"
|
||||
text you type per release is separate and lives only in the Play Console.
|
||||
|
||||
@@ -283,23 +283,38 @@ action except the items marked **[code]**.
|
||||
CANCEL"*, 13–18 s) while the scan kept running behind it, and the finished results
|
||||
(*17 duplicate groups · 54 of 5,385 photos*) at the end.
|
||||
|
||||
**The link to paste** is the committed file's own URL — no login, no second host:
|
||||
**The link to paste** is the file's page in Gitea — what the repository's own *view* of
|
||||
the file opens, and what plays it:
|
||||
|
||||
```
|
||||
https://gitea.softwareapp-hb.de/fishfacegit/foto-duplicate-finder/raw/branch/main/store-assets/fgs-demo/foreground-service-demo.mp4
|
||||
https://gitea.softwareapp-hb.de/fishfacegit/foto-duplicate-finder/src/branch/main/store-assets/fgs-demo/foreground-service-demo.mp4
|
||||
```
|
||||
|
||||
Checked from outside this network: **200**, `Content-Type: video/mp4`,
|
||||
`Content-Length: 1847900` and `Content-Disposition: inline`, so a reviewer's browser plays
|
||||
it in the tab rather than prompting a download, and the served bytes are identical to the
|
||||
recording (sha256 match). A `Range: bytes=0-99` request answers **206** with exactly those
|
||||
100 bytes even though `Accept-Ranges` is not advertised, so seeking works and a player can
|
||||
start without fetching the whole file first — which matters here because `screenrecord`
|
||||
writes the `moov` atom at the very end of the file. Two things to know before pasting: the
|
||||
URL is pinned to `main`, so a re-shoot has to **overwrite the file in place** rather than
|
||||
be added alongside it; and replacing the video at all means resubmitting the declaration.
|
||||
(An unlisted YouTube upload of the same file works equally well if a link that survives
|
||||
future commits is preferred — but then it is a second place to keep in step.)
|
||||
⚠️ **Not the `/raw/` URL**, even though that is the file itself. Gitea answers raw
|
||||
responses with `Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline';
|
||||
sandbox` — a deliberate guard against raw HTML and SVG — and that blocks the media load of a
|
||||
document that navigates straight to a video: the tab stays blank with no error, which is
|
||||
exactly how this behaved in a browser. The same URL is fine as a *subresource*: the page
|
||||
above embeds `<video controls src="…/raw/branch/main/…mp4">`, which is why previewing the
|
||||
file plays it and opening `/raw/` does not. Use `/raw/` for `curl` and downloads, never as
|
||||
the link in the form.
|
||||
|
||||
Verified about the **page** URL: **200** anonymously, containing that `<video controls>`
|
||||
element. Verified about the **raw** URL, from outside this network and unauthenticated:
|
||||
**200**, `Content-Type: video/mp4`, `Content-Length: 1847900`, `Content-Disposition:
|
||||
inline`, bytes sha256-identical to the recording, and `Range: bytes=0-99` → **206** with
|
||||
exactly those 100 bytes although `Accept-Ranges` is not advertised (which matters because
|
||||
`screenrecord` writes the `moov` atom at the very end of the file). Two things to know
|
||||
before pasting: the URL is pinned to `main`, so a re-shoot has to **overwrite the file in
|
||||
place** rather than be added alongside it; and replacing the video at all means resubmitting
|
||||
the declaration.
|
||||
|
||||
If a link with no Gitea chrome around the player is preferred later, there are two ways and
|
||||
neither is needed for the declaration: an unlisted YouTube upload of the same file, or a
|
||||
rule on the Gitea host's Caddy proxy dropping the header for this one path
|
||||
(`@fgsdemo path …/store-assets/fgs-demo/*.mp4` plus `header_down @fgsdemo
|
||||
-Content-Security-Policy`) — **scoped to `store-assets/` only**, since that header is what
|
||||
makes raw HTML and SVG safe to serve. Neither was tried from here.
|
||||
|
||||
Both flags exist because this take found two traps worth knowing before a re-shoot:
|
||||
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ Legend: ✅ done · 🚧 in progress · ⏳ not started · 💡 candidate (propo
|
||||
|---|---|
|
||||
| ~~Large-library scan stall~~ ✅ **fixed — verified on device** | The 5,363-photo library used to freeze at *“74 / 100 · Comparing file signatures…”* (a **blocked syscall**: zero CPU, workers sleeping). `BoundedCall` now abandons a stuck provider call **and** trips a stall breaker after three consecutive timeouts. **Verified on the real test device (Android 12 / MIUI): the scan completes — 20.2 s cold, 11.0 s warm, 15 groups, 6 unreadable — where it used to hang.** Covered by `BoundedCallTest` and `ScanEngineEndToEndTest` |
|
||||
| ~~Unreleased `BoundedCall` fix~~ ✅ | The provider-call deadline shipped in **1.0.1** (`versionCode` 2) |
|
||||
| **Play Console: foreground-service declaration** | Declaring the `dataSync` type (App content › Foreground service permissions) is release-blocking. **[code]** the manifest already declares it, [PLAY_RELEASE_CHECKLIST](PLAY_RELEASE_CHECKLIST.md) step 5 carries all four things the form asks for (description, defer/interrupt impact, use case **Local processing: Other**, video link), and the **video now exists**: recorded on the test device on 2026-10-14 with `INSTALL=0 WARMUP=0 tools/record-fgs-demo.sh`, then verified frame by frame rather than assumed — intro at 0 s, the scan starting, **~6 s of the shade with the live "Finding duplicate photos / Looking for visually similar photos… / CANCEL" notification while the scan kept running**, and the results at the end. It is committed, and that is what published it: Gitea serves the raw file with `Content-Type: video/mp4` and `Content-Disposition: inline`, verified by hash from outside the network, so pasting that URL into the declaration is all that is left. Two details of the take are recorded in step 5 because they each cost an attempt: a *warm* scan is 5.4 s, shorter than the shade takes to open, so `WARMUP=0` films the cold one; and MIUI refuses `adb install` outright, so `INSTALL=0` records with the build already on the device. On a KVM-less emulator the same video cannot be made at all: every scan start dies as `am_crash … ForegroundServiceDidNotStartInTimeException` → *Force finishing activity* with no `am_kill`, i.e. emulation is too slow to reach `startForeground()` inside Android's budget (QEMU's `-icount` clock dilation made it work once, not reproducibly) |
|
||||
| **Play Console: foreground-service declaration** | Declaring the `dataSync` type (App content › Foreground service permissions) is release-blocking. **[code]** the manifest already declares it, [PLAY_RELEASE_CHECKLIST](PLAY_RELEASE_CHECKLIST.md) step 5 carries all four things the form asks for (description, defer/interrupt impact, use case **Local processing: Other**, video link), and the **video now exists**: recorded on the test device on 2026-10-14 with `INSTALL=0 WARMUP=0 tools/record-fgs-demo.sh`, then verified frame by frame rather than assumed — intro at 0 s, the scan starting, **~6 s of the shade with the live "Finding duplicate photos / Looking for visually similar photos… / CANCEL" notification while the scan kept running**, and the results at the end. It is committed, and that is what published it — the link is the file's **page** in Gitea (`/src/branch/main/…`), which plays it anonymously. The `/raw/` URL is deliberately not the link: Gitea sends `Content-Security-Policy: default-src 'none'` with raw responses, so a browser navigating to the video sits on a blank tab even though the bytes are perfect. Pasting the page URL into the declaration is all that is left. Two details of the take are recorded in step 5 because they each cost an attempt: a *warm* scan is 5.4 s, shorter than the shade takes to open, so `WARMUP=0` films the cold one; and MIUI refuses `adb install` outright, so `INSTALL=0` records with the build already on the device. On a KVM-less emulator the same video cannot be made at all: every scan start dies as `am_crash … ForegroundServiceDidNotStartInTimeException` → *Force finishing activity* with no `am_kill`, i.e. emulation is too slow to reach `startForeground()` inside Android's budget (QEMU's `-icount` clock dilation made it work once, not reproducibly) |
|
||||
| **Remaining Play paperwork** | Console actions only, no code: the **first upload + Play App Signing enrolment**, the **hosted privacy policy URL** (the working copy is now published to Gitea as [`fishfacegit/foto-duplicate-finder`](https://gitea.softwareapp-hb.de/fishfacegit/foto-duplicate-finder), so the policy renders at a public, login-free URL and only the Console paste is left), **Data safety**, the **Photo/video permissions** declaration, the **content rating / audience / app access / ads / export** questions, and the **listing copy + the three screenshot sets** (phone, 7-inch and 10-inch, all captured), plus the **paid-app setup**: a payments profile with tax rates, the €2.99 price and the 14-day €1.49 launch sale on *Products > App pricing* — the price must be set before the first upload, because free → paid is not possible afterwards (M9). Keystore, `versionCode`, listing copy, graphics, permission audit and the signed `.aab` (`sha256 f8e6a29a…`) are all in place; rollout goes internal → closed → production. Step-by-step: [PLAY_RELEASE_CHECKLIST](PLAY_RELEASE_CHECKLIST.md) |
|
||||
| ~~Store screenshots need one re-shoot~~ ✅ **done — verified on device** | The old captures showed the *Photo access needed* banner (an app-op is not a grant) and `03-results.png` showed the developer's own library. After a real grant the set was re-shot over the generated demo library: **01-empty** (no tap needed — results and selection live in `ScannerViewModel`, so force-stop + relaunch returns to the empty state with the grant still in place), **02-scanning** (`5353 / 5385`), **03-results** (**17 groups, 54 of 5385 photos, 83.5 MB reclaimable**; the headline group's 43.1 MB out-ranks the device's own 39.8 MB, so no real photo is in frame) and **04-options**. Captures are taken by watching for UI **text** changes, because MIUI refuses synthetic taps (`INJECT_EVENTS`). The demo library is deleted from the test device afterwards — MediaStore **5,386 → 5,364** rows, and the 22 stale rows it leaves in the hash index are pruned by `PhotoIndexStore.pruneTo` on the next scan. Deliberately not captured: the staged state and the system confirmation dialog, because staging clears the demo group and would put real photos back in frame. Details: [STORE_LISTING](STORE_LISTING.md) |
|
||||
| ~~Tablet store screenshots outstanding~~ ✅ **done — captured on AVDs** | Play has a **7-inch tablet** and a **10-inch tablet** screenshot slot of its own (asking for **≥ 4** screenshots on large screens). Both are now captured on AOSP Android 12 AVDs — `Nexus 7 2013` (1200×1920, `sw600dp`) and `medium_tablet` (1600×2560, `sw800dp`) — because this machine exposes no `/dev/kvm`: they ran in **pure software emulation** (`-accel off -no-window -gpu swiftshader_indirect`), ≈3 min to boot, with `hide_error_dialogs` on so SystemUI's ANR dialog could not sit in frame. Four screens per tablet over the same generated demo library: empty, scanning (20 / 22), results (**2 groups · 16 of 22 photos · 43.7 MB reclaimable**, 39.7 s / 53.1 s) and Options. `tools/make-store-screenshots.sh` is form-factor aware (`FORM_FACTOR=phone\|tablet-7in\|tablet-10in`) and the tablet form factors crop **0 px** of chrome, because a tablet frame is already inside Play's 2:1 cap. Recipe: [STORE_LISTING](STORE_LISTING.md) |
|
||||
|
||||
@@ -41,15 +41,19 @@ python3 tools/make-demo-photos.py # synthetic photo li
|
||||
```
|
||||
|
||||
Committing `store-assets/` is also what *publishes* it: Gitea serves every committed file without a
|
||||
login, at
|
||||
login, and pushing the commit **is** the deployment step — there is no separate upload, so a
|
||||
re-shot screenshot or re-recorded video goes live the moment it is pushed. There are two URL shapes,
|
||||
and which one is right depends on what does the reading:
|
||||
|
||||
```
|
||||
https://gitea.softwareapp-hb.de/fishfacegit/foto-duplicate-finder/raw/branch/main/<path>
|
||||
https://gitea.softwareapp-hb.de/fishfacegit/foto-duplicate-finder/raw/branch/main/<path> the bytes — curl, downloads
|
||||
https://gitea.softwareapp-hb.de/fishfacegit/foto-duplicate-finder/src/branch/main/<path> a page — what a browser opens
|
||||
```
|
||||
|
||||
so pushing the commit **is** the deployment step — there is no separate upload, and a re-shot
|
||||
screenshot or re-recorded video goes live as soon as it is pushed. Play's foreground-service
|
||||
declaration takes its video link from exactly there; the URL is written out in
|
||||
For media that distinction is not cosmetic: Gitea sends `Content-Security-Policy: default-src
|
||||
'none'` with `/raw/` responses, so a video opened at a raw URL shows a blank tab, while the same file
|
||||
plays on its `src` page. Play's foreground-service declaration therefore takes the **page** URL as
|
||||
its video link; the reasoning and what was verified about both URLs are in
|
||||
[PLAY_RELEASE_CHECKLIST](PLAY_RELEASE_CHECKLIST.md) step 5.
|
||||
|
||||
| Asset | Play's spec | Result | Source |
|
||||
|
||||
Reference in New Issue
Block a user