Skip to content

Commit 3dc8d96

Browse files
Abbondanzofacebook-github-bot
authored andcommitted
Isolate Activity Result support in its own Buck target
Summary: Move Android Activity Result support out of the bridge target's aggregate source list and into a dedicated library. Reuse the bridge's lifecycle interface and inject an `ActivityResultRegistryProvider` plus an exception callback into the implementation. `ReactContext` owns registry discovery and lifecycle-listener registration, so the new target's only React bridge dependency is the bridge interfaces target. Remove the individual source export shims, make the bridge depend on the new target, and give Activity Result tests direct friend access to the implementation target. Changelog: [Internal] - Isolate Android Activity Result support in its own build target Differential Revision: D121859455
1 parent 3b75504 commit 3dc8d96

6 files changed

Lines changed: 103 additions & 61 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.activityresult
9+
10+
import androidx.activity.result.ActivityResultRegistry
11+
12+
internal fun interface ActivityResultRegistryProvider {
13+
fun getActivityResultRegistry(): ActivityResultRegistry?
14+
}

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/activityresult/DeferredActivityResultLauncher.kt‎

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import androidx.activity.result.ActivityResultRegistry
1212
import androidx.activity.result.contract.ActivityResultContract
1313
import androidx.core.app.ActivityOptionsCompat
1414
import com.facebook.common.logging.FLog
15-
import com.facebook.react.bridge.UiThreadUtil
1615
import com.facebook.react.common.ReactConstants
1716

