Media Bubbles (Android) — org.neiam.mediabubbles
A persistent, draggable media-control bubble drawn over other apps (e.g. a map during navigation). Recreates the deprecated “media controls in navigation” feature without depending on any particular player or map app.
Reads playback through the system MediaSessionManager, using the same
notification-listener mechanism as ../scropple-android. Kotlin · Jetpack Compose.
Build & run
export JAVA_HOME=~/.gradle/jdks/eclipse_adoptium-21-amd64-linux.2 # JDK 17–21; JDK 25 (system + Android Studio JBR) breaks lint
./gradlew :app:assembleDebug
./gradlew :app:installDebug
First-run setup
- Media access → enable the app under Notification access (unlocks media sessions; no notification content is read).
- Display over other apps → allow.
- (Android 13+, optional) Notifications → lets the silent “bubble active / Hide” notification appear.
- Flip Show bubble, or add the Media bubble Quick Settings tile.
Using the bubble
| Gesture | Action |
|---|---|
| Tap | Expand / collapse controls |
| Double-tap (collapsed, optional) | Play / pause |
| Drag | Move; snaps to the nearest left/right edge, position remembered |
| Long-press | Open the playing app |
| Drag to the bottom target | Dismiss (stays off until re-enabled; can be turned off in settings) |
Collapsed, the bubble shows album art with a progress ring and a pause glyph when paused. Expanded, it grows inward from the docked edge and shows title/artist plus prev / ±10 s / play-pause / next. Touches outside the bubble pass straight through to the app underneath.
Configuration
Size (40–120 dp), opacity, auto-collapse delay, and toggles for title, prev/next, seek ±10 s, the progress ring, double-tap play/pause, and hiding when no session exists. Settings apply live, and the settings screen includes a working preview.
Look & feel
Shared with the other neiam Android apps (bigtrippr, fieri-eternal), with
bigtrippr as the reference copy:
ui/theme/AppTheme.kt: the daisyUI palettes (Light, Dark, Her, After Dark, Forest, Sky, Clays, Stones, Blueprint) defined in OKLCh, with every Material 3 slot pinned so Material’s baseline purple never shows. System follows the OS between Light and Dark.Typography.kt: B612 throughout.Shapes.kt: the shared corner radii.ThemePicker.kt: the web picker’s list with swatches.Common.kt: anAppBarTitlein the accent color, B612 Bold.- System bars are painted in the palette’s base color, and the window starts on After Dark so launch doesn’t flash white.
The bubble follows the chosen palette too: base-100 body, primary play button and progress ring, base-content text.
Architecture
| Package | Role |
|---|---|
media |
FloatyListenerService (notification listener; also restores the bubble after boot), MediaSessionWatcher (picks the most relevant session), MediaRepository (state + transport controls) |
overlay |
OverlayService (foreground specialUse service that owns the TYPE_APPLICATION_OVERLAY window), DragFrameLayout (raw-coordinate drag), MediaBubble (Compose UI) |
data |
BubblePrefs — config + position as StateFlows |
tile |
BubbleTileService — Quick Settings toggle |
ui |
Settings screen, theme, permission helpers |
Session selection: the session that most recently started playing, else the most recently active one, else the system’s top-priority session.
Known gaps
- Not yet verified on a physical device.
- No manual source picker or per-app filter when several players are active.
- No auto-show tied to a specific app being in the foreground (would need Usage Access).
CI
.forgejo/workflows/android.yml + Dockerfile, ported from Scropple: on push to
master, builds the release APK in a container, signs it with the
ANDROID_KEYSTORE / ANDROID_KEYSTORE_PASS repo secrets (skips cleanly if unset),
and publishes it to the forge’s F-Droid repository. Use a new keystore for this
app; the signing cert can never change without forcing reinstalls.