Music comes back after a call when it plays through Gearslip

When the car took the speakers for a moment, for a call or its own voice, Gearslip
paused the music and gave up its claim, so the car never handed the sound back.
It now keeps the claim and plays the music again once the car is done.
This commit is contained in:
Seb3thehacker committed 2026-11-03 12:22:01 -08:00
1 parent 9ddab16645
commit 86e61bcc1d
3 files changed
+82 -10

No files matched your search

@@ -589,6 +589,8 @@ class GearslipRunner(
// The driver picked the radio or another source on the car: pause the phone's music
// as Android Auto does, instead of letting it play on to no one.
onTakenBack = { CarServices.pausePhoneMedia("the car switched to another source") },
onPausedForNow = { CarServices.pausePhoneMediaForNow() },
onGivenBack = { CarServices.resumePhoneMedia() },
)
if (CarSettings.pipeAudio.value) {
openChannel(media.serviceId)
@@ -36,6 +36,9 @@ class AudioLink(
private val onClose: (AudioLink) -> Unit = { CarAudio.detach(it) },
/** The car took the sound back after giving it, as when the driver switches to the radio. */
private val onTakenBack: () -> Unit = {},
/** The car wants the speakers for a moment (a call, its own voice), and then [onGivenBack]. */
private val onPausedForNow: () -> Unit = {},
private val onGivenBack: () -> Unit = {},
) : CarAudio.Link {
private var configIndex = 0
@@ -43,6 +46,8 @@ class AudioLink(
@Volatile private var started = false
@Volatile private var focusGranted = false
@Volatile private var focusRequested = false
/** The car took the speakers for a moment, and we paused the phone's music until it's done. */
@Volatile private var pausedForNow = false
@Volatile private var inFlight = 0
// The Uconnect asks for 4 on its video channel; assume the same until its Config says otherwise.
@Volatile private var maxUnacked = 4
@@ -222,9 +227,32 @@ class AudioLink(
val had = focusGranted
focusGranted = state == FOCUS_STATE_GAIN || state == FOCUS_STATE_GAIN_TRANSIENT ||
state == FOCUS_STATE_GAIN_MEDIA_ONLY
if (focusGranted) startStream() else stop()
// A ducking request leaves the music playing; a loss means another source has the speakers.
if (had && (state == FOCUS_STATE_LOSS || state == FOCUS_STATE_LOSS_TRANSIENT)) onTakenBack()
when {
focusGranted -> {
startStream()
if (pausedForNow) {
pausedForNow = false
onGivenBack()
}
}
// A call or the car's own voice. Releasing focus here meant the car never handed it
// back, so the music stayed paused after the call: keep the claim and wait for GAIN.
state == FOCUS_STATE_LOSS_TRANSIENT -> {
synchronized(startLock) { stopLocked(release = false) }
if (had) {
pausedForNow = true
onPausedForNow()
}
}
else -> {
stop()
// A ducking request leaves the music playing; a loss means another source has the speakers.
if (state == FOCUS_STATE_LOSS && (had || pausedForNow)) {
pausedForNow = false
onTakenBack()
}
}
}
}
/**
@@ -299,19 +327,20 @@ class AudioLink(
synchronized(startLock) { stopLocked() }
}
private fun stopLocked() {
/** [release] false keeps our claim on the speakers, so the car hands them back on its own. */
private fun stopLocked(release: Boolean = true) {
if (!started) return
started = false
synchronized(ackLock) { ackLock.notifyAll() }
runCatching {
sendOnChannel(MSG_STOP, Protobuf.varintField(1, sessionId.toLong()))
if (focusRequested) sendControl(MSG_FOCUS_REQUEST, Protobuf.varintField(1, FOCUS_RELEASE.toLong()))
if (release && focusRequested) sendControl(MSG_FOCUS_REQUEST, Protobuf.varintField(1, FOCUS_RELEASE.toLong()))
}
focusRequested = false
if (release) focusRequested = false
focusGranted = false
aac?.release()
aac = null
GearslipLog.i("$tag: -> Stop, AudioFocusRequest(RELEASE) after $chunks chunks / $bytes bytes")
GearslipLog.i("$tag: -> Stop${if (release) ", AudioFocusRequest(RELEASE)" else ""} after $chunks chunks / $bytes bytes")
chunks = 0
bytes = 0
// A new stream starts with the car's buffer empty again: earn the lead back if it's needed.
@@ -2,6 +2,7 @@ package app.seb3thehacker.gearslip.car
import android.content.ComponentName
import android.content.Context
import android.media.session.MediaController
import android.media.session.MediaSessionManager
import android.media.session.PlaybackState
import app.seb3thehacker.gearslip.audio.CarAudio
@@ -254,7 +255,41 @@ object CarServices {
* just the one Gearslip opened: anything the car was playing was going through the car.
*/
fun pausePhoneMedia(why: String = "the car left") {
val ctx = context ?: return
pausedForNow = null
pausedOwnForNow = false
pause(why)
}
/** What [pausePhoneMediaForNow] paused, for [resumePhoneMedia] to play again. */
@Volatile private var pausedForNow: List<MediaController>? = null
/** Without notification access, whether it paused the app Gearslip drives itself. */
@Volatile private var pausedOwnForNow = false
/** The car wants the speakers for a moment: pause, and remember what to play after. */
fun pausePhoneMediaForNow() {
pausedOwnForNow = false
pausedForNow = pause("the car needs the speakers for a moment")
}
/** The car handed the speakers back: play what it paused, unless the driver has since. */
fun resumePhoneMedia() {
val paused = pausedForNow ?: return
pausedForNow = null
if (pausedOwnForNow) {
pausedOwnForNow = false
mediaOrNull()?.let { if (!it.now.value.playing) it.togglePlay() }
return
}
val waiting = paused.filter { it.playbackState?.state == PlaybackState.STATE_PAUSED }
waiting.forEach { runCatching { it.transportControls.play() } }
if (waiting.isNotEmpty()) {
app.seb3thehacker.gearslip.GearslipLog.i("media: playing ${waiting.joinToString { it.packageName }} again: the car gave the speakers back")
}
}
/** What it paused. Without notification access it can only reach the app Gearslip drives itself. */
private fun pause(why: String): List<MediaController> {
val ctx = context ?: return emptyList()
val manager = ctx.getSystemService(MediaSessionManager::class.java)
val listener = ComponentName(ctx, GearslipNotificationListener::class.java)
val playing = try {
@@ -264,13 +299,19 @@ object CarServices {
}
if (playing == null) {
// No notification access: the app Gearslip itself is driving is all it can reach.
mediaOrNull()?.let { if (it.now.value.playing) it.togglePlay() }
return
mediaOrNull()?.let {
if (it.now.value.playing) {
it.togglePlay()
pausedOwnForNow = true
}
}
return emptyList()
}
playing.forEach { runCatching { it.transportControls.pause() } }
if (playing.isNotEmpty()) {
app.seb3thehacker.gearslip.GearslipLog.i("media: paused ${playing.joinToString { it.packageName }}: $why")
}
return playing
}
/** The car went away: nothing should keep running for a screen that no longer exists. */