Skip to content

Commit c007439

Browse files
committed
fix: trim comments + cleanup
1 parent 584c1bd commit c007439

8 files changed

Lines changed: 171 additions & 342 deletions

File tree

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

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,13 @@ import com.facebook.react.bridge.UiThreadUtil
1616
import com.facebook.react.common.ReactConstants
1717

1818
/**
19-
* An [ActivityResultLauncher] handed out before the host Activity's `ActivityResultRegistry` is
20-
* available. It delegates to the real launcher once [bind] is called, and queues a single pending
21-
* [launch] issued while unbound, firing it on bind. [unbind] detaches it when the host Activity is
22-
* destroyed so that [ReactActivityResultCallerImpl] can rebind it against the next host's registry.
19+
* An [ActivityResultLauncher] that may exist before any `ActivityResultRegistry` is available: it
20+
* delegates to the real launcher once [bind] is called, queues a single [launch] issued while
21+
* unbound (fired on bind), and can be [unbind]-ed and rebound against a new host's registry.
2322
*
24-
* [launch] and [unregister] are called off the UI thread but reach `@MainThread` registry methods,
25-
* so both hop. [delegate] and [pendingLaunch] are therefore UI-thread only and need no lock. Note
26-
* [launch] decides bound-vs-queue *inside* the hop: doing it before would let a concurrent [unbind]
27-
* strand the launch on a dead registry.
23+
* [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
25+
* a concurrent [unbind] cannot leave it pointed at a dead registry.
2826
*/
2927
internal class DeferredActivityResultLauncher<I>(
3028
private val key: String,
@@ -58,7 +56,7 @@ internal class DeferredActivityResultLauncher<I>(
5856
}
5957

6058
override fun unregister() {
61-
// Drop the registration first, so nothing rebinds this launcher while the hop is in flight.
59+
// Drop the registration first so nothing rebinds this launcher in the meantime.
6260
onUnregister()
6361
onUiThread {
6462
delegate?.unregister()
@@ -68,8 +66,8 @@ internal class DeferredActivityResultLauncher<I>(
6866
}
6967

7068
/**
71-
* Attaches [launcher], obtained from [registry], and fires any launch queued while unbound.
72-
* [registry] is remembered so [isBoundTo] can tell whether a later host is a different one.
69+
* Attaches [launcher], obtained from [registry] (remembered for [isBoundTo]), and fires any
70+
* queued launch.
7371
*/
7472
fun bind(registry: ActivityResultRegistry, launcher: ActivityResultLauncher<I>) {
7573
UiThreadUtil.assertOnUiThread()
@@ -89,6 +87,6 @@ internal class DeferredActivityResultLauncher<I>(
8987
boundRegistry = null
9088
}
9189

92-
/** Whether this launcher is already bound to [registry] specifically -- not merely to something. */
90+
/** Whether this launcher is bound to [registry] itself, not just to any registry. */
9391
fun isBoundTo(registry: ActivityResultRegistry): Boolean = boundRegistry === registry
9492
}

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

Lines changed: 15 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,37 +12,25 @@ import androidx.activity.result.ActivityResultLauncher
1212
import androidx.activity.result.contract.ActivityResultContract
1313

1414
/**
15-
* Lets a native module register an AndroidX [ActivityResultContract] against the host Activity's
16-
* `ActivityResultRegistry` and receive results, without any changes to the consumer's
17-
* `MainActivity`.
18-
*
19-
* The API deliberately mirrors `androidx.activity.ComponentActivity.registerForActivityResult`:
20-
* same method name, same [ActivityResultCallback] shape, same returned [ActivityResultLauncher]
21-
* type. Unlike an Activity, a caller obtained from a `ReactContext` may register at any time --
22-
* including before any Activity exists -- and the returned launcher binds lazily to the real
23-
* registry once the host resumes.
15+
* Lets a native module register an AndroidX [ActivityResultContract] and receive results without
16+
* any changes to the consumer's `MainActivity`. Mirrors
17+
* `androidx.activity.ComponentActivity.registerForActivityResult`, except registration is legal at
18+
* any time: the returned launcher binds to the real registry once a host Activity resumes.
2419
*
2520
* Every registration carries a key that must be unique within the `ReactContext` and stable across
26-
* process death -- after the process is killed mid-flow, AndroidX replays the restored result to
27-
* whichever registration reproduces the same key string. The default key is scoped to the caller
28-
* (`"<owner class>:<contract class>"`), which is what lets two unrelated libraries both register a
29-
* stock contract such as `ActivityResultContracts.GetContent` without colliding.
30-
*
31-
* A collision throws an [IllegalStateException] at registration time. With owner scoping this is
32-
* only reachable when a single owner registers the same contract class twice; the fix is the
33-
* overload that takes an extra `key`, which is appended to -- not substituted for -- the
34-
* owner-and-contract scope, so a poorly chosen key can never reintroduce a cross-library collision.
21+
* process death (AndroidX replays a restored result to whichever registration reproduces the same
22+
* key). The default key `"<owner class>:<contract class>"` lets unrelated libraries register the
23+
* same stock contract without colliding; a collision throws [IllegalStateException] at
24+
* registration time, and the keyed overload (which appends to that scope, not replaces it)
25+
* resolves it.
3526
*/
3627
internal interface ReactActivityResultCaller {
3728

3829
/**
3930
* Registers [contract] under the key `"<owner class>:<contract class>"` and returns a launcher
40-
* for it.
41-
*
42-
* [owner] should be a stable, long-lived object -- typically the native module itself. An
43-
* anonymous object or a short-lived per-call helper yields a synthetic name such as
44-
* `com.example.Foo$1`, which is fragile across builds and defeats re-association after process
45-
* death.
31+
* for it. [owner] should be a stable, long-lived object, typically the native module itself: an
32+
* anonymous class's generated name can change between builds, which breaks result delivery
33+
* after the process is killed and restored.
4634
*
4735
* @throws IllegalStateException if [owner] already registered this contract class
4836
*/
@@ -53,12 +41,9 @@ internal interface ReactActivityResultCaller {
5341
): ActivityResultLauncher<I>
5442

5543
/**
56-
* Registers [contract] under the key `"<owner class>:<contract class>:<key>"`. Use this when one
57-
* owner needs several launchers of the same contract class.
58-
*
59-
* [key] only has to be unique among [owner]'s registrations of this contract class -- the
60-
* owner-and-contract scope is still applied -- but it must be stable across process death, so
61-
* derive it from a constant rather than from runtime state.
44+
* Registers [contract] under the key `"<owner class>:<contract class>:<key>"`. Use this when
45+
* one owner needs several launchers of the same contract class. [key] only has to be unique
46+
* among those, but must stay the same across process restarts, so derive it from a constant.
6247
*
6348
* @throws IllegalStateException if [owner] already registered this contract class under [key]
6449
*/

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

Lines changed: 23 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,9 @@ import com.facebook.react.common.ReactConstants
2020
import 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
*/
2927
internal 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
*/
6048
internal 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

Comments
 (0)