Skip to content

Fix/dead player settings and bookmarks - #131

Closed
Hirobreak wants to merge 2 commits into
developfrom
fix/dead-player-settings-and-bookmarks
Closed

Hirobreak wants to merge 2 commits into
developfrom
fix/dead-player-settings-and-bookmarks

Conversation

@Hirobreak

Copy link
Copy Markdown
Collaborator

Purpose

Bring video playback back and make the player settings that were showing but doing nothing actually work.

  • Videos play as videos again. Since chapter context became the default, every video played as audio with no picture and no fullscreen button. Fixed, and two new settings mirror iOS: Continue Video
    Audio in Background
    (default on) and Picture in Picture (default off).
  • Three player settings now do what they say: Auto Sleep Timer re-arms your last timer when you press play, Global Speed Control off gives every book its own speed (synced across devices), and Progress
    Bar Seeking off hides the lock-screen scrubber.
  • Bookmarks sync both ways and can now be edited and deleted from the list.

Bugfix

The bookmark list could show a previous book's bookmarks after an edit, and an edited note didn't refresh until something else changed. Both fixed.

Related tasks

  • iOS parity with the feature/videoplayer branch and the existing iOS speed, sleep-timer and bookmark behavior.

Approach

  • The session player now passes the real video track through in every display mode, which is what the player screen keys off.
  • Per-book speed adds a speed column to library items (Room migration 10 → 11) that rides along with the existing metadata sync.
  • Picture in Picture uses the platform's own PiP window with rewind, play/pause and forward buttons; the "background audio" rule is tied to the whole app going to the background, not to a picker or browser
    covering the screen.
  • Bookmark pull uses the existing bookmarks endpoint (its response shape was wrong on Android) and merges without ever deleting local bookmarks.
  • Covered by new unit tests; build, tests and lint pass.

