fix(talk): bind Android cancellation to action turn

Co-authored-by: Zhilong Zheng <zhengzhilong1115@gmail.com>
This commit is contained in:
Vincent Koc
2026-08-23 13:46:30 +08:00
parent 3bb4670637
commit d79d803ab1
2 changed files with 44 additions and 5 deletions
@@ -1725,6 +1725,8 @@ class TalkModeManager internal constructor(
}
internal suspend fun pauseRealtimeCaptureForPushToTalk(captureId: String) {
val cancellationSessionId = realtimeSessionId
val cancellationTurnId = realtimeOutputTurnId?.trim()?.takeIf(String::isNotEmpty)
val captureJobs =
synchronized(realtimeCapturePauseLock) {
val currentSessionId = realtimeSessionId
@@ -1741,7 +1743,13 @@ class TalkModeManager internal constructor(
appendJob?.cancelAndJoin()
// Stop input first so no frame can create new provider output while the
// cancellation boundary is being established.
if (!cancelRealtimeOutput(reason = "android-push-to-talk")) {
if (
!cancelRealtimeOutput(
reason = "android-push-to-talk",
sessionId = cancellationSessionId,
turnId = cancellationTurnId,
)
) {
Log.w(tag, "realtime output cancellation was not confirmed; closing relay")
stopRealtimeRelay(preserveStatus = true)
synchronized(realtimeCapturePauseLock) {
@@ -2916,18 +2924,32 @@ class TalkModeManager internal constructor(
}
fun stopTts() {
val sessionId = realtimeSessionId
val turnId = realtimeOutputTurnId?.trim()?.takeIf(String::isNotEmpty)
realtimeOutputSuppressed = true
stopRealtimePlayback()
scope.launch { cancelRealtimeOutput(reason = "android-stop-tts") }
if (sessionId != null && turnId != null) {
scope.launch {
cancelRealtimeOutput(
reason = "android-stop-tts",
sessionId = sessionId,
turnId = turnId,
)
}
}
stopSpeaking(resetInterrupt = true)
_isSpeaking.value = false
setStatus(nativeText("Listening"))
}
private suspend fun cancelRealtimeOutput(reason: String): Boolean =
private suspend fun cancelRealtimeOutput(
reason: String,
sessionId: String?,
turnId: String?,
): Boolean =
realtimeOutputCancellationMutex.withLock {
val sessionId = realtimeSessionId ?: return@withLock true
val turnId = realtimeOutputTurnId?.trim()?.takeIf(String::isNotEmpty) ?: return@withLock false
sessionId ?: return@withLock true
turnId ?: return@withLock false
val clear = CompletableDeferred<String?>()
pendingRealtimeOutputClear = clear
try {
@@ -120,6 +120,23 @@ class TalkModeManagerTest {
assertTrue(readPrivateField(manager, "pendingRealtimeOutputClear") === pendingClear)
}
@Test
fun stopTtsCancelsTheOutputOwnedWhenTheActionStarted() =
runTest {
val manager = createManager(scope = this)
val priorClear = CompletableDeferred<String?>()
setPrivateField(manager, "realtimeSessionId", "relay-a")
setPrivateField(manager, "realtimeOutputTurnId", "turn-a")
setPrivateField(manager, "pendingRealtimeOutputClear", priorClear)
manager.stopTts()
setPrivateField(manager, "realtimeSessionId", null)
setPrivateField(manager, "realtimeOutputTurnId", "turn-b")
runCurrent()
assertNull(readPrivateField(manager, "pendingRealtimeOutputClear"))
}
@Test
fun disablingPlaybackCancelsTrackedJobOnce() {
val manager = createManager()