mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 04:15:48 -06:00
refactor(ios): share voice permission support (#120176)
This commit is contained in:
Generated
+3
-7
@@ -19542,7 +19542,7 @@
|
||||
"sites": [
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager+Permissions.swift"
|
||||
"path": "apps/ios/Sources/Voice/VoicePermissionSupport.swift"
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
@@ -19557,7 +19557,7 @@
|
||||
"sites": [
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager+Permissions.swift"
|
||||
"path": "apps/ios/Sources/Voice/VoicePermissionSupport.swift"
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
@@ -19572,11 +19572,7 @@
|
||||
"sites": [
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager+Permissions.swift"
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"path": "apps/ios/Sources/Voice/VoiceWakeManager.swift"
|
||||
"path": "apps/ios/Sources/Voice/VoicePermissionSupport.swift"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -695,7 +695,7 @@ final class TalkModeManager: NSObject {
|
||||
let micOk = if self.allowSimulatorCapture {
|
||||
true
|
||||
} else {
|
||||
await Self.requestMicrophonePermission()
|
||||
await VoicePermissionSupport.requestMicrophonePermission(timeoutErrorDomain: "TalkMode")
|
||||
}
|
||||
GatewayDiagnostics.log(
|
||||
"talk.timeline microphone permission ok=\(micOk) "
|
||||
@@ -734,13 +734,13 @@ final class TalkModeManager: NSObject {
|
||||
let speechOk = if self.allowSimulatorCapture {
|
||||
true
|
||||
} else {
|
||||
await Self.requestSpeechPermission()
|
||||
await VoicePermissionSupport.requestSpeechPermission(timeoutErrorDomain: "TalkMode")
|
||||
}
|
||||
guard speechOk else {
|
||||
self.logger.warning("start blocked: speech permission denied")
|
||||
self.stopNativeCaptureAndDiscardTranscript()
|
||||
deactivateAudioSession()
|
||||
let status = Self.permissionMessage(
|
||||
let status = VoicePermissionSupport.speechPermissionMessage(
|
||||
kind: String(localized: "Speech recognition"),
|
||||
status: SFSpeechRecognizer.authorizationStatus())
|
||||
self.setStatus(
|
||||
@@ -1278,7 +1278,7 @@ final class TalkModeManager: NSObject {
|
||||
{
|
||||
guard !self.allowSimulatorCapture else { return }
|
||||
|
||||
let micOk = await Self.requestMicrophonePermission()
|
||||
let micOk = await VoicePermissionSupport.requestMicrophonePermission(timeoutErrorDomain: "TalkMode")
|
||||
try self.ensurePushToTalkStartCurrent(captureId: captureId, canStartCapture: canStartCapture)
|
||||
guard micOk else {
|
||||
self.setStatus(
|
||||
@@ -1290,10 +1290,10 @@ final class TalkModeManager: NSObject {
|
||||
])
|
||||
}
|
||||
|
||||
let speechOk = await Self.requestSpeechPermission()
|
||||
let speechOk = await VoicePermissionSupport.requestSpeechPermission(timeoutErrorDomain: "TalkMode")
|
||||
try self.ensurePushToTalkStartCurrent(captureId: captureId, canStartCapture: canStartCapture)
|
||||
guard speechOk else {
|
||||
let status = Self.permissionMessage(
|
||||
let status = VoicePermissionSupport.speechPermissionMessage(
|
||||
kind: String(localized: "Speech recognition"),
|
||||
status: SFSpeechRecognizer.authorizationStatus())
|
||||
self.setStatus(
|
||||
|
||||
+23
-24
@@ -3,25 +3,23 @@ import Foundation
|
||||
import OpenClawKit
|
||||
import Speech
|
||||
|
||||
extension TalkModeManager {
|
||||
nonisolated static func requestMicrophonePermission() async -> Bool {
|
||||
enum VoicePermissionSupport {
|
||||
static func requestMicrophonePermission(timeoutErrorDomain: String) async -> Bool {
|
||||
switch AVAudioApplication.shared.recordPermission {
|
||||
case .granted:
|
||||
return true
|
||||
case .denied:
|
||||
return false
|
||||
case .undetermined:
|
||||
return await self.requestPermissionWithTimeout { completion in
|
||||
AVAudioApplication.requestRecordPermission(completionHandler: { ok in
|
||||
completion(ok)
|
||||
})
|
||||
return await self.requestPermissionWithTimeout(errorDomain: timeoutErrorDomain) { completion in
|
||||
AVAudioApplication.requestRecordPermission(completionHandler: completion)
|
||||
}
|
||||
@unknown default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated static func requestSpeechPermission() async -> Bool {
|
||||
static func requestSpeechPermission(timeoutErrorDomain: String) async -> Bool {
|
||||
let status = SFSpeechRecognizer.authorizationStatus()
|
||||
switch status {
|
||||
case .authorized:
|
||||
@@ -34,29 +32,14 @@ extension TalkModeManager {
|
||||
return false
|
||||
}
|
||||
|
||||
return await self.requestPermissionWithTimeout { completion in
|
||||
return await self.requestPermissionWithTimeout(errorDomain: timeoutErrorDomain) { completion in
|
||||
SFSpeechRecognizer.requestAuthorization { authStatus in
|
||||
completion(authStatus == .authorized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private nonisolated static func requestPermissionWithTimeout(
|
||||
_ operation: @escaping @Sendable (@escaping @Sendable (Bool) -> Void) -> Void) async -> Bool
|
||||
{
|
||||
do {
|
||||
return try await AsyncTimeout.withTimeout(
|
||||
seconds: 8,
|
||||
onTimeout: { NSError(domain: "TalkMode", code: 6, userInfo: [
|
||||
NSLocalizedDescriptionKey: "permission request timed out",
|
||||
]) },
|
||||
operation: { await PermissionRequestBridge.awaitRequest(operation) })
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
static func permissionMessage(
|
||||
static func speechPermissionMessage(
|
||||
kind: String,
|
||||
status: SFSpeechRecognizerAuthorizationStatus) -> String
|
||||
{
|
||||
@@ -83,4 +66,20 @@ extension TalkModeManager {
|
||||
kind)
|
||||
}
|
||||
}
|
||||
|
||||
private static func requestPermissionWithTimeout(
|
||||
errorDomain: String,
|
||||
operation: @escaping @Sendable (@escaping @Sendable (Bool) -> Void) -> Void) async -> Bool
|
||||
{
|
||||
do {
|
||||
return try await AsyncTimeout.withTimeout(
|
||||
seconds: 8,
|
||||
onTimeout: { NSError(domain: errorDomain, code: 6, userInfo: [
|
||||
NSLocalizedDescriptionKey: "permission request timed out",
|
||||
]) },
|
||||
operation: { await PermissionRequestBridge.awaitRequest(operation) })
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import AVFAudio
|
||||
import Foundation
|
||||
import Observation
|
||||
import OpenClawKit
|
||||
import Speech
|
||||
import SwabbleKit
|
||||
|
||||
@@ -279,7 +278,7 @@ final class VoiceWakeManager: NSObject {
|
||||
|
||||
self.statusText = String(localized: "Requesting permissions…")
|
||||
|
||||
let micOk = await Self.requestMicrophonePermission()
|
||||
let micOk = await VoicePermissionSupport.requestMicrophonePermission(timeoutErrorDomain: "VoiceWake")
|
||||
guard micOk else {
|
||||
self.statusText = Self.microphonePermissionMessage(
|
||||
kind: String(localized: "Microphone"))
|
||||
@@ -287,9 +286,9 @@ final class VoiceWakeManager: NSObject {
|
||||
return
|
||||
}
|
||||
|
||||
let speechOk = await Self.requestSpeechPermission()
|
||||
let speechOk = await VoicePermissionSupport.requestSpeechPermission(timeoutErrorDomain: "VoiceWake")
|
||||
guard speechOk else {
|
||||
self.statusText = Self.permissionMessage(
|
||||
self.statusText = VoicePermissionSupport.speechPermissionMessage(
|
||||
kind: String(localized: "Speech recognition"),
|
||||
status: SFSpeechRecognizer.authorizationStatus())
|
||||
self.isListening = false
|
||||
@@ -549,23 +548,6 @@ final class VoiceWakeManager: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
private nonisolated static func requestMicrophonePermission() async -> Bool {
|
||||
switch AVAudioApplication.shared.recordPermission {
|
||||
case .granted:
|
||||
return true
|
||||
case .denied:
|
||||
return false
|
||||
case .undetermined:
|
||||
break
|
||||
@unknown default:
|
||||
return false
|
||||
}
|
||||
|
||||
return await self.requestPermissionWithTimeout { completion in
|
||||
AVAudioApplication.requestRecordPermission(completionHandler: completion)
|
||||
}
|
||||
}
|
||||
|
||||
private nonisolated static func microphonePermissionMessage(kind: String) -> String {
|
||||
let status = AVAudioApplication.shared.recordPermission
|
||||
return self.deniedByDefaultPermissionMessage(
|
||||
@@ -573,69 +555,6 @@ final class VoiceWakeManager: NSObject {
|
||||
isUndetermined: status == .undetermined)
|
||||
}
|
||||
|
||||
private nonisolated static func requestSpeechPermission() async -> Bool {
|
||||
let status = SFSpeechRecognizer.authorizationStatus()
|
||||
switch status {
|
||||
case .authorized:
|
||||
return true
|
||||
case .denied, .restricted:
|
||||
return false
|
||||
case .notDetermined:
|
||||
break
|
||||
@unknown default:
|
||||
return false
|
||||
}
|
||||
|
||||
return await self.requestPermissionWithTimeout { completion in
|
||||
SFSpeechRecognizer.requestAuthorization { authStatus in
|
||||
completion(authStatus == .authorized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private nonisolated static func requestPermissionWithTimeout(
|
||||
_ operation: @escaping @Sendable (@escaping @Sendable (Bool) -> Void) -> Void) async -> Bool
|
||||
{
|
||||
do {
|
||||
return try await AsyncTimeout.withTimeout(
|
||||
seconds: 8,
|
||||
onTimeout: { NSError(domain: "VoiceWake", code: 6, userInfo: [
|
||||
NSLocalizedDescriptionKey: "permission request timed out",
|
||||
]) },
|
||||
operation: { await PermissionRequestBridge.awaitRequest(operation) })
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private static func permissionMessage(
|
||||
kind: String,
|
||||
status: SFSpeechRecognizerAuthorizationStatus) -> String
|
||||
{
|
||||
switch status {
|
||||
case .denied:
|
||||
return String(
|
||||
format: String(localized: "%@ permission denied"),
|
||||
kind)
|
||||
case .restricted:
|
||||
return String(
|
||||
format: String(localized: "%@ permission restricted"),
|
||||
kind)
|
||||
case .notDetermined:
|
||||
return String(
|
||||
format: String(localized: "%@ permission not granted"),
|
||||
kind)
|
||||
case .authorized:
|
||||
return String(
|
||||
format: String(localized: "%@ permission denied"),
|
||||
kind)
|
||||
@unknown default:
|
||||
return String(
|
||||
format: String(localized: "%@ permission denied"),
|
||||
kind)
|
||||
}
|
||||
}
|
||||
|
||||
private nonisolated static func deniedByDefaultPermissionMessage(kind: String, isUndetermined: Bool) -> String {
|
||||
if isUndetermined {
|
||||
return String(
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import Speech
|
||||
import Testing
|
||||
@testable import OpenClaw
|
||||
|
||||
struct VoicePermissionSupportTests {
|
||||
@Test func `speech permission messages preserve authorization detail`() {
|
||||
let kind = "Speech recognition"
|
||||
let cases: [(SFSpeechRecognizerAuthorizationStatus, String)] = [
|
||||
(.denied, "Speech recognition permission denied"),
|
||||
(.restricted, "Speech recognition permission restricted"),
|
||||
(.notDetermined, "Speech recognition permission not granted"),
|
||||
(.authorized, "Speech recognition permission denied"),
|
||||
]
|
||||
|
||||
for (status, expected) in cases {
|
||||
#expect(VoicePermissionSupport.speechPermissionMessage(kind: kind, status: status) == expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user