The card Kyle sees every day was a hand-built MediaStyle notification over a MediaSessionCompat, on `androidx.media`, deprecated since May. Android Auto needs a media3 session to browse, so the choice was to write the new thing against a deprecated library or to move the old thing first; this moves the old thing and adds no Auto. `MediaService` is a `MediaSessionService` and a new `SoulStreamPlayer` is the `SimpleBasePlayer` media3 asks for when the engine lives somewhere else — rodio, in Rust, behind a WebView round trip — so `getState()` is a pure read of the last `NowPlayingArgs` off the bridge, queue window and all, and every button routes back out through `MediaHub.act`. Three corrections to the plan, each the kind that only shows up on a phone. media3 **1.10.1**, not 1.11.0, whose POM drags kotlin-stdlib 2.2.10 into a project that compiles with Kotlin 1.9.25. The audio-focus and becoming-noisy blocks STAY hand-rolled: `setAudioAttributes(…, handleAudioFocus)` is an ExoPlayer convenience that lands in a `@ForOverride` method which throws, and deleting them would have lost "another app takes the output" and "headphones out" with nothing failing anywhere. And the service is BOUND, never started — `MediaSessionService.onStartCommand` returns `START_STICKY` without promoting for any intent that is not media3's own, so a `startForegroundService` would arm the ten-second kill timer and satisfy nothing, which is the 2026-09-13 bug in a new coat. Four more things are load-bearing and silent when they break: a playlist that must never be empty while a track exists, `COMMAND_GET_TIMELINE` with `COMMAND_GET_CURRENT_MEDIA_ITEM`, `STATE_READY` while paused, and both spellings of next and previous. Each ends in a card that is simply never posted, with nothing in any log. The window carries `queueBase` now, for a stable media-item uid as it slides, and `repeat`, whose numbering is swapped between the app and media3, so that what a controller is told about repeat is the truth and media3's own view of the end of the queue agrees with the `hasNext` the next button is advertised on. The 2026-09-13 bug has a second door, and `onStartCommand` is what shuts it. media3 hands the framework a media-button `PendingIntent` built with `PendingIntent.getForegroundService`, unconditionally, because the receiver the legacy stub resolves is now this service — there being no `MediaButtonReceiver` left for it to find. So the SYSTEM starts this service in the foreground on every media KEY, while media3 promotes only for a session it calls user-engaged. A key into a process built for that key, a key on a card paused past the ten-minute tail that does not start playback, any key with the bar cleared: none of them qualify, and each would take the app down ten seconds later. (The card's own buttons are milder — only play/pause, and only while paused, is a foreground start.) So `onStartCommand` promotes first, the way this service always did — through media3's own `onUpdateNotification` when there is a card to post, and otherwise with a plain notification of its own, posted and withdrawn in the same breath. `Service`'s `startForeground` is final, so the split is by what media3 can be asked to do, not by watching it. The walk gains `one-card` and `one-session` — the only checks that tell a landed migration from a doubled one — plus an advisory `media-plumbing` line; the logged window grew `base` and `rep`, which are the only readback either field has anywhere on the device and which `queue-window` now asserts; and the paused-restore wait goes to 25 s, which ANDROID.md had already claimed in one place and contradicted in another. ANDROID.md also says plainly what the walk cannot prove and Kyle has to try himself: headphones out, another app taking focus, the card's actual look, and a Bluetooth headset's own buttons. The Kotlin is unbuilt — no Kotlin compiler here — and was read against javap of media3 1.10.1 and the real sources of `SimpleBasePlayer`, `MediaSessionService`, `MediaNotificationManager` and `DefaultMediaNotificationProvider`; the manifest and resources compile and link under aapt2, and the Rust type-checks.
60 lines
2.6 KiB
Kotlin
60 lines
2.6 KiB
Kotlin
plugins {
|
|
id("com.android.library")
|
|
id("org.jetbrains.kotlin.android")
|
|
}
|
|
|
|
android {
|
|
namespace = "com.soulstream.mediasession"
|
|
compileSdk = 36
|
|
|
|
defaultConfig {
|
|
minSdk = 26
|
|
// the plugin's own R8 keep rules, handed to whatever app links this
|
|
// module — inert unless that app minifies, which only the release
|
|
// build type does
|
|
consumerProguardFiles("consumer-rules.pro")
|
|
}
|
|
|
|
compileOptions {
|
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
|
targetCompatibility = JavaVersion.VERSION_1_8
|
|
}
|
|
kotlinOptions {
|
|
jvmTarget = "1.8"
|
|
}
|
|
}
|
|
|
|
dependencies {
|
|
implementation("androidx.core:core-ktx:1.13.1")
|
|
// The session, the card, and a notification media3 posts and owns itself.
|
|
//
|
|
// 1.10.1 and NOT 1.11.0, which is newer (2026-08-05) and would break this
|
|
// build: media3-session-1.11.0's POM declares
|
|
// org.jetbrains.kotlin:kotlin-stdlib:2.2.10 at compile scope, this project
|
|
// compiles Kotlin with kotlin-gradle-plugin 1.9.25
|
|
// (src-tauri/gen/android/build.gradle.kts), and Gradle resolves to the
|
|
// HIGHEST version — so a 1.9 compiler would be handed Kotlin 2.2 metadata.
|
|
// Every media3 POM from 1.4.1 through 1.10.1 declares no kotlin-stdlib at
|
|
// all, and SimpleBasePlayer.java is byte for byte the same file at both
|
|
// tags, so nothing this plugin uses is newer than what is taken here.
|
|
//
|
|
// What rides along: media3-common at COMPILE scope, and with it
|
|
// com.google.guava:guava:33.3.1-android, also compile — that is where
|
|
// ListenableFuture, Futures and SettableFuture come from, which is why
|
|
// they are not named on a line of their own — plus
|
|
// androidx.annotation-experimental, which is androidx.annotation.OptIn.
|
|
// Then lifecycle-service 2.5.1, because MediaSessionService extends
|
|
// LifecycleService; media3-datasource, the default BitmapLoader that
|
|
// decodes the cover; and androidx.media:media:1.7.0 itself, still, at
|
|
// RUNTIME scope. The deprecated library has not left the APK and cannot —
|
|
// media3 uses its MediaBrowserServiceCompat. What changed is that none of
|
|
// OUR code compiles against it any more.
|
|
//
|
|
// media3 1.10.1's own minSdk is 23; this module's is 26. Guava adds about
|
|
// 2.8 MB of unminified classes to the debug APK, and minSdk 26 means
|
|
// native multidex, so there is no multiDexEnabled line to add.
|
|
implementation("androidx.media3:media3-session:1.10.1")
|
|
implementation("androidx.media3:media3-common:1.10.1")
|
|
implementation(project(":tauri-android"))
|
|
}
|