peachsim 0.1.648 → 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.
Files changed (132) hide show
  1. package/README.md +10 -9
  2. package/cli/command-registry.ts +2 -1
  3. package/cli/commands/guest-host.ts +67 -0
  4. package/cli/commands/kotlin.ts +231 -0
  5. package/cli/commands/project-report.ts +1 -1
  6. package/cli/commands/report-issue.ts +55 -32
  7. package/cli/commands/setup.ts +10 -14
  8. package/cli/commands/support.ts +399 -0
  9. package/cli/commands/swift.ts +6 -52
  10. package/cli/commands/telemetry.ts +10 -13
  11. package/cli/main.ts +22 -10
  12. package/cli/outbound-endpoints.ts +23 -2
  13. package/cli/privacy.ts +23 -25
  14. package/cli/runtime-notes.ts +3 -2
  15. package/cli/telemetry.ts +109 -14
  16. package/dist-lib/agent-daemon-client.cjs +1 -1
  17. package/dist-lib/agent-events.cjs +1 -1
  18. package/dist-lib/agent-identity.cjs +1 -1
  19. package/dist-lib/agent-sessions.cjs +1 -1
  20. package/dist-lib/attached-projects.cjs +1 -1
  21. package/dist-lib/auth/shared-session.cjs +1 -1
  22. package/dist-lib/backend-origin.cjs +1 -1
  23. package/dist-lib/beta.cjs +1 -1
  24. package/dist-lib/beta.mjs +1 -1
  25. package/dist-lib/bridge-constants.cjs +1 -1
  26. package/dist-lib/bridge-contract-input.cjs +1 -1
  27. package/dist-lib/bridge-contract-input.mjs +1 -1
  28. package/dist-lib/bridge-contract.cjs +1 -1
  29. package/dist-lib/bridge-contract.mjs +1 -1
  30. package/dist-lib/capture-contract.cjs +1 -1
  31. package/dist-lib/capture-contract.mjs +1 -1
  32. package/dist-lib/cli-constants.cjs +1 -1
  33. package/dist-lib/cloud-contract.cjs +1 -1
  34. package/dist-lib/cloud-contract.mjs +1 -1
  35. package/dist-lib/cloud-test-args.cjs +1 -1
  36. package/dist-lib/cloud-test-args.mjs +1 -1
  37. package/dist-lib/cloud.cjs +1 -1
  38. package/dist-lib/cloud.mjs +1 -1
  39. package/dist-lib/config.cjs +1 -1
  40. package/dist-lib/detox/index.cjs +1 -1
  41. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  42. package/dist-lib/home-paths.cjs +3 -3
  43. package/dist-lib/host/bridge-host.cjs +1 -1
  44. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  45. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  46. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  47. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  48. package/dist-lib/host/websocket-proxy.cjs +1 -1
  49. package/dist-lib/index.cjs +1 -1
  50. package/dist-lib/jump-to-source-babel.cjs +1 -1
  51. package/dist-lib/jump-to-source-native.cjs +1 -1
  52. package/dist-lib/kotlin-host.cjs +102 -21
  53. package/dist-lib/menu.cjs +1 -1
  54. package/dist-lib/menu.mjs +1 -1
  55. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  56. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  57. package/dist-lib/metro-production-bundle.cjs +1 -1
  58. package/dist-lib/metro-production-bundle.mjs +1 -1
  59. package/dist-lib/metro.cjs +1 -1
  60. package/dist-lib/native-app-identity.cjs +1 -1
  61. package/dist-lib/native-app-identity.mjs +1 -1
  62. package/dist-lib/profiles.cjs +1 -1
  63. package/dist-lib/public-brand.cjs +1 -1
  64. package/dist-lib/react-native-host-modules.cjs +1 -1
  65. package/dist-lib/react-native-host-modules.mjs +1 -1
  66. package/dist-lib/render-mode.cjs +1 -1
  67. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  68. package/dist-lib/sdk.cjs +1 -1
  69. package/dist-lib/sdk.mjs +1 -1
  70. package/dist-lib/skills.cjs +28 -39
  71. package/dist-lib/swift-compile.cjs +6 -3
  72. package/dist-lib/swift-compile.mjs +6 -3
  73. package/dist-lib/swift-package.cjs +1 -1
  74. package/dist-lib/swift-package.mjs +1 -1
  75. package/dist-lib/swift-submit.cjs +1 -1
  76. package/dist-lib/swift-submit.mjs +1 -1
  77. package/dist-lib/swift.cjs +1 -1
  78. package/dist-lib/vite-swift.cjs +6 -3
  79. package/dist-lib/vite.cjs +1 -1
  80. package/kotlin/AppSources/android/app/Activity.kt +85 -0
  81. package/kotlin/AppSources/android/app/Application.kt +7 -0
  82. package/kotlin/AppSources/android/content/Context.kt +5 -0
  83. package/kotlin/AppSources/android/content/Intent.kt +16 -0
  84. package/kotlin/AppSources/android/os/Bundle.kt +5 -0
  85. package/kotlin/AppSources/android/util/SuperNotCalledException.kt +3 -0
  86. package/kotlin/AppSources/android/view/View.kt +72 -0
  87. package/kotlin/AppSources/android/view/Window.kt +44 -0
  88. package/kotlin/AppSources/android/widget/FrameLayout.kt +6 -0
  89. package/kotlin/AppSources/androidx/activity/ComponentActivity.kt +57 -0
  90. package/kotlin/AppSources/androidx/activity/compose/ComponentActivity.kt +29 -0
  91. package/kotlin/AppSources/androidx/compose/ui/platform/ComposeView.kt +67 -0
  92. package/kotlin/AppSources/dev/peach/app/PeachApp.kt +136 -0
  93. package/kotlin/AppSources/dev/peach/app/PeachWindowHost.kt +13 -0
  94. package/kotlin/ComposeSources/PeachComposeHost.kt +316 -0
  95. package/kotlin/Sources/dev/onejs/one/OneNativeSourceViewDispatch.kt +13 -0
  96. package/kotlin/Sources/org/json/JSON.kt +6 -0
  97. package/kotlin/fixtures/counter/Counter.d.kt.ts +16 -0
  98. package/kotlin/fixtures/counter/Counter.kt +18 -0
  99. package/kotlin/fixtures/counter/CounterApp.native.tsx +34 -0
  100. package/kotlin/fixtures/counter/CounterApp.tsx +3 -0
  101. package/kotlin/fixtures/counter/CounterScreen.d.kt.ts +7 -0
  102. package/kotlin/fixtures/counter/CounterScreen.kt +18 -3
  103. package/kotlin/fixtures/counter/app/_layout.tsx +4 -0
  104. package/kotlin/fixtures/counter/app/index+spa.tsx +1 -0
  105. package/kotlin/fixtures/counter/app/routes.d.ts +18 -0
  106. package/kotlin/fixtures/counter/package.json +7 -0
  107. package/kotlin/fixtures/counter/tsconfig.json +19 -0
  108. package/kotlin/fixtures/counter/vite.config.ts +10 -0
  109. package/kotlin/fixtures/headless-counter/Headless.kt +5 -0
  110. package/kotlin/fixtures/kotlin-app/app/build.gradle.kts +30 -0
  111. package/kotlin/fixtures/kotlin-app/app/src/main/AndroidManifest.xml +13 -0
  112. package/kotlin/fixtures/kotlin-app/app/src/main/java/dev/contrast/kotlinapp/MainActivity.kt +60 -0
  113. package/kotlin/fixtures/kotlin-app/app/src/main/java/dev/contrast/kotlinapp/ProbeApplication.kt +18 -0
  114. package/kotlin/fixtures/kotlin-app/build.gradle.kts +5 -0
  115. package/kotlin/fixtures/kotlin-app/gradle.properties +4 -0
  116. package/kotlin/fixtures/kotlin-app/settings.gradle.kts +4 -0
  117. package/kotlin/fork/NOTICE +7 -5
  118. package/kotlin/fork/README.md +41 -49
  119. package/kotlin/fork/build.sh +3 -2
  120. package/kotlin/fork/compose-worker-globals.patch +22 -11
  121. package/kotlin/fork/fork.json +2 -2
  122. package/kotlin/fork/publish.ts +30 -15
  123. package/kotlin/fork/skiko-worker-clock.patch +38 -0
  124. package/kotlin/toolchain.json +9 -9
  125. package/package.json +1 -1
  126. package/skills/contrast/SKILL.md +2 -1
  127. package/skills/peach-setup/SKILL.md +2 -2
  128. package/src/{swift-guest.ts → guest-bundle.ts} +22 -7
  129. package/src/home-paths.ts +5 -3
  130. package/src/kotlin-app.ts +411 -0
  131. package/src/kotlin-host.ts +148 -25
  132. package/src/swift-compile.ts +7 -2
