feat: add isAccessibilityServiceEnabled (#31396)

Summary:
fix https://github.com/facebook/react-native/issues/30863

This PR adds `isAccessibilityServiceEnabled` to get if accessibility services are enabled on Android.

## Changelog

<!-- Help reviewers and the release process by writing your own changelog entry. For an example, see:
https://github.com/facebook/react-native/wiki/Changelog
-->

[Android] [Added] - Added `isAccessibilityServiceEnabled` to get if accessibility services are enabled

Pull Request resolved: https://github.com/facebook/react-native/pull/31396

Test Plan: ![accessibilityService](https://user-images.githubusercontent.com/40130327/115560972-11d5b100-a2f0-11eb-8aa2-7c52dc71ca59.gif)

Reviewed By: yungsters

Differential Revision: D31911880

Pulled By: lunaleaps

fbshipit-source-id: 9ae294999a6d46bf051ab658507bf97764a945d2
This commit is contained in:
grgr-dkrk
2021-10-29 18:40:59 -07:00
committed by Facebook GitHub Bot
parent bbb52aa276
commit c8b83d4e0b
5 changed files with 98 additions and 1 deletions
@@ -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}],
@@ -29,6 +34,7 @@ type AccessibilityEventDefinitionsIOS = {
};
type AccessibilityEventDefinitions = {
...AccessibilityEventDefinitionsAndroid,
...AccessibilityEventDefinitionsIOS,
change: [boolean], // screenReaderChanged
reduceMotionChanged: [boolean],
@@ -44,6 +50,7 @@ const EventNames: Map<$Keys<AccessibilityEventDefinitions>, string> =
['change', 'touchExplorationDidChange'],
['reduceMotionChanged', 'reduceMotionDidChange'],
['screenReaderChanged', 'touchExplorationDidChange'],
['accessibilityServiceChanged', 'accessibilityServiceDidChange'],
])
: new Map([
['announcementFinished', 'announcementFinished'],
@@ -224,6 +231,33 @@ 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.
*
* @platform android
*
* See https://reactnative.dev/docs/accessibilityinfo/#isaccessibilityserviceenabled-android
*/
isAccessibilityServiceEnabled(): Promise<boolean> {
return new Promise((resolve, reject) => {
if (Platform.OS === 'android') {
if (
NativeAccessibilityInfoAndroid != null &&
NativeAccessibilityInfoAndroid.isAccessibilityServiceEnabled != null
) {
NativeAccessibilityInfoAndroid.isAccessibilityServiceEnabled(resolve);
} else {
reject(null);
}
} else {
reject(null);
}
});
},
/**
* Add an event handler. Supported events:
*
@@ -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?: (
@@ -46,6 +46,19 @@ public class AccessibilityInfoModule extends NativeAccessibilityInfoSpec
}
}
// 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())) {
@@ -64,13 +77,16 @@ public class AccessibilityInfoModule extends NativeAccessibilityInfoSpec
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);
@@ -79,8 +95,10 @@ public class AccessibilityInfoModule extends NativeAccessibilityInfoSpec
(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
@@ -106,6 +124,11 @@ public class AccessibilityInfoModule extends NativeAccessibilityInfoSpec
successCallback.invoke(mTouchExplorationEnabled);
}
@Override
public void isAccessibilityServiceEnabled(Callback successCallback) {
successCallback.invoke(mAccessibilityServiceEnabled);
}
private void updateAndSendReduceMotionChangeEvent() {
boolean isReduceMotionEnabled = this.getIsReduceMotionEnabledValue();
@@ -134,16 +157,31 @@ public class AccessibilityInfoModule extends NativeAccessibilityInfoSpec
}
}
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)
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();
}
@@ -152,6 +190,8 @@ public class AccessibilityInfoModule extends NativeAccessibilityInfoSpec
public void onHostPause() {
mAccessibilityManager.removeTouchExplorationStateChangeListener(
mTouchExplorationStateChangeListener);
mAccessibilityManager.removeAccessibilityStateChangeListener(
mAccessibilityServiceChangeListener);
mContentResolver.unregisterContentObserver(animationScaleObserver);
}
@@ -160,6 +200,7 @@ public class AccessibilityInfoModule extends NativeAccessibilityInfoSpec
public void initialize() {
getReactApplicationContext().addLifecycleEventListener(this);
updateAndSendTouchExplorationChangeEvent(mAccessibilityManager.isTouchExplorationEnabled());
updateAndSendAccessibilityServiceChangeEvent(mAccessibilityManager.isEnabled());
updateAndSendReduceMotionChangeEvent();
}
+1
View File
@@ -123,6 +123,7 @@ jest
default: {
addEventListener: jest.fn(),
announceForAccessibility: jest.fn(),
isAccessibilityServiceEnabled: jest.fn(),
isBoldTextEnabled: jest.fn(),
isGrayscaleEnabled: jest.fn(),
isInvertColorsEnabled: jest.fn(),
@@ -942,6 +942,19 @@ class EnabledExamples extends React.Component<{}> {
</>
) : null}
{Platform.OS === 'android' ? (
<RNTesterBlock
title="isAccessibilityServiceEnabled()"
description={
'Event emitted whenever an accessibility service is enabled. This includes TalkBack as well as assistive technologies such as "Select to Speak".'
}>
<EnabledExample
test="any accessibility service"
eventListener="accessibilityServiceChanged"
/>
</RNTesterBlock>
) : null}
<RNTesterBlock title="isReduceMotionEnabled()">
<EnabledExample
test="reduce motion"
@@ -969,7 +982,8 @@ class EnabledExample extends React.Component<
| 'invertColorsChanged'
| 'reduceTransparencyChanged'
| 'reduceMotionChanged'
| 'screenReaderChanged',
| 'screenReaderChanged'
| 'accessibilityServiceChanged',
test: string,
},
{
@@ -991,6 +1005,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;
}