craft-native 0.0.237 → 0.0.241
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -0
- package/dist/android/src/emulator.d.ts +23 -0
- package/dist/android/src/index.d.ts +24 -2
- package/dist/android/src/index.js +362 -103
- package/dist/android/src/native-map.d.ts +7 -0
- package/dist/android/templates/CraftBridge.kt.template +67 -55
- package/dist/android/templates/CraftNative.kt.template +27 -0
- package/dist/android/templates/CraftNativeCapabilities.kt.template +36 -5
- package/dist/android/templates/CraftNativeMapGoogle.kt.template +440 -0
- package/dist/android/templates/CraftNativeMapLibre.kt.template +438 -0
- package/dist/android/templates/CraftNativeMessagingService.kt.template +3 -1
- package/dist/android/templates/CraftNativeNotificationReceiver.kt.template +2 -1
- package/dist/android/templates/CraftNativeServices.kt.template +45 -35
- package/dist/android/templates/CraftNativeShell.kt.template +103 -20
- package/dist/android/templates/LocationRecordingService.kt.template +255 -47
- package/dist/android/templates/MainActivityNative.kt.template +7 -428
- package/dist/android/templates/build.gradle.kts.app.template +1 -1
- package/dist/android/templates/build.gradle.kts.project.template +1 -1
- package/dist/android-adb.d.ts +2 -0
- package/dist/api/index.d.ts +1 -1
- package/dist/api/mobile.d.ts +49 -14
- package/dist/browser/storage.d.ts +3 -0
- package/dist/browser.js +163 -51
- package/dist/cli.js +1 -1
- package/dist/index.cjs +126 -12
- package/dist/index.js +126 -12
- package/dist/ios/src/index.d.ts +5 -0
- package/dist/ios/src/index.js +17 -1
- package/dist/ios/src/testing.d.ts +16 -2
- package/dist/ios/templates/CraftApp.swift +418 -93
- package/dist/ios/templates/CraftHeartRate.swift +141 -44
- package/dist/ios/templates/CraftNativeActions.swift +46 -48
- package/dist/ios/templates/CraftNativeScreen.swift +82 -17
- package/dist/ios/templates/CraftPersistentStorage.swift +99 -0
- package/dist/ios/templates/CraftRecordingArchive.swift +85 -0
- package/dist/ios/templates/testing/DriverTests.swift.template +45 -1
- package/dist/macos/templates/native_layout.zig +41 -5
- package/dist/mobile-testing.js +30 -4
- package/dist/mobile.js +169 -17
- package/package.json +1 -1
|
@@ -1,9 +1,51 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../android/src/index.ts
|
|
3
|
-
import { cpSync, existsSync, mkdirSync, readFileSync as readFileSync2, rmSync, statSync, writeFileSync } from "fs";
|
|
4
|
-
import { dirname as dirname2, extname, isAbsolute, join as
|
|
3
|
+
import { cpSync, existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, statSync, writeFileSync } from "fs";
|
|
4
|
+
import { dirname as dirname2, extname, isAbsolute as isAbsolute2, join as join3, relative, resolve as resolve2, sep } from "path";
|
|
5
5
|
var {$ } = globalThis.Bun;
|
|
6
6
|
|
|
7
|
+
// ../android/src/native-map.ts
|
|
8
|
+
function androidMapHost(provider = "google") {
|
|
9
|
+
if (provider === "google")
|
|
10
|
+
return {
|
|
11
|
+
template: "CraftNativeMapGoogle.kt.template",
|
|
12
|
+
imports: `import com.google.android.gms.maps.CameraUpdateFactory
|
|
13
|
+
import com.google.android.gms.maps.GoogleMap
|
|
14
|
+
import com.google.android.gms.maps.MapView
|
|
15
|
+
import com.google.android.gms.maps.OnMapReadyCallback
|
|
16
|
+
import com.google.android.gms.maps.model.BitmapDescriptorFactory
|
|
17
|
+
import com.google.android.gms.maps.model.LatLng
|
|
18
|
+
import com.google.android.gms.maps.model.LatLngBounds
|
|
19
|
+
import com.google.android.gms.maps.model.Marker
|
|
20
|
+
import com.google.android.gms.maps.model.MarkerOptions
|
|
21
|
+
import com.google.android.gms.maps.model.Polyline
|
|
22
|
+
import com.google.android.gms.maps.model.PolylineOptions`,
|
|
23
|
+
dependency: "com.google.android.gms:play-services-maps:18.2.0"
|
|
24
|
+
};
|
|
25
|
+
if (provider === "maplibre")
|
|
26
|
+
return {
|
|
27
|
+
template: "CraftNativeMapLibre.kt.template",
|
|
28
|
+
imports: `import org.maplibre.android.MapLibre
|
|
29
|
+
import org.maplibre.android.camera.CameraPosition
|
|
30
|
+
import org.maplibre.android.camera.CameraUpdateFactory
|
|
31
|
+
import org.maplibre.android.geometry.LatLng
|
|
32
|
+
import org.maplibre.android.geometry.LatLngBounds
|
|
33
|
+
import org.maplibre.android.location.LocationComponentActivationOptions
|
|
34
|
+
import org.maplibre.android.maps.MapLibreMap
|
|
35
|
+
import org.maplibre.android.maps.MapView
|
|
36
|
+
import org.maplibre.android.maps.OnMapReadyCallback
|
|
37
|
+
import org.maplibre.android.maps.Style
|
|
38
|
+
import org.maplibre.android.style.expressions.Expression
|
|
39
|
+
import org.maplibre.android.style.layers.CircleLayer
|
|
40
|
+
import org.maplibre.android.style.layers.LineLayer
|
|
41
|
+
import org.maplibre.android.style.layers.SymbolLayer
|
|
42
|
+
import org.maplibre.android.style.layers.PropertyFactory.*
|
|
43
|
+
import org.maplibre.android.style.sources.GeoJsonSource`,
|
|
44
|
+
dependency: "org.maplibre.gl:android-sdk:13.7.1"
|
|
45
|
+
};
|
|
46
|
+
throw new Error(`Unknown Android native map provider: ${provider}. Expected google or maplibre.`);
|
|
47
|
+
}
|
|
48
|
+
|
|
7
49
|
// ../android/src/promise-runtime.ts
|
|
8
50
|
var ANDROID_PROMISE_RUNTIME = `
|
|
9
51
|
if (window.__craftRejectPendingPromises) {
|
|
@@ -132,10 +174,192 @@ tasks.register('runFixture', JavaExec) {
|
|
|
132
174
|
await rm(directory, { recursive: true, force: true });
|
|
133
175
|
}
|
|
134
176
|
}
|
|
177
|
+
// ../android/src/emulator.ts
|
|
178
|
+
import { appendFileSync, closeSync, mkdirSync, mkdtempSync, openSync, rmSync } from "fs";
|
|
179
|
+
import { availableParallelism, cpus, tmpdir as tmpdir2, totalmem } from "os";
|
|
180
|
+
import { basename, isAbsolute, join as join2, resolve } from "path";
|
|
181
|
+
async function createAndroidEmulator(options) {
|
|
182
|
+
if (!["darwin", "linux"].includes(process.platform))
|
|
183
|
+
throw new Error("Android emulator sessions require macOS or Linux");
|
|
184
|
+
if (!["hardware", "software"].includes(options.acceleration))
|
|
185
|
+
throw new Error("Select hardware or software Android emulation explicitly");
|
|
186
|
+
if (![options.sdkmanager, options.avdmanager].every(isAbsolute))
|
|
187
|
+
throw new Error("Select absolute Android command-line tool paths");
|
|
188
|
+
const abi = process.arch === "arm64" ? "arm64-v8a" : process.arch === "x64" ? "x86_64" : undefined;
|
|
189
|
+
if (!abi || !/^system-images;android-\d+;[^;\s]+;[^;\s]+$/.test(options.image) || !options.image.endsWith(`;${abi}`))
|
|
190
|
+
throw new Error("The Android system image must match the host architecture");
|
|
191
|
+
if (options.acceleration === "software" && process.arch !== "x64")
|
|
192
|
+
throw new Error("Android software emulation requires an x86_64 host and system image");
|
|
193
|
+
const bootTimeoutMs = options.bootTimeoutMs ?? 600000;
|
|
194
|
+
const maximumBootTimeoutMs = options.acceleration === "software" ? 1200000 : 600000;
|
|
195
|
+
if (!Number.isSafeInteger(bootTimeoutMs) || bootTimeoutMs < 1000 || bootTimeoutMs > maximumBootTimeoutMs)
|
|
196
|
+
throw new Error(`Android bootTimeoutMs must be an integer between 1000 and ${maximumBootTimeoutMs} milliseconds`);
|
|
197
|
+
const port = options.port ?? 5558;
|
|
198
|
+
if (!Number.isInteger(port) || port < 5554 || port > 5682 || port % 2 !== 0)
|
|
199
|
+
throw new Error("Android console ports must be even and between 5554 and 5682");
|
|
200
|
+
const sdk = resolve(options.sdkRoot);
|
|
201
|
+
const artifacts = resolve(options.artifactsDirectory);
|
|
202
|
+
const temporary = options.temporaryDirectory ?? tmpdir2();
|
|
203
|
+
mkdirSync(artifacts, { recursive: true });
|
|
204
|
+
mkdirSync(temporary, { recursive: true });
|
|
205
|
+
const owned = mkdtempSync(join2(temporary, "craft-android-emulator-"));
|
|
206
|
+
const name = basename(owned);
|
|
207
|
+
const serial = `emulator-${port}`;
|
|
208
|
+
const env = {
|
|
209
|
+
...process.env,
|
|
210
|
+
...options.env,
|
|
211
|
+
ANDROID_HOME: sdk,
|
|
212
|
+
ANDROID_SDK_ROOT: sdk,
|
|
213
|
+
ANDROID_SDK_HOME: owned,
|
|
214
|
+
ANDROID_USER_HOME: join2(owned, ".android"),
|
|
215
|
+
ANDROID_EMULATOR_HOME: join2(owned, ".android"),
|
|
216
|
+
ANDROID_AVD_HOME: join2(owned, "avd"),
|
|
217
|
+
ANDROID_SERIAL: serial,
|
|
218
|
+
AVDMANAGER_OPTS: `'-Dcom.android.sdkmanager.toolsdir=${join2(sdk, "cmdline-tools/latest")}'`
|
|
219
|
+
};
|
|
220
|
+
const adb = join2(sdk, "platform-tools/adb");
|
|
221
|
+
const executable = join2(sdk, "emulator/emulator");
|
|
222
|
+
let child;
|
|
223
|
+
let log;
|
|
224
|
+
let created = false;
|
|
225
|
+
let sequence = 0;
|
|
226
|
+
let disposal;
|
|
227
|
+
let bootStartedAt = 0;
|
|
228
|
+
let cpuStart = 0;
|
|
229
|
+
let idleStart = 0;
|
|
230
|
+
let identity;
|
|
231
|
+
async function recordBootResult(status, endedAt = Date.now()) {
|
|
232
|
+
if (!identity)
|
|
233
|
+
return;
|
|
234
|
+
const cpuEnd = cpus();
|
|
235
|
+
const totalDeltaMs = cpuEnd.reduce((total, cpu) => total + Object.values(cpu.times).reduce((sum, value) => sum + value, 0), 0) - cpuStart;
|
|
236
|
+
const idleDeltaMs = cpuEnd.reduce((total, cpu) => total + cpu.times.idle, 0) - idleStart;
|
|
237
|
+
await Bun.write(join2(artifacts, "android-emulator.json"), JSON.stringify({
|
|
238
|
+
...identity,
|
|
239
|
+
status,
|
|
240
|
+
[status === "booted" ? "bootedAt" : "failedAt"]: new Date(endedAt).toISOString(),
|
|
241
|
+
bootElapsedMs: endedAt - bootStartedAt,
|
|
242
|
+
hostCpuInterval: { totalDeltaMs, idleDeltaMs, busyDeltaMs: totalDeltaMs - idleDeltaMs }
|
|
243
|
+
}, null, 2) + `
|
|
244
|
+
`);
|
|
245
|
+
}
|
|
246
|
+
async function run(args, input, timeout = 900000, allowFailure = false) {
|
|
247
|
+
const startedAt = new Date().toISOString();
|
|
248
|
+
const command = Bun.spawn(args, { env, stdin: input === undefined ? "ignore" : new TextEncoder().encode(input), stdout: "pipe", stderr: "pipe", signal: AbortSignal.timeout(timeout) });
|
|
249
|
+
const [code, stdout, stderr] = await Promise.all([command.exited, new Response(command.stdout).text(), new Response(command.stderr).text()]);
|
|
250
|
+
appendFileSync(join2(artifacts, "device-commands.jsonl"), JSON.stringify({ sequence: ++sequence, startedAt, finishedAt: new Date().toISOString(), args, code, stdout, stderr }) + `
|
|
251
|
+
`);
|
|
252
|
+
if (code !== 0 && !allowFailure)
|
|
253
|
+
throw new Error(`${basename(args[0])} failed with exit ${code}; inspect ${artifacts}`);
|
|
254
|
+
return { code, stdout };
|
|
255
|
+
}
|
|
256
|
+
const dispose = () => disposal ??= (async () => {
|
|
257
|
+
try {
|
|
258
|
+
if (child && child.exitCode === null) {
|
|
259
|
+
child.kill("SIGTERM");
|
|
260
|
+
await Promise.race([child.exited, Bun.sleep(1e4)]);
|
|
261
|
+
if (child.exitCode === null)
|
|
262
|
+
child.kill("SIGKILL");
|
|
263
|
+
await child.exited;
|
|
264
|
+
}
|
|
265
|
+
if (created)
|
|
266
|
+
await run([options.avdmanager, "delete", "avd", "--name", name], undefined, 30000);
|
|
267
|
+
} finally {
|
|
268
|
+
if (log !== undefined)
|
|
269
|
+
closeSync(log);
|
|
270
|
+
rmSync(owned, { recursive: true, force: true });
|
|
271
|
+
}
|
|
272
|
+
})();
|
|
273
|
+
try {
|
|
274
|
+
mkdirSync(env.ANDROID_AVD_HOME, { recursive: true });
|
|
275
|
+
mkdirSync(env.ANDROID_USER_HOME, { recursive: true });
|
|
276
|
+
if (options.acceptLicenses)
|
|
277
|
+
await run([options.sdkmanager, `--sdk_root=${sdk}`, "--licenses"], `y
|
|
278
|
+
`.repeat(100));
|
|
279
|
+
await run([options.sdkmanager, `--sdk_root=${sdk}`, "platform-tools", "emulator"], options.acceptLicenses ? `y
|
|
280
|
+
`.repeat(100) : undefined);
|
|
281
|
+
const acceleration = await run([executable, "-accel-check"], undefined, 30000, true);
|
|
282
|
+
if (options.acceleration === "hardware" && acceleration.code !== 0)
|
|
283
|
+
throw new Error(`Android hardware acceleration is unavailable; inspect ${artifacts}`);
|
|
284
|
+
await run([options.sdkmanager, `--sdk_root=${sdk}`, options.image], options.acceptLicenses ? `y
|
|
285
|
+
`.repeat(100) : undefined);
|
|
286
|
+
const devices = (await run([adb, "devices"], undefined, 1e4)).stdout;
|
|
287
|
+
if (devices.split(`
|
|
288
|
+
`).some((line) => line.startsWith(`${serial} `)))
|
|
289
|
+
throw new Error(`${serial} is already in use`);
|
|
290
|
+
await run([options.avdmanager, "create", "avd", "--name", name, "--package", options.image, "--device", "pixel_6"], `no
|
|
291
|
+
`);
|
|
292
|
+
created = true;
|
|
293
|
+
log = openSync(join2(artifacts, "emulator.log"), "w");
|
|
294
|
+
bootStartedAt = Date.now();
|
|
295
|
+
const hostCpus = cpus();
|
|
296
|
+
cpuStart = hostCpus.reduce((total, cpu) => total + Object.values(cpu.times).reduce((sum, value) => sum + value, 0), 0);
|
|
297
|
+
idleStart = hostCpus.reduce((total, cpu) => total + cpu.times.idle, 0);
|
|
298
|
+
child = Bun.spawn([executable, "-avd", name, "-port", String(port), "-no-window", "-no-snapshot", "-no-audio", "-no-boot-anim", "-gpu", "software", "-memory", "2048", "-cores", "2", "-accel", options.acceleration === "software" ? "off" : "on"], { env, stdout: log, stderr: log });
|
|
299
|
+
identity = {
|
|
300
|
+
image: options.image,
|
|
301
|
+
acceleration: options.acceleration,
|
|
302
|
+
accelerationProbeExitCode: acceleration.code,
|
|
303
|
+
bootTimeoutMs,
|
|
304
|
+
bootStartedAt: new Date(bootStartedAt).toISOString(),
|
|
305
|
+
host: { cpuModel: hostCpus[0]?.model, logicalCpus: hostCpus.length, availableParallelism: availableParallelism(), memoryBytes: totalmem() },
|
|
306
|
+
virtualCpus: 2,
|
|
307
|
+
memoryMb: 2048,
|
|
308
|
+
serial,
|
|
309
|
+
avdDirectory: env.ANDROID_AVD_HOME,
|
|
310
|
+
emulatorPid: child.pid
|
|
311
|
+
};
|
|
312
|
+
await Bun.write(join2(artifacts, "android-emulator.json"), JSON.stringify(identity, null, 2) + `
|
|
313
|
+
`);
|
|
314
|
+
const deadline = bootStartedAt + bootTimeoutMs;
|
|
315
|
+
const probeTimeout = () => Math.max(1, Math.min(1e4, deadline - Date.now()));
|
|
316
|
+
while (Date.now() < deadline && child.exitCode === null) {
|
|
317
|
+
let booted = false;
|
|
318
|
+
const connection = await run([adb, "devices"], undefined, probeTimeout(), true);
|
|
319
|
+
if (connection.code === 0 && connection.stdout.includes(`${serial} device`)) {
|
|
320
|
+
const completion = await run([adb, "-s", serial, "shell", "getprop", "sys.boot_completed"], undefined, probeTimeout(), true);
|
|
321
|
+
if (completion.code === 0 && completion.stdout.trim() === "1") {
|
|
322
|
+
const settings = await run([adb, "-s", serial, "shell", "settings", "get", "global", "animator_duration_scale"], undefined, probeTimeout(), true);
|
|
323
|
+
const packages = await run([adb, "-s", serial, "shell", "pm", "path", "android"], undefined, probeTimeout(), true);
|
|
324
|
+
const systemUi = await run([adb, "-s", serial, "shell", "pidof", "com.android.systemui"], undefined, probeTimeout(), true);
|
|
325
|
+
const events = await run([adb, "-s", serial, "logcat", "-b", "events", "-d", "-t", "1"], undefined, probeTimeout(), true);
|
|
326
|
+
booted = settings.code === 0 && packages.code === 0 && systemUi.code === 0 && events.code === 0 && /^package:\/\S+/m.test(packages.stdout) && /^\d+(?:\s+\d+)*$/.test(systemUi.stdout.trim());
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const checkedAt = Date.now();
|
|
330
|
+
if (booted && checkedAt <= deadline) {
|
|
331
|
+
await recordBootResult("booted", checkedAt);
|
|
332
|
+
return { serial, adb, env, dispose };
|
|
333
|
+
}
|
|
334
|
+
await Bun.sleep(Math.min(2000, Math.max(0, deadline - Date.now())));
|
|
335
|
+
}
|
|
336
|
+
throw new Error(`The owned Android emulator did not boot within ${bootTimeoutMs}ms; inspect ${artifacts}`);
|
|
337
|
+
} catch (error) {
|
|
338
|
+
if (child) {
|
|
339
|
+
try {
|
|
340
|
+
await recordBootResult("failed");
|
|
341
|
+
} catch (diagnostic) {
|
|
342
|
+
console.error(`Could not retain emulator boot timing: ${diagnostic}`);
|
|
343
|
+
}
|
|
344
|
+
for (const args of [["logcat", "-d", "-b", "all"], ["shell", "getprop"], ["shell", "dumpsys", "activity", "processes"]]) {
|
|
345
|
+
try {
|
|
346
|
+
await run([adb, "-s", serial, ...args], undefined, 1e4, true);
|
|
347
|
+
} catch {}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
try {
|
|
351
|
+
await dispose();
|
|
352
|
+
} catch (cleanup) {
|
|
353
|
+
throw new AggregateError([error, cleanup], "Android emulator setup and cleanup failed");
|
|
354
|
+
}
|
|
355
|
+
throw error;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
135
358
|
|
|
136
359
|
// ../android/src/index.ts
|
|
360
|
+
var nativeMapProviders = ["google", "maplibre"];
|
|
137
361
|
function syncNativeFonts(fonts, output) {
|
|
138
|
-
const destination =
|
|
362
|
+
const destination = join3(output, "app/src/main/assets/NativeFonts");
|
|
139
363
|
const names = new Set;
|
|
140
364
|
const files = Object.entries(fonts ?? {}).map(([family, entry]) => {
|
|
141
365
|
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(family))
|
|
@@ -143,7 +367,7 @@ function syncNativeFonts(fonts, output) {
|
|
|
143
367
|
if (names.has(family.toLowerCase()))
|
|
144
368
|
throw new Error(`Duplicate native font name: ${family}`);
|
|
145
369
|
names.add(family.toLowerCase());
|
|
146
|
-
if ([entry.src, entry.license].some((file) => file &&
|
|
370
|
+
if ([entry.src, entry.license].some((file) => file && resolve2(file).startsWith(resolve2(destination) + sep)))
|
|
147
371
|
throw new Error("Native font sources must be outside the generated NativeFonts directory");
|
|
148
372
|
const extension = extname(entry.src).toLowerCase();
|
|
149
373
|
if (![".ttf", ".otf"].includes(extension) || !existsSync(entry.src) || !statSync(entry.src).isFile())
|
|
@@ -152,23 +376,23 @@ function syncNativeFonts(fonts, output) {
|
|
|
152
376
|
throw new Error(`Native font license not found: ${entry.license}`);
|
|
153
377
|
return { family, ...entry, filename: `${family}${extension}` };
|
|
154
378
|
});
|
|
155
|
-
|
|
379
|
+
rmSync2(destination, { recursive: true, force: true });
|
|
156
380
|
if (!files.length)
|
|
157
381
|
return [];
|
|
158
|
-
|
|
382
|
+
mkdirSync2(destination, { recursive: true });
|
|
159
383
|
for (const file of files) {
|
|
160
|
-
cpSync(file.src,
|
|
384
|
+
cpSync(file.src, join3(destination, file.filename));
|
|
161
385
|
if (file.license)
|
|
162
|
-
cpSync(file.license,
|
|
386
|
+
cpSync(file.license, join3(destination, `${file.family}.LICENSE.txt`));
|
|
163
387
|
}
|
|
164
388
|
return files.map((file) => `NativeFonts/${file.filename}`);
|
|
165
389
|
}
|
|
166
|
-
var TEMPLATES_DIR =
|
|
390
|
+
var TEMPLATES_DIR = join3(dirname2(import.meta.dir), "templates");
|
|
167
391
|
function resolveCraftAndroidVersion(startDir = import.meta.dir) {
|
|
168
|
-
let directory =
|
|
392
|
+
let directory = resolve2(startDir);
|
|
169
393
|
while (true) {
|
|
170
394
|
try {
|
|
171
|
-
const pkg = JSON.parse(readFileSync2(
|
|
395
|
+
const pkg = JSON.parse(readFileSync2(join3(directory, "package.json"), "utf8"));
|
|
172
396
|
if ((pkg.name === "@craft-native/android" || pkg.name === "craft-native") && typeof pkg.version === "string" && pkg.version)
|
|
173
397
|
return pkg.version;
|
|
174
398
|
} catch {}
|
|
@@ -305,7 +529,7 @@ function requireRegularFile(path, label) {
|
|
|
305
529
|
}
|
|
306
530
|
function containsPath(parent, candidate) {
|
|
307
531
|
const relativePath = relative(parent, candidate);
|
|
308
|
-
return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !
|
|
532
|
+
return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute2(relativePath);
|
|
309
533
|
}
|
|
310
534
|
function validateGoogleServicesFile(path, packageName) {
|
|
311
535
|
requireRegularFile(path, "Google services file");
|
|
@@ -368,6 +592,7 @@ function normalizeAndroidNetworkConfig(config) {
|
|
|
368
592
|
config.trustedOrigins = [...new Set(trustedOrigins)];
|
|
369
593
|
}
|
|
370
594
|
function validateAndroidConfig(config) {
|
|
595
|
+
androidMapHost(config.mapProvider);
|
|
371
596
|
if (!config.appName.trim())
|
|
372
597
|
throw new Error("Android app name must not be empty");
|
|
373
598
|
if (config.renderer !== undefined && config.renderer !== "web" && config.renderer !== "native") {
|
|
@@ -498,36 +723,37 @@ function validateNativeRouteConfig(config) {
|
|
|
498
723
|
}
|
|
499
724
|
}
|
|
500
725
|
function writeAndroidConfig(output, config) {
|
|
501
|
-
writeFileSync(
|
|
726
|
+
writeFileSync(join3(output, "craft.config.json"), JSON.stringify(config, null, 2));
|
|
502
727
|
const runtimeConfig = { ...config };
|
|
503
728
|
delete runtimeConfig.appIconPath;
|
|
729
|
+
delete runtimeConfig.notificationIconPath;
|
|
504
730
|
delete runtimeConfig.googleServicesFile;
|
|
505
|
-
writeFileSync(
|
|
731
|
+
writeFileSync(join3(output, "app/src/main/assets/craft.config.json"), JSON.stringify(runtimeConfig, null, 2));
|
|
506
732
|
}
|
|
507
733
|
function syncAndroidWebAssets(source, output) {
|
|
508
|
-
const sourcePath =
|
|
734
|
+
const sourcePath = resolve2(source);
|
|
509
735
|
if (!existsSync(sourcePath))
|
|
510
736
|
throw new Error(`Web asset path not found: ${source}`);
|
|
511
737
|
const sourceStat = statSync(sourcePath);
|
|
512
738
|
if (!sourceStat.isDirectory() && !sourceStat.isFile()) {
|
|
513
739
|
throw new Error(`Web asset path must be a file or directory: ${source}`);
|
|
514
740
|
}
|
|
515
|
-
const assetsDir =
|
|
741
|
+
const assetsDir = resolve2(output, "app/src/main/assets");
|
|
516
742
|
if (containsPath(sourcePath, assetsDir) || containsPath(assetsDir, sourcePath)) {
|
|
517
743
|
throw new Error(`Web asset source must not overlap generated asset directory: ${source}`);
|
|
518
744
|
}
|
|
519
745
|
if (sourceStat.isDirectory()) {
|
|
520
|
-
requireRegularFile(
|
|
746
|
+
requireRegularFile(join3(sourcePath, "index.html"), "Web asset directory entry point");
|
|
521
747
|
}
|
|
522
|
-
const configPath =
|
|
748
|
+
const configPath = join3(assetsDir, "craft.config.json");
|
|
523
749
|
const config = existsSync(configPath) ? readFileSync2(configPath) : undefined;
|
|
524
|
-
|
|
525
|
-
|
|
750
|
+
rmSync2(assetsDir, { recursive: true, force: true });
|
|
751
|
+
mkdirSync2(assetsDir, { recursive: true });
|
|
526
752
|
if (sourceStat.isDirectory())
|
|
527
753
|
cpSync(sourcePath, assetsDir, { recursive: true });
|
|
528
754
|
else
|
|
529
|
-
cpSync(sourcePath,
|
|
530
|
-
if (!existsSync(
|
|
755
|
+
cpSync(sourcePath, join3(assetsDir, "index.html"));
|
|
756
|
+
if (!existsSync(join3(assetsDir, "index.html")))
|
|
531
757
|
throw new Error(`Web asset directory must contain index.html: ${source}`);
|
|
532
758
|
if (config)
|
|
533
759
|
writeFileSync(configPath, config);
|
|
@@ -571,10 +797,10 @@ function renderAndroidPermissions(config) {
|
|
|
571
797
|
`);
|
|
572
798
|
}
|
|
573
799
|
function writeNativeShellSources(output, packagePath, packageName, config) {
|
|
574
|
-
const javaDir =
|
|
800
|
+
const javaDir = join3(output, "app/src/main/java", packagePath);
|
|
575
801
|
const write = (template, file, replace = (source) => source) => {
|
|
576
|
-
const source = readFileSync2(
|
|
577
|
-
writeFileSync(
|
|
802
|
+
const source = readFileSync2(join3(TEMPLATES_DIR, template), "utf-8").replace(/\{\{PACKAGE_NAME\}\}/g, packageName);
|
|
803
|
+
writeFileSync(join3(javaDir, file), replace(source));
|
|
578
804
|
};
|
|
579
805
|
write("CraftBluetooth.kt.template", "CraftBluetooth.kt");
|
|
580
806
|
write("CraftNativeShell.kt.template", "CraftNativeShell.kt");
|
|
@@ -596,9 +822,9 @@ function writeNativeShellSources(output, packagePath, packageName, config) {
|
|
|
596
822
|
if (config.enablePushNotifications)
|
|
597
823
|
write("CraftNativeMessagingService.kt.template", "CraftNativeMessagingService.kt");
|
|
598
824
|
else
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
writeFileSync(
|
|
825
|
+
rmSync2(join3(javaDir, "CraftNativeMessagingService.kt"), { force: true });
|
|
826
|
+
mkdirSync2(join3(output, "app/src/main/res/xml"), { recursive: true });
|
|
827
|
+
writeFileSync(join3(output, "app/src/main/res/xml/craft_file_paths.xml"), `<?xml version="1.0" encoding="utf-8"?>
|
|
602
828
|
<paths>
|
|
603
829
|
<cache-path name="craft-files" path="craft-files/" />
|
|
604
830
|
<files-path name="craft-media" path="craft-media/" />
|
|
@@ -621,7 +847,9 @@ function renderNativeShellManifest(config) {
|
|
|
621
847
|
<intent-filter>
|
|
622
848
|
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
|
623
849
|
</intent-filter>
|
|
624
|
-
</service
|
|
850
|
+
</service>
|
|
851
|
+
<meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/craft_notification_icon" />${config.notificationColor ? `
|
|
852
|
+
<meta-data android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/craft_notification_color" />` : ""}`);
|
|
625
853
|
}
|
|
626
854
|
if (config.enableHealthConnect) {
|
|
627
855
|
parts.push(` <activity-alias
|
|
@@ -677,7 +905,7 @@ function resolveRuntimeDir(override) {
|
|
|
677
905
|
return dir;
|
|
678
906
|
}
|
|
679
907
|
function installRuntime(output, runtimeDir) {
|
|
680
|
-
const found = RUNTIME_ABIS.map((abi) => ({ abi, source:
|
|
908
|
+
const found = RUNTIME_ABIS.map((abi) => ({ abi, source: join3(runtimeDir, abi, "libcraft.so") })).filter((entry) => existsSync(entry.source));
|
|
681
909
|
if (found.length === 0) {
|
|
682
910
|
throw new Error(`${runtimeDir} has no <abi>/libcraft.so for any of ${RUNTIME_ABIS.join(", ")}. ` + "Run `zig build build-android-all -Doptimize=ReleaseSafe` in packages/zig and point at its zig-out/android.");
|
|
683
911
|
}
|
|
@@ -685,12 +913,12 @@ function installRuntime(output, runtimeDir) {
|
|
|
685
913
|
const missing = RUNTIME_ABIS.filter((abi) => !found.some((entry) => entry.abi === abi));
|
|
686
914
|
console.warn(` \u26A0 only ${found.map((entry) => entry.abi).join(", ")} was found; ${missing.join(", ")} is missing. ` + "The app will fall back to the Kotlin shim on those devices.");
|
|
687
915
|
}
|
|
688
|
-
const dest =
|
|
689
|
-
|
|
916
|
+
const dest = join3(output, "app/src/main/jniLibs");
|
|
917
|
+
rmSync2(dest, { force: true, recursive: true });
|
|
690
918
|
for (const { abi, source } of found) {
|
|
691
|
-
const abiDir =
|
|
692
|
-
|
|
693
|
-
cpSync(source,
|
|
919
|
+
const abiDir = join3(dest, abi);
|
|
920
|
+
mkdirSync2(abiDir, { recursive: true });
|
|
921
|
+
cpSync(source, join3(abiDir, "libcraft.so"));
|
|
694
922
|
}
|
|
695
923
|
return true;
|
|
696
924
|
}
|
|
@@ -731,48 +959,60 @@ async function init(options) {
|
|
|
731
959
|
if (config.appIconPath && ![".gif", ".jpg", ".png", ".webp"].includes(appIconExtension ?? "")) {
|
|
732
960
|
throw new Error(`Unsupported Android app icon format: ${appIconExtension || "(none)"}`);
|
|
733
961
|
}
|
|
962
|
+
if (config.notificationIconPath)
|
|
963
|
+
requireRegularFile(config.notificationIconPath, "Notification icon");
|
|
964
|
+
const notificationIconExtension = config.notificationIconPath ? extname(config.notificationIconPath).toLowerCase() : undefined;
|
|
965
|
+
if (config.notificationIconPath && ![".png", ".xml"].includes(notificationIconExtension ?? "")) {
|
|
966
|
+
throw new Error(`Unsupported Android notification icon format: ${notificationIconExtension || "(none)"} (use a vector drawable .xml or a white-on-transparent .png)`);
|
|
967
|
+
}
|
|
968
|
+
if (config.notificationIconPath && notificationIconExtension === ".xml" && !/<vector[\s>]/.test(readFileSync2(config.notificationIconPath, "utf8"))) {
|
|
969
|
+
throw new Error("Notification icon .xml must be an Android vector drawable (<vector>)");
|
|
970
|
+
}
|
|
971
|
+
if (config.notificationColor !== undefined && !/^#[0-9a-f]{6}$/i.test(config.notificationColor)) {
|
|
972
|
+
throw new Error(`notificationColor must be #RRGGBB, not ${String(config.notificationColor)}`);
|
|
973
|
+
}
|
|
734
974
|
const dirs = [
|
|
735
975
|
output,
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
976
|
+
join3(output, "app/src/main/java", packagePath),
|
|
977
|
+
join3(output, "app/src/main/res/layout"),
|
|
978
|
+
join3(output, "app/src/main/res/values"),
|
|
979
|
+
join3(output, "app/src/main/res/values-night"),
|
|
980
|
+
join3(output, "app/src/main/res/drawable"),
|
|
981
|
+
join3(output, "app/src/main/assets"),
|
|
982
|
+
join3(output, "gradle/wrapper")
|
|
743
983
|
];
|
|
744
984
|
if (existsSync(output) && !statSync(output).isDirectory()) {
|
|
745
985
|
throw new Error(`Android project output must be a directory: ${output}`);
|
|
746
986
|
}
|
|
747
987
|
for (const dir of dirs) {
|
|
748
988
|
if (!existsSync(dir)) {
|
|
749
|
-
|
|
989
|
+
mkdirSync2(dir, { recursive: true });
|
|
750
990
|
}
|
|
751
991
|
}
|
|
752
992
|
writeAndroidConfig(output, config);
|
|
753
993
|
const hasGoogleServices = Boolean(config.googleServicesFile);
|
|
754
994
|
if (config.googleServicesFile) {
|
|
755
|
-
cpSync(config.googleServicesFile,
|
|
995
|
+
cpSync(config.googleServicesFile, join3(output, "app/google-services.json"));
|
|
756
996
|
}
|
|
757
|
-
const mainActivityTemplate = readFileSync2(
|
|
758
|
-
const mainActivity = mainActivityTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{APP_NAME\}\}/g, name);
|
|
759
|
-
writeFileSync(
|
|
760
|
-
writeFileSync(
|
|
761
|
-
writeFileSync(
|
|
997
|
+
const mainActivityTemplate = readFileSync2(join3(TEMPLATES_DIR, config.renderer === "native" ? "MainActivityNative.kt.template" : "MainActivity.kt.template"), "utf-8");
|
|
998
|
+
const mainActivity = mainActivityTemplate.replace(/\{\{NATIVE_MAP_IMPORTS\}\}/g, () => androidMapHost(config.mapProvider).imports).replace(/\{\{NATIVE_MAP_HOST\}\}/g, () => readFileSync2(join3(TEMPLATES_DIR, androidMapHost(config.mapProvider).template), "utf8")).replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{APP_NAME\}\}/g, name);
|
|
999
|
+
writeFileSync(join3(output, "app/src/main/java", packagePath, "MainActivity.kt"), mainActivity);
|
|
1000
|
+
writeFileSync(join3(output, "app/src/main/java", packagePath, "CraftAppLinks.kt"), readFileSync2(join3(TEMPLATES_DIR, "CraftAppLinks.kt.template"), "utf8").replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
1001
|
+
writeFileSync(join3(output, "app/src/main/java", packagePath, "CraftCrashReporter.kt"), readFileSync2(join3(TEMPLATES_DIR, "CraftCrashReporter.kt.template"), "utf-8").replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{CRAFT_VERSION\}\}/g, CRAFT_ANDROID_VERSION));
|
|
762
1002
|
if (config.renderer === "native") {
|
|
763
|
-
const mutationTemplate = readFileSync2(
|
|
764
|
-
const flatListTemplate = readFileSync2(
|
|
765
|
-
const refreshableFlatListTemplate = readFileSync2(
|
|
766
|
-
const capabilityTemplate = readFileSync2(
|
|
767
|
-
const notificationTemplate = readFileSync2(
|
|
768
|
-
writeFileSync(
|
|
769
|
-
writeFileSync(
|
|
770
|
-
writeFileSync(
|
|
771
|
-
writeFileSync(
|
|
772
|
-
writeFileSync(
|
|
1003
|
+
const mutationTemplate = readFileSync2(join3(TEMPLATES_DIR, "CraftNativeMutation.kt.template"), "utf-8");
|
|
1004
|
+
const flatListTemplate = readFileSync2(join3(TEMPLATES_DIR, "CraftNativeFlatList.kt.template"), "utf-8");
|
|
1005
|
+
const refreshableFlatListTemplate = readFileSync2(join3(TEMPLATES_DIR, "CraftNativeRefreshableFlatList.kt.template"), "utf-8");
|
|
1006
|
+
const capabilityTemplate = readFileSync2(join3(TEMPLATES_DIR, "CraftNativeCapabilities.kt.template"), "utf-8");
|
|
1007
|
+
const notificationTemplate = readFileSync2(join3(TEMPLATES_DIR, "CraftNativeNotificationReceiver.kt.template"), "utf-8");
|
|
1008
|
+
writeFileSync(join3(output, "app/src/main/java", packagePath, "CraftNativeMutation.kt"), mutationTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
1009
|
+
writeFileSync(join3(output, "app/src/main/java", packagePath, "CraftNativeFlatList.kt"), flatListTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
1010
|
+
writeFileSync(join3(output, "app/src/main/java", packagePath, "CraftNativeRefreshableFlatList.kt"), refreshableFlatListTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
1011
|
+
writeFileSync(join3(output, "app/src/main/java", packagePath, "CraftNativeCapabilities.kt"), capabilityTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
1012
|
+
writeFileSync(join3(output, "app/src/main/java", packagePath, "CraftNativeNotificationReceiver.kt"), notificationTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
773
1013
|
writeNativeShellSources(output, packagePath, finalPackageName, config);
|
|
774
1014
|
}
|
|
775
|
-
const craftBridgeTemplate = readFileSync2(
|
|
1015
|
+
const craftBridgeTemplate = readFileSync2(join3(TEMPLATES_DIR, "CraftBridge.kt.template"), "utf-8");
|
|
776
1016
|
const craftBridge = craftBridgeTemplate.replace(/\{\{PROMISE_RUNTIME\}\}/g, () => renderAndroidPromiseRuntime(" ")).replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{ENABLE_SPEECH\}\}/g, String(Boolean(config.enableSpeechRecognition))).replace(/\{\{ENABLE_HAPTICS\}\}/g, String(Boolean(config.enableHaptics))).replace(/\{\{ENABLE_SHARE\}\}/g, String(Boolean(config.enableShare))).replace(/\{\{ENABLE_CAMERA\}\}/g, String(Boolean(config.enableCamera))).replace(/\{\{ENABLE_BIOMETRIC\}\}/g, String(Boolean(config.enableBiometric))).replace(/\{\{ENABLE_PUSH\}\}/g, String(Boolean(config.enablePushNotifications))).replace(/\{\{ENABLE_SECURE_STORAGE\}\}/g, String(Boolean(config.enableSecureStorage))).replace(/\{\{ENABLE_GEOLOCATION\}\}/g, String(Boolean(config.enableGeolocation))).replace(/\{\{ENABLE_BACKGROUND_LOCATION\}\}/g, String(Boolean(config.enableBackgroundLocation))).replace(/\{\{ENABLE_KEEP_AWAKE\}\}/g, String(Boolean(config.enableKeepAwake))).replace(/\{\{ENABLE_DEEP_LINKS\}\}/g, String(Boolean(config.enableDeepLinks))).replace(/\{\{ENABLE_HEALTH_CONNECT\}\}/g, String(Boolean(config.enableHealthConnect))).replace(/\{\{FIREBASE_IMPORT\}\}/g, config.enablePushNotifications ? "import com.google.firebase.messaging.FirebaseMessaging" : "").replace(/\{\{REGISTER_PUSH_IMPLEMENTATION\}\}/g, config.enablePushNotifications ? `activity.runOnUiThread {
|
|
777
1017
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
|
|
778
1018
|
&& ContextCompat.checkSelfPermission(activity, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
|
@@ -792,17 +1032,17 @@ async function init(options) {
|
|
|
792
1032
|
}` : `evaluatePromiseJavascript(
|
|
793
1033
|
"window._craftPushReject && window._craftPushReject('Push notifications are disabled')"
|
|
794
1034
|
)`);
|
|
795
|
-
writeFileSync(
|
|
796
|
-
const craftNative = readFileSync2(
|
|
797
|
-
const nativeDir =
|
|
798
|
-
|
|
799
|
-
writeFileSync(
|
|
800
|
-
const serviceTemplate = readFileSync2(
|
|
801
|
-
writeFileSync(
|
|
802
|
-
const healthTemplate = readFileSync2(
|
|
803
|
-
writeFileSync(
|
|
804
|
-
const manifestTemplate = readFileSync2(
|
|
805
|
-
const manifest = manifestTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{PERMISSIONS\}\}/g, renderAndroidPermissions(config)).replace(/\{\{USES_CLEARTEXT\}\}/g, config.devServerURL?.startsWith("http://") || config.enableLocalNetwork ? "true" : "false").replace(/\{\{DEEP_LINK_INTENT_FILTERS\}\}/g, renderAndroidDeepLinks(config)).replace(/\{\{BACKGROUND_SERVICE\}\}/g, config.enableBackgroundLocation ? ' <service android:name="com.craft.runtime.LocationRecordingService" android:exported="false" android:foregroundServiceType="location" android:stopWithTask="false" />' : "").replace(/\{\{NATIVE_NOTIFICATION_RECEIVER\}\}/g, config.renderer === "native" ? renderNativeShellManifest(config) : "").replace(/\{\{CRASH_REPORTER_PROVIDER\}\}/g, ` <provider android:name=".CraftCrashReporterInit" android:authorities="\${applicationId}.craft-crash" android:exported="false" android:initOrder="1000" />`).replace(/\{\{GOOGLE_MAPS_API_KEY\}\}/g, config.mapsApiKey ? ` <meta-data android:name="com.google.android.geo.API_KEY" android:value="${escapeXml(config.mapsApiKey)}" />` : "").replace(/\{\{HEALTH_CONNECT_QUERIES\}\}/g, config.enableHealthConnect ? ` <queries>
|
|
1035
|
+
writeFileSync(join3(output, "app/src/main/java", packagePath, "CraftBridge.kt"), craftBridge);
|
|
1036
|
+
const craftNative = readFileSync2(join3(TEMPLATES_DIR, "CraftNative.kt.template"), "utf-8");
|
|
1037
|
+
const nativeDir = join3(output, "app/src/main/java/com/craft/runtime");
|
|
1038
|
+
mkdirSync2(nativeDir, { recursive: true });
|
|
1039
|
+
writeFileSync(join3(nativeDir, "CraftNative.kt"), craftNative);
|
|
1040
|
+
const serviceTemplate = readFileSync2(join3(TEMPLATES_DIR, "LocationRecordingService.kt.template"), "utf-8");
|
|
1041
|
+
writeFileSync(join3(nativeDir, "LocationRecordingService.kt"), serviceTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
1042
|
+
const healthTemplate = readFileSync2(join3(TEMPLATES_DIR, config.enableHealthConnect ? "CraftHealthConnect.kt.template" : "CraftHealthConnectStub.kt.template"), "utf-8");
|
|
1043
|
+
writeFileSync(join3(output, "app/src/main/java", packagePath, "CraftHealthConnect.kt"), healthTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
1044
|
+
const manifestTemplate = readFileSync2(join3(TEMPLATES_DIR, "AndroidManifest.xml.template"), "utf-8");
|
|
1045
|
+
const manifest = manifestTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{PERMISSIONS\}\}/g, renderAndroidPermissions(config)).replace(/\{\{USES_CLEARTEXT\}\}/g, config.devServerURL?.startsWith("http://") || config.enableLocalNetwork ? "true" : "false").replace(/\{\{DEEP_LINK_INTENT_FILTERS\}\}/g, renderAndroidDeepLinks(config)).replace(/\{\{BACKGROUND_SERVICE\}\}/g, config.enableBackgroundLocation ? ' <service android:name="com.craft.runtime.LocationRecordingService" android:exported="false" android:foregroundServiceType="location" android:stopWithTask="false" />' : "").replace(/\{\{NATIVE_NOTIFICATION_RECEIVER\}\}/g, config.renderer === "native" ? renderNativeShellManifest(config) : "").replace(/\{\{CRASH_REPORTER_PROVIDER\}\}/g, ` <provider android:name=".CraftCrashReporterInit" android:authorities="\${applicationId}.craft-crash" android:exported="false" android:initOrder="1000" />`).replace(/\{\{GOOGLE_MAPS_API_KEY\}\}/g, config.mapProvider !== "maplibre" && config.mapsApiKey ? ` <meta-data android:name="com.google.android.geo.API_KEY" android:value="${escapeXml(config.mapsApiKey)}" />` : "").replace(/\{\{HEALTH_CONNECT_QUERIES\}\}/g, config.enableHealthConnect ? ` <queries>
|
|
806
1046
|
<package android:name="com.google.android.apps.healthdata" />
|
|
807
1047
|
</queries>` : "").replace(/\{\{HEALTH_CONNECT_RATIONALE\}\}/g, config.enableHealthConnect ? ` <intent-filter>
|
|
808
1048
|
<action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
|
|
@@ -811,28 +1051,28 @@ async function init(options) {
|
|
|
811
1051
|
<action android:name="android.intent.action.VIEW_PERMISSION_USAGE" />
|
|
812
1052
|
<category android:name="android.intent.category.HEALTH_PERMISSIONS" />
|
|
813
1053
|
</intent-filter>`}` : "");
|
|
814
|
-
writeFileSync(
|
|
815
|
-
const projectGradleTemplate = readFileSync2(
|
|
816
|
-
writeFileSync(
|
|
817
|
-
const appGradleTemplate = readFileSync2(
|
|
818
|
-
const appGradle = appGradleTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{VERSION_NAME\}\}/g, escapeKotlinString(config.version || "1.0.0")).replace(/\{\{VERSION_CODE\}\}/g, String(config.versionCode || 1)).replace(/\{\{MIN_SDK\}\}/g, String(config.minSdk ?? ANDROID_MIN_SDK)).replace(/\{\{COMPILE_SDK\}\}/g, String(config.compileSdk || 36)).replace(/\{\{TARGET_SDK\}\}/g, String(config.targetSdk || 35)).replace(/\{\{GOOGLE_SERVICES_PLUGIN\}\}/g, hasGoogleServices ? ' id("com.google.gms.google-services")' : "").replace(/\{\{FIREBASE_MESSAGING_DEPENDENCY\}\}/g, config.enablePushNotifications ? ' implementation("com.google.firebase:firebase-messaging:24.1.0")' : "").replace(/\{\{REFRESH_LAYOUT_DEPENDENCY\}\}/g, config.renderer === "native" ? ' implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")' : "").replace(/\{\{
|
|
819
|
-
implementation("
|
|
1054
|
+
writeFileSync(join3(output, "app/src/main/AndroidManifest.xml"), manifest);
|
|
1055
|
+
const projectGradleTemplate = readFileSync2(join3(TEMPLATES_DIR, "build.gradle.kts.project.template"), "utf-8");
|
|
1056
|
+
writeFileSync(join3(output, "build.gradle.kts"), projectGradleTemplate.replace(/\{\{GOOGLE_SERVICES_PLUGIN\}\}/g, hasGoogleServices ? ' id("com.google.gms.google-services") version "4.4.2" apply false' : ""));
|
|
1057
|
+
const appGradleTemplate = readFileSync2(join3(TEMPLATES_DIR, "build.gradle.kts.app.template"), "utf-8");
|
|
1058
|
+
const appGradle = appGradleTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{VERSION_NAME\}\}/g, escapeKotlinString(config.version || "1.0.0")).replace(/\{\{VERSION_CODE\}\}/g, String(config.versionCode || 1)).replace(/\{\{MIN_SDK\}\}/g, String(config.minSdk ?? ANDROID_MIN_SDK)).replace(/\{\{COMPILE_SDK\}\}/g, String(config.compileSdk || 36)).replace(/\{\{TARGET_SDK\}\}/g, String(config.targetSdk || 35)).replace(/\{\{GOOGLE_SERVICES_PLUGIN\}\}/g, hasGoogleServices ? ' id("com.google.gms.google-services")' : "").replace(/\{\{FIREBASE_MESSAGING_DEPENDENCY\}\}/g, config.enablePushNotifications ? ' implementation("com.google.firebase:firebase-messaging:24.1.0")' : "").replace(/\{\{REFRESH_LAYOUT_DEPENDENCY\}\}/g, config.renderer === "native" ? ' implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")' : "").replace(/\{\{NATIVE_MAP_DEPENDENCY\}\}/g, config.renderer === "native" ? ` // Native STX Map overlays and trail routes
|
|
1059
|
+
implementation("${androidMapHost(config.mapProvider).dependency}")` : "").replace(/\{\{HEALTH_CONNECT_DEPENDENCIES\}\}/g, config.enableHealthConnect ? ` implementation("androidx.health.connect:connect-client:1.1.0")
|
|
820
1060
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")` : "").replace(/\{\{JAVASCRIPT_ENGINE_DEPENDENCY\}\}/g, config.renderer === "native" ? ` implementation("androidx.javascriptengine:javascriptengine:1.1.0")
|
|
821
1061
|
// Native background refresh (WorkManager).
|
|
822
1062
|
implementation("androidx.work:work-runtime-ktx:2.9.1")` : "");
|
|
823
|
-
writeFileSync(
|
|
824
|
-
const proguardTemplate = readFileSync2(
|
|
825
|
-
writeFileSync(
|
|
826
|
-
const settingsTemplate = readFileSync2(
|
|
1063
|
+
writeFileSync(join3(output, "app/build.gradle.kts"), appGradle);
|
|
1064
|
+
const proguardTemplate = readFileSync2(join3(TEMPLATES_DIR, "proguard-rules.pro.template"), "utf-8");
|
|
1065
|
+
writeFileSync(join3(output, "app/proguard-rules.pro"), proguardTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
1066
|
+
const settingsTemplate = readFileSync2(join3(TEMPLATES_DIR, "settings.gradle.kts.template"), "utf-8");
|
|
827
1067
|
const settings = settingsTemplate.replace(/\{\{APP_NAME\}\}/g, escapeKotlinString(generatedGradleProjectName(name)));
|
|
828
|
-
writeFileSync(
|
|
1068
|
+
writeFileSync(join3(output, "settings.gradle.kts"), settings);
|
|
829
1069
|
const gradleProperties = `org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
|
830
1070
|
android.useAndroidX=true
|
|
831
1071
|
kotlin.code.style=official
|
|
832
1072
|
android.nonTransitiveRClass=true
|
|
833
1073
|
`;
|
|
834
|
-
writeFileSync(
|
|
835
|
-
writeFileSync(
|
|
1074
|
+
writeFileSync(join3(output, "gradle.properties"), gradleProperties);
|
|
1075
|
+
writeFileSync(join3(output, "local.properties"), `# SDK location will be set by Android Studio
|
|
836
1076
|
`);
|
|
837
1077
|
const gradleWrapperProps = `distributionBase=GRADLE_USER_HOME
|
|
838
1078
|
distributionPath=wrapper/dists
|
|
@@ -842,23 +1082,25 @@ validateDistributionUrl=true
|
|
|
842
1082
|
zipStoreBase=GRADLE_USER_HOME
|
|
843
1083
|
zipStorePath=wrapper/dists
|
|
844
1084
|
`;
|
|
845
|
-
writeFileSync(
|
|
1085
|
+
writeFileSync(join3(output, "gradle/wrapper/gradle-wrapper.properties"), gradleWrapperProps);
|
|
846
1086
|
const stringsXml = `<?xml version="1.0" encoding="utf-8"?>
|
|
847
1087
|
<resources>
|
|
848
1088
|
<string name="app_name">${escapeXml(name)}</string>
|
|
849
1089
|
</resources>
|
|
850
1090
|
`;
|
|
851
|
-
writeFileSync(
|
|
1091
|
+
writeFileSync(join3(output, "app/src/main/res/values/strings.xml"), stringsXml);
|
|
852
1092
|
const colorsXml = `<?xml version="1.0" encoding="utf-8"?>
|
|
853
1093
|
<resources>
|
|
854
1094
|
<color name="primary">#1a1a2e</color>
|
|
855
1095
|
<color name="primary_dark">#16162b</color>
|
|
856
1096
|
<color name="accent">#4ade80</color>
|
|
857
1097
|
<color name="background">${config.backgroundColor}</color>
|
|
1098
|
+
<!-- Transparent is NotificationCompat's COLOR_DEFAULT: the system picks. -->
|
|
1099
|
+
<color name="craft_notification_color">${config.notificationColor ?? "#00000000"}</color>
|
|
858
1100
|
</resources>
|
|
859
1101
|
`;
|
|
860
|
-
writeFileSync(
|
|
861
|
-
const darkColorsPath =
|
|
1102
|
+
writeFileSync(join3(output, "app/src/main/res/values/colors.xml"), colorsXml);
|
|
1103
|
+
const darkColorsPath = join3(output, "app/src/main/res/values-night/colors.xml");
|
|
862
1104
|
if (config.backgroundColorDark) {
|
|
863
1105
|
writeFileSync(darkColorsPath, `<?xml version="1.0" encoding="utf-8"?>
|
|
864
1106
|
<resources>
|
|
@@ -866,7 +1108,7 @@ zipStorePath=wrapper/dists
|
|
|
866
1108
|
</resources>
|
|
867
1109
|
`);
|
|
868
1110
|
} else {
|
|
869
|
-
|
|
1111
|
+
rmSync2(darkColorsPath, { force: true });
|
|
870
1112
|
}
|
|
871
1113
|
const appIconXml = `<?xml version="1.0" encoding="utf-8"?>
|
|
872
1114
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
@@ -879,9 +1121,24 @@ zipStorePath=wrapper/dists
|
|
|
879
1121
|
</vector>
|
|
880
1122
|
`;
|
|
881
1123
|
if (config.appIconPath) {
|
|
882
|
-
cpSync(config.appIconPath,
|
|
1124
|
+
cpSync(config.appIconPath, join3(output, `app/src/main/res/drawable/craft_app_icon${appIconExtension}`));
|
|
1125
|
+
} else {
|
|
1126
|
+
writeFileSync(join3(output, "app/src/main/res/drawable/craft_app_icon.xml"), appIconXml);
|
|
1127
|
+
}
|
|
1128
|
+
for (const extension of [".png", ".xml"])
|
|
1129
|
+
rmSync2(join3(output, `app/src/main/res/drawable/craft_notification_icon${extension}`), { force: true });
|
|
1130
|
+
if (config.notificationIconPath) {
|
|
1131
|
+
cpSync(config.notificationIconPath, join3(output, `app/src/main/res/drawable/craft_notification_icon${notificationIconExtension}`));
|
|
883
1132
|
} else {
|
|
884
|
-
writeFileSync(
|
|
1133
|
+
writeFileSync(join3(output, "app/src/main/res/drawable/craft_notification_icon.xml"), `<?xml version="1.0" encoding="utf-8"?>
|
|
1134
|
+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
1135
|
+
android:width="24dp"
|
|
1136
|
+
android:height="24dp"
|
|
1137
|
+
android:viewportWidth="108"
|
|
1138
|
+
android:viewportHeight="108">
|
|
1139
|
+
<path android:fillColor="#FFFFFFFF" android:pathData="M18,78L42,42l12,18l12,-12l24,30z" />
|
|
1140
|
+
</vector>
|
|
1141
|
+
`);
|
|
885
1142
|
}
|
|
886
1143
|
const themesXml = `<?xml version="1.0" encoding="utf-8"?>
|
|
887
1144
|
<resources>
|
|
@@ -892,7 +1149,7 @@ zipStorePath=wrapper/dists
|
|
|
892
1149
|
</style>
|
|
893
1150
|
</resources>
|
|
894
1151
|
`;
|
|
895
|
-
writeFileSync(
|
|
1152
|
+
writeFileSync(join3(output, "app/src/main/res/values/themes.xml"), themesXml);
|
|
896
1153
|
const activityMainXml = config.renderer === "native" ? `<?xml version="1.0" encoding="utf-8"?>
|
|
897
1154
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
898
1155
|
android:id="@+id/native_root"
|
|
@@ -912,7 +1169,7 @@ zipStorePath=wrapper/dists
|
|
|
912
1169
|
|
|
913
1170
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
914
1171
|
`;
|
|
915
|
-
writeFileSync(
|
|
1172
|
+
writeFileSync(join3(output, "app/src/main/res/layout/activity_main.xml"), activityMainXml);
|
|
916
1173
|
syncNativeFonts(config.nativeFonts, output);
|
|
917
1174
|
const placeholderHtml = `<!DOCTYPE html>
|
|
918
1175
|
<html>
|
|
@@ -951,7 +1208,7 @@ zipStorePath=wrapper/dists
|
|
|
951
1208
|
</script>
|
|
952
1209
|
</body>
|
|
953
1210
|
</html>`;
|
|
954
|
-
writeFileSync(
|
|
1211
|
+
writeFileSync(join3(output, "app/src/main/assets/index.html"), placeholderHtml);
|
|
955
1212
|
const runtimeDir = resolveRuntimeDir(options.runtimeDir);
|
|
956
1213
|
if (runtimeDir) {
|
|
957
1214
|
installRuntime(output, runtimeDir);
|
|
@@ -975,7 +1232,7 @@ async function build(options) {
|
|
|
975
1232
|
const { htmlPath, nativeBundlePath, devServer, output, release, bundle, compile = true } = options;
|
|
976
1233
|
console.log(`
|
|
977
1234
|
\uD83D\uDCE6 Building Craft Android project...`);
|
|
978
|
-
const configPath =
|
|
1235
|
+
const configPath = join3(output, "craft.config.json");
|
|
979
1236
|
if (!existsSync(configPath)) {
|
|
980
1237
|
throw new Error(`No craft.config.json found in ${output}. Run 'craft android init' first.`);
|
|
981
1238
|
}
|
|
@@ -985,13 +1242,13 @@ async function build(options) {
|
|
|
985
1242
|
if (config.renderer === "native") {
|
|
986
1243
|
if (htmlPath || devServer)
|
|
987
1244
|
throw new Error("A native Android screen cannot use --html-path or --dev-server.");
|
|
988
|
-
const destination =
|
|
989
|
-
if (nativeBundlePath &&
|
|
1245
|
+
const destination = join3(output, "app/src/main/assets/native-screen.js");
|
|
1246
|
+
if (nativeBundlePath && resolve2(nativeBundlePath) !== resolve2(destination))
|
|
990
1247
|
cpSync(nativeBundlePath, destination);
|
|
991
|
-
const icons =
|
|
1248
|
+
const icons = join3(output, "app/src/main/assets/craft-icons.json");
|
|
992
1249
|
if (options.iconsPath) {
|
|
993
1250
|
requireRegularFile(options.iconsPath, "Icon table");
|
|
994
|
-
if (
|
|
1251
|
+
if (resolve2(options.iconsPath) !== resolve2(icons))
|
|
995
1252
|
cpSync(options.iconsPath, icons);
|
|
996
1253
|
}
|
|
997
1254
|
if (!existsSync(destination)) {
|
|
@@ -1000,7 +1257,7 @@ async function build(options) {
|
|
|
1000
1257
|
} else if (nativeBundlePath) {
|
|
1001
1258
|
throw new Error('--native-bundle requires renderer: "native" in craft.config.json.');
|
|
1002
1259
|
}
|
|
1003
|
-
if (existsSync(
|
|
1260
|
+
if (existsSync(join3(output, "app/src/main/jniLibs"))) {
|
|
1004
1261
|
const runtimeDir = resolveRuntimeDir(options.runtimeDir);
|
|
1005
1262
|
if (runtimeDir) {
|
|
1006
1263
|
installRuntime(output, runtimeDir);
|
|
@@ -1028,7 +1285,7 @@ async function build(options) {
|
|
|
1028
1285
|
syncNativeFonts(config.nativeFonts, output);
|
|
1029
1286
|
if (!compile)
|
|
1030
1287
|
return;
|
|
1031
|
-
const gradlewPath =
|
|
1288
|
+
const gradlewPath = join3(output, "gradlew");
|
|
1032
1289
|
if (!existsSync(gradlewPath)) {
|
|
1033
1290
|
console.log(" Generating Gradle wrapper...");
|
|
1034
1291
|
try {
|
|
@@ -1075,13 +1332,13 @@ async function run(options) {
|
|
|
1075
1332
|
await build({ output });
|
|
1076
1333
|
console.log("\uD83D\uDCF1 Installing and running on device...");
|
|
1077
1334
|
try {
|
|
1078
|
-
const apkPath =
|
|
1335
|
+
const apkPath = join3(output, "app/build/outputs/apk/debug/app-debug.apk");
|
|
1079
1336
|
if (device) {
|
|
1080
1337
|
await $`adb -s ${device} install -r ${apkPath}`;
|
|
1081
1338
|
} else {
|
|
1082
1339
|
await $`adb install -r ${apkPath}`;
|
|
1083
1340
|
}
|
|
1084
|
-
const config = JSON.parse(readFileSync2(
|
|
1341
|
+
const config = JSON.parse(readFileSync2(join3(output, "craft.config.json"), "utf-8"));
|
|
1085
1342
|
const launchCmd = `${config.packageName}/.MainActivity`;
|
|
1086
1343
|
if (device) {
|
|
1087
1344
|
await $`adb -s ${device} shell am start -n ${launchCmd}`;
|
|
@@ -1105,9 +1362,11 @@ export {
|
|
|
1105
1362
|
renderAndroidPermissions,
|
|
1106
1363
|
renderAndroidDeepLinks,
|
|
1107
1364
|
open,
|
|
1365
|
+
nativeMapProviders,
|
|
1108
1366
|
installRuntime,
|
|
1109
1367
|
init,
|
|
1110
1368
|
healthConnectPermissions,
|
|
1369
|
+
createAndroidEmulator,
|
|
1111
1370
|
build,
|
|
1112
1371
|
HEALTH_CONNECT_PERMISSIONS,
|
|
1113
1372
|
ANDROID_MIN_SDK
|