summaryrefslogtreecommitdiff
path: root/CLAUDE.md
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-09-25 19:07:04 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-09-25 19:07:04 +0000
commit4965d5971c1443ee69af42b9f86920fd6121fea5 (patch)
treeabb32766511b063154fc167dc0c4f3793200e45c /CLAUDE.md
parent3f650131627f6737e01585b97519e03413119b44 (diff)
Advance the queue from the service, not from the activity
Marking an episode played and moving the queue on lived in PlayerViewModel, which is scoped to the activity and releases its MediaController in onCleared. So with the activity gone -- swiped off recents, or reclaimed under memory pressure -- the audio carried on from the foreground service while nothing marked anything played and nothing advanced the queue. Background listening with the screen off is the normal case for a podcast app, so this was broken in the case that matters most. The outro offset and ad-chapter skipping added last commit rode the same tick and had the same hole. Completion now belongs to PlaybackService, which is the component that survives backgrounding. Two new tested classes carry the thinking: PlayheadPolicy decides what a tick means (finish early on an outro offset, seek past a marked break, or nothing -- with finishing beating skipping, or a show with both would seek into the credits it was told to skip and sit there), and PlaybackAdvancer owns finishing, advancing, and working out where the next episode starts. Neither knows about Media3, so both are testable off-device; the service turns their answers into player calls. Where an episode starts is now decided in exactly one place. PlaybackAdvancer .start is used by the service when the queue moves on and by the view model when you tap Play, so an episode begins in the same position however it got there. PlayerConnection no longer carries its own copy of the resume rules. PlayerConnection deliberately stops handling STATE_ENDED and PlayerViewModel loses onFinished entirely -- with both in place the queue would advance twice. The view model now follows whatever is playing rather than only what it started, so the player screen's show, speed and chapter list stay right when the service moves on by itself. Verified on a device with "don't keep activities" enabled, which destroys the activity the moment it is backgrounded: an episode was resumed, the app sent to home with no activity records left, and the service took the episode to its outro line, marked it played, dropped it from the queue and started the next one, which went on playing. Also isolated the data loss seen twice this session: connectedDebugAndroidTest uninstalls the app when it finishes, taking the database with it. A plain adb install -r left four episodes in place and the next instrumented run left "run-as: unknown package". Not a migration fault, and now written down as such. No permission changes, no schema change. 385 unit tests green, detekt and lint clean. Coverage 66.23 -> 65.33 with an exception recorded: the logic moved out of the coverage-excluded ui/** tree into the counted service, and the ExoPlayer glue that remains cannot run off-device. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B
Diffstat (limited to 'CLAUDE.md')
-rw-r--r--CLAUDE.md28
1 files changed, 23 insertions, 5 deletions
diff --git a/CLAUDE.md b/CLAUDE.md
index 65655be..3e41672 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -240,11 +240,13 @@ out of a possibly-partial MP3, and the JSON path covers the shows that mark chap
slow drip on or off" rather than "Slow drip", because a description identical to the heading
beside it makes the tap ambiguous.
-- **`connectedDebugAndroidTest` can wipe the app's data.** A run cleared `listen.db` on the
- emulator mid-session. It is the test harness's install path, not a migration: a real v2
- database seeded with a subscription, an episode at position 424242 and a rule was upgraded in
- place by launching the v3 build, and all three survived with drip defaulting to off. Do not
- read an empty database after an instrumented run as a migration failure -- but do check.
+- **`connectedDebugAndroidTest` uninstalls the app when it finishes**, taking the database with
+ it. Isolated on the emulator: a plain `adb install -r` left four episodes in place, and the
+ very next instrumented run left `run-as: unknown package`. So an empty or missing database
+ after an instrumented run is the harness, not a migration -- and any device state you want to
+ keep has to be re-seeded afterwards. Migrations were separately proven safe by seeding a real
+ v2 database (a subscription, an episode at position 424242, a rule) and upgrading it in place
+ by launching the v3 build: all three survived, with drip defaulting to off.
- **The drip released-set is a query parameter, not stored state.** `observeVisible`/`getVisible`
take `dripEnabled` and hide unreleased episodes only when it is true, so switching drip off
@@ -252,6 +254,22 @@ out of a possibly-partial MP3, and the JSON path covers the shows that mark chap
left off. `getForSubscription` deliberately still returns everything: feed reconciliation must
see held-back episodes or it re-inserts them on every refresh.
+- **Completion belongs to the service, not to a view model.** Marking an episode played,
+ advancing the queue, the outro offset and ad-chapter skipping all live in `PlaybackService`
+ via `PlaybackAdvancer` and `PlayheadPolicy`. They used to live in `PlayerViewModel`, which is
+ scoped to the activity and releases its MediaController in `onCleared` -- so with the activity
+ gone the audio played on from the foreground service while nothing marked anything played or
+ moved the queue on. Background listening is the normal case for this app. Verified with
+ "don't keep activities" on: with no activity alive, an episode reached its outro line, was
+ marked played, and the next one started.
+
+ `PlayerConnection` deliberately no longer handles `STATE_ENDED`, and `PlayerViewModel` no
+ longer has an `onFinished`. Putting either back means the queue advances twice.
+
+- **Where an episode starts is decided in exactly one place**, `PlaybackAdvancer.start`, used by
+ both the service (queue advance) and the view model (a tap on Play). Two copies of the resume
+ rules would drift.
+
- **A start position belongs to `setMediaItem`, not a `seekTo` after `prepare()`.** Seeking
afterwards is silently dropped across the MediaController-to-service boundary. This was found
wiring up the intro offset -- an episode set to start sixty seconds in began at zero -- and it