peachsim 0.1.657 → 0.1.659

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 (115) hide show
  1. package/README.md +21 -2
  2. package/cli/app-config.ts +25 -9
  3. package/cli/bridge-flow-runner.ts +10 -23
  4. package/cli/commands/compat.ts +234 -10
  5. package/cli/commands/do-chain.ts +5 -1
  6. package/cli/commands/inspect/actions.ts +33 -76
  7. package/cli/commands/inspect/resolve-target.ts +67 -2
  8. package/cli/commands/inspect.ts +16 -1
  9. package/cli/drivers/playwright-provisioning.ts +95 -0
  10. package/cli/ws-bridge.ts +7 -7
  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 +9 -1
  22. package/dist-lib/bridge-contract-input.mjs +9 -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 +3 -2
  35. package/dist-lib/detox/index.cjs +2 -2
  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 +78 -35
  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 +51 -8
  43. package/dist-lib/host/websocket-proxy.cjs +1 -1
  44. package/dist-lib/index.cjs +317 -70
  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 +14 -4
  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 +300 -29
  55. package/dist-lib/native-app-identity.cjs +58 -25
  56. package/dist-lib/native-app-identity.mjs +57 -25
  57. package/dist-lib/plist.cjs +64 -0
  58. package/dist-lib/plist.mjs +29 -0
  59. package/dist-lib/profiles.cjs +1 -1
  60. package/dist-lib/public-brand.cjs +1 -1
  61. package/dist-lib/react-native-host-modules.cjs +1 -1
  62. package/dist-lib/react-native-host-modules.mjs +1 -1
  63. package/dist-lib/render-mode.cjs +1 -1
  64. package/dist-lib/scripts/dev-server-scanner.cjs +2 -2
  65. package/dist-lib/sdk.cjs +36 -49
  66. package/dist-lib/sdk.mjs +36 -49
  67. package/dist-lib/skills.cjs +257 -138
  68. package/dist-lib/swift-compile.cjs +1 -1
  69. package/dist-lib/swift-compile.mjs +1 -1
  70. package/dist-lib/swift-package.cjs +1 -1
  71. package/dist-lib/swift-package.mjs +1 -1
  72. package/dist-lib/swift-submit.cjs +1 -1
  73. package/dist-lib/swift-submit.mjs +1 -1
  74. package/dist-lib/swift.cjs +1 -1
  75. package/dist-lib/vite-swift.cjs +1 -1
  76. package/dist-lib/vite.cjs +51 -8
  77. package/kotlin/AppSources/android/app/Activity.kt +44 -0
  78. package/kotlin/AppSources/android/app/UiModeManager.kt +9 -0
  79. package/kotlin/AppSources/android/content/Context.kt +9 -2
  80. package/kotlin/AppSources/android/content/res/Configuration.kt +16 -0
  81. package/kotlin/AppSources/android/content/res/Resources.kt +7 -0
  82. package/kotlin/AppSources/android/graphics/Color.kt +13 -0
  83. package/kotlin/AppSources/android/os/Bundle.kt +9 -3
  84. package/kotlin/AppSources/android/view/View.kt +2 -0
  85. package/kotlin/AppSources/android/view/Window.kt +99 -8
  86. package/kotlin/AppSources/android/view/WindowInsetsController.kt +12 -0
  87. package/kotlin/AppSources/android/view/WindowManager.kt +16 -0
  88. package/kotlin/AppSources/androidx/activity/BackEventCompat.kt +17 -0
  89. package/kotlin/AppSources/androidx/activity/Cancellable.kt +5 -0
  90. package/kotlin/AppSources/androidx/activity/ComponentActivity.kt +22 -3
  91. package/kotlin/AppSources/androidx/activity/EdgeToEdge.kt +71 -0
  92. package/kotlin/AppSources/androidx/activity/OnBackPressedCallback.kt +28 -0
  93. package/kotlin/AppSources/androidx/activity/OnBackPressedDispatcher.kt +144 -0
  94. package/kotlin/AppSources/androidx/activity/OnBackPressedDispatcherOwner.kt +7 -0
  95. package/kotlin/AppSources/androidx/activity/compose/BackHandler.kt +55 -0
  96. package/kotlin/AppSources/androidx/core/view/WindowCompat.kt +32 -0
  97. package/kotlin/AppSources/dev/peach/app/PeachApp.kt +67 -12
  98. package/kotlin/AppSources/dev/peach/app/PeachWindowHost.kt +2 -1
  99. package/kotlin/AppSources/java/lang/Runnable.kt +6 -0
  100. package/kotlin/ComposeSources/PeachComposeHost.kt +11 -1
  101. package/package.json +8 -1
  102. package/skills/peach-setup/SKILL.md +27 -0
  103. package/src/android-theme.ts +167 -0
  104. package/src/android-xml.ts +70 -0
  105. package/src/bridge-contract-input.ts +10 -1
  106. package/src/bridge-contract.ts +7 -1
  107. package/src/config.ts +8 -0
  108. package/src/generated/android-framework-themes.json +6303 -0
  109. package/src/host/replacement-module-handler.ts +33 -0
  110. package/src/index.ts +1 -0
  111. package/src/kotlin-app.ts +86 -61
  112. package/src/kotlin-host.ts +13 -3
  113. package/src/metro-plugin.ts +46 -3
  114. package/src/native-app-identity.ts +75 -26
  115. package/src/sim-client.ts +2 -4
