From df1652a47d3810263f4f3b6616d48ba8dcb2e878 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Wed, 14 Oct 2026 05:20:09 +0300 Subject: [PATCH] Make getInitialURL() answer the link that relaunches a reclaimed Android app When Android ends a backgrounded app's process but keeps its task, a link relaunches the app into that task: Android recreates the activity from the task's first intent and hands the link to onNewIntent before React Native can emit it as a url event. getIntent() kept answering the task's first intent, so Linking.getInitialURL() returned the first link and the new one never reached JavaScript. ReactActivity.onNewIntent now calls setIntent(intent) first, so getIntent(), and getInitialURL() through it, answer the newest link. --- .../com/facebook/react/ReactActivity.java | 3 + .../com/facebook/react/ReactActivityTest.kt | 84 +++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 packages/react-native/ReactAndroid/src/test/java/com/facebook/react/ReactActivityTest.kt diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java index b375d1859dbc..3b5ca546ef37 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java @@ -132,6 +132,9 @@ public void invokeDefaultOnBackPressed() { @Override public void onNewIntent(Intent intent) { + // Linking.getInitialURL() reads getIntent(), and a link that relaunches a reclaimed task + // arrives here before React Native can emit it as a url event. + setIntent(intent); if (!mDelegate.onNewIntent(intent)) { super.onNewIntent(intent); } diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/ReactActivityTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/ReactActivityTest.kt new file mode 100644 index 000000000000..967eb35d64a6 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/ReactActivityTest.kt @@ -0,0 +1,84 @@ +/* + * 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.react + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.modules.intent.IntentModule +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.android.controller.ActivityController + +@RunWith(RobolectricTestRunner::class) +class ReactActivityTest { + + @Test + fun onNewIntent_makesTheDeliveredIntentTheActivityIntent() { + val controller = createActivity(FIRST_LINK) + val link = viewIntent(SECOND_LINK) + + controller.newIntent(link) + + assertThat(controller.get().intent).isSameAs(link) + } + + @Test + fun getInitialURL_answersTheLinkDeliveredAfterLaunch() { + val controller = createActivity(FIRST_LINK) + controller.newIntent(viewIntent(SECOND_LINK)) + + verify(requestInitialURL(controller.get())).resolve(SECOND_LINK) + } + + @Test + fun getInitialURL_answersTheLaunchLinkWhenNoOtherArrives() { + val controller = createActivity(FIRST_LINK) + + verify(requestInitialURL(controller.get())).resolve(FIRST_LINK) + } + + private fun createActivity(link: String): ActivityController { + val controller = Robolectric.buildActivity(TestReactActivity::class.java, viewIntent(link)) + controller.get().setTheme(R.style.Theme_ReactNative_AppCompat_Light) + return controller.create() + } + + private fun requestInitialURL(activity: ReactActivity): Promise { + val context = mock() + whenever(context.currentActivity).thenReturn(activity) + val promise = mock() + IntentModule(context).getInitialURL(promise) + return promise + } + + private fun viewIntent(link: String): Intent = Intent(Intent.ACTION_VIEW, Uri.parse(link)) + + /** Hands every new intent to a React host that is not ready yet, as a relaunch does. */ + class TestReactActivity : ReactActivity() { + override fun createReactActivityDelegate(): ReactActivityDelegate = + object : ReactActivityDelegate(this, "Test") { + override fun onCreate(savedInstanceState: Bundle?) = Unit + + override fun onNewIntent(intent: Intent?): Boolean = true + } + } + + private companion object { + const val FIRST_LINK = "rntester://first" + const val SECOND_LINK = "rntester://second" + } +}