Skip to content

Commit b76fa6a

Browse files
authored
Merge pull request #488 from loopandlearn/bluetooth-button-snooze
Add snooze with bluetooth play-pause control
2 parents dc77a91 + 1e5d2d1 commit b76fa6a

File tree

2 files changed

+128
-4
lines changed

2 files changed

+128
-4
lines changed

LoopFollow/Controllers/AlarmSound.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ class AlarmSound {
8888
audioPlayer = try AVAudioPlayer(contentsOf: soundURL)
8989
audioPlayer!.delegate = audioPlayerDelegate
9090

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

9494
audioPlayer?.numberOfLoops = 0
@@ -126,7 +126,7 @@ class AlarmSound {
126126
audioPlayer = try AVAudioPlayer(contentsOf: soundURL)
127127
audioPlayer!.delegate = audioPlayerDelegate
128128

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

132132
// Only use numberOfLoops if we're not using delay-based repeating
@@ -213,7 +213,7 @@ class AlarmSound {
213213
audioPlayer = try AVAudioPlayer(contentsOf: soundURL)
214214
audioPlayer!.delegate = audioPlayerDelegate
215215

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

219219
// Play endless loops
@@ -262,8 +262,9 @@ class AlarmSound {
262262

263263
fileprivate static func enableAudio() {
264264
do {
265-
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: .mixWithOthers)
265+
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [])
266266
try AVAudioSession.sharedInstance().setActive(true)
267+
LogManager.shared.log(category: .alarm, message: "Audio session configured for alarm playback")
267268
} catch {
268269
LogManager.shared.log(category: .alarm, message: "Enable audio error: \(error)")
269270
}

LoopFollow/Controllers/VolumeButtonHandler.swift

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import AVFoundation
55
import Combine
66
import Foundation
7+
import MediaPlayer
78
import UIKit
89

910
class VolumeButtonHandler: NSObject {
@@ -30,6 +31,9 @@ class VolumeButtonHandler: NSObject {
3031
private var lastSignificantVolumeChange: Date?
3132
private var volumeChangePattern: [TimeInterval] = []
3233

34+
// Remote command center for handling bluetooth/CarPlay buttons
35+
private var remoteCommandsEnabled = false
36+
3337
private var cancellables = Set<AnyCancellable>()
3438

3539
override private init() {
@@ -112,11 +116,127 @@ class VolumeButtonHandler: NSObject {
112116
volumeChangePattern.removeAll()
113117
}
114118

119+
private func setupRemoteCommandCenter() {
120+
guard !remoteCommandsEnabled else { return }
121+
122+
let commandCenter = MPRemoteCommandCenter.shared()
123+
124+
// Log current audio route to help with debugging
125+
let currentRoute = AVAudioSession.sharedInstance().currentRoute
126+
LogManager.shared.log(category: .volumeButtonSnooze, message: "Audio route: \(currentRoute.outputs.map { $0.portName }.joined(separator: ", "))")
127+
128+
// Enable pause command - handles play/pause button on bluetooth devices and CarPlay
129+
commandCenter.pauseCommand.isEnabled = true
130+
commandCenter.pauseCommand.addTarget { [weak self] _ in
131+
guard let self = self else { return .commandFailed }
132+
133+
LogManager.shared.log(category: .volumeButtonSnooze, message: "Pause command received from remote")
134+
135+
// Check if alarm is currently active and activation delay has passed
136+
if let alarmStartTime = self.alarmStartTime {
137+
let timeSinceAlarmStart = Date().timeIntervalSince(alarmStartTime)
138+
139+
if timeSinceAlarmStart > self.volumeButtonActivationDelay {
140+
// Check cooldown
141+
if let lastPress = self.lastVolumeButtonPressTime {
142+
let timeSinceLastPress = Date().timeIntervalSince(lastPress)
143+
if timeSinceLastPress < self.volumeButtonCooldown {
144+
return .success
145+
}
146+
}
147+
148+
LogManager.shared.log(category: .volumeButtonSnooze, message: "Remote command pause received - snoozing alarm")
149+
self.snoozeActiveAlarm()
150+
return .success
151+
}
152+
}
153+
154+
return .commandFailed
155+
}
156+
157+
// Enable play command as well for symmetry
158+
commandCenter.playCommand.isEnabled = true
159+
commandCenter.playCommand.addTarget { [weak self] _ in
160+
guard let self = self else { return .commandFailed }
161+
162+
LogManager.shared.log(category: .volumeButtonSnooze, message: "Play command received from remote")
163+
164+
if let alarmStartTime = self.alarmStartTime {
165+
let timeSinceAlarmStart = Date().timeIntervalSince(alarmStartTime)
166+
167+
if timeSinceAlarmStart > self.volumeButtonActivationDelay {
168+
if let lastPress = self.lastVolumeButtonPressTime {
169+
let timeSinceLastPress = Date().timeIntervalSince(lastPress)
170+
if timeSinceLastPress < self.volumeButtonCooldown {
171+
return .success
172+
}
173+
}
174+
175+
LogManager.shared.log(category: .volumeButtonSnooze, message: "Remote command play received - snoozing alarm")
176+
self.snoozeActiveAlarm()
177+
return .success
178+
}
179+
}
180+
181+
return .commandFailed
182+
}
183+
184+
// Enable toggle play/pause command - common on many bluetooth devices
185+
commandCenter.togglePlayPauseCommand.isEnabled = true
186+
commandCenter.togglePlayPauseCommand.addTarget { [weak self] _ in
187+
guard let self = self else { return .commandFailed }
188+
189+
LogManager.shared.log(category: .volumeButtonSnooze, message: "Toggle play/pause command received from remote")
190+
191+
if let alarmStartTime = self.alarmStartTime {
192+
let timeSinceAlarmStart = Date().timeIntervalSince(alarmStartTime)
193+
194+
if timeSinceAlarmStart > self.volumeButtonActivationDelay {
195+
if let lastPress = self.lastVolumeButtonPressTime {
196+
let timeSinceLastPress = Date().timeIntervalSince(lastPress)
197+
if timeSinceLastPress < self.volumeButtonCooldown {
198+
return .success
199+
}
200+
}
201+
202+
LogManager.shared.log(category: .volumeButtonSnooze, message: "Remote command toggle play/pause received - snoozing alarm")
203+
self.snoozeActiveAlarm()
204+
return .success
205+
}
206+
}
207+
208+
return .commandFailed
209+
}
210+
211+
remoteCommandsEnabled = true
212+
LogManager.shared.log(category: .volumeButtonSnooze, message: "Remote command center configured for bluetooth/CarPlay button handling")
213+
}
214+
215+
private func disableRemoteCommandCenter() {
216+
guard remoteCommandsEnabled else { return }
217+
218+
let commandCenter = MPRemoteCommandCenter.shared()
219+
commandCenter.pauseCommand.isEnabled = false
220+
commandCenter.playCommand.isEnabled = false
221+
commandCenter.togglePlayPauseCommand.isEnabled = false
222+
223+
// Remove all targets
224+
commandCenter.pauseCommand.removeTarget(nil)
225+
commandCenter.playCommand.removeTarget(nil)
226+
commandCenter.togglePlayPauseCommand.removeTarget(nil)
227+
228+
remoteCommandsEnabled = false
229+
LogManager.shared.log(category: .volumeButtonSnooze, message: "Remote command center disabled")
230+
}
231+
115232
func startMonitoring() {
116233
guard !isMonitoring else { return }
117234

118235
isMonitoring = true
119236

237+
// Setup remote command center for bluetooth/CarPlay button handling
238+
setupRemoteCommandCenter()
239+
120240
volumeObserver = AVAudioSession.sharedInstance().observe(\.outputVolume, options: [.new]) { [weak self] session, _ in
121241
guard let self = self, let alarmStartTime = self.alarmStartTime else { return }
122242

@@ -176,6 +296,9 @@ class VolumeButtonHandler: NSObject {
176296
volumeObserver?.invalidate()
177297
volumeObserver = nil
178298

299+
// Disable remote command center
300+
disableRemoteCommandCenter()
301+
179302
isMonitoring = false
180303
lastVolume = 0.0 // Reset for the next alarm.
181304
}

0 commit comments

Comments
 (0)