peachsim 0.1.658 → 0.1.660

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 (94) hide show
  1. package/cli/cli-update.ts +53 -21
  2. package/cli/commands/compat.ts +6 -2
  3. package/cli/commands/control.ts +12 -3
  4. package/cli/commands/inspect.ts +16 -14
  5. package/cli/commands/upgrade.ts +4 -2
  6. package/cli/current-sim.ts +4 -0
  7. package/dist-lib/agent-daemon-client.cjs +1 -1
  8. package/dist-lib/agent-events.cjs +1 -1
  9. package/dist-lib/agent-identity.cjs +1 -1
  10. package/dist-lib/agent-sessions.cjs +1 -1
  11. package/dist-lib/attached-projects.cjs +1 -1
  12. package/dist-lib/auth/shared-session.cjs +1 -1
  13. package/dist-lib/backend-origin.cjs +1 -1
  14. package/dist-lib/beta.cjs +1 -1
  15. package/dist-lib/beta.mjs +1 -1
  16. package/dist-lib/bridge-constants.cjs +1 -1
  17. package/dist-lib/bridge-contract-input.cjs +1 -1
  18. package/dist-lib/bridge-contract-input.mjs +1 -1
  19. package/dist-lib/bridge-contract.cjs +1 -1
  20. package/dist-lib/bridge-contract.mjs +1 -1
  21. package/dist-lib/capture-contract.cjs +1 -1
  22. package/dist-lib/capture-contract.mjs +1 -1
  23. package/dist-lib/cli-constants.cjs +1 -1
  24. package/dist-lib/cloud-contract.cjs +1 -1
  25. package/dist-lib/cloud-contract.mjs +1 -1
  26. package/dist-lib/cloud-test-args.cjs +1 -1
  27. package/dist-lib/cloud-test-args.mjs +1 -1
  28. package/dist-lib/cloud.cjs +1 -1
  29. package/dist-lib/cloud.mjs +1 -1
  30. package/dist-lib/config.cjs +1 -1
  31. package/dist-lib/detox/index.cjs +2 -2
  32. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  33. package/dist-lib/home-paths.cjs +1 -1
  34. package/dist-lib/host/bridge-host.cjs +1 -1
  35. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  36. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  37. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  38. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  39. package/dist-lib/host/websocket-proxy.cjs +1 -1
  40. package/dist-lib/index.cjs +5 -1
  41. package/dist-lib/jump-to-source-babel.cjs +1 -1
  42. package/dist-lib/jump-to-source-native.cjs +1 -1
  43. package/dist-lib/kotlin-host.cjs +11 -1
  44. package/dist-lib/menu.cjs +1 -1
  45. package/dist-lib/menu.mjs +1 -1
  46. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  47. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  48. package/dist-lib/metro-production-bundle.cjs +1 -1
  49. package/dist-lib/metro-production-bundle.mjs +1 -1
  50. package/dist-lib/metro.cjs +1 -1
  51. package/dist-lib/native-app-identity.cjs +1 -1
  52. package/dist-lib/native-app-identity.mjs +1 -1
  53. package/dist-lib/plist.cjs +1 -1
  54. package/dist-lib/plist.mjs +1 -1
  55. package/dist-lib/profiles.cjs +1 -1
  56. package/dist-lib/public-brand.cjs +1 -1
  57. package/dist-lib/react-native-host-modules.cjs +1 -1
  58. package/dist-lib/react-native-host-modules.mjs +1 -1
  59. package/dist-lib/render-mode.cjs +1 -1
  60. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  61. package/dist-lib/sdk.cjs +1 -1
  62. package/dist-lib/sdk.mjs +1 -1
  63. package/dist-lib/skills.cjs +15 -11
  64. package/dist-lib/swift-compile.cjs +1 -1
  65. package/dist-lib/swift-compile.mjs +1 -1
  66. package/dist-lib/swift-package.cjs +1 -1
  67. package/dist-lib/swift-package.mjs +1 -1
  68. package/dist-lib/swift-submit.cjs +1 -1
  69. package/dist-lib/swift-submit.mjs +1 -1
  70. package/dist-lib/swift.cjs +1 -1
  71. package/dist-lib/vite-swift.cjs +1 -1
  72. package/dist-lib/vite.cjs +1 -1
  73. package/kotlin/AppSources/android/app/Activity.kt +90 -1
  74. package/kotlin/AppSources/android/app/Application.kt +4 -0
  75. package/kotlin/AppSources/android/content/res/Configuration.kt +4 -0
  76. package/kotlin/AppSources/android/content/res/Resources.kt +3 -1
  77. package/kotlin/AppSources/android/os/Bundle.kt +9 -3
  78. package/kotlin/AppSources/androidx/activity/BackEventCompat.kt +17 -0
  79. package/kotlin/AppSources/androidx/activity/Cancellable.kt +5 -0
  80. package/kotlin/AppSources/androidx/activity/ComponentActivity.kt +58 -6
  81. package/kotlin/AppSources/androidx/activity/OnBackPressedCallback.kt +28 -0
  82. package/kotlin/AppSources/androidx/activity/OnBackPressedDispatcher.kt +144 -0
  83. package/kotlin/AppSources/androidx/activity/OnBackPressedDispatcherOwner.kt +7 -0
  84. package/kotlin/AppSources/androidx/activity/compose/BackHandler.kt +55 -0
  85. package/kotlin/AppSources/dev/peach/app/PeachApp.kt +231 -47
  86. package/kotlin/AppSources/java/lang/Runnable.kt +6 -0
  87. package/kotlin/ComposeSources/PeachComposeHost.kt +15 -3
  88. package/kotlin/fork/README.md +1 -0
  89. package/kotlin/fork/compose-worker-globals.patch +26 -0
  90. package/kotlin/fork/fork.json +1 -1
  91. package/kotlin/toolchain.json +3 -3
  92. package/package.json +1 -1
  93. package/src/kotlin-app.ts +18 -0
  94. package/src/kotlin-host.ts +10 -0
