Skip to content

Fix alarm sound session activation failures in background#596

Open
bjorkert wants to merge 3 commits intodevfrom
fix/alarm-audio-session-activation
Open

Fix alarm sound session activation failures in background#596
bjorkert wants to merge 3 commits intodevfrom
fix/alarm-audio-session-activation

Conversation

@bjorkert
Copy link
Copy Markdown
Member

@bjorkert bjorkert commented Apr 9, 2026

Fixes #590Unable to play alarm: Session activation failed.

Background activation of a non-mixable .playback audio session is denied by iOS with cannotInterruptOthers (560557684) unless the app is already actively playing audio. With Silent Tune the silent loop holds a mixable session alive, so activation succeeds. With a Bluetooth-heartbeat refresh (Dexcom / RileyLink / Omnipod Dash) there's no warm session at the moment of alarm, so setActive(true) fails and the alarm is silent.

Changes

  • AlarmSound.swift — at the five setCategory call sites, choose options: based on app state:

    • foreground OR backgroundRefreshType == .silentTune[] (legacy aggressive interrupt — no behavioral change for those users)
    • background AND not Silent Tune → .duckOthers (mixable; permits activation)

    Centralized in a new sessionCategoryOptions() helper.

  • AlarmManager.sendNotification — accepts an optional soundFile: SoundFile?. When provided, the notification uses UNNotificationSound(named: "<soundFile>.caf") instead of .default.

  • Alarm.trigger() — passes the alarm's soundFile to the notification only when both playSound is true AND we're in the fail-prone state (background + no Silent Tune). The system-delivered notification then carries the user's configured alarm sound as an audible fallback in case the in-app player can't activate. In foreground / Silent Tune the notification keeps .default so it doesn't echo the in-app loop.

Use .duckOthers instead of empty options when configuring the audio
session for alarm playback. The empty options created a non-mixable
session that conflicted with the background silent audio player
(which uses .mixWithOthers), causing setActive(true) to fail with
"Session activation failed" when the app was in the background.
@bjorkert bjorkert marked this pull request as draft April 26, 2026 08:39
@bjorkert
Copy link
Copy Markdown
Member Author

Changed it to draft, needs more testing.

Limit the .duckOthers option to the only state where legacy options: []
fails: background without Silent Tune holding a mixable session alive.
In foreground or with Silent Tune, restore options: [] so the alarm
continues to dominate other audio with no behavioral change for those
users.

In that same fail-prone state, plumb the alarm's soundFile through
AlarmManager.sendNotification so the system-delivered notification
carries the user's configured alarm sound as an audible fallback. In
other states the notification keeps .default to avoid an echo with the
in-app AVAudioPlayer loop.
@bjorkert bjorkert marked this pull request as ready for review April 29, 2026 14:22
@marionbarker
Copy link
Copy Markdown
Collaborator

Test

✅ successful test

Demonstrate the problem

Build using dev: 6.0.9, c9f74f4

  • configured LoopFollow phone with a RileyLink for background refresh
  • enable high and low alerts configured so I should get an alert and lock the phone
  • wait after CGM update - no alert sound
  • unlock phone, check alert status - see an alert but did not hear one
  • wait while app is open - alarm sounds

Demonstrate this PR fixes the problem

Build using fix/alarm-audio-session-activation, 54c2a5d

  • enable high and low alerts configured so I should get an alert and lock the phone
  • wait after CGM update - ✅ alert sounds even though phone is locked.

Copy link
Copy Markdown
Collaborator

@marionbarker marionbarker left a comment

Choose a reason for hiding this comment

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

approve by test

Copy link
Copy Markdown
Collaborator

@dnzxy dnzxy left a comment

Choose a reason for hiding this comment

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

The .duckOthers gating is a reasonable fix for cannotInterruptOthers, but I don’t think the notification path is safe as written. Since the notification with custom sound is scheduled before in-app playback is attempted, it can double-play when .duckOthers succeeds.

I'd suggest to please either make the notification sound truly conditional on playback failure, or intentionally make the background/no-Silent-Tune path notification-only.

We could also consider using applicationState == .background instead of != .active, and centralize the duplicated policy logic.

}()

AlarmManager.shared.sendNotification(title: type.rawValue, actionTitle: snoozeDuration == 0 ? "Acknowledge" : "Snooze")
// When backgrounded without Silent Tune holding a session alive, the in-app
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Isn't something like potentially safer to avoid unintended duplicate plays?

let playbackStarted = AlarmSound.play(...)

if !playbackStarted && inBackgroundWithoutSilentTune {
    AlarmManager.shared.sendNotification(
        title: type.rawValue,
        actionTitle: ...,
        soundFile: soundFile
    )
} else {
    AlarmManager.shared.sendNotification(
        title: type.rawValue,
        actionTitle: ...,
        soundFile: nil
    )
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants