craft-native 0.0.246 → 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 +9 -0
- package/dist/android/src/index.js +11 -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/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/mobile-testing.js +19 -1
- package/dist/testing/media-cache/media-cache.kt +4 -2
- package/package.json +1 -1
|
@@ -60,6 +60,15 @@ 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;
|
|
64
73
|
/**
|
|
65
74
|
* The Health Connect data the app reads and writes, by the shared type
|
|
@@ -498,6 +498,7 @@ var DEFAULT_CONFIG = {
|
|
|
498
498
|
enableGeolocation: false,
|
|
499
499
|
enableBackgroundLocation: false,
|
|
500
500
|
enableBluetooth: false,
|
|
501
|
+
enableHeartRate: false,
|
|
501
502
|
enableHealthConnect: false,
|
|
502
503
|
enableKeepAwake: false,
|
|
503
504
|
enableDeepLinks: false,
|
|
@@ -790,7 +791,11 @@ function renderAndroidPermissions(config) {
|
|
|
790
791
|
healthConnectPermissions(config).forEach((permission) => permissions.add(permission));
|
|
791
792
|
if (config.renderer === "native" && config.backgroundRefresh?.enabled)
|
|
792
793
|
permissions.add("android.permission.RECEIVE_BOOT_COMPLETED");
|
|
793
|
-
|
|
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 ? [
|
|
794
799
|
' <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />',
|
|
795
800
|
' <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />',
|
|
796
801
|
' <uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />',
|
|
@@ -1019,7 +1024,7 @@ async function init(options) {
|
|
|
1019
1024
|
writeNativeShellSources(output, packagePath, finalPackageName, config);
|
|
1020
1025
|
}
|
|
1021
1026
|
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 {
|
|
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 {
|
|
1023
1028
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
|
|
1024
1029
|
&& ContextCompat.checkSelfPermission(activity, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
|
1025
1030
|
ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 4204)
|
|
@@ -1047,8 +1052,11 @@ async function init(options) {
|
|
|
1047
1052
|
writeFileSync(join3(nativeDir, "LocationRecordingService.kt"), serviceTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
1048
1053
|
const healthTemplate = readFileSync2(join3(TEMPLATES_DIR, config.enableHealthConnect ? "CraftHealthConnect.kt.template" : "CraftHealthConnectStub.kt.template"), "utf-8");
|
|
1049
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
|
+
}
|
|
1050
1058
|
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>
|
|
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>
|
|
1052
1060
|
<package android:name="com.google.android.apps.healthdata" />
|
|
1053
1061
|
</queries>` : "").replace(/\{\{HEALTH_CONNECT_RATIONALE\}\}/g, config.enableHealthConnect ? ` <intent-filter>
|
|
1054
1062
|
<action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
|
|
@@ -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) }
|