craft-native 0.0.436 → 0.0.442
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +30 -0
- package/README.md +35 -0
- package/dist/android/src/index.d.ts +8 -0
- package/dist/android/src/index.js +17 -2
- package/dist/android/templates/CraftHealthServices.kt.template +2 -0
- package/dist/android/templates/CraftNativeCapabilities.kt.template +42 -1
- package/dist/android/templates/CraftNativeMotion.kt.template +269 -0
- package/dist/android/templates/CraftNativeServices.kt.template +6 -1
- package/dist/android/templates/CraftPassiveSteps.kt.template +159 -0
- package/dist/android/templates/CraftRecordingPressure.kt.template +156 -0
- package/dist/android/templates/LocationRecordingService.kt.template +77 -9
- package/dist/android/templates/MainActivityNative.kt.template +129 -1
- package/dist/cli.js +289 -108
- package/dist/components/index.d.ts +25 -0
- package/dist/index.cjs +207 -27
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +321 -140
- package/dist/installer-background.d.ts +2 -0
- package/dist/installer-layout.d.ts +1 -0
- package/dist/ios/src/index.d.ts +10 -0
- package/dist/ios/src/index.js +28 -2
- package/dist/ios/templates/CraftApp.swift +14 -0
- package/dist/ios/templates/CraftHealth.swift +2 -1
- package/dist/ios/templates/CraftHealthBackground.swift +308 -0
- package/dist/ios/templates/CraftNativeActions.swift +153 -0
- package/dist/ios/templates/CraftNativePedometer.swift +181 -0
- package/dist/ios/templates/CraftNativeScreen.swift +76 -0
- package/dist/macos/src/index.js +1 -1
- package/dist/macos/src/toolchain.d.ts +1 -1
- package/dist/macos/templates/CraftMacApplication.swift +124 -39
- package/dist/macos/templates/CraftMacRenderer.swift +111 -33
- package/dist/macos/templates/CraftMacRuntime.swift +270 -81
- package/dist/package.d.ts +40 -0
- package/dist/types.d.ts +26 -0
- package/package.json +1 -1
package/LICENSE.md
CHANGED
|
@@ -19,3 +19,33 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
19
19
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
20
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
21
|
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
## Installer layout notices
|
|
24
|
+
|
|
25
|
+
The Finder store and Carbon Alias format implementation follows the MIT-licensed
|
|
26
|
+
references [gwend/dsstore](https://github.com/gwend/dsstore),
|
|
27
|
+
[rigsmith/rigsmith](https://github.com/rigsmith/rigsmith/tree/main/core/dsstore), and
|
|
28
|
+
[al45tair/mac_alias](https://github.com/al45tair/mac_alias).
|
|
29
|
+
|
|
30
|
+
Copyright (c) 2020 Kenty Gwend
|
|
31
|
+
Copyright (c) 2026 John Campion Jr
|
|
32
|
+
Copyright (c) 2014 Alastair Houghton
|
|
33
|
+
Copyright (c) 2022 Russell Keith-Magee
|
|
34
|
+
|
|
35
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
36
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
37
|
+
in the Software without restriction, including without limitation the rights
|
|
38
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
39
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
40
|
+
furnished to do so, subject to the following conditions:
|
|
41
|
+
|
|
42
|
+
The above copyright notice and this permission notice shall be included in all
|
|
43
|
+
copies or substantial portions of the Software.
|
|
44
|
+
|
|
45
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
46
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
47
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
48
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
49
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
50
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
51
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -740,3 +740,38 @@ Craft's bundled-backend host reads `backend: { executable: 'stx-backend' }` from
|
|
|
740
740
|
the helper with `--serve --managed`, and consumes its authenticated loopback URL.
|
|
741
741
|
The helper's standard input is a lifetime lease: it must stop when that pipe
|
|
742
742
|
closes. `craft --capabilities` advertises `bundleBackend` on macOS.
|
|
743
|
+
|
|
744
|
+
## Drag-to-Applications disk images
|
|
745
|
+
|
|
746
|
+
`createDiskImage` packages an existing macOS app bundle with an Applications
|
|
747
|
+
shortcut, installation artwork and a Finder icon layout. The layout is written
|
|
748
|
+
without launching Finder or requesting Automation permission.
|
|
749
|
+
|
|
750
|
+
```ts
|
|
751
|
+
import { createDiskImage } from 'craft-native'
|
|
752
|
+
|
|
753
|
+
const image = await createDiskImage({
|
|
754
|
+
appBundlePath: './dist/Example.app',
|
|
755
|
+
outputPath: './dist/Example.dmg',
|
|
756
|
+
volumeName: 'Example',
|
|
757
|
+
})
|
|
758
|
+
if (!image.success) throw new Error(image.error)
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
Signing and notarization belong to the publishing pipeline. Stacks applications
|
|
762
|
+
use Buddy and Pantry's native workflow to sign, notarize and staple the image
|
|
763
|
+
before attaching its ZIP archive. Craft's updater accepts both app ZIPs and ZIPs
|
|
764
|
+
containing a single DMG, and verifies the extracted app before installation.
|
|
765
|
+
|
|
766
|
+
The shipped LICENSE file includes the installer layout's third-party notices.
|
|
767
|
+
|
|
768
|
+
## Framework bridge types
|
|
769
|
+
|
|
770
|
+
Core owns the optional `Window.craft` declaration. React, Svelte and Vue bindings
|
|
771
|
+
depend on core for types, so importing a binding alongside `craft-native` does
|
|
772
|
+
not redeclare that global with an incompatible shape.
|
|
773
|
+
|
|
774
|
+
`CraftBindingsAPI` describes the bindings' existing flat host API (`getPlatform`,
|
|
775
|
+
`invoke`, and related methods). Their hooks and stores retain that view and their
|
|
776
|
+
runtime behavior. This type does not install a bridge, add flat methods to a
|
|
777
|
+
desktop host, or replace the structured `CraftBridgeAPI` and typed event APIs.
|
|
@@ -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
|
|
@@ -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,
|
|
@@ -1013,10 +1017,14 @@ function renderAndroidPermissions(config) {
|
|
|
1013
1017
|
}
|
|
1014
1018
|
if (config.enablePushNotifications)
|
|
1015
1019
|
permissions.add("android.permission.POST_NOTIFICATIONS");
|
|
1016
|
-
if (config.enableKeepAwake)
|
|
1020
|
+
if (config.enableKeepAwake || config.enableBarometer && config.enableBackgroundLocation)
|
|
1017
1021
|
permissions.add("android.permission.WAKE_LOCK");
|
|
1018
1022
|
if (config.enableHealthConnect)
|
|
1019
1023
|
healthConnectPermissions(config).forEach((permission) => permissions.add(permission));
|
|
1024
|
+
if (config.enablePedometer)
|
|
1025
|
+
permissions.add("android.permission.ACTIVITY_RECOGNITION");
|
|
1026
|
+
if (config.enablePedometer && config.enableHealthServices)
|
|
1027
|
+
permissions.add("android.permission.RECEIVE_BOOT_COMPLETED");
|
|
1020
1028
|
if (config.renderer === "native" && config.backgroundRefresh?.enabled)
|
|
1021
1029
|
permissions.add("android.permission.RECEIVE_BOOT_COMPLETED");
|
|
1022
1030
|
if (config.enableHeartRate) {
|
|
@@ -1056,13 +1064,16 @@ function writeNativeShellSources(output, packagePath, packageName, config) {
|
|
|
1056
1064
|
write("CraftSnapshotFile.kt.template", "CraftSnapshotFile.kt");
|
|
1057
1065
|
write("CraftNativeImageCache.kt.template", "CraftNativeImageCache.kt");
|
|
1058
1066
|
write("CraftNativeHaptics.kt.template", "CraftNativeHaptics.kt");
|
|
1067
|
+
write("CraftNativeMotion.kt.template", "CraftNativeMotion.kt");
|
|
1059
1068
|
write("CraftNativeBackground.kt.template", "CraftNativeBackground.kt");
|
|
1060
1069
|
write(config.enableHealthConnect ? "CraftNativeHealth.kt.template" : "CraftNativeHealthStub.kt.template", "CraftNativeHealth.kt");
|
|
1061
1070
|
write(config.enableHealthServices ? "CraftHealthServices.kt.template" : "CraftHealthServicesStub.kt.template", "CraftHealthServices.kt");
|
|
1062
1071
|
if (config.enableHealthServices) {
|
|
1063
1072
|
write("CraftHealthServicesService.kt.template", "CraftHealthServicesService.kt");
|
|
1073
|
+
write("CraftPassiveSteps.kt.template", "CraftPassiveSteps.kt");
|
|
1064
1074
|
} else {
|
|
1065
1075
|
rmSync2(join3(javaDir, "CraftHealthServicesService.kt"), { force: true });
|
|
1076
|
+
rmSync2(join3(javaDir, "CraftPassiveSteps.kt"), { force: true });
|
|
1066
1077
|
}
|
|
1067
1078
|
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
1079
|
import com.google.android.gms.pay.PayApiAvailabilityStatus
|
|
@@ -1348,6 +1359,7 @@ async function init(options) {
|
|
|
1348
1359
|
const nativeDir = join3(output, "app/src/main/java/com/craft/runtime");
|
|
1349
1360
|
mkdirSync2(nativeDir, { recursive: true });
|
|
1350
1361
|
writeFileSync(join3(nativeDir, "CraftNative.kt"), craftNative);
|
|
1362
|
+
writeFileSync(join3(nativeDir, "CraftRecordingPressure.kt"), readFileSync2(join3(TEMPLATES_DIR, "CraftRecordingPressure.kt.template"), "utf-8"));
|
|
1351
1363
|
const serviceTemplate = readFileSync2(join3(TEMPLATES_DIR, "LocationRecordingService.kt.template"), "utf-8");
|
|
1352
1364
|
writeFileSync(join3(nativeDir, "LocationRecordingService.kt"), serviceTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
1353
1365
|
const healthTemplate = readFileSync2(join3(TEMPLATES_DIR, config.enableHealthConnect ? "CraftHealthConnect.kt.template" : "CraftHealthConnectStub.kt.template"), "utf-8");
|
|
@@ -1356,7 +1368,9 @@ async function init(options) {
|
|
|
1356
1368
|
writeFileSync(join3(output, "app/src/main/java", packagePath, file), readFileSync2(join3(TEMPLATES_DIR, template), "utf8").replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
1357
1369
|
}
|
|
1358
1370
|
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" />'
|
|
1371
|
+
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 ? `
|
|
1372
|
+
<service android:name=".CraftPassiveStepsService" android:exported="true" android:permission="com.google.android.wearable.healthservices.permission.PASSIVE_DATA_BINDING" />
|
|
1373
|
+
<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
1374
|
<package android:name="com.google.android.apps.healthdata" />
|
|
1361
1375
|
</queries>` : "").replace(/\{\{HEALTH_CONNECT_RATIONALE\}\}/g, config.enableHealthConnect ? ` <intent-filter>
|
|
1362
1376
|
<action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
|
|
@@ -1704,6 +1718,7 @@ export {
|
|
|
1704
1718
|
nativeMessages,
|
|
1705
1719
|
nativeRouteBundles,
|
|
1706
1720
|
nativeRouteRuntime,
|
|
1721
|
+
nativeSensorCapabilities,
|
|
1707
1722
|
open,
|
|
1708
1723
|
renderAndroidDeepLinks,
|
|
1709
1724
|
renderAndroidPermissions,
|
|
@@ -20,9 +20,11 @@ internal class CraftHealthServices(
|
|
|
20
20
|
private val activityProvider: () -> Activity?,
|
|
21
21
|
private val fallback: ((String, JSONArray, (CraftNativeCapabilityResult) -> Unit) -> Unit)? = null,
|
|
22
22
|
) {
|
|
23
|
+
private val passiveSteps by lazy { CraftPassiveSteps(context) }
|
|
23
24
|
private fun failure(code: String, message: String) = CraftNativeCapabilityResult(code = code, message = message)
|
|
24
25
|
|
|
25
26
|
fun perform(method: String, args: JSONArray, done: (CraftNativeCapabilityResult) -> Unit) {
|
|
27
|
+
if (method.startsWith("passive:")) { passiveSteps.perform(method.removePrefix("passive:"), done); return }
|
|
26
28
|
when (method) {
|
|
27
29
|
"startWorkout" -> start(args.optJSONObject(0) ?: JSONObject(), done)
|
|
28
30
|
"pauseWorkout" -> command(CraftHealthServicesService.ACTION_PAUSE, done)
|
|
@@ -22,6 +22,7 @@ import android.net.NetworkCapabilities
|
|
|
22
22
|
import android.provider.MediaStore
|
|
23
23
|
import android.util.Base64
|
|
24
24
|
import androidx.core.content.ContextCompat
|
|
25
|
+
import androidx.core.app.ActivityCompat
|
|
25
26
|
import androidx.biometric.BiometricManager
|
|
26
27
|
import androidx.biometric.BiometricPrompt
|
|
27
28
|
import androidx.fragment.app.FragmentActivity
|
|
@@ -50,6 +51,7 @@ private const val CRAFT_NATIVE_FILE_PICK_FIRST = 0x4000
|
|
|
50
51
|
private const val CRAFT_NATIVE_FILE_PICK_LAST = 0xffff
|
|
51
52
|
private const val CRAFT_NATIVE_FILE_PICK_STATE = "craft.filePicker.nextCode"
|
|
52
53
|
private const val CRAFT_NATIVE_NOTIFICATION_PERMISSION_REQUEST = 4102
|
|
54
|
+
private const val CRAFT_NATIVE_PEDOMETER_PERMISSION_REQUEST = 4358
|
|
53
55
|
private const val CRAFT_NATIVE_FILE_DEFAULT_MAX_PIXELS = 2048
|
|
54
56
|
private const val CRAFT_NATIVE_FILE_LARGEST_MAX_PIXELS = 8192
|
|
55
57
|
private const val CRAFT_NATIVE_FILE_DEFAULT_QUALITY = 0.85
|
|
@@ -88,6 +90,8 @@ internal class CraftNativeCapabilities(
|
|
|
88
90
|
override fun dismiss(requestCode: Int) { host.finishActivity(requestCode) }
|
|
89
91
|
}
|
|
90
92
|
},
|
|
93
|
+
private val requestPedometerPermission: ((Int) -> Unit)? = null,
|
|
94
|
+
private val checkPedometerPermission: (() -> Boolean)? = null,
|
|
91
95
|
) {
|
|
92
96
|
companion object {
|
|
93
97
|
private val pickerCodes = CraftNativePickerCodes()
|
|
@@ -127,6 +131,8 @@ internal class CraftNativeCapabilities(
|
|
|
127
131
|
private val fileRoot by lazy { File(context.cacheDir, "craft-files").apply { mkdirs() } }
|
|
128
132
|
private var pendingFilePick: PendingFilePick? = null
|
|
129
133
|
private var pendingNotificationPermission: ((Boolean) -> Unit)? = null
|
|
134
|
+
private var pendingPedometerPermission: Pair<String, (CraftNativeCapabilityResult) -> Unit>? = null
|
|
135
|
+
private var pedometerPermissionInFlight = false
|
|
130
136
|
private var database: SQLiteDatabase? = null
|
|
131
137
|
private val biometricPrompts = ConcurrentHashMap<String, BiometricPrompt>()
|
|
132
138
|
|
|
@@ -214,6 +220,29 @@ internal class CraftNativeCapabilities(
|
|
|
214
220
|
if (method == "randomUUID") complete(requestToken, success(UUID.randomUUID().toString()), completion)
|
|
215
221
|
else complete(requestToken, failure("UNKNOWN_ACTION", "Unsupported Crypto method"), completion)
|
|
216
222
|
}
|
|
223
|
+
"Pedometer" -> {
|
|
224
|
+
if (!config.optBoolean("enablePedometer")) complete(requestToken, failure("CAPABILITY_DISABLED", "Pedometer is disabled"), completion)
|
|
225
|
+
else if (method != "requestPermission") complete(requestToken, failure("UNKNOWN_ACTION", "Unsupported Pedometer permission method"), completion)
|
|
226
|
+
else onMain {
|
|
227
|
+
if (cancelled.remove(requestToken) || closed.get()) return@onMain
|
|
228
|
+
val host = activity
|
|
229
|
+
if (host == null && requestPedometerPermission == null) complete(requestToken, failure("NEEDS_ACTIVITY", "Pedometer permission needs a screen"), completion)
|
|
230
|
+
else if (pedometerPermissionGranted()) complete(requestToken, success("granted"), completion)
|
|
231
|
+
else if (pedometerPermissionInFlight) complete(requestToken, failure("PERMISSION_BUSY", "An activity recognition request is already open"), completion)
|
|
232
|
+
else {
|
|
233
|
+
pendingPedometerPermission = requestToken to completion
|
|
234
|
+
pedometerPermissionInFlight = true
|
|
235
|
+
try {
|
|
236
|
+
if (requestPedometerPermission != null) requestPedometerPermission.invoke(CRAFT_NATIVE_PEDOMETER_PERMISSION_REQUEST)
|
|
237
|
+
else ActivityCompat.requestPermissions(requireNotNull(host), arrayOf(Manifest.permission.ACTIVITY_RECOGNITION), CRAFT_NATIVE_PEDOMETER_PERMISSION_REQUEST)
|
|
238
|
+
} catch (error: Exception) {
|
|
239
|
+
pendingPedometerPermission = null
|
|
240
|
+
pedometerPermissionInFlight = false
|
|
241
|
+
complete(requestToken, failure("PERMISSION_ERROR", error.message ?: "Permission could not be requested"), completion)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
217
246
|
"Bluetooth" -> {
|
|
218
247
|
if (!config.optBoolean("enableBluetooth")) complete(requestToken, failure("CAPABILITY_DISABLED", "Bluetooth is disabled"), completion)
|
|
219
248
|
else bluetooth.perform(method, args) { complete(requestToken, it, completion) }
|
|
@@ -268,6 +297,7 @@ internal class CraftNativeCapabilities(
|
|
|
268
297
|
cancelled.add(requestToken)
|
|
269
298
|
fetchConnections.remove(requestToken)?.disconnect()
|
|
270
299
|
onMain {
|
|
300
|
+
if (pendingPedometerPermission?.first == requestToken) pendingPedometerPermission = null
|
|
271
301
|
if (pendingFilePick?.requestToken == requestToken) {
|
|
272
302
|
dismissFilePick()
|
|
273
303
|
cancelled.remove(requestToken)
|
|
@@ -285,13 +315,24 @@ internal class CraftNativeCapabilities(
|
|
|
285
315
|
networkExecutor.shutdownNow()
|
|
286
316
|
fetchConnections.values.forEach(HttpURLConnection::disconnect)
|
|
287
317
|
fetchConnections.clear()
|
|
288
|
-
onMain { dismissFilePick() }
|
|
318
|
+
onMain { dismissFilePick(); pendingPedometerPermission = null; pedometerPermissionInFlight = false }
|
|
289
319
|
database?.close()
|
|
290
320
|
database = null
|
|
291
321
|
biometricPrompts.values.forEach { it.cancelAuthentication() }
|
|
292
322
|
biometricPrompts.clear()
|
|
293
323
|
}
|
|
294
324
|
|
|
325
|
+
fun pedometerPermissionGranted(): Boolean = checkPedometerPermission?.invoke() ?: (Build.VERSION.SDK_INT < 29 ||
|
|
326
|
+
ContextCompat.checkSelfPermission(context, Manifest.permission.ACTIVITY_RECOGNITION) == PackageManager.PERMISSION_GRANTED)
|
|
327
|
+
|
|
328
|
+
fun onPedometerPermissions(requestCode: Int) {
|
|
329
|
+
if (requestCode != CRAFT_NATIVE_PEDOMETER_PERMISSION_REQUEST) return
|
|
330
|
+
pedometerPermissionInFlight = false
|
|
331
|
+
val pending = pendingPedometerPermission
|
|
332
|
+
pendingPedometerPermission = null
|
|
333
|
+
pending?.let { (token, completion) -> complete(token, success(if (pedometerPermissionGranted()) "granted" else "denied"), completion) }
|
|
334
|
+
}
|
|
335
|
+
|
|
295
336
|
private fun onMain(work: () -> Unit) {
|
|
296
337
|
if (android.os.Looper.myLooper() == android.os.Looper.getMainLooper()) work()
|
|
297
338
|
else mainHandler.post { work() }
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
package {{PACKAGE_NAME}}
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.hardware.Sensor
|
|
5
|
+
import android.hardware.SensorEvent
|
|
6
|
+
import android.hardware.SensorEventListener
|
|
7
|
+
import android.hardware.SensorManager
|
|
8
|
+
import android.os.Handler
|
|
9
|
+
import android.os.Looper
|
|
10
|
+
import org.json.JSONArray
|
|
11
|
+
import org.json.JSONObject
|
|
12
|
+
|
|
13
|
+
/** Registration owns its listener. Closing an old registration cannot stop a replacement. */
|
|
14
|
+
internal interface CraftMotionRegistration { fun close() }
|
|
15
|
+
internal interface CraftMotionSource {
|
|
16
|
+
fun available(type: Int): Boolean
|
|
17
|
+
fun register(type: Int, periodUs: Int, sample: (FloatArray, Long, Int) -> Unit): CraftMotionRegistration?
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
internal class CraftAndroidMotionSource(context: Context) : CraftMotionSource {
|
|
21
|
+
private val manager = context.getSystemService(Context.SENSOR_SERVICE) as? SensorManager
|
|
22
|
+
override fun available(type: Int): Boolean = manager?.getDefaultSensor(type) != null
|
|
23
|
+
override fun register(type: Int, periodUs: Int, sample: (FloatArray, Long, Int) -> Unit): CraftMotionRegistration? {
|
|
24
|
+
val manager = manager ?: return null
|
|
25
|
+
val sensor = manager.getDefaultSensor(type) ?: return null
|
|
26
|
+
val listener = object : SensorEventListener {
|
|
27
|
+
override fun onSensorChanged(event: SensorEvent) {
|
|
28
|
+
sample(event.values.clone(), event.timestamp, event.accuracy)
|
|
29
|
+
}
|
|
30
|
+
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
if (!manager.registerListener(listener, sensor, periodUs, Handler(Looper.getMainLooper()))) {
|
|
34
|
+
manager.unregisterListener(listener)
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
} catch (error: Exception) {
|
|
38
|
+
manager.unregisterListener(listener)
|
|
39
|
+
throw error
|
|
40
|
+
}
|
|
41
|
+
return object : CraftMotionRegistration {
|
|
42
|
+
override fun close() { manager.unregisterListener(listener) }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Foreground, route-owned raw readings; deliberately distinct from iOS CMDeviceMotion. */
|
|
48
|
+
internal class CraftNativeMotion(private val source: CraftMotionSource) {
|
|
49
|
+
private data class Kind(val type: Int, val unit: String)
|
|
50
|
+
private val kinds = linkedMapOf(
|
|
51
|
+
"accelerometer" to Kind(Sensor.TYPE_ACCELEROMETER, "m/s²"),
|
|
52
|
+
"gyroscope" to Kind(Sensor.TYPE_GYROSCOPE, "rad/s"),
|
|
53
|
+
"magnetometer" to Kind(Sensor.TYPE_MAGNETIC_FIELD, "µT"),
|
|
54
|
+
"rotationVector" to Kind(Sensor.TYPE_ROTATION_VECTOR, "quaternion"),
|
|
55
|
+
)
|
|
56
|
+
private class Stream { var registration: CraftMotionRegistration? = null }
|
|
57
|
+
private val streams = mutableMapOf<String, Stream>()
|
|
58
|
+
private var closed = false
|
|
59
|
+
var emit: (JSONObject) -> Unit = {}
|
|
60
|
+
|
|
61
|
+
fun perform(method: String, args: JSONArray): CraftNativeCapabilityResult {
|
|
62
|
+
if (closed) return failure("HOST_CLOSED", "Motion host is closed")
|
|
63
|
+
return when (method) {
|
|
64
|
+
"getAvailability" -> CraftNativeCapabilityResult(data = JSONObject().apply {
|
|
65
|
+
kinds.forEach { (name, kind) -> put(name, source.available(kind.type)) }
|
|
66
|
+
})
|
|
67
|
+
"start" -> start(args.optJSONObject(0))
|
|
68
|
+
"stop" -> {
|
|
69
|
+
val name = args.opt(0) as? String
|
|
70
|
+
if (name !in kinds) failure("INVALID_ARGUMENT", "Unknown motion sensor")
|
|
71
|
+
else CraftNativeCapabilityResult(data = stop(name!!))
|
|
72
|
+
}
|
|
73
|
+
"stopAll" -> { stopAll(); CraftNativeCapabilityResult(data = true) }
|
|
74
|
+
else -> failure("UNKNOWN_ACTION", "Unsupported MotionSensors method")
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private fun start(options: JSONObject?): CraftNativeCapabilityResult {
|
|
79
|
+
val name = options?.opt("sensor") as? String
|
|
80
|
+
val kind = kinds[name] ?: return failure("INVALID_ARGUMENT", "Unknown motion sensor")
|
|
81
|
+
val rawInterval = if (options?.has("intervalMs") == true) options.opt("intervalMs") else 100
|
|
82
|
+
val interval = (rawInterval as? Number)?.toDouble() ?: Double.NaN
|
|
83
|
+
// Do not silently opt an app into Android's high sampling-rate permission.
|
|
84
|
+
if (!interval.isFinite() || interval < 5 || interval > 60_000 || interval % 1 != 0.0) {
|
|
85
|
+
return failure("INVALID_ARGUMENT", "intervalMs must be an integer from 5 through 60000")
|
|
86
|
+
}
|
|
87
|
+
if (!source.available(kind.type)) return failure("UNAVAILABLE", "Sensor is not present: $name")
|
|
88
|
+
val stream = Stream()
|
|
89
|
+
val registration = try {
|
|
90
|
+
source.register(kind.type, (interval * 1000).toInt()) { values, timestamp, accuracy ->
|
|
91
|
+
// Android can still deliver a queued sample after unregisterListener.
|
|
92
|
+
if (!closed && streams[name] === stream && values.size >= 3) {
|
|
93
|
+
val reading = if (kind.type == Sensor.TYPE_ROTATION_VECTOR) {
|
|
94
|
+
val quaternion = FloatArray(4)
|
|
95
|
+
SensorManager.getQuaternionFromVector(quaternion, values)
|
|
96
|
+
JSONObject().put("w", quaternion[0]).put("x", quaternion[1])
|
|
97
|
+
.put("y", quaternion[2]).put("z", quaternion[3])
|
|
98
|
+
} else JSONObject().put("x", values[0]).put("y", values[1]).put("z", values[2])
|
|
99
|
+
emit(JSONObject().put("sensor", name).put("values", reading).put("unit", kind.unit)
|
|
100
|
+
.put("timestampMs", timestamp / 1_000_000.0).put("accuracy", accuracy))
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} catch (error: Exception) {
|
|
104
|
+
return failure("SENSOR_ERROR", error.message ?: "Sensor registration failed")
|
|
105
|
+
} ?: return failure("UNAVAILABLE", "Sensor registration was rejected: $name")
|
|
106
|
+
stream.registration = registration
|
|
107
|
+
// Validate and acquire the replacement before retiring a working stream.
|
|
108
|
+
val previous = streams.put(name!!, stream)
|
|
109
|
+
previous?.registration?.close()
|
|
110
|
+
return CraftNativeCapabilityResult(data = true)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private fun failure(code: String, message: String) = CraftNativeCapabilityResult(code = code, message = message)
|
|
114
|
+
private fun stop(name: String): Boolean {
|
|
115
|
+
val stream = streams.remove(name) ?: return false
|
|
116
|
+
stream.registration?.close()
|
|
117
|
+
return true
|
|
118
|
+
}
|
|
119
|
+
fun stopAll() { streams.keys.toList().forEach(::stop) }
|
|
120
|
+
fun close() { closed = true; stopAll(); emit = {} }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** A pressure-only stream with a new reference on each successful start. */
|
|
124
|
+
internal class CraftNativeBarometer(private val source: CraftMotionSource) {
|
|
125
|
+
private class Stream {
|
|
126
|
+
var registration: CraftMotionRegistration? = null
|
|
127
|
+
var reference: Float? = null
|
|
128
|
+
}
|
|
129
|
+
private var stream: Stream? = null
|
|
130
|
+
private var closed = false
|
|
131
|
+
var emit: (JSONObject) -> Unit = {}
|
|
132
|
+
|
|
133
|
+
fun perform(method: String, args: JSONArray): CraftNativeCapabilityResult {
|
|
134
|
+
if (closed) return failure("HOST_CLOSED", "Barometer host is closed")
|
|
135
|
+
return when (method) {
|
|
136
|
+
"getAvailability" -> CraftNativeCapabilityResult(data = JSONObject()
|
|
137
|
+
.put("relativeAltitude", source.available(Sensor.TYPE_PRESSURE))
|
|
138
|
+
.put("absoluteAltitude", false).put("background", false))
|
|
139
|
+
"start" -> start(args.optJSONObject(0), args.length() != 0)
|
|
140
|
+
"stop" -> CraftNativeCapabilityResult(data = stop("requested"))
|
|
141
|
+
else -> failure("UNKNOWN_ACTION", "Unsupported Barometer method")
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private fun start(options: JSONObject?, supplied: Boolean): CraftNativeCapabilityResult {
|
|
146
|
+
if (supplied && options == null) return failure("INVALID_ARGUMENT", "Barometer.start needs an options object")
|
|
147
|
+
if (options?.has("background") == true && options.opt("background") !is Boolean)
|
|
148
|
+
return failure("INVALID_ARGUMENT", "background must be boolean")
|
|
149
|
+
if (options?.optBoolean("background", false) == true)
|
|
150
|
+
return failure("UNSUPPORTED", "Background barometer recording is not supported")
|
|
151
|
+
val raw = if (options?.has("intervalMs") == true) options.opt("intervalMs") else 100
|
|
152
|
+
val interval = (raw as? Number)?.toDouble() ?: Double.NaN
|
|
153
|
+
if (!interval.isFinite() || interval < 5 || interval > 60_000 || interval % 1 != 0.0)
|
|
154
|
+
return failure("INVALID_ARGUMENT", "intervalMs must be an integer from 5 through 60000")
|
|
155
|
+
if (!source.available(Sensor.TYPE_PRESSURE)) return failure("UNAVAILABLE", "Barometer is not present")
|
|
156
|
+
val next = Stream()
|
|
157
|
+
val registration = try {
|
|
158
|
+
source.register(Sensor.TYPE_PRESSURE, (interval * 1000).toInt()) { values, timestamp, _ ->
|
|
159
|
+
if (!closed && stream === next) {
|
|
160
|
+
val pressure = values.firstOrNull()
|
|
161
|
+
if (pressure == null || !pressure.isFinite() || pressure <= 0) {
|
|
162
|
+
stop("sensorError")
|
|
163
|
+
} else {
|
|
164
|
+
val reference = next.reference ?: pressure.also { next.reference = it }
|
|
165
|
+
val altitude = SensorManager.getAltitude(reference, pressure)
|
|
166
|
+
if (!altitude.isFinite()) stop("sensorError")
|
|
167
|
+
else emit(JSONObject().put("state", "sample").put("pressureHpa", pressure)
|
|
168
|
+
.put("relativeAltitudeMeters", altitude).put("referencePressureHpa", reference)
|
|
169
|
+
.put("timestampMs", timestamp / 1_000_000.0))
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
} catch (error: Exception) {
|
|
174
|
+
return failure("SENSOR_ERROR", error.message ?: "Barometer registration failed")
|
|
175
|
+
} ?: return failure("UNAVAILABLE", "Barometer registration was rejected")
|
|
176
|
+
next.registration = registration
|
|
177
|
+
val previous = stream
|
|
178
|
+
stream = next
|
|
179
|
+
previous?.registration?.close()
|
|
180
|
+
return CraftNativeCapabilityResult(data = true)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
fun stop(reason: String): Boolean {
|
|
184
|
+
val previous = stream ?: return false
|
|
185
|
+
stream = null
|
|
186
|
+
previous.registration?.close()
|
|
187
|
+
emit(JSONObject().put("state", "stopped").put("reason", reason))
|
|
188
|
+
return true
|
|
189
|
+
}
|
|
190
|
+
fun close() { closed = true; stop("disposed"); emit = {} }
|
|
191
|
+
private fun failure(code: String, message: String) = CraftNativeCapabilityResult(code = code, message = message)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Android counter and detector semantics stay distinct from interval totals. */
|
|
195
|
+
internal class CraftNativePedometer(private val source: CraftMotionSource, private val hasPermission: () -> Boolean) {
|
|
196
|
+
private class Stream { var registration: CraftMotionRegistration? = null }
|
|
197
|
+
private var stream: Stream? = null
|
|
198
|
+
private var closed = false
|
|
199
|
+
var emit: (JSONObject) -> Unit = {}
|
|
200
|
+
|
|
201
|
+
fun perform(method: String, args: JSONArray): CraftNativeCapabilityResult {
|
|
202
|
+
if (closed) return failure("HOST_CLOSED", "Pedometer host is closed")
|
|
203
|
+
return when (method) {
|
|
204
|
+
"getAvailability" -> CraftNativeCapabilityResult(data = JSONObject()
|
|
205
|
+
.put("counter", source.available(Sensor.TYPE_STEP_COUNTER))
|
|
206
|
+
.put("detector", source.available(Sensor.TYPE_STEP_DETECTOR))
|
|
207
|
+
.put("history", false).put("background", false).put("cadence", false).put("floors", false))
|
|
208
|
+
"getPermissionStatus" -> CraftNativeCapabilityResult(data = if (hasPermission()) "granted" else "denied")
|
|
209
|
+
"start" -> start(args.optJSONObject(0), args.length() != 0)
|
|
210
|
+
"stop" -> CraftNativeCapabilityResult(data = stop("requested"))
|
|
211
|
+
"query" -> failure("UNSUPPORTED", "Android's step sensor does not provide interval history")
|
|
212
|
+
else -> failure("UNKNOWN_ACTION", "Unsupported Pedometer method")
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private fun start(options: JSONObject?, supplied: Boolean): CraftNativeCapabilityResult {
|
|
217
|
+
if (supplied && options == null) return failure("INVALID_ARGUMENT", "Pedometer.start needs an options object")
|
|
218
|
+
if (options?.has("background") == true && options.opt("background") !is Boolean)
|
|
219
|
+
return failure("INVALID_ARGUMENT", "background must be boolean")
|
|
220
|
+
if (options?.optBoolean("background", false) == true)
|
|
221
|
+
return failure("UNSUPPORTED", "Background step streaming is not supported by this sensor adapter")
|
|
222
|
+
val name = if (options?.has("sensor") == true) options.opt("sensor") as? String else "counter"
|
|
223
|
+
val type = when (name) {
|
|
224
|
+
"counter" -> Sensor.TYPE_STEP_COUNTER
|
|
225
|
+
"detector" -> Sensor.TYPE_STEP_DETECTOR
|
|
226
|
+
else -> return failure("INVALID_ARGUMENT", "sensor must be counter or detector")
|
|
227
|
+
}
|
|
228
|
+
if (options?.has("startDate") == true || options?.has("endDate") == true)
|
|
229
|
+
return failure("UNSUPPORTED", "Android steps are not counts for a requested interval")
|
|
230
|
+
if (!source.available(type)) return failure("UNAVAILABLE", "Requested step sensor is not present")
|
|
231
|
+
if (!hasPermission()) return failure("PERMISSION_DENIED", "Activity recognition permission is required")
|
|
232
|
+
val next = Stream()
|
|
233
|
+
val registration = try {
|
|
234
|
+
source.register(type, 200_000) { values, timestamp, _ ->
|
|
235
|
+
if (!closed && stream === next) {
|
|
236
|
+
val value = values.firstOrNull()
|
|
237
|
+
if (!hasPermission()) stop("permissionDenied")
|
|
238
|
+
else if (value == null || !value.isFinite() || value < 0 || value % 1f != 0f || (name == "detector" && value != 1f)) stop("sensorError")
|
|
239
|
+
else {
|
|
240
|
+
val payload = JSONObject().put("state", "sample").put("kind", if (name == "counter") "counter" else "step")
|
|
241
|
+
.put("timestampMs", timestamp / 1_000_000.0)
|
|
242
|
+
if (name == "counter") payload.put("stepsSinceBoot", value.toDouble())
|
|
243
|
+
else payload.put("count", 1)
|
|
244
|
+
emit(payload)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
} catch (error: SecurityException) {
|
|
249
|
+
return failure("PERMISSION_DENIED", "Activity recognition permission is required")
|
|
250
|
+
} catch (error: Exception) {
|
|
251
|
+
return failure("SENSOR_ERROR", error.message ?: "Step sensor registration failed")
|
|
252
|
+
} ?: return failure("UNAVAILABLE", "Step sensor registration was rejected")
|
|
253
|
+
next.registration = registration
|
|
254
|
+
val previous = stream
|
|
255
|
+
stream = next
|
|
256
|
+
previous?.registration?.close()
|
|
257
|
+
return CraftNativeCapabilityResult(data = true)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
fun stop(reason: String): Boolean {
|
|
261
|
+
val previous = stream ?: return false
|
|
262
|
+
stream = null
|
|
263
|
+
previous.registration?.close()
|
|
264
|
+
emit(JSONObject().put("state", "stopped").put("reason", reason))
|
|
265
|
+
return true
|
|
266
|
+
}
|
|
267
|
+
fun close() { closed = true; stop("disposed"); emit = {} }
|
|
268
|
+
private fun failure(code: String, message: String) = CraftNativeCapabilityResult(code = code, message = message)
|
|
269
|
+
}
|
|
@@ -129,10 +129,12 @@ internal class CraftNativeServices(
|
|
|
129
129
|
add("healthAvailability")
|
|
130
130
|
}
|
|
131
131
|
if (healthServices != null) add("healthServices")
|
|
132
|
+
if (healthServices != null && config.optBoolean("enablePedometer")) add("pedometerPassive")
|
|
132
133
|
if (config.optBoolean("enableWallet")) add("wallet")
|
|
133
134
|
if (config.optBoolean("enableKeepAwake")) add("keepAwake")
|
|
134
135
|
if (config.optBoolean("enableGeolocation") || config.optBoolean("enableBackgroundLocation")) {
|
|
135
136
|
add("geolocation")
|
|
137
|
+
if (config.optBoolean("enableBarometer") && config.optBoolean("enableBackgroundLocation")) add("locationRecordingBarometer")
|
|
136
138
|
if (CraftNative.supportsRecordingOwnership()) add("locationRecordingOwnership")
|
|
137
139
|
if (CraftNative.supportsRecordingIntentIdentity()) add("locationRecordingIntentIdentity")
|
|
138
140
|
if (CraftNative.supportsRecordingPowerMode()) add("locationRecordingPowerMode")
|
|
@@ -146,7 +148,7 @@ internal class CraftNativeServices(
|
|
|
146
148
|
}
|
|
147
149
|
|
|
148
150
|
fun handles(module: String): Boolean = module in setOf(
|
|
149
|
-
"Dialog", "Browser", "Linking", "Share", "Push", "Health", "KeepAwake", "Brightness", "StatusBar", "Speech", "Background",
|
|
151
|
+
"Dialog", "Browser", "Linking", "Share", "Push", "Health", "PedometerPassive", "KeepAwake", "Brightness", "StatusBar", "Speech", "Background",
|
|
150
152
|
"Location", "Media", "CrashReporter", "HeartRate", "AudioRecording",
|
|
151
153
|
"Wallet",
|
|
152
154
|
)
|
|
@@ -165,6 +167,9 @@ internal class CraftNativeServices(
|
|
|
165
167
|
"Health" -> if (healthServices != null && method in setOf("startWorkout", "pauseWorkout", "resumeWorkout", "endWorkout")) {
|
|
166
168
|
healthServices.perform(method, args, done)
|
|
167
169
|
} else health?.perform(method, args, done) ?: done(failure("CAPABILITY_DISABLED", "Health is not enabled for this app"))
|
|
170
|
+
"PedometerPassive" -> if (config.optBoolean("enablePedometer") && healthServices != null) {
|
|
171
|
+
healthServices.perform("passive:$method", args, done)
|
|
172
|
+
} else done(failure("CAPABILITY_DISABLED", "Passive steps require pedometer and Health Services"))
|
|
168
173
|
"Wallet" -> wallet(method, args, done)
|
|
169
174
|
"KeepAwake" -> keepAwake(args.optBoolean(0, true), done)
|
|
170
175
|
"Brightness" -> brightness(method, args, done)
|