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
15 changes: 14 additions & 1 deletion LoopFollow/Alarm/Alarm.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import Foundation
import HealthKit
import UIKit
import UserNotifications

protocol DayNightDisplayable {
Expand Down Expand Up @@ -259,7 +260,19 @@ struct Alarm: Identifiable, Codable, Equatable {
}
}()

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
    )
}

// AVAudioSession activation can fail (cannotInterruptOthers, 560557684). Pass
// the alarm's soundFile to the notification so the system delivers the user's
// configured sound as a fallback. In foreground or with Silent Tune, the in-app
// player is reliable and we keep .default to avoid an echo with the looping player.
let inBackgroundWithoutSilentTune = UIApplication.shared.applicationState != .active
&& Storage.shared.backgroundRefreshType.value != .silentTune
let notificationSound: SoundFile? = (playSound && inBackgroundWithoutSilentTune) ? soundFile : nil
AlarmManager.shared.sendNotification(
title: type.rawValue,
actionTitle: snoozeDuration == 0 ? "Acknowledge" : "Snooze",
soundFile: notificationSound
)

if playSound {
AlarmSound.setSoundFile(str: soundFile.rawValue)
Expand Down
8 changes: 6 additions & 2 deletions LoopFollow/Alarm/AlarmManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ class AlarmManager {
UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
}

func sendNotification(title: String, actionTitle: String? = nil) {
func sendNotification(title: String, actionTitle: String? = nil, soundFile: SoundFile? = nil) {
UNUserNotificationCenter.current().removeAllPendingNotificationRequests()

let content = UNMutableNotificationContent()
Expand All @@ -185,7 +185,11 @@ class AlarmManager {
content.subtitle += Observable.shared.directionText.value + " "
content.subtitle += Observable.shared.deltaText.value
content.categoryIdentifier = "category"
content.sound = .default
if let soundFile {
content.sound = UNNotificationSound(named: UNNotificationSoundName("\(soundFile.rawValue).caf"))
} else {
content.sound = .default
}

let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
Expand Down
22 changes: 17 additions & 5 deletions LoopFollow/Controllers/AlarmSound.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ class AlarmSound {
audioPlayer = try AVAudioPlayer(contentsOf: soundURL)
audioPlayer!.delegate = audioPlayerDelegate

try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [])
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: sessionCategoryOptions())
try AVAudioSession.sharedInstance().setActive(true)

audioPlayer?.numberOfLoops = 0
Expand Down Expand Up @@ -126,7 +126,7 @@ class AlarmSound {
audioPlayer = try AVAudioPlayer(contentsOf: soundURL)
audioPlayer!.delegate = audioPlayerDelegate

try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [])
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: sessionCategoryOptions())
try AVAudioSession.sharedInstance().setActive(true)

// Only use numberOfLoops if we're not using delay-based repeating
Expand Down Expand Up @@ -184,7 +184,7 @@ class AlarmSound {
audioPlayer = try AVAudioPlayer(contentsOf: soundURL)
audioPlayer!.delegate = audioPlayerDelegate

try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [])
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: sessionCategoryOptions())
try AVAudioSession.sharedInstance().setActive(true)

audioPlayer!.numberOfLoops = 0
Expand Down Expand Up @@ -213,7 +213,7 @@ class AlarmSound {
audioPlayer = try AVAudioPlayer(contentsOf: soundURL)
audioPlayer!.delegate = audioPlayerDelegate

try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [])
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: sessionCategoryOptions())
try AVAudioSession.sharedInstance().setActive(true)

// Play endless loops
Expand Down Expand Up @@ -262,13 +262,25 @@ class AlarmSound {

fileprivate static func enableAudio() {
do {
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [])
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: sessionCategoryOptions())
try AVAudioSession.sharedInstance().setActive(true)
LogManager.shared.log(category: .alarm, message: "Audio session configured for alarm playback")
} catch {
LogManager.shared.log(category: .alarm, message: "Enable audio error: \(error)")
}
}

// Background activation of a non-mixable .playback session is denied by iOS
// (cannotInterruptOthers, 560557684) unless the app is already actively playing
// audio. In foreground, or with Silent Tune holding a mixable session alive,
// the legacy options: [] succeeds and lets the alarm dominate other audio.
// For Bluetooth-heartbeat users with no Silent Tune, fall back to .duckOthers
// (mixable + ducks others) so activation is permitted from background.
fileprivate static func sessionCategoryOptions() -> AVAudioSession.CategoryOptions {
let inBackgroundWithoutSilentTune = UIApplication.shared.applicationState != .active
&& Storage.shared.backgroundRefreshType.value != .silentTune
return inBackgroundWithoutSilentTune ? .duckOthers : []
}
}

class AudioPlayerDelegate: NSObject, AVAudioPlayerDelegate {
Expand Down
Loading