@@ -17,9 +17,11 @@ class VolumeButtonHandler: NSObject {
1717 private let volumeButtonPressTimeWindow : TimeInterval = 0.3
1818 private let volumeButtonCooldown : TimeInterval = 0.5
1919
20+ // KVO observer for system volume
21+ private var volumeObserver : NSKeyValueObservation ?
22+
2023 private var lastVolume : Float = 0.0
2124 private var isMonitoring = false
22- private var volumeMonitoringTimer : Timer ?
2325 private var alarmStartTime : Date ?
2426 private var lastVolumeButtonPressTime : Date ?
2527
@@ -46,45 +48,6 @@ class VolumeButtonHandler: NSObject {
4648 . store ( in: & cancellables)
4749 }
4850
49- private func checkVolumeChange( ) {
50- let currentVolume = AVAudioSession . sharedInstance ( ) . outputVolume
51- let volumeDifference = abs ( currentVolume - lastVolume)
52- let now = Date ( )
53-
54- if volumeDifference > volumeButtonPressThreshold {
55- if let startTime = alarmStartTime {
56- let timeSinceAlarmStart = now. timeIntervalSince ( startTime)
57-
58- // Ignore volume changes from alarm system
59- if timeSinceAlarmStart < 2.0 , currentVolume > lastVolume {
60- if volumeDifference <= 0.15 , timeSinceAlarmStart < 1.5 {
61- lastVolume = currentVolume
62- return
63- }
64- }
65- }
66-
67- recordVolumeChange ( currentVolume: currentVolume, timestamp: now)
68-
69- if lastVolume > 0 , let startTime = alarmStartTime {
70- let timeSinceAlarmStart = now. timeIntervalSince ( startTime)
71-
72- if timeSinceAlarmStart > volumeButtonActivationDelay {
73- if let lastPress = lastVolumeButtonPressTime {
74- let timeSinceLastPress = now. timeIntervalSince ( lastPress)
75- if timeSinceLastPress < volumeButtonCooldown { return }
76- }
77-
78- if isLikelyVolumeButtonPress ( volumeDifference: volumeDifference, timestamp: now) {
79- snoozeActiveAlarm ( )
80- }
81- }
82- }
83- }
84-
85- lastVolume = currentVolume
86- }
87-
8851 private func recordVolumeChange( currentVolume: Float , timestamp: Date ) {
8952 recentVolumeChanges. append ( ( volume: currentVolume, timestamp: timestamp) )
9053
@@ -99,7 +62,6 @@ class VolumeButtonHandler: NSObject {
9962 volumeChangePattern. removeFirst ( )
10063 }
10164 }
102-
10365 lastSignificantVolumeChange = timestamp
10466 }
10567
@@ -129,75 +91,92 @@ class VolumeButtonHandler: NSObject {
12991
13092 private func alarmStarted( ) {
13193 guard Storage . shared. alarmConfiguration. value. enableVolumeButtonSnooze else { return }
94+ LogManager . shared. log ( category: . volumeButtonSnooze, message: " Alarm start detected, setting up volume observer. " )
13295
133- LogManager . shared. log ( category: . volumeButtonSnooze, message: " Alarm start detected " )
13496 alarmStartTime = Date ( )
135-
13697 recentVolumeChanges. removeAll ( )
13798 lastSignificantVolumeChange = nil
13899 volumeChangePattern. removeAll ( )
139100
140101 startMonitoring ( )
141102 }
142103
143- func startMonitoring( retryCount: Int = 0 ) {
104+ private func alarmStopped( ) {
105+ LogManager . shared. log ( category: . volumeButtonSnooze, message: " Alarm stop detected " )
106+
107+ alarmStartTime = nil
108+ stopMonitoring ( )
109+
110+ recentVolumeChanges. removeAll ( )
111+ lastSignificantVolumeChange = nil
112+ volumeChangePattern. removeAll ( )
113+ }
114+
115+ func startMonitoring( ) {
144116 guard !isMonitoring else { return }
145117
146- let audioSession = AVAudioSession . sharedInstance ( )
147- let currentVolume = audioSession. outputVolume
118+ isMonitoring = true
148119
149- if currentVolume > 0 {
150- LogManager . shared. log ( category: . volumeButtonSnooze, message: " Successfully started volume monitoring. " )
151- lastVolume = currentVolume
152- isMonitoring = true
153- startVolumeMonitoringTimer ( )
154- return
155- }
120+ volumeObserver = AVAudioSession . sharedInstance ( ) . observe ( \. outputVolume, options: [ . new] ) { [ weak self] session, _ in
121+ guard let self = self , let alarmStartTime = self . alarmStartTime else { return }
156122
157- // Failure case: Volume is still 0. Let's retry if we can.
158- let maxRetries = 5
159- if retryCount < maxRetries {
160- LogManager . shared. log ( category: . volumeButtonSnooze, message: " Did not get a valid volume, retrying... (Attempt \( retryCount + 1 ) / \( maxRetries) ) " )
161- let delay = 0.2 // 200ms delay between retries
162- DispatchQueue . main. asyncAfter ( deadline: . now( ) + delay) { [ weak self] in
163- self ? . startMonitoring ( retryCount: retryCount + 1 )
123+ let currentVolume = session. outputVolume
124+ let now = Date ( )
125+
126+ // On the first observation, capture the initial volume when the audio session
127+ // becomes active. This solves the race condition. We then return to avoid
128+ // treating this initial setup as a user-initiated button press.
129+ if self . lastVolume == 0.0 , currentVolume > 0.0 {
130+ LogManager . shared. log ( category: . volumeButtonSnooze, message: " Observer received initial valid volume: \( currentVolume) " )
131+ self . lastVolume = currentVolume
132+ return
164133 }
165- } else {
166- // We've exhausted all retries, log the final failure.
167- LogManager . shared. log ( category: . volumeButtonSnooze, message: " Did not get a valid volume after \( maxRetries) retries, not monitoring. " )
168- }
169- }
170134
171- private func startVolumeMonitoringTimer( ) {
172- guard volumeMonitoringTimer == nil else { return }
135+ guard self . lastVolume > 0.0 else { return }
173136
174- let timer = Timer ( timeInterval: 0.1 , repeats: true ) { [ weak self] _ in
175- self ? . checkVolumeChange ( )
176- }
137+ let volumeDifference = abs ( currentVolume - self . lastVolume)
177138
178- volumeMonitoringTimer = timer
139+ if volumeDifference > self . volumeButtonPressThreshold {
140+ let timeSinceAlarmStart = now. timeIntervalSince ( alarmStartTime)
179141
180- RunLoop . main. add ( timer, forMode: . common)
181- }
182-
183- private func alarmStopped( ) {
184- LogManager . shared. log ( category: . volumeButtonSnooze, message: " Alarm stop detected " )
142+ // Ignore volume changes from the alarm system's own ramp-up.
143+ if timeSinceAlarmStart < 2.0 , currentVolume > self . lastVolume {
144+ if volumeDifference <= 0.15 , timeSinceAlarmStart < 1.5 {
145+ self . lastVolume = currentVolume
146+ return
147+ }
148+ }
185149
186- alarmStartTime = nil
150+ self . recordVolumeChange ( currentVolume : currentVolume , timestamp : now )
187151
188- stopMonitoring ( )
152+ if timeSinceAlarmStart > self . volumeButtonActivationDelay {
153+ if let lastPress = self . lastVolumeButtonPressTime {
154+ let timeSinceLastPress = now. timeIntervalSince ( lastPress)
155+ if timeSinceLastPress < self . volumeButtonCooldown {
156+ self . lastVolume = currentVolume
157+ return
158+ }
159+ }
189160
190- recentVolumeChanges. removeAll ( )
191- lastSignificantVolumeChange = nil
192- volumeChangePattern. removeAll ( )
161+ if self . isLikelyVolumeButtonPress ( volumeDifference: volumeDifference, timestamp: now) {
162+ self . snoozeActiveAlarm ( )
163+ }
164+ }
165+ }
166+ self . lastVolume = currentVolume
167+ }
193168 }
194169
195170 func stopMonitoring( ) {
196171 guard isMonitoring else { return }
197172
198- isMonitoring = false
173+ LogManager . shared. log ( category: . volumeButtonSnooze, message: " Invalidating volume observer. " )
174+
175+ // Invalidate the observer to stop receiving notifications and prevent memory leaks.
176+ volumeObserver? . invalidate ( )
177+ volumeObserver = nil
199178
200- volumeMonitoringTimer ? . invalidate ( )
201- volumeMonitoringTimer = nil
179+ isMonitoring = false
180+ lastVolume = 0.0 // Reset for the next alarm.
202181 }
203182}
0 commit comments