44import AVFoundation
55import Combine
66import Foundation
7+ import MediaPlayer
78import UIKit
89
910class 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