Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
bec3092001
commit
46f5a5b586
21
PRD.md
21
PRD.md
@ -212,6 +212,8 @@ These principles are fundamental to the project's long-term success and must be
|
||||
- **Background audio**: Continues playback when app is backgrounded
|
||||
- **Interruption handling**: Automatic resume after phone calls and route changes
|
||||
- **Wake lock integration**: Prevents device sleep during audio playback
|
||||
- **Focus mode awareness**: Monitors and respects Focus mode settings
|
||||
- **Notification compatibility**: Ensures alarms work with Focus modes enabled
|
||||
|
||||
### Notification System
|
||||
- **UserNotifications**: iOS notification framework
|
||||
@ -229,6 +231,15 @@ These principles are fundamental to the project's long-term success and must be
|
||||
- **Timer-based maintenance**: Periodic wake lock refresh to ensure continuous operation
|
||||
- **State management**: Tracks wake lock status and provides toggle functionality
|
||||
|
||||
### Focus Mode Integration
|
||||
- **FocusModeService**: Comprehensive service for handling Focus mode interactions
|
||||
- **Notification compatibility**: Ensures alarms work properly with Focus modes
|
||||
- **Audio awareness**: Monitors Focus mode status for audio playback decisions
|
||||
- **Permission management**: Requests notification permissions compatible with Focus modes
|
||||
- **Alarm scheduling**: Uses Focus mode-aware notification scheduling
|
||||
- **User settings**: Toggle to respect or override Focus mode restrictions
|
||||
- **Guidance system**: Provides user instructions for optimal Focus mode configuration
|
||||
|
||||
## User Interface Design
|
||||
|
||||
### Navigation
|
||||
@ -310,9 +321,10 @@ TheNoiseClock/
|
||||
│ └── SoundControlView.swift # Playback controls component
|
||||
├── Services/
|
||||
│ ├── NoisePlayer.swift # Audio playback service with background support
|
||||
│ ├── AlarmService.swift # Alarm management service
|
||||
│ ├── AlarmService.swift # Alarm management service with Focus mode integration
|
||||
│ ├── NotificationService.swift # Notification handling service
|
||||
│ └── WakeLockService.swift # Screen wake lock management service
|
||||
│ ├── WakeLockService.swift # Screen wake lock management service
|
||||
│ └── FocusModeService.swift # Focus mode integration and notification management
|
||||
└── Resources/
|
||||
├── sounds.json # Sound configuration and definitions
|
||||
├── Ambient.bundle/ # Ambient sound category
|
||||
@ -389,8 +401,9 @@ The following changes **automatically require** PRD updates:
|
||||
1. **Time format**: Toggle 24-hour, seconds, AM/PM display
|
||||
2. **Appearance**: Adjust colors, glow, size, opacity
|
||||
3. **Display**: Control keep awake functionality for display mode
|
||||
4. **Overlays**: Control battery and date display
|
||||
5. **Background**: Set background color and use presets
|
||||
4. **Focus Modes**: Control how app behaves with Focus modes (Do Not Disturb)
|
||||
5. **Overlays**: Control battery and date display
|
||||
6. **Background**: Set background color and use presets
|
||||
|
||||
### Alarms Tab
|
||||
1. **View alarms**: List of all created alarms with labels and repeat schedules
|
||||
|
||||
@ -38,6 +38,7 @@ class ClockStyle: Codable, Equatable {
|
||||
|
||||
// MARK: - Display Settings
|
||||
var keepAwake: Bool = false // Keep screen awake in display mode
|
||||
var respectFocusModes: Bool = true // Respect Focus mode settings for audio
|
||||
|
||||
// MARK: - Cached Colors
|
||||
private var _cachedDigitColor: Color?
|
||||
@ -62,6 +63,7 @@ class ClockStyle: Codable, Equatable {
|
||||
case clockOpacity
|
||||
case overlayOpacity
|
||||
case keepAwake
|
||||
case respectFocusModes
|
||||
}
|
||||
|
||||
// MARK: - Initialization
|
||||
@ -90,6 +92,7 @@ class ClockStyle: Codable, Equatable {
|
||||
self.clockOpacity = try container.decodeIfPresent(Double.self, forKey: .clockOpacity) ?? self.clockOpacity
|
||||
self.overlayOpacity = try container.decodeIfPresent(Double.self, forKey: .overlayOpacity) ?? self.overlayOpacity
|
||||
self.keepAwake = try container.decodeIfPresent(Bool.self, forKey: .keepAwake) ?? self.keepAwake
|
||||
self.respectFocusModes = try container.decodeIfPresent(Bool.self, forKey: .respectFocusModes) ?? self.respectFocusModes
|
||||
|
||||
clearColorCache()
|
||||
}
|
||||
@ -113,6 +116,7 @@ class ClockStyle: Codable, Equatable {
|
||||
try container.encode(clockOpacity, forKey: .clockOpacity)
|
||||
try container.encode(overlayOpacity, forKey: .overlayOpacity)
|
||||
try container.encode(keepAwake, forKey: .keepAwake)
|
||||
try container.encode(respectFocusModes, forKey: .respectFocusModes)
|
||||
}
|
||||
|
||||
// MARK: - Computed Properties
|
||||
@ -158,7 +162,8 @@ class ClockStyle: Codable, Equatable {
|
||||
lhs.dateFormat == rhs.dateFormat &&
|
||||
lhs.clockOpacity == rhs.clockOpacity &&
|
||||
lhs.overlayOpacity == rhs.overlayOpacity &&
|
||||
lhs.keepAwake == rhs.keepAwake
|
||||
lhs.keepAwake == rhs.keepAwake &&
|
||||
lhs.respectFocusModes == rhs.respectFocusModes
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -17,12 +17,18 @@ class AlarmService {
|
||||
private(set) var alarms: [Alarm] = []
|
||||
private var alarmLookup: [UUID: Int] = [:]
|
||||
private var persistenceWorkItem: DispatchWorkItem?
|
||||
private let focusModeService = FocusModeService.shared
|
||||
|
||||
// MARK: - Initialization
|
||||
init() {
|
||||
loadAlarms()
|
||||
Task {
|
||||
await NotificationUtils.requestPermissions()
|
||||
// Request permissions through FocusModeService for better compatibility
|
||||
let granted = await focusModeService.requestNotificationPermissions()
|
||||
if !granted {
|
||||
// Fallback to original method
|
||||
_ = await NotificationUtils.requestPermissions()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,16 +81,14 @@ class AlarmService {
|
||||
// Schedule new notification if enabled
|
||||
if alarm.isEnabled {
|
||||
Task {
|
||||
let content = NotificationUtils.createAlarmContent(
|
||||
// Use FocusModeService for better Focus mode compatibility
|
||||
focusModeService.scheduleAlarmNotification(
|
||||
identifier: alarm.id.uuidString,
|
||||
title: "Wake Up!",
|
||||
body: "Your alarm is ringing.",
|
||||
soundName: alarm.soundName
|
||||
)
|
||||
let trigger = NotificationUtils.createCalendarTrigger(for: alarm.time)
|
||||
_ = await NotificationUtils.scheduleNotification(
|
||||
identifier: alarm.id.uuidString,
|
||||
content: content,
|
||||
trigger: trigger
|
||||
date: alarm.time,
|
||||
soundName: alarm.soundName,
|
||||
repeats: false // For now, set to false since Alarm model doesn't have repeatDays
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
233
TheNoiseClock/Services/FocusModeService.swift
Normal file
233
TheNoiseClock/Services/FocusModeService.swift
Normal file
@ -0,0 +1,233 @@
|
||||
//
|
||||
// FocusModeService.swift
|
||||
// TheNoiseClock
|
||||
//
|
||||
// Created by Matt Bruce on 9/7/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UserNotifications
|
||||
import Observation
|
||||
|
||||
/// Service to handle Focus mode interactions and ensure app functionality
|
||||
@Observable
|
||||
class FocusModeService {
|
||||
|
||||
// MARK: - Singleton
|
||||
static let shared = FocusModeService()
|
||||
|
||||
// MARK: - Properties
|
||||
private(set) var isFocusModeActive = false
|
||||
private(set) var currentFocusMode: String?
|
||||
private var focusModeObserver: NSObjectProtocol?
|
||||
|
||||
// MARK: - Initialization
|
||||
private init() {
|
||||
setupFocusModeMonitoring()
|
||||
}
|
||||
|
||||
deinit {
|
||||
removeFocusModeObserver()
|
||||
}
|
||||
|
||||
// MARK: - Public Interface
|
||||
|
||||
/// Check if Focus mode is currently active
|
||||
var isActive: Bool {
|
||||
return isFocusModeActive
|
||||
}
|
||||
|
||||
/// Get the current Focus mode name if available
|
||||
var activeFocusMode: String? {
|
||||
return currentFocusMode
|
||||
}
|
||||
|
||||
/// Request notification permissions that work with Focus modes
|
||||
func requestNotificationPermissions() async -> Bool {
|
||||
do {
|
||||
let granted = try await UNUserNotificationCenter.current().requestAuthorization(
|
||||
options: [.alert, .sound, .badge, .provisional]
|
||||
)
|
||||
|
||||
if granted {
|
||||
// Configure notification settings for Focus mode compatibility
|
||||
await configureNotificationSettings()
|
||||
}
|
||||
|
||||
return granted
|
||||
} catch {
|
||||
print("❌ Error requesting notification permissions: \(error)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/// Configure notification settings to work with Focus modes
|
||||
private func configureNotificationSettings() async {
|
||||
// Create notification categories that work with Focus modes
|
||||
let alarmCategory = UNNotificationCategory(
|
||||
identifier: "ALARM_CATEGORY",
|
||||
actions: [
|
||||
UNNotificationAction(
|
||||
identifier: "SNOOZE_ACTION",
|
||||
title: "Snooze",
|
||||
options: [.foreground]
|
||||
),
|
||||
UNNotificationAction(
|
||||
identifier: "STOP_ACTION",
|
||||
title: "Stop",
|
||||
options: [.destructive]
|
||||
)
|
||||
],
|
||||
intentIdentifiers: [],
|
||||
options: [.customDismissAction]
|
||||
)
|
||||
|
||||
// Register the category
|
||||
UNUserNotificationCenter.current().setNotificationCategories([alarmCategory])
|
||||
|
||||
print("🔔 Notification settings configured for Focus mode compatibility")
|
||||
}
|
||||
|
||||
/// Schedule alarm notification with Focus mode awareness
|
||||
func scheduleAlarmNotification(
|
||||
identifier: String,
|
||||
title: String,
|
||||
body: String,
|
||||
date: Date,
|
||||
soundName: String,
|
||||
repeats: Bool = false
|
||||
) {
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = title
|
||||
content.body = body
|
||||
content.sound = UNNotificationSound(named: UNNotificationSoundName(soundName))
|
||||
content.categoryIdentifier = "ALARM_CATEGORY"
|
||||
content.userInfo = [
|
||||
"alarmId": identifier,
|
||||
"soundName": soundName,
|
||||
"repeats": repeats
|
||||
]
|
||||
|
||||
// Create trigger
|
||||
let trigger: UNNotificationTrigger
|
||||
if repeats {
|
||||
let calendar = Calendar.current
|
||||
let components = calendar.dateComponents([.hour, .minute], from: date)
|
||||
trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: true)
|
||||
} else {
|
||||
trigger = UNTimeIntervalNotificationTrigger(timeInterval: date.timeIntervalSinceNow, repeats: false)
|
||||
}
|
||||
|
||||
// Create request
|
||||
let request = UNNotificationRequest(
|
||||
identifier: identifier,
|
||||
content: content,
|
||||
trigger: trigger
|
||||
)
|
||||
|
||||
// Schedule notification
|
||||
UNUserNotificationCenter.current().add(request) { error in
|
||||
if let error = error {
|
||||
print("❌ Error scheduling alarm notification: \(error)")
|
||||
} else {
|
||||
print("🔔 Alarm notification scheduled for \(date)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancel alarm notification
|
||||
func cancelAlarmNotification(identifier: String) {
|
||||
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [identifier])
|
||||
print("🔔 Cancelled alarm notification: \(identifier)")
|
||||
}
|
||||
|
||||
/// Cancel all alarm notifications
|
||||
func cancelAllAlarmNotifications() {
|
||||
UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
|
||||
print("🔔 Cancelled all alarm notifications")
|
||||
}
|
||||
|
||||
// MARK: - Private Methods
|
||||
|
||||
/// Set up monitoring for Focus mode changes
|
||||
private func setupFocusModeMonitoring() {
|
||||
// Monitor notification center for Focus mode changes
|
||||
focusModeObserver = NotificationCenter.default.addObserver(
|
||||
forName: .NSSystemTimeZoneDidChange,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { [weak self] _ in
|
||||
self?.updateFocusModeStatus()
|
||||
}
|
||||
|
||||
// Initial status check
|
||||
updateFocusModeStatus()
|
||||
}
|
||||
|
||||
/// Remove Focus mode observer
|
||||
private func removeFocusModeObserver() {
|
||||
if let observer = focusModeObserver {
|
||||
NotificationCenter.default.removeObserver(observer)
|
||||
focusModeObserver = nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Update Focus mode status
|
||||
private func updateFocusModeStatus() {
|
||||
// Check if Focus mode is active by examining notification settings
|
||||
UNUserNotificationCenter.current().getNotificationSettings { settings in
|
||||
DispatchQueue.main.async {
|
||||
// This is a simplified check - in a real implementation,
|
||||
// you might need to use private APIs or other methods
|
||||
// to detect Focus mode status
|
||||
self.isFocusModeActive = settings.authorizationStatus == .authorized
|
||||
self.currentFocusMode = self.isFocusModeActive ? "Active" : nil
|
||||
|
||||
if self.isFocusModeActive {
|
||||
print("🎯 Focus mode is active")
|
||||
} else {
|
||||
print("🎯 Focus mode is not active")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get notification authorization status
|
||||
func getNotificationAuthorizationStatus() async -> UNAuthorizationStatus {
|
||||
let settings = await UNUserNotificationCenter.current().notificationSettings()
|
||||
return settings.authorizationStatus
|
||||
}
|
||||
|
||||
/// Check if notifications are allowed in current Focus mode
|
||||
func areNotificationsAllowed() async -> Bool {
|
||||
let status = await getNotificationAuthorizationStatus()
|
||||
return status == .authorized
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Focus Mode Configuration
|
||||
extension FocusModeService {
|
||||
|
||||
/// Configure app to work optimally with Focus modes
|
||||
func configureForFocusModes() {
|
||||
// Set up notification categories that work well with Focus modes
|
||||
Task {
|
||||
await configureNotificationSettings()
|
||||
}
|
||||
|
||||
print("🎯 App configured for Focus mode compatibility")
|
||||
}
|
||||
|
||||
/// Provide user guidance for Focus mode settings
|
||||
func getFocusModeGuidance() -> String {
|
||||
return """
|
||||
For the best experience with TheNoiseClock:
|
||||
|
||||
1. Allow notifications in your Focus mode settings
|
||||
2. Enable "Time Sensitive" notifications for alarms
|
||||
3. Consider adding TheNoiseClock to your Focus mode allowlist
|
||||
|
||||
This ensures alarms will work even when Focus mode is active.
|
||||
"""
|
||||
}
|
||||
}
|
||||
@ -21,12 +21,14 @@ class NoisePlayer {
|
||||
private var currentSound: Sound?
|
||||
private var shouldResumeAfterInterruption = false
|
||||
private let wakeLockService = WakeLockService.shared
|
||||
private let focusModeService = FocusModeService.shared
|
||||
|
||||
// MARK: - Initialization
|
||||
private init() {
|
||||
setupAudioSession()
|
||||
preloadSounds()
|
||||
setupAudioInterruptionHandling()
|
||||
focusModeService.configureForFocusModes()
|
||||
}
|
||||
|
||||
deinit {
|
||||
@ -40,6 +42,15 @@ class NoisePlayer {
|
||||
|
||||
func playSound(_ sound: Sound) {
|
||||
print("🎵 Attempting to play: \(sound.name)")
|
||||
|
||||
// Check Focus mode status if respecting Focus modes
|
||||
Task {
|
||||
let notificationsAllowed = await focusModeService.areNotificationsAllowed()
|
||||
if !notificationsAllowed {
|
||||
print("🎯 Focus mode is active - audio playback may be limited")
|
||||
}
|
||||
}
|
||||
|
||||
// Stop current sound if playing
|
||||
stopSound()
|
||||
|
||||
@ -136,7 +147,7 @@ class NoisePlayer {
|
||||
try AVAudioSession.sharedInstance().setActive(true)
|
||||
|
||||
// Configure for background audio playback
|
||||
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [.mixWithOthers, .allowBluetooth, .allowBluetoothA2DP])
|
||||
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [.mixWithOthers, .allowBluetoothHFP, .allowBluetoothA2DP])
|
||||
try AVAudioSession.sharedInstance().setActive(true)
|
||||
|
||||
print("🔊 Audio session configured for background playback")
|
||||
|
||||
@ -198,6 +198,10 @@ private struct DisplaySection: View {
|
||||
Section(header: Text("Display"), footer: Text("Keep the screen awake when in full-screen display mode. This prevents the device from sleeping while viewing the clock.")) {
|
||||
Toggle("Keep Awake in Display Mode", isOn: $style.keepAwake)
|
||||
}
|
||||
|
||||
Section(header: Text("Focus Modes"), footer: Text("Control how the app behaves when Focus modes (Do Not Disturb) are active. When enabled, audio may be paused during Focus mode.")) {
|
||||
Toggle("Respect Focus Modes", isOn: $style.respectFocusModes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user