craft-native 0.0.55 → 0.0.72
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 +73 -0
- package/dist/android/src/index.js +177 -37
- package/dist/android/templates/AndroidManifest.xml.template +10 -36
- package/dist/android/templates/CraftBridge.kt.template +192 -60
- package/dist/android/templates/CraftHealthConnect.kt.template +200 -0
- package/dist/android/templates/CraftHealthConnectStub.kt.template +28 -0
- package/dist/android/templates/LocationRecordingService.kt.template +160 -0
- package/dist/android/templates/MainActivity.kt.template +140 -19
- package/dist/android/templates/build.gradle.kts.app.template +23 -1
- package/dist/android/templates/build.gradle.kts.project.template +3 -2
- package/dist/android/templates/proguard-rules.pro.template +9 -0
- package/dist/api/android-advanced.d.ts +5 -6
- package/dist/api/app.d.ts +0 -1
- package/dist/api/clipboard.d.ts +0 -1
- package/dist/api/crypto.d.ts +1 -2
- package/dist/api/db.d.ts +1 -2
- package/dist/api/dialog.d.ts +0 -1
- package/dist/api/fs.d.ts +1 -2
- package/dist/api/gestures.d.ts +66 -0
- package/dist/api/http.d.ts +1 -2
- package/dist/api/index.d.ts +37 -34
- package/dist/api/ios-advanced.d.ts +6 -7
- package/dist/api/macos-advanced.d.ts +2 -3
- package/dist/api/media.d.ts +0 -1
- package/dist/api/mobile.d.ts +109 -1
- package/dist/api/process.d.ts +0 -1
- package/dist/api/sidebar.d.ts +3 -20
- package/dist/api/spaces.d.ts +83 -0
- package/dist/api/tray.d.ts +2 -3
- package/dist/api/window.d.ts +1 -2
- package/dist/api/windows-advanced.d.ts +6 -7
- package/dist/assets/index.d.ts +0 -1
- package/dist/binary-resolver.d.ts +23 -1
- package/dist/bridge/android.d.ts +0 -1
- package/dist/bridge/core.d.ts +0 -38
- package/dist/bridge/ids.d.ts +0 -1
- package/dist/bridge/ios.d.ts +0 -1
- package/dist/bridge/webkit-pending.d.ts +0 -1
- package/dist/build/bundle-optimizer.d.ts +0 -1
- package/dist/cli.js +666 -90
- package/dist/components/index.d.ts +6 -7
- package/dist/components/native.d.ts +0 -1
- package/dist/components/sidebar.d.ts +2 -81
- package/dist/dev/devtools.d.ts +0 -1
- package/dist/dev/error-overlay.d.ts +0 -1
- package/dist/dev/hot-reload.d.ts +0 -1
- package/dist/index.cjs +113 -3
- package/dist/index.d.cts +16 -16
- package/dist/index.d.ts +16 -16
- package/dist/index.js +112 -4
- package/dist/ios/src/index.d.ts +172 -0
- package/dist/ios/src/index.js +440 -42
- package/dist/ios/templates/CraftActivityAttributes.swift +14 -0
- package/dist/ios/templates/CraftApp.swift +1001 -73
- package/dist/ios/templates/CraftLiveActivityWidget.swift.template +62 -0
- package/dist/ios/templates/CraftWatchApp.swift.template +155 -0
- package/dist/ios/templates/Info.plist.template +10 -9
- package/dist/ios/templates/WatchApp.Info.plist.template +26 -0
- package/dist/ios/templates/WidgetExtension.Info.plist +31 -0
- package/dist/ios/templates/project.yml.template +12 -1
- package/dist/logging/index.d.ts +0 -1
- package/dist/mobile.d.ts +8 -0
- package/dist/mobile.js +755 -0
- package/dist/optimizations/index.d.ts +3 -4
- package/dist/package.d.ts +11 -1
- package/dist/performance/runtime.d.ts +0 -1
- package/dist/performance/startup.d.ts +0 -1
- package/dist/plugins/index.d.ts +0 -1
- package/dist/security/index.d.ts +0 -1
- package/dist/sidebar-types.d.ts +102 -0
- package/dist/signing/index.d.ts +0 -1
- package/dist/styles/headwind.d.ts +0 -1
- package/dist/styles/index.d.ts +9 -10
- package/dist/styles/sidebar-templates.d.ts +0 -1
- package/dist/styles/sidebars.d.ts +0 -1
- package/dist/types.d.ts +131 -1
- package/dist/updater/index.d.ts +0 -1
- package/dist/utils/audio.d.ts +0 -1
- package/dist/utils/index.d.ts +4 -5
- package/dist/utils/native.d.ts +1 -2
- package/dist/utils/react.d.ts +0 -1
- package/dist/utils/storage.d.ts +0 -1
- package/dist/utils/svelte.d.ts +0 -1
- package/dist/utils/timer.d.ts +0 -1
- package/dist/utils/vue.d.ts +0 -1
- package/package.json +32 -4
- package/dist/android/src/index.js.map +0 -10
- package/dist/api/android-advanced.d.ts.map +0 -1
- package/dist/api/app.d.ts.map +0 -1
- package/dist/api/clipboard.d.ts.map +0 -1
- package/dist/api/crypto.d.ts.map +0 -1
- package/dist/api/db.d.ts.map +0 -1
- package/dist/api/dialog.d.ts.map +0 -1
- package/dist/api/fs.d.ts.map +0 -1
- package/dist/api/http.d.ts.map +0 -1
- package/dist/api/index.d.ts.map +0 -1
- package/dist/api/ios-advanced.d.ts.map +0 -1
- package/dist/api/macos-advanced.d.ts.map +0 -1
- package/dist/api/media.d.ts.map +0 -1
- package/dist/api/mobile.d.ts.map +0 -1
- package/dist/api/process.d.ts.map +0 -1
- package/dist/api/sidebar.d.ts.map +0 -1
- package/dist/api/tray.d.ts.map +0 -1
- package/dist/api/window.d.ts.map +0 -1
- package/dist/api/windows-advanced.d.ts.map +0 -1
- package/dist/assets/index.d.ts.map +0 -1
- package/dist/binary-resolver.d.ts.map +0 -1
- package/dist/bridge/android.d.ts.map +0 -1
- package/dist/bridge/core.d.ts.map +0 -1
- package/dist/bridge/ids.d.ts.map +0 -1
- package/dist/bridge/ios.d.ts.map +0 -1
- package/dist/bridge/webkit-pending.d.ts.map +0 -1
- package/dist/build/bundle-optimizer.d.ts.map +0 -1
- package/dist/components/index.d.ts.map +0 -1
- package/dist/components/native.d.ts.map +0 -1
- package/dist/components/sidebar.d.ts.map +0 -1
- package/dist/dev/devtools.d.ts.map +0 -1
- package/dist/dev/error-overlay.d.ts.map +0 -1
- package/dist/dev/hot-reload.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -45
- package/dist/ios/src/index.js.map +0 -10
- package/dist/logging/index.d.ts.map +0 -1
- package/dist/optimizations/index.d.ts.map +0 -1
- package/dist/package.d.ts.map +0 -1
- package/dist/package.test.d.ts +0 -2
- package/dist/package.test.d.ts.map +0 -1
- package/dist/performance/runtime.d.ts.map +0 -1
- package/dist/performance/startup.d.ts.map +0 -1
- package/dist/plugins/index.d.ts.map +0 -1
- package/dist/security/index.d.ts.map +0 -1
- package/dist/signing/index.d.ts.map +0 -1
- package/dist/styles/headwind.d.ts.map +0 -1
- package/dist/styles/index.d.ts.map +0 -1
- package/dist/styles/sidebar-templates.d.ts.map +0 -1
- package/dist/styles/sidebars.d.ts.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/updater/index.d.ts.map +0 -1
- package/dist/utils/audio.d.ts.map +0 -1
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/native.d.ts.map +0 -1
- package/dist/utils/react.d.ts.map +0 -1
- package/dist/utils/storage.d.ts.map +0 -1
- package/dist/utils/svelte.d.ts.map +0 -1
- package/dist/utils/timer.d.ts.map +0 -1
- package/dist/utils/vue.d.ts.map +0 -1
|
@@ -49,6 +49,7 @@ import com.google.android.gms.location.LocationResult
|
|
|
49
49
|
import com.google.android.gms.location.LocationServices
|
|
50
50
|
import com.google.android.gms.location.Priority
|
|
51
51
|
import com.google.android.play.core.review.ReviewManagerFactory
|
|
52
|
+
{{FIREBASE_IMPORT}}
|
|
52
53
|
import android.content.ContentResolver
|
|
53
54
|
import android.content.ContentUris
|
|
54
55
|
import android.content.ContentValues
|
|
@@ -95,10 +96,12 @@ import android.graphics.BitmapFactory
|
|
|
95
96
|
import java.io.ByteArrayOutputStream
|
|
96
97
|
import java.io.File
|
|
97
98
|
import java.util.concurrent.TimeUnit
|
|
99
|
+
import java.util.UUID
|
|
98
100
|
|
|
99
101
|
class CraftBridge(
|
|
100
102
|
private val activity: Activity,
|
|
101
|
-
private val webView: WebView
|
|
103
|
+
private val webView: WebView,
|
|
104
|
+
private val healthConnect: CraftHealthConnect
|
|
102
105
|
) {
|
|
103
106
|
private var speechRecognizer: SpeechRecognizer? = null
|
|
104
107
|
private var isListening = false
|
|
@@ -167,42 +170,43 @@ class CraftBridge(
|
|
|
167
170
|
window.craft = {
|
|
168
171
|
platform: 'android',
|
|
169
172
|
capabilities: {
|
|
170
|
-
haptics:
|
|
171
|
-
speechRecognition: ${SpeechRecognizer.isRecognitionAvailable(activity)},
|
|
172
|
-
share:
|
|
173
|
-
camera:
|
|
174
|
-
biometric: ${isBiometricAvailable()},
|
|
175
|
-
pushNotifications:
|
|
176
|
-
secureStorage:
|
|
177
|
-
geolocation:
|
|
178
|
-
|
|
173
|
+
haptics: {{ENABLE_HAPTICS}},
|
|
174
|
+
speechRecognition: {{ENABLE_SPEECH}} && ${SpeechRecognizer.isRecognitionAvailable(activity)},
|
|
175
|
+
share: {{ENABLE_SHARE}},
|
|
176
|
+
camera: {{ENABLE_CAMERA}},
|
|
177
|
+
biometric: {{ENABLE_BIOMETRIC}} && ${isBiometricAvailable()},
|
|
178
|
+
pushNotifications: {{ENABLE_PUSH}},
|
|
179
|
+
secureStorage: {{ENABLE_SECURE_STORAGE}},
|
|
180
|
+
geolocation: {{ENABLE_GEOLOCATION}},
|
|
181
|
+
backgroundLocation: {{ENABLE_BACKGROUND_LOCATION}},
|
|
182
|
+
clipboard: false,
|
|
179
183
|
deviceInfo: true,
|
|
180
|
-
appBadge:
|
|
184
|
+
appBadge: {{ENABLE_PUSH}},
|
|
181
185
|
networkStatus: true,
|
|
182
186
|
appReview: true,
|
|
183
|
-
flashlight: ${hasFlashlight()},
|
|
187
|
+
flashlight: {{ENABLE_CAMERA}} && ${hasFlashlight()},
|
|
184
188
|
openURL: true,
|
|
185
189
|
vibrationPattern: true,
|
|
186
190
|
appState: true,
|
|
187
|
-
contacts:
|
|
188
|
-
calendar:
|
|
189
|
-
localNotifications:
|
|
190
|
-
inAppPurchase:
|
|
191
|
-
keepAwake:
|
|
192
|
-
orientationLock:
|
|
193
|
-
deepLinks:
|
|
194
|
-
qrScanner:
|
|
195
|
-
filePicker:
|
|
196
|
-
fileDownload:
|
|
197
|
-
googleSignIn:
|
|
198
|
-
audioRecording:
|
|
199
|
-
videoRecording:
|
|
200
|
-
motionSensors:
|
|
201
|
-
localDatabase:
|
|
202
|
-
bluetooth:
|
|
203
|
-
nfc:
|
|
204
|
-
|
|
205
|
-
screenCapture:
|
|
191
|
+
contacts: false,
|
|
192
|
+
calendar: false,
|
|
193
|
+
localNotifications: {{ENABLE_PUSH}},
|
|
194
|
+
inAppPurchase: false,
|
|
195
|
+
keepAwake: {{ENABLE_KEEP_AWAKE}},
|
|
196
|
+
orientationLock: false,
|
|
197
|
+
deepLinks: {{ENABLE_DEEP_LINKS}},
|
|
198
|
+
qrScanner: false,
|
|
199
|
+
filePicker: false,
|
|
200
|
+
fileDownload: false,
|
|
201
|
+
googleSignIn: false,
|
|
202
|
+
audioRecording: false,
|
|
203
|
+
videoRecording: false,
|
|
204
|
+
motionSensors: false,
|
|
205
|
+
localDatabase: false,
|
|
206
|
+
bluetooth: false,
|
|
207
|
+
nfc: false,
|
|
208
|
+
health: {{ENABLE_HEALTH_CONNECT}},
|
|
209
|
+
screenCapture: false
|
|
206
210
|
},
|
|
207
211
|
|
|
208
212
|
// Haptic feedback
|
|
@@ -595,7 +599,7 @@ class CraftBridge(
|
|
|
595
599
|
},
|
|
596
600
|
|
|
597
601
|
// Fitness / Health
|
|
598
|
-
|
|
602
|
+
health: {
|
|
599
603
|
requestAuthorization: function(types) {
|
|
600
604
|
return new Promise(function(resolve, reject) {
|
|
601
605
|
window._craftFitnessAuthResolve = resolve;
|
|
@@ -603,11 +607,19 @@ class CraftBridge(
|
|
|
603
607
|
CraftAndroid.requestFitnessAuthorization(JSON.stringify(types || []));
|
|
604
608
|
});
|
|
605
609
|
},
|
|
606
|
-
getData: function(type,
|
|
610
|
+
getData: function(type, options) {
|
|
611
|
+
options = options || {};
|
|
607
612
|
return new Promise(function(resolve, reject) {
|
|
608
613
|
window._craftFitnessDataResolve = resolve;
|
|
609
614
|
window._craftFitnessDataReject = reject;
|
|
610
|
-
CraftAndroid.getFitnessData(type, startDate || 0, endDate || 0);
|
|
615
|
+
CraftAndroid.getFitnessData(type, options.startDate || 0, options.endDate || 0);
|
|
616
|
+
});
|
|
617
|
+
},
|
|
618
|
+
saveWorkout: function(workout) {
|
|
619
|
+
return new Promise(function(resolve, reject) {
|
|
620
|
+
window._craftFitnessSaveResolve = resolve;
|
|
621
|
+
window._craftFitnessSaveReject = reject;
|
|
622
|
+
CraftAndroid.saveHealthWorkout(JSON.stringify(workout || {}));
|
|
611
623
|
});
|
|
612
624
|
}
|
|
613
625
|
},
|
|
@@ -1202,6 +1214,73 @@ class CraftBridge(
|
|
|
1202
1214
|
}
|
|
1203
1215
|
};
|
|
1204
1216
|
|
|
1217
|
+
(function installCraftMobileContract(craft) {
|
|
1218
|
+
var legacyShare = craft.share.bind(craft);
|
|
1219
|
+
var legacyOpenCamera = craft.openCamera.bind(craft);
|
|
1220
|
+
var legacyPickImage = craft.pickImage.bind(craft);
|
|
1221
|
+
var legacyAuthenticate = craft.authenticate.bind(craft);
|
|
1222
|
+
var legacySecureStore = craft.secureStore;
|
|
1223
|
+
craft.contractVersion = '1.0.0';
|
|
1224
|
+
craft.device = {
|
|
1225
|
+
getInfo: function() { return Promise.resolve(craft.getDeviceInfo()); },
|
|
1226
|
+
getCapabilities: function() { return Promise.resolve(Object.assign({}, craft.capabilities)); }
|
|
1227
|
+
};
|
|
1228
|
+
craft.haptics = {
|
|
1229
|
+
impact: function(style) { craft.haptic(style || 'medium'); return Promise.resolve(); },
|
|
1230
|
+
notification: function(type) { craft.haptic(type === 'error' ? 'heavy' : 'light'); return Promise.resolve(); },
|
|
1231
|
+
selection: function() { craft.haptic('selection'); return Promise.resolve(); },
|
|
1232
|
+
vibrate: function(pattern) { craft.vibrate(pattern || []); return Promise.resolve(); }
|
|
1233
|
+
};
|
|
1234
|
+
craft.camera = {
|
|
1235
|
+
takePicture: function() { return legacyOpenCamera().then(normalizePhoto); },
|
|
1236
|
+
pickImage: function() { return legacyPickImage().then(normalizePhoto); },
|
|
1237
|
+
pickMultiple: function() { return legacyPickImage().then(function(photo) { return [normalizePhoto(photo)]; }); },
|
|
1238
|
+
isAvailable: function() { return Promise.resolve(!!craft.capabilities.camera); }
|
|
1239
|
+
};
|
|
1240
|
+
craft.biometrics = {
|
|
1241
|
+
isAvailable: function() { return Promise.resolve(!!craft.capabilities.biometric); },
|
|
1242
|
+
getBiometricType: function() { return Promise.resolve(craft.capabilities.biometric ? 'fingerprint' : null); },
|
|
1243
|
+
authenticate: function(reason) { return legacyAuthenticate(reason); }
|
|
1244
|
+
};
|
|
1245
|
+
craft.secureStorage = {
|
|
1246
|
+
set: function(key, value) { return Promise.resolve(legacySecureStore.set(key, value)).then(function() {}); },
|
|
1247
|
+
get: function(key) { return Promise.resolve(legacySecureStore.get(key)); },
|
|
1248
|
+
delete: function(key) { return Promise.resolve(legacySecureStore.remove(key)).then(function() {}); },
|
|
1249
|
+
clear: function() { return Promise.resolve(CraftAndroid.secureClear()).then(function() {}); }
|
|
1250
|
+
};
|
|
1251
|
+
craft.location = {
|
|
1252
|
+
getCurrentPosition: craft.getCurrentPosition,
|
|
1253
|
+
watchPosition: craft.watchPosition,
|
|
1254
|
+
clearWatch: craft.clearWatch,
|
|
1255
|
+
startRecording: function(options) { return Promise.resolve(JSON.parse(CraftAndroid.startLocationRecording(JSON.stringify(options || {})))); },
|
|
1256
|
+
pauseRecording: function() { return Promise.resolve(JSON.parse(CraftAndroid.pauseLocationRecording())); },
|
|
1257
|
+
resumeRecording: function() { return Promise.resolve(JSON.parse(CraftAndroid.resumeLocationRecording())); },
|
|
1258
|
+
stopRecording: function() { return Promise.resolve(JSON.parse(CraftAndroid.stopLocationRecording())); },
|
|
1259
|
+
getRecordingState: function() { return Promise.resolve(JSON.parse(CraftAndroid.getLocationRecordingState())); },
|
|
1260
|
+
readRecording: function() { return Promise.resolve(JSON.parse(CraftAndroid.readLocationRecording())); }
|
|
1261
|
+
};
|
|
1262
|
+
var shareApi = function(text, title) { return legacyShare(text, title); };
|
|
1263
|
+
shareApi.share = function(options) {
|
|
1264
|
+
options = options || {};
|
|
1265
|
+
return Promise.resolve(legacyShare([options.text, options.url].filter(Boolean).join(' '), options.title || ''));
|
|
1266
|
+
};
|
|
1267
|
+
craft.share = shareApi;
|
|
1268
|
+
craft.lifecycle = {
|
|
1269
|
+
getState: function() { return document.visibilityState === 'visible' ? 'active' : 'background'; },
|
|
1270
|
+
onStateChange: function(callback) {
|
|
1271
|
+
var handler = function() { callback(document.visibilityState === 'visible' ? 'active' : 'background'); };
|
|
1272
|
+
document.addEventListener('visibilitychange', handler);
|
|
1273
|
+
return function() { document.removeEventListener('visibilitychange', handler); };
|
|
1274
|
+
}
|
|
1275
|
+
};
|
|
1276
|
+
function normalizePhoto(photo) {
|
|
1277
|
+
if (photo && typeof photo === 'object') return photo;
|
|
1278
|
+
var value = String(photo || '');
|
|
1279
|
+
var base64 = value.indexOf(',') >= 0 ? value.split(',').pop() : value;
|
|
1280
|
+
return {base64: base64, uri: value, width: 0, height: 0, mimeType: 'image/jpeg'};
|
|
1281
|
+
}
|
|
1282
|
+
})(window.craft);
|
|
1283
|
+
|
|
1205
1284
|
// Dispatch ready event
|
|
1206
1285
|
window.dispatchEvent(new CustomEvent('craftReady', {detail: window.craft}));
|
|
1207
1286
|
console.log('Craft Android bridge initialized');
|
|
@@ -1455,24 +1534,7 @@ class CraftBridge(
|
|
|
1455
1534
|
|
|
1456
1535
|
@JavascriptInterface
|
|
1457
1536
|
fun registerPush() {
|
|
1458
|
-
|
|
1459
|
-
// For now, return a placeholder token
|
|
1460
|
-
activity.runOnUiThread {
|
|
1461
|
-
// In a real implementation, you would use Firebase:
|
|
1462
|
-
// FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
|
|
1463
|
-
// if (task.isSuccessful) {
|
|
1464
|
-
// val token = task.result
|
|
1465
|
-
// webView.evaluateJavascript("window._craftPushResolve('$token')", null)
|
|
1466
|
-
// } else {
|
|
1467
|
-
// webView.evaluateJavascript("window._craftPushReject('Failed to get token')", null)
|
|
1468
|
-
// }
|
|
1469
|
-
// }
|
|
1470
|
-
|
|
1471
|
-
webView.evaluateJavascript(
|
|
1472
|
-
"window._craftPushResolve && window._craftPushResolve('push-token-placeholder')",
|
|
1473
|
-
null
|
|
1474
|
-
)
|
|
1475
|
-
}
|
|
1537
|
+
{{REGISTER_PUSH_IMPLEMENTATION}}
|
|
1476
1538
|
}
|
|
1477
1539
|
|
|
1478
1540
|
// ==================== Secure Storage ====================
|
|
@@ -1506,6 +1568,16 @@ class CraftBridge(
|
|
|
1506
1568
|
}
|
|
1507
1569
|
}
|
|
1508
1570
|
|
|
1571
|
+
@JavascriptInterface
|
|
1572
|
+
fun secureClear(): Boolean {
|
|
1573
|
+
return try {
|
|
1574
|
+
securePrefs.edit().clear().apply()
|
|
1575
|
+
true
|
|
1576
|
+
} catch (e: Exception) {
|
|
1577
|
+
false
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1509
1581
|
// ==================== Logging ====================
|
|
1510
1582
|
|
|
1511
1583
|
@JavascriptInterface
|
|
@@ -1654,6 +1726,58 @@ class CraftBridge(
|
|
|
1654
1726
|
locationCallback = null
|
|
1655
1727
|
}
|
|
1656
1728
|
|
|
1729
|
+
@JavascriptInterface
|
|
1730
|
+
fun startLocationRecording(optionsJson: String): String {
|
|
1731
|
+
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
|
|
1732
|
+
return JSONObject().apply {
|
|
1733
|
+
put("id", JSONObject.NULL)
|
|
1734
|
+
put("active", false)
|
|
1735
|
+
put("paused", false)
|
|
1736
|
+
put("startedAt", JSONObject.NULL)
|
|
1737
|
+
put("sampleCount", 0)
|
|
1738
|
+
}.toString()
|
|
1739
|
+
}
|
|
1740
|
+
val id = UUID.randomUUID().toString()
|
|
1741
|
+
CraftLocationRecordingStore.start(activity, id, System.currentTimeMillis())
|
|
1742
|
+
ContextCompat.startForegroundService(
|
|
1743
|
+
activity,
|
|
1744
|
+
Intent(activity, LocationRecordingService::class.java).setAction(LocationRecordingService.ACTION_START)
|
|
1745
|
+
)
|
|
1746
|
+
return CraftLocationRecordingStore.state(activity).toString()
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
@JavascriptInterface
|
|
1750
|
+
fun pauseLocationRecording(): String {
|
|
1751
|
+
CraftLocationRecordingStore.setPaused(activity, true)
|
|
1752
|
+
return CraftLocationRecordingStore.state(activity).toString()
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
@JavascriptInterface
|
|
1756
|
+
fun resumeLocationRecording(): String {
|
|
1757
|
+
CraftLocationRecordingStore.setPaused(activity, false)
|
|
1758
|
+
if (CraftLocationRecordingStore.isActive(activity)) {
|
|
1759
|
+
ContextCompat.startForegroundService(
|
|
1760
|
+
activity,
|
|
1761
|
+
Intent(activity, LocationRecordingService::class.java).setAction(LocationRecordingService.ACTION_START)
|
|
1762
|
+
)
|
|
1763
|
+
}
|
|
1764
|
+
return CraftLocationRecordingStore.state(activity).toString()
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
@JavascriptInterface
|
|
1768
|
+
fun stopLocationRecording(): String {
|
|
1769
|
+
CraftLocationRecordingStore.stop(activity)
|
|
1770
|
+
val result = CraftLocationRecordingStore.state(activity, includeLocations = true).toString()
|
|
1771
|
+
activity.stopService(Intent(activity, LocationRecordingService::class.java).setAction(LocationRecordingService.ACTION_STOP))
|
|
1772
|
+
return result
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
@JavascriptInterface
|
|
1776
|
+
fun getLocationRecordingState(): String = CraftLocationRecordingStore.state(activity).toString()
|
|
1777
|
+
|
|
1778
|
+
@JavascriptInterface
|
|
1779
|
+
fun readLocationRecording(): String = CraftLocationRecordingStore.locations(activity).toString()
|
|
1780
|
+
|
|
1657
1781
|
// ==================== Clipboard ====================
|
|
1658
1782
|
|
|
1659
1783
|
@JavascriptInterface
|
|
@@ -1824,8 +1948,9 @@ class CraftBridge(
|
|
|
1824
1948
|
}
|
|
1825
1949
|
} else {
|
|
1826
1950
|
activity.runOnUiThread {
|
|
1951
|
+
val message = JSONObject.quote(task.exception?.message ?: "Review flow failed")
|
|
1827
1952
|
webView.evaluateJavascript(
|
|
1828
|
-
"window._craftReviewReject && window._craftReviewReject(
|
|
1953
|
+
"window._craftReviewReject && window._craftReviewReject($message)",
|
|
1829
1954
|
null
|
|
1830
1955
|
)
|
|
1831
1956
|
}
|
|
@@ -2769,17 +2894,24 @@ class CraftBridge(
|
|
|
2769
2894
|
|
|
2770
2895
|
@JavascriptInterface
|
|
2771
2896
|
fun requestFitnessAuthorization(typesJson: String) {
|
|
2772
|
-
|
|
2773
|
-
activity.runOnUiThread {
|
|
2774
|
-
webView.evaluateJavascript("window._craftFitnessAuthReject && window._craftFitnessAuthReject('Fitness requires Google Fit configuration')", null)
|
|
2775
|
-
}
|
|
2897
|
+
healthConnect.requestAuthorization(typesJson)
|
|
2776
2898
|
}
|
|
2777
2899
|
|
|
2778
2900
|
@JavascriptInterface
|
|
2779
2901
|
fun getFitnessData(type: String, startDate: Long, endDate: Long) {
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2902
|
+
healthConnect.getData(type, startDate, endDate)
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
@JavascriptInterface
|
|
2906
|
+
fun saveHealthWorkout(workoutJson: String) {
|
|
2907
|
+
healthConnect.saveWorkout(workoutJson)
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean =
|
|
2911
|
+
healthConnect.onActivityResult(requestCode, resultCode, data)
|
|
2912
|
+
|
|
2913
|
+
fun close() {
|
|
2914
|
+
healthConnect.close()
|
|
2783
2915
|
}
|
|
2784
2916
|
|
|
2785
2917
|
// ==================== Screen Capture ====================
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
package {{PACKAGE_NAME}}
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.content.Intent
|
|
5
|
+
import android.webkit.WebView
|
|
6
|
+
import androidx.health.connect.client.HealthConnectClient
|
|
7
|
+
import androidx.health.connect.client.PermissionController
|
|
8
|
+
import androidx.health.connect.client.permission.HealthPermission
|
|
9
|
+
import androidx.health.connect.client.records.ActiveCaloriesBurnedRecord
|
|
10
|
+
import androidx.health.connect.client.records.DistanceRecord
|
|
11
|
+
import androidx.health.connect.client.records.ExerciseSessionRecord
|
|
12
|
+
import androidx.health.connect.client.records.HeartRateRecord
|
|
13
|
+
import androidx.health.connect.client.records.StepsRecord
|
|
14
|
+
import androidx.health.connect.client.records.metadata.Metadata
|
|
15
|
+
import androidx.health.connect.client.request.AggregateRequest
|
|
16
|
+
import androidx.health.connect.client.time.TimeRangeFilter
|
|
17
|
+
import androidx.health.connect.client.units.Energy
|
|
18
|
+
import androidx.health.connect.client.units.Length
|
|
19
|
+
import kotlinx.coroutines.CoroutineScope
|
|
20
|
+
import kotlinx.coroutines.Dispatchers
|
|
21
|
+
import kotlinx.coroutines.SupervisorJob
|
|
22
|
+
import kotlinx.coroutines.cancel
|
|
23
|
+
import kotlinx.coroutines.launch
|
|
24
|
+
import org.json.JSONArray
|
|
25
|
+
import org.json.JSONObject
|
|
26
|
+
import java.time.Instant
|
|
27
|
+
import java.time.ZoneId
|
|
28
|
+
|
|
29
|
+
class CraftHealthConnect(
|
|
30
|
+
private val activity: Activity,
|
|
31
|
+
private val webView: WebView
|
|
32
|
+
) {
|
|
33
|
+
companion object {
|
|
34
|
+
private const val PERMISSION_REQUEST_CODE = 8407
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
|
38
|
+
private val permissionContract = PermissionController.createRequestPermissionResultContract()
|
|
39
|
+
private var pendingPermissions: Set<String> = emptySet()
|
|
40
|
+
|
|
41
|
+
private val client: HealthConnectClient?
|
|
42
|
+
get() = if (HealthConnectClient.getSdkStatus(activity) == HealthConnectClient.SDK_AVAILABLE) {
|
|
43
|
+
HealthConnectClient.getOrCreate(activity)
|
|
44
|
+
} else {
|
|
45
|
+
null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
fun requestAuthorization(typesJson: String) {
|
|
49
|
+
val requested = permissionsFor(JSONArray(typesJson))
|
|
50
|
+
val healthClient = client ?: return reject("_craftFitnessAuthReject", "Health Connect is unavailable or requires an update")
|
|
51
|
+
scope.launch {
|
|
52
|
+
try {
|
|
53
|
+
val granted = healthClient.permissionController.getGrantedPermissions()
|
|
54
|
+
if (granted.containsAll(requested)) {
|
|
55
|
+
resolve("_craftFitnessAuthResolve", "true")
|
|
56
|
+
} else {
|
|
57
|
+
pendingPermissions = requested
|
|
58
|
+
activity.runOnUiThread {
|
|
59
|
+
val intent = permissionContract.createIntent(activity, requested)
|
|
60
|
+
activity.startActivityForResult(intent, PERMISSION_REQUEST_CODE)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} catch (error: Exception) {
|
|
64
|
+
reject("_craftFitnessAuthReject", error.message ?: "Health permissions could not be requested")
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
|
|
70
|
+
if (requestCode != PERMISSION_REQUEST_CODE) return false
|
|
71
|
+
val granted = permissionContract.parseResult(resultCode, data)
|
|
72
|
+
resolve("_craftFitnessAuthResolve", if (granted.containsAll(pendingPermissions)) "true" else "false")
|
|
73
|
+
pendingPermissions = emptySet()
|
|
74
|
+
return true
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fun getData(type: String, startDate: Long, endDate: Long) {
|
|
78
|
+
val healthClient = client ?: return reject("_craftFitnessDataReject", "Health Connect is unavailable or requires an update")
|
|
79
|
+
val end = if (endDate > 0) Instant.ofEpochMilli(endDate) else Instant.now()
|
|
80
|
+
val start = if (startDate > 0) Instant.ofEpochMilli(startDate) else end.minusSeconds(7 * 24 * 60 * 60)
|
|
81
|
+
scope.launch {
|
|
82
|
+
try {
|
|
83
|
+
val (unit, value) = when (type) {
|
|
84
|
+
"steps" -> {
|
|
85
|
+
val metric = StepsRecord.COUNT_TOTAL
|
|
86
|
+
val result = healthClient.aggregate(AggregateRequest(setOf(metric), TimeRangeFilter.between(start, end)))
|
|
87
|
+
Pair("count", (result[metric] ?: 0L).toDouble())
|
|
88
|
+
}
|
|
89
|
+
"heartRate" -> {
|
|
90
|
+
val metric = HeartRateRecord.BPM_AVG
|
|
91
|
+
val result = healthClient.aggregate(AggregateRequest(setOf(metric), TimeRangeFilter.between(start, end)))
|
|
92
|
+
Pair("count/min", (result[metric] ?: 0L).toDouble())
|
|
93
|
+
}
|
|
94
|
+
"activeEnergy" -> {
|
|
95
|
+
val metric = ActiveCaloriesBurnedRecord.ACTIVE_CALORIES_TOTAL
|
|
96
|
+
val result = healthClient.aggregate(AggregateRequest(setOf(metric), TimeRangeFilter.between(start, end)))
|
|
97
|
+
Pair("kcal", (result[metric] ?: Energy.kilocalories(0.0)).inKilocalories)
|
|
98
|
+
}
|
|
99
|
+
"distance" -> {
|
|
100
|
+
val metric = DistanceRecord.DISTANCE_TOTAL
|
|
101
|
+
val result = healthClient.aggregate(AggregateRequest(setOf(metric), TimeRangeFilter.between(start, end)))
|
|
102
|
+
Pair("m", (result[metric] ?: Length.meters(0.0)).inMeters)
|
|
103
|
+
}
|
|
104
|
+
else -> throw IllegalArgumentException("Unsupported Health Connect data type")
|
|
105
|
+
}
|
|
106
|
+
val payload = JSONObject().put("value", value).put("unit", unit).toString()
|
|
107
|
+
resolve("_craftFitnessDataResolve", payload)
|
|
108
|
+
} catch (error: Exception) {
|
|
109
|
+
reject("_craftFitnessDataReject", error.message ?: "Health data could not be read")
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fun saveWorkout(workoutJson: String) {
|
|
115
|
+
val healthClient = client ?: return reject("_craftFitnessSaveReject", "Health Connect is unavailable or requires an update")
|
|
116
|
+
scope.launch {
|
|
117
|
+
try {
|
|
118
|
+
val data = JSONObject(workoutJson)
|
|
119
|
+
val activityId = data.getString("activityId")
|
|
120
|
+
val start = Instant.ofEpochMilli(data.getLong("startDate"))
|
|
121
|
+
val end = Instant.ofEpochMilli(data.getLong("endDate"))
|
|
122
|
+
require(end.isAfter(start)) { "Workout endDate must be after startDate" }
|
|
123
|
+
val zone = ZoneId.systemDefault().rules.getOffset(start)
|
|
124
|
+
val exerciseType = when (data.getString("type")) {
|
|
125
|
+
"running" -> ExerciseSessionRecord.EXERCISE_TYPE_RUNNING
|
|
126
|
+
"walking" -> ExerciseSessionRecord.EXERCISE_TYPE_WALKING
|
|
127
|
+
"hiking" -> ExerciseSessionRecord.EXERCISE_TYPE_HIKING
|
|
128
|
+
"cycling" -> ExerciseSessionRecord.EXERCISE_TYPE_BIKING
|
|
129
|
+
else -> throw IllegalArgumentException("Unsupported workout type")
|
|
130
|
+
}
|
|
131
|
+
val records = mutableListOf<androidx.health.connect.client.records.Record>()
|
|
132
|
+
records.add(ExerciseSessionRecord(
|
|
133
|
+
startTime = start,
|
|
134
|
+
startZoneOffset = zone,
|
|
135
|
+
endTime = end,
|
|
136
|
+
endZoneOffset = zone,
|
|
137
|
+
exerciseType = exerciseType,
|
|
138
|
+
title = "WildLoop",
|
|
139
|
+
metadata = Metadata.manualEntry(clientRecordId = activityId)
|
|
140
|
+
))
|
|
141
|
+
val distance = data.optDouble("distanceMeters", 0.0)
|
|
142
|
+
if (distance > 0) records.add(DistanceRecord(
|
|
143
|
+
startTime = start,
|
|
144
|
+
startZoneOffset = zone,
|
|
145
|
+
endTime = end,
|
|
146
|
+
endZoneOffset = zone,
|
|
147
|
+
distance = Length.meters(distance),
|
|
148
|
+
metadata = Metadata.manualEntry(clientRecordId = "$activityId-distance")
|
|
149
|
+
))
|
|
150
|
+
val calories = data.optDouble("activeEnergyCalories", 0.0)
|
|
151
|
+
if (calories > 0) records.add(ActiveCaloriesBurnedRecord(
|
|
152
|
+
startTime = start,
|
|
153
|
+
startZoneOffset = zone,
|
|
154
|
+
endTime = end,
|
|
155
|
+
endZoneOffset = zone,
|
|
156
|
+
energy = Energy.kilocalories(calories),
|
|
157
|
+
metadata = Metadata.manualEntry(clientRecordId = "$activityId-energy")
|
|
158
|
+
))
|
|
159
|
+
val result = healthClient.insertRecords(records)
|
|
160
|
+
val payload = JSONObject().put("id", result.recordIdsList.firstOrNull() ?: activityId).toString()
|
|
161
|
+
resolve("_craftFitnessSaveResolve", payload)
|
|
162
|
+
} catch (error: Exception) {
|
|
163
|
+
reject("_craftFitnessSaveReject", error.message ?: "Workout could not be saved")
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
fun close() {
|
|
169
|
+
scope.cancel()
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private fun permissionsFor(types: JSONArray): Set<String> {
|
|
173
|
+
val permissions = mutableSetOf<String>()
|
|
174
|
+
for (index in 0 until types.length()) {
|
|
175
|
+
when (types.optString(index)) {
|
|
176
|
+
"steps" -> permissions.add(HealthPermission.getReadPermission(StepsRecord::class))
|
|
177
|
+
"heartRate" -> permissions.add(HealthPermission.getReadPermission(HeartRateRecord::class))
|
|
178
|
+
"activeEnergy" -> permissions.add(HealthPermission.getReadPermission(ActiveCaloriesBurnedRecord::class))
|
|
179
|
+
"distance" -> permissions.add(HealthPermission.getReadPermission(DistanceRecord::class))
|
|
180
|
+
"workouts" -> {
|
|
181
|
+
permissions.add(HealthPermission.getReadPermission(ExerciseSessionRecord::class))
|
|
182
|
+
permissions.add(HealthPermission.getWritePermission(ExerciseSessionRecord::class))
|
|
183
|
+
permissions.add(HealthPermission.getWritePermission(ActiveCaloriesBurnedRecord::class))
|
|
184
|
+
permissions.add(HealthPermission.getWritePermission(DistanceRecord::class))
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return permissions
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private fun resolve(callback: String, json: String) {
|
|
192
|
+
activity.runOnUiThread {
|
|
193
|
+
webView.evaluateJavascript("window.$callback && window.$callback($json)", null)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private fun reject(callback: String, message: String) {
|
|
198
|
+
resolve(callback, JSONObject.quote(message))
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
package {{PACKAGE_NAME}}
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.content.Intent
|
|
5
|
+
import android.webkit.WebView
|
|
6
|
+
import org.json.JSONObject
|
|
7
|
+
|
|
8
|
+
class CraftHealthConnect(
|
|
9
|
+
private val activity: Activity,
|
|
10
|
+
private val webView: WebView
|
|
11
|
+
) {
|
|
12
|
+
fun requestAuthorization(typesJson: String) = reject("_craftFitnessAuthReject")
|
|
13
|
+
|
|
14
|
+
fun getData(type: String, startDate: Long, endDate: Long) = reject("_craftFitnessDataReject")
|
|
15
|
+
|
|
16
|
+
fun saveWorkout(workoutJson: String) = reject("_craftFitnessSaveReject")
|
|
17
|
+
|
|
18
|
+
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean = false
|
|
19
|
+
|
|
20
|
+
fun close() = Unit
|
|
21
|
+
|
|
22
|
+
private fun reject(callback: String) {
|
|
23
|
+
activity.runOnUiThread {
|
|
24
|
+
val reason = JSONObject.quote("Health Connect is disabled")
|
|
25
|
+
webView.evaluateJavascript("window.$callback && window.$callback($reason)", null)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|