peachsim 0.1.657 → 0.1.659

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.md +21 -2
  2. package/cli/app-config.ts +25 -9
  3. package/cli/bridge-flow-runner.ts +10 -23
  4. package/cli/commands/compat.ts +234 -10
  5. package/cli/commands/do-chain.ts +5 -1
  6. package/cli/commands/inspect/actions.ts +33 -76
  7. package/cli/commands/inspect/resolve-target.ts +67 -2
  8. package/cli/commands/inspect.ts +16 -1
  9. package/cli/drivers/playwright-provisioning.ts +95 -0
  10. package/cli/ws-bridge.ts +7 -7
  11. package/dist-lib/agent-daemon-client.cjs +1 -1
  12. package/dist-lib/agent-events.cjs +1 -1
  13. package/dist-lib/agent-identity.cjs +1 -1
  14. package/dist-lib/agent-sessions.cjs +1 -1
  15. package/dist-lib/attached-projects.cjs +1 -1
  16. package/dist-lib/auth/shared-session.cjs +1 -1
  17. package/dist-lib/backend-origin.cjs +1 -1
  18. package/dist-lib/beta.cjs +1 -1
  19. package/dist-lib/beta.mjs +1 -1
  20. package/dist-lib/bridge-constants.cjs +1 -1
  21. package/dist-lib/bridge-contract-input.cjs +9 -1
  22. package/dist-lib/bridge-contract-input.mjs +9 -1
  23. package/dist-lib/bridge-contract.cjs +1 -1
  24. package/dist-lib/bridge-contract.mjs +1 -1
  25. package/dist-lib/capture-contract.cjs +1 -1
  26. package/dist-lib/capture-contract.mjs +1 -1
  27. package/dist-lib/cli-constants.cjs +1 -1
  28. package/dist-lib/cloud-contract.cjs +1 -1
  29. package/dist-lib/cloud-contract.mjs +1 -1
  30. package/dist-lib/cloud-test-args.cjs +1 -1
  31. package/dist-lib/cloud-test-args.mjs +1 -1
  32. package/dist-lib/cloud.cjs +1 -1
  33. package/dist-lib/cloud.mjs +1 -1
  34. package/dist-lib/config.cjs +3 -2
  35. package/dist-lib/detox/index.cjs +2 -2
  36. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  37. package/dist-lib/home-paths.cjs +1 -1
  38. package/dist-lib/host/bridge-host.cjs +78 -35
  39. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  40. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  41. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  42. package/dist-lib/host/replacement-module-handler.cjs +51 -8
  43. package/dist-lib/host/websocket-proxy.cjs +1 -1
  44. package/dist-lib/index.cjs +317 -70
  45. package/dist-lib/jump-to-source-babel.cjs +1 -1
  46. package/dist-lib/jump-to-source-native.cjs +1 -1
  47. package/dist-lib/kotlin-host.cjs +14 -4
  48. package/dist-lib/menu.cjs +1 -1
  49. package/dist-lib/menu.mjs +1 -1
  50. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  51. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  52. package/dist-lib/metro-production-bundle.cjs +1 -1
  53. package/dist-lib/metro-production-bundle.mjs +1 -1
  54. package/dist-lib/metro.cjs +300 -29
  55. package/dist-lib/native-app-identity.cjs +58 -25
  56. package/dist-lib/native-app-identity.mjs +57 -25
  57. package/dist-lib/plist.cjs +64 -0
  58. package/dist-lib/plist.mjs +29 -0
  59. package/dist-lib/profiles.cjs +1 -1
  60. package/dist-lib/public-brand.cjs +1 -1
  61. package/dist-lib/react-native-host-modules.cjs +1 -1
  62. package/dist-lib/react-native-host-modules.mjs +1 -1
  63. package/dist-lib/render-mode.cjs +1 -1
  64. package/dist-lib/scripts/dev-server-scanner.cjs +2 -2
  65. package/dist-lib/sdk.cjs +36 -49
  66. package/dist-lib/sdk.mjs +36 -49
  67. package/dist-lib/skills.cjs +257 -138
  68. package/dist-lib/swift-compile.cjs +1 -1
  69. package/dist-lib/swift-compile.mjs +1 -1
  70. package/dist-lib/swift-package.cjs +1 -1
  71. package/dist-lib/swift-package.mjs +1 -1
  72. package/dist-lib/swift-submit.cjs +1 -1
  73. package/dist-lib/swift-submit.mjs +1 -1
  74. package/dist-lib/swift.cjs +1 -1
  75. package/dist-lib/vite-swift.cjs +1 -1
  76. package/dist-lib/vite.cjs +51 -8
  77. package/kotlin/AppSources/android/app/Activity.kt +44 -0
  78. package/kotlin/AppSources/android/app/UiModeManager.kt +9 -0
  79. package/kotlin/AppSources/android/content/Context.kt +9 -2
  80. package/kotlin/AppSources/android/content/res/Configuration.kt +16 -0
  81. package/kotlin/AppSources/android/content/res/Resources.kt +7 -0
  82. package/kotlin/AppSources/android/graphics/Color.kt +13 -0
  83. package/kotlin/AppSources/android/os/Bundle.kt +9 -3
  84. package/kotlin/AppSources/android/view/View.kt +2 -0
  85. package/kotlin/AppSources/android/view/Window.kt +99 -8
  86. package/kotlin/AppSources/android/view/WindowInsetsController.kt +12 -0
  87. package/kotlin/AppSources/android/view/WindowManager.kt +16 -0
  88. package/kotlin/AppSources/androidx/activity/BackEventCompat.kt +17 -0
  89. package/kotlin/AppSources/androidx/activity/Cancellable.kt +5 -0
  90. package/kotlin/AppSources/androidx/activity/ComponentActivity.kt +22 -3
  91. package/kotlin/AppSources/androidx/activity/EdgeToEdge.kt +71 -0
  92. package/kotlin/AppSources/androidx/activity/OnBackPressedCallback.kt +28 -0
  93. package/kotlin/AppSources/androidx/activity/OnBackPressedDispatcher.kt +144 -0
  94. package/kotlin/AppSources/androidx/activity/OnBackPressedDispatcherOwner.kt +7 -0
  95. package/kotlin/AppSources/androidx/activity/compose/BackHandler.kt +55 -0
  96. package/kotlin/AppSources/androidx/core/view/WindowCompat.kt +32 -0
  97. package/kotlin/AppSources/dev/peach/app/PeachApp.kt +67 -12
  98. package/kotlin/AppSources/dev/peach/app/PeachWindowHost.kt +2 -1
  99. package/kotlin/AppSources/java/lang/Runnable.kt +6 -0
  100. package/kotlin/ComposeSources/PeachComposeHost.kt +11 -1
  101. package/package.json +8 -1
  102. package/skills/peach-setup/SKILL.md +27 -0
  103. package/src/android-theme.ts +167 -0
  104. package/src/android-xml.ts +70 -0
  105. package/src/bridge-contract-input.ts +10 -1
  106. package/src/bridge-contract.ts +7 -1
  107. package/src/config.ts +8 -0
  108. package/src/generated/android-framework-themes.json +6303 -0
  109. package/src/host/replacement-module-handler.ts +33 -0
  110. package/src/index.ts +1 -0
  111. package/src/kotlin-app.ts +86 -61
  112. package/src/kotlin-host.ts +13 -3
  113. package/src/metro-plugin.ts +46 -3
  114. package/src/native-app-identity.ts +75 -26
  115. package/src/sim-client.ts +2 -4
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.657 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.659 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.657 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.659 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // src/swift-compile.ts
4
4
  import { execFile as execFile2 } from "node:child_process";
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.657 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.659 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.657 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.659 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // src/swift-package.ts
4
4
  function isSwiftManifestFile(file) {
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.657 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.659 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.657 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.659 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // src/swift-submit.ts
4
4
  var SWIFT_COMPILE_PATH = "/api/swift-compile";
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.657 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.659 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.657 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.659 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
package/dist-lib/vite.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.657 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.659 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -184,6 +184,7 @@ function isDaemonLockfileFresh(lock, now = Date.now()) {
184
184
  }
185
185
 
186
186
  // src/host/replacement-module-handler.ts
187
+ var import_node_fs3 = require("node:fs");
187
188
  var import_promises = __toESM(require("node:fs/promises"), 1);
188
189
  var import_node_path3 = __toESM(require("node:path"), 1);
189
190
 
@@ -313,12 +314,15 @@ function transformsFor(filePath) {
313
314
  );
314
315
  }
