Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
171 commits
Select commit Hold shift + click to select a range
315efbc
docs(android): design media-aware buffer sizing
RXWatcher Jul 27, 2026
6a64e17
docs(android): plan media-aware buffer sizing
RXWatcher Jul 27, 2026
1118f38
test(android): specify media-aware buffer bitrate selection
RXWatcher Jul 27, 2026
d48a717
fix(android): size playback buffer from media bitrate
RXWatcher Jul 27, 2026
6af50cc
fix(android): saturate overflowing buffer targets
RXWatcher Jul 27, 2026
681b12c
docs(android): correct buffer verification tasks
RXWatcher Jul 27, 2026
33b612c
Merge main into fix/android-buffer-sizing
RXWatcher Jul 27, 2026
e0917cb
Merge pull request #118 from Silo-Server/fix/android-buffer-sizing
RXWatcher Jul 27, 2026
508d377
docs: design Watch Together profile entry
RXWatcher Jul 27, 2026
9e143ca
docs: clarify Watch Together host authority
RXWatcher Jul 27, 2026
9bb5bba
docs: clarify Watch Together host continuity
RXWatcher Jul 27, 2026
2c15e2b
docs: plan Watch Together profile entry
RXWatcher Jul 27, 2026
7cdc2c5
docs: keep Requests above Watch Together
RXWatcher Jul 27, 2026
0d5fbdd
refactor: define Watch Together entry boundary
RXWatcher Jul 27, 2026
008a93d
feat: add phone Watch Together menu actions
RXWatcher Jul 27, 2026
b43f745
feat: add Watch Together to phone profile menus
RXWatcher Jul 27, 2026
ace50d5
fix: preserve phone room while browsing
RXWatcher Jul 27, 2026
13ae8bd
feat: add TV Watch Together menu actions
RXWatcher Jul 27, 2026
029f6ac
feat: add Watch Together to TV profile menu
RXWatcher Jul 27, 2026
5c7ed38
fix: preserve TV room while browsing
RXWatcher Jul 27, 2026
482ecaf
test: lock Watch Together menu boundaries
RXWatcher Jul 27, 2026
41bb217
test(tv): await structured subtitle cleanup deterministically
RXWatcher Jul 27, 2026
6d9dfc7
Merge pull request #123 from Silo-Server/fix/subtitle-cleanup-await
RXWatcher Jul 27, 2026
57a4ec8
Merge pull request #120 from Silo-Server/docs/watch-together-user-men…
RXWatcher Jul 27, 2026
60f4945
docs(tv): design navigation remediation
RXWatcher Jul 27, 2026
daf2933
docs(tv): plan navigation remediation
RXWatcher Jul 27, 2026
0ed7ba5
perf(android): hydrate inline home sections once
RXWatcher Jul 27, 2026
f0627c5
perf(tv): make marquee prefetch cache first
RXWatcher Jul 27, 2026
2060871
perf(tv): defer skyline work until focus settles
RXWatcher Jul 27, 2026
408fd36
fix(tv): enter for-you rows from filter controls
RXWatcher Jul 27, 2026
a69b5c0
fix(tv): close cold navigation review gaps
RXWatcher Jul 27, 2026
e7939f3
fix(tv): settle prefetch without caller races
RXWatcher Jul 27, 2026
e32ced5
docs(tv): define navigation follow-up behavior
RXWatcher Jul 27, 2026
4c411aa
docs(tv): plan navigation follow-up fixes
RXWatcher Jul 27, 2026
1a295a1
docs(tv): test navigation behavior at real boundaries
RXWatcher Jul 27, 2026
ccf2e37
fix(tv): stop held up at the first home row
RXWatcher Jul 27, 2026
2a21361
fix(tv): unify for-you saved-list routes
RXWatcher Jul 27, 2026
f08aec6
docs(tv): summarize navigation remediation
RXWatcher Jul 27, 2026
e3dec4d
fix(tv): focus applied for-you selection
RXWatcher Jul 27, 2026
4eef154
docs(tv): design active header focus and editorial hero
RXWatcher Jul 28, 2026
b1742c4
docs(tv): plan active header focus and editorial hero
RXWatcher Jul 28, 2026
2fc204c
fix(tv): restore active header focus from content
RXWatcher Jul 28, 2026
0482bd9
fix(tv): show editorial metadata in browse heroes
RXWatcher Jul 28, 2026
7098914
docs(android): extend editorial heroes to phone
RXWatcher Jul 28, 2026
41bbe4e
docs(android): extend hero plan to phone
RXWatcher Jul 28, 2026
59ad7f1
fix(android): align editorial browse hero metadata
RXWatcher Jul 28, 2026
9c25129
fix(tv): avoid home focus fallback on secondary routes
RXWatcher Jul 28, 2026
a28d10f
docs(android): report task 4 verification
RXWatcher Jul 28, 2026
6478528
fix(tv): return calendar controls up to active tab
RXWatcher Jul 28, 2026
ade712c
fix(shared): scope home requests to identity
RXWatcher Jul 28, 2026
d6f2c15
fix(tv): keep repeated up in content
RXWatcher Jul 28, 2026
e922af0
docs(android): design fixed library chrome inset
RXWatcher Jul 28, 2026
e156e76
docs(android): plan fixed library chrome inset
RXWatcher Jul 28, 2026
677a0e7
fix(android): keep library content below chrome
RXWatcher Jul 28, 2026
469f217
docs(android): trim library inset spec
RXWatcher Jul 28, 2026
b55f6a6
fix(tv): bound top menu focus retries
RXWatcher Jul 28, 2026
c9de4b7
docs(android): plan release audit fixes
RXWatcher Jul 28, 2026
c812e06
fix(shared): keep cache writes identity scoped
RXWatcher Jul 28, 2026
e4d4ae9
fix(android): keep library results and browse chrome current
RXWatcher Jul 28, 2026
1fbcda4
fix(shared): propagate cache write ownership
RXWatcher Jul 28, 2026
18fba48
fix(android): keep browse filters query scoped
RXWatcher Jul 28, 2026
1ae0e46
docs(android): extend cache ownership plan to home
RXWatcher Jul 28, 2026
e098749
fix(shared): keep home cache writes identity scoped
RXWatcher Jul 28, 2026
e1b4b6e
test(android): retain deferred library requests
RXWatcher Jul 28, 2026
3717c9d
docs(android): plan final release ledger fixes
RXWatcher Jul 28, 2026
e4b086e
fix(watch-together): reconcile rooms and surface lobby errors
RXWatcher Jul 28, 2026
14c06a6
fix(android): finalize audiobook sessions asynchronously
RXWatcher Jul 28, 2026
b02ea4f
fix(tv): preserve subtitle title-safe positions
RXWatcher Jul 28, 2026
fc13dfe
chore(shared): remove unused no-op auth plugin
RXWatcher Jul 28, 2026
576c2a6
fix(watch-together): wait for session attach echo
RXWatcher Jul 28, 2026
632a3f0
fix(android): clear failed download progress
RXWatcher Jul 28, 2026
ff8b648
fix(watch-together): bind attach echoes to connection epoch
RXWatcher Jul 28, 2026
f822d22
test(tv): keep subtitle cleanup scheduler single-threaded
RXWatcher Jul 27, 2026
a49170a
docs: specify PR 126 review remediation
RXWatcher Jul 28, 2026
3b57abb
docs: plan PR 126 review remediation
RXWatcher Jul 28, 2026
7f87291
docs: correct marquee review assertion
RXWatcher Jul 28, 2026
fac001c
docs: use available shared test task
RXWatcher Jul 28, 2026
cd43af9
fix(tv): close reviewed focus routing gaps
RXWatcher Jul 28, 2026
5d0d44b
test(tv): make focus wiring anchors fail closed
RXWatcher Jul 28, 2026
5a93c22
fix(android): bound hero metadata and marquee identity
RXWatcher Jul 28, 2026
3b757fd
refactor(watch-together): make delivery keys explicit
RXWatcher Jul 28, 2026
d916238
refactor(android): clarify reviewed request guards
RXWatcher Jul 28, 2026
98e5b7b
docs: resolve PR 126 review findings
RXWatcher Jul 28, 2026
4d18591
docs: remove remaining local paths
RXWatcher Jul 28, 2026
f2bba1c
docs: generalize tester artifact destinations
RXWatcher Jul 28, 2026
77c8711
fix(tv): close final PR 126 review findings
RXWatcher Jul 28, 2026
615ff87
test(android): fail closed room source anchors
RXWatcher Jul 28, 2026
21ab78b
test(tv): keep subtitle adoption on test scheduler
RXWatcher Jul 28, 2026
58a2485
test(android): await transient library request siblings
RXWatcher Jul 28, 2026
2fb5318
Merge pull request #126 from Silo-Server/fix/tv-for-you-cold-navigation
RXWatcher Jul 28, 2026
de95528
docs: design subtitle aspect recentering
RXWatcher Jul 28, 2026
9e40689
docs: plan subtitle aspect recentering
RXWatcher Jul 28, 2026
9857607
fix(subtitles): recenter canvas for fill modes
RXWatcher Jul 28, 2026
a0c5b55
fix(subtitles): reconcile canvas after aspect layout
RXWatcher Jul 28, 2026
8ee564d
test(subtitles): lock aspect recenter wiring
RXWatcher Jul 28, 2026
fb5fda1
fix(subtitles): reconcile cropped canvas after layout
RXWatcher Jul 28, 2026
061327b
test(subtitles): prove reconciliation coalescing
RXWatcher Jul 28, 2026
d6327b9
test(subtitles): harden aspect wiring contracts
RXWatcher Jul 28, 2026
8155220
docs(subtitles): specify Pixel aspect and sizing fixes
RXWatcher Jul 28, 2026
4763151
docs(subtitles): plan Pixel aspect and sizing fixes
RXWatcher Jul 28, 2026
c26d88c
fix(subtitles): scale phone caption presets
RXWatcher Jul 28, 2026
b655735
fix(subtitles): converge aspect bounds after layout
RXWatcher Jul 28, 2026
c21cd7e
fix(subtitles): stabilize phone fit presentation
RXWatcher Jul 28, 2026
74ca8b0
docs(subtitles): specify startup mount settlement
RXWatcher Jul 28, 2026
02006e8
docs(subtitles): plan stable startup restoration
RXWatcher Jul 28, 2026
075fa0a
fix(subtitles): await stable tracks on playback restore
RXWatcher Jul 28, 2026
d50183f
docs(player): specify system-managed phone brightness
RXWatcher Jul 28, 2026
28db814
docs(player): plan system-managed phone brightness
RXWatcher Jul 28, 2026
fa0e664
fix(player): leave phone brightness system-managed
RXWatcher Jul 28, 2026
e989ad5
docs(subtitles): align phone sizing plan
RXWatcher Jul 28, 2026
bfc6ab5
Merge pull request #127 from Silo-Server/fix/subtitle-aspect-recenter
RXWatcher Jul 28, 2026
9c1f860
feat(onboarding): add invite claim and server-driven feature tour (#125)
Quick104 Jul 28, 2026
98dafaf
fix(home): show metadata runtime in hero when playback duration is ab…
RXWatcher Jul 29, 2026
1bb3bb6
feat(tv): show Directed-by credit under the movie synopsis
RXWatcher Jul 29, 2026
b433fb0
docs: design phone director credit parity
RXWatcher Jul 29, 2026
d09a760
docs: plan phone director credit parity
RXWatcher Jul 29, 2026
5228320
refactor(detail): share movie director credit
RXWatcher Jul 29, 2026
e92113f
feat(phone): show movie director credit
RXWatcher Jul 29, 2026
d204861
test(home): cover hero runtime preference
RXWatcher Jul 29, 2026
def0194
test(downloads): gate purger second-pass assertion
RXWatcher Jul 29, 2026
add7078
test(downloads): always clean up purger observer
RXWatcher Jul 29, 2026
eb9d0e2
docs: clean design spec whitespace
RXWatcher Jul 29, 2026
1894dcf
fix(phone): align director credit styling
RXWatcher Jul 29, 2026
371ad9e
Merge pull request #129 from RXWatcher/feat/tv-detail-director-credit
RXWatcher Jul 29, 2026
7c05b4a
docs(tv): specify removal of starring overlay
RXWatcher Jul 29, 2026
4b1e6a4
docs(tv): plan removal of starring overlay
RXWatcher Jul 29, 2026
1f6deb9
fix(tv): remove duplicated hero starring overlay
RXWatcher Jul 29, 2026
b869b61
docs(tv): remove trailing whitespace
RXWatcher Jul 29, 2026
a08b118
docs(tv): clarify hero parity
RXWatcher Jul 29, 2026
3ab4385
test(tv): harden starring overlay regression
RXWatcher Jul 29, 2026
998a487
Merge pull request #130 from Silo-Server/fix/remove-tv-detail-starrin…
RXWatcher Jul 29, 2026
6fd0813
test(shared): stabilize profile-switch request fixture
RXWatcher Jul 29, 2026
1788c40
Merge pull request #131 from Silo-Server/fix/section-cache-profile-sw…
RXWatcher Jul 29, 2026
db82b43
docs: define specials-first season ordering
RXWatcher Jul 29, 2026
7fc01e5
docs: plan specials-first Android ordering
RXWatcher Jul 29, 2026
7fe9c2b
fix(catalog): place specials first in season order
RXWatcher Jul 29, 2026
8c8faa8
fix(android): keep regular season selected by default
RXWatcher Jul 29, 2026
4427655
fix(tv): preserve specials deep links
RXWatcher Jul 29, 2026
2d1e1e0
fix(android): keep specials labels and loads aligned
RXWatcher Jul 29, 2026
18e83c6
fix(tv): keep option dialogs inside viewport
RXWatcher Jul 29, 2026
a3cc647
docs: playback buffer architecture design
RXWatcher Jul 30, 2026
6bb557f
docs: playback buffer architecture implementation plan
RXWatcher Jul 30, 2026
11a24b1
fix(playback): bound the load-idle window so proxies stop dropping th…
RXWatcher Jul 30, 2026
f1596e0
feat(playback): fit buffer depth to the device memory budget
RXWatcher Jul 30, 2026
fcee34e
feat(playback): size the byte target from the affordable buffer depth
RXWatcher Jul 30, 2026
10ea8c0
refactor(playback): split buffer-sizing policy from the Media3 adapter
RXWatcher Jul 30, 2026
857ca86
fix(playback): heap-proportional budget, honest depth, and a compile-…
RXWatcher Jul 30, 2026
d7ff760
fix(playback): half-heap memory budget, low-RAM proportional floor, s…
RXWatcher Jul 30, 2026
42815ee
Revert the playback buffer architecture series
RXWatcher Jul 30, 2026
e116068
docs: playback buffer architecture design
RXWatcher Jul 30, 2026
c0bda8f
docs: playback buffer architecture implementation plan
RXWatcher Jul 30, 2026
bf1225c
fix(playback): bound the load-idle window so proxies stop dropping th…
RXWatcher Jul 30, 2026
9851af5
feat(playback): fit buffer depth to the device memory budget
RXWatcher Jul 30, 2026
79d690d
feat(playback): size the byte target from the affordable buffer depth
RXWatcher Jul 30, 2026
4996d26
refactor(playback): split buffer-sizing policy from the Media3 adapter
RXWatcher Jul 30, 2026
8711161
fix(playback): heap-proportional budget, honest depth, and a compile-…
RXWatcher Jul 30, 2026
6000128
fix(playback): half-heap memory budget, low-RAM proportional floor, s…
RXWatcher Jul 30, 2026
cab938f
fix(player): keep the memory budget authoritative when it is below th…
RXWatcher Jul 30, 2026
4bb5b2a
test(player): make the idle-window guard strict so it fails on a revert
RXWatcher Jul 30, 2026
3404f0f
docs(player): correct the MAX_LOAD_IDLE_MS derivation and the stale p…
RXWatcher Jul 30, 2026
2e19971
fix(settings): send language tags, not display names (#119)
Quick104 Jul 30, 2026
51d671a
fix(android): resolve settings end-to-end defects (#146)
Quick104 Jul 30, 2026
917c570
Merge pull request #134 from Silo-Server/fix/specials-first-season-order
RXWatcher Jul 30, 2026
68a5e80
Merge pull request #135 from Silo-Server/fix/tv-option-dialog-viewport
RXWatcher Jul 30, 2026
da1dddc
Merge pull request #147 from RXWatcher/feat/playback-buffers
RXWatcher Jul 30, 2026
83e23da
fix(test): share Room schemas across build variants
Quick104 Jul 30, 2026
ad3524b
feat(settings): adopt contract language catalogs (#154)
Quick104 Jul 30, 2026
349b347
fix(tv): keep the For You list selection, scroll, and a legible top b…
RXWatcher Jul 30, 2026
1ddbb7f
Merge Silo-Server/silo-android main (168 commits)
cursoragent Jul 31, 2026
519e135
fix(test): align season display order with specials-first sort
cursoragent Jul 31, 2026
4b06343
test(shared): cover onboarding and invitation helpers for Kover gate
cursoragent Jul 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ owner-driven cancellation remains silent.
### Credential boundary

The access token is still required to attempt the room socket but is no longer
placed in the request target. The existing same-origin Silo auth plugin adds
placed in the request target. The existing same-origin Prairie auth plugin adds
`Authorization`, `X-Profile-Id`, and `X-Profile-Token` headers. The server
currently still requires `room_token`, `profile_id`, and `profile_token` query
parameters; the client KDoc records that residual request-target exposure.
Expand Down Expand Up @@ -213,9 +213,9 @@ The exact-scope validation at connect start is not the final token read: the
auth plugin reads the scoped token again while building the request so it can
honor rotation. A credential scope can disappear between those reads. The
Watch Together connector now marks its pinned request with the internal,
opt-in `requireSiloAuth()` attribute. When that marked request's exact scoped
token is null or blank, `PrairieAuthPlugin` removes Silo headers and throws before
the engine runs. Unmarked public requests and `skipSiloAuth()` paths retain
opt-in `requirePrairieAuth()` attribute. When that marked request's exact scoped
token is null or blank, `PrairieAuthPlugin` removes Prairie headers and throws before
the engine runs. Unmarked public requests and `skipPrairieAuth()` paths retain
their existing behavior.

### Correction 2 TDD evidence
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# Task 4 Report: Android TV Focus and Phone/TV Hero Verification

Date: 2026-07-28
Worktree: `fix/tv-for-you-cold-navigation worktree`
Final local HEAD: `9c251293dc321385ea9be205a0732cc7b14b1251`

## Summary

Task 4 verification found one Important review issue in the previously approved Task 1 focus path. I fixed it locally in `9c251293` by preventing non-Search secondary routes from falling through to Home focus when `selectedRoot == null`. The repeat independent review approved the updated diff.

Automated tests, supply-chain checks, release builds, APK signing verification, and artifact packaging completed. Dedicated emulator smoke did not produce a fully clean pass, so I did not push or update PR #126.

## Local Commits Added

- `9c251293 fix(tv): avoid home focus fallback on secondary routes`

## Verification Commands

Focused TV regressions:

```bash
./gradlew :androidTvApp:testDebugUnitTest \
--tests "org.prairieserver.prairie.tv.ui.shell.TvTopMenuFocusRequestTest" \
--tests "org.prairieserver.prairie.tv.ui.shell.TvShellFocusStateTest" \
--tests "org.prairieserver.prairie.tv.ui.components.TvSkylineUpNavigationTest" \
--tests "org.prairieserver.prairie.tv.ui.components.TvFocusMarqueeModelTest" \
--rerun-tasks --max-workers=2 --no-daemon
```

Result: `BUILD SUCCESSFUL`, 68 tasks executed.

Focused phone regression:

```bash
./gradlew :androidApp:testDebugUnitTest \
--tests "org.prairieserver.prairie.android.ui.screens.home.FeaturedHeroMetadataTest" \
--rerun-tasks --max-workers=2 --no-daemon
```

Result: `BUILD SUCCESSFUL`, 80 tasks executed.

Supply-chain checks:

```bash
./scripts/test-check-build-supply-chain.sh
./scripts/check-build-supply-chain.sh
```

Result: both exited 0. Self-test output: `All supply-chain policy self-tests passed`.

Full release gate:

```bash
./gradlew \
:androidApp:testDebugUnitTest \
:androidTvApp:testDebugUnitTest \
:androidApp:assembleRelease \
:androidTvApp:assembleRelease \
-PallowDebugReleaseSigning=true \
--rerun-tasks --max-workers=2 --no-daemon
```

Result: `BUILD SUCCESSFUL in 6m 29s`, 313 tasks executed.

The test XML directory immediately after the full release gate only contained focused suites, so I reran the unfiltered unit-test tasks explicitly:

```bash
./gradlew :androidApp:testDebugUnitTest :androidTvApp:testDebugUnitTest \
--rerun-tasks --max-workers=2 --no-daemon
```

Result: `BUILD SUCCESSFUL in 1m 20s`, 104 tasks executed.

XML totals after explicit unfiltered rerun:

- `androidApp/build/test-results/testDebugUnitTest`: 85 suites, 477 tests, 0 skipped, 0 failures, 0 errors.
- `androidTvApp/build/test-results/testDebugUnitTest`: 88 suites, 646 tests, 0 skipped, 0 failures, 0 errors.

Diff hygiene:

```bash
git diff --check origin/main...HEAD
git status --short --branch
git log --oneline origin/main..HEAD
```

Result before writing this report: no whitespace errors; branch ahead of `origin/fix/tv-for-you-cold-navigation`.

## Independent Review

Initial review finding:

- Important: non-Search secondary routes could call `requestMenuFocus(null)` and fall through to Home in `TvTopMenuBar`.

Fix:

- Added `TvShellFocusState.requestMenuFocusIfAvailable`.
- Updated content-Up routing in `TvMainShell` to allow null targeting only for Search.
- Added regression coverage in `TvShellFocusStateTest`.

Repeat review result:

- No blocking findings. Approved.
- Residual risk noted by reviewer: the content-Up fix is covered by state/helper unit tests, not a full Compose focus integration test.

## Emulator Smoke

Physical device safety:

- ADB showed an unapproved physical TV device; I did not target it.
- TV smoke used the dedicated TV emulator.
- Phone smoke used the dedicated phone emulator.

TV smoke on final TV APK:

- Installed `androidTvApp/build/outputs/apk/release/androidTvApp-universal-release.apk`
on the dedicated TV emulator.
- Home: from content card, fresh Up focused the Home top pill. Pass.
- Movies library: from first content card, first Up left focus on the content card; second Up focused the Movies pill. Not a clean pass.
- For You: from content, fresh Up focused the For You control. Pass.
- Calendar: targeted Calendar and entered content; Up focused the in-page `Following` control rather than the top Calendar pill. Not a clean top-pill pass.
- Hold-Up from lower row: not cleanly completed after the caveats above.
- Search route focus: not completed after the caveats above.
- TV hero metadata: visual/UIAutomator dumps showed editorial hero metadata for focused content and no technical badges in the large hero; content cards still show technical badges outside the hero.

Phone smoke on final phone APK:

- Installed `androidApp/build/outputs/apk/release/androidApp-universal-release.apk`
on the dedicated phone emulator.
- Opened Libraries > Movies. The Movies library Recommended hero displayed editorial chips `2004`, `7.3`, `War`, `R`, with no generic `Movie` chip, and Play / More Info visible.
- The selected movie did not show a runtime chip in the UIAutomator dump, so the requested movie smoke is partial rather than a full pass.
- Episode carousel page smoke was not completed.
- Narrow viewport wrapping was not completed beyond the default `1080x2400` phone emulator viewport.

Smoke conclusion: not fully green. PR #126 was not pushed or updated.

## APK Artifacts

Built universal minified release APKs from local HEAD `9c251293` with `-PallowDebugReleaseSigning=true`.

Signing caveat: these are debug-signed release builds. They only upgrade installations signed with the same debug certificate.

Copied artifacts:

- `Prairie-Phone-Universal-0.3.11-FocusHeroFix-9c251293.apk`
- `Prairie-TV-Universal-0.3.11-TVFocusHeroFix-9c251293.apk`

SHA-256:

- Phone: `0c4c68d15d47c28de41ef1fc1db080ba266f899aa2c3dad663a20fdb5dd5ed50`
- TV: `9e135e3cdad20f1b000ae2ef55573ceb6070e1412718d4ebde47a1977e492a09`

`apksigner verify --verbose` on copied APKs:

- Phone: verifies; v2 scheme true; number of signers 1.
- TV: verifies; v2 scheme true; number of signers 1.

`apkanalyzer` status:

- `apkanalyzer` failed with `IllegalStateException: Cannot locate latest build tools`, even with `ANDROID_HOME` and `ANDROID_SDK_ROOT` set.
- Used SDK `aapt` as fallback for package/version/ABI metadata.

`aapt dump badging` metadata:

- Phone: `applicationId=org.prairieserver.prairie`, `versionCode=14`, `versionName=0.3.11`, native ABIs `arm64-v8a, armeabi-v7a, x86, x86_64`.
- TV: `applicationId=org.prairieserver.prairie`, `versionCode=15`, `versionName=0.3.11`, native ABIs `arm64-v8a, armeabi-v7a, x86, x86_64`.

## PR Status

No push performed.

No PR #126 description update performed because the required emulator smoke was not fully green.
3 changes: 2 additions & 1 deletion android-shared/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ kotlin {
android {
namespace = "org.prairieserver.prairie.common"
compileSdk = 36
sourceSets.getByName("debug").assets.srcDir("$projectDir/schemas")
// Room migration schemas are local-test fixtures shared by both build variants.
sourceSets.getByName("test").assets.srcDir("$projectDir/schemas")
defaultConfig {
minSdk = 24
// Gate for preferring FFmpeg audio decoders over platform decoders.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package org.prairieserver.prairie.common.data.repository

import org.prairieserver.prairie.common.data.db.PrairieDatabase
import org.prairieserver.prairie.common.data.db.SiloDatabase
import org.prairieserver.prairie.common.data.db.entity.CatalogCacheEntity
import org.prairieserver.prairie.model.catalog.CatalogResponse
import org.prairieserver.prairie.model.catalog.EpisodesResponse
Expand All @@ -9,7 +9,10 @@ import org.prairieserver.prairie.model.catalog.SeasonsResponse
import org.prairieserver.prairie.model.personal.UserLibrary
import org.prairieserver.prairie.model.section.ResolvedSection
import org.prairieserver.prairie.network.AuthScopeSnapshot
import org.prairieserver.prairie.network.DefaultIdentityTransitionBarrier
import org.prairieserver.prairie.network.IdentityTransitionBarrier
import org.prairieserver.prairie.repository.port.CatalogCachePort
import org.prairieserver.prairie.repository.port.CatalogCacheWriteLease
import kotlinx.serialization.json.Json

/**
Expand All @@ -19,53 +22,94 @@ import kotlinx.serialization.json.Json
* rather than crashing the browse screen.
*/
class RoomCatalogCacheRepository(
db: PrairieDatabase,
db: SiloDatabase,
private val snapshotProvider: suspend () -> AuthScopeSnapshot?,
private val identityTransitions: IdentityTransitionBarrier = DefaultIdentityTransitionBarrier(),
private val now: () -> Long = { System.currentTimeMillis() },
) : CatalogCachePort {

private val dao = db.catalogCacheDao()
private val json = Json { ignoreUnknownKeys = true }

override suspend fun cacheLibraries(libraries: List<UserLibrary>) =
put(KEY_LIBRARIES, json.encodeToString(libraries))
cacheLibraries(libraries, currentWriteLease())

override suspend fun cacheLibraries(libraries: List<UserLibrary>, lease: CatalogCacheWriteLease) =
put(KEY_LIBRARIES, json.encodeToString(libraries), lease)

override suspend fun getCachedLibraries(): List<UserLibrary>? =
get(KEY_LIBRARIES)?.let { runCatching { json.decodeFromString<List<UserLibrary>>(it) }.getOrNull() }

override suspend fun cacheDefaultLibraryPage(libraryId: Int, response: CatalogResponse) =
put(libraryKey(libraryId), json.encodeToString(response))
cacheDefaultLibraryPage(libraryId, response, currentWriteLease())

override suspend fun cacheDefaultLibraryPage(
libraryId: Int,
response: CatalogResponse,
lease: CatalogCacheWriteLease,
) = put(libraryKey(libraryId), json.encodeToString(response), lease)

override suspend fun getCachedDefaultLibraryPage(libraryId: Int): CatalogResponse? =
get(libraryKey(libraryId))?.let { runCatching { json.decodeFromString<CatalogResponse>(it) }.getOrNull() }

override suspend fun cacheLibrarySections(libraryId: Int, sections: List<ResolvedSection>) =
put(librarySectionsKey(libraryId), json.encodeToString(sections))
cacheLibrarySections(libraryId, sections, currentWriteLease())

override suspend fun cacheLibrarySections(
libraryId: Int,
sections: List<ResolvedSection>,
lease: CatalogCacheWriteLease,
) = put(librarySectionsKey(libraryId), json.encodeToString(sections), lease)

override suspend fun getCachedLibrarySections(libraryId: Int): List<ResolvedSection>? =
get(librarySectionsKey(libraryId))?.let { runCatching { json.decodeFromString<List<ResolvedSection>>(it) }.getOrNull() }

override suspend fun cacheItemDetail(contentId: String, detail: ItemDetail) =
put(itemDetailKey(contentId), json.encodeToString(detail))
cacheItemDetail(contentId, detail, currentWriteLease())

override suspend fun cacheItemDetail(
contentId: String,
detail: ItemDetail,
lease: CatalogCacheWriteLease,
) = put(itemDetailKey(contentId), json.encodeToString(detail), lease)

override suspend fun getCachedItemDetail(contentId: String): ItemDetail? =
get(itemDetailKey(contentId))?.let { runCatching { json.decodeFromString<ItemDetail>(it) }.getOrNull() }

override suspend fun cacheSeasons(seriesId: String, response: SeasonsResponse) =
put(seasonsKey(seriesId), json.encodeToString(response))
cacheSeasons(seriesId, response, currentWriteLease())

override suspend fun cacheSeasons(
seriesId: String,
response: SeasonsResponse,
lease: CatalogCacheWriteLease,
) = put(seasonsKey(seriesId), json.encodeToString(response), lease)

override suspend fun getCachedSeasons(seriesId: String): SeasonsResponse? =
get(seasonsKey(seriesId))?.let { runCatching { json.decodeFromString<SeasonsResponse>(it) }.getOrNull() }

override suspend fun cacheEpisodes(seriesId: String, seasonNumber: Int, response: EpisodesResponse) =
put(episodesKey(seriesId, seasonNumber), json.encodeToString(response))
cacheEpisodes(seriesId, seasonNumber, response, currentWriteLease())

override suspend fun cacheEpisodes(
seriesId: String,
seasonNumber: Int,
response: EpisodesResponse,
lease: CatalogCacheWriteLease,
) = put(episodesKey(seriesId, seasonNumber), json.encodeToString(response), lease)

override suspend fun getCachedEpisodes(seriesId: String, seasonNumber: Int): EpisodesResponse? =
get(episodesKey(seriesId, seasonNumber))?.let { runCatching { json.decodeFromString<EpisodesResponse>(it) }.getOrNull() }

private suspend fun put(cacheKey: String, jsonStr: String) {
private suspend fun put(
cacheKey: String,
jsonStr: String,
lease: CatalogCacheWriteLease,
) {
if (lease.identityGeneration != identityTransitions.generation.value) return
val snapshot = snapshotProvider() ?: return
val profileId = snapshot.profileId ?: return
if (lease.identityGeneration != identityTransitions.generation.value) return
// A Room row must fit SQLite's ~2MB CursorWindow or the *read* throws
// SQLiteBlobTooBigException. Big library pages can exceed it, so don't
// store an unreadable row — drop any prior row for this key and skip.
Expand All @@ -84,6 +128,9 @@ class RoomCatalogCacheRepository(
)
}

private fun currentWriteLease() =
CatalogCacheWriteLease(identityTransitions.generation.value)

private suspend fun get(cacheKey: String): String? {
val snapshot = snapshotProvider() ?: return null
val profileId = snapshot.profileId ?: return null
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package org.prairieserver.prairie.common.data.repository

import org.prairieserver.prairie.common.data.db.PrairieDatabase
import org.prairieserver.prairie.common.data.db.SiloDatabase
import org.prairieserver.prairie.common.data.db.entity.HomeCacheEntity
import org.prairieserver.prairie.model.section.ResolvedSection
import org.prairieserver.prairie.network.AuthScopeSnapshot
import org.prairieserver.prairie.network.DefaultIdentityTransitionBarrier
import org.prairieserver.prairie.network.IdentityTransitionBarrier
import org.prairieserver.prairie.repository.port.HomeCachePort
import org.prairieserver.prairie.repository.port.HomeCacheSnapshot
import org.prairieserver.prairie.repository.port.HomeCacheWriteLease
import kotlinx.serialization.json.Json

/**
Expand All @@ -17,17 +20,30 @@ import kotlinx.serialization.json.Json
* JSON decodes to null rather than crashing the home screen.
*/
class RoomHomeCacheRepository(
db: PrairieDatabase,
db: SiloDatabase,
private val snapshotProvider: suspend () -> AuthScopeSnapshot?,
private val identityTransitions: IdentityTransitionBarrier = DefaultIdentityTransitionBarrier(),
private val now: () -> Long = { System.currentTimeMillis() },
) : HomeCachePort {

private val dao = db.homeCacheDao()
private val json = Json { ignoreUnknownKeys = true }

override suspend fun cacheHome(sections: List<ResolvedSection>) {
cacheHome(
sections = sections,
lease = HomeCacheWriteLease(identityTransitions.generation.value),
)
}

override suspend fun cacheHome(
sections: List<ResolvedSection>,
lease: HomeCacheWriteLease,
) {
if (lease.identityGeneration != identityTransitions.generation.value) return
val snapshot = snapshotProvider() ?: return
val profileId = snapshot.profileId ?: return
if (lease.identityGeneration != identityTransitions.generation.value) return
val sectionsJson = json.encodeToString(sections)
// A Room row must fit SQLite's ~2MB CursorWindow or the *read* throws
// SQLiteBlobTooBigException. Large reorganized home layouts can exceed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ val playerInfraModule = module {
DefaultServerSettingsFlusher(
settingsApi = get(),
scope = CoroutineScope(SupervisorJob() + Dispatchers.IO),
// Same source the settings store stamps its ops with, so a
// retained retry can tell whether the server it was authored
// against is still the one requests would reach.
getServerUrl = { get<TokenManager>().getServerUrl() },
)
}

Expand Down
Loading
Loading