craft-native 0.0.246 → 0.0.248
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/android/src/index.d.ts +11 -0
- package/dist/android/src/index.js +33 -21
- package/dist/android/templates/AndroidManifest.xml.template +1 -0
- package/dist/android/templates/CraftBridge.kt.template +136 -1
- package/dist/android/templates/CraftHeartRate.kt.template +938 -0
- package/dist/android/templates/CraftHeartRateCore.kt.template +485 -0
- package/dist/android/templates/CraftNative.kt.template +17 -0
- package/dist/android/templates/CraftNativeFlatList.kt.template +115 -40
- package/dist/android/templates/CraftNativeMapGoogle.kt.template +92 -5
- package/dist/android/templates/CraftNativeMapLibre.kt.template +90 -3
- package/dist/android/templates/CraftNativeMutation.kt.template +32 -0
- package/dist/android/templates/CraftNativeRefreshableFlatList.kt.template +7 -2
- package/dist/android/templates/CraftNativeServices.kt.template +59 -1
- package/dist/android/templates/CraftNativeShell.kt.template +54 -14
- package/dist/android/templates/MainActivityNative.kt.template +369 -69
- package/dist/android/templates/build.gradle.kts.project.template +1 -1
- package/dist/api/mobile.d.ts +39 -3
- package/dist/browser.js +9 -9
- package/dist/cli.js +90 -673
- package/dist/components/index.d.ts +50 -9
- package/dist/index.cjs +394 -438
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +446 -493
- package/dist/ios/src/index.d.ts +2 -0
- package/dist/ios/src/index.js +54 -53
- package/dist/ios/templates/CraftApp.swift +37 -2
- package/dist/ios/templates/CraftHealth.swift +217 -0
- package/dist/ios/templates/CraftHeartRate.swift +6 -3
- package/dist/ios/templates/CraftHybrid.swift +18 -19
- package/dist/ios/templates/CraftLiveActivities.swift +1 -1
- package/dist/ios/templates/CraftNativeActions.swift +87 -8
- package/dist/ios/templates/CraftNativeFiles.swift +15 -3
- package/dist/ios/templates/CraftNativeFlatList.swift +18 -1
- package/dist/ios/templates/CraftNativeMutation.swift +36 -0
- package/dist/ios/templates/CraftNativeScreen.swift +813 -65
- package/dist/ios/templates/CraftNativeServices.swift +4 -2
- package/dist/ios/templates/CraftNativeUI.swift +10 -2
- package/dist/ios/templates/testing/DriverTests.swift.template +1 -0
- package/dist/ios/templates/testing/HostApp.swift +9 -2
- package/dist/macos/templates/CraftNativeMutation.swift +36 -0
- package/dist/macos/templates/native_layout.zig +226 -2
- package/dist/mobile-testing.js +21 -3
- package/dist/mobile.js +101 -63
- package/dist/testing/media-cache/media-cache.kt +4 -2
- package/package.json +1 -1
|
@@ -60,7 +60,18 @@ export interface CraftAndroidConfig {
|
|
|
60
60
|
enableGeolocation?: boolean;
|
|
61
61
|
enableBackgroundLocation?: boolean;
|
|
62
62
|
enableBluetooth?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* A Bluetooth heart-rate strap, or a watch broadcasting its heart rate (the
|
|
65
|
+
* standard Heart Rate Service), through `craft.heartRate` on a page and the
|
|
66
|
+
* `HeartRate` module on a native screen (CraftHeartRate.kt): scan, connect,
|
|
67
|
+
* live readings as `craftHeartRate` events, and a recording kept natively,
|
|
68
|
+
* in the foreground (a connectedDevice service) while it runs. The iOS
|
|
69
|
+
* builder's flag of the same name.
|
|
70
|
+
*/
|
|
71
|
+
enableHeartRate?: boolean;
|
|
63
72
|
enableHealthConnect?: boolean;
|
|
73
|
+
/** Google Wallet save-link handoff for native screens. */
|
|
74
|
+
enableWallet?: boolean;
|
|
64
75
|
/**
|
|
65
76
|
* The Health Connect data the app reads and writes, by the shared type
|
|
66
77
|
* names (`steps`, `sleep`, `workouts`...). The manifest declares only
|
|
@@ -18,6 +18,8 @@ import com.google.android.gms.maps.model.LatLng
|
|
|
18
18
|
import com.google.android.gms.maps.model.LatLngBounds
|
|
19
19
|
import com.google.android.gms.maps.model.Marker
|
|
20
20
|
import com.google.android.gms.maps.model.MarkerOptions
|
|
21
|
+
import com.google.android.gms.maps.model.Polygon
|
|
22
|
+
import com.google.android.gms.maps.model.PolygonOptions
|
|
21
23
|
import com.google.android.gms.maps.model.Polyline
|
|
22
24
|
import com.google.android.gms.maps.model.PolylineOptions`,
|
|
23
25
|
dependency: "com.google.android.gms:play-services-maps:18.2.0"
|
|
@@ -37,6 +39,7 @@ import org.maplibre.android.maps.OnMapReadyCallback
|
|
|
37
39
|
import org.maplibre.android.maps.Style
|
|
38
40
|
import org.maplibre.android.style.expressions.Expression
|
|
39
41
|
import org.maplibre.android.style.layers.CircleLayer
|
|
42
|
+
import org.maplibre.android.style.layers.FillLayer
|
|
40
43
|
import org.maplibre.android.style.layers.LineLayer
|
|
41
44
|
import org.maplibre.android.style.layers.SymbolLayer
|
|
42
45
|
import org.maplibre.android.style.layers.PropertyFactory.*
|
|
@@ -498,7 +501,9 @@ var DEFAULT_CONFIG = {
|
|
|
498
501
|
enableGeolocation: false,
|
|
499
502
|
enableBackgroundLocation: false,
|
|
500
503
|
enableBluetooth: false,
|
|
504
|
+
enableHeartRate: false,
|
|
501
505
|
enableHealthConnect: false,
|
|
506
|
+
enableWallet: false,
|
|
502
507
|
enableKeepAwake: false,
|
|
503
508
|
enableDeepLinks: false,
|
|
504
509
|
urlSchemes: [],
|
|
@@ -790,7 +795,11 @@ function renderAndroidPermissions(config) {
|
|
|
790
795
|
healthConnectPermissions(config).forEach((permission) => permissions.add(permission));
|
|
791
796
|
if (config.renderer === "native" && config.backgroundRefresh?.enabled)
|
|
792
797
|
permissions.add("android.permission.RECEIVE_BOOT_COMPLETED");
|
|
793
|
-
|
|
798
|
+
if (config.enableHeartRate) {
|
|
799
|
+
permissions.add("android.permission.FOREGROUND_SERVICE");
|
|
800
|
+
permissions.add("android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE");
|
|
801
|
+
}
|
|
802
|
+
const bluetooth = config.enableBluetooth || config.enableHeartRate ? [
|
|
794
803
|
' <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />',
|
|
795
804
|
' <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />',
|
|
796
805
|
' <uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />',
|
|
@@ -1019,7 +1028,7 @@ async function init(options) {
|
|
|
1019
1028
|
writeNativeShellSources(output, packagePath, finalPackageName, config);
|
|
1020
1029
|
}
|
|
1021
1030
|
const craftBridgeTemplate = readFileSync2(join3(TEMPLATES_DIR, "CraftBridge.kt.template"), "utf-8");
|
|
1022
|
-
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 {
|
|
1031
|
+
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(/\{\{ENABLE_HEART_RATE\}\}/g, String(Boolean(config.enableHeartRate))).replace(/\{\{FIREBASE_IMPORT\}\}/g, config.enablePushNotifications ? "import com.google.firebase.messaging.FirebaseMessaging" : "").replace(/\{\{REGISTER_PUSH_IMPLEMENTATION\}\}/g, config.enablePushNotifications ? `activity.runOnUiThread {
|
|
1023
1032
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
|
|
1024
1033
|
&& ContextCompat.checkSelfPermission(activity, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
|
1025
1034
|
ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 4204)
|
|
@@ -1047,8 +1056,11 @@ async function init(options) {
|
|
|
1047
1056
|
writeFileSync(join3(nativeDir, "LocationRecordingService.kt"), serviceTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
1048
1057
|
const healthTemplate = readFileSync2(join3(TEMPLATES_DIR, config.enableHealthConnect ? "CraftHealthConnect.kt.template" : "CraftHealthConnectStub.kt.template"), "utf-8");
|
|
1049
1058
|
writeFileSync(join3(output, "app/src/main/java", packagePath, "CraftHealthConnect.kt"), healthTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
1059
|
+
for (const [template, file] of [["CraftHeartRateCore.kt.template", "CraftHeartRateCore.kt"], ["CraftHeartRate.kt.template", "CraftHeartRate.kt"]]) {
|
|
1060
|
+
writeFileSync(join3(output, "app/src/main/java", packagePath, file), readFileSync2(join3(TEMPLATES_DIR, template), "utf8").replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
1061
|
+
}
|
|
1050
1062
|
const manifestTemplate = readFileSync2(join3(TEMPLATES_DIR, "AndroidManifest.xml.template"), "utf-8");
|
|
1051
|
-
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>
|
|
1063
|
+
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(/\{\{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>
|
|
1052
1064
|
<package android:name="com.google.android.apps.healthdata" />
|
|
1053
1065
|
</queries>` : "").replace(/\{\{HEALTH_CONNECT_RATIONALE\}\}/g, config.enableHealthConnect ? ` <intent-filter>
|
|
1054
1066
|
<action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
|
|
@@ -1374,23 +1386,23 @@ async function run(options) {
|
|
|
1374
1386
|
}
|
|
1375
1387
|
}
|
|
1376
1388
|
export {
|
|
1377
|
-
|
|
1378
|
-
syncAndroidWebAssets,
|
|
1379
|
-
runKotlinFixture,
|
|
1380
|
-
run,
|
|
1381
|
-
resolveRuntimeDir,
|
|
1382
|
-
renderAndroidPermissions,
|
|
1383
|
-
renderAndroidDeepLinks,
|
|
1384
|
-
open,
|
|
1385
|
-
nativeRouteRuntime,
|
|
1386
|
-
nativeRouteBundles,
|
|
1387
|
-
nativeMessages,
|
|
1388
|
-
nativeMapProviders,
|
|
1389
|
-
installRuntime,
|
|
1390
|
-
init,
|
|
1391
|
-
healthConnectPermissions,
|
|
1392
|
-
createAndroidEmulator,
|
|
1393
|
-
build,
|
|
1389
|
+
ANDROID_MIN_SDK,
|
|
1394
1390
|
HEALTH_CONNECT_PERMISSIONS,
|
|
1395
|
-
|
|
1391
|
+
build,
|
|
1392
|
+
createAndroidEmulator,
|
|
1393
|
+
healthConnectPermissions,
|
|
1394
|
+
init,
|
|
1395
|
+
installRuntime,
|
|
1396
|
+
nativeMapProviders,
|
|
1397
|
+
nativeMessages,
|
|
1398
|
+
nativeRouteBundles,
|
|
1399
|
+
nativeRouteRuntime,
|
|
1400
|
+
open,
|
|
1401
|
+
renderAndroidDeepLinks,
|
|
1402
|
+
renderAndroidPermissions,
|
|
1403
|
+
resolveRuntimeDir,
|
|
1404
|
+
run,
|
|
1405
|
+
runKotlinFixture,
|
|
1406
|
+
syncAndroidWebAssets,
|
|
1407
|
+
syncNativeFonts
|
|
1396
1408
|
};
|
|
@@ -444,6 +444,9 @@ class CraftBridge(
|
|
|
444
444
|
bluetooth: false,
|
|
445
445
|
nfc: false,
|
|
446
446
|
health: {{ENABLE_HEALTH_CONNECT}},
|
|
447
|
+
heartRate: {{ENABLE_HEART_RATE}},
|
|
448
|
+
heartRateRecordingOwnership: {{ENABLE_HEART_RATE}},
|
|
449
|
+
heartRateRecordingIntentIdentity: {{ENABLE_HEART_RATE}},
|
|
447
450
|
screenCapture: false
|
|
448
451
|
},
|
|
449
452
|
|
|
@@ -1808,6 +1811,85 @@ class CraftBridge(
|
|
|
1808
1811
|
readRecording: function(options) { return recordingCall('readRecording', options); },
|
|
1809
1812
|
acknowledgeRecording: function(options) { return recordingCall('acknowledgeRecording', options); }
|
|
1810
1813
|
};
|
|
1814
|
+
// A Bluetooth heart-rate strap, or a watch broadcasting its
|
|
1815
|
+
// heart rate (CraftHeartRate.kt), in the iOS page's names.
|
|
1816
|
+
// Readings, devices found by scan() and connection changes
|
|
1817
|
+
// arrive as craftHeartRate events ({type: 'reading' | 'device'
|
|
1818
|
+
// | 'state'}); startRecording() keeps every reading natively,
|
|
1819
|
+
// screen locked or app closed, until stopRecording(). Calls
|
|
1820
|
+
// that wait on Android's Nearby devices prompt have no deadline.
|
|
1821
|
+
var heartRateSequence = 0;
|
|
1822
|
+
var heartRateRequests = Object.create(null);
|
|
1823
|
+
var heartRateClosed = false;
|
|
1824
|
+
window.__craftHeartRateResult = function(id, value) {
|
|
1825
|
+
var pending = heartRateRequests[id];
|
|
1826
|
+
if (!pending) return;
|
|
1827
|
+
delete heartRateRequests[id];
|
|
1828
|
+
pending.resolve(value);
|
|
1829
|
+
};
|
|
1830
|
+
window.__craftHeartRateError = function(id, message, code) {
|
|
1831
|
+
var pending = heartRateRequests[id];
|
|
1832
|
+
if (!pending) return;
|
|
1833
|
+
delete heartRateRequests[id];
|
|
1834
|
+
var error = new Error(message);
|
|
1835
|
+
error.code = code || 'CRAFT_ERROR';
|
|
1836
|
+
error.bridge = true;
|
|
1837
|
+
pending.reject(error);
|
|
1838
|
+
};
|
|
1839
|
+
window.__craftRejectHeartRateRequests = function(message) {
|
|
1840
|
+
heartRateClosed = true;
|
|
1841
|
+
Object.keys(heartRateRequests).forEach(function(id) {
|
|
1842
|
+
var pending = heartRateRequests[id];
|
|
1843
|
+
delete heartRateRequests[id];
|
|
1844
|
+
pending.reject(new Error(message || 'Android bridge closed'));
|
|
1845
|
+
});
|
|
1846
|
+
};
|
|
1847
|
+
function heartRate(method, options) {
|
|
1848
|
+
options = options || {};
|
|
1849
|
+
if (['startRecording', 'pauseRecording', 'resumeRecording', 'stopRecording', 'readRecording', 'getRecordingState', 'acknowledgeRecording'].indexOf(method) !== -1) {
|
|
1850
|
+
if (Object.prototype.hasOwnProperty.call(options, 'recordingId') &&
|
|
1851
|
+
(typeof options.recordingId !== 'string' || !options.recordingId.trim())) {
|
|
1852
|
+
var invalidStart = new Error('recordingId must be a nonempty recording ID');
|
|
1853
|
+
invalidStart.code = 'INVALID_ARGUMENT';
|
|
1854
|
+
return Promise.reject(invalidStart);
|
|
1855
|
+
}
|
|
1856
|
+
if (Object.prototype.hasOwnProperty.call(options, 'expectedId') &&
|
|
1857
|
+
(typeof options.expectedId !== 'string' || !options.expectedId.trim())) {
|
|
1858
|
+
var invalid = new Error('expectedId must be a nonempty recording ID');
|
|
1859
|
+
invalid.code = 'INVALID_ARGUMENT';
|
|
1860
|
+
return Promise.reject(invalid);
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
if (heartRateClosed) return Promise.reject(new Error('Android bridge is closed'));
|
|
1864
|
+
return new Promise(function(resolve, reject) {
|
|
1865
|
+
heartRateSequence += 1;
|
|
1866
|
+
var id = heartRateSequence;
|
|
1867
|
+
heartRateRequests[id] = {resolve: resolve, reject: reject};
|
|
1868
|
+
try {
|
|
1869
|
+
CraftAndroid.heartRate(method, JSON.stringify(options), id);
|
|
1870
|
+
} catch (error) {
|
|
1871
|
+
delete heartRateRequests[id];
|
|
1872
|
+
reject(error);
|
|
1873
|
+
}
|
|
1874
|
+
});
|
|
1875
|
+
}
|
|
1876
|
+
craft.heartRate = {
|
|
1877
|
+
getState: function() { return heartRate('getState'); },
|
|
1878
|
+
requestAuthorization: function() { return heartRate('requestAuthorization'); },
|
|
1879
|
+
scan: function(options) { return heartRate('scan', {timeout: options && typeof options.timeout === 'number' ? options.timeout : null}); },
|
|
1880
|
+
stopScan: function() { return heartRate('stopScan'); },
|
|
1881
|
+
getDevices: function() { return heartRate('getDevices'); },
|
|
1882
|
+
connect: function(id) { return heartRate('connect', {id: typeof id === 'string' ? id : id && id.id}); },
|
|
1883
|
+
reconnect: function() { return heartRate('reconnect'); },
|
|
1884
|
+
disconnect: function(options) { return heartRate('disconnect', {forget: !!(options && options.forget)}); },
|
|
1885
|
+
startRecording: function(options) { return heartRate('startRecording', options); },
|
|
1886
|
+
pauseRecording: function(options) { return heartRate('pauseRecording', options); },
|
|
1887
|
+
resumeRecording: function(options) { return heartRate('resumeRecording', options); },
|
|
1888
|
+
stopRecording: function(options) { return heartRate('stopRecording', options); },
|
|
1889
|
+
getRecordingState: function(options) { return heartRate('getRecordingState', options); },
|
|
1890
|
+
acknowledgeRecording: function(options) { return heartRate('acknowledgeRecording', options); },
|
|
1891
|
+
readRecording: function(options) { return heartRate('readRecording', options); }
|
|
1892
|
+
};
|
|
1811
1893
|
var shareApi = function(text, title) { return legacyShare(text, title); };
|
|
1812
1894
|
shareApi.share = function(options) {
|
|
1813
1895
|
options = options || {};
|
|
@@ -2537,6 +2619,7 @@ class CraftBridge(
|
|
|
2537
2619
|
}
|
|
2538
2620
|
|
|
2539
2621
|
fun onRequestPermissionsResult(requestCode: Int): Boolean {
|
|
2622
|
+
if (heartRateEnabled && CraftHeartRate.get(activity).onPermissions(requestCode)) return true
|
|
2540
2623
|
if (closed) return requestCode in PERMISSION_REQUEST_START..PERMISSION_REQUEST_END
|
|
2541
2624
|
// startListening asked for the microphone and is waiting on this.
|
|
2542
2625
|
if (requestCode == REQUEST_SPEECH) {
|
|
@@ -2820,6 +2903,54 @@ class CraftBridge(
|
|
|
2820
2903
|
return legacyRecordingAction("readRecording")
|
|
2821
2904
|
}
|
|
2822
2905
|
|
|
2906
|
+
// ==================== Heart rate (CraftHeartRate.kt) ====================
|
|
2907
|
+
|
|
2908
|
+
private val heartRateEnabled = {{ENABLE_HEART_RATE}}
|
|
2909
|
+
|
|
2910
|
+
/** The strap's events, to this page as `craftHeartRate`. */
|
|
2911
|
+
private val heartRateSink: (JSONObject) -> Unit = { detail ->
|
|
2912
|
+
evaluateJavascriptUnlessClosed("window.dispatchEvent(new CustomEvent('craftHeartRate', {detail: $detail}));")
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
init {
|
|
2916
|
+
if (heartRateEnabled) CraftHeartRate.get(activity).sinks["page"] = heartRateSink
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2919
|
+
/**
|
|
2920
|
+
* One `craft.heartRate` call, by method name, answered through
|
|
2921
|
+
* `__craftHeartRateResult` / `__craftHeartRateError` with its id. An
|
|
2922
|
+
* answer for a page that has since navigated away is dropped.
|
|
2923
|
+
*/
|
|
2924
|
+
@JavascriptInterface
|
|
2925
|
+
fun heartRate(method: String, optionsJson: String, callbackId: Int) {
|
|
2926
|
+
activity.runOnUiThread {
|
|
2927
|
+
if (closed) return@runOnUiThread
|
|
2928
|
+
if (!heartRateEnabled) return@runOnUiThread rejectHeartRate(callbackId, navigationGeneration, "Heart rate is disabled", "CAPABILITY_DISABLED")
|
|
2929
|
+
val generation = navigationGeneration
|
|
2930
|
+
val options = runCatching { JSONObject(optionsJson) }.getOrNull()
|
|
2931
|
+
?: return@runOnUiThread rejectHeartRate(callbackId, generation, "Invalid heart-rate options", "INVALID_ARGUMENT")
|
|
2932
|
+
CraftHeartRate.get(activity).perform(method, options, activity) { result ->
|
|
2933
|
+
result.fold(
|
|
2934
|
+
onSuccess = { value ->
|
|
2935
|
+
if (generation != navigationGeneration) return@fold
|
|
2936
|
+
val json = if (value is String) JSONObject.quote(value) else value.toString()
|
|
2937
|
+
evaluateJavascriptUnlessClosed("window.__craftHeartRateResult && window.__craftHeartRateResult($callbackId, $json)")
|
|
2938
|
+
},
|
|
2939
|
+
onFailure = { error ->
|
|
2940
|
+
rejectHeartRate(callbackId, generation, error.message ?: "Heart rate failed", (error as? CraftHeartRateException)?.code ?: "CRAFT_ERROR")
|
|
2941
|
+
},
|
|
2942
|
+
)
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
private fun rejectHeartRate(callbackId: Int, generation: Int, message: String, code: String) {
|
|
2948
|
+
if (generation != navigationGeneration) return
|
|
2949
|
+
evaluateJavascriptUnlessClosed(
|
|
2950
|
+
"window.__craftHeartRateError && window.__craftHeartRateError($callbackId, ${jsQuote(message)}, ${jsQuote(code)})"
|
|
2951
|
+
)
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2823
2954
|
// ==================== Clipboard ====================
|
|
2824
2955
|
|
|
2825
2956
|
@JavascriptInterface
|
|
@@ -4473,13 +4604,17 @@ class CraftBridge(
|
|
|
4473
4604
|
webView.evaluateJavascript(
|
|
4474
4605
|
"window.__craftRejectPendingPromises && window.__craftRejectPendingPromises('Android bridge closed');" +
|
|
4475
4606
|
"window.__craftRejectPermissionRequests && window.__craftRejectPermissionRequests('Android bridge closed');" +
|
|
4476
|
-
"window.__craftRejectSpeechRequests && window.__craftRejectSpeechRequests('Android bridge closed')"
|
|
4607
|
+
"window.__craftRejectSpeechRequests && window.__craftRejectSpeechRequests('Android bridge closed');" +
|
|
4608
|
+
"window.__craftRejectHeartRateRequests && window.__craftRejectHeartRateRequests('Android bridge closed')",
|
|
4477
4609
|
null
|
|
4478
4610
|
)
|
|
4479
4611
|
}
|
|
4480
4612
|
}
|
|
4481
4613
|
CraftNative.close(activity)
|
|
4482
4614
|
healthConnect.close()
|
|
4615
|
+
// The strap and its recording carry on (the service is the
|
|
4616
|
+
// process's); only this page stops hearing them.
|
|
4617
|
+
if (heartRateEnabled) CraftHeartRate.get(activity).sinks.remove("page", heartRateSink)
|
|
4483
4618
|
|
|
4484
4619
|
if (shareReceiverRegistered) {
|
|
4485
4620
|
runCatching { activity.unregisterReceiver(shareChosenReceiver) }
|