package dev.stave.cartemplate
import androidx.car.app.CarContext
import androidx.car.app.annotations.ExperimentalCarApi
import androidx.car.app.Screen
import androidx.car.app.Session
import androidx.car.app.media.MediaPlaybackManager
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
/**
* Car App Library session. One instance per projection connection.
*
* Lifecycle:
* onCreate → connect the [CarSessionMediaBridge] to PlaybackService,
* register the media playback token so the host can read
* playback state for MediaPlaybackTemplate.
* onDestroy → release the bridge.
*
* The start screen is [NowPlayingCarScreen] which uses
* [MediaPlaybackTemplate] — the host renders album art, metadata,
* and transport controls from the MediaSession automatically.
*/
@OptIn(ExperimentalCarApi::class)
class EarhornCarSession : Session() {
private lateinit var bridge: CarSessionMediaBridge
override fun onCreateScreen(intent: android.content.Intent): Screen {
bridge = CarSessionMediaBridge(carContext)
lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
android.util.Log.i("EarhornCar", "╠══ CarSession.onCreate — connecting bridge ══╣")
bridge.connect()
// Register the MediaSession platform token so the Auto host
// can read playback state (album art, track info, progress,
// controls) for MediaPlaybackTemplate. Without this call,
// the template renders empty.
// PlaybackService may not have built its session yet on a
// cold start. One retry after 2 s used to be all there was:
// a slow service start left the Auto template empty until
// the car reconnected (review N.91, the maintainer's order
// 2026-09-02). Retry every 2 s for up to 30 s, and stop when
// the session is destroyed.
val handler = android.os.Handler(android.os.Looper.getMainLooper())
var attempts = 0
lateinit var tryRegister: Runnable
tryRegister = Runnable {
if (!owner.lifecycle.currentState.isAtLeast(androidx.lifecycle.Lifecycle.State.CREATED)) return@Runnable
attempts++
val token = dev.stave.playback.PlaybackService.sessionCompatToken
if (token != null) {
android.util.Log.i("EarhornCar",
"╠══ Registering media playback token (attempt $attempts) ══╣")
runCatching {
val manager = carContext.getCarService(CarContext.MEDIA_PLAYBACK_SERVICE)
as MediaPlaybackManager
manager.registerMediaPlaybackToken(token)
}.onFailure {
android.util.Log.e("EarhornCar", "Failed to register playback token", it)
}
} else if (attempts < 15) {
android.util.Log.w("EarhornCar",
"Platform session token not yet available — retry $attempts/15 in 2 s")
handler.postDelayed(tryRegister, 2000)
} else {
android.util.Log.e("EarhornCar", "Platform session token still null after 30 s — giving up")
}
}
tryRegister.run()
}
override fun onDestroy(owner: LifecycleOwner) {
android.util.Log.i("EarhornCar", "╠══ CarSession.onDestroy — releasing bridge ══╣")
bridge.release()
}
})
return NowPlayingCarScreen(carContext, bridge)
}
}