Skip to content

Commit

Permalink
feat: add isAccessibilityServiceEnabled (#31396)
Browse files Browse the repository at this point in the history
Summary:
fix #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: #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
  • Loading branch information
grgr-dkrk authored and facebook-github-bot committed Oct 30, 2021
1 parent bbb52aa commit c8b83d4
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 1 deletion.
34 changes: 34 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,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:
*
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)
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,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"
Expand Down Expand Up @@ -969,7 +982,8 @@ class EnabledExample extends React.Component<
| 'invertColorsChanged'
| 'reduceTransparencyChanged'
| 'reduceMotionChanged'
| 'screenReaderChanged',
| 'screenReaderChanged'
| 'accessibilityServiceChanged',
test: string,
},
{
Expand All @@ -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;
}
Expand Down

0 comments on commit c8b83d4

Please sign in to comment.