Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add isAccessibilityServiceEnabled #31396

Closed
29 changes: 29 additions & 0 deletions Libraries/Components/AccessibilityInfo/AccessibilityInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ import NativeAccessibilityManagerIOS from './NativeAccessibilityManager';
import legacySendAccessibilityEvent from './legacySendAccessibilityEvent';
import type {ElementRef} from 'react';

// Events that are only supported on Android.
type AccessibilityEventDefinitionsAndroid = {
accessibilityServiceChanged: [boolean],
};

// Events that are only supported on iOS.
type AccessibilityEventDefinitionsIOS = {
announcementFinished: [{announcement: string, success: boolean}],
Expand All @@ -29,6 +34,7 @@ type AccessibilityEventDefinitionsIOS = {
};

type AccessibilityEventDefinitions = {
...AccessibilityEventDefinitionsAndroid,
...AccessibilityEventDefinitionsIOS,
change: [boolean], // screenReaderChanged
reduceMotionChanged: [boolean],
Expand All @@ -44,6 +50,7 @@ const EventNames: Map<$Keys<AccessibilityEventDefinitions>, string> =
['change', 'touchExplorationDidChange'],
['reduceMotionChanged', 'reduceMotionDidChange'],
['screenReaderChanged', 'touchExplorationDidChange'],
['accessibilityServiceChanged', 'accessibilityServiceDidChange'],
])
: new Map([
['announcementFinished', 'announcementFinished'],
Expand Down Expand Up @@ -224,6 +231,28 @@ const AccessibilityInfo = {
});
},

/**
* Query whether Accessibility Service is currently enabled.
*
* Returns a promise which resolves to a boolean.
* The result is `true` when any service is enabled and `false` otherwise.
*
* See https://reactnative.dev/docs/accessibilityinfo.html#isAccessibilityServiceEnabled
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Point by @philIip that this comment should also mention that it's an Android only feature

*/
isAccessibilityServiceEnabled(): Promise<boolean> {
return new Promise((resolve, reject) => {
if (Platform.OS === 'android') {
if (NativeAccessibilityInfoAndroid != null) {
NativeAccessibilityInfoAndroid.isAccessibilityServiceEnabled(resolve);
} else {
reject(null);
}
} else {
reject(null);
}
});
},

