@craft-native/android 0.0.104 → 0.0.107
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +34 -7
- package/dist/index.js.map +3 -3
- package/package.json +1 -1
- package/templates/MainActivityNative.kt.template +390 -0
- package/templates/build.gradle.kts.app.template +1 -0
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Build native Android apps with web technologies using Craft.
|
|
|
6
6
|
|
|
7
7
|
### Core
|
|
8
8
|
|
|
9
|
-
- **WebView** -
|
|
9
|
+
- **WebView or native STX** - Choose the browser renderer or Android views driven by a compiled STX bundle
|
|
10
10
|
- **Dark Mode** - Native dark/light theme support
|
|
11
11
|
|
|
12
12
|
### Input & Feedback
|
|
@@ -86,6 +86,31 @@ craft android build --html-path ../dist/index.html
|
|
|
86
86
|
craft android build --dev-server http://10.0.2.2:3456
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
+
For a WebView-free app, initialize with `--renderer native` and give the build
|
|
90
|
+
the same route bundle that `stx-native` produces for iOS:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
craft android init MyApp --package com.example.myapp --renderer native
|
|
94
|
+
cd android
|
|
95
|
+
stx-native compile ../screens --format bundle --output ../native-screen.js
|
|
96
|
+
craft android build --native-bundle ../native-screen.js
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Native mode maps STX `View`, `Text`, `Button`, and `TextInput` to Android views.
|
|
100
|
+
Push, replace, and back preserve the previous screen's views and JavaScript
|
|
101
|
+
state; Android's system back button pops the route. Native mode accepts neither
|
|
102
|
+
`--html-path` nor `--dev-server`. Its initial bridge supports device info,
|
|
103
|
+
clipboard read/write, and haptic impact (when `enableHaptics` is set); the
|
|
104
|
+
broader browser bridge remains specific to WebView mode.
|
|
105
|
+
|
|
106
|
+
The native runtime requires Android's JavaScriptSandbox. It prefers message
|
|
107
|
+
ports with promise-return support and uses the sandbox's console callback
|
|
108
|
+
when those features are unavailable. If neither bridge is supported, the app
|
|
109
|
+
displays a clear unsupported-device error; it never silently opens a WebView.
|
|
110
|
+
Support depends on the installed WebView provider, so validate on each device
|
|
111
|
+
class you plan to ship. The [Android JavaScriptEngine guide](https://developer.android.com/develop/ui/views/layout/webapps/jsengine)
|
|
112
|
+
documents the sandbox and device-support check.
|
|
113
|
+
|
|
89
114
|
### 3. Open in Android Studio
|
|
90
115
|
|
|
91
116
|
```bash
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export declare const ANDROID_MIN_SDK = 26;
|
|
|
8
8
|
export interface CraftAndroidConfig {
|
|
9
9
|
appName: string;
|
|
10
10
|
packageName: string;
|
|
11
|
+
renderer?: 'web' | 'native';
|
|
11
12
|
version?: string;
|
|
12
13
|
versionCode?: number;
|
|
13
14
|
darkMode?: boolean;
|
|
@@ -51,6 +52,7 @@ export interface InitOptions {
|
|
|
51
52
|
}
|
|
52
53
|
export interface BuildOptions {
|
|
53
54
|
htmlPath?: string;
|
|
55
|
+
nativeBundlePath?: string;
|
|
54
56
|
devServer?: string;
|
|
55
57
|
output: string;
|
|
56
58
|
release?: boolean;
|
package/dist/index.js
CHANGED
|
@@ -112,6 +112,7 @@ var KOTLIN_KEYWORDS = new Set([
|
|
|
112
112
|
]);
|
|
113
113
|
var ANDROID_MIN_SDK = 26;
|
|
114
114
|
var DEFAULT_CONFIG = {
|
|
115
|
+
renderer: "web",
|
|
115
116
|
version: "1.0.0",
|
|
116
117
|
versionCode: 1,
|
|
117
118
|
darkMode: true,
|
|
@@ -212,6 +213,9 @@ function normalizeAndroidNetworkConfig(config) {
|
|
|
212
213
|
function validateAndroidConfig(config) {
|
|
213
214
|
if (!config.appName.trim())
|
|
214
215
|
throw new Error("Android app name must not be empty");
|
|
216
|
+
if (config.renderer !== undefined && config.renderer !== "web" && config.renderer !== "native") {
|
|
217
|
+
throw new Error(`Unknown Android renderer: ${config.renderer}. Expected web or native.`);
|
|
218
|
+
}
|
|
215
219
|
const packageSegments = config.packageName.split(".");
|
|
216
220
|
if (packageSegments.length < 2 || packageSegments.some((segment) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(segment) || KOTLIN_KEYWORDS.has(segment))) {
|
|
217
221
|
throw new Error(`Invalid Android package name: ${config.packageName}`);
|
|
@@ -408,7 +412,7 @@ async function init(options) {
|
|
|
408
412
|
if (config.googleServicesFile) {
|
|
409
413
|
cpSync(config.googleServicesFile, join(output, "app/google-services.json"));
|
|
410
414
|
}
|
|
411
|
-
const mainActivityTemplate = readFileSync(join(TEMPLATES_DIR, "MainActivity.kt.template"), "utf-8");
|
|
415
|
+
const mainActivityTemplate = readFileSync(join(TEMPLATES_DIR, config.renderer === "native" ? "MainActivityNative.kt.template" : "MainActivity.kt.template"), "utf-8");
|
|
412
416
|
const mainActivity = mainActivityTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{APP_NAME\}\}/g, name);
|
|
413
417
|
writeFileSync(join(output, "app/src/main/java", packagePath, "MainActivity.kt"), mainActivity);
|
|
414
418
|
const craftBridgeTemplate = readFileSync(join(TEMPLATES_DIR, "CraftBridge.kt.template"), "utf-8");
|
|
@@ -455,7 +459,7 @@ async function init(options) {
|
|
|
455
459
|
writeFileSync(join(output, "build.gradle.kts"), projectGradleTemplate.replace(/\{\{GOOGLE_SERVICES_PLUGIN\}\}/g, hasGoogleServices ? ' id("com.google.gms.google-services") version "4.4.2" apply false' : ""));
|
|
456
460
|
const appGradleTemplate = readFileSync(join(TEMPLATES_DIR, "build.gradle.kts.app.template"), "utf-8");
|
|
457
461
|
const appGradle = appGradleTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{VERSION_NAME\}\}/g, escapeKotlinString(config.version || "1.0.0")).replace(/\{\{VERSION_CODE\}\}/g, String(config.versionCode || 1)).replace(/\{\{MIN_SDK\}\}/g, String(config.minSdk ?? ANDROID_MIN_SDK)).replace(/\{\{COMPILE_SDK\}\}/g, String(config.compileSdk || 36)).replace(/\{\{TARGET_SDK\}\}/g, String(config.targetSdk || 35)).replace(/\{\{GOOGLE_SERVICES_PLUGIN\}\}/g, hasGoogleServices ? ' id("com.google.gms.google-services")' : "").replace(/\{\{FIREBASE_MESSAGING_DEPENDENCY\}\}/g, config.enablePushNotifications ? ' implementation("com.google.firebase:firebase-messaging:24.1.0")' : "").replace(/\{\{HEALTH_CONNECT_DEPENDENCIES\}\}/g, config.enableHealthConnect ? ` implementation("androidx.health.connect:connect-client:1.1.0")
|
|
458
|
-
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")` : "");
|
|
462
|
+
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")` : "").replace(/\{\{JAVASCRIPT_ENGINE_DEPENDENCY\}\}/g, config.renderer === "native" ? ' implementation("androidx.javascriptengine:javascriptengine:1.1.0")' : "");
|
|
459
463
|
writeFileSync(join(output, "app/build.gradle.kts"), appGradle);
|
|
460
464
|
const proguardTemplate = readFileSync(join(TEMPLATES_DIR, "proguard-rules.pro.template"), "utf-8");
|
|
461
465
|
writeFileSync(join(output, "app/proguard-rules.pro"), proguardTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
@@ -519,7 +523,12 @@ zipStorePath=wrapper/dists
|
|
|
519
523
|
</resources>
|
|
520
524
|
`;
|
|
521
525
|
writeFileSync(join(output, "app/src/main/res/values/themes.xml"), themesXml);
|
|
522
|
-
const activityMainXml = `<?xml version="1.0" encoding="utf-8"?>
|
|
526
|
+
const activityMainXml = config.renderer === "native" ? `<?xml version="1.0" encoding="utf-8"?>
|
|
527
|
+
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
528
|
+
android:id="@+id/native_root"
|
|
529
|
+
android:layout_width="match_parent"
|
|
530
|
+
android:layout_height="match_parent" />
|
|
531
|
+
` : `<?xml version="1.0" encoding="utf-8"?>
|
|
523
532
|
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
|
524
533
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
525
534
|
android:layout_width="match_parent"
|
|
@@ -581,13 +590,18 @@ zipStorePath=wrapper/dists
|
|
|
581
590
|
console.log("");
|
|
582
591
|
console.log("Next steps:");
|
|
583
592
|
console.log(` 1. cd ${output}`);
|
|
584
|
-
|
|
585
|
-
|
|
593
|
+
if (config.renderer === "native") {
|
|
594
|
+
console.log(" 2. Compile your screen: stx-native compile Screen.stx --format bundle --output screen.js");
|
|
595
|
+
console.log(" 3. Run: craft android build --native-bundle screen.js");
|
|
596
|
+
} else {
|
|
597
|
+
console.log(" 2. Add your web content to app/src/main/assets/index.html");
|
|
598
|
+
console.log(" 3. Run: craft android build");
|
|
599
|
+
}
|
|
586
600
|
console.log(" 4. Run: craft android open");
|
|
587
601
|
console.log("");
|
|
588
602
|
}
|
|
589
603
|
async function build(options) {
|
|
590
|
-
const { htmlPath, devServer, output, release, compile = true } = options;
|
|
604
|
+
const { htmlPath, nativeBundlePath, devServer, output, release, compile = true } = options;
|
|
591
605
|
console.log(`
|
|
592
606
|
\uD83D\uDCE6 Building Craft Android project...`);
|
|
593
607
|
const configPath = join(output, "craft.config.json");
|
|
@@ -595,6 +609,19 @@ async function build(options) {
|
|
|
595
609
|
throw new Error(`No craft.config.json found in ${output}. Run 'craft android init' first.`);
|
|
596
610
|
}
|
|
597
611
|
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
612
|
+
validateAndroidConfig(config);
|
|
613
|
+
if (config.renderer === "native") {
|
|
614
|
+
if (htmlPath || devServer)
|
|
615
|
+
throw new Error("A native Android screen cannot use --html-path or --dev-server.");
|
|
616
|
+
const destination = join(output, "app/src/main/assets/native-screen.js");
|
|
617
|
+
if (nativeBundlePath && resolve(nativeBundlePath) !== resolve(destination))
|
|
618
|
+
cpSync(nativeBundlePath, destination);
|
|
619
|
+
if (!existsSync(destination)) {
|
|
620
|
+
throw new Error("Native Android mode needs app/src/main/assets/native-screen.js. Compile a .stx screen with stx-native first.");
|
|
621
|
+
}
|
|
622
|
+
} else if (nativeBundlePath) {
|
|
623
|
+
throw new Error('--native-bundle requires renderer: "native" in craft.config.json.');
|
|
624
|
+
}
|
|
598
625
|
if (existsSync(join(output, "app/src/main/jniLibs"))) {
|
|
599
626
|
const runtimeDir = resolveRuntimeDir(options.runtimeDir);
|
|
600
627
|
if (runtimeDir) {
|
|
@@ -700,4 +727,4 @@ export {
|
|
|
700
727
|
ANDROID_MIN_SDK
|
|
701
728
|
};
|
|
702
729
|
|
|
703
|
-
//# debugId=
|
|
730
|
+
//# debugId=2BDBD4A5F771012864756E2164756E21
|
package/dist/index.js.map
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index.ts", "../src/promise-runtime.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * Craft Android Builder\n *\n * Generates native Android apps from web content using WebView.\n */\n\nimport { cpSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'\nimport { dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path'\nimport { $ } from 'bun'\nimport { renderAndroidPromiseRuntime } from './promise-runtime'\n\nconst TEMPLATES_DIR = join(dirname(import.meta.dir), 'templates')\nconst LOCAL_DEVELOPMENT_HOSTS = new Set(['localhost', '127.0.0.1', '10.0.2.2'])\nconst KOTLIN_KEYWORDS = new Set([\n 'as',\n 'break',\n 'class',\n 'continue',\n 'do',\n 'else',\n 'false',\n 'for',\n 'fun',\n 'if',\n 'in',\n 'interface',\n 'is',\n 'null',\n 'object',\n 'package',\n 'return',\n 'super',\n 'this',\n 'throw',\n 'true',\n 'try',\n 'typealias',\n 'typeof',\n 'val',\n 'var',\n 'when',\n 'while',\n])\n\n/** Oldest Android API level supported by the generated Kotlin runtime. */\nexport const ANDROID_MIN_SDK = 26\n\nexport interface CraftAndroidConfig {\n appName: string\n packageName: string\n version?: string\n versionCode?: number\n darkMode?: boolean\n backgroundColor?: string\n enableSpeechRecognition?: boolean\n enableHaptics?: boolean\n enableShare?: boolean\n enableCamera?: boolean\n enableBiometric?: boolean\n enablePushNotifications?: boolean\n enableSecureStorage?: boolean\n enableGeolocation?: boolean\n enableBackgroundLocation?: boolean\n enableHealthConnect?: boolean\n enableKeepAwake?: boolean\n enableDeepLinks?: boolean\n urlSchemes?: string[]\n trustedOrigins?: string[]\n appIconPath?: string\n googleServicesFile?: string\n devServerURL?: string\n hasBundledFallback?: boolean\n /** Android API level, at least ANDROID_MIN_SDK (26). */\n minSdk?: number\n compileSdk?: number\n targetSdk?: number\n}\n\nexport interface InitOptions {\n name: string\n packageName?: string\n output: string\n config?: Partial<CraftAndroidConfig>\n /**\n * Where to find the Zig runtime's `<abi>/libcraft.so`.\n *\n * `null` means no runtime regardless of the environment; `undefined` falls\n * through to `CRAFT_ANDROID_RUNTIME`. Same shape as the iOS builder's\n * `runtimeDir`.\n */\n runtimeDir?: string | null\n}\n\nexport interface BuildOptions {\n htmlPath?: string\n devServer?: string\n output: string\n release?: boolean\n compile?: boolean\n /** Same meaning as `InitOptions.runtimeDir`; refreshes what init installed. */\n runtimeDir?: string | null\n}\n\nexport interface OpenOptions {\n output: string\n}\n\nexport interface RunOptions {\n device?: string\n output: string\n}\n\nconst DEFAULT_CONFIG: Omit<CraftAndroidConfig, 'appName' | 'packageName'> = {\n version: '1.0.0',\n versionCode: 1,\n darkMode: true,\n backgroundColor: '#0b1712',\n enableSpeechRecognition: false,\n enableHaptics: false,\n enableShare: false,\n enableCamera: false,\n enableBiometric: false,\n enablePushNotifications: false,\n enableSecureStorage: false,\n enableGeolocation: false,\n enableBackgroundLocation: false,\n enableHealthConnect: false,\n enableKeepAwake: false,\n enableDeepLinks: false,\n urlSchemes: [],\n trustedOrigins: [],\n minSdk: ANDROID_MIN_SDK,\n compileSdk: 36,\n targetSdk: 35,\n}\n\nfunction escapeXml(value: string): string {\n return value\n .replaceAll('&', '&')\n .replaceAll('<', '<')\n .replaceAll('>', '>')\n .replaceAll('\"', '"')\n .replaceAll('\\'', ''')\n}\n\nfunction escapeKotlinString(value: string): string {\n return value\n .replaceAll('\\\\', '\\\\\\\\')\n .replaceAll('\"', '\\\\\"')\n .replaceAll('$', '\\\\$')\n .replaceAll('\\r', '\\\\r')\n .replaceAll('\\n', '\\\\n')\n}\n\nfunction generatedPackageSegment(name: string): string {\n const normalized = name.toLowerCase().replace(/[^a-z0-9_]/g, '')\n if (!normalized) return 'app'\n return /^[a-z_]/.test(normalized) ? normalized : `app${normalized}`\n}\n\nfunction generatedGradleProjectName(name: string): string {\n const invalidCharacters = ['/', '\\\\', ':', '<', '>', '\"', '?', '*', '|']\n const normalized = invalidCharacters.reduce(\n (value, character) => value.replaceAll(character, '-'),\n name,\n ).trim()\n return normalized || 'craft-app'\n}\n\nfunction requireRegularFile(path: string, label: string): void {\n if (!existsSync(path)) throw new Error(`${label} not found: ${path}`)\n if (!statSync(path).isFile()) throw new Error(`${label} must be a file: ${path}`)\n}\n\nfunction containsPath(parent: string, candidate: string): boolean {\n const relativePath = relative(parent, candidate)\n return relativePath === ''\n || (relativePath !== '..' && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath))\n}\n\nfunction validateGoogleServicesFile(path: string, packageName: string): void {\n requireRegularFile(path, 'Google services file')\n\n let document: {\n client?: Array<{\n client_info?: { android_client_info?: { package_name?: string } }\n }>\n }\n try {\n document = JSON.parse(readFileSync(path, 'utf8'))\n }\n catch (error) {\n throw new Error(`Google services file must contain valid JSON: ${path}`, { cause: error })\n }\n\n const matchingClient = document.client?.some((client) => {\n return client.client_info?.android_client_info?.package_name === packageName\n })\n if (!matchingClient) {\n throw new Error(`Google services file has no client for Android package ${packageName}: ${path}`)\n }\n}\n\nfunction androidWebUrl(value: string, field: string): URL {\n let url: URL\n try {\n url = new URL(value)\n }\n catch {\n throw new Error(`Invalid ${field}: ${value}`)\n }\n\n const localDevelopment = url.protocol === 'http:' && LOCAL_DEVELOPMENT_HOSTS.has(url.hostname)\n if ((url.protocol !== 'https:' && !localDevelopment) || url.username || url.password) {\n throw new Error(`${field} must use HTTPS or local HTTP without credentials: ${value}`)\n }\n return url\n}\n\nfunction normalizeAndroidNetworkConfig(config: CraftAndroidConfig): void {\n const schemes = config.urlSchemes?.map(value => value.trim().toLowerCase()) ?? []\n if (schemes.some(value => !/^[a-z][a-z0-9+.-]*$/.test(value))) {\n throw new Error('Android deep-link schemes must be valid URI schemes')\n }\n config.urlSchemes = [...new Set(schemes)]\n if (config.enableDeepLinks && config.urlSchemes.length === 0) {\n throw new Error('Android deep links require at least one URL scheme')\n }\n\n const trustedOrigins = (config.trustedOrigins ?? []).map((value) => {\n return androidWebUrl(value, 'Android trusted origin').origin\n })\n if (config.devServerURL) {\n const devServer = androidWebUrl(config.devServerURL, 'Android dev server URL')\n config.devServerURL = devServer.toString()\n trustedOrigins.push(devServer.origin)\n }\n config.trustedOrigins = [...new Set(trustedOrigins)]\n}\n\nfunction validateAndroidConfig(config: CraftAndroidConfig): void {\n if (!config.appName.trim()) throw new Error('Android app name must not be empty')\n\n const packageSegments = config.packageName.split('.')\n if (packageSegments.length < 2\n || packageSegments.some(segment => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(segment) || KOTLIN_KEYWORDS.has(segment))) {\n throw new Error(`Invalid Android package name: ${config.packageName}`)\n }\n\n if (!/^#(?:[\\dA-F]{3,4}|[\\dA-F]{6}|[\\dA-F]{8})$/i.test(config.backgroundColor ?? '')) {\n throw new Error(`Invalid Android background color: ${config.backgroundColor}`)\n }\n\n for (const [name, value] of [\n ['versionCode', config.versionCode],\n ['minSdk', config.minSdk],\n ['compileSdk', config.compileSdk],\n ['targetSdk', config.targetSdk],\n ] as const) {\n if (!Number.isInteger(value) || Number(value) < 1) {\n throw new Error(`Android ${name} must be a positive integer`)\n }\n }\n\n if (Number(config.minSdk) < ANDROID_MIN_SDK) {\n throw new Error(`Android minSdk must be at least ${ANDROID_MIN_SDK} (Android 8.0) for the Craft runtime`)\n }\n if (Number(config.minSdk) > Number(config.targetSdk)) {\n throw new Error('Android minSdk must not exceed targetSdk')\n }\n if (Number(config.targetSdk) > Number(config.compileSdk)) {\n throw new Error('Android targetSdk must not exceed compileSdk')\n }\n}\n\nfunction writeAndroidConfig(output: string, config: CraftAndroidConfig): void {\n writeFileSync(join(output, 'craft.config.json'), JSON.stringify(config, null, 2))\n\n // Generator-only paths can reveal the developer machine's directory layout\n // and have no meaning to the installed app. Keep them in the project config\n // but never package them into the APK's assets.\n const runtimeConfig = { ...config }\n delete runtimeConfig.appIconPath\n delete runtimeConfig.googleServicesFile\n writeFileSync(\n join(output, 'app/src/main/assets/craft.config.json'),\n JSON.stringify(runtimeConfig, null, 2),\n )\n}\n\nexport function syncAndroidWebAssets(source: string, output: string): void {\n const sourcePath = resolve(source)\n if (!existsSync(sourcePath)) throw new Error(`Web asset path not found: ${source}`)\n const sourceStat = statSync(sourcePath)\n if (!sourceStat.isDirectory() && !sourceStat.isFile()) {\n throw new Error(`Web asset path must be a file or directory: ${source}`)\n }\n\n const assetsDir = resolve(output, 'app/src/main/assets')\n if (containsPath(sourcePath, assetsDir) || containsPath(assetsDir, sourcePath)) {\n throw new Error(`Web asset source must not overlap generated asset directory: ${source}`)\n }\n if (sourceStat.isDirectory()) {\n requireRegularFile(join(sourcePath, 'index.html'), 'Web asset directory entry point')\n }\n\n const configPath = join(assetsDir, 'craft.config.json')\n const config = existsSync(configPath) ? readFileSync(configPath) : undefined\n rmSync(assetsDir, { recursive: true, force: true })\n mkdirSync(assetsDir, { recursive: true })\n if (sourceStat.isDirectory()) cpSync(sourcePath, assetsDir, { recursive: true })\n else cpSync(sourcePath, join(assetsDir, 'index.html'))\n if (!existsSync(join(assetsDir, 'index.html'))) throw new Error(`Web asset directory must contain index.html: ${source}`)\n if (config) writeFileSync(configPath, config)\n}\n\nexport function renderAndroidPermissions(config: CraftAndroidConfig): string {\n const permissions = new Set(['android.permission.INTERNET', 'android.permission.ACCESS_NETWORK_STATE'])\n if (config.enableSpeechRecognition) permissions.add('android.permission.RECORD_AUDIO')\n if (config.enableCamera) permissions.add('android.permission.CAMERA')\n if (config.enableHaptics) permissions.add('android.permission.VIBRATE')\n if (config.enableBiometric) permissions.add('android.permission.USE_BIOMETRIC')\n if (config.enableGeolocation || config.enableBackgroundLocation) {\n permissions.add('android.permission.ACCESS_FINE_LOCATION')\n permissions.add('android.permission.ACCESS_COARSE_LOCATION')\n }\n if (config.enableBackgroundLocation) {\n permissions.add('android.permission.ACCESS_BACKGROUND_LOCATION')\n permissions.add('android.permission.FOREGROUND_SERVICE')\n permissions.add('android.permission.FOREGROUND_SERVICE_LOCATION')\n }\n if (config.enablePushNotifications) permissions.add('android.permission.POST_NOTIFICATIONS')\n if (config.enableKeepAwake) permissions.add('android.permission.WAKE_LOCK')\n if (config.enableHealthConnect) {\n permissions.add('android.permission.health.READ_STEPS')\n permissions.add('android.permission.health.READ_HEART_RATE')\n permissions.add('android.permission.health.READ_ACTIVE_CALORIES_BURNED')\n permissions.add('android.permission.health.READ_DISTANCE')\n permissions.add('android.permission.health.READ_EXERCISE')\n permissions.add('android.permission.health.WRITE_EXERCISE')\n permissions.add('android.permission.health.WRITE_ACTIVE_CALORIES_BURNED')\n permissions.add('android.permission.health.WRITE_DISTANCE')\n }\n return [...permissions].map(permission => ` <uses-permission android:name=\"${permission}\" />`).join('\\n')\n}\n\nexport function renderAndroidDeepLinks(config: CraftAndroidConfig): string {\n if (!config.enableDeepLinks) return ''\n const schemes = [...new Set(config.urlSchemes?.map(value => value.trim()).filter(value => /^[a-z][a-z0-9+.-]*$/i.test(value)) ?? [])]\n return schemes.map(scheme => ` <intent-filter>\n <action android:name=\"android.intent.action.VIEW\" />\n <category android:name=\"android.intent.category.DEFAULT\" />\n <category android:name=\"android.intent.category.BROWSABLE\" />\n <data android:scheme=\"${scheme}\" />\n </intent-filter>`).join('\\n')\n}\n\n/**\n * Initialize a new Android project\n */\n/**\n * The ABI directories a runtime directory is expected to hold.\n *\n * These are Android's own names for the two architectures craft builds, and\n * they are also `zig build build-android-all`'s output layout — `zig-out/android/`\n * already contains `arm64-v8a/libcraft.so` and `x86_64/libcraft.so`, which is\n * exactly the shape `app/src/main/jniLibs/` wants. So the install below is a\n * copy and not a rearrangement.\n *\n * x86_64 is not optional-in-practice: every Android emulator anyone tests on\n * is x86_64, so a runtime shipped for arm64 alone is one that never runs in\n * CI and never runs on a developer's machine either.\n */\nconst RUNTIME_ABIS = ['arm64-v8a', 'x86_64'] as const\n\n/**\n * Where the runtime directory comes from when the caller does not say.\n *\n * The same shape as `CRAFT_BIN` and the iOS builder's `CRAFT_IOS_RUNTIME`: an\n * explicit override for the monorepo dev loop, not a lookup path. Shipping the\n * runtime to real apps means putting these libraries in the pantry package\n * beside the `craft` binary, which is a distribution decision this function\n * does not make.\n */\nexport function resolveRuntimeDir(override?: string | null): string | null {\n if (override === null) return null\n const dir = override ?? process.env.CRAFT_ANDROID_RUNTIME\n if (!dir) return null\n if (!existsSync(dir)) {\n const source = override === undefined ? 'CRAFT_ANDROID_RUNTIME points at' : 'runtimeDir is'\n throw new Error(`${source} ${dir}, which does not exist.`)\n }\n return dir\n}\n\n/**\n * Copy the Zig runtime into the generated project as\n * `app/src/main/jniLibs/<abi>/libcraft.so`.\n *\n * That path is AGP's default `jniLibs.srcDirs`, so nothing in the Gradle\n * templates has to know about it — the library is packaged into the APK and\n * `System.loadLibrary(\"craft\")` finds it because the file is named for the\n * `craft` it asks for.\n *\n * Until this existed, nothing put the library anywhere. `CraftNative` caught\n * the `UnsatisfiedLinkError`, set `isAvailable = false`, and every action fell\n * through to the Kotlin shim — by design, so an app with no runtime still\n * works, which is also why nobody noticed that *every* generated app was in\n * that state and the whole Android half of the Zig bridge had never run.\n *\n * Returns true when a runtime was installed.\n */\nexport function installRuntime(output: string, runtimeDir: string): boolean {\n // Every ABI resolved before anything is written, so a half-populated\n // runtime directory cannot leave a project carrying one architecture and\n // claiming both. A single-ABI APK installs fine and then fails to load on\n // the other kind of device, which is the break that only shows up on\n // somebody else's phone.\n const found = RUNTIME_ABIS.map(abi => ({ abi, source: join(runtimeDir, abi, 'libcraft.so') }))\n .filter(entry => existsSync(entry.source))\n\n if (found.length === 0) {\n throw new Error(\n `${runtimeDir} has no <abi>/libcraft.so for any of ${RUNTIME_ABIS.join(', ')}. `\n + 'Run `zig build build-android-all -Doptimize=ReleaseSafe` in packages/zig and point at its zig-out/android.',\n )\n }\n\n if (found.length < RUNTIME_ABIS.length) {\n const missing = RUNTIME_ABIS.filter(abi => !found.some(entry => entry.abi === abi))\n console.warn(\n ` ⚠ only ${found.map(entry => entry.abi).join(', ')} was found; ${missing.join(', ')} is missing. `\n + 'The app will fall back to the Kotlin shim on those devices.',\n )\n }\n\n const dest = join(output, 'app/src/main/jniLibs')\n // Wiped rather than merged, so an ABI dropped from a later build does not\n // linger in the project as a stale library the APK still ships.\n rmSync(dest, { force: true, recursive: true })\n\n for (const { abi, source } of found) {\n const abiDir = join(dest, abi)\n mkdirSync(abiDir, { recursive: true })\n cpSync(source, join(abiDir, 'libcraft.so'))\n }\n\n return true\n}\n\nexport async function init(options: InitOptions): Promise<void> {\n const { name, packageName, output } = options\n\n console.log(`\\n⚡ Initializing Craft Android project: ${name}`)\n console.log(` Output: ${output}\\n`)\n\n // Generate package name from app name if not provided\n const finalPackageName = packageName || `com.craft.${generatedPackageSegment(name)}`\n const packagePath = finalPackageName.replace(/\\./g, '/')\n\n // Command-level identity always wins over duplicate fields in `config`.\n // Normalize implied capabilities before validation so craft.config.json and\n // the generated Gradle source describe the same effective project.\n // Undefined keeps the default instead of erasing it (see the iOS builder).\n const given = Object.fromEntries(Object.entries(options.config ?? {}).filter(([, value]) => value !== undefined))\n const config: CraftAndroidConfig = {\n ...DEFAULT_CONFIG,\n ...given,\n appName: name,\n packageName: finalPackageName,\n }\n if (config.enableBackgroundLocation) config.enableGeolocation = true\n if (config.enableHealthConnect) {\n config.compileSdk = Math.max(config.compileSdk ?? 36, 36)\n }\n normalizeAndroidNetworkConfig(config)\n validateAndroidConfig(config)\n if (config.enablePushNotifications && !config.googleServicesFile) {\n throw new Error('Android push notifications require a googleServicesFile')\n }\n if (config.googleServicesFile) validateGoogleServicesFile(config.googleServicesFile, finalPackageName)\n if (config.appIconPath) requireRegularFile(config.appIconPath, 'App icon')\n const appIconExtension = config.appIconPath ? extname(config.appIconPath).toLowerCase() : undefined\n if (config.appIconPath && !['.gif', '.jpg', '.png', '.webp'].includes(appIconExtension ?? '')) {\n throw new Error(`Unsupported Android app icon format: ${appIconExtension || '(none)'}`)\n }\n\n // Create directory structure\n const dirs = [\n output,\n join(output, 'app/src/main/java', packagePath),\n join(output, 'app/src/main/res/layout'),\n join(output, 'app/src/main/res/values'),\n join(output, 'app/src/main/res/drawable'),\n join(output, 'app/src/main/assets'),\n join(output, 'gradle/wrapper'),\n ]\n\n if (existsSync(output) && !statSync(output).isDirectory()) {\n throw new Error(`Android project output must be a directory: ${output}`)\n }\n\n for (const dir of dirs) {\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true })\n }\n }\n\n // Create craft.config.json\n writeAndroidConfig(output, config)\n\n const hasGoogleServices = Boolean(config.googleServicesFile)\n if (config.googleServicesFile) {\n cpSync(config.googleServicesFile, join(output, 'app/google-services.json'))\n }\n\n // Copy templates\n const mainActivityTemplate = readFileSync(join(TEMPLATES_DIR, 'MainActivity.kt.template'), 'utf-8')\n const mainActivity = mainActivityTemplate\n .replace(/\\{\\{PACKAGE_NAME\\}\\}/g, finalPackageName)\n .replace(/\\{\\{APP_NAME\\}\\}/g, name)\n\n writeFileSync(join(output, 'app/src/main/java', packagePath, 'MainActivity.kt'), mainActivity)\n\n // Create CraftBridge.kt\n const craftBridgeTemplate = readFileSync(join(TEMPLATES_DIR, 'CraftBridge.kt.template'), 'utf-8')\n const craftBridge = craftBridgeTemplate\n .replace(/\\{\\{PROMISE_RUNTIME\\}\\}/g, () => renderAndroidPromiseRuntime(' '))\n .replace(/\\{\\{PACKAGE_NAME\\}\\}/g, finalPackageName)\n .replace(/\\{\\{ENABLE_SPEECH\\}\\}/g, String(Boolean(config.enableSpeechRecognition)))\n .replace(/\\{\\{ENABLE_HAPTICS\\}\\}/g, String(Boolean(config.enableHaptics)))\n .replace(/\\{\\{ENABLE_SHARE\\}\\}/g, String(Boolean(config.enableShare)))\n .replace(/\\{\\{ENABLE_CAMERA\\}\\}/g, String(Boolean(config.enableCamera)))\n .replace(/\\{\\{ENABLE_BIOMETRIC\\}\\}/g, String(Boolean(config.enableBiometric)))\n .replace(/\\{\\{ENABLE_PUSH\\}\\}/g, String(Boolean(config.enablePushNotifications)))\n .replace(/\\{\\{ENABLE_SECURE_STORAGE\\}\\}/g, String(Boolean(config.enableSecureStorage)))\n .replace(/\\{\\{ENABLE_GEOLOCATION\\}\\}/g, String(Boolean(config.enableGeolocation)))\n .replace(/\\{\\{ENABLE_BACKGROUND_LOCATION\\}\\}/g, String(Boolean(config.enableBackgroundLocation)))\n .replace(/\\{\\{ENABLE_KEEP_AWAKE\\}\\}/g, String(Boolean(config.enableKeepAwake)))\n .replace(/\\{\\{ENABLE_DEEP_LINKS\\}\\}/g, String(Boolean(config.enableDeepLinks)))\n .replace(/\\{\\{ENABLE_HEALTH_CONNECT\\}\\}/g, String(Boolean(config.enableHealthConnect)))\n .replace(/\\{\\{FIREBASE_IMPORT\\}\\}/g, config.enablePushNotifications ? 'import com.google.firebase.messaging.FirebaseMessaging' : '')\n .replace(/\\{\\{REGISTER_PUSH_IMPLEMENTATION\\}\\}/g, config.enablePushNotifications\n ? `activity.runOnUiThread {\n if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU\n && ContextCompat.checkSelfPermission(activity, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {\n ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 4204)\n }\n try {\n FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->\n val token = if (task.isSuccessful) task.result else null\n val callback = if (token.isNullOrBlank()) \"window._craftPushReject\" else \"window._craftPushResolve\"\n val payload = JSONObject.quote(token ?: task.exception?.message ?: \"Firebase Cloud Messaging is not configured\")\n evaluatePromiseJavascript(\"$callback && $callback($payload)\")\n }\n } catch (error: Exception) {\n val payload = JSONObject.quote(error.message ?: \"Firebase Cloud Messaging is not configured\")\n evaluatePromiseJavascript(\"window._craftPushReject && window._craftPushReject($payload)\")\n }\n }`\n : `evaluatePromiseJavascript(\n \"window._craftPushReject && window._craftPushReject('Push notifications are disabled')\"\n )`)\n writeFileSync(join(output, 'app/src/main/java', packagePath, 'CraftBridge.kt'), craftBridge)\n\n // CraftNative goes to a fixed package, not the app's. The prebuilt\n // libcraft.so binds its natives by class name in JNI_OnLoad, and it cannot\n // know a name chosen here — so this one file is deliberately not templated\n // and deliberately not under packagePath.\n const craftNative = readFileSync(join(TEMPLATES_DIR, 'CraftNative.kt.template'), 'utf-8')\n const nativeDir = join(output, 'app/src/main/java/com/craft/runtime')\n mkdirSync(nativeDir, { recursive: true })\n writeFileSync(join(nativeDir, 'CraftNative.kt'), craftNative)\n // The bridge and native holder both reference the recording store/service,\n // even when the service is not registered in the manifest. Keep these\n // support types in the same fixed package as CraftNative so every generated\n // app compiles regardless of its chosen package name or feature flags.\n const serviceTemplate = readFileSync(join(TEMPLATES_DIR, 'LocationRecordingService.kt.template'), 'utf-8')\n writeFileSync(join(nativeDir, 'LocationRecordingService.kt'), serviceTemplate)\n const healthTemplate = readFileSync(join(\n TEMPLATES_DIR,\n config.enableHealthConnect ? 'CraftHealthConnect.kt.template' : 'CraftHealthConnectStub.kt.template',\n ), 'utf-8')\n writeFileSync(\n join(output, 'app/src/main/java', packagePath, 'CraftHealthConnect.kt'),\n healthTemplate.replace(/\\{\\{PACKAGE_NAME\\}\\}/g, finalPackageName),\n )\n // Create AndroidManifest.xml\n const manifestTemplate = readFileSync(join(TEMPLATES_DIR, 'AndroidManifest.xml.template'), 'utf-8')\n const manifest = manifestTemplate\n .replace(/\\{\\{PACKAGE_NAME\\}\\}/g, finalPackageName)\n .replace(/\\{\\{APP_NAME\\}\\}/g, name)\n .replace(/\\{\\{PERMISSIONS\\}\\}/g, renderAndroidPermissions(config))\n .replace(/\\{\\{USES_CLEARTEXT\\}\\}/g, config.devServerURL?.startsWith('http://') ? 'true' : 'false')\n .replace(/\\{\\{DEEP_LINK_INTENT_FILTERS\\}\\}/g, renderAndroidDeepLinks(config))\n .replace(/\\{\\{BACKGROUND_SERVICE\\}\\}/g, config.enableBackgroundLocation\n ? ' <service android:name=\"com.craft.runtime.LocationRecordingService\" android:exported=\"false\" android:foregroundServiceType=\"location\" android:stopWithTask=\"false\" />'\n : '')\n .replace(/\\{\\{HEALTH_CONNECT_QUERIES\\}\\}/g, config.enableHealthConnect\n ? ' <queries>\\n <package android:name=\"com.google.android.apps.healthdata\" />\\n </queries>'\n : '')\n .replace(/\\{\\{HEALTH_CONNECT_RATIONALE\\}\\}/g, config.enableHealthConnect\n ? ` <intent-filter>\n <action android:name=\"androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE\" />\n </intent-filter>\n <intent-filter>\n <action android:name=\"android.intent.action.VIEW_PERMISSION_USAGE\" />\n <category android:name=\"android.intent.category.HEALTH_PERMISSIONS\" />\n </intent-filter>`\n : '')\n\n writeFileSync(join(output, 'app/src/main/AndroidManifest.xml'), manifest)\n\n // Create build.gradle.kts (project level)\n const projectGradleTemplate = readFileSync(join(TEMPLATES_DIR, 'build.gradle.kts.project.template'), 'utf-8')\n writeFileSync(join(output, 'build.gradle.kts'), projectGradleTemplate\n .replace(/\\{\\{GOOGLE_SERVICES_PLUGIN\\}\\}/g, hasGoogleServices ? ' id(\"com.google.gms.google-services\") version \"4.4.2\" apply false' : ''))\n\n // Create build.gradle.kts (app level)\n const appGradleTemplate = readFileSync(join(TEMPLATES_DIR, 'build.gradle.kts.app.template'), 'utf-8')\n const appGradle = appGradleTemplate\n .replace(/\\{\\{PACKAGE_NAME\\}\\}/g, finalPackageName)\n .replace(/\\{\\{VERSION_NAME\\}\\}/g, escapeKotlinString(config.version || '1.0.0'))\n .replace(/\\{\\{VERSION_CODE\\}\\}/g, String(config.versionCode || 1))\n .replace(/\\{\\{MIN_SDK\\}\\}/g, String(config.minSdk ?? ANDROID_MIN_SDK))\n .replace(/\\{\\{COMPILE_SDK\\}\\}/g, String(config.compileSdk || 36))\n .replace(/\\{\\{TARGET_SDK\\}\\}/g, String(config.targetSdk || 35))\n .replace(/\\{\\{GOOGLE_SERVICES_PLUGIN\\}\\}/g, hasGoogleServices ? ' id(\"com.google.gms.google-services\")' : '')\n .replace(/\\{\\{FIREBASE_MESSAGING_DEPENDENCY\\}\\}/g, config.enablePushNotifications\n ? ' implementation(\"com.google.firebase:firebase-messaging:24.1.0\")'\n : '')\n .replace(/\\{\\{HEALTH_CONNECT_DEPENDENCIES\\}\\}/g, config.enableHealthConnect\n ? ` implementation(\"androidx.health.connect:connect-client:1.1.0\")\n implementation(\"org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1\")`\n : '')\n\n writeFileSync(join(output, 'app/build.gradle.kts'), appGradle)\n const proguardTemplate = readFileSync(join(TEMPLATES_DIR, 'proguard-rules.pro.template'), 'utf-8')\n writeFileSync(\n join(output, 'app/proguard-rules.pro'),\n proguardTemplate.replace(/\\{\\{PACKAGE_NAME\\}\\}/g, finalPackageName),\n )\n\n // Create settings.gradle.kts\n const settingsTemplate = readFileSync(join(TEMPLATES_DIR, 'settings.gradle.kts.template'), 'utf-8')\n const settings = settingsTemplate.replace(\n /\\{\\{APP_NAME\\}\\}/g,\n escapeKotlinString(generatedGradleProjectName(name)),\n )\n writeFileSync(join(output, 'settings.gradle.kts'), settings)\n\n // Create gradle.properties\n const gradleProperties = `org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8\nandroid.useAndroidX=true\nkotlin.code.style=official\nandroid.nonTransitiveRClass=true\n`\n writeFileSync(join(output, 'gradle.properties'), gradleProperties)\n\n // Create local.properties placeholder\n writeFileSync(join(output, 'local.properties'), '# SDK location will be set by Android Studio\\n')\n\n // Create gradle wrapper properties\n const gradleWrapperProps = `distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\\\://services.gradle.org/distributions/gradle-8.11.1-bin.zip\nnetworkTimeout=10000\nvalidateDistributionUrl=true\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n`\n writeFileSync(join(output, 'gradle/wrapper/gradle-wrapper.properties'), gradleWrapperProps)\n\n // Create res/values files\n const stringsXml = `<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <string name=\"app_name\">${escapeXml(name)}</string>\n</resources>\n`\n writeFileSync(join(output, 'app/src/main/res/values/strings.xml'), stringsXml)\n\n const colorsXml = `<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <color name=\"primary\">#1a1a2e</color>\n <color name=\"primary_dark\">#16162b</color>\n <color name=\"accent\">#4ade80</color>\n <color name=\"background\">${config.backgroundColor}</color>\n</resources>\n`\n writeFileSync(join(output, 'app/src/main/res/values/colors.xml'), colorsXml)\n\n const appIconXml = `<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"108dp\"\n android:height=\"108dp\"\n android:viewportWidth=\"108\"\n android:viewportHeight=\"108\">\n <path android:fillColor=\"${config.backgroundColor}\" android:pathData=\"M0,0h108v108h-108z\" />\n <path android:fillColor=\"#10B981\" android:pathData=\"M18,78L42,42l12,18l12,-12l24,30z\" />\n</vector>\n`\n if (config.appIconPath) {\n cpSync(\n config.appIconPath,\n join(output, `app/src/main/res/drawable/craft_app_icon${appIconExtension}`),\n )\n }\n else {\n writeFileSync(join(output, 'app/src/main/res/drawable/craft_app_icon.xml'), appIconXml)\n }\n\n const themesXml = `<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <style name=\"Theme.CraftApp\" parent=\"Theme.Material3.DayNight.NoActionBar\">\n <item name=\"android:statusBarColor\">@color/primary_dark</item>\n <item name=\"android:navigationBarColor\">@color/primary</item>\n <item name=\"android:windowBackground\">@color/background</item>\n </style>\n</resources>\n`\n writeFileSync(join(output, 'app/src/main/res/values/themes.xml'), themesXml)\n\n // Create activity_main.xml\n const activityMainXml = `<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout\n xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:layout_width=\"match_parent\"\n android:layout_height=\"match_parent\"\n android:fitsSystemWindows=\"true\">\n\n <WebView\n android:id=\"@+id/webview\"\n android:layout_width=\"match_parent\"\n android:layout_height=\"match_parent\" />\n\n</androidx.coordinatorlayout.widget.CoordinatorLayout>\n`\n writeFileSync(join(output, 'app/src/main/res/layout/activity_main.xml'), activityMainXml)\n\n // Create placeholder index.html\n const placeholderHtml = `<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"viewport-fit=cover, width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\">\n <title>${escapeXml(name)}</title>\n <style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body {\n font-family: 'Roboto', sans-serif;\n background: ${config.backgroundColor};\n color: white;\n min-height: 100vh;\n display: flex;\n justify-content: center;\n align-items: center;\n }\n .container { text-align: center; padding: 2rem; }\n h1 { font-size: 2.5rem; margin-bottom: 1rem; }\n p { opacity: 0.7; }\n .ready { color: #4ade80; font-size: 0.9rem; margin-top: 2rem; }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <h1>⚡ ${escapeXml(name)}</h1>\n <p>Built with Craft Android</p>\n <p class=\"ready\" id=\"status\">Waiting for Craft bridge...</p>\n </div>\n <script>\n window.addEventListener('craftReady', (e) => {\n document.getElementById('status').textContent = \\`✓ Craft bridge ready (platform: \\${e.detail.platform})\\`;\n console.log('Craft capabilities:', e.detail.capabilities);\n });\n </script>\n</body>\n</html>`\n\n writeFileSync(join(output, 'app/src/main/assets/index.html'), placeholderHtml)\n\n // The Zig runtime, when one was configured. Absent, the generated app is the\n // Kotlin shim and behaves exactly as it always has — which is the fallback\n // `CraftNative` was written for, not a failure.\n const runtimeDir = resolveRuntimeDir(options.runtimeDir)\n if (runtimeDir) {\n installRuntime(output, runtimeDir)\n console.log(' Installed the Zig runtime from', runtimeDir)\n }\n\n console.log('✅ Project initialized')\n console.log('')\n console.log('Next steps:')\n console.log(` 1. cd ${output}`)\n console.log(' 2. Add your web content to app/src/main/assets/index.html')\n console.log(' 3. Run: craft android build')\n console.log(' 4. Run: craft android open')\n console.log('')\n}\n\n/**\n * Build Android project\n */\nexport async function build(options: BuildOptions): Promise<void> {\n const { htmlPath, devServer, output, release, compile = true } = options\n\n console.log('\\n📦 Building Craft Android project...')\n\n // Load config\n const configPath = join(output, 'craft.config.json')\n if (!existsSync(configPath)) {\n throw new Error(`No craft.config.json found in ${output}. Run 'craft android init' first.`)\n }\n\n const config: CraftAndroidConfig = JSON.parse(readFileSync(configPath, 'utf-8'))\n\n // Refresh the runtime the same way the iOS builder does: only for a project\n // that already has one, so `craft android build` in a shim-only project does\n // not quietly acquire a native library it was never generated with.\n if (existsSync(join(output, 'app/src/main/jniLibs'))) {\n const runtimeDir = resolveRuntimeDir(options.runtimeDir)\n if (runtimeDir) {\n installRuntime(output, runtimeDir)\n console.log(' Refreshed the Zig runtime from', runtimeDir)\n }\n else {\n console.log(' Keeping the Zig runtime installed at init (no runtime directory configured)')\n }\n }\n\n // Update dev server URL if provided\n if (devServer) {\n const url = androidWebUrl(devServer, 'Android dev server URL')\n config.devServerURL = url.toString()\n config.trustedOrigins = [...new Set([\n ...(config.trustedOrigins ?? []).map(value => androidWebUrl(value, 'Android trusted origin').origin),\n url.origin,\n ])]\n writeAndroidConfig(output, config)\n console.log(` Dev server: ${devServer}`)\n }\n\n // Copy the complete web application if provided.\n if (htmlPath) {\n syncAndroidWebAssets(htmlPath, output)\n config.hasBundledFallback = Boolean(devServer)\n writeAndroidConfig(output, config)\n console.log(` Synced: ${htmlPath} → assets/`)\n }\n\n if (!compile) return\n\n // Check if gradlew exists\n const gradlewPath = join(output, 'gradlew')\n if (!existsSync(gradlewPath)) {\n console.log(' Generating Gradle wrapper...')\n try {\n await $`cd ${output} && gradle wrapper`.quiet()\n }\n catch (error) {\n throw new Error('Gradle is unavailable. Install Android Studio or Gradle before building Android.', { cause: error })\n }\n }\n\n // Build the project\n const buildType = release ? 'assembleRelease' : 'assembleDebug'\n console.log(` Building ${release ? 'release' : 'debug'} APK...`)\n\n try {\n await $`cd ${output} && ./gradlew ${buildType}`\n const apkPath = release\n ? 'app/build/outputs/apk/release/app-release.apk'\n : 'app/build/outputs/apk/debug/app-debug.apk'\n console.log(`✅ APK built: ${apkPath}`)\n }\ncatch (error) {\n console.error('Build failed. Open in Android Studio for details.')\n throw error\n }\n\n console.log('')\n}\n\n/**\n * Open Android project in Android Studio\n */\nexport async function open(options: OpenOptions): Promise<void> {\n const { output } = options\n\n if (!existsSync(output)) {\n throw new Error(`No Android project found in ${output}. Run 'craft android init' first.`)\n }\n\n console.log(`🚀 Opening Android project in Android Studio...`)\n\n // Try to open with Android Studio\n try {\n // macOS\n await $`open -a \"Android Studio\" ${output}`.quiet()\n }\ncatch {\n try {\n // Linux\n await $`studio ${output}`.quiet()\n }\ncatch {\n console.log('⚠️ Android Studio not found.')\n console.log(' Please open the project manually in Android Studio:')\n console.log(` ${output}`)\n }\n }\n}\n\n/**\n * Run on Android device or emulator\n */\nexport async function run(options: RunOptions): Promise<void> {\n const { device, output } = options\n\n // First build\n await build({ output })\n\n // Install and run\n console.log('📱 Installing and running on device...')\n\n try {\n const apkPath = join(output, 'app/build/outputs/apk/debug/app-debug.apk')\n\n if (device) {\n await $`adb -s ${device} install -r ${apkPath}`\n }\nelse {\n await $`adb install -r ${apkPath}`\n }\n\n // Get package name from config\n const config: CraftAndroidConfig = JSON.parse(\n readFileSync(join(output, 'craft.config.json'), 'utf-8')\n )\n\n // Launch the app\n const launchCmd = `${config.packageName}/.MainActivity`\n if (device) {\n await $`adb -s ${device} shell am start -n ${launchCmd}`\n }\nelse {\n await $`adb shell am start -n ${launchCmd}`\n }\n\n console.log('✅ App launched')\n }\ncatch (error) {\n console.error('Failed to install/run. Make sure:')\n console.error(' 1. ADB is installed and in PATH')\n console.error(' 2. A device/emulator is connected (run: adb devices)')\n throw error\n }\n}\n",
|
|
5
|
+
"/**\n * Craft Android Builder\n *\n * Generates native Android apps from web content using WebView.\n */\n\nimport { cpSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'\nimport { dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path'\nimport { $ } from 'bun'\nimport { renderAndroidPromiseRuntime } from './promise-runtime'\n\nconst TEMPLATES_DIR = join(dirname(import.meta.dir), 'templates')\nconst LOCAL_DEVELOPMENT_HOSTS = new Set(['localhost', '127.0.0.1', '10.0.2.2'])\nconst KOTLIN_KEYWORDS = new Set([\n 'as',\n 'break',\n 'class',\n 'continue',\n 'do',\n 'else',\n 'false',\n 'for',\n 'fun',\n 'if',\n 'in',\n 'interface',\n 'is',\n 'null',\n 'object',\n 'package',\n 'return',\n 'super',\n 'this',\n 'throw',\n 'true',\n 'try',\n 'typealias',\n 'typeof',\n 'val',\n 'var',\n 'when',\n 'while',\n])\n\n/** Oldest Android API level supported by the generated Kotlin runtime. */\nexport const ANDROID_MIN_SDK = 26\n\nexport interface CraftAndroidConfig {\n appName: string\n packageName: string\n renderer?: 'web' | 'native'\n version?: string\n versionCode?: number\n darkMode?: boolean\n backgroundColor?: string\n enableSpeechRecognition?: boolean\n enableHaptics?: boolean\n enableShare?: boolean\n enableCamera?: boolean\n enableBiometric?: boolean\n enablePushNotifications?: boolean\n enableSecureStorage?: boolean\n enableGeolocation?: boolean\n enableBackgroundLocation?: boolean\n enableHealthConnect?: boolean\n enableKeepAwake?: boolean\n enableDeepLinks?: boolean\n urlSchemes?: string[]\n trustedOrigins?: string[]\n appIconPath?: string\n googleServicesFile?: string\n devServerURL?: string\n hasBundledFallback?: boolean\n /** Android API level, at least ANDROID_MIN_SDK (26). */\n minSdk?: number\n compileSdk?: number\n targetSdk?: number\n}\n\nexport interface InitOptions {\n name: string\n packageName?: string\n output: string\n config?: Partial<CraftAndroidConfig>\n /**\n * Where to find the Zig runtime's `<abi>/libcraft.so`.\n *\n * `null` means no runtime regardless of the environment; `undefined` falls\n * through to `CRAFT_ANDROID_RUNTIME`. Same shape as the iOS builder's\n * `runtimeDir`.\n */\n runtimeDir?: string | null\n}\n\nexport interface BuildOptions {\n htmlPath?: string\n nativeBundlePath?: string\n devServer?: string\n output: string\n release?: boolean\n compile?: boolean\n /** Same meaning as `InitOptions.runtimeDir`; refreshes what init installed. */\n runtimeDir?: string | null\n}\n\nexport interface OpenOptions {\n output: string\n}\n\nexport interface RunOptions {\n device?: string\n output: string\n}\n\nconst DEFAULT_CONFIG: Omit<CraftAndroidConfig, 'appName' | 'packageName'> = {\n renderer: 'web',\n version: '1.0.0',\n versionCode: 1,\n darkMode: true,\n backgroundColor: '#0b1712',\n enableSpeechRecognition: false,\n enableHaptics: false,\n enableShare: false,\n enableCamera: false,\n enableBiometric: false,\n enablePushNotifications: false,\n enableSecureStorage: false,\n enableGeolocation: false,\n enableBackgroundLocation: false,\n enableHealthConnect: false,\n enableKeepAwake: false,\n enableDeepLinks: false,\n urlSchemes: [],\n trustedOrigins: [],\n minSdk: ANDROID_MIN_SDK,\n compileSdk: 36,\n targetSdk: 35,\n}\n\nfunction escapeXml(value: string): string {\n return value\n .replaceAll('&', '&')\n .replaceAll('<', '<')\n .replaceAll('>', '>')\n .replaceAll('\"', '"')\n .replaceAll('\\'', ''')\n}\n\nfunction escapeKotlinString(value: string): string {\n return value\n .replaceAll('\\\\', '\\\\\\\\')\n .replaceAll('\"', '\\\\\"')\n .replaceAll('$', '\\\\$')\n .replaceAll('\\r', '\\\\r')\n .replaceAll('\\n', '\\\\n')\n}\n\nfunction generatedPackageSegment(name: string): string {\n const normalized = name.toLowerCase().replace(/[^a-z0-9_]/g, '')\n if (!normalized) return 'app'\n return /^[a-z_]/.test(normalized) ? normalized : `app${normalized}`\n}\n\nfunction generatedGradleProjectName(name: string): string {\n const invalidCharacters = ['/', '\\\\', ':', '<', '>', '\"', '?', '*', '|']\n const normalized = invalidCharacters.reduce(\n (value, character) => value.replaceAll(character, '-'),\n name,\n ).trim()\n return normalized || 'craft-app'\n}\n\nfunction requireRegularFile(path: string, label: string): void {\n if (!existsSync(path)) throw new Error(`${label} not found: ${path}`)\n if (!statSync(path).isFile()) throw new Error(`${label} must be a file: ${path}`)\n}\n\nfunction containsPath(parent: string, candidate: string): boolean {\n const relativePath = relative(parent, candidate)\n return relativePath === ''\n || (relativePath !== '..' && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath))\n}\n\nfunction validateGoogleServicesFile(path: string, packageName: string): void {\n requireRegularFile(path, 'Google services file')\n\n let document: {\n client?: Array<{\n client_info?: { android_client_info?: { package_name?: string } }\n }>\n }\n try {\n document = JSON.parse(readFileSync(path, 'utf8'))\n }\n catch (error) {\n throw new Error(`Google services file must contain valid JSON: ${path}`, { cause: error })\n }\n\n const matchingClient = document.client?.some((client) => {\n return client.client_info?.android_client_info?.package_name === packageName\n })\n if (!matchingClient) {\n throw new Error(`Google services file has no client for Android package ${packageName}: ${path}`)\n }\n}\n\nfunction androidWebUrl(value: string, field: string): URL {\n let url: URL\n try {\n url = new URL(value)\n }\n catch {\n throw new Error(`Invalid ${field}: ${value}`)\n }\n\n const localDevelopment = url.protocol === 'http:' && LOCAL_DEVELOPMENT_HOSTS.has(url.hostname)\n if ((url.protocol !== 'https:' && !localDevelopment) || url.username || url.password) {\n throw new Error(`${field} must use HTTPS or local HTTP without credentials: ${value}`)\n }\n return url\n}\n\nfunction normalizeAndroidNetworkConfig(config: CraftAndroidConfig): void {\n const schemes = config.urlSchemes?.map(value => value.trim().toLowerCase()) ?? []\n if (schemes.some(value => !/^[a-z][a-z0-9+.-]*$/.test(value))) {\n throw new Error('Android deep-link schemes must be valid URI schemes')\n }\n config.urlSchemes = [...new Set(schemes)]\n if (config.enableDeepLinks && config.urlSchemes.length === 0) {\n throw new Error('Android deep links require at least one URL scheme')\n }\n\n const trustedOrigins = (config.trustedOrigins ?? []).map((value) => {\n return androidWebUrl(value, 'Android trusted origin').origin\n })\n if (config.devServerURL) {\n const devServer = androidWebUrl(config.devServerURL, 'Android dev server URL')\n config.devServerURL = devServer.toString()\n trustedOrigins.push(devServer.origin)\n }\n config.trustedOrigins = [...new Set(trustedOrigins)]\n}\n\nfunction validateAndroidConfig(config: CraftAndroidConfig): void {\n if (!config.appName.trim()) throw new Error('Android app name must not be empty')\n if (config.renderer !== undefined && config.renderer !== 'web' && config.renderer !== 'native') {\n throw new Error(`Unknown Android renderer: ${config.renderer}. Expected web or native.`)\n }\n\n const packageSegments = config.packageName.split('.')\n if (packageSegments.length < 2\n || packageSegments.some(segment => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(segment) || KOTLIN_KEYWORDS.has(segment))) {\n throw new Error(`Invalid Android package name: ${config.packageName}`)\n }\n\n if (!/^#(?:[\\dA-F]{3,4}|[\\dA-F]{6}|[\\dA-F]{8})$/i.test(config.backgroundColor ?? '')) {\n throw new Error(`Invalid Android background color: ${config.backgroundColor}`)\n }\n\n for (const [name, value] of [\n ['versionCode', config.versionCode],\n ['minSdk', config.minSdk],\n ['compileSdk', config.compileSdk],\n ['targetSdk', config.targetSdk],\n ] as const) {\n if (!Number.isInteger(value) || Number(value) < 1) {\n throw new Error(`Android ${name} must be a positive integer`)\n }\n }\n\n if (Number(config.minSdk) < ANDROID_MIN_SDK) {\n throw new Error(`Android minSdk must be at least ${ANDROID_MIN_SDK} (Android 8.0) for the Craft runtime`)\n }\n if (Number(config.minSdk) > Number(config.targetSdk)) {\n throw new Error('Android minSdk must not exceed targetSdk')\n }\n if (Number(config.targetSdk) > Number(config.compileSdk)) {\n throw new Error('Android targetSdk must not exceed compileSdk')\n }\n}\n\nfunction writeAndroidConfig(output: string, config: CraftAndroidConfig): void {\n writeFileSync(join(output, 'craft.config.json'), JSON.stringify(config, null, 2))\n\n // Generator-only paths can reveal the developer machine's directory layout\n // and have no meaning to the installed app. Keep them in the project config\n // but never package them into the APK's assets.\n const runtimeConfig = { ...config }\n delete runtimeConfig.appIconPath\n delete runtimeConfig.googleServicesFile\n writeFileSync(\n join(output, 'app/src/main/assets/craft.config.json'),\n JSON.stringify(runtimeConfig, null, 2),\n )\n}\n\nexport function syncAndroidWebAssets(source: string, output: string): void {\n const sourcePath = resolve(source)\n if (!existsSync(sourcePath)) throw new Error(`Web asset path not found: ${source}`)\n const sourceStat = statSync(sourcePath)\n if (!sourceStat.isDirectory() && !sourceStat.isFile()) {\n throw new Error(`Web asset path must be a file or directory: ${source}`)\n }\n\n const assetsDir = resolve(output, 'app/src/main/assets')\n if (containsPath(sourcePath, assetsDir) || containsPath(assetsDir, sourcePath)) {\n throw new Error(`Web asset source must not overlap generated asset directory: ${source}`)\n }\n if (sourceStat.isDirectory()) {\n requireRegularFile(join(sourcePath, 'index.html'), 'Web asset directory entry point')\n }\n\n const configPath = join(assetsDir, 'craft.config.json')\n const config = existsSync(configPath) ? readFileSync(configPath) : undefined\n rmSync(assetsDir, { recursive: true, force: true })\n mkdirSync(assetsDir, { recursive: true })\n if (sourceStat.isDirectory()) cpSync(sourcePath, assetsDir, { recursive: true })\n else cpSync(sourcePath, join(assetsDir, 'index.html'))\n if (!existsSync(join(assetsDir, 'index.html'))) throw new Error(`Web asset directory must contain index.html: ${source}`)\n if (config) writeFileSync(configPath, config)\n}\n\nexport function renderAndroidPermissions(config: CraftAndroidConfig): string {\n const permissions = new Set(['android.permission.INTERNET', 'android.permission.ACCESS_NETWORK_STATE'])\n if (config.enableSpeechRecognition) permissions.add('android.permission.RECORD_AUDIO')\n if (config.enableCamera) permissions.add('android.permission.CAMERA')\n if (config.enableHaptics) permissions.add('android.permission.VIBRATE')\n if (config.enableBiometric) permissions.add('android.permission.USE_BIOMETRIC')\n if (config.enableGeolocation || config.enableBackgroundLocation) {\n permissions.add('android.permission.ACCESS_FINE_LOCATION')\n permissions.add('android.permission.ACCESS_COARSE_LOCATION')\n }\n if (config.enableBackgroundLocation) {\n permissions.add('android.permission.ACCESS_BACKGROUND_LOCATION')\n permissions.add('android.permission.FOREGROUND_SERVICE')\n permissions.add('android.permission.FOREGROUND_SERVICE_LOCATION')\n }\n if (config.enablePushNotifications) permissions.add('android.permission.POST_NOTIFICATIONS')\n if (config.enableKeepAwake) permissions.add('android.permission.WAKE_LOCK')\n if (config.enableHealthConnect) {\n permissions.add('android.permission.health.READ_STEPS')\n permissions.add('android.permission.health.READ_HEART_RATE')\n permissions.add('android.permission.health.READ_ACTIVE_CALORIES_BURNED')\n permissions.add('android.permission.health.READ_DISTANCE')\n permissions.add('android.permission.health.READ_EXERCISE')\n permissions.add('android.permission.health.WRITE_EXERCISE')\n permissions.add('android.permission.health.WRITE_ACTIVE_CALORIES_BURNED')\n permissions.add('android.permission.health.WRITE_DISTANCE')\n }\n return [...permissions].map(permission => ` <uses-permission android:name=\"${permission}\" />`).join('\\n')\n}\n\nexport function renderAndroidDeepLinks(config: CraftAndroidConfig): string {\n if (!config.enableDeepLinks) return ''\n const schemes = [...new Set(config.urlSchemes?.map(value => value.trim()).filter(value => /^[a-z][a-z0-9+.-]*$/i.test(value)) ?? [])]\n return schemes.map(scheme => ` <intent-filter>\n <action android:name=\"android.intent.action.VIEW\" />\n <category android:name=\"android.intent.category.DEFAULT\" />\n <category android:name=\"android.intent.category.BROWSABLE\" />\n <data android:scheme=\"${scheme}\" />\n </intent-filter>`).join('\\n')\n}\n\n/**\n * Initialize a new Android project\n */\n/**\n * The ABI directories a runtime directory is expected to hold.\n *\n * These are Android's own names for the two architectures craft builds, and\n * they are also `zig build build-android-all`'s output layout — `zig-out/android/`\n * already contains `arm64-v8a/libcraft.so` and `x86_64/libcraft.so`, which is\n * exactly the shape `app/src/main/jniLibs/` wants. So the install below is a\n * copy and not a rearrangement.\n *\n * x86_64 is not optional-in-practice: every Android emulator anyone tests on\n * is x86_64, so a runtime shipped for arm64 alone is one that never runs in\n * CI and never runs on a developer's machine either.\n */\nconst RUNTIME_ABIS = ['arm64-v8a', 'x86_64'] as const\n\n/**\n * Where the runtime directory comes from when the caller does not say.\n *\n * The same shape as `CRAFT_BIN` and the iOS builder's `CRAFT_IOS_RUNTIME`: an\n * explicit override for the monorepo dev loop, not a lookup path. Shipping the\n * runtime to real apps means putting these libraries in the pantry package\n * beside the `craft` binary, which is a distribution decision this function\n * does not make.\n */\nexport function resolveRuntimeDir(override?: string | null): string | null {\n if (override === null) return null\n const dir = override ?? process.env.CRAFT_ANDROID_RUNTIME\n if (!dir) return null\n if (!existsSync(dir)) {\n const source = override === undefined ? 'CRAFT_ANDROID_RUNTIME points at' : 'runtimeDir is'\n throw new Error(`${source} ${dir}, which does not exist.`)\n }\n return dir\n}\n\n/**\n * Copy the Zig runtime into the generated project as\n * `app/src/main/jniLibs/<abi>/libcraft.so`.\n *\n * That path is AGP's default `jniLibs.srcDirs`, so nothing in the Gradle\n * templates has to know about it — the library is packaged into the APK and\n * `System.loadLibrary(\"craft\")` finds it because the file is named for the\n * `craft` it asks for.\n *\n * Until this existed, nothing put the library anywhere. `CraftNative` caught\n * the `UnsatisfiedLinkError`, set `isAvailable = false`, and every action fell\n * through to the Kotlin shim — by design, so an app with no runtime still\n * works, which is also why nobody noticed that *every* generated app was in\n * that state and the whole Android half of the Zig bridge had never run.\n *\n * Returns true when a runtime was installed.\n */\nexport function installRuntime(output: string, runtimeDir: string): boolean {\n // Every ABI resolved before anything is written, so a half-populated\n // runtime directory cannot leave a project carrying one architecture and\n // claiming both. A single-ABI APK installs fine and then fails to load on\n // the other kind of device, which is the break that only shows up on\n // somebody else's phone.\n const found = RUNTIME_ABIS.map(abi => ({ abi, source: join(runtimeDir, abi, 'libcraft.so') }))\n .filter(entry => existsSync(entry.source))\n\n if (found.length === 0) {\n throw new Error(\n `${runtimeDir} has no <abi>/libcraft.so for any of ${RUNTIME_ABIS.join(', ')}. `\n + 'Run `zig build build-android-all -Doptimize=ReleaseSafe` in packages/zig and point at its zig-out/android.',\n )\n }\n\n if (found.length < RUNTIME_ABIS.length) {\n const missing = RUNTIME_ABIS.filter(abi => !found.some(entry => entry.abi === abi))\n console.warn(\n ` ⚠ only ${found.map(entry => entry.abi).join(', ')} was found; ${missing.join(', ')} is missing. `\n + 'The app will fall back to the Kotlin shim on those devices.',\n )\n }\n\n const dest = join(output, 'app/src/main/jniLibs')\n // Wiped rather than merged, so an ABI dropped from a later build does not\n // linger in the project as a stale library the APK still ships.\n rmSync(dest, { force: true, recursive: true })\n\n for (const { abi, source } of found) {\n const abiDir = join(dest, abi)\n mkdirSync(abiDir, { recursive: true })\n cpSync(source, join(abiDir, 'libcraft.so'))\n }\n\n return true\n}\n\nexport async function init(options: InitOptions): Promise<void> {\n const { name, packageName, output } = options\n\n console.log(`\\n⚡ Initializing Craft Android project: ${name}`)\n console.log(` Output: ${output}\\n`)\n\n // Generate package name from app name if not provided\n const finalPackageName = packageName || `com.craft.${generatedPackageSegment(name)}`\n const packagePath = finalPackageName.replace(/\\./g, '/')\n\n // Command-level identity always wins over duplicate fields in `config`.\n // Normalize implied capabilities before validation so craft.config.json and\n // the generated Gradle source describe the same effective project.\n // Undefined keeps the default instead of erasing it (see the iOS builder).\n const given = Object.fromEntries(Object.entries(options.config ?? {}).filter(([, value]) => value !== undefined))\n const config: CraftAndroidConfig = {\n ...DEFAULT_CONFIG,\n ...given,\n appName: name,\n packageName: finalPackageName,\n }\n if (config.enableBackgroundLocation) config.enableGeolocation = true\n if (config.enableHealthConnect) {\n config.compileSdk = Math.max(config.compileSdk ?? 36, 36)\n }\n normalizeAndroidNetworkConfig(config)\n validateAndroidConfig(config)\n if (config.enablePushNotifications && !config.googleServicesFile) {\n throw new Error('Android push notifications require a googleServicesFile')\n }\n if (config.googleServicesFile) validateGoogleServicesFile(config.googleServicesFile, finalPackageName)\n if (config.appIconPath) requireRegularFile(config.appIconPath, 'App icon')\n const appIconExtension = config.appIconPath ? extname(config.appIconPath).toLowerCase() : undefined\n if (config.appIconPath && !['.gif', '.jpg', '.png', '.webp'].includes(appIconExtension ?? '')) {\n throw new Error(`Unsupported Android app icon format: ${appIconExtension || '(none)'}`)\n }\n\n // Create directory structure\n const dirs = [\n output,\n join(output, 'app/src/main/java', packagePath),\n join(output, 'app/src/main/res/layout'),\n join(output, 'app/src/main/res/values'),\n join(output, 'app/src/main/res/drawable'),\n join(output, 'app/src/main/assets'),\n join(output, 'gradle/wrapper'),\n ]\n\n if (existsSync(output) && !statSync(output).isDirectory()) {\n throw new Error(`Android project output must be a directory: ${output}`)\n }\n\n for (const dir of dirs) {\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true })\n }\n }\n\n // Create craft.config.json\n writeAndroidConfig(output, config)\n\n const hasGoogleServices = Boolean(config.googleServicesFile)\n if (config.googleServicesFile) {\n cpSync(config.googleServicesFile, join(output, 'app/google-services.json'))\n }\n\n // Copy templates\n const mainActivityTemplate = readFileSync(join(\n TEMPLATES_DIR,\n config.renderer === 'native' ? 'MainActivityNative.kt.template' : 'MainActivity.kt.template',\n ), 'utf-8')\n const mainActivity = mainActivityTemplate\n .replace(/\\{\\{PACKAGE_NAME\\}\\}/g, finalPackageName)\n .replace(/\\{\\{APP_NAME\\}\\}/g, name)\n\n writeFileSync(join(output, 'app/src/main/java', packagePath, 'MainActivity.kt'), mainActivity)\n\n // Create CraftBridge.kt\n const craftBridgeTemplate = readFileSync(join(TEMPLATES_DIR, 'CraftBridge.kt.template'), 'utf-8')\n const craftBridge = craftBridgeTemplate\n .replace(/\\{\\{PROMISE_RUNTIME\\}\\}/g, () => renderAndroidPromiseRuntime(' '))\n .replace(/\\{\\{PACKAGE_NAME\\}\\}/g, finalPackageName)\n .replace(/\\{\\{ENABLE_SPEECH\\}\\}/g, String(Boolean(config.enableSpeechRecognition)))\n .replace(/\\{\\{ENABLE_HAPTICS\\}\\}/g, String(Boolean(config.enableHaptics)))\n .replace(/\\{\\{ENABLE_SHARE\\}\\}/g, String(Boolean(config.enableShare)))\n .replace(/\\{\\{ENABLE_CAMERA\\}\\}/g, String(Boolean(config.enableCamera)))\n .replace(/\\{\\{ENABLE_BIOMETRIC\\}\\}/g, String(Boolean(config.enableBiometric)))\n .replace(/\\{\\{ENABLE_PUSH\\}\\}/g, String(Boolean(config.enablePushNotifications)))\n .replace(/\\{\\{ENABLE_SECURE_STORAGE\\}\\}/g, String(Boolean(config.enableSecureStorage)))\n .replace(/\\{\\{ENABLE_GEOLOCATION\\}\\}/g, String(Boolean(config.enableGeolocation)))\n .replace(/\\{\\{ENABLE_BACKGROUND_LOCATION\\}\\}/g, String(Boolean(config.enableBackgroundLocation)))\n .replace(/\\{\\{ENABLE_KEEP_AWAKE\\}\\}/g, String(Boolean(config.enableKeepAwake)))\n .replace(/\\{\\{ENABLE_DEEP_LINKS\\}\\}/g, String(Boolean(config.enableDeepLinks)))\n .replace(/\\{\\{ENABLE_HEALTH_CONNECT\\}\\}/g, String(Boolean(config.enableHealthConnect)))\n .replace(/\\{\\{FIREBASE_IMPORT\\}\\}/g, config.enablePushNotifications ? 'import com.google.firebase.messaging.FirebaseMessaging' : '')\n .replace(/\\{\\{REGISTER_PUSH_IMPLEMENTATION\\}\\}/g, config.enablePushNotifications\n ? `activity.runOnUiThread {\n if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU\n && ContextCompat.checkSelfPermission(activity, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {\n ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 4204)\n }\n try {\n FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->\n val token = if (task.isSuccessful) task.result else null\n val callback = if (token.isNullOrBlank()) \"window._craftPushReject\" else \"window._craftPushResolve\"\n val payload = JSONObject.quote(token ?: task.exception?.message ?: \"Firebase Cloud Messaging is not configured\")\n evaluatePromiseJavascript(\"$callback && $callback($payload)\")\n }\n } catch (error: Exception) {\n val payload = JSONObject.quote(error.message ?: \"Firebase Cloud Messaging is not configured\")\n evaluatePromiseJavascript(\"window._craftPushReject && window._craftPushReject($payload)\")\n }\n }`\n : `evaluatePromiseJavascript(\n \"window._craftPushReject && window._craftPushReject('Push notifications are disabled')\"\n )`)\n writeFileSync(join(output, 'app/src/main/java', packagePath, 'CraftBridge.kt'), craftBridge)\n\n // CraftNative goes to a fixed package, not the app's. The prebuilt\n // libcraft.so binds its natives by class name in JNI_OnLoad, and it cannot\n // know a name chosen here — so this one file is deliberately not templated\n // and deliberately not under packagePath.\n const craftNative = readFileSync(join(TEMPLATES_DIR, 'CraftNative.kt.template'), 'utf-8')\n const nativeDir = join(output, 'app/src/main/java/com/craft/runtime')\n mkdirSync(nativeDir, { recursive: true })\n writeFileSync(join(nativeDir, 'CraftNative.kt'), craftNative)\n // The bridge and native holder both reference the recording store/service,\n // even when the service is not registered in the manifest. Keep these\n // support types in the same fixed package as CraftNative so every generated\n // app compiles regardless of its chosen package name or feature flags.\n const serviceTemplate = readFileSync(join(TEMPLATES_DIR, 'LocationRecordingService.kt.template'), 'utf-8')\n writeFileSync(join(nativeDir, 'LocationRecordingService.kt'), serviceTemplate)\n const healthTemplate = readFileSync(join(\n TEMPLATES_DIR,\n config.enableHealthConnect ? 'CraftHealthConnect.kt.template' : 'CraftHealthConnectStub.kt.template',\n ), 'utf-8')\n writeFileSync(\n join(output, 'app/src/main/java', packagePath, 'CraftHealthConnect.kt'),\n healthTemplate.replace(/\\{\\{PACKAGE_NAME\\}\\}/g, finalPackageName),\n )\n // Create AndroidManifest.xml\n const manifestTemplate = readFileSync(join(TEMPLATES_DIR, 'AndroidManifest.xml.template'), 'utf-8')\n const manifest = manifestTemplate\n .replace(/\\{\\{PACKAGE_NAME\\}\\}/g, finalPackageName)\n .replace(/\\{\\{APP_NAME\\}\\}/g, name)\n .replace(/\\{\\{PERMISSIONS\\}\\}/g, renderAndroidPermissions(config))\n .replace(/\\{\\{USES_CLEARTEXT\\}\\}/g, config.devServerURL?.startsWith('http://') ? 'true' : 'false')\n .replace(/\\{\\{DEEP_LINK_INTENT_FILTERS\\}\\}/g, renderAndroidDeepLinks(config))\n .replace(/\\{\\{BACKGROUND_SERVICE\\}\\}/g, config.enableBackgroundLocation\n ? ' <service android:name=\"com.craft.runtime.LocationRecordingService\" android:exported=\"false\" android:foregroundServiceType=\"location\" android:stopWithTask=\"false\" />'\n : '')\n .replace(/\\{\\{HEALTH_CONNECT_QUERIES\\}\\}/g, config.enableHealthConnect\n ? ' <queries>\\n <package android:name=\"com.google.android.apps.healthdata\" />\\n </queries>'\n : '')\n .replace(/\\{\\{HEALTH_CONNECT_RATIONALE\\}\\}/g, config.enableHealthConnect\n ? ` <intent-filter>\n <action android:name=\"androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE\" />\n </intent-filter>\n <intent-filter>\n <action android:name=\"android.intent.action.VIEW_PERMISSION_USAGE\" />\n <category android:name=\"android.intent.category.HEALTH_PERMISSIONS\" />\n </intent-filter>`\n : '')\n\n writeFileSync(join(output, 'app/src/main/AndroidManifest.xml'), manifest)\n\n // Create build.gradle.kts (project level)\n const projectGradleTemplate = readFileSync(join(TEMPLATES_DIR, 'build.gradle.kts.project.template'), 'utf-8')\n writeFileSync(join(output, 'build.gradle.kts'), projectGradleTemplate\n .replace(/\\{\\{GOOGLE_SERVICES_PLUGIN\\}\\}/g, hasGoogleServices ? ' id(\"com.google.gms.google-services\") version \"4.4.2\" apply false' : ''))\n\n // Create build.gradle.kts (app level)\n const appGradleTemplate = readFileSync(join(TEMPLATES_DIR, 'build.gradle.kts.app.template'), 'utf-8')\n const appGradle = appGradleTemplate\n .replace(/\\{\\{PACKAGE_NAME\\}\\}/g, finalPackageName)\n .replace(/\\{\\{VERSION_NAME\\}\\}/g, escapeKotlinString(config.version || '1.0.0'))\n .replace(/\\{\\{VERSION_CODE\\}\\}/g, String(config.versionCode || 1))\n .replace(/\\{\\{MIN_SDK\\}\\}/g, String(config.minSdk ?? ANDROID_MIN_SDK))\n .replace(/\\{\\{COMPILE_SDK\\}\\}/g, String(config.compileSdk || 36))\n .replace(/\\{\\{TARGET_SDK\\}\\}/g, String(config.targetSdk || 35))\n .replace(/\\{\\{GOOGLE_SERVICES_PLUGIN\\}\\}/g, hasGoogleServices ? ' id(\"com.google.gms.google-services\")' : '')\n .replace(/\\{\\{FIREBASE_MESSAGING_DEPENDENCY\\}\\}/g, config.enablePushNotifications\n ? ' implementation(\"com.google.firebase:firebase-messaging:24.1.0\")'\n : '')\n .replace(/\\{\\{HEALTH_CONNECT_DEPENDENCIES\\}\\}/g, config.enableHealthConnect\n ? ` implementation(\"androidx.health.connect:connect-client:1.1.0\")\n implementation(\"org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1\")`\n : '')\n .replace(/\\{\\{JAVASCRIPT_ENGINE_DEPENDENCY\\}\\}/g, config.renderer === 'native'\n ? ' implementation(\"androidx.javascriptengine:javascriptengine:1.1.0\")'\n : '')\n\n writeFileSync(join(output, 'app/build.gradle.kts'), appGradle)\n const proguardTemplate = readFileSync(join(TEMPLATES_DIR, 'proguard-rules.pro.template'), 'utf-8')\n writeFileSync(\n join(output, 'app/proguard-rules.pro'),\n proguardTemplate.replace(/\\{\\{PACKAGE_NAME\\}\\}/g, finalPackageName),\n )\n\n // Create settings.gradle.kts\n const settingsTemplate = readFileSync(join(TEMPLATES_DIR, 'settings.gradle.kts.template'), 'utf-8')\n const settings = settingsTemplate.replace(\n /\\{\\{APP_NAME\\}\\}/g,\n escapeKotlinString(generatedGradleProjectName(name)),\n )\n writeFileSync(join(output, 'settings.gradle.kts'), settings)\n\n // Create gradle.properties\n const gradleProperties = `org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8\nandroid.useAndroidX=true\nkotlin.code.style=official\nandroid.nonTransitiveRClass=true\n`\n writeFileSync(join(output, 'gradle.properties'), gradleProperties)\n\n // Create local.properties placeholder\n writeFileSync(join(output, 'local.properties'), '# SDK location will be set by Android Studio\\n')\n\n // Create gradle wrapper properties\n const gradleWrapperProps = `distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\\\://services.gradle.org/distributions/gradle-8.11.1-bin.zip\nnetworkTimeout=10000\nvalidateDistributionUrl=true\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n`\n writeFileSync(join(output, 'gradle/wrapper/gradle-wrapper.properties'), gradleWrapperProps)\n\n // Create res/values files\n const stringsXml = `<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <string name=\"app_name\">${escapeXml(name)}</string>\n</resources>\n`\n writeFileSync(join(output, 'app/src/main/res/values/strings.xml'), stringsXml)\n\n const colorsXml = `<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <color name=\"primary\">#1a1a2e</color>\n <color name=\"primary_dark\">#16162b</color>\n <color name=\"accent\">#4ade80</color>\n <color name=\"background\">${config.backgroundColor}</color>\n</resources>\n`\n writeFileSync(join(output, 'app/src/main/res/values/colors.xml'), colorsXml)\n\n const appIconXml = `<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"108dp\"\n android:height=\"108dp\"\n android:viewportWidth=\"108\"\n android:viewportHeight=\"108\">\n <path android:fillColor=\"${config.backgroundColor}\" android:pathData=\"M0,0h108v108h-108z\" />\n <path android:fillColor=\"#10B981\" android:pathData=\"M18,78L42,42l12,18l12,-12l24,30z\" />\n</vector>\n`\n if (config.appIconPath) {\n cpSync(\n config.appIconPath,\n join(output, `app/src/main/res/drawable/craft_app_icon${appIconExtension}`),\n )\n }\n else {\n writeFileSync(join(output, 'app/src/main/res/drawable/craft_app_icon.xml'), appIconXml)\n }\n\n const themesXml = `<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <style name=\"Theme.CraftApp\" parent=\"Theme.Material3.DayNight.NoActionBar\">\n <item name=\"android:statusBarColor\">@color/primary_dark</item>\n <item name=\"android:navigationBarColor\">@color/primary</item>\n <item name=\"android:windowBackground\">@color/background</item>\n </style>\n</resources>\n`\n writeFileSync(join(output, 'app/src/main/res/values/themes.xml'), themesXml)\n\n // Create activity_main.xml\n const activityMainXml = config.renderer === 'native'\n ? `<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:id=\"@+id/native_root\"\n android:layout_width=\"match_parent\"\n android:layout_height=\"match_parent\" />\n`\n : `<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout\n xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:layout_width=\"match_parent\"\n android:layout_height=\"match_parent\"\n android:fitsSystemWindows=\"true\">\n\n <WebView\n android:id=\"@+id/webview\"\n android:layout_width=\"match_parent\"\n android:layout_height=\"match_parent\" />\n\n</androidx.coordinatorlayout.widget.CoordinatorLayout>\n`\n writeFileSync(join(output, 'app/src/main/res/layout/activity_main.xml'), activityMainXml)\n\n // Create placeholder index.html\n const placeholderHtml = `<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"viewport-fit=cover, width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\">\n <title>${escapeXml(name)}</title>\n <style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body {\n font-family: 'Roboto', sans-serif;\n background: ${config.backgroundColor};\n color: white;\n min-height: 100vh;\n display: flex;\n justify-content: center;\n align-items: center;\n }\n .container { text-align: center; padding: 2rem; }\n h1 { font-size: 2.5rem; margin-bottom: 1rem; }\n p { opacity: 0.7; }\n .ready { color: #4ade80; font-size: 0.9rem; margin-top: 2rem; }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <h1>⚡ ${escapeXml(name)}</h1>\n <p>Built with Craft Android</p>\n <p class=\"ready\" id=\"status\">Waiting for Craft bridge...</p>\n </div>\n <script>\n window.addEventListener('craftReady', (e) => {\n document.getElementById('status').textContent = \\`✓ Craft bridge ready (platform: \\${e.detail.platform})\\`;\n console.log('Craft capabilities:', e.detail.capabilities);\n });\n </script>\n</body>\n</html>`\n\n writeFileSync(join(output, 'app/src/main/assets/index.html'), placeholderHtml)\n\n // The Zig runtime, when one was configured. Absent, the generated app is the\n // Kotlin shim and behaves exactly as it always has — which is the fallback\n // `CraftNative` was written for, not a failure.\n const runtimeDir = resolveRuntimeDir(options.runtimeDir)\n if (runtimeDir) {\n installRuntime(output, runtimeDir)\n console.log(' Installed the Zig runtime from', runtimeDir)\n }\n\n console.log('✅ Project initialized')\n console.log('')\n console.log('Next steps:')\n console.log(` 1. cd ${output}`)\n if (config.renderer === 'native') {\n console.log(' 2. Compile your screen: stx-native compile Screen.stx --format bundle --output screen.js')\n console.log(' 3. Run: craft android build --native-bundle screen.js')\n }\n else {\n console.log(' 2. Add your web content to app/src/main/assets/index.html')\n console.log(' 3. Run: craft android build')\n }\n console.log(' 4. Run: craft android open')\n console.log('')\n}\n\n/**\n * Build Android project\n */\nexport async function build(options: BuildOptions): Promise<void> {\n const { htmlPath, nativeBundlePath, devServer, output, release, compile = true } = options\n\n console.log('\\n📦 Building Craft Android project...')\n\n // Load config\n const configPath = join(output, 'craft.config.json')\n if (!existsSync(configPath)) {\n throw new Error(`No craft.config.json found in ${output}. Run 'craft android init' first.`)\n }\n\n const config: CraftAndroidConfig = JSON.parse(readFileSync(configPath, 'utf-8'))\n validateAndroidConfig(config)\n\n if (config.renderer === 'native') {\n if (htmlPath || devServer) throw new Error('A native Android screen cannot use --html-path or --dev-server.')\n const destination = join(output, 'app/src/main/assets/native-screen.js')\n if (nativeBundlePath && resolve(nativeBundlePath) !== resolve(destination)) cpSync(nativeBundlePath, destination)\n if (!existsSync(destination)) {\n throw new Error('Native Android mode needs app/src/main/assets/native-screen.js. Compile a .stx screen with stx-native first.')\n }\n }\n else if (nativeBundlePath) {\n throw new Error('--native-bundle requires renderer: \"native\" in craft.config.json.')\n }\n\n // Refresh the runtime the same way the iOS builder does: only for a project\n // that already has one, so `craft android build` in a shim-only project does\n // not quietly acquire a native library it was never generated with.\n if (existsSync(join(output, 'app/src/main/jniLibs'))) {\n const runtimeDir = resolveRuntimeDir(options.runtimeDir)\n if (runtimeDir) {\n installRuntime(output, runtimeDir)\n console.log(' Refreshed the Zig runtime from', runtimeDir)\n }\n else {\n console.log(' Keeping the Zig runtime installed at init (no runtime directory configured)')\n }\n }\n\n // Update dev server URL if provided\n if (devServer) {\n const url = androidWebUrl(devServer, 'Android dev server URL')\n config.devServerURL = url.toString()\n config.trustedOrigins = [...new Set([\n ...(config.trustedOrigins ?? []).map(value => androidWebUrl(value, 'Android trusted origin').origin),\n url.origin,\n ])]\n writeAndroidConfig(output, config)\n console.log(` Dev server: ${devServer}`)\n }\n\n // Copy the complete web application if provided.\n if (htmlPath) {\n syncAndroidWebAssets(htmlPath, output)\n config.hasBundledFallback = Boolean(devServer)\n writeAndroidConfig(output, config)\n console.log(` Synced: ${htmlPath} → assets/`)\n }\n\n if (!compile) return\n\n // Check if gradlew exists\n const gradlewPath = join(output, 'gradlew')\n if (!existsSync(gradlewPath)) {\n console.log(' Generating Gradle wrapper...')\n try {\n await $`cd ${output} && gradle wrapper`.quiet()\n }\n catch (error) {\n throw new Error('Gradle is unavailable. Install Android Studio or Gradle before building Android.', { cause: error })\n }\n }\n\n // Build the project\n const buildType = release ? 'assembleRelease' : 'assembleDebug'\n console.log(` Building ${release ? 'release' : 'debug'} APK...`)\n\n try {\n await $`cd ${output} && ./gradlew ${buildType}`\n const apkPath = release\n ? 'app/build/outputs/apk/release/app-release.apk'\n : 'app/build/outputs/apk/debug/app-debug.apk'\n console.log(`✅ APK built: ${apkPath}`)\n }\ncatch (error) {\n console.error('Build failed. Open in Android Studio for details.')\n throw error\n }\n\n console.log('')\n}\n\n/**\n * Open Android project in Android Studio\n */\nexport async function open(options: OpenOptions): Promise<void> {\n const { output } = options\n\n if (!existsSync(output)) {\n throw new Error(`No Android project found in ${output}. Run 'craft android init' first.`)\n }\n\n console.log(`🚀 Opening Android project in Android Studio...`)\n\n // Try to open with Android Studio\n try {\n // macOS\n await $`open -a \"Android Studio\" ${output}`.quiet()\n }\ncatch {\n try {\n // Linux\n await $`studio ${output}`.quiet()\n }\ncatch {\n console.log('⚠️ Android Studio not found.')\n console.log(' Please open the project manually in Android Studio:')\n console.log(` ${output}`)\n }\n }\n}\n\n/**\n * Run on Android device or emulator\n */\nexport async function run(options: RunOptions): Promise<void> {\n const { device, output } = options\n\n // First build\n await build({ output })\n\n // Install and run\n console.log('📱 Installing and running on device...')\n\n try {\n const apkPath = join(output, 'app/build/outputs/apk/debug/app-debug.apk')\n\n if (device) {\n await $`adb -s ${device} install -r ${apkPath}`\n }\nelse {\n await $`adb install -r ${apkPath}`\n }\n\n // Get package name from config\n const config: CraftAndroidConfig = JSON.parse(\n readFileSync(join(output, 'craft.config.json'), 'utf-8')\n )\n\n // Launch the app\n const launchCmd = `${config.packageName}/.MainActivity`\n if (device) {\n await $`adb -s ${device} shell am start -n ${launchCmd}`\n }\nelse {\n await $`adb shell am start -n ${launchCmd}`\n }\n\n console.log('✅ App launched')\n }\ncatch (error) {\n console.error('Failed to install/run. Make sure:')\n console.error(' 1. ADB is installed and in PATH')\n console.error(' 2. A device/emulator is connected (run: adb devices)')\n throw error\n }\n}\n",
|
|
6
6
|
"const ANDROID_PROMISE_RUNTIME = `\nif (window.__craftRejectPendingPromises) {\n window.__craftRejectPendingPromises('Android bridge reinitialized');\n}\nif (window.__craftRejectPermissionRequests) {\n window.__craftRejectPermissionRequests('Android bridge reinitialized');\n}\nwindow.__craftPromiseRuntimeClosed = false;\nwindow.__craftPendingPromises = Object.create(null);\nwindow.__craftPromise = function(channel, resolveName, rejectName, invoke, timeoutMs, timeoutError) {\n if (window.__craftPromiseRuntimeClosed) {\n return Promise.reject(new Error('Android bridge is closed'));\n }\n if (window.__craftPendingPromises[channel]) {\n return Promise.reject(new Error('A '.concat(channel, ' request is already in progress')));\n }\n\n return new Promise(function(resolve, reject) {\n var entry = {settled: false, timer: null, settle: null};\n var resolveCallback;\n var rejectCallback;\n\n entry.settle = function(succeeded, value) {\n if (entry.settled || window.__craftPendingPromises[channel] !== entry) return;\n entry.settled = true;\n if (entry.timer !== null) clearTimeout(entry.timer);\n if (window[resolveName] === resolveCallback) window[resolveName] = null;\n if (window[rejectName] === rejectCallback) window[rejectName] = null;\n delete window.__craftPendingPromises[channel];\n if (succeeded) resolve(value);\n else reject(value);\n };\n\n resolveCallback = function(value) {\n entry.settle(true, value);\n };\n rejectCallback = function(error) {\n entry.settle(false, error);\n };\n window[resolveName] = resolveCallback;\n window[rejectName] = rejectCallback;\n window.__craftPendingPromises[channel] = entry;\n\n if (timeoutMs > 0) {\n entry.timer = setTimeout(function() {\n entry.settle(false, timeoutError || new Error(channel.concat(' request timed out')));\n }, timeoutMs);\n }\n\n try {\n invoke();\n }\n catch (error) {\n entry.settle(false, error);\n }\n });\n};\n\nwindow.__craftRejectPendingPromises = function(message) {\n window.__craftPromiseRuntimeClosed = true;\n Object.keys(window.__craftPendingPromises).forEach(function(channel) {\n var entry = window.__craftPendingPromises[channel];\n if (entry) entry.settle(false, new Error(message || 'Android bridge closed'));\n });\n};\n`\n\nexport function renderAndroidPromiseRuntime(indent = ''): string {\n return ANDROID_PROMISE_RUNTIME\n .trim()\n .split('\\n')\n .map(line => `${indent}${line}`)\n .join('\\n')\n}\n\nexport { ANDROID_PROMISE_RUNTIME }\n"
|
|
7
7
|
],
|
|
8
|
-
"mappings": "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",
|
|
9
|
-
"debugId": "
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "2BDBD4A5F771012864756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
package {{PACKAGE_NAME}}
|
|
2
|
+
|
|
3
|
+
import android.content.ClipData
|
|
4
|
+
import android.content.ClipboardManager
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.graphics.Color
|
|
7
|
+
import android.os.Build
|
|
8
|
+
import android.os.Bundle
|
|
9
|
+
import android.os.VibrationEffect
|
|
10
|
+
import android.os.Vibrator
|
|
11
|
+
import android.text.Editable
|
|
12
|
+
import android.text.TextWatcher
|
|
13
|
+
import android.util.Log
|
|
14
|
+
import android.view.View
|
|
15
|
+
import android.view.ViewGroup
|
|
16
|
+
import android.widget.Button
|
|
17
|
+
import android.widget.EditText
|
|
18
|
+
import android.widget.FrameLayout
|
|
19
|
+
import android.widget.LinearLayout
|
|
20
|
+
import android.widget.TextView
|
|
21
|
+
import androidx.appcompat.app.AppCompatActivity
|
|
22
|
+
import androidx.core.content.ContextCompat
|
|
23
|
+
import androidx.javascriptengine.JavaScriptIsolate
|
|
24
|
+
import androidx.javascriptengine.JavaScriptConsoleCallback
|
|
25
|
+
import androidx.javascriptengine.JavaScriptSandbox
|
|
26
|
+
import androidx.javascriptengine.Message
|
|
27
|
+
import androidx.javascriptengine.MessagePort
|
|
28
|
+
import androidx.javascriptengine.MessagePortClient
|
|
29
|
+
import org.json.JSONArray
|
|
30
|
+
import org.json.JSONObject
|
|
31
|
+
import java.util.UUID
|
|
32
|
+
|
|
33
|
+
/** WebView-free host for a compiled stx-native route bundle. */
|
|
34
|
+
class MainActivity : AppCompatActivity() {
|
|
35
|
+
private class Screen(
|
|
36
|
+
val route: String?,
|
|
37
|
+
val params: JSONObject,
|
|
38
|
+
val view: LinearLayout,
|
|
39
|
+
val isolate: JavaScriptIsolate,
|
|
40
|
+
val port: MessagePort?
|
|
41
|
+
) {
|
|
42
|
+
val drafts = mutableMapOf<String, String>()
|
|
43
|
+
val controls = mutableMapOf<String, View>()
|
|
44
|
+
val handlers = mutableMapOf<View, String>()
|
|
45
|
+
var active = true
|
|
46
|
+
|
|
47
|
+
fun close() {
|
|
48
|
+
active = false
|
|
49
|
+
port?.close()
|
|
50
|
+
isolate.close()
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private lateinit var root: FrameLayout
|
|
55
|
+
private var sandbox: JavaScriptSandbox? = null
|
|
56
|
+
private val screens = mutableListOf<Screen>()
|
|
57
|
+
private var bundle = ""
|
|
58
|
+
private var hapticsEnabled = false
|
|
59
|
+
private var defaultTextColor = Color.WHITE
|
|
60
|
+
private var useMessagePorts = false
|
|
61
|
+
|
|
62
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
63
|
+
super.onCreate(savedInstanceState)
|
|
64
|
+
setContentView(R.layout.activity_main)
|
|
65
|
+
root = findViewById(R.id.native_root)
|
|
66
|
+
val config = runCatching {
|
|
67
|
+
assets.open("craft.config.json").bufferedReader().use {
|
|
68
|
+
JSONObject(it.readText())
|
|
69
|
+
}
|
|
70
|
+
}.getOrDefault(JSONObject())
|
|
71
|
+
hapticsEnabled = config.optBoolean("enableHaptics", false)
|
|
72
|
+
val background = color(config.optString("backgroundColor", "#1a1a2e"), Color.BLACK)
|
|
73
|
+
root.setBackgroundColor(background)
|
|
74
|
+
defaultTextColor = if (Color.luminance(background) < 0.5) Color.WHITE else Color.BLACK
|
|
75
|
+
bundle = runCatching {
|
|
76
|
+
assets.open("native-screen.js").bufferedReader().use { it.readText() }
|
|
77
|
+
}.getOrElse {
|
|
78
|
+
showError("Missing native-screen.js. Build with --native-bundle after compiling STX.")
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
if (!JavaScriptSandbox.isSupported()) {
|
|
82
|
+
showError("Native STX requires Android JavaScriptSandbox, which is unavailable on this device.")
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
val connection = JavaScriptSandbox.createConnectedInstanceAsync(this)
|
|
86
|
+
connection.addListener({
|
|
87
|
+
try {
|
|
88
|
+
val engine = connection.get()
|
|
89
|
+
if (isDestroyed) {
|
|
90
|
+
engine.close()
|
|
91
|
+
return@addListener
|
|
92
|
+
}
|
|
93
|
+
useMessagePorts = engine.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_MESSAGE_PORTS)
|
|
94
|
+
&& engine.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_PROMISE_RETURN)
|
|
95
|
+
if (!useMessagePorts && !engine.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_CONSOLE_MESSAGING)) {
|
|
96
|
+
engine.close()
|
|
97
|
+
showError("Native STX requires JavaScript message ports or console messaging, unavailable on this device.")
|
|
98
|
+
return@addListener
|
|
99
|
+
}
|
|
100
|
+
Log.i(TAG, "Native STX bridge: ${if (useMessagePorts) "message ports" else "console callback"}")
|
|
101
|
+
sandbox = engine
|
|
102
|
+
openScreen(null)
|
|
103
|
+
} catch (error: Exception) {
|
|
104
|
+
Log.e(TAG, "JavaScriptSandbox connection failed", error)
|
|
105
|
+
showError("Native STX JavaScript engine could not start: ${error.message}")
|
|
106
|
+
}
|
|
107
|
+
}, ContextCompat.getMainExecutor(this))
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private fun openScreen(route: String?, params: JSONObject = JSONObject()) {
|
|
111
|
+
val engine = sandbox ?: return
|
|
112
|
+
val isolate = engine.createIsolate()
|
|
113
|
+
val view = LinearLayout(this).apply {
|
|
114
|
+
orientation = LinearLayout.VERTICAL
|
|
115
|
+
layoutParams = FrameLayout.LayoutParams(-1, -1)
|
|
116
|
+
}
|
|
117
|
+
var screen: Screen? = null
|
|
118
|
+
val executor = ContextCompat.getMainExecutor(this)
|
|
119
|
+
val token = UUID.randomUUID().toString()
|
|
120
|
+
val prefix = "CRAFT_STX_$token:"
|
|
121
|
+
val port = if (useMessagePorts) {
|
|
122
|
+
isolate.createMessageChannel("craft", executor, MessagePortClient { message ->
|
|
123
|
+
val current = screen ?: return@MessagePortClient
|
|
124
|
+
if (current.active && message.type == Message.TYPE_STRING) receive(current, message.string)
|
|
125
|
+
})
|
|
126
|
+
} else {
|
|
127
|
+
isolate.setConsoleCallback(executor, JavaScriptConsoleCallback { entry ->
|
|
128
|
+
val current = screen ?: return@JavaScriptConsoleCallback
|
|
129
|
+
if (current.active && entry.message.startsWith(prefix)) {
|
|
130
|
+
receive(current, entry.message.removePrefix(prefix))
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
null
|
|
134
|
+
}
|
|
135
|
+
val activeScreen = Screen(route, params, view, isolate, port)
|
|
136
|
+
screen = activeScreen
|
|
137
|
+
screens.add(activeScreen)
|
|
138
|
+
show(activeScreen)
|
|
139
|
+
val routePrelude = """
|
|
140
|
+
globalThis.__stxNativeRoute = ${route?.let { JSONObject.quote(it) } ?: "undefined"};
|
|
141
|
+
globalThis.__stxNativeParams = ${params};
|
|
142
|
+
""".trimIndent()
|
|
143
|
+
val prelude = if (useMessagePorts) """
|
|
144
|
+
(async function() {
|
|
145
|
+
const port = await android.getNamedPort('craft');
|
|
146
|
+
$routePrelude
|
|
147
|
+
globalThis.__stxNativeBridge = {
|
|
148
|
+
postMessage: function(value) { port.postMessage(value); },
|
|
149
|
+
onMessage: function(callback) { port.onmessage = function(event) { callback(event.data); }; }
|
|
150
|
+
};
|
|
151
|
+
})()
|
|
152
|
+
""".trimIndent() else """
|
|
153
|
+
$routePrelude
|
|
154
|
+
globalThis.__stxNativeBridge = {
|
|
155
|
+
postMessage: function(value) { console.log(${JSONObject.quote(prefix)} + value); },
|
|
156
|
+
onMessage: function(callback) { globalThis.__stxNativeCallback = callback; }
|
|
157
|
+
};
|
|
158
|
+
""".trimIndent()
|
|
159
|
+
val setup = isolate.evaluateJavaScriptAsync(prelude)
|
|
160
|
+
setup.addListener({
|
|
161
|
+
if (!activeScreen.active) return@addListener
|
|
162
|
+
try {
|
|
163
|
+
setup.get()
|
|
164
|
+
val load = isolate.evaluateJavaScriptAsync(bundle)
|
|
165
|
+
load.addListener({
|
|
166
|
+
try { load.get() } catch (error: Exception) {
|
|
167
|
+
Log.e(TAG, "Native STX bundle failed", error)
|
|
168
|
+
if (activeScreen.active) showError("Native STX bundle failed: ${error.message}")
|
|
169
|
+
}
|
|
170
|
+
}, executor)
|
|
171
|
+
} catch (error: Exception) {
|
|
172
|
+
Log.e(TAG, "Native STX bridge failed", error)
|
|
173
|
+
showError("Native STX bridge failed: ${error.message}")
|
|
174
|
+
}
|
|
175
|
+
}, executor)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private fun show(screen: Screen) {
|
|
179
|
+
root.removeAllViews()
|
|
180
|
+
(screen.view.parent as? ViewGroup)?.removeView(screen.view)
|
|
181
|
+
root.addView(screen.view)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private fun receive(screen: Screen, raw: String) {
|
|
185
|
+
val message = runCatching { JSONObject(raw) }.getOrNull() ?: return
|
|
186
|
+
val payload = message.optJSONObject("payload") ?: JSONObject()
|
|
187
|
+
when (message.optString("type")) {
|
|
188
|
+
"RENDER" -> payload.optJSONObject("document")?.let { render(screen, it) }
|
|
189
|
+
"NAVIGATE", "NAVIGATE_REPLACE" -> {
|
|
190
|
+
if (screens.lastOrNull() !== screen) return
|
|
191
|
+
val route = payload.optString("screen").takeIf { it.isNotBlank() } ?: return
|
|
192
|
+
if (message.optString("type") == "NAVIGATE_REPLACE") {
|
|
193
|
+
screens.removeAt(screens.lastIndex).close()
|
|
194
|
+
}
|
|
195
|
+
openScreen(route, payload.optJSONObject("params") ?: JSONObject())
|
|
196
|
+
}
|
|
197
|
+
"NAVIGATE_BACK" -> if (screens.lastOrNull() === screen) back()
|
|
198
|
+
"API_REQUEST" -> answerAPI(screen, message, payload)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private fun render(screen: Screen, document: JSONObject) {
|
|
203
|
+
// Keep live controls (especially the focused EditText) across renders.
|
|
204
|
+
val next = mutableMapOf<String, View>()
|
|
205
|
+
val tree = makeView(screen, document, "root", next)
|
|
206
|
+
if (screen.view.getChildAt(0) !== tree) {
|
|
207
|
+
(tree.parent as? ViewGroup)?.removeView(tree)
|
|
208
|
+
screen.view.removeAllViews()
|
|
209
|
+
screen.view.addView(tree)
|
|
210
|
+
}
|
|
211
|
+
screen.controls.clear()
|
|
212
|
+
screen.controls.putAll(next)
|
|
213
|
+
screen.handlers.keys.retainAll(next.values.toSet())
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private fun makeView(screen: Screen, node: JSONObject, path: String, next: MutableMap<String, View>): View {
|
|
217
|
+
val type = node.optString("type", "View")
|
|
218
|
+
val props = node.optJSONObject("props") ?: JSONObject()
|
|
219
|
+
val style = node.optJSONObject("style") ?: JSONObject()
|
|
220
|
+
val events = node.optJSONObject("events") ?: JSONObject()
|
|
221
|
+
val children = node.optJSONArray("children") ?: JSONArray()
|
|
222
|
+
val explicitKey = listOf(node.optString("key"), props.optString("key"), props.optString("testID"))
|
|
223
|
+
.firstOrNull { it.isNotBlank() }
|
|
224
|
+
val identity = explicitKey?.let { "${path.substringBeforeLast('.', "")}/$it" } ?: path
|
|
225
|
+
val previous = screen.controls[identity]
|
|
226
|
+
val control = when (type) {
|
|
227
|
+
"Text" -> ((previous as? TextView)?.takeUnless { it is Button || it is EditText } ?: TextView(this)).apply {
|
|
228
|
+
text = childText(children)
|
|
229
|
+
setTextColor(color(style.optString("color"), defaultTextColor))
|
|
230
|
+
if (style.has("fontSize")) textSize = style.optDouble("fontSize").toFloat()
|
|
231
|
+
}
|
|
232
|
+
"Button" -> (previous as? Button ?: Button(this)).apply {
|
|
233
|
+
text = props.optString("title").ifBlank { childText(children) }
|
|
234
|
+
val handler = events.optString("onPress").ifBlank { events.optString("onClick") }
|
|
235
|
+
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
236
|
+
setOnClickListener {
|
|
237
|
+
val current = screen.handlers[this] ?: return@setOnClickListener
|
|
238
|
+
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
"TextInput" -> (previous as? EditText ?: EditText(this)).apply {
|
|
242
|
+
hint = props.optString("placeholder")
|
|
243
|
+
if (props.has("value") && text.toString() != props.optString("value")) setText(props.optString("value"))
|
|
244
|
+
else if (this !== previous && screen.drafts.containsKey(identity)) setText(screen.drafts[identity])
|
|
245
|
+
val handler = events.optString("onChange").ifBlank { events.optString("onChangeText") }
|
|
246
|
+
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
247
|
+
if (this !== previous) addTextChangedListener(object : TextWatcher {
|
|
248
|
+
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
|
249
|
+
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
|
250
|
+
val updated = s?.toString().orEmpty()
|
|
251
|
+
screen.drafts[identity] = updated
|
|
252
|
+
val current = screen.handlers[this@apply] ?: return
|
|
253
|
+
send(screen, "EVENT", JSONObject()
|
|
254
|
+
.put("handlerName", current)
|
|
255
|
+
.put("nativeEvent", JSONObject().put("text", updated)))
|
|
256
|
+
}
|
|
257
|
+
override fun afterTextChanged(s: Editable?) {}
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
else -> (previous as? LinearLayout ?: LinearLayout(this)).apply {
|
|
261
|
+
orientation = if (style.optString("flexDirection") == "row")
|
|
262
|
+
LinearLayout.HORIZONTAL else LinearLayout.VERTICAL
|
|
263
|
+
val padding = style.optDouble("padding", 0.0)
|
|
264
|
+
setPadding(
|
|
265
|
+
dp(style.optDouble("paddingLeft", padding)),
|
|
266
|
+
dp(style.optDouble("paddingTop", padding)),
|
|
267
|
+
dp(style.optDouble("paddingRight", padding)),
|
|
268
|
+
dp(style.optDouble("paddingBottom", padding))
|
|
269
|
+
)
|
|
270
|
+
val desired = mutableListOf<View>()
|
|
271
|
+
for (index in 0 until children.length()) {
|
|
272
|
+
val child = children.opt(index)
|
|
273
|
+
val childView = when (child) {
|
|
274
|
+
is JSONObject -> makeView(screen, child, "$path.$index", next)
|
|
275
|
+
is String -> TextView(this@MainActivity).apply {
|
|
276
|
+
text = child
|
|
277
|
+
setTextColor(defaultTextColor)
|
|
278
|
+
}
|
|
279
|
+
else -> null
|
|
280
|
+
}
|
|
281
|
+
if (childView != null) desired.add(childView)
|
|
282
|
+
}
|
|
283
|
+
val desiredSet = desired.toSet()
|
|
284
|
+
for ((index, childView) in desired.withIndex()) {
|
|
285
|
+
if (getChildAt(index) === childView) continue
|
|
286
|
+
if (index < childCount && getChildAt(index) !in desiredSet) removeViewAt(index)
|
|
287
|
+
(childView.parent as? ViewGroup)?.removeView(childView)
|
|
288
|
+
addView(childView, index)
|
|
289
|
+
}
|
|
290
|
+
while (childCount > desired.size) removeViewAt(childCount - 1)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
control.contentDescription = explicitKey ?: path
|
|
294
|
+
next[identity] = control
|
|
295
|
+
if (style.has("backgroundColor")) control.setBackgroundColor(color(style.optString("backgroundColor"), Color.TRANSPARENT))
|
|
296
|
+
control.layoutParams = LinearLayout.LayoutParams(
|
|
297
|
+
if (style.has("width")) dp(style.optDouble("width")) else ViewGroup.LayoutParams.MATCH_PARENT,
|
|
298
|
+
if (style.has("height")) dp(style.optDouble("height")) else ViewGroup.LayoutParams.WRAP_CONTENT
|
|
299
|
+
)
|
|
300
|
+
return control
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private fun childText(children: JSONArray): String = buildString {
|
|
304
|
+
for (index in 0 until children.length()) {
|
|
305
|
+
val value = children.opt(index)
|
|
306
|
+
if (value is String) append(value)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private fun dp(value: Double) = (value * resources.displayMetrics.density).toInt()
|
|
311
|
+
private fun color(raw: String, fallback: Int) = runCatching { Color.parseColor(raw) }.getOrDefault(fallback)
|
|
312
|
+
|
|
313
|
+
private fun answerAPI(screen: Screen, message: JSONObject, payload: JSONObject) {
|
|
314
|
+
val id = message.optString("id")
|
|
315
|
+
if (id.isBlank()) return
|
|
316
|
+
val args = payload.optJSONArray("args") ?: JSONArray()
|
|
317
|
+
val response = JSONObject().put("requestId", id)
|
|
318
|
+
val result = when (payload.optString("module") to payload.optString("method")) {
|
|
319
|
+
"Device" to "getInfo" -> response.put("data", JSONObject()
|
|
320
|
+
.put("platform", "android")
|
|
321
|
+
.put("osVersion", Build.VERSION.RELEASE)
|
|
322
|
+
.put("model", Build.MODEL)
|
|
323
|
+
.put("manufacturer", Build.MANUFACTURER))
|
|
324
|
+
"Clipboard" to "write" -> {
|
|
325
|
+
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
|
326
|
+
clipboard.setPrimaryClip(ClipData.newPlainText("", args.optString(0)))
|
|
327
|
+
response.put("data", JSONObject().put("ok", true))
|
|
328
|
+
}
|
|
329
|
+
"Clipboard" to "read" -> {
|
|
330
|
+
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
|
331
|
+
response.put("data", clipboard.primaryClip?.getItemAt(0)?.coerceToText(this)?.toString() ?: "")
|
|
332
|
+
}
|
|
333
|
+
"Haptics" to "impact" -> {
|
|
334
|
+
if (!hapticsEnabled) response.put("code", "CAPABILITY_DISABLED")
|
|
335
|
+
.put("message", "Haptics are disabled")
|
|
336
|
+
else {
|
|
337
|
+
val vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
|
338
|
+
vibrator.vibrate(VibrationEffect.createOneShot(30, VibrationEffect.DEFAULT_AMPLITUDE))
|
|
339
|
+
response.put("data", JSONObject().put("ok", true))
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
else -> response.put("code", "UNKNOWN_ACTION").put("message", "Unsupported native API")
|
|
343
|
+
}
|
|
344
|
+
send(screen, if (result.has("code")) "API_ERROR" else "API_RESPONSE", result, id)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private fun send(screen: Screen, type: String, payload: JSONObject, correlationId: String? = null) {
|
|
348
|
+
if (!screen.active) return
|
|
349
|
+
val message = JSONObject().put("type", type).put("payload", payload)
|
|
350
|
+
if (correlationId != null) message.put("correlationId", correlationId)
|
|
351
|
+
if (screen.port != null) {
|
|
352
|
+
screen.port.postMessage(Message.createStringMessage(message.toString()))
|
|
353
|
+
} else {
|
|
354
|
+
val script = "globalThis.__stxNativeCallback && globalThis.__stxNativeCallback(${JSONObject.quote(message.toString())})"
|
|
355
|
+
screen.isolate.evaluateJavaScriptAsync(script)
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private fun back() {
|
|
360
|
+
if (screens.size > 1) {
|
|
361
|
+
screens.removeAt(screens.lastIndex).close()
|
|
362
|
+
show(screens.last())
|
|
363
|
+
} else {
|
|
364
|
+
finish()
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
@Deprecated("Uses the activity back callback for native route navigation")
|
|
369
|
+
override fun onBackPressed() = back()
|
|
370
|
+
|
|
371
|
+
private fun showError(message: String) {
|
|
372
|
+
root.removeAllViews()
|
|
373
|
+
root.addView(TextView(this).apply {
|
|
374
|
+
text = message
|
|
375
|
+
setTextColor(Color.RED)
|
|
376
|
+
textSize = 16f
|
|
377
|
+
setPadding(24, 24, 24, 24)
|
|
378
|
+
})
|
|
379
|
+
Log.e(TAG, message)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
override fun onDestroy() {
|
|
383
|
+
screens.forEach { it.close() }
|
|
384
|
+
screens.clear()
|
|
385
|
+
sandbox?.close()
|
|
386
|
+
super.onDestroy()
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
companion object { private const val TAG = "CraftNativeAndroid" }
|
|
390
|
+
}
|
|
@@ -59,6 +59,7 @@ dependencies {
|
|
|
59
59
|
implementation("androidx.appcompat:appcompat:1.6.1")
|
|
60
60
|
implementation("androidx.fragment:fragment-ktx:1.6.2")
|
|
61
61
|
implementation("androidx.webkit:webkit:1.12.1")
|
|
62
|
+
{{JAVASCRIPT_ENGINE_DEPENDENCY}}
|
|
62
63
|
implementation("com.google.android.material:material:1.11.0")
|
|
63
64
|
implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0")
|
|
64
65
|
|