From 04c288e050ed9433e56ca9a1d535fef3b68e6626 Mon Sep 17 00:00:00 2001 From: Gregory Moskaliuk Date: Wed, 28 Oct 2026 11:00:49 -0700 Subject: [PATCH 1/2] Build Android touch event payloads in one JNI call (#59554) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: On Android, every touch event goes through `TouchesHelper.sendTouchEvent`, which builds the payload JS receives as nested `WritableNativeMap`s. **Current behavior.** For each pointer of the `MotionEvent`, `createPointersArray` creates a native map and puts eight values into it, one JNI call each. Then, for each changed pointer, the payload is assembled from copies of those maps: the event itself is a copy of the changed pointer, and `changedTouches` and `touches` are arrays of further copies (`copy()`, `pushMap()`, `putArray()`, each one or more JNI calls). With one finger moving, that is about 20–25 JNI calls and 8 native-backed objects for every `ACTION_MOVE`. **With this change.** `TouchesHelper` collects the pointer values once into a `DoubleArray`. For each event it creates an empty `WritableNativeMap` and passes it with the pointer data to a new native method, `TouchesHelper.nativePutTouchPayload`, which fills it in C++ through the map's existing `putDouble`, `putInt` and `putNativeArray`. That is two JNI calls per event (creating the map and filling it) instead of 20–25. `TouchesHelper` still decides which pointers go into `changedTouches` and `touches`, now as bit masks of pointer indices (start: the new pointer / all; end: the lifted pointer / all but it; move: all / all; cancel: all / none). The keys, values and value types of the payload are the same as before, and the events are dispatched exactly as before. `WritableNativeMap` is unchanged. The native method is registered like `TransformHelper`'s, from `TouchesHelper.cpp` in `reactnativejni_common`, and `TouchesHelper` is `DoNotStrip` because the class is looked up by name when the library loads. `TouchesHelper` is `internal`, so this adds no public Kotlin API; the C++ API snapshots get the new `TouchesHelper` class. The layout of the pointer data (eight values per pointer) is documented on `nativePutTouchPayload`, and the C++ side and the test shadow point to it. ## Changelog: [ANDROID] [CHANGED] - Build touch event payloads in native code, making each touch event cheaper on the main thread Pull Request resolved: https://github.com/react/react-native/pull/59554 Test Plan: - `TouchEventDispatchTest` passes. It checks the payloads of the start/move/end, start/move/cancel and multi-pointer start/pointer-up/cancel sequences, now through `ShadowTouchesHelper`, a Kotlin version of `nativePutTouchPayload`, and compares the map contents. With the end case deliberately broken (lifted pointer kept in `touches`), 2 of its 3 tests fail. - `ktfmt` and the repository's `clang-format` report no changes for the touched files. The C++ API snapshots were regenerated with `scripts/cxx-api`. - `./gradlew :packages:rn-tester:android:app:assembleRelease -PreactNativeArchitectures=arm64-v8a` succeeds, from `main` at 01d55273bc. - **Same payloads.** A local self-test (not part of this PR) built synthetic `MotionEvent`s for 1 to 5 pointers, a pointer going down and up at every index, a move and a cancel, ran them through the previous implementation and through the first version of this PR, and compared the payloads with `toHashMap()`: 40 events, 60 payloads, no differences, on a moto g57 power. The current version builds the payload with the same C++ code; only the way the result is put into the map changed. - **Release builds of this PR and of `main`.** Two separate RNTester release APKs (this PR, and `main` at 01d55273bc), RNTester Playground screen with a `PanResponder` drag area, 36 drags of 1.8 s per launch driven by `adb shell input swipe` (about 4,200 touch events per launch), installs alternated, moto g57 power (Android 16). On-CPU time of the main and JS threads during the drags from `/proc`, trace sections from Perfetto: | | `main` | this PR | change | |---|--:|--:|--:| | Main thread, 36 drags | 9.07 / 8.75 / 8.99 s | 8.22 / 8.12 / 8.15 s | −8.7% | | JS thread, 36 drags | 5.01 / 4.88 / 5.05 s | 4.97 / 4.92 / 5.00 s | no change | | `TouchesHelper.sentTouchEventModern(topTouchMove)` trace section, ~700 events | 449 µs | 309 µs | −31% | | Native heap after the drags (`dumpsys meminfo`) | 44.3 / 42.5 / 42.4 MB | 33.2 / 33.3 / 33.1 MB | −10 MB | The trace-section times include the tracing overhead. `FabricEventEmitter.receiveEvent('topTouchMove')` is about 11–17 µs slower with this PR (95 → 111 µs), which is small next to the saving in `sentTouchEventModern`. In every run JS received all touch moves, 10 out of 10 taps on `Pressable` rows were handled, and there were no errors in logcat. - Not run: a C++ unit test of `nativePutTouchPayload` (JVM tests can't load the native library; it is covered on the device by the runs above and, through the shadow, by `TouchEventDispatchTest`). 🤖 Generated with [Claude Code](https://claude.com/claude-code) ## Internal Buck validation - `buck build fbsource//xplat/js/react-native-github/packages/react-native/ReactAndroid/src/main/jni/react/jni:jniAndroid fbsource//xplat/js/react-native-github/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events:events-commonAndroid fbsource//xplat/js/react-native-github/packages/react-native/ReactAndroid/src/test/java/com/facebook/testutils/shadows:shadowsAndroid` — passed. - `buck test fbsource//xplat/js/react-native-github/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/events:eventsAndroid -- TouchEventDispatchTest` — 4 passed, 0 failed. Test run: https://www.internalfb.com/intern/testinfra/testrun/14355224025898138 - `arc lint -a` — passed. - `arc lint -e extra --take CLANGTIDY --never-apply-patches xplat/js/react-native-github/packages/react-native/ReactAndroid/src/main/jni/react/jni/TouchesHelper.cpp` — no lint issues. - Repository-managed `clang-format --dry-run --Werror` on `TouchesHelper.cpp` — passed. - Compared the eight Kotlin/C++ field-index constants and the pointer stride; all match, and the producer, native readers and shadow use named indices. Reviewed By: christophpurrer Differential Revision: D128055442 Pulled By: javache --- .../react/uimanager/events/TouchesHelper.kt | 147 +++++++++--------- .../src/main/jni/react/jni/CMakeLists.txt | 1 + .../src/main/jni/react/jni/OnLoad.cpp | 2 + .../src/main/jni/react/jni/TouchesHelper.cpp | 104 +++++++++++++ .../src/main/jni/react/jni/TouchesHelper.h | 21 +++ .../fabric/events/TouchEventDispatchTest.kt | 26 +++- .../testutils/shadows/ShadowTouchesHelper.kt | 62 ++++++++ .../api-snapshots/ReactAndroidDebugCxx.api | 5 + .../ReactAndroidFrameworksCxx.api | 5 + .../api-snapshots/ReactAndroidNewarchCxx.api | 5 + .../api-snapshots/ReactAndroidReleaseCxx.api | 5 + 11 files changed, 306 insertions(+), 77 deletions(-) create mode 100644 packages/react-native/ReactAndroid/src/main/jni/react/jni/TouchesHelper.cpp create mode 100644 packages/react-native/ReactAndroid/src/main/jni/react/jni/TouchesHelper.h create mode 100644 packages/react-native/ReactAndroid/src/test/java/com/facebook/testutils/shadows/ShadowTouchesHelper.kt diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchesHelper.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchesHelper.kt index 435ac67afbf6..8e30d686e41f 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchesHelper.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchesHelper.kt @@ -7,9 +7,9 @@ package com.facebook.react.uimanager.events -import com.facebook.react.bridge.Arguments -import com.facebook.react.bridge.WritableArray -import com.facebook.react.bridge.WritableMap +import com.facebook.proguard.annotations.DoNotStrip +import com.facebook.react.bridge.ReactNativeJNISoLoader +import com.facebook.react.bridge.WritableNativeMap import com.facebook.react.uimanager.PixelUtil.pxToDp import com.facebook.systrace.Systrace @@ -17,28 +17,31 @@ import com.facebook.systrace.Systrace * Class responsible for generating catalyst touch events based on android * [android.view.MotionEvent]. */ +@DoNotStrip internal object TouchesHelper { - @JvmField @Deprecated("Not used in New Architecture") val TARGET_KEY: String = "target" - - private const val TARGET_SURFACE_KEY = "targetSurface" - private const val CHANGED_TOUCHES_KEY = "changedTouches" - private const val TOUCHES_KEY = "touches" - private const val PAGE_X_KEY = "pageX" - private const val PAGE_Y_KEY = "pageY" - private const val TIMESTAMP_KEY = "timestamp" - private const val POINTER_IDENTIFIER_KEY = "identifier" + init { + ReactNativeJNISoLoader.staticInit() + } - private const val LOCATION_X_KEY = "locationX" - private const val LOCATION_Y_KEY = "locationY" + // Keep in sync with the field indices in TouchesHelper.cpp. + const val PAGE_X_INDEX = 0 + const val PAGE_Y_INDEX = 1 + const val LOCATION_X_INDEX = 2 + const val LOCATION_Y_INDEX = 3 + const val TARGET_SURFACE_INDEX = 4 + const val TARGET_INDEX = 5 + const val TIMESTAMP_INDEX = 6 + const val IDENTIFIER_INDEX = 7 + const val POINTER_FIELDS = 8 /** - * Creates catalyst pointers array in format that is expected by RCTEventEmitter JS module from - * given {@param event} instance. This method use {@param reactTarget} parameter to set as a - * target view id associated with current gesture. + * Fills the pointer data of [nativeCreateTouchPayload]: for each pointer of the + * [android.view.MotionEvent], in order, pageX, pageY, locationX, locationY, targetSurface, + * target, timestamp and identifier. */ - private fun createPointersArray(event: TouchEvent): Array { + private fun createPointerData(event: TouchEvent): DoubleArray { val motionEvent = event.getMotionEvent() - val touches = arrayOfNulls(motionEvent.pointerCount) + val pointerData = DoubleArray(motionEvent.pointerCount * POINTER_FIELDS) // Calculate the coordinates for the target view. // The MotionEvent contains the X,Y of the touch in the coordinate space of the root view @@ -51,29 +54,27 @@ internal object TouchesHelper { val targetViewCoordinateY = motionEvent.y - event.viewY for (index in 0 until motionEvent.pointerCount) { - val touch = Arguments.createMap() + val field = index * POINTER_FIELDS // pageX,Y values are relative to the RootReactView // the motionEvent already contains coordinates in that view - touch.putDouble(PAGE_X_KEY, motionEvent.getX(index).pxToDp().toDouble()) - touch.putDouble(PAGE_Y_KEY, motionEvent.getY(index).pxToDp().toDouble()) + pointerData[field + PAGE_X_INDEX] = motionEvent.getX(index).pxToDp().toDouble() + pointerData[field + PAGE_Y_INDEX] = motionEvent.getY(index).pxToDp().toDouble() // locationX,Y values are relative to the target view // To compute the values for the view, we subtract that views location from the event X,Y val locationX = motionEvent.getX(index) - targetViewCoordinateX val locationY = motionEvent.getY(index) - targetViewCoordinateY - touch.putDouble(LOCATION_X_KEY, locationX.pxToDp().toDouble()) - touch.putDouble(LOCATION_Y_KEY, locationY.pxToDp().toDouble()) + pointerData[field + LOCATION_X_INDEX] = locationX.pxToDp().toDouble() + pointerData[field + LOCATION_Y_INDEX] = locationY.pxToDp().toDouble() - touch.putInt(TARGET_SURFACE_KEY, event.surfaceId) - @Suppress("DEPRECATION") touch.putInt(TARGET_KEY, event.viewTag) - touch.putDouble(TIMESTAMP_KEY, event.timestampMs.toDouble()) - touch.putDouble(POINTER_IDENTIFIER_KEY, motionEvent.getPointerId(index).toDouble()) - - touches[index] = touch + pointerData[field + TARGET_SURFACE_INDEX] = event.surfaceId.toDouble() + pointerData[field + TARGET_INDEX] = event.viewTag.toDouble() + pointerData[field + TIMESTAMP_INDEX] = event.timestampMs.toDouble() + pointerData[field + IDENTIFIER_INDEX] = motionEvent.getPointerId(index).toDouble() } - return touches + return pointerData } /** @@ -82,8 +83,9 @@ internal object TouchesHelper { * more efficient manner. * * Touches have to be dispatched as separate events for each changed pointer to make JS process - * them correctly. To avoid allocations, we preprocess touch events in Java world and then convert - * them to native before dispatch. + * them correctly. The pointers are collected once into a [DoubleArray], and each event's payload + * is built from it in a single JNI call, instead of putting every value into a native map one by + * one. * * @param eventEmitter emitter to dispatch event to * @param event the touch event to extract data from @@ -95,52 +97,44 @@ internal object TouchesHelper { "TouchesHelper.sentTouchEventModern(" + event.getEventName() + ")", ) try { - val type = event.getTouchEventType() val motionEvent = event.getMotionEvent() - var touches = createPointersArray(event) - val changedTouches: Array? - - when (type) { + val pointerCount = motionEvent.pointerCount + // Pointers are passed to native as bit masks of their indices. + check(pointerCount < Int.SIZE_BITS) { "Too many pointers: $pointerCount" } + val allPointers = (1 shl pointerCount) - 1 + val changedPointers: Int + val touchPointers: Int + + when (event.getTouchEventType()) { TouchEventType.START -> { - val newPointerIndex = motionEvent.actionIndex - - changedTouches = arrayOf(touches[newPointerIndex]?.copy()) + changedPointers = 1 shl motionEvent.actionIndex + touchPointers = allPointers } TouchEventType.END -> { - val finishedPointerIndex = motionEvent.actionIndex + changedPointers = 1 shl motionEvent.actionIndex /* - * Clear finished pointer index for compatibility with W3C touch "end" events, where the + * Leave out the finished pointer for compatibility with W3C touch "end" events, where the * active touches don't include the set that has just been "ended". */ - val finishedPointer = touches[finishedPointerIndex] - touches[finishedPointerIndex] = null - - changedTouches = arrayOf(finishedPointer) + touchPointers = allPointers and changedPointers.inv() } TouchEventType.MOVE -> { - changedTouches = arrayOfNulls(touches.size) - var i = 0 - while (i < touches.size) { - changedTouches[i] = touches[i]?.copy() - i++ - } + changedPointers = allPointers + touchPointers = allPointers } TouchEventType.CANCEL -> { - changedTouches = touches - touches = arrayOfNulls(0) + changedPointers = allPointers + touchPointers = 0 } } - for (touchData in changedTouches) { + val pointerData = createPointerData(event) + for (pointer in 0 until pointerCount) { + if (changedPointers and (1 shl pointer) == 0) { + continue + } val eventData = - touchData?.let { td -> - val ed = td.copy() - val changedTouchesArray = getWritableArray(/* copyObjects */ true, changedTouches) - val touchesArray = getWritableArray(/* copyObjects */ true, touches) - ed.putArray(CHANGED_TOUCHES_KEY, changedTouchesArray) - ed.putArray(TOUCHES_KEY, touchesArray) - ed - } + nativeCreateTouchPayload(pointerData, pointer, changedPointers, touchPointers) eventEmitter.receiveEvent( event.surfaceId, @@ -158,13 +152,20 @@ internal object TouchesHelper { } } - private fun getWritableArray(copyObjects: Boolean, objects: Array): WritableArray { - val result = Arguments.createArray() - for (obj in objects) { - if (obj != null) { - result.pushMap(if (copyObjects) obj.copy() else obj) - } - } - return result - } + /** + * Creates the payload of a touch event: the fields of pointer [eventPointer] and the + * `changedTouches` and `touches` arrays of the pointers whose bits are set in [changedPointers] + * and [touchPointers]. + * + * @param pointerData 8 values for each pointer, in order: pageX, pageY, locationX, locationY, + * targetSurface, target, timestamp and identifier. + */ + @DoNotStrip + @JvmStatic + private external fun nativeCreateTouchPayload( + pointerData: DoubleArray, + eventPointer: Int, + changedPointers: Int, + touchPointers: Int, + ): WritableNativeMap } diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/jni/CMakeLists.txt b/packages/react-native/ReactAndroid/src/main/jni/react/jni/CMakeLists.txt index 5229c2af141a..1b58d0879f3d 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/jni/CMakeLists.txt +++ b/packages/react-native/ReactAndroid/src/main/jni/react/jni/CMakeLists.txt @@ -32,6 +32,7 @@ add_library( OnLoad-common.cpp ReadableNativeArray.cpp ReadableNativeMap.cpp + TouchesHelper.cpp TransformHelper.cpp WritableNativeArray.cpp WritableNativeMap.cpp diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp index 04ef88c09e98..ba536d2cce35 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp @@ -11,6 +11,7 @@ #include #include "InspectorNetworkRequestListener.h" +#include "TouchesHelper.h" #include "TransformHelper.h" #ifndef WITH_GLOGINIT @@ -34,6 +35,7 @@ extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { FLAGS_minloglevel = 0; #endif InspectorNetworkRequestListener::registerNatives(); + TouchesHelper::registerNatives(); TransformHelper::registerNatives(); }); } diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/jni/TouchesHelper.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/jni/TouchesHelper.cpp new file mode 100644 index 000000000000..6696c93d0bb8 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/jni/react/jni/TouchesHelper.cpp @@ -0,0 +1,104 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "TouchesHelper.h" + +#include + +#include "WritableNativeArray.h" +#include "WritableNativeMap.h" + +using namespace facebook::jni; + +namespace facebook::react { + +namespace { + +// Keep in sync with the field indices in TouchesHelper.kt. +constexpr jint kPageXIndex = 0; +constexpr jint kPageYIndex = 1; +constexpr jint kLocationXIndex = 2; +constexpr jint kLocationYIndex = 3; +constexpr jint kTargetSurfaceIndex = 4; +constexpr jint kTargetIndex = 5; +constexpr jint kTimestampIndex = 6; +constexpr jint kIdentifierIndex = 7; +constexpr jint kPointerFields = 8; + +folly::dynamic createTouch(const double* fields) { + folly::dynamic touch = folly::dynamic::object(); + // NOLINTBEGIN(cppcoreguidelines-pro-bounds-pointer-arithmetic) + touch.insert("pageX", fields[kPageXIndex]); + touch.insert("pageY", fields[kPageYIndex]); + touch.insert("locationX", fields[kLocationXIndex]); + touch.insert("locationY", fields[kLocationYIndex]); + touch.insert("targetSurface", static_cast(fields[kTargetSurfaceIndex])); + touch.insert("target", static_cast(fields[kTargetIndex])); + touch.insert("timestamp", fields[kTimestampIndex]); + touch.insert("identifier", fields[kIdentifierIndex]); + // NOLINTEND(cppcoreguidelines-pro-bounds-pointer-arithmetic) + return touch; +} + +local_ref createTouchPayload( + alias_ref /*unused*/, + alias_ref pointerData, + jint eventPointer, + jint changedPointers, + jint touchPointers) { + auto length = static_cast(pointerData->size()); + jint pointerCount = length / kPointerFields; + // Pointers are selected by bits of the jint masks. + if (length % kPointerFields != 0 || pointerCount >= 32 || eventPointer < 0 || + eventPointer >= pointerCount) { + throw std::invalid_argument("Invalid touch pointer data"); + } + auto values = pointerData->getRegion(0, length); + + folly::dynamic changedTouches = folly::dynamic::array(); + folly::dynamic touches = folly::dynamic::array(); + for (jint index = 0; index < pointerCount; index++) { + // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) + const double* fields = values.get() + index * kPointerFields; + if ((changedPointers & (1 << index)) != 0) { + changedTouches.push_back(createTouch(fields)); + } + if ((touchPointers & (1 << index)) != 0) { + touches.push_back(createTouch(fields)); + } + } + + auto jPayload = WritableNativeMap::newObjectJavaArgs(); + auto* payload = jPayload->cthis(); + // NOLINTBEGIN(cppcoreguidelines-pro-bounds-pointer-arithmetic) + const double* fields = values.get() + eventPointer * kPointerFields; + payload->putDouble("pageX", fields[kPageXIndex]); + payload->putDouble("pageY", fields[kPageYIndex]); + payload->putDouble("locationX", fields[kLocationXIndex]); + payload->putDouble("locationY", fields[kLocationYIndex]); + payload->putInt( + "targetSurface", static_cast(fields[kTargetSurfaceIndex])); + payload->putInt("target", static_cast(fields[kTargetIndex])); + payload->putDouble("timestamp", fields[kTimestampIndex]); + payload->putDouble("identifier", fields[kIdentifierIndex]); + // NOLINTEND(cppcoreguidelines-pro-bounds-pointer-arithmetic) + WritableNativeArray changedTouchesArray(std::move(changedTouches)); + payload->putNativeArray("changedTouches", &changedTouchesArray); + WritableNativeArray touchesArray(std::move(touches)); + payload->putNativeArray("touches", &touchesArray); + return jPayload; +} + +} // namespace + +void TouchesHelper::registerNatives() { + javaClassLocal()->registerNatives({ + makeNativeMethod("nativeCreateTouchPayload", createTouchPayload), + }); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/jni/TouchesHelper.h b/packages/react-native/ReactAndroid/src/main/jni/react/jni/TouchesHelper.h new file mode 100644 index 000000000000..5037ca0edb56 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/jni/react/jni/TouchesHelper.h @@ -0,0 +1,21 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +namespace facebook::react { + +class TouchesHelper : public jni::JavaClass { + public: + static auto constexpr *kJavaDescriptor = "Lcom/facebook/react/uimanager/events/TouchesHelper;"; + + static void registerNatives(); +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/events/TouchEventDispatchTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/events/TouchEventDispatchTest.kt index 00fc7e3489db..55cef1f4abf0 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/events/TouchEventDispatchTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/events/TouchEventDispatchTest.kt @@ -27,7 +27,11 @@ import com.facebook.react.uimanager.events.TouchEvent import com.facebook.react.uimanager.events.TouchEventCoalescingKeyHelper import com.facebook.react.uimanager.events.TouchEventType import com.facebook.testutils.shadows.ShadowArguments +import com.facebook.testutils.shadows.ShadowNativeLoader +import com.facebook.testutils.shadows.ShadowNativeMap import com.facebook.testutils.shadows.ShadowSoLoader +import com.facebook.testutils.shadows.ShadowTouchesHelper +import com.facebook.testutils.shadows.ShadowWritableNativeMap import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.Before @@ -41,9 +45,19 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config +import org.robolectric.shadow.api.Shadow @RunWith(RobolectricTestRunner::class) -@Config(shadows = [ShadowSoLoader::class, ShadowArguments::class]) +@Config( + shadows = + [ + ShadowSoLoader::class, + ShadowNativeLoader::class, + ShadowArguments::class, + ShadowWritableNativeMap::class, + ShadowTouchesHelper::class, + ] +) class TouchEventDispatchTest { private val touchEventCoalescingKeyHelper = TouchEventCoalescingKeyHelper() @@ -558,7 +572,7 @@ class TouchEventDispatchTest { anyInt(), anyLong(), ) - assertThat(startMoveEndExpectedSequence).isEqualTo(argument.allValues) + assertThat(startMoveEndExpectedSequence).isEqualTo(argument.payloads()) } @Test @@ -578,7 +592,7 @@ class TouchEventDispatchTest { anyInt(), anyLong(), ) - assertThat(startMoveCancelExpectedSequence).isEqualTo(argument.allValues) + assertThat(startMoveCancelExpectedSequence).isEqualTo(argument.payloads()) } @Test @@ -598,7 +612,7 @@ class TouchEventDispatchTest { anyInt(), anyLong(), ) - assertThat(startPointerMoveUpExpectedSequence).isEqualTo(argument.allValues) + assertThat(startPointerMoveUpExpectedSequence).isEqualTo(argument.payloads()) } private fun createTouchEvent( @@ -696,5 +710,9 @@ class TouchEventDispatchTest { } } +/** The contents of the captured payloads, which are shadowed native maps. */ +private fun ArgumentCaptor.payloads(): List = + allValues.map { Shadow.extract(it).backingMap } + private fun IntArray.toPointerProperties(): Array = this.map { PointerProperties().apply { id = it } }.toTypedArray() diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/testutils/shadows/ShadowTouchesHelper.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/testutils/shadows/ShadowTouchesHelper.kt new file mode 100644 index 000000000000..588f3787a331 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/testutils/shadows/ShadowTouchesHelper.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.testutils.shadows + +import com.facebook.react.bridge.JavaOnlyArray +import com.facebook.react.bridge.JavaOnlyMap +import com.facebook.react.bridge.WritableMap +import com.facebook.react.bridge.WritableNativeMap +import com.facebook.react.uimanager.events.TouchesHelper +import org.robolectric.annotation.Implementation +import org.robolectric.annotation.Implements + +@Implements(TouchesHelper::class) +class ShadowTouchesHelper { + + companion object { + + /** Builds the same payload as the native TouchesHelper::createTouchPayload. */ + @JvmStatic + @Implementation + fun nativeCreateTouchPayload( + pointerData: DoubleArray, + eventPointer: Int, + changedPointers: Int, + touchPointers: Int, + ): WritableNativeMap { + val pointerCount = pointerData.size / TouchesHelper.POINTER_FIELDS + val changedTouches = JavaOnlyArray() + val touches = JavaOnlyArray() + for (index in 0 until pointerCount) { + if (changedPointers and (1 shl index) != 0) { + changedTouches.pushMap(JavaOnlyMap().also { putTouch(it, pointerData, index) }) + } + if (touchPointers and (1 shl index) != 0) { + touches.pushMap(JavaOnlyMap().also { putTouch(it, pointerData, index) }) + } + } + val payload = WritableNativeMap() + putTouch(payload, pointerData, eventPointer) + payload.putArray("changedTouches", changedTouches) + payload.putArray("touches", touches) + return payload + } + + private fun putTouch(touch: WritableMap, pointerData: DoubleArray, index: Int) { + val field = index * TouchesHelper.POINTER_FIELDS + touch.putDouble("pageX", pointerData[field + TouchesHelper.PAGE_X_INDEX]) + touch.putDouble("pageY", pointerData[field + TouchesHelper.PAGE_Y_INDEX]) + touch.putDouble("locationX", pointerData[field + TouchesHelper.LOCATION_X_INDEX]) + touch.putDouble("locationY", pointerData[field + TouchesHelper.LOCATION_Y_INDEX]) + touch.putInt("targetSurface", pointerData[field + TouchesHelper.TARGET_SURFACE_INDEX].toInt()) + touch.putInt("target", pointerData[field + TouchesHelper.TARGET_INDEX].toInt()) + touch.putDouble("timestamp", pointerData[field + TouchesHelper.TIMESTAMP_INDEX]) + touch.putDouble("identifier", pointerData[field + TouchesHelper.IDENTIFIER_INDEX]) + } + } +} diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index ca72d3985fc9..97a6a1f652cc 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -2426,6 +2426,11 @@ class facebook::react::TouchEventEmitter : public facebook::react::EventEmitter public void onTouchStart(facebook::react::TouchEvent event) const; } +class facebook::react::TouchesHelper : public facebook::jni::JavaClass { + public static constexpr auto * kJavaDescriptor; + public static void registerNatives(); +} + class facebook::react::TracingDelegate : public facebook::react::jsinspector_modern::HostTargetTracingDelegate { public TracingState getTracingState(); public size_t registerTracingStateListener(facebook::react::TracingStateListener listener); diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api index 5e5dad5f5ce5..4de5fdc97f51 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api @@ -4079,6 +4079,11 @@ class facebook::react::TouchEventEmitter : public facebook::react::EventEmitter public void onTouchStart(facebook::react::TouchEvent event) const; } +class facebook::react::TouchesHelper : public facebook::jni::JavaClass { + public static constexpr auto * kJavaDescriptor; + public static void registerNatives(); +} + class facebook::react::TracingDelegate : public facebook::react::jsinspector_modern::HostTargetTracingDelegate { public TracingState getTracingState(); public size_t registerTracingStateListener(facebook::react::TracingStateListener listener); diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api index 1a105ab8080c..05dc03891252 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api @@ -2419,6 +2419,11 @@ class facebook::react::TouchEventEmitter : public facebook::react::EventEmitter public void onTouchStart(facebook::react::TouchEvent event) const; } +class facebook::react::TouchesHelper : public facebook::jni::JavaClass { + public static constexpr auto * kJavaDescriptor; + public static void registerNatives(); +} + class facebook::react::TracingDelegate : public facebook::react::jsinspector_modern::HostTargetTracingDelegate { public TracingState getTracingState(); public size_t registerTracingStateListener(facebook::react::TracingStateListener listener); diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api index 1a105ab8080c..05dc03891252 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -2419,6 +2419,11 @@ class facebook::react::TouchEventEmitter : public facebook::react::EventEmitter public void onTouchStart(facebook::react::TouchEvent event) const; } +class facebook::react::TouchesHelper : public facebook::jni::JavaClass { + public static constexpr auto * kJavaDescriptor; + public static void registerNatives(); +} + class facebook::react::TracingDelegate : public facebook::react::jsinspector_modern::HostTargetTracingDelegate { public TracingState getTracingState(); public size_t registerTracingStateListener(facebook::react::TracingStateListener listener); From f8ea32aa988f38767450f943a898d91244dcbb39 Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Wed, 28 Oct 2026 17:22:15 -0700 Subject: [PATCH 2/2] Accept readable Android event payloads (#59595) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Pull Request resolved: https://github.com/react/react-native/pull/59595 Widen Android event payload contracts from `WritableMap` to `ReadableMap` across the event emitters, UI managers and Fabric dispatch pipeline, including `Event.getEventData`. Existing event producers can continue returning writable maps. Build native touch payloads as a complete `folly::dynamic` and return them through `ReadableNativeMap::createWithContents` without constructing and populating a writable Java map. Document Fabric’s native-backed, consumed-payload contract, update the touch test shadow to return a read-only native map and regenerate the Android API snapshot. Changelog: [Android][Breaking] - Change Android event payload APIs from `WritableMap` to `ReadableMap`; update custom emitter and UI manager implementations to match. Differential Revision: D128208007 --- .../ReactAndroid/api/ReactAndroid.api | 40 ++++++++++--------- .../react/animated/EventAnimationDriver.kt | 3 +- .../com/facebook/react/bridge/UIManager.kt | 16 ++++---- .../react/fabric/FabricUIManager.java | 10 ++--- .../fabric/events/EventEmitterWrapper.kt | 12 +++--- .../react/fabric/events/FabricEventEmitter.kt | 6 +-- .../react/fabric/mounting/MountingManager.kt | 3 +- .../fabric/mounting/SurfaceMountingManager.kt | 4 +- .../react/internal/interop/InteropEvent.kt | 6 +-- .../internal/interop/InteropEventEmitter.kt | 4 +- .../react/uimanager/UIManagerModule.kt | 4 +- .../facebook/react/uimanager/events/Event.kt | 6 +-- .../react/uimanager/events/RCTEventEmitter.kt | 4 +- .../uimanager/events/RCTModernEventEmitter.kt | 13 +++--- .../events/SynchronousEventReceiver.kt | 6 +-- .../react/uimanager/events/TouchesHelper.kt | 4 +- .../src/main/jni/react/jni/TouchesHelper.cpp | 34 ++++++---------- .../interop/InteropModuleRegistryTest.kt | 4 +- .../fabric/events/TouchEventDispatchTest.kt | 21 ++++++---- .../facebook/testutils/fakes/FakeUIManager.kt | 4 +- .../testutils/shadows/ShadowTouchesHelper.kt | 12 ++++-- 21 files changed, 111 insertions(+), 105 deletions(-) diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index a5fa388742a7..8c91b5df70c8 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -1372,8 +1372,8 @@ public abstract interface class com/facebook/react/bridge/UIManager : com/facebo public abstract fun initialize ()V public abstract fun invalidate ()V public abstract fun markActiveTouchForTag (II)V - public abstract fun receiveEvent (IILjava/lang/String;Lcom/facebook/react/bridge/WritableMap;)V - public abstract fun receiveEvent (ILjava/lang/String;Lcom/facebook/react/bridge/WritableMap;)V + public abstract fun receiveEvent (IILjava/lang/String;Lcom/facebook/react/bridge/ReadableMap;)V + public abstract fun receiveEvent (ILjava/lang/String;Lcom/facebook/react/bridge/ReadableMap;)V public abstract fun removeUIManagerEventListener (Lcom/facebook/react/bridge/UIManagerListener;)V public abstract fun resolveCustomDirectEventName (Ljava/lang/String;)Ljava/lang/String; public abstract fun resolveView (I)Landroid/view/View; @@ -2275,11 +2275,11 @@ public class com/facebook/react/fabric/FabricUIManager : com/facebook/react/brid public fun onRequestEventBeat ()V public fun prependUIBlock (Lcom/facebook/react/fabric/interop/UIBlock;)V public fun profileNextBatch ()V - public fun receiveEvent (IILjava/lang/String;Lcom/facebook/react/bridge/WritableMap;)V - public fun receiveEvent (IILjava/lang/String;ZLcom/facebook/react/bridge/WritableMap;I)V - public fun receiveEvent (IILjava/lang/String;ZLcom/facebook/react/bridge/WritableMap;IZ)V - public fun receiveEvent (IILjava/lang/String;ZLcom/facebook/react/bridge/WritableMap;IZJ)V - public fun receiveEvent (ILjava/lang/String;Lcom/facebook/react/bridge/WritableMap;)V + public fun receiveEvent (IILjava/lang/String;Lcom/facebook/react/bridge/ReadableMap;)V + public fun receiveEvent (IILjava/lang/String;ZLcom/facebook/react/bridge/ReadableMap;I)V + public fun receiveEvent (IILjava/lang/String;ZLcom/facebook/react/bridge/ReadableMap;IZ)V + public fun receiveEvent (IILjava/lang/String;ZLcom/facebook/react/bridge/ReadableMap;IZJ)V + public fun receiveEvent (ILjava/lang/String;Lcom/facebook/react/bridge/ReadableMap;)V public fun removeUIManagerEventListener (Lcom/facebook/react/bridge/UIManagerListener;)V public fun resolveCustomDirectEventName (Ljava/lang/String;)Ljava/lang/String; public fun resolveView (I)Landroid/view/View; @@ -3503,6 +3503,7 @@ public final class com/facebook/react/uimanager/NativeViewHierarchyOptimizer { public final class com/facebook/react/uimanager/OnLayoutEvent : com/facebook/react/uimanager/events/Event { public static final field Companion Lcom/facebook/react/uimanager/OnLayoutEvent$Companion; + public synthetic fun getEventData ()Lcom/facebook/react/bridge/ReadableMap; public fun getEventName ()Ljava/lang/String; public static final fun obtain (IIIII)Lcom/facebook/react/uimanager/OnLayoutEvent; public static final fun obtain (IIIIII)Lcom/facebook/react/uimanager/OnLayoutEvent; @@ -4275,8 +4276,8 @@ public class com/facebook/react/uimanager/UIManagerModule : com/facebook/react/b public fun onHostResume ()V public fun prependUIBlock (Lcom/facebook/react/uimanager/UIBlock;)V public fun profileNextBatch ()V - public fun receiveEvent (IILjava/lang/String;Lcom/facebook/react/bridge/WritableMap;)V - public fun receiveEvent (ILjava/lang/String;Lcom/facebook/react/bridge/WritableMap;)V + public fun receiveEvent (IILjava/lang/String;Lcom/facebook/react/bridge/ReadableMap;)V + public fun receiveEvent (ILjava/lang/String;Lcom/facebook/react/bridge/ReadableMap;)V public fun removeRootView (I)V public fun removeUIManagerEventListener (Lcom/facebook/react/bridge/UIManagerListener;)V public fun resolveCustomDirectEventName (Ljava/lang/String;)Ljava/lang/String; @@ -4679,6 +4680,7 @@ public abstract interface class com/facebook/react/uimanager/events/BatchEventDi public final class com/facebook/react/uimanager/events/ContentSizeChangeEvent : com/facebook/react/uimanager/events/Event { public fun (III)V public fun (IIII)V + public synthetic fun getEventData ()Lcom/facebook/react/bridge/ReadableMap; public fun getEventName ()Ljava/lang/String; } @@ -4695,7 +4697,7 @@ public abstract class com/facebook/react/uimanager/events/Event { public fun getCoalescingKey ()S public fun getEventAnimationDriverMatchSpec ()Lcom/facebook/react/uimanager/events/Event$EventAnimationDriverMatchSpec; protected fun getEventCategory ()I - protected fun getEventData ()Lcom/facebook/react/bridge/WritableMap; + protected fun getEventData ()Lcom/facebook/react/bridge/ReadableMap; public abstract fun getEventName ()Ljava/lang/String; public final fun getSurfaceId ()I public final fun getTimestampMs ()J @@ -4734,7 +4736,7 @@ public final class com/facebook/react/uimanager/events/NativeGestureUtil { } public abstract interface class com/facebook/react/uimanager/events/RCTEventEmitter : com/facebook/react/bridge/JavaScriptModule { - public abstract fun receiveEvent (ILjava/lang/String;Lcom/facebook/react/bridge/WritableMap;)V + public abstract fun receiveEvent (ILjava/lang/String;Lcom/facebook/react/bridge/ReadableMap;)V public fun receiveTouches (Ljava/lang/String;Lcom/facebook/react/bridge/WritableArray;Lcom/facebook/react/bridge/WritableArray;)V } @@ -4743,16 +4745,16 @@ public final class com/facebook/react/uimanager/events/RCTEventEmitter$DefaultIm } public abstract interface class com/facebook/react/uimanager/events/RCTModernEventEmitter : com/facebook/react/uimanager/events/RCTEventEmitter { - public fun receiveEvent (IILjava/lang/String;Lcom/facebook/react/bridge/WritableMap;)V - public abstract fun receiveEvent (IILjava/lang/String;ZILcom/facebook/react/bridge/WritableMap;I)V - public fun receiveEvent (IILjava/lang/String;ZILcom/facebook/react/bridge/WritableMap;IJ)V - public fun receiveEvent (ILjava/lang/String;Lcom/facebook/react/bridge/WritableMap;)V + public fun receiveEvent (IILjava/lang/String;Lcom/facebook/react/bridge/ReadableMap;)V + public abstract fun receiveEvent (IILjava/lang/String;ZILcom/facebook/react/bridge/ReadableMap;I)V + public fun receiveEvent (IILjava/lang/String;ZILcom/facebook/react/bridge/ReadableMap;IJ)V + public fun receiveEvent (ILjava/lang/String;Lcom/facebook/react/bridge/ReadableMap;)V } public final class com/facebook/react/uimanager/events/RCTModernEventEmitter$DefaultImpls { - public static fun receiveEvent (Lcom/facebook/react/uimanager/events/RCTModernEventEmitter;IILjava/lang/String;Lcom/facebook/react/bridge/WritableMap;)V - public static fun receiveEvent (Lcom/facebook/react/uimanager/events/RCTModernEventEmitter;IILjava/lang/String;ZILcom/facebook/react/bridge/WritableMap;IJ)V - public static fun receiveEvent (Lcom/facebook/react/uimanager/events/RCTModernEventEmitter;ILjava/lang/String;Lcom/facebook/react/bridge/WritableMap;)V + public static fun receiveEvent (Lcom/facebook/react/uimanager/events/RCTModernEventEmitter;IILjava/lang/String;Lcom/facebook/react/bridge/ReadableMap;)V + public static fun receiveEvent (Lcom/facebook/react/uimanager/events/RCTModernEventEmitter;IILjava/lang/String;ZILcom/facebook/react/bridge/ReadableMap;IJ)V + public static fun receiveEvent (Lcom/facebook/react/uimanager/events/RCTModernEventEmitter;ILjava/lang/String;Lcom/facebook/react/bridge/ReadableMap;)V public static fun receiveTouches (Lcom/facebook/react/uimanager/events/RCTModernEventEmitter;Ljava/lang/String;Lcom/facebook/react/bridge/WritableArray;Lcom/facebook/react/bridge/WritableArray;)V } @@ -5238,6 +5240,7 @@ public final class com/facebook/react/views/image/ImageLoadEvent : com/facebook/ public static final fun createProgressEvent (ILjava/lang/String;II)Lcom/facebook/react/views/image/ImageLoadEvent; public static final fun eventNameForType (I)Ljava/lang/String; public fun getCoalescingKey ()S + public synthetic fun getEventData ()Lcom/facebook/react/bridge/ReadableMap; public fun getEventName ()Ljava/lang/String; } @@ -5924,6 +5927,7 @@ public final class com/facebook/react/views/scroll/ReactScrollViewManager$Compan public final class com/facebook/react/views/scroll/ScrollEvent : com/facebook/react/uimanager/events/Event { public static final field Companion Lcom/facebook/react/views/scroll/ScrollEvent$Companion; public fun canCoalesce ()Z + public synthetic fun getEventData ()Lcom/facebook/react/bridge/ReadableMap; public fun getEventName ()Ljava/lang/String; public static final fun obtain (IILcom/facebook/react/views/scroll/ScrollEventType;FFFFIIII)Lcom/facebook/react/views/scroll/ScrollEvent; public static final fun obtain (ILcom/facebook/react/views/scroll/ScrollEventType;FFFFIIII)Lcom/facebook/react/views/scroll/ScrollEvent; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/EventAnimationDriver.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/EventAnimationDriver.kt index b40e19efa966..431ac82094b5 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/EventAnimationDriver.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/EventAnimationDriver.kt @@ -11,7 +11,6 @@ import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.ReadableType import com.facebook.react.bridge.UnexpectedNativeTypeException -import com.facebook.react.bridge.WritableMap import com.facebook.react.uimanager.events.EventCategoryDef import com.facebook.react.uimanager.events.RCTModernEventEmitter @@ -29,7 +28,7 @@ internal class EventAnimationDriver( eventName: String, canCoalesceEvent: Boolean, customCoalesceKey: Int, - params: WritableMap?, + params: ReadableMap?, @EventCategoryDef category: Int, ) { requireNotNull(params) { "Native animated events must have event data." } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/UIManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/UIManager.kt index 7587d9cc7a89..ac10eaf2de32 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/UIManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/UIManager.kt @@ -25,7 +25,7 @@ public interface UIManager : PerformanceCounter { @ThreadConfined(ThreadConfined.UI) @Deprecated( "This legacy surface API will be removed in a future release. " + - "Use ReactHost.createSurface(...) and ReactSurface.start() instead." + "Use ReactHost.createSurface(...) and ReactSurface.start() instead.", ) public fun addRootView(rootView: T, initialProps: WritableMap?): Int @@ -37,7 +37,7 @@ public interface UIManager : PerformanceCounter { @AnyThread @Deprecated( "This legacy surface API will be removed in a future release. " + - "Use ReactHost.createSurface(...) and ReactSurface.start() instead." + "Use ReactHost.createSurface(...) and ReactSurface.start() instead.", ) public fun startSurface( rootView: T, @@ -55,7 +55,7 @@ public interface UIManager : PerformanceCounter { */ @AnyThread @Deprecated( - "This legacy surface API will be removed in a future release. Use ReactSurface.stop() instead." + "This legacy surface API will be removed in a future release. Use ReactSurface.stop() instead.", ) public fun stopSurface(surfaceId: Int) @@ -156,10 +156,11 @@ public interface UIManager : PerformanceCounter { * * @param reactTag tag * @param eventName name of the event - * @param event parameters + * @param event native-backed event parameters; Fabric consumes the payload during dispatch, so it + * must not be reused afterwards */ @Deprecated("", ReplaceWith("receiveEvent(surfaceId, reactTag, eventName, event)")) - public fun receiveEvent(reactTag: Int, eventName: String, event: WritableMap?) + public fun receiveEvent(reactTag: Int, eventName: String, event: ReadableMap?) /** * This method dispatches events from RN Android code to JS. The delivery of this event will not @@ -168,9 +169,10 @@ public interface UIManager : PerformanceCounter { * @param surfaceId * @param reactTag tag * @param eventName name of the event - * @param event parameters + * @param event native-backed event parameters; Fabric consumes the payload during dispatch, so it + * must not be reused afterwards */ - public fun receiveEvent(surfaceId: Int, reactTag: Int, eventName: String, event: WritableMap?) + public fun receiveEvent(surfaceId: Int, reactTag: Int, eventName: String, event: ReadableMap?) /** Resolves Direct Event name exposed to JS from the one known to the Native side. */ @Deprecated("") public fun resolveCustomDirectEventName(eventName: String): String? diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java index 2727e880d083..4d43b78a3bbc 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java @@ -1180,14 +1180,14 @@ public void updateRootLayoutSpecs( @Deprecated @Override - public void receiveEvent(int reactTag, String eventName, @Nullable WritableMap params) { + public void receiveEvent(int reactTag, String eventName, @Nullable ReadableMap params) { receiveEvent(View.NO_ID, reactTag, eventName, false, params, EventCategoryDef.UNSPECIFIED); } @Deprecated @Override public void receiveEvent( - int surfaceId, int reactTag, String eventName, @Nullable WritableMap params) { + int surfaceId, int reactTag, String eventName, @Nullable ReadableMap params) { receiveEvent(surfaceId, reactTag, eventName, false, params, EventCategoryDef.UNSPECIFIED); } @@ -1212,7 +1212,7 @@ public void receiveEvent( int reactTag, String eventName, boolean canCoalesceEvent, - @Nullable WritableMap params, + @Nullable ReadableMap params, @EventCategoryDef int eventCategory) { receiveEvent( surfaceId, @@ -1248,7 +1248,7 @@ public void receiveEvent( int reactTag, String eventName, boolean canCoalesceEvent, - @Nullable WritableMap params, + @Nullable ReadableMap params, @EventCategoryDef int eventCategory, boolean experimentalIsSynchronous) { receiveEvent( @@ -1284,7 +1284,7 @@ public void receiveEvent( int reactTag, String eventName, boolean canCoalesceEvent, - @Nullable WritableMap params, + @Nullable ReadableMap params, @EventCategoryDef int eventCategory, boolean experimentalIsSynchronous, long eventTimestamp) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/events/EventEmitterWrapper.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/events/EventEmitterWrapper.kt index 4a61bf606ad9..5c662ba71242 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/events/EventEmitterWrapper.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/events/EventEmitterWrapper.kt @@ -11,8 +11,8 @@ import android.annotation.SuppressLint import com.facebook.jni.HybridClassBase import com.facebook.proguard.annotations.DoNotStripAny import com.facebook.react.bridge.NativeMap +import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.UiThreadUtil -import com.facebook.react.bridge.WritableMap import com.facebook.react.fabric.FabricSoLoader.staticInit import com.facebook.react.uimanager.events.EventCategoryDef @@ -46,14 +46,14 @@ internal class EventEmitterWrapper private constructor() : HybridClassBase() { * Invokes the execution of the C++ EventEmitter. * * @param eventName [String] name of the event to execute. - * @param params [WritableMap] payload of the event + * @param params [ReadableMap] payload of the event * @param eventCategory event category * @param eventTimestamp timestamp when the event was triggered (in milliseconds since boot) */ @Synchronized fun dispatch( eventName: String, - params: WritableMap?, + params: ReadableMap?, @EventCategoryDef eventCategory: Int, eventTimestamp: Long, ) { @@ -64,7 +64,7 @@ internal class EventEmitterWrapper private constructor() : HybridClassBase() { } @Synchronized - fun dispatchEventSynchronously(eventName: String, params: WritableMap?, eventTimestamp: Long) { + fun dispatchEventSynchronously(eventName: String, params: ReadableMap?, eventTimestamp: Long) { if (!isValid) { return } @@ -77,11 +77,11 @@ internal class EventEmitterWrapper private constructor() : HybridClassBase() { * target. * * @param eventName [String] name of the event to execute. - * @param params [WritableMap] payload of the event + * @param params [ReadableMap] payload of the event * @param eventTimestamp timestamp when the event was triggered (in milliseconds since boot) */ @Synchronized - fun dispatchUnique(eventName: String, params: WritableMap?, eventTimestamp: Long) { + fun dispatchUnique(eventName: String, params: ReadableMap?, eventTimestamp: Long) { if (!isValid) { return } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/events/FabricEventEmitter.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/events/FabricEventEmitter.kt index dad9cd3d5556..565e64249144 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/events/FabricEventEmitter.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/events/FabricEventEmitter.kt @@ -8,7 +8,7 @@ package com.facebook.react.fabric.events import android.os.SystemClock -import com.facebook.react.bridge.WritableMap +import com.facebook.react.bridge.ReadableMap import com.facebook.react.fabric.FabricUIManager import com.facebook.react.uimanager.events.EventCategoryDef import com.facebook.react.uimanager.events.RCTModernEventEmitter @@ -22,7 +22,7 @@ internal class FabricEventEmitter(private val uiManager: FabricUIManager) : RCTM eventName: String, canCoalesceEvent: Boolean, customCoalesceKey: Int, - params: WritableMap?, + params: ReadableMap?, @EventCategoryDef category: Int, ) { receiveEvent( @@ -43,7 +43,7 @@ internal class FabricEventEmitter(private val uiManager: FabricUIManager) : RCTM eventName: String, canCoalesceEvent: Boolean, customCoalesceKey: Int, - params: WritableMap?, + params: ReadableMap?, @EventCategoryDef category: Int, eventTimestamp: Long, ) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.kt index 72b717d243da..87b3dee12e2f 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.kt @@ -18,7 +18,6 @@ import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.RetryableMountingLayerException import com.facebook.react.bridge.UiThreadUtil.assertOnUiThread -import com.facebook.react.bridge.WritableMap import com.facebook.react.fabric.events.EventEmitterWrapper import com.facebook.react.fabric.mounting.mountitems.MountItem import com.facebook.react.touch.JSResponderHandler @@ -342,7 +341,7 @@ internal class MountingManager( reactTag: Int, eventName: String, canCoalesceEvent: Boolean, - params: WritableMap?, + params: ReadableMap?, @EventCategoryDef eventCategory: Int, eventTimestamp: Long, ) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.kt index 0c8c7bf05502..8053c2a7b0aa 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.kt @@ -1171,7 +1171,7 @@ internal constructor( reactTag: Int, eventName: String, canCoalesceEvent: Boolean, - params: WritableMap?, + params: ReadableMap?, @EventCategoryDef eventCategory: Int, eventTimestamp: Long, ) { @@ -1252,7 +1252,7 @@ internal constructor( private class PendingViewEvent( private val eventName: String, - private val params: WritableMap?, + private val params: ReadableMap?, @field:EventCategoryDef private val eventCategory: Int, private val canCoalesceEvent: Boolean, private val eventTimestamp: Long, diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/interop/InteropEvent.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/interop/InteropEvent.kt index b4008e1fe064..249eb4ea6ee6 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/interop/InteropEvent.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/interop/InteropEvent.kt @@ -7,7 +7,7 @@ package com.facebook.react.internal.interop -import com.facebook.react.bridge.WritableMap +import com.facebook.react.bridge.ReadableMap import com.facebook.react.common.annotations.internal.InteropLegacyArchitecture import com.facebook.react.common.annotations.internal.LegacyArchitectureLogger import com.facebook.react.uimanager.events.Event @@ -20,14 +20,14 @@ import com.facebook.react.uimanager.events.Event @InteropLegacyArchitecture internal class InteropEvent( @get:JvmName("eventName") val interopEventName: String, - @get:JvmName("eventData") val eventData: WritableMap?, + @get:JvmName("eventData") val eventData: ReadableMap?, surfaceId: Int, viewTag: Int, ) : Event(surfaceId, viewTag) { override fun getEventName(): String = interopEventName - override fun getEventData(): WritableMap? = eventData + override fun getEventData(): ReadableMap? = eventData private companion object { init { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/interop/InteropEventEmitter.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/interop/InteropEventEmitter.kt index a4f2aad529aa..a08bbfc19bce 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/interop/InteropEventEmitter.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/interop/InteropEventEmitter.kt @@ -10,7 +10,7 @@ package com.facebook.react.internal.interop import com.facebook.react.bridge.ReactContext -import com.facebook.react.bridge.WritableMap +import com.facebook.react.bridge.ReadableMap import com.facebook.react.common.annotations.VisibleForTesting import com.facebook.react.common.annotations.internal.InteropLegacyArchitecture import com.facebook.react.common.annotations.internal.LegacyArchitectureLogger @@ -34,7 +34,7 @@ internal class InteropEventEmitter(private val reactContext: ReactContext) : RCT private var eventDispatcherOverride: EventDispatcher? = null @Deprecated("Deprecated in Java") - override fun receiveEvent(targetTag: Int, eventName: String, params: WritableMap?) { + override fun receiveEvent(targetTag: Int, eventName: String, params: ReadableMap?) { val dispatcher = eventDispatcherOverride ?: UIManagerHelper.getEventDispatcher(reactContext) val surfaceId = UIManagerHelper.getSurfaceId(reactContext) dispatcher?.dispatchEvent(InteropEvent(eventName, params, surfaceId, targetTag)) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.kt index 245e1a0e29b2..a65277d07edd 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.kt @@ -551,11 +551,11 @@ public open class UIManagerModule : ReactContextBaseJavaModule, LifecycleEventLi return null } - override fun receiveEvent(reactTag: Int, eventName: String, event: WritableMap?) { + override fun receiveEvent(reactTag: Int, eventName: String, event: ReadableMap?) { receiveEvent(-1, reactTag, eventName, event) } - override fun receiveEvent(surfaceId: Int, reactTag: Int, eventName: String, event: WritableMap?) { + override fun receiveEvent(surfaceId: Int, reactTag: Int, eventName: String, event: ReadableMap?) { assert(false) } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/Event.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/Event.kt index 0b351afa723a..24e1bd3eaea7 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/Event.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/Event.kt @@ -9,7 +9,7 @@ package com.facebook.react.uimanager.events -import com.facebook.react.bridge.WritableMap +import com.facebook.react.bridge.ReadableMap import com.facebook.react.common.SystemClock.uptimeMillis /** @@ -152,13 +152,13 @@ public abstract class Event> { } /** Can be overridden by classes when no custom logic for dispatching is needed. */ - protected open fun getEventData(): WritableMap? = null + protected open fun getEventData(): ReadableMap? = null /** * NOTE: This is a transitional method that allows accessing event data from outside the * sublcasses, but inside the package. */ - internal fun internal_getEventData(): WritableMap? = getEventData() + internal fun internal_getEventData(): ReadableMap? = getEventData() protected open fun getEventCategory(): Int = EventCategoryDef.UNSPECIFIED diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/RCTEventEmitter.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/RCTEventEmitter.kt index edfd77430ed3..aa970ee2ccbe 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/RCTEventEmitter.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/RCTEventEmitter.kt @@ -9,8 +9,8 @@ package com.facebook.react.uimanager.events import com.facebook.proguard.annotations.DoNotStripAny import com.facebook.react.bridge.JavaScriptModule +import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.WritableArray -import com.facebook.react.bridge.WritableMap import com.facebook.react.common.annotations.internal.InteropLegacyArchitecture /** @@ -28,7 +28,7 @@ public interface RCTEventEmitter : JavaScriptModule { * @param params event params */ @Deprecated("Use [RCTModernEventEmitter.receiveEvent] instead") - public fun receiveEvent(targetTag: Int, eventName: String, params: WritableMap?) + public fun receiveEvent(targetTag: Int, eventName: String, params: ReadableMap?) @Deprecated("Use [EventDispatcher] to dispatch touch events instead") public fun receiveTouches( diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/RCTModernEventEmitter.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/RCTModernEventEmitter.kt index 2720516397b7..628b5454208a 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/RCTModernEventEmitter.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/RCTModernEventEmitter.kt @@ -7,7 +7,7 @@ package com.facebook.react.uimanager.events -import com.facebook.react.bridge.WritableMap +import com.facebook.react.bridge.ReadableMap /** * This is a transitional replacement for [RCTEventEmitter] that works with Fabric and non-Fabric @@ -17,6 +17,9 @@ import com.facebook.react.bridge.WritableMap * This interface will *also* be deleted in the distant future and be replaced with a new interface * that doesn't need the old `receiveEvent` method at all. But for the foreseeable future, this is * the recommended interface to use for EventEmitters. + * + * Fabric requires native-backed event payloads and consumes them during dispatch. Callers must not + * reuse a payload after passing it to [receiveEvent]. */ @Suppress("DEPRECATION") public interface RCTModernEventEmitter : RCTEventEmitter { @@ -24,12 +27,12 @@ public interface RCTModernEventEmitter : RCTEventEmitter { "Use RCTModernEventEmitter", ReplaceWith("RCTModernEventEmitter.receiveEvent(surfaceId, targetTag, eventName, params)"), ) - override fun receiveEvent(targetTag: Int, eventName: String, params: WritableMap?) { + override fun receiveEvent(targetTag: Int, eventName: String, params: ReadableMap?) { receiveEvent(-1, targetTag, eventName, params) } @Deprecated("Use the overload with eventTimestamp parameter instead.") - public fun receiveEvent(surfaceId: Int, targetTag: Int, eventName: String, params: WritableMap?) { + public fun receiveEvent(surfaceId: Int, targetTag: Int, eventName: String, params: ReadableMap?) { // We assume this event can't be coalesced. `customCoalesceKey` has no meaning in Fabric. receiveEvent(surfaceId, targetTag, eventName, false, 0, params, EventCategoryDef.UNSPECIFIED) } @@ -41,7 +44,7 @@ public interface RCTModernEventEmitter : RCTEventEmitter { eventName: String, canCoalesceEvent: Boolean, customCoalesceKey: Int, - params: WritableMap?, + params: ReadableMap?, @EventCategoryDef category: Int, ) @@ -58,7 +61,7 @@ public interface RCTModernEventEmitter : RCTEventEmitter { eventName: String, canCoalesceEvent: Boolean, customCoalesceKey: Int, - params: WritableMap?, + params: ReadableMap?, @EventCategoryDef category: Int, eventTimestamp: Long, ) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/SynchronousEventReceiver.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/SynchronousEventReceiver.kt index 7d6cb6c429bf..27d8dcf20e20 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/SynchronousEventReceiver.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/SynchronousEventReceiver.kt @@ -7,7 +7,7 @@ package com.facebook.react.uimanager.events -import com.facebook.react.bridge.WritableMap +import com.facebook.react.bridge.ReadableMap import com.facebook.react.common.annotations.UnstableReactNativeAPI @UnstableReactNativeAPI @@ -17,7 +17,7 @@ internal interface SynchronousEventReceiver { reactTag: Int, eventName: String, canCoalesceEvent: Boolean, - params: WritableMap?, + params: ReadableMap?, @EventCategoryDef eventCategory: Int, experimentalIsSynchronous: Boolean, ) @@ -34,7 +34,7 @@ internal interface SynchronousEventReceiver { reactTag: Int, eventName: String, canCoalesceEvent: Boolean, - params: WritableMap?, + params: ReadableMap?, @EventCategoryDef eventCategory: Int, experimentalIsSynchronous: Boolean, eventTimestamp: Long, diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchesHelper.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchesHelper.kt index 8e30d686e41f..dd952343b35c 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchesHelper.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchesHelper.kt @@ -9,7 +9,7 @@ package com.facebook.react.uimanager.events import com.facebook.proguard.annotations.DoNotStrip import com.facebook.react.bridge.ReactNativeJNISoLoader -import com.facebook.react.bridge.WritableNativeMap +import com.facebook.react.bridge.ReadableMap import com.facebook.react.uimanager.PixelUtil.pxToDp import com.facebook.systrace.Systrace @@ -167,5 +167,5 @@ internal object TouchesHelper { eventPointer: Int, changedPointers: Int, touchPointers: Int, - ): WritableNativeMap + ): ReadableMap } diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/jni/TouchesHelper.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/jni/TouchesHelper.cpp index 6696c93d0bb8..ab23fd8f3d76 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/jni/TouchesHelper.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/jni/TouchesHelper.cpp @@ -8,9 +8,9 @@ #include "TouchesHelper.h" #include +#include -#include "WritableNativeArray.h" -#include "WritableNativeMap.h" +#include "ReadableNativeMap.h" using namespace facebook::jni; @@ -44,7 +44,7 @@ folly::dynamic createTouch(const double* fields) { return touch; } -local_ref createTouchPayload( +local_ref createTouchPayload( alias_ref /*unused*/, alias_ref pointerData, jint eventPointer, @@ -72,25 +72,15 @@ local_ref createTouchPayload( } } - auto jPayload = WritableNativeMap::newObjectJavaArgs(); - auto* payload = jPayload->cthis(); - // NOLINTBEGIN(cppcoreguidelines-pro-bounds-pointer-arithmetic) - const double* fields = values.get() + eventPointer * kPointerFields; - payload->putDouble("pageX", fields[kPageXIndex]); - payload->putDouble("pageY", fields[kPageYIndex]); - payload->putDouble("locationX", fields[kLocationXIndex]); - payload->putDouble("locationY", fields[kLocationYIndex]); - payload->putInt( - "targetSurface", static_cast(fields[kTargetSurfaceIndex])); - payload->putInt("target", static_cast(fields[kTargetIndex])); - payload->putDouble("timestamp", fields[kTimestampIndex]); - payload->putDouble("identifier", fields[kIdentifierIndex]); - // NOLINTEND(cppcoreguidelines-pro-bounds-pointer-arithmetic) - WritableNativeArray changedTouchesArray(std::move(changedTouches)); - payload->putNativeArray("changedTouches", &changedTouchesArray); - WritableNativeArray touchesArray(std::move(touches)); - payload->putNativeArray("touches", &touchesArray); - return jPayload; + // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) + auto payload = createTouch(values.get() + eventPointer * kPointerFields); + payload.insert("changedTouches", std::move(changedTouches)); + payload.insert("touches", std::move(touches)); + // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast) + return adopt_local( + reinterpret_cast( + ReadableNativeMap::createWithContents(std::move(payload)).release())); + // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast) } } // namespace diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/bridge/interop/InteropModuleRegistryTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/bridge/interop/InteropModuleRegistryTest.kt index 5e51cda15d0d..ab759f59b311 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/bridge/interop/InteropModuleRegistryTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/bridge/interop/InteropModuleRegistryTest.kt @@ -11,7 +11,7 @@ package com.facebook.react.bridge.interop import com.facebook.react.bridge.JavaScriptModule -import com.facebook.react.bridge.WritableMap +import com.facebook.react.bridge.ReadableMap import com.facebook.react.common.annotations.UnstableReactNativeAPI import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsDefaults @@ -32,7 +32,7 @@ class InteropModuleRegistryTest { @Suppress("DEPRECATION") private class FakeRCTEventEmitter : RCTEventEmitter { @Deprecated("Use [RCTModernEventEmitter.receiveEvent] instead") - override fun receiveEvent(targetTag: Int, eventName: String, params: WritableMap?) = Unit + override fun receiveEvent(targetTag: Int, eventName: String, params: ReadableMap?) = Unit } @Before diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/events/TouchEventDispatchTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/events/TouchEventDispatchTest.kt index 55cef1f4abf0..20a9e9d9b332 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/events/TouchEventDispatchTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/events/TouchEventDispatchTest.kt @@ -15,6 +15,7 @@ import com.facebook.react.bridge.JavaOnlyArray import com.facebook.react.bridge.JavaOnlyMap import com.facebook.react.bridge.ReactTestHelper import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.ReadableNativeMap import com.facebook.react.bridge.WritableMap import com.facebook.react.bridge.queue.MessageQueueThread import com.facebook.react.bridge.queue.ReactQueueConfiguration @@ -29,9 +30,9 @@ import com.facebook.react.uimanager.events.TouchEventType import com.facebook.testutils.shadows.ShadowArguments import com.facebook.testutils.shadows.ShadowNativeLoader import com.facebook.testutils.shadows.ShadowNativeMap +import com.facebook.testutils.shadows.ShadowReadableNativeMap import com.facebook.testutils.shadows.ShadowSoLoader import com.facebook.testutils.shadows.ShadowTouchesHelper -import com.facebook.testutils.shadows.ShadowWritableNativeMap import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.Before @@ -54,9 +55,9 @@ import org.robolectric.shadow.api.Shadow ShadowSoLoader::class, ShadowNativeLoader::class, ShadowArguments::class, - ShadowWritableNativeMap::class, + ShadowReadableNativeMap::class, ShadowTouchesHelper::class, - ] + ], ) class TouchEventDispatchTest { private val touchEventCoalescingKeyHelper = TouchEventCoalescingKeyHelper() @@ -560,7 +561,7 @@ class TouchEventDispatchTest { for (event in startMoveEndSequence) { eventDispatcher.dispatchEvent(event) } - val argument = ArgumentCaptor.forClass(WritableMap::class.java) + val argument = ArgumentCaptor.forClass(ReadableMap::class.java) verify(eventEmitter, times(4)) .receiveEvent( anyInt(), @@ -580,7 +581,7 @@ class TouchEventDispatchTest { for (event in startMoveCancelSequence) { eventDispatcher.dispatchEvent(event) } - val argument = ArgumentCaptor.forClass(WritableMap::class.java) + val argument = ArgumentCaptor.forClass(ReadableMap::class.java) verify(eventEmitter, times(6)) .receiveEvent( anyInt(), @@ -600,7 +601,7 @@ class TouchEventDispatchTest { for (event in startPointerMoveUpSequence) { eventDispatcher.dispatchEvent(event) } - val argument = ArgumentCaptor.forClass(WritableMap::class.java) + val argument = ArgumentCaptor.forClass(ReadableMap::class.java) verify(eventEmitter, times(6)) .receiveEvent( anyInt(), @@ -711,8 +712,12 @@ class TouchEventDispatchTest { } /** The contents of the captured payloads, which are shadowed native maps. */ -private fun ArgumentCaptor.payloads(): List = - allValues.map { Shadow.extract(it).backingMap } +private fun ArgumentCaptor.payloads(): List = + allValues.map { payload -> + assertThat(payload).isInstanceOf(ReadableNativeMap::class.java) + assertThat(payload).isNotInstanceOf(WritableMap::class.java) + Shadow.extract(payload).backingMap + } private fun IntArray.toPointerProperties(): Array = this.map { PointerProperties().apply { id = it } }.toTypedArray() diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/testutils/fakes/FakeUIManager.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/testutils/fakes/FakeUIManager.kt index e1e708edcd1b..0631cc864f07 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/testutils/fakes/FakeUIManager.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/testutils/fakes/FakeUIManager.kt @@ -93,11 +93,11 @@ class FakeUIManager : UIManager, UIBlockViewResolver { } @Deprecated("") - override fun receiveEvent(reactTag: Int, eventName: String, event: WritableMap?) { + override fun receiveEvent(reactTag: Int, eventName: String, event: ReadableMap?) { error("Not yet implemented") } - override fun receiveEvent(surfaceId: Int, reactTag: Int, eventName: String, event: WritableMap?) { + override fun receiveEvent(surfaceId: Int, reactTag: Int, eventName: String, event: ReadableMap?) { error("Not yet implemented") } diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/testutils/shadows/ShadowTouchesHelper.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/testutils/shadows/ShadowTouchesHelper.kt index 588f3787a331..f35f09e0b494 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/testutils/shadows/ShadowTouchesHelper.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/testutils/shadows/ShadowTouchesHelper.kt @@ -9,11 +9,13 @@ package com.facebook.testutils.shadows import com.facebook.react.bridge.JavaOnlyArray import com.facebook.react.bridge.JavaOnlyMap +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.ReadableNativeMap import com.facebook.react.bridge.WritableMap -import com.facebook.react.bridge.WritableNativeMap import com.facebook.react.uimanager.events.TouchesHelper import org.robolectric.annotation.Implementation import org.robolectric.annotation.Implements +import org.robolectric.shadow.api.Shadow @Implements(TouchesHelper::class) class ShadowTouchesHelper { @@ -28,7 +30,7 @@ class ShadowTouchesHelper { eventPointer: Int, changedPointers: Int, touchPointers: Int, - ): WritableNativeMap { + ): ReadableMap { val pointerCount = pointerData.size / TouchesHelper.POINTER_FIELDS val changedTouches = JavaOnlyArray() val touches = JavaOnlyArray() @@ -40,11 +42,13 @@ class ShadowTouchesHelper { touches.pushMap(JavaOnlyMap().also { putTouch(it, pointerData, index) }) } } - val payload = WritableNativeMap() + val payload = JavaOnlyMap() putTouch(payload, pointerData, eventPointer) payload.putArray("changedTouches", changedTouches) payload.putArray("touches", touches) - return payload + val nativeMap = Shadow.newInstanceOf(ReadableNativeMap::class.java) + Shadow.extract(nativeMap).backingMap = payload + return nativeMap } private fun putTouch(touch: WritableMap, pointerData: DoubleArray, index: Int) {