mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Working notification
This commit is contained in:
parent
fa1bd97d84
commit
bb2284bafc
10 changed files with 367 additions and 372 deletions
|
|
@ -25,6 +25,7 @@ struct ContentView: View {
|
|||
@ObservedObject var statsManager = StatsManager.shared
|
||||
@ObservedObject var recordingManager = ScreenRecordingManager.shared
|
||||
@ObservedObject var privacyManager = PrivacyIndicatorManager.shared
|
||||
@ObservedObject var doNotDisturbManager = DoNotDisturbManager.shared
|
||||
@ObservedObject var lockScreenManager = LockScreenManager.shared
|
||||
|
||||
@Default(.enableStatsFeature) var enableStatsFeature
|
||||
|
|
@ -412,6 +413,8 @@ struct ContentView: View {
|
|||
TimerLiveActivity()
|
||||
} else if (!coordinator.expandingView.show || coordinator.expandingView.type == .recording) && vm.notchState == .closed && (recordingManager.isRecording || !recordingManager.isRecorderIdle) && Defaults[.enableScreenRecordingDetection] && !vm.hideOnClosed {
|
||||
RecordingLiveActivity()
|
||||
} else if (!coordinator.expandingView.show || coordinator.expandingView.type == .doNotDisturb) && vm.notchState == .closed && doNotDisturbManager.isDoNotDisturbActive && Defaults[.enableDoNotDisturbDetection] && Defaults[.showDoNotDisturbIndicator] && !vm.hideOnClosed && !lockScreenManager.isLocked {
|
||||
DoNotDisturbLiveActivity()
|
||||
} else if (!coordinator.expandingView.show || coordinator.expandingView.type == .lockScreen) && vm.notchState == .closed && (lockScreenManager.isLocked || !lockScreenManager.isLockIdle) && Defaults[.enableLockScreenLiveActivity] && !vm.hideOnClosed {
|
||||
LockScreenLiveActivity()
|
||||
} else if (!coordinator.expandingView.show || coordinator.expandingView.type == .privacy) && vm.notchState == .closed && privacyManager.hasAnyIndicator && (Defaults[.enableCameraDetection] || Defaults[.enableMicrophoneDetection]) && !vm.hideOnClosed {
|
||||
|
|
|
|||
|
|
@ -379,6 +379,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
ScreenRecordingManager.shared.startMonitoring()
|
||||
}
|
||||
|
||||
// Setup Do Not Disturb Manager
|
||||
if Defaults[.enableDoNotDisturbDetection] {
|
||||
dndManager.startMonitoring()
|
||||
}
|
||||
|
||||
// Setup Privacy Indicator Manager (camera and microphone monitoring)
|
||||
PrivacyIndicatorManager.shared.startMonitoring()
|
||||
|
||||
|
|
@ -435,6 +440,16 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
}
|
||||
}.store(in: &cancellables)
|
||||
|
||||
Defaults.publisher(.enableDoNotDisturbDetection, options: []).sink { [weak self] _ in
|
||||
guard let self else { return }
|
||||
|
||||
if Defaults[.enableDoNotDisturbDetection] {
|
||||
self.dndManager.startMonitoring()
|
||||
} else {
|
||||
self.dndManager.stopMonitoring()
|
||||
}
|
||||
}.store(in: &cancellables)
|
||||
|
||||
// Note: Polling setting removed - now uses event-driven private API detection only
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ enum SneakContentType {
|
|||
case download
|
||||
case timer
|
||||
case recording
|
||||
case doNotDisturb
|
||||
case bluetoothAudio
|
||||
case privacy
|
||||
case lockScreen
|
||||
|
|
|
|||
|
|
@ -1805,6 +1805,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Active - No Focus" : {
|
||||
|
||||
},
|
||||
"Active - No Recording" : {
|
||||
"comment" : "A description of the recording detection feature, indicating that recording is not currently active.",
|
||||
|
|
@ -6769,6 +6772,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Do Not Disturb" : {
|
||||
|
||||
},
|
||||
"Download" : {
|
||||
"localizations" : {
|
||||
|
|
@ -8491,6 +8497,13 @@
|
|||
},
|
||||
"Fit to Notch" : {
|
||||
|
||||
},
|
||||
"Focus mode active" : {
|
||||
"comment" : "An accessibility label for the focus indicator.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Focus Status" : {
|
||||
|
||||
},
|
||||
"Free · %@" : {
|
||||
"comment" : "A label displaying the amount of free storage on a disk device. The argument is the number of free bytes on the disk.",
|
||||
|
|
@ -10478,6 +10491,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Listens for Focus session changes via distributed notifications" : {
|
||||
|
||||
},
|
||||
"LIVE" : {
|
||||
"comment" : "A label indicating that the music is live.",
|
||||
|
|
@ -15692,7 +15708,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Screen Recording Detection" : {
|
||||
"Screen Recording" : {
|
||||
|
||||
},
|
||||
"Screenshot options" : {
|
||||
|
|
|
|||
119
DynamicIsland/components/Focus/DoNotDisturbLiveActivity.swift
Normal file
119
DynamicIsland/components/Focus/DoNotDisturbLiveActivity.swift
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
//
|
||||
// DoNotDisturbLiveActivity.swift
|
||||
// DynamicIsland
|
||||
//
|
||||
// Renders the closed-notch Focus indicator with a purple moon badge.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct DoNotDisturbLiveActivity: View {
|
||||
@EnvironmentObject var vm: DynamicIslandViewModel
|
||||
@ObservedObject var manager = DoNotDisturbManager.shared
|
||||
@State private var isExpanded = false
|
||||
|
||||
private let focusColor = Color.purple
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
leadingWing
|
||||
.frame(width: leadingWingWidth, height: wingHeight)
|
||||
|
||||
Rectangle()
|
||||
.fill(Color.black)
|
||||
.frame(width: vm.closedNotchSize.width)
|
||||
|
||||
trailingWing
|
||||
.frame(width: trailingWingWidth, height: wingHeight)
|
||||
}
|
||||
.frame(height: vm.effectiveClosedNotchHeight)
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel(accessibilityDescription)
|
||||
.onAppear {
|
||||
withAnimation(.smooth(duration: 0.35)) {
|
||||
isExpanded = true
|
||||
}
|
||||
}
|
||||
.onChange(of: manager.isDoNotDisturbActive) { _, isActive in
|
||||
withAnimation(.smooth(duration: 0.35)) {
|
||||
isExpanded = isActive
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var wingHeight: CGFloat {
|
||||
max(vm.effectiveClosedNotchHeight - 10, 20)
|
||||
}
|
||||
|
||||
private var leadingWingWidth: CGFloat {
|
||||
isExpanded ? max(vm.effectiveClosedNotchHeight - 10, 20) : 0
|
||||
}
|
||||
|
||||
private var trailingWingWidth: CGFloat {
|
||||
guard isExpanded else { return 0 }
|
||||
return max(vm.closedNotchSize.width * 0.65, 120)
|
||||
}
|
||||
|
||||
private var focusSymbol: String {
|
||||
if let symbol = FocusModeType(rawValue: manager.currentFocusModeIdentifier)?.sfSymbol {
|
||||
return symbol
|
||||
}
|
||||
return FocusModeType.doNotDisturb.sfSymbol
|
||||
}
|
||||
|
||||
private var focusLabel: String {
|
||||
if !manager.currentFocusModeName.isEmpty {
|
||||
return manager.currentFocusModeName
|
||||
}
|
||||
return FocusModeType(rawValue: manager.currentFocusModeIdentifier)?.displayName ?? "Do Not Disturb"
|
||||
}
|
||||
|
||||
private var accessibilityDescription: String {
|
||||
"Focus mode active: \(focusLabel)"
|
||||
}
|
||||
|
||||
private var leadingWing: some View {
|
||||
Color.clear
|
||||
.background {
|
||||
if isExpanded {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 6, style: .continuous)
|
||||
.fill(focusColor.opacity(0.18))
|
||||
|
||||
Image(systemName: focusSymbol)
|
||||
.font(.system(size: 12, weight: .semibold))
|
||||
.foregroundColor(focusColor)
|
||||
}
|
||||
.frame(width: leadingWingWidth, height: wingHeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var trailingWing: some View {
|
||||
Color.clear
|
||||
.background {
|
||||
if isExpanded {
|
||||
HStack {
|
||||
Text(focusLabel)
|
||||
.font(.system(size: 12, weight: .semibold, design: .rounded))
|
||||
.foregroundColor(focusColor)
|
||||
.lineLimit(1)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
.background(
|
||||
Capsule(style: .continuous)
|
||||
.fill(focusColor.opacity(0.18))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
DoNotDisturbLiveActivity()
|
||||
.environmentObject(DynamicIslandViewModel())
|
||||
.frame(width: 320, height: 54)
|
||||
.background(Color.black)
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ struct DynamicIslandHeader: View {
|
|||
@ObservedObject var clipboardManager = ClipboardManager.shared
|
||||
@ObservedObject var tvm = TrayDrop.shared
|
||||
@ObservedObject var timerManager = TimerManager.shared
|
||||
@ObservedObject var doNotDisturbManager = DoNotDisturbManager.shared
|
||||
@State private var showClipboardPopover = false
|
||||
@State private var showColorPickerPopover = false
|
||||
@State private var showTimerPopover = false
|
||||
|
|
@ -196,6 +197,12 @@ struct DynamicIslandHeader: View {
|
|||
RecordingIndicator()
|
||||
.frame(width: 30, height: 30) // Same size as other header elements
|
||||
}
|
||||
|
||||
if Defaults[.enableDoNotDisturbDetection] && Defaults[.showDoNotDisturbIndicator] && doNotDisturbManager.isDoNotDisturbActive {
|
||||
FocusIndicator()
|
||||
.frame(width: 30, height: 30)
|
||||
.transition(.opacity)
|
||||
}
|
||||
|
||||
if Defaults[.showBatteryIndicator] {
|
||||
DynamicIslandBatteryView(
|
||||
|
|
|
|||
|
|
@ -157,6 +157,7 @@ struct GeneralSettings: View {
|
|||
@ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
|
||||
@ObservedObject var recordingManager = ScreenRecordingManager.shared
|
||||
@ObservedObject var privacyManager = PrivacyIndicatorManager.shared
|
||||
@ObservedObject var doNotDisturbManager = DoNotDisturbManager.shared
|
||||
|
||||
@Default(.mirrorShape) var mirrorShape
|
||||
@Default(.showEmojis) var showEmojis
|
||||
|
|
@ -172,6 +173,8 @@ struct GeneralSettings: View {
|
|||
@Default(.openNotchOnHover) var openNotchOnHover
|
||||
@Default(.enableScreenRecordingDetection) var enableScreenRecordingDetection
|
||||
@Default(.showRecordingIndicator) var showRecordingIndicator
|
||||
@Default(.enableDoNotDisturbDetection) var enableDoNotDisturbDetection
|
||||
@Default(.showDoNotDisturbIndicator) var showDoNotDisturbIndicator
|
||||
@Default(.enableMinimalisticUI) var enableMinimalisticUI
|
||||
@Default(.lockScreenGlassStyle) var lockScreenGlassStyle
|
||||
@Default(.lockScreenShowAppIcon) var lockScreenShowAppIcon
|
||||
|
|
@ -235,12 +238,12 @@ struct GeneralSettings: View {
|
|||
|
||||
Section {
|
||||
Defaults.Toggle("Enable Screen Recording Detection", key: .enableScreenRecordingDetection)
|
||||
|
||||
|
||||
Defaults.Toggle("Show Recording Indicator", key: .showRecordingIndicator)
|
||||
.disabled(!enableScreenRecordingDetection)
|
||||
|
||||
|
||||
// Note: Polling removed - now uses event-driven private API detection
|
||||
|
||||
|
||||
if recordingManager.isMonitoring {
|
||||
HStack {
|
||||
Text("Detection Status")
|
||||
|
|
@ -260,11 +263,48 @@ struct GeneralSettings: View {
|
|||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Screen Recording Detection")
|
||||
Text("Screen Recording")
|
||||
} footer: {
|
||||
Text("Uses event-driven private API for real-time screen recording detection")
|
||||
}
|
||||
|
||||
|
||||
Section {
|
||||
Defaults.Toggle("Enable Focus Detection", key: .enableDoNotDisturbDetection)
|
||||
|
||||
Defaults.Toggle("Show Focus Indicator", key: .showDoNotDisturbIndicator)
|
||||
.disabled(!enableDoNotDisturbDetection)
|
||||
|
||||
if doNotDisturbManager.isMonitoring {
|
||||
HStack {
|
||||
Text("Focus Status")
|
||||
Spacer()
|
||||
if doNotDisturbManager.isDoNotDisturbActive {
|
||||
HStack(spacing: 4) {
|
||||
Circle()
|
||||
.fill(Color.purple)
|
||||
.frame(width: 8, height: 8)
|
||||
Text(doNotDisturbManager.currentFocusModeName.isEmpty ? "Focus Enabled" : doNotDisturbManager.currentFocusModeName)
|
||||
.foregroundColor(.purple)
|
||||
}
|
||||
} else {
|
||||
Text("Active - No Focus")
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
HStack {
|
||||
Text("Focus Status")
|
||||
Spacer()
|
||||
Text("Disabled")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Do Not Disturb")
|
||||
} footer: {
|
||||
Text("Listens for Focus session changes via distributed notifications")
|
||||
}
|
||||
|
||||
Section {
|
||||
Defaults.Toggle("Enable Camera Detection", key: .enableCameraDetection)
|
||||
Defaults.Toggle("Enable Microphone Detection", key: .enableMicrophoneDetection)
|
||||
|
|
|
|||
45
DynamicIsland/components/UI/FocusIndicator.swift
Normal file
45
DynamicIsland/components/UI/FocusIndicator.swift
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
//
|
||||
// FocusIndicator.swift
|
||||
// DynamicIsland
|
||||
//
|
||||
// Small purple moon badge shown while Focus mode is active.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct FocusIndicator: View {
|
||||
@ObservedObject var manager = DoNotDisturbManager.shared
|
||||
|
||||
private let focusColor = Color.purple
|
||||
|
||||
var body: some View {
|
||||
Capsule()
|
||||
.fill(Color.black)
|
||||
.overlay {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(focusColor.opacity(0.2))
|
||||
.frame(width: 20, height: 20)
|
||||
|
||||
Image(systemName: focusSymbol)
|
||||
.font(.system(size: 12, weight: .semibold))
|
||||
.foregroundStyle(focusColor)
|
||||
}
|
||||
}
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel("Focus mode active")
|
||||
}
|
||||
|
||||
private var focusSymbol: String {
|
||||
if let symbol = FocusModeType(rawValue: manager.currentFocusModeIdentifier)?.sfSymbol {
|
||||
return symbol
|
||||
}
|
||||
return FocusModeType.doNotDisturb.sfSymbol
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
FocusIndicator()
|
||||
.frame(width: 30, height: 30)
|
||||
.background(Color.gray.opacity(0.2))
|
||||
}
|
||||
|
|
@ -2,398 +2,143 @@
|
|||
// DoNotDisturbManager.swift
|
||||
// DynamicIsland
|
||||
//
|
||||
// Created for Do Not Disturb / Focus Mode detection
|
||||
// Monitors macOS Focus Mode state changes via distributed notifications
|
||||
// Replaces the legacy polling-based Focus detection with
|
||||
// NSDistributedNotificationCenter-backed monitoring.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import AppKit
|
||||
import Combine
|
||||
import Defaults
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import UserNotifications // For notification permission checks
|
||||
import ApplicationServices // For AXIsProcessTrusted
|
||||
|
||||
/// Manages detection and state tracking for macOS Focus Mode (Do Not Disturb)
|
||||
class DoNotDisturbManager: ObservableObject {
|
||||
final class DoNotDisturbManager: ObservableObject {
|
||||
static let shared = DoNotDisturbManager()
|
||||
|
||||
// MARK: - Published Properties
|
||||
@Published var isDoNotDisturbActive: Bool = false
|
||||
|
||||
@Published private(set) var isMonitoring = false
|
||||
@Published var isDoNotDisturbActive = false
|
||||
@Published var currentFocusModeName: String = ""
|
||||
@Published var currentFocusModeIdentifier: String = ""
|
||||
|
||||
// MARK: - Private Properties
|
||||
private var observers: [NSObjectProtocol] = []
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
// MARK: - Initialization
|
||||
private init() {
|
||||
print("🔔 [DoNotDisturbManager] Initializing...")
|
||||
checkPermissions()
|
||||
setupFocusModeObservers()
|
||||
checkInitialState()
|
||||
}
|
||||
|
||||
|
||||
private let notificationCenter = DistributedNotificationCenter.default()
|
||||
private let metadataExtractionQueue = DispatchQueue(label: "com.dynamicisland.focus.metadata", qos: .userInitiated)
|
||||
private var hasLoggedPayload = false
|
||||
|
||||
private init() {}
|
||||
|
||||
deinit {
|
||||
cleanup()
|
||||
stopMonitoring()
|
||||
}
|
||||
|
||||
// MARK: - Permission Checks
|
||||
|
||||
/// Checks if the app has necessary permissions for notification observation
|
||||
private func checkPermissions() {
|
||||
print("🔔 [DoNotDisturbManager] ==========================================")
|
||||
print("🔔 [DoNotDisturbManager] 🔐 CHECKING PERMISSIONS & CAPABILITIES")
|
||||
print("🔔 [DoNotDisturbManager] ==========================================")
|
||||
|
||||
// Check if running in sandbox
|
||||
let isSandboxed = ProcessInfo.processInfo.environment["APP_SANDBOX_CONTAINER_ID"] != nil
|
||||
print(" 📦 Sandboxed: \(isSandboxed ? "YES ⚠️" : "NO ✅")")
|
||||
|
||||
// Check notification center permissions
|
||||
let notificationCenter = UNUserNotificationCenter.current()
|
||||
notificationCenter.getNotificationSettings { settings in
|
||||
print(" 🔔 Notification Authorization: \(settings.authorizationStatus.rawValue)")
|
||||
print(" - .notDetermined = 0")
|
||||
print(" - .denied = 1")
|
||||
print(" - .authorized = 2")
|
||||
print(" - .provisional = 3")
|
||||
print(" - .ephemeral = 4")
|
||||
}
|
||||
|
||||
// Check accessibility permissions
|
||||
let trusted = AXIsProcessTrusted()
|
||||
print(" ♿️ Accessibility Trusted: \(trusted ? "YES ✅" : "NO ⚠️")")
|
||||
if !trusted {
|
||||
print(" ⚠️ App needs Accessibility permission!")
|
||||
print(" ⚠️ Go to: System Settings > Privacy & Security > Accessibility")
|
||||
}
|
||||
|
||||
// Check entitlements
|
||||
print(" 📝 Checking Entitlements...")
|
||||
if let entitlements = Bundle.main.object(forInfoDictionaryKey: "Entitlements") as? [String: Any] {
|
||||
print(" Entitlements found: \(entitlements.keys)")
|
||||
} else {
|
||||
print(" ⚠️ No entitlements in Info.plist")
|
||||
}
|
||||
|
||||
// Test if we can observe notifications at all
|
||||
print(" 🧪 Testing notification observation capability...")
|
||||
testNotificationObservation()
|
||||
|
||||
print("🔔 [DoNotDisturbManager] ==========================================")
|
||||
}
|
||||
|
||||
/// Test if notification observation is working at all
|
||||
private func testNotificationObservation() {
|
||||
// Test 1: Local NotificationCenter
|
||||
let testObserver = NotificationCenter.default.addObserver(
|
||||
forName: NSNotification.Name("TEST_NOTIFICATION"),
|
||||
|
||||
func startMonitoring() {
|
||||
guard !isMonitoring else { return }
|
||||
|
||||
notificationCenter.addObserver(
|
||||
self,
|
||||
selector: #selector(handleFocusEnabled(_:)),
|
||||
name: .focusModeEnabled,
|
||||
object: nil,
|
||||
queue: .main,
|
||||
using: { notification in
|
||||
print(" ✅ Local NotificationCenter works!")
|
||||
}
|
||||
suspensionBehavior: .deliverImmediately
|
||||
)
|
||||
NotificationCenter.default.post(name: NSNotification.Name("TEST_NOTIFICATION"), object: nil)
|
||||
NotificationCenter.default.removeObserver(testObserver)
|
||||
|
||||
// Test 2: Distributed NotificationCenter (this is what we need)
|
||||
let dnc = DistributedNotificationCenter.default()
|
||||
print(" 🌐 DistributedNotificationCenter instance: \(dnc)")
|
||||
print(" 🌐 Attempting to post test distributed notification...")
|
||||
|
||||
let testDistributedObserver = dnc.addObserver(
|
||||
forName: NSNotification.Name("com.test.distributed.notification"),
|
||||
|
||||
notificationCenter.addObserver(
|
||||
self,
|
||||
selector: #selector(handleFocusDisabled(_:)),
|
||||
name: .focusModeDisabled,
|
||||
object: nil,
|
||||
queue: .main,
|
||||
using: { notification in
|
||||
print(" ✅ Distributed NotificationCenter observation works!")
|
||||
}
|
||||
suspensionBehavior: .deliverImmediately
|
||||
)
|
||||
|
||||
// Post a test distributed notification
|
||||
dnc.postNotificationName(
|
||||
NSNotification.Name("com.test.distributed.notification"),
|
||||
object: nil,
|
||||
userInfo: ["test": "data"],
|
||||
deliverImmediately: true
|
||||
)
|
||||
|
||||
// Wait a bit then remove observer
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
||||
dnc.removeObserver(testDistributedObserver)
|
||||
print(" 🧹 Test observer cleaned up")
|
||||
|
||||
isMonitoring = true
|
||||
}
|
||||
|
||||
func stopMonitoring() {
|
||||
guard isMonitoring else { return }
|
||||
|
||||
notificationCenter.removeObserver(self, name: .focusModeEnabled, object: nil)
|
||||
notificationCenter.removeObserver(self, name: .focusModeDisabled, object: nil)
|
||||
|
||||
isMonitoring = false
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.isDoNotDisturbActive = false
|
||||
self.currentFocusModeIdentifier = ""
|
||||
self.currentFocusModeName = ""
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Setup Methods
|
||||
|
||||
/// Sets up distributed notification observers for Focus Mode changes
|
||||
private func setupFocusModeObservers() {
|
||||
print("🔔 [DoNotDisturbManager] ==========================================")
|
||||
print("🔔 [DoNotDisturbManager] 🚨 SETTING UP NOTIFICATION OBSERVERS 🚨")
|
||||
print("🔔 [DoNotDisturbManager] ==========================================")
|
||||
|
||||
// Method 1: Darwin Notifications (low-level, no permissions needed)
|
||||
setupDarwinNotificationObserver()
|
||||
|
||||
// Method 2: Distributed Notifications (system-wide)
|
||||
setupDistributedNotificationObserver()
|
||||
|
||||
// Method 3: NSWorkspace Notifications (local)
|
||||
setupNSWorkspaceNotificationObserver()
|
||||
|
||||
// Method 4: Polling as fallback
|
||||
startPollingForDND()
|
||||
|
||||
print("🔔 [DoNotDisturbManager] ✅ All observers registered")
|
||||
print("🔔 [DoNotDisturbManager] Please toggle Do Not Disturb now...")
|
||||
print("🔔 [DoNotDisturbManager] ==========================================")
|
||||
|
||||
@objc private func handleFocusEnabled(_ notification: Notification) {
|
||||
apply(notification: notification, isActive: true)
|
||||
}
|
||||
|
||||
private func setupDarwinNotificationObserver() {
|
||||
print("🔔 [DoNotDisturbManager] Setting up DARWIN notification observer...")
|
||||
|
||||
// Darwin notifications - these are low-level and work without special permissions
|
||||
let darwinNotificationNames = [
|
||||
"com.apple.controlcenter.modeChanged",
|
||||
"com.apple.donotdisturb.changed",
|
||||
"com.apple.donotdisturb.state",
|
||||
"com.apple.springboard.donotdisturb"
|
||||
]
|
||||
|
||||
let center = CFNotificationCenterGetDarwinNotifyCenter()
|
||||
|
||||
for notificationName in darwinNotificationNames {
|
||||
CFNotificationCenterAddObserver(
|
||||
center,
|
||||
Unmanaged.passUnretained(self).toOpaque(),
|
||||
{ (center, observer, name, object, userInfo) in
|
||||
guard let name = name else { return }
|
||||
let notificationName = name.rawValue as String
|
||||
print("🎯 [DARWIN NOTIFICATION] \(notificationName)")
|
||||
|
||||
// Get the manager instance
|
||||
if let observer = observer {
|
||||
let manager = Unmanaged<DoNotDisturbManager>.fromOpaque(observer).takeUnretainedValue()
|
||||
manager.handleDarwinNotification(notificationName)
|
||||
}
|
||||
},
|
||||
notificationName as CFString,
|
||||
nil,
|
||||
.deliverImmediately
|
||||
)
|
||||
print(" ✅ Darwin observer registered for: \(notificationName)")
|
||||
}
|
||||
|
||||
@objc private func handleFocusDisabled(_ notification: Notification) {
|
||||
apply(notification: notification, isActive: false)
|
||||
}
|
||||
|
||||
private func handleDarwinNotification(_ name: String) {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
print("🔔🔔🔔 [DARWIN] Focus Mode notification received: \(name)")
|
||||
// Toggle DND state
|
||||
self?.isDoNotDisturbActive.toggle()
|
||||
self?.showFocusModeActivatedHUD()
|
||||
}
|
||||
}
|
||||
|
||||
private func setupDistributedNotificationObserver() {
|
||||
let dnc = DistributedNotificationCenter.default()
|
||||
|
||||
print("🔔 [DoNotDisturbManager] Setting up DISTRIBUTED notification observer...")
|
||||
|
||||
// Observer for ANY AND ALL distributed notifications (for discovery)
|
||||
let allNotificationsObserver = dnc.addObserver(
|
||||
forName: nil, // nil = listen to EVERYTHING
|
||||
object: nil,
|
||||
queue: .main,
|
||||
using: { [weak self] notification in
|
||||
let notificationName = notification.name.rawValue
|
||||
|
||||
// PRINT EVERY SINGLE NOTIFICATION (no filtering)
|
||||
print("📢 [DISTRIBUTED] \(notificationName)")
|
||||
|
||||
// Also check if it looks Focus Mode related
|
||||
if notificationName.lowercased().contains("focus") ||
|
||||
notificationName.lowercased().contains("dnd") ||
|
||||
notificationName.lowercased().contains("disturb") ||
|
||||
notificationName.lowercased().contains("mode") ||
|
||||
notificationName.contains("com.apple.controlcenter") ||
|
||||
notificationName.contains("com.apple.donotdisturb") {
|
||||
|
||||
print("🔔🔔🔔 [DoNotDisturbManager] ⚡️⚡️⚡️ POTENTIAL FOCUS MODE NOTIFICATION:")
|
||||
print(" Name: \(notificationName)")
|
||||
print(" Object: \(String(describing: notification.object))")
|
||||
print(" UserInfo Keys: \(notification.userInfo?.keys.map { String(describing: $0) } ?? [])")
|
||||
print(" UserInfo: \(notification.userInfo ?? [:])")
|
||||
print(" ===============================================")
|
||||
|
||||
self?.handleFocusModeNotification(notification)
|
||||
}
|
||||
}
|
||||
)
|
||||
observers.append(allNotificationsObserver)
|
||||
print(" ✅ Distributed notification observer registered")
|
||||
}
|
||||
|
||||
private func setupNSWorkspaceNotificationObserver() {
|
||||
let nc = NSWorkspace.shared.notificationCenter
|
||||
|
||||
print("🔔 [DoNotDisturbManager] Setting up NSWORKSPACE notification observer...")
|
||||
|
||||
// Try to observe workspace notifications
|
||||
let workspaceObserver = nc.addObserver(
|
||||
forName: nil, // Listen to all workspace notifications
|
||||
object: nil,
|
||||
queue: .main,
|
||||
using: { notification in
|
||||
let notificationName = notification.name.rawValue
|
||||
|
||||
// Print all workspace notifications
|
||||
print("🏢 [NSWORKSPACE] \(notificationName)")
|
||||
|
||||
// Check for Focus Mode related
|
||||
if notificationName.lowercased().contains("focus") ||
|
||||
notificationName.lowercased().contains("dnd") ||
|
||||
notificationName.lowercased().contains("disturb") {
|
||||
print("🔔🔔🔔 [WORKSPACE FOCUS NOTIFICATION]")
|
||||
print(" Name: \(notificationName)")
|
||||
print(" UserInfo: \(notification.userInfo ?? [:])")
|
||||
}
|
||||
}
|
||||
)
|
||||
observers.append(workspaceObserver)
|
||||
print(" ✅ NSWorkspace notification observer registered")
|
||||
}
|
||||
|
||||
private func startPollingForDND() {
|
||||
print("🔔 [DoNotDisturbManager] Starting polling fallback (every 2 seconds)...")
|
||||
|
||||
// Poll every 2 seconds to check for DND state changes
|
||||
Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { [weak self] _ in
|
||||
self?.checkDNDStateViaDefaults()
|
||||
}
|
||||
print(" ✅ Polling timer started")
|
||||
}
|
||||
|
||||
private func checkDNDStateViaDefaults() {
|
||||
// Try to read DND state from UserDefaults or system preferences
|
||||
// This is a fallback detection method
|
||||
|
||||
let cfPrefs = CFPreferencesCopyAppValue("dndStart" as CFString, "com.apple.controlcenter" as CFString)
|
||||
if let prefs = cfPrefs {
|
||||
print("🔍 [POLLING] Found controlcenter prefs: \(prefs)")
|
||||
}
|
||||
|
||||
// Try reading notification center settings
|
||||
let ncPrefs = CFPreferencesCopyAppValue("doNotDisturb" as CFString, "com.apple.notificationcenterui" as CFString)
|
||||
if let nc = ncPrefs {
|
||||
print("🔍 [POLLING] Found notification center prefs: \(nc)")
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks initial Focus Mode state on app launch
|
||||
private func checkInitialState() {
|
||||
// TODO: Find a way to check current Focus Mode state
|
||||
// Options to explore:
|
||||
// 1. NSWorkspace properties (if any exist)
|
||||
// 2. Private APIs
|
||||
// 3. AppleScript (last resort)
|
||||
|
||||
print("🔔 [DoNotDisturbManager] Checking initial Focus Mode state...")
|
||||
print(" Note: Initial state detection not yet implemented")
|
||||
}
|
||||
|
||||
// MARK: - Notification Handling
|
||||
|
||||
/// Handles incoming Focus Mode notifications
|
||||
private func handleFocusModeNotification(_ notification: Notification) {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
|
||||
private func apply(notification: Notification, isActive: Bool) {
|
||||
metadataExtractionQueue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
// Parse notification payload
|
||||
let userInfo = notification.userInfo ?? [:]
|
||||
|
||||
// Try to extract Focus Mode state and identifier
|
||||
// Note: Actual keys depend on the notification structure (to be discovered)
|
||||
if let stateString = userInfo["state"] as? String {
|
||||
self.isDoNotDisturbActive = (stateString == "enabled" || stateString == "on" || stateString == "active")
|
||||
} else if let enabled = userInfo["enabled"] as? Bool {
|
||||
self.isDoNotDisturbActive = enabled
|
||||
} else if let active = userInfo["active"] as? Bool {
|
||||
self.isDoNotDisturbActive = active
|
||||
}
|
||||
|
||||
// Try to extract mode name
|
||||
if let modeName = userInfo["name"] as? String {
|
||||
self.currentFocusModeName = modeName
|
||||
} else if let mode = userInfo["mode"] as? String {
|
||||
self.currentFocusModeName = mode
|
||||
}
|
||||
|
||||
// Try to extract mode identifier
|
||||
if let identifier = userInfo["identifier"] as? String {
|
||||
self.currentFocusModeIdentifier = identifier
|
||||
}
|
||||
|
||||
print("🔔 [DoNotDisturbManager] State updated:")
|
||||
print(" Active: \(self.isDoNotDisturbActive)")
|
||||
print(" Mode Name: \(self.currentFocusModeName.isEmpty ? "N/A" : self.currentFocusModeName)")
|
||||
print(" Identifier: \(self.currentFocusModeIdentifier.isEmpty ? "N/A" : self.currentFocusModeIdentifier)")
|
||||
|
||||
// Trigger HUD display
|
||||
if self.isDoNotDisturbActive {
|
||||
self.showFocusModeActivatedHUD()
|
||||
} else {
|
||||
self.showFocusModeDeactivatedHUD()
|
||||
|
||||
let metadata = self.extractMetadata(from: notification)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if let identifier = metadata.identifier, identifier != self.currentFocusModeIdentifier {
|
||||
self.currentFocusModeIdentifier = identifier
|
||||
}
|
||||
|
||||
if let name = metadata.name {
|
||||
if name != self.currentFocusModeName {
|
||||
self.currentFocusModeName = name
|
||||
}
|
||||
} else if !self.currentFocusModeIdentifier.isEmpty {
|
||||
let derivedName = FocusModeType(rawValue: self.currentFocusModeIdentifier)?.displayName ?? ""
|
||||
if !derivedName.isEmpty {
|
||||
self.currentFocusModeName = derivedName
|
||||
}
|
||||
}
|
||||
|
||||
guard self.isDoNotDisturbActive != isActive else { return }
|
||||
|
||||
withAnimation(.smooth(duration: 0.25)) {
|
||||
self.isDoNotDisturbActive = isActive
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - HUD Display Methods
|
||||
|
||||
/// Shows HUD when Focus Mode is activated
|
||||
private func showFocusModeActivatedHUD() {
|
||||
print("🔔 [DoNotDisturbManager] 📱 Showing Focus Mode ACTIVATED HUD")
|
||||
|
||||
// TODO: Integrate with DynamicIslandViewCoordinator to show HUD
|
||||
// coordinator.toggleSneakPeek(status: true, type: .doNotDisturb, value: 1)
|
||||
}
|
||||
|
||||
/// Shows HUD when Focus Mode is deactivated
|
||||
private func showFocusModeDeactivatedHUD() {
|
||||
print("🔔 [DoNotDisturbManager] 📱 Showing Focus Mode DEACTIVATED HUD")
|
||||
|
||||
// TODO: Integrate with DynamicIslandViewCoordinator to show HUD
|
||||
// coordinator.toggleSneakPeek(status: true, type: .doNotDisturb, value: 0)
|
||||
}
|
||||
|
||||
// MARK: - Public Methods
|
||||
|
||||
/// Manually refresh Focus Mode state
|
||||
func refreshState() {
|
||||
print("🔔 [DoNotDisturbManager] Manual state refresh requested")
|
||||
checkInitialState()
|
||||
}
|
||||
|
||||
// MARK: - Cleanup
|
||||
|
||||
private func cleanup() {
|
||||
print("🔔 [DoNotDisturbManager] Cleaning up observers...")
|
||||
|
||||
// Remove Darwin notification observers
|
||||
let center = CFNotificationCenterGetDarwinNotifyCenter()
|
||||
CFNotificationCenterRemoveEveryObserver(center, Unmanaged.passUnretained(self).toOpaque())
|
||||
|
||||
// Remove other observers
|
||||
let dnc = DistributedNotificationCenter.default()
|
||||
for observer in observers {
|
||||
dnc.removeObserver(observer)
|
||||
|
||||
private func extractMetadata(from notification: Notification) -> (name: String?, identifier: String?) {
|
||||
guard let userInfo = notification.userInfo else { return (nil, nil) }
|
||||
|
||||
if !hasLoggedPayload {
|
||||
hasLoggedPayload = true
|
||||
debugPrint("[DoNotDisturbManager] focus notification payload: \(userInfo)")
|
||||
}
|
||||
observers.removeAll()
|
||||
cancellables.removeAll()
|
||||
|
||||
let identifierKeys = [
|
||||
"FocusModeIdentifier",
|
||||
"focusModeIdentifier",
|
||||
"identifier",
|
||||
"Identifier"
|
||||
]
|
||||
|
||||
let nameKeys = [
|
||||
"FocusModeName",
|
||||
"focusModeName",
|
||||
"name",
|
||||
"Name"
|
||||
]
|
||||
|
||||
let identifier = identifierKeys.compactMap { userInfo[$0] as? String }.first
|
||||
let name = nameKeys.compactMap { userInfo[$0] as? String }.first
|
||||
|
||||
return (name, identifier)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension Notification.Name {
|
||||
static let focusModeEnabled = Notification.Name("_NSDoNotDisturbEnabledNotification")
|
||||
static let focusModeDisabled = Notification.Name("_NSDoNotDisturbDisabledNotification")
|
||||
}
|
||||
|
||||
// MARK: - Focus Mode Types
|
||||
|
|
|
|||
|
|
@ -483,6 +483,10 @@ extension Defaults.Keys {
|
|||
static let showRecordingIndicator = Key<Bool>("showRecordingIndicator", default: true)
|
||||
// Polling removed - now uses event-driven private API detection (CGSIsScreenWatcherPresent)
|
||||
// static let enableScreenRecordingPolling = Key<Bool>("enableScreenRecordingPolling", default: false)
|
||||
|
||||
// MARK: Focus / Do Not Disturb Detection
|
||||
static let enableDoNotDisturbDetection = Key<Bool>("enableDoNotDisturbDetection", default: true)
|
||||
static let showDoNotDisturbIndicator = Key<Bool>("showDoNotDisturbIndicator", default: true)
|
||||
|
||||
// MARK: Privacy Indicators (Camera & Microphone Detection)
|
||||
static let enableCameraDetection = Key<Bool>("enableCameraDetection", default: true)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue