Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* 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.activityresult

import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContract
import androidx.core.app.ActivityOptionsCompat
import com.facebook.common.logging.FLog
import com.facebook.react.common.ReactConstants

/**
* An [ActivityResultLauncher] handed out before the host Activity's `ActivityResultRegistry` is
* available. It delegates to the real launcher once [bind] is called, and queues a single pending
* [launch] issued while unbound, firing it on bind. [unbind] detaches it when the host Activity is
* destroyed so that [ReactActivityResultCallerImpl] can rebind it against the next host's registry.
*/
internal class DeferredActivityResultLauncher<I>(
private val key: String,
private val contract: ActivityResultContract<I, *>,
private val onUnregister: () -> Unit,
) : ActivityResultLauncher<I>() {

override fun getContract(): ActivityResultContract<I, *> = contract

private class PendingLaunch<I>(val input: I, val options: ActivityOptionsCompat?)

private var delegate: ActivityResultLauncher<I>? = null
private var pendingLaunch: PendingLaunch<I>? = null

@Synchronized
override fun launch(input: I, options: ActivityOptionsCompat?) {
val boundDelegate = delegate
if (boundDelegate != null) {
boundDelegate.launch(input, options)
} else {
if (pendingLaunch != null) {
FLog.w(
ReactConstants.TAG,
"Launcher for '$key' was launched again before an Activity was available; " +
"replacing the previously queued launch.")
}
pendingLaunch = PendingLaunch(input, options)
}
}

@Synchronized
override fun unregister() {
delegate?.unregister()
delegate = null
pendingLaunch = null
onUnregister()
}

/** Attaches the real launcher and fires any launch queued while unbound. */
@Synchronized
fun bind(launcher: ActivityResultLauncher<I>) {
delegate = launcher
pendingLaunch?.let { pending ->
pendingLaunch = null
launcher.launch(pending.input, pending.options)
}
}

/** Detaches from a dying registry, keeping any queued launch for the next [bind]. */
@Synchronized
fun unbind() {
delegate?.unregister()
delegate = null
}

@get:Synchronized
val isBound: Boolean
get() = delegate != null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* 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.activityresult

import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContract

/**
* Lets a native module register an AndroidX [ActivityResultContract] against the host Activity's
* `ActivityResultRegistry` and receive results, without any changes to the consumer's
* `MainActivity`.
*
* The API deliberately mirrors `androidx.activity.ComponentActivity.registerForActivityResult`:
* same method name, same [ActivityResultCallback] shape, same returned [ActivityResultLauncher]
* type. Unlike an Activity, a caller obtained from a `ReactContext` may register at any time --
* including before any Activity exists -- and the returned launcher binds lazily to the real
* registry once the host resumes.
*
* Registrations are keyed by the contract's fully-qualified class name. Registering the same
* contract class twice throws an [IllegalStateException] at registration time; disambiguate by
* subclassing the contract or using the [registerForActivityResult] overload that takes an `owner`.
*/
internal interface ReactActivityResultCaller {

/**
* Registers [contract] and returns a launcher for it. The registration key is the contract's
* fully-qualified class name.
*
* @throws IllegalStateException if a registration with the same key already exists
*/
fun <I, O> registerForActivityResult(
contract: ActivityResultContract<I, O>,
callback: ActivityResultCallback<O>,
): ActivityResultLauncher<I>

/**
* Same as [registerForActivityResult], but scopes the registration key to [owner]
* (`"<owner class>:<contract class>"`). Use this when two independent callers need the same
* stock contract class, or when registering from something other than a native module.
*/
fun <I, O> registerForActivityResult(
owner: Any,
contract: ActivityResultContract<I, O>,
callback: ActivityResultCallback<O>,
): ActivityResultLauncher<I>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* 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.activityresult

import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.ActivityResultRegistry
import androidx.activity.result.ActivityResultRegistryOwner
import androidx.activity.result.contract.ActivityResultContract
import com.facebook.common.logging.FLog
import com.facebook.react.bridge.LifecycleEventListener
import com.facebook.react.bridge.ReactContext
import com.facebook.react.common.ReactConstants

/**
* Default [ReactActivityResultCaller], owned by a [ReactContext].
*
* Registrations are accepted at any time -- native modules are created lazily, typically well after
* the host Activity has resumed -- and bound to the current Activity's [ActivityResultRegistry]
* either immediately (when an Activity is already available) or on the next `onHostResume`. When
* the host Activity is destroyed the registrations are kept and rebound against the new Activity's
* registry under the same keys, so AndroidX can re-associate a result that arrives after Activity
* recreation.
*/
internal class ReactActivityResultCallerImpl(private val reactContext: ReactContext) :
ReactActivityResultCaller, LifecycleEventListener {

private class Entry<I, O>(
val key: String,
val registrantDescription: String,
val contract: ActivityResultContract<I, O>,
val callback: ActivityResultCallback<O>,
val launcher: DeferredActivityResultLauncher<I>,
)

private val entries = LinkedHashMap<String, Entry<*, *>>()

init {
reactContext.addLifecycleEventListener(this)
}

override fun <I, O> registerForActivityResult(
contract: ActivityResultContract<I, O>,
callback: ActivityResultCallback<O>,
): ActivityResultLauncher<I> =
register(contract.javaClass.name, callback.javaClass.name, contract, callback)

override fun <I, O> registerForActivityResult(
owner: Any,
contract: ActivityResultContract<I, O>,
callback: ActivityResultCallback<O>,
): ActivityResultLauncher<I> =
register(
"${owner.javaClass.name}:${contract.javaClass.name}",
owner.javaClass.name,
contract,
callback)

@Synchronized
private fun <I, O> register(
key: String,
registrantDescription: String,
contract: ActivityResultContract<I, O>,
callback: ActivityResultCallback<O>,
): ActivityResultLauncher<I> {
entries[key]?.let { existing ->
throw IllegalStateException(
"A launcher is already registered for key '$key' (registered by " +
"${existing.registrantDescription}, now requested by $registrantDescription). " +
"Subclass the contract to get a distinct key, or use the " +
"registerForActivityResult(owner, contract, callback) overload.")
}
val launcher = DeferredActivityResultLauncher(key, contract) { unregister(key) }
val entry = Entry(key, registrantDescription, contract, callback, launcher)
entries[key] = entry
currentRegistry()?.let { registry -> bind(entry, registry) }
return launcher
}

@Synchronized
private fun unregister(key: String) {
entries.remove(key)
}

@Synchronized
override fun onHostResume() {
val registry = currentRegistry() ?: return
for (entry in entries.values) {
if (!entry.launcher.isBound) {
bind(entry, registry)
}
}
}

override fun onHostPause(): Unit = Unit

@Synchronized
override fun onHostDestroy() {
// Detach from the dying registry but keep the registrations: they are rebound against the next
// host's registry (same keys) on the next onHostResume, which is also how a result that
// outlives the Activity gets re-associated by AndroidX.
for (entry in entries.values) {
entry.launcher.unbind()
}
}

private fun currentRegistry(): ActivityResultRegistry? {
val activity = reactContext.currentActivity ?: return null
val owner = activity as? ActivityResultRegistryOwner
if (owner == null) {
FLog.w(
ReactConstants.TAG,
"Current Activity ${activity.javaClass.name} is not an ActivityResultRegistryOwner; " +
"ActivityResultContract launchers will stay queued until one is available.")
return null
}
return owner.activityResultRegistry
}

private fun <I, O> bind(entry: Entry<I, O>, registry: ActivityResultRegistry) {
entry.launcher.bind(registry.register(entry.key, entry.contract, entry.callback))
}
}
Loading
Loading