mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-28 20:22:45 +00:00
Backport expo/expo#49634 to SDK 54 so retained launchers register against the current Activity after their previous owner is destroyed. Cover repeated recreation and replacement-before-destroy ordering with native regression tests, and document the native rebuild requirement. Co-authored-by: luke <luopeihuan@gmail.com> Co-authored-by: Ido Yana <idoyana@gmail.com> Co-authored-by: RoundRedDot <roundreddot@gmail.com> Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
145 lines
6.4 KiB
Diff
145 lines
6.4 KiB
Diff
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<I, O>() {
|
|
override fun launch(input: I, callback: ActivityResultCallback<O>) {
|
|
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 <I : Serializable, O> reregister(
|
|
+ key: String,
|
|
+ contract: AppContextActivityResultContract<I, O>,
|
|
+ fallbackCallback: AppContextActivityResultFallbackCallback<I, O>
|
|
+ ): 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<String, String> {
|
|
+ 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<String>()
|
|
+ val launcher = registry.register("image", controller.get(), contract) { _, result ->
|
|
+ fallbackResults.add(result)
|
|
+ }
|
|
+ val results = mutableListOf<String>()
|
|
+
|
|
+ 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<String>(), fallbackResults)
|
|
+ controller.pause().stop().destroy()
|
|
+ }
|
|
+
|
|
+ @Test
|
|
+ fun retainedLauncherUsesReplacementThatResumesBeforePreviousActivityIsDestroyed() {
|
|
+ val previous = createActivity()
|
|
+ val provider = ActivityProvider(previous.get())
|
|
+ val registry = AppContextActivityResultRegistry(provider)
|
|
+ val results = mutableListOf<String>()
|
|
+ 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()
|
|
+ }
|
|
+}
|