88 lines
2.9 KiB
Swift
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
|
|
}
|
|
}
|