create-cmp-cli 0.2.0
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/LICENSE +21 -0
- package/README.md +232 -0
- package/bin/create-cmp.mjs +91 -0
- package/options.schema.json +110 -0
- package/package.json +52 -0
- package/src/bootstrap/checks.mjs +283 -0
- package/src/bootstrap/exec.mjs +90 -0
- package/src/commands/clean.mjs +195 -0
- package/src/commands/create.mjs +223 -0
- package/src/commands/doctor.mjs +256 -0
- package/src/commands/upgrade.mjs +196 -0
- package/src/commands/verify.mjs +94 -0
- package/src/doctor.mjs +109 -0
- package/src/lib/args.mjs +38 -0
- package/src/lib/clean.mjs +72 -0
- package/src/lib/fsutil.mjs +90 -0
- package/src/lib/log.mjs +40 -0
- package/src/lib/project-doctor.mjs +329 -0
- package/src/lib/registry.mjs +102 -0
- package/src/lib/rename.mjs +116 -0
- package/src/lib/schema.mjs +123 -0
- package/src/lib/toggle.mjs +130 -0
- package/src/lib/tokens.mjs +96 -0
- package/src/lib/toml.mjs +137 -0
- package/src/lib/upgrade.mjs +159 -0
- package/src/lib/verify.mjs +97 -0
- package/src/scaffold.mjs +347 -0
- package/src/verify.mjs +4 -0
- package/src/versions/registry.json +42 -0
- package/template/.github/workflows/verify.yml +70 -0
- package/template/build.gradle.kts +17 -0
- package/template/composeApp/build.gradle.kts +257 -0
- package/template/composeApp/google-services.json +29 -0
- package/template/composeApp/proguard-rules.pro +34 -0
- package/template/composeApp/src/androidDebug/AndroidManifest.xml +13 -0
- package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/ComposeRootRegistry.kt +40 -0
- package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/InspectorCatalog.kt +80 -0
- package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/InspectorHttpServer.kt +322 -0
- package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/InspectorInit.kt +16 -0
- package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/LiveSemanticsJson.kt +102 -0
- package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/RemoteControlPage.kt +106 -0
- package/template/composeApp/src/androidDebug/res/xml/debug_network_security_config.xml +9 -0
- package/template/composeApp/src/androidMain/AndroidManifest.xml +28 -0
- package/template/composeApp/src/androidMain/kotlin/com/example/app/AppApplication.kt +72 -0
- package/template/composeApp/src/androidMain/kotlin/com/example/app/MainActivity.kt +24 -0
- package/template/composeApp/src/androidMain/kotlin/com/example/app/core/connectivity/NetworkMonitor.kt +42 -0
- package/template/composeApp/src/androidMain/kotlin/com/example/app/data/local/DatabaseBuilder.android.kt +13 -0
- package/template/composeApp/src/androidMain/kotlin/com/example/app/di/AndroidModule.kt +9 -0
- package/template/composeApp/src/androidMain/kotlin/com/example/app/presentation/components/TestTagAutomation.android.kt +11 -0
- package/template/composeApp/src/androidMain/res/drawable/ic_launcher_background.xml +9 -0
- package/template/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml +6 -0
- package/template/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml +6 -0
- package/template/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
- package/template/composeApp/src/androidMain/res/values/colors.xml +6 -0
- package/template/composeApp/src/androidMain/res/values/themes.xml +9 -0
- package/template/composeApp/src/androidMain/res/values-v31/themes.xml +8 -0
- package/template/composeApp/src/androidRelease/kotlin/com/example/app/inspector/InspectorInit.kt +13 -0
- package/template/composeApp/src/commonMain/composeResources/font/DMSans_Bold.ttf +0 -0
- package/template/composeApp/src/commonMain/composeResources/font/DMSans_Medium.ttf +0 -0
- package/template/composeApp/src/commonMain/composeResources/font/DMSans_Regular.ttf +0 -0
- package/template/composeApp/src/commonMain/composeResources/font/DMSans_SemiBold.ttf +0 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/App.kt +7 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/core/connectivity/NetworkMonitor.kt +7 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/data/local/AppDatabase.kt +28 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/data/local/DatabaseBuilder.kt +14 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/data/local/ItemDao.kt +18 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/data/remote/FirebaseConfig.kt +5 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/data/remote/ItemRepositoryImpl.kt +22 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/di/AppModule.kt +23 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/domain/model/Item.kt +8 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/domain/repository/ItemRepository.kt +8 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/domain/usecase/GetItemsUseCase.kt +12 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/App.kt +13 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/BaseScreen.kt +69 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/TestTagAutomation.kt +18 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/DetailScreen.kt +39 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/HomeScreen.kt +87 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/HomeViewModel.kt +35 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/AppNavHost.kt +43 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/AppShell.kt +144 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/AppTab.kt +25 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/Screen.kt +15 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/profile/ProfileScreen.kt +43 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/theme/DesignToken.kt +25 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/theme/Shape.kt +12 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/theme/Theme.kt +64 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/theme/Tokens.kt +19 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/theme/Typography.kt +36 -0
- package/template/composeApp/src/desktopMain/kotlin/com/example/app/core/connectivity/NetworkMonitor.desktop.kt +9 -0
- package/template/composeApp/src/desktopMain/kotlin/com/example/app/data/local/DatabaseBuilder.desktop.kt +13 -0
- package/template/composeApp/src/desktopMain/kotlin/com/example/app/di/DesktopModule.kt +32 -0
- package/template/composeApp/src/desktopMain/kotlin/com/example/app/main.kt +29 -0
- package/template/composeApp/src/desktopMain/kotlin/com/example/app/presentation/components/TestTagAutomation.desktop.kt +6 -0
- package/template/composeApp/src/iosMain/kotlin/com/example/app/KoinHelper.kt +50 -0
- package/template/composeApp/src/iosMain/kotlin/com/example/app/MainViewController.kt +10 -0
- package/template/composeApp/src/iosMain/kotlin/com/example/app/core/connectivity/NetworkMonitor.kt +32 -0
- package/template/composeApp/src/iosMain/kotlin/com/example/app/data/local/DatabaseBuilder.ios.kt +10 -0
- package/template/composeApp/src/iosMain/kotlin/com/example/app/presentation/components/TestTagAutomation.ios.kt +11 -0
- package/template/docs/dev-client.md +52 -0
- package/template/gitignore +21 -0
- package/template/gradle/libs.versions.toml +100 -0
- package/template/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/template/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/template/gradle.properties +9 -0
- package/template/gradlew +152 -0
- package/template/gradlew.bat +94 -0
- package/template/iosApp/Podfile +27 -0
- package/template/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png +0 -0
- package/template/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json +14 -0
- package/template/iosApp/iosApp/Assets.xcassets/Contents.json +6 -0
- package/template/iosApp/iosApp/ContentView.swift +17 -0
- package/template/iosApp/iosApp/GoogleService-Info.plist +34 -0
- package/template/iosApp/iosApp/Info.plist +43 -0
- package/template/iosApp/iosApp/PrivacyInfo.xcprivacy +42 -0
- package/template/iosApp/iosApp/iOSApp.swift +33 -0
- package/template/iosApp/project.yml +56 -0
- package/template/local.properties.example +2 -0
- package/template/manifest.json +125 -0
- package/template/qa/appium/README.md +23 -0
- package/template/qa/appium/lib/appium-client.mjs +225 -0
- package/template/qa/appium/package.json +8 -0
- package/template/qa/appium/run-android-smoke.mjs +39 -0
- package/template/settings.gradle.kts +34 -0
- package/template/tests/appium/cmp/conftest.py +96 -0
- package/template/tests/appium/cmp/test_smoke.py +17 -0
package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/InspectorHttpServer.kt
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
package __PACKAGE__.inspector
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap
|
|
4
|
+
import android.graphics.Canvas
|
|
5
|
+
import android.os.Handler
|
|
6
|
+
import android.os.Looper
|
|
7
|
+
import android.os.SystemClock
|
|
8
|
+
import android.util.Log
|
|
9
|
+
import android.view.MotionEvent
|
|
10
|
+
import androidx.core.view.drawToBitmap
|
|
11
|
+
import java.io.BufferedReader
|
|
12
|
+
import java.io.ByteArrayOutputStream
|
|
13
|
+
import java.io.InputStreamReader
|
|
14
|
+
import java.net.InetAddress
|
|
15
|
+
import java.net.ServerSocket
|
|
16
|
+
import java.net.Socket
|
|
17
|
+
import java.nio.charset.StandardCharsets
|
|
18
|
+
import java.util.concurrent.CountDownLatch
|
|
19
|
+
import java.util.concurrent.TimeUnit
|
|
20
|
+
import java.util.concurrent.atomic.AtomicReference
|
|
21
|
+
import kotlinx.serialization.json.Json
|
|
22
|
+
import kotlinx.serialization.json.JsonPrimitive
|
|
23
|
+
import kotlinx.serialization.json.floatOrNull
|
|
24
|
+
import kotlinx.serialization.json.jsonObject
|
|
25
|
+
import kotlinx.serialization.json.jsonPrimitive
|
|
26
|
+
import kotlin.math.roundToInt
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Debug-only, zero-dependency inspection server: a hand-rolled HTTP/1.1 responder over a
|
|
30
|
+
* plain [ServerSocket]. Binds LOOPBACK ONLY (never on the LAN); the host reaches it via
|
|
31
|
+
* `adb forward tcp:9500 tcp:9500`.
|
|
32
|
+
*
|
|
33
|
+
* Routes (JSON unless noted, `Connection: close`):
|
|
34
|
+
* GET /inspect/health → { status, schemaVersion, source, appId, buildType }
|
|
35
|
+
* GET /inspect/tree → the semantics-tree contract document (source "live-android"),
|
|
36
|
+
* read from the topmost Compose root ON THE MAIN THREAD.
|
|
37
|
+
* 503 while no Compose root is attached yet (cold start).
|
|
38
|
+
* GET /inspect/design-system → the declared token catalog { colors, dimens }.
|
|
39
|
+
* GET /inspect/screenshot → PNG bytes of the current Compose root (`image/png`) —
|
|
40
|
+
* pixels for the HUMAN's live view, never for model context.
|
|
41
|
+
* POST /inspect/tap → body {"x":<px>,"y":<px>} (root-relative px, exactly as the
|
|
42
|
+
* tree's bounds report them) → dispatches a down+up MotionEvent
|
|
43
|
+
* pair to the root view → {"tapped":true,"x":…,"y":…}.
|
|
44
|
+
* GET /inspect/remote → the self-contained remote-control HTML page (same-origin,
|
|
45
|
+
* zero CORS): live screenshot + click-to-tap in a browser.
|
|
46
|
+
*
|
|
47
|
+
* Single-threaded accept loop on a daemon thread = one client at a time = bounded by design.
|
|
48
|
+
* Failure to bind logs a warning and gives up — the inspector must never crash or block
|
|
49
|
+
* app startup. This class exists only in the androidDebug source set; release builds do
|
|
50
|
+
* not compile it at all.
|
|
51
|
+
*/
|
|
52
|
+
object InspectorHttpServer {
|
|
53
|
+
|
|
54
|
+
const val PORT = 9500
|
|
55
|
+
private const val TAG = "CmpInspector"
|
|
56
|
+
|
|
57
|
+
// The main thread is busy during cold start (first setContent/layout) — be generous.
|
|
58
|
+
private const val MAIN_THREAD_TIMEOUT_MS = 5_000L
|
|
59
|
+
|
|
60
|
+
// Gap between the synthetic ACTION_DOWN and ACTION_UP — a natural, unambiguous tap
|
|
61
|
+
// (well under the long-press timeout).
|
|
62
|
+
private const val TAP_UP_DELAY_MS = 50L
|
|
63
|
+
|
|
64
|
+
private const val JSON_TYPE = "application/json; charset=utf-8"
|
|
65
|
+
private const val HTML_TYPE = "text/html; charset=utf-8"
|
|
66
|
+
private const val PNG_TYPE = "image/png"
|
|
67
|
+
|
|
68
|
+
@Volatile private var started = false
|
|
69
|
+
|
|
70
|
+
fun start(appId: String) {
|
|
71
|
+
if (started) return
|
|
72
|
+
started = true
|
|
73
|
+
val thread = Thread({ serve(appId) }, "cmp-inspector-http")
|
|
74
|
+
thread.isDaemon = true
|
|
75
|
+
thread.start()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private fun serve(appId: String) {
|
|
79
|
+
val socket = try {
|
|
80
|
+
ServerSocket(PORT, 1, InetAddress.getLoopbackAddress())
|
|
81
|
+
} catch (t: Throwable) {
|
|
82
|
+
Log.w(TAG, "inspector server failed to bind 127.0.0.1:$PORT — giving up", t)
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
Log.i(TAG, "inspector server listening on 127.0.0.1:$PORT (debug build only)")
|
|
86
|
+
while (true) {
|
|
87
|
+
val client = try {
|
|
88
|
+
socket.accept()
|
|
89
|
+
} catch (t: Throwable) {
|
|
90
|
+
Log.w(TAG, "inspector accept failed — stopping", t)
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
client.use { handle(it, appId) }
|
|
95
|
+
} catch (t: Throwable) {
|
|
96
|
+
// Never let a bad request take the app (or the accept loop) down.
|
|
97
|
+
Log.w(TAG, "inspector request failed", t)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private fun handle(client: Socket, appId: String) {
|
|
103
|
+
client.soTimeout = 5_000
|
|
104
|
+
val reader = BufferedReader(InputStreamReader(client.getInputStream(), StandardCharsets.UTF_8))
|
|
105
|
+
val requestLine = reader.readLine() ?: return
|
|
106
|
+
// Drain headers; the only one we ever need is Content-Length (POST /inspect/tap).
|
|
107
|
+
var contentLength = 0
|
|
108
|
+
while (true) {
|
|
109
|
+
val line = reader.readLine() ?: break
|
|
110
|
+
if (line.isEmpty()) break
|
|
111
|
+
if (line.startsWith("Content-Length:", ignoreCase = true)) {
|
|
112
|
+
contentLength = line.substringAfter(':').trim().toIntOrNull() ?: 0
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
val parts = requestLine.split(" ")
|
|
116
|
+
val method = parts.getOrNull(0) ?: ""
|
|
117
|
+
val path = (parts.getOrNull(1) ?: "").substringBefore('?')
|
|
118
|
+
|
|
119
|
+
when {
|
|
120
|
+
method == "GET" && path == "/inspect/health" ->
|
|
121
|
+
writeJson(client, 200, healthJson(appId))
|
|
122
|
+
method == "GET" && path == "/inspect/tree" ->
|
|
123
|
+
treeResponse().let { (s, b) -> writeJson(client, s, b) }
|
|
124
|
+
method == "GET" && path == "/inspect/design-system" ->
|
|
125
|
+
writeJson(client, 200, InspectorCatalog.json())
|
|
126
|
+
method == "GET" && path == "/inspect/screenshot" ->
|
|
127
|
+
screenshotResponse(client)
|
|
128
|
+
method == "GET" && path == "/inspect/remote" ->
|
|
129
|
+
writeResponse(client, 200, RemoteControlPage.html(appId).toByteArray(StandardCharsets.UTF_8), HTML_TYPE)
|
|
130
|
+
method == "POST" && path == "/inspect/tap" ->
|
|
131
|
+
tapResponse(readBody(reader, contentLength)).let { (s, b) -> writeJson(client, s, b) }
|
|
132
|
+
method != "GET" && method != "POST" ->
|
|
133
|
+
writeJson(client, 405, errorJson("method not allowed"))
|
|
134
|
+
else ->
|
|
135
|
+
writeJson(client, 404, errorJson("unknown path"))
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Read the request body. The stream is already wrapped in a UTF-8 reader, so we read
|
|
141
|
+
* [contentLength] CHARS — exact for the ASCII JSON `{"x":…,"y":…}` this route accepts
|
|
142
|
+
* (and never under-reads it), which is all this debug server needs.
|
|
143
|
+
*/
|
|
144
|
+
private fun readBody(reader: BufferedReader, contentLength: Int): String {
|
|
145
|
+
if (contentLength <= 0) return ""
|
|
146
|
+
val buf = CharArray(contentLength.coerceAtMost(8_192))
|
|
147
|
+
var read = 0
|
|
148
|
+
while (read < buf.size) {
|
|
149
|
+
val n = reader.read(buf, read, buf.size - read)
|
|
150
|
+
if (n < 0) break
|
|
151
|
+
read += n
|
|
152
|
+
}
|
|
153
|
+
return String(buf, 0, read)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private fun healthJson(appId: String): String =
|
|
157
|
+
"""{"status":"ok","schemaVersion":1,"source":"live-android","appId":${JsonPrimitive(appId)},"buildType":"debug"}"""
|
|
158
|
+
|
|
159
|
+
private fun treeResponse(): Pair<Int, String> {
|
|
160
|
+
val root = ComposeRootRegistry.current()
|
|
161
|
+
?: return 503 to errorJson(
|
|
162
|
+
"compose root not ready yet — no Compose root attached (cold start?). Retry shortly."
|
|
163
|
+
)
|
|
164
|
+
// Semantics/layout nodes are not thread-safe: read the tree on the main thread and
|
|
165
|
+
// await with a generous timeout (the main thread is busy during first composition).
|
|
166
|
+
val result = AtomicReference<Pair<Int, String>>()
|
|
167
|
+
val latch = CountDownLatch(1)
|
|
168
|
+
Handler(Looper.getMainLooper()).post {
|
|
169
|
+
result.set(
|
|
170
|
+
try {
|
|
171
|
+
// Merged tree — matches the Phase 0 harness (onRoot() default).
|
|
172
|
+
200 to LiveSemanticsJson.dumpTree(root.semanticsOwner.rootSemanticsNode)
|
|
173
|
+
} catch (t: Throwable) {
|
|
174
|
+
500 to errorJson("failed to walk semantics tree: ${t.message}")
|
|
175
|
+
}
|
|
176
|
+
)
|
|
177
|
+
latch.countDown()
|
|
178
|
+
}
|
|
179
|
+
return if (latch.await(MAIN_THREAD_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
|
|
180
|
+
result.get()
|
|
181
|
+
} else {
|
|
182
|
+
503 to errorJson("main thread did not respond within ${MAIN_THREAD_TIMEOUT_MS}ms — app busy (cold start?). Retry.")
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* PNG of the current Compose root. The Bitmap is rendered on the MAIN thread (views are
|
|
188
|
+
* not thread-safe); PNG compression — tens of ms for a full screen — happens back on the
|
|
189
|
+
* server thread so the UI never pays for it.
|
|
190
|
+
*/
|
|
191
|
+
private fun screenshotResponse(client: Socket) {
|
|
192
|
+
val root = ComposeRootRegistry.current()
|
|
193
|
+
if (root == null) {
|
|
194
|
+
writeJson(
|
|
195
|
+
client, 503,
|
|
196
|
+
errorJson("compose root not ready yet — no Compose root attached (cold start?). Retry shortly.")
|
|
197
|
+
)
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
val bitmapRef = AtomicReference<Bitmap?>()
|
|
201
|
+
val errorRef = AtomicReference<String?>()
|
|
202
|
+
val latch = CountDownLatch(1)
|
|
203
|
+
Handler(Looper.getMainLooper()).post {
|
|
204
|
+
try {
|
|
205
|
+
val view = root.view
|
|
206
|
+
bitmapRef.set(
|
|
207
|
+
try {
|
|
208
|
+
// androidx.core.view.drawToBitmap (core-ktx — already an androidMain dep).
|
|
209
|
+
view.drawToBitmap()
|
|
210
|
+
} catch (t: Throwable) {
|
|
211
|
+
// Not laid out yet / hardware path refused — plain Canvas draw fallback.
|
|
212
|
+
Bitmap.createBitmap(
|
|
213
|
+
view.width.coerceAtLeast(1),
|
|
214
|
+
view.height.coerceAtLeast(1),
|
|
215
|
+
Bitmap.Config.ARGB_8888,
|
|
216
|
+
).also { view.draw(Canvas(it)) }
|
|
217
|
+
}
|
|
218
|
+
)
|
|
219
|
+
} catch (t: Throwable) {
|
|
220
|
+
errorRef.set("failed to render screenshot: ${t.message}")
|
|
221
|
+
}
|
|
222
|
+
latch.countDown()
|
|
223
|
+
}
|
|
224
|
+
if (!latch.await(MAIN_THREAD_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
|
|
225
|
+
writeJson(client, 503, errorJson("main thread did not respond within ${MAIN_THREAD_TIMEOUT_MS}ms — app busy (cold start?). Retry."))
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
errorRef.get()?.let {
|
|
229
|
+
writeJson(client, 500, errorJson(it))
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
val bitmap = bitmapRef.get()
|
|
233
|
+
if (bitmap == null) {
|
|
234
|
+
writeJson(client, 500, errorJson("failed to render screenshot: no bitmap produced"))
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
val bytes = ByteArrayOutputStream().use { out ->
|
|
238
|
+
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
|
|
239
|
+
out.toByteArray()
|
|
240
|
+
}
|
|
241
|
+
bitmap.recycle()
|
|
242
|
+
writeResponse(client, 200, bytes, PNG_TYPE)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Dispatch a synthetic tap (ACTION_DOWN, then ACTION_UP ~50ms later) to the topmost
|
|
247
|
+
* Compose root, on the MAIN thread. Coordinates are root-relative px — exactly the
|
|
248
|
+
* space the tree's `bounds` report, so callers can tap what they just inspected.
|
|
249
|
+
*/
|
|
250
|
+
private fun tapResponse(body: String): Pair<Int, String> {
|
|
251
|
+
val (x, y) = try {
|
|
252
|
+
val obj = Json.parseToJsonElement(body).jsonObject
|
|
253
|
+
val px = obj["x"]?.jsonPrimitive?.floatOrNull
|
|
254
|
+
val py = obj["y"]?.jsonPrimitive?.floatOrNull
|
|
255
|
+
if (px == null || py == null) {
|
|
256
|
+
return 400 to errorJson("""tap body must be {"x":<px>,"y":<px>} (root-relative px).""")
|
|
257
|
+
}
|
|
258
|
+
px to py
|
|
259
|
+
} catch (t: Throwable) {
|
|
260
|
+
return 400 to errorJson("""tap body must be {"x":<px>,"y":<px>} (root-relative px).""")
|
|
261
|
+
}
|
|
262
|
+
val root = ComposeRootRegistry.current()
|
|
263
|
+
?: return 503 to errorJson(
|
|
264
|
+
"compose root not ready yet — no Compose root attached (cold start?). Retry shortly."
|
|
265
|
+
)
|
|
266
|
+
val latch = CountDownLatch(1)
|
|
267
|
+
val handler = Handler(Looper.getMainLooper())
|
|
268
|
+
handler.post {
|
|
269
|
+
try {
|
|
270
|
+
val view = root.view
|
|
271
|
+
val downTime = SystemClock.uptimeMillis()
|
|
272
|
+
val down = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN, x, y, 0)
|
|
273
|
+
view.dispatchTouchEvent(down)
|
|
274
|
+
down.recycle()
|
|
275
|
+
handler.postDelayed({
|
|
276
|
+
try {
|
|
277
|
+
val up = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, x, y, 0)
|
|
278
|
+
view.dispatchTouchEvent(up)
|
|
279
|
+
up.recycle()
|
|
280
|
+
} finally {
|
|
281
|
+
latch.countDown()
|
|
282
|
+
}
|
|
283
|
+
}, TAP_UP_DELAY_MS)
|
|
284
|
+
} catch (t: Throwable) {
|
|
285
|
+
latch.countDown()
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return if (latch.await(MAIN_THREAD_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
|
|
289
|
+
200 to """{"tapped":true,"x":${x.roundToInt()},"y":${y.roundToInt()}}"""
|
|
290
|
+
} else {
|
|
291
|
+
503 to errorJson("main thread did not respond within ${MAIN_THREAD_TIMEOUT_MS}ms — app busy (cold start?). Retry.")
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private fun errorJson(message: String): String =
|
|
296
|
+
"""{"error":${JsonPrimitive(message)}}"""
|
|
297
|
+
|
|
298
|
+
private fun writeJson(client: Socket, status: Int, body: String) =
|
|
299
|
+
writeResponse(client, status, body.toByteArray(StandardCharsets.UTF_8), JSON_TYPE)
|
|
300
|
+
|
|
301
|
+
private fun writeResponse(client: Socket, status: Int, bytes: ByteArray, contentType: String) {
|
|
302
|
+
val reason = when (status) {
|
|
303
|
+
200 -> "OK"
|
|
304
|
+
400 -> "Bad Request"
|
|
305
|
+
404 -> "Not Found"
|
|
306
|
+
405 -> "Method Not Allowed"
|
|
307
|
+
503 -> "Service Unavailable"
|
|
308
|
+
else -> "Internal Server Error"
|
|
309
|
+
}
|
|
310
|
+
val head = buildString {
|
|
311
|
+
append("HTTP/1.1 ").append(status).append(' ').append(reason).append("\r\n")
|
|
312
|
+
append("Content-Type: ").append(contentType).append("\r\n")
|
|
313
|
+
append("Content-Length: ").append(bytes.size).append("\r\n")
|
|
314
|
+
append("Connection: close\r\n")
|
|
315
|
+
append("\r\n")
|
|
316
|
+
}
|
|
317
|
+
val out = client.getOutputStream()
|
|
318
|
+
out.write(head.toByteArray(StandardCharsets.UTF_8))
|
|
319
|
+
out.write(bytes)
|
|
320
|
+
out.flush()
|
|
321
|
+
}
|
|
322
|
+
}
|
package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/InspectorInit.kt
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
package __PACKAGE__.inspector
|
|
2
|
+
|
|
3
|
+
import android.app.Application
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* DEBUG variant: install the Compose root registry (must happen BEFORE any Activity so the
|
|
7
|
+
* `onViewCreatedCallback` hook catches every root), then start the loopback-only inspection
|
|
8
|
+
* server on 127.0.0.1:9500. Reach it from the host via `adb forward tcp:9500 tcp:9500`.
|
|
9
|
+
*
|
|
10
|
+
* The release source set carries a same-signature no-op twin — the compiler picks the variant
|
|
11
|
+
* body, so release builds contain no inspector code at all (structural absence, not a flag).
|
|
12
|
+
*/
|
|
13
|
+
fun Application.startInspector() {
|
|
14
|
+
ComposeRootRegistry.install()
|
|
15
|
+
InspectorHttpServer.start(appId = packageName)
|
|
16
|
+
}
|
package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/LiveSemanticsJson.kt
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
package __PACKAGE__.inspector
|
|
2
|
+
|
|
3
|
+
import androidx.compose.ui.semantics.SemanticsActions
|
|
4
|
+
import androidx.compose.ui.semantics.SemanticsNode
|
|
5
|
+
import androidx.compose.ui.semantics.SemanticsProperties
|
|
6
|
+
import androidx.compose.ui.semantics.getOrNull
|
|
7
|
+
import __PACKAGE__.presentation.theme.DesignTokenKey
|
|
8
|
+
import kotlinx.serialization.json.Json
|
|
9
|
+
import kotlinx.serialization.json.JsonElement
|
|
10
|
+
import kotlinx.serialization.json.JsonNull
|
|
11
|
+
import kotlinx.serialization.json.JsonObject
|
|
12
|
+
import kotlinx.serialization.json.JsonPrimitive
|
|
13
|
+
import kotlinx.serialization.json.buildJsonArray
|
|
14
|
+
import kotlinx.serialization.json.buildJsonObject
|
|
15
|
+
import kotlin.math.roundToInt
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Walks a Compose [SemanticsNode] tree and serialises it to JSON matching the create-cmp
|
|
19
|
+
* inspector contract (schemaVersion 1, source "live-android"). This mirrors the Phase 0
|
|
20
|
+
* harness serializer (`inspector/harness/.../SemanticsJson.kt`) node-for-node — the live
|
|
21
|
+
* path yields the SAME SemanticsNode type, so tier-0 and tier-1 output are structurally
|
|
22
|
+
* identical by construction. Every node carries pixel, root-relative bounds and a (possibly
|
|
23
|
+
* empty) `children` array; testTag/text/contentDescription/designToken are nullable.
|
|
24
|
+
*
|
|
25
|
+
* Optional contract fields (still schemaVersion 1):
|
|
26
|
+
* - `role` string|null — [SemanticsProperties.Role] (e.g. "Button").
|
|
27
|
+
* - `clickable` boolean — presence of [SemanticsActions.OnClick].
|
|
28
|
+
* - `disabled` boolean — presence of [SemanticsProperties.Disabled].
|
|
29
|
+
*/
|
|
30
|
+
object LiveSemanticsJson {
|
|
31
|
+
|
|
32
|
+
private val prettyJson = Json { prettyPrint = true }
|
|
33
|
+
|
|
34
|
+
/** Serialises [root] as the top-level contract document string. */
|
|
35
|
+
fun dumpTree(root: SemanticsNode): String {
|
|
36
|
+
val doc = buildJsonObject {
|
|
37
|
+
put("schemaVersion", JsonPrimitive(1))
|
|
38
|
+
put("source", JsonPrimitive("live-android"))
|
|
39
|
+
put("root", nodeToJson(root))
|
|
40
|
+
}
|
|
41
|
+
return prettyJson.encodeToString(JsonElement.serializer(), doc)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private fun nodeToJson(node: SemanticsNode): JsonObject = buildJsonObject {
|
|
45
|
+
put("testTag", node.testTag().toJson())
|
|
46
|
+
put("text", node.text().toJson())
|
|
47
|
+
put("contentDescription", node.contentDescription().toJson())
|
|
48
|
+
put("role", node.roleName().toJson())
|
|
49
|
+
put("clickable", JsonPrimitive(node.isClickable()))
|
|
50
|
+
put("disabled", JsonPrimitive(node.isDisabled()))
|
|
51
|
+
put("bounds", node.boundsJson())
|
|
52
|
+
put("designToken", node.designTokenJson())
|
|
53
|
+
put("children", buildJsonArray {
|
|
54
|
+
node.children.forEach { add(nodeToJson(it)) }
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private fun SemanticsNode.testTag(): String? =
|
|
59
|
+
config.getOrNull(SemanticsProperties.TestTag)
|
|
60
|
+
|
|
61
|
+
private fun SemanticsNode.text(): String? =
|
|
62
|
+
config.getOrNull(SemanticsProperties.Text)
|
|
63
|
+
?.joinToString(separator = " ") { it.text }
|
|
64
|
+
?.takeIf { it.isNotEmpty() }
|
|
65
|
+
|
|
66
|
+
private fun SemanticsNode.contentDescription(): String? =
|
|
67
|
+
config.getOrNull(SemanticsProperties.ContentDescription)
|
|
68
|
+
?.joinToString(separator = " ")
|
|
69
|
+
?.takeIf { it.isNotEmpty() }
|
|
70
|
+
|
|
71
|
+
private fun SemanticsNode.roleName(): String? =
|
|
72
|
+
config.getOrNull(SemanticsProperties.Role)?.toString()
|
|
73
|
+
|
|
74
|
+
private fun SemanticsNode.isClickable(): Boolean =
|
|
75
|
+
config.contains(SemanticsActions.OnClick)
|
|
76
|
+
|
|
77
|
+
private fun SemanticsNode.isDisabled(): Boolean =
|
|
78
|
+
config.contains(SemanticsProperties.Disabled)
|
|
79
|
+
|
|
80
|
+
private fun SemanticsNode.boundsJson(): JsonObject {
|
|
81
|
+
val rect = boundsInRoot
|
|
82
|
+
return buildJsonObject {
|
|
83
|
+
put("x", JsonPrimitive(rect.left.roundToInt()))
|
|
84
|
+
put("y", JsonPrimitive(rect.top.roundToInt()))
|
|
85
|
+
put("width", JsonPrimitive(rect.width.roundToInt()))
|
|
86
|
+
put("height", JsonPrimitive(rect.height.roundToInt()))
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private fun SemanticsNode.designTokenJson(): JsonElement {
|
|
91
|
+
val info = config.getOrNull(DesignTokenKey) ?: return JsonNull
|
|
92
|
+
return buildJsonObject {
|
|
93
|
+
put("tokens", buildJsonArray { info.tokens.forEach { add(JsonPrimitive(it)) } })
|
|
94
|
+
put("resolved", buildJsonObject {
|
|
95
|
+
info.resolved.forEach { (k, v) -> put(k, JsonPrimitive(v)) }
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private fun String?.toJson(): JsonElement =
|
|
101
|
+
if (this == null) JsonNull else JsonPrimitive(this)
|
|
102
|
+
}
|
package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/RemoteControlPage.kt
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
package __PACKAGE__.inspector
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The `/inspect/remote` page: a self-contained, same-origin (zero CORS) remote-control view
|
|
5
|
+
* of the running app — the "Running Devices window" for a browser. The HUMAN watches the live
|
|
6
|
+
* screenshot (re-fetched ~every 700ms with a cache-buster) and clicks it; the click is scaled
|
|
7
|
+
* from displayed-image space to device px and delivered via `POST /inspect/tap`, and the next
|
|
8
|
+
* poll shows the result. A light `/inspect/tree` poll (~every 3s) feeds the header stats.
|
|
9
|
+
*
|
|
10
|
+
* No external resources, dark-friendly, phone-aspect. Pixels here flow to the HUMAN's browser
|
|
11
|
+
* only — the agent keeps asserting on the tree. Debug builds only, loopback only, like every
|
|
12
|
+
* other route on this server.
|
|
13
|
+
*/
|
|
14
|
+
object RemoteControlPage {
|
|
15
|
+
|
|
16
|
+
fun html(appId: String): String = """<!doctype html>
|
|
17
|
+
<html>
|
|
18
|
+
<head>
|
|
19
|
+
<meta charset="utf-8">
|
|
20
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
21
|
+
<title>$appId — live device view</title>
|
|
22
|
+
<style>
|
|
23
|
+
body { margin: 0; background: #101216; color: #e8eaed; display: flex; flex-direction: column;
|
|
24
|
+
min-height: 100vh; font: 13px/1.4 -apple-system, BlinkMacSystemFont, sans-serif; }
|
|
25
|
+
header { display: flex; justify-content: space-between; align-items: baseline; gap: 12px;
|
|
26
|
+
padding: 10px 16px; background: #1a1d23; position: sticky; top: 0;
|
|
27
|
+
border-bottom: 1px solid #2a2e36; }
|
|
28
|
+
header .app { font-weight: 600; }
|
|
29
|
+
#stats { color: #7ee2a8; font-variant-numeric: tabular-nums; }
|
|
30
|
+
main { flex: 1; display: flex; justify-content: center; align-items: flex-start; padding: 14px; }
|
|
31
|
+
#screen { max-height: calc(100vh - 92px); max-width: 94vw; border: 1px solid #2a2e36;
|
|
32
|
+
border-radius: 14px; background: #000; cursor: crosshair; }
|
|
33
|
+
#tapdot { position: fixed; width: 16px; height: 16px; border-radius: 50%; pointer-events: none;
|
|
34
|
+
background: rgba(126, 226, 168, .9); transform: translate(-50%, -50%); opacity: 0;
|
|
35
|
+
transition: opacity .4s; }
|
|
36
|
+
footer { padding: 6px 16px 10px; color: #7a8090; font-size: 11px; text-align: center; }
|
|
37
|
+
</style>
|
|
38
|
+
</head>
|
|
39
|
+
<body>
|
|
40
|
+
<header>
|
|
41
|
+
<span class="app">$appId · live device view</span>
|
|
42
|
+
<span id="stats">connecting…</span>
|
|
43
|
+
</header>
|
|
44
|
+
<main><img id="screen" alt="live device screen (click to tap)"></main>
|
|
45
|
+
<div id="tapdot"></div>
|
|
46
|
+
<footer>click the screen to tap the real device · create-cmp debug inspector · loopback only, debug builds only</footer>
|
|
47
|
+
<script>
|
|
48
|
+
"use strict";
|
|
49
|
+
var img = document.getElementById("screen");
|
|
50
|
+
var stats = document.getElementById("stats");
|
|
51
|
+
var dot = document.getElementById("tapdot");
|
|
52
|
+
var busy = false;
|
|
53
|
+
|
|
54
|
+
// Live screenshot: re-fetch ~every 700ms (cache-busted); swap only once loaded (no flicker).
|
|
55
|
+
function poll() {
|
|
56
|
+
if (busy) return;
|
|
57
|
+
busy = true;
|
|
58
|
+
var next = new Image();
|
|
59
|
+
next.onload = function () { img.src = next.src; busy = false; };
|
|
60
|
+
next.onerror = function () { busy = false; };
|
|
61
|
+
next.src = "/inspect/screenshot?t=" + Date.now();
|
|
62
|
+
}
|
|
63
|
+
setInterval(poll, 700);
|
|
64
|
+
poll();
|
|
65
|
+
|
|
66
|
+
// Click → scale displayed coords to device px (natural vs displayed size) → POST /inspect/tap.
|
|
67
|
+
img.addEventListener("click", function (e) {
|
|
68
|
+
if (!img.naturalWidth) return;
|
|
69
|
+
var r = img.getBoundingClientRect();
|
|
70
|
+
var x = Math.round((e.clientX - r.left) * (img.naturalWidth / r.width));
|
|
71
|
+
var y = Math.round((e.clientY - r.top) * (img.naturalHeight / r.height));
|
|
72
|
+
dot.style.left = e.clientX + "px";
|
|
73
|
+
dot.style.top = e.clientY + "px";
|
|
74
|
+
dot.style.opacity = "1";
|
|
75
|
+
setTimeout(function () { dot.style.opacity = "0"; }, 350);
|
|
76
|
+
fetch("/inspect/tap", {
|
|
77
|
+
method: "POST",
|
|
78
|
+
headers: { "Content-Type": "application/json" },
|
|
79
|
+
body: JSON.stringify({ x: x, y: y })
|
|
80
|
+
}).catch(function () {});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Header stats: a light tree poll — node + tagged counts, ~every 3s.
|
|
84
|
+
function countNodes(node) {
|
|
85
|
+
var acc = { nodes: 1, tags: node.testTag ? 1 : 0 };
|
|
86
|
+
(node.children || []).forEach(function (c) {
|
|
87
|
+
var m = countNodes(c);
|
|
88
|
+
acc.nodes += m.nodes;
|
|
89
|
+
acc.tags += m.tags;
|
|
90
|
+
});
|
|
91
|
+
return acc;
|
|
92
|
+
}
|
|
93
|
+
function pollTree() {
|
|
94
|
+
fetch("/inspect/tree").then(function (res) { return res.json(); }).then(function (tree) {
|
|
95
|
+
if (!tree.root) { stats.textContent = "tree: not ready"; return; }
|
|
96
|
+
var n = countNodes(tree.root);
|
|
97
|
+
stats.textContent = n.nodes + " nodes · " + n.tags + " tagged";
|
|
98
|
+
}).catch(function () { stats.textContent = "tree: unreachable"; });
|
|
99
|
+
}
|
|
100
|
+
setInterval(pollTree, 3000);
|
|
101
|
+
pollTree();
|
|
102
|
+
</script>
|
|
103
|
+
</body>
|
|
104
|
+
</html>
|
|
105
|
+
"""
|
|
106
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<network-security-config>
|
|
3
|
+
<!-- Debug-only cleartext to the local Firebase emulators / dev backends. -->
|
|
4
|
+
<domain-config cleartextTrafficPermitted="true">
|
|
5
|
+
<domain includeSubdomains="true">127.0.0.1</domain>
|
|
6
|
+
<domain includeSubdomains="true">10.0.2.2</domain>
|
|
7
|
+
<domain includeSubdomains="true">localhost</domain>
|
|
8
|
+
</domain-config>
|
|
9
|
+
</network-security-config>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
3
|
+
|
|
4
|
+
<uses-permission android:name="android.permission.INTERNET" />
|
|
5
|
+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
6
|
+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
7
|
+
|
|
8
|
+
<application
|
|
9
|
+
android:name=".AppApplication"
|
|
10
|
+
android:label="__APP_NAME__"
|
|
11
|
+
android:icon="@mipmap/ic_launcher"
|
|
12
|
+
android:roundIcon="@mipmap/ic_launcher_round"
|
|
13
|
+
android:supportsRtl="true"
|
|
14
|
+
android:usesCleartextTraffic="${usesCleartextTraffic}"
|
|
15
|
+
android:theme="@style/Theme.App">
|
|
16
|
+
|
|
17
|
+
<activity
|
|
18
|
+
android:name=".MainActivity"
|
|
19
|
+
android:exported="true"
|
|
20
|
+
android:windowSoftInputMode="adjustResize">
|
|
21
|
+
<intent-filter>
|
|
22
|
+
<action android:name="android.intent.action.MAIN" />
|
|
23
|
+
<category android:name="android.intent.category.LAUNCHER" />
|
|
24
|
+
</intent-filter>
|
|
25
|
+
</activity>
|
|
26
|
+
|
|
27
|
+
</application>
|
|
28
|
+
</manifest>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
package __PACKAGE__
|
|
2
|
+
|
|
3
|
+
import android.app.Application
|
|
4
|
+
// >>> cmp:feature firebase
|
|
5
|
+
import __PACKAGE__.data.remote.FIREBASE_FUNCTIONS_REGION
|
|
6
|
+
import dev.gitlive.firebase.Firebase
|
|
7
|
+
import dev.gitlive.firebase.auth.auth
|
|
8
|
+
import dev.gitlive.firebase.firestore.firestore
|
|
9
|
+
import dev.gitlive.firebase.functions.functions
|
|
10
|
+
import dev.gitlive.firebase.storage.storage
|
|
11
|
+
// <<< cmp:feature firebase
|
|
12
|
+
// >>> cmp:feature room
|
|
13
|
+
import __PACKAGE__.data.local.AppDatabase
|
|
14
|
+
import __PACKAGE__.data.local.appContext
|
|
15
|
+
import __PACKAGE__.data.local.buildDatabase
|
|
16
|
+
// <<< cmp:feature room
|
|
17
|
+
import __PACKAGE__.core.connectivity.NetworkMonitor
|
|
18
|
+
import __PACKAGE__.di.androidModule
|
|
19
|
+
// >>> cmp:feature inspector
|
|
20
|
+
import __PACKAGE__.inspector.startInspector
|
|
21
|
+
// <<< cmp:feature inspector
|
|
22
|
+
import __PACKAGE__.di.appModules
|
|
23
|
+
import org.koin.android.ext.koin.androidContext
|
|
24
|
+
import org.koin.android.ext.koin.androidLogger
|
|
25
|
+
import org.koin.core.context.startKoin
|
|
26
|
+
import org.koin.dsl.module
|
|
27
|
+
|
|
28
|
+
class AppApplication : Application() {
|
|
29
|
+
override fun onCreate() {
|
|
30
|
+
super.onCreate()
|
|
31
|
+
// >>> cmp:feature inspector
|
|
32
|
+
// Debug builds only: the androidRelease twin is a no-op (see inspector/InspectorInit.kt).
|
|
33
|
+
// Must run before any Activity so the Compose root registry catches every root.
|
|
34
|
+
startInspector()
|
|
35
|
+
// <<< cmp:feature inspector
|
|
36
|
+
// >>> cmp:feature room
|
|
37
|
+
appContext = this
|
|
38
|
+
// <<< cmp:feature room
|
|
39
|
+
// >>> cmp:feature firebase
|
|
40
|
+
configureFirebaseEmulators()
|
|
41
|
+
// <<< cmp:feature firebase
|
|
42
|
+
startKoin {
|
|
43
|
+
androidLogger()
|
|
44
|
+
androidContext(this@AppApplication)
|
|
45
|
+
modules(
|
|
46
|
+
module {
|
|
47
|
+
// >>> cmp:feature room
|
|
48
|
+
single<AppDatabase> { buildDatabase() }
|
|
49
|
+
// <<< cmp:feature room
|
|
50
|
+
single { NetworkMonitor(androidContext()) }
|
|
51
|
+
},
|
|
52
|
+
androidModule,
|
|
53
|
+
*appModules.toTypedArray()
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// >>> cmp:feature firebase
|
|
59
|
+
// Debug builds talk to the local Firebase emulators (BuildConfig flags set in build.gradle.kts).
|
|
60
|
+
private fun configureFirebaseEmulators() {
|
|
61
|
+
if (!BuildConfig.USE_FIREBASE_EMULATORS) return
|
|
62
|
+
val host = BuildConfig.FIREBASE_EMULATOR_HOST
|
|
63
|
+
runCatching {
|
|
64
|
+
Firebase.auth.useEmulator(host, BuildConfig.FIREBASE_AUTH_PORT)
|
|
65
|
+
Firebase.firestore.useEmulator(host, BuildConfig.FIREBASE_FIRESTORE_PORT)
|
|
66
|
+
Firebase.functions(FIREBASE_FUNCTIONS_REGION)
|
|
67
|
+
.useEmulator(host, BuildConfig.FIREBASE_FUNCTIONS_PORT)
|
|
68
|
+
Firebase.storage.useEmulator(host, BuildConfig.FIREBASE_STORAGE_PORT)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// <<< cmp:feature firebase
|
|
72
|
+
}
|