@craft-native/android 0.0.436 → 0.0.443

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
@@ -7,6 +7,8 @@ export type { AndroidMapProvider } from './native-map.js';
7
7
  * well as the one-file bundle, and the app runs each screen from its own
8
8
  * route's file. Stacks asks for route bundles only when this is true.
9
9
  */
10
+ /** Build-time proof for Stacks capability mappings, including unreleased source builds. */
11
+ export declare const nativeSensorCapabilities: readonly ['motionSensors', 'barometer', 'pedometer'];
10
12
  export declare const nativeRouteBundles = true;
11
13
  /**
12
14
  * The app evaluates stx's shared runtime (`__runtime.js`, stx's
@@ -65,6 +67,12 @@ export interface CraftAndroidConfig {
65
67
  enableSecureStorage?: boolean;
66
68
  enableGeolocation?: boolean;
67
69
  enableBackgroundLocation?: boolean;
70
+ /** Foreground raw Android sensors on native screens; separate from iOS fused motion. */
71
+ enableMotionSensors?: boolean;
72
+ /** Foreground pressure and session-relative altitude; does not enable background recording. */
73
+ enableBarometer?: boolean;
74
+ /** Foreground hardware step counters/detectors; requires activity recognition on Android 10+. */
75
+ enablePedometer?: boolean;
68
76
  enableBluetooth?: boolean;
69
77
  /**
70
78
  * A Bluetooth heart-rate strap, or a watch broadcasting its heart rate (the
@@ -114,6 +122,18 @@ export interface CraftAndroidConfig {
114
122
  }>;
115
123
  trustedOrigins?: string[];
116
124
  appIconPath?: string;
125
+ /**
126
+ * The launch splash: a directory laid out as Android resources, holding
127
+ * `drawable-<density>/splash_icon.png` (Android 12's splash icon, 288dp
128
+ * with its content inside the 192dp circle) and `splash_logo.png` (the
129
+ * logo the window background centres before Android 12), with Dark Mode
130
+ * ones in `drawable-night-<density>`. ts-images' `generateSplashScreens`
131
+ * writes exactly this (`android.resources`). Both sit on `backgroundColor`
132
+ * (`backgroundColorDark` at night), the colour the first frame continues.
133
+ * Without it, Android 12 shows the launcher icon and older versions the
134
+ * background colour.
135
+ */
136
+ splashResources?: string;
117
137
  /**
118
138
  * The small icon in the status bar and on every notification the app
119
139
  * shows, push or local: an Android vector drawable (`.xml`) or a `.png`,
@@ -260,6 +280,30 @@ export interface RunOptions {
260
280
  device?: string;
261
281
  output: string;
262
282
  }
283
+ /** A splash file, by its place under `res/` (`drawable-night-xxhdpi/splash_icon.png`). */
284
+ export interface AndroidSplashFile {
285
+ folder: string;
286
+ name: 'splash_icon.png' | 'splash_logo.png';
287
+ path: string;
288
+ }
289
+ /**
290
+ * The splash files in a resources directory: `splash_icon.png` and
291
+ * `splash_logo.png` in each `drawable[-night][-<density>]` folder. A directory
292
+ * with neither is a mistake, not an app without a splash.
293
+ */
294
+ export declare function androidSplashFiles(resources: string): AndroidSplashFile[];
295
+ /**
296
+ * The launch theme the activity starts in, and the theme it swaps to before
297
+ * its first frame. Android 12 and later draw their splash from
298
+ * `windowSplashScreen*`; earlier versions draw the window background, here
299
+ * the logo centred on the background colour. Either way the colour is the
300
+ * app's own `background`, so nothing flashes when the first frame replaces it.
301
+ */
302
+ export declare function androidLaunchThemes(splash: Pick<AndroidSplashFile, 'name'>[]): {
303
+ base: string;
304
+ v31: string;
305
+ background?: string;
306
+ };
263
307
  export declare function syncAndroidWebAssets(source: string, output: string): void;
264
308
  export declare function renderAndroidPermissions(config: CraftAndroidConfig): string;
265
309
  export declare function renderAndroidDeepLinks(config: CraftAndroidConfig): string;
package/dist/index.js CHANGED
@@ -549,6 +549,7 @@ function compressNativeSources(target) {
549
549
  if (packed.length < bytes.length)
550
550
  writeFileSync(target, packed);
551
551
  }
552
+ var nativeSensorCapabilities = ["motionSensors", "barometer", "pedometer"];
552
553
  var nativeRouteBundles = true;
553
554
  var nativeRouteRuntime = true;
554
555
  var nativeMessages = true;