@@ -0,0 +1,85 @@
1
+ package android.app
2
+
3
+ import android.content.ComponentName
4
+ import android.content.ContextWrapper
5
+ import android.content.Intent
6
+ import android.os.Bundle
7
+ import android.util.SuperNotCalledException
8
+ import android.view.View
9
+ import android.view.ViewGroup
10
+ import android.view.Window
11
+
12
+ // the cold-start half of Activity: ActivityThread attaches it, then drives the
13
+ // perform calls in its own order. each lifecycle method must call through to
14
+ // super, as Activity.mCalled enforces, and the post callbacks run after it
15
+ // returns, where ActivityLifecycleCallbacks see them.
16
+ open class Activity : ContextWrapper() {
17
+ private var called = false
18
+ private var attachedIntent: Intent? = null
19
+ private var attachedApplication: Application? = null
20
+ private var attachedWindow: Window? = null
21
+ private var attachedComponent: ComponentName? = null
22
+
23
+ val intent: Intent get() = checkNotNull(attachedIntent)
24
+ val application: Application get() = checkNotNull(attachedApplication)
25
+ val window: Window get() = checkNotNull(attachedWindow)
26
+
27
+ // the activity class that runs; launched through an alias, the intent
28
+ // still names the alias
29
+ val componentName: ComponentName get() = checkNotNull(attachedComponent)
30
+
31
+ internal fun attach(application: Application, component: ComponentName, intent: Intent, window: Window) {
32
+ attachedApplication = application
33
+ attachedComponent = component
34
+ attachedIntent = intent
35
+ attachedWindow = window
36
+ }
37
+
38
+ open fun setContentView(view: View) {
39
+ setContentView(view, ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
40
+ }
41
+
42
+ open fun setContentView(view: View, params: ViewGroup.LayoutParams) {
43
+ window.setContentView(view, params)
44
+ }
45
+
46
+ protected open fun onCreate(savedInstanceState: Bundle?) { called = true }
47
+ protected open fun onStart() { called = true }
48
+ protected open fun onPostCreate(savedInstanceState: Bundle?) { called = true }
49
+ protected open fun onResume() { called = true }
50
+ protected open fun onPostResume() { called = true }
51
+
52
+ // ActivityLifecycleCallbacks' post events; ComponentActivity's lifecycle
53
+ // moves on these, as its ReportFragment does from API 29.
54
+ internal open fun dispatchActivityPostCreated() {}
55
+ internal open fun dispatchActivityPostStarted() {}
56
+ internal open fun dispatchActivityPostResumed() {}
57
+
58
+ private fun require(method: String, call: () -> Unit) {
59
+ called = false
60
+ call()
61
+ if (!called) throw SuperNotCalledException(
62
+ "Activity {${componentName.flattenToShortString()}} did not call through to super.$method()"
63
+ )
64
+ }
65
+
66
+ internal fun performCreate(icicle: Bundle?) {
67
+ require("onCreate") { onCreate(icicle) }
68
+ dispatchActivityPostCreated()
69
+ }
70
+
71
+ internal fun performStart() {
72
+ require("onStart") { onStart() }
73
+ dispatchActivityPostStarted()
74
+ }
75
+
76
+ internal fun performPostCreate(icicle: Bundle?) {
77
+ require("onPostCreate") { onPostCreate(icicle) }
78
+ }
79
+
80
+ internal fun performResume() {
81
+ require("onResume") { onResume() }
82
+ require("onPostResume") { onPostResume() }
83
+ dispatchActivityPostResumed()
84
+ }
85
+ }
@@ -0,0 +1,7 @@
1
+ package android.app
2
+
3
+ import android.content.ContextWrapper
4
+
5
+ open class Application : ContextWrapper() {
6
+ open fun onCreate() {}
7
+ }
@@ -0,0 +1,5 @@
1
+ package android.content
2
+
3
+ abstract class Context
4
+
5
+ open class ContextWrapper : Context()
@@ -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,5 @@
1
+ package android.os
2
+
3
+ // a cold start has no saved instance state, so the launcher receives null;
4
+ // the type exists for the signatures that name it.
5
+ class Bundle
@@ -0,0 +1,3 @@
1
+ package android.util
2
+
3
+ class SuperNotCalledException(message: String) : RuntimeException(message)
@@ -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,6 @@
1
+ package android.widget
2
+
3
+ import android.content.Context
4
+ import android.view.ViewGroup
5
+
6
+ open class FrameLayout(context: Context) : ViewGroup(context)
@@ -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
+ }