EARHORNMUSIC UPnP · Android TV · Phones · Android Auto · GNU/Linux
Release 0.5.0

EarhornCarSession.kt

82 lines · 4.0 KB

Changed in: 0.5.0

1package dev.stave.cartemplate
2
3import androidx.car.app.CarContext
4import androidx.car.app.annotations.ExperimentalCarApi
5import androidx.car.app.Screen
6import androidx.car.app.Session
7import androidx.car.app.media.MediaPlaybackManager
8import androidx.lifecycle.DefaultLifecycleObserver
9import 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)
25class 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}