Skip to content

Commit

Permalink
feat: allow to run the app without screen-recording permissions
Browse files Browse the repository at this point in the history
closes #1082
  • Loading branch information
lwouis committed Oct 17, 2024
1 parent 19f59b2 commit 129d061
Show file tree
Hide file tree
Showing 9 changed files with 190 additions and 97 deletions.
42 changes: 30 additions & 12 deletions resources/l10n/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@
/* No comment provided by engineer. */
"AltTab is currently set to show Applications. This setting is only available when AltTab is set to show Windows." = "AltTab is currently set to show Applications. This setting is only available when AltTab is set to show Windows.";

/* Menubar callout */
"AltTab is running without Screen Recording permissions. Thumbnails won’t show." = "AltTab is running without Screen Recording permissions. Thumbnails won’t show.";

/* No comment provided by engineer. */
"AltTab needs some permissions" = "AltTab needs some permissions";

Expand Down Expand Up @@ -187,6 +190,9 @@
/* No comment provided by engineer. */
"General" = "General";

/* Menubar callout button */
"Grant permission" = "Grant permission";

/* No comment provided by engineer. */
"Hide" = "Hide";

Expand Down Expand Up @@ -226,9 +232,18 @@
/* No comment provided by engineer. */
"Ignore shortcuts when active" = "Ignore shortcuts when active";

/* No comment provided by engineer. */
"Language" = "Language";

/* No comment provided by engineer. */
"Language Change" = "Language Change";

/* No comment provided by engineer. */
"Large" = "Large";

/* No comment provided by engineer. */
"Later" = "Later";

/* No comment provided by engineer. */
"Latest releases" = "Latest releases";

Expand Down Expand Up @@ -310,6 +325,9 @@
/* No comment provided by engineer. */
"Reset preferences and restart…" = "Reset preferences and restart…";

/* No comment provided by engineer. */
"Restart Now" = "Restart Now";

/* No comment provided by engineer. */
"Screen including menu bar" = "Screen including menu bar";

Expand Down Expand Up @@ -427,6 +445,9 @@
/* No comment provided by engineer. */
"Size" = "Size";

/* No comment provided by engineer. */
"Skipped" = "Skipped";

/* No comment provided by engineer. */
"Small" = "Small";

Expand Down Expand Up @@ -454,6 +475,12 @@
/* No comment provided by engineer. */
"System" = "System";

/* No comment provided by engineer. */
"System Default" = "System Default";

/* No comment provided by engineer. */
"The application needs to restart to apply the language change." = "The application needs to restart to apply the language change.";

/* No comment provided by engineer. */
"Theme" = "Theme";

Expand Down Expand Up @@ -481,6 +508,9 @@
/* No comment provided by engineer. */
"Updates policy" = "Updates policy";

/* No comment provided by engineer. */
"Use the app without this permission. Thumbnails won’t show." = "Use the app without this permission. Thumbnails won’t show.";

/* No comment provided by engineer. */
"Version" = "Version";

Expand Down Expand Up @@ -525,15 +555,3 @@

/* No comment provided by engineer. */
"You didn’t write your email, thus can’t receive any response." = "You didn’t write your email, thus can’t receive any response.";

"Language" = "Language";

"System Default" = "System Default";

"Language Change" = "Language Change";

"The application needs to restart to apply the language change." = "The application needs to restart to apply the language change.";

"Restart Now" = "Restart Now";

"Later" = "Later";
2 changes: 2 additions & 0 deletions src/logic/Preferences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ class Preferences {
"hideWindowlessApps": "false",
"hideThumbnails": "false",
"previewFocusedWindow": "false",
"screenRecordingPermissionSkipped": "false",
]

