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.
- package/cli/cli-update.ts +53 -21
- package/cli/commands/compat.ts +6 -2
- package/cli/commands/control.ts +12 -3
- package/cli/commands/inspect.ts +16 -14
- package/cli/commands/upgrade.ts +4 -2
- package/cli/current-sim.ts +4 -0
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract-input.cjs +1 -1
- package/dist-lib/bridge-contract-input.mjs +1 -1
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +1 -1
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/cloud-contract.cjs +1 -1
- package/dist-lib/cloud-contract.mjs +1 -1
- package/dist-lib/cloud-test-args.cjs +1 -1
- package/dist-lib/cloud-test-args.mjs +1 -1
- package/dist-lib/cloud.cjs +1 -1
- package/dist-lib/cloud.mjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +2 -2
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +1 -1
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/replacement-module-handler.cjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +5 -1
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +1 -1
- package/dist-lib/kotlin-host.cjs +11 -1
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro-fingerprint-registry.cjs +1 -1
- package/dist-lib/metro-fingerprint-registry.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +1 -1
- package/dist-lib/metro-production-bundle.mjs +1 -1
- package/dist-lib/metro.cjs +1 -1
- package/dist-lib/native-app-identity.cjs +1 -1
- package/dist-lib/native-app-identity.mjs +1 -1
- package/dist-lib/plist.cjs +1 -1
- package/dist-lib/plist.mjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +1 -1
- package/dist-lib/sdk.mjs +1 -1
- package/dist-lib/skills.cjs +15 -11
- package/dist-lib/swift-compile.cjs +1 -1
- package/dist-lib/swift-compile.mjs +1 -1
- package/dist-lib/swift-package.cjs +1 -1
- package/dist-lib/swift-package.mjs +1 -1
- package/dist-lib/swift-submit.cjs +1 -1
- package/dist-lib/swift-submit.mjs +1 -1
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +1 -1
- package/dist-lib/vite.cjs +1 -1
- package/kotlin/AppSources/android/app/Activity.kt +90 -1
- package/kotlin/AppSources/android/app/Application.kt +4 -0
- package/kotlin/AppSources/android/content/res/Configuration.kt +4 -0
- package/kotlin/AppSources/android/content/res/Resources.kt +3 -1
- package/kotlin/AppSources/android/os/Bundle.kt +9 -3
- package/kotlin/AppSources/androidx/activity/BackEventCompat.kt +17 -0
- package/kotlin/AppSources/androidx/activity/Cancellable.kt +5 -0
- package/kotlin/AppSources/androidx/activity/ComponentActivity.kt +58 -6
- package/kotlin/AppSources/androidx/activity/OnBackPressedCallback.kt +28 -0
- package/kotlin/AppSources/androidx/activity/OnBackPressedDispatcher.kt +144 -0
- package/kotlin/AppSources/androidx/activity/OnBackPressedDispatcherOwner.kt +7 -0
- package/kotlin/AppSources/androidx/activity/compose/BackHandler.kt +55 -0
- package/kotlin/AppSources/dev/peach/app/PeachApp.kt +231 -47
- package/kotlin/AppSources/java/lang/Runnable.kt +6 -0
- package/kotlin/ComposeSources/PeachComposeHost.kt +15 -3
- package/kotlin/fork/README.md +1 -0
- package/kotlin/fork/compose-worker-globals.patch +26 -0
- package/kotlin/fork/fork.json +1 -1
- package/kotlin/toolchain.json +3 -3
- package/package.json +1 -1
- package/src/kotlin-app.ts +18 -0
- 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,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(
|
|
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
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
127
|
-
|
|
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
|
+
}
|
|
@@ -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 {
|
|
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)
|
package/kotlin/fork/README.md
CHANGED
|
@@ -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
|
package/kotlin/fork/fork.json
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"tag": "v1.13.0-alpha01",
|
|
19
19
|
"commit": "a9e47e8aa04c95cfe572ab4297c770f3cbfb234e",
|
|
20
20
|
"patch": "compose-worker-globals.patch",
|
|
21
|
-
"patchSha256": "
|
|
21
|
+
"patchSha256": "5c45b67b3028637e6eafa351db004f23ad50c6f00ac2b5e7738fd026ac93cef0"
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
"name": "skiko",
|