…ookmark edit/delete

  Auto Sleep Timer, Global Speed Control and Progress Bar Seeking were saved
  and shown but read by no playback code. All three now behave as on iOS:

  - Auto Sleep Timer re-arms the timer the user last set on every
    user-initiated play (never on the autoplay advance into the next book).
    The last value lives in a new  pref using the
    deep-link encoding (-1 off, -2 end of chapter, else seconds); an explicit
    Off is remembered, a natural expiry is not.
  - Global Speed Control off (the default) means per-book speed. Books gain a
    nullable  column (Room 10 -> 11), written on every speed change for
    the loaded book, sent as  on metadata updates and merged back from
    fetches (server null keeps local). A book without its own speed falls
    back to the last-used speed so nothing snaps to 1x on update.
  - Progress Bar Seeking off withholds the seek commands from OS controllers
    (notification/lock screen, Auto, Bluetooth) and re-sends them live when
    the toggle flips; the in-app controller is never gated.

  Bookmarks now sync both ways. GET /v1/library/bookmarks was declared with
  the wrong response shape and never called; it returns {bookmarks: [...]}.
  Opening the Bookmarks sheet pulls and merges the server list (match on
  whole seconds, server note wins, nothing local deleted, skipped while sync
  tasks are pending). The sheet gains a pencil to edit the note and an
  end-to-start swipe to delete after confirmation, both also exposed as
  accessibility actions.

  Also fixed while here: the sheet's bookmark collector was launched inside
  collectLatest but parented to the outer scope, so every book played kept
  a live collector and a stale one could overwrite the list with a previous
  book's bookmarks; note edits mutated the listed entity in place, which made
  the StateFlow drop Room's re-emission. Android Auto browse-play now goes
  through shouldRestartFromZero (behavior unchanged: a manual tap always
  restarts a finished book).
  Mirror the iOS feature/videoplayer settings under Player Controls → Video
  Playback:

  - Continue Video Audio in Background (default on). Off pauses a playing
    video when the whole app leaves the foreground (ProcessLifecycleOwner,
    so a picker or the Auth Tab covering the activity doesn't count).
  - Picture in Picture (default off, needs the first). Leaving the app from
    the player while a video plays shrinks it into a PiP window: auto-enter
    on Android 12+, onUserLeaveHint before; aspect ratio from the video,
    clamped to the platform range; rewind / play-pause / forward actions via
    a non-exported broadcast receiver. In PiP the player renders only the
    video (fullscreen layout, overlay controls hidden).

  Video detection moves to :core (VideoTracks) so PlaybackManager can expose
  hasVideo for the activity. Manifest: supportsPictureInPicture plus the
  configChanges PiP needs.
if (width <= 0 || height <= 0) return null
val ratio = width.toDouble() / height
val clamped = ratio.coerceIn(1 / MAX_RATIO, MAX_RATIO)
return if (clamped == ratio) width to height else (clamped * 10_000).roundToInt() to 10_000

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 WARN — Clamped tall ratio is still outside the range Android accepts, so the app can crash. 1 / 2.39 = 0.418410…; (0.418410 * 10_000).roundToInt() gives 4184, and 4184/10000 = 0.4184 is below the platform minimum (config_pictureInPictureMinAspectRatio ≈ 0.41841). setPictureInPictureParams / enterPictureInPictureMode then throw IllegalArgumentException("Aspect ratio is too extreme"). MainActivity.applyPictureInPictureParams and onUserLeaveHint only catch IllegalStateException, so this crashes the collector in lifecycleScope. It is rare (videos taller than 1:2.39), and OEMs can also configure narrower ranges.

Fix: round toward the inside of the range (ceil when clamping to the min, floor for the max), or clamp to 1 / MAX_RATIO + 1e-4. Also catch IllegalArgumentException next to IllegalStateException in both MainActivity call sites. Update the test that currently pins 4_184 to 10_000.

val toInsert = mutableListOf<BookmarkEntity>()
val toUpdate = mutableListOf<BookmarkEntity>()
val seenSeconds = mutableSetOf<Long>()
for (row in remote) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 WARN — Deleted bookmarks may come back. Deleting a bookmark doesn't remove it on the server: DeleteBookmarkProcessor sends setBookmark with "active" to false. The KDoc on BookmarksResponse says each row carries {title, key, note, time, active}, but SyncableBookmark doesn't map active and plan() inserts every row that has no local match. If GET /v1/library/bookmarks returns soft-deleted rows, a bookmark the user deleted is inserted again as a USER bookmark the next time the list opens (the queue-empty guard only protects the moment before the delete uploads).

Fix: add @SerializedName("active") val active: Boolean? = null to SyncableBookmark and skip active == false rows in plan(), with a test case for it. Or confirm the endpoint only returns active rows and say so in the KDoc.

if (!remoteSeekEnabled && !isInAppController(session, controller)) {
builder
.remove(Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM)
.remove(Player.COMMAND_SEEK_TO_MEDIA_ITEM)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 WARN — Progress Bar Seeking off removes more than the scrubber. Only COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM backs the lock-screen/notification scrubber (legacy ACTION_SEEK_TO). Removing COMMAND_SEEK_TO_MEDIA_ITEM as well also turns off Media3's ACTION_SKIP_TO_QUEUE_ITEM. So with the setting off, Android Auto (and any other queue-showing controller) can no longer jump to a chapter from the queue: each chapter is its own window in chapter-context mode. iOS's setting only toggles changePlaybackPositionCommand, the scrubber.

Fix: gate only COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM and leave COMMAND_SEEK_TO_MEDIA_ITEM available so chapter/queue navigation keeps working.

return BookmarkSync.pull(delegate, syncTaskRepository, item)
}

override suspend fun updateItemSpeed(uuid: String, speed: Double) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 WARN — New repository sync behavior has no tests. The test fakes only stub updateItemSpeed and syncBookmarksFromCloud. Nothing checks that:

  • updateItemSpeed writes the row and enqueues an update task only when subscribed, with the fresh speed in the payload;
  • syncBookmarksFromCloud no-ops when not subscribed and merges through the plain delegate, so no set_bookmark echoes are created;
  • BookmarkSync.pull skips when the sync queue is non-empty and bails when the book was deleted mid-request.

BookmarkSync.plan is well covered, but these gates are where regressions would silently duplicate tasks or overwrite local edits. Add a SyncingLibraryRepositoryTest case for each (the fake task repo already exists there).

<string name="player_bookmarks_title">Bookmarks</string>
<string name="player_add_bookmark">Create bookmark</string>
<string name="player_bookmarks_manual">Manual</string>
<string name="player_bookmark_edit_note">Edit note</string>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 INFO — The 7 new strings (player_bookmark_edit_note, player_bookmark_delete, player_bookmark_delete_title, player_settings_video_*) exist only in values/. All 10 values-* locales (which already translate neighbouring keys such as player_settings_use_chapter_context) will fall back to English for the new Video Playback section and the bookmark actions. Consider adding the translations or tracking them in a follow-up.

@github-actions

Copy link
Copy Markdown

🟡 Claude PR Review — WARN

Brings back video in chapter/whole-book modes by passing real tracks through BookTimelinePlayer. Adds video background-audio and Picture in Picture settings, per-book speed (Room 10→11 migration plus metadata sync), Auto Sleep Timer re-arm, remote seek gating for Progress Bar Seeking, and a two-way bookmark pull with edit/delete in the list. The migration is additive and nullable, the bookmarks flow now uses flatMapLatest (the stale-list bug is fixed), the PiP receiver is RECEIVER_NOT_EXPORTED and unregistered in onDestroy, and the :core additions stay free of Compose and :app. Main risks: (1) a crash when entering PiP for very tall videos, because the clamped ratio rounds just below the platform minimum and only IllegalStateException is caught; (2) the bookmark pull ignores the server's soft-delete active flag, so deleted bookmarks may come back; (3) with seeking off, removing COMMAND_SEEK_TO_MEDIA_ITEM also breaks Android Auto queue/chapter selection. The pull/speed-sync repository paths have no tests, and the 7 new strings have no translations.

Findings: 4 warn · 1 info

Model claude-opus-5-5 · run log · 5 new · 0 carried over · 0 resolved · advisory (a human should still review). Findings are de-duplicated across pushes; an earlier finding closes only when the verification pass judges it against the current code — fixed, no longer applicable, accepted by a maintainer, or a duplicate of a finding reported on this push.

@Hirobreak Hirobreak closed this Sep 30, 2026

This branch was successfully deployed

1 active deployment
reviewer — 23db2bb6 Deployed Sep 30, 2026 by Hirobreak via review #447
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