diff --git a/.gitignore b/.gitignore index d1c9dae..0926331 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,7 @@ DerivedData .idea/ .project + +src/android/.classpath + +src/.gitignore diff --git a/plugin.xml b/plugin.xml index 227cba8..25d3730 100644 --- a/plugin.xml +++ b/plugin.xml @@ -1,5 +1,4 @@ - - + + + @@ -32,6 +33,18 @@ + + + + + + + + + + + + + - diff --git a/src/android/com/phonegap/plugin/mobileaccessibility/AbstractMobileAccessibilityHelper.java b/src/android/com/phonegap/plugin/mobileaccessibility/AbstractMobileAccessibilityHelper.java new file mode 100644 index 0000000..32d1d2b --- /dev/null +++ b/src/android/com/phonegap/plugin/mobileaccessibility/AbstractMobileAccessibilityHelper.java @@ -0,0 +1,18 @@ +package com.phonegap.plugin.mobileaccessibility; + +import android.view.ViewParent; + +public abstract class AbstractMobileAccessibilityHelper { + protected MobileAccessibility mMobileAccessibility; + protected ViewParent mParent; + public abstract void initialize(MobileAccessibility mobileAccessibility); + public abstract boolean isClosedCaptioningEnabled(); + public abstract boolean isScreenReaderRunning(); + public abstract boolean isTouchExplorationEnabled(); + public abstract void onAccessibilityStateChanged(boolean enabled); + public abstract void onCaptioningEnabledChanged(boolean enabled); + public abstract void onTouchExplorationStateChanged(boolean enabled); + public abstract void addStateChangeListeners(); + public abstract void removeStateChangeListeners(); + public abstract void announceForAccessibility(CharSequence text); +} diff --git a/src/android/com/phonegap/plugin/mobileaccessibility/DonutMobileAccessibilityHelper.java b/src/android/com/phonegap/plugin/mobileaccessibility/DonutMobileAccessibilityHelper.java new file mode 100644 index 0000000..0d6c21b --- /dev/null +++ b/src/android/com/phonegap/plugin/mobileaccessibility/DonutMobileAccessibilityHelper.java @@ -0,0 +1,80 @@ +package com.phonegap.plugin.mobileaccessibility; + + +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Build; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.webkit.WebView; + +@TargetApi(Build.VERSION_CODES.DONUT) +public class DonutMobileAccessibilityHelper extends + AbstractMobileAccessibilityHelper { + protected AccessibilityManager mAccessibilityManager; + protected WebView mWebView; + + @Override + public void initialize(MobileAccessibility mobileAccessibility) { + mMobileAccessibility = mobileAccessibility; + mWebView = mobileAccessibility.webView; + mParent = mobileAccessibility.webView.getParentForAccessibility(); + mAccessibilityManager = (AccessibilityManager) mMobileAccessibility.cordova.getActivity().getSystemService(Context.ACCESSIBILITY_SERVICE); + } + + @Override + public boolean isClosedCaptioningEnabled() { + return false; + } + + @Override + public boolean isScreenReaderRunning() { + return mAccessibilityManager.isEnabled(); + } + + @Override + public boolean isTouchExplorationEnabled() { + return false; + } + + @Override + public void onAccessibilityStateChanged(boolean enabled) { + mMobileAccessibility.onAccessibilityStateChanged(enabled); + } + + @Override + public void onCaptioningEnabledChanged(boolean enabled) { + mMobileAccessibility.onCaptioningEnabledChanged(enabled); + } + + @Override + public void onTouchExplorationStateChanged(boolean enabled) { + mMobileAccessibility.onTouchExplorationStateChanged(enabled); + } + + @Override + public void addStateChangeListeners() { + } + + @Override + public void removeStateChangeListeners() { + } + + @Override + public void announceForAccessibility(CharSequence text) { + if (!mAccessibilityManager.isEnabled()) { + return; + } + + final int eventType = AccessibilityEvent.TYPE_VIEW_FOCUSED; + final AccessibilityEvent event = AccessibilityEvent.obtain(eventType); + event.getText().add(text); + event.setEnabled(mWebView.isEnabled()); + event.setClassName(mWebView.getClass().getName()); + event.setPackageName(mWebView.getContext().getPackageName()); + event.setContentDescription(null); + + mAccessibilityManager.sendAccessibilityEvent(event); + } + +} diff --git a/src/android/com/phonegap/plugin/mobileaccessibility/IceCreamSandwichMobileAccessibilityHelper.java b/src/android/com/phonegap/plugin/mobileaccessibility/IceCreamSandwichMobileAccessibilityHelper.java new file mode 100644 index 0000000..abbcec4 --- /dev/null +++ b/src/android/com/phonegap/plugin/mobileaccessibility/IceCreamSandwichMobileAccessibilityHelper.java @@ -0,0 +1,40 @@ +package com.phonegap.plugin.mobileaccessibility; + +import android.accessibilityservice.AccessibilityServiceInfo; +import android.annotation.TargetApi; +import android.os.Build; +import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener; + +@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) +public class IceCreamSandwichMobileAccessibilityHelper extends + DonutMobileAccessibilityHelper { + protected AccessibilityStateChangeListener mAccessibilityStateChangeListener; + + @Override + public boolean isScreenReaderRunning() { + return mAccessibilityManager.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_SPOKEN).size() > 0; + } + + @Override + public void addStateChangeListeners() { + if (mAccessibilityStateChangeListener == null) { + mAccessibilityStateChangeListener = new InternalAccessibilityStateChangeListener(); + } + mAccessibilityManager.addAccessibilityStateChangeListener(mAccessibilityStateChangeListener); + } + + @Override + public void removeStateChangeListeners() { + mAccessibilityManager.removeAccessibilityStateChangeListener(mAccessibilityStateChangeListener); + mAccessibilityStateChangeListener = null; + } + + protected class InternalAccessibilityStateChangeListener + implements AccessibilityStateChangeListener { + + @Override + public void onAccessibilityStateChanged(boolean enabled) { + mMobileAccessibility.onAccessibilityStateChanged(enabled); + } + } +} diff --git a/src/android/com/phonegap/plugin/mobileaccessibility/JellyBeanMobileAccessibilityHelper.java b/src/android/com/phonegap/plugin/mobileaccessibility/JellyBeanMobileAccessibilityHelper.java new file mode 100644 index 0000000..5d6d34d --- /dev/null +++ b/src/android/com/phonegap/plugin/mobileaccessibility/JellyBeanMobileAccessibilityHelper.java @@ -0,0 +1,22 @@ +package com.phonegap.plugin.mobileaccessibility; + +import android.annotation.TargetApi; +import android.os.Build; +import android.view.accessibility.AccessibilityEvent; + +@TargetApi(Build.VERSION_CODES.JELLY_BEAN) +public class JellyBeanMobileAccessibilityHelper extends + IceCreamSandwichMobileAccessibilityHelper { + + @Override + public void announceForAccessibility(CharSequence text) { + if (mAccessibilityManager.isEnabled() && mParent != null) { + AccessibilityEvent event = AccessibilityEvent.obtain( + AccessibilityEvent.TYPE_ANNOUNCEMENT); + mWebView.onInitializeAccessibilityEvent(event); + event.getText().add(text); + event.setContentDescription(null); + mParent.requestSendAccessibilityEvent(mWebView, event); + } + } +} diff --git a/src/android/com/phonegap/plugin/mobileaccessibility/KitKatMobileAccessibilityHelper.java b/src/android/com/phonegap/plugin/mobileaccessibility/KitKatMobileAccessibilityHelper.java new file mode 100644 index 0000000..f1f9792 --- /dev/null +++ b/src/android/com/phonegap/plugin/mobileaccessibility/KitKatMobileAccessibilityHelper.java @@ -0,0 +1,79 @@ +package com.phonegap.plugin.mobileaccessibility; + +import android.accessibilityservice.AccessibilityServiceInfo; +import android.annotation.TargetApi; +import android.content.Context; +import android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener; +import android.view.accessibility.CaptioningManager; +import android.view.accessibility.CaptioningManager.CaptioningChangeListener; + +@TargetApi(19) +public class KitKatMobileAccessibilityHelper extends + JellyBeanMobileAccessibilityHelper { + protected CaptioningManager mCaptioningManager; + protected CaptioningChangeListener mCaptioningChangeListener; + protected TouchExplorationStateChangeListener mTouchExplorationStateChangeListener; + + @Override + public void initialize(MobileAccessibility mobileAccessibility) { + super.initialize(mobileAccessibility); + mCaptioningManager = (CaptioningManager) mobileAccessibility.cordova.getActivity().getSystemService(Context.CAPTIONING_SERVICE); + } + + @Override + public boolean isScreenReaderRunning() { + return mAccessibilityManager.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_BRAILLE | AccessibilityServiceInfo.FEEDBACK_SPOKEN).size() > 0; + } + + @Override + public boolean isClosedCaptioningEnabled() { + return mCaptioningManager.isEnabled(); + } + + @Override + public boolean isTouchExplorationEnabled() { + return mAccessibilityManager.isTouchExplorationEnabled(); + } + + @Override + public void addStateChangeListeners() { + super.addStateChangeListeners(); + if (mCaptioningChangeListener == null) { + mCaptioningChangeListener = new CaptioningChangeListener() { + /** @hide */ + @Override + public void onEnabledChanged(boolean enabled) { + onCaptioningEnabledChanged(enabled); + } + }; + } + mCaptioningManager.addCaptioningChangeListener(mCaptioningChangeListener); + + if (mTouchExplorationStateChangeListener == null) { + mTouchExplorationStateChangeListener = new InternalTouchExplorationStateChangeListener(); + } + mAccessibilityManager.addTouchExplorationStateChangeListener(mTouchExplorationStateChangeListener); + } + + @Override + public void removeStateChangeListeners() { + super.removeStateChangeListeners(); + if (mCaptioningChangeListener != null) { + mCaptioningManager.removeCaptioningChangeListener(mCaptioningChangeListener); + mCaptioningChangeListener = null; + } + if (mTouchExplorationStateChangeListener != null) { + mAccessibilityManager.removeTouchExplorationStateChangeListener(mTouchExplorationStateChangeListener); + mTouchExplorationStateChangeListener = null; + } + } + + protected class InternalTouchExplorationStateChangeListener + implements TouchExplorationStateChangeListener { + + @Override + public void onTouchExplorationStateChanged(boolean enabled) { + mMobileAccessibility.onTouchExplorationStateChanged(enabled); + } + } +} diff --git a/src/android/com/phonegap/plugin/mobileaccessibility/MobileAccessibility.java b/src/android/com/phonegap/plugin/mobileaccessibility/MobileAccessibility.java new file mode 100644 index 0000000..4ac0ce4 --- /dev/null +++ b/src/android/com/phonegap/plugin/mobileaccessibility/MobileAccessibility.java @@ -0,0 +1,240 @@ +package com.phonegap.plugin.mobileaccessibility; + +import org.apache.cordova.CallbackContext; +import org.apache.cordova.CordovaInterface; +import org.apache.cordova.CordovaPlugin; +import org.apache.cordova.CordovaWebView; +import org.apache.cordova.PluginResult; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import android.os.Build; +import android.util.Log; + +/** + * This class provides information on the status of native accessibility services to JavaScript. + */ +public class MobileAccessibility extends CordovaPlugin { + protected AbstractMobileAccessibilityHelper mMobileAccessibilityHelper; + protected CallbackContext mCallbackContext = null; + protected boolean mIsScreenReaderRunning = false; + protected boolean mClosedCaptioningEnabled = false; + protected boolean mTouchExplorationEnabled = false; + protected boolean mCachedIsScreenReaderRunning = false; + + @Override + public void initialize(CordovaInterface cordova, CordovaWebView webView) { + super.initialize(cordova, webView); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + mMobileAccessibilityHelper = new KitKatMobileAccessibilityHelper(); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + mMobileAccessibilityHelper = new JellyBeanMobileAccessibilityHelper(); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + mMobileAccessibilityHelper = new IceCreamSandwichMobileAccessibilityHelper(); + } else { + mMobileAccessibilityHelper = new DonutMobileAccessibilityHelper(); + } + mMobileAccessibilityHelper.initialize(this); + } + + @Override + public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { + try { + if (action.equals("isScreenReaderRunning")) { + isScreenReaderRunning(callbackContext); + return true; + } else if (action.equals("isClosedCaptioningEnabled")) { + isClosedCaptioningEnabled(callbackContext); + return true; + } else if (action.equals("isTouchExplorationEnabled")) { + isTouchExplorationEnabled(callbackContext); + return true; + } else if (action.equals("postNotification")) { + if (args.length() > 1) { + String string = args.getString(1); + if (!string.isEmpty()) { + announceForAccessibility(string, callbackContext); + } + } + return true; + } else if (action.equals("start")) { + start(callbackContext); + return true; + } else if (action.equals("stop")) { + stop(); + return true; + } + } catch (JSONException e) { + e.printStackTrace(); + callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION)); + } + return false; + } + + /** + * Called when the system is about to start resuming a previous activity. + * + * @param multitasking Flag indicating if multitasking is turned on for app + */ + @Override + public void onPause(boolean multitasking) { + Log.i("MobileAccessibility", "onPause"); + mCachedIsScreenReaderRunning = mIsScreenReaderRunning; + } + + /** + * Called when the activity will start interacting with the user. + * + * @param multitasking Flag indicating if multitasking is turned on for app + */ + @Override + public void onResume(boolean multitasking) { + Log.i("MobileAccessibility", "onResume"); + if (mIsScreenReaderRunning && !mCachedIsScreenReaderRunning) { + Log.i("MobileAccessibility", "Reloading page on reload because the Accessibility State has changed."); + mCachedIsScreenReaderRunning = mIsScreenReaderRunning; + stop(); + cordova.getActivity().runOnUiThread(new Runnable() { + public void run() { + webView.reload(); + } + }); + + } + + } + + /** + * The final call you receive before your activity is destroyed. + */ + public void onDestroy() { + stop(); + } + + protected boolean isScreenReaderRunning(final CallbackContext callbackContext) { + mIsScreenReaderRunning = mMobileAccessibilityHelper.isScreenReaderRunning(); + cordova.getThreadPool().execute(new Runnable() { + public void run() { + callbackContext.success(mIsScreenReaderRunning ? 1 : 0); + } + }); + return mIsScreenReaderRunning; + } + + protected boolean isScreenReaderRunning() { + mIsScreenReaderRunning = mMobileAccessibilityHelper.isScreenReaderRunning(); + return mIsScreenReaderRunning; + } + + protected boolean isClosedCaptioningEnabled(final CallbackContext callbackContext) { + mClosedCaptioningEnabled = mMobileAccessibilityHelper.isClosedCaptioningEnabled(); + cordova.getThreadPool().execute(new Runnable() { + public void run() { + callbackContext.success(mClosedCaptioningEnabled ? 1 : 0); + } + }); + return mClosedCaptioningEnabled; + } + + protected boolean isClosedCaptioningEnabled() { + mClosedCaptioningEnabled = mMobileAccessibilityHelper.isClosedCaptioningEnabled(); + return mClosedCaptioningEnabled; + } + + protected boolean isTouchExplorationEnabled(final CallbackContext callbackContext) { + mTouchExplorationEnabled= mMobileAccessibilityHelper.isTouchExplorationEnabled(); + cordova.getThreadPool().execute(new Runnable() { + public void run() { + callbackContext.success(mTouchExplorationEnabled ? 1 : 0); + } + }); + return mTouchExplorationEnabled; + } + + protected boolean isTouchExplorationEnabled() { + mTouchExplorationEnabled = mMobileAccessibilityHelper.isTouchExplorationEnabled(); + return mTouchExplorationEnabled; + } + + protected void announceForAccessibility(CharSequence text, final CallbackContext callbackContext) { + mMobileAccessibilityHelper.announceForAccessibility(text); + if (callbackContext != null) { + JSONObject info = new JSONObject(); + try { + info.put("stringValue", text); + info.put("wasSuccessful", mIsScreenReaderRunning); + } catch (JSONException e) { + e.printStackTrace(); + } + callbackContext.success(info); + } + } + + public void onAccessibilityStateChanged(boolean enabled) { + mIsScreenReaderRunning = enabled; + cordova.getActivity().runOnUiThread(new Runnable() { + public void run() { + sendMobileAccessibilityStatusChangedCallback(); + } + }); + } + + public void onCaptioningEnabledChanged(boolean enabled) { + mClosedCaptioningEnabled = enabled; + cordova.getActivity().runOnUiThread(new Runnable() { + public void run() { + sendMobileAccessibilityStatusChangedCallback(); + } + }); + } + + public void onTouchExplorationStateChanged(boolean enabled) { + mTouchExplorationEnabled = enabled; + cordova.getActivity().runOnUiThread(new Runnable() { + public void run() { + sendMobileAccessibilityStatusChangedCallback(); + } + }); + } + + protected void sendMobileAccessibilityStatusChangedCallback() { + if (this.mCallbackContext != null) { + PluginResult result = new PluginResult(PluginResult.Status.OK, getMobileAccessibilityStatus()); + result.setKeepCallback(true); + this.mCallbackContext.sendPluginResult(result); + } + } + + /* Get the current mobile accessibility status. */ + protected JSONObject getMobileAccessibilityStatus() { + JSONObject status = new JSONObject(); + try { + status.put("isScreenReaderRunning", mIsScreenReaderRunning); + status.put("isClosedCaptioningEnabled", mClosedCaptioningEnabled); + status.put("isTouchExplorationEnabled", mTouchExplorationEnabled); + Log.i("MobileAccessibility", "MobileAccessibility.isScreenReaderRunning == " + status.getString("isScreenReaderRunning") + + "\nMobileAccessibility.isClosedCaptioningEnabled == " + status.getString("isClosedCaptioningEnabled") + + "\nMobileAccessibility.isTouchExplorationEnabled == " + status.getString("isTouchExplorationEnabled") ); + } catch (JSONException e) { + e.printStackTrace(); + } + return status; + } + + protected void start(CallbackContext callbackContext) { + Log.i("MobileAccessibility", "MobileAccessibility.start"); + mCallbackContext = callbackContext; + mMobileAccessibilityHelper.addStateChangeListeners(); + sendMobileAccessibilityStatusChangedCallback(); + } + + protected void stop() { + Log.i("MobileAccessibility", "MobileAccessibility.stop"); + if (mCallbackContext != null) { + sendMobileAccessibilityStatusChangedCallback(); + mMobileAccessibilityHelper.removeStateChangeListeners(); + mCallbackContext = null; + } + } +} \ No newline at end of file diff --git a/www/mobile-accessibility.js b/www/mobile-accessibility.js index 4d175f2..e2ccf51 100644 --- a/www/mobile-accessibility.js +++ b/www/mobile-accessibility.js @@ -21,7 +21,8 @@ var argscheck = require('cordova/argscheck'), utils = require('cordova/utils'), - exec = require('cordova/exec'); + exec = require('cordova/exec'), + device = require('org.apache.cordova.device.device'); var MobileAccessibility = function() { this._isScreenReaderRunning = false; @@ -29,13 +30,15 @@ var MobileAccessibility = function() { this._isGuidedAccessEnabled = false; this._isInvertColorsEnabled = false; this._isMonoAudioEnabled = false; + this._isTouchExplorationEnabled = false; // Create new event handlers on the window (returns a channel instance) this.channels = { screenreaderstatuschanged:cordova.addWindowEventHandler("screenreaderstatuschanged"), closedcaptioningstatusdidchange:cordova.addWindowEventHandler("closedcaptioningstatusdidchange"), guidedaccessstatusdidchange:cordova.addWindowEventHandler("guidedaccessstatusdidchange"), invertcolorsstatusdidchange:cordova.addWindowEventHandler("invertcolorsstatusdidchange"), - monoaudiostatusdidchange:cordova.addWindowEventHandler("monoaudiostatusdidchange") + monoaudiostatusdidchange:cordova.addWindowEventHandler("monoaudiostatusdidchange"), + touchexplorationstatechanged:cordova.addWindowEventHandler("touchexplorationstatechanged") }; for (var key in this.channels) { this.channels[key].onHasSubscribersChange = MobileAccessibility.onHasSubscribersChange; @@ -51,7 +54,8 @@ function handlers() { mobileAccessibility.channels.closedcaptioningstatusdidchange.numHandlers + mobileAccessibility.channels.invertcolorsstatusdidchange.numHandlers + mobileAccessibility.channels.monoaudiostatusdidchange.numHandlers + - mobileAccessibility.channels.guidedaccessstatusdidchange.numHandlers; + mobileAccessibility.channels.guidedaccessstatusdidchange.numHandlers + + mobileAccessibility.channels.touchexplorationstatechanged.numHandlers; }; /** @@ -76,9 +80,25 @@ MobileAccessibility.onHasSubscribersChange = function() { * @param {function} callback A callback method to receive the asynchronous result from the native MobileAccessibility. */ MobileAccessibility.prototype.isScreenReaderRunning = function(callback) { - exec(callback, null, "MobileAccessibility", "isScreenReaderRunning", []); + exec(function(bool) { + if (device.platform==="Android") { + if (typeof cvox === "undefined") { + if (bool) { + console.warn('A screen reader is running but ChromeVox has failed to initialize.'); + } + } else { + // activate or deactivate ChromeVox based on whether or not or not the screen reader is running. + cvox.ChromeVox.host.activateOrDeactivateChromeVox(bool); + } + } + callback(Boolean(bool)); + }, null, "MobileAccessibility", "isScreenReaderRunning", []); }; MobileAccessibility.prototype.isVoiceOverRunning = MobileAccessibility.prototype.isScreenReaderRunning; +MobileAccessibility.prototype.isTalkBackRunning = MobileAccessibility.prototype.isScreenReaderRunning; +MobileAccessibility.prototype.isChromeVoxActive = function () { + return typeof cvox !== "undefined" && cvox.ChromeVox.host.ttsLoaded() && cvox.Api.isChromeVoxActive(); +}; /** * Asynchronous call to native MobileAccessibility to determine if closed captioning is enabled. @@ -112,6 +132,14 @@ MobileAccessibility.prototype.isGuidedAccessEnabled = function(callback) { exec(callback, null, "MobileAccessibility", "isGuidedAccessEnabled", []); }; +/** + * Asynchronous call to native MobileAccessibility to determine if Touch Exploration is enabled on Android. + * @param {function} callback A callback method to receive the asynchronous result from the native MobileAccessibility. + */ +MobileAccessibility.prototype.isTouchExplorationEnabled = function(callback) { + exec(callback, null, "MobileAccessibility", "isTouchExplorationEnabled", []); +}; + MobileAccessibility.prototype.MobileAccessibilityNotifications = { SCREEN_CHANGED : 1000, LAYOUT_CHANGED : 1001, @@ -129,20 +157,56 @@ MobileAccessibility.prototype.postNotification = function(mobileAccessibilityNot exec(callback, null, "MobileAccessibility", "postNotification", [mobileAccessibilityNotification, string]); }; +/** + * Speaks the given string, and if ChromeVox is active, it will use the specified queueMode and properties. + * @param {string} string A string to be announced by a screen reader. + * @param {number} [queueMode] Optional number. Valid modes are 0 for flush; 1 for queue. + * @param {Object} [properties] Speech properties to use for this utterance. + */ +MobileAccessibility.prototype.speak = function(string, queueMode, properties) { + if (this.isChromeVoxActive()) { + cvox.ChromeVox.tts.speak(string, queueMode, properties); + } else { + exec(null, null, "MobileAccessibility", "postNotification", [mobileAccessibility.MobileAccessibilityNotifications.ANNOUNCEMENT, string]); + } +} + +/** + * Stops speech. + */ +MobileAccessibility.prototype.stop = function() { + if (this.isChromeVoxActive()) { + cvox.ChromeVox.tts.stop(); + } else { + exec(null, null, "MobileAccessibility", "postNotification", [mobileAccessibility.MobileAccessibilityNotifications.ANNOUNCEMENT]); + } +} + /** * Callback from native MobileAccessibility returning an which describes the status of MobileAccessibility features. * * @param {Object} info * @config {Boolean} [isScreenReaderRunning] Boolean to indicate screen reader 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. + * @config {Boolean} [isGuidedAccessEnabled] Boolean to indicate guided access status (ios). + * @config {Boolean} [isInvertColorsEnabled] Boolean to indicate invert colors status (ios). + * @config {Boolean} [isMonoAudioEnabled] Boolean to indicate mono audio status (ios). + * @config {Boolean} [isTouchExplorationEnabled] Boolean to indicate touch exploration status (android). */ MobileAccessibility.prototype._status = function(info) { if (info) { - if (mobileAccessibility._isScreenReaderRunning !== info.isScreenReaderRunning) { - cordova.fireWindowEvent("screenreaderstatuschanged", info); + if (device.platform === "Android") { + if (typeof cvox === "undefined") { + if (info.isScreenReaderRunning) { + console.warn('A screen reader is running but ChromeVox has failed to initialize.'); + } + } else { + // activate or deactivate ChromeVox based on whether or not or not the screen reader is running. + cvox.ChromeVox.host.activateOrDeactivateChromeVox(info.isScreenReaderRunning); + } + } + if (mobileAccessibility._isScreenReaderRunning !== info.isScreenReaderRunning) { + cordova.fireWindowEvent("screenreaderstatuschanged", info); mobileAccessibility._isScreenReaderRunning = info.isScreenReaderRunning; } if (mobileAccessibility._isClosedCaptioningEnabled !== info.isClosedCaptioningEnabled) { @@ -161,6 +225,10 @@ MobileAccessibility.prototype._status = function(info) { cordova.fireWindowEvent("monoaudiostatusdidchange", info); mobileAccessibility._isMonoAudioEnabled = info.isMonoAudioEnabled; } + if (mobileAccessibility._isTouchExplorationEnabled !== info.isTouchExplorationEnabled) { + cordova.fireWindowEvent("touchexplorationstatechanged", info); + mobileAccessibility._isTouchExplorationEnabled = info.isTouchExplorationEnabled; + } } };