| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133 |
- import AirAcceptCore
- import AppKit
- import ApplicationServices
- import Foundation
- @MainActor
- final class AirPlayPromptWatcher {
- private let configStore: ConfigStore
- private let logger: AirAcceptLogger
- private var timer: Timer?
- private var handledFingerprints: [String: Date] = [:]
- private var enabled = true
- private var lastTrustedState: Bool?
- init(configStore: ConfigStore, logger: AirAcceptLogger) {
- self.configStore = configStore
- self.logger = logger
- }
- var isEnabled: Bool {
- enabled
- }
- func setEnabled(_ enabled: Bool) {
- self.enabled = enabled
- if enabled {
- start()
- } else {
- stop()
- }
- }
- func start() {
- guard enabled else {
- return
- }
- if updateTrustedState(prompt: true) {
- scanOnce()
- } else {
- logger.info("Accessibility permission is not granted yet")
- }
- let interval = currentPollInterval()
- scheduleTimer(interval: interval)
- logger.info("watcher started with poll interval \(interval)s")
- }
- func stop() {
- timer?.invalidate()
- timer = nil
- logger.info("watcher stopped")
- }
- func reloadConfiguration() {
- guard enabled else {
- return
- }
- let interval = currentPollInterval()
- scheduleTimer(interval: interval)
- logger.info("watcher poll interval updated to \(interval)s")
- }
- func scanOnce() {
- guard enabled, updateTrustedState(prompt: false) else {
- return
- }
- guard let result = AccessibilityReader.notificationCenterCandidate() else {
- pruneHandledFingerprints()
- return
- }
- handle(result: result)
- pruneHandledFingerprints()
- }
- private func scheduleTimer(interval: TimeInterval) {
- timer?.invalidate()
- timer = Timer.scheduledTimer(withTimeInterval: max(interval, 0.25), repeats: true) { [weak self] _ in
- Task { @MainActor in
- self?.scanOnce()
- }
- }
- }
- private func currentPollInterval() -> TimeInterval {
- let config = (try? configStore.loadOrCreate()) ?? AirAcceptConfig()
- return TimeInterval(max(config.pollIntervalMilliseconds, 250)) / 1_000.0
- }
- private func updateTrustedState(prompt: Bool) -> Bool {
- let trusted = AccessibilityReader.isTrusted(prompt: prompt)
- if lastTrustedState != trusted {
- lastTrustedState = trusted
- logger.info("Accessibility permission is \(trusted ? "granted" : "not granted")")
- }
- return trusted
- }
- private func handle(result: PromptCandidateWithButton) {
- let fingerprint = "\(result.match.summary)|\(result.match.deviceName ?? "unknown")"
- guard handledFingerprints[fingerprint] == nil else {
- return
- }
- let axError = AccessibilityReader.press(result.acceptButton, actionName: result.acceptActionName)
- if axError == .success {
- handledFingerprints[fingerprint] = Date()
- logger.event(AirAcceptEvent(
- hostProcess: result.candidate.hostProcess,
- summary: result.match.summary,
- deviceName: result.match.deviceName,
- action: "accept",
- reason: "accepted matched AirPlay request"
- ))
- } else {
- logger.event(AirAcceptEvent(
- hostProcess: result.candidate.hostProcess,
- summary: result.match.summary,
- deviceName: result.match.deviceName,
- action: "error",
- reason: "failed to press Accept: \(axError.rawValue)"
- ))
- }
- }
- private func pruneHandledFingerprints() {
- let cutoff = Date().addingTimeInterval(-30)
- handledFingerprints = handledFingerprints.filter { $0.value > cutoff }
- }
- }
|