- Model: Audiobook/AudiobookPart with a pure progress codec and speed ladder (0.75/1.0/1.25/1.5/2.0). - Scanner: a second MediaStore pass detects books by genre signal or numbered-chapter sibling directories; grouping heuristics are pure and unit-tested; the music scan is unchanged. - Room v7 (MIGRATION_6_7): audiobooks + audiobook_parts tables, DAO, AudiobookRepository with group-keyed rebuild that preserves resume and one bookmark per book. - UI: 'audiobooks' as the sixth music crossbar pivot (canon §3.1 home menu untouched; GemLibraryAudiobookPartScene parity), book list, book detail with resume CTA, parts list, bookmark jump, compact player with speed steps. - PlaybackController: PlaybackSource.AUDIOBOOK, playAudiobook(startPositionMs), setSpeed, speed/currentAudiobookId flows, reset on non-book queues; resume persisted from controller state. - Canon §3.3/§10 and roadmap D4 updated. 30 new tests; suite 1,551/0.
22 KiB
Dorado-HD Parity Roadmap
Planning artifact for the next parity passes. Mirrors the structure of the
sibling dorado docs/parity/
corpus. Every gap below is backed by a code location so the roadmap stays
falsifiable.
Ground rules (AGENTS.md): the canon is law — change
docs/zune-hd-ui-canon.md first with a source citation, then the code.
DesignInvariantTest audits zero corner radius, no Material chrome leakage,
hearts-not-stars, and the motion rules. Features the Zune HD never shipped
must be labelled post-device extension (canon §10), never canon.
1. Verified baseline
A code-verified snapshot (not README claims):
| Surface | State | Anchor |
|---|---|---|
| Home (9 entries, kinetic, edge-crop) | ✅ | ui/screens/HomeScreens.kt, design/components/KineticList.kt |
| Quickplay (3D parallax, Smart DJ lane) | ✅ | ui/screens/HomeScreens.kt |
| Crossbar pivots (music/artist/marketplace/videos/pictures/podcasts/radio) | ✅ | design/components/CrossbarBar.kt |
| Now Playing (scrub, transport overlay, swipe-skip, screensaver) | ✅ | ui/screens/NowPlayingScreen.kt |
| Tri-state heart rating | ✅ | media/PlaybackController.kt (setRating) |
| Lock/wake shade (WallpaperManager) | ✅ | ui/DoradoRoot.kt:115, design/components/LockShade.kt |
| 9 media pivots + Internet + Social(mock) | ✅ | ui/nav/DoradoNav.kt:14-37 |
| 29 mini-apps (12 util + 9 games + 8 mocks) | ✅ | ui/apps/DoradoApps.kt |
| Frozen 62-entry marketplace catalog | ✅ | data/official/OfficialCatalog.kt |
| Design-invariant scanner | ✅ | app/src/test/.../DesignInvariantTest.kt |
2. Parity axes
- On-device parity — every behavior the Zune HD itself had (widget, lock screen, the M4–M7 wishlist).
- Cross-project parity — interop with the sister desktop
dorado(Wi-Fi sync, and eventually physical Zune HD over USB). - Modern parity — what great Zune-successor projects solved that the 2009 device could not (dynamic mixes, on-device recommendation, scrobbling, lyrics).
3. Verified gap inventory
| Gap | Evidence (current code) | Axis | Effort |
|---|---|---|---|
| Home-screen widget | ✅ Glance NowPlayingWidget + appwidget receiver |
device | ✅ done |
| Wi-Fi sync with Dorado | ✅ mDNS discovery + paired LAN session + Device view | cross-project | ✅ done |
| USB MTP/MTPZ to a physical Zune HD | stretch; spec is in dorado/.agents/skills/zune-hardware-sync (MTPZ handshake, F-marker ZMDB, PPP interceptor 0x922C/0x922D, host 192.168.55.100 / device .101) |
cross-project | XL |
| Long-press → pin incomplete | was onLongClick = {} across DetailScreens/MediaScreens/MusicScreen/Podcasts/MarketplaceScreens; now pin + shared trackMenuActions |
device (M4) | ✅ done |
| Play-next / Quicklist action | was absent; now trackMenuActions adds play-next to every track row |
device (M4) | ✅ done |
| Marketplace curated pivots | was EmptyPivot("videos","coming soon"); now library-backed videos/podcasts |
device (M4) | ✅ done |
| Games pivot in Marketplace | was absent; now a 5th crossbar pivot over installed + frozen catalog games | device (M4) | ✅ done |
| Share / Zune-Card artwork export | no ACTION_SEND anywhere |
device (M5) | S |
| EQ presets / crossfade | 0 matches; PlaybackController has shuffle/repeat/rating only |
modern (M6) | M |
| Related-artists pivot | ✅ ranks by on-device audio similarity (M9.1); shows an empty-state only when no comparable tracks | modern | ✅ done |
| Tag editor | Scrobbling + lyrics shipped (M9.4: ScrobbleStore, LrcLibService); only an on-device tag editor remains |
modern | M |
| Live-radio pause-and-cache | Radio.kt dial/presets only |
device (M6) | M |
| Canon drift | §3.1 still says only music/settings functional; §5 still calls the in-app shade a stretch goal |
doc | XS |
4. Milestones
M4 — Mini-app + marketplace completeness ✅ complete
- Wire
onLongClick → QuickplayRepository.pin(...)at every empty list site (Videos, Pictures, podcast feeds + episodes, marketplace albums + installed apps, artist → songs, playlist detail, genre). Picture buckets and buttons had no meaningful action, so their deadonLongClickwas removed instead. - Add play-next to the long-press context menu (the Quicklist
replacement); back it with
PlaybackController.enqueue(PlaybackController.kt:132). - Replace
EmptyPivot("coming soon")in Marketplace with curated slices of the user's library. - Add a
gamespivot usingOfficialCatalog.GAMES. - Accept: no empty
onLongClick = {}remains underui/screens/(mini-app internals are out of scope); track rows use the sharedtrackMenuActions(pin + play-next); the PinKind coverage test names every Quickplay surface.
M5 — Social + discovery
- ✅ Now Playing system share sheet (
ACTION_SEND, artist + title + album). - Zune-Card-style artwork export (PNG/JPEG).
- ✅ On-device podcast search: local subscribed-feed filter plus the
dorado-cloud podcast directory when enabled (
PodcastSearch.kt,CloudPodcastDirectory.kt; graceful local-only labelling). - Wire Smart DJ as a tap-to-build-mix action in the music crossbar.
M6 — Community wishlist
- ✅ EQ presets (rock / acoustic / hip-hop / pop / classical / electronic).
- ✅ Crossfade on
play()— settings ▸ playback cycles off/2/4/6/8 s; the implementation is a fade-through (Media3 cannot overlap items in one player): tail fade-out, next-item fade-in. Pure ramp math inmedia/FadeRamp.ktwith unit tests. - Live-radio pause-and-cache ("Live / Rewind ±5 min") — capability-gated on a real-time streaming source.
- ✅ Lock-screen Now Playing art + controls:
DoradoMediaNotificationProvider(public low-importance channel, compact transport from the session command layout, album art via a caching session bitmap loader, content intent to the app). Heart-on-lock-screen stays N/A — it would need service-side DB access. - Soft keyboard "bulge around the finger" affordance (Wikipedia-cited).
M7 — Design-token + invariant-test hygiene
- Tokenize remaining raw dp sizes.
- Unit tests for
KineticListA–Z popup,HomePagesparallax math,StaggerEntranceclamp. - Keep
DesignInvariantTestcatching namedColor.Black/White/….
M8 — Device Link (Wi-Fi sync with Dorado) — complete (8.1–8.3 + 8.2b landed)
Mirror the desktop Phase 9 semantics on the phone side, 1:1 with
Dorado.Domain/Models/SyncModels.cs:
- ✅ M8.1 — pure-JVM core. New
sync/package (SyncModels.kt,SyncEngine.kt):SyncMode,SyncCategoryRule,SyncGroup,SyncPlan,TransferItem,DeviceSnapshotmatching the C# contract, plusbuildDefaultGroup()andbuildPlan().SyncEngineTestlocks the output against goldens generated from the real desktop engine (a throwaway C# harness referencingDorado.Application). - ✅ M8.2 — transport core.
DeviceTransportinterface +SimulatedDeviceTransport(mirrors the desktop simulator: 32 GB seed, system-partition accounting, category/title ordering) +SyncEngine.applyPlan(removals first, progress reporting), all unit-tested.SyncProtocolholds the JSON-RPC 2.0 contract constants and the 6-digit pairing-code generator. - ✅ M8.2b — live transport. Discovery via mDNS (
NsdLanSyncDiscovery,_dorado-sync._tcp), a plain-TCP JSON-RPC session (TcpSyncConnector+LanSync) runningsync.hello→sync.pair, surfaced in the Device view ("find dorado on your network" / "connect by address"). The desktop advertises the service and hosts the socket. (TLS remains a hardening item; the desktop endpoint is plain TCP today.) - ✅ M8.3 — Device view.
DeviceScreen(Settings → device link): pairing state + 6-digit code, storage gas gauge, the four sync rules (tap to cycle presets), guest-session toggle, dry-run "what will sync" manifest, apply-to-device, and reverse copy-back → pending-imports queue. Backed byDeviceLinkRepository(source = phone library; target =SimulatedDeviceTransport). The four rule strings persist inDoradoSettings. - Accept: golden test —
buildPlanoutput equals the desktopSyncPlanfor identical inputs; JVM round-trip test;SyncRulePresets/mapping tests.
Cross-repo behavior (the goldens lock it in):
- ✅ Fixed upstream in
doradothis pass, mirrored here, both sides re-goldened:REMOVEnow carries the device item'sSizeBytes, so freed space backs later adds —totalRemoveBytesis meaningful and the desktop Device view's "projected free" is now correct (was always 0).NewestCountnow parses the first digit run anywhere —"Newest 25 Items"→ 25 (was null). The desktop suite is 189/189 green;SyncEngineTestis green against the new fixtures.
- ⚠️ Intentional desktop parity (do not change on one side only):
- The picture rule inherits the video rule's mode
(
BuildPlan_KeepsExisting_AndRemovesStaleasserts this). - A successful
KEEPcarries only title + size (no detail/source path).
- The picture rule inherits the video rule's mode
(
The golden generator is vendored at tools/sync_golden/ (C# console
referencing dorado/src/Dorado.Application); re-run it after any desktop
SyncEngine change.
M9 — Modern Listening (Rune-inspired) — complete
- ✅ M9.1 — audio feature + similarity core. Ported the sibling desktop
Phase 13a (
AudioFeatures,AudioFeatureExtractor,AudioAnalysisService, cosine ranking) intoanalysis/. The artist page'srelatedpivot now ranks by on-device audio similarity — the device genuinely sourced this from Last.fm/friends, so a real signal is the honest fix — with genre overlap as fallback. Pure JVM, unit-tested. - ✅ M9.2 — Dynamic Mix.
DynamicMix/DynamicMixService(mirrors the desktop): SimilarToTrack / SimilarToAlbum / SimilarToFavorites / PlaylistsIncludingArtist, plus TopPlayed (reads persisted play counts viaPlayCountStore; surfaced as a play top played mix row in Quickplay). Surfaced as start mix in every track's long-press menu and a play favorites mix row in Quickplay. - ✅ M9.3 — DSP + persistence.
FeatureMath(radix-2 FFT, RMS, zero-crossing rate, loudness, spectral centroid) + hybridAudioFeatureExtractor.fromPcm(DSP-refined energy/centroid/acousticness over the metadata prior);PcmDecoder(MediaExtractor/MediaCodec) +PcmFeatureAnalyzerglue; Roomtrack_features(DB v3 + migration) behindFeatureStore/RoomFeatureStore;AudioAnalysisServicepreloads on start and persists analyses. Trigger via Settings ▸ device ▸ analyze library. DSP math and the store are tested; the decoder is best-effort glue that falls back to the metadata prior. - ✅ M9.4 — scrobbling + lyrics. Last.fm
track.scrobblewith the documentedapi_sigsigning (LastFmSignature), a durable offline queue (Roomscrobble_queue, DB v4) behindScrobbleStore/RoomScrobbleStore, andScrobbleService(order-preserving flush, stops at the first failure).PlaybackControllerqueues a scrobble once a track passes the Last.fm threshold (half, or 4 min). Settings ▸ scrobbling holds the opt-in, the user-supplied API key/secret/session key, and a manual flush. Lyrics via LRCLIB (LrcLibService+ pureLrcLibParser) surfaced from Now Playing. Signing, queue semantics and parsing tested; the HTTP clients are glue. - ✅ M9.2b — play counts. Persisted per-track counts (
play_counts, DB v5 +MIGRATION_4_5,PlayCountStore/RoomPlayCountStore), recorded at the playback transition behind the same threshold as scrobbling, so Top Played is real. - All of the above are canon §10 post-device extensions.
M10 — Always-on surfaces — in progress (widget shipped)
- ✅ Glance widget (
widget/NowPlayingWidget): Zune-style Now Playing + square transport glyphs, driven by the shared Media3 session; receiver declared inAndroidManifest.xml. No Material chrome; the matte-black canvas and white/muted text honor the invariant's spirit. - ⬜ Richer lock-screen Now Playing art/controls.
- ✅ Optional sleep timer (post-device extension): settings ▸ playback arms
off/15/30/45/60/90 min; the countdown shows in the Now Playing status OSD and
playback fades out when it expires (
PlaybackController.tickSleepTimer).
M11 — Hardware stretch (capability-gated)
- USB-host MTP/MTPZ transport, F-marker ZMDB parser, PPP HTTP interceptor
per
zune-hardware-sync. Likely N-A; document rather than ship. Never bundle Microsoft binaries.
Parity closeout program (M12–M15, approved 2026-09-10)
Derived from the on-device parity audit
(zune-hd-parity-audit.md) and its machine-readable
register (zune-hd-parity-gaps.json). Goal: every
device scene/behaviour (74 Gem*Scene + HUD/service surfaces) is implemented or
explicitly documented (long-term / N-A) — no gap unplanned.
M12 — Quick wins + kinetics (first sprint)
| # | Task | Device evidence | HD anchor |
|---|---|---|---|
| A1 | Now Playing scrubber + elapsed/remaining | zhud_serv ProgressSlider/PlayPosition/Elapsed/Remaining |
PlaybackController.seekTo; NowPlayingScreen.kt |
| A2 | Queue surface (showlist) | GemQueueListScene |
enqueue / queue StateFlow |
| A3 | Library search UI | zcontent_serv content service |
unused LibraryRepository.search |
| A4 | Fling velocity cap + item snap | xuidll FUN_4184C310 (32.0), XuiTouchSnapToTarget@0x41833134 |
ZuneFling.kt, KineticList.kt |
| A5 | String-parity pass | Various Artist, " by ", noItems/Empty, Ffwd, Mute (canon §11) |
scanner / Common.kt / empty states |
| A6 | Font-import decision | Zegoe assets/fonts |
canon / DoradoTheme |
| B1 | Kinetic model re-derivation (dt-scaled, 33 ms floor, 62.5 Hz, clamp, rubber-band/axis hysteresis) | xuidll@0x41841D58/49F18/48C70/48FC4 |
DoradoMotion.kt, KineticList.kt, ZuneFling.kt |
M13 — Motion & HUD
- ✅ B2 Dimmer ladder:
DoradoTokens.IDLE_DIM_MS+ a Now Playing dim veil. - ✅ B3 Status OSD: battery percent + clock in the transport overlay (
StatusOsd). - 🟡 B4 Dedicated Radio / Picture Now Playing — the picture viewer gained pinch-zoom (B6); radio still reuses the unified Now Playing.
- ✅ B5 Artist photo grid — cloud artwork module → MusicBrainz release
groups → Cover Art Archive → cached wallpaper fallback, with a full-bleed
photo viewer (
ArtistPhotos.kt). - ✅ B6 Picture pinch-zoom / pan / double-tap (
PictureDetailScreen). - ✅ A4 remainder: item snap (
KineticList(snap = true)on Songs).
M14 — Playback & library depth
- ✅ C1 EQ presets:
analysis/EqPreset+media/EqualizerController(best-effort platformEqualizeron the session) + Settings selector. - ✅ C4 Sort-key normalization (
data/model/SortKeys) + tests. - ✅ C5 Real-FFT visualizer binding:
TeeAudioProcessorPCM tap in the playback service (TapRenderersFactory→VisualizerBus), log-spaced bands with perceptual scaling inanalysis/SpectrumBands.kt, per-frame polling in the Now Playing screensaver (falls back to the procedural animation before the first window). Permission-free; noRECORD_AUDIO.
M15 — Social / content / commerce
- ✅ D1 Share (
ACTION_SEND) from Now Playing. - ✅ D3 Marketplace discovery: catalog search + entry details over the frozen catalog.
- 🟡 D2 Social user card: a local Zune Card (tracks/hearts/plays) on the Social screen; the inbox still needs a backend (mock shell only).
- ✅ D4 Audiobooks:
AudiobookGrouping(genre/directory heuristics) → Room v7audiobooks/audiobook_parts+AudiobookRepository→ music crossbaraudiobookspivot (book list, parts, resume/bookmark, 0.75–2.0× speed). Entry-point as post-device extension; canon §3.3/§10 updated.
Long-term backlog (documented, not scheduled)
Recorded so nothing is unplanned; revisit after M15: PIN/screen-lock,
built-in playlists (BuiltIn-*), deep queue engine (capacity/history/
MediaItemPositioned events), crossfade, marketplace backend +
Passport/subscriptions.
N-A register (documented, never built)
DRM (CeDRM_Mgr_*/XDRM*), WMA/WMV + ZWmtStreamer, MTPZ/UPnP device identity,
TV-out selector, usage/telemetry reporting, ACS firmware update
(GemAcsFWScene), Wi-Fi config screen (Android-owned). Rationale in
zune-hd-parity-audit.md §11.
5. OSINT source map (inspiration, not code)
| Source | What to borrow | License posture |
|---|---|---|
| losses/rune | Dynamic Mix, on-device audio analysis → recommendations, scrobbling, lyrics, tag editor | MPL-2.0 → behavioral only, never copy |
| ZuneDev/Xune | Iris UI-library semantics (motion/typography cross-check) | reference only |
| zunes/Zune-Research | firmware / ZCP / OpenZDK ground truth for canon citations | docs reference |
| ZuneDev/ZuneNet | self-hostable social backend shape (beyond the mock) | reference only |
| zunes/ZuneDiscordRPC | now-playing presence idea | reference only |
| dumbie/ZuseMe | Last.fm scrobble pattern | reference only |
| cdtinney/spune | visualizer for the screensaver backdrop | reference only |
| whoozle/android-file-transfer-linux | MTP client reference for the stretch transport | reference only |
sibling dorado |
SyncModels.cs, SyncEngine, IDeviceTransport, Plugins.Protocol |
MIT — same project family |
6. Canon + doc changes (applied)
- ✅ Canon §3.1 — all nine home entries are functional; Sept-2009 source citation added.
- ✅ Canon §5 — the in-app shade is implemented (
WallpaperManager-backed;DoradoRoot.kt), citing ITPro Today. - ✅ Canon §10 — Post-device extensions — reclassified into shipped (Mix, DSP, scrobble, lyrics, play counts, widget, cloud update-check, Device Link) and pending (EQ, crossfade, live-radio cache, richer lock screen, sleep timer).
7. Verification gates (every milestone)
./gradlew assembleDebug test lintgreen (JDK 21 / AGP 9.4).DesignInvariantTest0 violations; noRoundedCornerShape, nospring(, no raw hex / namedColor.*inui/.- New sizes/colors/durations go through
DoradoTokens/LocalDoradoColors. - New pure-logic engines unit-tested (the games set this bar).
8. Next sprint (M12 — quick wins + kinetics)
- A1–A4 — Now Playing scrubber + elapsed/remaining; queue surface; library search UI; fling velocity cap + item snap.
- A5–A6 — string-parity pass (canon §11); font-import decision (implement or drop the claim).
- B1 — kinetic model re-derivation (dt-scaled, 62.5 Hz, clamp).
Then M13 (motion & HUD) → M14 (playback & library depth) → M15 (social/content/commerce). Corpus mining continues opportunistically.
9. Official-app reimplementation program (M16) — complete
Every one of the 62 official Zune HD marketplace packages now has a native,
launchable implementation in DoradoApps / OfficialCatalog (previously 21).
The program is documented-first: each package has a synthesized behavioral spec
with Assembly!Type.Method citations under apps/, and the machine
register is official-apps.json with the generated audit
official-apps-audit.md.
| Wave | Apps | Scope | Evidence |
|---|---|---|---|
| W1 | 12 | utilities/music fidelity upgrades (calculator expression machine, four-page alarm, calendar agenda/recurrence, piano/drum, quiz rounds) | *-LogicTest/*EngineTest |
| W2 | 9 | card/board rules + scored AI tiers (solitaire scoring, sudoku generator, hexic cluster rebuild, negascout checkers, opening-book chess, 6-seat poker) | *RulesTest, *AiTest |
| W3 | 12 | new casual/puzzle engines (flood fill, chain reaction, slider, shell game, flick physics, tile melt, platformer, …) | per-app *Test |
| W4 | 11 | new casual/word titles (tower defense, siege, hive harvester, cipher crossword, shmup, climbers, flick soccer, offline prediction) | per-app *Test |
| W5 | 5 | touch/toy/physics + the first engine3d title (3D picture puzzle, optics tracer, rigid-body solver, physics totems, paint) |
per-app *Test |
| W6 | 6 | big engines (echo-replay arena, 116-maze marble, AudioFeatures surf courses, bowling sim, 30 Hz racing with kudos/career, pool skating) | per-app *Test |
| W7 | 7+social | dead-service screen inventories rebuilt as pixel-faithful offline UIs | *ModelTest |
Gates: 1,086/1,086 unit tests; DesignInvariantTest 0 violations; the
OfficialAppsTest register⇔catalog⇔registry⇔spec consistency gate; corpus
sources stay external (tools/zune_archive_mirror.py,
tools/zune_app_corpus.py, tools/app_mine.py) and no Microsoft content is
committed.
Known deltas (documented per app): content is re-authored (fewer cars/tracks/levels than the shipping binaries), mid-level resume exists where an engine serializes state, and network/multiplayer surfaces are local-only because their backends are dead (canon §11 N-A register).
Corpus acquisition: the full Zune Archive (70 items, 51.4 GB) is mirrored
externally with per-file MD5 verification; the decompiled app tree verifies
7,843/7,844 files (the one miss is absent upstream). color-spill remains
corpus-blocked (its package shares a GUID with Reversi) and its implementation
uses provisional genre constants, marked in the spec.