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 66fa21ae9df3..b9e680714159 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 @@ -150,6 +150,7 @@ public class ReactHostImpl( private val reactInstanceEventListeners: MutableList = CopyOnWriteArrayList() private val beforeDestroyListeners: MutableList<() -> Unit> = CopyOnWriteArrayList() + private val pendingActivityResults: MutableList = CopyOnWriteArrayList() internal var reactHostInspectorTarget: ReactHostInspectorTarget? = null private var frameTimingsObserver: FrameTimingsObserver? = null @@ -667,7 +668,9 @@ public class ReactHostImpl( if (currentContext != null) { currentContext.onActivityResult(activity, requestCode, resultCode, data) } else { - raiseSoftException(method, "Tried to access onActivityResult while context is not ready") + stateTracker.enterState(method, "Queuing activity result until context is ready") + pendingActivityResults.add( + PendingActivityResult(WeakReference(activity), requestCode, resultCode, data)) } } @@ -893,10 +896,33 @@ public class ReactHostImpl( @ThreadConfined(ThreadConfined.UI) private fun moveToHostDestroy(currentContext: ReactContext?) { reactLifecycleStateManager.moveToOnHostDestroy(currentContext) + pendingActivityResults.clear() currentActivity = null frameTimingsObserver?.setCurrentWindow(null) } + private fun replayPendingActivityResults(reactContext: ReactContext) { + val activityResults = pendingActivityResults.toList() + pendingActivityResults.clear() + + for (activityResult in activityResults) { + val activity = activityResult.activity.get() + if (activity != null) { + reactContext.onActivityResult( + activity, + activityResult.requestCode, + activityResult.resultCode, + activityResult.data, + ) + } else { + raiseSoftException( + "replayPendingActivityResults()", + "Dropping queued activity result because activity was garbage collected", + ) + } + } + } + private fun raiseSoftException( callingMethod: String, message: String, @@ -1010,6 +1036,13 @@ public class ReactHostImpl( val isReloading: Boolean, ) + private class PendingActivityResult( + val activity: WeakReference, + val requestCode: Int, + val resultCode: Int, + val data: Intent?, + ) + @ThreadConfined("ReactHost") private fun getOrCreateReactInstanceTask(): Task { val method = "getOrCreateReactInstanceTask()" @@ -1121,6 +1154,8 @@ public class ReactHostImpl( reactLifecycleStateManager.resumeReactContextIfHostResumed(reactContext, currentActivity) } + replayPendingActivityResults(reactContext) + stateTracker.enterState(method, "Executing ReactInstanceEventListeners") for (listener in reactInstanceEventListeners) { listener.onReactContextInitialized(reactContext) 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 75d0e0ee6d6b..941e21d464ab 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 @@ -168,4 +170,29 @@ class ReactHostTest { reactHost.onHostDestroy(activityController.get()) assertThat(reactHost.lifecycleState).isEqualTo(LifecycleState.BEFORE_CREATE) } + + @Test + fun onActivityResult_beforeContextIsReady_replaysAfterStart() { + val activity = activityController.get() + val data = Intent("test.intent") + + reactHost.onActivityResult(activity, 100, Activity.RESULT_OK, data) + reactHost.start() + + val reactContext = mockedBridgelessReactContextCtor.constructed().first() + verify(reactContext).onActivityResult(activity, 100, Activity.RESULT_OK, data) + } + + @Test + fun onActivityResult_beforeContextIsReady_dropsOnHostDestroy() { + val activity = activityController.get() + val data = Intent("test.intent") + + reactHost.onActivityResult(activity, 100, Activity.RESULT_OK, data) + reactHost.onHostDestroy() + reactHost.start() + + val reactContext = mockedBridgelessReactContextCtor.constructed().first() + verify(reactContext, never()).onActivityResult(activity, 100, Activity.RESULT_OK, data) + } }