@@ -20,11 +20,9 @@ import com.facebook.react.common.ReactConstants
2020import java.util.concurrent.ConcurrentHashMap
2121
2222/* *
23- * Runs [block] on the UI thread, inline if already there.
24- *
25- * [ActivityResultRegistry] is `@MainThread` and its key tables are unsynchronized. Nothing enforces
26- * that at runtime, so an off-thread call corrupts them silently rather than throwing -- and RN
27- * registers on the JS thread and launches on the native-modules thread.
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.
2826 */
2927internal fun onUiThread (block : () -> Unit ) {
3028 if (UiThreadUtil .isOnUiThread()) block() else UiThreadUtil .runOnUiThread(block)
@@ -33,36 +31,25 @@ internal fun onUiThread(block: () -> Unit) {
3331/* *
3432 * Default [ReactActivityResultCaller], owned by a [ReactContext].
3533 *
36- * Registrations are accepted at any time -- native modules are created lazily, typically well after
37- * the host Activity has resumed -- and bound to the current Activity's [ActivityResultRegistry]
38- * either immediately (when an Activity is already available) or on the next `onHostResume`.
39- * Registrations outlive any single Activity: keys stay stable, so AndroidX can re-associate a result
40- * that arrives after Activity recreation.
41- *
42- * ## Which registry a launcher is bound to
43- *
44- * Every `onHostResume` reconciles each launcher against the *current* registry, rebinding it if it
45- * is attached to a different one. It deliberately does not stop at "already bound to something":
46- * with multi-Activity navigation the new Activity resumes before the old one is destroyed, and
47- * `ReactHostImpl.onHostDestroy(activity)` drops the old Activity's destroy entirely once
48- * `currentActivity` has moved on. A launcher that only checked "am I bound?" would stay attached to
49- * the previous Activity's dead registry -- leaking it, and misrouting anything launched from the new
50- * screen.
34+ * Registrations are accepted at any time and bound to the current Activity's
35+ * [ActivityResultRegistry] immediately or on the next `onHostResume`. They outlive any single
36+ * Activity: keys stay stable so AndroidX can re-associate a result after Activity recreation.
5137 *
52- * ## Threading
38+ * Every `onHostResume` checks each launcher against the *current* registry, not just "already
39+ * bound to something": with multi-Activity navigation the new Activity resumes before the old one
40+ * is destroyed (whose onHostDestroy is dropped once `currentActivity` moves on), so a bound-only
41+ * check would leave launchers attached to the previous Activity's dead registry.
5342 *
54- * [entries] is concurrent and reachable from any thread. Everything that touches
55- * [ActivityResultRegistry] goes through [onUiThread].
56- *
57- * Registration itself stays on the caller's thread, so the launcher is returned immediately and a
58- * duplicate key throws from the frame that caused it. Only the registry call is hopped.
43+ * 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
46+ * moves to the UI thread.
5947 */
6048internal class ReactActivityResultCallerImpl (private val reactContext : ReactContext ) :
6149 ReactActivityResultCaller , LifecycleEventListener {
6250
6351 private class Entry <I , O >(
6452 val key : String ,
65- val registrantDescription : String ,
6653 private val contract : ActivityResultContract <I , O >,
6754 private val callback : ActivityResultCallback <O >,
6855 val launcher : DeferredActivityResultLauncher <I >,
@@ -73,8 +60,8 @@ internal class ReactActivityResultCallerImpl(private val reactContext: ReactCont
7360 */
7461 fun bindTo (registry : ActivityResultRegistry ) {
7562 if (launcher.isBoundTo(registry)) return
76- // Release the previous host's registry first: it may already be dead, and leaving the
77- // callback registered there leaks that Activity and misroutes anything launched from it .
63+ // Release any previous (possibly dead) registry first; staying registered there leaks its
64+ // Activity and sends launches to the wrong one .
7865 launcher.unbind()
7966 launcher.bind(registry, registry.register(key, contract, callback))
8067 }
@@ -86,17 +73,13 @@ internal class ReactActivityResultCallerImpl(private val reactContext: ReactCont
8673 reactContext.addLifecycleEventListener(this )
8774 }
8875
89- private fun getOwnerId (owner : Any ): String = owner.javaClass.name
90-
9176 override fun <I , O > registerForActivityResult (
9277 owner : Any ,
9378 contract : ActivityResultContract <I , O >,
9479 callback : ActivityResultCallback <O >,
9580 ): ActivityResultLauncher <I > {
96- val id = getOwnerId(owner)
9781 return register(
98- key = " $id :${contract.javaClass.name} " ,
99- registrantDescription = id,
82+ key = " ${owner.javaClass.name} :${contract.javaClass.name} " ,
10083 collisionHint =
10184 " Register once and reuse the launcher, or pass a distinct key per launcher: " +
10285 " registerForActivityResult(owner, \" someName\" , contract, callback)." ,
@@ -110,28 +93,24 @@ internal class ReactActivityResultCallerImpl(private val reactContext: ReactCont
11093 contract : ActivityResultContract <I , O >,
11194 callback : ActivityResultCallback <O >,
11295 ): ActivityResultLauncher <I > {
113- val id = getOwnerId(owner)
11496 return register(
115- key = " $id :${contract.javaClass.name} :$key " ,
116- registrantDescription = id,
97+ key = " ${owner.javaClass.name} :${contract.javaClass.name} :$key " ,
11798 collisionHint = " Pass a key that is unique among this owner's launchers of this contract." ,
11899 contract = contract,
119100 callback = callback)
120101 }
121102
122103 private fun <I , O > register (
123104 key : String ,
124- registrantDescription : String ,
125105 collisionHint : String ,
126106 contract : ActivityResultContract <I , O >,
127107 callback : ActivityResultCallback <O >,
128108 ): ActivityResultLauncher <I > {
129109 val launcher = DeferredActivityResultLauncher (key, contract) { entries.remove(key) }
130- val entry = Entry (key, registrantDescription, contract, callback, launcher)
131- entries.putIfAbsent(key, entry)?. let { existing ->
110+ val entry = Entry (key, contract, callback, launcher)
111+ if ( entries.putIfAbsent(key, entry) != null ) {
132112 throw IllegalStateException (
133- " ${existing.registrantDescription} already registered a launcher for key '$key '. " +
134- collisionHint)
113+ " A launcher is already registered for key '$key '. $collisionHint " )
135114 }
136115 onUiThread { currentRegistry()?.let { registry -> entry.bindTo(registry) } }
137116 return launcher
@@ -145,9 +124,8 @@ internal class ReactActivityResultCallerImpl(private val reactContext: ReactCont
145124 override fun onHostPause (): Unit = Unit
146125
147126 override fun onHostDestroy () = onUiThread {
148- // Detach from the dying registry but keep the registrations: they rebind against the next host's
149- // registry under the same keys on the next onHostResume, which is how AndroidX re-associates a
150- // result that outlives the Activity.
127+ // Detach from the dying registry but keep the registrations: they rebind under the same keys
128+ // on the next onHostResume, which is how AndroidX re-associates a surviving result.
151129 entries.values.forEach { it.launcher.unbind() }
152130 }
153131
0 commit comments