Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/audioswitch-start-stop-guard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"client-sdk-android": patch
---

Fix AudioSwitchHandler leaving audio routing active if stop() is called while start() is still setting up the audio switch
Original file line number Diff line number Diff line change
Expand Up @@ -221,9 +221,10 @@ constructor(private val context: Context) : AudioHandler {
handler = Handler(thread!!.looper)
}

if (audioSwitch == null) {
handler?.removeCallbacksAndMessages(null)
handler?.postAtFrontOfQueue {
val startHandler = handler
if (audioSwitch == null && startHandler != null) {
startHandler.removeCallbacksAndMessages(null)
startHandler.postAtFrontOfQueue {
val switch =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
CommDeviceAudioSwitch(
Expand Down Expand Up @@ -255,7 +256,15 @@ constructor(private val context: Context) : AudioHandler {
switch.audioAttributeContentType = audioAttributeContentType
switch.forceHandleAudioRouting = forceHandleAudioRouting

audioSwitch = switch
// stop() may have run while this switch was being created, and its teardown
// saw no switch to stop. Only publish the switch if this start is still current;
// otherwise drop it, since nothing is registered until switch.start() below.
synchronized(this@AudioSwitchHandler) {
if (handler !== startHandler) {
return@postAtFrontOfQueue
}
audioSwitch = switch
Comment on lines +262 to +266

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Repeated starts leave audio routing active

If start() runs again during switch construction, both jobs pass the startHandler check and publish separate switches. The second replaces audioSwitch, so stop() cannot stop the first; audio routing remains active.

Learn more

A start job is queued on the handler thread, but the switch reference stays null until construction finishes. If another start() occurs while construction is already running, start observes null and queues a second job on the same handler. Both jobs pass the handler identity check. The second job replaces the first switch reference; stop only retains and stops the latest switch.

Example: Thread A begins constructing switch A. A second start() queues switch B before A publishes. Both publish and activate. A later stop() stops B; A stays active.

Recommended fix: Track a start generation or pending-start state, and reject jobs that no longer own the current generation. Ensure a running job is stopped if a newer start supersedes it; add a test that pauses construction across a repeated start().

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

}
switch.start(audioDeviceChangeDispatcher)
switch.activate()
}
Expand Down
Loading