diff --git a/src/ios/CDVMobileAccessibility.h b/src/ios/CDVMobileAccessibility.h index 77d08d0..4b4781f 100644 --- a/src/ios/CDVMobileAccessibility.h +++ b/src/ios/CDVMobileAccessibility.h @@ -21,12 +21,28 @@ @interface CDVMobileAccessibility : CDVPlugin { NSString* callbackId; + NSString* commandCallbackId; + BOOL voiceOverRunning; + BOOL closeCaptioningEnabled; + BOOL guidedAccessEnabled; + BOOL invertColorsEnabled; + BOOL monoAudioEnabled; } @property (strong) NSString* callbackId; +@property (strong) NSString* commandCallbackId; +@property BOOL voiceOverRunning; +@property BOOL closeCaptioningEnabled; +@property BOOL guidedAccessEnabled; +@property BOOL invertColorsEnabled; +@property BOOL monoAudioEnabled; - (void) isVoiceOverRunning:(CDVInvokedUrlCommand*)command; - (void) isClosedCaptioningEnabled:(CDVInvokedUrlCommand*)command; +- (void) isGuidedAccessEnabled:(CDVInvokedUrlCommand*)command; +- (void) isInvertColorsEnabled:(CDVInvokedUrlCommand*)command; +- (void) isMonoAudioEnabled:(CDVInvokedUrlCommand*)command; +- (void) postAnnouncementNotification:(CDVInvokedUrlCommand*)command; - (void) start:(CDVInvokedUrlCommand*)command; - (void) stop:(CDVInvokedUrlCommand*)command; diff --git a/src/ios/CDVMobileAccessibility.m b/src/ios/CDVMobileAccessibility.m index 28f3e81..4486936 100644 --- a/src/ios/CDVMobileAccessibility.m +++ b/src/ios/CDVMobileAccessibility.m @@ -19,6 +19,7 @@ #import "CDVMobileAccessibility.h" #import +#import @interface CDVMobileAccessibility () // add any property overrides @@ -27,6 +28,12 @@ @implementation CDVMobileAccessibility @synthesize callbackId; +@synthesize commandCallbackId; +@synthesize voiceOverRunning; +@synthesize closeCaptioningEnabled; +@synthesize guidedAccessEnabled; +@synthesize invertColorsEnabled; +@synthesize monoAudioEnabled; // ////////////////////////////////////////////////// @@ -45,6 +52,8 @@ if ([self settingForKey:setting]) { // set your setting, other init here } + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onPause) name:UIApplicationDidEnterBackgroundNotification object:nil]; } // ////////////////////////////////////////////////// @@ -52,6 +61,9 @@ - (void)dealloc { [self stop:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillEnterForegroundNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil]; } - (void)onReset @@ -59,24 +71,126 @@ [self stop:nil]; } +- (void) onPause { + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onResume) name:UIApplicationWillEnterForegroundNotification object:nil]; +} + +- (void)onResume +{ + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillEnterForegroundNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onPause) name:UIApplicationDidEnterBackgroundNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onApplicationDidBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil]; +} + +- (void)onApplicationDidBecomeActive +{ + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil]; + [self performSelector:@selector(sendMobileAccessibilityStatusChangedCallback) withObject:nil afterDelay:0.1 ]; +} + // ////////////////////////////////////////////////// #pragma Plugin interface - (void)isVoiceOverRunning:(CDVInvokedUrlCommand*)command { - CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsBool:UIAccessibilityIsVoiceOverRunning()]; - [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + [self.commandDelegate runInBackground:^{ + self.voiceOverRunning = UIAccessibilityIsVoiceOverRunning(); + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsBool:self.voiceOverRunning]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + }]; } - (void)isClosedCaptioningEnabled:(CDVInvokedUrlCommand*)command { - CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsBool:UIAccessibilityIsClosedCaptioningEnabled()]; - [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + [self.commandDelegate runInBackground:^{ + self.closeCaptioningEnabled = [self getClosedCaptioningEnabledStatus]; + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsBool:self.closeCaptioningEnabled]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + }]; } +- (void)isGuidedAccessEnabled:(CDVInvokedUrlCommand*)command +{ + [self.commandDelegate runInBackground:^{ + self.guidedAccessEnabled = UIAccessibilityIsGuidedAccessEnabled(); + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsBool:self.guidedAccessEnabled]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + }]; +} + +- (void)isInvertColorsEnabled:(CDVInvokedUrlCommand*)command +{ + [self.commandDelegate runInBackground:^{ + self.invertColorsEnabled = UIAccessibilityIsInvertColorsEnabled(); + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsBool:self.invertColorsEnabled]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + }]; +} + +- (void)isMonoAudioEnabled:(CDVInvokedUrlCommand*)command +{ + [self.commandDelegate runInBackground:^{ + self.monoAudioEnabled = UIAccessibilityIsMonoAudioEnabled(); + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsBool:self.monoAudioEnabled]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + }]; +} + +- (void)postAnnouncementNotification:(CDVInvokedUrlCommand *)command +{ + CDVPluginResult* result = nil; + NSString* notificationString = [command.arguments objectAtIndex:0]; + + if (UIAccessibilityIsVoiceOverRunning() && + notificationString != nil && [notificationString length] > 0) { + self.commandCallbackId = command.callbackId; + [self.commandDelegate runInBackground:^{ + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mobileAccessibilityAnnouncementDidFinish:) name:UIAccessibilityAnnouncementDidFinishNotification object:nil]; + UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, notificationString); + }]; + } else { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + } +} + +- (void)mobileAccessibilityAnnouncementDidFinish:(NSNotification *)dict +{ + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIAccessibilityAnnouncementDidFinishNotification object:nil]; + + NSString* valueSpoken = [[dict userInfo] objectForKey:UIAccessibilityAnnouncementKeyStringValue]; + NSString* wasSuccessful = [[dict userInfo] objectForKey:UIAccessibilityAnnouncementKeyWasSuccessful]; + + NSMutableDictionary* data = [NSMutableDictionary dictionaryWithCapacity:2]; + [data setObject:valueSpoken forKey:@"stringValue"]; + [data setObject:wasSuccessful forKey:@"wasSuccessful"]; + + if (self.commandCallbackId) { + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:data]; + [self.commandDelegate sendPluginResult:result callbackId:self.commandCallbackId]; + self.commandCallbackId = nil; + } +} + +- (BOOL)getClosedCaptioningEnabledStatus +{ + BOOL status = false; + if (&MACaptionAppearanceGetDisplayType) { + status = (MACaptionAppearanceGetDisplayType(kMACaptionAppearanceDomainUser) > kMACaptionAppearanceDisplayTypeAutomatic); + } else { + status = UIAccessibilityIsClosedCaptioningEnabled(); + } + return status; +} - (void)mobileAccessibilityStatusChanged:(NSNotification *)notification +{ + [self sendMobileAccessibilityStatusChangedCallback]; +} + +- (void)sendMobileAccessibilityStatusChangedCallback { if (self.callbackId) { CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:[self getMobileAccessibilityStatus]]; @@ -88,38 +202,59 @@ /* Get the current mobile accessibility status. */ - (NSDictionary*)getMobileAccessibilityStatus { - NSMutableDictionary* mobileAccessibilityData = [NSMutableDictionary dictionaryWithCapacity:2]; - [mobileAccessibilityData setObject:[NSNumber numberWithBool:UIAccessibilityIsVoiceOverRunning()] forKey:@"isVoiceOverRunning"]; - [mobileAccessibilityData setObject:[NSNumber numberWithBool:UIAccessibilityIsClosedCaptioningEnabled()] forKey:@"isClosedCaptioningEnabled"]; + self.voiceOverRunning = UIAccessibilityIsVoiceOverRunning(); + self.closeCaptioningEnabled = [self getClosedCaptioningEnabledStatus]; + self.guidedAccessEnabled = UIAccessibilityIsGuidedAccessEnabled(); + self.invertColorsEnabled = UIAccessibilityIsInvertColorsEnabled(); + self.monoAudioEnabled = UIAccessibilityIsMonoAudioEnabled(); + + NSMutableDictionary* mobileAccessibilityData = [NSMutableDictionary dictionaryWithCapacity:5]; + [mobileAccessibilityData setObject:[NSNumber numberWithBool:self.voiceOverRunning] forKey:@"isVoiceOverRunning"]; + [mobileAccessibilityData setObject:[NSNumber numberWithBool:self.closeCaptioningEnabled] forKey:@"isClosedCaptioningEnabled"]; + [mobileAccessibilityData setObject:[NSNumber numberWithBool:self.guidedAccessEnabled] forKey:@"isGuidedAccessEnabled"]; + [mobileAccessibilityData setObject:[NSNumber numberWithBool:self.invertColorsEnabled] forKey:@"isInvertColorsEnabled"]; + [mobileAccessibilityData setObject:[NSNumber numberWithBool:self.monoAudioEnabled] forKey:@"isMonoAudioEnabled"]; return mobileAccessibilityData; } -/* turn on MobileAccessibility monitoring*/ +/* start MobileAccessibility monitoring */ - (void)start:(CDVInvokedUrlCommand*)command { - self.callbackId = command.callbackId; + [self.commandDelegate runInBackground:^{ + self.callbackId = command.callbackId; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mobileAccessibilityStatusChanged:) name:UIAccessibilityVoiceOverStatusChanged object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mobileAccessibilityStatusChanged:) name:UIAccessibilityClosedCaptioningStatusDidChangeNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mobileAccessibilityStatusChanged:) name:UIAccessibilityGuidedAccessStatusDidChangeNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mobileAccessibilityStatusChanged:) name:UIAccessibilityInvertColorsStatusDidChangeNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mobileAccessibilityStatusChanged:) name:UIAccessibilityMonoAudioStatusDidChangeNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mobileAccessibilityStatusChanged:) - name:UIAccessibilityVoiceOverStatusChanged object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mobileAccessibilityStatusChanged:) - name:UIAccessibilityClosedCaptioningStatusDidChangeNotification object:nil]; + // Update the callback on start + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:[self getMobileAccessibilityStatus]]; + [result setKeepCallbackAsBool:YES]; + [self.commandDelegate sendPluginResult:result callbackId:self.callbackId]; + }]; } +/* stop MobileAccessibility monitoring */ - (void)stop:(CDVInvokedUrlCommand*)command { - // callback one last time to clear the callback function on JS side - if (self.callbackId) - { - CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:[self getMobileAccessibilityStatus]]; - [result setKeepCallbackAsBool:NO]; - [self.commandDelegate sendPluginResult:result callbackId:self.callbackId]; - } - self.callbackId = nil; - [[NSNotificationCenter defaultCenter] removeObserver:self - name:UIAccessibilityVoiceOverStatusChanged object:nil]; - [[NSNotificationCenter defaultCenter] removeObserver:self - name:UIAccessibilityClosedCaptioningStatusDidChangeNotification object:nil]; + [self.commandDelegate runInBackground:^{ + // callback one last time to clear the callback function on JS side + if (self.callbackId) + { + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:[self getMobileAccessibilityStatus]]; + [result setKeepCallbackAsBool:NO]; + [self.commandDelegate sendPluginResult:result callbackId:self.callbackId]; + } + self.callbackId = nil; + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIAccessibilityVoiceOverStatusChanged object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIAccessibilityClosedCaptioningStatusDidChangeNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIAccessibilityGuidedAccessStatusDidChangeNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIAccessibilityInvertColorsStatusDidChangeNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIAccessibilityAnnouncementDidFinishNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIAccessibilityMonoAudioStatusDidChangeNotification object:nil]; + }]; } @end diff --git a/www/mobile-accessibility.js b/www/mobile-accessibility.js index d43061b..37dcdef 100644 --- a/www/mobile-accessibility.js +++ b/www/mobile-accessibility.js @@ -1,4 +1,4 @@ -/* +cordova.define("com.phonegap.plugin.mobile-accessibility.mobile-accessibility", function(require, exports, module) {/* * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file @@ -26,58 +26,132 @@ var argscheck = require('cordova/argscheck'), var MobileAccessibility = function() { this._isVoiceOverRunning = false; this._isClosedCaptioningEnabled = false; + this._isGuidedAccessEnabled = false; + this._isInvertColorsEnabled = false; + this._isMonoAudioEnabled = false; // Create new event handlers on the window (returns a channel instance) this.channels = { voiceoverstatuschanged:cordova.addWindowEventHandler("voiceoverstatuschanged"), - closedcaptioningstatusdidchange:cordova.addWindowEventHandler("closedcaptioningstatusdidchange") + closedcaptioningstatusdidchange:cordova.addWindowEventHandler("closedcaptioningstatusdidchange"), + guidedaccessstatusdidchange:cordova.addWindowEventHandler("guidedaccessstatusdidchange"), + invertcolorsstatusdidchange:cordova.addWindowEventHandler("invertcolorsstatusdidchange"), + monoaudiostatusdidchange:cordova.addWindowEventHandler("monoaudiostatusdidchange") }; for (var key in this.channels) { this.channels[key].onHasSubscribersChange = MobileAccessibility.onHasSubscribersChange; } }; - + +/** + * @private + * @ignore + */ function handlers() { return mobileAccessibility.channels.voiceoverstatuschanged.numHandlers + - mobileAccessibility.channels.closedcaptioningstatusdidchange.numHandlers; + mobileAccessibility.channels.closedcaptioningstatusdidchange.numHandlers + + mobileAccessibility.channels.invertcolorsstatusdidchange.numHandlers + + mobileAccessibility.channels.monoaudiostatusdidchange.numHandlers + + mobileAccessibility.channels.guidedaccessstatusdidchange.numHandlers; }; /** + * * Event handlers for when callbacks get registered for mobileAccessibility. * Keep track of how many handlers we have so we can start and stop the native MobileAccessibility listener * appropriately. + * @private + * @ignore */ MobileAccessibility.onHasSubscribersChange = function() { // If we just registered the first handler, make sure native listener is started. if (this.numHandlers === 1 && handlers() === 1) { + console.log("MobileAccessibility.onHasSubscribersChange "+handlers()); exec(mobileAccessibility._status, mobileAccessibility._error, "MobileAccessibility", "start", []); } else if (handlers() === 0) { exec(null, null, "MobileAccessibility", "stop", []); } }; +/** + * Asynchronous call to native MobileAccessibility detemine if VoiceOver is running. + * @param {function} callback A callback method to recieve the asynchronous result from the native MobileAccessibility. + */ MobileAccessibility.prototype.isVoiceOverRunning = function(callback) { exec(callback, null, "MobileAccessibility", "isVoiceOverRunning", []); }; +/** + * Asynchronous call to native MobileAccessibility to detemine if closed captioning is enabled. + * @param {function} callback A callback method to recieve the asynchronous result from the native MobileAccessibility. + */ MobileAccessibility.prototype.isClosedCaptioningEnabled = function(callback) { exec(callback, null, "MobileAccessibility", "isClosedCaptioningEnabled", []); }; /** - * Callback for mobileAccessibility status + * Asynchronous call to native MobileAccessibility to detemine if the display colors have been inverted. + * @param {function} callback A callback method to recieve the asynchronous result from the native MobileAccessibility. + */ +MobileAccessibility.prototype.isInvertColorsEnabled = function(callback) { + exec(callback, null, "MobileAccessibility", "isInvertColorsEnabled", []); +}; + +/** + * Asynchronous call to native MobileAccessibility to detemine if mono audio is enabled. + * @param {function} callback A callback method to recieve the asynchronous result from the native MobileAccessibility. + */ +MobileAccessibility.prototype.isMonoAudioEnabled = function(callback) { + exec(callback, null, "MobileAccessibility", "isMonoAudioEnabled", []); +}; + +/** + * Asynchronous call to native MobileAccessibility to detemine if Guided Access is enabled. + * @param {function} callback A callback method to recieve the asynchronous result from the native MobileAccessibility. + */ +MobileAccessibility.prototype.isGuidedAccessEnabled = function(callback) { + exec(callback, null, "MobileAccessibility", "isGuidedAccessEnabled", []); +}; + +/** + * Posts an announcement notification with a string for VoiceOver to announce, if it is running. + * @param {string} string A string to be announced by VoiceOver. + * @param {function} callback A callback method to recieve the asynchronous result from the native MobileAccessibility, when the announcement is finished, the function should expect an object containing the stringValue that was voiced and a boolean indicating that the announcement wasSuccessful. + */ +MobileAccessibility.prototype.postAnnouncementNotification = function(string, callback) { + exec(callback, null, "MobileAccessibility", "postAnnouncementNotification", [string]); +}; + +/** + * Callback from native MobileAccessibility returning an which describes the status of iOS accessibility features. * - * @param {Object} info keys: isVoiceOverRunning, isClosedCaptioningEnabled + * @param {Object} info + * @config {Boolean} [isVoiceOverRunning] Boolean to indicate VoiceOver status. + * @config {Boolean} [isClosedCaptioningEnabled] Boolean to indicate closed captioning status. + * @config {Boolean} [isGuidedAccessEnabled] Boolean to indicate guided access status. + * @config {Boolean} [isInvertColorsEnabled] Boolean to indicate invert colors status. + * @config {Boolean} [isMonoAudioEnabled] Boolean to indicate mono audio status. */ MobileAccessibility.prototype._status = function(info) { if (info) { - var me = mobileAccessibility; - if (me._isVoiceOverRunning !== info.isVoiceOverRunning) { + if (mobileAccessibility._isVoiceOverRunning !== info.isVoiceOverRunning) { cordova.fireWindowEvent("voiceoverstatuschanged", info); - me._isVoiceOverRunning = info.isVoiceOverRunning; + mobileAccessibility._isVoiceOverRunning = info.isVoiceOverRunning; } - if (me._isClosedCaptioningEnabled !== info.isClosedCaptioningEnabled) { + if (mobileAccessibility._isClosedCaptioningEnabled !== info.isClosedCaptioningEnabled) { cordova.fireWindowEvent("closedcaptioningstatusdidchange", info); - me._isClosedCaptioningEnabled = info.isClosedCaptioningEnabled; + mobileAccessibility._isClosedCaptioningEnabled = info.isClosedCaptioningEnabled; + } + if (mobileAccessibility._isGuidedAccessEnabled !== info.isGuidedAccessEnabled) { + cordova.fireWindowEvent("guidedaccessstatusdidchange", info); + mobileAccessibility._isGuidedAccessEnabled = info.isGuidedAccessEnabled; + } + if (mobileAccessibility._isInvertColorsEnabled !== info.isInvertColorsEnabled) { + cordova.fireWindowEvent("invertcolorsstatusdidchange", info); + mobileAccessibility._isInvertColorsEnabled = info.isInvertColorsEnabled; + } + if (mobileAccessibility._isMonoAudioEnabled !== info.isMonoAudioEnabled) { + cordova.fireWindowEvent("monoaudiostatusdidchange", info); + mobileAccessibility._isMonoAudioEnabled = info.isMonoAudioEnabled; } } }; @@ -92,4 +166,4 @@ MobileAccessibility.prototype._error = function(e) { var mobileAccessibility = new MobileAccessibility(); module.exports = mobileAccessibility; -start \ No newline at end of file +});