From 626568f9a3f956a52f6c55df1dc3bc5cd017e353 Mon Sep 17 00:00:00 2001 From: Wandji Emmanuel junior <81602473+leg234-png@users.noreply.github.com> Date: Thu, 31 Jul 2025 10:28:28 -0700 Subject: [PATCH] fix(android): Stabilize custom accessibility action IDs for TalkBack (#52724) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: This pull request resolves a critical accessibility bug on Android where custom `accessibilityActions` fail to execute when activated via TalkBack's swipe gestures. **The Problem:** - When a user focuses a component with custom `accessibilityActions` (like a `TouchableOpacity`), TalkBack correctly announces the action labels as the user swipes up or down. - However, when the user double-taps to activate the selected action, TalkBack reports an "incompatible action," and the `onAccessibilityAction` event is never triggered. **The Root Cause:** The investigation revealed that the `ReactAccessibilityDelegate` was generating **new, unstable IDs** for custom actions on every UI update. This instability prevents the Android accessibility service from reliably tracking and invoking the selected action. **The Solution:** This change introduces a static, thread-safe cache (`ConcurrentHashMap`) within `ReactAccessibilityDelegate`. This ensures that each unique action name is mapped to a single, stable ID for the entire lifecycle of the application. This provides the consistency required by TalkBack to function correctly. This addresses the issue described in https://github.com/facebook/react-native/issues/47268. --- ## Changelog: [Android] [Fixed] - Stabilize custom accessibility action IDs to prevent "incompatible action" errors in TalkBack. --- Pull Request resolved: https://github.com/facebook/react-native/pull/52724 Test Plan: The fix was validated extensively using the RNTester app on a physical Android device and an Android emulator. ### Steps to Reproduce (Before Fix) 1. Enable TalkBack on an Android device. 2. Navigate to a `TouchableOpacity` component with several custom `accessibilityActions`. 3. Swipe up or down to cycle through the actions. TalkBack correctly announces them (e.g., "add to cart"). 4. Double-tap to execute the selected action. 5. **Result (Bug):** TalkBack announces *"incompatible action"*, and the `onAccessibilityAction` event is not triggered. ### Validation Steps (After Fix) 1. Follow the same steps as above on the patched version. 2. **Result (Fixed):** After double-tapping, the `onAccessibilityAction` event is **correctly triggered** with the appropriate action name. The "incompatible action" issue is fully resolved. *A screen recording demonstrating the successful fix can be provided if needed.* Uploading fixed bugs view problems (1).mp4… Fixes https://github.com/facebook/react-native/issues/47268 Reviewed By: jorge-cab Differential Revision: D78737471 Pulled By: cipolleschi fbshipit-source-id: 877b196597472ac6a4f6df81a05a43956fb34629 --- .../uimanager/ReactAccessibilityDelegate.java | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java index a14b5ddadc9..6e577b90293 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java @@ -43,6 +43,7 @@ import com.facebook.react.uimanager.events.EventDispatcher; import com.facebook.react.uimanager.util.ReactFindViewUtil; import java.util.HashMap; import java.util.List; +import java.util.Map; /** * Utility class that handles the addition of a "role" for accessibility to either a View or @@ -54,7 +55,8 @@ public class ReactAccessibilityDelegate extends ExploreByTouchHelper { public static final HashMap sActionIdMap = new HashMap<>(); private static final String TAG = "ReactAccessibilityDelegate"; - private static int sCounter = 0x3f000000; + private static int sCustomActionCounter = 0x3f000000; + private static final Map sCustomActionIdMap = new HashMap<>(); private static final int TIMEOUT_SEND_ACCESSIBILITY_EVENT = 200; private static final int SEND_EVENT = 1; private static final String delimiter = ", "; @@ -192,14 +194,23 @@ public class ReactAccessibilityDelegate extends ExploreByTouchHelper { if (!action.hasKey("name")) { throw new IllegalArgumentException("Unknown accessibility action."); } - int actionId = sCounter; + + String actionName = action.getString("name"); String actionLabel = action.hasKey("label") ? action.getString("label") : null; - if (sActionIdMap.containsKey(action.getString("name"))) { - actionId = sActionIdMap.get(action.getString("name")); + int actionId; + + if (sActionIdMap.containsKey(actionName)) { + actionId = sActionIdMap.get(actionName); } else { - sCounter++; + if (sCustomActionIdMap.containsKey(actionName)) { + actionId = sCustomActionIdMap.get(actionName); + } else { + actionId = sCustomActionCounter++; + sCustomActionIdMap.put(actionName, actionId); + } } - mAccessibilityActionsMap.put(actionId, action.getString("name")); + + mAccessibilityActionsMap.put(actionId, actionName); final AccessibilityActionCompat accessibilityAction = new AccessibilityActionCompat(actionId, actionLabel); info.addAction(accessibilityAction);