315
316
  }
317
+ function isLocalSpecifier(specifier) {
318
+ return specifier.startsWith("./") || specifier.startsWith("../");
319
+ }
316
320
  function isContrastKitSpecifier(specifier) {
317
321
  return specifier === "contrast-kit" || specifier.startsWith("contrast-kit/");
318
322
  }
319
323
  function unsupportedSpecifier(specifier) {
320
324
  return new Error(
321
- `replacement modules may only import react, react-native, contrast-kit, and the packages rnx stubs for this app; '${specifier}' is none of those. vendor the code into the replacement file, or add the package to peach.config so the simulator bridges it.`
325
+ `replacement modules may only import react, react-native, contrast-kit, the packages rnx stubs for this app, and other project files by relative path; '${specifier}' is none of those. vendor the code into a project file, or add the package to peach.config so the simulator bridges it.`
322
326
  );
323
327
  }
324
328
  function bridgeDeclaration(name, specifier) {
@@ -343,11 +347,9 @@ function compileReplacementModule(input) {
343
347
  const transformed = (0, import_dist.transform)(input.source, {
344
348
  transforms: transformsFor(input.filePath),
345
349
  filePath: input.filePath,
346
- // classic runtime, matching what the app's own Metro build does for a file
347
- // that has no automatic-runtime pragma: the file imports React itself.
348
- jsxRuntime: "classic",
349
- jsxPragma: "React.createElement",
350
- jsxFragmentPragma: "React.Fragment",
350
+ // the automatic runtime, as @react-native/babel-preset and babel-preset-expo
351
+ // compile the app's own files: jsx needs no React import.
352
+ jsxRuntime: "automatic",
351
353
  production: true,
352
354
  sourceMapOptions: { compiledFilename: input.filePath }
353
355
  });
@@ -374,6 +376,13 @@ function compileReplacementModule(input) {
374
376
  return name;
375
377
  };
376
378
  const edits = [];
379
+ const pointAtLocalModule = (specifierToken, specifier) => {
380
+ edits.push({
381
+ start: specifierToken.start,
382
+ end: specifierToken.end,
383
+ text: JSON.stringify(input.localModuleUrl(specifier))
384
+ });
385
+ };
377
386
  for (let index = 0; index < tokens.length; index++) {
378
387
  const token = tokens[index];
379
388
  if (token.type === EXPORT_KEYWORD) {
@@ -393,6 +402,10 @@ function compileReplacementModule(input) {
393
402
  }
394
403
  if (fromAt > 0 && tokens[fromAt] && read(tokens[fromAt]) === "from" && tokens[fromAt + 1]?.type === import_types.TokenType.string) {
395
404
  const specifier2 = readSpecifier(tokens[fromAt + 1]);
405
+ if (isLocalSpecifier(specifier2)) {
406
+ pointAtLocalModule(tokens[fromAt + 1], specifier2);
407
+ continue;
408
+ }
396
409
  throw new Error(
397
410
  `a replacement module cannot re-export from another module, and this one re-exports from '${specifier2}'. import what you need and export the binding.`
398
411
  );
@@ -445,6 +458,10 @@ function compileReplacementModule(input) {
445
458
  const specifierToken = tokens[cursor];
446
459
  if (!specifierToken) throw new Error("replacement module import is unterminated");
447
460
  const specifier = readSpecifier(specifierToken);
461
+ if (isLocalSpecifier(specifier)) {
462
+ pointAtLocalModule(specifierToken, specifier);
463
+ continue;
464
+ }
448
465
  if (specifier !== "react" && specifier !== "react-native" && !JSX_RUNTIME_SPECIFIERS.has(specifier) && !isContrastKitSpecifier(specifier) && !input.runtimePackages.has(specifier)) {
449
466
  throw unsupportedSpecifier(specifier);
450
467
  }
@@ -501,6 +518,25 @@ function sendJavaScript(req, res, source) {
501
518
  });
502
519
  res.end(req.method === "HEAD" ? void 0 : source);
503
520
  }
521
+ function resolveLocalImport(fromFile, specifier) {
522
+ const base = import_node_path3.default.resolve(import_node_path3.default.dirname(fromFile), specifier);
523
+ const candidates = [
524
+ ...REPLACEMENT_MODULE_EXTENSIONS.some(
525
+ (ext) => ext === import_node_path3.default.extname(base).toLowerCase()
526
+ ) ? [base] : [],
527
+ ...REPLACEMENT_MODULE_EXTENSIONS.map((ext) => base + ext),
528
+ ...REPLACEMENT_MODULE_EXTENSIONS.map((ext) => import_node_path3.default.join(base, `index${ext}`))
529
+ ];
530
+ const found = candidates.find(
531
+ (candidate) => (0, import_node_fs3.statSync)(candidate, { throwIfNoEntry: false })?.isFile()
532
+ );
533
+ if (!found) {
534
+ throw new Error(
535
+ `'${specifier}' imported by ${fromFile} matches no ${REPLACEMENT_MODULE_EXTENSIONS.join(", ")} file`
536
+ );
537
+ }
538
+ return found;
539
+ }
504
540
  async function handleReplacementModuleRequest(req, res, options = {}) {
505
541
  const rawUrl = req.url || "";
506
542
  let url;
@@ -582,7 +618,14 @@ async function handleReplacementModuleRequest(req, res, options = {}) {
582
618
  const compiled = compileReplacementModule({
583
619
  filePath: resolvedTargetPath,
584
620
  source: await import_promises.default.readFile(resolvedTargetPath, "utf8"),
585
- runtimePackages
621
+ runtimePackages,
622
+ // the imported file's url is this request's with only its path changed,
623
+ // so the token, cache key and runtime packages carry over.
624
+ localModuleUrl: (specifier) => {
625
+ const local = new URL(url.href);
626
+ local.searchParams.set("path", resolveLocalImport(targetPath, specifier));
627
+ return `${local.pathname}${local.search}`;
628
+ }
586
629
  });
587
630
  const map = Buffer.from(JSON.stringify(compiled.sourceMap), "utf8").toString("base64");
588
631
  sendJavaScript(
@@ -48,12 +48,30 @@ open class Activity : ContextWrapper() {
48
48
  protected open fun onPostCreate(savedInstanceState: Bundle?) { called = true }
49
49
  protected open fun onResume() { called = true }
50
50
  protected open fun onPostResume() { called = true }
51
+ protected open fun onPause() { called = true }
52
+ protected open fun onStop() { called = true }
53
+ protected open fun onRestart() { called = true }
54
+
55
+ // Activity's own save writes the window's view hierarchy state; the decor
56
+ // window here holds no view state of its own
57
+ protected open fun onSaveInstanceState(outState: Bundle) {}
58
+
59
+ // a back the window's callbacks left: the activity asks the task manager,
60
+ // which moves a root launcher task to the back rather than finishing it
61
+ // (from Android 12). the only activity Peach runs is that task's root.
62
+ open fun onBackPressed() {
63
+ movedTaskToBack = true
64
+ }
65
+
66
+ internal var movedTaskToBack = false
51
67
 
52
68
  // ActivityLifecycleCallbacks' post events; ComponentActivity's lifecycle
53
69
  // moves on these, as its ReportFragment does from API 29.
54
70
  internal open fun dispatchActivityPostCreated() {}
55
71
  internal open fun dispatchActivityPostStarted() {}
56
72
  internal open fun dispatchActivityPostResumed() {}
73
+ internal open fun dispatchActivityPrePaused() {}
74
+ internal open fun dispatchActivityPreStopped() {}
57
75
 
58
76
  private fun require(method: String, call: () -> Unit) {
59
77
  called = false
@@ -77,6 +95,32 @@ open class Activity : ContextWrapper() {
77
95
  require("onPostCreate") { onPostCreate(icicle) }
78
96
  }
79
97
 
98
+ internal fun performPause() {
99
+ dispatchActivityPrePaused()
100
+ require("onPause") { onPause() }
101
+ }
102
+
103
+ private var stopped = false
104
+
105
+ internal fun performStop() {
106
+ dispatchActivityPreStopped()
107
+ require("onStop") { onStop() }
108
+ stopped = true
109
+ }
110
+
111
+ internal fun performSaveInstanceState(outState: Bundle) {
112
+ onSaveInstanceState(outState)
113
+ }
114
+
115
+ // ActivityThread brings a stopped activity back with onRestart, then
116
+ // onStart; one that never stopped has nothing to restart
117
+ internal fun performRestart() {
118
+ if (!stopped) return
119
+ stopped = false
120
+ require("onRestart") { onRestart() }
121
+ performStart()
122
+ }
123
+
80
124
  internal fun performResume() {
81
125
  require("onResume") { onResume() }
82
126
  require("onPostResume") { onPostResume() }
@@ -0,0 +1,9 @@
1
+ package android.app
2
+
3
+ class UiModeManager private constructor() {
4
+ companion object {
5
+ const val MODE_NIGHT_AUTO = 0
6
+ const val MODE_NIGHT_NO = 1
7
+ const val MODE_NIGHT_YES = 2
8
+ }
9
+ }
@@ -1,5 +1,12 @@
1
1
  package android.content
2
2
 
3
- abstract class Context
3
+ import android.content.res.Resources
4
+ import android.content.res.processResources
4
5
 
5
- open class ContextWrapper : Context()
6
+ abstract class Context {
7
+ abstract val resources: Resources
8
+ }
9
+
10
+ open class ContextWrapper : Context() {
11
+ override val resources: Resources get() = processResources
12
+ }
@@ -0,0 +1,16 @@
1
+ package android.content.res
2
+
3
+ // the device configuration an app reads through its resources: a phone's
4
+ // normal ui mode type, night or not as the device's dark theme is set
5
+ class Configuration {
6
+ var uiMode: Int = UI_MODE_TYPE_NORMAL or UI_MODE_NIGHT_NO
7
+
8
+ companion object {
9
+ const val UI_MODE_TYPE_MASK = 0x0f
10
+ const val UI_MODE_TYPE_NORMAL = 0x01
11
+ const val UI_MODE_NIGHT_MASK = 0x30
12
+ const val UI_MODE_NIGHT_UNDEFINED = 0x00
13
+ const val UI_MODE_NIGHT_NO = 0x10
14
+ const val UI_MODE_NIGHT_YES = 0x20
15
+ }
16
+ }
@@ -0,0 +1,7 @@
1
+ package android.content.res
2
+
3
+ class Resources internal constructor(val configuration: Configuration)
4
+
5
+ // the app process's resources, set once the device configuration is known at
6
+ // cold start; every context in the process reads the same configuration
7
+ internal lateinit var processResources: Resources
@@ -0,0 +1,13 @@
1
+ package android.graphics
2
+
3
+ // the packed ARGB ints android.graphics.Color names and builds
4
+ object Color {
5
+ const val BLACK = -0x1000000
6
+ const val WHITE = -0x1
7
+ const val TRANSPARENT = 0
8
+
9
+ fun argb(alpha: Int, red: Int, green: Int, blue: Int): Int =
10
+ (alpha shl 24) or (red shl 16) or (green shl 8) or blue
11
+
12
+ fun alpha(color: Int): Int = color ushr 24
13
+ }
@@ -1,5 +1,11 @@
1
1
  package android.os
2
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
3
+ import androidx.savedstate.SavedState
4
+ import androidx.savedstate.savedState
5
+
6
+ // on Android androidx's SavedState is Bundle; here a Bundle holds the
7
+ // multiplatform SavedState that a save writes into. a cold start has no saved
8
+ // instance state, so the launcher receives null.
9
+ class Bundle internal constructor(internal val state: SavedState) {
10
+ constructor() : this(savedState())
11
+ }
@@ -1,8 +1,10 @@
1
1
  package android.view
2
2
 
3
3
  import android.content.Context
4
+ import android.content.res.Resources
4
5
 
5
6
  open class View(val context: Context) {
7
+ val resources: Resources get() = context.resources
6
8
  var parent: ViewGroup? = null
7
9
  internal set
8
10
  var layoutParams: ViewGroup.LayoutParams? = null
@@ -1,33 +1,124 @@
1
1
  package android.view
2
2
 
3
3
  import android.content.Context
4
+ import android.graphics.Color
4
5
  import android.widget.FrameLayout
5
6
  import androidx.compose.runtime.getValue
6
7
  import androidx.compose.runtime.mutableStateOf
7
8
  import androidx.compose.runtime.setValue
8
9
  import dev.peach.app.PeachWindowHost
10
+ import org.json.JSONObject
11
+
12
+ // what PhoneWindow.generateLayout read from the activity's theme
13
+ internal class WindowTheme(
14
+ val edgeToEdgeEnforced: Boolean,
15
+ val statusBarColor: Int,
16
+ val statusBarContrastEnforced: Boolean,
17
+ val navigationBarContrastEnforced: Boolean,
18
+ val lightStatusBar: Boolean,
19
+ val lightNavigationBar: Boolean,
20
+ )
21
+
22
+ // DecorView's light contrast scrim behind a transparent bar
23
+ private const val SCRIM_LIGHT = -0x19000001
9
24
 
10
25
  // PhoneWindow's content parent (android.R.id.content) inside its decor. the
11
26
  // window is added, and its views attached, after the activity resumes.
12
27
  class Window internal constructor(
13
28
  context: Context,
14
- private val edgeToEdgeEnforced: Boolean,
29
+ theme: WindowTheme,
15
30
  internal val host: PeachWindowHost,
16
31
  ) {
32
+ private val edgeToEdgeEnforced = theme.edgeToEdgeEnforced
17
33
  internal val contentParent = FrameLayout(context)
18
34
  val decorView: View get() = contentParent
35
+ val attributes = WindowManager.LayoutParams()
19
36
 
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
37
+ // PhoneWindow fits system windows until told otherwise; an enforced edge
38
+ // to edge window never does, and ignores the request
39
+ private var fits by mutableStateOf(!edgeToEdgeEnforced)
40
+ internal val fitsSystemWindows: Boolean get() = fits
24
41
 
25
42
  fun setDecorFitsSystemWindows(decorFitsSystemWindows: Boolean) {
26
- val before = fitsSystemWindows
27
- this.decorFitsSystemWindows = decorFitsSystemWindows
28
- if (fitsSystemWindows != before) host.windowChanged()
43
+ if (edgeToEdgeEnforced || fits == decorFitsSystemWindows) return
44
+ fits = decorFitsSystemWindows
45
+ host.windowChanged()
29
46
  }
30
47
 
48
+ fun decorFitsSystemWindows(): Boolean = fits
49
+
50
+ // the theme's statusBarColor, transparent while edge to edge is enforced,
51
+ // where setting it does nothing
52
+ private var statusBar = if (edgeToEdgeEnforced) Color.TRANSPARENT else theme.statusBarColor
53
+
54
+ var statusBarColor: Int
55
+ get() = statusBar
56
+ set(color) {
57
+ if (edgeToEdgeEnforced || color == statusBar) return
58
+ statusBar = color
59
+ host.windowChanged()
60
+ }
61
+
62
+ fun getStatusBarColor(): Int = statusBarColor
63
+ fun setStatusBarColor(color: Int) { statusBarColor = color }
64
+
65
+ // the theme's navigation bar color resolves through device configuration
66
+ // peach kotlin does not model, so it reads only once the app sets it; an
67
+ // enforced edge to edge window reports transparent
68
+ private var navigationBar: Int? = null
69
+
70
+ var navigationBarColor: Int
71
+ get() = if (edgeToEdgeEnforced) Color.TRANSPARENT
72
+ else checkNotNull(navigationBar) { "the theme's navigation bar color is not modeled; read it after setting it" }
73
+ set(color) { navigationBar = color }
74
+
75
+ fun getNavigationBarColor(): Int = navigationBarColor
76
+ fun setNavigationBarColor(color: Int) { navigationBarColor = color }
77
+
78
+ var isStatusBarContrastEnforced: Boolean = theme.statusBarContrastEnforced
79
+ set(value) {
80
+ if (field == value) return
81
+ field = value
82
+ host.windowChanged()
83
+ }
84
+
85
+ var isNavigationBarContrastEnforced: Boolean = theme.navigationBarContrastEnforced
86
+
87
+ fun setStatusBarContrastEnforced(enforced: Boolean) { isStatusBarContrastEnforced = enforced }
88
+ fun setNavigationBarContrastEnforced(enforced: Boolean) { isNavigationBarContrastEnforced = enforced }
89
+
90
+ val insetsController: WindowInsetsController = object : WindowInsetsController {
91
+ private var appearance =
92
+ (if (theme.lightStatusBar) WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS else 0) or
93
+ (if (theme.lightNavigationBar) WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS else 0)
94
+
95
+ override val systemBarsAppearance: Int get() = appearance
96
+
97
+ override fun setSystemBarsAppearance(appearance: Int, mask: Int) {
98
+ val next = (this.appearance and mask.inv()) or (appearance and mask)
99
+ if (next == this.appearance) return
100
+ this.appearance = next
101
+ host.windowChanged()
102
+ }
103
+ }
104
+
105
+ // DecorView.calculateBarColor for the status bar of a window that draws
106
+ // its bar backgrounds: a transparent color under enforced contrast shows
107
+ // the scrim for the bar's appearance
108
+ private fun drawnStatusBarColor(): Int {
109
+ if (!isStatusBarContrastEnforced || Color.alpha(statusBar) != 0) return statusBar
110
+ check(insetsController.systemBarsAppearance and WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS != 0) {
111
+ "a dark status bar's contrast scrim is the device's dynamic system_bar_background_semi_transparent, which peach kotlin does not model"
112
+ }
113
+ return SCRIM_LIGHT
114
+ }
115
+
116
+ // what the decor draws for this window: its inset policy and its status
117
+ // bar color as unsigned ARGB
118
+ internal fun describe(): JSONObject = JSONObject()
119
+ .put("decorFitsSystemWindows", fits)
120
+ .put("statusBarColor", drawnStatusBarColor().toLong() and 0xffffffffL)
121
+
31
122
  // Activity.setContentView(View) fills the content parent
32
123
  internal fun setContentView(view: View, params: ViewGroup.LayoutParams) {
33
124
  contentParent.removeAllViews()
@@ -0,0 +1,12 @@
1
+ package android.view
2
+
3
+ // the window's requested system bar appearance: light bars ask for dark icons
4
+ interface WindowInsetsController {
5
+ fun setSystemBarsAppearance(appearance: Int, mask: Int)
6
+ val systemBarsAppearance: Int
7
+
8
+ companion object {
9
+ const val APPEARANCE_LIGHT_STATUS_BARS = 1 shl 3
10
+ const val APPEARANCE_LIGHT_NAVIGATION_BARS = 1 shl 4
11
+ }
12
+ }
@@ -0,0 +1,16 @@
1
+ package android.view
2
+
3
+ interface WindowManager {
4
+ // the window attributes edge to edge code sets; the cutout mode is held as
5
+ // the app sets it
6
+ class LayoutParams {
7
+ var layoutInDisplayCutoutMode: Int = LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
8
+
9
+ companion object {
10
+ const val LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT = 0
11
+ const val LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES = 1
12
+ const val LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER = 2
13
+ const val LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS = 3
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,17 @@
1
+ package androidx.activity
2
+
3
+ // androidx.activity's BackEventCompat: one predictive back gesture sample
4
+ class BackEventCompat(
5
+ val touchX: Float,
6
+ val touchY: Float,
7
+ val progress: Float,
8
+ val swipeEdge: Int,
9
+ ) {
10
+ override fun toString(): String =
11
+ "BackEventCompat{touchX=$touchX, touchY=$touchY, progress=$progress, swipeEdge=$swipeEdge}"
12
+
13
+ companion object {
14
+ const val EDGE_LEFT = 0
15
+ const val EDGE_RIGHT = 1
16
+ }
17
+ }
@@ -0,0 +1,5 @@
1
+ package androidx.activity
2
+
3
+ internal interface Cancellable {
4
+ fun cancel()
5
+ }
@@ -21,10 +21,11 @@ import androidx.savedstate.SavedStateRegistryOwner
21
21
 
22
22
  // androidx.activity's ComponentActivity owners, on the multiplatform
23
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.
24
+ // constructor and restores in onCreate before super, the lifecycle moves up on
25
+ // the post callbacks and down on the pre callbacks, as ReportFragment does from
26
+ // API 29, and destroy clears the view model store.
26
27
  open class ComponentActivity : Activity(), LifecycleOwner, ViewModelStoreOwner,
27
- HasDefaultViewModelProviderFactory, SavedStateRegistryOwner {
28
+ HasDefaultViewModelProviderFactory, SavedStateRegistryOwner, OnBackPressedDispatcherOwner {
28
29
  private val lifecycleRegistry = LifecycleRegistry(this)
29
30
  private val savedStateRegistryController = SavedStateRegistryController.create(this)
30
31
 
@@ -51,7 +52,25 @@ open class ComponentActivity : Activity(), LifecycleOwner, ViewModelStoreOwner,
51
52
  super.onCreate(savedInstanceState)
52
53
  }
53
54
 
55
+ override fun onSaveInstanceState(outState: Bundle) {
56
+ lifecycleRegistry.currentState = Lifecycle.State.CREATED
57
+ super.onSaveInstanceState(outState)
58
+ savedStateRegistryController.performSave(outState.state)
59
+ }
60
+
61
+ // the dispatcher gets the back first; without an enabled callback it falls
62
+ // back to Activity's own
63
+ override fun onBackPressed() {
64
+ onBackPressedDispatcher.onBackPressed()
65
+ }
66
+
67
+ final override val onBackPressedDispatcher: OnBackPressedDispatcher by lazy {
68
+ OnBackPressedDispatcher { super@ComponentActivity.onBackPressed() }
69
+ }
70
+
54
71
  override fun dispatchActivityPostCreated() = lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
55
72
  override fun dispatchActivityPostStarted() = lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
56
73
  override fun dispatchActivityPostResumed() = lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
74
+ override fun dispatchActivityPrePaused() = lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
75
+ override fun dispatchActivityPreStopped() = lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
57
76
  }
@@ -0,0 +1,71 @@
1
+ package androidx.activity
2
+
3
+ import android.app.UiModeManager
4
+ import android.content.res.Configuration
5
+ import android.content.res.Resources
6
+ import android.graphics.Color
7
+ import android.view.WindowManager
8
+ import androidx.core.view.WindowCompat
9
+ import androidx.core.view.WindowInsetsControllerCompat
10
+
11
+ // androidx.activity 1.9.3's EdgeToEdge as it runs on Peach's API 37 device,
12
+ // where it takes EdgeToEdgeApi30: the decor stops fitting system windows, the
13
+ // bars take each style's scrim (transparent for auto styles), status bar
14
+ // contrast is off, navigation bar contrast follows an auto style, the bars'
15
+ // appearance follows dark mode and the window lays out into every cutout.
16
+
17
+ // DecorView's light scrim and the platform's dark scrim
18
+ internal val DefaultLightScrim = Color.argb(0xe6, 0xFF, 0xFF, 0xFF)
19
+ internal val DefaultDarkScrim = Color.argb(0x80, 0x1b, 0x1b, 0x1b)
20
+
21
+ fun ComponentActivity.enableEdgeToEdge(
22
+ statusBarStyle: SystemBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT),
23
+ navigationBarStyle: SystemBarStyle = SystemBarStyle.auto(DefaultLightScrim, DefaultDarkScrim),
24
+ ) {
25
+ val view = window.decorView
26
+ val statusBarIsDark = statusBarStyle.detectDarkMode(view.resources)
27
+ val navigationBarIsDark = navigationBarStyle.detectDarkMode(view.resources)
28
+ WindowCompat.setDecorFitsSystemWindows(window, false)
29
+ window.statusBarColor = statusBarStyle.getScrimWithEnforcedContrast(statusBarIsDark)
30
+ window.navigationBarColor = navigationBarStyle.getScrimWithEnforcedContrast(navigationBarIsDark)
31
+ window.isStatusBarContrastEnforced = false
32
+ window.isNavigationBarContrastEnforced = navigationBarStyle.nightMode == UiModeManager.MODE_NIGHT_AUTO
33
+ WindowInsetsControllerCompat(window, view).run {
34
+ isAppearanceLightStatusBars = !statusBarIsDark
35
+ isAppearanceLightNavigationBars = !navigationBarIsDark
36
+ }
37
+ window.attributes.layoutInDisplayCutoutMode =
38
+ WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
39
+ }
40
+
41
+ class SystemBarStyle private constructor(
42
+ private val lightScrim: Int,
43
+ internal val darkScrim: Int,
44
+ internal val nightMode: Int,
45
+ internal val detectDarkMode: (Resources) -> Boolean,
46
+ ) {
47
+ companion object {
48
+ fun auto(
49
+ lightScrim: Int,
50
+ darkScrim: Int,
51
+ detectDarkMode: (Resources) -> Boolean = { resources ->
52
+ (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) ==
53
+ Configuration.UI_MODE_NIGHT_YES
54
+ },
55
+ ): SystemBarStyle = SystemBarStyle(lightScrim, darkScrim, UiModeManager.MODE_NIGHT_AUTO, detectDarkMode)
56
+
57
+ fun dark(scrim: Int): SystemBarStyle =
58
+ SystemBarStyle(scrim, scrim, UiModeManager.MODE_NIGHT_YES) { true }
59
+
60
+ fun light(scrim: Int, darkScrim: Int): SystemBarStyle =
61
+ SystemBarStyle(scrim, darkScrim, UiModeManager.MODE_NIGHT_NO) { false }
62
+ }
63
+
64
+ internal fun getScrim(isDark: Boolean) = if (isDark) darkScrim else lightScrim
65
+
66
+ internal fun getScrimWithEnforcedContrast(isDark: Boolean): Int = when {
67
+ nightMode == UiModeManager.MODE_NIGHT_AUTO -> Color.TRANSPARENT
68
+ isDark -> darkScrim
69
+ else -> lightScrim
70
+ }
71
+ }