@@ -715,6 +716,9 @@ var DEFAULT_CONFIG = {
715
716
  enableSecureStorage: false,
716
717
  enableGeolocation: false,
717
718
  enableBackgroundLocation: false,
719
+ enableMotionSensors: false,
720
+ enableBarometer: false,
721
+ enablePedometer: false,
718
722
  enableBluetooth: false,
719
723
  enableHeartRate: false,
720
724
  enableHealthConnect: false,
@@ -753,6 +757,49 @@ function requireRegularFile(path, label) {
753
757
  if (!statSync(path).isFile())
754
758
  throw new Error(`${label} must be a file: ${path}`);
755
759
  }
760
+ function androidSplashFiles(resources) {
761
+ if (!existsSync(resources) || !statSync(resources).isDirectory())
762
+ throw new Error(`Splash resources must be a directory: ${resources}`);
763
+ const files = [];
764
+ for (const folder of readdirSync(resources).sort()) {
765
+ if (!/^drawable(?:-night)?(?:-(?:l|m|tv|h|xh|xxh|xxxh)dpi)?$/.test(folder))
766
+ continue;
767
+ for (const name of ["splash_icon.png", "splash_logo.png"]) {
768
+ const path = join3(resources, folder, name);
769
+ if (existsSync(path) && statSync(path).isFile())
770
+ files.push({ folder, name, path });
771
+ }
772
+ }
773
+ if (!files.length)
774
+ throw new Error(`Splash resources hold no drawable*/splash_icon.png or splash_logo.png: ${resources}`);
775
+ return files;
776
+ }
777
+ function androidLaunchThemes(splash) {
778
+ const icon = splash.some((file) => file.name === "splash_icon.png");
779
+ const logo = splash.some((file) => file.name === "splash_logo.png");
780
+ const base = logo ? ` <style name="Theme.CraftApp.Launch" parent="Theme.CraftApp">
781
+ <item name="android:windowBackground">@drawable/craft_launch_background</item>
782
+ </style>
783
+ ` : ` <style name="Theme.CraftApp.Launch" parent="Theme.CraftApp" />
784
+ `;
785
+ const v31 = `<?xml version="1.0" encoding="utf-8"?>
786
+ <resources>
787
+ <style name="Theme.CraftApp.Launch" parent="Theme.CraftApp">
788
+ <item name="android:windowSplashScreenBackground">@color/background</item>${icon ? `
789
+ <item name="android:windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>` : ""}
790
+ </style>
791
+ </resources>
792
+ `;
793
+ const background = logo ? `<?xml version="1.0" encoding="utf-8"?>
794
+ <layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque">
795
+ <item android:drawable="@color/background" />
796
+ <item>
797
+ <bitmap android:gravity="center" android:src="@drawable/splash_logo" />
798
+ </item>
799
+ </layer-list>
800
+ ` : undefined;
801
+ return { base, v31, background };
802
+ }
756
803
  function containsPath(parent, candidate) {
757
804
  const relativePath = relative(parent, candidate);
758
805
  return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute2(relativePath);
@@ -960,6 +1007,7 @@ function writeAndroidConfig(output, config) {
960
1007
  writeFileSync(join3(output, "craft.config.json"), JSON.stringify(config, null, 2));
961
1008
  const runtimeConfig = { ...config };
962
1009
  delete runtimeConfig.appIconPath;
1010
+ delete runtimeConfig.splashResources;
963
1011
  delete runtimeConfig.notificationIconPath;
964
1012
  delete runtimeConfig.googleServicesFile;
965
1013
  writeFileSync(join3(output, "app/src/main/assets/craft.config.json"), JSON.stringify(runtimeConfig, null, 2));
@@ -1013,10 +1061,14 @@ function renderAndroidPermissions(config) {
1013
1061
  }
1014
1062
  if (config.enablePushNotifications)
1015
1063
  permissions.add("android.permission.POST_NOTIFICATIONS");
1016
- if (config.enableKeepAwake)
1064
+ if (config.enableKeepAwake || config.enableBarometer && config.enableBackgroundLocation)
1017
1065
  permissions.add("android.permission.WAKE_LOCK");
1018
1066
  if (config.enableHealthConnect)
1019
1067
  healthConnectPermissions(config).forEach((permission) => permissions.add(permission));
1068
+ if (config.enablePedometer)
1069
+ permissions.add("android.permission.ACTIVITY_RECOGNITION");
1070
+ if (config.enablePedometer && config.enableHealthServices)
1071
+ permissions.add("android.permission.RECEIVE_BOOT_COMPLETED");
1020
1072
  if (config.renderer === "native" && config.backgroundRefresh?.enabled)
1021
1073
  permissions.add("android.permission.RECEIVE_BOOT_COMPLETED");
1022
1074
  if (config.enableHeartRate) {
@@ -1056,13 +1108,16 @@ function writeNativeShellSources(output, packagePath, packageName, config) {
1056
1108
  write("CraftSnapshotFile.kt.template", "CraftSnapshotFile.kt");
1057
1109
  write("CraftNativeImageCache.kt.template", "CraftNativeImageCache.kt");
1058
1110
  write("CraftNativeHaptics.kt.template", "CraftNativeHaptics.kt");
1111
+ write("CraftNativeMotion.kt.template", "CraftNativeMotion.kt");
1059
1112
  write("CraftNativeBackground.kt.template", "CraftNativeBackground.kt");
1060
1113
  write(config.enableHealthConnect ? "CraftNativeHealth.kt.template" : "CraftNativeHealthStub.kt.template", "CraftNativeHealth.kt");
1061
1114
  write(config.enableHealthServices ? "CraftHealthServices.kt.template" : "CraftHealthServicesStub.kt.template", "CraftHealthServices.kt");
1062
1115
  if (config.enableHealthServices) {
1063
1116
  write("CraftHealthServicesService.kt.template", "CraftHealthServicesService.kt");
1117
+ write("CraftPassiveSteps.kt.template", "CraftPassiveSteps.kt");
1064
1118
  } else {
1065
1119
  rmSync2(join3(javaDir, "CraftHealthServicesService.kt"), { force: true });
1120
+ rmSync2(join3(javaDir, "CraftPassiveSteps.kt"), { force: true });
1066
1121
  }
1067
1122
  write("CraftNativeServices.kt.template", "CraftNativeServices.kt", (source) => source.replace("{{NATIVE_PUSH_IMPORTS}}", config.enablePushNotifications ? "import com.google.firebase.messaging.FirebaseMessaging" : "").replace("{{NATIVE_WALLET_IMPORTS}}", config.enableWallet ? `import com.google.android.gms.pay.Pay
1068
1123
  import com.google.android.gms.pay.PayApiAvailabilityStatus
@@ -1255,6 +1310,7 @@ async function init(options) {
1255
1310
  if (config.appIconPath && ![".gif", ".jpg", ".png", ".webp"].includes(appIconExtension ?? "")) {
1256
1311
  throw new Error(`Unsupported Android app icon format: ${appIconExtension || "(none)"}`);
1257
1312
  }
1313
+ const splash = config.splashResources ? androidSplashFiles(config.splashResources) : [];
1258
1314
  if (config.notificationIconPath)
1259
1315
  requireRegularFile(config.notificationIconPath, "Notification icon");
1260
1316
  const notificationIconExtension = config.notificationIconPath ? extname(config.notificationIconPath).toLowerCase() : undefined;
@@ -1348,6 +1404,7 @@ async function init(options) {
1348
1404
  const nativeDir = join3(output, "app/src/main/java/com/craft/runtime");
1349
1405
  mkdirSync2(nativeDir, { recursive: true });
1350
1406
  writeFileSync(join3(nativeDir, "CraftNative.kt"), craftNative);
1407
+ writeFileSync(join3(nativeDir, "CraftRecordingPressure.kt"), readFileSync2(join3(TEMPLATES_DIR, "CraftRecordingPressure.kt.template"), "utf-8"));
1351
1408
  const serviceTemplate = readFileSync2(join3(TEMPLATES_DIR, "LocationRecordingService.kt.template"), "utf-8");
1352
1409
  writeFileSync(join3(nativeDir, "LocationRecordingService.kt"), serviceTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
1353
1410
  const healthTemplate = readFileSync2(join3(TEMPLATES_DIR, config.enableHealthConnect ? "CraftHealthConnect.kt.template" : "CraftHealthConnectStub.kt.template"), "utf-8");
@@ -1356,7 +1413,9 @@ async function init(options) {
1356
1413
  writeFileSync(join3(output, "app/src/main/java", packagePath, file), readFileSync2(join3(TEMPLATES_DIR, template), "utf8").replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
1357
1414
  }
1358
1415
  const manifestTemplate = readFileSync2(join3(TEMPLATES_DIR, "AndroidManifest.xml.template"), "utf-8");
1359
- 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(/\{\{HEART_RATE_SERVICE\}\}/g, config.enableHeartRate ? ' <service android:name=".CraftHeartRateService" android:exported="false" android:foregroundServiceType="connectedDevice" />' : "").replace(/\{\{HEALTH_SERVICES_SERVICE\}\}/g, config.enableHealthServices ? ' <service android:name=".CraftHealthServicesService" android:exported="false" android:foregroundServiceType="health" 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>
1416
+ 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(/\{\{HEART_RATE_SERVICE\}\}/g, config.enableHeartRate ? ' <service android:name=".CraftHeartRateService" android:exported="false" android:foregroundServiceType="connectedDevice" />' : "").replace(/\{\{HEALTH_SERVICES_SERVICE\}\}/g, config.enableHealthServices ? ' <service android:name=".CraftHealthServicesService" android:exported="false" android:foregroundServiceType="health" android:stopWithTask="false" />' + (config.enablePedometer ? `
1417
+ <service android:name=".CraftPassiveStepsService" android:exported="true" android:permission="com.google.android.wearable.healthservices.permission.PASSIVE_DATA_BINDING" />
1418
+ <receiver android:name=".CraftPassiveStepsBootReceiver" android:exported="true"><intent-filter><action android:name="android.intent.action.BOOT_COMPLETED" /></intent-filter></receiver>` : "") : "").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>
1360
1419
  <package android:name="com.google.android.apps.healthdata" />
1361
1420
  </queries>` : "").replace(/\{\{HEALTH_CONNECT_RATIONALE\}\}/g, config.enableHealthConnect ? ` <intent-filter>
1362
1421
  <action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
@@ -1461,6 +1520,17 @@ zipStorePath=wrapper/dists
1461
1520
  </vector>
1462
1521
  `);
1463
1522
  }
1523
+ const resDir = join3(output, "app/src/main/res");
1524
+ for (const folder of existsSync(resDir) ? readdirSync(resDir) : []) {
1525
+ if (folder.startsWith("drawable"))
1526
+ for (const name of ["splash_icon.png", "splash_logo.png"])
1527
+ rmSync2(join3(resDir, folder, name), { force: true });
1528
+ }
1529
+ for (const file of splash) {
1530
+ mkdirSync2(join3(resDir, file.folder), { recursive: true });
1531
+ cpSync(file.path, join3(resDir, file.folder, file.name));
1532
+ }
1533
+ const launch = androidLaunchThemes(splash);
1464
1534
  const themesXml = `<?xml version="1.0" encoding="utf-8"?>
1465
1535
  <resources>
1466
1536
  <style name="Theme.CraftApp" parent="Theme.Material3.DayNight.NoActionBar">
@@ -1468,9 +1538,15 @@ zipStorePath=wrapper/dists
1468
1538
  <item name="android:navigationBarColor">@color/primary</item>
1469
1539
  <item name="android:windowBackground">@color/background</item>
1470
1540
  </style>
1471
- </resources>
1541
+ ${launch.base}</resources>
1472
1542
  `;
1473
1543
  writeFileSync(join3(output, "app/src/main/res/values/themes.xml"), themesXml);
1544
+ mkdirSync2(join3(output, "app/src/main/res/values-v31"), { recursive: true });
1545
+ writeFileSync(join3(output, "app/src/main/res/values-v31/themes.xml"), launch.v31);
1546
+ if (launch.background)
1547
+ writeFileSync(join3(output, "app/src/main/res/drawable/craft_launch_background.xml"), launch.background);
1548
+ else
1549
+ rmSync2(join3(output, "app/src/main/res/drawable/craft_launch_background.xml"), { force: true });
1474
1550
  const activityMainXml = config.renderer === "native" ? `<?xml version="1.0" encoding="utf-8"?>
1475
1551
  <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
1476
1552
  android:id="@+id/native_root"
@@ -1694,6 +1770,8 @@ async function run(options) {
1694
1770
  export {
1695
1771
  ANDROID_MIN_SDK,
1696
1772
  HEALTH_CONNECT_PERMISSIONS,
1773
+ androidLaunchThemes,
1774
+ androidSplashFiles,
1697
1775
  build,
1698
1776
  compressNativeSources,
1699
1777
  createAndroidEmulator,
@@ -1704,6 +1782,7 @@ export {
1704
1782
  nativeMessages,
1705
1783
  nativeRouteBundles,
1706
1784
  nativeRouteRuntime,
1785
+ nativeSensorCapabilities,
1707
1786
  open,
1708
1787
  renderAndroidDeepLinks,
1709
1788
  renderAndroidPermissions,
@@ -1715,4 +1794,4 @@ export {
1715
1794
  syncNativeFonts
1716
1795
  };
1717
1796
 
1718
- //# debugId=6765F03CB34C0C1D64756E2164756E21
1797
+ //# debugId=607F99B9A5B4A43764756E2164756E21