Skip to content

Radio 1.2.0: detached-audio playback fixes (BT controls, background streaming) + wakelock/UI polish - #2

Open
fenleon wants to merge 20 commits into
mrrobashcroft:mainfrom
fenleon:radio-1.2.0
Open

Radio 1.2.0: detached-audio playback fixes (BT controls, background streaming) + wakelock/UI polish#2
fenleon wants to merge 20 commits into
mrrobashcroft:mainfrom
fenleon:radio-1.2.0

Conversation

@fenleon

@fenleon fenleon commented Aug 19, 2026

Copy link
Copy Markdown

20 commits of development + LP3-verified polish on top of b2d8cab. Everything below is verified on a real Light Phone 3 and the lightos emulator.

1.2.0 — detached-audio playback fixes (the big one)

Playback now runs on the SDK's detached-audio model (capabilities = ["detached-audio"]): the player lives in an SDK-owned LightAudioService (MediaSessionService), and every tool instance is a controller of one shared session. Fixes the five remaining bugs:

  • BT icon now opens Bluetooth settings — via a transparent BluetoothSettingsActivity bridge launched from the tool's foreground activity (the old static LightMediaService path silently no-op'd).
  • BT play/pause actions the current station — media buttons route to the one shared session, not a stale leftover one.
  • stop → exit → play resumes the same station — no stale per-launch players holding old queues.
  • no more double playback — one player, one session, by construction.
  • stream no longer stops ~10s after screen-off / tool inactive — detached playback survives the tool losing foreground.

Also: LightActivity.onDestroy clears screen ViewModelStores so the detached handle never leaks across relaunches; LightBluetooth/LightVolume observation moved into LightAudioService; SHOUTcast/HLS MIME hints + ICY metadata preserved in the player.

Earlier: the wakelock leak

LightMediaService acquired a PARTIAL_WAKE_LOCK + high-perf wifi lock in onCreate() and held them until onDestroy() — a paused/stopped player kept the device awake indefinitely. On a real LP3 overnight audit this showed as 4h1m of wakelock for 24m24s of actual audio (~123 mAh wasted). Locks are now tied to the player's real state (held only while playing/buffering, released on pause/stop/end; the service stops itself when the player is released). The original background-suspension behavior is preserved — buffering still holds the lock.

UI (feedback-driven, LP3-verified)

  • Find Stations: one field that both searches Radio Browser and plays a direct stream URL; a separate "Search Results" page with back-to-edit; the embedded LightSDK keyboard (caps-start, no emoji/mic/enter) sitting at the native height with the search action nested below it (the passes/podcast pattern — additionalBottomHeight + nested bottomBar).
  • Now Playing: no top bar; station name + current track with 2-line clamp and pause-then-scroll marquee; play does nothing until a real station is selected.
  • Library: Favourites/Recent tabs with label-hugging underlines, flush-right delete, edge-to-edge scrollbar.
  • Volume panel: the native LP3 panel replica shows on every rocker press — media while playing, ringer otherwise — and the tool routes the rocker to media volume only while open (setVolumeControlStream). Panel is driven by VOLUME_CHANGED_ACTION (the SDK never forwards volume keys to the screen).
  • Bluetooth: bottom-right glyph shows connected state (BLUETOOTH_CONNECTED underline variant); the tap opens BT settings through the SDK's bridge activity.

