peachsim 0.1.669 → 0.1.671

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/cli/commands/control.ts +8 -5
  2. package/cli/commands/inspect/core.ts +7 -5
  3. package/cli/commands/inspect/wait-selector.ts +2 -11
  4. package/cli/commands/inspect.ts +6 -59
  5. package/cli/commands/kotlin.ts +6 -4
  6. package/cli/commands/skills.ts +28 -7
  7. package/cli/commands/upgrade.ts +18 -3
  8. package/cli/main.ts +21 -0
  9. package/cli/output-redaction.ts +182 -0
  10. package/cli/parse-args.ts +1 -0
  11. package/dist-lib/agent-daemon-client.cjs +1 -1
  12. package/dist-lib/agent-events.cjs +1 -1
  13. package/dist-lib/agent-identity.cjs +1 -1
  14. package/dist-lib/agent-sessions.cjs +1 -1
  15. package/dist-lib/attached-projects.cjs +1 -1
  16. package/dist-lib/auth/shared-session.cjs +1 -1
  17. package/dist-lib/backend-origin.cjs +1 -1
  18. package/dist-lib/beta.cjs +1 -1
  19. package/dist-lib/beta.mjs +1 -1
  20. package/dist-lib/bridge-constants.cjs +1 -1
  21. package/dist-lib/bridge-contract-input.cjs +1 -1
  22. package/dist-lib/bridge-contract-input.mjs +1 -1
  23. package/dist-lib/bridge-contract.cjs +1 -1
  24. package/dist-lib/bridge-contract.mjs +1 -1
  25. package/dist-lib/capture-contract.cjs +1 -1
  26. package/dist-lib/capture-contract.mjs +1 -1
  27. package/dist-lib/cli-constants.cjs +1 -1
  28. package/dist-lib/cloud-contract.cjs +1 -1
  29. package/dist-lib/cloud-contract.mjs +1 -1
  30. package/dist-lib/cloud-test-args.cjs +1 -1
  31. package/dist-lib/cloud-test-args.mjs +1 -1
  32. package/dist-lib/cloud.cjs +1 -1
  33. package/dist-lib/cloud.mjs +1 -1
  34. package/dist-lib/config.cjs +1 -1
  35. package/dist-lib/detox/index.cjs +1 -1
  36. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  37. package/dist-lib/home-paths.cjs +1 -1
  38. package/dist-lib/host/bridge-host.cjs +30 -22
  39. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  40. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  41. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  42. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  43. package/dist-lib/host/websocket-proxy.cjs +1 -1
  44. package/dist-lib/index.cjs +260 -17
  45. package/dist-lib/jump-to-source-babel.cjs +1 -1
  46. package/dist-lib/jump-to-source-native.cjs +1 -1
  47. package/dist-lib/kotlin-host.cjs +2 -1
  48. package/dist-lib/menu.cjs +1 -1
  49. package/dist-lib/menu.mjs +1 -1
  50. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  51. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  52. package/dist-lib/metro-production-bundle.cjs +1 -1
  53. package/dist-lib/metro-production-bundle.mjs +1 -1
  54. package/dist-lib/metro.cjs +259 -16
  55. package/dist-lib/native-app-identity.cjs +1 -1
  56. package/dist-lib/native-app-identity.mjs +1 -1
  57. package/dist-lib/native-firebase-metadata.cjs +115 -0
  58. package/dist-lib/native-firebase-metadata.mjs +86 -0
  59. package/dist-lib/plist.cjs +1 -1
  60. package/dist-lib/plist.mjs +1 -1
  61. package/dist-lib/profiles.cjs +1 -1
  62. package/dist-lib/public-brand.cjs +1 -1
  63. package/dist-lib/react-native-host-modules.cjs +1 -1
  64. package/dist-lib/react-native-host-modules.mjs +1 -1
  65. package/dist-lib/render-mode.cjs +1 -1
  66. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  67. package/dist-lib/sdk.cjs +2 -2
  68. package/dist-lib/sdk.mjs +2 -2
  69. package/dist-lib/skills.cjs +480 -206
  70. package/dist-lib/swift-compile.cjs +1 -1
  71. package/dist-lib/swift-compile.mjs +1 -1
  72. package/dist-lib/swift-package.cjs +1 -1
  73. package/dist-lib/swift-package.mjs +1 -1
  74. package/dist-lib/swift-submit.cjs +1 -1
  75. package/dist-lib/swift-submit.mjs +1 -1
  76. package/dist-lib/swift.cjs +1 -1
  77. package/dist-lib/vite-swift.cjs +21 -13
  78. package/dist-lib/vite.cjs +1 -1
  79. package/kotlin/AppSources/androidx/activity/ActivityFlags.kt +9 -0
  80. package/kotlin/AppSources/androidx/activity/BackEventCompat.kt +25 -4
  81. package/kotlin/AppSources/androidx/activity/ComponentActivity.kt +15 -4
  82. package/kotlin/AppSources/androidx/activity/EdgeToEdge.kt +12 -8
  83. package/kotlin/AppSources/androidx/activity/ExperimentalActivityApi.kt +6 -0
  84. package/kotlin/AppSources/androidx/activity/OnBackPressedCallback.kt +50 -9
  85. package/kotlin/AppSources/androidx/activity/OnBackPressedDispatcher.kt +67 -89
  86. package/kotlin/AppSources/androidx/activity/compose/BackHandler.kt +55 -24
  87. package/kotlin/AppSources/androidx/activity/compose/internal/BackHandlerCompat.kt +38 -0
  88. package/kotlin/AppSources/androidx/activity/compose/internal/BackHandlerDispatcherCompat.kt +27 -0
  89. package/kotlin/AppSources/dev/peach/app/PeachApp.kt +5 -5
  90. package/kotlin/ComposeSources/PeachComposeHost.kt +31 -7
  91. package/kotlin/fixtures/kotlin-app/app/build.gradle.kts +1 -1
  92. package/kotlin/fixtures/kotlin-app-variants/Browse.kt +73 -0
  93. package/kotlin/fixtures/kotlin-app-variants/Focus.kt +62 -0
  94. package/kotlin/fixtures/kotlin-app-variants/Lists.kt +57 -0
  95. package/kotlin/fixtures/kotlin-app-variants/Nav.kt +47 -0
  96. package/kotlin/fixtures/kotlin-app-variants/Nested.kt +64 -0
  97. package/kotlin/fixtures/kotlin-app-variants/Pan.kt +38 -0
  98. package/kotlin/fixtures/kotlin-app-variants/Pickers.kt +44 -0
  99. package/kotlin/fixtures/kotlin-app-variants/Press.kt +43 -0
  100. package/kotlin/fixtures/kotlin-app-variants/Transition.kt +69 -0
  101. package/kotlin/fixtures/kotlin-app-variants/ViewConfig.kt +22 -0
  102. package/kotlin/toolchain.json +30 -2
  103. package/package.json +8 -1
  104. package/skills/contrast/SKILL.md +2 -1
  105. package/skills/peach-debug/SKILL.md +2 -2
  106. package/skills/peach-test/SKILL.md +3 -3
  107. package/src/expo-swift-host.ts +13 -4
  108. package/src/host/bridge-host.ts +3 -3
  109. package/src/kotlin-app.ts +35 -16
  110. package/src/kotlin-host.ts +1 -0
  111. package/src/metro-plugin.ts +24 -0
  112. package/src/native-firebase-config.ts +202 -21
  113. package/src/native-firebase-metadata.ts +114 -0
  114. package/src/swift-host.ts +30 -15
  115. package/src/vite-plugin.ts +46 -2
  116. package/kotlin/AppSources/androidx/activity/Cancellable.kt +0 -5
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.669 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.671 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.669 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.671 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // src/swift-compile.ts
4
4
  import { execFile as execFile2 } from "node:child_process";
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.669 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.671 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.669 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.671 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // src/swift-package.ts
4
4
  function isSwiftManifestFile(file) {
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.669 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.671 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.669 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.671 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // src/swift-submit.ts
4
4
  var SWIFT_COMPILE_PATH = "/api/swift-compile";
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.669 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.671 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.669 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.671 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -1718,13 +1718,11 @@ async function compile(packagePath, entry, stamp, moduleRootOverride, hosted, sn
1718
1718
  stamp,
1719
1719
  package: content
1720
1720
  };
1721
- const toolchain = JSON.parse(
1722
- import_node_fs4.default.readFileSync(import_node_path5.default.join(moduleRoot, "toolchain.json"), "utf8")
1723
- );
1724
- const release = toolchain && typeof toolchain === "object" ? Reflect.get(toolchain, "release") : void 0;
1725
- if (typeof release !== "string") {
1726
- throw new Error(`swift toolchain manifest at ${moduleRoot} has no release`);
1727
- }
1721
+ const toolchain = localSwiftToolchain(moduleRoot);
1722
+ if (!toolchain)
1723
+ throw new Error(
1724
+ `the Swift toolchain ${moduleRoot}/toolchain.json names is not installed under ~/Library/Developer/Toolchains`
1725
+ );
1728
1726
  const artifact = await compileSwiftProjectUnit(snapshotCompileInput(snapshot, entry), {
1729
1727
  // the compile rewrites Sources/App in its root before swiftpm builds, and
1730
1728
  // its lock orders one process only: two vites on one root (a lane's dev
@@ -1737,14 +1735,24 @@ async function compile(packagePath, entry, stamp, moduleRootOverride, hosted, sn
1737
1735
  (0, import_node_crypto3.createHash)("sha256").update(`${moduleRoot}\0${import_node_path5.default.resolve(packagePath)}`).digest("hex").slice(0, 16)
1738
1736
  ),
1739
1737
  moduleRoot,
1740
- release,
1741
- toolchainBin: import_node_path5.default.join(
1742
- import_node_os3.default.homedir(),
1743
- `Library/Developer/Toolchains/${release}.xctoolchain/usr/bin`
1744
- )
1738
+ ...toolchain
1745
1739
  });
1746
1740
  return { ...artifact, stamp, package: content };
1747
1741
  }