@@ -0,0 +1,28 @@
1
+ package androidx.activity
2
+
3
+ // androidx.activity 1.9.3's OnBackPressedCallback
4
+ abstract class OnBackPressedCallback(enabled: Boolean) {
5
+ var isEnabled: Boolean = enabled
6
+ set(value) {
7
+ field = value
8
+ enabledChangedCallback?.invoke()
9
+ }
10
+
11
+ private val cancellables = mutableListOf<Cancellable>()
12
+ internal var enabledChangedCallback: (() -> Unit)? = null
13
+
14
+ fun remove() = cancellables.toList().forEach { it.cancel() }
15
+
16
+ open fun handleOnBackStarted(backEvent: BackEventCompat) {}
17
+ open fun handleOnBackProgressed(backEvent: BackEventCompat) {}
18
+ abstract fun handleOnBackPressed()
19
+ open fun handleOnBackCancelled() {}
20
+
21
+ internal fun addCancellable(cancellable: Cancellable) {
22
+ cancellables.add(cancellable)
23
+ }
24
+
25
+ internal fun removeCancellable(cancellable: Cancellable) {
26
+ cancellables.remove(cancellable)
27
+ }
28
+ }
@@ -0,0 +1,144 @@
1
+ package androidx.activity
2
+
3
+ import java.lang.Runnable
4
+ import androidx.lifecycle.Lifecycle
5
+ import androidx.lifecycle.LifecycleEventObserver
6
+ import androidx.lifecycle.LifecycleOwner
7
+
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
+ class OnBackPressedDispatcher(
13
+ private val fallbackOnBackPressed: Runnable?,
14
+ private val onHasEnabledCallbacksChanged: ((Boolean) -> Unit)?,
15
+ ) {
16
+ private val onBackPressedCallbacks = ArrayDeque<OnBackPressedCallback>()
17
+ private var inProgressCallback: OnBackPressedCallback? = null
18
+ private var hasEnabledCallbacks = false
19
+
20
+ constructor(fallbackOnBackPressed: Runnable? = null) : this(fallbackOnBackPressed, null)
21
+
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
+ }
29
+
30
+ 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
41
+ }
42
+
43
+ fun addCallback(owner: LifecycleOwner, onBackPressedCallback: OnBackPressedCallback) {
44
+ val lifecycle = owner.lifecycle
45
+ if (lifecycle.currentState === Lifecycle.State.DESTROYED) return
46
+ onBackPressedCallback.addCancellable(LifecycleOnBackPressedCancellable(lifecycle, onBackPressedCallback))
47
+ updateEnabledCallbacks()
48
+ onBackPressedCallback.enabledChangedCallback = ::updateEnabledCallbacks
49
+ }
50
+
51
+ fun hasEnabledCallbacks(): Boolean = hasEnabledCallbacks
52
+
53
+ fun dispatchOnBackStarted(backEvent: BackEventCompat) {
54
+ val callback = onBackPressedCallbacks.lastOrNull { it.isEnabled }
55
+ if (inProgressCallback != null) dispatchOnBackCancelled()
56
+ inProgressCallback = callback
57
+ callback?.handleOnBackStarted(backEvent)
58
+ }
59
+
60
+ fun dispatchOnBackProgressed(backEvent: BackEventCompat) {
61
+ val callback = inProgressCallback ?: onBackPressedCallbacks.lastOrNull { it.isEnabled }
62
+ callback?.handleOnBackProgressed(backEvent)
63
+ }
64
+
65
+ 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()
73
+ }
74
+
75
+ fun dispatchOnBackCancelled() {
76
+ val callback = inProgressCallback ?: onBackPressedCallbacks.lastOrNull { it.isEnabled }
77
+ inProgressCallback = null
78
+ callback?.handleOnBackCancelled()
79
+ }
80
+
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
+ }
106
+
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
+ }
115
+ }
116
+
117
+ override fun cancel() {
118
+ lifecycle.removeObserver(this)
119
+ onBackPressedCallback.removeCancellable(this)
120
+ currentCancellable?.cancel()
121
+ currentCancellable = null
122
+ }
123
+ }
124
+ }
125
+
126
+ // androidx.activity's Kotlin registration: a callback from a lambda, bound to
127
+ // owner's lifecycle when one is given
128
+ fun OnBackPressedDispatcher.addCallback(
129
+ owner: LifecycleOwner? = null,
130
+ enabled: Boolean = true,
131
+ onBackPressed: OnBackPressedCallback.() -> Unit
132
+ ): OnBackPressedCallback {
133
+ val callback = object : OnBackPressedCallback(enabled) {
134
+ override fun handleOnBackPressed() {
135
+ onBackPressed()
136
+ }
137
+ }
138
+ if (owner != null) {
139
+ addCallback(owner, callback)
140
+ } else {
141
+ addCallback(callback)
142
+ }
143
+ return callback
144
+ }
@@ -0,0 +1,7 @@
1
+ package androidx.activity
2
+
3
+ import androidx.lifecycle.LifecycleOwner
4
+
5
+ interface OnBackPressedDispatcherOwner : LifecycleOwner {
6
+ val onBackPressedDispatcher: OnBackPressedDispatcher
7
+ }
@@ -0,0 +1,55 @@
1
+ package androidx.activity.compose
2
+
3
+ import androidx.activity.OnBackPressedCallback
4
+ import androidx.activity.OnBackPressedDispatcherOwner
5
+ import androidx.compose.runtime.Composable
6
+ import androidx.compose.runtime.DisposableEffect
7
+ import androidx.compose.runtime.ProvidedValue
8
+ import androidx.compose.runtime.SideEffect
9
+ import androidx.compose.runtime.compositionLocalOf
10
+ import androidx.compose.runtime.getValue
11
+ import androidx.compose.runtime.remember
12
+ import androidx.compose.runtime.rememberUpdatedState
13
+ import androidx.lifecycle.compose.LocalLifecycleOwner
14
+
15
+ // activity-compose 1.9.3's LocalOnBackPressedDispatcherOwner. on the device
16
+ // an unprovided value resolves from the view tree or the context, both of
17
+ // which reach the activity that set the content; the window host provides that
18
+ // activity here.
19
+ object LocalOnBackPressedDispatcherOwner {
20
+ private val LocalOnBackPressedDispatcherOwner =
21
+ compositionLocalOf<OnBackPressedDispatcherOwner?> { null }
22
+
23
+ val current: OnBackPressedDispatcherOwner?
24
+ @Composable
25
+ get() = LocalOnBackPressedDispatcherOwner.current
26
+
27
+ infix fun provides(dispatcherOwner: OnBackPressedDispatcherOwner): ProvidedValue<OnBackPressedDispatcherOwner?> =
28
+ LocalOnBackPressedDispatcherOwner.provides(dispatcherOwner)
29
+ }
30
+
31
+ // activity-compose 1.9.3's BackHandler
32
+ @Composable
33
+ 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
44
+ }
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()
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,32 @@
1
+ package androidx.core.view
2
+
3
+ import android.view.View
4
+ import android.view.Window
5
+ import android.view.WindowInsetsController
6
+
7
+ // androidx.core's window helpers as they run from API 30, on the platform
8
+ // window and its insets controller
9
+ object WindowCompat {
10
+ fun setDecorFitsSystemWindows(window: Window, decorFitsSystemWindows: Boolean) {
11
+ window.setDecorFitsSystemWindows(decorFitsSystemWindows)
12
+ }
13
+
14
+ fun getInsetsController(window: Window, view: View): WindowInsetsControllerCompat =
15
+ WindowInsetsControllerCompat(window, view)
16
+ }
17
+
18
+ class WindowInsetsControllerCompat(window: Window, @Suppress("UNUSED_PARAMETER") view: View) {
19
+ private val controller = window.insetsController
20
+
21
+ private fun appearance(flag: Int) = controller.systemBarsAppearance and flag != 0
22
+ private fun setAppearance(flag: Int, light: Boolean) =
23
+ controller.setSystemBarsAppearance(if (light) flag else 0, flag)
24
+
25
+ var isAppearanceLightStatusBars: Boolean
26
+ get() = appearance(WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS)
27
+ set(light) = setAppearance(WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS, light)
28
+
29
+ var isAppearanceLightNavigationBars: Boolean
30
+ get() = appearance(WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS)
31
+ set(light) = setAppearance(WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS, light)
32
+ }
@@ -2,13 +2,22 @@
2
2
 
