diff --git a/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/activityresult/AppContextActivityResultRegistry.kt b/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/activityresult/AppContextActivityResultRegistry.kt index a5b971e..7ef1b59 100644 --- a/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/activityresult/AppContextActivityResultRegistry.kt +++ b/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/activityresult/AppContextActivityResultRegistry.kt @@ -29,6 +29,7 @@ import expo.modules.kotlin.safeGetParcelable import expo.modules.kotlin.safeGetParcelableExtra import java.io.Serializable import java.util.Random +import java.util.concurrent.CountDownLatch private const val TAG = "ActivityResultRegistry" @@ -222,6 +223,7 @@ class AppContextActivityResultRegistry( return object : AppContextActivityResultLauncher() { override fun launch(input: I, callback: ActivityResultCallback) { val requestCode = keyToRequestCode[key] + ?: reregister(key, contract, fallbackCallback) ?: throw IllegalStateException("Attempting to launch an unregistered ActivityResultLauncher with contract $contract and input $input. You must ensure the ActivityResultLauncher is registered before calling launch()") keyToCallbacksAndContract[key] = CallbacksAndContract(fallbackCallback, callback, contract) @@ -240,6 +242,39 @@ class AppContextActivityResultRegistry( } } + /** + * A registration is bound to one Activity's lifecycle and dropped by its ON_DESTROY, + * while launchers outlive the Activity inside the AppContext. Registers [key] again + * against the live Activity and returns its request code. register() adds a lifecycle + * observer, which androidx requires on the main thread; launch() runs on the module's + * IO dispatcher, so the call is marshalled there. + */ + private fun reregister( + key: String, + contract: AppContextActivityResultContract, + fallbackCallback: AppContextActivityResultFallbackCallback + ): Int? { + val liveActivity = activity + if (Looper.myLooper() == Looper.getMainLooper()) { + register(key, liveActivity, contract, fallbackCallback) + } else { + val registered = CountDownLatch(1) + var failure: Throwable? = null + Handler(Looper.getMainLooper()).post { + try { + register(key, liveActivity, contract, fallbackCallback) + } catch (e: Throwable) { + failure = e + } finally { + registered.countDown() + } + } + registered.await() + failure?.let { throw it } + } + return keyToRequestCode[key] + } + /** * Persist the state of the registry. */ diff --git a/node_modules/expo-modules-core/android/src/test/java/expo/modules/kotlin/activityresult/AppContextActivityResultRegistryRecreationTest.kt b/node_modules/expo-modules-core/android/src/test/java/expo/modules/kotlin/activityresult/AppContextActivityResultRegistryRecreationTest.kt new file mode 100644 index 0000000..89a8eb1 --- /dev/null +++ b/node_modules/expo-modules-core/android/src/test/java/expo/modules/kotlin/activityresult/AppContextActivityResultRegistryRecreationTest.kt @@ -0,0 +1,78 @@ +package expo.modules.kotlin.activityresult + +import android.app.Activity +import android.content.Context +import android.content.Intent +import androidx.appcompat.app.AppCompatActivity +import expo.modules.kotlin.providers.CurrentActivityProvider +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.Shadows.shadowOf +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [33], manifest = Config.NONE) +class AppContextActivityResultRegistryRecreationTest { + private class ActivityProvider(override var currentActivity: Activity?) : CurrentActivityProvider + + private val contract = object : AppContextActivityResultContract { + override fun createIntent(context: Context, input: String) = Intent("test.PICK_IMAGE") + override fun parseResult(input: String, resultCode: Int, intent: Intent?) = "$input:$resultCode" + } + + private fun createActivity() = Robolectric.buildActivity(AppCompatActivity::class.java).apply { + get().setTheme(androidx.appcompat.R.style.Theme_AppCompat) + }.setup() + + @Test + fun retainedLauncherDeliversResultsAfterRepeatedActivityRecreation() { + var controller = createActivity() + val provider = ActivityProvider(controller.get()) + val registry = AppContextActivityResultRegistry(provider) + val fallbackResults = mutableListOf() + val launcher = registry.register("image", controller.get(), contract) { _, result -> + fallbackResults.add(result) + } + val results = mutableListOf() + + repeat(3) { iteration -> + controller.pause().stop().destroy() + controller = createActivity() + provider.currentActivity = controller.get() + + launcher.launch("image-$iteration") { results.add(it) } + val started = shadowOf(controller.get()).nextStartedActivityForResult + assertEquals("test.PICK_IMAGE", started.intent.action) + assertTrue(registry.dispatchResult(started.requestCode, Activity.RESULT_OK, null)) + } + + assertEquals(listOf("image-0:-1", "image-1:-1", "image-2:-1"), results) + assertEquals(emptyList(), fallbackResults) + controller.pause().stop().destroy() + } + + @Test + fun retainedLauncherUsesReplacementThatResumesBeforePreviousActivityIsDestroyed() { + val previous = createActivity() + val provider = ActivityProvider(previous.get()) + val registry = AppContextActivityResultRegistry(provider) + val results = mutableListOf() + val launcher = registry.register("image", previous.get(), contract) { _, result -> + results.add("fallback:$result") + } + previous.pause() + val replacement = createActivity() + provider.currentActivity = replacement.get() + previous.stop().destroy() + + launcher.launch("replacement") { results.add(it) } + val started = shadowOf(replacement.get()).nextStartedActivityForResult + assertTrue(registry.dispatchResult(started.requestCode, Activity.RESULT_OK, null)) + assertEquals(listOf("replacement:-1"), results) + replacement.pause().stop().destroy() + } +}