Files
sst/Sources/Sst/PermissionMonitor.swift
T

88 lines
2.9 KiB
Swift

import AppKit
import ApplicationServices
import AVFoundation
import Combine
/// Reads the three macOS permissions and keeps them current: on every app activation and while polling is on.
@MainActor
final class PermissionMonitor: ObservableObject {
enum Kind: CaseIterable, Identifiable {
case microphone, accessibility, inputMonitoring
var id: Self { self }
var title: String {
switch self {
case .microphone: Strings.microphone
case .accessibility: Strings.accessibility
case .inputMonitoring: Strings.inputMonitoring
}
}
fileprivate var pane: String {
switch self {
case .microphone: "Privacy_Microphone"
case .accessibility: "Privacy_Accessibility"
case .inputMonitoring: "Privacy_ListenEvent"
}
}
}
@Published private(set) var microphone = false
@Published private(set) var accessibility = false
@Published private(set) var inputMonitoring = false
private var timer: Timer?
private var activation: AnyCancellable?
init() {
refresh()
activation = NotificationCenter.default
.publisher(for: NSApplication.didBecomeActiveNotification)
.sink { [weak self] _ in MainActor.assumeIsolated { self?.refresh() } }
}
var allGranted: Bool { microphone && accessibility && inputMonitoring }
func isGranted(_ kind: Kind) -> Bool {
switch kind {
case .microphone: microphone
case .accessibility: accessibility
case .inputMonitoring: inputMonitoring
}
}
func refresh() {
microphone = AVCaptureDevice.authorizationStatus(for: .audio) == .authorized
accessibility = AXIsProcessTrusted()
inputMonitoring = CGPreflightListenEventAccess()
}
/// Asks macOS for every permission that is still missing (each shows its own system prompt).
func requestMissing() async {
if !microphone { _ = await AVCaptureDevice.requestAccess(for: .audio) }
if !inputMonitoring { _ = CGRequestListenEventAccess() }
if !accessibility { _ = AXIsProcessTrustedWithOptions(["AXTrustedCheckOptionPrompt": true] as CFDictionary) }
refresh()
}
func openSettings(for kind: Kind) {
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?\(kind.pane)") {
NSWorkspace.shared.open(url)
}
}
/// Polls while a view that shows the lamps is visible, so granting a permission shows up without a click.
func startPolling(every interval: TimeInterval = 2) {
guard timer == nil else { return }
timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in
MainActor.assumeIsolated { self?.refresh() }
}
}
func stopPolling() {
timer?.invalidate()
timer = nil
}
}