@@ -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
+ }
@@ -2,6 +2,8 @@
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
@@ -13,19 +15,25 @@ import android.view.ViewGroup
13
15
  import android.view.Window
14
16
  import android.view.WindowTheme
15
17
  import androidx.activity.ComponentActivity
18
+ import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
19
+ import androidx.compose.runtime.Composable
20
+ import androidx.compose.runtime.CompositionLocalProvider
16
21
  import androidx.compose.ui.platform.ComposeView
17
22
  import androidx.compose.ui.platform.PlatformArchitectureComponentsOwner
18
23
  import androidx.compose.ui.platform.PlatformInsets
19
24
  import androidx.compose.ui.platform.PlatformWindowInsets
20
25
  import androidx.compose.runtime.getValue
21
26
  import androidx.compose.runtime.key
27
+ import androidx.compose.runtime.mutableIntStateOf
22
28
  import androidx.compose.runtime.mutableStateOf
23
29
  import androidx.compose.runtime.setValue
30
+ import androidx.compose.ui.text.font.FontFamily
24
31
  import androidx.lifecycle.Lifecycle
25
32
  import androidx.navigationevent.NavigationEventDispatcher
26
33
  import androidx.navigationevent.NavigationEventDispatcherOwner
27
34
  import PeachComposeMount
28
35
  import addMount
36
+ import mounts
29
37
  import preparePeachDevice
30
38
  import robotoFamily
31
39
  import org.json.JSONObject
@@ -40,6 +48,9 @@ internal class PeachAppSpec(
40
48
  val window: WindowTheme,
41
49
  val createApplication: () -> Application,
42
50
  val createActivity: () -> Activity,
51
+ // the activity's configChanges includes uiMode: it takes a night mode
52
+ // change in onConfigurationChanged rather than relaunching
53
+ val handlesUiMode: Boolean,
43
54
  )
44
55
 
45
56
  // the system bars the device delivers to the window's content, in pixels: all
@@ -66,6 +77,41 @@ private class ActivityOwners(activity: ComponentActivity) : PlatformArchitecture
66
77
  }
67
78
  }
68
79
 
