Rebuilds the deleted WebView-shell Android app, now pointed at the kwaba-sync website's server-side TV mode instead of the desktop site. Key changes from the original: room selection via a new website /tv room-list route instead of a baked-in room ID, and the Menu-button-to-popup mapping is already confirmed working on real hardware ahead of any app code existing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LHgWjmEJXsJEi1EVAoTp4K
167 lines
10 KiB
Markdown
167 lines
10 KiB
Markdown
# KwabaTV Rebuild — Design
|
||
|
||
Date: 2026-09-16
|
||
Status: approved, pending implementation plan
|
||
|
||
## Background
|
||
|
||
The original `KwabaTV` Android app (package `de.ggb.kwabatv`) was a pure
|
||
WebView shell around the normal desktop Kwaba Sync site, mapping Fire TV
|
||
remote input to the site's existing desktop keyboard shortcuts via real
|
||
Android `KeyEvent`s (`webView.dispatchKeyEvent()`, not JS-simulated events —
|
||
Chromium requires a trusted user gesture for autoplay-unlock and
|
||
`requestFullscreen()`). Hardware testing on that app surfaced real layout/
|
||
focus-navigation problems (documented in `Claude outputs/kwaba-tv-mode-
|
||
handover.md`), which led to building a proper server-side TV mode directly
|
||
in the `kwaba-sync` website first (`/tv/:id`, a pinned 60/20/80 layout, a
|
||
menu popup, guarded shortcuts) rather than continuing to patch the app
|
||
against a desktop-shaped page. That work is done, deployed, and separately
|
||
specced in `kwaba-sync`'s own
|
||
`docs/superpowers/specs/2026-09-15-tv-mode-design.md`.
|
||
|
||
The original app's project folder was deleted. This spec covers rebuilding
|
||
it from scratch, now that it has a real TV-shaped page to point at instead
|
||
of the desktop site.
|
||
|
||
## Goal
|
||
|
||
A thin WebView-shell Android app for Fire TV / Android TV that loads
|
||
Kwaba Sync's TV-mode pages and forwards real remote-control key events to
|
||
them — reusing the desktop-shortcut-to-KeyEvent mechanism proven in the
|
||
original hardware test, retargeted at the new `/tv` routes and their
|
||
simplified key surface (individual per-action shortcuts mostly collapsed
|
||
into the one menu popup).
|
||
|
||
## Decisions made during this design pass
|
||
|
||
**New repo, new location.** `C:\Projekte\kwabatv`, its own git repo on the
|
||
same Gitea instance (`ssh://git@git.whodovoodoo.de:2222/GGB/KwabaTV.git`,
|
||
under the user's own account, not David's — this is a personal project,
|
||
distinct from contributing to `kwaba-sync`). Different tech stack (native
|
||
Kotlin/Android vs. the Node web repo), different release process (APK
|
||
sideload, no CONTRIBUTING.md deploy webhook), so it does not live inside
|
||
`kwaba-sync`.
|
||
|
||
**Room targeting: a new `/tv` room-list route, not a baked-in room ID.**
|
||
The obvious approach (bake a fixed room ID into `BuildConfig.WEBSITE_URL`,
|
||
matching the original app's fixed-URL design) was rejected: the user wants
|
||
to pick from multiple rooms via a DPAD-navigable list, not type a room ID
|
||
into a settings screen or rebuild the APK to change rooms. Rather than
|
||
build a native Android room-list screen (fetch `/api/rooms` directly,
|
||
render it with Android's own TV-focus system), the design extends the
|
||
website with one more server-side TV-mode route: `GET /tv`, a DPAD-
|
||
navigable room list in the same spirit as `/tv/:id` — arrow-key focus
|
||
between room cards, Enter/OK joins the focused one. This means:
|
||
- `BuildConfig.WEBSITE_URL` becomes `https://kwaba.whodovoodoo.pasteaf.de/tv`
|
||
— the room list, not a specific room. No per-build room configuration at
|
||
all.
|
||
- No native Android list/focus UI to write — the WebView shell handles
|
||
both screens identically, since both are server-rendered pages.
|
||
- Rejected alternative (native Android screen) would have meant real new
|
||
app-side UI code and a second navigation model (native list → WebView
|
||
room) instead of one consistent WebView-only flow.
|
||
|
||
**Reachable from inside a room too.** The existing TV-mode menu popup
|
||
(`#tvMenuOverlay` in `kwaba-sync`, built in the prior spec) gets a 7th
|
||
button, "Räume", linking to `/tv` — so leaving a room to pick another one
|
||
doesn't require the Back button's room-list navigation (below); it's also
|
||
reachable through the same menu used for Resync/Subtitles/Skip/Spoiler/
|
||
Mute/Fullscreen.
|
||
|
||
**Key mapping**, reusing the original's real-`KeyEvent` mechanism, retargeted:
|
||
|
||
| Remote input | Android event | Sent to WebView | Website behavior |
|
||
|---|---|---|---|
|
||
| DPAD OK/Center, Play/Pause | `KEYCODE_DPAD_CENTER` / `KEYCODE_MEDIA_PLAY_PAUSE` | `Space` | Play/Pause (and the one-time "Beitreten" tap) |
|
||
| DPAD Left/Right, tap | `KEYCODE_DPAD_LEFT/RIGHT`, `repeatCount==0` | `ArrowLeft`/`ArrowRight` | 5s seek |
|
||
| DPAD Left/Right, held | same keycodes, `repeatCount>0`, app-side throttled to ~1 event/200ms | `Shift+ArrowLeft/Right` | 30s seek. Android's own hardware key-repeat rate is faster than 200ms, so the app must throttle before forwarding — otherwise this floods the room's sync socket with seek commands broadcast to every viewer far faster than intended. |
|
||
| DPAD Up/Down, Prev/Next media keys | `KEYCODE_DPAD_UP/DOWN`, `KEYCODE_MEDIA_PREVIOUS/NEXT` | `P` / `N` | Prev/next title — deliberately not `ArrowUp/Down`: those are explicit no-ops under `body.tv` in the website (added specifically because volume is TV/AVR-controlled, not site-controlled) |
|
||
| Menu button | `KEYCODE_MENU` | `ContextMenu` | Opens the popup (Resync/Subtitles/Skip/Spoiler/Mute/Fullscreen/Räume). **Confirmed working**: the user tested a real Fire TV remote's Menu button against the deployed `/tv/:id` page (via a browser, ahead of this app existing) and the popup opened — the riskiest unknown in this design is already de-risked before any app code exists. |
|
||
| Back, in room + fullscreen | `KEYCODE_BACK` | `Escape` | Browser exits HTML5 fullscreen natively on Escape (standard browser/WebView behavior) — the page itself has no matching keydown case, which is fine, nothing else needs to happen. |
|
||
| Back, in room + not fullscreen | `KEYCODE_BACK` | *(not a key event)* | App navigates the WebView directly to `/tv` (room list) itself — native-side navigation, not something the website reacts to. Reuses fullscreen-state tracking the app needs anyway (see below) and the WebView's current-URL tracking to distinguish "in a room" from "on the room list". |
|
||
| Back, in room list | `KEYCODE_BACK` | *(consumed)* | Does nothing — blocks Android's default back-behavior, which would otherwise exit the app. |
|
||
| Home | `KEYCODE_HOME` | not intercepted | Native Fire TV home; app doesn't touch it. |
|
||
|
||
Fullscreen-state tracking (needed for the Back-button branch above) comes
|
||
from `WebChromeClient.onShowCustomView`/`onHideCustomView` — the standard
|
||
Android WebView hook fired when a page enters/exits HTML5 fullscreen via
|
||
`requestFullscreen()`. Any WebView that supports fullscreen video already
|
||
needs this implemented, so it's not new complexity, just something to
|
||
carry over correctly. "In a room vs. on the room list" comes from tracking
|
||
the WebView's currently-loaded URL path (`/tv` exactly vs. `/tv/<id>`) via
|
||
`onPageFinished`/`shouldOverrideUrlLoading`.
|
||
|
||
## Fixed decisions carried over unchanged from the original app
|
||
|
||
(From `Claude outputs/README.md` and the handover doc — still correct, not
|
||
revisited in this pass.)
|
||
|
||
- Package `de.ggb.kwabatv`, `compileSdk 34` / `minSdk 21` / `targetSdk 34`,
|
||
Gradle 8.7 + AGP 8.5.2 (the tested, working combination).
|
||
- `LEANBACK_LAUNCHER` manifest category for the Fire TV launcher entry.
|
||
- Standard `network_security_config.xml` (system trust anchors only) — the
|
||
domain runs Let's Encrypt, no certificate pinning needed.
|
||
- Reuse the existing branding assets already sitting in this session's
|
||
`Claude outputs/` folder (`ic_launcher_art.png` 512×512,
|
||
`banner_tv_art.png` 640×360) rather than regenerating them.
|
||
- Distribution stays APK sideload only — no store listing.
|
||
- Login is a one-time action: the WebView loads `/login` on first launch
|
||
if there's no session cookie yet; persistent cookie/DOM storage keeps
|
||
the session alive across app restarts. No native login screen.
|
||
- No chat, no mic/camera permission requests (`onPermissionRequest` →
|
||
`deny()`) — matches the website's own "no chat in TV mode" decision and
|
||
the fact that Fire TV Sticks typically have no camera anyway.
|
||
- Volume is not touched by the app; the website already forces its
|
||
internal volume to 100 in TV mode and the TV/AVR handles real volume.
|
||
|
||
## Explicitly out of scope for this pass
|
||
|
||
- The website's `/tv` room-list route and the popup's 7th "Räume" button
|
||
are **website changes**, made in the `kwaba-sync` repo, not this one —
|
||
tracked here only as a dependency this app needs, with its own
|
||
implementation plan on that side.
|
||
- Real further hardware quirks beyond the Menu-key mapping (already
|
||
confirmed working) — e.g. whether `KEYCODE_MEDIA_PREVIOUS/NEXT` are
|
||
actually sent by either of the user's two test remotes, exact fullscreen
|
||
visual behavior — are Task-5-style manual verification once the app is
|
||
built, not something to speculate about further in this spec.
|
||
- The two items already deferred in the website's own TV-mode spec (the
|
||
suspected buffer/sync feedback loop, and YouTube's native controls
|
||
showing despite `controls=0`) remain deferred; unrelated to the app.
|
||
|
||
## Anticipated file changes
|
||
|
||
**In `kwaba-sync`** (separate implementation plan on that side):
|
||
- `src/server.js` — new `GET /tv` route (room list)
|
||
- `public/` — new markup/CSS/JS for the room-list page (DPAD focus
|
||
navigation between cards), reusing existing branding/CSS tokens
|
||
- `public/room.html` + `public/js/room.js` — 7th "Räume" button in the
|
||
existing `#tvMenuOverlay`
|
||
|
||
**In this repo (`kwabatv`)**:
|
||
- Standard Android Studio project scaffold (`settings.gradle.kts`,
|
||
`build.gradle.kts`, Gradle wrapper pinned to 8.7)
|
||
- `app/src/main/java/de/ggb/kwabatv/MainActivity.kt` — WebView shell,
|
||
`dispatchKeyEvent` key-mapping table above, fullscreen tracking via
|
||
`WebChromeClient`, current-URL tracking for the Back-button branch
|
||
- `app/src/main/res/xml/network_security_config.xml`
|
||
- `app/src/main/AndroidManifest.xml` — `LEANBACK_LAUNCHER`, permissions
|
||
- `app/src/main/res/` — icon/banner drawables from the reused branding
|
||
assets
|
||
- `docs/superpowers/specs/` and `docs/superpowers/plans/` — this spec and
|
||
its implementation plan
|
||
|
||
## Testing approach
|
||
|
||
Real hardware is available this time — two devices (a TV remote and other
|
||
hardware with a Fire TV remote) — so verification is a direct build →
|
||
sideload → test loop rather than the browser-only approximation the
|
||
website work had to use. Per key-mapping row above:
|
||
- Confirmed already: Menu button → popup opens.
|
||
- To verify once the app exists: DPAD tap vs. hold seek timing/throttling,
|
||
Prev/Next media keys (not guaranteed to exist on every remote), Back-
|
||
button behavior in all three states (fullscreen / in-room / room-list),
|
||
Home behavior, fullscreen entry/exit visually, and cross-checking
|
||
behavior across both available remote/hardware combinations in case they
|
||
differ.
|