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
9 changes: 5 additions & 4 deletions LoopFollow/Controllers/AlarmSound.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,7 +85,7 @@ class AlarmSound{
audioPlayer = try AVAudioPlayer(contentsOf: soundURL)
audioPlayer!.delegate = audioPlayerDelegate

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

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

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

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

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

// Play endless loops
Expand DownExpand Up@@ -210,8 +210,9 @@ class AlarmSound{

fileprivate static func enableAudio(){
do{
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: .mixWithOthers)
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [])
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)")
}
Expand Down
123 changes: 123 additions & 0 deletions LoopFollow/Controllers/VolumeButtonHandler.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@
import AVFoundation
import Combine
import Foundation
import MediaPlayer
import UIKit

class VolumeButtonHandler: NSObject{
Expand All@@ -30,6 +31,9 @@ class VolumeButtonHandler: NSObject{
private var lastSignificantVolumeChange: Date?
private var volumeChangePattern: [TimeInterval] = []

// Remote command center for handling bluetooth/CarPlay buttons
private var remoteCommandsEnabled = false

private var cancellables = Set<AnyCancellable>()

override private init(){
Expand DownExpand Up@@ -112,11 +116,127 @@ class VolumeButtonHandler: NSObject{
volumeChangePattern.removeAll()
}

private func setupRemoteCommandCenter(){
guard !remoteCommandsEnabled else{return }

let commandCenter = MPRemoteCommandCenter.shared()

// Log current audio route to help with debugging
let currentRoute = AVAudioSession.sharedInstance().currentRoute
LogManager.shared.log(category: .volumeButtonSnooze, message: "Audio route: \(currentRoute.outputs.map{$0.portName }.joined(separator: ", "))")

// Enable pause command - handles play/pause button on bluetooth devices and CarPlay
commandCenter.pauseCommand.isEnabled = true
commandCenter.pauseCommand.addTarget{[weak self] _ in
guard let self = self else{return .commandFailed }

LogManager.shared.log(category: .volumeButtonSnooze, message: "Pause command received from remote")

// Check if alarm is currently active and activation delay has passed
if let alarmStartTime = self.alarmStartTime{
let timeSinceAlarmStart = Date().timeIntervalSince(alarmStartTime)

if timeSinceAlarmStart > self.volumeButtonActivationDelay{
// Check cooldown
if let lastPress = self.lastVolumeButtonPressTime{
let timeSinceLastPress = Date().timeIntervalSince(lastPress)
if timeSinceLastPress < self.volumeButtonCooldown{
return .success
}
}

LogManager.shared.log(category: .volumeButtonSnooze, message: "Remote command pause received - snoozing alarm")
self.snoozeActiveAlarm()
return .success
}
}

return .commandFailed
}

// Enable play command as well for symmetry
commandCenter.playCommand.isEnabled = true
commandCenter.playCommand.addTarget{[weak self] _ in
guard let self = self else{return .commandFailed }

LogManager.shared.log(category: .volumeButtonSnooze, message: "Play command received from remote")

if let alarmStartTime = self.alarmStartTime{
let timeSinceAlarmStart = Date().timeIntervalSince(alarmStartTime)

if timeSinceAlarmStart > self.volumeButtonActivationDelay{
if let lastPress = self.lastVolumeButtonPressTime{
let timeSinceLastPress = Date().timeIntervalSince(lastPress)
if timeSinceLastPress < self.volumeButtonCooldown{
return .success
}
}

LogManager.shared.log(category: .volumeButtonSnooze, message: "Remote command play received - snoozing alarm")
self.snoozeActiveAlarm()
return .success
}
}

return .commandFailed
}

// Enable toggle play/pause command - common on many bluetooth devices
commandCenter.togglePlayPauseCommand.isEnabled = true
commandCenter.togglePlayPauseCommand.addTarget{[weak self] _ in
guard let self = self else{return .commandFailed }

LogManager.shared.log(category: .volumeButtonSnooze, message: "Toggle play/pause command received from remote")

if let alarmStartTime = self.alarmStartTime{
let timeSinceAlarmStart = Date().timeIntervalSince(alarmStartTime)

if timeSinceAlarmStart > self.volumeButtonActivationDelay{
if let lastPress = self.lastVolumeButtonPressTime{
let timeSinceLastPress = Date().timeIntervalSince(lastPress)
if timeSinceLastPress < self.volumeButtonCooldown{
return .success
}
}

LogManager.shared.log(category: .volumeButtonSnooze, message: "Remote command toggle play/pause received - snoozing alarm")
self.snoozeActiveAlarm()
return .success
}
}

return .commandFailed
}

remoteCommandsEnabled = true
LogManager.shared.log(category: .volumeButtonSnooze, message: "Remote command center configured for bluetooth/CarPlay button handling")
}

private func disableRemoteCommandCenter(){
guard remoteCommandsEnabled else{return }

let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.pauseCommand.isEnabled = false
commandCenter.playCommand.isEnabled = false
commandCenter.togglePlayPauseCommand.isEnabled = false

// Remove all targets
commandCenter.pauseCommand.removeTarget(nil)
commandCenter.playCommand.removeTarget(nil)
commandCenter.togglePlayPauseCommand.removeTarget(nil)

remoteCommandsEnabled = false
LogManager.shared.log(category: .volumeButtonSnooze, message: "Remote command center disabled")
}

func startMonitoring(){
guard !isMonitoring else{return }

isMonitoring = true

// Setup remote command center for bluetooth/CarPlay button handling
setupRemoteCommandCenter()

volumeObserver = AVAudioSession.sharedInstance().observe(\.outputVolume, options: [.new]){[weak self] session, _ in
guard let self = self, let alarmStartTime = self.alarmStartTime else{return }

Expand DownExpand Up@@ -176,6 +296,9 @@ class VolumeButtonHandler: NSObject{
volumeObserver?.invalidate()
volumeObserver = nil

// Disable remote command center
disableRemoteCommandCenter()

isMonitoring = false
lastVolume = 0.0 // Reset for the next alarm.
}
Expand Down