EarhornCarSession.kt
82 lines · 4.0 KB
Changed in: 0.5.0
| 1 | package dev.stave.cartemplate |
| 2 | |
| 3 | import androidx.car.app.CarContext |
| 4 | import androidx.car.app.annotations.ExperimentalCarApi |
| 5 | import androidx.car.app.Screen |
| 6 | import androidx.car.app.Session |
| 7 | import androidx.car.app.media.MediaPlaybackManager |
| 8 | import androidx.lifecycle.DefaultLifecycleObserver |
| 9 | import androidx.lifecycle.LifecycleOwner |
| 10 | |
| 11 | /** |
| 12 | * Car App Library session. One instance per projection connection. |
| 13 | * |
| 14 | * Lifecycle: |
| 15 | * onCreate → connect the [CarSessionMediaBridge] to PlaybackService, |
| 16 | * register the media playback token so the host can read |
| 17 | * playback state for MediaPlaybackTemplate. |
| 18 | * onDestroy → release the bridge. |
| 19 | * |
| 20 | * The start screen is [NowPlayingCarScreen] which uses |
| 21 | * [MediaPlaybackTemplate] — the host renders album art, metadata, |
| 22 | * and transport controls from the MediaSession automatically. |
| 23 | */ |
| 24 | @OptIn(ExperimentalCarApi::class) |
| 25 | class EarhornCarSession : Session() { |
| 26 | |
| 27 | private lateinit var bridge: CarSessionMediaBridge |
| 28 | |
| 29 | override fun onCreateScreen(intent: android.content.Intent): Screen { |
| 30 | bridge = CarSessionMediaBridge(carContext) |
| 31 | |
| 32 | lifecycle.addObserver(object : DefaultLifecycleObserver { |
| 33 | override fun onCreate(owner: LifecycleOwner) { |
| 34 | android.util.Log.i("EarhornCar", "╠══ CarSession.onCreate — connecting bridge ══╣") |
| 35 | bridge.connect() |
| 36 | |
| 37 | // Register the MediaSession platform token so the Auto host |
| 38 | // can read playback state (album art, track info, progress, |
| 39 | // controls) for MediaPlaybackTemplate. Without this call, |
| 40 | // the template renders empty. |
| 41 | // PlaybackService may not have built its session yet on a |
| 42 | // cold start. One retry after 2 s used to be all there was: |
| 43 | // a slow service start left the Auto template empty until |
| 44 | // the car reconnected (review N.91, the maintainer's order |
| 45 | // 2026-09-02). Retry every 2 s for up to 30 s, and stop when |
| 46 | // the session is destroyed. |
| 47 | val handler = android.os.Handler(android.os.Looper.getMainLooper()) |
| 48 | var attempts = 0 |
| 49 | lateinit var tryRegister: Runnable |
| 50 | tryRegister = Runnable { |
| 51 | if (!owner.lifecycle.currentState.isAtLeast(androidx.lifecycle.Lifecycle.State.CREATED)) return@Runnable |
| 52 | attempts++ |
| 53 | val token = dev.stave.playback.PlaybackService.sessionCompatToken |
| 54 | if (token != null) { |
| 55 | android.util.Log.i("EarhornCar", |
| 56 | "╠══ Registering media playback token (attempt $attempts) ══╣") |
| 57 | runCatching { |
| 58 | val manager = carContext.getCarService(CarContext.MEDIA_PLAYBACK_SERVICE) |
| 59 | as MediaPlaybackManager |
| 60 | manager.registerMediaPlaybackToken(token) |
| 61 | }.onFailure { |
| 62 | android.util.Log.e("EarhornCar", "Failed to register playback token", it) |
| 63 | } |
| 64 | } else if (attempts < 15) { |
| 65 | android.util.Log.w("EarhornCar", |
| 66 | "Platform session token not yet available — retry $attempts/15 in 2 s") |
| 67 | handler.postDelayed(tryRegister, 2000) |
| 68 | } else { |
| 69 | android.util.Log.e("EarhornCar", "Platform session token still null after 30 s — giving up") |
| 70 | } |
| 71 | } |
| 72 | tryRegister.run() |
| 73 | } |
| 74 | override fun onDestroy(owner: LifecycleOwner) { |
| 75 | android.util.Log.i("EarhornCar", "╠══ CarSession.onDestroy — releasing bridge ══╣") |
| 76 | bridge.release() |
| 77 | } |
| 78 | }) |
| 79 | |
| 80 | return NowPlayingCarScreen(carContext, bridge) |
| 81 | } |
| 82 | } |