3
3
  package dev.peach.app
4
4
 
5
+ import android.os.Bundle
6
+
5
7
  import android.app.Activity
6
8
  import android.app.Application
7
9
  import android.content.ComponentName
8
10
  import android.content.Intent
11
+ import android.content.res.Configuration
12
+ import android.content.res.Resources
13
+ import android.content.res.processResources
9
14
  import android.view.ViewGroup
10
15
  import android.view.Window
16
+ import android.view.WindowTheme
11
17
  import androidx.activity.ComponentActivity
18
+ import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
19
+ import androidx.compose.runtime.Composable
20
+ import androidx.compose.runtime.CompositionLocalProvider
12
21
  import androidx.compose.ui.platform.ComposeView
13
22
  import androidx.compose.ui.platform.PlatformArchitectureComponentsOwner
14
23
  import androidx.compose.ui.platform.PlatformInsets
@@ -26,15 +35,14 @@ import preparePeachDevice
26
35
  import robotoFamily
27
36
  import org.json.JSONObject
28
37
 
29
- // what the manifest and Gradle files resolved: the launcher component the
30
- // intent names (an alias stays the alias), the activity class it runs, the
31
- // app's target SDK and theme opt-out, and constructors for the manifest's
38
+ // what the manifest, Gradle files and theme resolved: the launcher component
39
+ // the intent names (an alias stays the alias), the activity class it runs, what
40
+ // its window read from the theme, and constructors for the manifest's
32
41
  // Application and that class (wasm has no reflection).
