DEV Community

Darshan Rathod
Darshan Rathod

Posted on

I Got a Phone Call, My Music Stopped Instantly, So I Went Digging Into Android's Source to Find Out Why

I was halfway through a track when my phone rang. The music didn't fade, didn't glitch, and didn't overlap with the ringtone for even a fraction of a second it just stopped, cleanly, like something had reached in and flipped a switch. A minute later I hung up, music started again

As an embedded engineer, that kind of "too clean to be an accident" behaviour is impossible to ignore. Somewhere under the app layer, something is deciding instantly and unambiguously who gets to own the audio output. So I went digging through the Android source to find out what that "something" actually is. What I found was a resource-arbitration mechanism that looks a lot more like RTOS scheduling than a media API, and it forced me to answer a question every systems engineer eventually asks: is this a mutex, a semaphore, or something else entirely?

The behaviour and the API that causes it
At the app level, Android exposes this through AudioManager and the concept of audio focus. Any app that wants to play sound must request it:

val audioManager = context. getSystemService(Context.AUDIO_SERVICE) as AudioManager

val focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
    .setOnAudioFocusChangeListener { focusChange ->
        when (focusChange) {
            AudioManager.AUDIOFOCUS_LOSS,
            AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> player.pause()
            AudioManager.AUDIOFOCUS_GAIN -> player.play()
        }
    }
    .build()

audioManager.requestAudioFocus(focusRequest)
Enter fullscreen mode Exit fullscreen mode

iOS exposes the same idea through AVAudioSession, using interruption notifications instead of a focus-change callback:

NotificationCenter.default.addObserver(
    self,
    selector: #selector(handleInterruption),
    name: AVAudioSession.interruptionNotification,
    object: AVAudioSession.sharedInstance()
)

@objc func handleInterruption(notification: Notification) {
    guard let info = notification.userInfo,
          let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
          let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { return }

    if type == .began {
        player.pause()
    } else if type == .ended {
        player.play()
    }
}
Enter fullscreen mode Exit fullscreen mode

Neither Spotify nor your app is polling anything. This is a push model you register a listener once, and the OS notifies you when your ownership of the audio output changes.

What's actually arbitrating this
The interesting part isn't the callback it's what decides who wins. On Android, that's AudioPolicyManager, running inside the native audioserver process (not inside your app and not inside the kernel). When the dialer app detects a ringing call, it requests audio focus with a high-priority category. That request goes through AudioManager → Binder IPC → AudioService in system_server → AudioPolicyManager, which holds the actual focus stack.

AudioPolicyManager maintains something conceptually simple: a stack of focus requests, ordered by gain type and arrival, with exactly one request holding the "active, un-ducked" slot at any time. Granting the incoming call's request means revoking Spotify's and that revocation is delivered back down the same IPC path as an AUDIOFOCUS_LOSS event.

This is worth pausing on if you come from a driver or RTOS background: the "lock" here is not a variable in shared memory that both apps can see and manipulate. It's a remote object owned by a system service, mutated only through IPC calls. There's no compare-and-swap, no futex shared between Spotify's process and the dialer's process ownership is tracked centrally, and changes are pushed out as messages.

So mutex, semaphore, or neither?
Here's the classification that actually holds up:

It is not a counting semaphore. A semaphore permits N concurrent holders up to a count. There is no "N" here audio focus in its exclusive form (AUDIOFOCUS_GAIN) allows exactly one owner. If this were modelled as a semaphore with count = 1, it would just be a binary semaphore, which in practice collapses to the next category anyway.

It behaves like a mutex but with preemption, not blocking. A textbook mutex has a waiter block until the holder releases it. That's not what happens here: the incoming call does not wait for Spotify to voluntarily give up focus. It's granted focus immediately, and Spotify is preempted and told after the fact via AUDIOFOCUS_LOSS. That "grab now, notify the loser asynchronously" pattern is closer to a priority-based preemption protocol than a passive mutex the same shape as a high-priority ISR or task preempting a lower-priority one holding a shared resource in an RTOS, where the preempted task gets a signal to unwind cleanly rather than being forcibly suspended mid-operation.

Ducking is a reader-writer wrinkle. AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK (used for things like turn-by-turn nav prompts) doesn't revoke Spotify's focus outright it tells Spotify to lower its volume while the requester plays over it. That's a second grant level coexisting with the first, which maps loosely onto a reader-writer lock: one exclusive writer state (full AUDIOFOCUS_GAIN), and a degraded shared state that two holders can occupy simultaneously.

The arbitration itself is a centralized service, not a shared-memory primitive. Calling it a "distributed mutex" is the most accurate one-line description: single ownership semantics, enforced by a broker process over IPC, with asynchronous release notification instead of blocking acquisition.

It's worth being clear about where real mutexes and semaphores do still show up here: several layers down, inside the ALSA driver and the HAL, buffer access and DMA ring management absolutely use kernel mutexes, spinlocks, and futexes. Those are invisible to anyone working at the AudioManager / AVAudioSession layer they belong to the audio driver's internal concurrency control, not to the focus-arbitration model an app developer interacts with.

Why this matters if you're building embedded audio
If you're building a custom Android-based device an infotainment head unit, a kiosk, a handheld with a dedicated audio pipeline this arbitration model is not optional plumbing you can ignore. Get the AudioAttributes / usage type wrong on a system service, and you'll either fail to interrupt a lower-priority stream when you should, or you'll get interrupted yourself by something that shouldn't outrank you. Debugging that class of bug means reading AudioPolicyManager's focus stack logs (adb shell dumpsys audio), not stepping through your app's Kotlin code the actual decision is made in a process you don't own.

The takeaway
What looks like a UI nicety music politely stepping aside for a phone call is a real resource-arbitration protocol: single-owner, priority-preemptive, IPC-brokered, with a limited shared-access mode bolted on for ducking. It's not a semaphore, and it's a mutex only if you squint and ignore that the "lock" preempts instead of blocks. The closest honest label is a priority-ceiling-style exclusive lock managed by a system service.

If you've hit audio focus bugs on a custom Android build stuck focus grants, ducking that never releases, a preempted app that never gets its AUDIOFOCUS_GAIN back I'd like to hear the war story in the comments.

Top comments (0)