-
v2026.06.05
Stablereleased this
2026-06-05 13:31:28 -04:00 | 103 commits to main since this releaseFirst cut of v2026.06.05 (per-day mutable CalVer). Unifies the Android client's fragmented offline-detection code into one signal-driven authority, fixes the sticky offline banner, and reworks the offline playlist surface. (PR #86.)
The problem
Connectivity was answered by three half-systems that didn't talk to each other:
ConnectivityObserver(device link),VersionCheckController(which had reachability bolted onto a version poll), andServerHealthController(a passive derive with no triggers). Real failures — a track fast-failing, a stream read dying — were dead-ended; they never fed back. Home read the raw device link while the banner read the tri-state, so the two halves of "offline" disagreed. And recovery was bound to the 5-minute/healthzpoll, so the banner stuck around long after the connection returned.The fix
One authority —
NetworkStatusControllerAbsorbs
VersionCheckController+ServerHealthController. Every consumer (banner, Home, Playlists, search,OfflineGatedDataSource,TrackRow,LocalServerHealth) now reads its singlestate;ConnectivityObserveris just its device-link input. A pure, unit-testedReachabilityMachineholds the state logic.Signal-driven, corroborated reachability
- Success is self-proving — a successful stream read / API 2xx snaps straight to
Healthy(no waiting for the next poll). - Failure is ambiguous — a track fast-fail triggers a
/healthzprobe to arbitrate: corroborated →ServerDown; probe-ok → stayHealthy. One bad transcode no longer reads as "server down." - Inputs feeding the controller: device-link transitions (link-return → immediate probe), an adaptive poll (5 min healthy / ~20 s degraded), an OkHttp interceptor + the audio data source + the playback-error reporter (
reportSuccess/reportFailure), and pull-to-refresh (recheck).
Sticky banner — fixed
Link-return and any successful op now flip to
Healthyin seconds. The banner observes the controller directly (the oldWhileSubscribedre-wrap is gone).New non-gating
UnstablestateA mild "Reconnecting…" banner during the arbitration window — it never gates playback or flips a screen into offline mode (preserves the anti-flicker intent of the
/healthzhysteresis). Plus a transient "Back online" confirmation on recovery.Offline playlist UX
PlaylistRef.fullyCachedvia a DAO LEFT-JOIN (cached_playlist_tracks×audio_cache_index).- Tiles that can't be played offline (
refreshable || !fullyCached) grey out but stay tappable (the detail screen shuffles whatever subset is cached). - The offline Playlists row leads with the Recently-played / Liked pools, orders fully-cached before greyed, and drops not-yet-generated placeholders.
- Home and the Playlists list now derive
offlinefrom the unified controller, so they react toServerDown(server down on Wi-Fi), not just the device link.
New JVM unit suites run in CI:
ReachabilityMachineTest,PlaylistsRepositoryCacheMergeTest,BuildPlaylistsRowTest.Verify on device
- Toggle airplane mode mid-playback → the banner appears, then clears within seconds on reconnect (not minutes).
- A single unplayable/404 track does not flip the app to offline.
adb logcat -s MinstrelshowsNetworkStatus -> Xtransitions; a brief blip readsUnstable("Reconnecting…"), not an instantServerDown.- Go offline → the Playlists row leads with Recently-played / Liked; un-cached or refreshable tiles are greyed but still open into their detail.
- On recovery, a brief "Back online" flash appears before the banner collapses.
Downloads
- Success is self-proving — a successful stream read / API 2xx snaps straight to