80
+ // the device and the application one started app process keeps, and what
81
+ // ActivityThread's record holds for its one activity: whether it is paused or
82
+ // stopped, and the state it saved when it stopped, until it resumes
83
+ private class AppProcess(
84
+ val spec: PeachAppSpec,
85
+ val application: Application,
86
+ val density: Float, val fontScale: Float, val locale: String, val gpu: Boolean, val stencilBits: Int,
87
+ val fonts: FontFamily,
88
+ val statusBar: Int, val navigationBar: Int,
89
+ val invalidate: (Boolean) -> Unit,
90
+ ) {
91
+ lateinit var activity: Activity
92
+ lateinit var mount: PeachComposeMount
93
+ // the window the device presents, once its scene is in place
94
+ var window: Window? = null
95
+ var mountId = 0
96
+ // the held view's place among its window's views; a relaunched window's
97
+ // first view takes the place the last window's view held, as it takes
98
+ // that view's saved state on the device
99
+ var view = 0
100
+ var paused = false
101
+ var stopped = false
102
+ var state: Bundle? = null
103
+ // the night mode last reported to the activity: ActivityRecord compares
104
+ // the configuration with it before a relaunch or onConfigurationChanged
105
+ var reportedNight = false
106
+ }
107
+
108
+ private var process: AppProcess? = null
109
+
110
+ private fun nightConfiguration(night: Boolean) = Configuration().apply {
111
+ uiMode = Configuration.UI_MODE_TYPE_NORMAL or
112
+ if (night) Configuration.UI_MODE_NIGHT_YES else Configuration.UI_MODE_NIGHT_NO
113
+ }
114
+
69
115
  // a cold start in ActivityThread's order: the Application and its onCreate,
70
116
  // then the launcher activity attached to its window and launcher intent,
71
117
  // onCreate, onStart, onPostCreate, onResume and onPostResume, and only then the
