Skip to content

fix: keep EPUB reading position after read-aloud (refs #286) - #296

Merged
van-geaux merged 7 commits into
van-geaux:mainfrom
chortya:fix/issue-286-epub-tts-position
Oct 20, 2026
Merged

van-geaux merged 7 commits into
van-geaux:mainfrom
chortya:fix/issue-286-epub-tts-position

Conversation

@chortya

@chortya chortya commented Oct 18, 2026

Copy link
Copy Markdown
Contributor

Contribution pause

Maintainer-requested exception

  • A maintainer requested this pull request or confirmed the exception. (Offer: read-aloud position fixes, TTS voice language, and foldable two-page reading (asking before PR) #286: "Please go ahead and submit the read-aloud position fixes as a PR.")
  • The change is scoped to the requested work.
  • The change is compatible with the multi-server/provider-module refactor. (Reader and TTS service only; no provider-module changes.)
  • Tests and manual-validation status are documented.
  • No credentials, cookies, API keys, private server URLs, or generated local data are included.

Summary

Refs #286. Fixes EPUB reading positions jumping back after read-aloud (TTS) was used. Builds on #277: its server-vs-local choice on open is unchanged, and its stale-narration cleanup still runs on every real move.

While the reader is in the background, EpubTtsPlaybackService keeps saving each utterance to ReadiumEpubLocatorStore, so the stored position moves on after onSaveInstanceState has captured the reader's state. Three paths then put the reader back at an older position, saved it, and reported it to the server:

  1. Saved-state restore (process death). requestedLocator, the locator from the saved instance state, won over the newer stored position. Now epubRestoredLocator prefers the stored position, since the stores are written on every move. Saved state is only the fallback, for preview, which never writes the store. The same applies to the saved TTS locator.
  2. Returning to the foreground. repeatOnLifecycle(STARTED) re-collects currentLocator, and PaginationListener.onPageChanged re-fires after resume. Both replayed the last visual position over the newer read-aloud position. Both now go through recordVisualLocator. Real moves are recorded as before. A re-report of the same place, including the later report enriched with position/totalProgression, is written only while the store still holds this reader's own last position.
  3. Rebuilt reader following an old read-aloud position. restoreTtsPositionAfterRecreation fell back to the saved TTS locator even with no TTS session running. After rotation, fold or process death, the reader jumped to where listening last stopped, which can be far behind where the book has been read since. It now follows only a live session for this reader (hasSession). The saved TTS position remains the Keep listening start point.

Also, pausing read-aloud sends the current position to the server straight away. Previously the 15 s / 0.2% throttle could leave the last stretch unsent if the app was killed while paused.

Verification

  • ./gradlew :app:testDebugUnitTest — 946/946 pass. :app:lintDebug — 0 errors. :app:compileDebugAndroidTestKotlin and :app:assembleDebug succeed. git diff --check is clean.
  • New EpubPositionRestoreInstrumentedTest (6 tests) on a Pixel Fold API 36 emulator. The three bug tests fail on current main and pass with this change, in two consecutive runs. The others cover normal rotation and the two helpers. The rebuild test records every position written, because on main the jump to the old read-aloud position is sometimes overwritten again moments later.
  • ReadiumEpubOpenInstrumentedTest (17) and EpubReaderProgressFooterInstrumentedTest (1) pass, except selectsARealReadiumPositionForNonzeroServerPercentage and generatedEpubImageColorOverrideIsUniversalAndIdempotent, which fail the same way on unmodified main on this emulator.
  • Background read-aloud is simulated in the tests by writing the stores the way the service does, because the emulator doesn't produce speech.
  • Manual: the same fixes have been in use on a Galaxy Z Fold8 (fork build) and resolved the reported jumps. Not yet validated on a device with this exact branch.

chortya and others added 2 commits October 18, 2026 10:52
Background read-aloud saves each utterance position after the reader
stopped. Two paths then put the reader back at the older visual page
and saved it over the newer position (and reported it to the server):

- Rebuilding the reader from saved instance state (process death)
  preferred the saved-state locator over the newer stored position.
  The stores, written on every move, now win; saved state is only a
  fallback (preview).
- Returning to the foreground replayed the navigator's last position
  through the locator flow and the pagination listener. Only real moves
  are recorded now; re-reports of the same place are written only while
  the store still holds this reader's own position.

Also send the current read-aloud position to the server when playback
is paused, past the 15 s / 0.2% progress throttle.

Tests: instrumented regression tests for both paths (red before the
fix) and for configuration recreation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After recreation (rotation, fold, process death) the reader followed the
saved read-aloud position even with no read-aloud session running. That
position is where listening last stopped, often far behind where the
book was read since, and the jump was then saved and synced. Only a live
session for this reader is followed now; the saved position remains the
Keep listening start point.

Test: instrumented regression (red before the fix).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@van-geaux

van-geaux commented Oct 19, 2026 •

Copy link
Copy Markdown
Owner

@chortya Thanks for the focused fix and for adding thorough regression tests. The changes clearly target the reported issue, and the coverage for saved-state restoration and returning to the reader is valuable.

One edge case remains:

When the reader is backgrounded, read-aloud can save a newer position in the background. When the reader returns to the foreground, Readium may re-paginate and report the previous visual page again, but with slightly different progression, fragment, or position details. In that case, shouldRecordVisualLocator() may treat the callback as a genuine user move and save the older visual position over the newer read-aloud position.

Please add a regression test for this scenario or adjust the protection so a stale visual callback cannot replace newer read-aloud progress.

Other than this edge case, the PR looks good. Once this is addressed, it will be merged.

chortya and others added 5 commits October 19, 2026 08:19
…aloud progress

Returning to the foreground, Readium can re-report the page the reader
showed before with slightly different progression, fragment or position
details, which counted as a move and overwrote the position background
read-aloud saved meanwhile. When the stored position is not the one this
reader wrote, the reader now goes to it and ignores visual reports that are
not nearer it than the old page until it arrives (at most 5 s).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only reports that look like the stale page are skipped (its resource when
read-aloud moved on to another one, or nearer the stale page than the
newer position). Explicit navigation ends the catch-up, and when the window
passes the navigator's current position is recorded unless it is still the
stale page. The regression tests inject reports while the catch-up is
demonstrably pending.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the reader

Tap-zone navigation is a deliberate move and ends the catch-up like the
other navigation paths. When the window passes, the navigator's current
position is recorded as is: any re-pagination replay is over by then, so
a swipe made meanwhile is not lost. The tests hold the catch-up
navigation behind a barrier, so the replayed report and the arrival are
delivered in a fixed order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A catch-up still pending when the reader is stopped again could time out
in the background and record the stale visual page over newer read-aloud
progress. Stopping ends it; the next start catches up afresh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The directional navigation adapter reports the start of a drag as a
deliberate move (without consuming it), so a swipe made while catching up
is recorded instead of waiting for the timeout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chortya

chortya commented Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

Thanks! Addressed in the new commits on this branch.

Fix. When the reader returns to the foreground and the stored position is no longer the one it wrote, background read-aloud has moved on. The reader then:

  • navigates to that newer position;
  • while catching up, skips visual reports that look like the stale page: the old resource when read-aloud moved to another one, or the same resource but nearer the old page than the newer position (re-pagination only nudges the old page's progression);
  • treats everything else as real and records it: the arrival, another chapter, tap-zone or volume page turns, swipes, TOC/progress navigation (each of these also ends the catch-up);
  • bounds the catch-up to 5 s and records the navigator's current position afterwards, so a move is never lost;
  • ends the catch-up if it is stopped again before then, so nothing is recorded while read-aloud owns the position.

Regression tests (EpubPositionRestoreInstrumentedTest). The catch-up navigation is held behind a test barrier, so report order is deterministic:

  • aRepaginatedReReportOfTheOldPageDoesNotReplaceNewerReadAloudProgress: your scenario. The old page is re-reported with a different progression, fragment and position while catching up; the newer read-aloud position is kept, and the reader then arrives there. Fails without the fix.
  • aRealMoveWhileCatchingUpIsStillRecorded: moving to another chapter during the catch-up is saved.
  • stoppingAgainWhileCatchingUpLeavesTheBackgroundPositionAlone
  • onlyReReportsOfTheStalePageAreIgnoredWhileCatchingUp: the classification rules.

EpubPositionRestoreInstrumentedTest passes 10/10 on a Pixel Fold API 36 emulator (repeated runs). Unit tests, lint and assemble are clean. The branch merges cleanly with current main.

@van-geaux

Copy link
Copy Markdown
Owner

Thanks @chortya, this looks good now. The remaining read-aloud position issue is covered, and the added tests address the edge cases we discussed.

No further changes needed from me. This is ready to merge.

@van-geaux
van-geaux merged commit f27a8c8 into van-geaux:main Oct 20, 2026
1 check passed
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.

2 participants