From 0405099b4ffabfb8eace7ce16d178b92c9e30b4d Mon Sep 17 00:00:00 2001 From: Sorin Cioban Date: Thu, 29 Oct 2026 13:34:52 +0000 Subject: [PATCH] Defer activity results through ReactHost tasks --- .../facebook/react/runtime/ReactHostImpl.kt | 35 ++++++-- .../facebook/react/runtime/ReactHostTest.kt | 85 +++++++++++++++++++ 2 files changed, 115 insertions(+), 5 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt index f54557bf690..1c59ffe5836 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt @@ -660,11 +660,10 @@ public class ReactHostImpl( val method = "onActivityResult(activity = \"$activity\", requestCode = \"$requestCode\", resultCode = \"$resultCode\", data = \"$data\")" - val currentContext = currentReactContext - if (currentContext != null) { - currentContext.onActivityResult(activity, requestCode, resultCode, data) - } else { - raiseSoftException(method, "Tried to access onActivityResult while context is not ready") + callWithExistingReactContext(method) { reactContext -> + if (!activity.isDestroyed) { + reactContext.onActivityResult(activity, requestCode, resultCode, data) + } } } @@ -930,6 +929,32 @@ public class ReactHostImpl( executor, ) + /** Schedule UI work after an already requested ReactInstance has finished initializing. */ + private fun callWithExistingReactContext( + callingMethod: String, + runnable: (reactContext: ReactContext) -> Unit, + ): Task = + // Read the task on the host executor, after any previously scheduled start work. + Task.call({ createReactInstanceTaskRef.get() }, bgExecutor) + .onSuccess( + { instanceTask -> + val reactContext = currentReactContext + if (instanceTask.getResult() == null || reactContext == null) { + raiseSoftException( + "callWithExistingReactContext($callingMethod)", + "Execute: ReactContext is null. Dropping work.", + ) + false + } else if (instanceTask.getResult() !== reactInstance) { + false + } else { + runnable(reactContext) + true + } + }, + uiExecutor, + ) + /** Create a ReactInstance if it doesn't exist already, and schedule work on it. */ private fun callAfterGetOrCreateReactInstance( callingMethod: String, diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/runtime/ReactHostTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/runtime/ReactHostTest.kt index e36969e4d30..2c7d7d188aa 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/runtime/ReactHostTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/runtime/ReactHostTest.kt @@ -8,6 +8,7 @@ package com.facebook.react.runtime import android.app.Activity +import android.content.Intent import com.facebook.react.MemoryPressureRouter import com.facebook.react.bridge.UIManager import com.facebook.react.common.LifecycleState @@ -31,6 +32,7 @@ import org.mockito.kotlin.any import org.mockito.kotlin.doNothing import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.spy import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -143,6 +145,89 @@ class ReactHostTest { assertThat(reactHost.currentReactContext).isNull() } + @Test + fun testActivityResultWithInitializedContext() { + reactHost.start() + val intent = Intent("test-result") + + reactHost.onActivityResult(activityController.get(), 7, Activity.RESULT_OK, intent) + + verify(checkNotNull(reactHost.currentReactContext)) + .onActivityResult(activityController.get(), 7, Activity.RESULT_OK, intent) + } + + @Test + fun testActivityResultsWaitForScheduledInitialization() { + val bgTasks = ArrayDeque() + reactHost = + spy( + ReactHostImpl( + activityController.get().application, + reactHostDelegate, + mock(), + { bgTasks.add(it) }, + Task.IMMEDIATE_EXECUTOR, + false /* allowPackagerServerAccess */, + false /* useDevSupport */, + ), + ) + doReturn(null).whenever(reactHost).getOrCreateReactHostInspectorTarget() + doNothing().whenever(reactHost).unregisterInstanceFromInspector(any()) + + reactHost.start() + val intent = Intent("test-result") + reactHost.onActivityResult(activityController.get(), 7, Activity.RESULT_OK, intent) + reactHost.onActivityResult(activityController.get(), 8, Activity.RESULT_CANCELED, null) + assertThat(reactHost.currentReactContext).isNull() + + while (bgTasks.isNotEmpty()) bgTasks.removeFirst().run() + + val reactContext = checkNotNull(reactHost.currentReactContext) + val ordered = org.mockito.Mockito.inOrder(reactContext) + ordered + .verify(reactContext) + .onActivityResult(activityController.get(), 7, Activity.RESULT_OK, intent) + ordered + .verify(reactContext) + .onActivityResult(activityController.get(), 8, Activity.RESULT_CANCELED, null) + } + + @Test + fun testActivityResultDoesNotStartAnUnusedHost() { + reactHost.onActivityResult(activityController.get(), 7, Activity.RESULT_OK, null) + + assertThat(reactHost.currentReactContext).isNull() + assertThat(mockedReactInstanceCtor.constructed()).isEmpty() + } + + @Test + fun testActivityResultIsNotDeliveredToDestroyedActivity() { + val bgTasks = ArrayDeque() + reactHost = + spy( + ReactHostImpl( + activityController.get().application, + reactHostDelegate, + mock(), + { bgTasks.add(it) }, + Task.IMMEDIATE_EXECUTOR, + false /* allowPackagerServerAccess */, + false /* useDevSupport */, + ), + ) + doReturn(null).whenever(reactHost).getOrCreateReactHostInspectorTarget() + doNothing().whenever(reactHost).unregisterInstanceFromInspector(any()) + + val activity = activityController.get() + reactHost.start() + reactHost.onActivityResult(activity, 7, Activity.RESULT_OK, null) + activityController.pause().stop().destroy() + while (bgTasks.isNotEmpty()) bgTasks.removeFirst().run() + + verify(checkNotNull(reactHost.currentReactContext), never()) + .onActivityResult(activity, 7, Activity.RESULT_OK, null) + } + @Test fun testReload() { reactHost.start()