@craft-native/android 0.0.177 → 0.0.180
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/dist/index.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ export interface CraftAndroidConfig {
|
|
|
25
25
|
enableSecureStorage?: boolean;
|
|
26
26
|
enableGeolocation?: boolean;
|
|
27
27
|
enableBackgroundLocation?: boolean;
|
|
28
|
+
enableBluetooth?: boolean;
|
|
28
29
|
enableHealthConnect?: boolean;
|
|
29
30
|
/**
|
|
30
31
|
* The Health Connect data the app reads and writes, by the shared type
|
package/dist/index.js
CHANGED
|
@@ -183,6 +183,7 @@ var DEFAULT_CONFIG = {
|
|
|
183
183
|
enableSecureStorage: false,
|
|
184
184
|
enableGeolocation: false,
|
|
185
185
|
enableBackgroundLocation: false,
|
|
186
|
+
enableBluetooth: false,
|
|
186
187
|
enableHealthConnect: false,
|
|
187
188
|
enableKeepAwake: false,
|
|
188
189
|
enableDeepLinks: false,
|
|
@@ -371,7 +372,15 @@ function renderAndroidPermissions(config) {
|
|
|
371
372
|
healthConnectPermissions(config).forEach((permission) => permissions.add(permission));
|
|
372
373
|
if (config.renderer === "native" && config.backgroundRefresh?.enabled)
|
|
373
374
|
permissions.add("android.permission.RECEIVE_BOOT_COMPLETED");
|
|
374
|
-
|
|
375
|
+
const bluetooth = config.enableBluetooth ? [
|
|
376
|
+
' <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />',
|
|
377
|
+
' <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />',
|
|
378
|
+
' <uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />',
|
|
379
|
+
' <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />',
|
|
380
|
+
...config.enableGeolocation ? [] : [' <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />']
|
|
381
|
+
].join(`
|
|
382
|
+
`) : "";
|
|
383
|
+
return [bluetooth, ...[...permissions].map((permission) => ` <uses-permission android:name="${permission}" />`)].filter(Boolean).join(`
|
|
375
384
|
`);
|
|
376
385
|
}
|
|
377
386
|
function writeNativeShellSources(output, packagePath, packageName, config) {
|
|
@@ -380,6 +389,7 @@ function writeNativeShellSources(output, packagePath, packageName, config) {
|
|
|
380
389
|
const source = readFileSync(join(TEMPLATES_DIR, template), "utf-8").replace(/\{\{PACKAGE_NAME\}\}/g, packageName);
|
|
381
390
|
writeFileSync(join(javaDir, file), replace(source));
|
|
382
391
|
};
|
|
392
|
+
write("CraftBluetooth.kt.template", "CraftBluetooth.kt");
|
|
383
393
|
write("CraftNativeShell.kt.template", "CraftNativeShell.kt");
|
|
384
394
|
write("CraftNativeBackground.kt.template", "CraftNativeBackground.kt");
|
|
385
395
|
write(config.enableHealthConnect ? "CraftNativeHealth.kt.template" : "CraftNativeHealthStub.kt.template", "CraftNativeHealth.kt");
|
|
@@ -896,4 +906,4 @@ export {
|
|
|
896
906
|
ANDROID_MIN_SDK
|
|
897
907
|
};
|
|
898
908
|
|
|
899
|
-
//# debugId=
|
|
909
|
+
//# debugId=186096D4B88D325264756E2164756E21
|
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')\n\n/**\n * This builder's version, which the crash reporter names as its SDK version.\n * Walks up from here because a bundled build puts this module below the\n * package root.\n */\nfunction resolveCraftAndroidVersion(startDir: string = import.meta.dir): string {\n let directory = resolve(startDir)\n while (true) {\n try {\n const pkg = JSON.parse(readFileSync(join(directory, 'package.json'), 'utf8')) as { name?: unknown, version?: unknown }\n if ((pkg.name === '@craft-native/android' || pkg.name === 'craft-native') && typeof pkg.version === 'string' && pkg.version)\n return pkg.version\n }\n catch {\n // Keep walking.\n }\n const parent = dirname(directory)\n if (parent === directory) return '0.0.0'\n directory = parent\n }\n}\n\nconst CRAFT_ANDROID_VERSION = resolveCraftAndroidVersion()\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 /** \"system\" follows the device; light/dark pin the generated host. */\n appearance?: 'light' | 'dark' | 'system'\n version?: string\n versionCode?: number\n darkMode?: boolean\n backgroundColor?: string\n backgroundColorDark?: 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 /**\n * The Health Connect data the app reads and writes, by the shared type\n * names (`steps`, `sleep`, `workouts`...). The manifest declares only\n * these, as Play's health permissions review requires; without it, the\n * original set (steps, heart rate, energy, distance, workouts).\n */\n healthConnect?: {\n read?: HealthConnectType[]\n write?: HealthConnectType[]\n /** Read while the app is in the background (a WorkManager refresh): Android 15's READ_HEALTH_DATA_IN_BACKGROUND. */\n background?: boolean\n /** Read further back than the 30 days before the first grant: READ_HEALTH_DATA_HISTORY. */\n history?: boolean\n }\n enableHealthConnectBackground?: boolean\n enableHealthConnectHistory?: boolean\n /** The app's privacy policy, which Health Connect links to from its permission screen. */\n privacyPolicyURL?: string\n enableKeepAwake?: boolean\n enableDeepLinks?: boolean\n /** Reach the person's own devices on the local network over plain HTTP (a Mac companion). */\n enableLocalNetwork?: boolean\n urlSchemes?: string[]\n trustedOrigins?: string[]\n appIconPath?: string\n googleServicesFile?: string\n /** Google Maps Android SDK key used by native `Map` screens. */\n mapsApiKey?: 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 * Native renderer: the app's paths and the screen drawing each\n * (`/m/workout/:id` → `Workout`), its tabs, and the bundle route of the\n * script that runs beside the screens. The same keys as the iOS builder.\n */\n nativeScreens?: Record<string, string>\n tabs?: Array<{ id: string, title: string, symbol?: string }>\n nativeApp?: string\n /** Wake the app script every so often (`craftBackgroundRefresh`), through WorkManager. */\n backgroundRefresh?: { enabled?: boolean, minimumIntervalMinutes?: number }\n /**\n * Crash and error reporting to bughq (CraftCrashReporter.kt): uncaught JVM\n * exceptions, ANRs and native crashes (ApplicationExitInfo), uncaught\n * JavaScript errors, and what the page sends through `craft.crashReporter`.\n * Queued on disk and sent when online. Release builds are minified: upload\n * `app/build/outputs/mapping/release/mapping.txt` so bughq can retrace.\n */\n crashReporting?: {\n /** The bughq project's public ingest key. */\n key: string\n /** Default https://bughq.org. */\n host?: string\n /** Default `production`. */\n environment?: string\n /** Default `<versionName>+<versionCode>`, e.g. `1.0.0+41`. */\n release?: string\n /** One session per launch, for crash-free sessions. Default on. */\n sessions?: boolean\n }\n}\n\n/** The shared health data types (the iOS builder's HealthKit names). */\nexport type HealthConnectType = 'steps' | 'heartRate' | 'activeEnergy' | 'distance' | 'workouts' | 'restingHeartRate'\n | 'heartRateVariability' | 'bodyMass' | 'sleep' | 'mindful' | 'dietaryEnergy' | 'dietaryProtein'\n | 'dietaryCarbohydrates' | 'dietaryFat'\n\n/**\n * The Health Connect permissions behind each type, for reading and for\n * writing. CraftNativeHealth.kt asks for the same ones; a permission the\n * manifest does not declare is refused without a prompt.\n */\nexport const HEALTH_CONNECT_PERMISSIONS: Record<HealthConnectType, { read: string[], write: string[] }> = {\n steps: { read: ['READ_STEPS'], write: [] },\n heartRate: { read: ['READ_HEART_RATE'], write: [] },\n activeEnergy: { read: ['READ_ACTIVE_CALORIES_BURNED'], write: ['WRITE_ACTIVE_CALORIES_BURNED'] },\n distance: { read: ['READ_DISTANCE'], write: ['WRITE_DISTANCE'] },\n workouts: {\n read: ['READ_EXERCISE', 'READ_HEART_RATE', 'READ_ACTIVE_CALORIES_BURNED', 'READ_DISTANCE'],\n write: ['WRITE_EXERCISE', 'WRITE_ACTIVE_CALORIES_BURNED', 'WRITE_DISTANCE'],\n },\n restingHeartRate: { read: ['READ_RESTING_HEART_RATE'], write: [] },\n heartRateVariability: { read: ['READ_HEART_RATE_VARIABILITY'], write: [] },\n bodyMass: { read: ['READ_WEIGHT'], write: [] },\n sleep: { read: ['READ_SLEEP'], write: [] },\n mindful: { read: ['READ_MINDFULNESS'], write: ['WRITE_MINDFULNESS'] },\n dietaryEnergy: { read: ['READ_NUTRITION'], write: ['WRITE_NUTRITION'] },\n dietaryProtein: { read: ['READ_NUTRITION'], write: ['WRITE_NUTRITION'] },\n dietaryCarbohydrates: { read: ['READ_NUTRITION'], write: ['WRITE_NUTRITION'] },\n dietaryFat: { read: ['READ_NUTRITION'], write: ['WRITE_NUTRITION'] },\n}\n\nconst DEFAULT_HEALTH_CONNECT = { read: ['steps', 'heartRate', 'activeEnergy', 'distance', 'workouts'], write: ['workouts'] } as const\n\n/** Every `android.permission.health.*` the app's Health Connect types need. */\nexport function healthConnectPermissions(config: Pick<CraftAndroidConfig, 'healthConnect'>): string[] {\n const read = config.healthConnect?.read ?? DEFAULT_HEALTH_CONNECT.read\n const write = config.healthConnect?.write ?? (config.healthConnect ? [] : DEFAULT_HEALTH_CONNECT.write)\n const permissions = new Set<string>()\n for (const type of read) {\n const known = HEALTH_CONNECT_PERMISSIONS[type as HealthConnectType]\n if (!known) throw new Error(`Unknown Health Connect type: ${type}`)\n known.read.forEach(permission => permissions.add(`android.permission.health.${permission}`))\n }\n for (const type of write) {\n const known = HEALTH_CONNECT_PERMISSIONS[type as HealthConnectType]\n if (!known) throw new Error(`Unknown Health Connect type: ${type}`)\n known.write.forEach(permission => permissions.add(`android.permission.health.${permission}`))\n }\n if (config.healthConnect?.background) permissions.add('android.permission.health.READ_HEALTH_DATA_IN_BACKGROUND')\n if (config.healthConnect?.history) permissions.add('android.permission.health.READ_HEALTH_DATA_HISTORY')\n return [...permissions]\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 /**\n * Native renderer: the app's icons as vectors (`{ viewBox, strokeWidth,\n * icons: { name: svgBody } }`, keyed by Iconify name and SF Symbol), which\n * `<Icon>`, the tab bar and the bar buttons draw. Without it they fall\n * back to the glyphs Android's fonts have.\n */\n iconsPath?: string\n devServer?: string\n output: string\n release?: boolean\n /**\n * An Android App Bundle (`bundleRelease`, `.aab`), what Google Play takes,\n * rather than an APK. Signed with CRAFT_ANDROID_KEYSTORE,\n * CRAFT_ANDROID_KEYSTORE_PASSWORD, CRAFT_ANDROID_KEY_ALIAS and\n * CRAFT_ANDROID_KEY_PASSWORD when they are set.\n */\n bundle?: 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 if (config.appearance !== undefined && !['light', 'dark', 'system'].includes(config.appearance)) {\n throw new Error(`Unknown Android appearance: ${config.appearance}. Expected light, dark, or system.`)\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 if (config.backgroundColorDark !== undefined && !config.backgroundColorDark.match(/^#[\\dA-F]{3,4}$|^#[\\dA-F]{6}$|^#[\\dA-F]{8}$/i)) {\n throw new Error(`Invalid Android dark background color: ${config.backgroundColorDark}`)\n }\n\n if (config.mapsApiKey !== undefined && !config.mapsApiKey.trim()) {\n throw new Error('Android mapsApiKey must not be empty')\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) healthConnectPermissions(config).forEach(permission => permissions.add(permission))\n // A background refresh (WorkManager) after a restart.\n if (config.renderer === 'native' && config.backgroundRefresh?.enabled) permissions.add('android.permission.RECEIVE_BOOT_COMPLETED')\n return [...permissions].map(permission => ` <uses-permission android:name=\"${permission}\" />`).join('\\n')\n}\n\n/**\n * The native renderer's own sources: the app around the screens (routes,\n * stores, bars), its services (dialogs, browser, share, push, Health\n * Connect), background refresh, and Firebase messaging when push is on.\n */\nfunction writeNativeShellSources(output: string, packagePath: string, packageName: string, config: CraftAndroidConfig): void {\n const javaDir = join(output, 'app/src/main/java', packagePath)\n const write = (template: string, file: string, replace: (source: string) => string = source => source): void => {\n const source = readFileSync(join(TEMPLATES_DIR, template), 'utf-8').replace(/\\{\\{PACKAGE_NAME\\}\\}/g, packageName)\n writeFileSync(join(javaDir, file), replace(source))\n }\n write('CraftNativeShell.kt.template', 'CraftNativeShell.kt')\n write('CraftNativeBackground.kt.template', 'CraftNativeBackground.kt')\n write(config.enableHealthConnect ? 'CraftNativeHealth.kt.template' : 'CraftNativeHealthStub.kt.template', 'CraftNativeHealth.kt')\n write('CraftNativeServices.kt.template', 'CraftNativeServices.kt', source => source\n .replace('{{NATIVE_PUSH_IMPORTS}}', config.enablePushNotifications ? 'import com.google.firebase.messaging.FirebaseMessaging' : '')\n .replace('{{NATIVE_PUSH_REGISTER}}', config.enablePushNotifications\n ? ` try {\n FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->\n val token = if (task.isSuccessful) task.result else null\n if (token.isNullOrBlank()) done(failure(\"PUSH_UNAVAILABLE\", task.exception?.message ?: \"Firebase Cloud Messaging gave no token\"))\n else {\n context.getSharedPreferences(\"craft-native-push\", Context.MODE_PRIVATE).edit().putString(\"token\", token).apply()\n done(success(token))\n }\n }\n } catch (error: Exception) {\n done(failure(\"PUSH_UNAVAILABLE\", error.message ?: \"Firebase Cloud Messaging is not configured\"))\n }`\n : ' done(failure(\"CAPABILITY_DISABLED\", \"Push notifications are disabled\"))'))\n if (config.enablePushNotifications) write('CraftNativeMessagingService.kt.template', 'CraftNativeMessagingService.kt')\n else rmSync(join(javaDir, 'CraftNativeMessagingService.kt'), { force: true })\n // Files a screen shares (craft.files' copies, in the cache) go to other apps through this provider.\n mkdirSync(join(output, 'app/src/main/res/xml'), { recursive: true })\n writeFileSync(join(output, 'app/src/main/res/xml/craft_file_paths.xml'), `<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<paths>\n <cache-path name=\"craft-files\" path=\"craft-files/\" />\n <files-path name=\"craft-media\" path=\"craft-media/\" />\n</paths>\n`)\n}\n\n/** What the native renderer adds to the manifest's application element. */\nfunction renderNativeShellManifest(config: CraftAndroidConfig): string {\n const parts = [\n ' <receiver android:name=\".CraftNativeNotificationReceiver\" android:exported=\"false\" />',\n ` <provider\n android:name=\"androidx.core.content.FileProvider\"\n android:authorities=\"${config.packageName}.craftfiles\"\n android:exported=\"false\"\n android:grantUriPermissions=\"true\">\n <meta-data android:name=\"android.support.FILE_PROVIDER_PATHS\" android:resource=\"@xml/craft_file_paths\" />\n </provider>`,\n ]\n if (config.enablePushNotifications) {\n parts.push(` <service android:name=\".CraftNativeMessagingService\" android:exported=\"false\">\n <intent-filter>\n <action android:name=\"com.google.firebase.MESSAGING_EVENT\" />\n </intent-filter>\n </service>`)\n }\n if (config.enableHealthConnect) {\n // Android 14 and later open the app's privacy policy from Health\n // Connect's permission screen through this alias.\n parts.push(` <activity-alias\n android:name=\"ViewPermissionUsageActivity\"\n android:exported=\"true\"\n android:targetActivity=\".MainActivity\"\n android:permission=\"android.permission.START_VIEW_PERMISSION_USAGE\">\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 </activity-alias>`)\n }\n return parts.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 config.enableHealthConnectBackground = Boolean(config.healthConnect?.background)\n config.enableHealthConnectHistory = Boolean(config.healthConnect?.history)\n healthConnectPermissions(config)\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/values-night'),\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 // Both renderers: crash reporting does not depend on how screens are drawn.\n writeFileSync(\n join(output, 'app/src/main/java', packagePath, 'CraftCrashReporter.kt'),\n readFileSync(join(TEMPLATES_DIR, 'CraftCrashReporter.kt.template'), 'utf-8')\n .replace(/\\{\\{PACKAGE_NAME\\}\\}/g, finalPackageName)\n .replace(/\\{\\{CRAFT_VERSION\\}\\}/g, CRAFT_ANDROID_VERSION),\n )\n if (config.renderer === 'native') {\n const mutationTemplate = readFileSync(join(TEMPLATES_DIR, 'CraftNativeMutation.kt.template'), 'utf-8')\n const flatListTemplate = readFileSync(join(TEMPLATES_DIR, 'CraftNativeFlatList.kt.template'), 'utf-8')\n const refreshableFlatListTemplate = readFileSync(join(TEMPLATES_DIR, 'CraftNativeRefreshableFlatList.kt.template'), 'utf-8')\n const capabilityTemplate = readFileSync(join(TEMPLATES_DIR, 'CraftNativeCapabilities.kt.template'), 'utf-8')\n const notificationTemplate = readFileSync(join(TEMPLATES_DIR, 'CraftNativeNotificationReceiver.kt.template'), 'utf-8')\n writeFileSync(\n join(output, 'app/src/main/java', packagePath, 'CraftNativeMutation.kt'),\n mutationTemplate.replace(/\\{\\{PACKAGE_NAME\\}\\}/g, finalPackageName),\n )\n writeFileSync(\n join(output, 'app/src/main/java', packagePath, 'CraftNativeFlatList.kt'),\n flatListTemplate.replace(/\\{\\{PACKAGE_NAME\\}\\}/g, finalPackageName),\n )\n writeFileSync(\n join(output, 'app/src/main/java', packagePath, 'CraftNativeRefreshableFlatList.kt'),\n refreshableFlatListTemplate.replace(/\\{\\{PACKAGE_NAME\\}\\}/g, finalPackageName),\n )\n writeFileSync(\n join(output, 'app/src/main/java', packagePath, 'CraftNativeCapabilities.kt'),\n capabilityTemplate.replace(/\\{\\{PACKAGE_NAME\\}\\}/g, finalPackageName),\n )\n writeFileSync(\n join(output, 'app/src/main/java', packagePath, 'CraftNativeNotificationReceiver.kt'),\n notificationTemplate.replace(/\\{\\{PACKAGE_NAME\\}\\}/g, finalPackageName),\n )\n writeNativeShellSources(output, packagePath, finalPackageName, config)\n }\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 // Plain HTTP only for a local dev server, or an app that talks to the\n // person's own devices on the LAN (which have no public certificate).\n .replace(/\\{\\{USES_CLEARTEXT\\}\\}/g, config.devServerURL?.startsWith('http://') || config.enableLocalNetwork ? '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(/\\{\\{NATIVE_NOTIFICATION_RECEIVER\\}\\}/g, config.renderer === 'native'\n ? renderNativeShellManifest(config)\n : '')\n // Starts the crash reporter before Application.onCreate, for every way\n // the process starts (CraftCrashReporter.kt). Inert without a key.\n .replace(/\\{\\{CRASH_REPORTER_PROVIDER\\}\\}/g, ` <provider android:name=\".CraftCrashReporterInit\" android:authorities=\"\\${applicationId}.craft-crash\" android:exported=\"false\" android:initOrder=\"1000\" />`)\n .replace(/\\{\\{GOOGLE_MAPS_API_KEY\\}\\}/g, config.mapsApiKey\n ? ` <meta-data android:name=\"com.google.android.geo.API_KEY\" android:value=\"${escapeXml(config.mapsApiKey)}\" />`\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>${config.renderer === 'native' ? '' : `\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(/\\{\\{REFRESH_LAYOUT_DEPENDENCY\\}\\}/g, config.renderer === 'native'\n ? ' implementation(\"androidx.swiperefreshlayout:swiperefreshlayout:1.1.0\")'\n : '')\n .replace(/\\{\\{GOOGLE_MAPS_DEPENDENCY\\}\\}/g, config.renderer === 'native'\n ? ' // Native STX Map overlays and trail routes\\n implementation(\"com.google.android.gms:play-services-maps:18.2.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 // The in-app browser (Custom Tabs) and background refresh (WorkManager).\n implementation(\"androidx.browser:browser:1.8.0\")\n implementation(\"androidx.work:work-runtime-ktx:2.9.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 const darkColorsPath = join(output, 'app/src/main/res/values-night/colors.xml')\n if (config.backgroundColorDark) {\n writeFileSync(darkColorsPath, `<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <color name=\"background\">${config.backgroundColorDark}</color>\n</resources>\n`)\n }\n else {\n rmSync(darkColorsPath, { force: true })\n }\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, bundle, 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 const icons = join(output, 'app/src/main/assets/craft-icons.json')\n if (options.iconsPath) {\n requireRegularFile(options.iconsPath, 'Icon table')\n if (resolve(options.iconsPath) !== resolve(icons)) cpSync(options.iconsPath, icons)\n }\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 compile` 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 variant = release ? 'Release' : 'Debug'\n const buildType = `${bundle ? 'bundle' : 'assemble'}${variant}`\n console.log(` Building ${release ? 'release' : 'debug'} ${bundle ? 'app bundle' : 'APK'}...`)\n if (release && !process.env.CRAFT_ANDROID_KEYSTORE)\n console.log(' CRAFT_ANDROID_KEYSTORE is not set: the release build is unsigned.')\n\n try {\n await $`cd ${output} && ./gradlew ${buildType}`\n const artifact = bundle\n ? `app/build/outputs/bundle/${variant.toLowerCase()}/app-${variant.toLowerCase()}.aab`\n : release\n ? 'app/build/outputs/apk/release/app-release.apk'\n : 'app/build/outputs/apk/debug/app-debug.apk'\n console.log(`✅ ${bundle ? 'App bundle' : 'APK'} built: ${artifact}`)\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')\n\n/**\n * This builder's version, which the crash reporter names as its SDK version.\n * Walks up from here because a bundled build puts this module below the\n * package root.\n */\nfunction resolveCraftAndroidVersion(startDir: string = import.meta.dir): string {\n let directory = resolve(startDir)\n while (true) {\n try {\n const pkg = JSON.parse(readFileSync(join(directory, 'package.json'), 'utf8')) as { name?: unknown, version?: unknown }\n if ((pkg.name === '@craft-native/android' || pkg.name === 'craft-native') && typeof pkg.version === 'string' && pkg.version)\n return pkg.version\n }\n catch {\n // Keep walking.\n }\n const parent = dirname(directory)\n if (parent === directory) return '0.0.0'\n directory = parent\n }\n}\n\nconst CRAFT_ANDROID_VERSION = resolveCraftAndroidVersion()\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 /** \"system\" follows the device; light/dark pin the generated host. */\n appearance?: 'light' | 'dark' | 'system'\n version?: string\n versionCode?: number\n darkMode?: boolean\n backgroundColor?: string\n backgroundColorDark?: 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 enableBluetooth?: boolean\n enableHealthConnect?: boolean\n /**\n * The Health Connect data the app reads and writes, by the shared type\n * names (`steps`, `sleep`, `workouts`...). The manifest declares only\n * these, as Play's health permissions review requires; without it, the\n * original set (steps, heart rate, energy, distance, workouts).\n */\n healthConnect?: {\n read?: HealthConnectType[]\n write?: HealthConnectType[]\n /** Read while the app is in the background (a WorkManager refresh): Android 15's READ_HEALTH_DATA_IN_BACKGROUND. */\n background?: boolean\n /** Read further back than the 30 days before the first grant: READ_HEALTH_DATA_HISTORY. */\n history?: boolean\n }\n enableHealthConnectBackground?: boolean\n enableHealthConnectHistory?: boolean\n /** The app's privacy policy, which Health Connect links to from its permission screen. */\n privacyPolicyURL?: string\n enableKeepAwake?: boolean\n enableDeepLinks?: boolean\n /** Reach the person's own devices on the local network over plain HTTP (a Mac companion). */\n enableLocalNetwork?: boolean\n urlSchemes?: string[]\n trustedOrigins?: string[]\n appIconPath?: string\n googleServicesFile?: string\n /** Google Maps Android SDK key used by native `Map` screens. */\n mapsApiKey?: 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 * Native renderer: the app's paths and the screen drawing each\n * (`/m/workout/:id` → `Workout`), its tabs, and the bundle route of the\n * script that runs beside the screens. The same keys as the iOS builder.\n */\n nativeScreens?: Record<string, string>\n tabs?: Array<{ id: string, title: string, symbol?: string }>\n nativeApp?: string\n /** Wake the app script every so often (`craftBackgroundRefresh`), through WorkManager. */\n backgroundRefresh?: { enabled?: boolean, minimumIntervalMinutes?: number }\n /**\n * Crash and error reporting to bughq (CraftCrashReporter.kt): uncaught JVM\n * exceptions, ANRs and native crashes (ApplicationExitInfo), uncaught\n * JavaScript errors, and what the page sends through `craft.crashReporter`.\n * Queued on disk and sent when online. Release builds are minified: upload\n * `app/build/outputs/mapping/release/mapping.txt` so bughq can retrace.\n */\n crashReporting?: {\n /** The bughq project's public ingest key. */\n key: string\n /** Default https://bughq.org. */\n host?: string\n /** Default `production`. */\n environment?: string\n /** Default `<versionName>+<versionCode>`, e.g. `1.0.0+41`. */\n release?: string\n /** One session per launch, for crash-free sessions. Default on. */\n sessions?: boolean\n }\n}\n\n/** The shared health data types (the iOS builder's HealthKit names). */\nexport type HealthConnectType = 'steps' | 'heartRate' | 'activeEnergy' | 'distance' | 'workouts' | 'restingHeartRate'\n | 'heartRateVariability' | 'bodyMass' | 'sleep' | 'mindful' | 'dietaryEnergy' | 'dietaryProtein'\n | 'dietaryCarbohydrates' | 'dietaryFat'\n\n/**\n * The Health Connect permissions behind each type, for reading and for\n * writing. CraftNativeHealth.kt asks for the same ones; a permission the\n * manifest does not declare is refused without a prompt.\n */\nexport const HEALTH_CONNECT_PERMISSIONS: Record<HealthConnectType, { read: string[], write: string[] }> = {\n steps: { read: ['READ_STEPS'], write: [] },\n heartRate: { read: ['READ_HEART_RATE'], write: [] },\n activeEnergy: { read: ['READ_ACTIVE_CALORIES_BURNED'], write: ['WRITE_ACTIVE_CALORIES_BURNED'] },\n distance: { read: ['READ_DISTANCE'], write: ['WRITE_DISTANCE'] },\n workouts: {\n read: ['READ_EXERCISE', 'READ_HEART_RATE', 'READ_ACTIVE_CALORIES_BURNED', 'READ_DISTANCE'],\n write: ['WRITE_EXERCISE', 'WRITE_ACTIVE_CALORIES_BURNED', 'WRITE_DISTANCE'],\n },\n restingHeartRate: { read: ['READ_RESTING_HEART_RATE'], write: [] },\n heartRateVariability: { read: ['READ_HEART_RATE_VARIABILITY'], write: [] },\n bodyMass: { read: ['READ_WEIGHT'], write: [] },\n sleep: { read: ['READ_SLEEP'], write: [] },\n mindful: { read: ['READ_MINDFULNESS'], write: ['WRITE_MINDFULNESS'] },\n dietaryEnergy: { read: ['READ_NUTRITION'], write: ['WRITE_NUTRITION'] },\n dietaryProtein: { read: ['READ_NUTRITION'], write: ['WRITE_NUTRITION'] },\n dietaryCarbohydrates: { read: ['READ_NUTRITION'], write: ['WRITE_NUTRITION'] },\n dietaryFat: { read: ['READ_NUTRITION'], write: ['WRITE_NUTRITION'] },\n}\n\nconst DEFAULT_HEALTH_CONNECT = { read: ['steps', 'heartRate', 'activeEnergy', 'distance', 'workouts'], write: ['workouts'] } as const\n\n/** Every `android.permission.health.*` the app's Health Connect types need. */\nexport function healthConnectPermissions(config: Pick<CraftAndroidConfig, 'healthConnect'>): string[] {\n const read = config.healthConnect?.read ?? DEFAULT_HEALTH_CONNECT.read\n const write = config.healthConnect?.write ?? (config.healthConnect ? [] : DEFAULT_HEALTH_CONNECT.write)\n const permissions = new Set<string>()\n for (const type of read) {\n const known = HEALTH_CONNECT_PERMISSIONS[type as HealthConnectType]\n if (!known) throw new Error(`Unknown Health Connect type: ${type}`)\n known.read.forEach(permission => permissions.add(`android.permission.health.${permission}`))\n }\n for (const type of write) {\n const known = HEALTH_CONNECT_PERMISSIONS[type as HealthConnectType]\n if (!known) throw new Error(`Unknown Health Connect type: ${type}`)\n known.write.forEach(permission => permissions.add(`android.permission.health.${permission}`))\n }\n if (config.healthConnect?.background) permissions.add('android.permission.health.READ_HEALTH_DATA_IN_BACKGROUND')\n if (config.healthConnect?.history) permissions.add('android.permission.health.READ_HEALTH_DATA_HISTORY')\n return [...permissions]\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 /**\n * Native renderer: the app's icons as vectors (`{ viewBox, strokeWidth,\n * icons: { name: svgBody } }`, keyed by Iconify name and SF Symbol), which\n * `<Icon>`, the tab bar and the bar buttons draw. Without it they fall\n * back to the glyphs Android's fonts have.\n */\n iconsPath?: string\n devServer?: string\n output: string\n release?: boolean\n /**\n * An Android App Bundle (`bundleRelease`, `.aab`), what Google Play takes,\n * rather than an APK. Signed with CRAFT_ANDROID_KEYSTORE,\n * CRAFT_ANDROID_KEYSTORE_PASSWORD, CRAFT_ANDROID_KEY_ALIAS and\n * CRAFT_ANDROID_KEY_PASSWORD when they are set.\n */\n bundle?: 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 enableBluetooth: 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 if (config.appearance !== undefined && !['light', 'dark', 'system'].includes(config.appearance)) {\n throw new Error(`Unknown Android appearance: ${config.appearance}. Expected light, dark, or system.`)\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 if (config.backgroundColorDark !== undefined && !config.backgroundColorDark.match(/^#[\\dA-F]{3,4}$|^#[\\dA-F]{6}$|^#[\\dA-F]{8}$/i)) {\n throw new Error(`Invalid Android dark background color: ${config.backgroundColorDark}`)\n }\n\n if (config.mapsApiKey !== undefined && !config.mapsApiKey.trim()) {\n throw new Error('Android mapsApiKey must not be empty')\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) healthConnectPermissions(config).forEach(permission => permissions.add(permission))\n // A background refresh (WorkManager) after a restart.\n if (config.renderer === 'native' && config.backgroundRefresh?.enabled) permissions.add('android.permission.RECEIVE_BOOT_COMPLETED')\n const bluetooth = config.enableBluetooth ? [\n ' <uses-permission android:name=\"android.permission.BLUETOOTH\" android:maxSdkVersion=\"30\" />',\n ' <uses-permission android:name=\"android.permission.BLUETOOTH_ADMIN\" android:maxSdkVersion=\"30\" />',\n ' <uses-permission android:name=\"android.permission.BLUETOOTH_SCAN\" android:usesPermissionFlags=\"neverForLocation\" />',\n ' <uses-permission android:name=\"android.permission.BLUETOOTH_CONNECT\" />',\n ...(config.enableGeolocation ? [] : [' <uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\" android:maxSdkVersion=\"30\" />']),\n ].join('\\n') : ''\n return [bluetooth, ...[...permissions].map(permission => ` <uses-permission android:name=\"${permission}\" />`)].filter(Boolean).join('\\n')\n}\n\n/**\n * The native renderer's own sources: the app around the screens (routes,\n * stores, bars), its services (dialogs, browser, share, push, Health\n * Connect), background refresh, and Firebase messaging when push is on.\n */\nfunction writeNativeShellSources(output: string, packagePath: string, packageName: string, config: CraftAndroidConfig): void {\n const javaDir = join(output, 'app/src/main/java', packagePath)\n const write = (template: string, file: string, replace: (source: string) => string = source => source): void => {\n const source = readFileSync(join(TEMPLATES_DIR, template), 'utf-8').replace(/\\{\\{PACKAGE_NAME\\}\\}/g, packageName)\n writeFileSync(join(javaDir, file), replace(source))\n }\n write('CraftBluetooth.kt.template', 'CraftBluetooth.kt')\n write('CraftNativeShell.kt.template', 'CraftNativeShell.kt')\n write('CraftNativeBackground.kt.template', 'CraftNativeBackground.kt')\n write(config.enableHealthConnect ? 'CraftNativeHealth.kt.template' : 'CraftNativeHealthStub.kt.template', 'CraftNativeHealth.kt')\n write('CraftNativeServices.kt.template', 'CraftNativeServices.kt', source => source\n .replace('{{NATIVE_PUSH_IMPORTS}}', config.enablePushNotifications ? 'import com.google.firebase.messaging.FirebaseMessaging' : '')\n .replace('{{NATIVE_PUSH_REGISTER}}', config.enablePushNotifications\n ? ` try {\n FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->\n val token = if (task.isSuccessful) task.result else null\n if (token.isNullOrBlank()) done(failure(\"PUSH_UNAVAILABLE\", task.exception?.message ?: \"Firebase Cloud Messaging gave no token\"))\n else {\n context.getSharedPreferences(\"craft-native-push\", Context.MODE_PRIVATE).edit().putString(\"token\", token).apply()\n done(success(token))\n }\n }\n } catch (error: Exception) {\n done(failure(\"PUSH_UNAVAILABLE\", error.message ?: \"Firebase Cloud Messaging is not configured\"))\n }`\n : ' done(failure(\"CAPABILITY_DISABLED\", \"Push notifications are disabled\"))'))\n if (config.enablePushNotifications) write('CraftNativeMessagingService.kt.template', 'CraftNativeMessagingService.kt')\n else rmSync(join(javaDir, 'CraftNativeMessagingService.kt'), { force: true })\n // Files a screen shares (craft.files' copies, in the cache) go to other apps through this provider.\n mkdirSync(join(output, 'app/src/main/res/xml'), { recursive: true })\n writeFileSync(join(output, 'app/src/main/res/xml/craft_file_paths.xml'), `<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<paths>\n <cache-path name=\"craft-files\" path=\"craft-files/\" />\n <files-path name=\"craft-media\" path=\"craft-media/\" />\n</paths>\n`)\n}\n\n/** What the native renderer adds to the manifest's application element. */\nfunction renderNativeShellManifest(config: CraftAndroidConfig): string {\n const parts = [\n ' <receiver android:name=\".CraftNativeNotificationReceiver\" android:exported=\"false\" />',\n ` <provider\n android:name=\"androidx.core.content.FileProvider\"\n android:authorities=\"${config.packageName}.craftfiles\"\n android:exported=\"false\"\n android:grantUriPermissions=\"true\">\n <meta-data android:name=\"android.support.FILE_PROVIDER_PATHS\" android:resource=\"@xml/craft_file_paths\" />\n </provider>`,\n ]\n if (config.enablePushNotifications) {\n parts.push(` <service android:name=\".CraftNativeMessagingService\" android:exported=\"false\">\n <intent-filter>\n <action android:name=\"com.google.firebase.MESSAGING_EVENT\" />\n </intent-filter>\n </service>`)\n }\n if (config.enableHealthConnect) {\n // Android 14 and later open the app's privacy policy from Health\n // Connect's permission screen through this alias.\n parts.push(` <activity-alias\n android:name=\"ViewPermissionUsageActivity\"\n android:exported=\"true\"\n android:targetActivity=\".MainActivity\"\n android:permission=\"android.permission.START_VIEW_PERMISSION_USAGE\">\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 </activity-alias>`)\n }\n return parts.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 config.enableHealthConnectBackground = Boolean(config.healthConnect?.background)\n config.enableHealthConnectHistory = Boolean(config.healthConnect?.history)\n healthConnectPermissions(config)\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/values-night'),\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 // Both renderers: crash reporting does not depend on how screens are drawn.\n writeFileSync(\n join(output, 'app/src/main/java', packagePath, 'CraftCrashReporter.kt'),\n readFileSync(join(TEMPLATES_DIR, 'CraftCrashReporter.kt.template'), 'utf-8')\n .replace(/\\{\\{PACKAGE_NAME\\}\\}/g, finalPackageName)\n .replace(/\\{\\{CRAFT_VERSION\\}\\}/g, CRAFT_ANDROID_VERSION),\n )\n if (config.renderer === 'native') {\n const mutationTemplate = readFileSync(join(TEMPLATES_DIR, 'CraftNativeMutation.kt.template'), 'utf-8')\n const flatListTemplate = readFileSync(join(TEMPLATES_DIR, 'CraftNativeFlatList.kt.template'), 'utf-8')\n const refreshableFlatListTemplate = readFileSync(join(TEMPLATES_DIR, 'CraftNativeRefreshableFlatList.kt.template'), 'utf-8')\n const capabilityTemplate = readFileSync(join(TEMPLATES_DIR, 'CraftNativeCapabilities.kt.template'), 'utf-8')\n const notificationTemplate = readFileSync(join(TEMPLATES_DIR, 'CraftNativeNotificationReceiver.kt.template'), 'utf-8')\n writeFileSync(\n join(output, 'app/src/main/java', packagePath, 'CraftNativeMutation.kt'),\n mutationTemplate.replace(/\\{\\{PACKAGE_NAME\\}\\}/g, finalPackageName),\n )\n writeFileSync(\n join(output, 'app/src/main/java', packagePath, 'CraftNativeFlatList.kt'),\n flatListTemplate.replace(/\\{\\{PACKAGE_NAME\\}\\}/g, finalPackageName),\n )\n writeFileSync(\n join(output, 'app/src/main/java', packagePath, 'CraftNativeRefreshableFlatList.kt'),\n refreshableFlatListTemplate.replace(/\\{\\{PACKAGE_NAME\\}\\}/g, finalPackageName),\n )\n writeFileSync(\n join(output, 'app/src/main/java', packagePath, 'CraftNativeCapabilities.kt'),\n capabilityTemplate.replace(/\\{\\{PACKAGE_NAME\\}\\}/g, finalPackageName),\n )\n writeFileSync(\n join(output, 'app/src/main/java', packagePath, 'CraftNativeNotificationReceiver.kt'),\n notificationTemplate.replace(/\\{\\{PACKAGE_NAME\\}\\}/g, finalPackageName),\n )\n writeNativeShellSources(output, packagePath, finalPackageName, config)\n }\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 // Plain HTTP only for a local dev server, or an app that talks to the\n // person's own devices on the LAN (which have no public certificate).\n .replace(/\\{\\{USES_CLEARTEXT\\}\\}/g, config.devServerURL?.startsWith('http://') || config.enableLocalNetwork ? '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(/\\{\\{NATIVE_NOTIFICATION_RECEIVER\\}\\}/g, config.renderer === 'native'\n ? renderNativeShellManifest(config)\n : '')\n // Starts the crash reporter before Application.onCreate, for every way\n // the process starts (CraftCrashReporter.kt). Inert without a key.\n .replace(/\\{\\{CRASH_REPORTER_PROVIDER\\}\\}/g, ` <provider android:name=\".CraftCrashReporterInit\" android:authorities=\"\\${applicationId}.craft-crash\" android:exported=\"false\" android:initOrder=\"1000\" />`)\n .replace(/\\{\\{GOOGLE_MAPS_API_KEY\\}\\}/g, config.mapsApiKey\n ? ` <meta-data android:name=\"com.google.android.geo.API_KEY\" android:value=\"${escapeXml(config.mapsApiKey)}\" />`\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>${config.renderer === 'native' ? '' : `\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(/\\{\\{REFRESH_LAYOUT_DEPENDENCY\\}\\}/g, config.renderer === 'native'\n ? ' implementation(\"androidx.swiperefreshlayout:swiperefreshlayout:1.1.0\")'\n : '')\n .replace(/\\{\\{GOOGLE_MAPS_DEPENDENCY\\}\\}/g, config.renderer === 'native'\n ? ' // Native STX Map overlays and trail routes\\n implementation(\"com.google.android.gms:play-services-maps:18.2.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 // The in-app browser (Custom Tabs) and background refresh (WorkManager).\n implementation(\"androidx.browser:browser:1.8.0\")\n implementation(\"androidx.work:work-runtime-ktx:2.9.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 const darkColorsPath = join(output, 'app/src/main/res/values-night/colors.xml')\n if (config.backgroundColorDark) {\n writeFileSync(darkColorsPath, `<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <color name=\"background\">${config.backgroundColorDark}</color>\n</resources>\n`)\n }\n else {\n rmSync(darkColorsPath, { force: true })\n }\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, bundle, 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 const icons = join(output, 'app/src/main/assets/craft-icons.json')\n if (options.iconsPath) {\n requireRegularFile(options.iconsPath, 'Icon table')\n if (resolve(options.iconsPath) !== resolve(icons)) cpSync(options.iconsPath, icons)\n }\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 compile` 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 variant = release ? 'Release' : 'Debug'\n const buildType = `${bundle ? 'bundle' : 'assemble'}${variant}`\n console.log(` Building ${release ? 'release' : 'debug'} ${bundle ? 'app bundle' : 'APK'}...`)\n if (release && !process.env.CRAFT_ANDROID_KEYSTORE)\n console.log(' CRAFT_ANDROID_KEYSTORE is not set: the release build is unsigned.')\n\n try {\n await $`cd ${output} && ./gradlew ${buildType}`\n const artifact = bundle\n ? `app/build/outputs/bundle/${variant.toLowerCase()}/app-${variant.toLowerCase()}.aab`\n : release\n ? 'app/build/outputs/apk/release/app-release.apk'\n : 'app/build/outputs/apk/debug/app-debug.apk'\n console.log(`✅ ${bundle ? 'App bundle' : 'APK'} built: ${artifact}`)\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": "186096D4B88D325264756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
package {{PACKAGE_NAME}}
|
|
2
|
+
|
|
3
|
+
import android.Manifest
|
|
4
|
+
import android.app.Activity
|
|
5
|
+
import android.bluetooth.*
|
|
6
|
+
import android.bluetooth.le.*
|
|
7
|
+
import android.content.Context
|
|
8
|
+
import android.content.pm.PackageManager
|
|
9
|
+
import android.os.Build
|
|
10
|
+
import android.os.Handler
|
|
11
|
+
import android.os.Looper
|
|
12
|
+
import android.os.ParcelUuid
|
|
13
|
+
import androidx.core.app.ActivityCompat
|
|
14
|
+
import androidx.core.content.ContextCompat
|
|
15
|
+
import org.json.JSONArray
|
|
16
|
+
import org.json.JSONObject
|
|
17
|
+
import java.util.UUID
|
|
18
|
+
|
|
19
|
+
/** A selected GATT channel; one request/notification exchange at a time. */
|
|
20
|
+
@Suppress("MissingPermission", "DEPRECATION")
|
|
21
|
+
internal class CraftBluetooth(private val context: Context) {
|
|
22
|
+
private val handler = Handler(Looper.getMainLooper())
|
|
23
|
+
private val adapter get() = (context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager)?.adapter
|
|
24
|
+
private val devices = linkedMapOf<String, BluetoothDevice>()
|
|
25
|
+
private val rows = linkedMapOf<String, JSONObject>()
|
|
26
|
+
private var scanDone: ((CraftNativeCapabilityResult) -> Unit)? = null
|
|
27
|
+
private var permissionScan: JSONObject? = null
|
|
28
|
+
private var prefix = ""
|
|
29
|
+
private var gatt: BluetoothGatt? = null
|
|
30
|
+
private var writer: BluetoothGattCharacteristic? = null
|
|
31
|
+
private var notifier: BluetoothGattCharacteristic? = null
|
|
32
|
+
private var serviceId: UUID? = null
|
|
33
|
+
private var writeId: UUID? = null
|
|
34
|
+
private var notifyId: UUID? = null
|
|
35
|
+
private var connected = false
|
|
36
|
+
private var mtu = 23
|
|
37
|
+
private var connectDone: ((CraftNativeCapabilityResult) -> Unit)? = null
|
|
38
|
+
private var exchangeDone: ((CraftNativeCapabilityResult) -> Unit)? = null
|
|
39
|
+
private var responsePrefix = byteArrayOf()
|
|
40
|
+
private var scanTimer: Runnable? = null
|
|
41
|
+
private var connectTimer: Runnable? = null
|
|
42
|
+
private var exchangeTimer: Runnable? = null
|
|
43
|
+
private fun failure(code: String, message: String) = CraftNativeCapabilityResult(code = code, message = message)
|
|
44
|
+
private fun timer(ms: Long, action: () -> Unit) = Runnable(action).also { handler.postDelayed(it, ms) }
|
|
45
|
+
private fun permitted() = permissions().all { ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED }
|
|
46
|
+
private fun permissions() = if (Build.VERSION.SDK_INT >= 31) arrayOf(Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT) else arrayOf(Manifest.permission.ACCESS_FINE_LOCATION)
|
|
47
|
+
|
|
48
|
+
fun perform(method: String, args: JSONArray, done: (CraftNativeCapabilityResult) -> Unit) {
|
|
49
|
+
handler.post {
|
|
50
|
+
val options = args.optJSONObject(0) ?: JSONObject()
|
|
51
|
+
try {
|
|
52
|
+
when (method) {
|
|
53
|
+
"getState" -> done(CraftNativeCapabilityResult(JSONObject().put("available", adapter != null).put("connected", connected).put("scanning", scanDone != null)))
|
|
54
|
+
"scan" -> scan(options, done)
|
|
55
|
+
"stopScan" -> { stopScan(failure("CANCELLED", "Bluetooth scan cancelled")); done(CraftNativeCapabilityResult(true)) }
|
|
56
|
+
"connect" -> connect(options, done)
|
|
57
|
+
"exchange" -> exchange(options, done)
|
|
58
|
+
"disconnect" -> { disconnect(); done(CraftNativeCapabilityResult(true)) }
|
|
59
|
+
else -> done(failure("UNKNOWN_ACTION", "Unsupported Bluetooth method"))
|
|
60
|
+
}
|
|
61
|
+
} catch (_: SecurityException) { disconnect(); done(failure("NOT_AUTHORIZED", "Bluetooth permission is required")) }
|
|
62
|
+
catch (_: IllegalArgumentException) { done(failure("INVALID_ARGUMENT", "Invalid Bluetooth channel options")) }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private fun scan(options: JSONObject, done: (CraftNativeCapabilityResult) -> Unit) {
|
|
67
|
+
if (scanDone != null || connectDone != null || exchangeDone != null) { done(failure("BUSY", "A Bluetooth operation is running")); return }
|
|
68
|
+
val services = options.optJSONArray("services") ?: throw IllegalArgumentException()
|
|
69
|
+
require(services.length() in 1..8)
|
|
70
|
+
(0 until services.length()).forEach { UUID.fromString(services.getString(it)) }
|
|
71
|
+
scanDone = done
|
|
72
|
+
if (!permitted()) {
|
|
73
|
+
val activity = context as? Activity
|
|
74
|
+
if (activity == null) { stopScan(failure("NEEDS_ACTIVITY", "Bluetooth setup needs a screen")); return }
|
|
75
|
+
permissionScan = options
|
|
76
|
+
ActivityCompat.requestPermissions(activity, permissions(), 4317)
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
startScan(options)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
fun onPermissions(requestCode: Int) {
|
|
83
|
+
if (requestCode != 4317) return
|
|
84
|
+
val options = permissionScan ?: return
|
|
85
|
+
permissionScan = null
|
|
86
|
+
if (!permitted()) stopScan(failure("NOT_AUTHORIZED", "Allow Nearby devices to find your bottle"))
|
|
87
|
+
else try { startScan(options) } catch (_: Exception) { stopScan(failure("SCAN_FAILED", "Bluetooth scan could not start")) }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private fun startScan(options: JSONObject) {
|
|
91
|
+
val radio = adapter
|
|
92
|
+
if (radio == null || !radio.isEnabled) { stopScan(failure("BLUETOOTH_OFF", "Turn on Bluetooth")); return }
|
|
93
|
+
devices.clear(); rows.clear()
|
|
94
|
+
prefix = options.optString("namePrefix")
|
|
95
|
+
val services = options.getJSONArray("services")
|
|
96
|
+
val filters = (0 until services.length()).map { ScanFilter.Builder().setServiceUuid(ParcelUuid(UUID.fromString(services.getString(it)))).build() }
|
|
97
|
+
radio.bluetoothLeScanner.startScan(filters, ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build(), scanner)
|
|
98
|
+
scanTimer = timer(options.optLong("timeoutMs", 10000).coerceIn(1000, 20000)) {
|
|
99
|
+
stopScan(CraftNativeCapabilityResult(JSONArray(rows.values.toList())))
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
private val scanner = object : ScanCallback() {
|
|
103
|
+
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
|
104
|
+
handler.post {
|
|
105
|
+
if (scanDone == null || rows.size >= 64) return@post
|
|
106
|
+
val name = result.scanRecord?.deviceName ?: ""
|
|
107
|
+
if (!name.startsWith(prefix, ignoreCase = true)) return@post
|
|
108
|
+
val id = result.device.address
|
|
109
|
+
devices[id] = result.device
|
|
110
|
+
rows[id] = JSONObject().put("id", id).put("name", name).put("rssi", result.rssi)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
override fun onScanFailed(errorCode: Int) { handler.post { stopScan(failure("SCAN_FAILED", "Bluetooth scan failed ($errorCode)")) } }
|
|
114
|
+
}
|
|
115
|
+
private fun stopScan(result: CraftNativeCapabilityResult) {
|
|
116
|
+
if (permitted()) runCatching { adapter?.bluetoothLeScanner?.stopScan(scanner) }
|
|
117
|
+
scanTimer?.let(handler::removeCallbacks); scanTimer = null; permissionScan = null
|
|
118
|
+
val done = scanDone; scanDone = null; done?.invoke(result)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private fun connect(options: JSONObject, done: (CraftNativeCapabilityResult) -> Unit) {
|
|
122
|
+
if (scanDone != null || connectDone != null || exchangeDone != null) { done(failure("BUSY", "A Bluetooth operation is running")); return }
|
|
123
|
+
if (!permitted()) { done(failure("NOT_AUTHORIZED", "Bluetooth permission is required")); return }
|
|
124
|
+
val device = devices[options.optString("id")] ?: throw IllegalArgumentException()
|
|
125
|
+
disconnect()
|
|
126
|
+
serviceId = UUID.fromString(options.getString("service")); writeId = UUID.fromString(options.getString("write")); notifyId = UUID.fromString(options.getString("notify"))
|
|
127
|
+
connectDone = done
|
|
128
|
+
gatt = device.connectGatt(context, false, callbacks, BluetoothDevice.TRANSPORT_LE)
|
|
129
|
+
connectTimer = timer(15000) { failConnection("TIMEOUT", "Device did not connect; wake it and try again") }
|
|
130
|
+
}
|
|
131
|
+
private fun finishConnect(result: CraftNativeCapabilityResult) {
|
|
132
|
+
connectTimer?.let(handler::removeCallbacks); connectTimer = null
|
|
133
|
+
val done = connectDone; connectDone = null; done?.invoke(result)
|
|
134
|
+
}
|
|
135
|
+
private fun failConnection(code: String, message: String) { finishConnect(failure(code, message)); disconnect() }
|
|
136
|
+
private val callbacks = object : BluetoothGattCallback() {
|
|
137
|
+
override fun onConnectionStateChange(peer: BluetoothGatt, status: Int, newState: Int) { handler.post {
|
|
138
|
+
if (peer !== gatt) return@post
|
|
139
|
+
if (status != BluetoothGatt.GATT_SUCCESS || newState == BluetoothProfile.STATE_DISCONNECTED) { failConnection("DISCONNECTED", "Bluetooth device disconnected"); return@post }
|
|
140
|
+
if (newState == BluetoothProfile.STATE_CONNECTED && !peer.requestMtu(247)) failConnection("MTU_TOO_SMALL", "Could not negotiate Bluetooth MTU")
|
|
141
|
+
} }
|
|
142
|
+
override fun onMtuChanged(peer: BluetoothGatt, value: Int, status: Int) { handler.post {
|
|
143
|
+
if (peer !== gatt) return@post
|
|
144
|
+
if (status != BluetoothGatt.GATT_SUCCESS || value < 185) { failConnection("MTU_TOO_SMALL", "Bluetooth MTU is too small for sensor history"); return@post }
|
|
145
|
+
mtu = value
|
|
146
|
+
if (!peer.discoverServices()) failConnection("CONNECT_FAILED", "Could not discover Bluetooth services")
|
|
147
|
+
} }
|
|
148
|
+
override fun onServicesDiscovered(peer: BluetoothGatt, status: Int) { handler.post {
|
|
149
|
+
if (peer !== gatt) return@post
|
|
150
|
+
val service = serviceId?.let(peer::getService)
|
|
151
|
+
writer = writeId?.let { service?.getCharacteristic(it) }; notifier = notifyId?.let { service?.getCharacteristic(it) }
|
|
152
|
+
val write = writer; val notify = notifier
|
|
153
|
+
if (status != BluetoothGatt.GATT_SUCCESS || write == null || notify == null || write.properties and (BluetoothGattCharacteristic.PROPERTY_WRITE or BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) == 0 || notify.properties and BluetoothGattCharacteristic.PROPERTY_NOTIFY == 0) { failConnection("UNSUPPORTED_DEVICE", "The selected GATT channel is missing"); return@post }
|
|
154
|
+
val descriptor = notify.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"))
|
|
155
|
+
if (descriptor == null || !peer.setCharacteristicNotification(notify, true)) { failConnection("CONNECT_FAILED", "Could not subscribe to the device"); return@post }
|
|
156
|
+
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
|
|
157
|
+
if (!peer.writeDescriptor(descriptor)) failConnection("CONNECT_FAILED", "Could not enable notifications")
|
|
158
|
+
} }
|
|
159
|
+
override fun onDescriptorWrite(peer: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) { handler.post {
|
|
160
|
+
if (peer !== gatt || descriptor.characteristic !== notifier) return@post
|
|
161
|
+
if (status != BluetoothGatt.GATT_SUCCESS) { failConnection("CONNECT_FAILED", "Could not enable notifications"); return@post }
|
|
162
|
+
connected = true
|
|
163
|
+
finishConnect(CraftNativeCapabilityResult(JSONObject().put("id", peer.device.address)))
|
|
164
|
+
} }
|
|
165
|
+
override fun onCharacteristicWrite(peer: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) { handler.post {
|
|
166
|
+
if (peer === gatt && characteristic === writer && status != BluetoothGatt.GATT_SUCCESS) finishExchange(failure("WRITE_FAILED", "Bluetooth write failed"))
|
|
167
|
+
} }
|
|
168
|
+
override fun onCharacteristicChanged(peer: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
|
|
169
|
+
val bytes = characteristic.value?.copyOf() ?: return
|
|
170
|
+
handler.post {
|
|
171
|
+
if (peer !== gatt || characteristic !== notifier || exchangeDone == null) return@post
|
|
172
|
+
if (bytes.size < responsePrefix.size || !bytes.take(responsePrefix.size).toByteArray().contentEquals(responsePrefix)) return@post
|
|
173
|
+
if (bytes.size > 4096) finishExchange(failure("INVALID_RESPONSE", "Bluetooth response is too large"))
|
|
174
|
+
else finishExchange(CraftNativeCapabilityResult(JSONArray(bytes.map { it.toInt() and 255 })))
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
private fun byteArray(options: JSONObject, name: String, max: Int): ByteArray {
|
|
179
|
+
val values = options.getJSONArray(name)
|
|
180
|
+
require(values.length() in 1..max)
|
|
181
|
+
return ByteArray(values.length()) { index ->
|
|
182
|
+
val value = values.get(index)
|
|
183
|
+
require(value is Number && value.toDouble() == value.toInt().toDouble() && value.toInt() in 0..255)
|
|
184
|
+
value.toInt().toByte()
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
private fun exchange(options: JSONObject, done: (CraftNativeCapabilityResult) -> Unit) {
|
|
188
|
+
if (exchangeDone != null) { done(failure("BUSY", "A Bluetooth request is running")); return }
|
|
189
|
+
val peer = gatt; val write = writer
|
|
190
|
+
if (!connected || peer == null || write == null) { done(failure("DISCONNECTED", "Bluetooth device is disconnected")); return }
|
|
191
|
+
val bytes = byteArray(options, "bytes", 182)
|
|
192
|
+
require(bytes.size <= mtu - 3)
|
|
193
|
+
responsePrefix = byteArray(options, "responsePrefix", 16)
|
|
194
|
+
exchangeDone = done
|
|
195
|
+
exchangeTimer = timer(options.optLong("timeoutMs", 10000).coerceIn(1000, 20000)) { finishExchange(failure("TIMEOUT", "Bluetooth device did not answer")) }
|
|
196
|
+
write.writeType = if (write.properties and BluetoothGattCharacteristic.PROPERTY_WRITE != 0) BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT else BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
|
|
197
|
+
write.value = bytes
|
|
198
|
+
if (!peer.writeCharacteristic(write)) finishExchange(failure("WRITE_FAILED", "Bluetooth write queue is busy"))
|
|
199
|
+
}
|
|
200
|
+
private fun finishExchange(result: CraftNativeCapabilityResult) {
|
|
201
|
+
exchangeTimer?.let(handler::removeCallbacks); exchangeTimer = null; responsePrefix = byteArrayOf()
|
|
202
|
+
val done = exchangeDone; exchangeDone = null; done?.invoke(result)
|
|
203
|
+
}
|
|
204
|
+
fun disconnect() {
|
|
205
|
+
stopScan(failure("CANCELLED", "Bluetooth scan cancelled"))
|
|
206
|
+
finishConnect(failure("CANCELLED", "Bluetooth connection cancelled"))
|
|
207
|
+
finishExchange(failure("DISCONNECTED", "Bluetooth device disconnected"))
|
|
208
|
+
val old = gatt; gatt = null; connected = false; writer = null; notifier = null; mtu = 23
|
|
209
|
+
runCatching { old?.disconnect(); old?.close() }
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -63,6 +63,8 @@ internal class CraftNativeCapabilities(
|
|
|
63
63
|
) {
|
|
64
64
|
/** The screen's activity; null in a background refresh, where nothing can be shown. */
|
|
65
65
|
private val activity: MainActivity? get() = context as? MainActivity
|
|
66
|
+
private val bluetooth by lazy { CraftBluetooth(context) }
|
|
67
|
+
fun onBluetoothPermissions(requestCode: Int) { if (config.optBoolean("enableBluetooth")) bluetooth.onPermissions(requestCode) }
|
|
66
68
|
private val mainHandler = android.os.Handler(android.os.Looper.getMainLooper())
|
|
67
69
|
private val storage = context.getSharedPreferences("craft-native-storage", Context.MODE_PRIVATE)
|
|
68
70
|
// The typed native renderer has no CraftNative Zig holder. Keep its
|
|
@@ -111,6 +113,7 @@ internal class CraftNativeCapabilities(
|
|
|
111
113
|
|
|
112
114
|
fun names(): JSONArray = JSONArray().apply {
|
|
113
115
|
listOf("device", "clipboard", "haptics", "storage", "lifecycle", "fetch", "files").forEach(::put)
|
|
116
|
+
if (config.optBoolean("enableBluetooth")) put("bluetooth")
|
|
114
117
|
if (config.optBoolean("enableBiometric")) put("biometric")
|
|
115
118
|
if (config.optBoolean("enableSecureStorage")) put("secureStorage")
|
|
116
119
|
if (config.optBoolean("enableLocalDatabase")) put("database")
|
|
@@ -132,6 +135,10 @@ internal class CraftNativeCapabilities(
|
|
|
132
135
|
return
|
|
133
136
|
}
|
|
134
137
|
when (module) {
|
|
138
|
+
"Bluetooth" -> {
|
|
139
|
+
if (!config.optBoolean("enableBluetooth")) complete(requestToken, failure("CAPABILITY_DISABLED", "Bluetooth is disabled"), completion)
|
|
140
|
+
else bluetooth.perform(method, args) { complete(requestToken, it, completion) }
|
|
141
|
+
}
|
|
135
142
|
"Storage" -> complete(requestToken, storage(method, args), completion)
|
|
136
143
|
"SecureStorage" -> {
|
|
137
144
|
if (!config.optBoolean("enableSecureStorage")) complete(requestToken, failure("CAPABILITY_DISABLED", "Secure storage is disabled"), completion)
|
|
@@ -177,6 +184,7 @@ internal class CraftNativeCapabilities(
|
|
|
177
184
|
}
|
|
178
185
|
|
|
179
186
|
fun close() {
|
|
187
|
+
if (config.optBoolean("enableBluetooth")) bluetooth.disconnect()
|
|
180
188
|
databaseExecutor.shutdownNow()
|
|
181
189
|
networkExecutor.shutdownNow()
|
|
182
190
|
fetchConnections.values.forEach(HttpURLConnection::disconnect)
|
|
@@ -856,15 +856,18 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
856
856
|
requestLayout()
|
|
857
857
|
}
|
|
858
858
|
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
859
|
+
companion object {
|
|
860
|
+
/** Clears the memoized measurements of every flex host above `start`. The activity calls it too (an image whose source changed). */
|
|
861
|
+
fun invalidateMeasurementAncestors(start: ViewParent?) {
|
|
862
|
+
var ancestor = start
|
|
863
|
+
while (ancestor != null) {
|
|
864
|
+
when (ancestor) {
|
|
865
|
+
is CraftNativeFlexLayout -> ancestor.invalidateMeasurements()
|
|
866
|
+
is CraftNativeKeyboardAvoidingView -> ancestor.content.invalidateMeasurements()
|
|
867
|
+
is CraftNativeScrollView -> ancestor.invalidateContentLayout()
|
|
868
|
+
}
|
|
869
|
+
ancestor = (ancestor as? View)?.parent
|
|
866
870
|
}
|
|
867
|
-
ancestor = (ancestor as? View)?.parent
|
|
868
871
|
}
|
|
869
872
|
}
|
|
870
873
|
|
|
@@ -4086,7 +4089,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4086
4089
|
includeFontPadding = false
|
|
4087
4090
|
configureText(this, craftNativeOptionalString(props.opt("title")) ?: childText(children), style)
|
|
4088
4091
|
val linkColor = craftNativeOptionalString(props.opt("color")) ?: craftNativeOptionalString(style.opt("color"))
|
|
4089
|
-
setTextColor(color(linkColor, Color.rgb(33, 150, 243)))
|
|
4092
|
+
setTextColor(color(linkColor.orEmpty(), Color.rgb(33, 150, 243)))
|
|
4090
4093
|
paintFlags = paintFlags or Paint.UNDERLINE_TEXT_FLAG
|
|
4091
4094
|
isEnabled = !props.optBoolean("disabled", false)
|
|
4092
4095
|
isClickable = true
|
|
@@ -4369,7 +4372,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4369
4372
|
} else {
|
|
4370
4373
|
craftNativeOptionalString(events.opt("onDismiss"))
|
|
4371
4374
|
}
|
|
4372
|
-
if (lifecycleHandler.
|
|
4375
|
+
if (!lifecycleHandler.isNullOrBlank() && !suppressDismiss) {
|
|
4373
4376
|
// Deliver lifecycle callbacks after reconciliation returns to
|
|
4374
4377
|
// the queue; the handler may immediately schedule another
|
|
4375
4378
|
// render against this native view.
|
|
@@ -5452,7 +5455,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
5452
5455
|
private fun imageDidChange(view: ImageView) {
|
|
5453
5456
|
view.invalidate()
|
|
5454
5457
|
view.requestLayout()
|
|
5455
|
-
invalidateMeasurementAncestors(view.parent)
|
|
5458
|
+
CraftNativeFlexLayout.invalidateMeasurementAncestors(view.parent)
|
|
5456
5459
|
}
|
|
5457
5460
|
|
|
5458
5461
|
private fun emitImageEvent(screen: Screen, view: ImageView, handler: String?, nativeEvent: JSONObject = JSONObject()) {
|
|
@@ -5774,6 +5777,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
5774
5777
|
|
|
5775
5778
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
|
5776
5779
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
|
5780
|
+
if (::capabilities.isInitialized) capabilities.onBluetoothPermissions(requestCode)
|
|
5777
5781
|
if (::services.isInitialized) services.onRequestPermissionsResult(requestCode)
|
|
5778
5782
|
}
|
|
5779
5783
|
|