fix(android): Stabilize custom accessibility action IDs for TalkBack (#52724)

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
This commit is contained in:
Wandji Emmanuel junior
2025-07-31 10:28:28 -07:00
committed by Facebook GitHub Bot
parent 2c540ac35a
commit 626568f9a3
@@ -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<String, Integer> sActionIdMap = new HashMap<>();
private static final String TAG = "ReactAccessibilityDelegate";
private static int sCounter = 0x3f000000;
private static int sCustomActionCounter = 0x3f000000;
private static final Map<String, Integer> 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);