Repository navigation
feat(sideplay): add Spotify playback prototype - #45
manuel-rtsch wants to merge 46 commits into
Conversation
Spotify is coming as a second media provider, and nothing currently stops two players from holding a session and talking over each other. Add a neutral MediaSessionOwner (identity, notification slot, active flag, pause) plus a process-wide registry that pauses every other active owner when one starts. MusicPlayback implements it: it claims the session in publish() and releases it on pause/stop. MusicService takes its channel and notification id from the owner instead of private constants, so a second provider can register its own slot without touching the service. No track/queue types cross the boundary; the Music UI keeps MusicPlayer.
PrivateSettings already encrypts with KeystoreCipher, which lived in BankingRepository.kt, so every app that wants sealed storage was importing the banking app for it. Move the cipher into apps/integrations next to PrivateSettings, name the interface SecretCipher since it is not banking-specific, and leave banking a BookCipher typealias so its call sites don't change. The default key alias stays "sidekit_banking" so existing banking.sealed files still open.
System.loadLibrary ran from EmulatorCore's initialiser, and the emulator caught only Exceptions, so a build without that ABI died with an UnsatisfiedLinkError before any UI could say why. NativeLibraries loads once, caches success and failure, and returns false. The emulator checks it and shows "not available in this build". The Spotify bridge needs the same convention: arm64-only device builds and x86_64 emulator builds will each miss the other's library.
spotlessCheck ratchets from origin/main, so any file this branch changes must satisfy ktlint. These are formatter-only rewrites (trailing commas, brace placement, import order) in BankingRepository, PrivateSettings, SecretCipher, MusicPlayback and MusicService. Unrelated files the formatter also rewrites on this branch are left alone; they were already out of date before this work.
Spotify artwork is HTTPS, while Thumbnails only asks MediaProvider for a local content:// thumbnail, and the catalog has no Coil/Glide/OkHttp. RemoteArtwork does the smaller job: same 1/32-of-heap LRU as Thumbnails, a 16 MB disk cache keyed by url+size and trimmed oldest-first, two fetches at a time with timeouts and an 8 MB cap, cancellation that disconnects the socket when a row leaves composition, and a bounds/inSampleSize decode into an RGB_565 crop so a 104 px tile never holds a full cover. RemoteArtworkImage draws it into a box and shows a Sidekit icon until the image arrives or if it never does. Failures are not cached, so scrolling back retries.
- Read library, search, collections, artists and shows through librespot SpClient/Pathfinder instead of the rate-limited Web API. - Sync Now Playing with native playback events and refresh the native catalog token. - Fix remote-artwork OOM crashes and format podcast dates/playlist owners. - Document the vendored librespot-core patch, implementation status and remaining work.
Retry show and episode metadata once when Spotify's extended-metadata endpoint returns a transient error, and fall back to the context resolver for show details and episode order. Mark video episodes as non-playable. Show ten search results per section and let More reveal the next ten locally before fetching another page. The More row no longer reports how many items it will reveal.
- Move Modifier to the first optional parameter in SpotifyIconTile and EmptyMessage, updating call sites to named arguments. - Replace Uri.parse with the KTX String.toUri() extension.
…value The shared SidekitItem started rendering the link arrow even when a secondary value was present, which would silently add an arrow to existing rows such as Apps and Mail folders. Gate that behind showLinkArrowWithSecondary (default false) and opt in only from Sideplay's settings rows, which were designed with the trailing arrow.
|
Follow-up in 85f5fa1: the shared |
…ery track
After an hour or two idle, librespot logs 'Connection to server closed'
and marks its session invalid, but the Rust engine kept the Session handle
and accepted token updates. The gateway only reconnected on a token change,
so playback kept using the dead session: every track failed to fetch an
audio key, the decoder hit end-of-stream, and the player emitted Unavailable
for each item ('This track isn't available'), while HTTP catalog reads still
worked.
- engine::set_access_token now reports SESSION_INVALID when
Session::is_invalid(), so the Kotlin side can rebuild.
- NativeSpotifyGateway revalidates the session on every ensureConnected()
instead of returning early when the token is unchanged, and reconnects
when validation fails. Catalog session checks do the same.
- ensureConnected() reports whether it rebuilt the session; resume() reloads
the current item at its position because a rebuild discards the loaded
track.
Verified on SP01GE260600886: after closing the access point with airplane
mode, the next play logs 'Connecting to AP' / 'Authenticated as ...' and
loads the track normally.
A dead librespot session was already rebuilt on the next play, but if the access point had stalled, Session::shutdown could block behind a component mutex while the engine held its global STATE lock. That wedged every native call, so the play button hung with no error. - bound the native connect handshake (20s) and the Kotlin wrapper (25s) - take player/session out of STATE and release the lock before stopping the player or shutting the session down - run the old session's shutdown on a detached thread so reconnect is not blocked by a teardown that may never return
|
Fixed the "every track isn't available" failure after Sideplay sits idle. Root cause: when librespot's access-point connection or keep-alive dies, the session is marked invalid, but the engine kept reusing the stale Also found while testing: rebuilding the session called Changes:
Verified on device (SP01GE260600886, dev build): killed the access point with airplane mode for ~95 s, restored the network, then played. Log now shows a fresh
Note: this does not change the P0/P1 device checklist in the PR description; the session-liveness item still wants a >1 h token-rotation check. |
Home and Artist render Spotify items with the media id as the LazyColumn key. Older history files could contain the same id twice, and artist album pages can overlap because the native catalog chains albums_current with singles_current. Either case throws IllegalArgumentException during the draw pass. Deduplicate history on load, deduplicate artist albums and tracks on first load and when paging, and prefix the remaining raw-id keys with the row index so a future duplicate cannot crash the list.
Share the Item API with HarlenPostill#42 so the two branches merge cleanly.
Share the Item API with HarlenPostill#45 so the two branches merge cleanly, and leave the Sundial demo layout to HarlenPostill#44.
Enable librespot's bounded audio cache (1 GiB, LRU eviction) in the native session and point it at the app cache directory. Clearing the cache now drops both artwork and audio. Remove the Settings pill that advertised offline downloads as unavailable.
What changed?
This PR adds Sideplay, Sidekit's Spotify sub-app: sign-in, library browsing, search, playback, queue/history, lock-screen/notification transport, and a keypad-first UI drawn with Sidekit chrome.
Warning
This is an early prototype. Use with caution.
Sideplay is not production-ready. It uses a native librespot bridge, stores Spotify credentials on-device, and exercises playback, account, and notification paths that are still being hardened. Expect rough edges and possible breakage. Prefer a test Spotify account, and do not rely on it as your only playback client.
The branch is intentionally focused on Sideplay plus the shared seams it needs (media-session ownership, remote artwork, encrypted credential storage, and the native librespot bridge). It is based on the latest
upstream/mainat the time of opening.Included
docs/spotify/SIDEPLAY.md, including guardrails, known limitations, and the remaining roadmap.How to test
./gradlew :app:assembleDev.app/build/outputs/apk/dev/app-dev.apkon a Sidephone.Real-device testing
e6ddc88onintegration; this PR branch contains the same Sideplay changes rebased ontoupstream/main(916fe56).app/screenshots/sideplay_*.png.Screenshots
Rendered from the current
pr/sideplaytip (916fe56) with the Roborazzi screenshot tests (SideplayScreenshotTest, mock catalog data — no account or library data).Home — Now playing, recently played, and the two primary destinations.
Library — Album filter with explicit rows and paging.
Album — Collection view with play, save, and artist actions.
Search — Grouped results for tracks, albums, and playlists.
Now Playing — Artwork, seek, transport, shuffle/repeat, and save.
Queue — Current item, upcoming queue, and history.
Checklist
mainand this PR targetsmain.Build and automated test results
Run on
pr/sideplay:./gradlew :app:testDebugUnitTest spotlessCheck :app:assembleDev— pass./gradlew :app:testDebugUnitTest --tests 'hrln.interactive.sidekit.apps.spotify.*' spotlessCheck— passcargo test --manifest-path native/sidekit-spotify/Cargo.toml— 2 passedcargo fmt --manifest-path native/sidekit-spotify/Cargo.toml -- --check— passapp/build/outputs/apk/dev/app-dev.apk1.2.3-158-g1d296e0-dev(versionCode=251)Known issues and exceptions
librespot-corepatch are required..sidespot/is GPLv3 reference-only and is not committed or linked.MusicScreens.kt; the integration branch keeps Harlen's Now Playing layout.Maintainer review
This is a draft PR for early review. Please do not merge until the remaining P0 device checklist and P1 test-coverage items in
docs/spotify/SIDEPLAY.mdare complete.