@@ -80,57 +126,17 @@ internal fun peachStartApp(
80
126
  try {
81
127
  require(backend == "graphite" || backend == "raster")
82
128
  preparePeachDevice(locale)
83
- processResources = Resources(Configuration().apply {
84
- uiMode = Configuration.UI_MODE_TYPE_NORMAL or
85
- if (night) Configuration.UI_MODE_NIGHT_YES else Configuration.UI_MODE_NIGHT_NO
86
- })
87
- val fonts = robotoFamily(regular, medium, bold)
129
+ processResources = Resources(nightConfiguration(night))
88
130
  val application = spec.createApplication()
89
131
  application.onCreate()
90
- val activity = spec.createActivity()
91
- val componentActivity = activity as? ComponentActivity
92
- lateinit var window: Window
93
- lateinit var mount: PeachComposeMount
94
- var mountId = 0
95
- // the window's one scene composes the ComposeView it holds; a new
96
- // view starts a new composition, as on the device
97
- var held by mutableStateOf<ComposeView?>(null)
98
- val host = object : PeachWindowHost {
99
- override fun compose(view: ComposeView): AutoCloseable {
100
- held = view
101
- return AutoCloseable { if (held === view) held = null }
102
- }
103
-
104
- override fun windowChanged() {
105
- if (mountId != 0)
106
- mount.emitAtFrame("window") { window.describe().toString() }
107
- }
108
- }
109
- window = Window(activity, spec.window, host)
110
- activity.attach(
111
- application,
112
- spec.component,
113
- Intent(Intent.ACTION_MAIN, setOf(Intent.CATEGORY_LAUNCHER), spec.launcher),
114
- window,
115
- )
116
- activity.performCreate(null)
117
- activity.performStart()
118
- activity.performPostCreate(null)
119
- activity.performResume()
120
- mount = PeachComposeMount(
121
- density, fontScale, locale, backend == "graphite", stencilBits, fonts, invalidate,
122
- owners = componentActivity?.let(::ActivityOwners),
123
- insets = SystemBarInsets(window, statusBar, navigationBar),
124
- clockRunning = { componentActivity == null || componentActivity.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) },
132
+ val app = AppProcess(
133
+ spec, application, density, fontScale, locale, backend == "graphite", stencilBits,
134
+ robotoFamily(regular, medium, bold), statusBar, navigationBar, invalidate,
125
135
  )
126
- mount.setContent { held?.let { view -> key(view) { view.Content() } } }
127
- mountId = addMount(mount)
128
- window.addToWindowManager()
129
- val view = checkNotNull(held) {
130
- "${spec.component.flattenToShortString()} set no ComposeView as its content; Peach composes only ComposeView content"
131
- }
136
+ process = app
137
+ val view = launchActivity(app, null)
132
138
  val params = view.layoutParams
133
- return window.describe().put("ok", true).put("id", mountId)
139
+ return app.activity.window.describe().put("ok", true).put("id", app.mountId)
134
140
  .put("wrapWidth", params?.width == ViewGroup.LayoutParams.WRAP_CONTENT)
135
141
  .put("wrapHeight", params?.height == ViewGroup.LayoutParams.WRAP_CONTENT)
136
142
  .toString()
@@ -139,3 +145,181 @@ internal fun peachStartApp(
139
145
  .put("error", "${error::class.simpleName}: ${error.message}").toString()
140
146
  }
141
147
  }
148
+
149
+ // ActivityThread's handleLaunchActivity: a new instance of the launcher
150
+ // activity created from the state its last instance saved, if any, with what
151
+ // that instance retained, then started, restored, resumed and its window
152
+ // added. the window's scene takes the process's mount id, so the device keeps
153
+ // presenting it.
154
+ private fun launchActivity(app: AppProcess, lastNonConfigurationInstance: Any?): ComposeView {
155
+ val spec = app.spec
156
+ val activity = spec.createActivity()
157
+ val componentActivity = activity as? ComponentActivity
158
+ val state = app.state
159
+ lateinit var window: Window
160
+ lateinit var mount: PeachComposeMount
161
+ // the window's one scene composes the ComposeView it holds; a new
162
+ // view starts a new composition, as on the device. the key is the view's
163
+ // place, not the view, so a relaunched window's view lands on the keys its
164
+ // saved state was written under
165
+ var held by mutableStateOf<ComposeView?>(null)
166
+ var place by mutableIntStateOf((app.view - 1).coerceAtLeast(0))
167
+ val host = object : PeachWindowHost {
168
+ override fun compose(view: ComposeView): AutoCloseable {
169
+ held = view
170
+ place += 1
171
+ app.view = place
172
+ return AutoCloseable { if (held === view) held = null }
173
+ }
174
+
175
+ // a change before the window is presented reaches the device with
176
+ // the window itself
177
+ override fun windowChanged() {
178
+ if (app.window === window)
179
+ mount.emitAtFrame("window") { window.describe().toString() }
180
+ }
181
+ }
182
+ window = Window(activity, spec.window, host)
183
+ activity.attach(
184
+ app.application,
185
+ spec.component,
186
+ Intent(Intent.ACTION_MAIN, setOf(Intent.CATEGORY_LAUNCHER), spec.launcher),
187
+ window,
188
+ lastNonConfigurationInstance,
189
+ )
190
+ activity.performCreate(state)
191
+ activity.performStart()
192
+ if (state != null) activity.performRestoreInstanceState(state)
193
+ activity.performPostCreate(state)
194
+ activity.performResume()
195
+ app.activity = activity
196
+ app.paused = false
197
+ app.stopped = false
198
+ app.state = null
199
+ app.reportedNight = isNight(activity.resources.configuration)
200
+ mount = PeachComposeMount(
201
+ app.density, app.fontScale, app.locale, app.gpu, app.stencilBits, app.fonts, app.invalidate,
202
+ night = isNight(activity.resources.configuration),
203
+ owners = componentActivity?.let(::ActivityOwners),
204
+ insets = SystemBarInsets(window, app.statusBar, app.navigationBar),
205
+ clockRunning = { componentActivity == null || componentActivity.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) },
206
+ )
207
+ mount.setContent {
208
+ val content = @Composable { held?.let { view -> key(place) { view.Content() } }; Unit }
209
+ // the content view's tree resolves the activity as its back dispatcher owner
210
+ if (componentActivity != null)
211
+ CompositionLocalProvider(LocalOnBackPressedDispatcherOwner provides componentActivity, content = content)
212
+ else content()
213
+ }
214
+ if (app.mountId == 0) app.mountId = addMount(mount)
215
+ else {
216
+ // the relaunched window takes the device's bounds and reports its
217
+ // window, which the new instance may have set differently
218
+ mount.constraints = app.mount.constraints
219
+ mounts[app.mountId] = mount
220
+ mount.emitAtFrame("window") { window.describe().toString() }
221
+ app.invalidate(true)
222
+ }
223
+ app.mount = mount
224
+ app.window = window
225
+ window.addToWindowManager()
226
+ return checkNotNull(held) {
227
+ "${spec.component.flattenToShortString()} set no ComposeView as its content; Peach composes only ComposeView content"
228
+ }
229
+ }
230
+
231
+ private fun runningApp() = checkNotNull(process) { "the app is not running" }
232
+
233
+ // the device's back: an activity with predictive back gets it through its
234
+ // window's back dispatch, where ComponentActivity's dispatcher takes it only
235
+ // while a callback is enabled and the activity's own default handles the
236
+ // rest; without predictive back the key reaches onBackPressed. false when the
237
+ // task moved to the back, which the device then shows as home.
238
+ internal fun peachAppBack(predictive: Boolean): Boolean {
239
+ val activity = runningApp().activity
240
+ activity.movedTaskToBack = false
241
+ val dispatcher = (activity as? ComponentActivity)?.onBackPressedDispatcher
242
+ if (!predictive) activity.onBackPressed()
243
+ else if (dispatcher?.hasEnabledCallbacks() == true) dispatcher.onBackPressed()
244
+ else activity.movedTaskToBack = true
245
+ return !activity.movedTaskToBack
246
+ }
247
+
248
+ // ActivityThread's pause and stop; from API 28 an activity that stops saves
249
+ // its state after onStop, kept for a relaunch until it resumes
250
+ private fun pauseAndStop(app: AppProcess) {
251
+ if (!app.paused) {
252
+ app.activity.performPause()
253
+ app.paused = true
254
+ }
255
+ if (!app.stopped) {
256
+ app.activity.performStop()
257
+ app.stopped = true
258
+ app.state = Bundle().also(app.activity::performSaveInstanceState)
259
+ }
260
+ }
261
+
262
+ // the task left the foreground (onPause, onStop, onSaveInstanceState) or came
263
+ // back to it (onRestart, onStart, onResume, onPostResume). a configuration
264
+ // that changed while it was stopped reaches it first: a relaunch in place of
265
+ // the restart, or onConfigurationChanged.
266
+ internal fun peachAppTask(foreground: Boolean) {
267
+ val app = runningApp()
268
+ if (!foreground) return pauseAndStop(app)
269
+ if (reportConfiguration(app)) return
270
+ app.activity.performRestart()
271
+ app.activity.performResume()
272
+ app.paused = false
273
+ app.stopped = false
274
+ app.state = null
275
+ // the frame clock runs again once the activity is started
276
+ app.mount.invalidate(false)
277
+ }
278
+
279
+ // gives a visible activity the process configuration when it differs from the
280
+ // one last reported to it: a relaunch, or onConfigurationChanged under
281
+ // configChanges uiMode. true when it relaunched.
282
+ private fun reportConfiguration(app: AppProcess): Boolean {
283
+ val configuration = processResources.configuration
284
+ if (isNight(configuration) == app.reportedNight) return false
285
+ if (!app.spec.handlesUiMode) {
286
+ relaunch(app)
287
+ return true
288
+ }
289
+ app.reportedNight = isNight(configuration)
290
+ app.activity.performConfigurationChanged(Configuration().apply { setTo(configuration) })
291
+ app.mount.night = app.reportedNight
292
+ return false
293
+ }
294
+
295
+ private fun isNight(configuration: Configuration) =
296
+ configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
297
+
298
+ // the device's night mode changed. the process's configuration changes in
299
+ // place and the Application hears it first; a visible activity then takes it.
300
+ // a stopped activity takes it once visible again, if it still differs.
301
+ internal fun peachAppNight(night: Boolean) {
302
+ val app = runningApp()
303
+ val configuration = processResources.configuration
304
+ if (isNight(configuration) == night) return
305
+ configuration.setTo(nightConfiguration(night))
306
+ app.application.onConfigurationChanged(Configuration().apply { setTo(configuration) })
307
+ if (!app.stopped) reportConfiguration(app)
308
+ }
309
+
310
+ // ActivityThread's handleRelaunchActivityInner: pause and stop with the state
311
+ // saved, collect what the next instance keeps, destroy, then launch anew
312
+ private fun relaunch(app: AppProcess) {
313
+ val old = app.activity
314
+ val oldMount = app.mount
315
+ old.isChangingConfigurations = true
316
+ pauseAndStop(app)
317
+ val retained = old.retainNonConfigurationInstances()
318
+ old.performDestroy()
319
+ old.window.removeFromWindowManager()
320
+ try {
321
+ launchActivity(app, retained)
322
+ } finally {
323
+ oldMount.close()
324
+ }
325
+ }
@@ -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
+ }
@@ -7,6 +7,7 @@ import androidx.compose.runtime.mutableStateOf
7
7
  import androidx.compose.runtime.remember
