peachsim 0.1.649 → 0.1.650
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/README.md +10 -9
- package/cli/command-registry.ts +2 -1
- package/cli/commands/guest-host.ts +67 -0
- package/cli/commands/kotlin.ts +231 -0
- package/cli/commands/project-report.ts +1 -1
- package/cli/commands/report-issue.ts +55 -32
- package/cli/commands/setup.ts +10 -14
- package/cli/commands/support.ts +399 -0
- package/cli/commands/swift.ts +6 -52
- package/cli/commands/telemetry.ts +10 -13
- package/cli/main.ts +22 -10
- package/cli/outbound-endpoints.ts +23 -2
- package/cli/privacy.ts +23 -25
- package/cli/runtime-notes.ts +3 -2
- package/cli/telemetry.ts +109 -14
- 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 +1 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +3 -3
- 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 +1 -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 +93 -28
- 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/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 -26
- package/dist-lib/swift-compile.cjs +6 -3
- package/dist-lib/swift-compile.mjs +6 -3
- 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 +6 -3
- package/dist-lib/vite.cjs +1 -1
- package/kotlin/AppSources/android/app/Activity.kt +85 -0
- package/kotlin/AppSources/android/app/Application.kt +7 -0
- package/kotlin/AppSources/android/content/Context.kt +5 -0
- package/kotlin/AppSources/android/content/Intent.kt +16 -0
- package/kotlin/AppSources/android/os/Bundle.kt +5 -0
- package/kotlin/AppSources/android/util/SuperNotCalledException.kt +3 -0
- package/kotlin/AppSources/android/view/View.kt +72 -0
- package/kotlin/AppSources/android/view/Window.kt +44 -0
- package/kotlin/AppSources/android/widget/FrameLayout.kt +6 -0
- package/kotlin/AppSources/androidx/activity/ComponentActivity.kt +57 -0
- package/kotlin/AppSources/androidx/activity/compose/ComponentActivity.kt +29 -0
- package/kotlin/AppSources/androidx/compose/ui/platform/ComposeView.kt +67 -0
- package/kotlin/AppSources/dev/peach/app/PeachApp.kt +136 -0
- package/kotlin/AppSources/dev/peach/app/PeachWindowHost.kt +13 -0
- package/kotlin/ComposeSources/PeachComposeHost.kt +58 -37
- package/package.json +1 -1
- package/skills/contrast/SKILL.md +2 -1
- package/skills/peach-setup/SKILL.md +2 -2
- package/src/{swift-guest.ts → guest-bundle.ts} +22 -7
- package/src/home-paths.ts +5 -3
- package/src/kotlin-app.ts +411 -0
- package/src/kotlin-host.ts +139 -32
- package/src/swift-compile.ts +7 -2
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
package android.content
|
|
2
|
+
|
|
3
|
+
class ComponentName(val packageName: String, val className: String) {
|
|
4
|
+
fun flattenToShortString(): String =
|
|
5
|
+
if (className.startsWith("$packageName.")) "$packageName/${className.removePrefix(packageName)}"
|
|
6
|
+
else "$packageName/$className"
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
class Intent(val action: String?, val categories: Set<String>?, val component: ComponentName?) {
|
|
10
|
+
fun hasCategory(category: String): Boolean = categories?.contains(category) == true
|
|
11
|
+
|
|
12
|
+
companion object {
|
|
13
|
+
const val ACTION_MAIN = "android.intent.action.MAIN"
|
|
14
|
+
const val CATEGORY_LAUNCHER = "android.intent.category.LAUNCHER"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
package android.view
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
|
|
5
|
+
open class View(val context: Context) {
|
|
6
|
+
var parent: ViewGroup? = null
|
|
7
|
+
internal set
|
|
8
|
+
var layoutParams: ViewGroup.LayoutParams? = null
|
|
9
|
+
internal var attachedWindow: Window? = null
|
|
10
|
+
|
|
11
|
+
val isAttachedToWindow: Boolean get() = attachedWindow != null
|
|
12
|
+
|
|
13
|
+
// ViewRootImpl dispatches attach through the tree once the window is added
|
|
14
|
+
internal open fun dispatchAttachedToWindow(window: Window) {
|
|
15
|
+
attachedWindow = window
|
|
16
|
+
onAttachedToWindow()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
internal open fun dispatchDetachedFromWindow() {
|
|
20
|
+
onDetachedFromWindow()
|
|
21
|
+
attachedWindow = null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
protected open fun onAttachedToWindow() {}
|
|
25
|
+
protected open fun onDetachedFromWindow() {}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
open class ViewGroup(context: Context) : View(context) {
|
|
29
|
+
open class LayoutParams(val width: Int, val height: Int) {
|
|
30
|
+
companion object {
|
|
31
|
+
const val MATCH_PARENT = -1
|
|
32
|
+
const val WRAP_CONTENT = -2
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private val children = mutableListOf<View>()
|
|
37
|
+
|
|
38
|
+
val childCount: Int get() = children.size
|
|
39
|
+
fun getChildAt(index: Int): View? = children.getOrNull(index)
|
|
40
|
+
|
|
41
|
+
fun addView(child: View, params: LayoutParams) {
|
|
42
|
+
child.layoutParams = params
|
|
43
|
+
addView(child)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
open fun addView(child: View) {
|
|
47
|
+
check(child.parent == null) {
|
|
48
|
+
"The specified child already has a parent. You must call removeView() on the child's parent first."
|
|
49
|
+
}
|
|
50
|
+
children += child
|
|
51
|
+
child.parent = this
|
|
52
|
+
attachedWindow?.let { child.dispatchAttachedToWindow(it) }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
fun removeAllViews() {
|
|
56
|
+
for (child in children) {
|
|
57
|
+
if (child.isAttachedToWindow) child.dispatchDetachedFromWindow()
|
|
58
|
+
child.parent = null
|
|
59
|
+
}
|
|
60
|
+
children.clear()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
override fun dispatchAttachedToWindow(window: Window) {
|
|
64
|
+
super.dispatchAttachedToWindow(window)
|
|
65
|
+
for (child in children.toList()) child.dispatchAttachedToWindow(window)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
override fun dispatchDetachedFromWindow() {
|
|
69
|
+
for (child in children.toList()) child.dispatchDetachedFromWindow()
|
|
70
|
+
super.dispatchDetachedFromWindow()
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
package android.view
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.widget.FrameLayout
|
|
5
|
+
import androidx.compose.runtime.getValue
|
|
6
|
+
import androidx.compose.runtime.mutableStateOf
|
|
7
|
+
import androidx.compose.runtime.setValue
|
|
8
|
+
import dev.peach.app.PeachWindowHost
|
|
9
|
+
|
|
10
|
+
// PhoneWindow's content parent (android.R.id.content) inside its decor. the
|
|
11
|
+
// window is added, and its views attached, after the activity resumes.
|
|
12
|
+
class Window internal constructor(
|
|
13
|
+
context: Context,
|
|
14
|
+
private val edgeToEdgeEnforced: Boolean,
|
|
15
|
+
internal val host: PeachWindowHost,
|
|
16
|
+
) {
|
|
17
|
+
internal val contentParent = FrameLayout(context)
|
|
18
|
+
val decorView: View get() = contentParent
|
|
19
|
+
|
|
20
|
+
// PhoneWindow fits system windows unless told otherwise, and applies that
|
|
21
|
+
// only while edge to edge is not enforced for the app's target SDK
|
|
22
|
+
private var decorFitsSystemWindows by mutableStateOf(true)
|
|
23
|
+
internal val fitsSystemWindows: Boolean get() = decorFitsSystemWindows && !edgeToEdgeEnforced
|
|
24
|
+
|
|
25
|
+
fun setDecorFitsSystemWindows(decorFitsSystemWindows: Boolean) {
|
|
26
|
+
val before = fitsSystemWindows
|
|
27
|
+
this.decorFitsSystemWindows = decorFitsSystemWindows
|
|
28
|
+
if (fitsSystemWindows != before) host.windowChanged()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Activity.setContentView(View) fills the content parent
|
|
32
|
+
internal fun setContentView(view: View, params: ViewGroup.LayoutParams) {
|
|
33
|
+
contentParent.removeAllViews()
|
|
34
|
+
contentParent.addView(view, params)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
internal fun addToWindowManager() {
|
|
38
|
+
contentParent.dispatchAttachedToWindow(this)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
internal fun removeFromWindowManager() {
|
|
42
|
+
if (contentParent.isAttachedToWindow) contentParent.dispatchDetachedFromWindow()
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
package androidx.activity
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.os.Bundle
|
|
5
|
+
import androidx.lifecycle.HasDefaultViewModelProviderFactory
|
|
6
|
+
import androidx.lifecycle.Lifecycle
|
|
7
|
+
import androidx.lifecycle.LifecycleEventObserver
|
|
8
|
+
import androidx.lifecycle.LifecycleOwner
|
|
9
|
+
import androidx.lifecycle.LifecycleRegistry
|
|
10
|
+
import androidx.lifecycle.SAVED_STATE_REGISTRY_OWNER_KEY
|
|
11
|
+
import androidx.lifecycle.SavedStateViewModelFactory
|
|
12
|
+
import androidx.lifecycle.VIEW_MODEL_STORE_OWNER_KEY
|
|
13
|
+
import androidx.lifecycle.ViewModelStore
|
|
14
|
+
import androidx.lifecycle.ViewModelStoreOwner
|
|
15
|
+
import androidx.lifecycle.enableSavedStateHandles
|
|
16
|
+
import androidx.lifecycle.viewmodel.CreationExtras
|
|
17
|
+
import androidx.lifecycle.viewmodel.MutableCreationExtras
|
|
18
|
+
import androidx.savedstate.SavedStateRegistry
|
|
19
|
+
import androidx.savedstate.SavedStateRegistryController
|
|
20
|
+
import androidx.savedstate.SavedStateRegistryOwner
|
|
21
|
+
|
|
22
|
+
// androidx.activity's ComponentActivity owners, on the multiplatform
|
|
23
|
+
// lifecycle, viewmodel and savedstate libraries: the registry attaches in the
|
|
24
|
+
// constructor and restores in onCreate before super, the lifecycle moves on
|
|
25
|
+
// the post callbacks, and destroy clears the view model store.
|
|
26
|
+
open class ComponentActivity : Activity(), LifecycleOwner, ViewModelStoreOwner,
|
|
27
|
+
HasDefaultViewModelProviderFactory, SavedStateRegistryOwner {
|
|
28
|
+
private val lifecycleRegistry = LifecycleRegistry(this)
|
|
29
|
+
private val savedStateRegistryController = SavedStateRegistryController.create(this)
|
|
30
|
+
|
|
31
|
+
override val lifecycle: Lifecycle get() = lifecycleRegistry
|
|
32
|
+
override val viewModelStore: ViewModelStore = ViewModelStore()
|
|
33
|
+
override val savedStateRegistry: SavedStateRegistry get() = savedStateRegistryController.savedStateRegistry
|
|
34
|
+
override val defaultViewModelProviderFactory = SavedStateViewModelFactory()
|
|
35
|
+
override val defaultViewModelCreationExtras: CreationExtras
|
|
36
|
+
get() = MutableCreationExtras().also {
|
|
37
|
+
it[SAVED_STATE_REGISTRY_OWNER_KEY] = this
|
|
38
|
+
it[VIEW_MODEL_STORE_OWNER_KEY] = this
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
init {
|
|
42
|
+
lifecycleRegistry.addObserver(LifecycleEventObserver { _, event ->
|
|
43
|
+
if (event == Lifecycle.Event.ON_DESTROY) viewModelStore.clear()
|
|
44
|
+
})
|
|
45
|
+
savedStateRegistryController.performAttach()
|
|
46
|
+
enableSavedStateHandles()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
50
|
+
savedStateRegistryController.performRestore(null)
|
|
51
|
+
super.onCreate(savedInstanceState)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
override fun dispatchActivityPostCreated() = lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
|
|
55
|
+
override fun dispatchActivityPostStarted() = lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
|
|
56
|
+
override fun dispatchActivityPostResumed() = lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
|
57
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
package androidx.activity.compose
|
|
2
|
+
|
|
3
|
+
import android.view.ViewGroup
|
|
4
|
+
import androidx.activity.ComponentActivity
|
|
5
|
+
import androidx.compose.runtime.Composable
|
|
6
|
+
import androidx.compose.runtime.CompositionContext
|
|
7
|
+
import androidx.compose.ui.platform.ComposeView
|
|
8
|
+
|
|
9
|
+
private val DefaultActivityContentLayoutParams =
|
|
10
|
+
ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
|
11
|
+
|
|
12
|
+
// activity-compose's setContent: reuse the ComposeView already in the content
|
|
13
|
+
// parent, otherwise create one and set it as the content view. the activity
|
|
14
|
+
// itself supplies the view tree owners.
|
|
15
|
+
fun ComponentActivity.setContent(
|
|
16
|
+
parent: CompositionContext? = null,
|
|
17
|
+
content: @Composable () -> Unit,
|
|
18
|
+
) {
|
|
19
|
+
val existingComposeView =
|
|
20
|
+
(window.decorView as ViewGroup).getChildAt(0) as? ComposeView
|
|
21
|
+
if (existingComposeView != null) with(existingComposeView) {
|
|
22
|
+
setParentCompositionContext(parent)
|
|
23
|
+
setContent(content)
|
|
24
|
+
} else ComposeView(this).apply {
|
|
25
|
+
setParentCompositionContext(parent)
|
|
26
|
+
setContent(content)
|
|
27
|
+
setContentView(this, DefaultActivityContentLayoutParams)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
package androidx.compose.ui.platform
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.view.ViewGroup
|
|
5
|
+
import androidx.compose.runtime.Composable
|
|
6
|
+
import androidx.compose.runtime.CompositionContext
|
|
7
|
+
import androidx.compose.runtime.mutableStateOf
|
|
8
|
+
|
|
9
|
+
// AbstractComposeView and ComposeView as compose-ui's Android source has them:
|
|
10
|
+
// setContent stores a state-backed lambda, composition starts when the view
|
|
11
|
+
// attaches to a window, a later setContent replaces the lambda inside the same
|
|
12
|
+
// composition, and detaching disposes it (the default view composition
|
|
13
|
+
// strategy).
|
|
14
|
+
class ComposeView(context: Context) : ViewGroup(context) {
|
|
15
|
+
private val content = mutableStateOf<(@Composable () -> Unit)?>(null)
|
|
16
|
+
private var shouldCreateCompositionOnAttachedToWindow = false
|
|
17
|
+
private var parentContext: CompositionContext? = null
|
|
18
|
+
private var composition: AutoCloseable? = null
|
|
19
|
+
|
|
20
|
+
val hasComposition: Boolean get() = composition != null
|
|
21
|
+
|
|
22
|
+
@Composable
|
|
23
|
+
internal fun Content() {
|
|
24
|
+
content.value?.invoke()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fun setParentCompositionContext(parent: CompositionContext?) {
|
|
28
|
+
parentContext = parent
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fun setContent(content: @Composable () -> Unit) {
|
|
32
|
+
shouldCreateCompositionOnAttachedToWindow = true
|
|
33
|
+
this.content.value = content
|
|
34
|
+
if (isAttachedToWindow) createComposition()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fun createComposition() {
|
|
38
|
+
check(parentContext != null || isAttachedToWindow) {
|
|
39
|
+
"createComposition requires a previous call to createComposition(ComposeViewContext)," +
|
|
40
|
+
" a parent reference, or the View to be attached to a window. Attach the View or " +
|
|
41
|
+
"call setParentCompositionReference."
|
|
42
|
+
}
|
|
43
|
+
ensureCompositionCreated()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fun disposeComposition() {
|
|
47
|
+
composition?.close()
|
|
48
|
+
composition = null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private fun ensureCompositionCreated() {
|
|
52
|
+
if (composition != null) return
|
|
53
|
+
check(parentContext == null) { "a ComposeView composing into a parent composition is not supported in Peach yet" }
|
|
54
|
+
val window = checkNotNull(attachedWindow) { "ComposeView is not attached to a window" }
|
|
55
|
+
composition = window.host.compose(this)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
override fun onAttachedToWindow() {
|
|
59
|
+
super.onAttachedToWindow()
|
|
60
|
+
if (shouldCreateCompositionOnAttachedToWindow) ensureCompositionCreated()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
override fun onDetachedFromWindow() {
|
|
64
|
+
super.onDetachedFromWindow()
|
|
65
|
+
disposeComposition()
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
@file:OptIn(androidx.compose.ui.InternalComposeUiApi::class)
|
|
2
|
+
|
|
3
|
+
package dev.peach.app
|
|
4
|
+
|
|
5
|
+
import android.app.Activity
|
|
6
|
+
import android.app.Application
|
|
7
|
+
import android.content.ComponentName
|
|
8
|
+
import android.content.Intent
|
|
9
|
+
import android.view.ViewGroup
|
|
10
|
+
import android.view.Window
|
|
11
|
+
import androidx.activity.ComponentActivity
|
|
12
|
+
import androidx.compose.ui.platform.ComposeView
|
|
13
|
+
import androidx.compose.ui.platform.PlatformArchitectureComponentsOwner
|
|
14
|
+
import androidx.compose.ui.platform.PlatformInsets
|
|
15
|
+
import androidx.compose.ui.platform.PlatformWindowInsets
|
|
16
|
+
import androidx.compose.runtime.getValue
|
|
17
|
+
import androidx.compose.runtime.key
|
|
18
|
+
import androidx.compose.runtime.mutableStateOf
|
|
19
|
+
import androidx.compose.runtime.setValue
|
|
20
|
+
import androidx.lifecycle.Lifecycle
|
|
21
|
+
import androidx.navigationevent.NavigationEventDispatcher
|
|
22
|
+
import androidx.navigationevent.NavigationEventDispatcherOwner
|
|
23
|
+
import PeachComposeMount
|
|
24
|
+
import addMount
|
|
25
|
+
import preparePeachDevice
|
|
26
|
+
import robotoFamily
|
|
27
|
+
import org.json.JSONObject
|
|
28
|
+
|
|
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
|
|
32
|
+
// Application and that class (wasm has no reflection).
|
|
33
|
+
internal class PeachAppSpec(
|
|
34
|
+
val launcher: ComponentName,
|
|
35
|
+
val component: ComponentName,
|
|
36
|
+
val targetSdk: Int,
|
|
37
|
+
val optOutEdgeToEdge: Boolean,
|
|
38
|
+
val createApplication: () -> Application,
|
|
39
|
+
val createActivity: () -> Activity,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
// the system bars the device delivers to the window's content, in pixels: all
|
|
43
|
+
// of them on an edge-to-edge window, none once the decor fits system windows.
|
|
44
|
+
private class SystemBarInsets(
|
|
45
|
+
private val window: Window,
|
|
46
|
+
private val status: Int,
|
|
47
|
+
private val navigation: Int,
|
|
48
|
+
) : PlatformWindowInsets {
|
|
49
|
+
// read when compose lays out, so a later change to the window applies
|
|
50
|
+
private fun delivered(pixels: Int) = if (window.fitsSystemWindows) 0 else pixels
|
|
51
|
+
override val statusBars = PlatformInsets(getTop = { delivered(status) })
|
|
52
|
+
override val navigationBars = PlatformInsets(getBottom = { delivered(navigation) })
|
|
53
|
+
override val systemBars =
|
|
54
|
+
PlatformInsets(getTop = { delivered(status) }, getBottom = { delivered(navigation) })
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private class ActivityOwners(activity: ComponentActivity) : PlatformArchitectureComponentsOwner {
|
|
58
|
+
override val lifecycleOwner = activity
|
|
59
|
+
override val viewModelStoreOwner = activity
|
|
60
|
+
override val savedStateRegistryOwner = activity
|
|
61
|
+
override val navigationEventDispatcherOwner = object : NavigationEventDispatcherOwner {
|
|
62
|
+
override val navigationEventDispatcher = NavigationEventDispatcher()
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// a cold start in ActivityThread's order: the Application and its onCreate,
|
|
67
|
+
// then the launcher activity attached to its window and launcher intent,
|
|
68
|
+
// onCreate, onStart, onPostCreate, onResume and onPostResume, and only then the
|
|
69
|
+
// window added, which attaches the content view and composes it.
|
|
70
|
+
internal fun peachStartApp(
|
|
71
|
+
spec: PeachAppSpec,
|
|
72
|
+
density: Float, fontScale: Float, locale: String, backend: String, stencilBits: Int,
|
|
73
|
+
regular: String, medium: String, bold: String,
|
|
74
|
+
statusBar: Int, navigationBar: Int,
|
|
75
|
+
invalidate: (Boolean) -> Unit,
|
|
76
|
+
): String {
|
|
77
|
+
try {
|
|
78
|
+
require(backend == "graphite" || backend == "raster")
|
|
79
|
+
preparePeachDevice(locale)
|
|
80
|
+
val fonts = robotoFamily(regular, medium, bold)
|
|
81
|
+
val application = spec.createApplication()
|
|
82
|
+
application.onCreate()
|
|
83
|
+
val activity = spec.createActivity()
|
|
84
|
+
val componentActivity = activity as? ComponentActivity
|
|
85
|
+
lateinit var window: Window
|
|
86
|
+
lateinit var mount: PeachComposeMount
|
|
87
|
+
var mountId = 0
|
|
88
|
+
// the window's one scene composes the ComposeView it holds; a new
|
|
89
|
+
// view starts a new composition, as on the device
|
|
90
|
+
var held by mutableStateOf<ComposeView?>(null)
|
|
91
|
+
val host = object : PeachWindowHost {
|
|
92
|
+
override fun compose(view: ComposeView): AutoCloseable {
|
|
93
|
+
held = view
|
|
94
|
+
return AutoCloseable { if (held === view) held = null }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
override fun windowChanged() {
|
|
98
|
+
if (mountId != 0)
|
|
99
|
+
mount.emit("window", JSONObject().put("decorFitsSystemWindows", window.fitsSystemWindows).toString())
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
val enforced = spec.targetSdk > 35 || (spec.targetSdk == 35 && !spec.optOutEdgeToEdge)
|
|
103
|
+
window = Window(activity, enforced, host)
|
|
104
|
+
activity.attach(
|
|
105
|
+
application,
|
|
106
|
+
spec.component,
|
|
107
|
+
Intent(Intent.ACTION_MAIN, setOf(Intent.CATEGORY_LAUNCHER), spec.launcher),
|
|
108
|
+
window,
|
|
109
|
+
)
|
|
110
|
+
activity.performCreate(null)
|
|
111
|
+
activity.performStart()
|
|
112
|
+
activity.performPostCreate(null)
|
|
113
|
+
activity.performResume()
|
|
114
|
+
mount = PeachComposeMount(
|
|
115
|
+
density, fontScale, locale, backend == "graphite", stencilBits, fonts, invalidate,
|
|
116
|
+
owners = componentActivity?.let(::ActivityOwners),
|
|
117
|
+
insets = SystemBarInsets(window, statusBar, navigationBar),
|
|
118
|
+
clockRunning = { componentActivity == null || componentActivity.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) },
|
|
119
|
+
)
|
|
120
|
+
mount.setContent { held?.let { view -> key(view) { view.Content() } } }
|
|
121
|
+
mountId = addMount(mount)
|
|
122
|
+
window.addToWindowManager()
|
|
123
|
+
val view = checkNotNull(held) {
|
|
124
|
+
"${spec.component.flattenToShortString()} set no ComposeView as its content; Peach composes only ComposeView content"
|
|
125
|
+
}
|
|
126
|
+
val params = view.layoutParams
|
|
127
|
+
return JSONObject().put("ok", true).put("id", mountId)
|
|
128
|
+
.put("decorFitsSystemWindows", window.fitsSystemWindows)
|
|
129
|
+
.put("wrapWidth", params?.width == ViewGroup.LayoutParams.WRAP_CONTENT)
|
|
130
|
+
.put("wrapHeight", params?.height == ViewGroup.LayoutParams.WRAP_CONTENT)
|
|
131
|
+
.toString()
|
|
132
|
+
} catch (error: Throwable) {
|
|
133
|
+
return JSONObject().put("ok", false).put("code", "E_KOTLIN_APP")
|
|
134
|
+
.put("error", "${error::class.simpleName}: ${error.message}").toString()
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
package dev.peach.app
|
|
2
|
+
|
|
3
|
+
import androidx.compose.ui.platform.ComposeView
|
|
4
|
+
|
|
5
|
+
// the device side of a window: one Peach scene for the window's life, which
|
|
6
|
+
// composes whichever ComposeView the window holds once that view attaches.
|
|
7
|
+
// the composition ends when the view disposes it.
|
|
8
|
+
internal interface PeachWindowHost {
|
|
9
|
+
fun compose(view: ComposeView): AutoCloseable
|
|
10
|
+
|
|
11
|
+
// the window's inset policy changed after it was added
|
|
12
|
+
fun windowChanged()
|
|
13
|
+
}
|
|
@@ -15,6 +15,8 @@ import androidx.compose.ui.input.pointer.PointerEventType
|
|
|
15
15
|
import androidx.compose.ui.input.pointer.PointerType
|
|
16
16
|
import androidx.compose.ui.platform.FrameRecomposer
|
|
17
17
|
import androidx.compose.ui.platform.LocalFontFamilyResolver
|
|
18
|
+
import androidx.compose.ui.platform.PlatformArchitectureComponentsOwner
|
|
19
|
+
import androidx.compose.ui.platform.PlatformWindowInsets
|
|
18
20
|
import androidx.compose.ui.platform.PlatformContext
|
|
19
21
|
import androidx.compose.ui.platform.WindowInfo
|
|
20
22
|
import androidx.compose.ui.platform.registerSkikoComposeImplementation
|
|
@@ -59,11 +61,19 @@ import org.jetbrains.skiko.setHostOsProvider
|
|
|
59
61
|
import org.json.JSONArray
|
|
60
62
|
import org.json.JSONObject
|
|
61
63
|
|
|
62
|
-
|
|
64
|
+
// owners and window insets come from the hosting window: an activity's
|
|
65
|
+
// ComposeView passes the activity and the insets its window delivers; an
|
|
66
|
+
// imported composable keeps the empty defaults.
|
|
67
|
+
internal class PeachComposePlatform(
|
|
63
68
|
val density: Density,
|
|
64
69
|
override val localeList: LocaleList,
|
|
65
70
|
var size: IntSize,
|
|
66
|
-
|
|
71
|
+
owners: PlatformArchitectureComponentsOwner?,
|
|
72
|
+
insets: PlatformWindowInsets?,
|
|
73
|
+
base: PlatformContext = PlatformContext.Empty(),
|
|
74
|
+
) : PlatformContext by base, PlatformContext.SemanticsOwnerListener {
|
|
75
|
+
override val architectureComponentsOwner: PlatformArchitectureComponentsOwner = owners ?: base.architectureComponentsOwner
|
|
76
|
+
override val windowInsets: PlatformWindowInsets = insets ?: base.windowInsets
|
|
67
77
|
val owners = mutableSetOf<SemanticsOwner>()
|
|
68
78
|
override val windowInfo: WindowInfo = object : WindowInfo {
|
|
69
79
|
override val isWindowFocused: Boolean get() = true
|
|
@@ -87,7 +97,7 @@ private class PeachComposePlatform(
|
|
|
87
97
|
// android's default and sans-serif families use the same device font bytes as
|
|
88
98
|
// peach's react native renderer. compose owns shaping and all other families.
|
|
89
99
|
@Composable
|
|
90
|
-
|
|
100
|
+
internal fun PeachAndroidFonts(roboto: FontFamily, content: @Composable () -> Unit) {
|
|
91
101
|
val parent = LocalFontFamilyResolver.current
|
|
92
102
|
val resolver = remember(parent, roboto) {
|
|
93
103
|
val backend = checkNotNull(parent.platformTypefacesLoader())
|
|
@@ -102,23 +112,26 @@ private fun PeachAndroidFonts(roboto: FontFamily, content: @Composable () -> Uni
|
|
|
102
112
|
CompositionLocalProvider(LocalFontFamilyResolver provides resolver, content = content)
|
|
103
113
|
}
|
|
104
114
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
115
|
+
// one compose scene presented by the tenant canvas. the frame clock runs only
|
|
116
|
+
// while clockRunning answers true, as Android's window recomposer pauses its
|
|
117
|
+
// clock below STARTED.
|
|
118
|
+
internal class PeachComposeMount(
|
|
109
119
|
density: Float,
|
|
110
120
|
fontScale: Float,
|
|
111
121
|
locale: String,
|
|
112
122
|
private val gpu: Boolean,
|
|
113
123
|
private val stencilBits: Int,
|
|
114
|
-
fonts: FontFamily,
|
|
124
|
+
private val fonts: FontFamily,
|
|
115
125
|
val invalidate: (Boolean) -> Unit,
|
|
126
|
+
owners: PlatformArchitectureComponentsOwner? = null,
|
|
127
|
+
insets: PlatformWindowInsets? = null,
|
|
128
|
+
private val clockRunning: () -> Boolean = { true },
|
|
116
129
|
) : AutoCloseable {
|
|
117
|
-
var props by mutableStateOf(
|
|
130
|
+
var props by mutableStateOf("")
|
|
118
131
|
var constraints = Constraints()
|
|
119
132
|
set(value) { if (field != value) { field = value; needsMeasure = true } }
|
|
120
133
|
private val events = mutableListOf<JSONObject>()
|
|
121
|
-
private val platform = PeachComposePlatform(Density(density, fontScale), LocaleList(locale), IntSize.Zero)
|
|
134
|
+
private val platform = PeachComposePlatform(Density(density, fontScale), LocaleList(locale), IntSize.Zero, owners, insets)
|
|
122
135
|
private val recomposer = FrameRecomposer(Dispatchers.Main) { invalidate(false) }
|
|
123
136
|
private var needsMeasure = true
|
|
124
137
|
private var measuredSize = IntSize.Zero
|
|
@@ -132,22 +145,20 @@ private class PeachComposeMount(
|
|
|
132
145
|
private var pixels: Bitmap? = null
|
|
133
146
|
private var size = IntSize.Zero
|
|
134
147
|
|
|
135
|
-
|
|
148
|
+
fun setContent(content: @Composable () -> Unit) {
|
|
136
149
|
try {
|
|
137
|
-
scene.setContent {
|
|
138
|
-
PeachAndroidFonts(fonts) {
|
|
139
|
-
dispatch.Content(view, props) { name, args ->
|
|
140
|
-
events.add(JSONObject().put("name", name).put("args", args))
|
|
141
|
-
invalidate(false)
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
150
|
+
scene.setContent { PeachAndroidFonts(fonts, content) }
|
|
145
151
|
} catch (failure: Throwable) {
|
|
146
152
|
close()
|
|
147
153
|
throw failure
|
|
148
154
|
}
|
|
149
155
|
}
|
|
150
156
|
|
|
157
|
+
fun emit(name: String, args: Any?) {
|
|
158
|
+
events.add(JSONObject().put("name", name).put("args", args))
|
|
159
|
+
invalidate(false)
|
|
160
|
+
}
|
|
161
|
+
|
|
151
162
|
fun measure(): IntSize {
|
|
152
163
|
val measured = scene.measureContent(constraints)
|
|
153
164
|
scene.size = measured
|
|
@@ -159,7 +170,7 @@ private class PeachComposeMount(
|
|
|
159
170
|
|
|
160
171
|
fun frame(nanos: Long): String {
|
|
161
172
|
Snapshot.sendApplyNotifications()
|
|
162
|
-
recomposer.performFrame(nanos)
|
|
173
|
+
if (clockRunning()) recomposer.performFrame(nanos)
|
|
163
174
|
val measured = if (needsMeasure) measure() else measuredSize
|
|
164
175
|
// a zero-size composition still runs its effects and reports its size;
|
|
165
176
|
// its backing surface uses one pixel until layout gives it real bounds.
|
|
@@ -241,35 +252,45 @@ private class PeachComposeMount(
|
|
|
241
252
|
}
|
|
242
253
|
}
|
|
243
254
|
|
|
244
|
-
|
|
255
|
+
internal val mounts = mutableMapOf<Int, PeachComposeMount>()
|
|
245
256
|
private var nextMount = 1
|
|
246
257
|
private var deviceLocale: String? = null
|
|
247
258
|
|
|
259
|
+
internal fun addMount(mount: PeachComposeMount): Int = nextMount++.also { mounts[it] = mount }
|
|
260
|
+
|
|
261
|
+
// the process-wide device setup every scene shares: skiko's platform, the
|
|
262
|
+
// host OS and the device locale, fixed for the life of the process.
|
|
263
|
+
internal fun preparePeachDevice(locale: String) {
|
|
264
|
+
registerSkikoComposeImplementation()
|
|
265
|
+
if (deviceLocale == null) {
|
|
266
|
+
setHostOsProvider { OS.Android }
|
|
267
|
+
val locales = LocaleList(locale)
|
|
268
|
+
setPlatformLocaleProvider { locales }
|
|
269
|
+
deviceLocale = locale
|
|
270
|
+
}
|
|
271
|
+
require(deviceLocale == locale) { "device locale changes require a new Kotlin process" }
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
internal fun robotoFamily(regular: String, medium: String, bold: String) = FontFamily(
|
|
275
|
+
Font("Roboto-400", Base64.decode(regular), FontWeight.Normal),
|
|
276
|
+
Font("Roboto-500", Base64.decode(medium), FontWeight.Medium),
|
|
277
|
+
Font("Roboto-700", Base64.decode(bold), FontWeight.Bold),
|
|
278
|
+
)
|
|
279
|
+
|
|
248
280
|
@JsExport
|
|
249
281
|
fun peachViewMount(source: String, view: String, hash: String, props: String,
|
|
250
282
|
density: Float, fontScale: Float, locale: String, backend: String, stencilBits: Int,
|
|
251
283
|
regular: String, medium: String, bold: String, invalidate: (Boolean) -> Unit): String {
|
|
252
284
|
try {
|
|
253
|
-
registerSkikoComposeImplementation()
|
|
254
285
|
val dispatch = peachViewDispatcher(source)
|
|
255
286
|
if (dispatch.contractHash != hash) return JSONObject().put("ok", false).put("code", "E_NATIVE_SOURCE_REBUILD")
|
|
256
287
|
.put("error", "composable source $source changed; rebuild the app").toString()
|
|
257
288
|
require(backend == "graphite" || backend == "raster")
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
264
|
-
require(deviceLocale == locale) { "device locale changes require a new Kotlin process" }
|
|
265
|
-
val fonts = FontFamily(
|
|
266
|
-
Font("Roboto-400", Base64.decode(regular), FontWeight.Normal),
|
|
267
|
-
Font("Roboto-500", Base64.decode(medium), FontWeight.Medium),
|
|
268
|
-
Font("Roboto-700", Base64.decode(bold), FontWeight.Bold),
|
|
269
|
-
)
|
|
270
|
-
val id = nextMount++
|
|
271
|
-
mounts[id] = PeachComposeMount(dispatch, view, props, density, fontScale, locale, backend == "graphite", stencilBits, fonts, invalidate)
|
|
272
|
-
return JSONObject().put("ok", true).put("id", id).toString()
|
|
289
|
+
preparePeachDevice(locale)
|
|
290
|
+
val mount = PeachComposeMount(density, fontScale, locale, backend == "graphite", stencilBits, robotoFamily(regular, medium, bold), invalidate)
|
|
291
|
+
mount.props = props
|
|
292
|
+
mount.setContent { dispatch.Content(view, mount.props, mount::emit) }
|
|
293
|
+
return JSONObject().put("ok", true).put("id", addMount(mount)).toString()
|
|
273
294
|
} catch (error: Throwable) {
|
|
274
295
|
return JSONObject().put("ok", false).put("code", "E_NATIVE_SOURCE").put("error", "$source.$view: ${error.message}").toString()
|
|
275
296
|
}
|