startForegroundService() requires startForeground() within ~5 seconds.
The async coroutine (DB queries, old job join) delayed the Media3
notification pipeline past this deadline. Now we:
- Create the media session in onCreate() instead of lazily
- Post a minimal "Connecting…" notification synchronously in
onStartCommand before launching the async play coroutine
- Media3 replaces it with the proper media notification once the
adapter state updates to Playing
Made-with: Cursor
- updateMetadata() now also updates _currentMediaItem so track info
and artwork flow through to the notification/lockscreen via getState()
- Remove else -> stopSelf() from onStartCommand to avoid killing the
service on internal MediaLibraryService intents
Made-with: Cursor
Covers architecture for replacing MediaSessionCompat with Media3
MediaLibraryService, RadioPlayerAdapter facade, browse tree for
Android Auto, and notification improvements.
Made-with: Cursor
Was scrolling an extra container-width of invisible distance after the
text had already left the screen, adding ~11s of dead time. Now scrolls
to -textWidthPx (text just off-screen) instead of -(textWidthPx +
containerWidthPx).
Made-with: Cursor
Text now fades in over 300ms when it reappears instead of popping in.
Restart pause shortened from 1.5s to 0.5s. Initial 1.5s delay kept.
Switched from infiniteRepeatable to Animatable + LaunchedEffect loop
for independent control over initial vs subsequent cycles.
Made-with: Cursor
Replaces BounceMarqueeText with TickerText that scrolls at a fixed
33 dp/s regardless of text length. Text scrolls left off-screen with
a container-width gap before looping. Initial 1.5s delay lets the
user read the beginning.
Made-with: Cursor
Captures the design for replacing BounceMarqueeText with a
constant-speed TickerText composable (33 dp/s, continuous leftward
scroll with gap looping).
Made-with: Cursor
AudioEngine.stop() only called Thread.interrupt(), which doesn't
interrupt blocking InputStream.read() on OkHttp streams. This caused
audio to continue after stop and blocked subsequent play attempts
(old job never completed). Now closes timedStream to force the
blocking read to fail.
Removed LowLatencySocketFactory (16KB receive buffer) which triggered
Icecast slow-client disconnection on burst-on-connect. Force HTTP/1.1
to avoid HTTP/2 negotiation issues with Icecast servers.
Also fixed: awaitEngine() SharedFlow collector coroutine leak, and
added MAX_CATCHUP_FRAMES safety cap to prevent infinite frame skipping.
Made-with: Cursor
The detectDragGesturesAfterLongPress modifier was consuming the
long-press event before combinedClickable.onLongClick could fire.
Replace combinedClickable with movement-tracking inside the drag
handler — small movement on release shows the context menu, large
movement commits the reorder. Move tap handling into Tab.onClick.
Made-with: Cursor
- Create QualityOverrideDialog with radio selection for preferred quality
- Add Quality menu item to station long-press (only for stations with streams)
- Add setQualityOverride, getStreamsForStation, getQualityOverrideForStation to ViewModel
- Add getStationIdsWithStreams to StationStreamDao for hasStreams lookup
Made-with: Cursor
- Add StationPreferenceDao to StreamResolver constructor
- Check station_preferences before station.qualityOverride / global prefs
- Mock stationPrefDao in StreamResolverTest, add precedence test
Made-with: Cursor
- Add StationPreference entity with FK to Station, unique index on stationId
- Add StationPreferenceDao with getByStationId, upsert, deleteByStationId
- Add station_preferences table via MIGRATION_4_5
Made-with: Cursor
- Add transition() - all state changes route through it
- Add ConnectionFailedException for connection failures
- Refactor startEngine to accept urls: List<String>, iterate with Connecting state
- Extract awaitEngine() for event collection
- Set Connecting (not Playing) initially; Playing only on AudioEngineEvent.Started
- Fix handlePlay finally block: handle all states (Paused vs else)
- Update reconnectLoop to resolve URLs inside loop
- Add Service import for STOP_FOREGROUND_REMOVE
Made-with: Cursor
The lintVitalAnalyzeRelease task fails with an IncompatibleClassChangeError
in NonNullableMutableLiveDataDetector, which is a known bug in the lint
library. Disabling this specific check unblocks release APK builds.
Made-with: Cursor
Build failures now display a colored error banner with the last 20
lines of Gradle output. Removed stderr suppression from clean_build
so Gradle errors are visible.
Made-with: Cursor
- Wrap SettingsScreen in Scaffold to fix invisible text in dark mode
- Fix notification showing raw ICY metadata instead of parsed track info
- Add proper white-on-transparent notification icon
- Create branded adaptive launcher icon (amber tower on BlueGray900)
- Add edge-to-edge support with proper window inset handling
- Add About section to Settings with version and app info
- Enable BuildConfig generation for version display
Made-with: Cursor
Redesign Now Playing screen with blurred album art background, dominant
color extraction, bounce marquee for long text, cross-fade artwork
transitions, icon-labeled timers, and stream quality badge (bitrate,
codec, SSL). Add StreamInfo propagation from connection through to UI.
Fix MediaCodec PTS spam by providing incrementing presentation timestamps.
Made-with: Cursor
Add tabbed playlist UI with SomaFM as a built-in library including live
listener counts, station hiding, and stream quality selection. Implement
settings panel with quality preferences, listening history, and playlist
import/export improvements. Includes DB migrations 1-4, SomaFM seed
data, stream resolver, and now-playing history logging.
Made-with: Cursor