1742
+ function localSwiftToolchain(moduleRoot) {
1743
+ const manifest = import_node_path5.default.join(moduleRoot, "toolchain.json");
1744
+ if (!import_node_fs4.default.existsSync(manifest)) return null;
1745
+ const toolchain = JSON.parse(import_node_fs4.default.readFileSync(manifest, "utf8"));
1746
+ const release = toolchain && typeof toolchain === "object" ? Reflect.get(toolchain, "release") : void 0;
1747
+ if (typeof release !== "string") {
1748
+ throw new Error(`swift toolchain manifest at ${moduleRoot} has no release`);
1749
+ }
1750
+ const toolchainBin = import_node_path5.default.join(
1751
+ import_node_os3.default.homedir(),
1752
+ `Library/Developer/Toolchains/${release}.xctoolchain/usr/bin`
1753
+ );
1754
+ return import_node_fs4.default.existsSync(import_node_path5.default.join(toolchainBin, "swift")) ? { release, toolchainBin } : null;
1755
+ }
1748
1756
  async function compileRemote(packagePath, entry, snapshot, hosted) {
1749
1757
  const auth = hosted ? null : resolveCliAuth();
1750
1758
  if (!hosted && (!auth || auth.kind === "github")) {
package/dist-lib/vite.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.669 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.671 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -0,0 +1,9 @@
1
+ package androidx.activity
2
+
3
+ // androidx.activity 1.13.0's ActivityFlags
4
+ @ExperimentalActivityApi
5
+ object ActivityFlags {
6
+ // a lifecycle-bound back callback keeps its place in the dispatch queue
7
+ // across ON_STOP and ON_START, rather than moving to the top on each start
8
+ var isOnBackPressedLifecycleOrderMaintained: Boolean = true
9
+ }
@@ -1,17 +1,38 @@
1
1
  package androidx.activity
2
2
 
3
- // androidx.activity's BackEventCompat: one predictive back gesture sample
3
+ import androidx.navigationevent.NavigationEvent
4
+
5
+ // androidx.activity 1.13.0's BackEventCompat: one predictive back gesture sample
4
6
  class BackEventCompat(
5
7
  val touchX: Float,
6
8
  val touchY: Float,
7
9
  val progress: Float,
8
10
  val swipeEdge: Int,
11
+ val frameTimeMillis: Long = 0,
9
12
  ) {
13
+ constructor(navigationEvent: NavigationEvent) : this(
14
+ touchX = navigationEvent.touchX,
15
+ touchY = navigationEvent.touchY,
16
+ progress = navigationEvent.progress,
17
+ swipeEdge = navigationEvent.swipeEdge,
18
+ frameTimeMillis = navigationEvent.frameTimeMillis,
19
+ )
20
+
21
+ fun toNavigationEvent(): NavigationEvent = NavigationEvent(
22
+ touchX = touchX,
23
+ touchY = touchY,
24
+ progress = progress,
25
+ swipeEdge = swipeEdge,
26
+ frameTimeMillis = frameTimeMillis,
27
+ )
28
+
10
29
  override fun toString(): String =
11
- "BackEventCompat{touchX=$touchX, touchY=$touchY, progress=$progress, swipeEdge=$swipeEdge}"
30
+ "BackEventCompat(touchX=$touchX, touchY=$touchY, progress=$progress, " +
31
+ "swipeEdge=$swipeEdge, frameTimeMillis=$frameTimeMillis)"
12
32
 
13
33
  companion object {
14
- const val EDGE_LEFT = 0
15
- const val EDGE_RIGHT = 1
34
+ const val EDGE_LEFT: Int = 0
35
+ const val EDGE_RIGHT: Int = 1
36
+ const val EDGE_NONE: Int = 2
16
37
  }
17
38
  }
@@ -14,6 +14,9 @@ import androidx.lifecycle.ViewModelStoreOwner
14
14
  import androidx.lifecycle.enableSavedStateHandles
15
15
  import androidx.lifecycle.viewmodel.CreationExtras
16
16
  import androidx.lifecycle.viewmodel.MutableCreationExtras
17
+ import androidx.navigationevent.DirectNavigationEventInput
18
+ import androidx.navigationevent.NavigationEventDispatcher
19
+ import androidx.navigationevent.NavigationEventDispatcherOwner
17
20
  import androidx.savedstate.SavedStateRegistry
18
21
  import androidx.savedstate.SavedStateRegistryController
19
22
  import androidx.savedstate.SavedStateRegistryOwner
@@ -25,7 +28,8 @@ import androidx.savedstate.SavedStateRegistryOwner
25
28
  // API 29, and destroy clears the view model store unless the activity is
26
29
  // changing configurations.
27
30
  open class ComponentActivity : Activity(), LifecycleOwner, ViewModelStoreOwner,
28
- HasDefaultViewModelProviderFactory, SavedStateRegistryOwner, OnBackPressedDispatcherOwner {
31
+ HasDefaultViewModelProviderFactory, SavedStateRegistryOwner, OnBackPressedDispatcherOwner,
32
+ NavigationEventDispatcherOwner {
29
33
  private val lifecycleRegistry = LifecycleRegistry(this)
30
34
  private val savedStateRegistryController = SavedStateRegistryController.create(this)
31
35
 
@@ -92,16 +96,23 @@ open class ComponentActivity : Activity(), LifecycleOwner, ViewModelStoreOwner,
92
96
  savedStateRegistryController.performSave(outState.state)
93
97
  }
94
98
 
95
- // the dispatcher gets the back first; without an enabled callback it falls
96
- // back to Activity's own
99
+ // onBackPressed's back, an input of the navigation event dispatcher: an
100
+ // enabled handler gets it first; without one it falls back to Activity's own
101
+ private val onBackPressedInput: DirectNavigationEventInput by lazy {
102
+ DirectNavigationEventInput().also(navigationEventDispatcher::addInput)
103
+ }
104
+
97
105
  override fun onBackPressed() {
98
- onBackPressedDispatcher.onBackPressed()
106
+ onBackPressedInput.backCompleted()
99
107
  }
100
108
 
101
109
  final override val onBackPressedDispatcher: OnBackPressedDispatcher by lazy {
102
110
  OnBackPressedDispatcher { super@ComponentActivity.onBackPressed() }
103
111
  }
104
112
 
113
+ override val navigationEventDispatcher: NavigationEventDispatcher
114
+ get() = onBackPressedDispatcher.eventDispatcher
115
+
105
116
  override fun dispatchActivityPostCreated() = lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
106
117
  override fun dispatchActivityPostStarted() = lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
107
118
  override fun dispatchActivityPostResumed() = lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
@@ -8,11 +8,12 @@ import android.view.WindowManager
8
8
  import androidx.core.view.WindowCompat
9
9
  import androidx.core.view.WindowInsetsControllerCompat
10
10
 
11
- // androidx.activity 1.9.3's EdgeToEdge as it runs on Peach's API 37 device,
12
- // where it takes EdgeToEdgeApi30: the decor stops fitting system windows, the
13
- // bars take each style's scrim (transparent for auto styles), status bar
14
- // contrast is off, navigation bar contrast follows an auto style, the bars'
15
- // appearance follows dark mode and the window lays out into every cutout.
11
+ // androidx.activity 1.13.0's EdgeToEdge as it runs on Peach's API 37 device,
12
+ // where it takes EdgeToEdgeApi35: the decor stops fitting system windows, the
13
+ // bars turn transparent, navigation bar contrast follows an auto style, the
14
+ // bars' appearance follows dark mode and the window lays out into every
15
+ // cutout. a style's non-transparent scrim is an androidx.core ProtectionLayout
16
+ // view added to the decor, which Peach's ComposeView-only window cannot host.
16
17
 
17
18
  // DecorView's light scrim and the platform's dark scrim
18
19
  internal val DefaultLightScrim = Color.argb(0xe6, 0xFF, 0xFF, 0xFF)
@@ -26,9 +27,12 @@ fun ComponentActivity.enableEdgeToEdge(
26
27
  val statusBarIsDark = statusBarStyle.detectDarkMode(view.resources)
27
28
  val navigationBarIsDark = navigationBarStyle.detectDarkMode(view.resources)
28
29
  WindowCompat.setDecorFitsSystemWindows(window, false)
29
- window.statusBarColor = statusBarStyle.getScrimWithEnforcedContrast(statusBarIsDark)
30
- window.navigationBarColor = navigationBarStyle.getScrimWithEnforcedContrast(navigationBarIsDark)
31
- window.isStatusBarContrastEnforced = false
30
+ window.statusBarColor = Color.TRANSPARENT
31
+ window.navigationBarColor = Color.TRANSPARENT
32
+ check(
33
+ statusBarStyle.getScrimWithEnforcedContrast(statusBarIsDark) == Color.TRANSPARENT &&
34
+ navigationBarStyle.getScrimWithEnforcedContrast(navigationBarIsDark) == Color.TRANSPARENT
35
+ ) { "enableEdgeToEdge with a non-transparent scrim adds a ProtectionLayout view; Peach composes only ComposeView content" }
32
36
  window.isNavigationBarContrastEnforced = navigationBarStyle.nightMode == UiModeManager.MODE_NIGHT_AUTO
33
37
  WindowInsetsControllerCompat(window, view).run {
34
38
  isAppearanceLightStatusBars = !statusBarIsDark
@@ -0,0 +1,6 @@
1
+ package androidx.activity
2
+
3
+ // androidx.activity 1.13.0's ExperimentalActivityApi
4
+ @RequiresOptIn(level = RequiresOptIn.Level.WARNING)
5
+ @Retention(AnnotationRetention.BINARY)
6
+ annotation class ExperimentalActivityApi
@@ -1,28 +1,69 @@
1
1
  package androidx.activity
2
2
 
3
- // androidx.activity 1.9.3's OnBackPressedCallback
3
+ import androidx.navigationevent.NavigationEvent
4
+ import androidx.navigationevent.NavigationEventHandler
5
+ import androidx.navigationevent.NavigationEventInfo
6
+
7
+ // androidx.activity 1.13.0's OnBackPressedCallback: each registration is a
8
+ // NavigationEventHandler on the dispatcher's NavigationEventDispatcher
4
9
  abstract class OnBackPressedCallback(enabled: Boolean) {
10
+ private val eventHandlers: MutableList<OnBackPressedEventHandler> = mutableListOf()
11
+
5
12
  var isEnabled: Boolean = enabled
6
13
  set(value) {
7
14
  field = value
8
- enabledChangedCallback?.invoke()
15
+ // a handler registered without a lifecycle is always lifecycle active
16
+ for (callback in eventHandlers) callback.isBackEnabled = callback.isLifecycleActive && value
9
17
  }
10
18
 
11
- private val cancellables = mutableListOf<Cancellable>()
12
- internal var enabledChangedCallback: (() -> Unit)? = null
19
+ private val closeables = mutableListOf<AutoCloseable>()
13
20
 
14
- fun remove() = cancellables.toList().forEach { it.cancel() }
21
+ fun remove() {
22
+ for (closeable in closeables.toList()) closeable.close()
23
+ closeables.clear()
24
+ for (callback in eventHandlers) callback.remove()
25
+ eventHandlers.clear()
26
+ }
15
27
 
16
28
  open fun handleOnBackStarted(backEvent: BackEventCompat) {}
17
29
  open fun handleOnBackProgressed(backEvent: BackEventCompat) {}
18
30
  abstract fun handleOnBackPressed()
19
31
  open fun handleOnBackCancelled() {}
20
32
 
21
- internal fun addCancellable(cancellable: Cancellable) {
22
- cancellables.add(cancellable)
33
+ internal fun addCloseable(closeable: AutoCloseable) {
34
+ closeables += closeable
23
35
  }
24
36
 
25
- internal fun removeCancellable(cancellable: Cancellable) {
26
- cancellables.remove(cancellable)
37
+ internal fun createNavigationEventHandler(info: NavigationEventInfo): OnBackPressedEventHandler {
38
+ val newHandler = OnBackPressedEventHandler(onBackPressedCallback = this, info)
39
+ eventHandlers += newHandler
40
+ return newHandler
41
+ }
42
+
43
+ internal class OnBackPressedEventHandler(
44
+ private val onBackPressedCallback: OnBackPressedCallback,
45
+ info: NavigationEventInfo,
46
+ ) : NavigationEventHandler<NavigationEventInfo>(initialInfo = info, isBackEnabled = onBackPressedCallback.isEnabled) {
47
+ var isLifecycleActive: Boolean = true
48
+ set(value) {
49
+ field = value
50
+ isBackEnabled = value && onBackPressedCallback.isEnabled
51
+ }
52
+
53
+ override fun onBackStarted(event: NavigationEvent) {
54
+ onBackPressedCallback.handleOnBackStarted(BackEventCompat(event))
55
+ }
56
+
57
+ override fun onBackProgressed(event: NavigationEvent) {
58
+ onBackPressedCallback.handleOnBackProgressed(BackEventCompat(event))
59
+ }
60
+
61
+ override fun onBackCompleted() {
62
+ onBackPressedCallback.handleOnBackPressed()
63
+ }
64
+
65
+ override fun onBackCancelled() {
66
+ onBackPressedCallback.handleOnBackCancelled()
67
+ }
27
68
  }
28
69
  }
@@ -1,125 +1,101 @@
1
1
  package androidx.activity
2
2
 
3
- import java.lang.Runnable
4
3
  import androidx.lifecycle.Lifecycle
5
4
  import androidx.lifecycle.LifecycleEventObserver
6
5
  import androidx.lifecycle.LifecycleOwner
6
+ import androidx.navigationevent.NavigationEvent
7
+ import androidx.navigationevent.NavigationEventDispatcher
8
+ import androidx.navigationevent.NavigationEventInfo
9
+ import androidx.navigationevent.NavigationEventInput
10
+ import java.lang.Runnable
7
11
 
8
- // androidx.activity 1.9.3's OnBackPressedDispatcher. the device's back
9
- // dispatch stands in for its OnBackInvokedDispatcher registration: a window
10
- // with predictive back gives this dispatcher the back only while it has an
11
- // enabled callback (hasEnabledCallbacks), as the registered callback does.
12
+ // androidx.activity 1.13.0's OnBackPressedDispatcher, a NavigationEventInput
13
+ // over its own NavigationEventDispatcher. the device's back dispatch stands in
14
+ // for its OnBackInvokedDispatcher inputs: a window with predictive back gives
15
+ // this dispatcher the back only while it has an enabled handler
16
+ // (hasEnabledCallbacks), as the registered OnBackInvokedCallback does.
12
17
  class OnBackPressedDispatcher(
13
18
  private val fallbackOnBackPressed: Runnable?,
14
19
  private val onHasEnabledCallbacksChanged: ((Boolean) -> Unit)?,
15
20
  ) {
16
- private val onBackPressedCallbacks = ArrayDeque<OnBackPressedCallback>()
17
- private var inProgressCallback: OnBackPressedCallback? = null
18
21
  private var hasEnabledCallbacks = false
22
+ private val eventInput by lazy { OnBackPressedEventInput() }
19
23
 
20
- constructor(fallbackOnBackPressed: Runnable? = null) : this(fallbackOnBackPressed, null)
24
+ internal val eventDispatcher
25
+ get() = eventInput.dispatcher
21
26
 
22
- private fun updateEnabledCallbacks() {
23
- val hadEnabledCallbacks = hasEnabledCallbacks
24
- val hasEnabledCallbacks = onBackPressedCallbacks.any { it.isEnabled }
25
- this.hasEnabledCallbacks = hasEnabledCallbacks
26
- if (hasEnabledCallbacks != hadEnabledCallbacks)
27
- onHasEnabledCallbacksChanged?.invoke(hasEnabledCallbacks)
28
- }
27
+ constructor(fallbackOnBackPressed: Runnable? = null) : this(fallbackOnBackPressed, null)
29
28
 
30
29
  fun addCallback(onBackPressedCallback: OnBackPressedCallback) {
31
- addCancellableCallback(onBackPressedCallback)
32
- }
33
-
34
- internal fun addCancellableCallback(onBackPressedCallback: OnBackPressedCallback): Cancellable {
35
- onBackPressedCallbacks.add(onBackPressedCallback)
36
- val cancellable = OnBackPressedCancellable(onBackPressedCallback)
37
- onBackPressedCallback.addCancellable(cancellable)
38
- updateEnabledCallbacks()
39
- onBackPressedCallback.enabledChangedCallback = ::updateEnabledCallbacks
40
- return cancellable
30
+ val info = OnBackPressedCallbackInfo(onBackPressedCallback)
31
+ val handler = onBackPressedCallback.createNavigationEventHandler(info)
32
+ eventDispatcher.addHandler(handler)
41
33
  }
42
34
 
35
+ @OptIn(ExperimentalActivityApi::class)
43
36
  fun addCallback(owner: LifecycleOwner, onBackPressedCallback: OnBackPressedCallback) {
44
37
  val lifecycle = owner.lifecycle
45
38
  if (lifecycle.currentState === Lifecycle.State.DESTROYED) return
46
- onBackPressedCallback.addCancellable(LifecycleOnBackPressedCancellable(lifecycle, onBackPressedCallback))
47
- updateEnabledCallbacks()
48
- onBackPressedCallback.enabledChangedCallback = ::updateEnabledCallbacks
39
+ val info = OnBackPressedCallbackInfo(onBackPressedCallback, owner)
40
+ val eventHandler = onBackPressedCallback.createNavigationEventHandler(info)
41
+ // the order flag adds the handler once, so its place in the queue holds,
42
+ // and toggles it with the lifecycle; the legacy order re-adds it on each
43
+ // start, which moves it to the top
44
+ if (ActivityFlags.isOnBackPressedLifecycleOrderMaintained) {
45
+ eventHandler.isLifecycleActive = false
46
+ eventDispatcher.addHandler(eventHandler)
47
+ }
48
+ val observer = object : LifecycleEventObserver {
49
+ override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
50
+ when (event) {
51
+ Lifecycle.Event.ON_START ->
52
+ if (ActivityFlags.isOnBackPressedLifecycleOrderMaintained) eventHandler.isLifecycleActive = true
53
+ else eventDispatcher.addHandler(eventHandler)
54
+ Lifecycle.Event.ON_STOP ->
55
+ if (ActivityFlags.isOnBackPressedLifecycleOrderMaintained) eventHandler.isLifecycleActive = false
56
+ else eventHandler.remove()
57
+ Lifecycle.Event.ON_DESTROY -> {
58
+ eventHandler.remove()
59
+ lifecycle.removeObserver(this)
60
+ }
61
+ else -> Unit
62
+ }
63
+ }
64
+ }
65
+ lifecycle.addObserver(observer)
66
+ onBackPressedCallback.addCloseable(AutoCloseable { lifecycle.removeObserver(observer) })
49
67
  }
50
68
 
51
69
  fun hasEnabledCallbacks(): Boolean = hasEnabledCallbacks
52
70
 
53
71
  fun dispatchOnBackStarted(backEvent: BackEventCompat) {
54
- val callback = onBackPressedCallbacks.lastOrNull { it.isEnabled }
55
- if (inProgressCallback != null) dispatchOnBackCancelled()
56
- inProgressCallback = callback
57
- callback?.handleOnBackStarted(backEvent)
72
+ eventInput.backStarted(backEvent.toNavigationEvent())
58
73
  }
59
74
 
60
75
  fun dispatchOnBackProgressed(backEvent: BackEventCompat) {
61
- val callback = inProgressCallback ?: onBackPressedCallbacks.lastOrNull { it.isEnabled }
62
- callback?.handleOnBackProgressed(backEvent)
76
+ eventInput.backProgressed(backEvent.toNavigationEvent())
63
77
  }
64
78
 
65
79
  fun onBackPressed() {
66
- val callback = inProgressCallback ?: onBackPressedCallbacks.lastOrNull { it.isEnabled }
67
- inProgressCallback = null
68
- if (callback != null) {
69
- callback.handleOnBackPressed()
70
- return
71
- }
72
- fallbackOnBackPressed?.run()
80
+ eventInput.backCompleted()
73
81
  }
74
82
 
75
83
  fun dispatchOnBackCancelled() {
76
- val callback = inProgressCallback ?: onBackPressedCallbacks.lastOrNull { it.isEnabled }
77
- inProgressCallback = null
78
- callback?.handleOnBackCancelled()
84
+ eventInput.backCancelled()
79
85
  }
80
86
 
81
- private inner class OnBackPressedCancellable(
82
- private val onBackPressedCallback: OnBackPressedCallback,
83
- ) : Cancellable {
84
- override fun cancel() {
85
- onBackPressedCallbacks.remove(onBackPressedCallback)
86
- if (inProgressCallback == onBackPressedCallback) {
87
- onBackPressedCallback.handleOnBackCancelled()
88
- inProgressCallback = null
89
- }
90
- onBackPressedCallback.removeCancellable(this)
91
- onBackPressedCallback.enabledChangedCallback?.invoke()
92
- onBackPressedCallback.enabledChangedCallback = null
93
- }
94
- }
95
-
96
- // a lifecycle-bound callback receives back only between ON_START and ON_STOP
97
- private inner class LifecycleOnBackPressedCancellable(
98
- private val lifecycle: Lifecycle,
99
- private val onBackPressedCallback: OnBackPressedCallback,
100
- ) : LifecycleEventObserver, Cancellable {
101
- private var currentCancellable: Cancellable? = null
102
-
103
- init {
104
- lifecycle.addObserver(this)
105
- }
87
+ private inner class OnBackPressedEventInput : NavigationEventInput() {
88
+ val dispatcher = NavigationEventDispatcher { fallbackOnBackPressed?.run() }.also { it.addInput(this) }
106
89
 
107
- override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
108
- if (event === Lifecycle.Event.ON_START) {
109
- currentCancellable = addCancellableCallback(onBackPressedCallback)
110
- } else if (event === Lifecycle.Event.ON_STOP) {
111
- currentCancellable?.cancel()
112
- } else if (event === Lifecycle.Event.ON_DESTROY) {
113
- cancel()
114
- }
90
+ override fun onHasEnabledHandlersChanged(hasEnabledHandlers: Boolean) {
91
+ hasEnabledCallbacks = hasEnabledHandlers
92
+ onHasEnabledCallbacksChanged?.invoke(hasEnabledHandlers)
115
93
  }
116
94
 
117
- override fun cancel() {
118
- lifecycle.removeObserver(this)
119
- onBackPressedCallback.removeCancellable(this)
120
- currentCancellable?.cancel()
121
- currentCancellable = null
122
- }
95
+ fun backStarted(event: NavigationEvent) = dispatchOnBackStarted(event)
96
+ fun backProgressed(event: NavigationEvent) = dispatchOnBackProgressed(event)
97
+ fun backCancelled() = dispatchOnBackCancelled()
98
+ fun backCompleted() = dispatchOnBackCompleted()
123
99
  }
124
100
  }
125
101
 
@@ -128,17 +104,19 @@ class OnBackPressedDispatcher(
128
104
  fun OnBackPressedDispatcher.addCallback(
129
105
  owner: LifecycleOwner? = null,
130
106
  enabled: Boolean = true,
131
- onBackPressed: OnBackPressedCallback.() -> Unit
107
+ onBackPressed: OnBackPressedCallback.() -> Unit,
132
108
  ): OnBackPressedCallback {
133
109
  val callback = object : OnBackPressedCallback(enabled) {
134
110
  override fun handleOnBackPressed() {
135
111
  onBackPressed()
136
112
  }
137
113
  }
138
- if (owner != null) {
139
- addCallback(owner, callback)
140
- } else {
141
- addCallback(callback)
142
- }
114
+ if (owner != null) addCallback(owner, callback)
115
+ else addCallback(callback)
143
116
  return callback
144
117
  }
118
+
119
+ private data class OnBackPressedCallbackInfo(
120
+ val callback: OnBackPressedCallback,
121
+ val owner: LifecycleOwner? = null,
122
+ ) : NavigationEventInfo()
@@ -1,18 +1,23 @@
1
1
  package androidx.activity.compose
2
2
 
3
- import androidx.activity.OnBackPressedCallback
3
+ import androidx.activity.ActivityFlags
4
+ import androidx.activity.ExperimentalActivityApi
4
5
  import androidx.activity.OnBackPressedDispatcherOwner
6
+ import androidx.activity.compose.internal.BackHandlerCompat
7
+ import androidx.activity.compose.internal.BackHandlerDispatcherCompat
5
8
  import androidx.compose.runtime.Composable
6
9
  import androidx.compose.runtime.DisposableEffect
7
10
  import androidx.compose.runtime.ProvidedValue
8
11
  import androidx.compose.runtime.SideEffect
9
12
  import androidx.compose.runtime.compositionLocalOf
10
- import androidx.compose.runtime.getValue
13
+ import androidx.compose.runtime.currentCompositeKeyHashCode
11
14
  import androidx.compose.runtime.remember
12
- import androidx.compose.runtime.rememberUpdatedState
13
- import androidx.lifecycle.compose.LocalLifecycleOwner
15
+ import androidx.lifecycle.compose.LifecycleStartEffect
16
+ import androidx.navigationevent.NavigationEventDispatcherOwner
17
+ import androidx.navigationevent.NavigationEventInfo
18
+ import androidx.navigationevent.compose.LocalNavigationEventDispatcherOwner
14
19
 
15
- // activity-compose 1.9.3's LocalOnBackPressedDispatcherOwner. on the device
20
+ // activity-compose 1.13.0's LocalOnBackPressedDispatcherOwner. on the device
16
21
  // an unprovided value resolves from the view tree or the context, both of
17
22
  // which reach the activity that set the content; the window host provides that
18
23
  // activity here.
@@ -28,28 +33,54 @@ object LocalOnBackPressedDispatcherOwner {
28
33
  LocalOnBackPressedDispatcherOwner.provides(dispatcherOwner)
29
34
  }
30
35
 
31
- // activity-compose 1.9.3's BackHandler
36
+ // activity-compose 1.13.0's BackHandler: a handler on the navigation event
37
+ // dispatcher when one is provided, else a callback on the back dispatcher
38
+ @OptIn(ExperimentalActivityApi::class)
32
39
  @Composable
33
40
  fun BackHandler(enabled: Boolean = true, onBack: () -> Unit) {
34
- val currentOnBack by rememberUpdatedState(onBack)
35
- val backCallback = remember {
36
- object : OnBackPressedCallback(enabled) {
37
- override fun handleOnBackPressed() {
38
- currentOnBack()
39
- }
40
- }
41
- }
42
- SideEffect {
43
- backCallback.isEnabled = enabled
41
+ val owner = LocalNavigationEventDispatcherOwner.current
42
+ ?: LocalOnBackPressedDispatcherOwner.current
43
+ ?: error(
44
+ "No NavigationEventDispatcherOwner was provided via " +
45
+ "LocalNavigationEventDispatcherOwner and no OnBackPressedDispatcherOwner was " +
46
+ "provided via LocalOnBackPressedDispatcherOwner. Please provide one of the two."
47
+ )
48
+ val dispatcher = remember(owner) {
49
+ BackHandlerDispatcherCompat(
50
+ (owner as? NavigationEventDispatcherOwner)?.navigationEventDispatcher,
51
+ (owner as? OnBackPressedDispatcherOwner)?.onBackPressedDispatcher,
52
+ )
44
53
  }
45
- val backDispatcher = checkNotNull(LocalOnBackPressedDispatcherOwner.current) {
46
- "No OnBackPressedDispatcherOwner was provided via LocalOnBackPressedDispatcherOwner"
47
- }.onBackPressedDispatcher
48
- val lifecycleOwner = LocalLifecycleOwner.current
49
- DisposableEffect(lifecycleOwner, backDispatcher) {
50
- backDispatcher.addCallback(lifecycleOwner, backCallback)
51
- onDispose {
52
- backCallback.remove()
54
+ val compositeKey = currentCompositeKeyHashCode
55
+ val handler = remember(dispatcher, compositeKey) { ComposeBackHandler(BackHandlerInfo(owner, compositeKey)) }
56
+ if (ActivityFlags.isOnBackPressedLifecycleOrderMaintained) {
57
+ SideEffect { handler.currentOnBackCompleted = onBack }
58
+ LifecycleStartEffect(enabled, handler) {
59
+ handler.isBackEnabled = enabled
60
+ onStopOrDispose { handler.isBackEnabled = false }
61
+ }
62
+ DisposableEffect(dispatcher, handler) {
63
+ dispatcher.addHandler(handler)
64
+ onDispose { dispatcher.removeHandler(handler) }
65
+ }
66
+ } else {
67
+ SideEffect {
68
+ handler.isBackEnabled = enabled
69
+ handler.currentOnBackCompleted = onBack
70
+ }
71
+ LifecycleStartEffect(dispatcher, handler) {
72
+ dispatcher.addHandler(handler)
73
+ onStopOrDispose { dispatcher.removeHandler(handler) }
53
74
  }
54
75
  }
55
76
  }
77
+
78
+ private class ComposeBackHandler(info: BackHandlerInfo) : BackHandlerCompat(info) {
79
+ var currentOnBackCompleted: () -> Unit = {}
80
+
81
+ override fun onBackCompleted() {
82
+ currentOnBackCompleted()
83
+ }
84
+ }
85
+
86
+ private data class BackHandlerInfo(val owner: Any, val compositeKey: Long) : NavigationEventInfo()