SDK (vendored copy in this fork)

  • Detached audio ported from upstream (LightAudioService, DetachedSessionState, PendingPlayerCommands, DetachedAudioCapability); newPlayer(playback = Detached); the Gradle plugin translates capabilities = ["detached-audio"] into the FGS permissions, capability marker, and service declaration.
  • LightBluetooth / LightVolume — permission-free audio-framework observers (the tool plugin bans Context; no BLUETOOTH permission needed).
  • LightText.onTextLayout passthrough, LightBottomBar.topPadding (matches upstream's newer signature), LightEmbeddedLp3Keyboard ported to the current upstream version (additionalBottomHeight/bottomBar/overlay), BLUETOOTH_CONNECTED icon + drawable, setVolumeControlStream in LightActivity.
  • serverPackage default is now com.lightos (real device); emulator flip documented in the toml.

Release

  • versionCode 3 / versionName 1.2.0; R8-minified release APK 6.3 MB (was 83 MB debug) — the unused CameraX + ML Kit QR stack is excluded from the APK.
  • README rewritten; LP3-captured screenshots added.

fenleon added 20 commits August 19, 2026 00:09
…playing

The background service acquired a PARTIAL_WAKE_LOCK + high-perf wifi lock in
onCreate() and held them until onDestroy(), so a paused or stopped player
kept the device awake indefinitely (observed on the LP3: 4h1m wakelock for
24m24s of actual audio, ~123 mAh wasted).

Locks are now tied to the player's real state via a Player.Listener:
acquired only while playing or buffering, released on pause/stop/end, and
the service stops itself when the session detaches (player released).
Buffering still holds the lock, so the background-stream suspension fix
from d78107a is not regressed.
…p bars, track title marquee

- Search and direct-URL entry share one field: a URL plays immediately
  (host-derived name), anything else searches the Radio Browser directory;
  the separate AddStationUrlScreen is gone and the bottom bar drops the
  redundant ADD slot (search · library · bluetooth).
- Bluetooth: bottom-right glyph shows connection state —
  BLUETOOTH_CONNECTED (underline variant, new drawable) when a BT A2DP
  device is connected, BLUETOOTH otherwise; tap still opens BT settings.
  State comes from the new permission-free LightBluetooth SDK helper
  (AudioDeviceCallback + AudioManager.getDevices, no BLUETOOTH permission,
  started by LightMediaService so the tool never needs Context).
- Top bars: back arrows removed; centered titles act as the back
  affordance on sub-screens (native LightOS grammar), Home is title-only.
- Titles clamp at 2 lines; long station names and the current-track line
  switch to a pause-then-scroll marquee instead of clipping.
- Current track (song/artist) from player mediaMetadata (media3
  onMediaMetadataChanged), shown when it differs from the station name;
  LightText gains an onTextLayout passthrough for overflow detection.
The fork previously shipped with the emulator SDK server as its default;
on a real LP3 that package does not exist, so all server calls (BT
settings, media-session registration) silently failed. com.lightos is
LightOS's own SDK server on the device (verified hosting LightSdkService)
and implements the standard methods; the emulator flip is documented in
the toml comment.
…restored, SDK keyboard, volume panel

- Home: top bar removed entirely (tool button minimizes); the 'Radio' title
  is gone from the Home screen.
- Bluetooth: pressing it now opens the system Bluetooth settings directly
  via the SDK's LightMediaService (the platform server's OpenBluetoothSettings
  bridge isn't implemented on the LP3 — previously a silent no-op).
- Library and Find stations: back buttons restored in the top bars (the
  tap-the-title affordance wasn't discoverable); the Find stations top bar
  also drops its redundant search icon (the keyboard submits).
- Find stations: the text input now uses the embedded LightSDK keyboard
  (light-keyboard, same as the old URL screen) instead of the system soft
  keyboard; search/URL detection and results unchanged.
- Volume controls (from audiobooks): new permission-free LightVolume SDK
  helper (VOLUME_CHANGED_ACTION receiver + AudioManager reads, started by
  LightMediaService); a shared RadioBaseViewModel owns the panel state and
  shows it instantly on rocker presses before letting the key reach LightOS;
  the canonical VolumePanelOverlay (tools/volume-panel, synced via
  tools/sync-volume-panel) is hosted on every screen's root.
…arch keyboard

- Home: the 'Radio' top-bar title is back (title only, no back button).
- Library (and Find stations results): the scrollbar is flush to the screen
  edge — the 24dp side padding moved from the scroll view to its content.
- Find stations keyboard:
  - hides after submitting a search (tap the input row to bring it back,
    results then use the full height);
  - sits flush at the bottom (the bottom-bar search button is gone — the
    keyboard's return submits);
  - loads in caps mode (setCapsMode(true)) and drops the emoji row
    (KeyboardOptions with an empty emoji list) — matching the native
    podcast keyboard;
  - light-keyboard bumped v0.0.16 -> v0.0.18 (the version the real SDK
    uses; source-compatible).
…, no premature 'no results'

- Library: the active tab underline now hugs the label (was a wide 60% bar);
  the delete X is flush right (row padding moved onto the name column).
- Find stations:
  - 'No results found' only shows after a search has actually completed
    (new hasSearched flag) — no more false message while typing;
  - mic button removed from the keyboard (displayVoice = false);
  - search icon restored in the bottom bar, below the keyboard;
  - podcast-style two-view flow: a 'Find Stations' typing view (input text
    bumped to heading size) submits to a separate 'Search Results' page
    (back arrow, no query shown); back returns to editing the search;
  - title capitalised to 'Find Stations' (results page is 'Search Results').
…put; fix volume panel not showing

- Search keyboard: no enter key (displayReturn = false — submit via the
  bottom-bar search icon); input text dropped from heading to subheading
  (38sp -> 30sp); the bottom bar gained an optional topMarginUnits param
  and the search screen uses 0f so the keyboard sits tight against the bar
  (icon size unchanged).
- Volume panel fix (not showing on the LP3): the SDK's LightActivity routes
  KEYCODE_VOLUME_UP/DOWN to super.onKeyDown, so the key-based trigger in
  RadioBaseViewModel never fired. The panel is now driven by LightVolume's
  VOLUME_CHANGED_ACTION receiver — every real volume change (rocker or BT
  AVRCP) surfaces in the panel; onKeyDown stays as a fallback.
…rd podcast-style; fix panel flash

- Volume panel now tracks the ringer stream too: while audio plays the rocker
  adjusts media (Media panel); with nothing playing it adjusts the ringer
  (Ringer/Silent/Vibrate panel) — the panel shows whether or not music is
  playing, as long as the tool is foreground (LightVolume.State extended with
  ringerLevel/max/mode).
- Panel flash on return to Home fixed: only the visible screen's view model
  reacts to volume changes (screenVisible guard in RadioBaseViewModel, wired
  via super.onScreenShow in the three screen VMs + onScreenHide/onAppPause).
- Rename keyboard now matches the podcast-style keyboard (no mic, enter, or
  emoji keys — KeyboardOptions with empty emojis, displayReturn/Voice false);
  submit via SAVE.
…l flash

- LightActivity sets volume control stream to STREAM_MUSIC — while the
  radio tool is foreground the rocker always adjusts media volume, playing
  or not (Android would route it to the ringer when nothing plays).
- Startup flash fixed: LightVolume.State is now 'seeded' only after the
  first real read; the panel trigger ignores transitions from an unseeded
  state (the view-model collector can start before LightMediaService's
  observe() seeds the volumes — previously the default 0/0 -> real read
  looked like a change and flashed the panel on app load).
Copied from the podcast app's search screen (captured 2026-08-19, 1080x1240):

- Input text: subheading size at ~y300 (was right under the top bar) — a
  4.5-unit spacer above it, label removed (the podcast has none).
- Side padding 2 grid units (~80px), underline 2dp (~6px, full width) —
  measured from the podcast's 81px padding / 6px line.
- The bottom-bar search button is gone: the search icon now floats centered
  ~80px below the keyboard (5.75-unit area, icon 2 units), like the podcast.
- The keyboard keeps caps-start / no emoji / no mic / no enter.
…e passes

- The 'Search stations or enter a URL' hint is back, sitting just above the
  input (the podcast-style input position is kept).
- The bottom area now follows the passes add-code editor: the search icon
  sits in a bottom bar flush at the screen bottom with the keyboard resting
  directly on it (topMarginUnits = 0) — replacing the floating podcast icon.
…H bar)

Root-caused from LP3 captures: the library's keyboard wrapper renders
LP3_KEYBOARD_HEIGHT_DP + max(additionalBottomHeight, 36dp). The real SDK
(passes) passes additionalBottomHeight = 5 units and nests the bar inside
the keyboard's zone, which puts the keys ~55px lower than the fork's old
layout (keys + separate flush bar). Ported:

- LightEmbeddedLp3Keyboard now matches the real SDK: additionalBottomHeight,
  bottomBar, onOverlayDismissed, overlay params + 10dp top padding.
- LightBottomBar's topMarginUnits renamed to topPadding: Dp (real SDK
  signature).
- The search screen renders the keyboard with additionalBottomHeight =
  5 units and a bottomBar containing a centered 'SEARCH' text button
  (passes' 'SAVE' style) — the keyboard now sits at the passes/podcast
  height with the action nested below the keys.
The submit path flipped to the results view before search()'s own
length guard, so an empty query still landed on an empty Search Results
page. onSubmit now only navigates when the query is a URL or >= 2 chars.
The auto-scroll LaunchedEffect (present in the original URL screen) was
dropped during the search rewrite; long inputs scrolled the cursor
off-screen.
togglePlayback now only starts playback after a real station has been
loaded (playStation / restored last-played); the default 'No Station
Selected' state can't trigger a bogus connect.
- Release build: R8 + resource shrinking on (already configured); the
  SDK's unused CameraX + ML Kit QR stack is now excluded from the APK
  (83 MB debug -> 6.25 MB release; dex 4.5 MB, no native bloat).
- versionCode 2 / versionName 1.1.0 (lighttool.toml).
- README rewritten for the fork: features, architecture, build/install,
  screens, credits (was upstream light-sdk boilerplate).
- Release verified: apksigner (LightSDK dev key), aapt badging, sha256
  75dd70e85fd73e022dc2f61f1562cfd0d1b4639a34ea00c40c2cf567c037bec7,
  emulator smoke test (launch renders; R8 registry keep intact).
Home, Find Stations (keyboard up), Search Results, Library — captured on the
real Light Phone 3 (2026-08-19) alongside the upstream captures.
Move playback into the SDK's LightAudioService (detached-audio capability),
so the stream survives the tool losing foreground and every tool instance
shares one session. Fixes:
- BT icon now opens Bluetooth settings (transparent bridge activity)
- BT play/pause actions the current station (one shared session)
- stop -> exit -> play resumes the same station (no stale sessions)
- no double playback (one player, one session)
- stream no longer stops ~10s after screen-off / tool inactive

Also: LightActivity clears screen ViewModelStores on destroy so the
detached handle never leaks across relaunches; audio-demo kept compiling
against the new player API.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant