Using AppleScript to enable assistive access for an application on Mavericks: localization and elegance
-
02-01-2020 - |
Question
Starting with Mac OS 10.9 Mavericks, Apple requires that Assistive Access must be enabled explicitly for each application that wants to use it.
I have written the following AppleScript function to deal with this, for a given application. The script works for me but it has 2 flaws:
When the operating system is running in a language other than English, the hard-coded button names will be wrong, and the script will fail. How can I discover what language the OS is running in, and what names the "Click the lock to make changes." button will have in that language? Alternatively, is there a way to determine whether this button is in the
locked
,authenticating
orunlocked
state, without reading the name of the button?The script uses a tight repeat loop while it is waiting for the user to enter an admin username and password. Is there a better strategy I can use to wait until the dialog has been been successfully dismissed?
====
set output to allowAssistiveAccessFor("Skype")
if (the |quitWhenDone| of output) then
tell application "System Preferences" to quit
end if
on allowAssistiveAccessFor(applicationName)
set quitWhenDone to not (application "System Preferences" is running)
set output to {quitWhenDone:quitWhenDone}
tell application "System Preferences"
activate
reveal anchor "Privacy_Accessibility" of pane id "com.apple.preference.security"
tell application "System Events"
tell process "System Preferences"
-- Find the table that contains the application icons and checkboxes
try
set appTable to table 1 of scroll area 1 of group 1 of tab group 1 of window "Security & Privacy"
on error errorMessage
return output & {state:-1, message:errorMessage}
end try
set total to the number of rows of appTable
-- Find the row that refers to applicationName
repeat with rowNumber from 1 to total
if (name of UI element 1 of row rowNumber of appTable = applicationName) then
set appCheckbox to checkbox 1 of UI element 1 of row rowNumber of appTable
if (value of appCheckbox as boolean) then
-- Assistive access is already enabled for this application
return output & {state:0, message:"Already enabled"}
else
-- Click the “Click the lock to make changes.” button.
if exists button "Click the lock to make changes." of window "Security & Privacy" then
click button "Click the lock to make changes." of window "Security & Privacy"
-- The user will now have to enter an admin password. This can take some time.
-- The name of the button will change to "Authenticating"...
set unlocking to button "Authenticating…" of window "Security & Privacy"
repeat while exists unlocking
end repeat
-- ... and then to "Click the lock to prevent further changes." ... unless the user cancelled
if exists button "Click the lock to make changes." of window "Security & Privacy" then
return output & {state:-1, message:"User cancelled"}
end if
end if
-- Click the <applicationName> checkbox.
-- If we had to unlock the Security & Privacy pane, then an immediate click might not have
-- an effect. Try as many times as possible for 1 second, and give up if unsuccessful
set failMessage to "Cannot allow the " & applicationName & " application to control your computer"
set endDate to (current date) + 1.0 -- 1 second from now
repeat
try
if ((current date) > endDate) then
-- Time's up
return output & {state:-1, message:failMessage}
end if
click appCheckbox
if (value of appCheckbox as boolean) then
return output & {state:0, message:"Success"}
end if
on error errorMessage
-- Something dreadful happened. Keep trying until time is up
end try
end repeat
end if
end if
end repeat
end tell
end tell
end tell
return output & {state:-1, message:"Application " & applicationName & " not found"}
end allowAssistiveAccessFor
Solution 2
This version of my script works regardless of what language the operating system is running in. Tested in French and Russian. It still uses a tight repeat loop for checking if the user has closed the authentication dialog, but this does not seem to affect performance.
set output to allowAssistiveAccessFor("skype")
if (the |quitWhenDone| of output) then
tell application "System Preferences" to quit
end if
get output
on allowAssistiveAccessFor(applicationName)
set quitWhenDone to not (application "System Preferences" is running)
set output to {quitWhenDone:quitWhenDone}
tell application "System Preferences"
reopen -- to ensure that the application will be open and window 1 will contain a tab group
activate
-- Open the Accessibility pane of the Security & Privacy Preferences
reveal anchor "Privacy_Accessibility" of pane id "com.apple.preference.security"
-- The "Security & Privacy" window and its contents are only available through System Events
tell application "System Events" to tell process "System Preferences"
-- Find the table that contains the application icons and checkboxes. This will be in
-- window 1 which is called "Security & Privacy" in English. If assistive access is
-- not enabled for AppleScript, then we will have no access to any window.
try
set securityAndPrivacyWindow to window 1
set appTable to table 1 of scroll area 1 of group 1 of tab group 1 of securityAndPrivacyWindow
on error errorMessage
return output & {state:-1, message:errorMessage}
end try
-- If we get here, then the assistive access is enabled for AppleScript.
-- Let's find the row that refers to applicationName
set total to the number of rows of appTable
repeat with rowNumber from 1 to total
if (name of UI element 1 of row rowNumber of appTable = applicationName) then
-- We have found the row containing the applicationName checkbox
set appCheckbox to checkbox 1 of UI element 1 of row rowNumber of appTable
if (value of appCheckbox as boolean) then
-- Assistive access is already enabled for this application
return output & {state:0, message:"Already enabled"}
else
(* Attempt to click on appCheckbox. If its value changes then the
Security & Privacy window was already unlocked. If it fails, then we will
need to ask the user to enter an admin name and password
*)
click appCheckbox
if (value of appCheckbox as boolean) then
return output & {state:0, message:"Success"}
end if
(* If we get here, then the click on appCheckbox had no effect, presumably
because this window is locked. We need to simulate a click on the
“Click the lock to make changes.” button. This may have different names in
different languages. All we know for certain is:
• It is button 4 (in Mavericks at least)
• Its name will change to something meaning "Authenticating…" when it is
clicked
• If it was in its locked state:
- a dialog will now show, and it will take the user a significant amount of
time to respond to the dialog
- Its name will change again when the user clicks on one of the buttons
in the dialog
- If its name reverts to the name it had before, then the user canceled
the dialog
- If its name becomes something new, then the user successfully
entered an admin name and password
• If it were in its unlocked state, it would immediately lock and its name would
not change again, so we would wait forever in the `repeat while` loop
However, if it were in its unlocked state then we would already have changed
the state of the applicationName checkbox, so we would not be here.
*)
set lockButton to button 4 of securityAndPrivacyWindow
click lockButton
-- The name of the button will have changed to something like "Authenticating"...
set unlocking to button 4 of securityAndPrivacyWindow
-- The user will now have to enter an admin password. This can take some time.
repeat while exists unlocking
-- The user has not yet clicked on either the Cancel or the Unlock button yet
end repeat
(* The user has closed the dialog. If s/he clicked Cancel then:
• The original name of the button will have been restored
• We cannot continue
*)
if exists lockButton then
return output & {state:-1, message:"User canceled"}
end if
-- If we get here, we can simulate a click on the <applicationName> checkbox.
click appCheckbox
if (value of appCheckbox as boolean) then
return output & {state:0, message:"Success after authentication"}
else
return output & {state:-1, message:"Failure after authentication"}
end if
end if
end if
end repeat
end tell
end tell
return output & {state:-1, message:"Application " & applicationName & " not found"}
end allowAssistiveAccessFor
OTHER TIPS
Instead of using the window and button names, you can refer to the window as “window 1” and the lock button as “button 4.” Then it shouldn’t matter what language the system is using.
tell application "System Preferences"
activate
reveal anchor "Privacy_Accessibility" of pane id "com.apple.preference.security"
tell application "System Events"
tell process "System Preferences"
tell button 4 of window 1 to click
end tell
end tell
end tell
Once the user has authenticated, the name of button 4 changes. So you could loop until you see that change. The following is not a perfect solution, because it only works on English language systems, but maybe it helps to get you a step closer.
tell application "System Preferences"
activate
reveal anchor "Privacy_Accessibility" of pane id "com.apple.preference.security"
tell application "System Events"
tell process "System Preferences"
tell button 4 of window 1 to click
with timeout of 180 seconds
repeat until button "Click the lock to prevent further changes." of window 1 exists
end repeat
display dialog "Preference pane unlocked." with title (the name as text) buttons {"OK"} default button "OK" giving up after 60
end timeout
end tell
end tell
end tell