# Listen — Agent Instructions Personal Android podcast app, `org.terst.listen`. Sibling to FeeDreader (`/workspace/feedreader`), built for Peter's own use. Greenfield, hands-on-testing-rounds workflow: sideload a debug build, use it, batch feedback. Design plan: https://claude.ai/code/artifact/85137e78-3718-4d84-8ab8-f72885170653 ## What this app is, and isn't Replacing Pocket Casts completely. The distinguishing features are audio-based intro/outro skipping, slow-drip archive subscriptions, and rule-driven auto-queueing. **Non-goals, deliberately:** anything social, new-episode notifications (or any alert — the media notification is a transport control and is mandatory for background audio, which is a different thing), favourites, bookmarks. Discovery and video were both cut from scope. ## Relationship to FeeDreader Separate repos, no shared module. That was a considered call, not an accident: FeeDreader's `FeedParser` treats `` as an image source and its `FilterRuleEngine` is 46 lines of substring matching that doesn't generalise to queue rules, so only `Opml.kt` (~96 lines) genuinely transferred. **Standing instruction:** watch for things that do generalise, and avoid pointless drift between the two apps. If a change here would obviously improve FeeDreader too, say so. Don't force sharing where the problems are actually different — but don't let two solutions to the same problem quietly diverge either. ## Test-driven development is mandatory Same rule as FeeDreader, same reasons. Every change to `app/src/main/` follows red-green-refactor, in that literal order: 1. **Write the test first**, for the specific behaviour you're about to add or change. 2. **Run it. Confirm it fails, and fails for the right reason** — an assertion mismatch showing the missing behaviour, not a compile error. 3. Write the minimal production code to pass. 4. Run again. Confirm green. 5. Refactor if needed, keeping it green. 6. Run the full quality gate before committing. **If production code was written before or without a preceding failing test**, discard the diff and redo it test-first. Do not backfill tests onto existing code — a test written against working code confirms what the code does, not what it should do. **Scoping exception:** pure resource/config/manifest changes with no unit-testable behaviour don't get a red step; the static-analysis gate covers those. ## Verification — mandatory **Never conclude a build or test result from grepped console output**, and never verify a launch with `adb shell monkey`. Use the shared wrappers, which report from exit codes, parsed JUnit XML, and a logcat capture started before launch: ``` /workspace/devtools/bin/gradle-test /workspace/listen -- --rerun-tasks /workspace/devtools/bin/android-launch-check org.terst.listen \ --apk app/build/outputs/apk/debug/app-debug.apk --settle 15 ``` See `/workspace/devtools/README.md`. These exist because grepped console text has produced wrong claims three separate times across these repos. ## Quality gate — every commit 1. **Build** — `./gradlew compileDebugKotlin` succeeds. 2. **Tests** — `./gradlew testDebugUnitTest` passes in full. 3. **Static analysis** — `./gradlew detekt lintDebug` clean: zero lint errors, zero new detekt findings beyond the baseline. 4. **Coverage must not decrease** against `.coverage-baseline` (line coverage over `app/src/main/java` excluding `ui/**`). Set `COVERAGE_EXCEPTION="reason"` for justified drops, e.g. deleting covered dead code. 5. **Manifest permission diff** — if `` changed, the commit message says why. 6. **Schema migrations** — if `AppDatabase.kt`'s `version` changed, a migration test must be touched in the same diff. Room tracks by version; a wrong migration corrupts a real, non-reproducible library. ## Domain notes worth keeping - **Playback position is the crown jewel of the data model.** It must survive process death. Save on a timer, on pause, and on lifecycle transitions — not just on stop. - **Never assume audio.** An enclosure carries a MIME type. Video was cut from scope, but the model stays media-agnostic so it isn't a retrofit. - **GUIDs move.** Publishers rewrite feeds; match episodes on GUID first, then enclosure URL, then title+date. An episode that loses its identity loses its playback position. - **Retention is auto-delete-once-played** with a grace period, so a mis-tap doesn't destroy an episode you were still listening to. - **YouTube audio is yt-dlp, embedded.** Chaquopy runs an in-process CPython with real yt-dlp; see `youtube/YouTubeAudio.kt` and `src/main/python/ytaudio.py`. No Termux, no podkit, no server round trip. - **Pin the Python version.** Chaquopy defaults to 3.8, which yt-dlp no longer supports, so pip silently resolves the last 3.8-compatible release (2024.10.22) and YouTube extraction fails while the build succeeds. `YouTubeAudioTest` fails on a pre-2025 yt-dlp for exactly this reason. - **Audio-only, always.** Separate audio and video streams need ffmpeg to merge, which is the documented failure mode for yt-dlp on Android. A single pre-muxed audio stream has nothing to merge and needs no ffmpeg. - **No proof-of-origin tokens.** Obtaining one means running Google's attestation script in a WebView, which this app does not do. It works without one from an ordinary phone connection. - **Known risk:** yt-dlp has deprecated YouTube extraction without a JavaScript runtime and warns that some formats may be missing. There is no deno/node on Android. If yt-dlp makes a JS runtime mandatory, this breaks and the fallback is bundling a JS engine it accepts. - **Gradle is pinned to 8.14.3, not 9.x**, because Chaquopy 16.1.0 calls `org.gradle.util. VersionNumber`, removed in Gradle 9. Chaquopy 17 moved to AGP 9.x; going there means bumping AGP, Gradle and Chaquopy together. This is the one deliberate divergence from FeeDreader.