Skip to content

Add rewind on pause - #492

Closed
skorokithakis wants to merge 5 commits into
GrakovNe:mainfrom
skorokithakis:feature/rewind-on-pause
Closed

Add rewind on pause#492
skorokithakis wants to merge 5 commits into
GrakovNe:mainfrom
skorokithakis:feature/rewind-on-pause

Conversation

@skorokithakis

@skorokithakis skorokithakis commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

I have an issue where, if I haven't listened to a book I a while, I forget my place. I implemented a feature to set an interval to rewind by, eg 60 seconds.

When you pause a book, I store the time of the pause. When you unpause, the book rewinds a specific amount, up to the full 60 seconds (or whatever you set) , depending on how long the pause was. A short pause rewinds 5 seconds, a 5-minute (or longer) pause rewinds the full duration.

I hope that's useful.

LLM details follow:

Moves the position back when a book is continued, based on how long that book was paused:

rewind = 5s + (amount - 5s) * min(pause, 300s) / 300s

So any resume gives you a little context back, and a pause of five minutes or more rewinds the full amount that is set. With the default 30s amount: a momentary pause rewinds 5s, a 10 second pause about 5.8s, a 150 second pause 17.5s, and five minutes or more the full 30s.

The rewind never crosses into the previous chapter. If the position is closer to the chapter start than the rewind amount, it stops at the start of the current chapter.

The setting

"Rewind on pause" in Settings -> Playback -> Seek settings: a toggle and an amount (10-60s, presets 10/15/30/60), on by default with 30s. It is part of the settings backup, and the amount is clamped to its range both on read and on import, so an edited backup cannot produce a strange rewind. The amount cannot go below 10 seconds, because the rewind always starts at 5 seconds and a smaller maximum would be inert. The manual rewind and forward intervals keep their own 1-60 range.

This puts the dormant RewindOnPauseTime domain class to use and removes the SeekTimeOption enum, which nothing else referenced.

Per book, not global

The last active time is stored per book (a bookId -> epoch millis map, capped at the 100 newest books). Pausing book A and starting book B must not reset A's clock: the rewind models how much of that book the listener forgot. A book with no stored time gets the full rewind, because "no record" means the listener probably forgot more, not less.

One choke point

The rewind is applied by a thin ForwardingPlayer around the ExoPlayer that the MediaLibrarySession is built on. The seek therefore happens before the audio starts, so there is no audible jump, and every source behaves the same: the app, the notification, media buttons, Android Auto, the widget, the sleep timer, a book switch, and a resume after the process was killed.

Two details in there:

  • It fires only on a real playWhenReady false -> true change, so a repeated play command during a session cannot rewind.
  • The automatic resume after a short audio focus loss (a notification, a navigation prompt, a call, with the "pause" policy) never issues a play command, because ExoPlayer only suppresses playback. That path is handled from the playback suppression reason instead. A buffering stall also stops playback and is deliberately excluded, because nothing was missed. On this one path the audio has already restarted when the seek happens, so a very short blip is possible; avoiding it would need a stop/seek/start dance that risks losing the audio focus.

Notes for review

  • The clamp at the chapter start is a correctness fix, not a taste decision. The timeline holds one media item per chapter, and PlaybackNavigationService.onPositionDiscontinuity treats a backward seek into a different media item as a discontinuity. On a partly downloaded book, a rewind that walks into a missing chapter becomes a jump to the start of a much earlier chapter, or a stall when there is no available chapter to skip to. Clamping inside the current chapter keeps that handler on its currentIndex != previousIndex guard, so the interaction cannot arise. It also means the rewind needs no chapter durations at all.
  • The book that the player has loaded is tracked in the handler rather than read from the playing item preference. On a book switch the preference already points at the next book before the previous one stops, so reading it in the listener would record the stop against the wrong book.

Tests

Unit tests for the interpolated amount, the seek target (including the clamp at the start of the chapter), the preference storage and its cap, the handler (book switch, suppression resume, the playWhenReady gate), the shared slider range, the settings backup, and the view model. ./gradlew lintKotlin test passes.

Moves the position back when a book is continued, proportionally to how
long that book was paused: amount * min(pause, 300s) / 300s. A momentary
pause moves almost nothing, a pause of five minutes or more moves the
full amount.

The setting lives in Settings -> Playback -> Seek settings (on by
default, 30s, 1-60s) and is part of the settings backup. It reuses the
dormant RewindOnPauseTime domain class and drops the unused
SeekTimeOption enum.

The last active time is stored per book, so pausing book A and listening
to book B does not reset A. A book with no stored time gets the full
rewind. The map keeps the 100 newest books.

The rewind is applied in one place, a forwarding player around the
ExoPlayer that the media session is built on, so the seek happens before
the audio starts and every source behaves the same: the app, the
notification, media buttons, Android Auto, the widget, and the sleep
timer. It fires only on a real playWhenReady false -> true change, so a
repeated play command cannot rewind. The automatic resume after a short
audio focus loss has no play command, so it is covered separately by the
playback suppression reason; a buffering stall is excluded there.
The proportional part alone means a short pause moves the position
almost not at all, which is not useful: after any interruption you want
a little context back. Every resume now rewinds at least 5 seconds, and
the proportional amount applies above that floor.

The amount can therefore no longer be set below 10 seconds, because 5
and less would be inert. The manual rewind and forward intervals keep
their own range.
The 5 seconds is a starting point, not a floor that swallows short
pauses: a 10 second pause with a 30 second amount now rewinds about 6
seconds instead of 5. Both ends are unchanged, a momentary pause gives 5
seconds and a pause of five minutes gives exactly the amount that is
set.
The timeline holds one media item per chapter, and
PlaybackNavigationService.onPositionDiscontinuity treats a backward seek
into a different media item as a discontinuity. With a partly downloaded
book, a rewind that walks into a missing chapter becomes a jump to the
start of a much earlier chapter, or a stall when no chapter is available
to skip to. Clamp the rewind at position 0 of the current chapter so the
discontinuity handler exits at its currentIndex != previousIndex guard
and the interaction cannot happen.
@skorokithakis

Copy link
Copy Markdown
Contributor Author

I've tested this on my phone and it works great, with #493 as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant