package dev.stave.cartemplate
import android.content.ComponentName
import android.content.Context
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken
import com.google.common.util.concurrent.MoreExecutors
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
/**
* Bridges the Car App Library [Session] lifecycle to the existing
* [dev.stave.playback.PlaybackService] MediaSession.
*
* The Car App Library does NOT handle audio playback — it only renders the
* car-projected UI. Playback continues to flow through PlaybackService's
* MediaSession exactly as it does for the standard Auto MediaLibraryService
* path. This bridge connects via [MediaController] so the cartemplate
* screens can:
* - Read currently-playing track metadata (title, artist, album, art)
* - Send transport commands (play, pause, next, previous, seek)
* - Set the playlist (replacing the queue with album-context tracks)
*
* No duplicate playback engine. No duplicate queue. The MediaController and
* the existing MediaSession share state — same way Auto's standard browse
* UI talks to PlaybackService today.
*
* Lifecycle: created when [EarhornCarSession] is constructed, released
* when the Session is destroyed. Connection is async; [isConnected]
* flips to true once the controller has handshaken with the service.
*
* Thread model: callbacks are invoked on the application main thread.
* Both reads of [currentItem] / [playbackState] and writes through the
* controller (play/pause/skip) must happen on the main thread.
*/
class CarSessionMediaBridge(private val context: Context) {
private var controller: MediaController? = null
private val _isConnected = MutableStateFlow(false)
val isConnected: StateFlow<Boolean> = _isConnected.asStateFlow()
private val _currentItem = MutableStateFlow<MediaItem?>(null)
/** The currently-playing MediaItem, or null when nothing is loaded.
* Updated whenever the underlying session reports a transition. */
val currentItem: StateFlow<MediaItem?> = _currentItem.asStateFlow()
private val _isPlaying = MutableStateFlow(false)
val isPlaying: StateFlow<Boolean> = _isPlaying.asStateFlow()
/** Listener that mirrors session state into the bridge's StateFlows.
* Re-registered each time the controller is built. */
private val playerListener = object : Player.Listener {
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
_currentItem.value = mediaItem
}
override fun onIsPlayingChanged(isPlaying: Boolean) {
_isPlaying.value = isPlaying
}
}
/** Build the MediaController and attach the listener. The future
* resolves on the main thread once connection completes; we mark
* [isConnected] = true and prime the StateFlows with the current
* session state so screens that recompose immediately see the
* current track instead of a transient null.
*
* Idempotent — a second connect() while a controller exists is a no-op. */
fun connect() {
if (controller != null) return
android.util.Log.i("EarhornCar", "Bridge.connect — building MediaController")
val token = SessionToken(
context,
ComponentName(context, dev.stave.playback.PlaybackService::class.java),
)
val future = MediaController.Builder(context, token).buildAsync()
future.addListener({
runCatching {
val c = future.get()
controller = c
c.addListener(playerListener)
_currentItem.value = c.currentMediaItem
_isPlaying.value = c.isPlaying
_isConnected.value = true
}
}, MoreExecutors.directExecutor())
}
/** Release the controller. Called from [EarhornCarSession.onDestroy].
* Safe to call multiple times. */
fun release() {
controller?.let { c ->
c.removeListener(playerListener)
c.release()
}
controller = null
_isConnected.value = false
}
// ── Transport commands. All are no-ops if not connected. ────────
fun play() { controller?.play() }
fun pause() { controller?.pause() }
fun next() { controller?.seekToNextMediaItem() }
fun previous() { controller?.seekToPreviousMediaItem() }
/** Replace the queue with [items] starting at [startIndex] and play.
* Used by AlbumDetailCarScreen when the user taps a track in the
* album listing — the whole album becomes the queue, not just the
* tapped track. */
fun playFromList(items: List<MediaItem>, startIndex: Int) {
val c = controller ?: return
c.setMediaItems(items, startIndex.coerceIn(0, items.lastIndex.coerceAtLeast(0)), 0L)
c.prepare()
c.play()
}
/** Snapshot of the current MediaItem for synchronous reads (e.g. the
* tap-on-album-art handler that needs to know which album to open). */
fun snapshotCurrentItem(): MediaItem? = controller?.currentMediaItem
}