mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-22 01:02:41 +00:00
fix(android): restore image picker after Activity recreation (#7546)
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>
This commit is contained in:
co-authored by
luke
Ido Yana
RoundRedDot
parent
fbd90cb88d
commit
5d70bf0645
@@ -61,6 +61,22 @@ Gradle auto-fetches the platform/build-tools it needs once licenses are accepted
|
||||
|
||||
## Local build + install
|
||||
|
||||
The locked `expo-modules-core@57.0.23` needs the patch installed by `npm install`.
|
||||
It backports
|
||||
[Expo's Activity-result launcher fix](https://github.com/expo/expo/pull/49634): changing
|
||||
font size or another unhandled configuration can otherwise disable image selection
|
||||
until the app process restarts. Rebuild the native app after changing this patch;
|
||||
a Metro reload cannot update it. Remove the patch when the installed
|
||||
`expo-modules-core` includes the fix.
|
||||
|
||||
Run its native regression tests from `packages/app/android` after prebuild:
|
||||
|
||||
```bash
|
||||
./gradlew :expo-modules-core:testDebugUnitTest \
|
||||
--tests expo.modules.kotlin.activityresult.AppContextActivityResultRegistryRecreationTest \
|
||||
--no-daemon --max-workers=1 -Dorg.gradle.parallel=false
|
||||
```
|
||||
|
||||
From repo root:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
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()
|
||||
+ }
|
||||
+}
|
||||
@@ -8,6 +8,12 @@ import { join, relative } from "node:path";
|
||||
// workspace root live in their workspace's own node_modules, and patch-package resolves
|
||||
// the patch's node_modules/... paths relative to its working directory.
|
||||
const patchedPackages = [
|
||||
// Backport for expo-modules-core 57.0.23: https://github.com/expo/expo/pull/49634.
|
||||
// Remove when the installed Expo SDK includes the Activity recreation fix.
|
||||
{
|
||||
nodeModulesPath: "node_modules/expo-modules-core",
|
||||
patchPrefix: "expo-modules-core+",
|
||||
},
|
||||
{
|
||||
nodeModulesPath: "packages/app/node_modules/react-native-enriched-markdown",
|
||||
patchPrefix: "react-native-enriched-markdown+",
|
||||
|
||||
Reference in New Issue
Block a user