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/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/BorderColors.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/BorderColors.kt index d42df53d3884..605c1e087bb8 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/BorderColors.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/BorderColors.kt @@ -59,27 +59,35 @@ internal value class BorderColors( * @throws IllegalArgumentException if layoutDirection is not LTR or RTL */ fun resolve(layoutDirection: Int, context: Context): ColorEdges { + // Fix for issue #38335: borderBottomColor does not override borderColor on Android + // Physical edge colors (LEFT, RIGHT, TOP, BOTTOM) should have higher priority than + // logical block colors (BLOCK_START, BLOCK_END, BLOCK) and shorthand colors (ALL, HORIZONTAL, VERTICAL) + // This ensures that setting borderBottomColor properly overrides borderBlockColor return when (layoutDirection) { LayoutDirection.LTR -> ColorEdges( + // Left: START > LEFT > HORIZONTAL > ALL edgeColors[LogicalEdge.START.ordinal] ?: edgeColors[LogicalEdge.LEFT.ordinal] ?: edgeColors[LogicalEdge.HORIZONTAL.ordinal] ?: edgeColors[LogicalEdge.ALL.ordinal] ?: Color.BLACK, - edgeColors[LogicalEdge.BLOCK_START.ordinal] - ?: edgeColors[LogicalEdge.TOP.ordinal] + // Top: Physical edge (TOP) > BLOCK_START > BLOCK > VERTICAL > ALL + edgeColors[LogicalEdge.TOP.ordinal] + ?: edgeColors[LogicalEdge.BLOCK_START.ordinal] ?: edgeColors[LogicalEdge.BLOCK.ordinal] ?: edgeColors[LogicalEdge.VERTICAL.ordinal] ?: edgeColors[LogicalEdge.ALL.ordinal] ?: Color.BLACK, + // Right: END > RIGHT > HORIZONTAL > ALL edgeColors[LogicalEdge.END.ordinal] ?: edgeColors[LogicalEdge.RIGHT.ordinal] ?: edgeColors[LogicalEdge.HORIZONTAL.ordinal] ?: edgeColors[LogicalEdge.ALL.ordinal] ?: Color.BLACK, - edgeColors[LogicalEdge.BLOCK_END.ordinal] - ?: edgeColors[LogicalEdge.BOTTOM.ordinal] + // Bottom: Physical edge (BOTTOM) > BLOCK_END > BLOCK > VERTICAL > ALL + edgeColors[LogicalEdge.BOTTOM.ordinal] + ?: edgeColors[LogicalEdge.BLOCK_END.ordinal] ?: edgeColors[LogicalEdge.BLOCK.ordinal] ?: edgeColors[LogicalEdge.VERTICAL.ordinal] ?: edgeColors[LogicalEdge.ALL.ordinal] @@ -87,50 +95,60 @@ internal value class BorderColors( ) LayoutDirection.RTL -> if (I18nUtil.instance.doLeftAndRightSwapInRTL(context)) { + // RTL with doLeftAndRightSwapInRTL: START/END swap with LEFT/RIGHT ColorEdges( + // Left (becomes RTL right): END > RIGHT > HORIZONTAL > ALL edgeColors[LogicalEdge.END.ordinal] ?: edgeColors[LogicalEdge.RIGHT.ordinal] ?: edgeColors[LogicalEdge.HORIZONTAL.ordinal] ?: edgeColors[LogicalEdge.ALL.ordinal] ?: Color.BLACK, - edgeColors[LogicalEdge.BLOCK_START.ordinal] - ?: edgeColors[LogicalEdge.TOP.ordinal] + // Top: Physical edge (TOP) > BLOCK_START > BLOCK > VERTICAL > ALL + edgeColors[LogicalEdge.TOP.ordinal] + ?: edgeColors[LogicalEdge.BLOCK_START.ordinal] ?: edgeColors[LogicalEdge.BLOCK.ordinal] ?: edgeColors[LogicalEdge.VERTICAL.ordinal] ?: edgeColors[LogicalEdge.ALL.ordinal] ?: Color.BLACK, + // Right (becomes RTL left): START > LEFT > HORIZONTAL > ALL edgeColors[LogicalEdge.START.ordinal] ?: edgeColors[LogicalEdge.LEFT.ordinal] ?: edgeColors[LogicalEdge.HORIZONTAL.ordinal] ?: edgeColors[LogicalEdge.ALL.ordinal] ?: Color.BLACK, - edgeColors[LogicalEdge.BLOCK_END.ordinal] - ?: edgeColors[LogicalEdge.BOTTOM.ordinal] + // Bottom: Physical edge (BOTTOM) > BLOCK_END > BLOCK > VERTICAL > ALL + edgeColors[LogicalEdge.BOTTOM.ordinal] + ?: edgeColors[LogicalEdge.BLOCK_END.ordinal] ?: edgeColors[LogicalEdge.BLOCK.ordinal] ?: edgeColors[LogicalEdge.VERTICAL.ordinal] ?: edgeColors[LogicalEdge.ALL.ordinal] ?: Color.BLACK, ) } else { + // RTL without swap: START/END don't affect LEFT/RIGHT ColorEdges( + // Left: END > LEFT > HORIZONTAL > ALL edgeColors[LogicalEdge.END.ordinal] ?: edgeColors[LogicalEdge.LEFT.ordinal] ?: edgeColors[LogicalEdge.HORIZONTAL.ordinal] ?: edgeColors[LogicalEdge.ALL.ordinal] ?: Color.BLACK, - edgeColors[LogicalEdge.BLOCK_START.ordinal] - ?: edgeColors[LogicalEdge.TOP.ordinal] + // Top: Physical edge (TOP) > BLOCK_START > BLOCK > VERTICAL > ALL + edgeColors[LogicalEdge.TOP.ordinal] + ?: edgeColors[LogicalEdge.BLOCK_START.ordinal] ?: edgeColors[LogicalEdge.BLOCK.ordinal] ?: edgeColors[LogicalEdge.VERTICAL.ordinal] ?: edgeColors[LogicalEdge.ALL.ordinal] ?: Color.BLACK, + // Right: START > RIGHT > HORIZONTAL > ALL edgeColors[LogicalEdge.START.ordinal] ?: edgeColors[LogicalEdge.RIGHT.ordinal] ?: edgeColors[LogicalEdge.HORIZONTAL.ordinal] ?: edgeColors[LogicalEdge.ALL.ordinal] ?: Color.BLACK, - edgeColors[LogicalEdge.BLOCK_END.ordinal] - ?: edgeColors[LogicalEdge.BOTTOM.ordinal] + // Bottom: Physical edge (BOTTOM) > BLOCK_END > BLOCK > VERTICAL > ALL + edgeColors[LogicalEdge.BOTTOM.ordinal] + ?: edgeColors[LogicalEdge.BLOCK_END.ordinal] ?: edgeColors[LogicalEdge.BLOCK.ordinal] ?: edgeColors[LogicalEdge.VERTICAL.ordinal] ?: edgeColors[LogicalEdge.ALL.ordinal] diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/hermes/instrumentation/HermesSamplingProfiler.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/hermes/instrumentation/HermesSamplingProfiler.cpp index 6bd061f26b9a..77015a6b7577 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/hermes/instrumentation/HermesSamplingProfiler.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/hermes/instrumentation/HermesSamplingProfiler.cpp @@ -34,7 +34,7 @@ void HermesSamplingProfiler::dumpSampledTraceToFile( void HermesSamplingProfiler::registerNatives() { javaClassLocal()->registerNatives({ makeNativeMethod("enable", HermesSamplingProfiler::enable), - makeNativeMethod("disable", HermesSamplingProfiler::enable), + makeNativeMethod("disable", HermesSamplingProfiler::disable), makeNativeMethod( "dumpSampledTraceToFile", HermesSamplingProfiler::dumpSampledTraceToFile), 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 358134c9ed1b..52a9bad8b72f 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'): ?Spec);