/**
* Add an event handler. Supported events:
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ export interface Spec extends TurboModule {
+isTouchExplorationEnabled: (
onSuccess: (isScreenReaderEnabled: boolean) => void,
) => void;
+isAccessibilityServiceEnabled: (
onSuccess: (isAccessibilityServiceEnabled: boolean) => void,
) => void;
+setAccessibilityFocus: (reactTag: number) => void;
+announceForAccessibility: (announcement: string) => void;
+getRecommendedTimeoutMillis?: (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,19 @@ public void onTouchExplorationStateChanged(boolean enabled) {
}
}

// Android can listen for accessibility service enable with `accessibilityStateChange`, but
// `accessibilityState` conflicts with React Native props and confuses developers. Therefore, the
// name `accessibilityServiceChange` is used here instead.
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private class ReactAccessibilityServiceChangeListener
implements AccessibilityManager.AccessibilityStateChangeListener {

@Override
public void onAccessibilityStateChanged(boolean enabled) {
updateAndSendAccessibilityServiceChangeEvent(enabled);
}
}

// Listener that is notified when the global TRANSITION_ANIMATION_SCALE.
private final ContentObserver animationScaleObserver =
new ContentObserver(new Handler(Looper.getMainLooper())) {
Expand All @@ -64,13 +77,16 @@ public void onChange(boolean selfChange, Uri uri) {

private @Nullable AccessibilityManager mAccessibilityManager;
private @Nullable ReactTouchExplorationStateChangeListener mTouchExplorationStateChangeListener;
private @Nullable ReactAccessibilityServiceChangeListener mAccessibilityServiceChangeListener;
private final ContentResolver mContentResolver;
private boolean mReduceMotionEnabled = false;
private boolean mTouchExplorationEnabled = false;
private boolean mAccessibilityServiceEnabled = false;
private int mRecommendedTimeout;

private static final String REDUCE_MOTION_EVENT_NAME = "reduceMotionDidChange";
private static final String TOUCH_EXPLORATION_EVENT_NAME = "touchExplorationDidChange";
private static final String ACCESSIBILITY_SERVICE_EVENT_NAME = "accessibilityServiceDidChange";

public AccessibilityInfoModule(ReactApplicationContext context) {
super(context);
Expand All @@ -79,8 +95,10 @@ public AccessibilityInfoModule(ReactApplicationContext context) {
(AccessibilityManager) appContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
mContentResolver = getReactApplicationContext().getContentResolver();
mTouchExplorationEnabled = mAccessibilityManager.isTouchExplorationEnabled();
mAccessibilityServiceEnabled = mAccessibilityManager.isEnabled();
mReduceMotionEnabled = this.getIsReduceMotionEnabledValue();
mTouchExplorationStateChangeListener = new ReactTouchExplorationStateChangeListener();
mAccessibilityServiceChangeListener = new ReactAccessibilityServiceChangeListener();
}

@Override
Expand All @@ -106,6 +124,11 @@ public void isTouchExplorationEnabled(Callback successCallback) {
successCallback.invoke(mTouchExplorationEnabled);
}

@Override
public void isAccessibilityServiceEnabled(Callback successCallback) {
successCallback.invoke(mAccessibilityServiceEnabled);
}

private void updateAndSendReduceMotionChangeEvent() {
boolean isReduceMotionEnabled = this.getIsReduceMotionEnabledValue();

Expand Down Expand Up @@ -134,16 +157,31 @@ private void updateAndSendTouchExplorationChangeEvent(boolean enabled) {
}
}

private void updateAndSendAccessibilityServiceChangeEvent(boolean enabled) {
if (mAccessibilityServiceEnabled != enabled) {
mAccessibilityServiceEnabled = enabled;

ReactApplicationContext reactApplicationContext = getReactApplicationContextIfActiveOrWarn();
if (reactApplicationContext != null) {
getReactApplicationContext()
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(ACCESSIBILITY_SERVICE_EVENT_NAME, mAccessibilityServiceEnabled);
}
}
}

@Override
@TargetApi(Build.VERSION_CODES.LOLLIPOP)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

google-java-format suggested changes:

@@ -176,2 +178 @@
-    mAccessibilityManager.addAccessibilityStateChangeListener(
-        mAccessibilityServiceChangeListener);
+    mAccessibilityManager.addAccessibilityStateChangeListener(mAccessibilityServiceChangeListener);

public void onHostResume() {
mAccessibilityManager.addTouchExplorationStateChangeListener(
mTouchExplorationStateChangeListener);
mAccessibilityManager.addAccessibilityStateChangeListener(mAccessibilityServiceChangeListener);

Uri transitionUri = Settings.Global.getUriFor(Settings.Global.TRANSITION_ANIMATION_SCALE);
mContentResolver.registerContentObserver(transitionUri, false, animationScaleObserver);

updateAndSendTouchExplorationChangeEvent(mAccessibilityManager.isTouchExplorationEnabled());
updateAndSendAccessibilityServiceChangeEvent(mAccessibilityManager.isEnabled());
updateAndSendReduceMotionChangeEvent();
}

Expand All @@ -152,6 +190,8 @@ public void onHostResume() {
public void onHostPause() {
mAccessibilityManager.removeTouchExplorationStateChangeListener(
mTouchExplorationStateChangeListener);
mAccessibilityManager.removeAccessibilityStateChangeListener(
mAccessibilityServiceChangeListener);

mContentResolver.unregisterContentObserver(animationScaleObserver);
}
Expand All @@ -160,6 +200,7 @@ public void onHostPause() {
public void initialize() {
getReactApplicationContext().addLifecycleEventListener(this);
updateAndSendTouchExplorationChangeEvent(mAccessibilityManager.isTouchExplorationEnabled());
updateAndSendAccessibilityServiceChangeEvent(mAccessibilityManager.isEnabled());
updateAndSendReduceMotionChangeEvent();
}

Expand Down
1 change: 1 addition & 0 deletions jest/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ jest
default: {
addEventListener: jest.fn(),
announceForAccessibility: jest.fn(),
isAccessibilityServiceEnabled: jest.fn(),
isBoldTextEnabled: jest.fn(),
isGrayscaleEnabled: jest.fn(),
isInvertColorsEnabled: jest.fn(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -942,6 +942,15 @@ class EnabledExamples extends React.Component<{}> {
</>
) : null}

{Platform.OS === 'android' ? (
<RNTesterBlock title="isAccessibilityServiceEnabled()">
<EnabledExample
test="any accessibility service"
eventListener="accessibilityServiceChanged"
/>
</RNTesterBlock>
) : null}

<RNTesterBlock title="isReduceMotionEnabled()">
<EnabledExample
test="reduce motion"
Expand Down Expand Up @@ -969,7 +978,8 @@ class EnabledExample extends React.Component<
| 'invertColorsChanged'
| 'reduceTransparencyChanged'
| 'reduceMotionChanged'
| 'screenReaderChanged',
| 'screenReaderChanged'
| 'accessibilityServiceChanged',
test: string,
},
{
Expand All @@ -991,6 +1001,10 @@ class EnabledExample extends React.Component<
return AccessibilityInfo.isReduceMotionEnabled().then(state => {
this.setState({isEnabled: state});
});
case 'accessibilityServiceChanged':
return AccessibilityInfo.isAccessibilityServiceEnabled().then(state => {
this.setState({isEnabled: state});
});
default:
return null;
}
Expand Down