mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 20:35:39 -06:00
fix(macos): recover denied speech recognition permissions (#129950)
This commit is contained in:
committed by
GitHub
parent
94a170089d
commit
f26c053605
@@ -36,6 +36,13 @@ enum PermissionManager {
|
||||
status == .authorized || status == .provisional
|
||||
}
|
||||
|
||||
static func shouldOpenSpeechRecognitionSettings(
|
||||
status: SFSpeechRecognizerAuthorizationStatus,
|
||||
interactive: Bool) -> Bool
|
||||
{
|
||||
interactive && (status == .denied || status == .restricted)
|
||||
}
|
||||
|
||||
static func isLocationAuthorized(status: CLAuthorizationStatus, requireAlways: Bool) -> Bool {
|
||||
if requireAlways { return status == .authorizedAlways }
|
||||
switch status {
|
||||
@@ -96,7 +103,10 @@ enum PermissionManager {
|
||||
return granted && self.isNotificationAuthorized(status: updated.authorizationStatus)
|
||||
}
|
||||
if settings.authorizationStatus == .denied, interactive {
|
||||
NotificationPermissionHelper.openSettings()
|
||||
SystemSettingsURLSupport.openFirst([
|
||||
"x-apple.systempreferences:com.apple.Notifications-Settings.extension",
|
||||
"x-apple.systempreferences:com.apple.preference.notifications",
|
||||
])
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -137,7 +147,7 @@ enum PermissionManager {
|
||||
return await AVCaptureDevice.requestAccess(for: .audio)
|
||||
case .denied, .restricted:
|
||||
if interactive {
|
||||
MicrophonePermissionHelper.openSettings()
|
||||
SystemSettingsURLSupport.openPrivacySettings(for: .microphone)
|
||||
}
|
||||
return false
|
||||
@unknown default:
|
||||
@@ -147,6 +157,9 @@ enum PermissionManager {
|
||||
|
||||
private static func ensureSpeechRecognition(interactive: Bool) async -> Bool {
|
||||
let status = SFSpeechRecognizer.authorizationStatus()
|
||||
if self.shouldOpenSpeechRecognitionSettings(status: status, interactive: interactive) {
|
||||
SystemSettingsURLSupport.openPrivacySettings(for: .speechRecognition)
|
||||
}
|
||||
if status == .notDetermined, interactive {
|
||||
await withUnsafeContinuation { (cont: UnsafeContinuation<Void, Never>) in
|
||||
SFSpeechRecognizer.requestAuthorization { _ in
|
||||
@@ -167,7 +180,7 @@ enum PermissionManager {
|
||||
return await AVCaptureDevice.requestAccess(for: .video)
|
||||
case .denied, .restricted:
|
||||
if interactive {
|
||||
CameraPermissionHelper.openSettings()
|
||||
SystemSettingsURLSupport.openPrivacySettings(for: .camera)
|
||||
}
|
||||
return false
|
||||
@unknown default:
|
||||
@@ -178,7 +191,7 @@ enum PermissionManager {
|
||||
private static func ensureLocation(interactive: Bool) async -> Bool {
|
||||
guard CLLocationManager.locationServicesEnabled() else {
|
||||
if interactive {
|
||||
await MainActor.run { LocationPermissionHelper.openSettings() }
|
||||
await MainActor.run { SystemSettingsURLSupport.openPrivacySettings(for: .location) }
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -192,7 +205,7 @@ enum PermissionManager {
|
||||
return self.isLocationAuthorized(status: updated, requireAlways: false)
|
||||
case .denied, .restricted:
|
||||
if interactive {
|
||||
await MainActor.run { LocationPermissionHelper.openSettings() }
|
||||
await MainActor.run { SystemSettingsURLSupport.openPrivacySettings(for: .location) }
|
||||
}
|
||||
return false
|
||||
@unknown default:
|
||||
@@ -271,42 +284,6 @@ enum PermissionManager {
|
||||
}
|
||||
}
|
||||
|
||||
enum NotificationPermissionHelper {
|
||||
static func openSettings() {
|
||||
SystemSettingsURLSupport.openFirst([
|
||||
"x-apple.systempreferences:com.apple.Notifications-Settings.extension",
|
||||
"x-apple.systempreferences:com.apple.preference.notifications",
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
enum MicrophonePermissionHelper {
|
||||
static func openSettings() {
|
||||
SystemSettingsURLSupport.openFirst([
|
||||
"x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone",
|
||||
"x-apple.systempreferences:com.apple.preference.security",
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
enum CameraPermissionHelper {
|
||||
static func openSettings() {
|
||||
SystemSettingsURLSupport.openFirst([
|
||||
"x-apple.systempreferences:com.apple.preference.security?Privacy_Camera",
|
||||
"x-apple.systempreferences:com.apple.preference.security",
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
enum LocationPermissionHelper {
|
||||
static func openSettings() {
|
||||
SystemSettingsURLSupport.openFirst([
|
||||
"x-apple.systempreferences:com.apple.preference.security?Privacy_LocationServices",
|
||||
"x-apple.systempreferences:com.apple.preference.security",
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class LocationPermissionRequestCoordinator {
|
||||
private var continuations: [CheckedContinuation<CLAuthorizationStatus, Never>] = []
|
||||
@@ -391,7 +368,7 @@ final class LocationPermissionRequester: NSObject, CLLocationManagerDelegate {
|
||||
return
|
||||
}
|
||||
guard !Task.isCancelled, let self, self.requests.hasPendingRequests else { return }
|
||||
LocationPermissionHelper.openSettings()
|
||||
SystemSettingsURLSupport.openPrivacySettings(for: .location)
|
||||
self.finish(status: self.manager.authorizationStatus)
|
||||
}
|
||||
}
|
||||
@@ -425,7 +402,7 @@ final class LocationPermissionRequester: NSObject, CLLocationManagerDelegate {
|
||||
let status = manager.authorizationStatus
|
||||
Task { @MainActor in
|
||||
if status == .denied || status == .restricted {
|
||||
LocationPermissionHelper.openSettings()
|
||||
SystemSettingsURLSupport.openPrivacySettings(for: .location)
|
||||
}
|
||||
self.finish(status: status)
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ private struct LocationAccessSettings: View {
|
||||
private func requestLocationAuthorization(mode: OpenClawLocationMode) async -> Bool {
|
||||
guard mode != .off else { return true }
|
||||
guard CLLocationManager.locationServicesEnabled() else {
|
||||
await MainActor.run { LocationPermissionHelper.openSettings() }
|
||||
await MainActor.run { SystemSettingsURLSupport.openPrivacySettings(for: .location) }
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,27 @@
|
||||
import AppKit
|
||||
import Foundation
|
||||
import OpenClawIPC
|
||||
|
||||
enum SystemSettingsURLSupport {
|
||||
static func privacySettingsCandidates(for capability: Capability) -> [String] {
|
||||
let pane: String? = switch capability {
|
||||
case .microphone: "Microphone"
|
||||
case .speechRecognition: "SpeechRecognition"
|
||||
case .camera: "Camera"
|
||||
case .location: "LocationServices"
|
||||
default: nil
|
||||
}
|
||||
guard let pane else { return [] }
|
||||
return [
|
||||
"x-apple.systempreferences:com.apple.preference.security?Privacy_\(pane)",
|
||||
"x-apple.systempreferences:com.apple.preference.security",
|
||||
]
|
||||
}
|
||||
|
||||
static func openPrivacySettings(for capability: Capability) {
|
||||
self.openFirst(self.privacySettingsCandidates(for: capability))
|
||||
}
|
||||
|
||||
static func openFirst(_ candidates: [String]) {
|
||||
for candidate in candidates {
|
||||
if let url = URL(string: candidate), NSWorkspace.shared.open(url) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import CoreLocation
|
||||
import OpenClawIPC
|
||||
import Speech
|
||||
import Testing
|
||||
import UserNotifications
|
||||
@testable import OpenClaw
|
||||
@@ -7,6 +8,47 @@ import UserNotifications
|
||||
@Suite(.serialized)
|
||||
@MainActor
|
||||
struct PermissionManagerTests {
|
||||
@Test(arguments: [
|
||||
(SFSpeechRecognizerAuthorizationStatus.notDetermined, false, false),
|
||||
(.notDetermined, true, false),
|
||||
(.denied, false, false),
|
||||
(.denied, true, true),
|
||||
(.restricted, false, false),
|
||||
(.restricted, true, true),
|
||||
(.authorized, false, false),
|
||||
(.authorized, true, false),
|
||||
])
|
||||
func `speech recovery only opens Settings for an already denied interactive request`(
|
||||
status: SFSpeechRecognizerAuthorizationStatus,
|
||||
interactive: Bool,
|
||||
shouldRecover: Bool)
|
||||
{
|
||||
#expect(PermissionManager.shouldOpenSpeechRecognitionSettings(
|
||||
status: status,
|
||||
interactive: interactive) == shouldRecover)
|
||||
}
|
||||
|
||||
@Test(arguments: [
|
||||
(Capability.microphone, "Microphone"),
|
||||
(.speechRecognition, "SpeechRecognition"),
|
||||
(.camera, "Camera"),
|
||||
(.location, "LocationServices"),
|
||||
])
|
||||
func `denied privacy permissions route to their own System Settings pane`(
|
||||
capability: Capability,
|
||||
pane: String)
|
||||
{
|
||||
#expect(SystemSettingsURLSupport.privacySettingsCandidates(for: capability) == [
|
||||
"x-apple.systempreferences:com.apple.preference.security?Privacy_\(pane)",
|
||||
"x-apple.systempreferences:com.apple.preference.security",
|
||||
])
|
||||
}
|
||||
|
||||
@Test(arguments: [Capability.appleScript, .notifications, .accessibility, .screenRecording])
|
||||
func `unrelated permissions do not inherit a privacy recovery pane`(capability: Capability) {
|
||||
#expect(SystemSettingsURLSupport.privacySettingsCandidates(for: capability).isEmpty)
|
||||
}
|
||||
|
||||
@Test func `notification authorization accepts provisional delivery`() throws {
|
||||
#expect(PermissionManager.isNotificationAuthorized(status: .authorized))
|
||||
#expect(PermissionManager.isNotificationAuthorized(status: .provisional))
|
||||
|
||||
Reference in New Issue
Block a user