diff --git a/packages/jest-preset/jest/mocks/AccessibilityInfo.js b/packages/jest-preset/jest/mocks/AccessibilityInfo.js index 4dc215bcabd7..ea75dab51f24 100644 --- a/packages/jest-preset/jest/mocks/AccessibilityInfo.js +++ b/packages/jest-preset/jest/mocks/AccessibilityInfo.js @@ -57,6 +57,12 @@ const AccessibilityInfo = { getRecommendedTimeoutMillis: jest.fn(() => Promise.resolve(false), ) as JestMockFn<$FlowFixMe, $FlowFixMe>, + getInstalledAccessibilityServices: jest.fn(() => + Promise.resolve([]), + ) as JestMockFn<$FlowFixMe, $FlowFixMe>, + getEnabledAccessibilityServices: jest.fn(() => + Promise.resolve([]), + ) as JestMockFn<$FlowFixMe, $FlowFixMe>, }; export default AccessibilityInfo; diff --git a/packages/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.d.ts b/packages/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.d.ts index bad75fca1346..3941716e0fac 100644 --- a/packages/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.d.ts +++ b/packages/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.d.ts @@ -40,6 +40,11 @@ type AccessibilityAnnouncementFinishedEventHandler = ( type AccessibilityEventTypes = 'click' | 'focus' | 'viewHoverEnter'; +type AccessibilityServiceInfo = { + id: string; + name: string; +}; + /** * @see https://reactnative.dev/docs/accessibilityinfo */ @@ -171,6 +176,31 @@ export interface AccessibilityInfoStatic { * @platform android */ getRecommendedTimeoutMillis: (originalTimeout: number) => Promise; + + /** + * Get a list of installed accessibility services. + * + * Returns a promise which resolves to an array of accessibility service objects. + * Each object contains: + * - `id`: The unique identifier of the accessibility service + * - `name`: The human-readable name of the accessibility service + * + * @platform android + */ + getInstalledAccessibilityServices: () => Promise>; + + /** + * Get a list of enabled accessibility services. + * + * Returns a promise which resolves to an array of accessibility service objects. + * Each object contains: + * - `id`: The unique identifier of the accessibility service + * - `name`: The human-readable name of the accessibility service + * + * @platform android + */ + getEnabledAccessibilityServices: () => Promise>; + sendAccessibilityEvent: ( handle: HostInstance, eventType: AccessibilityEventTypes, @@ -179,3 +209,4 @@ export interface AccessibilityInfoStatic { export const AccessibilityInfo: AccessibilityInfoStatic; export type AccessibilityInfo = AccessibilityInfoStatic; +export type {AccessibilityServiceInfo}; diff --git a/packages/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js b/packages/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js index b74d517151b0..37770af068d1 100644 --- a/packages/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js +++ b/packages/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js @@ -525,6 +525,62 @@ const AccessibilityInfo = { return Promise.resolve(originalTimeout); } }, + + /** + * Get a list of installed accessibility services. Android only. + * + * Returns a promise which resolves to an array of accessibility service objects. + * Each object contains: + * - `id`: The unique identifier of the accessibility service + * - `name`: The human-readable name of the accessibility service + * + * See https://reactnative.dev/docs/accessibilityinfo#getinstalledaccessibilityservices + */ + getInstalledAccessibilityServices(): Promise> { + return new Promise((resolve, reject) => { + if (Platform.OS === 'android') { + if (NativeAccessibilityInfoAndroid?.getInstalledAccessibilityServices != null) { + NativeAccessibilityInfoAndroid.getInstalledAccessibilityServices(resolve); + } else { + reject( + new Error( + 'NativeAccessibilityInfoAndroid.getInstalledAccessibilityServices is not available', + ), + ); + } + } else { + resolve([]); + } + }); + }, + + /** + * Get a list of enabled accessibility services. Android only. + * + * Returns a promise which resolves to an array of accessibility service objects. + * Each object contains: + * - `id`: The unique identifier of the accessibility service + * - `name`: The human-readable name of the accessibility service + * + * See https://reactnative.dev/docs/accessibilityinfo#getenabledaccessibilityservices + */ + getEnabledAccessibilityServices(): Promise> { + return new Promise((resolve, reject) => { + if (Platform.OS === 'android') { + if (NativeAccessibilityInfoAndroid?.getEnabledAccessibilityServices != null) { + NativeAccessibilityInfoAndroid.getEnabledAccessibilityServices(resolve); + } else { + reject( + new Error( + 'NativeAccessibilityInfoAndroid.getEnabledAccessibilityServices is not available', + ), + ); + } + } else { + resolve([]); + } + }); + }, }; export default AccessibilityInfo; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/accessibilityinfo/AccessibilityInfoModule.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/accessibilityinfo/AccessibilityInfoModule.kt index c3c5cff31518..c3c2e01b2c1d 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/accessibilityinfo/AccessibilityInfoModule.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/accessibilityinfo/AccessibilityInfoModule.kt @@ -15,6 +15,10 @@ import android.os.Build import android.provider.Settings import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityManager +import android.accessibilityservice.AccessibilityServiceInfo +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.WritableArray +import com.facebook.react.bridge.WritableMap import com.facebook.fbreact.specs.NativeAccessibilityInfoSpec import com.facebook.react.bridge.Callback import com.facebook.react.bridge.LifecycleEventListener @@ -308,6 +312,42 @@ internal class AccessibilityInfoModule(context: ReactApplicationContext) : successCallback.invoke(recommendedTimeout) } + override fun getInstalledAccessibilityServices(successCallback: Callback) { + val servicesList: WritableArray = Arguments.createArray() + if (accessibilityManager == null) { + successCallback.invoke(servicesList) + return + } + + val installedServices = accessibilityManager.getInstalledAccessibilityServiceList() + for (service in installedServices) { + val serviceMap: WritableMap = Arguments.createMap() + serviceMap.putString("id", service.id) + serviceMap.putString("name", service.resolveInfo.loadLabel(reactApplicationContext.packageManager).toString()) + servicesList.pushMap(serviceMap) + } + successCallback.invoke(servicesList) + } + + override fun getEnabledAccessibilityServices(successCallback: Callback) { + val servicesList: WritableArray = Arguments.createArray() + if (accessibilityManager == null) { + successCallback.invoke(servicesList) + return + } + + val enabledServices = accessibilityManager.getEnabledAccessibilityServiceList( + AccessibilityServiceInfo.FEEDBACK_ALL_MASK + ) + for (service in enabledServices) { + val serviceMap: WritableMap = Arguments.createMap() + serviceMap.putString("id", service.id) + serviceMap.putString("name", service.resolveInfo.loadLabel(reactApplicationContext.packageManager).toString()) + servicesList.pushMap(serviceMap) + } + successCallback.invoke(servicesList) + } + companion object { const val NAME: String = NativeAccessibilityInfoSpec.NAME private const val REDUCE_MOTION_EVENT_NAME = "reduceMotionDidChange" diff --git a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAccessibilityInfo.js b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAccessibilityInfo.js index 8f566494188d..c303bb394815 100644 --- a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAccessibilityInfo.js +++ b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAccessibilityInfo.js @@ -37,6 +37,12 @@ export interface Spec extends TurboModule { +isGrayscaleEnabled?: ( onSuccess: (isGrayscaleEnabled: boolean) => void, ) => void; + +getInstalledAccessibilityServices?: ( + onSuccess: (services: Array<{id: string, name: string}>) => void, + ) => void; + +getEnabledAccessibilityServices?: ( + onSuccess: (services: Array<{id: string, name: string}>) => void, + ) => void; } export default TurboModuleRegistry.get('AccessibilityInfo') as ?Spec;