Is it possible to create a sandboxed app that uses accessibility permission? And if so, how do I ask the user for that permission in a way that is allowed by the App Store?
Im creating a small menubar app and my current (rejected) solution is to create a pop-up, with link to Security & Privacy > Accessibility and the pop-up asks the user to manually add the app to the list and check the checkbox. This works in sandbox.
Reason for rejection:
"Specifically, your app requires to grant accessibility access, but once we opened the accessibility settings, your app was not listed."
I know it's not listed there and it has to be added manually. But its the only solution I've found to this issue. Is there perhaps any way to add the app there programmatically?
Im a bit confused since I've seen other apps in App Store that work the same way, where you have to add the app to the list manually. Eg. Flycut. 🤷♂️
I know about this alternative solution, and it's not allowed in sandboxed apps. It also adds the app to the accessibility list automagically:
func getPermission() {
AXIsProcessTrustedWithOptions([kAXTrustedCheckOptionPrompt.takeUnretainedValue():true] as CFDictionary).
}
Does anyone have a solution for this?
Best regards, Daniel
Is there any way to allow this in a sandboxed environment?
Yes, but you have to take a slightly different tack. Rather than using an NSEvent
global event monitor, use a CGEventTap
. For weird historical reasons, the former requires the Accessibility privilege whereas the latter requires the Input Monitoring privilege. The Input Monitoring privilege is easily available to sandboxed apps, and even apps published on the Mac App Store. It even has APIs to check for (CGPreflightListenEventAccess
) and explicitly request (CGRequestListenEventAccess
) that privilege.
CGEventTap
is not exactly fun to call from Swift. Pasted in below is a snippet that shows the basics. I pulled this out of a large test project, and removed some code that shouldn’t matter to you, so I apologise if this doesn’t compile out of the box.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
final class CGEventTapAction {
init(log: OSLog) {
self.log = log
}
let log: OSLog
private var runState: RunState? = nil
private struct RunState {
let port: CFMachPort
let setStatus: (String) -> Void
}
func start(_ setStatus: @escaping (String) -> Void) {
precondition(self.runState == nil)
os_log(.debug, log: self.log, "will create tap")
let info = Unmanaged.passRetained(self).toOpaque()
let mask = CGEventMask(1 << CGEventType.keyDown.rawValue)
guard let port = CGEvent.tapCreate(
tap: .cgSessionEventTap,
place: .headInsertEventTap,
options: .listenOnly,
eventsOfInterest: mask,
callback: { (proxy, type, event, info) -> Unmanaged<CGEvent>? in
let obj = Unmanaged<CGEventTapAction>.fromOpaque(info!).takeUnretainedValue()
obj.didReceiveEvent(event)
// We don’t replace the event, so the new event is the same as
// the old event, so we return it unretained.
return Unmanaged.passUnretained(event)
},
userInfo: info
) else {
os_log(.debug, log: self.log, "did not create tap")
// We retained `self` above, but the event tap didn’t get created so
// we need to clean up.
Unmanaged<CGEventTapAction>.fromOpaque(info).release()
setStatus("Failed to create event tap.")
return
}
let rls = CFMachPortCreateRunLoopSource(nil, port, 0)!
CFRunLoopAddSource(CFRunLoopGetCurrent(), rls, .defaultMode)
self.runState = RunState(port: port, setStatus: setStatus)
os_log(.debug, log: self.log, "did create tap")
}
private func didReceiveEvent(_ event: CGEvent) {
os_log(.debug, log: self.log, "did receive event")
guard let runState = self.runState else { return }
runState.setStatus("Last event at \(Date()).")
}
func stop() {
guard let runState = self.runState else { return }
self.runState = nil
os_log(.debug, log: self.log, "will stop tap")
CFMachPortInvalidate(runState.port)
// We passed a retained copy of `self` to the `info` parameter
// when we created the tap. We need to release that now that we’ve
// invalidated the tap.
Unmanaged.passUnretained(self).release()
os_log(.debug, log: self.log, "did stop tap")
}
}