From 5a15f559609880d3438feac33736af5826d15ffe Mon Sep 17 00:00:00 2001 From: Jakub Kosmydel Date: Thu, 18 Jun 2026 14:02:50 +0200 Subject: [PATCH 1/3] [Android] Fix ConcurrentModificationException in GestureHandlerOrchestrator Use local snapshots of `gestureHandlers` when delivering and cancelling events instead of a shared `preparedHandlers` field and the live `asReversed()` view, both of which could throw during re-entrant state updates. Co-authored-by: Cursor --- .../core/GestureHandlerOrchestrator.kt | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt index 3d371e0076..8542570f96 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt @@ -29,7 +29,6 @@ class GestureHandlerOrchestrator( var minimumAlphaForTraversal = DEFAULT_MIN_ALPHA_FOR_TRAVERSAL private val gestureHandlers = arrayListOf() private val awaitingHandlers = arrayListOf() - private val preparedHandlers = arrayListOf() // In `onHandlerStateChange` method we iterate through `awaitingHandlers`, but calling `tryActivate` may modify this list. // To avoid `ConcurrentModificationException` we iterate through copy. There is one more problem though - if handler was @@ -260,19 +259,18 @@ class GestureHandlerOrchestrator( } private fun deliverEventToGestureHandlers(event: MotionEvent) { - // Copy handlers to "prepared handlers" array, because the list of active handlers can change + // Copy handlers to a local snapshot, because the list of active handlers can change // as a result of state updates - preparedHandlers.clear() - preparedHandlers.addAll(gestureHandlers) + val handlersToProcess = gestureHandlers.toMutableList() // We want to deliver events to active handlers first in order of their activation (handlers // that activated first will first get event delivered). Otherwise we deliver events in the // order in which handlers has been added ("most direct" children goes first). Therefore we rely // on Arrays.sort providing a stable sort (as children are registered in order in which they // should be tested) - preparedHandlers.sortWith(handlersComparator) + handlersToProcess.sortWith(handlersComparator) - for (handler in preparedHandlers) { + for (handler in handlersToProcess) { deliverEventToGestureHandler(handler, event) } } @@ -284,12 +282,9 @@ class GestureHandlerOrchestrator( handler.cancel() } - // Copy handlers to "prepared handlers" array, because the list of active handlers can change + // Iterate over a copy, because the list of active handlers can change // as a result of state updates - preparedHandlers.clear() - preparedHandlers.addAll(gestureHandlers) - - for (handler in gestureHandlers.asReversed()) { + for (handler in gestureHandlers.reversed()) { handler.cancel() } } From a68eceefa9f3ee7aa324282b0038acafa913a87d Mon Sep 17 00:00:00 2001 From: Jakub Kosmydel Date: Fri, 19 Jun 2026 09:09:51 +0200 Subject: [PATCH 2/3] address review --- .../core/GestureHandlerOrchestrator.kt | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt index 8542570f96..a381c58249 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt @@ -30,6 +30,16 @@ class GestureHandlerOrchestrator( private val gestureHandlers = arrayListOf() private val awaitingHandlers = arrayListOf() + // Pool of reusable lists for snapshotting `gestureHandlers` during event delivery. + private val handlerListPool = ArrayDeque>() + + private fun obtainHandlerList() = handlerListPool.removeLastOrNull() ?: ArrayList() + + private fun recycleHandlerList(list: ArrayList) { + list.clear() + handlerListPool.addLast(list) + } + // In `onHandlerStateChange` method we iterate through `awaitingHandlers`, but calling `tryActivate` may modify this list. // To avoid `ConcurrentModificationException` we iterate through copy. There is one more problem though - if handler was // removed from `awaitingHandlers`, it was still present in copy of original list. This hashset helps us identify which handlers @@ -259,9 +269,10 @@ class GestureHandlerOrchestrator( } private fun deliverEventToGestureHandlers(event: MotionEvent) { - // Copy handlers to a local snapshot, because the list of active handlers can change - // as a result of state updates - val handlersToProcess = gestureHandlers.toMutableList() + // Snapshot handlers into a pooled list, because the list of active handlers can change + // as a result of state updates (and delivery can be re-entrant). + val handlersToProcess = obtainHandlerList() + handlersToProcess.addAll(gestureHandlers) // We want to deliver events to active handlers first in order of their activation (handlers // that activated first will first get event delivered). Otherwise we deliver events in the @@ -270,8 +281,12 @@ class GestureHandlerOrchestrator( // should be tested) handlersToProcess.sortWith(handlersComparator) - for (handler in handlersToProcess) { - deliverEventToGestureHandler(handler, event) + try { + for (handler in handlersToProcess) { + deliverEventToGestureHandler(handler, event) + } + } finally { + recycleHandlerList(handlersToProcess) } } From 0e180f620116103eff7f3b94f74afba6c74f4210 Mon Sep 17 00:00:00 2001 From: Jakub Kosmydel Date: Fri, 19 Jun 2026 09:16:07 +0200 Subject: [PATCH 3/3] fix build --- .../swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt index a381c58249..03bb1cfd8b 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt @@ -33,7 +33,7 @@ class GestureHandlerOrchestrator( // Pool of reusable lists for snapshotting `gestureHandlers` during event delivery. private val handlerListPool = ArrayDeque>() - private fun obtainHandlerList() = handlerListPool.removeLastOrNull() ?: ArrayList() + private fun obtainHandlerList() = handlerListPool.pollLast() ?: ArrayList() private fun recycleHandlerList(list: ArrayList) { list.clear()