// system preferences
Expand Down Expand Up @@ -137,6 +138,7 @@ class Preferences {
static var startAtLogin: Bool { defaults.bool("startAtLogin") }
static var blacklist: [BlacklistEntry] { jsonDecode([BlacklistEntry].self, defaults.string("blacklist")) }
static var previewFocusedWindow: Bool { defaults.bool("previewFocusedWindow") }
static var screenRecordingPermissionSkipped: Bool { defaults.bool("screenRecordingPermissionSkipped") }

// macro values
static var appearanceStyle: AppearanceStylePreference { defaults.macroPref("appearanceStyle", AppearanceStylePreference.allCases) }
Expand Down
103 changes: 62 additions & 41 deletions src/logic/SystemPermissions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,23 @@ import Cocoa

// macOS has some privacy restrictions. The user needs to grant certain permissions, app by app, in System Preferences > Security & Privacy
class SystemPermissions {
static var timer: Timer!
static var preStartupPermissionsPassed = false
static var timerPermissionsToUpdatePermissionsWindow: Timer?
static var timerPermissionsRemovedWhileAltTabIsRunning: Timer?

static func accessibilityIsGranted() -> Bool {
static func accessibilityIsGranted() -> PermissionStatus {
if #available(macOS 10.9, *) {
return AXIsProcessTrustedWithOptions([kAXTrustedCheckOptionPrompt.takeRetainedValue(): false] as CFDictionary)
return AXIsProcessTrustedWithOptions([kAXTrustedCheckOptionPrompt.takeRetainedValue(): false] as CFDictionary) ? .granted : .notGranted
}
return true
return .granted
}

static func screenRecordingIsGranted() -> Bool {
static func screenRecordingIsGranted() -> PermissionStatus {
if #available(macOS 10.15, *) {
return screenRecordingIsGranted_()
return screenRecordingIsGranted_() ? .granted :
(Preferences.screenRecordingPermissionSkipped ? .skipped : .notGranted)
}
return true
return .granted
}

// workaround: public API CGPreflightScreenCaptureAccess and private API SLSRequestScreenCaptureAccess exist, but
Expand All @@ -33,65 +36,83 @@ class SystemPermissions {
) != nil
}

static func observePermissionsPostStartup() {
static func observePermissionsRemovedWhileAltTabIsRunning() {
var counter = 0
timer = Timer(timeInterval: 5, repeats: true, block: { _ in
if !accessibilityIsGranted() {
App.app.restart()
}
if !screenRecordingIsGranted() {
// permission check may yield a false negative during wake-up
// we restart after 2 negative checks
if counter >= 2 {
timerPermissionsRemovedWhileAltTabIsRunning = Timer(timeInterval: 5, repeats: true, block: { _ in
DispatchQueue.main.async {
let accessibility = accessibilityIsGranted()
let screenRecording = screenRecordingIsGranted()
logger.d(accessibility, screenRecording, preStartupPermissionsPassed)
Menubar.permissionCalloutMenuItems?.forEach {
$0.isHidden = screenRecording != .skipped
}
if accessibility == .notGranted {
App.app.restart()
}
if screenRecording == .notGranted {
// permission check may yield a false negative during wake-up
// we restart after 2 negative checks
if counter >= 2 {
App.app.restart()
} else {
counter += 1
}
} else {
counter += 1
counter = 0
}
} else {
counter = 0
}
})
timer.tolerance = 4.9
CFRunLoopAddTimer(BackgroundWork.systemPermissionsThread.runLoop, timer, .defaultMode)
timerPermissionsRemovedWhileAltTabIsRunning!.tolerance = 4.9
CFRunLoopAddTimer(BackgroundWork.systemPermissionsThread.runLoop, timerPermissionsRemovedWhileAltTabIsRunning!, .defaultMode)
}

static func observePermissionsPreStartup(_ startupBlock: @escaping () -> Void) {
if #available(macOS 10.15, *) {
static func observePermissionsToUpdatePermissionsWindow(_ startupBlock: @escaping () -> Void) {
if #available(macOS 10.15, *), !preStartupPermissionsPassed {
// this call triggers the permission prompt, however it's the only way to force the app to be listed with a checkbox
SLSRequestScreenCaptureAccess()
}
timer = Timer(timeInterval: 0.1, repeats: true) { _ in
let accessibility = accessibilityIsGranted()
let screenRecording = screenRecordingIsGranted()
timerPermissionsToUpdatePermissionsWindow = Timer(timeInterval: 0.1, repeats: true) { _ in
DispatchQueue.main.async {
if accessibility && screenRecording {
timer.invalidate()
App.app.permissionsWindow?.close()
startupBlock()
} else {
if accessibility != App.app.permissionsWindow.accessibilityView.isPermissionGranted {
App.app.permissionsWindow.accessibilityView.updatePermissionStatus(accessibility)
let accessibility = accessibilityIsGranted()
let screenRecording = screenRecordingIsGranted()
logger.d(accessibility, screenRecording, preStartupPermissionsPassed)
Menubar.permissionCalloutMenuItems?.forEach {
$0.isHidden = screenRecording != .skipped
}
if accessibility != App.app.permissionsWindow?.accessibilityView?.permissionStatus {
App.app.permissionsWindow?.accessibilityView.updatePermissionStatus(accessibility)
}

if #available(macOS 10.15, *), screenRecording != App.app.permissionsWindow?.screenRecordingView?.permissionStatus {
App.app.permissionsWindow?.screenRecordingView?.updatePermissionStatus(screenRecording)
}
if !preStartupPermissionsPassed {
if accessibility != .notGranted && screenRecording != .notGranted {
preStartupPermissionsPassed = true
App.app.permissionsWindow?.close()
startupBlock()
}
if #available(macOS 10.15, *), screenRecording != App.app.permissionsWindow.screenRecordingView.isPermissionGranted {
App.app.permissionsWindow.screenRecordingView.updatePermissionStatus(screenRecording)
} else {
if accessibility == .notGranted || screenRecording == .notGranted {
App.app.restart()
}
}
}
}
timer.tolerance = 0.09
CFRunLoopAddTimer(BackgroundWork.systemPermissionsThread.runLoop, timer, .defaultMode)
timerPermissionsToUpdatePermissionsWindow!.tolerance = 0.09
CFRunLoopAddTimer(BackgroundWork.systemPermissionsThread.runLoop, timerPermissionsToUpdatePermissionsWindow!, .defaultMode)
}

static func ensurePermissionsAreGranted(_ continueAppStartup: @escaping () -> Void) {
let startupBlock = {
observePermissionsPostStartup()
observePermissionsRemovedWhileAltTabIsRunning()
continueAppStartup()
}
if accessibilityIsGranted() && screenRecordingIsGranted() {
if accessibilityIsGranted() != .notGranted && screenRecordingIsGranted() != .notGranted {
preStartupPermissionsPassed = true
startupBlock()
} else {
App.app.permissionsWindow.show()
observePermissionsPreStartup(startupBlock)
App.app.permissionsWindow.show(startupBlock)
}
}
}
10 changes: 5 additions & 5 deletions src/ui/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ class App: AppCenterApplication, NSApplicationDelegate {
static let repository = "https://github.com/lwouis/alt-tab-macos"
static let website = "https://alt-tab-macos.netlify.app"
static var app: App!
var menubar: Menubar!
var thumbnailsPanel: ThumbnailsPanel!
var previewPanel: PreviewPanel!
var preferencesWindow: PreferencesWindow!
Expand Down Expand Up @@ -59,14 +58,14 @@ class App: AppCenterApplication, NSApplicationDelegate {
PFMoveToApplicationsFolderIfNecessary()
#endif
AXUIElement.setGlobalTimeout()
Preferences.initialize()
BackgroundWork.startSystemPermissionThread()
permissionsWindow = PermissionsWindow()
SystemPermissions.ensurePermissionsAreGranted { [weak self] in
guard let self = self else { return }
BackgroundWork.start()
Preferences.initialize()
Appearance.update()
self.menubar = Menubar()
Menubar.initialize()
self.loadMainMenuXib()
self.thumbnailsPanel = ThumbnailsPanel()
self.previewPanel = PreviewPanel()
Expand All @@ -80,7 +79,7 @@ class App: AppCenterApplication, NSApplicationDelegate {
DispatchQueue.main.async { () -> () in Windows.sortByLevel() }
self.preloadWindows()
#if DEBUG
self.showPreferencesWindow()
// self.showPreferencesWindow()
#endif
}
}
Expand Down Expand Up @@ -117,6 +116,7 @@ class App: AppCenterApplication, NSApplicationDelegate {
func restart() {
// we use -n to open a new instance, to avoid calling applicationShouldHandleReopen
// we use Bundle.main.bundlePath in case of multiple AltTab versions on the machine
printStackTrace()
Process.launchedProcess(launchPath: "/usr/bin/open", arguments: ["-n", Bundle.main.bundlePath])
App.shared.terminate(self)
}
Expand Down Expand Up @@ -180,7 +180,7 @@ class App: AppCenterApplication, NSApplicationDelegate {
}

@objc func checkPermissions(_ sender: NSMenuItem) {
permissionsWindow.show()
permissionsWindow.show({})
}

@objc func supportProject() {
Expand Down
Loading

0 comments on commit 129d061

Please sign in to comment.