1817
/**
@@ -21,8 +20,9 @@ import com.facebook.react.common.ReactConstants
2120
* unbound (fired on bind), and can be [unbind]-ed and rebound against a new host's registry.
2221
*
2322
* [delegate] and [pendingLaunch] are only touched on the UI thread; [launch] and [unregister] get
24-
* there via [onUiThread]. [launch] decides between delegating and queueing *on* the UI thread, so a
25-
* concurrent [unbind] cannot leave it pointed at a dead registry.
23+
* there via [ReactActivityResultCallerImpl.UiThread.onUiThread]. [launch] decides between
24+
* delegating and queueing *on* the UI thread, so a concurrent [unbind] cannot leave it pointed at a
25+
* dead registry.
2626
*/
2727
internal class DeferredActivityResultLauncher<I>(
2828
private val key: String,
@@ -38,7 +38,7 @@ internal class DeferredActivityResultLauncher<I>(
3838
private var pendingLaunch: PendingLaunch<I>? = null
3939

4040
override fun launch(input: I, options: ActivityOptionsCompat?) {
41-
onUiThread {
41+
ReactActivityResultCallerImpl.UiThread.onUiThread {
4242
val boundDelegate = delegate
4343
if (boundDelegate != null) {
4444
launchSafely(boundDelegate, input, options)
@@ -58,7 +58,7 @@ internal class DeferredActivityResultLauncher<I>(
5858
override fun unregister() {
5959
// Drop the registration first so nothing rebinds this launcher in the meantime.
6060
onUnregister()
61-
onUiThread {
61+
ReactActivityResultCallerImpl.UiThread.onUiThread {
6262
try {
6363
delegate?.unregister()
6464
} catch (exception: RuntimeException) {
@@ -80,7 +80,6 @@ internal class DeferredActivityResultLauncher<I>(
8080
* queued launch.
8181
*/
8282
fun bind(registry: ActivityResultRegistry, launcher: ActivityResultLauncher<I>) {
83-
UiThreadUtil.assertOnUiThread()
8483
delegate = launcher
8584
boundRegistry = registry
8685
pendingLaunch?.let { pending ->
@@ -112,7 +111,6 @@ internal class DeferredActivityResultLauncher<I>(
112111

113112
/** Detaches from the bound registry, keeping any queued launch for the next [bind]. */
114113
fun unbind() {
115-
UiThreadUtil.assertOnUiThread()
116114
try {
117115
delegate?.unregister()
118116
} catch (exception: RuntimeException) {

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/activityresult/ReactActivityResultCallerImpl.kt‎

Lines changed: 42 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,19 @@
77

88
package com.facebook.react.activityresult
99

10+
import android.os.Handler
11+
import android.os.Looper
1012
import androidx.activity.result.ActivityResultCallback
1113
import androidx.activity.result.ActivityResultLauncher
1214
import androidx.activity.result.ActivityResultRegistry
13-
import androidx.activity.result.ActivityResultRegistryOwner
1415
import androidx.activity.result.contract.ActivityResultContract
1516
import com.facebook.common.logging.FLog
1617
import com.facebook.react.bridge.LifecycleEventListener
17-
import com.facebook.react.bridge.ReactContext
18-
import com.facebook.react.bridge.UiThreadUtil
1918
import com.facebook.react.common.ReactConstants
2019
import java.util.concurrent.ConcurrentHashMap
2120

2221
/**
23-
* Runs [block] on the UI thread, inline if already there. [ActivityResultRegistry] is `@MainThread`
24-
* but not enforced at runtime: an off-thread call corrupts it silently, and RN calls in from the JS
25-
* and native-modules threads.
26-
*/
27-
internal fun onUiThread(block: () -> Unit) {
28-
if (UiThreadUtil.isOnUiThread()) block() else UiThreadUtil.runOnUiThread(block)
29-
}
30-
31-
/**
32-
* Default [ReactActivityResultCaller], owned by a [ReactContext].
22+
* Default [ReactActivityResultCaller], owned by a React context.
3323
*
3424
* Registrations are accepted at any time and bound to the current Activity's
3525
* [ActivityResultRegistry] immediately or on the next `onHostResume`. They outlive any single
@@ -41,12 +31,24 @@ internal fun onUiThread(block: () -> Unit) {
4131
* would leave launchers attached to the previous Activity's dead registry.
4232
*
4333
* Threading: [entries] is concurrent and reachable from any thread; everything touching the
44-
* registry goes through [onUiThread]. Registration stays on the caller's thread so the launcher
45-
* returns immediately and a duplicate key throws at the causing frame. Only the registry call moves
46-
* to the UI thread.
34+
* registry goes through [UiThread.onUiThread]. Registration stays on the caller's thread so the
35+
* launcher returns immediately and a duplicate key throws at the causing frame. Only the registry
36+
* call moves to the UI thread.
4737
*/
48-
internal class ReactActivityResultCallerImpl(private val reactContext: ReactContext) :
49-
ReactActivityResultCaller, LifecycleEventListener {
38+
internal class ReactActivityResultCallerImpl(
39+
private val registryProvider: ActivityResultRegistryProvider,
40+
private val exceptionHandler: ActivityResultCallback<RuntimeException>,
41+
) : ReactActivityResultCaller, LifecycleEventListener {
42+
43+
internal object UiThread {
44+
private val uiHandler by lazy { Handler(Looper.getMainLooper()) }
45+
46+
/** Runs [block] on the UI thread, inline if already there. */
47+
fun onUiThread(block: () -> Unit) {
48+
if (Looper.getMainLooper().thread === Thread.currentThread()) block()
49+
else uiHandler.post(Runnable(block))
50+
}
51+
}
5052

5153
private class Entry<I, O>(
5254
val key: String,
@@ -69,10 +71,6 @@ internal class ReactActivityResultCallerImpl(private val reactContext: ReactCont
6971

7072
private val entries = ConcurrentHashMap<String, Entry<*, *>>()
7173

72-
init {
73-
reactContext.addLifecycleEventListener(this)
74-
}
75-
7674
override fun <I, O> registerForActivityResult(
7775
owner: Any,
7876
contract: ActivityResultContract<I, O>,
@@ -82,7 +80,7 @@ internal class ReactActivityResultCallerImpl(private val reactContext: ReactCont
8280
owner,
8381
contract,
8482
callback,
85-
ActivityResultCallback { exception -> reactContext.handleException(exception) },
83+
ActivityResultCallback { exception -> exceptionHandler.onActivityResult(exception) },
8684
)
8785

8886
override fun <I, O> registerForActivityResult(
@@ -120,7 +118,7 @@ internal class ReactActivityResultCallerImpl(private val reactContext: ReactCont
120118
key,
121119
contract,
122120
callback,
123-
ActivityResultCallback { exception -> reactContext.handleException(exception) },
121+
ActivityResultCallback { exception -> exceptionHandler.onActivityResult(exception) },
124122
)
125123

126124
override fun <I, O> registerForActivityResult(
@@ -164,19 +162,20 @@ internal class ReactActivityResultCallerImpl(private val reactContext: ReactCont
164162
if (entries.putIfAbsent(key, entry) != null) {
165163
throw IllegalStateException("A launcher is already registered for key '$key'. $collisionHint")
166164
}
167-
onUiThread { currentRegistry()?.let { registry -> bindSafely(entry, registry) } }
165+
UiThread.onUiThread { currentRegistry()?.let { registry -> bindSafely(entry, registry) } }
168166
return launcher
169167
}
170168

171-
override fun onHostResume() = onUiThread {
172-
if (entries.isEmpty()) return@onUiThread
173-
val registry = currentRegistry()
174-
if (registry == null) {
175-
entries.values.forEach(::unbindSafely)
176-
return@onUiThread
177-
}
178-
entries.values.forEach { entry -> bindSafely(entry, registry) }
179-
}
169+
override fun onHostResume() =
170+
UiThread.onUiThread {
171+
if (entries.isEmpty()) return@onUiThread
172+
val registry = currentRegistry()
173+
if (registry == null) {
174+
entries.values.forEach(::unbindSafely)
175+
return@onUiThread
176+
}
177+
entries.values.forEach { entry -> bindSafely(entry, registry) }
178+
}
180179

181180
private fun bindSafely(entry: Entry<*, *>, registry: ActivityResultRegistry) {
182181
try {
@@ -204,22 +203,14 @@ internal class ReactActivityResultCallerImpl(private val reactContext: ReactCont
204203

205204
override fun onHostPause(): Unit = Unit
206205

207-
override fun onHostDestroy() = onUiThread {
208-
// Detach from the dying registry but keep the registrations: they rebind under the same keys
209-
// on the next onHostResume, which is how AndroidX re-associates a surviving result.
210-
entries.values.forEach(::unbindSafely)
211-
}
206+
override fun onHostDestroy() =
207+
UiThread.onUiThread {
208+
// Detach from the dying registry but keep the registrations: they rebind under the same
209+
// keys
210+
// on the next onHostResume, which is how AndroidX re-associates a surviving result.
211+
entries.values.forEach(::unbindSafely)
212+
}
212213

213-
private fun currentRegistry(): ActivityResultRegistry? {
214-
val activity = reactContext.currentActivity ?: return null
215-
if (activity !is ActivityResultRegistryOwner) {
216-
FLog.w(
217-
ReactConstants.TAG,
218-
"Current Activity ${activity.javaClass.name} is not an ActivityResultRegistryOwner; " +
219-
"ActivityResultContract launchers will remain unbound.",
220-
)
221-
return null
222-
}
223-
return activity.activityResultRegistry
224-
}
214+
private fun currentRegistry(): ActivityResultRegistry? =
215+
registryProvider.getActivityResultRegistry()
225216
}

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactContext.java‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import android.view.Window;
1919
import androidx.activity.result.ActivityResultCallback;
2020
import androidx.activity.result.ActivityResultLauncher;
21+
import androidx.activity.result.ActivityResultRegistry;
22+
import androidx.activity.result.ActivityResultRegistryOwner;
2123
import androidx.activity.result.contract.ActivityResultContract;
2224
import androidx.annotation.NonNull;
2325
import androidx.annotation.Nullable;
@@ -537,9 +539,28 @@ public boolean startActivityForResult(Intent intent, int code, Bundle bundle) {
537539
return mCurrentActivity.get();
538540
}
539541

542+
private @Nullable ActivityResultRegistry getCurrentActivityResultRegistry() {
543+
Activity activity = getCurrentActivity();
544+
if (activity == null) {
545+
return null;
546+
}
547+
if (!(activity instanceof ActivityResultRegistryOwner)) {
548+
FLog.w(
549+
TAG,
550+
String.format(
551+
"Activity %s lacks ActivityResultRegistryOwner; launchers remain unbound.",
552+
activity.getClass().getName()));
553+
return null;
554+
}
555+
return ((ActivityResultRegistryOwner) activity).getActivityResultRegistry();
556+
}
557+
540558
private synchronized ReactActivityResultCallerImpl getActivityResultCaller() {
541559
if (mActivityResultCaller == null) {
542-
mActivityResultCaller = new ReactActivityResultCallerImpl(this);
560+
mActivityResultCaller =
561+
new ReactActivityResultCallerImpl(
562+
this::getCurrentActivityResultRegistry, this::handleException);
563+
addLifecycleEventListener(mActivityResultCaller);
543564
}
544565
return mActivityResultCaller;
545566
}

‎packages/react-native/ReactAndroid/src/test/java/com/facebook/react/activityresult/ReactActivityResultCallerImplTest.kt‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package com.facebook.react.activityresult
99

1010
import android.app.Activity
1111
import android.os.Bundle
12+
import androidx.activity.result.ActivityResultCallback
1213
import androidx.activity.result.ActivityResultRegistry
1314
import androidx.activity.result.ActivityResultRegistryOwner
1415
import androidx.activity.result.contract.ActivityResultContract
@@ -72,13 +73,21 @@ class ReactActivityResultCallerImplTest {
7273
private val moduleBName = ModuleB::class.java.name
7374
private val getContentName = GetContent::class.java.name
7475

76+
private fun createCaller(context: ReactApplicationContext): ReactActivityResultCallerImpl =
77+
ReactActivityResultCallerImpl(
78+
ActivityResultRegistryProvider {
79+
(context.currentActivity as? ActivityResultRegistryOwner)?.activityResultRegistry
80+
},
81+
ActivityResultCallback { exception -> context.handleException(exception) },
82+
)
83+
7584
@Before
7685
fun setUp() {
7786
val activity = Robolectric.buildActivity(TestActivity::class.java).create().get()
7887
registry = activity.activityResultRegistry as RecordingRegistry
7988
reactContext = mock<ReactApplicationContext>()
8089
whenever(reactContext.currentActivity).thenReturn(activity)
81-
caller = ReactActivityResultCallerImpl(reactContext)
90+
caller = createCaller(reactContext)
8291
}
8392

8493
@Test

‎packages/react-native/ReactAndroid/src/test/java/com/facebook/react/activityresult/ReactActivityResultCallerThreadingTest.kt‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import android.app.Activity
1111
import android.content.ActivityNotFoundException
1212
import android.os.Bundle
1313
import android.os.Looper
14+
import androidx.activity.result.ActivityResultCallback
1415
import androidx.activity.result.ActivityResultLauncher
1516
import androidx.activity.result.ActivityResultRegistry
1617
import androidx.activity.result.ActivityResultRegistryOwner
@@ -90,11 +91,19 @@ class ReactActivityResultCallerThreadingTest {
9091
private val moduleA = ModuleA()
9192
private val expectedKey = "${ModuleA::class.java.name}:${GetContent::class.java.name}"
9293

94+
private fun createCaller(context: ReactApplicationContext): ReactActivityResultCallerImpl =
95+
ReactActivityResultCallerImpl(
96+
ActivityResultRegistryProvider {
97+
(context.currentActivity as? ActivityResultRegistryOwner)?.activityResultRegistry
98+
},
99+
ActivityResultCallback { exception -> context.handleException(exception) },
100+
)
101+
93102
@Before
94103
fun setUp() {
95104
reactContext = mock<ReactApplicationContext>()
96105
registry = resumeNewActivity()
97-
caller = ReactActivityResultCallerImpl(reactContext)
106+
caller = createCaller(reactContext)
98107
}
99108

100109
/** Stands in for a new Activity becoming current, and returns its registry. */

0 commit comments

Comments
 (0)