33
42
  internal class PeachAppSpec(
34
43
  val launcher: ComponentName,
35
44
  val component: ComponentName,
36
- val targetSdk: Int,
37
- val optOutEdgeToEdge: Boolean,
45
+ val window: WindowTheme,
38
46
  val createApplication: () -> Application,
39
47
  val createActivity: () -> Activity,
40
48
  )
@@ -71,12 +79,16 @@ internal fun peachStartApp(
71
79
  spec: PeachAppSpec,
72
80
  density: Float, fontScale: Float, locale: String, backend: String, stencilBits: Int,
73
81
  regular: String, medium: String, bold: String,
74
- statusBar: Int, navigationBar: Int,
82
+ statusBar: Int, navigationBar: Int, night: Boolean,
75
83
  invalidate: (Boolean) -> Unit,
76
84
  ): String {
77
85
  try {
78
86
  require(backend == "graphite" || backend == "raster")
79
87
  preparePeachDevice(locale)
88
+ processResources = Resources(Configuration().apply {
89
+ uiMode = Configuration.UI_MODE_TYPE_NORMAL or
90
+ if (night) Configuration.UI_MODE_NIGHT_YES else Configuration.UI_MODE_NIGHT_NO
91
+ })
80
92
  val fonts = robotoFamily(regular, medium, bold)
81
93
  val application = spec.createApplication()
82
94
  application.onCreate()
@@ -96,11 +108,10 @@ internal fun peachStartApp(
96
108
 
97
109
  override fun windowChanged() {
98
110
  if (mountId != 0)
99
- mount.emit("window", JSONObject().put("decorFitsSystemWindows", window.fitsSystemWindows).toString())
111
+ mount.emitAtFrame("window") { window.describe().toString() }
100
112
  }
101
113
  }
102
- val enforced = spec.targetSdk > 35 || (spec.targetSdk == 35 && !spec.optOutEdgeToEdge)
103
- window = Window(activity, enforced, host)
114
+ window = Window(activity, spec.window, host)
104
115
  activity.attach(
105
116
  application,
106
117
  spec.component,
@@ -111,21 +122,28 @@ internal fun peachStartApp(
111
122
  activity.performStart()
112
123
  activity.performPostCreate(null)
113
124
  activity.performResume()
125
+ running = activity
114
126
  mount = PeachComposeMount(
115
127
  density, fontScale, locale, backend == "graphite", stencilBits, fonts, invalidate,
116
128
  owners = componentActivity?.let(::ActivityOwners),
117
129
  insets = SystemBarInsets(window, statusBar, navigationBar),
118
130
  clockRunning = { componentActivity == null || componentActivity.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) },
119
131
  )
120
- mount.setContent { held?.let { view -> key(view) { view.Content() } } }
132
+ mount.setContent {
133
+ val content = @Composable { held?.let { view -> key(view) { view.Content() } }; Unit }
134
+ // the content view's tree resolves the activity as its back dispatcher owner
135
+ if (componentActivity != null)
136
+ CompositionLocalProvider(LocalOnBackPressedDispatcherOwner provides componentActivity, content = content)
137
+ else content()
138
+ }
121
139
  mountId = addMount(mount)
140
+ runningMount = mount
122
141
  window.addToWindowManager()
123
142
  val view = checkNotNull(held) {
124
143
  "${spec.component.flattenToShortString()} set no ComposeView as its content; Peach composes only ComposeView content"
125
144
  }
126
145
  val params = view.layoutParams
127
- return JSONObject().put("ok", true).put("id", mountId)
128
- .put("decorFitsSystemWindows", window.fitsSystemWindows)
146
+ return window.describe().put("ok", true).put("id", mountId)
129
147
  .put("wrapWidth", params?.width == ViewGroup.LayoutParams.WRAP_CONTENT)
130
148
  .put("wrapHeight", params?.height == ViewGroup.LayoutParams.WRAP_CONTENT)
131
149
  .toString()
@@ -134,3 +152,40 @@ internal fun peachStartApp(
134
152
  .put("error", "${error::class.simpleName}: ${error.message}").toString()
135
153
  }
136
154
  }
155
+
156
+ // the activity the app runs, once started, and the scene its window composes
157
+ private var running: Activity? = null
158
+ private var runningMount: PeachComposeMount? = null
159
+
160
+ // the device's back: an activity with predictive back gets it through its
161
+ // window's back dispatch, where ComponentActivity's dispatcher takes it only
162
+ // while a callback is enabled and the activity's own default handles the
163
+ // rest; without predictive back the key reaches onBackPressed. false when the
164
+ // task moved to the back, which the device then shows as home.
165
+ internal fun peachAppBack(predictive: Boolean): Boolean {
166
+ val activity = checkNotNull(running) { "the app is not running" }
167
+ activity.movedTaskToBack = false
168
+ val dispatcher = (activity as? ComponentActivity)?.onBackPressedDispatcher
169
+ if (!predictive) activity.onBackPressed()
170
+ else if (dispatcher?.hasEnabledCallbacks() == true) dispatcher.onBackPressed()
171
+ else activity.movedTaskToBack = true
172
+ return !activity.movedTaskToBack
173
+ }
174
+
175
+ // the task left the foreground (onPause, onStop, onSaveInstanceState) or came back to it
176
+ // (onRestart, onStart, onResume, onPostResume), as ActivityThread drives it
177
+ internal fun peachAppTask(foreground: Boolean) {
178
+ val activity = checkNotNull(running) { "the app is not running" }
179
+ if (foreground) {
180
+ activity.performRestart()
181
+ activity.performResume()
182
+ // the frame clock runs again once the activity is started
183
+ checkNotNull(runningMount).invalidate(false)
184
+ } else {
185
+ activity.performPause()
186
+ activity.performStop()
187
+ // from API 28 an activity that stops saves its state after onStop;
188
+ // ActivityThread keeps it for a relaunch, which no path here takes yet
189
+ activity.performSaveInstanceState(Bundle())
190
+ }
191
+ }
@@ -8,6 +8,7 @@ import androidx.compose.ui.platform.ComposeView
8
8
  internal interface PeachWindowHost {
9
9
  fun compose(view: ComposeView): AutoCloseable
10
10
 
11
- // the window's inset policy changed after it was added
11
+ // the inset policy or status bar the window asks the device for changed
12
+ // after it was added
12
13
  fun windowChanged()
13
14
  }
@@ -0,0 +1,6 @@
1
+ package java.lang
2
+
3
+ // java.lang.Runnable, which the platform and androidx signatures name
4
+ fun interface Runnable {
5
+ fun run()
6
+ }
@@ -131,6 +131,9 @@ internal class PeachComposeMount(
131
131
  var constraints = Constraints()
132
132
  set(value) { if (field != value) { field = value; needsMeasure = true } }
133
133
  private val events = mutableListOf<JSONObject>()
134
+ // state reported once a frame, read when the frame draws: like the decor,
135
+ // the device sees the state a frame ends with, not each step to it
136
+ private val frameState = linkedMapOf<String, () -> Any?>()
134
137
  private val platform = PeachComposePlatform(Density(density, fontScale), LocaleList(locale), IntSize.Zero, owners, insets)
135
138
  private val recomposer = FrameRecomposer(Dispatchers.Main) { invalidate(false) }
136
139
  private var needsMeasure = true
@@ -159,6 +162,11 @@ internal class PeachComposeMount(
159
162
  invalidate(false)
160
163
  }
161
164
 
165
+ fun emitAtFrame(name: String, args: () -> Any?) {
166
+ frameState[name] = args
167
+ invalidate(false)
168
+ }
169
+
162
170
  fun measure(): IntSize {
163
171
  val measured = scene.measureContent(constraints)
164
172
  scene.size = measured
@@ -198,7 +206,9 @@ internal class PeachComposeMount(
198
206
  scene.draw(canvas.asComposeCanvas())
199
207
  val reply = JSONObject().put("width", measured.width).put("height", measured.height)
200
208
  .put("pending", recomposer.hasPendingWork() || scene.hasInvalidations())
201
- .put("events", JSONArray(events))
209
+ for ((name, args) in frameState) events.add(JSONObject().put("name", name).put("args", args()))
210
+ frameState.clear()
211
+ reply.put("events", JSONArray(events))
202
212
  events.clear()
203
213
  if (gpu) checkNotNull(surface).flushAndSubmit()
204
214
  else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peachsim",
3
- "version": "0.1.657",
3
+ "version": "0.1.659",
4
4
  "description": "Vite and Metro plugins, testing drivers, and SDK exports for peach.",
5
5
  "author": "Tamagui LLC",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -84,6 +84,13 @@
84
84
  "import": "./dist-lib/metro-production-bundle.mjs",
85
85
  "default": "./dist-lib/metro-production-bundle.cjs"
86
86
  },
87
+ "./plist": {
88
+ "types": "./src/plist.ts",
89
+ "source": "./src/plist.ts",
90
+ "bun": "./src/plist.ts",
91
+ "import": "./dist-lib/plist.mjs",
92
+ "default": "./dist-lib/plist.cjs"
93
+ },
87
94
  "./native-app-identity": {
88
95
  "types": "./src/native-app-identity.ts",
89
96
  "source": "./src/native-app-identity.ts",
@@ -113,6 +113,33 @@ peach open https://my-app.local # load a hosted bundle
113
113
  - the compatibility scan's partial, unsupported, unknown, and version-mismatched
114
114
  packages have been summarized without treating its aggregate score as a pass/fail gate
115
115
 
116
+ ## the app's own native code
117
+
118
+ an app's own native modules and views (a local Expo module, a
119
+ `requireNativeComponent` view written in Swift or Kotlin) have no Peach
120
+ implementation until the app supplies one. when the scan or the running app
121
+ shows one missing (an empty view where content belongs, a "native module not
122
+ found" error), add it to `peach.config.ts` at the app root:
123
+
124
+ ```ts
125
+ import { defineConfig } from 'peachsim/config'
126
+
127
+ export default defineConfig({
128
+ nativeModules: { MyAppModule: { file: './peach/my-app-module.ts' } },
129
+ nativeViews: {
130
+ RNBalanceLabel: { file: './peach/BalanceLabel.tsx' }, // requireNativeComponent name
131
+ MyModule_StatusView: { file: './peach/StatusView.tsx' }, // requireNativeView('MyModule', 'StatusView')
132
+ },
133
+ })
134
+ ```
135
+
136
+ a `nativeViews` file default-exports a component that receives the native
137
+ view's props and renders what it shows on device, using `react-native` and
138
+ `contrast-kit` primitives. a `nativeModules` file exports the module's
139
+ methods, served to `NativeModules` and to Expo's `requireNativeModule`. a
140
+ file may import other project files by relative path, so a large view splits
141
+ into components and helpers. reopen the app to load the change.
142
+
116
143
  ## compatibility feedback requires approval
117
144
 
118
145
  run `peach compat --json` from the app project root during setup. if the scan