Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions mac/Config/Installation/InstallationCheck.swift
Original file line number Diff line number Diff line change
Expand Up @@ -171,11 +171,19 @@ public class InstallationCheck {
public func startInstallationEvaluation() {
// call the input method to check whether Accessibility permission has been granted
if (self.isInputMethodInstalled && self.isInputMethodCurrent) &&
(self.isEvaluatingNewInstallation || self.installationState?.isComplete == true) {
self.inputMethodUtil.doAsyncAccessibilityCheck()
(self.isEvaluatingNewInstallation || self.isReadyForRepairCheckAtStartup()) {
self.inputMethodUtil.doAsyncAccessibilityCheck(forceInputMethodRestart: false)
}
}

/**
* Returns true if we should check at app startup whether the installation needs repair.
* Simply returns true if the installation is complete.
*/
func isReadyForRepairCheckAtStartup() -> Bool {
return self.installationState?.isComplete == true
}

/**
* Check whether the input method and configuration app are the same version.
* Because the version of the config app will not be sent when build locally, this can be overridden,
Expand Down
2 changes: 1 addition & 1 deletion mac/Config/Installation/InstallationContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@ public class InstallationContainer : ObservableObject {
* call Keyman as a separate process with an argument that checks whether accessibility has been granted by the user
*/
public func checkAccessibilityPermissionGranted() {
self.inputMethodUtil.doAsyncAccessibilityCheck()
self.inputMethodUtil.doAsyncAccessibilityCheck(forceInputMethodRestart: true)
}

/**
Expand Down
19 changes: 14 additions & 5 deletions mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

// distributed notifications
NSString *const kKeyboardsChanged = @"com.keyman.keyboards.changed";
NSString *const kAccessibilityCheckedRequest = @"com.keyman.accessibility.check.request";

// in-app notifications
NSString *const kKeymanKeyboardDownloadCompletedNotification = @"kKeymanKeyboardDownloadCompletedNotification";
Expand Down Expand Up @@ -83,9 +84,9 @@ - (id)init {
if (self) {
// first notify user and request access to Accessibility/PostEvent permissions
// pass block as completion handler to complete init with initCompletion
[PrivacyConsent.shared requestPrivacyAccess:^void (void){
[self initCompletion];
}];
// [PrivacyConsent.shared requestPrivacyAccess:^void (void){
// [self initCompletion];
// }];
}

return self;
Expand Down Expand Up @@ -131,12 +132,12 @@ - (void)initCompletion {

// register to receive notifications generated from Keyman Configuration App
[[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(handleKeyboardsChanged:) name:kKeyboardsChanged object:nil];

[[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(handleAccessibilityCheckRequested:) name:kAccessibilityCheckedRequest object:nil];

// start Input Method lifecycle
[KMInputMethodLifecycle.shared startLifecycle];
}


/**
* When packages have been installed, removed, enabled or disabled -- notification from the Keyman Configuration app
*/
Expand All @@ -145,6 +146,14 @@ - (void)handleKeyboardsChanged:(NSNotification *)notification {
[self reloadEnabledKeyboards];
}

/**
* When packages have been installed, removed, enabled or disabled -- notification from the Keyman Configuration app
*/
- (void)handleAccessibilityCheckRequested:(NSNotification *)notification {
BOOL hasAccess = checkAccessibility();
os_log_info([KMLogs startupLog], "KMInputMethodAppDelegate handleAccessibilityCheckRequested, hasAccess: %{public}@", hasAccess?@"YES":@"NO");
}

/**
* When the input method is activated -- notification from KMInputMethodLifecycle
*/
Expand Down
12 changes: 12 additions & 0 deletions mac/Keyman4MacIM/Keyman4MacIM/Privacy/PrivacyConsent.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@

NS_ASSUME_NONNULL_BEGIN

// command strings passed from Keyman Configuration
extern NSString *kMigrateCommand;
extern NSString *kAccessCommand;
extern NSString *kCheckCommand;

// notification messages sent to Keyman Configuration
extern NSString *kAcessibilityPermissionGrantedMessage;
extern NSString *kAcessibilityPermissionNotGrantedMessage;

int requestAccessibility(void);
int checkAccessibility(void);

@interface PrivacyConsent : NSObject
@property (nonatomic, strong) PrivacyWindowController *privacyDialog;
@property (nonatomic, copy, nullable) void (^completionHandler)(void);
Expand Down
42 changes: 42 additions & 0 deletions mac/Keyman4MacIM/Keyman4MacIM/Privacy/PrivacyConsent.m
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,47 @@
#import "PrivacyConsent.h"
#import "KMLogs.h"

// command strings passed from Keyman Configuration
NSString *kMigrateCommand = @"migrate";
NSString *kAccessCommand = @"access";
NSString *kCheckCommand = @"check";

// notification messages sent to Keyman Configuration
NSString *kAcessibilityPermissionGrantedMessage = @"granted";
NSString *kAcessibilityPermissionNotGrantedMessage = @"not-granted";

/**
* Make a request to the system to add Accessibility permissions for the Keyman input method.
* Executed as requested by the Keyman Configuration app.
*/
int requestAccessibility(void) {
os_log_info([KMLogs startupLog], "requestAccessibility executed");
[PrivacyConsent.shared requestPrivacyAccessForKeyman19:^void (void){
os_log_info([KMLogs startupLog], "requestAccessibility completion handler: requestPrivacyAccessForKeyman19 completed");
}];
return 0;
}

/**
* Check whether Accessibility permissions have been granted by the user for the Keyman input method.
* Executed as requested by the Keyman Configuration app.
*/
int checkAccessibility(void) {
BOOL hasAccess = NO;
NSString *message = kAcessibilityPermissionNotGrantedMessage;

hasAccess = [PrivacyConsent.shared checkPostEventAccess];
os_log_info([KMLogs startupLog], "checkAccessibility hasAccess: %{public}@", hasAccess?@"YES":@"NO");

if (hasAccess) {
message = kAcessibilityPermissionGrantedMessage;
}

[[NSDistributedNotificationCenter defaultCenter] postNotificationName:@"com.keyman.accessibility.state" object:message userInfo:nil deliverImmediately:YES];

return hasAccess;
}

@implementation PrivacyConsent

+ (PrivacyConsent *)shared
Expand Down Expand Up @@ -262,4 +303,5 @@ - (BOOL)requestPostEventAccess
return granted;
}


@end
45 changes: 0 additions & 45 deletions mac/Keyman4MacIM/Keyman4MacIM/main.m
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,6 @@
const NSString *kConnectionName = @"Keyman_Input_Connection";
IMKServer *server;

// command strings passed from Keyman Configuration
NSString *kMigrateCommand = @"migrate";
NSString *kAccessCommand = @"access";
NSString *kCheckCommand = @"check";

// notification messages sent to Keyman Configuration
NSString *kAcessibilityPermissionGrantedMessage = @"granted";
NSString *kAcessibilityPermissionNotGrantedMessage = @"not-granted";


void runAsInputMethod(void) {
os_log_info([KMLogs startupLog], "main runAsInputMethod");
NSString *identifier = [[NSBundle mainBundle] bundleIdentifier];
Expand Down Expand Up @@ -70,41 +60,6 @@ int doMigration(void) {
return 0;
}

/**
* Make a request to the system to add Accessibility permissions for the Keyman input method.
* Executed as requested by the Keyman Configuration app.
*/
int requestAccessibility(void) {
os_log_info([KMLogs startupLog], "doAccessibility executed");
[PrivacyConsent.shared requestPrivacyAccessForKeyman19:^void (void){
os_log_info([KMLogs startupLog], "doAccessibility completion handler: requestPrivacyAccessForKeyman19 completed");
}];
return 0;
}

/**
* Check whether Accessibility permissions have been granted by the user for the Keyman input method.
* Executed as requested by the Keyman Configuration app.
*/
int checkAccessibility(void) {
BOOL hasAccess = NO;
NSString *message = kAcessibilityPermissionNotGrantedMessage;

hasAccess = [PrivacyConsent.shared checkPostEventAccess];
os_log_info([KMLogs startupLog], "checkAccessibility hasAccess: %{public}@", hasAccess?@"YES":@"NO");

if (hasAccess) {
hasAccess = 0;
message = kAcessibilityPermissionGrantedMessage;
} else {
hasAccess = 1;
}

[[NSDistributedNotificationCenter defaultCenter] postNotificationName:@"com.keyman.accessibility.state" object:message userInfo:nil deliverImmediately:YES];

return hasAccess;
}

int main(int argc, const char * argv[]) {

@autoreleasepool {
Expand Down
52 changes: 44 additions & 8 deletions mac/KeymanSettings/Sources/KeymanSettings/InputMethodUtil.swift
Original file line number Diff line number Diff line change
Expand Up @@ -191,21 +191,57 @@ public class InputMethodUtil {

/**
* Calls Keyman input method to check whether it has accessibility permission granted.
* The actual result is not returned from Keyman when called as a separate process.
* The call varies based on whether Keyman is running or not.
*
* If the input method not running, then invoke it as a separate process with a special argument
* so that it runs, checks the access, sends a notification and exits.
*
* If the input method is running, then do not kill it unless `forceRestart` flag is set to true.
* Instead send a notification to instruct it to make the check and respond.
*
* If the input method is running and `forceRestart` is true, then kill Keyman and invoke it
* as a separate process. This is needed when a user is in the middle of an install or repair.
*
* The actual result is not returned from Keyman.
* After this function is called, listen to the `DistributedNotificationCenter` for the notification named
* `accessibilityStateResponse`
* It contains a message with a value of `granted` or `not-granted`
*/
func invokeKeymanInputMethodCheckAccess() throws {
func invokeKeymanInputMethodCheckAccess(forceInputMethodRestart forceRestart: Bool) throws {
Logger.setup.info("invokeKeymanInputMethodCheckAccess()")
LogUtil.infoBreadcrumb("invokeKeymanInputMethodCheckAccess()", category: .setup)
// because we are launching Keyman with a specific command line argument
// for this request, we must kill it first
_ = self.killKeymanInputMethod()

try self.launchKeymanInputMethodAsSeparateProcess(argument: kCheckCommand)
var keymanIsRunning = self.isKeymanInputMethodRunning()

if keymanIsRunning, forceRestart {
// kill Keyman so that it refreshes its accessibility state
_ = self.killKeymanInputMethod()
keymanIsRunning = false
Logger.setup.info("invokeKeymanInputMethodCheckAccess(), killed Keyman")
}

if keymanIsRunning {
// Keyman is running: check accessibility by sending distributed notification
self.sendAccessibilityCheckRequest()
} else {
// Keyman is not running, launch and check accessibility with specific command line argument
try self.launchKeymanInputMethodAsSeparateProcess(argument: kCheckCommand)
}
}

/**
* Send a distributed notification that the keyboards have changed.
* The input method will receive this and reload the enabled keyboards.
*/
func sendAccessibilityCheckRequest() {
DistributedNotificationCenter.default().postNotificationName (
.accessibilityCheckRequest,
object: nil,
userInfo: nil,
deliverImmediately: true
)
}

/**
* run Keyman as a subprocess with the specifed argument and return the result
*/
Expand Down Expand Up @@ -263,9 +299,9 @@ public class InputMethodUtil {
* Calls Keyman input method to check whether it has accessibility permission granted.
* Receives response as distributed notification named `accessibilityStateResponse`
*/
public func doAsyncAccessibilityCheck() {
public func doAsyncAccessibilityCheck(forceInputMethodRestart forceRestart: Bool) {
do {
try self.invokeKeymanInputMethodCheckAccess()
try self.invokeKeymanInputMethodCheckAccess(forceInputMethodRestart: forceRestart)
} catch {
Logger.setup.error("invoking Keyman failed: \(error as NSError, privacy: .public)")
LogUtil.errorBreadcrumb("invoking Keyman failed: \(error as NSError)", category: .setup)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ public extension Notification.Name {
static let accessibilityStateResponse = Notification.Name("com.keyman.accessibility.state")
// sent from config app (DefaultsRepository), received by input method
static let keyboardsChanged = Notification.Name("com.keyman.keyboards.changed")
// sent from config app (DefaultsRepository), received by input method
static let accessibilityCheckRequest = Notification.Name("com.keyman.accessibility.check.request")
}

// in-app notifications
Expand Down