craft-native 0.0.245 → 0.0.247
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 +23 -0
- package/dist/android/src/index.js +15 -3
- 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/CraftNativeBackground.kt.template +34 -1
- package/dist/android/templates/CraftNativeServices.kt.template +34 -1
- package/dist/android/templates/CraftNativeShell.kt.template +20 -6
- package/dist/api/mobile.d.ts +2 -2
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/src/index.d.ts +24 -1
- package/dist/ios/src/index.js +6 -0
- package/dist/ios/templates/CraftApp.swift +4 -0
- package/dist/ios/templates/CraftHybrid.swift +151 -14
- package/dist/ios/templates/CraftNativeFlatList.swift +13 -6
- package/dist/ios/templates/CraftNativeScreen.swift +353 -68
- package/dist/ios/templates/CraftWebAudio.swift +21 -13
- package/dist/mobile-testing.js +19 -1
- package/dist/testing/media-cache/media-cache.kt +4 -2
- package/package.json +1 -1
|
@@ -11,6 +11,20 @@ export type { AndroidMapProvider } from './native-map.js';
|
|
|
11
11
|
* route's file. Stacks asks for route bundles only when this is true.
|
|
12
12
|
*/
|
|
13
13
|
export declare const nativeRouteBundles = true;
|
|
14
|
+
/**
|
|
15
|
+
* The app evaluates stx's shared runtime (`__runtime.js`, stx's
|
|
16
|
+
* `sharedRuntime`) in each screen's context before the route's own file.
|
|
17
|
+
* Stacks asks stx for it only when this is true: a route file built so
|
|
18
|
+
* refuses to run without it.
|
|
19
|
+
*/
|
|
20
|
+
export declare const nativeRouteRuntime = true;
|
|
21
|
+
/**
|
|
22
|
+
* The app gives a screen its messages a language at a time (stx's
|
|
23
|
+
* `messages`, `native-screens/__messages/`): the areas its route uses,
|
|
24
|
+
* evaluated ahead of it (an isolate cannot read a file when asked). Stacks leaves the languages
|
|
25
|
+
* out of the screens' code only when this is true.
|
|
26
|
+
*/
|
|
27
|
+
export declare const nativeMessages = true;
|
|
14
28
|
/** Providers this published builder can generate as real Android map views. */
|
|
15
29
|
export declare const nativeMapProviders: readonly ['google', 'maplibre'];
|
|
16
30
|
export { runKotlinFixture } from './testing.js';
|
|
@@ -46,6 +60,15 @@ export interface CraftAndroidConfig {
|
|
|
46
60
|
enableGeolocation?: boolean;
|
|
47
61
|
enableBackgroundLocation?: boolean;
|
|
48
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;
|
|
49
72
|
enableHealthConnect?: boolean;
|
|
50
73
|
/**
|
|
51
74
|
* The Health Connect data the app reads and writes, by the shared type
|
|
@@ -360,6 +360,8 @@ async function createAndroidEmulator(options) {
|
|
|
360
360
|
|
|
361
361
|
// ../android/src/index.ts
|
|
362
362
|
var nativeRouteBundles = true;
|
|
363
|
+
var nativeRouteRuntime = true;
|
|
364
|
+
var nativeMessages = true;
|
|
363
365
|
var nativeMapProviders = ["google", "maplibre"];
|
|
364
366
|
function syncNativeFonts(fonts, output) {
|
|
365
367
|
const destination = join3(output, "app/src/main/assets/NativeFonts");
|
|
@@ -496,6 +498,7 @@ var DEFAULT_CONFIG = {
|
|
|
496
498
|
enableGeolocation: false,
|
|
497
499
|
enableBackgroundLocation: false,
|
|
498
500
|
enableBluetooth: false,
|
|
501
|
+
enableHeartRate: false,
|
|
499
502
|
enableHealthConnect: false,
|
|
500
503
|
enableKeepAwake: false,
|
|
501
504
|
enableDeepLinks: false,
|
|
@@ -788,7 +791,11 @@ function renderAndroidPermissions(config) {
|
|
|
788
791
|
healthConnectPermissions(config).forEach((permission) => permissions.add(permission));
|
|
789
792
|
if (config.renderer === "native" && config.backgroundRefresh?.enabled)
|
|
790
793
|
permissions.add("android.permission.RECEIVE_BOOT_COMPLETED");
|
|
791
|
-
|
|
794
|
+
if (config.enableHeartRate) {
|
|
795
|
+
permissions.add("android.permission.FOREGROUND_SERVICE");
|
|
796
|
+
permissions.add("android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE");
|
|
797
|
+
}
|
|
798
|
+
const bluetooth = config.enableBluetooth || config.enableHeartRate ? [
|
|
792
799
|
' <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />',
|
|
793
800
|
' <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />',
|
|
794
801
|
' <uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />',
|
|
@@ -1017,7 +1024,7 @@ async function init(options) {
|
|
|
1017
1024
|
writeNativeShellSources(output, packagePath, finalPackageName, config);
|
|
1018
1025
|
}
|
|
1019
1026
|
const craftBridgeTemplate = readFileSync2(join3(TEMPLATES_DIR, "CraftBridge.kt.template"), "utf-8");
|
|
1020
|
-
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 {
|
|
1027
|
+
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 {
|
|
1021
1028
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
|
|
1022
1029
|
&& ContextCompat.checkSelfPermission(activity, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
|
1023
1030
|
ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 4204)
|
|
@@ -1045,8 +1052,11 @@ async function init(options) {
|
|
|
1045
1052
|
writeFileSync(join3(nativeDir, "LocationRecordingService.kt"), serviceTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
1046
1053
|
const healthTemplate = readFileSync2(join3(TEMPLATES_DIR, config.enableHealthConnect ? "CraftHealthConnect.kt.template" : "CraftHealthConnectStub.kt.template"), "utf-8");
|
|
1047
1054
|
writeFileSync(join3(output, "app/src/main/java", packagePath, "CraftHealthConnect.kt"), healthTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
1055
|
+
for (const [template, file] of [["CraftHeartRateCore.kt.template", "CraftHeartRateCore.kt"], ["CraftHeartRate.kt.template", "CraftHeartRate.kt"]]) {
|
|
1056
|
+
writeFileSync(join3(output, "app/src/main/java", packagePath, file), readFileSync2(join3(TEMPLATES_DIR, template), "utf8").replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
1057
|
+
}
|
|
1048
1058
|
const manifestTemplate = readFileSync2(join3(TEMPLATES_DIR, "AndroidManifest.xml.template"), "utf-8");
|
|
1049
|
-
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>
|
|
1059
|
+
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>
|
|
1050
1060
|
<package android:name="com.google.android.apps.healthdata" />
|
|
1051
1061
|
</queries>` : "").replace(/\{\{HEALTH_CONNECT_RATIONALE\}\}/g, config.enableHealthConnect ? ` <intent-filter>
|
|
1052
1062
|
<action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
|
|
@@ -1380,7 +1390,9 @@ export {
|
|
|
1380
1390
|
renderAndroidPermissions,
|
|
1381
1391
|
renderAndroidDeepLinks,
|
|
1382
1392
|
open,
|
|
1393
|
+
nativeRouteRuntime,
|
|
1383
1394
|
nativeRouteBundles,
|
|
1395
|
+
nativeMessages,
|
|
1384
1396
|
nativeMapProviders,
|
|
1385
1397
|
installRuntime,
|
|
1386
1398
|
init,
|
|
@@ -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) }
|