8
8
  import androidx.compose.runtime.setValue
9
9
  import androidx.compose.runtime.snapshots.Snapshot
10
+ import androidx.compose.ui.LocalSystemTheme
10
11
  import androidx.compose.ui.geometry.Offset
11
12
  import androidx.compose.ui.graphics.asComposeCanvas
12
13
  import androidx.compose.ui.input.InputMode
@@ -44,6 +45,7 @@ import androidx.compose.ui.unit.LayoutDirection
44
45
  import dev.onejs.one.OneNativeSourceViewDispatch
45
46
  import kotlin.io.encoding.Base64
46
47
  import kotlinx.coroutines.Dispatchers
48
+ import org.jetbrains.skiko.SystemTheme
47
49
  import org.jetbrains.skia.BackendRenderTarget
48
50
  import org.jetbrains.skia.Bitmap
49
51
  import org.jetbrains.skia.Color
@@ -123,11 +125,14 @@ internal class PeachComposeMount(
123
125
  private val stencilBits: Int,
124
126
  private val fonts: FontFamily,
125
127
  val invalidate: (Boolean) -> Unit,
128
+ night: Boolean,
126
129
  owners: PlatformArchitectureComponentsOwner? = null,
127
130
  insets: PlatformWindowInsets? = null,
128
131
  private val clockRunning: () -> Boolean = { true },
129
132
  ) : AutoCloseable {
130
133
  var props by mutableStateOf("")
134
+ // the configuration's night mode, which isSystemInDarkTheme reads
135
+ var night by mutableStateOf(night)
131
136
  var constraints = Constraints()
132
137
  set(value) { if (field != value) { field = value; needsMeasure = true } }
133
138
  private val events = mutableListOf<JSONObject>()
@@ -150,7 +155,11 @@ internal class PeachComposeMount(
150
155
 
151
156
  fun setContent(content: @Composable () -> Unit) {
152
157
  try {
153
- scene.setContent { PeachAndroidFonts(fonts, content) }
158
+ scene.setContent {
159
+ CompositionLocalProvider(LocalSystemTheme provides if (night) SystemTheme.DARK else SystemTheme.LIGHT) {
160
+ PeachAndroidFonts(fonts, content)
161
+ }
162
+ }
154
163
  } catch (failure: Throwable) {
155
164
  close()
156
165
  throw failure
@@ -289,7 +298,7 @@ internal fun robotoFamily(regular: String, medium: String, bold: String) = FontF
289
298
 
290
299
  @JsExport
291
300
  fun peachViewMount(source: String, view: String, hash: String, props: String,
292
- density: Float, fontScale: Float, locale: String, backend: String, stencilBits: Int,
301
+ density: Float, fontScale: Float, locale: String, night: Boolean, backend: String, stencilBits: Int,
293
302
  regular: String, medium: String, bold: String, invalidate: (Boolean) -> Unit): String {
294
303
  try {
295
304
  val dispatch = peachViewDispatcher(source)
@@ -297,7 +306,7 @@ fun peachViewMount(source: String, view: String, hash: String, props: String,
297
306
  .put("error", "composable source $source changed; rebuild the app").toString()
298
307
  require(backend == "graphite" || backend == "raster")
299
308
  preparePeachDevice(locale)
300
- val mount = PeachComposeMount(density, fontScale, locale, backend == "graphite", stencilBits, robotoFamily(regular, medium, bold), invalidate)
309
+ val mount = PeachComposeMount(density, fontScale, locale, backend == "graphite", stencilBits, robotoFamily(regular, medium, bold), invalidate, night)
301
310
  mount.props = props
302
311
  mount.setContent { dispatch.Content(view, mount.props, mount::emit) }
303
312
  return JSONObject().put("ok", true).put("id", addMount(mount)).toString()
@@ -307,6 +316,9 @@ fun peachViewMount(source: String, view: String, hash: String, props: String,
307
316
  }
308
317
 
309
318
  @JsExport fun peachViewUpdate(id: Int, props: String) { checkNotNull(mounts[id]).props = props }
319
+ // a host activity that handles uiMode itself, as React Native's does, keeps
320
+ // its views and hands them the new configuration
321
+ @JsExport fun peachViewNight(id: Int, night: Boolean) { checkNotNull(mounts[id]).night = night }
310
322
  @JsExport fun peachViewConstraints(id: Int, minWidth: Int, maxWidth: Int, minHeight: Int, maxHeight: Int) {
311
323
  checkNotNull(mounts[id]).also {
312
324
  it.constraints = Constraints(minWidth, maxWidth, minHeight, maxHeight)
@@ -10,6 +10,7 @@ builds until upstream supplies equivalent worker and native host hooks.
10
10
  | --- | --- | --- |
11
11
  | Compose `ui` | `Actuals.wasm.kt` | clock reads `globalThis.performance.now()` |
12
12
  | Compose `ui` | `PlatformClipboard.web.kt` | worker-safe secure-context and clipboard reads |
13
+ | Compose `ui` | `SystemTheme.kt` | `LocalSystemTheme` visible to the host, which provides the device's night mode |
13
14
  | Compose `ui-text` | `PlatformLocale.web.kt` | explicit `setPlatformLocaleProvider`; its default keeps upstream navigator behavior |
14
15
  | Skiko | `OsArch.web.kt` | explicit `setHostOsProvider`; its default lazily retains upstream OS detection |
15
16
  | Skiko | `Actuals.web.kt` | nanosecond clock reads `globalThis.performance.now()` |
@@ -105,3 +105,29 @@ index 7aa3e26..47e20a8 100644
105
105
 
106
106
  /**
107
107
  * https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API
108
+ diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/SystemTheme.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/SystemTheme.kt
109
+ index a14f182..9c4862d 100644
110
+ --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/SystemTheme.kt
111
+ +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/SystemTheme.kt
112
+ @@ -12,6 +12,11 @@
113
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
114
+ * See the License for the specific language governing permissions and
115
+ * limitations under the License.
116
+ + *
117
+ + * modified for Peach: the system theme composition local is visible to a
118
+ + * native host, which provides the device's night mode to the scenes it owns,
119
+ + * as later upstream releases expose it. see the fork record for the patch and
120
+ + * upstream revision.
121
+ */
122
+
123
+ package androidx.compose.ui
124
+ @@ -27,7 +32,8 @@ enum class SystemTheme {
125
+ Dark, Light, Unknown
126
+ }
127
+
128
+ -internal val LocalSystemTheme = staticCompositionLocalOf { currentSystemTheme }
129
+ +@InternalComposeUiApi
130
+ +val LocalSystemTheme = staticCompositionLocalOf { currentSystemTheme }
131
+
132
+ @InternalComposeUiApi
133
+ @Composable
@@ -18,7 +18,7 @@
18
18
  "tag": "v1.13.0-alpha01",
19
19
  "commit": "a9e47e8aa04c95cfe572ab4297c770f3cbfb234e",
20
20
  "patch": "compose-worker-globals.patch",
21
- "patchSha256": "51fa45bda1e0aae7db2eab1b056def406352958257e1d3b899c5c3d8aa5edddb"
21
+ "patchSha256": "5c45b67b3028637e6eafa351db004f23ad50c6f00ac2b5e7738fd026ac93cef0"
22
22
  },
23
23
  {
24
24
  "name": "skiko",