From 1e5d2d1a7634af60920edbd971567375bc984498 Mon Sep 17 00:00:00 2001 From: codebymini Date: Sun, 23 Nov 2025 21:58:17 +0100 Subject: [PATCH] Add snooze with bluetooth play-pause control --- LoopFollow/Controllers/AlarmSound.swift | 9 +- .../Controllers/VolumeButtonHandler.swift | 123 ++++++++++++++++++ 2 files changed, 128 insertions(+), 4 deletions(-) diff --git a/LoopFollow/Controllers/AlarmSound.swift b/LoopFollow/Controllers/AlarmSound.swift index 13657d2bd..b811850ea 100644 --- a/LoopFollow/Controllers/AlarmSound.swift +++ b/LoopFollow/Controllers/AlarmSound.swift @@ -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 @@ -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 @@ -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 @@ -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)") } diff --git a/LoopFollow/Controllers/VolumeButtonHandler.swift b/LoopFollow/Controllers/VolumeButtonHandler.swift index 7f4d16de6..caa388d9f 100644 --- a/LoopFollow/Controllers/VolumeButtonHandler.swift +++ b/LoopFollow/Controllers/VolumeButtonHandler.swift @@ -4,6 +4,7 @@ import AVFoundation import Combine import Foundation +import MediaPlayer import UIKit class VolumeButtonHandler: NSObject { @@ -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() override private init() { @@ -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 } @@ -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. }